diff --git a/.ci/build-hk.sh b/.ci/build-hk.sh deleted file mode 100644 index 9cb1251bf..000000000 --- a/.ci/build-hk.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/sh - -set -e - - -# 移除海外版没有的文档 - -# 中文版「文档首页」 -rm -r docs/community # 社区运营指南 - -# 中文版「游戏商店」 -rm -r docs/store - -# 中文版「游戏服务」 -rm docs/sdk/start/agreement.mdx # 入门指南/TapSDK 隐私政策 -rm docs/sdk/start/compliance.mdx # 入门指南/TapSDK 合规使用说明 -rm -r docs/sdk/domain # 域名 -rm -r docs/sdk/taptap-appsafety # 安全加固 -rm docs/sdk/taptap-login/guide/upgrade1.x.mdx # TapTap 登录/开发指南/1.x 升级 3.x -rm docs/sdk/taptap-login/guide/upgrade2.x.mdx # TapTap 登录/开发指南/2.x 升级 3.x -rm docs/sdk/embedded-moments/features.mdx # 内嵌动态/功能介绍 -rm docs/sdk/embedded-moments/bestpractice.mdx # 内嵌动态/最佳实践 -rm -r docs/sdk/tap-friend # TapTap 好友 -rm docs/sdk/achievement/bestpractice.mdx # 成就系统/最佳实践 -rm -r docs/sdk/taplink # TapLink -rm -r docs/sdk/anti-addiction # 防沉迷 -rm -r docs/sdk/tap-play # TapPlay -rm -r docs/sdk/tap-canary # TapCanary -rm docs/sdk/engine/dedicated-IP.mdx # 云引擎/独立 IP -rm docs/shadow/push/guide/android-mixpush.mdx # 推送通知/开发指南/Android 混合推送 -rm -r docs/sdk/multiplayer # 多人在线对战 - -# 英文版「游戏服务」 -rm i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/features.mdx # 内嵌动态/功能介绍 - - -# 增加海外版独有的文档 - -# 中文版「文档首页」 -cp -r .ci/hk/zh-Hans/operations docs/operations # 开发者运营手册 - -# 中文版「游戏商店」 -cp -r .ci/hk/zh-Hans/store docs/store - -# 中文版「游戏服务」 -cp .ci/hk/zh-Hans/sdk/embedded-moments/features.mdx docs/sdk/embedded-moments/features.mdx # 内嵌动态/功能介绍 -cp .ci/hk/zh-Hans/sdk/push/guide/android-mixpush.mdx docs/shadow/push/guide/android-mixpush.mdx # 推送通知/开发指南/FCM 推送 -cp -r .ci/hk/zh-Hans/sdk/TapPayments docs/sdk # TapTap Payments - -# 英文版「文档首页」 -cp -r .ci/hk/en/operations i18n/en/docusaurus-plugin-content-docs/current/operations # 开发者运营手册 - -# 英文版「游戏商店」 -cp -r .ci/hk/en/store i18n/en/docusaurus-plugin-content-docs/current/store - -# 英文版「游戏服务」 -cp .ci/hk/en/sdk/embedded-moments/features.mdx i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/features.mdx # 内嵌动态/功能介绍 -cp .ci/hk/en/sdk/push/guide/android-mixpush.mdx i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/android-mixpush.mdx # 推送通知/开发指南/FCM 推送 -cp -r .ci/hk/en/sdk/TapPayments i18n/en/docusaurus-plugin-content-docs/current/sdk # TapTap Payments - - -# 替换配置文件 -cp .ci/hk/env.ts src/constants/env.ts -cp .ci/hk/docusaurus.config.js docusaurus.config.js -cp .ci/hk/sidebars.js sidebars.js - - -# 移除 v2 文档 -rm -r versioned_docs versioned_sidebars versions.json - - -# 构建 -yarn build --out-dir build-hk - - -# 重置到初始状态 -# git clean -df -# git checkout -f diff --git a/.ci/build-leancloud.sh b/.ci/build-leancloud.sh old mode 100644 new mode 100755 index 0bea5777a..cc25e8bca --- a/.ci/build-leancloud.sh +++ b/.ci/build-leancloud.sh @@ -2,22 +2,5 @@ set -e -# 移除 TDS 文档 -rm -rf docs i18n versioned_docs versioned_sidebars versions.json - -# 移动 LC 文档 -mv leancloud/docs . -mv leancloud/i18n . - -# 替换配置文件 -mv leancloud/conf/env.ts src/constants/env.ts -cp leancloud/conf/override.scss src/styles/override.scss -cp leancloud/conf/docusaurus.config.js docusaurus.config.js -cp leancloud/conf/sidebars.js sidebars.js - # 构建 yarn build --out-dir build-leancloud - -# 重置到初始状态 -# git clean -df -# git checkout -f diff --git a/.ci/hk/docusaurus.config.js b/.ci/hk/docusaurus.config.js deleted file mode 100644 index 29cf0825e..000000000 --- a/.ci/hk/docusaurus.config.js +++ /dev/null @@ -1,127 +0,0 @@ -// @ts-check - -const PREVIEW = process.env.PREVIEW ?? "false"; - -/** @type {import('@docusaurus/types').Config} */ -const config = { - title: "TapTap Developer Documentation", - url: "https://developer.taptap.io", - baseUrl: PREVIEW === "true" ? "/" : "/docs/", - onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", - favicon: "img/logoh.png", - trailingSlash: true, - customFields: { - searchUrl: "https://tds-doc-search-api.avosapps.us/search", - upItemListIndexUrl: "https://tds-doc-search-check-log.avosapps.us/api/check-log-up", - aiSearchUrl :"https://tds-doc-search-ai-api.ap-sg.tdsapps.com/api/ai-search?type=TDSGlobal", - aiSearchEnUrl :"https://tds-doc-search-ai-api.ap-sg.tdsapps.com/api/ai-search?type=TDSGlobalen", - searchProviderName: "LeanDB Elasticsearch", - searchProviderWebsite: - "https://developer.taptap.io/docs/sdk/engine/database/es/", - mainDomainHost: "https://www.taptap.io", - dcDomainHost: "https://developer.taptap.io?from=tds-docs", - }, - - i18n: { - localeConfigs: { - en: { - label: "English", - }, - "zh-Hans": { - label: "简体中文", - }, - }, - defaultLocale: "en", - locales: ["en", "zh-Hans"], - }, - - presets: [ - [ - "classic", - /** @type {import('@docusaurus/preset-classic').Options} */ - ({ - docs: { - sidebarPath: require.resolve("./sidebars.js"), - routeBasePath: "/", - lastVersion: "current", - versions: { - current: { - label: "v3", - }, - }, - }, - theme: { - customCss: require.resolve("./src/styles/index.scss"), - }, - googleAnalytics: { - trackingID: "UA-73963350-1", - }, - }), - ], - ], - - themeConfig: - /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ - ({ - navbar: { - items: [ - { - label: "文档首页", - to: "/", - position: "right", - activeBaseRegex: `^${ - PREVIEW === "true" ? "/" : "/docs/" - }(zh-Hans/)?(?!.+)`, - }, - { - label: "游戏商店", - to: "store", - position: "right", - }, - { - label: "游戏服务", - to: "sdk", - position: "right", - }, - { - label: "下载", - position: "right", - items: [ - { - label: "设计资源", - to: "/design", - }, - { - label: "SDK 工具包", - to: "/tap-download", - }, - ], - }, - { - type: "localeDropdown", - position: "right", - }, - ], - }, - prism: { - theme: require("./src/theme/prism-taptap"), - additionalLanguages: ["csharp", "java", "php", "groovy", "swift", "dart"], - }, - image: "/img/logo.svg", - metadata: [ - { - name: "keywords", - content: "taptap tds developer documentation", - }, - ], - colorMode: { - defaultMode: "light", - disableSwitch: true, - }, - }), - - plugins: ["docusaurus-plugin-sass"], -}; - -module.exports = config; diff --git a/.ci/hk/en/operations/manual.mdx b/.ci/hk/en/operations/manual.mdx deleted file mode 100644 index e1d682c4c..000000000 --- a/.ci/hk/en/operations/manual.mdx +++ /dev/null @@ -1,547 +0,0 @@ ---- -title: Content operation ---- - -## Where can players find the games and content inside the app? - -### Different sections inside the app - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LocationFunction Screenshot
Home - For you -

(By default) Game of the day - showcases the game card

-

(Swipe left) Story of the day - showcases the post

-

(Swipe left)Event of the day - showcases the community event

-
- - -
Game of the day & Story of the day & Event of the day
-
Home - For you -
  • Upcoming chart entry (would include all upcoming games already configured)
  • -
  • Game cards of upcoming games (Yesterday) showcases the first upcoming game of the previous day;Today showcases all the upcoming games of that day.Swipe left to see a total of 11 games in the Upcoming section marked by different dates
  • -
    - - -

    -
    Home - For you -
  • High-quality posts
  • -
  • Game cards
  • -
    - - -

    -
    Home - For you -
  • Trending hashtags
  • -

    Tap on the specific hashtag to see posts tagged with this hashtag

    -

    -

    -
  • Mini collections
  • -

    ✔Posts tagged with certain specific hashtag

    -

    ✔Game collections

    -
    - - -

    -
    Home - Dailies -

    All Game of the day & Story of the day & Event of the day sorted by dates

    -
    - - -

    -
    Discovers -
  • New releases (tap to see the game card list)
  • -
  • Top charts (tap to see the game card list)
  • -
  • Find a game (can be sorted by different filters)
  • -
    - - -

    -
    Discover - Trending -
  • Trending games
  • -
  • Trending hashtags
  • -
    - - -

    -
    Game page -
  • From the dev
  • -

    Developers can pin their featured posts to this section

    -
  • Custom button
  • -

    The TapTap operation team can help customize the button text and the redirect link

    -
  • Official Discord portal
  • -

    The TapTap operation team can help add the Discord portal to the game page

    -
    - - -

    -
    - -### Manually configurable promotional slots - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Configurable promotional slotsNotes
    Game of the day + push notifications of the game page to all the players -

    *Newly released games or games in the testing phase will be given higher priority

    -
    Story of the day -

    *Posts like Dev logs or developer interviews would be given higher priority

    -
    Community Event & Event of the day -

    *Games with upcoming news such as major updates, giveaway, beta tests or official launch may be featured in the TapTap community events to encourage user-generated content.

    -

    Past community events:

    -

    MyFavClashMiniHeroes

    -

    MyCreationsinProjectStars

    -
    Trending -

    *The game selected as the Game of the day will be put in the first place in the trending search on the same day

    -
    Upcoming -

    *Games with upcoming news such as official launch, open beta tests, closed beta tests, major updates will be configured in the upcoming section together with the exact date and event type

    -
    More exposure to game cards and posts -

    *Game cards and posts could be given more exposure in the feeds during important event milestones (official launch, beta tests, major update etc.)

    -
    Custom button in the game page -

    *The TapTap operation team can help customize the button text and redirect link according to the developers' needs (beta test recruitment, Discord invitation, official website pre-registration…)

    - - - -
    - -### Content types inside TapTap -- Content produced by TapTap editors and professional content creators - - Reviews and let’s plays produced by senior editors and content creators → Could produce targeted content such as previews and reviews to further promote the games - - TapTap profile pages of editors and some content creators: - - - - - - - - - - - - - - - - - - - -
    TitleName & Profile link
    TapTap editors -

    Philip

    -

    Ian

    -

    Jay

    -

    Aaron

    -

    Mandi

    -
    Content creators -

    IndieVoice

    -

    Kosh

    -
    - -- User generated content: voluntarily produced by TapTap users → Providing incentives such as hosting community events to encourage interations and promote the games -- Content produced by developers: first-hand news & content → Posting regularly from the official account to interact with players is recommended - -## How to plan and create your official posts? - -### Recommended content scheduling - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    StageContent typeExamples
    Onboarding -

    Dev say hi

    -
  • Brief introduction of the game and the development team
  • -
  • Brief introduction of future roadmaps (for upcoming games)
  • -
  • Call to action: To direct players to follow the official account and pre-register for the game on TapTap
  • -
    -

    Dungeons of Dreadrock

    -
  • 309 words, moderate length
  • -
  • The post is genuine, down-to-earth, and has unique and distinctive content (such as the original intention behind making DoD, the challenges faced, and expressing gratitude to the players)
  • -
  • The post resonates with the players and the official team also actively responded and engaged with comments in the comment section
  • -

    Bravery and Greed

    -
  • 821 words, slightly long
  • -
  • Video + graphics and text, making it easier for players to obtain more game-related information
  • -
  • The overall tone is relaxed and easy-going, not from the perspective of a certain company, but from a personal perspective
  • -
  • Could simplify the first half and focus on the game-related content in the second half of the article
  • -

    Truck World: Australia

    -
  • 358 words, moderate length
  • -
  • It's written more from an official perspective, and unlike the first two posts, it focuses mainly on the introduction of game mechanics and the future plans for the demo and full version release, since the game has not been launched yet
  • -
    Warm-up -

    Devlog

    -
  • For indie games, developers can talk about the motivation or source of inspiration, interesting behind-the-scenes stories, and character or gameplay designs. The general tone should be friendly and approachable
  • -
    -

    Fake Future

    -
  • 387 words, moderate length
  • -
  • The theme is clear, focusing on the game's art style
  • -
  • Written from a developer's personal perspective, and the tone is light-hearted and relatable
  • -

    Settlement Survival

    -
  • 253 words, moderate length
  • -
  • The post mainly focuses on the potential changes that the port from PC to mobile may bring
  • -
  • The post is written by the publisher, but it is still written from the perspective of the game developer
  • -

    Revival: Recolonization

    -
  • 303 words, moderate length
  • -
  • The post focuses on the latest development updates with the players
  • -
    -

    Preview or warm-up for major milestones (launch/beta/update). Should include:

    -
  • Important information related to the event (date, event type, and region)
  • -
  • Brief introduction of the game
  • -
  • Preview videos (trailer/gameplay) - recommended
  • -
    -

    Settlement Survival

    -

    Undawn

    -

    Arena Breakout

    -
    -

    Latest news/introduction

    -
  • Background story introduction
  • -
  • Main character first reveal
  • -
  • Gameplay introduction & video showcase
  • -
    -

    Otaku’s Adventure

    -

    Undawn

    -
    -

    Giveaways

    -
  • Community events
  • -
    -

    Otaku’s Adventure

    -

    Ace Racer

    -
    -

    FAQ

    -
  • A list of frequently asked questions to provide players with important information
  • -
    -

    Settlement Survival

    -
    After the official release/beta test/major updates -

    Official announcement post

    -
  • Emphasize key messages: time/event type/regions
  • -
  • Promotional videos/key visuals/screenshots
  • -
  • Brief introduction of the game
  • -
    -

    Ace Racer

    -

    Life Makeover

    -

    Settlement Survival

    -
    -

    Guides/Walkthroughs

    - -
    -

    Otaku’s Adventure

    -
    -

    Community events

    -
  • Giveaways
  • -
  • Call for content submissions
  • -
    -

    Settlement Survival community event:Share your best settlement

    -

    Project Stars community event:My reations in Project Stars

    -
    -

    In the case of beta tests (OBT & CBT), it’s recommended that the developers could send out another annoucement after the beta test ends, and pin it to From the dev, in order to inform players of this information

    -
    -
    - -### Content production & posting guidelines - -- **Suggested format and length**:An article that contains a video (to increase conversion)/key visuals or screenshots + texts. The post should include the key information needed and the suggested length is between 250 - 350 words - -- **Things to check before sending out the post [!important]**: - - *Whether a game has been “mentioned” in the post*: - - There is a **Mentioned games** section on the right-hand sidebar of the post editor. At least one game needs to be added (usually the game to be promoted), otherwise, it will not be distributed in the home feeds and will not appear on the game page of relevant games. The game cards inserted in the post will be automatically synchronized here - - *Whether the post status is **Public** *: - - The post's status should be **Public** in the **Visibility** section at the bottom right. If it is **Unlisted or Private**, it will not be distributed normally in feeds (players will still be able to see the Unlisted post via the link or in the writer’s personal profile, but will not see it elsewhere in the app) - - *Whether the post has a **thumbnail** *: - - At least one image should be added to the post (if the post contains a video, then don’t forget to upload the video cover) to function as the post thumbnail -- **Post title**: - - The post title should include the game name, event type, and event time to help players quickly grasp the key message. Developers could also add a word or two on the game's most typical feature (Zombie shooter xxx; indie puzzle game xxx etc.) to the title - - e.g. - - Fortnite new season update coming on Apr xx! - - Madtale | Closed beta test available now! - - Text-based puzzle game CaseCracker coming to TapTap on xxx! -- **Call to action**: - - Invite players to take the desired action (pre-register/join the official Discord server/participate in the beta test etc.) both at the beginning and the end of the post to emphasize its importance -- **A Brief introduction of the game**: - - Usually in one or two sentences about the general features and gameplay style of the game at the beginning of the post to help players know what the game is like - - e.g. - - Life Makeover is a limitless dress-up and social simulation game where you can create your very own avatar, customize dress-up and makeup, design one-and-only garment by yourself, build your dream house and chill with your besties! - -- **Images & gifs**: - - It’s recommended to add some key visuals, gifs or screenshots to the post to make it more accessible -- **Insert game cards**: - - It’s also recommended to insert the game card at the very beginning of the post for players to download/pre-register for the game directly, or to jump to the game description page for more details. Multiple games (no more than 20) can be added to the post and these selected games will also show up in the **Mentioned games** section - - - - - - - - - - - - - - -
    - - -

    Use this function to add game cards to the post


    - -
    - - -

    Search for the game name


    - - -

    Game card preview


    -
    - -- **Video** -1. Upload the video directly (recommended). Please don’t forget to upload a video cover as well - -2. Insert a YouTube link of the video (not recommended). Please insert the link and choose [Display as card], so that players can directly watch the video on TapTap without jumping to external websites - - - - - - - - -
    - - -

    Display as card


    - -
    - - -

    YouTube video preview


    -
    -- **Discord invitation link** - - Please put the invitation link at the beginning of the post if you wanna specifically invite more players to join the official Discord server. Please choose [Display as card] for the link as well - - - - - - - - - -
    - - -

    Display as card


    - -
    - - -

    Preview


    -
    - -- **Hashtags** - - Relevant hashtags can be found and added in the **Tags** section on the right-hand sidebar (check this to see all the hashtags currently in use). Appropriate hashtags can help distribute the post to more players - -![](https://capacity-files.lcfile.com/3V6YHDRzLvhk865rgvpW0TQDa5kuzs45/image-20230419-033426.png) -- **Schedule the post** - - Use the **Schedule** function to publish the post at the designated time - -![](https://capacity-files.lcfile.com/RDhj2erRMJkBRO3mgdWH0NrTRWzJNdvX/image-20230419-032447.png) - -### How to better promote the game to more players -- **Plan the community posts properly** - - Schedule the appropriate content according to the corresponding stages: [First reveal] - [Warm-up] - [Offically online], in order to accumulate pre-registrations and attention through constant content exposure - - Please contact the TapTap BD team or operation team in advance in terms of important milestones to apply for promotional slots on TapTap - -- **Developers can use the following functions to better manage the game page** - - - *Add new moderators*: - - New moderators can be added using the **Admin tools** by the admin of this game. [Webpage version of the game page] - [**Admin tools** on the right-hand sidebar] - [Authority management] - - - - *Pin important official posts to **From the dev** as featured posts*: - - [Webpage version of the game page] - [**Admin tools** on the right-hand sidebar] - [Posts] - [Use the post ID to search for the post] - [Pin the post] - - - *Push notifications in **Developer Center** (an effective approach to inform players interested in the game of the latest game news)*: - - - Developers can **push 1 post to all the players who have followed the game** (players who have pre-registered for/downloaded/wanted the game) - - - [Developer Center] - [Game Operations] - [Push Notification of Official Posts] - [New posts to push] - - - *The forum on the webpage version can also be managed via the **Admin tools**. Developers can pin revelant hashtags as filters and add useful links in the sidebar*: - - - **Pin hashtag**: [Webpage version of the game page] - [**Admin tools** on the right-hand sidebar] - [Manage pinned hashtags] - [Pin hashtag] - - - **Add useful links**: [Webpage version of the game page] - [**Admin tools** on the right-hand sidebar] - [Manage useful links] - [Add new links] - - - - - - - - - -
    - - -

    From the dev


    - - -

    Push notifications


    - -
    - - -

    Admin tools


    - - -

    Hashtags and useful links in the game forum on the webpage version


    -
    - -### Active interactions with players - -When responding to player reviews, it's important to be genuine and respectful. You should always make an effort to communicate with players in a calm and friendly manner, without being dismissive or indifferent. Honest and sincere communication can help players feel more secure. - -One thing to avoid is copying and pasting responses, as this can come across as impersonal and insincere. Instead, take the time to craft thoughtful and personalized responses that show you're engaged with the player's feedback. - -If you come across a long or negative review, it's important to respond in a timely manner and in a way that demonstrates your willingness to listen and address any issues raised. Responding to reviews can help build a positive image for your brand, and official responses will be given priority placement under player comments, making it more likely that subsequent players will see and engage with your message. - - diff --git a/.ci/hk/en/sdk/TapPayments/_category_.json b/.ci/hk/en/sdk/TapPayments/_category_.json deleted file mode 100644 index 9294586f6..000000000 --- a/.ci/hk/en/sdk/TapPayments/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapTap 支付", - "collapsed": true, - "position": 20 -} diff --git a/.ci/hk/en/sdk/TapPayments/appendix/_category_.json b/.ci/hk/en/sdk/TapPayments/appendix/_category_.json deleted file mode 100644 index e52e0c210..000000000 --- a/.ci/hk/en/sdk/TapPayments/appendix/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "附录", - "collapsed": true, - "position": 6 -} diff --git a/.ci/hk/en/sdk/TapPayments/appendix/payment-methods.mdx b/.ci/hk/en/sdk/TapPayments/appendix/payment-methods.mdx deleted file mode 100644 index 2ee7fb8c4..000000000 --- a/.ci/hk/en/sdk/TapPayments/appendix/payment-methods.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: 支持的支付方式 -sidebar_position: 1 ---- - -# 支持的支付方式 - -TapTap Payments 支持 173 个国家或地区和 42 种货币,并正在不断集成更加本地化的支付方式。目前,我们在全球范围内支持以下支付方式: - -| **支付方式** | **支持的国家和地区** | -| ------------------- | ------------------------ | -| 信用卡和借记卡 | 173 个国家或地区 | -| 支付宝 | 173 个国家或地区 | -| PayPal | 173 个国家或地区 | -| GrabPay | 菲律宾、新加坡、马拉西亚 | -| DANA | 印度尼西亚 | -| OVO | 印度尼西亚 | -| QRIS | 印度尼西亚 | -| GCash | 菲律宾 | -| Maya | 菲律宾 | -| PayNow | 新加坡 | -| TrueMoney | 泰国 | -| Rabbit LINE Pay | 泰国 | -| Touch 'n Go eWallet | 马来西亚 | -| Boost | 马来西亚 | -| UPI | 印度 | diff --git a/.ci/hk/en/sdk/TapPayments/appendix/regions-currencies.mdx b/.ci/hk/en/sdk/TapPayments/appendix/regions-currencies.mdx deleted file mode 100644 index f28aec0bb..000000000 --- a/.ci/hk/en/sdk/TapPayments/appendix/regions-currencies.mdx +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: 支持的国家地区及货币 -sidebar_position: 2 ---- - - - - - -TapTap Payments 支持全球 173 个国家地区和 40 多种货币。在 本地货币不支持的国家地区,或在开发者没有设定价格的国家地区中,用户将以美元(USD)进行付款。另外,大部分货币为二位十进制货币,例如 USD 0.99,而另一部分则是零位十进制货币,例如 JPY 130。请在设置价格时参照以下标准: - -| **国家地区名称** | **国家地区代码** | **货币名称** | **货币精度** | **支付方式** | -| ----------------------- | ---------------- | ------------ | ------------ | --------------------------------------------- | -| Afghanistan | AF | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Albania | AL | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Algeria | DZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Angola | AO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Anguilla | AI | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Antigua and Barbuda | AG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Argentina | AR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Armenia | AM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Australia | AU | AUD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Austria | AT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Azerbaijan | AZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bahamas | BS | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bahrain | BH | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Barbados | BB | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Belarus | BY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Belgium | BE | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Belize | BZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Benin | BJ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bermuda | BM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bhutan | BT | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bolivia | BO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bosnia and Herzegovina | BA | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Botswana | BW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Brazil | BR | BRL | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Brunei Darussalam | BN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bulgaria | BG | BGN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Burkina Faso | BF | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cabo Verde | CV | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cambodia | KH | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cameroon | CM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Canada | CA | CAD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cayman Islands | KY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Chad | TD | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Chile | CL | CLP | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Colombia | CO | COP | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Congo | CG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Congo(DRC) | CD | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Costa Rica | CR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Croatia | HR | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cyprus | CY | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Czechia | CZ | CZK | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Denmark | DK | DKK | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Dominica | DM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Dominican Republic | DO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ecuador | EC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Egypt | EG | EGP | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| El Salvador | SV | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Estonia | EE | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Eswatini | SZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Fiji | FJ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Finland | FI | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| France | FR | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Gabon | GA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Gambia | GM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Georgia | GE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Germany | DE | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ghana | GH | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Greece | GR | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Grenada | GD | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Guatemala | GT | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Guinea-Bissau | GW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Guyana | GY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Honduras | HN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Hong Kong (China) | HK | HKD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Hungary | HU | HUF | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Iceland | IS | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| India | IN | INR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、UPI | -| Indonesia | ID | IDR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、DANA、OVO、QRIS | -| Iraq | IQ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ireland | IE | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Israel | IL | ILS | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Italy | IT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ivory Coast | CI | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Jamaica | JM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Japan | JP | JPY | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Jordan | JO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kazakhstan | KZ | KZT | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kenya | KE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kosovo | XK | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kuwait | KW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kyrgyzstan | KG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Laos | LA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Latvia | LV | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Lebanon | LB | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Liberia | LR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Libya | LY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Lithuania | LT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Luxembourg | LU | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Macau(China) | MO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Madagascar | MG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Malawi | MW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Malaysia | MY | MYR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、GrabPay、Touch 'n Go eWallet、Boost | -| Maldives | MV | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mali | ML | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Malta | MT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mauritania | MR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mauritius | MU | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mexico | MX | MXN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Micronesia (Federated States of) | FM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Moldova, Republic of | MD | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mongolia | MN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Montenegro | ME | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Montserrat | MS | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Morocco | MA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mozambique | MZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Myanmar | MM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Namibia | NA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Nauru | NR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Nepal | NP | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Netherlands | NL | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| New Zealand | NZ | NZD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Nicaragua | NI | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Niger | NE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Nigeria | NG | NGN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| North Macedonia | MK | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Norway | NO | NOK | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Oman | OM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Pakistan | PK | PKR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Palau | PW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Panama | PA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Papua New Guinea | PG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Paraguay | PY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Peru | PE | PEN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Philippines | PH | PHP | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、GrabPay、GCash、Maya | -| Poland | PL | PLN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Portugal | PT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Qatar | QA | QAR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Romania | RO | RON | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Rwanda | RW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Saint Kitts and Nevis | KN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Saint Lucia | LC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Saint Vincent and the Grenadines | VC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Sao Tome and Principe | ST | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Saudi Arabia | SA | SAR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Senegal | SN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Serbia | RS | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Seychelles | SC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Sierra Leone | SL | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Singapore | SG | SGD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、GrabPay、PayNow | -| Slovakia | SK | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Slovenia | SI | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Solomon Islands | SB | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| South Africa | ZA | ZAR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| South Korea | KR | KRW | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Spain | ES | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Sri Lanka | LK | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Suriname | SR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Sweden | SE | SEK | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Switzerland | CH | CHF | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Taiwan (China) | TW | TWD | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Tajikistan | TJ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Tanzania | TZ | TZS | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Thailand | TH | THB | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、TrueMoney、Rabbit LINE Pay | -| Tonga | TO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Trinidad and Tobago | TT | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Tunisia | TN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Turkey | TR | TRY | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Turkmenistan | TM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Turks and Caicos Islands | TC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Uganda | UG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ukraine | UA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| United Arab Emirates | AE | AED | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| United Kingdom | GB | GBP | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| United States | US | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Uruguay | UY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Uzbekistan | UZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Vanuatu | VU | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Venezuela | VE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Vietnam | VN | VND | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Virgin Islands (British) | VG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Yemen | YE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Zambia | ZM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Zimbabwe | ZW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | diff --git a/.ci/hk/en/sdk/TapPayments/appendix/sandbox.mdx b/.ci/hk/en/sdk/TapPayments/appendix/sandbox.mdx deleted file mode 100644 index f17e72db8..000000000 --- a/.ci/hk/en/sdk/TapPayments/appendix/sandbox.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: 沙盒环境 -sidebar_position: 1 ---- - -# 沙盒环境 - -TapTap Payments 沙盒环境进行支付测试, 可以使用一些沙盒信用卡进行支付, - -## 沙盒账号设置 -进入开发者中心后台, 按照如下步骤设置和添加对应沙盒账号: - -![](https://img.tapimg.com/market/images/7309efbf7c1971d44c73a53494193bb4.png) - -## 沙盒信用卡 -在支付时, 可以使用以下沙盒信用卡进行支付测试: - -![](https://img.tapimg.com/market/images/7d3fb8a5a75d74f6f96f3c4a938693ff.png) - -## 沙盒异常模拟 -同时, 也可以使用以下卡号进行异常模拟 - -![](https://img.tapimg.com/market/images/7b4394a2690b01a1008fcd362ec44a99.png) diff --git a/.ci/hk/en/sdk/TapPayments/develop/android.mdx b/.ci/hk/en/sdk/TapPayments/develop/android.mdx deleted file mode 100644 index 533331966..000000000 --- a/.ci/hk/en/sdk/TapPayments/develop/android.mdx +++ /dev/null @@ -1,220 +0,0 @@ ---- -title: Android 集成指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import sdkVersions from '/src/docComponents/sdkVersions'; -import CodeBlock from '@theme/CodeBlock'; - - -## **环境要求** - -- Gradle 版本不低于 6.1.1,Android AGP 插件版本不低于 4.0.1; - -## **准备** - -- 参照 [准备工作](/sdk/start/get-ready/) 所述创建 app,配置 app 参数并且绑定 API 域名 -- 参照 [TapSDK 快速开始](/sdk/start/quickstart/) 配置包名和签名 - -## **SDK 指南** - -### **SDK 集成** - -打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: - - - {`dependencies { - ... - // TapTapIAP dependency - implementation 'com.taptap.android.payment:iap:${sdkVersions.tapGlobalPayments.android}' - implementation 'com.taptap.android.payment:base:${sdkVersions.tapGlobalPayments.android}' - implementation 'com.taptap.android.payment:stripe:${sdkVersions.tapGlobalPayments.android}' - implementation 'com.taptap.android.payment:braintree:${sdkVersions.tapGlobalPayments.android}' - implementation 'com.taptap.android.payment:alipay:${sdkVersions.tapGlobalPayments.android}' -} -`} - - - - - -### **SDK 初始化** - -添加 TapTapIAP 的依赖项后,您需要初始化 `TapTapIAP` 实例。`TapTapIAP` 是 SDK 与应用的其余部分之间进行通信。`TapTapIAP` 为许多常见的操作提供了方法。 - -首先 在应用启动时需要进行 SDK 初始化, 通过设置 `TapTapSdkOptions`, 完成 SDK 初始化, `TapTapSdk.init` 。 - -这里需要设置对应在开发者后台申请的 `ClientID` 和 `ClientToken`,用于校验是否有权限使用 `TapTapIAP`。 - -```java -TapTapSdkOptions sdkOptions = new TapTapSdkOptions( - "应用的 ClientID", // clientId 开发者平台申请 - "应用的 ClientToken", // clientToken 开发者平台申请 - TapTapRegion.GLOBAL, // 地区 - "", // 分包渠道名称 - "", // 游戏版本, 为空为null会取AppVersion - false, // 是否自动上报GooglePlay内购支付成功事件 仅 [TapTapRegion.GLOBAL] 生效 - false, // 自定义字段是否能覆盖内置字段 - null, // 自定义属性,启动首个预置事件(device_login)会带上这些属性 - null, // OAID证书, 用于上报 OAID 仅 [TapTapRegion.CN] 生效 - false // 是否开启 log,建议 Debug 开启,Release 关闭 -); - -TapTapSdk.init(context, sdkOptions); -``` - -创建 `TapTapIAP`,请使用 `newBuilder()`这里会根据SDK.init所设置的 `ClientID` 和 `ClientToken`校验是否有权限使用 `TapTapIAP`。 - -```java -// 创建 TapTapIAP 实例 - TapTapIAP tapTapIAP = TapTapIAP.newBuilder().build(); - ``` - -### **展示可供购买的商品** - -初始化完成 `TapTapIAP` 后,您就可以查询可售的商品并将其展示给用户了。 - -查询应用内商品详情,请调用 `queryProductDetailsAsync()`。为了处理该异步操作的结果,您还必须指定实现 ` ProductDetailsResponseListener` 接口的监听器。然后,您可以替换 `onProductDetailsResponse()`,该方法会在查询完成时通知监听器,如以下示例所示: - -```java -List queryProductList = new ArrayList<>(); -// 支持批量查询 Product, 设置好对应的 ProductID、ProductType -// ProductType 目前仅支持 ProductType.INAPP -for (int i = 0; i < products.length; i++) { - - String productId = products[i]; - Product product = Product.newBuilder() - .setProductId(productId) - .setProductType(ProductType.INAPP) - .build(); - queryProductList.add(product); -} -QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder() - .setProductList(queryProductList).build(); -tapTapIAP.queryProductDetailsAsync(params, new ProductDetailsResponseListener() { - @Override - public void onProductDetailsResponse(TapPaymentResult result, - List productDetails, List unavailableProductIds) { - ... - - // check TapPaymentResult - // process returned productDetails - } -}); -``` - -### **启动购买流程** - -如需从应用发起购买请求,请从应用的主线程调用 `launchBillingFlow()` 方法。此方法接受对 `BillingFlowParams` 对象的引用,该对象包含通过调用 `queryProductDetailsAsync()` 获取的相关 `ProductDetails` 对象。如需创建 `BillingFlowParams` 对象,请使用 `BillingFlowParams.Builder` 类。 - -```java -// An activity reference from which the billing flow will be launched. -Activity activity = ...; -ProductDetailsParams productDetailsParams = - ProductDetailsParams.newBuilder() - // retrieve a value for "productDetails" by calling queryProductDetailsAsync() - .setProductDetails(productDetails) - .build(); - -BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() - .setProductDetailsParams(productDetailsParams) - .setObfuscatedAccountId("xxx") //Specifies an optional obfuscated string that is uniquely associated with the order(or another information) in your app. - .build(); -// Launch the billing flow -TapPaymentResult result = tapTapIAP.launchBillingFlow(activity, - billingFlowParams, - new PurchaseUpdatedListener() { - @Override - public void onPurchaseUpdated(TapPaymentResult result, Purchase purchases) { - // To be implemented in a later section. - } - } -); -``` - -`launchBillingFlow()` 方法会返回 `TapPaymentResponseCode` 中列出的几个响应代码之一。请务必检查此结果,以确保在启动购买流程时没有错误。`TapPaymentResponseCode` 为 `OK` 表示成功启动。成功调用 `launchBillingFlow()` 后,会向用户展示收银台。 - -### **购买流程中订单状态监听** - -在购买流程中, `TapTapIAP` 会调用 `onPurchasesUpdated()`,以将购买的订单状态变更实时传给实现 PurchasesUpdatedListener 接口的监听器。您可以在初始化时使用 `setListener()` 方法指定监听器。您必须实现 `onPurchasesUpdated()` 来处理可能的响应代码。以下提供了一个 `onPurchasesUpdated()` 示例 : - -```java -@Override -public void onPurchaseUpdated(TapPaymentResult result, Purchase purchase) { - if (result.getResponseCode() == TapPaymentResponseCode.OK - && purchases != null) { - handlePurchase(purchase); - } else if (result.getResponseCode() == TapPaymentResponseCode.USER_CANCELED) { - // Handle an error caused by a user cancelling the purchase flow. - } else { - // Handle any other error codes. - } -} -``` - -### **进行商品的发放,且完成订单** - -在用户进行完了任意商品的购买, 请确认为该用户发放对应的商品或者解锁对应的关卡。在确定商品发放之后需要调用 `finishPurchaseAsync` 告知 `TapTapIAP` 已完成商品的发放。以下是个代码实例: - -```java -Purchase purchase = ...; -FinishPurchaseParams params = FinishPurchaseParams.newBuilder() - .setId(purchase.getOrderId()) // Required - .setOrderToken(purchase.getOrderToken()) // Required - .setPurchaseToken(purchase.getPurchaseToken()) // Required - .build(); -tapTapIAP.finishPurchaseAsync(params, new FinishPurchaseResponseListener() { - @Override - public void onFinishPurchaseResponse(TapPaymentResult result, Purchase purchase) { - } -}); -``` -:::tip -确认发放商品非常重要, 如果您没有调用 `finishPurchaseAsync` 来完成订单, 用户将无法再次购买该商品,且该订单将会在 3天后自动退款。 -::: - -### **获取未完成的订单列表** - -使用 `PurchasesUpdatedListener` 监听购买交易变更,无法完全确保您的应用会处理所有购买交易。有时您的应用可能不知道用户进行了部分购买交易。在下面这几种情况下,您的应用可能会跟踪不到或不知道购买交易的发生: - -- **在购买过程中出现网络问题**:用户成功购买了商品并收到了对应渠道的确认消息,但用户设备在通过 `PurchasesUpdatedListener` 收到购买交易的通知之前失去了网络连接。 -- **多部设备**:用户在一部设备上购买了一件商品,然后在切换设备时期望看到该商品。 -- **异常崩溃**:用户在外部购买成功时,应用出现了崩溃的情况。 - -为了处理这些情况,请确保您的应用在 `onResume()` 方法中调用 `tapTapIAP.queryUnfinishedPurchaseAsync()`,以确保所有购买交易都得到正确处理。 - -以下示例展示了如何提取用户的未完成订单列表: - -```java -tapTapIAP.queryUnfinishedPurchaseAsync(new PurchasesResponseListener() { - @Override - public void onQueryPurchasesResponse(TapPaymentResult result, List purchases) { - if (purchases != null) { - // Process Purchases. - ... - ... - } - } - }); -``` - -### **处理 TapPaymentResult 响应代码** - -当使用 `TapTapIAP` 结算库调用触发操作时,该库会返回 `TapPaymentResult` 响应,并将结果告知开发者。例如,如果您使用 `queryProductDetailsAsync` 且返回 `OK` ,并提供正确的 `ProductDetails` 对象;或者返回了其他类型,代表了无法提供 `ProductDetails` 对象的原因。 - -并非所有类型都是错误。下面列举了一些 `TapPaymentResponseCode` 不是错误的: - -- `TapPaymentResponseCode.OK`:代表业务已成功执行。 -- `TapPaymentResponseCode.USER_CANCELED`:代表用户没有完成流程,就离开了页面 - -其他的一些错误类型可以用于调试和上报使用: - -| **可重试的 CODE** | **问题** | **可以尝试的解决方案** | -| ------------------ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| NETWORK_ERROR | 此错误代表设备和 TapTapIAP 系统之间的网络连接出现问题 | 可以使用简单的重试策略或指数退避算法 | -| ITEM_ALREADY_OWNED | 这个类型表明用户已经购买过 非消耗品 商品, 再次购买时会返回该错误 | 为了避免出现这个问题, 在展示商品界面就提示用户已经购买该商品, 且无法点进进行购买流程. | -| USER_CANCELED | 用户已退出结算流程 | | -| ITEM_UNAVAILABLE | 商品无效, 有可能是商品已经过期或者商品已经被下架 | 确保您通过 `queryProductDetailsAsync` 刷新商品详情。如果商品无效则不在界面上展示给用户 | -| DEVELOPER_ERROR | 这是一个严重错误,表明您未正确使用 API。例如,向 launchBillingFlow 提供不正确的参数可能会导致此错误 | | diff --git a/.ci/hk/en/sdk/TapPayments/develop/faq.mdx b/.ci/hk/en/sdk/TapPayments/develop/faq.mdx deleted file mode 100644 index 9a2302a28..000000000 --- a/.ci/hk/en/sdk/TapPayments/develop/faq.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: 常见问题 -sidebar_position: 4 ---- - -### 1、Unity 2020.3.15 之前的版本升级 Gradle 版本的操作步骤 - -在 `Player Settings` -> `Player` -> `Android Tab` -> `Publish Settings` -> `Build`,然后勾选 **Custom Base Gradle Template**、**Custom Launcher Gradle Template**、**Custom Main Gradle Template**。 - -![](https://capacity-files.lcfile.com/5nSUEX6IWVRkwu4Nep1pXo6vUnl9VppH/%E5%8D%87%E7%BA%A7gradle_1.png) - -以下更改应用于 `Assets/Plugins/Android/` 该文件夹下生成的三个文件 `mainTemplate.gradle`、`launcherTemplate.gradle`、`baseProjectTemplate.gradle`。 - -打开 `baseProjectTemplate.gradle` 修改文件内容: - -```groovy -dependencies { - ... - // 将 3.x.0 版本修改为 4.0.0 - //classpath 'com.android.tools.build:gradle:3.x.0' - classpath 'com.android.tools.build:gradle:4.0.0' -} -``` - -分别打开 `mainTemplate.gradle` 和 `launcherTemplate.gradle` 文件,找到 `lintOptions` 标签, 分别添加 **checkReleaseBuilds false** 配置: - -```groovy -lintOptions { - abortOnError false - checkReleaseBuilds false -} -``` - - -同时,为了将 [Gradle 版本和 Android Gradle Plugin 版本对应](https://developer.android.com/studio/releases/gradle-plugin#expandable-1),需要更新 Gradle 版本,下载 [6.5.0 版本的 Gradle](https://services.gradle.org/distributions/gradle-6.5-all.zip),解压后放到自定义的文件夹中,同时**不**勾选 Unity 中的 `Preferences` -> `External Tools`-> `Android` -> `Gradle Installed with Unity(recommend)`,改为选择解压后 Gradle 文件夹的位置,如 `/gradle-6.5.0`。 - -![](https://capacity-files.lcfile.com/hrkFCRy9VuLapvsanFm6nhpkHEEz0qVE/%E5%8D%87%E7%BA%A7gradle_2.png) \ No newline at end of file diff --git a/.ci/hk/en/sdk/TapPayments/develop/server.mdx b/.ci/hk/en/sdk/TapPayments/develop/server.mdx deleted file mode 100644 index efbe5b4b9..000000000 --- a/.ci/hk/en/sdk/TapPayments/develop/server.mdx +++ /dev/null @@ -1,687 +0,0 @@ ---- -title: 服务端开发指南 -sidebar_position: 3 ---- - -import MultiLang from "/src/docComponents/MultiLang"; - -# 服务端开发指南 - -:::tip -服务通用规则请参考 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) -::: - -## 主查服务 - -#### 请求域名: -- `https://cloud-payment.tapapis.com` - - -| ** 接口地址 ** | **Method** | **描述** | -| ----------------------------- | ---------- | -------------------- | -| `/order/v1/info?client_id={{client_id}}&order_id={{order_id}}` | `GET` | 查询订单信息 | -| `/order/v1/unconfirmed?client_id={{client_id}}` | `GET` | 查询未核销的订单列表 | -| `/order/v1/verify?client_id={{client_id}}` | `POST` | 核销订单 | - -### 查询订单信息 - -#### 服务 URL -- https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}} - -#### 请求方式 -- GET - -#### 请求签算 -- 详见 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) - -通过订单 ID 查询订单详细信息和支付状态 - -``` -curl -X GET \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}} -``` - -`data.order` 对象结构见 [订单信息](/sdk/TapPayments/develop/server/#订单信息) - -``` -{ - "data": { - "order": {} - }, - "success": true -} -``` - -### 查询未核销的订单列表 - -#### 服务 URL -- https://{{domain}}/order/v1/unconfirmed?client_id={{client_id}} - -#### 请求方式 -- GET - -#### 请求签算 -- 详见 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) - -查询当前未核销的订单列表,正常情况下在用户支付成功后,应通过 verify 接口核销订单并同时保证对用户发货成功。如果因异常原因没有完成核销,可以通过此接口查询,重新 verify 并完成发货。 - -``` -curl -X GET \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - https://{{domain}}/order/v1/unconfirmed?client_id={{client_id}} -``` - -`data.list` 数组内对象结构见 [订单信息](/sdk/TapPayments/develop/server/#订单信息) - -``` -{ - "data": { - "list": [ - {} - ] - }, - "success": true -} -``` - -### 核销订单 - -#### 服务 URL -- https://{{domain}}/order/v1/verify?client_id={{client_id}} - -#### 请求方式 -- POST[application/json; charset=utf-8] - -#### 请求正文信息 - -| ** 参数名称 ** | ** 必填 ** | ** 格式 ** | ** 描述 ** | -| --------------| --------------| --------------| --------------| -| `order_id` | Y | string | 订单唯一 ID | -| `purchase_token` | Y | string | 用于订单核销的 token | - -#### 请求签算 -- 详见 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) - -当支付成功后,核销订单表示已经确认支付结果并已对买家完成发货,订单状态也会从 `charge.succeeded` 变为 `charge.confirmed` - -``` -curl -X POST \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - -H 'Content-Type: application/json; charset=utf-8' - -d '{"order_id":"{{order_id}}","purchase_token":"{{purchase_token}}"}' - https://{{domain}}/order/v1/verify?client_id={{client_id}} -``` - -`data.order` 对象结构见 [订单信息](/sdk/TapPayments/develop/server/#订单信息) - -``` -{ - "data": { - "order": {} - }, - "success": true -} -``` - -## Webhook 回调 - -:::tip -同样的通知可能会多次发送,商户系统必须正确处理重复通知。 - -推荐做法是:当商户系统收到通知时,首先进行签名验证,然后检查对应业务数据的状态,如未处理,则进行处理;如已处理,则直接返回成功。 - -在处理业务数据时建议采用数据锁进行并发控制,以避免可能出现的数据异常。 -::: - -### Webhook 说明 - -目前 Webhook 支持监听「充值成功」「退款成功」「退款失败」事件。对于「充值成功」建议采取主动核销订单,根据订单状态完成发货。 - -1. 需要依次进入 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > API 密钥**,检查是否有已生效的密钥,没有则需要 **添加新的密钥** -2. 需要依次进入 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > Webhooks 设置 > 添加**,添加有效的 **充值成功** URL。 - -### Webhook 请求 - -#### 服务 URL -- 由开发者提供,在 Webhooks 设置中添加 - -#### 请求方式 -- POST[application/json; charset=utf-8] - -#### 请求正文信息 - -| ** 参数名称 ** | ** 必填 ** | ** 格式 ** | ** 描述 ** | -| --------------| --------------| --------------| --------------| -| `order` | Y | object | 对象结构见 [订单信息](/sdk/TapPayments/develop/server/#订单信息) | -| `event_type` | Y | string | 事件枚举见 [Webhook的事件枚举](/sdk/TapPayments/develop/server/#webhook的事件枚举) | - -#### 请求签算 -- 详见 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) - -``` -curl -X POST \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - -H 'Content-Type: application/json; charset=utf-8' - -d '{"order":{},"event_type":"charge.succeeded"}' - {{your webhook url}} - -``` - -### Webhook 响应 - -``` -{ - "code": "SUCCESS", - "msg": "" -} -``` - -#### 字段描述 - -| **字段** | **类型** | **是否必须** | **描述** | -| -------- | -------- | ------------ | ---------------------------------------- | -| code | string | Y | 状态码,`SUCCESS` 为接收成功,`FAIL` 或其他均认为失败 | -| msg | string | N | 当接收失败时需返回失败原因 | - -## 请求规则和签算 - -### 请求 Headers - -| **Header** | **是否必须** | **描述** | -| ----------- | ------------ | ------------------------------------------------------------ | -| `X-Tap-Sign` | Y | 接口签算,详见 [签算](/sdk/TapPayments/develop/server/#签算) | -| `X-Tap-Ts` | Y | 请求方当前时间 unix timestamp | -| `X-Tap-Nonce` | Y | 随机数,需要大于等于 6 字节,小于等于 60 字节,每次请求需重新生成 | - -### 请求 Request - -#### 保留参数 - -所有 HTTP METHOD 必传,需要作为查询参数的一部分 - -| **Key** | **描述** | -| --------- | --------------- | -| client_id | 开发平台应用 ID | - -``` -https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}} -``` - -#### 当请求方法是 POST - -HTTP body 需使用 JSON 编码格式传输参数,即请求 headers 中应该携带 `Content-Type: application/json; charset=utf-8` - -``` -curl -X POST \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - -H 'Content-Type: application/json; charset=utf-8' - -d '{"order_id":{{order_id}},"purchase_token":"{{purchase_token}}"}' - https://{{domain}}/order/v1/verify?client_id={{client_id}} -``` - -### 请求 Response - -#### 请求成功响应 - -``` -{ - "data": {}, - "now": 1640966400, - "success": true -} -``` - -#### 请求异常响应 - -``` -{ - "data": { - "code": 100004, - "msg": "NotFound: Unknown Error", - "error_description": "order not found" - }, - "now": 1640966400, - "success": false -} -``` - -#### 字段描述 - -| **字段** | **类型** | **是否必须** | **描述** | -| -------- | -------- | ------------ | --------------------------- | -| data | object | Y | 业务数据,或异常信息描述 | -| now | int | Y | 服务端时间 (unix timestamp) | -| success | bool | Y | 响应状态,`true` 为成功 | - -异常响应 `data` 字段描述 - -| **字段** | **类型** | **是否必须** | **描述** | -| ----------------- | -------- | ------------ |------------------------------------------------------------| -| code | int | Y | [错误码](/sdk/TapPayments/develop/server/#错误码) | -| msg | string | Y | 通用错误描述 | -| error_description | string | Y | 详细错误描述,辅助理解和解决发生的错误 | - - -### 签算 - -签算采用 HMAC-SHA256 算法 - -#### 密钥获取 -- 可在 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > API 密钥** 查看。 - -#### 签算说明 - -sign = HMAC.New(Sha256, "{{Server Secret}}").Hash(message) - -以下为 `message` 组成 - -``` -{method}\n -{url_path_and_query}\n -{headers}\n -{body}\n -``` - -**method:** 请求 HTTP 方法,如 GET、POST。 - -**url_path_and_query:** 完整请求路径及参数,如 /service/v1/method?client_id={{client_id}}&foo={{foo}}&bar={{bar}}&foo_bar={{foo_bar}} - -**headers:** 所有 `X-Tap-` 前缀的 headers 组合,将 keys 按 ASCII-code order 排序后以换行符 \n 为分隔符拼接。如 {key1}:{value1}\n{key2}:{value2}\n{key3}:{value3}。**为避免不同网络框架导致 keys 的排序结果不一致,签算时需要执行 key 转小写操作 key = tolower(key)** - -**body:** 请求体,如果请求体为空,则最后一行仅为一个 \n - -以下为请求体为空时的 `message` 组成 - -``` -{method}\n -{url_path_and_query}\n -{headers}\n -\n -``` - -:::tip -请求体 body,无需处理请求参数顺序。建议用 String 接收 Webhook 请求的 RequestBody,验证请求签算后,再完成数据的反序列化 -::: - - - -<> - -```java -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpClient; -import java.net.http.HttpResponse; -import java.util.*; -import java.util.stream.Collectors; -public class SignatureExample { - - public static String signRequest(String method, URI uri, String body, Map> headers, String secret) throws Exception { - String urlPathAndQueryPart = uri.getRawPath() + (uri.getRawQuery() != null ? "?" + uri.getRawQuery() : ""); - String headersPart = getHeadersPart(headers); - - // 包含请求体的签名字符串部分 - String signParts = method.toUpperCase() + "\n" + urlPathAndQueryPart + "\n" + headersPart + "\n" + body + "\n"; - - System.out.println("Sign Parts:\n" + signParts); - - Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); - SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256"); - sha256_HMAC.init(secretKey); - - byte[] hash = sha256_HMAC.doFinal(signParts.getBytes()); - return Base64.getEncoder().encodeToString(hash); - } - - private static String getHeadersPart(Map> headers) throws Exception { - TreeMap sortedHeaders = new TreeMap<>(); - headers.forEach((key, value) -> { - String lowerKey = key.toLowerCase(); - if (lowerKey.equals("x-tap-sign")) { - return; - } - if (lowerKey.startsWith("x-tap-")) { - if (value.size() > 1) { - throw new RuntimeException("Invalid header, " + lowerKey + " has multiple values"); - } - sortedHeaders.put(lowerKey, value.get(0)); - } - }); - - return sortedHeaders.entrySet().stream() - .map(entry -> entry.getKey() + ":" + entry.getValue()) - .collect(Collectors.joining("\n")); - } - - public static void main(String[] args) { - try { - String secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO"; - String body = "{\"event_type\":\"charge.succeeded\",\"order\":{\"order_id\":\"1790288650833465345\",\"purchase_token\":\"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=\",\"client_id\":\"o6nD4iNavjQj75zPQk\",\"open_id\":\"4+Axcl2RFgXbt6MZwdh++w==\",\"user_region\":\"US\",\"goods_open_id\":\"com.goods.open_id\",\"goods_name\":\"TestGoodsName\",\"status\":\"charge.succeeded\",\"amount\":\"19000000000\",\"currency\":\"USD\",\"create_time\":\"1716168000\",\"pay_time\":\"1716168000\",\"extra\":\"1111111111111111111\"}}"; - URI uri = new URI("https://example.com/my-service/v1/my-method"); - - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(uri) - .header("Content-Type", "application/json; charset=utf-8") - .header("X-Tap-Ts", "1716168000") - .header("X-Tap-Nonce", "V7v7zJ"); - - // Considering body to be added for a POST request - HttpRequest request = requestBuilder - .POST(HttpRequest.BodyPublishers.ofString(body)) - .build(); - - String method = "POST"; // Since we are using the POST method in this example - String signature = signRequest(method, uri, body, request.headers().map(), secret); - System.out.println("Signature: " + signature); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - - -<> - -```go -package main - -import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "fmt" - "io" - "net/http" - "sort" - "strings" -) - -func main() { - //nolint:gosec - secret := "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO" - body := []byte(`{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}`) - url := "https://example.com/my-service/v1/my-method" - method := "POST" - header := http.Header{ - "Content-Type": {"Content-Type: application/json; charset=utf-8"}, - "X-Tap-Ts": {"1716168000"}, - "X-Tap-Nonce": {"V7v7zJ"}, - } - ctx := context.Background() - req, _ := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(body)) - req.Header = header - sign, err := Sign(req, secret) - if err != nil { - panic(err) - } - req.Header.Set("X-Tap-Sign", sign) - fmt.Println(sign) -} - -// Sign signs the request. -func Sign(req *http.Request, secret string) (string, error) { - methodPart := req.Method - urlPathAndQueryPart := req.URL.RequestURI() - headersPart, err := getHeadersPart(req.Header) - if err != nil { - return "", err - } - bodyPart, err := io.ReadAll(req.Body) - if err != nil { - return "", err - } - signParts := methodPart + "\n" + urlPathAndQueryPart + "\n" + headersPart + "\n" + string(bodyPart) + "\n" - fmt.Println(signParts) - h := hmac.New(sha256.New, []byte(secret)) - h.Write([]byte(signParts)) - rawSign := h.Sum(nil) - sign := base64.StdEncoding.EncodeToString(rawSign) - return sign, nil -} - -// getHeadersPart returns the headers part of the request. -func getHeadersPart(header http.Header) (string, error) { - var headerKeys []string - for k, v := range header { - k = strings.ToLower(k) - if !strings.HasPrefix(k, "x-tap-") { - continue - } - if k == "x-tap-sign" { - continue - } - if len(v) > 1 { - return "", fmt.Errorf("invalid header, %q has multiple values", k) - } - headerKeys = append(headerKeys, k) - } - sort.Strings(headerKeys) - headers := make([]string, 0, len(headerKeys)) - for _, k := range headerKeys { - headers = append(headers, fmt.Sprintf("%s:%s", k, header.Get(k))) - } - return strings.Join(headers, "\n"), nil -} -``` - - - -<> - -```python -import base64 -import hashlib -import hmac -from typing import Dict, List -from urllib.parse import urlparse - -def sign_request(method: str, url: str, body: str, headers: Dict[str, List[str]], secret: str) -> str: - # 提取URL路径和查询字符串部分 - parsed_url = urlparse(url) - url_path_and_query = parsed_url.path + ('?' + parsed_url.query if parsed_url.query else '') - - # 获取符合条件的头部信息并排序 - headers_part = get_headers_part(headers) - - # 拼接签名字符串 - sign_parts = f"{method}\n{url_path_and_query}\n{headers_part}\n{body}\n" - - print("Sign Parts:\n", sign_parts) - - # 使用HMAC SHA256算法生成签名 - raw_sign = hmac.new(secret.encode(), sign_parts.encode(), hashlib.sha256).digest() - - # 对签名进行Base64编码 - sign = base64.b64encode(raw_sign).decode() - - return sign - -def get_headers_part(headers: Dict[str, List[str]]) -> str: - # 筛选和排序头部 - headers = {k.lower(): v for k, v in headers.items() if k.lower().startswith('x-tap-') and k.lower() != "x-tap-sign"} - header_keys = sorted(headers.keys()) - - # 组装头部字符串 - headers_str = '\n'.join(f"{k}:{headers[k][0]}" for k in header_keys if len(headers[k]) == 1) - - if any(len(headers[k]) > 1 for k in header_keys): - raise ValueError("Invalid header: has multiple values") - - return headers_str - -# 示例使用 -secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO" -url = "https://example.com/my-service/v1/my-method" -method = "POST" -body = '{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345",' \ - '"purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk",' \ - '"open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id",' \ - '"goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD",' \ - '"create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}' -headers = { - "Content-Type": ["Content-Type: application/json; charset=utf-8"], - "X-Tap-Ts": ["1716168000"], - "X-Tap-Nonce": ["V7v7zJ"], -} - -try: - sign = sign_request(method, url, body, headers, secret) - print("Signature: ", sign) -except Exception as e: - print("Error: ", str(e)) - -``` - - - -<> - -```php - $value) { - $key = strtolower($key); - if (count($value) > 1) { - throw new Exception("Multiple values for header: " . $key); - } - if ($key === "x-tap-sign") { - continue; - } - if (strpos($key, 'x-tap-') === 0) { - $signHeaders[$key] = $value; // Assuming each header has a single value - } - } - $headerKeys = []; - foreach ($signHeaders as $key => $value) { - if (!(strpos($key, 'x-tap-') === 0)) { - continue; - } - $headerKeys[] = $key; - } - sort($headerKeys); - $headerParts = []; - foreach ($headerKeys as $key) { - $headerParts[] = $key . ':' . $signHeaders[$key][0]; - } - return implode("\n", $headerParts); -} - -// 示例使用 -$secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO"; -$url = "https://example.com/my-service/v1/my-method"; -$method = "POST"; -$body = '{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}'; -$headers = [ - "Content-Type" => ["Content-Type: application/json; charset=utf-8"], - "X-Tap-Ts" => ["1716168000"], - "X-Tap-Nonce" => ["V7v7zJ"] -]; -try { - $sign = signRequest($method, $url, $body, $headers, $secret); - echo "Signature: " . $sign . "\n"; -} catch (Exception $e) { - echo "Error: " . $e->getMessage() . "\n"; -} - -``` - - - - - -上述示例签算结果 - -``` -# 参与签算部分 -POST\n -/my-service/v1/my-method\n -x-tap-nonce:V7v7zJ\n -x-tap-ts:1716168000\n -{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}\n - -# 签算结果 (X-Tap-Sign) -PyKQzlI65e0I9noVxcQc7FPU3nEyEFHKfRde65F6vhI= -``` - -## 通用对象结构描述 - -### 订单信息 - -| **参数** | **类型** | **是否必须** | **描述** | -| -------------- | -------- | ------------ | ------------------------------------------------------------ | -| order_id | string | Y | 订单唯一 ID | -| purchase_token | string | Y | 用于订单核销的 token | -| client_id | string | Y | 应用的 Client ID | -| open_id | string | Y | 用户的开放平台 ID | -| user_region | string | Y | [用户地区](/sdk/TapPayments/appendix/regions-currencies) | -| goods_open_id | string | Y | 商品唯一 ID | -| goods_name | string | Y | 商品名称 | -| status | string | Y | [订单状态](/sdk/TapPayments/develop/server/#订单状态) | -| amount | string | Y | 金额(本币金额 x 1,000,000) | -| currency | string | Y | [币种](/sdk/TapPayments/appendix/regions-currencies) | -| create_time | string | Y | 创建时间 | -| pay_time | string | Y | 支付时间 | -| extra | string | Y | 商户自定义数据,如角色信息等,长度不超过 255 UTF-8 字符 | - -#### 订单状态 - -| **订单状态** | **描述** | -| ---------------- | ------------ | -| `charge.pending` | 待支付 | -| `charge.succeeded` | 支付成功 | -| `charge.confirmed` | 已核销 | -| `charge.overdue` | 支付超时关闭 | -| `refund.pending` | 退款中 | -| `refund.succeeded` | 退款成功 | -| `refund.failed` | 退款失败 | -| `refund.rejected` | 退款被拒绝 | - -### Webhook的事件枚举 - -| **event_type** | **描述** | -| ---------------- | -------- | -| `charge.succeeded` | 充值成功 | -| `refund.succeeded` | 退款成功 | -| `refund.failed` | 退款失败 | - -## 错误码 - -| **code** | **描述** | -| -------- | ------------ | -| `-1` | 非法请求 | -| `100000` | 支付服务异常 | -| `100004` | 订单不存在 | -| `100018` | 订单验证错误 | diff --git a/.ci/hk/en/sdk/TapPayments/develop/unity.mdx b/.ci/hk/en/sdk/TapPayments/develop/unity.mdx deleted file mode 100644 index 6d70ca494..000000000 --- a/.ci/hk/en/sdk/TapPayments/develop/unity.mdx +++ /dev/null @@ -1,252 +0,0 @@ ---- -title: Unity 集成指南 -sidebar_position: 1 ---- - - -import MultiLang from "/src/docComponents/MultiLang"; -import sdkVersions from '/src/docComponents/sdkVersions'; -import CodeBlock from '@theme/CodeBlock'; - - -## 环境要求 - -- 支持 Unity 2019.4 及以上版本 - -## 准备 - -- 参照 [准备工作](/sdk/start/get-ready/) 所述创建 app,配置 app 参数并且绑定 API 域名 -- 参照 [TapSDK 快速开始](/sdk/start/quickstart/) 配置包名和签名 - -## 获取 SDK - -:::tip -Unity 2020.3.15 之前的版本为了避免后面构建报错建议升级 Gradle 版本,可参考该 [Gradle 升级步骤文档](/sdk/TapPayments/develop/faq/#1unity-2020315-之前的版本升级-gradle-版本的操作步骤)。 -::: - -以下介绍了**通过 Unity Package Manager 导入**或者**修改 Packages/manifest.json 配置文件引入**两种方式,根据项目需要,任选其一即可: - -### 1、通过 Package Manager 可视化界面引入 - -因为 TapPayment SDK 依赖 **EDM4U(External Dependency Manager for Unity)** 来处理 Android 相关的依赖,因此引入依赖的步骤是**先安装 EDM4U 库,然后安装 TapPayment SDK**。 - -#### 安装 EDM4U - -下面介绍了两种方式安装 EDM4U,分别是通过 **OpenUPM 安装**以及**手动下载安装**; - -- **方法一:通过 OpenUPM 安装 EDM4U** - -EDM4U 可以通过 OpenUPM 进行下载,开发者可以通过 **Edit > Project Settings > Package Manager** 来注册使用 OpenUPM。 - -![](https://capacity-files.lcfile.com/gpS8BcTVJIdSpMxyDipIWORX4Gb2filA/NdoQbmRRvovet4x3LGUcsTXWnjb.png) - -- **方法二:手动下载安装** - -开发者可以通过 [Google APIs for Unity](https://developers.google.com/unity/archive#external_dependency_manager_for_unity) 下载 UPM 安装包. 并且通过[从本地磁盘安装](https://docs.unity3d.com/Manual/upm-ui-local.html)的方式进行安装。 - -#### 安装 TapPayment SDK - -TapPayment SDK 可以通过 NPMJS 进行安装, 开发者可以通过 **Edit > Project Settings > Package Manager** 来注册使用 NPMJS。 - -![](https://capacity-files.lcfile.com/sJDhXK4vAwAYX7BpQFh1SrQzeUmXk165/taptap.png) - -<> - 配置完成以后就可以在 Window > Package Manager > My Registries 中安装 TapTap Payments Global V2 ({sdkVersions.tapGlobalPayments.unity})。 - 如果安装目录中没有 TapTap Payments Global V2, 请尝试在 Edit > Project Settings > Package Manager 中重新注册 NPMJS。 - - -![](https://img.tapimg.com/market/images/4900806ff9290049ee68aa67ee05b71b.png) - -### 2、修改 Packages/manifest.json 文件 - - - {` "dependencies": { - "com.taptap.tds.payments.global.v2": "${sdkVersions.tapGlobalPayments.unity}", //添加 TapPayment - "com.unity.purchasing": "3.1.0", //TapPayment 所须依赖 - "com.google.external-dependency-manager": "1.2.179", //TapPayment 所须依赖 - "com.taptap.tds.common":"3.28.3", - "com.taptap.tds.login":"3.28.3", - ... - ... - }, - - // 添加 Registries - "scopedRegistries": [ - { - "name": "taptap", - "url": "https://registry.npmjs.org", - "scopes": ["com.tapsdk", "com.taptap", "com.leancloud"] - }, - { - "name": "openupm", - "url": "https://package.openupm.com", - "scopes": [ - "com.google" - ] - } - ]`} - - - -:::tip - -**处理安卓依赖失败** - -EDM4U 会自动监控 TapPayment SDK 引入的 Android 依赖,但是在某些特殊情况下自动依赖解析可能失败,开发者可以通过 **Assets > External Dependency Manager > Android Resolver > Force Resolve** 来强制依赖解析。在测试之前请确认对于 `com.taptap.android.payment:unity` 的依赖被加到了 mainTemplate.gradle 文件中,如果 EDM4U 没有自动添加这个依赖,开发者也可以通过手动添加的方式处理依赖。 -::: - -## SDK 指南 - -我们在 [Unity IAP](https://unity.com/products/iap) 的框架下实现了一个新的商店实现,通过这种方式降低已经有 Google Play 或者 App Store 内购的 app 的接入成本。如果开发者是第一次接触到内购的流程,我们会在接下来简要说明 Unity 的内购流程,开发者也可以在 Unity 的[官方文档](https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/manual/Overview.html) 中了解有关内购的详细内容。 - -### 初始化 Unity Gaming Services - -调用 `UnityServices.InitializeAsync()` 来一次性初始化 Unity Gaming Services。 这个方法会返回一个 `Task` 对象,开发者可以通过这个对象来获取初始化的状态信息。 - -```cs -using System; -using Unity.Services.Core; -using Unity.Services.Core.Environments; -using UnityEngine; - -public class InitializeUnityServices : MonoBehaviour -{ - public string environment = "production"; - - async void Start() - { - try - { - var options = new InitializationOptions() - .SetEnvironmentName(environment); - - await UnityServices.InitializeAsync(options); - } - catch (Exception exception) - { - // An error occurred during services initialization. - } - } -} -``` - -### 初始化 IAP - -确保你在初始化 IAP 的时候添加 `TapPurchasingModule`,并且设置正确的 `clientId` 和 `clientToken`。 - -```cs -using UnityEngine; -using UnityEngine.Purchasing; -using TapTap.Payments.Global.V2; - -public class MyIAPManager : IStoreListener { - - private IStoreController controller; - private IExtensionProvider extensions; - - public MyIAPManager () { - var builder = ConfigurationBuilder.Instance(TapPurchasingModule.Instance); - builder.Configure().SetClientId("Your Client ID Here"); - builder.Configure().SetClientToken("Your Client Token Here"); - builder.AddProduct("100_gold_coins", ProductType.Consumable, new IDs - { - {"100_gold_coins_google", GooglePlay.Name}, - {"100_gold_coins_mac", MacAppStore.Name} - }); - - UnityPurchasing.Initialize (this, builder); - } - - /// - /// Called when Unity IAP is ready to make purchases. - /// - public void OnInitialized (IStoreController controller, IExtensionProvider extensions) - { - this.controller = controller; - this.extensions = extensions; - } - - /// - /// Called when Unity IAP encounters an unrecoverable initialization error. - /// - /// Note that this will not be called if Internet is unavailable; Unity IAP - /// will attempt initialization until it becomes available. - /// - public void OnInitializeFailed (InitializationFailureReason error) - { - } - - public void OnInitializeFailed(InitializationFailureReason error, string str) - { - } - - - /// - /// Called when a purchase completes. - /// - /// May be called at any time after OnInitialized(). - /// - public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e) - { - return PurchaseProcessingResult.Complete; - } - - /// - /// Called when a purchase fails. - /// - public void OnPurchaseFailed (Product i, PurchaseFailureReason p) - { - } -} -``` - -### 发起内购流程 - -在 IAP 被成功初始化以后,你可以通过调用 `IStoreController.InitiatePurchase` 来发起内购流程。 - -```cs -// Example method called when the user taps a 'buy' button -// to start the purchase process. -public void OnPurchaseClicked(string productId) { - controller.InitiatePurchase(productId); -} -``` - -### 处理购买结果 - -购买完成时会调用商店监听器的 ProcessPurchase 函数。无论用户购买任何物品,您的应用程序都应该履单;例如,解锁本地内容或将购买收据发送给服务器以更新服务器端游戏模型。 - -此过程会返回结果以指出应用程序是否已完成对购买的处理: - -| 结果 | 描述 | -| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| PurchaseProcessingResult.Complete | 应用程序已完成对购买的处理,不应再次向应用程序通知此事。 | -| PurchaseProcessingResult.Pending | 应用程序仍在处理购买,除非调用 `IStoreController` 的 `ConfirmPendingPurchase` 函数,否则将在下一次应用程序启动时再次调用 `ProcessPurchase`。 | - -请注意,在初始化成功后,随时可能调用 `ProcessPurchase`。如果应用程序在 `ProcessPurchase` 处理程序执行过程中崩溃,那么在 Unity IAP 下次初始化时会再次调用它,因此您可能希望实现自己的额外重复数据删除功能。 - -#### 可靠性 - -Unity IAP 要求明确确认购买以确保在网络中断或应用程序崩溃的情况下可靠地完成购买。在应用程序离线时完成的任何购买都将在下次初始化时发送给应用程序。 - -#### 立即完成购买 - -返回 `PurchaseProcessingResult.Complete` 时,Unity IAP 立即完成交易(如下图所示)。 - -如果您正在销售可消耗商品并从服务器履行订单(例如,在网络游戏中提供游戏币),那么您不得返回 `PurchaseProcessingResult.Complete`。 - -否则,如果在保存到云端之前卸载应用程序,则购买的消耗品将面临丢失的风险。 - -![](https://capacity-files.lcfile.com/PYPIumvCKtaAwhJOPAf1qaApNMOdQ9nF/VrHVbfdKUoCHgXxmOYkccZLMn8e.png) - -#### 将购买保存到云端 - -如果要将消耗品购买交易保存到云端,您必须返回 `PurchaseProcessingResult.Pending`,并且仅在成功保存购买时才调用 `ConfirmPendingPurchase`。 - -返回 `Pending` 时,Unity IAP 会在底层商店中保持交易为未结 (open) 状态,直至确认为已处理为止,因此确保了即使在消耗品处于此待处理状态时用户重新安装您的应用程序,消耗品购买交易也不会丢失。 - -![](https://capacity-files.lcfile.com/zDX7oMjWBju6ME0K9i4zym9OOuIgCvrx/TFgMbu4JhohID1xHBnhcet7nnth.png) - -## 调试 -**我们当前只提供了 Android 的库实现, 请在 Android 环境下进行各功能调试**(其他平台会逐步补充完善)。 - diff --git a/.ci/hk/en/sdk/TapPayments/orders-refunds.mdx b/.ci/hk/en/sdk/TapPayments/orders-refunds.mdx deleted file mode 100644 index c58d21e15..000000000 --- a/.ci/hk/en/sdk/TapPayments/orders-refunds.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: 管理订单及处理退款 -sidebar_position: 4 ---- - - - - - -你可以通过 TapTap 开发者服务中心的「游戏服务」-「TapTap Payments」-「商品与订单」-「交易列表」来查看游戏内购买商品(IAP)的销售订单,并为用户办理退款请求。 - -![tappay](https://capacity-files.lcfile.com/8WMyxhKLhNA1SniirMSVSOt56kS2z2wb/tappay-orders.png) - -## 查找订单 - -TapTap Payments 支持多种查询方式: - -* __按用户标识查询__:输入用户的 TapTap ID,即可查询到当前用户购买的所有订单; - -* __按订单号查询__:用户在游戏内购买时创建的 TapTap 订单号; - -* __按交易流水号查询__:开发者自定义的订单 ID; - -* __按订单状态查询__:根据订单交易状态查询,包括“支付中”、“已支付”、“已验证”、“已过期”、“已退款”等; - -* __按订单时间查询__:筛选用户订单发生的日期查询; - -## 订单状态 - -订单页面会显示每一笔用户购买的状态,如下: - -| 订单支付状态 | 说明 | -| ---- | ---- | -| 支付中 | 用户还未完成付款,或系统正在处理订单 | -| 订单已支付 | 用户完成付款,系统已成功向用户收款 | -| 订单已过期 | 订单长时间未进行付款,或付款已取消 | - -| 订单退款状态 | 说明 | -| ---- | ---- | -| 订单退款中 | 用户的退款申请已被允许,系统正在处理退款 | -| 订单已退款 | 系统已全额退款至用户支付账户 | -| 订单已驳回 | 用户的退款申请不被允许 | -| 订单退款失败 | 系统退款不成功,或用户的付款方式遭到拒绝 | - -## 办理退款 - -为了保障和维护用户的合理权益,TapTap 为用户提供退款的服务。当用户向 TapTap 发起退款申请时, TapTap 会向开发者收集用户消费及物品消耗的材料,用于是否允许退款的决策判断。你可在「游戏服务」-「TapTap Payments」-「处理退款」查看到用户提交的退款请求: - -* __退款__:同意 TapTap 全额退款给用户,系统将用户支付金额全部退回至用户的付款账户。退款后,你有责任告知用户对游戏内商品的处理方式。 - -* __驳回__:不同意 TapTap 退款给用户,你必须向 TapTap 和用户说明不允许退款的合理原因,或出示用户已消费的相关凭证。 - -如若开发者消极处理或不予处理,TapTap 拥有最终决策处理权。 diff --git a/.ci/hk/en/sdk/TapPayments/overview.mdx b/.ci/hk/en/sdk/TapPayments/overview.mdx deleted file mode 100644 index 936668ef4..000000000 --- a/.ci/hk/en/sdk/TapPayments/overview.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: 概述 -sidebar_position: 1 ---- - -# 概述 - -## **什么是 TapTap Payments?** - -TapTap Payments 是一项付款服务,利用该服务可以在 TapTap 上架的游戏中销售各种商品 - -- **触达全球受众:**我们提供多种[全球主流支付方式](/sdk/TapPayments/appendix/payment-methods/),覆盖 [173 个国家和地区](/sdk/TapPayments/appendix/regions-currencies),支持 42 种货币,触达 TapTap 千万硬核玩家 -- **88%/12% 收入分成:**获得 88% 的收入分成,更具竞争力的价格,释放全球收入潜力 -- **流畅接入:** 提供简洁、易用、稳定的 SDK,让开发者快速接入支付功能,节省开发时间和成本 -- **全球合规:**经验丰富的全球财税法经验,符合各地合规标准,让您专注于游戏开发与变现 - -通过在你您的游戏中集成 TapTap Payments,您可以销售两种不同类型的游戏内商品: - -- **消耗型商品:**消耗型商品是指当用户消耗该商品时,游戏会分配相关联的游戏内容,而用户随后可以再次重复购买的商品。例如金币、道具等。 -- **非消耗型商品:**非消耗型商品是指购买一次便能永久使用的商品。例如付费升级和关卡包等。 - -## **销售游戏内商品的三个基本步骤** - -**第一步 申请商业卖家身份** - -在您可以对您的游戏内商品收费之前,需要通过我们的商务伙伴,申请商业卖家身份并注册您的付款信息,完成审核后将为您开通相关功能 - -**第二步 使用我们的 SDK 开发您的游戏 ** - -下载 TapTap Payments SDK 并将其集成到您的游戏中,我们提供了 Unity 和 Android 的集成选项: - -- [Unity 集成指南](/sdk/TapPayments/develop/unity) -- [Android 集成指南](/sdk/TapPayments/develop/android) - -**第三步 为您的游戏添加商品** - -在 [TapTap 开发者中心](https://developer.taptap.io/)添加商品并注册相关信息:项目 ID、项目标题、项目类型、价格等。进一步了解: - -- [在开发者中心管理商品](/sdk/TapPayments/product-management) diff --git a/.ci/hk/en/sdk/TapPayments/payout.mdx b/.ci/hk/en/sdk/TapPayments/payout.mdx deleted file mode 100644 index def5cc958..000000000 --- a/.ci/hk/en/sdk/TapPayments/payout.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: TapTap 付款 -sidebar_position: 5 ---- - - - - - -TapTap 付款仅用于 TapTap Payments 服务中,按照一定周期的付款时间为开发者结算收益,并付款至开发者的财务账户。在准备接收 TapTap 的付款之前,请先确认已完成财务付款资料: - -1. TapTap 开发者服务中心 -「财务与管理」-「财务主体」; - -2. 填写主体信息与财务账户信息; - -## 付款周期和报告 - -TapTap 的付款周期通常为自然月结束后的 45 天,例如自然月 1 月份( 1 月 1 日 - 1 月 31 日)的收入,会在 1 月 31 日结束之后,进入核算阶段,并在此后 45 天内完成付款,即 3 月 15 日是 TapTap 的最晚付款时间。请注意你的银行可能需要几天的时间才能将相应款项计入你的账户。 - -在进入核算阶段后,你可在「财务与管理」-「对账与结算」看到需要结算的报告单,包括在当前账期内,商品销售金额、用户退款金额、费用扣除、实际收益金额等数据。你必须对报告单进行仔细的核对,并在 7 个工作日之内与 TapTap 确认最终数目。 - -![tappay](https://capacity-files.lcfile.com/kmsrDieltrX4CXHUwcIwfMlrWLtuxsN6/tappay-dc-bill.png) - -## 状态说明 - -TapTap 根据游戏销售金额,退款、当地税费、支付渠道手续费等进行核对结算,计算出开发者所得金额。核算周期为每月一次,在每个自然月结束之后进入核算阶段。 - -* __待核算__:等待 TapTap 核算完成。 - -* __待确认__:TapTap 已经核算完成的账期,需要开发者确认核算结款金额。 - -* __待付款__:等待 TapTap 付款。 - -* __已付款__:TapTap 已将账单开发者实际收益金额付款至开发者预留的银行账户。 - -## 付款最低限额 -TapTap 付款的最低金额是 100 美元,如果你在上个月度的销售额没有超过 100 美元,则会累计到下个月度一并发放。如果你的销售额超过了 100 美元,但出于对服务费或跨境税的考虑,你也可以向 TapTap 申请累计到一定额度后付款。 - -## 付款货币 -TapTap 统一以“美元”进行付款。付款时由于接收地区或银行的不同,可能会产生一些跨境费用或转账手续费用,请联系你所在银行查看。 - -## TapTap 的服务费 -开发者使用 TapTap Payments 服务,TapTap 会针对上述内容收取服务费用。 diff --git a/.ci/hk/en/sdk/TapPayments/product-management.mdx b/.ci/hk/en/sdk/TapPayments/product-management.mdx deleted file mode 100644 index 8978f96fc..000000000 --- a/.ci/hk/en/sdk/TapPayments/product-management.mdx +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: 商品管理 -sidebar_position: 3 ---- - - - - - -若要在游戏中提供内购商品,必须在 TapTap 开发者服务中心录入内购商品信息,并添加到你的游戏内。每个商品须关联一个游戏应用(Client),并且只能用于该游戏。在使用过程中,你可以创建或删除商品,也可以修改完善现有商品的信息。 - -## 创建游戏内商品 - -在创建商品之前,请务必仔细规划您的产品 ID。商品 ID 对于您的游戏来说必须是唯一的,并且在创建后不能更改或重复使用。 - -- 商品 ID 必须以数字或字母开头,长度不超过 30 个字符,可以包含数字 (0-9)、英文字母 (a-z/A-Z)、下划线 (_) 和句点 (.) -- 创建商品后,您无法更改或重复使用商品 ID - -### 创建单个商品 - -1. 前往 TapTap 开发者服务中心,选择「游戏服务」-「TapTap Payments」-「商品与订单」; - -2. 点击「添加商品」,在弹出的表单中填写您的商品详细信息; - - - 商品类型:消耗型和非消耗型商品,详情请参见[商品类型](/sdk/TapPayments/overview) - - 商品 ID:商品的唯一标识,创建后不可更改和重复使用; - - 商品名称:商品的短名称,用户可见。建议保持在 80 个字符以内; - - 商品描述:对商品内容的细节描述,建议保持在 100 个字符以内; - - 商品定价:期望售卖的价格,默认以美元定价,开发者可下载所有定价等级查看不同国家的价格; - -3. 完成信息填写后,点击「提交审核」并等待审核结果; - -4. 审核通过后,如需商品生效,可点击商品对应操作列的「上架」按钮即可; - - -> 注意同一个游戏内商品 ID 不能重复,提交后将不可更改,删除后也无法重复使用,请仔细规划。商品在未上架状态时均可修改名称、描述、价格等信息,上架后的商品不可修改信息。具体见「编辑/修改商品」。 - - -![mahua](https://capacity-files.lcfile.com/GSpWNUzJ025ypkp42jD180Ko7u9wDiUA/add_sku.png) - -## 批量创建多个商品 - -1. 如需同时创建多个游戏内购商品,您可以选择使用「批量导入」功能,上传包含每个商品详细信息的 CSV 文件; - -2. 前往 TapTap 开发者服务中心,选择「游戏服务」-「TapTap Payments」-「商品与订单」; - -3. 点击「批量导入」: - - - 下载模版; - - 上传商品详细信息 CSV 文件; - - 请确保每个商品都为单独一行,单次上传不得超过 100 个商品,下载模板; - - 当商品详细信息 CSV 中的商品 ID 与后台列表中现有商品 ID 匹配时,此次商品详细信息 CSV 中的商品不被上传; - -> 商品详细信息 CSV 文件上传成功后,系统将直接提交审核,若上传了系统不支持的语言,则会忽略该语言内容并由默认语言替代。 - -![tappay](https://capacity-files.lcfile.com/6GwDJdk5zVDaVp1znwrPRUEx1TcmjQC5/add_sku_batch.png) - -## 编辑/修改商品 - -| 商品状态 | 编辑与操作 | -| ---- | ---- | -| 审核通过,待上架 | 可修改商品名称、商品说明 / 可上架 / 可删除 | -| 审核失败 | 可修改商品名称、商品说明 / 可删除 | -| 已上架 | 可修改商品名称、商品说明 / 可下架 | -| 已下架 | 可删除 | - -## 商品状态说明 - -* __审核中__:商品在发布之前需要提交审核,审核通过则进入待上架状态,审核失败会给出失败原因,需要根据失败原因进行修改后重新提交; - -* __审核通过__:审核通过的商品,处于待上架状态,可随时发布上架,上架的商品才会在生产环境生效; - -* __已上架__:上架后的商品,会在生产环境生效; - -* __已下架__:下架的商品,无法在生产环境使用。如用户点击该商品后提示“该商品已下架”; - -* __已删除__:开发者可以删除商品,删除的商品不可再复用; - -## 语言和本地化 - -如果你的游戏会在不同的国家/地区发布,那么也建议为你的商品添加本地化信息。通过添加不同地区的语言完成本地化信息的录入。 - -![tappay](https://capacity-files.lcfile.com/RuxW5n9niJRDFIVVAa2uOURPHbnpgx4F/product-multilingual.png) diff --git a/.ci/hk/en/sdk/embedded-moments/features.mdx b/.ci/hk/en/sdk/embedded-moments/features.mdx deleted file mode 100644 index 4317e639d..000000000 --- a/.ci/hk/en/sdk/embedded-moments/features.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Embedded Moments Features -sidebar_label: Features -sidebar_position: 1 ---- - -## Introduction - -By adding Embedded Moments to your game, you can have players access TapTap’s forum without leaving the game so they can easily browse walkthroughs, share their game highlights, and interact with other players and officials. - -## Core Advantages - -**For game developers:** - -- Allow players to share their gameplay with just a single click. -- Display officially published content on players’ feeds. -- View what players have posted and provide timely feedback to them. - -**For game players:** - -- Communicate and interact with other players on a delayed basis while gaming. -- Look up walkthroughs and top players’ solutions when stuck on certain scenes in the game. - -## Account System - -For a player to make a post or interact with other posts using Embedded Moments, they have to log in to a TapTap account. Therefore, you need to enable **[TapTap Login](/sdk/taptap-login/features/)** for your game before you can use Embedded Moments. - -![](https://capacity-files.lcfile.com/8C2DjigrigAz03drPAO3U49BcntTMhNz/login.png) - -## Moments - -### Games - -A player can access the TapTap forum directly from the “Games” module: - -- **Banners**: Banners can help you convey important notifications and events to players. This is a module exclusive to Embedded Moments. You can edit banners in “Gaming Ecosystem > Embedded Moments > Banner Configuration” and the edits will be displayed to the players once they are approved. -- **Feeds**: By default, a player will see the recommended posts when they open Embedded Moments. - -![](https://capacity-files.lcfile.com/vLkyj72lfbsuh1sv7fQOsrNFrsSEHJvp/Games.png) - -**Posting moments**: Players can post moments containing pictures and videos to the forum. - -![](https://capacity-files.lcfile.com/E9WodoFbRgquQaGG9rML5V4K9eUwYSqq/take_post.png) - -- **Interactions**: Players can **like, comment, and repost** other players’ moments. - -![](https://capacity-files.lcfile.com/NFiIX2c86mODiRJeLEk5Qw9F0zimdzPD/reply.png) - -### Feeds - -Players who logged in to TapTap can view the moments posted by their friends and the official accounts. When there are new moments available, there will be a **badge** on the “Follow” section on the navigation bar. This ensures that players will never miss out on important updates. - -![](https://capacity-files.lcfile.com/dY9bMBXUxolz6FL3fDO9cggQ1cFo7tjN/Following.png) - -### Messages - -Here players can view their messages triggered by events like interactions between players. - -![](https://capacity-files.lcfile.com/Uym4pMC7RV09OKcYdXRuFBdmJqhyAz3k/Notification.png) - -### Profile Page - -Players can find their past moments on the “Me” page. Here players can share their moments on other apps or delete their moments. - -![](https://capacity-files.lcfile.com/fACDedkGs1dbMAgFr2jyVU7w0k4fWCik/Me-global.png) - -### Search - -Players can search within Embedded Moments. Their search histories will be preserved so the system can give recommendations to the players. - -![](https://capacity-files.lcfile.com/1NB8Gzr77x8Xiybw6qd7EF8WaWOUuUiz/search.png) - -## SDK Features - -### Scenario-Based Portals - -You can make any of the objects in your game as a portal that opens up Embedded Moments. You can even specify landing pages for certain scenes in the Developer Center. This could be helpful if you want to allow players to quickly get help from the community when they’re stuck on certain scenes in the game. - -![](https://capacity-files.lcfile.com/ofMVqjfGnTpyvuYmesid6JdraU6pHIHX/FlashPartyHeroGuide.png) - -:::tip - -1. TDS doesn’t provide any guidelines for the design of portals. We encourage you to design your portals so they look harmonious with the scenes they’re placed at. -2. The landing page of a portal can be set up to be an article or a specific module according to your own needs. - -::: - -### Badges - -You can place buttons that can display badges in your game so that the players can be attracted to open the Embedded Moments when they see the badges. - -![](https://capacity-files.lcfile.com/ppMOzFxF2KRvfSbpt4bhPzfxuRUwGl51/Badges.png) - -:::tip - -1. Using badges can help you increase the chance for players to open the Embedded Moments. We encourage you to place buttons with badges on prominent places in your game. -2. The badges here share the same logic as the badges for the “Follow” module within the Embedded Moments. The new content posted by the users followed by the players will trigger a notification, and the interval of retrieving new notifications is once per minute (here 1 minute is the minimum interval; you can change it to 3 minutes, 5 minutes, etc.). -3. Once the player opens the Embedded Moments, the game needs to clear the badge and continue inquiring for the next display of the badge. - -::: - -### Quick Sharing - -Players can take screenshots within the game and quickly share them on Embedded Moments. Only text and images can be shared through this method. - -![](https://capacity-files.lcfile.com/qQu2WSd7lft6N2Ga62MKhbEkMkU9JhXK/share_data.gif) - -### Pop-up for Dynamically Closing Embedded Moments - -While the player is browsing Embedded Moments, if there are events that demand the player to immediately return to the game, a pop-up can be displayed to serve as a reminder and offer a shortcut for the player to close the Embedded Moments. - -![](https://capacity-files.lcfile.com/hBYaymebMR3iwSdFig6W53Gm2LHwzf6h/ClosingEmbeddedMoments.png) - -## Administration - -### Theme Configuration - -To have Embedded Moments fit better with the game scenes and not make players feel cut off, TDS allows you to customize the theme of the Embedded Moments. You can upload a background image and specify the colors of texts in “Game Services” > “Embedded Moments” > “Theme”. - -![](https://capacity-files.lcfile.com/cKlYPD05CODMXMbG6ygmxPhPpiHBGVeE/ThemeConfiguration.png) - -If the game only supports landscape _or_ portrait mode, you only need to provide one background image. Otherwise, you need to provide background images for both. -Images are subject to review, which usually takes 2 business days. - -### Banner Configuration - -You can set up banners in Embedded Moments to help you broadcast your events to the players. To set up banners, go to “Game Services” > “Embedded Moments” > “Banner Configuration”. **A title, background image, and link** are required for each banner. - -![](https://capacity-files.lcfile.com/uMGtukhn1wQQmLmJ0icNK6eJALcW7Yae/BannerConfiguration.png) - -You can add up to 5 banners that link to any website. -Banners are subject to review, which usually takes less than one day. - -### Scenario-Based Portal Configuration - -You can set up scenario-based portals in “Game Services” > “Embedded Moments” > “Scenario-based Portal”. Once you submit a **portal name, landing page type, and landing page**, you can use the generated portal ID in your game. This module doesn’t require any reviews, and you are free to change the landing page of each portal. - -![](https://capacity-files.lcfile.com/VATMXxjDD1U1OihW705a7BpuQgFfL1b4/Scenario-BasedPortalConfiguration.png) diff --git a/.ci/hk/en/sdk/push/guide/android-mixpush.mdx b/.ci/hk/en/sdk/push/guide/android-mixpush.mdx deleted file mode 100644 index 37e40be8d..000000000 --- a/.ci/hk/en/sdk/push/guide/android-mixpush.mdx +++ /dev/null @@ -1,221 +0,0 @@ ---- -title: FCM Push Guide -sidebar_label: FCM Push -sidebar_position: 4 -slug: /sdk/push/guide/android-mixpush/ ---- - - - - - -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; - -## FCM Push Overview - -Since Android 8.0, system permissions have been tightened and the lifecycle of third-party push channels has become more limited. This is why we have introduced our FCM push solution. It guarantees push delivery rate on mainstream Android systems. - -In the FCM push solution, the channel used for message delivery is no longer the long WebSocket connection that we maintain ourselves, but the communication is done through FCM. -A push message is sent in the following way: - -1. The developer calls the push API to request a push to all or specific devices; -2. The cloud push server forwards the request to the FCM; -3. The FCM sends the push message through the system channel to the mobile phone, while the system message receiver on the mobile phone displays the push message in the notification bar; -4. The end user clicks on the message and the target application or page is launched. - -The whole process is similar to Apple's APNs push, and the SDK is basically not called on the client side. - -The Android FCM push feature is only available for apps with Business Plans. If you would like to use this feature, please go to **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Settings > FCM** and enable FCM Push. - -Note that FCM push can be turned on and off at any time. If this option is turned off, the next Android push will automatically be sent to the client through our own channel, just like a normal push, with no effect other than the above mentioned issue of our own channel being limited on some ROMs. If this option is enabled again, the push channels of the vendors will be used again. - -When FCM push is enabled, a `registrationId` field is added to each device record in the Installation table to record the vendor-assigned registration ID (similar to the device token in APNs), and a `vendor` field is added (if this field does not exist, there is a client integration problem) with a value of `fcm`. - -### Displaying Badges or Push Notifications - -A badge appears on the application icon when there are new notifications. - -### Notification Bar Messages and Pass-Through Messages - -FCM supports pass-through messages to applications when the application is in the foreground. - -### Offline Push for Instant Messaging - -In the Instant Messaging service, offline pushing is available on iOS when the user goes offline. For Android users, there is no offline pushing when using the private channel because chat and push share the same long WebSocket connection, and if the user goes offline in the Instant Messaging service, pushing is necessarily unreachable. However, if FCM push is enabled, Android users have an offline push notification path because the push message goes through FCM, which is essentially the same as on iOS. -This means that when FCM push is enabled, the offline push and mute mechanisms in Instant Messaging are also available to Android users who use FCM push. - -### Limitations - -Push message length limit: - -- Messages support a maximum of 128 bytes for the application package name and 4KB for the message content. - -Minimum Android version requirement: - -- FCM push supports Android 4.1 or higher (minSdkVersion: 16). - -Description of factors that affect delivery rates: - -- Whether the device is online or not. If the device is offline, the push service will cache messages and push them to the device when it is online. -- Whether the application that integrates the Push Notification SDK is uninstalled on the mobile device. -- Whether the network status of the mobile device is stable. -- The security control policy of the mobile device. -- The delivery of transmissions is affected by the Android system and whether the application is running in the background. - -## Integration - -FCM push is essentially dependent on the FCM SDK and server-side capabilities. Our client SDK is a wrapper around the FCM SDK and the actual push requests are passed through LeanCloud to the FCM backend. Our client-side SDK may not be able to keep up with the iteration speed of the FCM, so we recommend that you interface directly with the FCM SDK and store the FCM-assigned "registration id" and FCM identifier (see vendor in previous chapter) in the device information table (`Installation`) so that you can then use our push API to correctly send push messages to all devices. This will allow us to send correct push messages to all devices via our push API. - -### Integration on the Client Side - -The developer inherits their implementation class from `FirebaseMessagingService` and then calls the code in the `onNewToken` callback function to save it as in the example above (remember to replace `vendor` with `fcm`). See [LCFirebaseMessagingService](https://github.com/leancloud/java-unified-sdk/blob/master/android-sdk/leancloud-fcm/src/main/java/cn/leancloud/LCFirebaseMessagingService.java#L69) for sample code. - -### Server-Side API for Sending FCM Pushes - -See [Push Notification REST API Guide](/sdk/push/guide/rest/). - -If you want to integrate our packaged FCM push SDK, you can read on, but if you want to access the FCM SDK yourself, you can ignore the following. - -## FCM Push Library Components - -[FCM](https://firebase.google.com/docs/cloud-messaging) (Firebase Cloud Messaging) is a service provided by Google/Firebase to send push notification messages to mobile phones. When integrating, the backend must configure the push key and certificate needed to connect to the FCM server, and the FCM-related token is applied by the LeanCloud SDK. - -### Environment Requirements - -The FCM client must run on a device running Android 4.1 or higher with the Google Play Store application installed, or in an emulator running Android 4.1 with Google API support. See [Set up a Firebase Cloud Messaging client app on Android](https://firebase.google.com/docs/cloud-messaging/android/client) for specific requirements. - -### Integrate the SDK - -#### Add Firebase Configuration File - -Download the latest configuration file (google-services.json) from the Firebase console and add it to the module (application level) directory of the application. - -#### Add Google Service Plugin to Gradle - -First, add rules to the root (project level) Gradle file (build.gradle) to include the Google services Gradle plugin: - -```groovy -buildscript { - - repositories { - // Check that you have the following line (if not, add it): - google() // Google's Maven repository - } - - dependencies { - // ... - - // Add the following line: - classpath 'com.google.gms:google-services:4.3.5' // Google Services plugin - } -} - -allprojects { - // ... - - repositories { - // Check that you have the following line (if not, add it): - google() // Google's Maven repository - // ... - } -} -``` - -Then, in the module (application level) Gradle file (usually app/build.gradle), apply the Google services Gradle plugin: - -```groovy -apply plugin: 'com.android.application' -// Add the following line: -apply plugin: 'com.google.gms.google-services' // Google Services plugin - -android { - // ... -} -``` - -#### Import SDK FCM Package - -In the module (application level) Gradle file (usually app/build.gradle), add the dependencies to the dependencies: - - -{`dependencies { - implementation 'cn.leancloud:leancloud-fcm:${sdkVersions.leancloud.java}@aar' - // For Instant Messaging and Push Notification - implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'\n - // Import the BoM for the Firebase platform - implementation platform('com.google.firebase:firebase-bom:27.0.0') - // Declare the dependencies for the Firebase Cloud Messaging and Analytics libraries - // When using the BoM, you don't specify versions in Firebase library dependencies - implementation 'com.google.firebase:firebase-messaging' - implementation 'com.google.firebase:firebase-analytics' -}`} - - -#### Modify the Application Manifest - -Add the following to your application's `AndroidManifest` file: - -- PushService service. - - ```xml - - ``` - -- `LCFirebaseMessagingService` service. This service must be added if you want to do any message processing in the background other than receiving application notifications. To receive notifications, receive data payloads, send uplink messages, etc. in the foreground application, you must inherit this service. - - ```xml - - - - - - ``` - -- (Optional) The metadata element in the application component used to set the default notification icon and color. If the incoming message does not explicitly set an icon and color, Android uses these values. - - ```xml - - - ``` - -- (Optional) As of Android 8.0 (API level 26), Android supports and recommends the use of [notification channels](https://developer.android.com/develop/ui/views/notifications#ManageChannels). FCM provides default notification channels with basic settings. If you want to [create](https://developer.android.com/develop/ui/views/notifications/channels) and use your own default channels, please set `default_notification_channel_id` to the ID of your notification channel object (as shown below); if the incoming message does not explicitly set a notification channel, FCM will use this value. - - ```xml - - ``` - -- If FCM is critical to the functionality of your Android app, be sure to set `minSdkVersion 16` or higher in the app's `build.gradle`. This will ensure that the Android app cannot be installed in an environment that does not allow it to function properly. - -### Application Initialization - -With FCM push, no special initialization is required for the client application. If the registration is successful, a new record should appear in the `_Installation` table with the field **vendor** as `fcm`. - -### Configure the Console (Set FCM's ProjectId and Private Key) - -You can get the private key file for the server side to send push requests in [Firebase Console](https://console.firebase.google.com/). Associate this file and the ProjectId with the cloud service application via **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Settings > FCM**. - -## Unregister FCM Push - -For users who are already registered for FCM push, if they want to unregister for FCM push and use the cloud service's own WebSocket instead, they can call the following function: - -```java -LCMixPushManager.unRegisterMixPush(); -``` - -This function is asynchronous. If the deregistration is successful, the log will say `Registration canceled successfully`. If the deregistration fails, the log will say `unRegisterMixPush error`. - -## Suggestions for Troubleshooting - -- If there are conditions that are not met when registering, the SDK will log the reason for the registration failure, for example `register error, mainifest is incomplete` means the manifest is not filled correctly. If the registration is successful, the corresponding record in the `_Installation` table should have the field **vendor** that is not null. - -- If the registration continues to fail, please submit a ticket or post to the forum with the relevant logs, device model, and system version number, and we will follow up to assist in troubleshooting. diff --git a/.ci/hk/en/store/store-admin.mdx b/.ci/hk/en/store/store-admin.mdx deleted file mode 100644 index 3867c654d..000000000 --- a/.ci/hk/en/store/store-admin.mdx +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: Authority Management -sidebar_position: 25 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -## Add Member or Master Administrator - -Super administrators have all the authorities as a provider and authorities for a specific game. Only super administrators can add new members to a game team or assign game management authorities. - -Go to Authority Management >> Click on Add User >> Enter user’s nickname or TapTap ID >> Select ‘Members’ or ‘Master Administrator’ for Role Type. - -![ ](/img/Administrator-Settings-1.png) - -![ ](/img/Administrator-Settings-2.png) - -## Role Configuration - -### View authority of default roles - -In Authority Management >> Role Configuration, there is a set of default roles that are commonly used such as developer, publisher and the game’s administrator. The authorities of default roles can be found in ‘View Role’ and cannot be changed. - -![ ](/img/Administrator-Settings-3.png) - -![ ](/img/Administrator-Settings-4.png) - -### Add Roles - -There might be some inconvenience in the actual work as the authority of default roles cannot be changed. In this case, go to Add Role and find two types of roles: the Game’s Role that deals with one specific game and the Provider’s Role that deals with multiple games of the same provider. After adding a new role, Master Administrator may name the role and select the authority according to the actual needs. - -![ ](/img/Administrator-Settings-5.png) - -![ ](/img/Administrator-Settings-6.png) - -## Role Configuration for Members - -### Provider’s Authority - -A member can obtain the provider’s authorities when necessary. For example, while provider administrator may manage the release and update of the game, members of finance role may focus on the orders, settlement, and reconciliation, etc. - -Go to Authority Management >> Select user >> Click on Edit Authority to set up this user’s authority. - -![ ](/img/Administrator-Settings-7.png) - -![ ](/img/Administrator-Settings-8.png) - -![ ](/img/Administrator-Settings-9.png) - -### Member’s authorities for a specific game - -The super administrator can assign the authorities for one or more games to the corresponding members at the same time. - -![ ](/img/Administrator-Settings-10.png) - -The authority of members for each game can be set separately. - -![ ](/img/Administrator-Settings-11.png) - -## Delete Member - -Go to Authority Management >> Find the user >> Click on ![‘deleteadmin’](https://img.tapimg.com/market/images/2e5c836549d866d6d44036d158095cbb.png). A pop-up window will appear, where the user can be added as a Master Administrator or be deleted. Notice that only Super Administrator can make this action. - -![ ](/img/Administrator-Settings-12.png) - -## Master Administrator of the forum (Group) - -### Add Master Administrator - -The master administrator of the Game’s Group has the highest management authority. Each game can be linked to one master administrator of the Group. You can link it by going to Group Settings >> Click on Tap.io to link with a user - -![ ](/img/Administrator-Settings-13.png) - -### Access Group Management Page - -After you have set up the Master Administrator of the Group, you can go to the game page >> click on the Group tab and click on Show All on the upper right corner to access the Group’s management features. - -![ ](/img/Administrator-Settings-14.png) - -![ ](/img/Administrator-Settings-15.png) - -## Q&A - -### 1. Can the Super Administrator be changed? Is it possible to set up a new Super Administrator? - -Yes. - -Send your request to [international_operation@taptap.com](mailto:international_operation@taptap.com). Please describe your needs and provide a valid proof that you are the developer of this game, or you have the right to make such change. - -**Template** - -> **Subject:** -> Unlink TapTap Super Admin  -  XXX (provider title) - ---- -> -> The title or the link to the page of the provider that needs to change the Super Administrator: -> -> Proof that you are the developer of this game, such as a scanned copy of the Business License or other documents (this can be included in the attachment). -> -> Briefly explain why you need to make this change: -> -> Contact of the applicant: - -Our staff will contact you within 2 business days after receiving your email. Please check your inbox regularly. - -After the original Master Administrator is deleted, you can submit the developer application in the name of the same provider. Once your application is approved, you will become the new Super Administrator of this provider account by default. - -### Can I change the linked email? - -Yes. - -Please make sure that the new email address has not been linked with any TapTap account. Please send your request to operation@taptap.com with proof that you or your organization is the owner of this account. - - -**Template** - -> **Subject** -> Change Linked Email - XXX (title of provider/game) - ---- -> -> TapTap ID whose linked email needs to be changed: -> -> Old email address (can be empty): -> -> New email address: -> -> Briefly explain the reason for this change: -> -> Proof of you as the developer: - -After receiving your email, our staff will forward your application to the relevant team, which will process within 2 business days. Once the process is completed, you will be able to receive verification codes with the new email address. - diff --git a/.ci/hk/en/store/store-agree.mdx b/.ci/hk/en/store/store-agree.mdx deleted file mode 100644 index 56885547c..000000000 --- a/.ci/hk/en/store/store-agree.mdx +++ /dev/null @@ -1,573 +0,0 @@ ---- -title: TapTap Game Review Specifications and Rules -sidebar_position: 20 ---- - - - -Please make sure that the game you submit in the Developer Center conforms to the TapTap Game Review Specifications and Rules. Games that violate the above rules may be removed from TapTap. - - - -## **Overview** - - - -### 1.1 Laws and Regulations - - - -1.1.1 -Your game cannot contain any illegal content or other sensitive information, including but not limited to information regarding politics, gambling, violence, gore, animal or child abuse and sexual content. - - - -1.1.2 -Your game shall not harass or intimidate an individual or group of individuals. It shall not infringe on the legitimate rights and interests of an individual or group of individuals (including but not limited to the copyright, trademark rights, portrait rights, rights to reputation, etc.) - - - -1.1.3 -Your game shall not make overly realistic representations of weapons. For example, it must not address the manufacturing process and parameters of weapons. Your game shall not promote violation of laws or misuse of weapons. - - - -1.1.4 Your game shall not violate any legal requirements in any region where it is made available. - - - - -1.1.5 -The platform has the right to refuse to release games that contain security risks, such as information that convenes, promotes, instigates crimes, or is suspected of violating laws and regulations. - - - -### 1.2 In-app Purchases - - - -1.2.1 -The price of all in-app purchases in your game must be clearly marked, and the services or content to be provided must also be clearly described. If the actual service or content does not match the description, your game may be removed, and your account may be suspended. - - - -1.2.2 -The game must not induce unprotected purchases in any form, including but not limited to direct payments outside the site, offline trades, and crowdfunding by QR code, etc. - - - -1.2.3 Your game shall not make automatic deductions of fees. - - - -### 1.3 In-game Advertising and Bundleware - - - -1.3.1 -Advertisements in your game cannot contain any illegal content or other sensitive information, including but not limited to information about politics, gambling, violence, gore, animal or child abuse, sexual suggestive content, and any expressions prohibited the advertising law. - - - -1.3.2 Advertisements in your game must not mislead users by imitating system notifications or prompts. - - - -1.3.3 Advertisements in your game shall not continue to exist when your game is closed or is running the back end. - - - -1.3.4 There shall not be any hover or pop-up advertisements that cannot be closed in your game. - - - -1.3.5 -The advertisements in your game cannot include any bundled downloads. For example, to proceed to the game's login and registration page, users should not have to download any other applications . - - - -1.3.6 Your game shall not force users to download other applications before they can play. - - - -### 1.4 Bundlewares - - - -1.4.1 Your game’s start screen should not have an option that is checked by default to allow downloading other applications without the user's permission. If there are other options checked by default, users must be clearly notified. - - - -1.4.2 Your game cannot automatically download other applications without the user's permission. - - - -### 1.5 Repetitions - - - -1.5.1 Do not upload multiple games of the same or similar content. Repetitive games may be removed. - - - -1.5.2 -Key features of the update of your game must not differ too much from the last version. For example, if the previous version is a text-based game, the new version cannot be updated to an action game. - - - -1.5.3 -Your game shall not be simply ported from a webpage or applying templates. Games with significantly poor user experience will be removed. - - - -1.5.4 The key features of your game must not rely on third-party applications or require redirecting to the web page to access content and features. - - - -1.5.5 -The content of your game shall not be the same as any other games that are already accepted on TapTap. You can file a complaint if you find the copyright of your game has been infringed. Click to learn more about the infringement complaint. - - - -### 1.6 Frauds - - - -1.6.1 -Games that involve fraud or mislead users will be removed. The developer account may also be suspended. - - - -1.6.2 -You should keep monitoring the content of your game before and after it passes the review. Games that provide illegal services after the release will be removed. The developer account may also be suspended. - - - -### 1.7 Games that will not accepted - - - -1.7.1 -Games that are pirated, fail to obtain authorization from the copyright owner or infringes the copyright of third parties (including but not limited to images, music and text assets, etc.) - - - -1.7.2 -Games involving gambling, casinos or lottery (including but not limited to cards, fishing and crane machines, etc.), or games promoting gambling, such as games using poker, roulette, or blackjack; or games including props based on real chips as in-app purchases. - - - -1.7.3 Games involving digital currencies, bitcoin or blockchains. - - - -1.7.4 Games that contain sexual, violent, blood and gore content. - - - -1.7.5 Games that promote cults and feudal superstitions, incite hatred and disparage an individual or group. - - ---- - - - -## 2. Information Requirements - - - -### 2.1 Providers - - - -2.1.1 The title of the provider can only contain numbers, letters, and their combinations; it cannot contain any punctuations or symbols. - - - -2.1.2 The title of the provider shall not use placeholder text, spaces, garbled text and other meaningless characters. - - - -2.1.3 The provider shall not be named after the title of any game. - - - -2.1.4 The provider shall not be named after any individual. - - - -2.1.5 The title of the provider cannot be the combination or list of multiple providers' titles. - - - -### 2.2 Genre, Compatibility and Payment - - - -2.2.1 The information needs to be filled in according to the actual situation of your game. The genre must match the actual features of your product. - - - -2.2.2 Options need to be selected according to the actual situation and must not be filled in incorrectly to mislead players. - - - -2.2.3 The payment information of the game should be filled in according to the actual situation and should be properly updated. - - - ---- - - - -## 3. Review of Assets - - - -### 3.1 Specifications - - - -3.1.1 -All visual materials such as images and videos must be clear with no obvious blurring, stretching, compression, black edges, white edges, etc. Visual materials cannot be made up of overly simple patterns such as solid colors and gradients. - - - -3.1.2 The visual materials must not violate any laws and regulations. - - - -3.1.3 -The visual materials must not contain content that violates the platform's review rules, including but not limited to cash withdrawals, tangible rewards information and related information. - - - -3.1.4 -The visual materials must not contain information that is not related to the content of the game, including but not limited to advertisements of other games or applications, or contact of the provider. - - - -3.1.5 -All visual materials must not contain any infringing content. Licensed or purchased assets must be uploaded with corresponding supporting documents or authorization letters. - - - -3.1.6 All visual materials must not contain statistics such as game store ratings. - - - -3.1.7 All visual materials must not contain information that is not related to the game. - - - -3.1.8 All visual materials must not contain information that will redirect users to other websites. - - - - - -### 3.2 Icons - - - -3.2.1 Icons shall not contain information or corner labels that mislead users or violate relevant regulations. And they shall not contain popular search terms or information unrelated to the content of the game, such as earning cash by playing. - - - -3.2.2 Icons must be consistent with the content of the game and must not contain popular search terms or information that is not related to the game. - - - -### 3.3 Screenshots - - - -3.3.1 The screenshots must not include UI of the device or UI that does not belong to the version provided for download. - - - -3.3.2 The screenshots need to be of the same size. - - - -3.3.3 Do not use repeated screenshots as the promotion image for the game detail page on TapTap. - - - -3.3.4 Screenshots must not contain information that mislead users or violate relevant regulations. - - - -3.3.5 The screenshots must be consistent with the actual content of the game and must not use assets that are not related to the game for publicity and promotion. - - - -### 3.4 Cover Image on Game Page and Other Promotion Images - - - -The above are hereafter collectively referred to as ‘Promotion Image’ - - - -3.4.1 Promotion Images cannot contain any text that is unrelated to the game, and cannot contain any content such as individual contact or UI of the game; - - - - 3.4.2 Screenshots of the game cannot be used as the Promotion Image. Please do not use collages or tiled images, either. - - - -3.4.3 Promotion Image must contain game logos (except for the square Promotion Image). The majority of the background should not be left blank. - - - -3.4.4 The Promotion Image must not contain images of actual mobile phones. - - - -3.4.5 The Promotion Image must not contain icons of the game. - - - -3.4.6 Promotion Image cannot be model sheets, drafts or modeling drafts. - - - -### 3.5 Text Materials That You Provide - - - -3.5.1 All text materials must not violate any laws and regulations. - - - -3.5.2 The text must not contain any content that violates the platform's review rules. - - - -3.5.3 The text must not contain information that redirects users to other third-party websites - - - -3.5.4 The text must not contain information that promotes conflicts and hatred. - - - - -### 3.6 Title of the Game - - - -3.6.1 The title of the game must be consistent with its registered information, and it shall not include popular search keywords as the subtitle. - - - -3.6.2 The title of the game shall not contain information that violates the platform’s rules. - - - -3.6.3 The title of the game shall not contain information that misleads users or is unrelated to the content of the game. - - - -3.6.4 The title of the game shall not contain information about regions. - - - -3.6.5 The title of the game displayed after it is installed on the phone should be the same as the title displayed on the game page. - - - -### 3.7 About and Notes - - - -3.7.1 The About section on the game page should introduce the features or gameplay. Do not include irrelevant information such as advertisements. - - - -3.7.2 Content of About and Notes cannot be similar. - - - -### 3.8 Players’ Workshop - - - -3.8.1 Player’s workshop is a group for players to communicate, such as a Discord channel. The title should match the game or the official title given for such a group. - - - -3.8.2 The title of the players' workshop must not contain information that leads or redirects to other application distribution platforms. - - - ---- - - - -## 4. Store Settings - - - -### 4.1 Status - - - -4.1.1 The game state must be filled in according to the actual situation - - - -4.1.2 For games whose test server is not open, the status cannot be ‘Pre-registration’ or ‘Open for Download’. - - - -### 4.2 Language Settings - - - -4.2.1 The information in Languages Settings should be consistent with the regions where the game is to be released. - - - -4.2.2 For each language, the visual and text materials filled in the settings must match with the language. - - - - -### 4.3 Links to Official Websites - - - -The link must be the game's official website; it cannot be a link to any third-party website. - - - ---- - - - -## 5. Application Package for Installation - - - -### 5.1 Package Name - - - -5.1.1 The package name must not include any markings of platforms. - - - -5.1.2 The package name must be consistent with the official package name - - - -5.1.3 For any changes to the package name, you will need to publish a related announcement and upload the new package. - - - -5.1.4 Do not use the default package name set by the packaging tool. - - - -### 5.2 Version Code - - - -5.2.1 The version code cannot be 0. - - - -5.2.2 The version code of the newly uploaded installation package must be earlier than the current one. - - - -### 5.3 Version Name - - - -The version name must be consistent with the one on the game's official website. - - - -### 5.4 Update Notes - - - -The content of the update note must be related to the changes to the game. It shall not include advertisements or other irrelevant information. - - - -### 5.5 Signature - - - -APK must not be signed with a public certificate - - - ---- - - - -## 6. Qualification and License Documents - - - -### 6.1 Operation and IP Licensing - - - -6.1.1 A complete and valid proof of the license chain is required if the content of the game is operated by another agency. - - - -6.1.2 A complete and valid proof of the license chain is required if the assets, storyline, music and other content of the game involve another IP. - - - - 6.1.3 A true and valid proof is required if the IP-related rights are owned by the company that uploads the game. - - - -6.1.4 The applied scope of the IP license must match the content of the uploaded game and it must be true and valid. - - - -### 6.2 Assets Licensing - - - -6.2.1 Assets in the game shall not use copyright-sensitive content, including but not limited to fan-made materials, materials related to a real individual, or in-game assets of other games. - - - -6.2.2 The authorization letter for the use of portrait is required if a character of real individual appears in the game. - - - -6.2.3 A true and valid proof is required if assets in the game are original. - - - -6.2.4 The proof of order is required if the assets in the game are purchased. - - - ---- - - - -## 7. Others - - - -7.1 TapTap reserves the right to interpret the Specifications and Rules to the extent allowed by law. - - - -7.2 TapTap Game Review Specifications and Rules takes effect as soon as it is published. - -TapTap reserves the right to make changes to this document at any time and to post the updated document on the website of TapTap Developer Center. diff --git a/.ci/hk/en/store/store-auth.mdx b/.ci/hk/en/store/store-auth.mdx deleted file mode 100644 index d59ed8c5a..000000000 --- a/.ci/hk/en/store/store-auth.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Developer Verification -sidebar_position: 35 ---- - -import { Blue } from "/src/docComponents/doc"; - -## What is the developer verification? - -Verified developers in TapTap will have a "√" in the corner of their profile picture. The verified title will appear on the user homepage too. - -The title will be usually displayed as ‘Provider title/Game title + Official/a specific role in the team’. - -![](/img/DC-v2/developer-verification-V2.png) - ---- - -## How can I be verified as a developer? - -You can contact us via **Developer Center** >> **Support** . You can also send your request to our team at [international_operation@taptap.com](mailto:international_operation@taptap.com). - -**Template** - -> **Subject** -> -> TapTap Developer Verification - XXX (Provider/Game title) - ---- - -> TapTap ID: -> -> Title to be verified: -> -> You can apply for more than one account to be verified as a developer. Please provide the list in .xlsx, .txt or other formats. -> -> TapTap ID and the title to be verified: -> -> Contact of the applicant: - -Our staff will process your application in 2 business days. - ---- - -## How do I remove the developer verification? - -You can contact us via **Developer Center** >> **Support** . You can also send your request to [international_operation@taptap.com](mailto:international_operation@taptap.com). - -**Template** - -> **Subject** -> Remove TapTap Developer Verification - XXX (Provider/Game title) - ---- - -> TapTap ID: -> -> The title to be removed: -> -> You can apply to remove the developer verification of more than one user. Please provide the list of TapTap ID and titles to be removed in .xlsx, .txt or other formats. -> -> Contact of the applicant: - -Our staff will process your application in 2 business days. diff --git a/.ci/hk/en/store/store-complaint.mdx b/.ci/hk/en/store/store-complaint.mdx deleted file mode 100644 index 75f0de548..000000000 --- a/.ci/hk/en/store/store-complaint.mdx +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Report Copyright Infringement -sidebar_label: Report Infringement -sidebar_position: 65 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - - -TapTap is a platform for downloads and purchases of games, and it is also a high-quality community for game enthusiasts. TapTap has formulated these guidelines according to the relevant laws and regulations. Please make the complaint referring to the following guidelines for the protection of the legitimate rights and interests of the right holder. - -## **1. Procedures** -**1.1 Inform TapTap** -If the right holder believes that the game provided by the third party on TapTap infringes his/her legitimate rights and interests, the right holder shall submit a written complaint to TapTap, the content should include but not be limited to: - -**(1) Information and related materials about the right holder** -Rights holder’s name (title), contact information, address, business license (for organizations) or ID card (for individuals), relevant authorization certificate and other materials to prove the legitimacy of the right holder. - -**(2) The request of the right holder** -The exact and accurate name and link of the game that the right holder requests to remove or disconnect. - -**(3) Preliminary materials that prove the infringement** -These materials should include: -a. Proof of right holder's ownership to the rights, which includes but is not limited to the copyright certificate, trademark certificate, or patent certificate issued by the relevant authority, the proof of the date of first public publication or release of the work, the manuscript or drafts during the creation, the time stamp of the creation of the work issued by the authority, the certificate of filing the work and other valid proof of ownership that proves that the right holder indeed owns the relevant rights. - -b. Proof that the game provided by the respondent constitutes infringement, which includes but is not limited to the valid proof that the game provided by the respondent constitutes infringement of the copyright, trademark right, patent right, or other rights of the right holder. - -**(2) Statement by the right holder** -The notice letter from the right holder must include the following statement. -The statements and materials provided by the right holder in the complaint are true, valid and legal; and the right holder guarantees to bear and compensate for any loss caused to TapTap as a result of the removal or modification of the infringing information or related content by TapTap based on the right holder's complaint, including but not limited to the damages incurred by TapTap as a result of compensation to the respondent or the users and the damage to TapTap's reputation, etc. - -Please refer to the instructions below for the matters needing attention when preparing the notice, the related certification materials and the mailing address. - -**1.2 Feedback from TapTap** -As a neutral platform service provider, TapTap will forward it to the respondent after receiving the notice from the right holder that complies with the requirements of these guidelines. - -**(1) The respondent accepts the complaint from the right holder.** -TapTap will process this as soon as possible in accordance with relevant laws and regulations. - -**(2) The respondent does not accept the complaint from the right holder.** -TapTap will forward the relevant materials provided by the respondent to the right holder. If the right holder disagrees with the respondent's opinion or the provided materials, TapTap suggests that the right holder resolve the relevant issues directly with the respondent through administrative complaints, litigation or other means. The right holder may also provide them to TapTap if the right holder has new supporting materials that can disprove the respondent's opinion - -## **2. Attention** -(1) The right holder in this document refers to the original owner or the agent that is legally authorized by the original owner of the legal rights of the protected copyright, trademark, patent, etc. This includes legal entities, individuals or other organizations. - -(2) In order to ensure the authenticity and validity of the complaint, the written notice of the right holder and other relevant supporting materials should, in principle, be provided in the original. If the original is not available, a copy should be provided (with the signature of the right holder on the copy). If the materials are foreign, they should be notarized and transferred in accordance with the relevant laws, and the corresponding documents about the notarization and transfer should be provided at the same time. - -(3) Contact TapTap: Send an email to [dmca@taptap.com](mailto:dmca@taptap.com) with the scanned copies of all hard copies of the aforementioned materials as attachments. - -(4) The written notice by the right holder in this document should include the notice itself and relevant entity qualification materials, proof of ownership, proof of infringement and other materials. - -(5) If the right holder has filed an administrative complaint or lawsuit with the relevant government department or court concerning the game provided by the respondent, he/she should submit the relevant certificate of acceptance and the evidential materials submitted to the government department or court to TapTap when submitting the notice. This will facilitate the processing of the right holder's complaint. - -(The End) - -## **Download [Infringement Complaint Template](https://lc-gluttony.s3.amazonaws.com/vmzp7NxP3swl/Vm0BDugjlYJQQ5veMyKm0YdzEcm6RdAv/TapTap%E5%B9%B3%E5%8F%B0%E4%BE%B5%E6%9D%83%E6%8A%95%E8%AF%89%E9%80%9A%E7%9F%A5%E4%B9%A6.doc.zip)** - ---- - -# **Response Letter to Infringement Complaint** - -## **1. Response Letter to TapTap by Respondent** - -The Respondent shall submit a written response letter to TapTap within five business days after receiving the relevant complaint submitted by the right holder and forwarded by TapTap. The response letter shall be in accordance with the following guidelines, which shall include, but not be limited to, the following content. - -**1.1 Information and related materials about the respondent** -The respondent’s name (title), contact information, address, business license (for organizations) or ID card (for individuals), relevant authorization certificate and other materials to prove the entity qualification of the respondent. - -**1.2 Respondent’s response to the complaint** -a. Admitting to the infringement. -b. Not admitting to the infringement. - -**1.3 Preliminary materials that disprove the infringement** -When not admitting to the infringement, the respondent shall provide materials that disprove the infringement, which should include the following. -(1) Proof of respondent’s ownership to the rights, which includes but is not limited to the copyright certificate, trademark certificate, or patent certificate issued by the relevant authority, the proof of the date of first public publication or release of the work, the manuscript or drafts during the creation, the time stamp of the creation of the work issued by the authority, the certificate of filing the work and other valid proof of ownership that proves that the respondent indeed owns the relevant rights. - -(2) Proof that the game provided by the respondent does not constitute infringement, which includes but is not limited to the valid proof that the game provided by the respondent does not constitute infringement of rights, such as the copyright, trademark right, or patent right. - -**1.4 Statement by the respondent** -The respondent shall make the following statement in the response letter. -The statements and materials provided by the respondent in the response letter are true, valid and legal; and the respondent guarantees to bear and compensate for any loss caused to TapTap as a result of the removal or modification of the infringing information or related content by TapTap based on the respondent’s response letter, including but not limited to the damages incurred by TapTap as a result of compensation to the right holder or the users and the damage to TapTap's reputation, etc. -Please refer to the instructions below for the matters needing attention when preparing the response letter, the related certification materials and the mailing address. - -## **2. Feedback from TapTap** -As a neutral platform service provider, TapTap will process the matter according to the relevant laws and regulations. - -**2.1 The respondent accepts the complaint from the right holder.** -TapTap will resolve the matter as soon as possible in accordance with relevant laws and regulations. - -**2.2 The respondent does not accept the complaint from the right holder.** -TapTap will forward the materials provided by the respondent to the right holder. TapTap will resolve the matter in accordance with relevant laws and regulations. - -## **3. Attention** -3.1 The right holder in this document refers to the original owner or the party that is legally authorized by the original owner of the legal rights of the protected copyright, trademark, patent, etc. This includes legal entities, individuals or other organizations. - -3.2 In order to ensure the authenticity and validity of the materials provided by the respondent, the written response letter of the respondent and other relevant supporting materials should, in principle, be provided in the original. If the original is not available, a copy should be provided (with the signature of the respondent on the copy). If the materials are foreign, they should be notarized and transferred in accordance with the relevant laws, and the corresponding documents about the notarization and transfer should be provided at the same time. - -3.3 Contact TapTap: Send an email to [dmca@taptap.com](mailto:dmca@taptap.com) with the scanned copies of all hard copies of the aforementioned materials as attachments. - -3.4 The written response letter by the respondent in this document should include the response letter itself and entity qualification materials, proof of ownership, proof that disproves infringement and other materials. - -(The End) - -## **Download [Response Letter to Complaint Template](https://lc-gluttony.s3.amazonaws.com/vmzp7NxP3swl/3hyv6Bn92qHDTsnetWTqhg8sHx2zcJCb/TapTap%E5%B9%B3%E5%8F%B0%E4%BE%B5%E6%9D%83%E6%8A%95%E8%AF%89%E5%8F%8D%E9%80%9A%E7%9F%A5%E4%B9%A6.doc.zip)** - ---- - -# Complaints to Reviews and Community Content - -TapTap is a platform for downloads and purchases of games, and it is also a high-quality community for game enthusiasts. TapTap has formulated these guidelines according to the relevant laws and regulations. Please make the complaint referring to the following guidelines for the protection of the legitimate rights and interests of the right holder. - -## **1. Procedures** -**1.1 Inform TapTap** -If the right holder believes that the information published by the third party on TapTap infringes his/her legitimate rights and interests, the right holder shall submit a written complaint to TapTap, the content should include but not be limited to: - -**(1) Information and related materials about the right holder** -Rights holder’s name (title), contact information, address, business license (for organizations) or ID card (for individuals), relevant authorization certificate and other materials to prove the legitimacy of the right holder. - -**(2) The request of the right holder** -Link to the information that the right holder requests to delete or modify. - -**(3) Preliminary materials that prove the infringement** -These materials should include: -a. Proof of right holder's ownership to the rights, which includes but is not limited to the copyright certificate, trademark certificate, or patent certificate issued by the relevant authority, the proof of the date of first public publication or release of the work, the manuscript or drafts during the creation, the time stamp of the creation of the work issued by the authority, the certificate of filing the work and other valid proof of ownership that proves that the right holder indeed owns the relevant rights. - -b. Proof that the game provided by the respondent constitutes infringement which includes but is not limited to the valid materials which can prove that the information published by the respondent constitutes infringement of the legitimate rights and interests of the right holder, etc. - -**(2) Statement by the right holder** -The notice letter from the right holder must include the following statement. -The statements and materials provided by the right holder in the complaint are true, valid and legal; and the right holder guarantees to bear and compensate for any loss caused to TapTap as a result of the removal or modification of the infringing information or related content by TapTap based on the right holder's complaint, including but not limited to the damages incurred by TapTap as a result of compensation to the respondent or the users and the damage to TapTap's reputation, etc. - -Please refer to the instructions below for the matters needing attention when filing the complaint, the related certification materials and the mailing address. - -**1.2 Feedback from TapTap** -After receiving the complaint from a rights holder that meets the requirements of the guidelines, TapTap will determine the type of complaint. - -**(1) If the complaint by the right hold can be decided inside TapTap** -TapTap will resolve the matter as soon as possible in accordance with relevant laws and regulations. - -**(2) If the complaint by the right hold cannot be decided inside TapTap** -TapTap suggests that the right holder resolve the issue directly with the respondent by administrative complaints or lawsuits, etc. - -TapTap will then process the rights holder's complaint as soon as possible and in accordance with the TapTap Community Management Rules and other relevant regulations. - -## **2. Attention** -(1) The right holder in this document refers to the original owner or the agent that is legally authorized by the original owner of the legal rights of the protected copyright, trademark, patent, etc. This includes legal entities, individuals or other organizations. - -(2) In order to ensure the authenticity and validity of the complaint, the written notice of the right holder and other relevant supporting materials should, in principle, be provided in the original. If the original is not available, a copy should be provided (with the signature of the right holder on the copy). If the materials are foreign, they should be notarized and transferred in accordance with the relevant laws, and the corresponding documents about the notarization and transfer should be provided at the same time. - -(3) Contact TapTap: Send an email to [dmca@taptap.com](mailto:dmca@taptap.com) with the scanned copies of all hard copies of the aforementioned materials as attachments. - -(4) The written complaint by the right holder in this document should include the complaint itself and relevant entity qualification materials, proof of ownership, proof of infringement and other materials. - -(5) If the right holder has filed an administrative complaint or lawsuit with the relevant government department or court concerning the information published by the respondent, he/she should submit the relevant certificate of acceptance and the evidential materials submitted to the government department or court to TapTap when submitting the complaint. This will facilitate the processing of the right holder's complaint. - -(The End) - -## **Download [Community Complaint Template](https://lc-gluttony.s3.amazonaws.com/vmzp7NxP3swl/gBKS91vj66v2crFLJ7w7xutBtF6j7zAF/TapTap%E5%B9%B3%E5%8F%B0%E8%AF%84%E4%BB%B7%E5%8F%8A%E7%A4%BE%E5%8C%BA%E6%8A%95%E8%AF%89%E4%B9%A6.doc.zip)** - diff --git a/.ci/hk/en/store/store-contact.mdx b/.ci/hk/en/store/store-contact.mdx deleted file mode 100644 index e4d9a1998..000000000 --- a/.ci/hk/en/store/store-contact.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Contact Us -sidebar_position: 70 ---- - -Feel free to contact us at the following email addresses if you have any questions. - ->Questions about Developer Center: - ->Business Opportunities: - diff --git a/.ci/hk/en/store/store-creategame.mdx b/.ci/hk/en/store/store-creategame.mdx deleted file mode 100644 index ccf55faeb..000000000 --- a/.ci/hk/en/store/store-creategame.mdx +++ /dev/null @@ -1,162 +0,0 @@ ---- -title: Create And Release Games -sidebar_position: 45 ---- - -import { Blue } from "/src/docComponents/doc"; - -Once your created TapTap developer account has been approved, you can create and publish your games in the Developer Center. - -## Create Game - -Go to **[Developer Center](https://developer.taptap.io/)** >> Click on **Create Game** on the left >> Fill in the required information of the game to create the game. - -- When creating a game page, the system will automatically generate a version name (for example: V-20220818) based ont the date created. The version name will be used to track the review status of this version. -- After the game is created for the first time, the status of the game will be 'Not Released'. To **release the game**, you will need to submit it for review. -- You can only create up to 10 games that are to be released. -- To delete a game that you have created, you may go to All Games and find ![‘deleteadmin’](https://img.tapimg.com/market/images/2e5c836549d866d6d44036d158095cbb.png) in the right - -![](/img/DC-v2/create-V2en-1.png) - -Example - -![](/img/DC-v2/create-V2en-2.png) - -After the game is created, you can view all created games in **All Games** page, and click ont the game title to view details. - -![](/img/DC-v2/create-V2en-3.png) - -## Release Game - -Go to game >> Fill in ** Store Information** >> Submit it for review. After approved, the game page will be av ailable to players on TapTap. - -### Select Region And Status - -If you need to specify the regions for the release, you can add the region in the game's **Region and Status** section, and your game will only be available for download or pre-registration in the selected regions. - -For unlisted regions, the page will show the basic information of the game such as the title, the provider and the banner on top. Playes will only be able to follow the game. - -![](/img/DC-v2/create-V2en-4.png) - -TapTap Interntional does not have a strict concept of regions. We want to create an app for users from all over the world to come and enjoy the games. When you are promoting your game, we recommend you to make your game available for pre-registration globally so it has more exposure and popularity. - -### Set Up Language Options - -You can add language options in **Store Info** >> **Game Assets**, so your game can be enjoyed by a wider audience. - -- English is required if you are going to release your game globally. -- If your game is going to be released to a specific country、region, it is recommeded that you add English and the official language of the region. - -For example, if the game is released to Korea, you can add 'English' and 'Korean' assets. - -Languages currently supported are: Traditional Chinese, English, Japanese, Korean, Indonesian, Thai, Portuguese, Vietnamese, Hindi, Malay, Spanish, French, German, Russian. - -You can add multilingual assets according to your needs. - -![](/img/DC-v2/create-V2en-5.png) - -Add/Remove Languages - -![](/img/DC-v2/create-V2en-6.png) - -### Set the APK - -The APK is required when the game is under test and open for download. You can go to **Developer Center** >> **Store Info** >> **APK** to upload. - -In order for players from all over the world to have a better experience after downloading the game, please upload the APK and fill in the **languages supported** according to the actual stuation. This will be displayed on the game page. - -![](/img/DC-v2/create-V2en-7.png) - -Package Formats - -- Developers can only upload packages in APK. -- If you need to upload APK+OBB, you can contact us from **Developer Center** >> **Support**, or send your request to [international_operation@taptap.com](mailto:international_operation@taptap.com). Our team will assist you to upload the packages. - -### Schedule Release Time - -When the game page information is complete, you can select **release options** in the **Release Settings**. - -![](/img/DC-v2/create-V2en-8.png) - -- Select **Release Immediately** if you want the game page to be displayed on TapTap as soon as the game has been approved. -- If you want to release the game page in TapTap at a fixed time, you can select **Scheduled** and set up the specific date and time for release in schedule. - -**Please note that you will need to set the time to at least 24 hours after the current time.** - -### Edit Schedule - -Refer to the following method if you need to change the schedule for release. - -- The Version has been approved - - Click on **Edit Schedule** to re-select the release time of the game page. You will only be able to select after the current time. - - You can also **Cancel Schedule**, and the status of this version will change to **Rejected**. You will need to set the new schedule and submit again for review. -- The version is under review - - Select **Cancel Review** and you will need to submit the version for review again after creating a schedule. - -## Submit For Review - -Once the game page information is complete, you can proceed to **Submit For Review**. The result is expected to be available within 2 business days. - -## Q&A - -### My game is already on TapTap. Can I claim it? - -Yes, of course. - -You can contact us via **Developer Center** >> **Support**. You can also send your request to our team at [international_operation@taptap.com](mailto:international_operation@taptap.com) and provide a scanned copy of the business license or other materials that can prove that the game is owned by you or your organization. - -**Template** - -> **Email subject:** Claim Game On TapTap - Game Title -> -> Title and link of the game to be claimed: -> -> Title of the provider making the request: -> -> Scanned copy of business license or other proof materials (you can also upload as attachments): -> -> Contact of the applicant: - -our operation team will process your request in 2 business days. - -### Can the provider of the game be changed? - -Yes. - -You can contact us via **Developer Center** >> **Support**. You can also send your request to our team at [international_operation@taptap.com](mailto:international_operation@taptap.com) and cc this to the other provider's email address as well. - -**Template** - -> **Email subject:** Change Provider on TapTap - Game Title -> -> Link to the game page on TapTap: -> -> Title of the original provider of the game: -> -> Title of the new provider of the game: -> -> Contact of the applicant: - -Please note that this email needs to be sent with the provider's company email and cc to the other provider's company email as well. If you are an individual developer, the email addresses of the original provider and the new provider must be the email addresses filled in the TapTap provider profile. The vendor who received the copied email needs to reply for confirmation. - -After receiving the confirmation email, our operation team will verify and process your request in 2 business days. - -### How do I take down the game from TapTap? - -You can contact us via **Developer Center** >> **Support**. You can also send your request to our team at [international_operation@taptap.com](mailto:international_operation@taptap.com) and provide a scanned copy of the business license or other materials that can prove that you are an employedd of the provider. - -**Template** - -> **Email subject:** Remove Game from TapTap - Game Title -> -> Link of the game to be removed: -> -> Reason for removal: -> -> Scanned copy of business license or other proof materials (you can also upload as attachments): -> -> Applicant Contact: - -Our operation team will process your request in 2 business days. - -**We greatly value the content created by our users. When your game is removed from TapTap, we will still keep your game's page to show users' reviews and posts but it will not be available for download anymore. If you need to remove the game page as well, please specify in your email.** diff --git a/.ci/hk/en/store/store-devagreement.mdx b/.ci/hk/en/store/store-devagreement.mdx deleted file mode 100644 index ca6bdb51b..000000000 --- a/.ci/hk/en/store/store-devagreement.mdx +++ /dev/null @@ -1,266 +0,0 @@ ---- -title: TapTap Developer Agreement -sidebar_position: 15 ---- - -# TapTap Developer Agreement - -**Release date: December 29, 2021** - -**Effective date: December 29, 2021** - -Welcome to TapTap Developer Center! - -To use the service provided by TapTap Platform to Developers (“Service”), you shall read and comply with this TapTap Developer Agreement (“Agreement”). - -**Please carefully read and fully understand the terms and conditions of this Agreement, especially those on waiver or restriction of liabilities, and separate agreements and rules on provision or use of a certain service, which may be written in bold.** - -**Unless you have read and accepted all the terms and conditions of this Agreement, relevant agreements and rules, etc., you have no right to use the Service. If you use the Service, it will be deemed that you have read and agreed to comply with such agreements and rules.** - -**If you have breached this Agreement, TapTap has the right to restrict, suspend or terminate the Service provided to you at any time, and hold you accountable for relevant liabilities.** - -**TapTap may update or amend the content, including but not limited to the terms and conditions, of this Agreement, the agreements and rules related to the Service from time to time. Any such update and/or amendment, once published, shall be integral to this Agreement, and you are expected to understand and comply with the same.** - -## 1. Accepting this Agreement - -1.1 This Agreement is a legal instrument between you and TapTap on Game release, publicity and promotion. "TapTap" refers to TapTap Pte. Ltd. and its affiliates, which have the right to operate the TapTap Platform in the relevant area. You agree that once you have accepted this Agreement and registered a Developer Account, Games will be displayed on the TapTap Platform only in your name (not in the name of TapTap) for Users to download (on a paid or free-of-charge basis). **You acknowledge and warrant that you have full civil capacity in your jurisdiction and accept this Agreement; you have the legal qualifications or have been approved by relevant authorities for using the Service, accessing and providing Games or related services; the qualifications or certificates and any other documents provided by you are true, correct and complete, and will be updated promptly if there is any change to such information; you have the capacity to perform the obligations and act under this Agreement; such performance and acts will not be in violation of any legal document that is binding upon you.** **Otherwise, you shall not use the Service provided by the TapTap Platform, and you shall bear all responsibilities and losses caused thereby to Users and TapTap.** If you refuse to accept this Agreement, you shall stop using the Service. - -1.2 You shall use the Service provided by the TapTap Platform with a Developer Account which cannot be changed, transferred, gifted or inherited. You shall keep in good custody and make proper use of the account, and be fully liable for the actions under the account. - -1.3 If you agree to comply with this Agreement on behalf of your employer or other entity, it shall be deemed that you declare and warrant that you have obtained all legal authorization to subject your employer or such entity to this Agreement. If you have not obtained such authorization, you shall not accept this Agreement or use the TapTap Platform on behalf of your employer or such entity. - -## 2. Definitions - -2.1 **Brand Marking**: a Game name, trademark, logo, domain name or other distinctive brand feature independently owned (or whose legal authorization is obtained) by either party hereto. - -2.2 **Developer or you**: any individual, legal person or organization that has successfully registered with the TapTap Platform and has obtained a Developer Account according to this Agreement. - -2.3 **Developer Account**: an account obtained by the Developer through registration and assigned to the Developer by the TapTap Platform, which allows the Developer to release a Game on and use other Services provided by the TapTap Platform. - -2.4 **Dashboard**: a backend tool used by the Developer with a Developer Account to manage Games and view data by logging in to “Developer Center” on the TapTap Platform via a console or/and other online tool or Service provided by the TapTap Platform. - -2.5 **Games or Products**: products developed by the Developer or for which the Developer has been licensed by the owner, and provided through the TapTap Platform to Users for downloading, pre-registering or testing, etc., including without limitation certain mobile Games and Game-related texts, music, pictures, videos, and entertainment applications. - -2.6 **Localized Versions:** Any versions of the Games created for specific languages or jurisdictions. - -2.7 **Platform Operation Data:** data generated during use of the Service or on the TapTap Platform, including but not limited to the information provided by Users and/or Developers, the data arising from their operation, and all types of transaction data. The ownership of and other rights in or to the platform operation data shall belong to the TapTap Platform and are the trade secrets of the TapTap Platform, except the rights owned by Users or enjoyed by Developers in accordance with the law. - -2.8 **TapTap Platform**: TapTap (with domain name including but not limited to [taptap.io](https://www.taptap.io/)) website and client in various forms (including new service forms developed with new technology in the future) used to serve you. - -2.9 **Users:** all players who directly or indirectly use the Games released or updated by you on the TapTap Platform. - -2.10 **User Data**: user-related data generated by a user on the TapTap Platform, including but not limited to the information provided by the user when using the TapTap Platform, the information obtained by the TapTap Platform with the user's consent, the data arising from the user's operation, and all types of transaction data. Except otherwise set forth in the TapTap [Privacy Policy](https://www.taptap.io/privacy-policy), the ownership of and other rights in or to user data shall belong to the TapTap Platform and are the trade secrets of the TapTap Platform. - -## 3. Profile - -3.1 TapTap is a platform open to the public on which Developers can publish their Games. To publish a Game on the TapTap Platform, you must obtain and maintain a valid Developer Account. - -3.2 If your Games are provided for Users to download on a paid basis, or as required by the TapTap Platform, you shall execute a separate agreement with TapTap to cover Game release. - -## 4. Both You and TapTap Agree and Understand: - -4.1 **The TapTap Platform is a neutral service provider which only provides Game-related services to Developers, including neutral network service or technical support, e.g. uploading and storage, Game release, pre-registering, testing, and transfer to links, so that Developers can release and promote their Games independently on the platform.** - -4.2 **With regard to the Developer’s Games, the Developer shall develop and/or operate them independently, have a legal license and assume all responsibilities for such Games. TapTap will not participate in development and/or operation of any Developer's Games or other activities, nor modify or edit such Games.** - -4.3 **Any dispute or liability arising from your Games and services and any consequence caused by your violation of laws, rules or regulations or breach of this Agreement shall be settled, handled or borne by you only. In case of infringement upon TapTap’s or any third party's rights and interests, the Developer shall assume full liability and compensate TapTap for all losses arising therefrom.** - -## 5. Pricing and Maintenance - -5.1 You have the right to decide the price of your Games on the TapTap Platform by yourself or through negotiation with TapTap, or allow Users to download Games for free; the TapTap Platform will display the Games in your name at the price set by you or as agreed upon. - -5.2 You shall provide support for your Games. If there are any defects or performance problems in the Games downloaded and installed from the TapTap Platform, Users will contact you accordingly. **You must be solely responsible for your Games. TapTap will not provide any support or maintenance service for your Games, nor settle all complaints related to your Games. A lack of appropriate information or support provided for your Games may result in low ratings, poor exposure, decreased sales, settlement disputes, etc., and TapTap has the right to remove such Games from the platform.** - -5.3 You may authorize Users to reinstall each Game downloaded via the TapTap Platform multiple times, unless the Game is removed by you or TapTap. - -## 6. Your Use of the TapTap Platform - -6.1 You agree to submit the Games to TapTap for release no later than the first commercial release of each Game or Localized Version, or, if already commercially released before the Effective Date, within thirty (30) days of the Effective Date. Thereafter, you shall submit to TapTap any Localized Versions and updated versions (in beta and final form) when available, but in no event later than they are provided to any other third party for commercial release. - -6.2 Except for the licensing rights granted under this Agreement or a separate agreement entered into between you and TapTap, TapTap will not obtain the rights, ownership or interests of Games from you (or your licensor), including the current intellectual property rights in or to such Games. - -6.3 You agree to use the TapTap Platform only when permitted by this Agreement and any applicable laws or regulations or generally accepted practices or guidelines in the relevant jurisdiction. - -6.4 You agree to protect the privacy and legal rights of Users when using the Service, at least to the degree, in principle, as set forth in the TapTap [Privacy Policy](https://www.taptap.io/privacy-policy). If your protection is obviously inferior to that degree, you shall inform the User and obtain his/her consent. For example, if Users provide their username, password, or other login information or personal information in order to access or use your Games, then you must not only let the Users know that such information will only be used for your Games, but also provide them with a privacy statement and sufficient protections. In addition, your Games can only use such information for the limited purposes permitted by Users. If your Games contain personal or sensitive information provided by Users, it must be stored securely and used or disclosed according to the local laws and regulations and the relevant national or regional standards. However, if Users choose to sign a separate agreement with you, which allows you or your Games to store or use their personal or sensitive information directly related to your Games (excluding other Games), then your use of such information will be subject to the said agreement. If Users provide the information of TapTap accounts or other accounts for your Games, then your Games can only use such information to access such accounts, and only for the limited purposes, as permitted by them. You shall inform Users of the manner to modify or delete User Data, ensure that Users can act in that manner by themselves when they request to delete their User Data, and that such data is completely deleted. - -6.5 You agree that you will not engage in or participate in any interference, interruption, destruction or unauthorized access to any third-party equipment, server, network or other property or service when using the TapTap Platform or the Service. This provision also applies to Game development or release. You shall not use any User information obtained from the TapTap Platform to sell or release Games outside of the TapTap Platform, nor provide data obtained from the TapTap Platform in any form to another party, nor sell or transfer customer information. Without the prior written consent of TapTap, you shall not use the relevant data for purposes other than stipulated in this Agreement. - -6.6 **You agree that you are solely responsible for the consequences of any Games you release on the TapTap Platform and that use any API or TapSDK; TapTap will not bear any responsibility to you or any third party. These consequences include, but are not limited to, responsibilities, consumer protection and/or intellectual property responsibilities related to your Games.** - -6.7 **You agree to take full responsibility for all violations of the obligations as set forth in this Agreement, any applicable third-party contract or service provisions, or any applicable laws or regulations, and the consequences thereof (TapTap shall be held irresponsible to you or any third party). If the Games you release on the TapTap Platform have defects in rights or infringes upon the legal rights of a third party (including without limitation patent rights, trademark rights, copyrights and adjacent copyrights, portrait rights, personal information rights, privacy rights, and reputation rights), which subject the TapTap Platform and its affiliates or the third parties that cooperate with the TapTap Platform (“indemnified parties”) to any claim or suit, or cause the indemnified parties to suffer any loss of reputation or property, you shall take all possible measures to indemnify and hold harmless the indemnified parties against such claims and suits. At the same time, you shall be fully liable for the direct and indirect financial losses borne by the indemnified parties.** - -6.8 You agree to classify your Games according to the local laws and regulations, or relevant policies and industry standards. **All legal consequences and losses caused by the improper classification you declare shall be borne by you, and you shall compensate TapTap for all resulting losses.** - -6.9 Users may rate and review Games on the TapTap Platform. You agree that you shall not artificially manipulate the user evaluation system for Games. TapTap has the right to determine or change the position of Games displayed on the TapTap Platform at its own discretion, and to display Games and their ratings and reviews to Users in a manner determined by TapTap. - -6.10 You are responsible for uploading your Games to the TapTap Platform, providing Users with the necessary information and support, and accurately disclosing the necessary security permissions to ensure the normal operation of your Games on Users’ devices. If your Games are not uploaded normally, they will not be released on the TapTap Platform. Your Games shall meet TapTap's requirements in technology and security to ensure their safety and stable operation on the TapTap Platform. At the same time, in order to provide Users with high-quality Products and services, **you shall continuously update your Games after they are launched and in operation (including without limitation downloading, test, and trial), and ensure that the Game version on the TapTap Platform is the latest version available via various public channels. That means the Game version you provide to the TapTap Platform shall not be lower than that on any other Android Game platform or channel regardless of whether the Game version on other platforms or channels is provided by you or a third party with your authorization**. - -6.11 You shall provide relevant holders with an effective complaint channel to ensure that they can claim rights against you when they believe that you have infringed upon their legal rights and interests. - -6.12 You shall not bypass, attempt to bypass, or claim to be able to bypass any content protection systems or data analysis tools provided by TapTap, or intentionally mislead Users into making them believe that they are directly interacting with TapTap. - -6.13 In order to provide you with more comprehensive and high-quality services, TapTap or its partners ("providers of individual services") may also provide you with other individual services, including without limitation advertising and TapSDK (or API) service. You have the right to decide by yourself whether or not to use such individual services as appropriate. - -6.14 For the purpose of this article, individual services may be subject to a separate agreement and/or rules, etc., or their functions, rules and requirements may be provided to you in the form of announcement or reminder, etc. If you use an individual service, you shall follow the requirements to open the service, and observe the said agreement, rules, announcement and/or reminder, etc. - -6.15 By accessing or using an individual service in any form, you have understood and agreed to comply with the agreement, rules, announcement and/or reminder, etc. - -6.16 You understand and agree that the provider of individual services has the right to operate independently and take the following measures without your consent or giving prior notice: - -- Amend the relevant agreement and rules of individual services, including without limitation using conditions, methods, and charges; -- Change the specific content of an individual service, suspend or terminate an individual service. - -If you do not accept the said change or amendment, you shall stop using the relevant service. Otherwise, if you continue to use such service, it shall be deemed that you have agreed and accepted such change or amendment. - -6.17 The Games that you publish on the TapTap Platform or for which you use the Services shall be in compliance with the TapTap policies, all applicable laws and other obligations worldwide. You shall declare and guarantee that the Games you release shall not contain the following content or behavior: - -- Content in violation of local laws and regulations; -- Content that is harmful to children, such as pornographic content related to minors, excessive violence and blood, and content that depicts or encourages harmful and dangerous activities; -- Spreading obscenity, pornography, gambling, violence, murder, terror or abetment; -- Content that violates public order and good customs and impairs public interest; -- Content that insults or slanders others or infringes upon the lawful rights and interests of others; -- Violent intimidation, threat or fraud against others, or conducting human flesh searches; -- Spreading commercial advertisements, or similar commercial solicitation information, excessive marketing information, and spam; -- Infringing upon the legal rights of a third party’s privacy, reputation, portrait, intellectual property, etc.; -- Other content or behaviors that TapTap rejects or deems inappropriate, or are prohibited by applicable laws worldwide. - -If you have violated this article, TapTap has the right to issue a warning, impose penalties (fines), restrict/suspend Service, remove Games, close the application portal, or terminate Service, etc. You shall compensate for all losses caused to Users and/or TapTap or any partners and/or affiliates of TapTap due to any violation of any of the above provisions. - -## 7. Grant of License - -7.1 You grant TapTap a worldwide, non-exclusive license to release Games (official or beta versions) you have uploaded to this platform, provide Users with Game downloading, pre-registering, test services, and/or release Games in the manner as set forth in this Agreement or a separate agreement executed by both parties. - -7.2 **For purposes of clarity, unless otherwise agreed upon in writing by you and TapTap, by providing Users with Game pre-registering or testing on the TapTap Platform, you agree to grant TapTap a non-exclusive license, including without limitation allowing TapTap to release the said Game including any subsequent version, providing Users with pre-registering, testing and downloading services for the Game, as well as the non-exclusive license as set forth in Article 7 of this Agreement, and agree to comply with the provisions hereof on relevant rights and obligations during the licensing period, regardless of whether or not you have granted or will grant an exclusive license to a third party other than TapTap to release the Game worldwide or in the designated region during or after pre-registering or testing of the Game provided on the TapTap Platform.** - -7.3 You grant TapTap a worldwide, non-exclusive license to copy, operate, display, analyze and use Games in the events related to: (a) operation and marketing of the TapTap Platform; (b) public presentation to introduce, promote, and advertise Games; (c) data sharing within the TapTap Platform; (d) improvement of TapTap services, and (e) verifying compliance with this Agreement and other related platform policies. - -7.4 You shall declare and guarantee that you own all intellectual property rights contained in and related to the Games, including all necessary patents, trademarks, trade secrets, copyrights and other proprietary rights. If you use the content provided by a third party, you shall declare and guarantee that you have the right to release such content in the Games and have had authorization from the third party. You agree that you will not submit to the TapTap Platform any content subject to copyright, trade secret or third-party proprietary rights (including patents, privacy rights and publicity rights), unless you are the owner of such rights, or the legal owner of such rights allows you to submit the content. - -7.5 **You agree and warrant that after the TapTap Platform allows Game pre-registering, testing, trial play or downloading, you shall continue to update and provide downloading service in order to ensure the interests of Users and user experience. If you fail to provide continuous updating and downloads, it shall be deemed that you authorize TapTap to update on its own, including the right to automatically update the TapTap Platform based on the version you have released in other channels. This provision shall prevail over any agreement executed between you and other channels, and all losses arising therefrom shall be borne by you, and you shall compensate TapTap for all losses incurred.** - -## 8. Intellectual Property Rights - -8.1 Each party has all its rights, ownership and interests, including but not limited to all intellectual property rights in or to its Brand Marking. Except for the limited scope expressly set forth in this Agreement, neither party shall grant any rights, ownership or interests (including without limitation any implied license) contained in or related to any Brand Marking to the other party; nor shall the other party acquire such rights, ownership or interests. According to this Agreement, the Developer grants TapTap and its affiliates a non-exclusive, limited license during the term of this Agreement to release Games on the TapTap Platform or fulfill their obligations under this Agreement and display the Developer’s Brand Marking submitted by the Developer to the TapTap Platform online or on mobile devices. Nothing in this Agreement grants the Developer the right to use any trade name, trademark, service mark, logo, domain name or other unique Brand Marking of the TapTap Platform. - -8.2 The Developer agrees that in order to help TapTap fully display Games, TapTap and its affiliates or a third party in cooperation with the TapTap Platform may also use the Developer’s Brand Marking submitted by the Developer to the TapTap Platform for: (a) any online or mobile Game/service owned by the TapTap Platform; (b) network, mobile, TV, outdoor (e.g. billboards) and printed advertisements other than the TapTap Platform (when the Games are mentioned together with the TapTap Platform or other Games on the platform) ; (c) Game release announcement; (d) presentation; and (e) customer list displayed online or on mobile devices (including but not limited to the customer list published on the TapTap Platform). - -8.3 **In order to promote Games, you authorize Users to release pictures or videos containing your Game’s content on the TapTap Platform. The content authorized to be released by Users shall not be in violation of local laws and regulations and the specifications of the TapTap Platform. You have the right to handle Users’ violations by yourself or make a complaint by methods defined by the TapTap Platform.** - -## 9. Game Removal - -9.1 Game removal by the Developer. Unless otherwise agreed upon in writing between you and TapTap, you may apply to terminate release of Games to Users on the TapTap Platform (“removal”) if: (a) you decide to terminate operation of the Games; and (b) you have removed the Games from all other Android platforms or channels, or ensure that the Games are not removed from such other platforms or channels later than from the TapTap Platform. You shall notify the TapTap Platform of your decision in writing 90 days earlier, stating the reason for removal, the plan for or the status of such Games on other platforms or channels, and the date such Games are to be removed from the TapTap Platform. The Games can be removed after confirmation by TapTap and fulfillment of the relevant procedures. - -Regardless of the reason, if you need to terminate your Games, you shall post an announcement of termination to Users on the Game-related page at least 60 days in advance according to the relevant regulations, and close the payment portal. The announcement shall be posted until the day of such termination. - -The removal of the Game: (a) will not affect the license of Users who have purchased or downloaded the Games; (b) will not result in any removal of such Games from the devices of Users who have acquired the Games; and (c) will not change your obligations of distribution or support to Users who have purchased or downloaded Games or services. - -9.2 Game removal by TapTap Platform. Although TapTap is not obliged to monitor the content of Games, if via your notifications, third-party complaints, User complaints, institutional supervision or other means, TapTap learns that your Games, including any part of them, or your Brand Marking: (a) infringe upon the intellectual property rights or any other rights of a third party; (b) are in violation of any applicable law or prohibition order; (c) are released improperly by you; (d) may subject TapTap or its authorized operators to legal liabilities; (e) are deemed by TapTap to carry a virus, or as malware or spyware, or to have adverse effects on TapTap or its authorized operators’ network; (f) are in violation of this Agreement or other agreements, terms or rules intended for the Developer; or (g) are displayed with effects on the integrity of the TapTap Platform server (that is to say Users cannot access the TapTap Platform, the Games, or encounter other difficulties in accessing the foregoing content), then TapTap may decide at its own discretion to remove the Game from the TapTap Platform, or re-classify and re-rate the Games. - -If you have violated any agreement reached with, or any rule disclosed by, the TapTap Platform, TapTap may restrict, suspend or terminate any Service provided to you, including removal of your Games, according to relevant agreements and rules. TapTap reserves the right to restrict, suspend and/or prohibit any Developer from using any Service of the TapTap Platform at its sole discretion. If your Games contain elements that may seriously damage Users’ devices or data, TapTap may disable the Games or remove the related Game package or disconnect the related link at its sole discretion. - -If your Game is removed from TapTap Platform due to above reasons and a User purchased such Game within a year (or a longer period as local consumer law mandates) before the date of removal, you agree to refund to the User all amounts paid by such User for such Game. - -9.3 **You hereby confirm and agree that Game reviews, ratings and posts are regarded as User Data, and the rights therein or thereto shall belong to Users or/and the TapTap Platform. If any third party uses User Data by reprinting or copying them without authorization, TapTap has the right to maintain the rights in its own name, including without limitation by sending a notice, filing a lawsuit and issuing an administrative complaint. In order to protect the rights and interests of related Users, TapTap has the right to retain the pages related to the removed Games, including all Product information, User reviews, Game ratings, news and forum content after removal, except that it will no longer provide service related to Game downloads.** - -## 10. Developer Account - -You agree to maintain your Developer Account registered on the TapTap Platform and assume all responsibilities for all Games uploaded or released using your account. If you choose to use the SDK or other service provided by TapTap, you shall comply with the local laws and regulations and the rules of the TapTap Platform. - -## 11. TapSDK (or API) Service - -11.1 If you choose to use the TapSDK (or API) service, you shall ensure (and urge your Users to ensure) that your (and your Users’) use of this service does not violate local laws and regulations, nor infringe upon any third-party rights and interests; any consequence or loss arising therefrom shall be borne by you (and your Users). - -11.2 You understand and agree (and shall urge your Users to understand and agree) that when you (and your Users) use the service, **unless permitted by local laws and regulations and with TapTap's prior written approval, you (and your Users) shall not, nor agree, authorize or instruct any third party to engage in activities including but not limited to the following:** - -- Delete any copyright notice, trademark notice or other ownership notice contained in the service, including without limitation any behavior that damages all related intellectual property rights of TapTap; -- Submit to any third party any content that mis-states or mis-implies that the service provided to you is owned, sponsored or endorsed by TapTap; -- Publicize or provide explanatory information about illegal behavior, promote personal injury to any group or individual, or spread any virus, worm, defect, Trojan horse or other destructive content, etc.; -- Reverse-engineer, edit or attempt to extract source code from the service or any part of the content related to the service, or obtain original data and other data, etc.; -- Copy, change and modify the service or any data or related interactive data released during the operation of the service, including without limitation by using plug-ins, add-ons, or unauthorized third-party tools or services to access the service or related systems; -- Create any website or application to reproduce or copy this service or the TapTap Platform. - -11.3 TapTap imposes certain limits on the number of service requests and concurrent requests you initiate each day. After the upper limit is reached, we may temporarily suspend our services to you (or your Users). As agreed upon, you (and your Users) shall mark the words "TapTap" or "Powered by TapTap" in the application correctly, completely and conspicuously. - -11.4 You (and your Users) shall make your own judgment on the content of the service, decide whether or not to use it, and bear all risks arising from the use of this service and its related content, including those arising from reliance on the authenticity, completeness, accuracy, timeliness and practicality of the service and its content. TapTap does not provide any warranty for that, nor bear responsibility to you (or your Users) for any consequence or loss caused by such risks. - -11.5 You agree (and shall have your Users’ prior consent) to grant TapTap free, permanent, irrevocable, non-exclusive and non-transferable rights and license to use your (and your Users’) logos or actions to promote your (and your Users’) use of this service during the term of this Agreement. - -11.6 If your application or service needs to collect any data from your Users, you must have prior consent from your Users, and collect the data only necessary for the operation of the application and realization of its functions. At the same time, you shall inform your Users about the purpose and scope of your collection of data and how you use such data to keep your Users informed. - -## 12. Privacy Policy and Data Protection - -12.1 In order to continuously innovate and improve the TapTap Platform, TapTap may collect certain usage statistics data through the TapTap Platform, including without limitation the information on how to use the platform. - -12.2 TapTap will conduct a summary study on the collected data in order to improve the TapTap Platform based on the needs of Users and Developers, and will retain such data in accordance with the TapTap [Privacy Policy](https://www.taptap.io/privacy-policy). To ensure the improvement of Games, TapTap will provide part of the Games’ data in the TapTap Platform on the Developer's backend. If you need additional data, you may apply to the TapTap Platform in writing, and TapTap will decide whether or not to provide such data. - -12.3 Except as otherwise set forth in the TapTap [Privacy Policy](https://www.taptap.io/privacy-policy), all rights in or to Platform Operation Data and User Data shall belong to TapTap and are the trade secrets of the TapTap Platform. Without the prior written consent of TapTap, you shall not use the said data for any purpose other than stipulated in this Agreement, nor provide the said data to any third party in any form. - -## 13. Termination of this Agreement - -13.1 This Agreement will remain in effect unless you or TapTap terminates this Agreement according to the following provisions. - -13.2 If you wish to remove Games and terminate this Agreement pursuant to Article 9.1, you shall notify TapTap in advance in writing notice, and this Agreement can be terminated only with TapTap's consent. Before any termination of this Agreement, you shall not remove any Game from the platform; otherwise you will be liable for breach of contract. - -13.3 Under any of the following circumstances, TapTap has the right to terminate this Agreement with a written notice: - -- You have breached this Agreement or a separate agreement executed with the TapTap Platform; -- The other conditions for Game removal, Service suspension or termination as set forth in this Agreement have occurred or been reached; -- The Service under this Agreement is terminated according to laws, regulations, judgments, arbitrations or as required by the government; -- Due to *force majeure*, you cannot continue to use the Service or the TapTap Platform cannot provide the Service; -- You no longer have the right to use Service provided by the TapTap Platform; -- TapTap decides to no longer provide Service for the platform. - -13.4 **If this Agreement or the Service is terminated for any reason, TapTap may choose at its own discretion to retain or delete all data in your account or saved in the server of the TapTap Platform when you use the Service, including any data that you have not completed before the termination of the Service.** - -13.5 **If this Agreement or the Service is suspended/terminated for any reason, you shall handle issues related to data backup and with your Users, and you shall be responsible for compensating for losses caused to the TapTap Platform as a result.** - -## 14. Disclaimer - -14.1 **You understand and agree that the TapTap Platform provides the Service on an "as-is" and "available" basis. TapTap does not provide any warranty to you, so you agree to solely bear the risks of using the TapTap Platform. TapTap will do its best to provide you with Services that are consistent and secure; however, TapTap cannot warrant that any Service it provides is free of defect to any degree or extent, nor foresee and prevent all legal, technical and other risks at any time, including without limitation \*force majeure\*, viruses, Trojan horses, hacking, system instability, third-party service defects, government actions, etc. and service interruption, data loss and other losses and risks that may be caused by such reasons. Therefore, you agree that even if the Services provided by the TapTap Platform are defective, such defects are unavoidable by the current technology in the industry, so they will not be regarded as a breach of contract on the part of TapTap. At the same time, if you lose data or information due to such defects, you agree not to hold TapTap responsible.** - -14.2 **You shall bear all responsibilities and risks arising from your use of the TapTap Platform, the Service and any content acquired by Users via downloading or other channels through your above-mentioned behavior; you shall bear all responsibility for damages or data losses to your or your Users’ computer system or other equipment caused by such use.** - -14.3 **TapTap further represents that the TapTap Platform does not provide any express or implied warranty or conditions, including without limitation implied warranties and conditions of merchantability, suitability for specific purposes, and non-infringement upon the rights of others.** - -14.4 **In view of the particularity of network service, TapTap has the right to, without notice, change, suspend or terminate part or all Service at any time based on the overall performance of the TapTap Platform or related specifications and rules. If any loss is caused to you for that reason, you agree not to hold TapTap responsible for such losses.** - -14.5 **To provide you with a more comprehensive Service, TapTap has the right to overhaul, maintain or upgrade the platform or related equipment that provides the Service on a regular or irregular basis, which may cause related Services to be interrupted or suspended within a reasonable period of time. If any loss is caused to you for that reason, you agree not to hold TapTap responsible for such losses.** - -14.6 **The Service may be interrupted by \*force majeure\* or other risks during use of such Service. For the purpose of this Agreement, “\*force majeure\*” refers to an event that cannot be foreseen, overcome or avoided and has significant impact on one party or both parties hereto, including without limitation natural disasters, e.g. floods, earthquakes, epidemics and storms, and social incidents, e.g. war, riot, government action, etc. When such an event happens, TapTap will try its best to cooperate with the relevant organizations promptly in order to make repairs immediately. If any loss is caused to you for that reason, you agree not to hold TapTap responsible.** - -## 15. Limitation of Liability - -**You understand and agree that TapTap, its affiliates and its licensor will not be liable for any direct, indirect, incidental, special, derivative or punitive excessive damages caused by your behavior as mentioned above (including any loss of data), whether or not based on any relevant liability theory, and whether or not TapTap or its representatives have been notified or shall be aware of the possibility of such damages.** - -## 16. Compensation - -16.1 To the maximum extent permitted by law, you agree to indemnify and hold harmless TapTap, its affiliates and their respective directors, officers, employees, agents and authorized operators against any third-party claim, legal action, legal proceeding and litigation process, as well as all losses, claims, damages, expenses and expenditures (including reasonable attorney fees) in the event that (a) your use of the Service is in violation of this Agreement, or (b) your Games infringe upon any third party's any copyright, trademark, trade secret, trade dress, patent or other intellectual property rights, or defame any third party, or infringe upon any third party's rights of publicity or privacy. - -16.2 To the maximum extent permitted by law, you agree to indemnify and hold harmless the payment processor, its affiliates and their respective directors, officers, employees and agents against any third-party claim, legal action, lawsuit/arbitration and lawsuit/arbitration process, as well as all losses, claims, damages, expenses and expenditures (including reasonable attorney fees) due to taxes arising from or in connection with the Service or Games released on the TapTap Platform. - -16.3 If your operation is banned by the local government due to violation of local laws and regulations, or the local regulatory authority investigates and prosecutes your illegal operations, you shall solely bear responsibility for all losses arising therefrom. If it has adverse effects on the TapTap Platform, you shall bear all losses caused to TapTap and restore its reputation. In such case, TapTap has the right to terminate this Agreement at its own discretion. If you have violated laws, or the Games you provide contain illegal information, or may infringe upon a third party's legal rights, regardless of whether or not adverse consequences have arisen, TapTap has the right to require you to replace or modify the content immediately, or terminate this Agreement, and request that you compensate for losses, if any. - -## 17. Changes to Agreement - -TapTap may amend or update this Agreement at any time. TapTap will post a notice on this page and/or the Dashboard explaining the change to this Agreement. Please check this Agreement regularly. Such change will not be retrospective. The change will come into effect and be deemed accepted by you: (a) for Developers who register as a Developer after the change, it will come into effect immediately; or (b) for existing Developers before the change, the change will come into effect as of the date set forth in the notice, or come into effect immediately if required by the local laws. **If you object to the amendment to this Agreement, you may notify TapTap in writing and both parties may negotiate how to settle the dispute. You shall stop using the Service provided by the TapTap Platform during the period from the day you send a notice of dispute to TapTap until both parties reach an agreement or you decide to accept this Agreement.** You agree that by continuing to use the TapTap Platform, including the related Service provided by the platform, you accept the amendment to this Agreement. - -## 18. General - -18.1 This Agreement shall constitute a full legal instrument between you and TapTap, and your use of the TapTap Platform will be governed by this Agreement. TapTap reserves the right to amend the terms and conditions of this Agreement and the platform policy at its sole discretion at any time. Matters not covered in this Agreement shall be subject to a separate agreement, if any, entered into by both parties. - -18.2 You agree that even if the TapTap Platform does not exercise or enforce any legal rights or remedies under this Agreement (or enjoyed by TapTap under any applicable law), it shall not be deemed that TapTap has waived such rights, and TapTap is still entitled to such rights or remedies. - -18.3 You agree that any software, service, hardware, material and file disclosed by TapTap to you as a Developer shall be TapTap’s confidential information. Unless with written consent from TapTap, you shall not disclose TapTap’s confidential information to any third party. The said confidentiality obligations shall survive any termination of this Agreement. - -18.4 If any court with jurisdiction over such matters holds that any provision of this Agreement is invalid, TapTap will delete the provision from this Agreement without affecting the remaining provisions of this Agreement. The remainder of this Agreement shall remain valid and enforceable as well. - -18.5 **Use of the Service or Games on the TapTap Platform may be restricted by the laws and regulations of the region where the Service is used. You shall comply with all relevant laws, regulations or policies applicable to the jurisdiction where your Games are released or used. These laws, regulations or policies include restrictions on the location of Service use, Users, and the end purpose.** - -18.6 You or TapTap shall not assign or transfer the rights granted in this Agreement without the prior written consent of the other party. You or TapTap shall not assign or transfer the obligations under this Agreement to any third party without the prior written consent of the other party. Any other attempt to assign or transfer such rights or obligations shall be invalid. - -18.7 **All complaints arising from or in connection with this Agreement or the relationship between you and TapTap under this Agreement shall be governed by Singapore law. Furthermore, any dispute related to this Agreement shall be settled by both parties through friendly negotiation. If such negotiation fails, both parties agree to refer to and finally resolve the dispute by arbitration administered by the Singapore International Arbitration Centre (“SIAC”) in accordance with the Arbitration Rules of the Singapore International Arbitration Centre (“SIAC Rules”) for the time being in force, which rules are deemed to be incorporated by reference in this clause. The seat of the arbitration shall be Singapore. The language of arbitration shall be English. Nevertheless, you agree that TapTap has the right to apply for injunctive relief from a court within any jurisdiction.** - -18.8 TapTap may send you agreements, rules, notices, and reminders with respect to the Service in the form of web announcement, web reminder, email, message or in-site letter, or by express or post, etc., which shall be deemed delivered once released or sent in any manner above and binding upon you. - -18.9 The terms and provisions with respect to confidentiality and dispute settlement shall survive any expiration or termination of this Agreement. [End] - diff --git a/.ci/hk/en/store/store-faq.mdx b/.ci/hk/en/store/store-faq.mdx deleted file mode 100644 index ecbda7e7e..000000000 --- a/.ci/hk/en/store/store-faq.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: FAQ ---- -import {FaqLink} from '/src/docComponents/doc'; - -## 1. Register as a Developer - -### [1. How to register?](/store/#how-to-register) -### [2. How to enter the developer center?](/store/#get-started) -### [3. What is the difference between the business developer and the individual developer? Can I change from one to the other?](/store/#what-is-the-difference-between-the-business-developer-and-the-individual-developer-can-i-change-from-one-to-the-other) -### [4. I am an independent developer. What should I fill into the title?](/store/#i-am-an-independent-developer-what-should-i-fill-in-the-title) -### [5. If the developer account already exists, what should I do? Can I claim it?](/store/#if-the-developer-account-already-exists-what-should-i-do-can-i-claim-it) -### [6. How do I change the provider title?](/store/#how-do-i-change-the-provider-title) -### [7. Do I have to start uploading games immediately after I have registered successfully?](/store/#do-i-have-to-start-uploading-games-immediately-after-i-have-registered-successfully) - - -## 2. Authority Management - -### [1. How to add a member or an administrator?](/store/store-admin/#add-member-or-master-administrator) -### [2. How to add roles?](/store/store-admin/#add-roles) -### [3. How to delete a member?](/store/store-admin/#delete-member) -### [4. Can the Super Administrator be changed? Is it possible to set up a new Super Administrator?](/store/store-admin/#1-can-the-super-administrator-be-changed--is-it-possible-to-set-up-a-new-super-administrator) - -## 3. Developer Verification - -### [1. What is the developer verification?](/store/store-auth/#what-is-the-developer-verification) -### [2. How can I be verified as a developer?](/store/store-auth/#how-can-i-be-verified-as-a-developer) -### [3. How do I remove the developer verification?](/store/store-auth/#how-do-i-remove-the-developer-verification) - - -## 4. Assets Requirements - -### [1. Icon requirements](/store/store-material/#icons) -### [2. About game requirements](/store/store-material/#about) -### [3. Video and screenshots requirements](/store/store-material/#video-and-screenshots) -### [4. Cover image requirements](/store/store-material/#cover-image-on-top-of-game-page) -### [5. Promotion image requirements](/store/store-material/#promotion-image) -### [6. License](/store/store-material/#license) -### [7. What should I pay attention to in order to pass the assets review?](/store/store-material/#what-should-i-pay-attention-to-in-order-to-pass-the-assets-review) - -## 5. Create Game - -### [1. Game creation process](/store/store-creategame/) -### [2. Do I have to start uploading games immediately after I have registered successfully?](/store/#do-i-have-to-start-uploading-games-immediately-after-i-have-registered-successfully) -### [3. Do I need to release my game immediately after it passes the review?](/store/store-creategame/#do-i-need-to-release-my-game-immediately-after-it-passes-the-review) -### [4. Will TapTap accept all types of games?](/store/store-creategame/#will-taptap-accept-all-types-of-games) - -## 6. Claim, Transfer, and Unpublish a Game - -### [1. My game is already on TapTap. Can I claim it?](/store/store-creategame/#my-game-is-already-on-taptap-can-i-claim-it) -### [2. Can the owner/provider of the game be changed?](/store/store-creategame/#can-the-ownerprovider-of-the-game-be-changed) -### [3. What should I do if I need to remove my game from TapTap?](/store/store-creategame/#what-should-i-do-if-i-need-to-remove-my-game-from-taptap) - -## 7. Update Game - -### [1. How to update the game?](/store/store-update/) -### [2. Can I change the title of the game?](/store/store-update/#can-i-change-the-title-of-the-game) - -## 8. Game Testing - -### [1. Testing on Android](/store/store-test/#testing-on-android) -### [2. Test types on Android](/store/store-test/#test-types-on-android) -### [3. Testing on iOS](/store/store-test/#testing-on-ios) -### [5. What is a test page? Why should I create a test page for my game? How to create a test page on TapTap?](/store/store-test/#what-is-a-test-page-why-should-i-create-a-test-page-for-my-game-how-to-create-a-test-page-on-taptap) -### [6. Resources](/store/store-test/#resources) diff --git a/.ci/hk/en/store/store-material.mdx b/.ci/hk/en/store/store-material.mdx deleted file mode 100644 index 56778caf5..000000000 --- a/.ci/hk/en/store/store-material.mdx +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: Assets Requirements -sidebar_position: 40 ---- - -import {Red, Blue} from '/src/docComponents/doc'; - -Those marked with * are required materials. - -## Icons* - -The size should be 512x512px, and the format must be JPG or PNG. Please keep the key content within the Safe Area as shown in the image below. - - - -![ ](/img/Assets-Requirements-1.png) - -## Text Materials* - -### Description - -In the game page, description is displayed below game videos and screenshots, and above rating & reviews. - -You can describe the genre, gameplay, and features of your game here. - -For the About to display normally on the TapTap app, do not leave blank lines in the text. - - - - -### What's new - -Update log is displayed below description, and you can tap on **More** to view the complete log. - -To avoid misunderstandings, do not fill in the Update if your game is released for the first time or in pre-registration. - -Please keep the Update concise and clear and relevant to the content of this update. - -For the Update to display normally on the TapTap app, do not leave blank lines in the text. - - - - - -## Video and Screenshots* - -In the game page, videos and screenshots are displayed above the game title. - -The video will be displayed first. When the video is not uploaded, the screenshots will be displayed in the order of upload. - -![](https://capacity-files.lcfile.com/1QVoTrTNvsCFh8Ubl1TAqOfQ9yXDDrkc/Assets-V2en-5.png) - -### Video - -Format: MP4 - -Size: up to 1GB - -Length: We would recommend your video to be about 20 seconds for players to best understand the key gameplay. It must not be longer than 1 minute. - -Resolution: 1280 x 720px and above, 1920 x 1080px recommended. - -**A cover image is required for the video.** - -The size for cover image is 1256x706px and the format must be JPG or PNG. - -The video will also appear on the card when your game is recommended on Home of TapTap. **This video is optional.** - -When there is no video, the promotion image will be displayed on the card. When neither the video nor the promotion image is available, the cover image on the game page will be displayed. - -As for the content of the video, we recommend you to present new characters, new gameplay, events, etc. We also recommend you to update the video regularly. - -![](https://capacity-files.lcfile.com/UUmqcFAWGiPrXXP4JpNQzX5HhLW8ReDl/Assets-V2en-6.png) - - -### Screenshots - -Please make sure all screenshots are of the same size, and that the format must be JPG or PNG. We recommend at least 3 screenshots for your game. Do not upload the same screenshot repeatedly. - -The file size of the screenshots should not be too large, for it may lower the speed of uploading and the loading on the game page. - -For horizontal screenshot, the recommended ratio is 16:9, and the size should be 1280x720px and above. - -For vertical screenshot, the recommended ratio is 9:16, and the size should be 1280x720px and above. - -![](https://capacity-files.lcfile.com/hpw19sE7Pux0G5y0hckmyrvRF331KRaG/Assets-V2en-3.png) - - - - - -## Cover image on top of game page* - -### Purpose - -This image will be displayed on the top of game's detail page in TapTap. **This must be provided.** - -![](https://capacity-files.lcfile.com/bIvVd5o3nQxFNHbwSN16BlXVc50inGnq/Assets-V2en-4.png) - - -### Best Practices - -The cover image on the game page is usually the first thing that players notice about your game. Therefore, we recommend you to highlight the main characters and logo of the game in the image. Make sure everything in the image is clearly displayed. - -Do not include text other than the game title. - -Please keep the focus of the image inside the safe area as shown below. - -### Size and Format - -The size of the cover image should be 1920x1080px (16:9) and the format must be JPG or PNG. - -Please keep the focus of the image, such as the logo or main characters inside the safe area of 1760x920px. - -![ ](/img/Assets-Requirements-8.png) - -## Promotion Image* - -### Purpose - -This image will be used on the game's card in Collections, Home and other locations in TapTap. **You can choose to provide this image.** - -When there is no promotion image, cover image on the top of game page will be displayed instead. When there is a video available, the promotion image will be displayed before the video is automatically played. - -### Best Practices - -For the promotion image, we recommend you to use assets for marketing or related to the latest version. Please make sure that the materials and logo are clearly visible, and all assets are updated regularly. - -Do not include text other than the game title. - -### Size and Format - -The size of the cover image on the game page should be 1920x1080px (16:9) and the format must be JPG or PNG. - -## License - -### Authorization - -If your game involves any sort of authorization, please provide a scanned copy of the agency authorization letter or IP authorization letter. If there are multiple levels of authorization, please provide scanned copies of all agency authorizations. - -The authorization letter must be JPG or PNG. Each image should not be bigger than 2M, and up to 10 images can be uploaded. - -## Q&A - -### What should I pay attention to in order to pass the assets review? - -Please make sure your game does not infringe the copyright or any other parties. If your game's materials involve another IP, please provide proof of authorization. - -The assets you provide must not contain information that is related to politics, gambling, violence, sexual or other illegal content. diff --git a/.ci/hk/en/store/store-notifications.mdx b/.ci/hk/en/store/store-notifications.mdx deleted file mode 100644 index e14418475..000000000 --- a/.ci/hk/en/store/store-notifications.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Description of the Platform's automated notifications -sidebar_position: 31 ---- - -import { Blue } from "/src/docComponents/doc"; - -TapTap will trigger automatic notifications based on the developer's operational behavior. Currently, the platform sends automatic notifications based on the following scenarios. Developers can independently configure whether to receive notifications according to their actual needs. - -- Regarding sending objects: Currently, the platform notifications are mainly directed to the roles of super administrators and game master administrators, etc. Developers can go to the backstage Manage Permissions module to configure the required roles independently. -- About sending forms: Now the platform supports sending forms such as email and in-site messages. Developers can go to the background Message Center-Reception Settings module to manage the relevant contact information. - -## Description of Automatic Notification Types - -一、 Developer Onboarding and Profile Management - -| Name of Notifications | Scene | Object | Send Form | -| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------ | ------------ | -| Notification of Developer Onboarding Audit Passed | Become a Certified Developer Audit Passed | Users who Submit to Become a Certified Developer | Mailbox | -| Notification of Developer Onboarding Audit Failure | Fail to Become a Certified Developer | Users who Submit to Become a Certified Developer | Email | -| Notification of Approval of Developer Information Change Review | For developers who have settled in, submit developer information change review and review has been passed | Super Administrator | Email, In-site Messages | -| Notification of Developer Information Change Review Failure | The settled developer submitted a review for the change of developer information, but the review failed. | Super Administrator | Email, In-site Messages | - - - -二、Games on the Shelf - -| Name of Notifications | Scene | Object | Send Form | -| ---------------- | ------------------------------------------------ | ---------- | ------------ | -| Notification of Game Approval | After the developer submits the game, the game is reviewed and approved | Game Administrator | E-mail、In-site Message | -| Game Review Failure Notification | Game review is rejected after developer submits game | Game Administrator | E-mail、In-site Message | -| Game Timing Notification | The game is set to go on shelves at a scheduled time. After passing the review, it reaches the time when it is timed to go live | Game Administrator | E-mail、In-site Message | diff --git a/.ci/hk/en/store/store-register.mdx b/.ci/hk/en/store/store-register.mdx deleted file mode 100644 index 0ddc554c4..000000000 --- a/.ci/hk/en/store/store-register.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: Register As A Developer -slug: /store -sidebar_position: 30 ---- - -import { Blue } from "/src/docComponents/doc"; - -## How To Apply - -Go to **[Developer Center](https://developer.taptap.io/)** >> Find **[Become a developer](https://developer.taptap.io/developer-apply/)** >> Select the type of developer (Registered/Verified) >> Fill in the information >> Submit for review (results available in 2 business days) >> After passing the review, you can log in to your account in Developer Center - -| **Type** | **Prerequisites** | **Required Information** | **Review** | **Developer Page** | -| ------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------------------------------------------------- | -| **Registered Developer** | Owning a valid [Tap.io](https://accounts.taptap.io/signup)account | Provider title, Contact email (requires verification) | No review required | Not available on [Tap.io](https://www.taptap.io/) | -| **Verified Developer(Recommended)** | Owning a valid [Tap.io](https://accounts.taptap.io/signup) account | **Individual Developer**
    • Provider title, Developer name, Contact address, Front and back of identity document
    • Contact email (requires verification)

    **Business Developer**
    • Provider title, Company name, Contact address, Company's business license
    • Contact email (requires verification)
    | Requires review by TapTap | Not available on [Tap.io](https://www.taptap.io/) | - -![](/img/DC-v2/register-V2en-1.png) - -## What are the differences between registered developer and verified developer? - -- **Registered Developer** - - Your account will be marked as 'Registered Developer' in **Provider Settings** of your developer page in Developer Center. Registered information can be modified and it will be automatically approved after submitted for review. If you would like to become a verified developer, you can start the process by clicking on 'Become A Verified Developer'. - - Registered developers only have the provider's permissions such as publisher settings, permission management, and tickets, as well as permissions related to game operation such as releasing games, accessing data overview, submitting qualification documents, etc. -- **Verified Developer** - - Your account will be marked as 'Verified Developer' in **Provider Settings** of your developer page in Developer Center. Registered information can be modified and submitted changes will need to be reviewed. - - In addition to provider's permissions such as publisher settings, permission management, and tickets, as well as permissions related to game operation such as releasing games, accessing data overview, submitting qualification documents, etc; verified developers also have finance permissions such as setting up financial entity and game service billing, and access to game service such as TDS technical services and data analysis. - -When you are a registered developer and need to release paid games on TapTap, after choosing to add provider's financial entity information, you will be redirected to the page for 'becoming a verified developer'. - -When you need to enable some game services function on the developer page, after clicking on **configuration**, you will also be redirected to the page for becoming a verified developer. - -## Submit For Review - -After the application for verified developer is submitted, the TapTap operation team is expected to complete the review in 2 business days. The result will be available in **Notifications** of your account. - -You can also view the review status on the application page: - -- If the status is 'Under review', the page cannot be resubmitted. Please wait for the review result. -- If the status shows 'Rejected', please refer to the reason for rejection to revise the application and resubmit for review. - -## Developer Page - -Once your developer account has been approved, you can go to Developer Center **Developer Center** to access your developer page. - -![](https://img.tapimg.com/market/images/2634c08f2c78303ed9daba736d9d308b.png) - -## Q&A - -### Can I switch between registered developer and verified developer? - -Currently TapTap does not support the switch between types of developer. Please choose the type of developer according to your situation. We recommend that you apply to become a verified developer. - -### I am an Individual developer. How do I fill in the provider title? - -We would recomend you to use a title of a team as the provider title instead of your own name. - -### If the developer account already exists. Can I claim it? - -You can contact us via **Developer Center** >> **Support**. You can also send your request to our team at [international_operation@taptap.com](mailto:international_operation@taptap.com) and provide a scanned copy of the business license or other materials that can prove that you are an employee if the provider. Please send the request under the same name as the existing developer. - -**Template** - -> **Email subject** Claim Provider Account On TapTap - Provider Title -> -> Title of the vendor to be claimed: -> -> Scanned copy of business license or other proof materials (you can also upload as attachments): -> -> Contact of the applicant: - -When your application is approved, you will be able to claim the provider account and you will become the provider's Master Administrator by default. - -TapTap operation team will process your request in 2 business days. - -### How to change the provider title? - -You can contact us via **Developer Center** >> **Support**. You can also send your request to our team at [international_operation@taptap.com](mailto:international_operation@taptap.com) and provide a scanned copy of the business license or other materials that can prove that you are an employee of the provider. - -**Template** - -> **Subject** -> Change provider title on TapTap - Provider's Current Title -> -> Title of the provider title to be changed: -> -> New provider title: -> -> Scanned copy of business license or other proof materials (you can also upload as attachments): -> -> Briefly explain the reason for change: -> -> Contact of the applicant: - -TapTap operation team will process your request in 2 business days. - -### Do I have to start uploading games immediately after I have registered successfully? - -No. - -TapTap does not have requirements on the time to creat game, so feel free to set up the game according to your own schedules. We do recommend you to upload the games as early as possible to get better prepared for the release. diff --git a/.ci/hk/en/store/store-test.mdx b/.ci/hk/en/store/store-test.mdx deleted file mode 100644 index 75054a545..000000000 --- a/.ci/hk/en/store/store-test.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Game Testing -sidebar_position: 60 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -## Testing on Android - -### Contact Us - -Please give us details for the game test you are planning by sending an email to [international_operation@taptap.com](mailto:international_operation@taptap.com). - -**Template** - -> **Subject** -> -> Test Information - XXX (Game Title) - ---- - -> -> Game Title: -> -> Is it a closed test: -> -> In-site page link: -> -> Pre-download on (leave blank if not applicable): yyyy-mm-dd, XX:XX (UTC+08:00) -> -> Server opens on (leave blank if not applicable): yyyy-mm-dd, XX:XX (UTC+08:00) -> -> Will the data be deleted after the test is over: -> -> Does this test include in-app purchases: - -When the test starts, TapTap will send notifications to Android users who have pre-registered for your game. - -### Change Status - -In addition to setting the release status in the update game,You can change the game status into "Testing" in the Store >> Change Release Status, then submit it for review. After 15 minutes, the game status will be changed automatically. - -Please note that you need to upload an APK and pass the review first. You will not be able to change the game status without an approved APK. - -![ ](/img/Game-Testing-1.png) - -## Testing on iOS - -For games to be released on iOS, you will need to test it with TestFlight. - -Please inform us about the testing time (UTC+8:00) and the public link of TestFlight via email at least two business days before the test starts. We will help you to configure the public link. You will need to change the status to Testing when the test starts. - -iOS users can join the test by clicking the Download button on the game page in the browser, which will guide them to install TestFlight. For users who have already installed TestFlight, the app will be launched and they will be eligible for joining the test. - -## Test Types on Android - -1. Unlimited Participants - - Refer to [Testing on Android]. Do not forget to inform us about your test via email. - -2. Limited Participants - - - Close Download - - You can choose to close the download when the number of participants reaches the limit. You can set the status to ‘Pre-registration’ or ‘Coming Soon’ instead. - - - Limited Codes - - You need to upload the activation codes in the backend in advance and notify us about the test via email. We will configure and distribute the codes according to your schedule. - -![ ](/img/Game-Testing-2.png) - -## Resources - -We will notify our editors about the test. During the test period, our editors will support your game by assigning resources such as visibility on TapTap. - - diff --git a/.ci/hk/en/store/store-update.mdx b/.ci/hk/en/store/store-update.mdx deleted file mode 100644 index 5a10be153..000000000 --- a/.ci/hk/en/store/store-update.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Update Game -sidebar_position: 55 ---- - -import { Blue } from "/src/docComponents/doc"; - -### Location - -After the release, we recommend you to regularly update the materials on the game page to keep players interested and increase conversions. You can set up the update by clicking on the **>New** button in **Developer Center** >> **Store Info**. - -- When creating a new version, the page will automatically read the last filled content. You can only modify the information that needs to be updated. - -![](/img/DC-v2/update-V2en-1.png) - -You can schedule the release time for the update in the **Release Settings**. - -- If you select **Release Now** in Release Settings, the new version of the information/APK will be updated to the game page immediately after passing the review. -- If you select **Scheduled** , the new version of the information/APK will be updated to the game page at the decided time after it passed the review. - -Please note that you will need to set the time to at least 24 hours after the current time. - -### Drafts - -When creating a new version, you can submit the filled information immediately for review, or you can save it as a draft. - -- After saving the draft, it will be loaded automatically when you open the page again. -- You can only save one draft. Saving a new draft it will overwrite the existing one. - -![](/img/DC-v2/update-V2en-2.png) - -### Submit For Review - -After the update is submitted for review, the result should be available within 24 hours. Please remember to check your notifications. - -### Version History - -You can check the review status, APK version name, version code, and other information of all past versions in **Developer Center** >> **Version History**. Or you can click on **Version Details** to see all the information submitted for that version. - -## Q&A - -### Can the game title be changed? - -Yes. - -When changing game name, you will also need to change the relevant assets such as icon, posters, screenshots and other materials and submit them for review. - -After the game title has been changed, we recommend you to post an announcement on TapTap where you explain to the players the reason for the change to avoid confusion or the rating. - -### Can I change the name of the package files? - -Yes, but it is not recommended. - -Changes of the package file name will cause the players to lose the data and have to re-install the game. If you are sure that you need to change the package name, we recommend that you post an announcement on TapTap to explain the situation to the players. And you will need to sent your request via ticket at **Developer Center** >> **Support**. Or you can contact our team at [international_operation@taptap.com](mailto:international_operation@taptap.com). diff --git a/.ci/hk/env.ts b/.ci/hk/env.ts deleted file mode 100644 index 22085daa9..000000000 --- a/.ci/hk/env.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const BRAND: string = "tds"; -export const REGION: string = "global"; - -// Cloud Engine -export const CLI_BINARY: string = BRAND === "tds" ? "tds" : "lean"; -export const HAS_SUB_DOMAIN: boolean = REGION === "global"; -export const HAS_ENGINE_CDN_DOMAIN: boolean = REGION === 'cn' diff --git a/.ci/hk/sidebars.js b/.ci/hk/sidebars.js deleted file mode 100644 index 51858820f..000000000 --- a/.ci/hk/sidebars.js +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-check - -/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ -const sidebars = { - store: [ - { - type: "link", - label: "关于 TapTap", - href: "https://www.taptap.io/about-us/", - }, - { - type: "autogenerated", - dirName: "store", - }, - ], - sdk: [ - { - type: "autogenerated", - dirName: "sdk", - }, - ], - design: [ - { - type: "autogenerated", - dirName: "design", - }, - { - type: "link", - label: "品牌素材", - href: "https://www.taptap.io/about-us/brand-resources", - }, - ], -}; - -module.exports = sidebars; diff --git a/.ci/hk/zh-Hans/operations/manual.mdx b/.ci/hk/zh-Hans/operations/manual.mdx deleted file mode 100644 index 3cb89b905..000000000 --- a/.ci/hk/zh-Hans/operations/manual.mdx +++ /dev/null @@ -1,552 +0,0 @@ ---- -title: 内容运营 ---- - -## 用户能在哪里看到游戏及相关内容? - -### 【App 主要版块&资源位分布】- 以下所有资源均由运营配置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    位置资源位描述 - 推广对象 资源位/官方编辑内容案例
    Home - For you 顶部 -

    (默认) Game of the day 推广游戏

    -

    (左滑) Story of the day 推广内容

    -

    (左滑)Event of the day 推广活动/内容

    -
    - - -

    Game of the day & Story of the day & 社区征稿活动预览


    - - -

    内容:首爆实机直播 + CTA 预约 TapTap


    - - -

    内容:首爆实机演示逐帧分析 TapTap


    - - -

    内容:官方编辑测评+实机视频 TapTap


    - - -

    内容:每周重点游戏专题汇总 TapTap


    - - -

    内容:汇总版本更新内容+玩家测评 TapTap


    - - -

    内容:官方和开发者访谈 TapTap


    - - -

    活动:评论盖楼送玩家福利 TapTap


    -
    Home - Weekly hits -
  • Weekly hits - 推广游戏
  • -

    展示根据运营配置的游戏事件

    -
    - - -

    展示在首页 Dailies 下方


    - - -

    首页 Weeklyhits 资源位


    -
    Home - For you -
  • 首页内容 feeds 流 - 推广内容 -
      -
    • 所有优质内容 POST
    • -
    • Trending hashtags
    • -
        -
      • 点进 hashtag 后会显示带有该 hashtag 的帖子
      • -
      -
    • Hashtag 内容聚合
    • -
        -
      • ✔ 某 hashtag 下的内容帖
      • -
      • ✔ 特定主题的游戏合集
      • -
      -
    -
  • -
    - - -

    首页 feeds 流


    - - -

    Hashtag & Hashtag 内容聚合


    -
    Discover 发现页 -

    Upcoming - 推广游戏

    -

    展示根据运营配置的游戏事件

    -
    - - -

    发现页 Upcoming 资源位入口


    - - -

    发现页 Upcoming 资源位


    -
    Discover 发现页 -

    Weekly hits -推广游戏

    -
    - - -

    发现页 weekly hits 资源位入口


    - - -

    发现页 Weeklyhits资源位


    -
    搜索 -

    推广游戏

    -
  • 自定义底纹词
  • -
  • Trending 热搜游戏
  • -
  • Trending 热搜 Hashtag
  • -
    - - -

    底纹词+热搜榜游戏+热搜榜 Hashtag


    -
    游戏详情页 -
  • From the dev
  • -

    内容置顶 - 厂商可将官方帖置顶添加到游戏详情页

    -
  • 自定义按钮
  • -

    支持厂商问卷调研、测试招募等测试需求

    -
  • 游戏官方 Discord 入口
  • -

    TapTap 运营侧可配置入口

    -
    - - - -
    推送 -

    推广游戏/内容

    -

    运营根据游戏事件配置推送

    -
    - - - -
    - -### TapTap 运营侧可手动配置的资源位 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    运营可配置资源备注
    Game of the day + 推送游戏详情页给相关玩家 -

    *优先考虑新上线及测试阶段游戏

    -

    *根据游戏品类风格等圈定推送玩家

    -
    Story of the day -

    *一般为 Dev log、研发访谈等深度内容

    -

    (可以贴一下 cheddar 整理的 story of the day 标准)

    -
    Community Event & Event of the day -

    *针对重大更新、送福利、测试、游戏新上线等事件开展全社区活动,邀请玩家互动参与

    -

    **社区活动形式可参考: MyFavClashMiniHeroes

    -
    Trending 热搜 -

    *Game of the day 当天配置到热搜 1 位

    -
    Upcoming 即将上线 -

    *会细化到具体事件日期及事件类型

    -
    游戏卡片加权 & 游戏帖子加权 -

    *在重大事件节点(上线、测试、更新)增加游戏卡片&帖子在首页的曝光

    -
  • 帖子【10 w 曝光/天,发贴时间+ 0 ~ 发贴时间+ 1 加权】
  • -
  • 卡片【34.5 w 曝光/天,上线/开测时间+ 0 ~ 上线/开测时间+ 2 加权】
  • -
    游戏详情页自定义按钮 -

    *可针对官网招募、Discord 引流等需求配置自定义按钮及跳转外链

    -
    - -### TapTap站内主要内容类型 - -- TapTap编辑内容&PGC: - - - 资深游戏编辑和专业创作者评测 → 可针对合作游戏产出抢先试玩、游戏评测等内容,提升游戏转化 - - - 北美编辑&部分优秀创作者站内主页 - - - - - - - - - - - - - - - - - - - -
    TitleName & Profile link
    北美编辑 -

    Philip

    -

    Ian

    -

    Jay

    -

    Aaron

    -

    Mandi

    -
    部分优秀创作者 -

    IndieVoice

    -

    Kosh

    -
    - -- UGC:用户自发评测 → 可以提供激励,在站内开展投稿活动,提升游戏转化 -- 厂商:一手内容及资讯 → 建议定期发帖,和用户互动,提升游戏在各个阶段的热度和转化 - -## 如何策划并制作官方账号的内容? - -### 内容规划 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    阶段内容类型参考链接
    入驻后 -

    Dev say hi

    -
  • 简单介绍游戏及团队情况
  • -
  • 简单描述未来运营计划 roadmap
  • -
  • 引导玩家关注官方账号、预注册游戏
  • -
    -

    Goose Goose Duck

    -

    Dungeons of Dreadrock

    -

    Bravery and Greed

    -

    Truck World: Australia

    -
    预热 -

    Devlog

    -
  • 独立游戏可侧重研发初衷、心路历程、behind-the-scenes 故事、玩法角色设计思路等方面撰写 Devlog,内容风格可偏亲切友好
  • -
    -

    Settlement Survival

    -

    Revival: Recolonization

    -
    -

    重要时间节点首曝预告帖

    -
  • 包含上线/开测/大版本更新等重要事件的重要信息:时间、事件类型、地区等
  • -
  • 游戏简单介绍
  • -
  • 视频前瞻物料( PV/gameplay 等)
  • -
    -

    Settlement Survival

    -

    Undawn

    -

    Arena Breakout

    -
    -

    游戏内容前瞻资讯

    -
  • 背景故事介绍
  • -
  • 重点角色首曝
  • -
  • 实机演示披露
  • -
  • 核心玩法展示
  • -
    -

    Otaku’s Adventure

    -

    Undawn

    -
    -

    玩家福利赠送

    -
  • 可以活动形式承载,积累预约与玩家互动
  • -
  • 可配合重要事件节点进行
  • -
    -

    Otaku’s Adventure

    -

    Ace Racer

    -
    -

    FAQ

    -
  • 整理常见问题
  • -
    -

    Settlement Survival

    -
    上线/测试/更新后 -

    上线/测试官宣帖

    -
  • 强调重要信息:时间、事件、地区
  • -
  • 视频/KV 等游戏物料
  • -
  • 游戏简介
  • -
    -

    Ace Racer

    -

    Life Makeover

    -

    Settlement Survival

    -
    -

    攻略

    - -
    -

    Otaku’s Adventure

    -
    -

    社区活动

    -
  • 抽奖送福利
  • -
  • 吸引互动 & 内容征集 *可联动 TapTap 站内社区活动
  • -
    -

    Settlement Survival community event:Share your best settlement

    -

    Project Stars community event:My reations in Project Stars

    -
    - -### 内容撰写 & 发布指南 - -- **建议发帖形式及字数**:建议以Article形式创建帖子,包含视频(预览显示为视频转化效率更高)+图片+文字内容,包含重要资讯(如上线、开测等)帖子的长度一般建议在 250 - 350 词左右 - -- **帖子发出前必须检查**: - - *帖子是否绑定游戏*:在帖子右侧边栏有 Mentioned games 版块,此处至少需要绑定一个游戏,否则帖子不会在首页进行分发,并且不会出现相关游戏的详情页中;帖子中插入的游戏卡片会自动同步到这里 - - *帖子状态是否是 Public *:右下角 Visibility 版块,帖子状态需为 Public ,若为 Unlisted,则无法正常分发 - - *帖子是否有封面*:可在个人主页检查草稿箱里的帖子是否有缩略图/封面图 - -- **标题**:需包含游戏名、事件、事件发生时间这三个要素,方便玩家快速抓住有效信息,可选择性添加游戏描述(Zombie shooter xxx; indie puzzle game xxx; 抓典型特征) - - e.g. - - *Fortnite new season update coming on Apr xx!* - - *Madtale | Closed beta test available now!* - - *Text-based puzzle game CaseCracker coming to TapTap on xxx!* - -- **Call to action**:在帖子开头结尾强调希望玩家进行的动作,比如 Pre-register for xxx, Join our official Discord server, Participate in our closed beta test 等 -- **游戏简介**:一到两句即可,尽量放在开头,方便未玩过游戏的玩家了解游戏;可从游戏详情页的介绍中截取一些能概括游戏品类玩法特征的内容 - - e.g. - - *Life Makeover is a limitless dress-up and social simulation game where you can create your very own avatar, customize dress-up and makeup, design one-and-only garment by yourself, build your dream house and chill with your besties!* - -- **图片&GIF 动图**:推荐在帖子中添加游戏 KV 图/ GIF 动图等,增加帖子可读性 -- **插入游戏卡片**:可在帖子开头插入此游戏的卡片,玩家点击游戏卡片可直接下载/预约,或跳转至游戏的详情页/直接点击下载/预约按钮,在开头插入可帮助加强转化效果 - - - - - - - - - - - -
    - - -

    选择此图标,可在帖子任意处添加游戏卡片


    - -
    - - -

    搜索游戏名


    - - -

    游戏卡片预览效果如图


    -
    - -- **插入视频**: - -1. 直接在编辑器内上传本地视频(推荐),注意不要忘记上传视频封面; - -2. 也可以直接插入 YouTube 链接(不推荐),但注意要以卡片形式插入链接,否则在手机端的呈现效果就是一个链接,而不是视频 - - - - - - - - - -
    - - -

    选择 Display as card


    - -
    - - -

    卡片模式视频预览效果如图


    -
    - -- **若有 Discord 引流需求**,尽量将 Discord 邀请链接放在帖子开头,比如第一段末尾的部分,注意将链接复制进编辑器后,点击链接,选择 Display as card (卡片形式) - - - - - - - - - -
    - - -

    选择 Display as card


    - -
    - - -

    卡片模式视频预览效果如图


    -
    - -- **Hashtag 添加**:右侧边栏 Tags 版块,可通过搜索定位并添加相关 hashtag,目前可使用的 hashtag 见表(后续添加跳转链接);添加合适的 hashtag 可以帮助增加帖子的分发 - -![](https://capacity-files.lcfile.com/3V6YHDRzLvhk865rgvpW0TQDa5kuzs45/image-20230419-033426.png) - -- **定时发布**:右下角 Schedule 功能可设定定时发布,置后帖子会在设定时间对外 - -![](https://capacity-files.lcfile.com/RDhj2erRMJkBRO3mgdWH0NrTRWzJNdvX/image-20230419-032447.png) - -### 如何增加玩家触达 - -- 发帖时间及运营节奏 - - - 围绕游戏重大节点,按照【事件首曝】-【预热】-【正式上线】的不同阶段规划内容,持续运营+曝光,积累玩家关注 - - - 重大节点及官方运营动作可提前同步 TapTap 侧的商务及运营团队,TapTap 侧视实际情况提供曝光方面的支持 - -- 厂商可使用的功能 - - - 置顶官方帖至手机端 From the dev 版块:【 PC 端游戏详情页】- 【右侧边栏 Admin tools 】- 【左侧 Posts 】-【搜索需置顶的帖子 ID 】- 【进行帖子置顶】 - - - 如需管理 PC 端论坛页面,除了置顶帖子外( PC 端置顶帖无特定版块,会显示某个帖子 Pinned by developer ),厂商可以进行 Hashtag 置顶及添加导航栏链接的操作: - - - Hashtag 置顶 【 PC 端游戏详情页】- 【右侧边栏 Admin tools 】- 【左侧 Manage pinned hashtags 】- 【右上角 Pin hashtag 】 - - - 导航栏链接配置 【 PC 端游戏详情页】- 【右侧边栏 Admin tools 】- 【左侧 Manage useful links 】- 【右上角 Add new links 】 - - - 开发者中心后台帖子推送功能 - - - 推送对象及频率:每周厂商可推送【1】则帖子给所有【关注该游戏的玩家】 - - - 路径:【开发者中心】-【游戏运营】-【官方帖推送】- 【新增推送帖】 - - - - - - - - - -
    - - -

    手机端 From the dev


    - - -

    开发者中心帖子推送


    - -
    - - -

    Admin tools 后台功能一览


    - - -

    PC 端 hashtag 及导航栏链接显示位置


    -
    - -### 玩家评论维护 - -评价的互动,请务必真实,尊重玩家发言,心平气和与玩家沟通,做到不敷衍,不冷漠。真诚的交流会给予玩家极大安全感,此处也往往是很多开发者在运营思路上的雷区。同时,请务必注意在互动时不要复制粘贴发言,极易引起玩家反感。面对长评价、帖子热评、用心的差评时,要及时回复,建立良好形象,官方回复会默认在玩家评论下优先显示,后续阅览的玩家会在阅读热评时看到开发者留言。 - diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/_category_.json b/.ci/hk/zh-Hans/sdk/TapPayments/_category_.json deleted file mode 100644 index 9294586f6..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapTap 支付", - "collapsed": true, - "position": 20 -} diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/appendix/_category_.json b/.ci/hk/zh-Hans/sdk/TapPayments/appendix/_category_.json deleted file mode 100644 index e52e0c210..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/appendix/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "附录", - "collapsed": true, - "position": 6 -} diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/appendix/payment-methods.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/appendix/payment-methods.mdx deleted file mode 100644 index 2ee7fb8c4..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/appendix/payment-methods.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: 支持的支付方式 -sidebar_position: 1 ---- - -# 支持的支付方式 - -TapTap Payments 支持 173 个国家或地区和 42 种货币,并正在不断集成更加本地化的支付方式。目前,我们在全球范围内支持以下支付方式: - -| **支付方式** | **支持的国家和地区** | -| ------------------- | ------------------------ | -| 信用卡和借记卡 | 173 个国家或地区 | -| 支付宝 | 173 个国家或地区 | -| PayPal | 173 个国家或地区 | -| GrabPay | 菲律宾、新加坡、马拉西亚 | -| DANA | 印度尼西亚 | -| OVO | 印度尼西亚 | -| QRIS | 印度尼西亚 | -| GCash | 菲律宾 | -| Maya | 菲律宾 | -| PayNow | 新加坡 | -| TrueMoney | 泰国 | -| Rabbit LINE Pay | 泰国 | -| Touch 'n Go eWallet | 马来西亚 | -| Boost | 马来西亚 | -| UPI | 印度 | diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/appendix/regions-currencies.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/appendix/regions-currencies.mdx deleted file mode 100644 index f28aec0bb..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/appendix/regions-currencies.mdx +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: 支持的国家地区及货币 -sidebar_position: 2 ---- - - - - - -TapTap Payments 支持全球 173 个国家地区和 40 多种货币。在 本地货币不支持的国家地区,或在开发者没有设定价格的国家地区中,用户将以美元(USD)进行付款。另外,大部分货币为二位十进制货币,例如 USD 0.99,而另一部分则是零位十进制货币,例如 JPY 130。请在设置价格时参照以下标准: - -| **国家地区名称** | **国家地区代码** | **货币名称** | **货币精度** | **支付方式** | -| ----------------------- | ---------------- | ------------ | ------------ | --------------------------------------------- | -| Afghanistan | AF | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Albania | AL | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Algeria | DZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Angola | AO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Anguilla | AI | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Antigua and Barbuda | AG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Argentina | AR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Armenia | AM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Australia | AU | AUD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Austria | AT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Azerbaijan | AZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bahamas | BS | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bahrain | BH | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Barbados | BB | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Belarus | BY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Belgium | BE | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Belize | BZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Benin | BJ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bermuda | BM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bhutan | BT | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bolivia | BO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bosnia and Herzegovina | BA | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Botswana | BW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Brazil | BR | BRL | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Brunei Darussalam | BN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Bulgaria | BG | BGN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Burkina Faso | BF | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cabo Verde | CV | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cambodia | KH | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cameroon | CM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Canada | CA | CAD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cayman Islands | KY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Chad | TD | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Chile | CL | CLP | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Colombia | CO | COP | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Congo | CG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Congo(DRC) | CD | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Costa Rica | CR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Croatia | HR | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Cyprus | CY | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Czechia | CZ | CZK | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Denmark | DK | DKK | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Dominica | DM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Dominican Republic | DO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ecuador | EC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Egypt | EG | EGP | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| El Salvador | SV | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Estonia | EE | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Eswatini | SZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Fiji | FJ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Finland | FI | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| France | FR | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Gabon | GA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Gambia | GM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Georgia | GE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Germany | DE | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ghana | GH | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Greece | GR | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Grenada | GD | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Guatemala | GT | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Guinea-Bissau | GW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Guyana | GY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Honduras | HN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Hong Kong (China) | HK | HKD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Hungary | HU | HUF | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Iceland | IS | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| India | IN | INR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、UPI | -| Indonesia | ID | IDR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、DANA、OVO、QRIS | -| Iraq | IQ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ireland | IE | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Israel | IL | ILS | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Italy | IT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ivory Coast | CI | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Jamaica | JM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Japan | JP | JPY | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Jordan | JO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kazakhstan | KZ | KZT | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kenya | KE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kosovo | XK | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kuwait | KW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Kyrgyzstan | KG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Laos | LA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Latvia | LV | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Lebanon | LB | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Liberia | LR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Libya | LY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Lithuania | LT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Luxembourg | LU | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Macau(China) | MO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Madagascar | MG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Malawi | MW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Malaysia | MY | MYR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、GrabPay、Touch 'n Go eWallet、Boost | -| Maldives | MV | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mali | ML | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Malta | MT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mauritania | MR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mauritius | MU | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mexico | MX | MXN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Micronesia (Federated States of) | FM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Moldova, Republic of | MD | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mongolia | MN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Montenegro | ME | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Montserrat | MS | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Morocco | MA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Mozambique | MZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Myanmar | MM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Namibia | NA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Nauru | NR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Nepal | NP | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Netherlands | NL | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| New Zealand | NZ | NZD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Nicaragua | NI | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Niger | NE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Nigeria | NG | NGN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| North Macedonia | MK | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Norway | NO | NOK | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Oman | OM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Pakistan | PK | PKR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Palau | PW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Panama | PA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Papua New Guinea | PG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Paraguay | PY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Peru | PE | PEN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Philippines | PH | PHP | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、GrabPay、GCash、Maya | -| Poland | PL | PLN | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Portugal | PT | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Qatar | QA | QAR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Romania | RO | RON | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Rwanda | RW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Saint Kitts and Nevis | KN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Saint Lucia | LC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Saint Vincent and the Grenadines | VC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Sao Tome and Principe | ST | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Saudi Arabia | SA | SAR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Senegal | SN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Serbia | RS | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Seychelles | SC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Sierra Leone | SL | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Singapore | SG | SGD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、GrabPay、PayNow | -| Slovakia | SK | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Slovenia | SI | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Solomon Islands | SB | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| South Africa | ZA | ZAR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| South Korea | KR | KRW | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Spain | ES | EUR | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Sri Lanka | LK | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Suriname | SR | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Sweden | SE | SEK | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Switzerland | CH | CHF | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Taiwan (China) | TW | TWD | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Tajikistan | TJ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Tanzania | TZ | TZS | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Thailand | TH | THB | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝、TrueMoney、Rabbit LINE Pay | -| Tonga | TO | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Trinidad and Tobago | TT | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Tunisia | TN | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Turkey | TR | TRY | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Turkmenistan | TM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Turks and Caicos Islands | TC | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Uganda | UG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Ukraine | UA | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| United Arab Emirates | AE | AED | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| United Kingdom | GB | GBP | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| United States | US | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Uruguay | UY | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Uzbekistan | UZ | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Vanuatu | VU | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Venezuela | VE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Vietnam | VN | VND | 整数 | 信用卡、借记卡、PayPal、支付宝 | -| Virgin Islands (British) | VG | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Yemen | YE | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Zambia | ZM | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | -| Zimbabwe | ZW | USD | 小数点后两位 | 信用卡、借记卡、PayPal、支付宝 | diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/appendix/sandbox.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/appendix/sandbox.mdx deleted file mode 100644 index f17e72db8..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/appendix/sandbox.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: 沙盒环境 -sidebar_position: 1 ---- - -# 沙盒环境 - -TapTap Payments 沙盒环境进行支付测试, 可以使用一些沙盒信用卡进行支付, - -## 沙盒账号设置 -进入开发者中心后台, 按照如下步骤设置和添加对应沙盒账号: - -![](https://img.tapimg.com/market/images/7309efbf7c1971d44c73a53494193bb4.png) - -## 沙盒信用卡 -在支付时, 可以使用以下沙盒信用卡进行支付测试: - -![](https://img.tapimg.com/market/images/7d3fb8a5a75d74f6f96f3c4a938693ff.png) - -## 沙盒异常模拟 -同时, 也可以使用以下卡号进行异常模拟 - -![](https://img.tapimg.com/market/images/7b4394a2690b01a1008fcd362ec44a99.png) diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/develop/android.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/develop/android.mdx deleted file mode 100644 index 5fcde5e48..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/develop/android.mdx +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: Android 集成指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import sdkVersions from '/src/docComponents/sdkVersions'; -import CodeBlock from '@theme/CodeBlock'; - - -## **环境要求** - -- Gradle 版本不低于 6.1.1,Android AGP 插件版本不低于 4.0.1; - -## **准备** - -- 参照 [准备工作](/sdk/start/get-ready/) 所述创建 app,配置 app 参数并且绑定 API 域名 -- 参照 [TapSDK 快速开始](/sdk/start/quickstart/) 配置包名和签名 - -## **SDK 指南** - -### **SDK 集成** - -打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: - - - {`dependencies { - ... - // TapTapIAP dependency - implementation 'com.taptap.android.payment:iap:${sdkVersions.tapGlobalPayments.android}' - implementation 'com.taptap.android.payment:base:${sdkVersions.tapGlobalPayments.android}' - implementation 'com.taptap.android.payment:stripe:${sdkVersions.tapGlobalPayments.android}' - implementation 'com.taptap.android.payment:braintree:${sdkVersions.tapGlobalPayments.android}' - implementation 'com.taptap.android.payment:alipay:${sdkVersions.tapGlobalPayments.android}' -} -`} - - - - - -### **SDK 初始化** - -添加 TapTapIAP 的依赖项后,您需要初始化 `TapTapIAP` 实例。`TapTapIAP` 是 SDK 与应用的其余部分之间进行通信。`TapTapIAP` 为许多常见的操作提供了方法。 - -首先 在应用启动时需要进行 SDK 初始化, 通过设置 `TapTapSdkOptions`, 完成 SDK 初始化, `TapTapSdk.init` 。 - -这里需要设置对应在开发者后台申请的 `ClientID` 和 `ClientToken`,用于校验是否有权限使用 `TapTapIAP`。 - -```java -TapTapSdkOptions sdkOptions = new TapTapSdkOptions( - "应用的 ClientID", // clientId 开发者平台申请 - "应用的 ClientToken", // clientToken 开发者平台申请 - TapTapRegion.GLOBAL, // 地区 - "", // 分包渠道名称 - "", // 游戏版本, 为空为null会取AppVersion - false, // 是否自动上报GooglePlay内购支付成功事件 仅 [TapTapRegion.GLOBAL] 生效 - false, // 自定义字段是否能覆盖内置字段 - null, // 自定义属性,启动首个预置事件(device_login)会带上这些属性 - null, // OAID证书, 用于上报 OAID 仅 [TapTapRegion.CN] 生效 - false // 是否开启 log,建议 Debug 开启,Release 关闭 -); - -TapTapSdk.init(context, sdkOptions); -``` - -创建 `TapTapIAP`,请使用 `newBuilder()`这里会根据SDK.init所设置的 `ClientID` 和 `ClientToken`校验是否有权限使用 `TapTapIAP`。 - -```java -// 创建 TapTapIAP 实例 - TapTapIAP tapTapIAP = TapTapIAP.newBuilder().build(); - ``` - -### **展示可供购买的商品** - -初始化完成 `TapTapIAP` 后,您就可以查询可售的商品并将其展示给用户了。 - -查询应用内商品详情,请调用 `queryProductDetailsAsync()`。为了处理该异步操作的结果,您还必须指定实现 ` ProductDetailsResponseListener` 接口的监听器。然后,您可以替换 `onProductDetailsResponse()`,该方法会在查询完成时通知监听器,如以下示例所示: - -```java -List queryProductList = new ArrayList<>(); -// 支持批量查询 Product, 设置好对应的 ProductID、ProductType -// ProductType 目前仅支持 ProductType.INAPP -for (int i = 0; i < products.length; i++) { - - String productId = products[i]; - Product product = Product.newBuilder() - .setProductId(productId) - .setProductType(ProductType.INAPP) - .build(); - queryProductList.add(product); -} -QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder() - .setProductList(queryProductList).build(); -tapTapIAP.queryProductDetailsAsync(params, new ProductDetailsResponseListener() { - @Override - public void onProductDetailsResponse(TapPaymentResult result, - List productDetails, List unavailableProductIds) { - ... - - // check TapPaymentResult - // process returned productDetails - } -}); -``` - -### **启动购买流程** - -如需从应用发起购买请求,请从应用的主线程调用 `launchBillingFlow()` 方法。此方法接受对 `BillingFlowParams` 对象的引用,该对象包含通过调用 `queryProductDetailsAsync()` 获取的相关 `ProductDetails` 对象。如需创建 `BillingFlowParams` 对象,请使用 `BillingFlowParams.Builder` 类。 - -```java -// An activity reference from which the billing flow will be launched. -Activity activity = ...; -ProductDetailsParams productDetailsParams = - ProductDetailsParams.newBuilder() - // retrieve a value for "productDetails" by calling queryProductDetailsAsync() - .setProductDetails(productDetails) - .build(); - -BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() - .setProductDetailsParams(productDetailsParams) - .setObfuscatedAccountId("xxx") //Specifies an optional obfuscated string that is uniquely associated with the order(or another information) in your app. - .build(); -// Launch the billing flow -TapPaymentResult result = tapTapIAP.launchBillingFlow(activity, - billingFlowParams, - new PurchaseUpdatedListener() { - @Override - public void onPurchaseUpdated(TapPaymentResult result, Purchase purchases) { - // To be implemented in a later section. - } - } -); -``` - -`launchBillingFlow()` 方法会返回 `TapPaymentResponseCode` 中列出的几个响应代码之一。请务必检查此结果,以确保在启动购买流程时没有错误。`TapPaymentResponseCode` 为 `OK` 表示成功启动。成功调用 `launchBillingFlow()` 后,会向用户展示收银台。 - -### **购买流程中订单状态监听** - -在购买流程中, `TapTapIAP` 会调用 `onPurchasesUpdated()`,以将购买的订单状态变更实时传给实现 PurchasesUpdatedListener 接口的监听器。您可以在初始化时使用 `setListener()` 方法指定监听器。您必须实现 `onPurchasesUpdated()` 来处理可能的响应代码。以下提供了一个 `onPurchasesUpdated()` 示例 : - -```java -@Override -public void onPurchaseUpdated(TapPaymentResult result, Purchase purchase) { - if (result.getResponseCode() == TapPaymentResponseCode.OK - && purchases != null) { - handlePurchase(purchase); - } else if (result.getResponseCode() == TapPaymentResponseCode.USER_CANCELED) { - // Handle an error caused by a user cancelling the purchase flow. - } else { - // Handle any other error codes. - } -} -``` -### **进行商品的发放,且完成订单** - -在用户进行完了任意商品的购买, 请确认为该用户发放对应的商品或者解锁对应的关卡。在确定商品发放之后需要调用 `finishPurchaseAsync` 告知 `TapTapIAP` 已完成商品的发放。以下是个代码实例: - -```java -Purchase purchase = ...; -FinishPurchaseParams params = FinishPurchaseParams.newBuilder() - .setId(purchase.getOrderId()) // Required - .setOrderToken(purchase.getOrderToken()) // Required - .setPurchaseToken(purchase.getPurchaseToken()) // Required - .build(); -tapTapIAP.finishPurchaseAsync(params, new FinishPurchaseResponseListener() { - @Override - public void onFinishPurchaseResponse(TapPaymentResult result, Purchase purchase) { - } -}); -``` -:::tip -确认发放商品非常重要, 如果您没有调用 `finishPurchaseAsync` 来完成订单, 用户将无法再次购买该商品,且该订单将会在 3天后自动退款。 -::: - -### **获取未完成的订单列表** - -使用 `PurchasesUpdatedListener` 监听购买交易变更,无法完全确保您的应用会处理所有购买交易。有时您的应用可能不知道用户进行了部分购买交易。在下面这几种情况下,您的应用可能会跟踪不到或不知道购买交易的发生: - -- **在购买过程中出现网络问题**:用户成功购买了商品并收到了对应渠道的确认消息,但用户设备在通过 `PurchasesUpdatedListener` 收到购买交易的通知之前失去了网络连接。 -- **多部设备**:用户在一部设备上购买了一件商品,然后在切换设备时期望看到该商品。 -- **异常崩溃**:用户在外部购买成功时,应用出现了崩溃的情况。 - -为了处理这些情况,请确保您的应用在 `onResume()` 方法中调用 `tapTapIAP.queryUnfinishedPurchaseAsync()`,以确保所有购买交易都得到正确处理。 - -以下示例展示了如何提取用户的未完成订单列表: - -```java -tapTapIAP.queryUnfinishedPurchaseAsync(new PurchasesResponseListener() { - @Override - public void onQueryPurchasesResponse(TapPaymentResult result, List purchases) { - if (purchases != null) { - // Process Purchases. - ... - ... - } - } - }); -``` - -### **处理 TapPaymentResult 响应代码** - -当使用 `TapTapIAP` 结算库调用触发操作时,该库会返回 `TapPaymentResult` 响应,并将结果告知开发者。例如,如果您使用 `queryProductDetailsAsync` 且返回 `OK` ,并提供正确的 `ProductDetails` 对象;或者返回了其他类型,代表了无法提供 `ProductDetails` 对象的原因。 - -并非所有类型都是错误。下面列举了一些 `TapPaymentResponseCode` 不是错误的: - -- `TapPaymentResponseCode.OK`:代表业务已成功执行。 -- `TapPaymentResponseCode.USER_CANCELED`:代表用户没有完成流程,就离开了页面 - -其他的一些错误类型可以用于调试和上报使用: - -| **可重试的 CODE** | **问题** | **可以尝试的解决方案** | -| ------------------ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| NETWORK_ERROR | 此错误代表设备和 TapTapIAP 系统之间的网络连接出现问题 | 可以使用简单的重试策略或指数退避算法 | -| ITEM_ALREADY_OWNED | 这个类型表明用户已经购买过 非消耗品 商品, 再次购买时会返回该错误 | 为了避免出现这个问题, 在展示商品界面就提示用户已经购买该商品, 且无法点进进行购买流程. | -| USER_CANCELED | 用户已退出结算流程 | | -| ITEM_UNAVAILABLE | 商品无效, 有可能是商品已经过期或者商品已经被下架 | 确保您通过 `queryProductDetailsAsync` 刷新商品详情。如果商品无效则不在界面上展示给用户 | -| DEVELOPER_ERROR | 这是一个严重错误,表明您未正确使用 API。例如,向 launchBillingFlow 提供不正确的参数可能会导致此错误 | | diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/develop/faq.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/develop/faq.mdx deleted file mode 100644 index 9a2302a28..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/develop/faq.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: 常见问题 -sidebar_position: 4 ---- - -### 1、Unity 2020.3.15 之前的版本升级 Gradle 版本的操作步骤 - -在 `Player Settings` -> `Player` -> `Android Tab` -> `Publish Settings` -> `Build`,然后勾选 **Custom Base Gradle Template**、**Custom Launcher Gradle Template**、**Custom Main Gradle Template**。 - -![](https://capacity-files.lcfile.com/5nSUEX6IWVRkwu4Nep1pXo6vUnl9VppH/%E5%8D%87%E7%BA%A7gradle_1.png) - -以下更改应用于 `Assets/Plugins/Android/` 该文件夹下生成的三个文件 `mainTemplate.gradle`、`launcherTemplate.gradle`、`baseProjectTemplate.gradle`。 - -打开 `baseProjectTemplate.gradle` 修改文件内容: - -```groovy -dependencies { - ... - // 将 3.x.0 版本修改为 4.0.0 - //classpath 'com.android.tools.build:gradle:3.x.0' - classpath 'com.android.tools.build:gradle:4.0.0' -} -``` - -分别打开 `mainTemplate.gradle` 和 `launcherTemplate.gradle` 文件,找到 `lintOptions` 标签, 分别添加 **checkReleaseBuilds false** 配置: - -```groovy -lintOptions { - abortOnError false - checkReleaseBuilds false -} -``` - - -同时,为了将 [Gradle 版本和 Android Gradle Plugin 版本对应](https://developer.android.com/studio/releases/gradle-plugin#expandable-1),需要更新 Gradle 版本,下载 [6.5.0 版本的 Gradle](https://services.gradle.org/distributions/gradle-6.5-all.zip),解压后放到自定义的文件夹中,同时**不**勾选 Unity 中的 `Preferences` -> `External Tools`-> `Android` -> `Gradle Installed with Unity(recommend)`,改为选择解压后 Gradle 文件夹的位置,如 `/gradle-6.5.0`。 - -![](https://capacity-files.lcfile.com/hrkFCRy9VuLapvsanFm6nhpkHEEz0qVE/%E5%8D%87%E7%BA%A7gradle_2.png) \ No newline at end of file diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/develop/server.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/develop/server.mdx deleted file mode 100644 index f78cf194e..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/develop/server.mdx +++ /dev/null @@ -1,688 +0,0 @@ ---- -title: 服务端开发指南 -sidebar_position: 3 ---- - -import MultiLang from "/src/docComponents/MultiLang"; - -# 服务端开发指南 - -:::tip -服务通用规则请参考 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) -::: - -## 主查服务 - -#### 请求域名: -- `https://cloud-payment.tapapis.com` - - -| ** 接口地址 ** | **Method** | **描述** | -| ----------------------------- | ---------- | -------------------- | -| `/order/v1/info?client_id={{client_id}}&order_id={{order_id}}` | `GET` | 查询订单信息 | -| `/order/v1/unconfirmed?client_id={{client_id}}` | `GET` | 查询未核销的订单列表 | -| `/order/v1/verify?client_id={{client_id}}` | `POST` | 核销订单 | - -### 查询订单信息 - -#### 服务 URL -- https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}} - -#### 请求方式 -- GET - -#### 请求签算 -- 详见 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) - -通过订单 ID 查询订单详细信息和支付状态 - -``` -curl -X GET \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}} -``` - -`data.order` 对象结构见 [订单信息](/sdk/TapPayments/develop/server/#订单信息) - -``` -{ - "data": { - "order": {} - }, - "success": true -} -``` - -### 查询未核销的订单列表 - -#### 服务 URL -- https://{{domain}}/order/v1/unconfirmed?client_id={{client_id}} - -#### 请求方式 -- GET - -#### 请求签算 -- 详见 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) - -查询当前未核销的订单列表,正常情况下在用户支付成功后,应通过 verify 接口核销订单并同时保证对用户发货成功。如果因异常原因没有完成核销,可以通过此接口查询,重新 verify 并完成发货。 - -``` -curl -X GET \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - https://{{domain}}/order/v1/unconfirmed?client_id={{client_id}} -``` - -`data.list` 数组内对象结构见 [订单信息](/sdk/TapPayments/develop/server/#订单信息) - -``` -{ - "data": { - "list": [ - {} - ] - }, - "success": true -} -``` - -### 核销订单 - -#### 服务 URL -- https://{{domain}}/order/v1/verify?client_id={{client_id}} - -#### 请求方式 -- POST[application/json; charset=utf-8] - -#### 请求正文信息 - -| ** 参数名称 ** | ** 必填 ** | ** 格式 ** | ** 描述 ** | -| --------------| --------------| --------------| --------------| -| `order_id` | Y | string | 订单唯一 ID | -| `purchase_token` | Y | string | 用于订单核销的 token | - -#### 请求签算 -- 详见 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) - -当支付成功后,核销订单表示已经确认支付结果并已对买家完成发货,订单状态也会从 `charge.succeeded` 变为 `charge.confirmed` - -``` -curl -X POST \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - -H 'Content-Type: application/json; charset=utf-8' - -d '{"order_id":"{{order_id}}","purchase_token":"{{purchase_token}}"}' - https://{{domain}}/order/v1/verify?client_id={{client_id}} -``` - -`data.order` 对象结构见 [订单信息](/sdk/TapPayments/develop/server/#订单信息) - -``` -{ - "data": { - "order": {} - }, - "success": true -} -``` - -## Webhook 回调 - -:::tip -同样的通知可能会多次发送,商户系统必须正确处理重复通知。 - -推荐做法是:当商户系统收到通知时,首先进行签名验证,然后检查对应业务数据的状态,如未处理,则进行处理;如已处理,则直接返回成功。 - -在处理业务数据时建议采用数据锁进行并发控制,以避免可能出现的数据异常。 -::: - -### Webhook 说明 - -目前 Webhook 支持监听「充值成功」「退款成功」「退款失败」事件。对于「充值成功」建议采取主动核销订单,根据订单状态完成发货。 - -1. 需要依次进入 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > API 密钥**,检查是否有已生效的密钥,没有则需要 **添加新的密钥** -2. 需要依次进入 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > Webhooks 设置 > 添加**,添加有效的 **充值成功** URL。 - -### Webhook 请求 - -#### 服务 URL -- 由开发者提供,在 Webhooks 设置中添加 - -#### 请求方式 -- POST[application/json; charset=utf-8] - -#### 请求正文信息 - -| ** 参数名称 ** | ** 必填 ** | ** 格式 ** | ** 描述 ** | -| --------------| --------------| --------------| --------------| -| `order` | Y | object | 对象结构见 [订单信息](/sdk/TapPayments/develop/server/#订单信息) | -| `event_type` | Y | string | 事件枚举见 [Webhook的事件枚举](/sdk/TapPayments/develop/server/#webhook的事件枚举) | - -#### 请求签算 -- 详见 [请求规则和签算](/sdk/TapPayments/develop/server/#请求规则和签算) - -``` -curl -X POST \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - -H 'Content-Type: application/json; charset=utf-8' - -d '{"order":{},"event_type":"charge.succeeded"}' - {{your webhook url}} - -``` - -### Webhook 响应 - -``` -{ - "code": "SUCCESS", - "msg": "" -} -``` - -#### 字段描述 - -| **字段** | **类型** | **是否必须** | **描述** | -| -------- | -------- | ------------ | ---------------------------------------- | -| code | string | Y | 状态码,`SUCCESS` 为接收成功,`FAIL` 或其他均认为失败 | -| msg | string | N | 当接收失败时需返回失败原因 | - -## 请求规则和签算 - -### 请求 Headers - -| **Header** | **是否必须** | **描述** | -| ----------- | ------------ | ------------------------------------------------------------ | -| `X-Tap-Sign` | Y | 接口签算,详见 [签算](/sdk/TapPayments/develop/server/#签算) | -| `X-Tap-Ts` | Y | 请求方当前时间 unix timestamp | -| `X-Tap-Nonce` | Y | 随机数,需要大于等于 6 字节,小于等于 60 字节,每次请求需重新生成 | - -### 请求 Request - -#### 保留参数 - -所有 HTTP METHOD 必传,需要作为查询参数的一部分 - -| **Key** | **描述** | -| --------- | --------------- | -| client_id | 开发平台应用 ID | - -``` -https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}} -``` - -#### 当请求方法是 POST - -HTTP body 需使用 JSON 编码格式传输参数,即请求 headers 中应该携带 `Content-Type: application/json; charset=utf-8` - -``` -curl -X POST \ - -H 'X-Tap-Sign: {{signature}}' \ - -H 'X-Tap-Ts: {{unix timestamp}}' \ - -H 'X-Tap-Nonce: {{random nonce}}' \ - -H 'Content-Type: application/json; charset=utf-8' - -d '{"order_id":{{order_id}},"purchase_token":"{{purchase_token}}"}' - https://{{domain}}/order/v1/verify?client_id={{client_id}} -``` - -### 请求 Response - -#### 请求成功响应 - -``` -{ - "data": {}, - "now": 1640966400, - "success": true -} -``` - -#### 请求异常响应 - -``` -{ - "data": { - "code": 100004, - "msg": "NotFound: Unknown Error", - "error_description": "order not found" - }, - "now": 1640966400, - "success": false -} -``` - -#### 字段描述 - -| **字段** | **类型** | **是否必须** | **描述** | -| -------- | -------- | ------------ | --------------------------- | -| data | object | Y | 业务数据,或异常信息描述 | -| now | int | Y | 服务端时间 (unix timestamp) | -| success | bool | Y | 响应状态,`true` 为成功 | - -异常响应 `data` 字段描述 - -| **字段** | **类型** | **是否必须** | **描述** | -| ----------------- | -------- | ------------ |------------------------------------------------------------| -| code | int | Y | [错误码](/sdk/TapPayments/develop/server/#错误码) | -| msg | string | Y | 通用错误描述 | -| error_description | string | Y | 详细错误描述,辅助理解和解决发生的错误 | - - -### 签算 - -签算采用 HMAC-SHA256 算法 - -#### 密钥获取 -- 可在 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > API 密钥** 查看。 - -#### 签算说明 - -sign = HMAC.New(Sha256, "{{Server Secret}}").Hash(message) - -以下为 `message` 组成 - -``` -{method}\n -{url_path_and_query}\n -{headers}\n -{body}\n -``` - -**method:** 请求 HTTP 方法,如 GET、POST。 - -**url_path_and_query:** 完整请求路径及参数,如 /service/v1/method?client_id={{client_id}}&foo={{foo}}&bar={{bar}}&foo_bar={{foo_bar}} - -**headers:** 所有 `X-Tap-` 前缀的 headers 组合,将 keys 按 ASCII-code order 排序后以换行符 \n 为分隔符拼接。如 {key1}:{value1}\n{key2}:{value2}\n{key3}:{value3}。**为避免不同网络框架导致 keys 的排序结果不一致,签算时需要执行 key 转小写操作 key = tolower(key)** - -**body:** 请求体,如果请求体为空,则最后一行仅为一个 \n - -以下为请求体为空时的 `message` 组成 - -``` -{method}\n -{url_path_and_query}\n -{headers}\n -\n -``` - -:::tip -请求体 body,无需处理请求参数的顺序。建议用 String 接收 Webhook 请求的 RequestBody,验证请求签算后,再完成数据的反序列化 -::: - - - - -<> - -```java -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpClient; -import java.net.http.HttpResponse; -import java.util.*; -import java.util.stream.Collectors; -public class SignatureExample { - - public static String signRequest(String method, URI uri, String body, Map> headers, String secret) throws Exception { - String urlPathAndQueryPart = uri.getRawPath() + (uri.getRawQuery() != null ? "?" + uri.getRawQuery() : ""); - String headersPart = getHeadersPart(headers); - - // 包含请求体的签名字符串部分 - String signParts = method.toUpperCase() + "\n" + urlPathAndQueryPart + "\n" + headersPart + "\n" + body + "\n"; - - System.out.println("Sign Parts:\n" + signParts); - - Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); - SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256"); - sha256_HMAC.init(secretKey); - - byte[] hash = sha256_HMAC.doFinal(signParts.getBytes()); - return Base64.getEncoder().encodeToString(hash); - } - - private static String getHeadersPart(Map> headers) throws Exception { - TreeMap sortedHeaders = new TreeMap<>(); - headers.forEach((key, value) -> { - String lowerKey = key.toLowerCase(); - if (lowerKey.equals("x-tap-sign")) { - return; - } - if (lowerKey.startsWith("x-tap-")) { - if (value.size() > 1) { - throw new RuntimeException("Invalid header, " + lowerKey + " has multiple values"); - } - sortedHeaders.put(lowerKey, value.get(0)); - } - }); - - return sortedHeaders.entrySet().stream() - .map(entry -> entry.getKey() + ":" + entry.getValue()) - .collect(Collectors.joining("\n")); - } - - public static void main(String[] args) { - try { - String secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO"; - String body = "{\"event_type\":\"charge.succeeded\",\"order\":{\"order_id\":\"1790288650833465345\",\"purchase_token\":\"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=\",\"client_id\":\"o6nD4iNavjQj75zPQk\",\"open_id\":\"4+Axcl2RFgXbt6MZwdh++w==\",\"user_region\":\"US\",\"goods_open_id\":\"com.goods.open_id\",\"goods_name\":\"TestGoodsName\",\"status\":\"charge.succeeded\",\"amount\":\"19000000000\",\"currency\":\"USD\",\"create_time\":\"1716168000\",\"pay_time\":\"1716168000\",\"extra\":\"1111111111111111111\"}}"; - URI uri = new URI("https://example.com/my-service/v1/my-method"); - - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(uri) - .header("Content-Type", "Content-Type: application/json; charset=utf-8") - .header("X-Tap-Ts", "1716168000") - .header("X-Tap-Nonce", "V7v7zJ"); - - // Considering body to be added for a POST request - HttpRequest request = requestBuilder - .POST(HttpRequest.BodyPublishers.ofString(body)) - .build(); - - String method = "POST"; // Since we are using the POST method in this example - String signature = signRequest(method, uri, body, request.headers().map(), secret); - System.out.println("Signature: " + signature); - } catch (Exception e) { - e.printStackTrace(); - } - } -} -``` - - -<> - -```go -package main - -import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "fmt" - "io" - "net/http" - "sort" - "strings" -) - -func main() { - //nolint:gosec - secret := "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO" - body := []byte(`{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}`) - url := "https://example.com/my-service/v1/my-method" - method := "POST" - header := http.Header{ - "Content-Type": {"Content-Type: application/json; charset=utf-8"}, - "X-Tap-Ts": {"1716168000"}, - "X-Tap-Nonce": {"V7v7zJ"}, - } - ctx := context.Background() - req, _ := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(body)) - req.Header = header - sign, err := Sign(req, secret) - if err != nil { - panic(err) - } - req.Header.Set("X-Tap-Sign", sign) - fmt.Println(sign) -} - -// Sign signs the request. -func Sign(req *http.Request, secret string) (string, error) { - methodPart := req.Method - urlPathAndQueryPart := req.URL.RequestURI() - headersPart, err := getHeadersPart(req.Header) - if err != nil { - return "", err - } - bodyPart, err := io.ReadAll(req.Body) - if err != nil { - return "", err - } - signParts := methodPart + "\n" + urlPathAndQueryPart + "\n" + headersPart + "\n" + string(bodyPart) + "\n" - fmt.Println(signParts) - h := hmac.New(sha256.New, []byte(secret)) - h.Write([]byte(signParts)) - rawSign := h.Sum(nil) - sign := base64.StdEncoding.EncodeToString(rawSign) - return sign, nil -} - -// getHeadersPart returns the headers part of the request. -func getHeadersPart(header http.Header) (string, error) { - var headerKeys []string - for k, v := range header { - k = strings.ToLower(k) - if !strings.HasPrefix(k, "x-tap-") { - continue - } - if k == "x-tap-sign" { - continue - } - if len(v) > 1 { - return "", fmt.Errorf("invalid header, %q has multiple values", k) - } - headerKeys = append(headerKeys, k) - } - sort.Strings(headerKeys) - headers := make([]string, 0, len(headerKeys)) - for _, k := range headerKeys { - headers = append(headers, fmt.Sprintf("%s:%s", k, header.Get(k))) - } - return strings.Join(headers, "\n"), nil -} -``` - - - -<> - -```python -import base64 -import hashlib -import hmac -from typing import Dict, List -from urllib.parse import urlparse - -def sign_request(method: str, url: str, body: str, headers: Dict[str, List[str]], secret: str) -> str: - # 提取URL路径和查询字符串部分 - parsed_url = urlparse(url) - url_path_and_query = parsed_url.path + ('?' + parsed_url.query if parsed_url.query else '') - - # 获取符合条件的头部信息并排序 - headers_part = get_headers_part(headers) - - # 拼接签名字符串 - sign_parts = f"{method}\n{url_path_and_query}\n{headers_part}\n{body}\n" - - print("Sign Parts:\n", sign_parts) - - # 使用HMAC SHA256算法生成签名 - raw_sign = hmac.new(secret.encode(), sign_parts.encode(), hashlib.sha256).digest() - - # 对签名进行Base64编码 - sign = base64.b64encode(raw_sign).decode() - - return sign - -def get_headers_part(headers: Dict[str, List[str]]) -> str: - # 筛选和排序头部 - headers = {k.lower(): v for k, v in headers.items() if k.lower().startswith('x-tap-') and k.lower() != "x-tap-sign"} - header_keys = sorted(headers.keys()) - - # 组装头部字符串 - headers_str = '\n'.join(f"{k}:{headers[k][0]}" for k in header_keys if len(headers[k]) == 1) - - if any(len(headers[k]) > 1 for k in header_keys): - raise ValueError("Invalid header: has multiple values") - - return headers_str - -# 示例使用 -secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO" -url = "https://example.com/my-service/v1/my-method" -method = "POST" -body = '{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345",' \ - '"purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk",' \ - '"open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id",' \ - '"goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD",' \ - '"create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}' -headers = { - "Content-Type": ["Content-Type: application/json; charset=utf-8"], - "X-Tap-Ts": ["1716168000"], - "X-Tap-Nonce": ["V7v7zJ"], -} - -try: - sign = sign_request(method, url, body, headers, secret) - print("Signature: ", sign) -except Exception as e: - print("Error: ", str(e)) - -``` - - - -<> - -```php - $value) { - $key = strtolower($key); - if (count($value) > 1) { - throw new Exception("Multiple values for header: " . $key); - } - if ($key === "x-tap-sign") { - continue; - } - if (strpos($key, 'x-tap-') === 0) { - $signHeaders[$key] = $value; // Assuming each header has a single value - } - } - $headerKeys = []; - foreach ($signHeaders as $key => $value) { - if (!(strpos($key, 'x-tap-') === 0)) { - continue; - } - $headerKeys[] = $key; - } - sort($headerKeys); - $headerParts = []; - foreach ($headerKeys as $key) { - $headerParts[] = $key . ':' . $signHeaders[$key][0]; - } - return implode("\n", $headerParts); -} - -// 示例使用 -$secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO"; -$url = "https://example.com/my-service/v1/my-method"; -$method = "POST"; -$body = '{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}'; -$headers = [ - "Content-Type" => ["Content-Type: application/json; charset=utf-8"], - "X-Tap-Ts" => ["1716168000"], - "X-Tap-Nonce" => ["V7v7zJ"] -]; -try { - $sign = signRequest($method, $url, $body, $headers, $secret); - echo "Signature: " . $sign . "\n"; -} catch (Exception $e) { - echo "Error: " . $e->getMessage() . "\n"; -} - -``` - - - - - -上述示例签算结果 - -``` -# 参与签算部分 -POST\n -/my-service/v1/my-method\n -x-tap-nonce:V7v7zJ\n -x-tap-ts:1716168000\n -{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}\n - -# 签算结果 (X-Tap-Sign) -PyKQzlI65e0I9noVxcQc7FPU3nEyEFHKfRde65F6vhI= -``` - -## 通用对象结构描述 - -### 订单信息 - -| **参数** | **类型** | **是否必须** | **描述** | -| -------------- | -------- | ------------ | ------------------------------------------------------------ | -| order_id | string | Y | 订单唯一 ID | -| purchase_token | string | Y | 用于订单核销的 token | -| client_id | string | Y | 应用的 Client ID | -| open_id | string | Y | 用户的开放平台 ID | -| user_region | string | Y | [用户地区](/sdk/TapPayments/appendix/regions-currencies) | -| goods_open_id | string | Y | 商品唯一 ID | -| goods_name | string | Y | 商品名称 | -| status | string | Y | [订单状态](/sdk/TapPayments/develop/server/#订单状态) | -| amount | string | Y | 金额(本币金额 x 1,000,000) | -| currency | string | Y | [币种](/sdk/TapPayments/appendix/regions-currencies) | -| create_time | string | Y | 创建时间 | -| pay_time | string | Y | 支付时间 | -| extra | string | Y | 商户自定义数据,如角色信息等,长度不超过 255 UTF-8 字符 | - -#### 订单状态 - -| **订单状态** | **描述** | -| ---------------- | ------------ | -| `charge.pending` | 待支付 | -| `charge.succeeded` | 支付成功 | -| `charge.confirmed` | 已核销 | -| `charge.overdue` | 支付超时关闭 | -| `refund.pending` | 退款中 | -| `refund.succeeded` | 退款成功 | -| `refund.failed` | 退款失败 | -| `refund.rejected` | 退款被拒绝 | - -### Webhook的事件枚举 - -| **event_type** | **描述** | -| ---------------- | -------- | -| `charge.succeeded` | 充值成功 | -| `refund.succeeded` | 退款成功 | -| `refund.failed` | 退款失败 | - -## 错误码 - -| **code** | **描述** | -| -------- | ------------ | -| `-1` | 非法请求 | -| `100000` | 支付服务异常 | -| `100004` | 订单不存在 | -| `100018` | 订单验证错误 | diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/develop/unity.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/develop/unity.mdx deleted file mode 100644 index 6a043270a..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/develop/unity.mdx +++ /dev/null @@ -1,252 +0,0 @@ ---- -title: Unity 集成指南 -sidebar_position: 1 ---- - - -import MultiLang from "/src/docComponents/MultiLang"; -import sdkVersions from '/src/docComponents/sdkVersions'; -import CodeBlock from '@theme/CodeBlock'; - - -## 环境要求 - -- 支持 Unity 2019.4 及以上版本 - -## 准备 - -- 参照 [准备工作](/sdk/start/get-ready/) 所述创建 app,配置 app 参数并且绑定 API 域名 -- 参照 [TapSDK 快速开始](/sdk/start/quickstart/) 配置包名和签名 - -## 获取 SDK - -:::tip -Unity 2020.3.15 之前的版本为了避免后面构建报错建议升级 Gradle 版本,可参考该 [Gradle 升级步骤文档](/sdk/TapPayments/develop/faq/#1unity-2020315-之前的版本升级-gradle-版本的操作步骤)。 -::: - -以下介绍了**通过 Unity Package Manager 导入**或者**修改 Packages/manifest.json 配置文件引入**两种方式,根据项目需要,任选其一即可: - -### 1、通过 Package Manager 可视化界面引入 - -因为 TapPayment SDK 依赖 **EDM4U(External Dependency Manager for Unity)** 来处理 Android 相关的依赖,因此引入依赖的步骤是**先安装 EDM4U 库,然后安装 TapPayment SDK**。 - -#### 安装 EDM4U - -下面介绍了两种方式安装 EDM4U,分别是通过 **OpenUPM 安装**以及**手动下载安装**; - -- **方法一:通过 OpenUPM 安装 EDM4U** - -EDM4U 可以通过 OpenUPM 进行下载,开发者可以通过 **Edit > Project Settings > Package Manager** 来注册使用 OpenUPM。 - -![](https://capacity-files.lcfile.com/gpS8BcTVJIdSpMxyDipIWORX4Gb2filA/NdoQbmRRvovet4x3LGUcsTXWnjb.png) - -- **方法二:手动下载安装** - -开发者可以通过 [Google APIs for Unity](https://developers.google.com/unity/archive#external_dependency_manager_for_unity) 下载 UPM 安装包. 并且通过[从本地磁盘安装](https://docs.unity3d.com/Manual/upm-ui-local.html)的方式进行安装。 - -#### 安装 TapPayment SDK - -TapPayment SDK 可以通过 NPMJS 进行安装, 开发者可以通过 **Edit > Project Settings > Package Manager** 来注册使用 NPMJS。 - -![](https://capacity-files.lcfile.com/sJDhXK4vAwAYX7BpQFh1SrQzeUmXk165/taptap.png) - -<> - 配置完成以后就可以在 Window > Package Manager > My Registries 中安装 TapTap Payments Global V2 ({sdkVersions.tapGlobalPayments.unity})。 - 如果安装目录中没有 TapTap Payments Global V2, 请尝试在 Edit > Project Settings > Package Manager 中重新注册 NPMJS。 - - -![](https://img.tapimg.com/market/images/4900806ff9290049ee68aa67ee05b71b.png) - - -### 2、修改 Packages/manifest.json 文件 - - - {` "dependencies": { - "com.taptap.tds.payments.global.v2": "${sdkVersions.tapGlobalPayments.unity}", //添加 TapPayment - "com.unity.purchasing": "3.1.0", //TapPayment 所须依赖 - "com.google.external-dependency-manager": "1.2.179", //TapPayment 所须依赖 - "com.taptap.tds.common":"3.28.3", - "com.taptap.tds.login":"3.28.3", - ... - ... - }, - - // 添加 Registries - "scopedRegistries": [ - { - "name": "taptap", - "url": "https://registry.npmjs.org", - "scopes": ["com.tapsdk", "com.taptap", "com.leancloud"] - }, - { - "name": "openupm", - "url": "https://package.openupm.com", - "scopes": [ - "com.google" - ] - } - ]`} - - - -:::tip - -**处理安卓依赖失败** - -EDM4U 会自动监控 TapPayment SDK 引入的 Android 依赖,但是在某些特殊情况下自动依赖解析可能失败,开发者可以通过 **Assets > External Dependency Manager > Android Resolver > Force Resolve** 来强制依赖解析。在测试之前请确认对于 `com.taptap.android.payment:unity` 的依赖被加到了 mainTemplate.gradle 文件中,如果 EDM4U 没有自动添加这个依赖,开发者也可以通过手动添加的方式处理依赖。 -::: - -## SDK 指南 - -我们在 [Unity IAP](https://unity.com/products/iap) 的框架下实现了一个新的商店实现,通过这种方式降低已经有 Google Play 或者 App Store 内购的 app 的接入成本。如果开发者是第一次接触到内购的流程,我们会在接下来简要说明 Unity 的内购流程,开发者也可以在 Unity 的[官方文档](https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/manual/Overview.html) 中了解有关内购的详细内容。 - -### 初始化 Unity Gaming Services - -调用 `UnityServices.InitializeAsync()` 来一次性初始化 Unity Gaming Services。 这个方法会返回一个 `Task` 对象,开发者可以通过这个对象来获取初始化的状态信息。 - -```cs -using System; -using Unity.Services.Core; -using Unity.Services.Core.Environments; -using UnityEngine; - -public class InitializeUnityServices : MonoBehaviour -{ - public string environment = "production"; - - async void Start() - { - try - { - var options = new InitializationOptions() - .SetEnvironmentName(environment); - - await UnityServices.InitializeAsync(options); - } - catch (Exception exception) - { - // An error occurred during services initialization. - } - } -} -``` - -### 初始化 IAP - -确保你在初始化 IAP 的时候添加 `TapPurchasingModule`,并且设置正确的 `clientId` 和 `clientToken`。 - -```cs -using UnityEngine; -using UnityEngine.Purchasing; -using TapTap.Payments.Global.V2; - -public class MyIAPManager : IStoreListener { - - private IStoreController controller; - private IExtensionProvider extensions; - - public MyIAPManager () { - var builder = ConfigurationBuilder.Instance(TapPurchasingModule.Instance); - builder.Configure().SetClientId("Your Client ID Here"); - builder.Configure().SetClientToken("Your Client Token Here"); - builder.AddProduct("100_gold_coins", ProductType.Consumable, new IDs - { - {"100_gold_coins_google", GooglePlay.Name}, - {"100_gold_coins_mac", MacAppStore.Name} - }); - - UnityPurchasing.Initialize (this, builder); - } - - /// - /// Called when Unity IAP is ready to make purchases. - /// - public void OnInitialized (IStoreController controller, IExtensionProvider extensions) - { - this.controller = controller; - this.extensions = extensions; - } - - /// - /// Called when Unity IAP encounters an unrecoverable initialization error. - /// - /// Note that this will not be called if Internet is unavailable; Unity IAP - /// will attempt initialization until it becomes available. - /// - public void OnInitializeFailed (InitializationFailureReason error) - { - } - - public void OnInitializeFailed(InitializationFailureReason error, string str) - { - } - - - /// - /// Called when a purchase completes. - /// - /// May be called at any time after OnInitialized(). - /// - public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e) - { - return PurchaseProcessingResult.Complete; - } - - /// - /// Called when a purchase fails. - /// - public void OnPurchaseFailed (Product i, PurchaseFailureReason p) - { - } -} -``` - -### 发起内购流程 - -在 IAP 被成功初始化以后,你可以通过调用 `IStoreController.InitiatePurchase` 来发起内购流程。 - -```cs -// Example method called when the user taps a 'buy' button -// to start the purchase process. -public void OnPurchaseClicked(string productId) { - controller.InitiatePurchase(productId); -} -``` - -### 处理购买结果 - -购买完成时会调用商店监听器的 ProcessPurchase 函数。无论用户购买任何物品,您的应用程序都应该履单;例如,解锁本地内容或将购买收据发送给服务器以更新服务器端游戏模型。 - -此过程会返回结果以指出应用程序是否已完成对购买的处理: - -| 结果 | 描述 | -| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| PurchaseProcessingResult.Complete | 应用程序已完成对购买的处理,不应再次向应用程序通知此事。 | -| PurchaseProcessingResult.Pending | 应用程序仍在处理购买,除非调用 `IStoreController` 的 `ConfirmPendingPurchase` 函数,否则将在下一次应用程序启动时再次调用 `ProcessPurchase`。 | - -请注意,在初始化成功后,随时可能调用 `ProcessPurchase`。如果应用程序在 `ProcessPurchase` 处理程序执行过程中崩溃,那么在 Unity IAP 下次初始化时会再次调用它,因此您可能希望实现自己的额外重复数据删除功能。 - -#### 可靠性 - -Unity IAP 要求明确确认购买以确保在网络中断或应用程序崩溃的情况下可靠地完成购买。在应用程序离线时完成的任何购买都将在下次初始化时发送给应用程序。 - -#### 立即完成购买 - -返回 `PurchaseProcessingResult.Complete` 时,Unity IAP 立即完成交易(如下图所示)。 - -如果您正在销售可消耗商品并从服务器履行订单(例如,在网络游戏中提供游戏币),那么您不得返回 `PurchaseProcessingResult.Complete`。 - -否则,如果在保存到云端之前卸载应用程序,则购买的消耗品将面临丢失的风险。 - -![](https://capacity-files.lcfile.com/PYPIumvCKtaAwhJOPAf1qaApNMOdQ9nF/VrHVbfdKUoCHgXxmOYkccZLMn8e.png) - -#### 将购买保存到云端 - -如果要将消耗品购买交易保存到云端,您必须返回 `PurchaseProcessingResult.Pending`,并且仅在成功保存购买时才调用 `ConfirmPendingPurchase`。 - -返回 `Pending` 时,Unity IAP 会在底层商店中保持交易为未结 (open) 状态,直至确认为已处理为止,因此确保了即使在消耗品处于此待处理状态时用户重新安装您的应用程序,消耗品购买交易也不会丢失。 - -![](https://capacity-files.lcfile.com/zDX7oMjWBju6ME0K9i4zym9OOuIgCvrx/TFgMbu4JhohID1xHBnhcet7nnth.png) - -## 调试 -**我们当前只提供了 Android 的库实现, 请在 Android 环境下进行各功能调试**(其他平台会逐步补充完善)。 \ No newline at end of file diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/orders-refunds.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/orders-refunds.mdx deleted file mode 100644 index c58d21e15..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/orders-refunds.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: 管理订单及处理退款 -sidebar_position: 4 ---- - - - - - -你可以通过 TapTap 开发者服务中心的「游戏服务」-「TapTap Payments」-「商品与订单」-「交易列表」来查看游戏内购买商品(IAP)的销售订单,并为用户办理退款请求。 - -![tappay](https://capacity-files.lcfile.com/8WMyxhKLhNA1SniirMSVSOt56kS2z2wb/tappay-orders.png) - -## 查找订单 - -TapTap Payments 支持多种查询方式: - -* __按用户标识查询__:输入用户的 TapTap ID,即可查询到当前用户购买的所有订单; - -* __按订单号查询__:用户在游戏内购买时创建的 TapTap 订单号; - -* __按交易流水号查询__:开发者自定义的订单 ID; - -* __按订单状态查询__:根据订单交易状态查询,包括“支付中”、“已支付”、“已验证”、“已过期”、“已退款”等; - -* __按订单时间查询__:筛选用户订单发生的日期查询; - -## 订单状态 - -订单页面会显示每一笔用户购买的状态,如下: - -| 订单支付状态 | 说明 | -| ---- | ---- | -| 支付中 | 用户还未完成付款,或系统正在处理订单 | -| 订单已支付 | 用户完成付款,系统已成功向用户收款 | -| 订单已过期 | 订单长时间未进行付款,或付款已取消 | - -| 订单退款状态 | 说明 | -| ---- | ---- | -| 订单退款中 | 用户的退款申请已被允许,系统正在处理退款 | -| 订单已退款 | 系统已全额退款至用户支付账户 | -| 订单已驳回 | 用户的退款申请不被允许 | -| 订单退款失败 | 系统退款不成功,或用户的付款方式遭到拒绝 | - -## 办理退款 - -为了保障和维护用户的合理权益,TapTap 为用户提供退款的服务。当用户向 TapTap 发起退款申请时, TapTap 会向开发者收集用户消费及物品消耗的材料,用于是否允许退款的决策判断。你可在「游戏服务」-「TapTap Payments」-「处理退款」查看到用户提交的退款请求: - -* __退款__:同意 TapTap 全额退款给用户,系统将用户支付金额全部退回至用户的付款账户。退款后,你有责任告知用户对游戏内商品的处理方式。 - -* __驳回__:不同意 TapTap 退款给用户,你必须向 TapTap 和用户说明不允许退款的合理原因,或出示用户已消费的相关凭证。 - -如若开发者消极处理或不予处理,TapTap 拥有最终决策处理权。 diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/overview.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/overview.mdx deleted file mode 100644 index 936668ef4..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/overview.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: 概述 -sidebar_position: 1 ---- - -# 概述 - -## **什么是 TapTap Payments?** - -TapTap Payments 是一项付款服务,利用该服务可以在 TapTap 上架的游戏中销售各种商品 - -- **触达全球受众:**我们提供多种[全球主流支付方式](/sdk/TapPayments/appendix/payment-methods/),覆盖 [173 个国家和地区](/sdk/TapPayments/appendix/regions-currencies),支持 42 种货币,触达 TapTap 千万硬核玩家 -- **88%/12% 收入分成:**获得 88% 的收入分成,更具竞争力的价格,释放全球收入潜力 -- **流畅接入:** 提供简洁、易用、稳定的 SDK,让开发者快速接入支付功能,节省开发时间和成本 -- **全球合规:**经验丰富的全球财税法经验,符合各地合规标准,让您专注于游戏开发与变现 - -通过在你您的游戏中集成 TapTap Payments,您可以销售两种不同类型的游戏内商品: - -- **消耗型商品:**消耗型商品是指当用户消耗该商品时,游戏会分配相关联的游戏内容,而用户随后可以再次重复购买的商品。例如金币、道具等。 -- **非消耗型商品:**非消耗型商品是指购买一次便能永久使用的商品。例如付费升级和关卡包等。 - -## **销售游戏内商品的三个基本步骤** - -**第一步 申请商业卖家身份** - -在您可以对您的游戏内商品收费之前,需要通过我们的商务伙伴,申请商业卖家身份并注册您的付款信息,完成审核后将为您开通相关功能 - -**第二步 使用我们的 SDK 开发您的游戏 ** - -下载 TapTap Payments SDK 并将其集成到您的游戏中,我们提供了 Unity 和 Android 的集成选项: - -- [Unity 集成指南](/sdk/TapPayments/develop/unity) -- [Android 集成指南](/sdk/TapPayments/develop/android) - -**第三步 为您的游戏添加商品** - -在 [TapTap 开发者中心](https://developer.taptap.io/)添加商品并注册相关信息:项目 ID、项目标题、项目类型、价格等。进一步了解: - -- [在开发者中心管理商品](/sdk/TapPayments/product-management) diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/payout.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/payout.mdx deleted file mode 100644 index def5cc958..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/payout.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: TapTap 付款 -sidebar_position: 5 ---- - - - - - -TapTap 付款仅用于 TapTap Payments 服务中,按照一定周期的付款时间为开发者结算收益,并付款至开发者的财务账户。在准备接收 TapTap 的付款之前,请先确认已完成财务付款资料: - -1. TapTap 开发者服务中心 -「财务与管理」-「财务主体」; - -2. 填写主体信息与财务账户信息; - -## 付款周期和报告 - -TapTap 的付款周期通常为自然月结束后的 45 天,例如自然月 1 月份( 1 月 1 日 - 1 月 31 日)的收入,会在 1 月 31 日结束之后,进入核算阶段,并在此后 45 天内完成付款,即 3 月 15 日是 TapTap 的最晚付款时间。请注意你的银行可能需要几天的时间才能将相应款项计入你的账户。 - -在进入核算阶段后,你可在「财务与管理」-「对账与结算」看到需要结算的报告单,包括在当前账期内,商品销售金额、用户退款金额、费用扣除、实际收益金额等数据。你必须对报告单进行仔细的核对,并在 7 个工作日之内与 TapTap 确认最终数目。 - -![tappay](https://capacity-files.lcfile.com/kmsrDieltrX4CXHUwcIwfMlrWLtuxsN6/tappay-dc-bill.png) - -## 状态说明 - -TapTap 根据游戏销售金额,退款、当地税费、支付渠道手续费等进行核对结算,计算出开发者所得金额。核算周期为每月一次,在每个自然月结束之后进入核算阶段。 - -* __待核算__:等待 TapTap 核算完成。 - -* __待确认__:TapTap 已经核算完成的账期,需要开发者确认核算结款金额。 - -* __待付款__:等待 TapTap 付款。 - -* __已付款__:TapTap 已将账单开发者实际收益金额付款至开发者预留的银行账户。 - -## 付款最低限额 -TapTap 付款的最低金额是 100 美元,如果你在上个月度的销售额没有超过 100 美元,则会累计到下个月度一并发放。如果你的销售额超过了 100 美元,但出于对服务费或跨境税的考虑,你也可以向 TapTap 申请累计到一定额度后付款。 - -## 付款货币 -TapTap 统一以“美元”进行付款。付款时由于接收地区或银行的不同,可能会产生一些跨境费用或转账手续费用,请联系你所在银行查看。 - -## TapTap 的服务费 -开发者使用 TapTap Payments 服务,TapTap 会针对上述内容收取服务费用。 diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/product-management.mdx b/.ci/hk/zh-Hans/sdk/TapPayments/product-management.mdx deleted file mode 100644 index 8978f96fc..000000000 --- a/.ci/hk/zh-Hans/sdk/TapPayments/product-management.mdx +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: 商品管理 -sidebar_position: 3 ---- - - - - - -若要在游戏中提供内购商品,必须在 TapTap 开发者服务中心录入内购商品信息,并添加到你的游戏内。每个商品须关联一个游戏应用(Client),并且只能用于该游戏。在使用过程中,你可以创建或删除商品,也可以修改完善现有商品的信息。 - -## 创建游戏内商品 - -在创建商品之前,请务必仔细规划您的产品 ID。商品 ID 对于您的游戏来说必须是唯一的,并且在创建后不能更改或重复使用。 - -- 商品 ID 必须以数字或字母开头,长度不超过 30 个字符,可以包含数字 (0-9)、英文字母 (a-z/A-Z)、下划线 (_) 和句点 (.) -- 创建商品后,您无法更改或重复使用商品 ID - -### 创建单个商品 - -1. 前往 TapTap 开发者服务中心,选择「游戏服务」-「TapTap Payments」-「商品与订单」; - -2. 点击「添加商品」,在弹出的表单中填写您的商品详细信息; - - - 商品类型:消耗型和非消耗型商品,详情请参见[商品类型](/sdk/TapPayments/overview) - - 商品 ID:商品的唯一标识,创建后不可更改和重复使用; - - 商品名称:商品的短名称,用户可见。建议保持在 80 个字符以内; - - 商品描述:对商品内容的细节描述,建议保持在 100 个字符以内; - - 商品定价:期望售卖的价格,默认以美元定价,开发者可下载所有定价等级查看不同国家的价格; - -3. 完成信息填写后,点击「提交审核」并等待审核结果; - -4. 审核通过后,如需商品生效,可点击商品对应操作列的「上架」按钮即可; - - -> 注意同一个游戏内商品 ID 不能重复,提交后将不可更改,删除后也无法重复使用,请仔细规划。商品在未上架状态时均可修改名称、描述、价格等信息,上架后的商品不可修改信息。具体见「编辑/修改商品」。 - - -![mahua](https://capacity-files.lcfile.com/GSpWNUzJ025ypkp42jD180Ko7u9wDiUA/add_sku.png) - -## 批量创建多个商品 - -1. 如需同时创建多个游戏内购商品,您可以选择使用「批量导入」功能,上传包含每个商品详细信息的 CSV 文件; - -2. 前往 TapTap 开发者服务中心,选择「游戏服务」-「TapTap Payments」-「商品与订单」; - -3. 点击「批量导入」: - - - 下载模版; - - 上传商品详细信息 CSV 文件; - - 请确保每个商品都为单独一行,单次上传不得超过 100 个商品,下载模板; - - 当商品详细信息 CSV 中的商品 ID 与后台列表中现有商品 ID 匹配时,此次商品详细信息 CSV 中的商品不被上传; - -> 商品详细信息 CSV 文件上传成功后,系统将直接提交审核,若上传了系统不支持的语言,则会忽略该语言内容并由默认语言替代。 - -![tappay](https://capacity-files.lcfile.com/6GwDJdk5zVDaVp1znwrPRUEx1TcmjQC5/add_sku_batch.png) - -## 编辑/修改商品 - -| 商品状态 | 编辑与操作 | -| ---- | ---- | -| 审核通过,待上架 | 可修改商品名称、商品说明 / 可上架 / 可删除 | -| 审核失败 | 可修改商品名称、商品说明 / 可删除 | -| 已上架 | 可修改商品名称、商品说明 / 可下架 | -| 已下架 | 可删除 | - -## 商品状态说明 - -* __审核中__:商品在发布之前需要提交审核,审核通过则进入待上架状态,审核失败会给出失败原因,需要根据失败原因进行修改后重新提交; - -* __审核通过__:审核通过的商品,处于待上架状态,可随时发布上架,上架的商品才会在生产环境生效; - -* __已上架__:上架后的商品,会在生产环境生效; - -* __已下架__:下架的商品,无法在生产环境使用。如用户点击该商品后提示“该商品已下架”; - -* __已删除__:开发者可以删除商品,删除的商品不可再复用; - -## 语言和本地化 - -如果你的游戏会在不同的国家/地区发布,那么也建议为你的商品添加本地化信息。通过添加不同地区的语言完成本地化信息的录入。 - -![tappay](https://capacity-files.lcfile.com/RuxW5n9niJRDFIVVAa2uOURPHbnpgx4F/product-multilingual.png) diff --git a/.ci/hk/zh-Hans/sdk/embedded-moments/features.mdx b/.ci/hk/zh-Hans/sdk/embedded-moments/features.mdx deleted file mode 100644 index 7819ec457..000000000 --- a/.ci/hk/zh-Hans/sdk/embedded-moments/features.mdx +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: 内嵌动态功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -## 产品介绍 - -玩家可以在游戏内进入 TapTap 的社区论坛,查看攻略资讯,分享自己的游戏精彩瞬间,也可以参与其他玩家、官方和大神之间的互动。 - -## 核心优势 - -对于游戏开发者: - -- 内容生产:可以通过一键分享,引导玩家分享自己在游戏内的内容 -- 内容触达:官方发布的内容会进入玩家的关注流,可在内嵌动态为玩家精准分发内容 -- 内容反馈:玩家发布的内容可以及时反馈和给予帮助解答 - -对于游戏玩家: - -- 社交需求:在游戏过程中,能与其他玩家进行延时性的交流和互动 -- 帮助决策:在游戏场景中,遇到困难,能快速查到游戏攻略和大神解说 - -## 账号体系 - -当玩家在内嵌动态内发布动态或者进行互动时,需要进行 TapTap 账号登录,故此开发者需要接入 **[TapTap 登录](/sdk/taptap-login/features/)**。 - -![](https://capacity-files.lcfile.com/8C2DjigrigAz03drPAO3U49BcntTMhNz/login.png) - -## 动态功能介绍 - -### 推荐流 - -玩家可以直接在「游戏」模块中浏览优质帖子。 - -- **运营位**:运营位可以帮助开发者展示重要的信息和活动,是内嵌动态独有的模块,该模块可在「游戏服务-内嵌动态-运营位配置」中编辑,过审后即可在内嵌动态中展示。 -- **推荐流**:玩家打开内嵌动态时默认查看推荐流。 - -![](https://capacity-files.lcfile.com/vLkyj72lfbsuh1sv7fQOsrNFrsSEHJvp/Games.png) - -**发布动态**:玩家可以在论坛内发布图文动态,和发布视频动态。 - -![](https://capacity-files.lcfile.com/E9WodoFbRgquQaGG9rML5V4K9eUwYSqq/take_post.png) - -**社区互动**:玩家可以点赞、评论、转发其他玩家的动态。 - -![](https://capacity-files.lcfile.com/NFiIX2c86mODiRJeLEk5Qw9F0zimdzPD/reply.png) - -### 关注流 - -已登录 TapTap 的用户,可在此查看自己的动态和 TapTap 上关注的好友发布的动态。当有新发布的内容时,「关注」的导航栏上会有红点提醒,让玩家不会错过重要发布的内容。 - -![](https://capacity-files.lcfile.com/dY9bMBXUxolz6FL3fDO9cggQ1cFo7tjN/Following.png) - -### 消息页 - -可以查询到最新消息,玩家之间的互动会触发通知,有助于玩家之间更好地建立联系和沉淀关系。 - -![](https://capacity-files.lcfile.com/Uym4pMC7RV09OKcYdXRuFBdmJqhyAz3k/Notification.png) - -### 个人页 - -玩家可以在「我的」页面找到自己发布过的动态,可以再进行外部分享和内容删除。 - -![](https://capacity-files.lcfile.com/fACDedkGs1dbMAgFr2jyVU7w0k4fWCik/Me-global.png) - -### 搜索页 - -玩家可以在内嵌动态中搜索自己想要的内容,同时也会保留搜索记录和为玩家推荐优质的推荐搜索。 - -![](https://capacity-files.lcfile.com/1NB8Gzr77x8Xiybw6qd7EF8WaWOUuUiz/search.png) - -## SDK 功能介绍 - -### 场景化入口 - -开发者可以在游戏内某一场景处绘制入口,然后在开发者中心后台配置玩家点击按钮后的落地页,帮助玩家在游戏内遇到困难和问题时给予决策辅助。 - -![](https://capacity-files.lcfile.com/ofMVqjfGnTpyvuYmesid6JdraU6pHIHX/FlashPartyHeroGuide.png) - -:::tip - -1、入口样式最好结合游戏场景去绘制,TDS 不提供样式规范,目的是为了让玩家进入时不会有违和感 - -2、跳转的落地页可以配置成某一帖子,根据开发者自身诉求自定义配置 - -::: - -### 入口红点 - -开发者可以在游戏内放置内嵌动态的入口,红点可以引导玩家进入内嵌动态里。 - -![](https://capacity-files.lcfile.com/ppMOzFxF2KRvfSbpt4bhPzfxuRUwGl51/Badges.png) - -:::tip - -1、红点对提升使用率至关重要,建议将入口放在游戏内显眼的位置 - -2、入口红点和内嵌动态内「关注」的红点逻辑是一致的,玩家关注的用户发布了新内容将触发消息通知,获取新消息间隔为 1 分钟一次(1 分钟是最小单位,轮询时长开发者可自行调整为 3 分钟、5 分钟等) - -3、当玩家打开内嵌动态后,游戏需要清除小红点展示,再继续下一次轮询请求来展示红点 - -::: - -### 一键发布 - -游戏内任意场景需要截图分享的,TDS 提供一键发布到内嵌动态内,仅支持图文动态发布。 - -![](https://capacity-files.lcfile.com/qQu2WSd7lft6N2Ga62MKhbEkMkU9JhXK/share_data.gif) - -### 动态关闭引导弹窗 - -若需要玩家立即退出内嵌动态回到游戏内及时响应的(如即时对战),开发者可以自定义弹窗引导和弹窗触发的节点。 - -![](https://capacity-files.lcfile.com/hBYaymebMR3iwSdFig6W53Gm2LHwzf6h/ClosingEmbeddedMoments.png) - -## 后台功能介绍 - -### 主题配置 - -为了更好的结合游戏场景,让玩家不会有割裂感,TDS 提供开发者自定义配置内嵌状态的样式主题,你可以在「游戏服务」-「内嵌动态」-「主题配置」中上传背景图片和设置字体配色。 - -![](https://capacity-files.lcfile.com/cKlYPD05CODMXMbG6ygmxPhPpiHBGVeE/ThemeConfiguration.png) - -如果游戏仅支持横屏或者竖屏,只需要上传一张即可,若是支持转屏则需要上传两张 -图片是需要进行人工审核的,一般会在 2 个工作日内完成 - -### 运营位配置 - -为了更好帮助游戏进行活动运营,TDS 提供开发者自定义配置内嵌状态的运营位,你可以在「游戏服务」-「内嵌动态」-「运营位配置」中新增运营位,需要提供标题、图片 Banner、和链接。 - -![](https://capacity-files.lcfile.com/uMGtukhn1wQQmLmJ0icNK6eJALcW7Yae/BannerConfiguration.png) - -最多同时配置 5 个运营位,TDS 对跳转链接域名不做限制 -运营位是需要进行人工审核的,一般会在当天内审核完 - -### 场景化入口配置 - -场景化入口可以在「游戏服务」-「内嵌动态」-「场景化入口配置」中创建,开发者提交入口名称、落地页类型、落地页后,生成的入口 ID 可以在游戏内使用。该模块是不需要审核的,开发者可以自由变更跳转路径。 - -![](https://capacity-files.lcfile.com/VATMXxjDD1U1OihW705a7BpuQgFfL1b4/Scenario-BasedPortalConfiguration.png) diff --git a/.ci/hk/zh-Hans/sdk/push/guide/android-mixpush.mdx b/.ci/hk/zh-Hans/sdk/push/guide/android-mixpush.mdx deleted file mode 100644 index 70024a4da..000000000 --- a/.ci/hk/zh-Hans/sdk/push/guide/android-mixpush.mdx +++ /dev/null @@ -1,221 +0,0 @@ ---- -title: FCM 推送指南 -sidebar_label: FCM 推送 -sidebar_position: 4 -slug: /sdk/push/guide/android-mixpush/ ---- - - - - - -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; - -## FCM 推送概述 - -自 Android 8.0 之后,系统权限控制越来越严,第三方推送通道的生命周期受到较大限制。为此,我们推出 FCM 推送方案。这保障了主流 Android 系统上的推送到达率。 - -在 FCM 推送方案里,消息下发时使用的通道不再是我们自己维持的 WebSocket 长连接,而是借用 FCM 进行通信。 -一条推送消息下发的步骤如下: - -1. 开发者调用云服务 Push API 请求对全部或特定设备进行推送; -2. 云推送服务端将请求转发给 FCM; -3. FCM 通过手机端的系统通道下发推送消息,同时手机端系统消息接收器将推送消息展示到通知栏; -4. 终端用户点击消息之后唤起目标应用或者页面。 - -整个流程与苹果的 APNs 推送类似,SDK 在客户端基本不会得到调用。 - -Android FCM 推送功能仅对商用版应用开放,如果希望使用该功能,请进入 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > FCM**,打开 FCM 推送的开关。 - -注意,FCM 推送可以随时按需开关。当该选项关闭后,下一次 Android 推送会与普通推送一样自动选择自有通道送达客户端,除了会再次遇到上面提到的自有通道在部分 ROM 上会受到限制的问题之外,不会有别的影响。而当该选项再次开启后,Android 推送又会去选择厂商推送渠道。 - -开启了 FCM 推送之后,Installation 表中每一个设备对应的记录,会增加 `registrationId` 字段,用于记录厂商分配的注册 id(类似于 APNs 的 device token),同时还会增加一个 `vendor` 字段(如果没有这一字段,则说明客户端集成有问题),其值为 `fcm`。 - -### 推送提醒的红点或角标展示 - -Android 系统默认支持根据 FCM 推送数量展示红点(桌面应用图标上显示)和角标(应用图标的长按菜单中显示)。 - -### 通知栏消息与透传消息 - -当应用在前台时,FCM 支持透传消息给应用。 - -### 即时通讯的离线推送 - -在即时通讯服务中,在 iOS 平台上如果用户下线,是可以启动离线消息推送机制的,对于 Android 用户来说,如果只是使用云推送自有通道,那么是不存在离线推送的,因为聊天和推送共享同一条 WebSocket 长链接,在即时通讯服务中用户下线了的话,那么推送也必然是不可达的。但是如果启用了 FCM 推送,因为推送消息走的是 FCM,这一点和 iOS 基本一致,所以这时候 Android 用户就存在离线推送的通知路径了。 -也就是说,如果开启了 FCM 推送,那么即时通讯里面的离线推送和静音机制,对使用了 FCM 推送的 Android 用户也是有效的。 - -### 受限说明 - -推送消息长度限制: - -- 消息中的应用包名最大支持 128 字节,消息内容最大支持 4KB。 - -最低 Android 版本要求: - -- FCM 推送支持 Android 4.1 或以上版本的手机系统(minSdkVersion:16)。 - -影响送达率的因素说明: - -- 终端设备是否在线。如果设备离线,推送服务会缓存消息,待设备上线后,再将消息推送给设备。 -- 终端设备上集成推送服务 SDK 的应用是否被卸载。 -- 终端设备的网络状况是否稳定。 -- 终端设备的安全控制策略。 -- 透传消息的送达受 Android 系统和应用是否驻留在后台影响。 - -## 推荐的接入方式 - -FCM 推送本质上还是依赖于 FCM 的 SDK 和服务端能力,我们的客户端 SDK 只是对 FCM SDK 的包装,而实际的推送请求也是通过 LeanCloud 中转之后发送到 FCM 后台。我们的客户端 SDK 更新速度可能跟不上 FCM 的迭代速度,因此建议大家直接对接 FCM SDK,然后在客户端把 FCM 分配的「注册 id」与 FCM 标识(见上一章 vendor 的说明)保存到设备信息(`Installation`)中,这样之后一样可以通过我们的推送 API 来给所有设备正确发送推送信息。 - -### 客户端接入方法 - -开发者从 `FirebaseMessagingService` 继承自己的实现类,然后在 `onNewToken` 回调函数中调用如上例代码进行保存(记得将 `vendor` 换成 `fcm`)。示例代码可以参考 [LCFirebaseMessagingService](https://github.com/leancloud/java-unified-sdk/blob/master/android-sdk/leancloud-fcm/src/main/java/cn/leancloud/LCFirebaseMessagingService.java#L69)。 - -### 发送 FCM 推送的服务端 API - -可以参考这里的说明来发送推送请求:[推送 REST API 使用指南](/sdk/push/guide/rest/)。 - -如果开发者要集成我们封装的 FCM 推送 SDK,可以继续往下阅读,如果自行接入 FCM SDK,则可以忽略下文。 - -## FCM 推送 library 的构成 - -[FCM](https://firebase.google.com/docs/cloud-messaging)(Firebase Cloud Messaging)是 Google/Firebase 提供的一项将推送通知消息发送到手机的服务。接入时后台需要配置连接 FCM 服务器需要的推送 key 和证书,FCM 相关的 token 由 LeanCloud SDK 来申请。 - -### 环境要求 - -FCM 客户端需要在运行 Android 4.1 或更高版本且安装了 Google Play 商店应用的设备上运行,或者在运行 Android 4.1 且支持 Google API 的模拟器中运行。具体要求参见 [在 Android 上设置 Firebase 云消息传递客户端应用](https://firebase.google.com/cloud-messaging/android/client)。 - -### 接入 SDK - -#### 添加 Firebase 配置文件 - -从 Firebase 控制台下载最新的配置文件(google-services.json),加入到应用的模块(应用级)目录中。 - -#### 将 Google 服务插件添加到 Gradle 文件中 - -首先,在根级(项目级)Gradle 文件 (build.gradle) 中添加规则,以纳入 Google 服务 Gradle 插件: - -```groovy -buildscript { - - repositories { - // Check that you have the following line (if not, add it): - google() // Google's Maven repository - } - - dependencies { - // ... - - // Add the following line: - classpath 'com.google.gms:google-services:4.3.5' // Google Services plugin - } -} - -allprojects { - // ... - - repositories { - // Check that you have the following line (if not, add it): - google() // Google's Maven repository - // ... - } -} -``` - -然后,在模块(应用级)Gradle 文件(通常是 app/build.gradle)中,应用 Google 服务 Gradle 插件: - -```groovy -apply plugin: 'com.android.application' -// Add the following line: -apply plugin: 'com.google.gms.google-services' // Google Services plugin - -android { - // ... -} -``` - -#### 导入 SDK FCM 包 - -在模块(应用级)Gradle 文件(通常是 app/build.gradle)中,在 dependencies 中添加依赖: - - -{`dependencies { - implementation 'cn.leancloud:leancloud-fcm:${sdkVersions.leancloud.java}@aar' - // 即时通信与推送需要的包 - implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'\n - // Import the BoM for the Firebase platform - implementation platform('com.google.firebase:firebase-bom:27.0.0') - // Declare the dependencies for the Firebase Cloud Messaging and Analytics libraries - // When using the BoM, you don't specify versions in Firebase library dependencies - implementation 'com.google.firebase:firebase-messaging' - implementation 'com.google.firebase:firebase-analytics' -}`} - - -#### 修改应用清单 - -将以下内容添加至应用的 `AndroidManifest` 文件中: - -- PushService 服务。 - - ```xml - - ``` - -- `LCFirebaseMessagingService` 服务。如果你希望在后台进行除接收应用通知之外的消息处理,则必须添加此服务。要接收前台应用中的通知、接收数据有效负载以及发送上行消息等,必须继承此服务。 - - ```xml - - - - - - ``` - -- (可选)应用组件中用于设置默认通知图标和颜色的元数据元素。如果传入的消息未明确设置图标和颜色,Android 就会使用这些值。 - - ```xml - - - ``` - -- (可选)从 Android 8.0(API 级别 26)和更高版本开始,Android 系统支持并推荐使用[通知渠道](https://developer.android.com/guide/topics/ui/notifiers/notifications.html?hl=zh-cn#ManageChannels)。FCM 提供具有基本设置的默认通知渠道。如果你希望[创建](https://developer.android.com/training/notify-user/channels?hl=zh-cn)和使用你自己的默认渠道,请将 `default_notification_channel_id` 设为你的通知渠道对象的 ID(如下所示);如果传入的消息未明确设置通知渠道,FCM 就会使用此值。 - - ```xml - - ``` - -- 如果 FCM 对于 Android 应用的功能至关重要,请务必在应用的 `build.gradle` 中设置 `minSdkVersion 16` 或更高版本。这可确保 Android 应用无法安装在不能让其正常运行的环境中。 - -### 程序初始化 - -使用 FCM 推送,客户端程序无需做特别的初始化。如果注册成功,`_Installation` 表中应该出现 **vendor** 这个字段为 `fcm` 的新记录。 - -### 配置控制台(设置 FCM 的 ProjectId 及 私钥文件) - -在 [Firebase 控制台](https://console.firebase.google.com/) > **项目设置** > **服务账号** > **Firebase Admin SDK** > **生成新的秘钥** 可以获得服务端发送推送请求的私钥文件。将此 文件 及 ProjectId 通过 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > FCM**,与云服务应用关联。 - -## 取消 FCM 推送注册 - -对于已经注册了 FCM 推送的用户,如果想取消 FCM 推送的注册而改走云服务自有的 WebSocket 的话,可以调用如下函数: - -```java -LCMixPushManager.unRegisterMixPush(); -``` - -此函数为异步函数,如果取消成功会有 `Registration canceled successfully` 的日志输出,万一取消注册失败的话会有类似 `unRegisterMixPush error` 的日志输出。 - -## 错误排查建议 - -- 只要注册时有条件不符合,SDK 会在日志中输出导致注册失败的原因,例如 `register error, mainifest is incomplete` 代表 manifest 未正确填写。如果注册成功,`_Installation` 表中的相关记录应该具有 **vendor** 这个字段并且不为空值。 - -- 如果注册一直失败的话,请提交工单或去论坛发帖,提供相关日志、具体机型以及系统版本号,我们会跟进协助来排查。 diff --git a/.ci/hk/zh-Hans/store/store-admin.mdx b/.ci/hk/zh-Hans/store/store-admin.mdx deleted file mode 100644 index fd210ce4b..000000000 --- a/.ci/hk/zh-Hans/store/store-admin.mdx +++ /dev/null @@ -1,173 +0,0 @@ ---- -title: 权限管理 -sidebar_position: 25 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -## 添加游戏成员或主管理员 - -超级管理员拥有所有厂商权限和游戏权限,可以根据实际运营需求,添加游戏成员或分配游戏管理权限。 - -操作路径:进入权限管理 >> 点击添加用户 >> 输入用户昵称或用户 TapTap ID >> 角色类型选择「游戏成员」或「主管理员」 - -![ ](/img/Administrator-Settings-1.png) - -![ ](/img/Administrator-Settings-2.png) - -## 管理角色配置 - -### 默认角色的权限查看 - -在权限管理 >> 角色配置中,默认配置了一些通常情况下会使用到的角色如:开发、发行、游戏管理员等,默认角色所拥有的权限可在查看⻆⾊中进⾏查看,默认⻆⾊的权限是⽆法修改的。 - -![ ](/img/Administrator-Settings-3.png) - -![ ](/img/Administrator-Settings-4.png) - -### 游戏角色添加 - -由于默认角色无法修改,实际工作中可能无法完全适用,此时可选择添加角色 ,其中角色类型有两种,一种为针对单个游戏的 游戏角色,另一种为针对同厂商多游戏的通行类角色 厂商角色,超级管理员可以添加角色后根据需要进行命名和权限勾选即可。 - -![ ](/img/Administrator-Settings-5.png) - -![ ](/img/Administrator-Settings-6.png) - -## 游戏成员角色设置 - -### 游戏成员的厂商权限设置 - -厂商权限可以根据项目需要划分游戏成员权限,如:厂商管理员可对游戏进行创建、更新等管理,而财务角色的后台会专注结算、对账等 -进入权限管理 >> 选中对应用户 >> 点击编辑权限 ,可对此用户的厂商权限进行设置 ,选中预设的权限并保存即可。 - -![ ](/img/Administrator-Settings-7.png) - -![ ](/img/Administrator-Settings-8.png) - -![ ](/img/Administrator-Settings-9.png) - -### 游戏成员的游戏权限设置 - -超级管理员可以同时分配一款或多款游戏的权限给对应的游戏成员 - -![ ](/img/Administrator-Settings-10.png) - -且每款游戏均可设置游戏成员在游戏中的角色权限 - -![ ](/img/Administrator-Settings-11.png) - -## 删除游戏成员 - -如果您想删除游戏成员,需要进入权限管理 >> 在对应用户后面 >> 点击 ![deleteadmin](https://img.tapimg.com/market/images/2e5c836549d866d6d44036d158095cbb.png),可弹出对该用户的权限调整弹窗,此处可移除该用户,也可以选择将用户设置为超级管理员,此操作只有超级管理员可执行。 - -![ ](/img/Administrator-Settings-12.png) - -## 社区管理中心 - -您可以使用 社区管理中心 功能,方便您更好的管理论坛的帖子内容。 - -### 添加论坛主管理员 - -论坛主管理员拥有游戏论坛的最高管理权限,每款游戏可以绑定一名论坛的主管理员,您可以在 财务与管理 >> 权限管理 >> 论坛主管理员设置处点击添加 Tap io 使用者的账号进行绑定。 - -![ ](https://capacity-files.lcfile.com/8mdz9kHfVHOFQRJeIhVdwgqVkjh5hiIU/Administrator-Settings-13.png) - -### 社区管理中心入口 - -当您绑定完论坛的主管理员账号后,进入 游戏详情页 >> 点击页面右下方 Admin tools,即可进入社区管理中心。 - -![ ](https://capacity-files.lcfile.com/40efpzdPtEH4w18EBaJN3NLaFidpLO2b/Administrator-Settings-14.png) - -您也可在开发者后台选择游戏,在 游戏运营 >> 内容管理处,点击 社区管理中心 进行页面跳转。 - -![ ](https://capacity-files.lcfile.com/nMHnT3aszCifqppERYAwmnlUl7PCRt4M/Administrator-Settings-15.png) - -### 功能介绍 - -进入 社区管理中心 后,可在当前页面进行如下操作: - -- 置顶官方帖:您可以使用这个功能置顶官方发布的关于游戏介绍的帖子 -- 配置 hashtags:可自行置顶站内已有的 hashtags -- 增添其他管理员:可在权限管理处添加新的官方游戏管理人员 - -![](https://capacity-files.lcfile.com/t7rqRdvbvM82PkOyteUNvshbbi9s27GH/Administrator-Settings-16.png) - -#### 置顶帖子 (常用) - -##### 可置顶的帖子 - -- 帖子状态:非删除状态 -- 关联游戏:只要关联当前游戏即可置顶 -- 置顶上限:每个游戏最多置顶十篇帖子 - -##### 帖子展示国家/地区 - -- 默认在所有地区展示,可根据自己需求选择帖子的展示国家/地区 - -##### 置顶时间 - -- 可自行选择帖子的置顶时间,结束时间距开始时间不可超过 30 天 -- 默认选择用户当前设备的时区,可自行修改 - -![](https://capacity-files.lcfile.com/HdeEFxXOcTEUEW29pIKdnnHG2WolIwVR/Administrator-Settings-17.png) - -#### 权限管理 - -进入权限管理板块之后,点击 Add a new moderator 弹出添加账号的页面。 - -![](https://capacity-files.lcfile.com/pUiLXfthsRtgyMYA4dYfkIUuYA38TNOY/Administrator-Settings-18.png) - -输入 User ID 后点击确认,即可将其添加为该游戏社区管理中心的管理员。 - -![](https://capacity-files.lcfile.com/E7qt5oJC5wdfRL6AMJz7IQztxwPIvmll/Administrator-Settings-19.png) - -## Q&A - -### 超级管理员可以解绑吗?可以设置新的超级管理员吗? - -可以。 - -请将您的需求发送至运营部邮箱 [international_operation@taptap.com](mailto:international_operation@taptap.com) , -并提供企业资质扫描件或相关材料证明证明您为该厂商官方人员的材料。 - -**邮件格式** - -> **邮件标题** -> TapTap 主管理员解绑申请 - XXX 厂商名称 -> -> 需进行超级管理员解绑的厂商名称或厂商页链接: -> -> 资质扫描件或相关材料证明(可以附件形式上传): -> -> 需解绑此主管理员的原因(简述): -> -> 申请人联系方式: - -TapTap 运营人员预计将在收到邮件后的 2 个工作日内为您进行操作并与您取得联系,请留意相关邮件回复。 - -原主管理员解绑后,请您以相同厂商名称进行开发者申请并提交审核,此开发者申请审核通过后,您将自动成为该厂商账号的新主管理员。 - -### 用户账号绑定的邮箱可以更换吗? - -可以。 - -请确认新邮箱未绑定其他账号,否则无法成功换绑。确认后请将您的需求发送至运营部邮箱,并提供材料以证明该账号为您或您的企业所属。 - - -**邮件格式** - -> **邮件标题** -> TapTap 账号换绑邮箱申请 - XXX 厂商名称 / 游戏名称 -> -> 需进行邮箱换绑账号的 TapTapID : -> -> 原邮箱(无法确认可不填): -> -> 新邮箱: -> -> 需申请邮箱换绑原因(简述): -> -> 相关材料: - -TapTap 运营人员将在收到邮件后将您的申请递交给相关人员,相关人员预计将在 2 个工作日内进行处理,处理完成后即可使用新邮箱接收验证码登录该账号。 - diff --git a/.ci/hk/zh-Hans/store/store-agree.mdx b/.ci/hk/zh-Hans/store/store-agree.mdx deleted file mode 100644 index 17d6074c2..000000000 --- a/.ci/hk/zh-Hans/store/store-agree.mdx +++ /dev/null @@ -1,570 +0,0 @@ ---- -title: TapTap 游戏审核规范细则 -sidebar_position: 20 ---- - - - -请确保你在开发者后台提交的游戏资料符合《TapTap 游戏审核规范细则》,违规游戏可能被处罚或下架 - - - -## 基本说明 - - - -### 1.1 法律法规 - - - -1.1.1 - -游戏不得含有违法内容或其他敏感信息,包括但不限于涉政、涉赌、暴力、血腥、虐待动物或儿童、淫秽、色情、性暗示等内容; - - - -1.1.2 - -游戏不得带有人身攻击或者侮辱、诽谤他人,侵害他人合法权益(包括但不限于著作权、商标权、肖像权、名誉权等); - - - -1.1.3 - -游戏不得对武器进行过于逼真的表述(如不能涉及武器的制造工艺和参数等),并宣扬违法或滥用武器; - - - -1.1.4 游戏不得存在违反国家/地区法律法规的行为; - - - -1.1.5 - -游戏内容存在安全隐患、召集、推销、鼓动犯罪、有明显侵犯社会善良风俗行为的、扰乱社会秩序或违规违法嫌疑信息,平台将有权拒绝上架; - - - -### 1.2 游戏内付费 - - - -1.2.1 - -游戏内所有付费必须明码标价,必须明确告知可享受的服务,如付费后使用的服务与说明不符,游戏可能被下架,严重者会被封禁账号; - - - -1.2.2 - -游戏不得存在诱导付费行为,包括但不限于诱导站外直接充值付费、诱导进行线下交易、二维码赞助众筹等; - - - -1.2.3 游戏不得存在自动扣费等行为; - - - -### 1.3 游戏内广告及捆绑下载 - - - -1.3.1 - -游戏广告不得含有违法内容或其他敏感信息,包括但不限于涉政、涉赌、暴力、血腥、虐待动物或儿童、淫秽、色情、性暗示等内容及广告法中不允许使用的词汇; - - - -1.3.2 游戏广告不得存在模仿系统通知或提示,从而诱导用户点击的行为; - - - -1.3.3 游戏广告不得在应用已关闭或者退出至后台时依然存在; - - - -1.3.4 游戏内不得存在无法关闭的悬浮窗广告或弹窗广告; - - - -1.3.5 - -游戏广告不得存在捆绑下载行为,例如,游戏登录注册页面默认勾选下载其他 APP 或者必须下载其他 APP 才可使用; - - - -1.3.6 游戏不得存在需强制用户下载其他应用或游戏才可使用; - - - -### 1.4 捆绑下载 - - - -1.4.1 游戏启动页未经用户许可不得默认勾选下载其他 APP,如存在默认勾选项需明确提示用户; - - - -1.4.2 游戏未经用户许可不得自动下载其他 APP - - - -### 1.5 游戏重复 - - - -1.5.1 请勿上传多个内容相似、相同的游戏,雷同游戏可能会被下架处理; - - - -1.5.2 - -游戏的新版本主体功能不得与旧版本的主体功能相差过大,如旧版本为文字游戏,新版本更新为动作游戏; - - - -1.5.3 - -游戏不得为简单网站页面打包或套用模板,用户体验质量过低的游戏可能会被下架处理; - - - -1.5.4 游戏主要功能不得依赖于第三方应用或者需跳转至网页来获取内容及功能; - - - -1.5.5 - -游戏内容不得与已经收录的游戏相同,如发现被侵权请进行申诉,点击查看侵权投诉流程; - - - -### 1.6 欺诈行为 - - - -1.6.1 游戏存在欺诈、误导用户的行为。将被下架,严重者会被封禁账号; - - - -1.6.2 - -游戏在审核前后通过服务端控制应用内容,在上架后开启违规服务。将被下架,严重者会被封禁账号; - - - -### 1.7 暂不收录游戏 - - - -1.7.1 - -破解、盗版、未获得版权所有者授权或重新打包第三方游戏的行为(包括但不限于图片、音乐和文字素材等); - - - -1.7.2 - -赌博类、博彩类地下彩类(包括但不限于棋牌、捕鱼和娃娃机等)或是带有宣扬赌博色彩的游戏,如使用包含炸金花(扎金花)、梭哈、百家乐、赌场、比大小、赢三张、三张牌、六合彩、轮盘、港式五张、21 点、黑杰克等词汇作为应用的名称;或包含以真实筹码为原型的道具购买内容; - - - -1.7.3 虚拟货币类、比特币类或区块链类的游戏; - - - -1.7.4 色情类、血腥暴力类的游戏; - - - -1.7.5 内容涉及宗教、民族文化,宣扬邪教和封建迷信,煽动民族仇恨、破坏民族团结的游戏; - - - -## 2. 基础信息 - - - -### 2.1 厂商 - - - -2.1.1 厂商名称仅限使用汉字、数字、字母,及其组合,不得使用特殊符号; - - - -2.1.2 厂商名称不得使用占位文本、空格、乱码等无关字符; - - - -2.1.3 厂商名不得使用游戏名命名; - - - -2.1.4 厂商名不得使用个人姓名命名; - - - -2.1.5 厂商名不得使用多个合并或并列厂商名; - - - -### 2.2 游戏类型、兼容性及付费 - - - -2.2.1 需根据游戏实际情况填写,不得选择与产品实际功能不符的分类; - - - -2.2.2 需根据实际情况选择,不得随意填写,否则会误导玩家; - - - -2.2.3 游戏付费情况请根据实际情况填写及后续更新; - - - -## 3. 物料审核 - - - -### 3.1 基本规范 - - - -3.1.1 - -图片及视频素材必须保证清晰,不得出现明显模糊、拉伸、压缩、黑边、白边等情况,不得使用纯色、渐变等过于简单图案; - - - -3.1.2 图片及视频不得违反基本法律法规,需符合基本要求; - - - -3.1.3 - -图片及视频中不得含有违反平台收录标准的内容,包括但不限于提现、实质性奖励信息内容及相关素材; - - - -3.1.4 - -图片及视频中不得含有与游戏内容无关的素材,包括但不限于其他游戏或应用广告、厂商联系方式; - - - -3.1.5 - -图片及视频中不得含有侵权内容,已获授权素材或购买素材需上传相应证明材料或授权书; - - - -3.1.6 图片及视频中不得含有游戏商店评分等数据信息; - - - -3.1.7 图片及视频中不得含有与该游戏无关信息; - - - -3.1.8 图片及视频中不得添加强制引导信息; - - - -3.1.9 图片及视频中不得添加引导至其他网站的信息; - - - -### 3.2 ICON 图标 - - - -3.2.1 ICON 不得添加存在误导用户或违反相关规定的素材或角标,不得添加与游戏内容无关的热门搜索词或信息,如极速版、提现等; - - - -3.2.2 ICON 必须符合游戏内容设定,不得添加与游戏内容无关的热门搜索词或信息; - - - -### 3.3 截图 - - - -3.3.1 截图不得含有手机 UI ,或不属于提供安装版本的 UI ; - - - -3.3.2 截图需保持尺寸基本一致; - - - -3.3.3 不得使用重复单一截图作为游戏详情页截图栏宣传素材; - - - -3.3.4 截图不得添加存在误导用户或违反相关规定的信息或素材; - - - -3.3.5 截图需和游戏实际内容一致,不得使用非该游戏内容的素材进行宣传推广; - - - -### **3.4 宣传图​** - -3.4.1 不得含有除游戏名称之外的宣传文案,不得含有人物名片、游戏 UI 元素等内容。 - - - -3.4.2 不得以游戏截图充当推广图,不得多图拼接、平铺作为推广图。 - - - -3.4.3 需含有游戏 Logo(方形推广图除外) ,背景不得大面积留白。 - - - -3.4.4 不得出现实物手机。 - - - -3.4.5 不得出现游戏 ICON 。 - - - -3.4.6 不得使用人物三视图、草稿图、建模图。 - - - - - -### 3.5 文案、简介及开发者的话 - - - -3.5.1 文案不得违反基本法律法规,需符合基本要求; - - - -3.5.2 文案中不得含有违反平台收录标准的内容; - - - -3.5.3 文案中不得含有强制引导等信息内容; - - - -3.5.4 文案中不得含有引战、蹭热度等信息; - - - -3.5.5 文案中不得含有引流信息; - - - -### 3.6 游戏标题 - - - -3.6.1 标题需与游戏资质对应,不得添加热门搜索词类副标题; - - - -3.6.2 标题中不得添加违反平台规定的信息; - - - -3.6.3 标题中不得添加误导玩家、与游戏内容无关的信息; - - - -3.6.4 标题中不得添加地区信息; - - - -3.6.5 游戏安装到手机上显示的 App 名称需与游戏详情页展示的标题一致; - - - -### 3.7 简介及开发者的话 - - - -3.7.1 简介用以介绍该游戏玩法、特色,请勿添加无关广告信息; - - - -3.7.2 简介内容不可与开发者的话内容高度相似; - - - -### 3.8 玩家交流群 - - - -3.8.1 玩家交流群名称需与游戏或官方匹配; - - - -3.8.2 玩家交流群名称不得带有引流信息; - - - -## 4. 商店配置 - - - -### 4.1 游戏状态 - - - -4.1.1 游戏状态需根据实际情况填写; - - - -4.1.2 测试服/先行服不支持选择游戏状态,前台默认为「关注」; - - - -### 4.2 多语言资料 - - - -4.2.1 游戏多语言资料页内容需与上架地区匹配; - - - -4.2.2 相应语言资料页物料素材内语言需匹配; - - - -### 4.3 官网链接 - - - -需填写游戏官方网站链接,不可填写第三方网站链接; - - - -## 5. 安装包文件 - - - -### 5.1 包名 - - - -5.1.1 包名不得含有任何渠道相关标识; - - - -5.1.2 包名须与官方包名保持一致; - - - -5.1.3 游戏包名变更,请发布变更公告后,再通过更新游戏上传; - - - -5.1.4 请勿使用打包工具默认包名; - - - -### 5.2 版本号(Version Code) - - - -5.2.1 游戏版本号不得为 0; - - - -5.2.2 新提交安装包版本号不得低于当前版本号; - - - -### 5.3 版本名(Version Name) - - - -版本名需与官网需保持一致; - - - -### 5.4 更新日志 - - - -更新日志内容需与游戏版本变动有关,不得出现宣传、广告或其他无关内容; - - - -### 5.5 签名 - - - -Apk 不得使用公用证书签名; - - - -## 6. 资质及授权文件 - - - -### 6.1运营和 IP 授权 - - - -6.1.1 游戏内容为运营代理,请提交完整、真实、可追溯的独家授权链证明; - - - -6.1.2 游戏素材、剧情、音乐等内容涉及相关 IP - -,需提交完整、真实、可追溯的授权链证明; - - - -6.1.3 IP 权利人若为上传游戏的公司所有,需提交真实有效的证明; - - - -6.1.4 IP 授权范围必须与上传游戏的内容吻合,且真实有效; - - - -### 6.2 素材授权 - - - -6.2.1 - -游戏素材不得使用版权敏感内容,包括但不限于网络素材、同人素材、其他游戏内素材; - - - -6.2.2 游戏素材若有真人形象,需提交肖像使用授权书; - - - -6.2.3 游戏素材若为原创素材,需提交真实有效的证明; - - - -6.2.4 游戏使用素材若为购买的商业素材,需提交素材购买页面和订单证明; - - - -## 7.其他 - - - -7.1 TapTap 在法律规定的范围内有权对本指南做出解释; - - - -7.2 审核指南一经公布即刻生效,TapTap - -有权随时对指南内容进行修改,修改后的结果公布于 TapTap 开发者平台网站。 \ No newline at end of file diff --git a/.ci/hk/zh-Hans/store/store-auth.mdx b/.ci/hk/zh-Hans/store/store-auth.mdx deleted file mode 100644 index 1222ddef6..000000000 --- a/.ci/hk/zh-Hans/store/store-auth.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: 开发者认证 -sidebar_position: 35 ---- - -import { Blue, Gray } from "/src/docComponents/doc"; - -## 什么是开发者认证? - -TapTap 为开发者提供开发者认证,已认证用户头像下方将会有蓝色「」标志,个人主页将会显示认证的头衔。 - -头衔一般形式为【厂商名称/游戏名称】 + 【职位/官方】 - -![](/img/DC-v2/developer-verification-V2.png) - ---- - -## 如何获得开发者认证? - -您可以通过 **开发者后台** >> **工单** 联系我们,或将您的需求发送至运营部邮箱: -[international_operation@taptap.com](mailto:international_operation@taptap.com) - -**邮件格式** - -> **邮件主题** -> -> TapTap 开发者认证申请 - XXX 游戏名称 或 XXX 厂商名称 -> -> TapTap ID: -> -> 需要认证的头衔: -> -> -> 支持多个用户申请开发者认证,可以 Excel、txt 等格式提供 TapTap ID -> 及需要认证的头衔 -> -> -> 申请人联系方式: - -TapTap 运营人员预计将在收到邮件后的 2 个工作日内为你进行操作。 - -## 如何移除开发者认证? - -您可以通过 **开发者后台** >> **工单** 联系我们,或将您的需求发送至运营部邮箱: -[international_operation@taptap.com](mailto:international_operation@taptap.com) - -**邮件格式** - -> **邮件主题** -> 移除 TapTap 开发者认证申请 - XXX 游戏名称 或 XXX 厂商名称 -> -> TapTap ID: -> -> 需要删除的头衔: -> -> 支持批量删除用户开发者认证,可以 Excel、txt 等格式提供 -> TapTapID 及需要移除的头衔 申请人联系方式: -> -> TapTap 运营人员预计将在收到邮件后的 2 个工作日内为你进行操作。 - -TapTap 运营人员预计将在收到邮件后的 2 个工作日内为你进行操作。 diff --git a/.ci/hk/zh-Hans/store/store-complaint.mdx b/.ci/hk/zh-Hans/store/store-complaint.mdx deleted file mode 100644 index 6909910cf..000000000 --- a/.ci/hk/zh-Hans/store/store-complaint.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: 侵权投诉 -sidebar_position: 65 ---- - - -TapTap 是一个游戏下载购买平台,亦是一个高品质玩家社区。根据相关法律法规,TapTap 特制定本指引,请参照以下指引进行申诉,以尽可能保护权利人的合法权益。 - -## 1. 流程 -**1.1 通知 TapTap** - -若权利人认为第三方在 TapTap 上提供的游戏侵犯其合法权益,权利人应向 TapTap 提出书面通知书,通知书的内容应包括但不限于以下内容: - -**(1)权利人主体信息和相关材料** -权利人的姓名(名称)、联系方式、地址及营业执照(单位)、身份证(个人)、相关授权证明等证明权利人主体资格的材料; - -**(2)权利人要求** -权利人要求删除或者断开链接的游戏准确名称和链接地址; - -**(3)构成侵权的初步证明材料** -该初步证明材料应包括: -I. 权利人拥有权利的权属证明材料:包括但不限于相关有权机构颁发的版权证书、商标权证书、专利权证书、作品首次公开发表或发行日期证明材料、创作手稿、经权威机构签发的作品创作时间戳、作品备案证书等能证明权利人拥有相关权利的有效权属证明; -II. 被投诉方提供的游戏构成侵权的证明:包括但不限于被投诉方提供的游戏构成对权利人的版权、商标权或专利权等侵权的有效证明材料等。 - -**(4)权利人保证** -权利人的通知书应包含以下保证: -权利人在通知书中的陈述和提供的相关材料皆是真实、有效和合法的,并保证承担和赔偿,因 TapTap 根据权利人的通知书而删除或者断开有关侵权游戏的链接或相关内容而给 TapTap 造成的任何损失,包括但不限于 TapTap 因向被投诉方或用户赔偿而产生的损失及 TapTap 名誉、商誉损害等。 - -通知书及相关证明材料准备时需注意的事项以及投寄地址详见下文注意事项中的说明。 - -**1.2 TapTap 反馈** -TapTap 作为中立的平台服务者,收到权利人符合本指引要求的通知书后,会将权利人的通知书转送给被投诉方: - -**(1)若被投诉方认可权利人的投诉** -TapTap 会尽快按照相关法规的规定进行处理; - -**(2)若被投诉方不认可权利人的投诉** -TapTap 会将被投诉方提供的相关材料转送给权利人,若权利人对于被投诉方的意见及其提供的相关材料有异议的,TapTap 建议权利人另行通过行政投诉、诉讼等方式直接和被投诉方解决相关问题。如果权利人有新的并可充分推翻被投诉方意见的证明材料的,也可向 TapTap 提供。 - -## 2. 注意事项 -(1)本指引中的权利人,指拥有版权、商标权、专利权等合法权益的原始所有人或经原始所有人合法授权的代理人,包括自然人、法人或其他组织等。 - -(2)为了确保投诉的真实性和有效性,权利人的书面通知书及其他相关证明材料,原则上应提供原件,不能提供原件的,应提供复印件(在复印件上应有权利人的签章),若材料涉外的,应按照法律的规定进行公证转递,并同时提供相应的公证转递材料。 - -(3)通知 TapTap 的方式:将前述全部的纸质版本材料扫描后通过电子邮件发送至 TapTap 指定电子邮箱 [dmca@taptap.com](mailto:dmca@taptap.com) - -(4)本指引中的权利人的书面通知书,均应包括通知书本身及相关的主体资格材料、权属证明、侵权证明等材料。 - -(5)若权利人已经因为被投诉人提供的游戏向相关政府部门或法院提起行政投诉或诉讼的,请在提交通知书时,将相关受理证明及提交政府部门或法院的证据材料一同提交给 TapTap ,这将有利于权利人的投诉的处理。 - -(完) - -## 附录: [侵权投诉通知书模板](https://lc-gluttony.s3.amazonaws.com/vmzp7NxP3swl/Vm0BDugjlYJQQ5veMyKm0YdzEcm6RdAv/TapTap%E5%B9%B3%E5%8F%B0%E4%BE%B5%E6%9D%83%E6%8A%95%E8%AF%89%E9%80%9A%E7%9F%A5%E4%B9%A6.doc.zip) - ---- - -# 二、 侵权投诉反通知指引 - -## 1. 被投诉方向 TapTap 提交书面反通知书 - -被投诉方在收到 TapTap 转送的权利人向 TapTap 提交的相关投诉材料后,应按照以下的指引要求在五个工作日内向 TapTap 提交书面反通知书,反通知书的内容应包括但不限于以下内容: - -**1.1 被投诉方主体信息和相关材料** -权利人的姓名(名称)、联系方式、地址及营业执照(单位)、身份证(个人)等证明被投诉方主体资格的材料; - -**1.2 被投诉方对投诉的观点** -I. 认可侵权; -II. 不认可侵权。 - -**1.3 不构成侵权的初步证明材料** -被投诉方不认可侵权的,应提供不侵权的初步证明材料,具体应包括: -1)被投诉方拥有权利的证明材料:包括但不限于相关有权机构颁发的版权证书、商标权证书、专利权证书、作品首次公开发表或发行日期证明材料、创作手稿、经权威机构签发的作品创作时间戳、作品备案证书等能证明被投诉方拥有相关权利的有效权属证明; -2)被投诉方提供的游戏不构成侵权的证明材料:包括但不限于被投诉方提供的游戏不构成版权、商标权或专利权等权利侵权的有效证明材料等。 - -**1.3 被投诉方保证** -被投诉方应在反通知书中明确保证: -被投诉方在反通知书中的陈述和提供的相关材料皆是真实、有效和合法的,并保证承担和赔偿 TapTap 因根据被投诉方的反通知书,而在 TapTap 上保留或删除、断开被投诉的游戏或相关内容而给 TapTap 造成的损失,包括但不限于 TapTap 因向权利人或用户赔偿而产生的损失等及 TapTap 名誉、商誉损害等。 -反通知书及相关证明材料准备时需注意的事项以及投寄地址详见下文注意事项中的说明。 - -## 2. TapTap 反馈 -TapTap 作为中立的平台服务者,会依法进行相关处理: - -**2.1 若被投诉方认可权利人的投诉的** -TapTap 会尽快按照相关法规的规定进行处理; - -**2.2 若被投诉方不认可权利人的投诉的** -TapTap 会将被投诉方提供的相关材料转送给权利人。TapTap 将根据相关法规,作出处理。 - -## 3. 注意事项 -3.1 本指引中的权利人,指拥有版权、商标权、专利权等合法权益的原始所有权人或经原始所有权人合法授权的人,包括自然人、法人或其他组织等。 - -3.2 为了确保被投诉方提供相关材料的真实性和有效性,被投诉方的书面反通知书(包括相关证明材料等)及其他相关证明材料,原则上应提供原件,不能提供原件的,应提供复印件(在复印件上应有被投诉方的签章),若材料涉外的,应按照法律的规定进行公证转递,并同时提供相应的公证转递材料。 - -3.3 通知 TapTap 的方式:将前述全部的纸质版本材料扫描后通过电子邮件发送至 TapTap 指定电子邮箱 [dmca@taptap.com](mailto:dmca@taptap.com) - -3.4 本指引中的被投诉人的书面反通知书,均应包括反通知书本身及相关的主体资格材料、权属证明、不构成侵权证明等材料。 - -(完) - -## 附录: [侵权投诉反通知书模板](https://lc-gluttony.s3.amazonaws.com/vmzp7NxP3swl/3hyv6Bn92qHDTsnetWTqhg8sHx2zcJCb/TapTap%E5%B9%B3%E5%8F%B0%E4%BE%B5%E6%9D%83%E6%8A%95%E8%AF%89%E5%8F%8D%E9%80%9A%E7%9F%A5%E4%B9%A6.doc.zip) - ---- - -# 三、 评价及社区投诉指引 - -TapTap 是一个游戏下载购买平台,亦是一个高品质玩家社区。根据相关法律法规,TapTap 特制定本指引,请参照以下指引进行申诉,以尽可能保护权利人的合法权益。 - -## 1. 流程 -**1.1 向 TapTap 提出** -若权利人认为第三方在 TapTap 上发布的信息侵犯其合法权益,权利人应向 TapTap 提出书面投诉书,投诉书的内容应包括但不限于以下内容: - -**(1)权利人主体信息和相关材料** -权利人的姓名(名称)、联系方式、地址及营业执照(单位)、身份证(个人)、相关授权证明等证明权利人主体资格的材料; - -**(2)权利人要求** -权利人要求删除或者修改的信息链接地址; - -**(3)构成侵权的初步证明材料** -该初步证明材料应包括: -I. 权利人拥有权利的权属证明材料:包括但不限于相关有权机构颁发的版权证书、商标权证书、专利权证书、作品首次公开发表或发行日期证明材料、创作手稿、经权威机构签发的作品创作时间戳、作品备案证书等能证明权利人拥有相关权利的有效权属证明; -II. 被投诉方提供的游戏构成侵权的证明:包括但不限于被投诉方发布的信息构成对权利人合法权益的侵权有效证明材料等。 - -**(4)权利人保证** -权利人的通知书应包含以下保证: -权利人在投诉书中的陈述和提供的相关材料皆是真实、有效和合法的,并保证承担和赔偿,因 TapTap 根据权利人的投诉书而删除或者修改有关侵权信息或相关内容而给 TapTap 造成的任何损失,包括但不限于 TapTap 因向被投诉方或用户赔偿而产生的损失及 TapTap 名誉、商誉损害等。 - -投诉书及相关证明材料准备时需注意的事项以及投寄地址详见下文注意事项中的说明。 - -**1.1 TapTap 反馈** -1.2.1 TapTap 收到权利人符合本指引要求的投诉书后,会尽快判断投诉类型: - -**(1)若权利人的投诉属于 TapTap 可独立判断范围** -TapTap 会尽快按照相关法规的规定进行处理; - -**(2)若权利人的投诉属于 TapTap 不可独立判断范围** -TapTap 建议权利人另行通过行政投诉、诉讼等方式直接和被投诉方解决相关问题。 - -1.2.2 随后 TapTap 会尽快处理权利人的申诉,并按照 TapTap 社区管理条例及有关规定进行处理。 - -## 2. 注意事项 -(1)本指引中的权利人,指拥有版权、商标权、专利权等合法权益的原始所有人或经原始所有人合法授权的代理人,包括自然人、法人或其他组织等。 - -(2)为了确保投诉的真实性和有效性,权利人的书面投诉书及其他相关证明材料,原则上应提供原件,不能提供原件的,应提供复印件(在复印件上应有权利人的签章),若材料涉外的,应按照法律的规定进行公证转递,并同时提供相应的公证转递材料。 - -(3)通知 TapTap 的方式:将前述全部的纸质版本材料扫描后通过电子邮件发送至 TapTap 指定电子邮箱 [dmca@taptap.com](mailto:dmca@taptap.com) - -(4)本指引中的权利人的书面投诉书,均应包括投诉书本身及相关的主体资格材料、权属证明、侵权证明等材料。 - -(5)若权利人已经因为被投诉人发布的信息向相关政府部门或法院提起行政投诉或诉讼的,请在提交投诉书时,将相关受理证明及提交政府部门或法院的证据材料一同提交给 TapTap ,这将有利于权利人的投诉处理。 - -(完) - -## 附录: [评价及社区投诉书模板](https://lc-gluttony.s3.amazonaws.com/vmzp7NxP3swl/gBKS91vj66v2crFLJ7w7xutBtF6j7zAF/TapTap%E5%B9%B3%E5%8F%B0%E8%AF%84%E4%BB%B7%E5%8F%8A%E7%A4%BE%E5%8C%BA%E6%8A%95%E8%AF%89%E4%B9%A6.doc.zip) diff --git a/.ci/hk/zh-Hans/store/store-contact.mdx b/.ci/hk/zh-Hans/store/store-contact.mdx deleted file mode 100644 index 7b1ca5ff7..000000000 --- a/.ci/hk/zh-Hans/store/store-contact.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: 联系我们 -sidebar_position: 70 ---- - -您在使用TapTap平台过程中,遇到了任何问题,请随时通过邮箱联系我们。 - ->运营部邮箱: - ->商务合作邮箱: - diff --git a/.ci/hk/zh-Hans/store/store-creategame.mdx b/.ci/hk/zh-Hans/store/store-creategame.mdx deleted file mode 100644 index b4f3dfa8f..000000000 --- a/.ci/hk/zh-Hans/store/store-creategame.mdx +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: 创建游戏 -sidebar_position: 45 ---- - -import { Blue } from "/src/docComponents/doc"; - -您创建的 TapTap 开发者账号通过审核后,就可以在开发者中心内创建和发布游戏了。 - -## 创建游戏页面 - -进入 **开发者后台** >> 点击左侧 **创建游戏** >> 填写游戏基础信息后即可创建游戏页面 - -- 创建游戏页面时,系统会根据提交日期自动生成一个版本名称(例如:V-20220818),版本名称会用于跟踪资料版本审核状态。 -- 第一次创建游戏页面后,该游戏为「XX 版本未发布」状态,**游戏上架** 需要提交审核。 -- 待上架游戏的上限为 10 个,超过将无法创建新的游戏页面。 -- 游戏页面创建完成后,可通过 **全部游戏** >> 游戏右下角,点击 ![‘deleteadmin’](https://img.tapimg.com/market/images/2e5c836549d866d6d44036d158095cbb.png) 进行删除。 - -![](/img/DC-v2/create-V2en-1.png) - -版本名称附图 - -![](/img/DC-v2/create-V2en-2.png) - -游戏页面创建完成后,在 **全部游戏** 页面可以查看创建过的所有游戏,点击游戏标题可进入游戏面板。 - -![](/img/DC-v2/create-V2en-3.png) - -## 发布游戏页面 - -点击 **游戏页面** >> 补充 **商店资料** 后 >> 点击 **提交审核**,审核通过后,游戏页面将会对玩家进行展示。 - -### 设置发布地区和状态 - -如果您对发行的区域有要求,可以在 **当前未发布版本** >> **发布地区和状态** 中添加想要发布游戏的地区,通过审核后,你的游戏页面就回上架到对应地区的商店中。 - -未上架地区只会展示游戏的基本信息(游戏名、厂商名称、详情页顶部图),玩家可以关注厂商,但无法下载游戏。 - -![](/img/DC-v2/create-V2en-4.png) - -TapTap 国际没有明确意义上的地区概念,而是全球用户来到一个 App 体验游戏,游戏宣发时期,我们更建议您将全球地区开放为预约状态,为游戏上线积攒人气。 - -### 设置多语言资料 - -您可以在 **商店资料** >> **推广资料** 中补充多语言资料吸引更多的玩家来关注游戏。 - -- 如果您的游戏发布到全球,则必须要补充「英文」的多语言资料 -- 果您的游戏发布到指定国家/地区,建议您补充「英文」资料 + 发行地区当地使用的官方语言 - -例如:游戏发布到韩国,可以补充「英文」+「韩文」这二套多语言资料 - -目前后台支持配置的多语言有:「繁体中文」、「英文」、「日文」、「韩文」、「印尼语」、「泰语」、「葡萄牙语」、「越南语」、「印度语」、「马来语」、「西班牙语」、「法语」、「德语」、「俄语」 - -您可以根据游戏发行需求添加相关多语言资料内容。 - -![](/img/DC-v2/create-V2en-5.png) - -勾选/去除 多语言资料 - -![](/img/DC-v2/create-V2en-6.png) - -### 设置 APK - -游戏在测试、开放下载的状态下 APK 必填,您可以在 **开发者后台** >> **商店资料** >> **APK** 位置上传。 - -为了全球各地玩家下载游戏后能有更好的游玩体验,请您上传 APK 后,根据实际情况填写 **游戏 APK 包内支持语言**,填写后的信息将会在游戏简介中展示。 - -![](/img/DC-v2/create-V2en-7.png) - -关于包体格式: - -- 目前仅支持开发者上传 APK 格式的包体; -- 若您需要上传 APK+OBB 格式的包体,可以从 **开发者后台** >> **工单** 位置联系我们,或请将您的需求发送至运营部邮箱 [international_operation@taptap.com](mailto:international_operation@taptap.com),平台运营收到需求后,会协助您上传包体。 - -### 设置发布时间 - -当游戏页面资料补充完整后,您可以在 **发布设置** 中选择游戏页面的 **上线方式** 。 - -![](/img/DC-v2/create-V2en-8.png) - -- 如果您希望游戏通过审核后,游戏页面在 TapTap 站内立即展示的话,上线方式可以选择为 **立即上线** -- 如果您希望在固定的时间在 TapTap 站内中展示游戏页面的话,上线方式可以选择 **定时上线** 并在 **定时上线** 模块中配置具体的上线日期和时间 - -**提示:定时上线时间仅支持选择当前时间 24h 之后的时间** - -### 修改页面定时发布时间 - -如果您需要修改版本的定时上线时间,您可以参考以下方法 - -- 版本已通过审核的情况 - - 点击 **修改定时** ,您可以重新选择游戏页面上线时间,目前仅支持选择当前时间之后的时间 - - 点击 **撤销定时** 游戏当前版本将会变成 **审核失败** 的状态,需要您重新定时后再次提交审核 -- 版本未通过审核的情况 - - 点击 **撤销审核** ,重新定时后需再次提交审核 - -## 提交审核 - -当游戏页面资料信息设置完成后,就可以点击 **提交审核** ,审核结果预计会在 2 个工作日内在个人中心-通知 中告诉您。 - -## Q & A - -### 我的游戏已经被 TapTap 收录,可以进行游戏认领吗? - -可以。 - -您可以通过 **开发者后台** >> **工单** 联系我们,或请将您的需求发送至运营部邮箱 [international_operation@taptap.com](mailto:international_operation@taptap.com),并提供企业营业执照扫描件或其他能证明该游戏为您(或您的厂商)所有的材料。 - -**邮件格式** - -> **邮件标题** -> TapTap 游戏认领申请 - XXX 游戏名称 -> -> 需认领游戏名称及页面链接: -> -> 申请认领的厂商名称: -> -> 营业执照扫描件或相关证明材料(可以附件形式上传): -> -> 申请人联系方式: - -TapTap 运营人员预计将在收到邮件后的 2 个工作日内为您进行操作。 - -### 游戏主体可以进行转移吗? - -可以。 - -您可以通过 **开发者后台** >> **工单** 联系我们,或请将您的需求发送至运营部邮箱 [international_operation@taptap.com](mailto:international_operation@taptap.com),并抄送另一方厂商邮箱。 - -**邮件格式** - -> **邮件标题** -> -> TapTap 游戏主体转移申请 - XXX 游戏名称 -> -> 需进行主体转移的游戏链接: -> -> 游戏主体原所有方厂商名称: -> -> 游戏主体新所有方厂商名称: -> -> 申请人联系方式: - -**请注意:此邮件需用厂商企业邮箱进行发送,并抄送另一方厂商企业邮箱;如果您是个人开发者,转移方与被转移方邮箱需为 TapTap 厂商资料中填写的邮箱。被抄送方需回复邮件确认。** - -TapTap 运营人员预计将在收到确认邮件后 2 个工作日内进行核实操作并处理完毕。 - -### 如何进行游戏下架? - -您可以通过 **开发者后台** >> **工单** 联系我们,或请将您的需求发送至运营部邮箱 [international_operation@taptap.com](mailto:international_operation@taptap.com),并提供企业营业执照扫描件或其他能证明您是该厂商官方人员的材料。 - -**邮件格式** - -> **邮件标题** -> TapTap 游戏下架申请 - XXX 游戏名称 -> -> 需下架游戏链接: -> -> 下架原因: -> -> 期望下架时间: -> -> 营业执照扫描件或相关证明材料(可以附件形式提供): -> -> 申请人联系方式: - -TapTap 运营人员将在收到邮件后的 2 个工作日内处理您的申请。 - -**请注意: TapTap 希望保留玩家产出内容,下架游戏一般不会进行页面删除处理。如果您有下架页面需求,请在邮件中补充说明。** diff --git a/.ci/hk/zh-Hans/store/store-devagreement.mdx b/.ci/hk/zh-Hans/store/store-devagreement.mdx deleted file mode 100644 index 8676a7bab..000000000 --- a/.ci/hk/zh-Hans/store/store-devagreement.mdx +++ /dev/null @@ -1,275 +0,0 @@ ---- -title: TapTap 平台开发者协议 -sidebar_position: 15 ---- - -欢迎您使用 TapTap 平台提供的服务! - -为使用 TapTap 平台提供的服务(以下简称“本服务”),您应当阅读并遵守本《TapTap 平台开发者协议》(以下简称“本协议”)规定的相关规则。 - -**请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,以及开通或使用某项服务的单独协议、规则,该等条款可能以加粗等方式提示您注意。** - -**除非您已阅读并接受本协议及相关协议、规则等所有条款,否则,您无权使用相关服务。您使用相关服务,即视为您已阅读并同意上述协议、规则等的约束。** - -**当您有违反本协议的任何行为时,TapTap 平台有权依照违反情况,随时单方限制、中止或终止向您提供服务,并有权追究您的相关责任。** - -**TapTap 平台可能就包括但不限于本协议内容、与本服务相关的协议、规则等内容不断进行更新、修改等,上述内容一经正式发布,即成为本协议不可分割的组成部分,您对此了解并同意同样遵守。** - -**1. 接受本协议** - -1.1 本协议是您和 TapTap 平台就游戏发布、宣传、推广等相关事宜达成的法律约束合约。您的缔约对象是在相关区域有权运营 TapTap 平台的对应企业实体。您同意,一经接受本协议并注册完成开发者账号,TapTap 平台将仅以您的名义(而非 TapTap 平台的名义)显示游戏供用户下载(付费购买或免费下载)。您承诺并保证您具有完全民事行为能力并接受本协议,**您具备使用本服务、接入和提供游戏或相关服务等行为的相关合法资质或经过了相关政府部门的审核批准;您提供的主体资质材料、相关资质或证明以及其他任何文件等信息真实、准确、完整,并在信息发生变更后,及时进行更新;您具备履行本协议项下之义务、各种行为的能力;您履行相关义务、从事相关行为不违反任何对您有约束力的法律文件。否则,您应不使用 TapTap 平台提供的相关服务,且应独自承担由此带来的一切责任及给用户、TapTap 平台造成的全部损失。**如果您不接受本协议,则不得使用本服务。 - -1.2 您应当以开发者账号使用 TapTap 平台提供的相关服务。开发者账号一经注册成功,不得变更,不可转让、不可赠与、不可继承。您应谨慎合理地保存、使用账号和密码,并对该账号下的操作承担全部责任。 - -1.3 如果您代表您的雇主或其他实体同意接受本协议的约束,则表示您声明并担保,您已取得充分的合法授权,可使您的雇主或此类实体受本协议的约束。如果您未获得必要的授权,则不得代表雇主或其他实体接受本协议或使用 TapTap 平台。 - -**2. 定义** - -2.1 **品牌标识:**是指各方独立拥有(或取得合法授权)的游戏名、商标、Logo、域名以及其他显著品牌特征的标识。 - -2.2 **开发者或您:**是指已在 TapTap 平台注册成功并且按照本协议条款取得开发者账号的任何个人、法人或其他组织。 - -2.3 **开发者账号:**是指开发者通过注册取得,并由 TapTap 平台分配给开发者的发布账号,可让开发者通过 TapTap 平台发布游戏及使用 TapTap 平台提供的其他服务。 - -2.4 **Dashboard:**是指开发者通过开发者账号登录 TapTap 平台开发者中心,通过 TapTap 平台提供的控制台或/和其他在线工具或服务,用来管理游戏和查看相关数据的后台工具。 - -2.5 **TapTap 平台:**是指 TapTap (域名包括但不限于 taptap.com、tap.io )网站及客户端,以包含 TapTap 平台网站、客户端等在内的各种形态(包括未来技术发展出现的新服务形态)向您提供各项服务的平台。 - -2.6 **游戏或产品:**是指由开发者开发,或开发者经权利人授权,通过 TapTap 平台向相关用户提供下载、预约、测试等相关服务的产品,包括但不限于特定移动端游戏、及游戏相关的文字、音乐、图片、视频、娱乐应用程序等。 - -2.7 **用户:**指所有直接或间接使用您发布或更新在 TapTap 平台中的游戏的玩家。 - -2.8 **用户数据:**是指用户在 TapTap 平台产生的与用户相关的数据,包括但不限于用户使用 TapTap 平台时主动提供的信息、经用户同意由 TapTap 平台获取的信息、用户操作行为形成的数据及各类交易数据等。除 TapTap 隐私政策另有约定外,“用户数据”的所有权及其他相关权利属于 TapTap 平台,且是 TapTap 平台的商业秘密。 - -2.9 **平台运营数据:**是指使用本服务或在 TapTap 平台中产生的相关数据,包括但不限于用户或/和开发者提交的数据、操作行为形成的数据及各类交易数据等。“平台运营数据”的所有权及其他相关权利属于 TapTap 平台,且是 TapTap 平台的商业秘密,依法属于用户或开发者享有的相关权利除外。 - -**3. 简介** - -3.1 TapTap 平台是一个向公众用户开放的平台,开发者可以通过该平台发布相关游戏。要在 TapTap 平台中发布游戏,您必须获取并维护有效的开发者账号。 - -3.2 如需针对您的游戏向用户提供付费下载,或根据 TapTap 平台的要求,您应当另外与 TapTap 平台签署相关游戏合作协议,对游戏发布事宜作出具体约定。 - -**4. TapTap 平台、开发者均同意和理解** - -4.1 **TapTap 平台是一个中立的平台服务提供者,仅向开发者提供游戏相关服务,包括上传存储、游戏发布、预约、测试、跳转链接等中立的网络服务或技术支持服务,以供开发者在中立的开放平台上自主发布、推广其游戏等;** - -4.2 **开发者的游戏由开发者自主开发、合法授权、独立运营并独立承担全部责任。TapTap 平台不会参与开发者游戏的研发、运营等任何活动,TapTap 平台不会对开发者的游戏进行任何的修改、编辑等;** - -4.3 **因开发者游戏及服务产生的任何纠纷、责任等,以及开发者违反相关法律法规或本协议约定引发的任何后果,均由开发者独立承担责任、赔偿损失,与 TapTap 平台无关。如侵害到 TapTap 平台或他人权益的,开发者须自行承担全部责任并赔偿一切损失。** - -**5. 定价和维护** - -5.1 开发者有权自行或与 TapTap 平台协商确定游戏在 TapTap 平台的销售价格,或选择免费提供游戏下载,TapTap 平台将按照您自行或协商设定的价格,以您的名义显示游戏。 - -5.2 您需为您的游戏提供支持。如果从 TapTap 平台下载及安装的游戏出现任何缺陷或性能问题,用户会依照说明与开发者联系。**您必须对相关问题全权负责,TapTap 平台无须负责承担或处理您游戏的支持和维护工作,以及处理所有与您的游戏相关的投诉。如果您的游戏没有提供适当的信息或支持,则可能会导致出现游戏评分较低、游戏曝光率降低、销售量下降、结算纠纷等情况,且 TapTap 平台有权将游戏从 TapTap 平台下架。** - -5.3 重新安装。您授权用户可以多次重新安装通过 TapTap 平台下载的每个游戏,除非根据相关规定由您或 TapTap 平台对游戏进行了彻底移除。 - -**6. 您对 TapTap 平台的使用行为** - -6.1 除本协议或您与 TapTap 平台另行签署的合作协议中授予的许可权之外,TapTap 平台不会从您(或您的许可人)那里获取游戏的权利、所有权或利益,包括该等游戏中现有的知识产权。 - -6.2 您同意,只在本协议和相关司法辖区中的任何适用法律法规或公认的惯例或准则允许的情况下使用 TapTap 平台。 - -6.3 您同意在使用本服务时保护用户的隐私权和合法权利,保护程度原则上应不低于 TapTap 隐私政策的同等水平,保护程度明显低于的部分,您应充分告知用户并取得用户的同意。例如,用户为了访问或使用您的游戏而向您提供了用户名、密码或者其他登录信息或个人信息,那么您不仅必须让用户知道此类信息将仅供您的游戏使用,还必须依法为这些用户提供充分的隐私权声明和保护。此外,您的游戏只能将此类信息用于用户允许的有限用途。如果您的游戏存储了用户提供的个人信息或敏感信息,则必须进行安全存储,且严格按照适用法律及相应的国家标准进行使用。但如果用户选择与您达成一份单独协议,其中允许您或您的游戏存储或使用与您的游戏(不包括其他游戏)直接相关的个人信息或敏感信息,那么您对此类信息的使用将受到该单独协议中条款的约束。如果用户为您的游戏提供了 TapTap 平台或其他账号信息,那么您的游戏只能在用户许可的情况下使用该信息访问用户的该等账号,并且只能将其用于用户许可的限定用途。您应当向用户提供修改、删除用户数据的方式,确保用户要求删除其用户数据时可通过该方式自行操作完成,并确保相关数据被完全删除。 - -6.4 禁止的行为。您同意不会在使用 TapTap 平台或本服务时从事或参与任何干扰、中断、破坏或未经授权即访问任何第三方的设备、服务器、网络或其他财产或服务的行为,这一规定亦适用于游戏的开发或发布活动。您不得使用从 TapTap 平台获得的用户信息在 TapTap 平台之外销售或发布游戏,或以任何形式将从 TapTap 平台获得的数据提供给任何他人使用,或将客户信息出售、转让。未经 TapTap 平台事先书面同意,您不得为本协议约定之外的目的使用相关数据。 - -6.5 **您同意对您通过 TapTap 平台发布的任何游戏,以及使用任何 TapTap API 或 TapTap SDK 的游戏造成的后果均由您自行承担全部责任,TapTap 平台对您或任何第三方均不承担任何责任。这些后果包括但不限于与您游戏相关的游戏责任、消费者保护和/或知识产权责任等。** - -6.6 **您同意就所有违反本协议所述义务、任何适用的第三方合同或服务条款,或任何适用法律法规的行为,以及由此造成的后果承担全部责任( TapTap 平台对您或任何第三方均不承担任何责任)。如因您通过 TapTap 平台发布的游戏存在权利瑕疵或侵犯了第三方的合法权益(包括但不限于专利权、商标权、著作权及著作权邻接权、肖像权、隐私权、名誉权等)而导致 TapTap 平台及其关联公司或与 TapTap 平台合作的第三方面临任何索赔、诉讼,或者使 TapTap 平台及其关联公司或与 TapTap 平台合作的第三方因此遭受任何名誉、声誉或者财产上的损失,您必须积极地采取一切可能采取的措施,以保证 TapTap 平台及其关联公司或与 TapTap 平台合作的第三方免受上述索赔、诉讼的影响。同时您应对 TapTap 平台及其关联公司或与 TapTap 平台合作的第三方因此遭受的直接及间接经济损失负有全部的赔偿责任。** - -6.7 游戏评分。用户可通过 TapTap 平台对游戏进行评分和评价。TapTap 平台有权自行确定或变更游戏在 TapTap 平台中的展示位置,并有权以其自行决定的方式向用户展示游戏及评分。 - -6.8 上传及更新。您要负责将您的游戏上传到 TapTap 平台,同时向用户提供所需的游戏信息和支持,并准确地公开必需的安全权限,以确保游戏在用户设备上的正常运行。如果游戏未正常上传,TapTap 平台将不予发布。您的游戏应符合 TapTap 平台在技术、安全等方面的统一要求,以确保您可以在 TapTap 平台安全、稳定地运营游戏。同时为了向用户提供优质的产品和服务,**您应在游戏上线运营(包括但不限于下载、测试、试玩等模式)后提供游戏的持续更新,并保证通过 TapTap 平台提供的游戏版本为用户通过各种公开渠道所能获得的最新版本,即您向 TapTap 平台提供的游戏版本不应低于该游戏在其他任何安卓游戏平台或渠道提供的游戏版本,无论其他平台或渠道提供的游戏版本是否系您自行或授权他人提供。** - -6.9 您应当向相关权利人提供有效的投诉途径,确保权利人在认为您侵犯其合法权益时可以向您主张权利。 - -6.10 为向您提供更加全面优质的服务,TapTap 或者 TapTap 的合作伙伴(简称“单项服务提供方”)可能还会为您提供其他单项服务,包括但不限于广告服务、云服务等。您有权根据需要,自主决定是否使用该单项服务。 - -6.11 本条所述单项服务可能配置单独的服务协议、使用规则等,也可能以公告、提示等方式向您说明该单项服务的功能、规则和要求,若您选择使用某个单项服务,则应按照其要求进行开通,并遵守前述相关协议、规则、公告、提示等。 - -6.12 若您以任何形式登录、使用单项服务,即表示您已理解并接受该单项服务的相关协议、规则、公告、提示等的约束。 - -6.13 您理解并同意,单项服务提供方享有自主运营权,有权无须经您同意或提前通知即可直接采取以下措施: - -(1)对单项服务的相关协议、规则等进行修改,包括但不限于使用条件、使用方式、资费等; - -(2)变更某个单项服务的具体服务内容,中止或终止提供某个单项服务。 - -若您不接受上述变更或调整的,应当停止使用相关服务。否则,如果您继续使用相关服务,即视为您已同意并接受相关的变更或调整。 - -6.14 **受限内容。**您通过 TapTap 平台发布或使用 TapTap 服务的游戏必须遵守 TapTap 平台的政策并在全球范围内遵守所有适用法律和其他义务,您需要声明并保证您发布的游戏不得含有以下内容,且游戏中不得存在以下行为: - -(a) 违反宪法确定的基本原则; - -(b) 危害国家安全,或者损害国家荣誉和利益; - -(c) 泄露国家秘密,颠覆国家政权,破坏国家统一、主权和领土完整; - -(d) 煽动民族仇恨、民族歧视,破坏民族团结,或者侵害民族风俗、习惯; - -(e) 破坏国家宗教政策,宣扬邪教和封建迷信; - -(f) 散布谣言,扰乱社会秩序,破坏社会稳定; - -(g) 散布淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪; - -(h) 违背社会公序良俗,损害公共利益; - -(i) 侮辱或者诽谤他人,侵害他人合法权益; - -(j) 对他人进行暴力恐吓、威胁、欺诈,或实施人肉搜索; - -(k) 散布商业广告,或类似的商业招揽信息、过度营销信息及垃圾信息; - -(l) 侵犯他人隐私权、名誉权、肖像权、知识产权等合法权益内容; - -(m) 其他 TapTap 平台认为不应该、不适当或在全球范围内所有适用法律禁止的内容或行为。 - -若您违反本条款,TapTap 平台有权根据具体情节,对您做出警告、追究违约金(罚款)、限制/中止提供服务、游戏下架、关闭应用入口、终止服务等处罚。您需负责赔偿因违反上述任何条款而给用户及/或 TapTap 平台或 TapTap 平台的任何合作伙伴、关联方所造成的一切损失。 - -**7. 授予许可** - -7.1 您授予 TapTap 平台在全球范围内非独占的许可,允许 TapTap 平台发布您上传至本平台的游戏(正式版或测试版),向用户提供游戏下载、预约、测试服务及/或以本协议或双方另行签署的合作协议中指定的方式发布游戏。 - -7.2 **为避免疑义,除非您与 TapTap 平台另有书面协议约定,一旦您通过 TapTap 平台向用户提供游戏预约或者测试游戏,即表示您同意授予 TapTap 平台相关非独占的许可,包括但不限于允许 TapTap 平台发布该游戏包括其任何后续版本,向用户提供该游戏预约、测试、下载服务以及本协议第 7 条所述的非独占许可,并自愿在上述许可期间内遵守本协议项下有关权利义务的规定,无论该游戏于 TapTap 平台提供预约或测试当时或之后您是否已授予或即将授予除 TapTap 平台以外的第三方在全球范围或指定区域发布游戏的独占许可。** - -7.3 您授予 TapTap 平台在全球范围内非独占的许可,允许 TapTap 平台在与以下事项相关的活动中复制、运行、展示、分析及使用游戏:(a) TapTap 平台的运营和营销活动;(b)以介绍、推广、宣传游戏为目的的公开展示;(c) TapTap 平台内部的数据共享;(d) 改善 TapTap 平台服务,以及(e)检查是否符合本协议及其他相关的平台政策。 - -7.4 您需要声明并保证您拥有游戏包含的以及与之相关的所有知识产权,包括所有必需的专利、商标、商业机密、版权或其他专有权利。如果您使用了第三方提供的内容,则需要声明和保证您有权发布游戏中的第三方内容,并取得第三方的授权文件。您同意,您不会向 TapTap 平台提交受版权、商业机密或第三方专有权利(包括专利、隐私权和公开权)保护的任何内容,除非您是此类权利的拥有者,或者此类权利的合法拥有者允许您提交这些内容。 - -7.5 **您同意并保证,在 TapTap 平台提供游戏预约、测试、试玩或下载后,为保证玩家利益及用户体验,您应持续不断地更新并提供下载。如您未履行持续更新及提供下载的义务,视为您授权 TapTap 平台有权自行更新,包括有权根据您在其他渠道发布的版本于 TapTap 平台进行自动更新等。本约定优先于您与其他渠道签订的任何协议,由此造成的所有损失由您独自承担,并且您应赔偿给 TapTap 平台造成的全部损失。** - -**8. 知识产权** - -8.1 各方均拥有其所有的权利、所有权和利益,包括但不限于与其品牌标识相关的所有知识产权。除了本协议中明确规定的有限范围之外,任何一方均不得将其任何品牌标识包含的或与之相关的任何权利、所有权或利益(包括但不限于任何隐含许可)授予另一方;另一方也不应获得这些权利、所有权或利益。根据本协议的相关条款,开发者授予 TapTap 平台及其关联公司在本协议有效期内非独占的有限许可,允许其出于通过 TapTap 平台发布游戏或与履行本协议中义务相关的用途,在线或在移动设备上展示由开发者提交给 TapTap 平台的开发者品牌标识。本协议中的任何内容均未授予开发者对 TapTap 平台的任何商号、商标、服务商标、Logo、徽标、域名或其他独有品牌特征的使用权利。 - -8.2 公共宣传。开发者同意为帮助 TapTap 充分展示游戏,TapTap 平台及其关联公司或与 TapTap 平台合作的第三方可能还会将开发者提交给 TapTap 平台的开发者品牌标识用于:(a) TapTap 平台拥有的任何在线或移动游戏/服务;(b) TapTap 平台之外的网络、移动、电视、户外(如广告牌)和印刷等广告格式(当该游戏与 TapTap 平台或平台中的其他游戏一同被提及时);(c)游戏发布通告;(d)演示文稿;以及(e)在线或在移动设备上显示的客户列表(包括但不限于 TapTap 平台上发布的客户列表)。 - -8.3 **为促进游戏的宣传推广,您授权用户有权在 TapTap 平台内发布含有您的游戏内容的图片或视频。用户通过授权发布的内容,不得违反法律法规及 TapTap 平台规范。用户的违规行为,您有权自行处理或通过 TapTap 平台规定的方式进行投诉。** - -**9. 游戏下架** - -9.1 开发者下架游戏。除非您与 TapTap 平台另有书面协议约定,下述情形下您可以主动申请终止通过 TapTap 平台向用户发布游戏(“下架”):(a)您决定终止游戏运营的;且(b)您已在所有其他安卓游戏平台或渠道下架该游戏,或保证其他平台或渠道下架该游戏的时间不晚于 TapTap 平台。您应当提前 90 日以书面通知方式告知 TapTap 平台相关决定,书面通知应当包括拟终止运营的原因、游戏在其他平台或渠道的下架计划或情况、游戏拟于 TapTap 平台下架的具体日期等,经 TapTap 平台确认并履行相关程序后方可下架。 -无论因为何种原因,如需要终止游戏运营的,您均应按照相关法规或办法的规定至少提前 60 日在游戏相关页面中向用户发布终止运营的公告,并关闭支付入口,公告应当持续发布至游戏正式终止运营之日止。 - -游戏下架,(a)不会影响之前已购买或下载该游戏的用户的许可权;(b)不会从已获得游戏的用户设备中移除该游戏;(c)不会更改您对用户之前所购买或下载的游戏或服务所承担的配送或支持义务。 - -9.2 TapTap 平台下架游戏。尽管 TapTap 平台没有义务监控游戏内容,但是如果 TapTap 平台通过您的通知、第三方投诉、用户举报、机构监管或其他方式知晓您的游戏包括其任何部分或者您的品牌特征具有以下行为:(a)侵犯第三方的知识产权或其他任何权利;(b)违反任何适用法律或受禁制令;(c)违反本协议第 6.14 条(受限内容),或者违反 TapTap 平台政策或由 TapTap 平台自行决定不时更新的其他服务条款;(d)您对其进行了不当发布;(e)可能导致 TapTap 平台或授权运营商承担法律责任;(f)被 TapTap 平台视为携带病毒,或者被视为恶意软件、间谍软件,或对 TapTap 平台或授权运营商的网络具有不利影响;(g)违反本协议或其他针对开发者的约定、条款或者规则;或者 (h)游戏的展示影响了 TapTap 平台服务器的完整性(即导致用户无法访问 TapTap 平台、该游戏或在访问前述内容的过程中遇到其他困难),则 TapTap 平台可以自行决定从 TapTap 平台中下架该游戏或重新对该游戏进行分类。 - -如果您违反了与 TapTap 平台达成的任何相关协议或违反了 TapTap 平台公开的任何规则,TapTap 平台可以依照相关协议及规则限制、暂停或终止向您提供任何服务,包括下架您的游戏。TapTap 平台保留自行决定限制、暂停并/或禁止任何开发者使用 TapTap 平台任何服务的权利。如果您的游戏包含可能严重损害用户设备或数据的元素, TapTap 平台可自行停用游戏或将其相关游戏包移除或断开相关链接。 - -9.3 **您在此确认并同意:游戏评论、游戏评分及游戏帖子均属于用户数据,相关权利属于用户或/和 TapTap 平台所有,若任何第三方擅自以转载、复制等方式使用用户数据时,TapTap 平台有权以自身名义单独进行维权,包括但不限于发送权利通知、提起民事诉讼、进行行政举报等。为保护相关用户权益,游戏下架后,除不再提供与游戏下载相关的服务外,TapTap 平台有权保留与下架游戏有关的页面包括所有产品信息、用户评论、游戏评分、动态及论坛内容等。** - -**10. 开发者账号** - -您同意自行维护您在 TapTap 平台注册的开发者账号,并且对使用您的开发者账号上传或发布的所有游戏承担全部责任。若您选择使用 TapTap 平台提供的 SDK 或其他服务,您应依据法律法规及 TapTap 平台规则进行使用。 - -**11. TapTap SDK(或 TapTap AP)服务** - -11.1 若您选择使用 TapTap SDK(或 TapTap API)服务,您需保证(并应促使您的用户保证)您(及您的用户)对本服务的使用不违反国家各项法律法规的规定,且不侵害任何第三方权益,如因此造成任何后果及损失,由您(及您的用户)自行承担全部责任。 - -11.2 您充分理解并同意(并应促使您的用户充分理解并同意),您(及您的用户)在使用本服务时,**除非法律法规允许且 TapTap 事先书面许可,您(及您的用户)不得从事以下活动,也不得同意、授权或指示任何第三人从事包括但不限于以下内容的活动:** - -(a) 删除本服务中包含的任何版权声明、商标声明或其他所有权声明;包括但不限于任何有损 TapTap 一切相关知识产权的行为。 - -(b) 向任何第三方提交错误地明示或暗示向您提供的服务为 TapTap 所属、赞助或认可的内容等; - -(c) 宣扬或提供关于非法行为的说明信息、宣扬针对任何团体或个人的人身伤害或传播任何病毒、蠕虫、缺陷、特洛伊木马或其他具有破坏性的内容等; - -(d) 对本服务进行逆向工程、反编辑或试图从本服务或本服务相关内容的任何部分提取源代码,或获取原始数据和其他数据等; - -(e) 对本服务或者本服务运行过程中释放出的任何数据或相关交互数据进行复制、更改、修改等操作,包括但不限于使用插件、外挂或非经授权的第三方工具或服务接入本服务或相关系统; - -(f) 创造任何网站或应用程序以重现或复制本服务或 TapTap 平台。 - -11.3 TapTap 对您每天发起的服务请求次数及并发请求量具有一定的限制。相关服务达到上限后,我们将可能暂时中断您(或您的用户)服务的正常使用。在双方协商一致的前提下,您(及您的用户)应在应用中正确、完整、醒目地标注「TapTap」或「技术由 TapTap 提供」的字样。 - -11.4 您(及您的用户)应对本服务的内容自行判断并决定是否使用,并承担因使用本服务及其相关内容而引起的所有风险,包括因对本服务及其内容的真实性、完整性、准确性、及时性及实用性的依赖而产生的风险。TapTap 不对此提供任何担保和保证,不对因前述风险而导致的任何后果或损失对您(或您的用户)承担责任。 - -11.5 您同意(并应取得您的用户事先同意)在此授予 TapTap 免费的、永久性的、不可撤销的、非独家的和不可转让的权利和许可,在本协议期限内使用您(及您的用户)的标志或行为来宣传您(及您的用户)对本服务的使用。 - -11.6 您的应用或服务需要收集您的用户任何数据的,必须事先获得您的用户的明确同意,且仅应当收集为应用程序运行及功能实现目的而必要的数据,同时应当告知您的用户相关数据收集的目的、范围及使用方式等,保障您的用户的知情权。 - -**12. 隐私政策及数据信息保护** - -12.1 为了不断地对 TapTap 平台进行创新和改进,TapTap 平台可能会通过 TapTap 平台收集某些使用统计数据,包括但不限于有关如何使用 TapTap 平台的信息。 - -12.2 TapTap 平台将对收集的数据进行汇总研究,以便根据用户和开发者的需求对 TapTap 平台进行完善,并会依照 TapTap 隐私政策保留这些数据。为了确保游戏得到改善, TapTap 平台将在开发者后台提供游戏在 TapTap 平台内的部分数据,若您需要额外数据,可以书面形式向 TapTap 平台申请,TapTap 平台视情况决定是否提供。 - -12.3 除 TapTap 隐私政策另有规定外,平台运营数据、用户数据等数据的全部权利,均归属 TapTap 平台,且是 TapTap 平台的商业秘密。未经 TapTap 平台事先书面同意, 您不得为本协议约定之外的目的使用前述数据,亦不得以任何形式将前述数据提供给任何第三方。 - -**13. 终止本协议** - -13.1 除非您或 TapTap 平台根据下述条款终止本协议,否则本协议将持续有效。 - -13.2 如果您希望依照第 9.1 条申请下架游戏并终止本协议的,必须提前书面通知 TapTap 平台,经 TapTap 平台同意后方可终止本协议。本协议终止之前,您不得将任意游戏下架,否则需承担违约责任。 - -13.3 如果出现以下任意一种情况,TapTap 平台有权经书面通知后单方终止本协议: - -(a) 您违反了本协议或与 TapTap 平台另行签署的合作协议的规定; - -(b) 本协议约定的其他游戏下架、服务中止或终止条件发生或实现; - -(c) 依照法律法规,相关判决、仲裁或政府机构的要求停止提供本协议项下相关服务; - -(d) 因不可抗力因素导致您无法继续使用本服务或 TapTap 平台无法提供本服务; - -(e) 您不再作为合法或有效的权利人有权使用 TapTap 平台服务; - -(f) TapTap 平台决定不再提供 TapTap 平台服务。 - -13.4 **如本协议或本服务因为任何原因终止的,对于您的账号中的全部数据或您因使用本服务而存储在 TapTap 平台服务器中的数据等任何信息,TapTap 平台可根据情况自主选择将该等信息保留或删除,包括服务终止前您尚未完成的任何数据。** - -13.5 **如本协议或本服务因为任何原因中止/终止的,您应自行处理好关于数据等信息的备份以及与您的用户之间的相关事项等,由此造成 TapTap 平台损失的,您应负责赔偿。** - -**14. 免责声明** - -14.1 **您明确了解并同意 TapTap 平台是在“现状”和“可提供”的基础上提供本服务,TapTap 平台不向您提供任何类型的担保,因此您同意自行承担使用 TapTap 平台的风险。TapTap 平台会尽最大努力向您提供服务,确保服务的连贯性和安全性;但 TapTap 平台不能保证其所提供的服务在任何程度和范围内都毫无瑕疵,也无法随时预见和防范全部法律、技术以及其他风险,包括但不限于不可抗力、病毒、木马、黑客攻击、系统不稳定、第三方服务瑕疵、政府行为等,以及因该等原因可能导致的服务中断、数据丢失以及其他的损失和风险。所以您也同意:即使 TapTap 平台提供的服务存在瑕疵,但上述瑕疵是当时行业技术水平所无法避免的,其将不被视为 TapTap 平台违约,同时,如由此给您造成数据或信息丢失等损失的,您同意放弃追究 TapTap 平台的责任。** - -14.2 **对于您使用 TapTap 平台及本服务的行为,以及用户通过您上述行为以下载或其他方式获取的任何内容,您必须自行承担全部责任和风险;对于因这种使用行为而导致的对您或相关用户的计算机系统或其他设备的损害或数据丢失,您必须承担全部责任。** - -14.3 **TapTap 平台进一步明确声明,TapTap 平台不提供任何形式的明示或默示的担保和条件,包括但不限于适销性、特定用途适用性以及不侵犯他人权利的默示担保和条件。** - -14.4 **鉴于网络服务的特殊性,TapTap 平台有权在无需通知您的情况下根据 TapTap 平台的整体运营情况或相关运营规范、规则等,随时变更、中止或终止部分或全部的服务,若由此给您造成损失的,您同意放弃追究 TapTap 平台的责任。** - -14.5 **为了向您提供更完善的服务,TapTap 平台有权定期或不定期地对提供本服务的平台或相关设备进行检修、维护、升级等,此类情况可能会造成相关服务在合理时间内中断或暂停,若由此给您造成损失的,您同意放弃追究 TapTap 平台的责任。** - -14.6 **在使用本服务的过程中,可能会遇到不可抗力等风险因素,使本服务发生中断。不可抗力是指不能预见、不能克服并不能避免且对一方或双方造成重大影响的客观事件,包括但不限于自然灾害如洪水、地震、瘟疫流行和风暴等以及社会事件如战争、动乱、政府行为等。出现上述情况时,TapTap 平台将努力在第一时间与相关方配合,及时进行修复,若由此给您造成损失的,您同意放弃追究 TapTap 平台的责任。** - -**15. 责任限制** - -**您明确了解并同意,TapTap 平台及其关联公司,以及其许可人,对于由您的上述使用行为而造成的任何直接的、间接的、偶发的、特殊的、衍生的或惩罚性的超额赔偿损失(包括任何数据的丢失),均不承担任何责任,无论是基于任何相关责任理论的论述,也无论 TapTap 平台或其代表是否已被告知或应该知道存在这种损失的可能性。** - -**16. 赔偿** - -16.1 在法律允许的最大限度内,您同意对于因为(a)您对本服务的使用行为违反了本协议,(b)您的游戏侵犯了他人的任何版权、商标、商业秘密、商业外观、专利或其他知识产权,或者诽谤他人、侵犯他人的公开权或隐私权,而导致的任何第三方索赔、法律行动、法律诉讼和诉讼过程,以及所有损失、索赔要求、损害、费用和支出(包括合理的律师费用),向 TapTap 平台、其关联公司、其各自相关的董事、主管人员、员工和代理人以及授权运营商作出赔偿,使其免受损害。 - -16.2 在法律允许的最大限度内,您同意对于因为本服务或通过 TapTap 平台发布游戏所产生的相关税款而导致的任何第三方索赔、法律行动、法律诉讼和诉讼过程,以及所有损失、索赔要求、损害、费用和支出(包括合理的律师费用),向适用的付款处理方(包括 TapTap 平台和/或第三方)、其关联公司、董事、主管人员、员工和代理人作出赔偿,使其免受损害。 - -16.3 您违反政府相关法律法规而被政府取缔运营,或因违规操作被行政监管部门查处等所引发的全部责任,由您独立承担。如因此给 TapTap 平台带来不利影响的,您应负责承担 TapTap 平台所有损失,并恢复 TapTap 平台声誉,此情况下 TapTap 平台有权单方终止本协议。如发现您有违法行为,或您提供的游戏中包含违法信息,或可能侵犯他人合法权益的信息,无论是否已造成不良后果,TapTap 平台均有权要求您立即更换、修改内容,或者单方终止本协议,并且有权要求您赔偿损失。 - -**17. 协议的变更** - -TapTap 平台可随时修改更新本协议,TapTap 平台将在本页面和/或 Dashboard 上发布通知,说明本协议中作出的更改,请定期查看本协议。更改将不具有追溯效力。更改生效且被视为开发者已予以接受的情况有以下两种:(a)对于在变更后注册成为开发者的人员,更改立即生效;(b)对于变更前既有的开发者,更改会于通知中指定的日期生效,但法律要求的更改会立即生效。**如果您对本协议的修改内容存有异议,可以书面通知 TapTap 平台并由双方协商解决,在您向 TapTap 平台发出异议通知直至双方达成一致协议或您决定接受本协议内容期间,您必须停止使用 TapTap 平台提供的相关服务。**您同意,继续使用 TapTap 平台包括平台提供的相关服务即代表您同意接受本协议中经过修改的条款约束。 - -**18. 一般法律条款** - -18.1 本协议构成您和 TapTap 平台之间的完整法律协议,且您对 TapTap 平台服务的使用将受本协议的约束,同时,本协议将完全取代您和 TapTap 平台之前就使用 TapTap 平台服务达成的开发者协议。TapTap 平台保留随时修改本协议条款、平台政策等的权利。本协议未尽事宜,双方另有约定的从其约定。 - -18.2 您同意,即使 TapTap 平台未行使或强制执行本协议中所述的(或 TapTap 平台根据任何适用的法律所享有的)任何法定权利或补救措施,也不应视为 TapTap 平台正式自动放弃这些权利,TapTap 平台仍然可以行使这些权利或采取相应补救措施。 - -18.3 您同意,TapTap 平台因您的开发者身份而向您披露的任何软件、服务、硬件、材料、文件等均属于 TapTap 的保密信息,除非获得 TapTap 平台书面同意,您不得将 TapTap 的保密信息透露给任何第三方。该等保密义务在本协议解除或终止后仍然有效。 - -18.4 如果对此类事项有司法决定权的任何法院判定本协议的任何规定无效,则 TapTap 平台将在不影响本协议其余部分的情况下,将该规定从本协议中删除。本协议的其余规定仍继续有效并可予以执行。 - -18.5 **地域限制。使用本服务或 TapTap 平台中的游戏可能会受到服务使用地相关法律和法规的限制。您必须遵守所有适用于您的游戏发布或使用所在国家或地区的相关法律、法规或政策。这些法律法规或政策包括对服务使用地、用户以及最终用途的限制。** - -18.6 未经另一方的事先书面许可,您或 TapTap 平台不得转让或转移本协议中授予的权利。未经另一方的事先书面许可,您或 TapTap 平台不得将本协议中规定的责任或义务委派给他人。其他任何试图转让的行为均无效。 - -18.7 **因本协议或您与 TapTap 平台依据本协议建立的关系而产生的或与之相关的所有申诉均受中华人民共和国大陆地区法律的约束。此外,有关本协议的任何争议应由双方秉承善意友好协商解决,若协商不成,双方同意将争议提交上海国际仲裁中心按照其仲裁规则进行仲裁,仲裁语言为中文。尽管如此,您同意 TapTap 平台仍然可以向任何司法辖区内的法院请求禁令救济。** - -18.8 TapTap 平台可能会以网页公告、网页提示、电子邮箱、手机短信、向您在 TapTap 平台账户发送站内信、快递、邮寄等方式,向您送达关于本服务的各种协议、规则、通知、提示等信息,该等信息以前述任何一种方式一经发布或发送,即视为送达,对您产生约束力。 - -18.9 本协议到期或终止后,有关保密以及争议解决条款仍然继续有效。(完) diff --git a/.ci/hk/zh-Hans/store/store-faq.mdx b/.ci/hk/zh-Hans/store/store-faq.mdx deleted file mode 100644 index 6ef2d3687..000000000 --- a/.ci/hk/zh-Hans/store/store-faq.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: 常见问题 ---- -import {FaqLink} from '/src/docComponents/doc'; - -## **一、 开发者注册及厂商账号** - -### [1. 开发者注册流程](/store/) -### [2. 如何进入开发者中心?](/store#后台入口) -### [3. 个人开发者和企业开发者可以相互转换吗?](/store#个人开发者与企业开发者有什么区别可以互相转换吗) -### [4. 个人开发者如何填写厂商名称?](/store#个人开发者没有公司如何填写厂商名称) -### [5. 如何认领厂商账号?](/store#厂商名称已存在如何进行厂商账号认领) -### [6. 如何修改厂商名称?](/store#如何修改厂商名称) - -## **二、 厂商账号后台管理员** - -### [1. 如何添加管理员?](/store/store-admin/) -### [2. 如何设置管理员权限?](/store/store-admin#管理角色配置) -### [3. 如何删除管理员?](/store/store-admin#删除游戏成员) -### [4. 如何解绑或更换主管理员?](/store/store-admin#超级管理员可以解绑吗可以设置新的超级管理员吗) - -## **三、 开发者认证** - -### [1. 什么是开发者认证?](/store/store-auth/) -### [2. 如何获得开发者认证?](/store/store-auth#如何获得开发者认证) -### [3. 如何移除开发者认证?](/store/store-auth#如何移除开发者认证) - -## **四、 物料要求** - -### [1. 图标要求](/store/store-material/) -### [2. 简介要求及展示位置](/store/store-material#简介) -### [3. 截图要求及展示位置](/store/store-material#视频及截图) -### [4. 推广图要求及展示位置](/store/store-material#推广图) -### [5. 视频要求及展示位置](/store/store-material#视频) -### [6. 资质上传要求](/store/store-material#资质) -### [7. 素材过审注意点](/store/store-material#素材过审有哪些注意点) - -## **五、 创建游戏** - -### [1. 游戏入库流程](/store/store-creategame/) -### [2. 游戏入库后需要立即发布吗?](/store/store-creategame#创建游戏审核通过后可暂不发布游戏吗) -### [3. TapTap 游戏收录标准](/store/store-creategame#taptap-是所有的游戏都收录吗) - -## **六、 游戏认领、转移及下架** - -### [1. 如何进行游戏认领?](/store/store-creategame#我的游戏已经被-taptap-收录可以进行游戏认领吗) -### [2. 如何进行游戏转移?](/store/store-creategame#游戏主体可以进行转移吗) -### [3. 如何进行游戏下架?](/store/store-creategame#如何进行游戏下架) - -## **七、 游戏更新** - -### [1. 如何进行游戏更新?](/store/store-update/) -### [2. 如何进行游戏更名?](/store/store-update#游戏名称可以修改吗) - -## **八、 游戏测试** - -### [1. 安卓端测试流程](/store/store-test/) -### [2. 安卓端测试形式](/store/store-test#安卓端测试形式) -### [3. iOS端测试流程](/store/store-test#ios-端测试流程) -### [4. 关于测试服](/store/store-test#什么是测试服测试服有什么优点如何创建测试服) -### [5. 如何获得测试资源位?](/store/store-test#测试资源) diff --git a/.ci/hk/zh-Hans/store/store-material.mdx b/.ci/hk/zh-Hans/store/store-material.mdx deleted file mode 100644 index 4388cd91c..000000000 --- a/.ci/hk/zh-Hans/store/store-material.mdx +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: 物料要求 -sidebar_position: 40 ---- - -import { Red, Blue } from "/src/docComponents/doc"; - -其中 \* 标注为必须提交的资料 - -## 图标 - -尺寸为 512x512px,格式必须为 JPG 或 PNG,你上传的图片会进行圆角裁剪(半径 114px),请将主要内容控制在下图所示的安全区域(Safety Area 内)。 - -![](/img/Assets-Requirements-1.png) - -## 文案 - -### 简介 - -简介在游戏详情页中展示于视频及截图下方,游戏评价上方; - -可用文字详细描述游戏的类型、玩法以及特色; - -简介中文字请勿空行,以免影响在客户端的展示。 - -### 更新日志 - -更新日志在游戏详情页中展示于游戏简介下方,点击 **了解更多** 查看; - -更新日志指版本更新,初次提交或预约状态的游戏请勿填写更新日志,以免误导玩家; - -更新日志用语建议简洁明了,仅对本次更新内容进行说明; - -## 视频及截图 - -在游戏详情页中,视频及截图将展示在游戏详情页游戏标题的上方。 - -视频将展示在最前面,未上传视频时,将按上传顺序展示截图。 - -![](https://capacity-files.lcfile.com/c3yv7ScLsO8DKr3RbmULsCQVOB7ItdU9/Assets-V2en-1.png) - -### 视频 - -游戏视频格式:MP4; - -大小:1GB 以内; - -时长:建议视频时长控制在 20s 左右(让玩家了解视频重点内容的最佳时长),视频时长请勿超过 1min - -分辨率:1280 x 720px 及以上,推荐 1920 x 1080px; - -**当有游戏视频时,必须提供视频封面** - -视频封面尺寸:1256x706 px,格式必须为 JPG 或 PNG。 - -除游戏详情页外,视频还将用于首页智能推荐。**视频可选择性提供。** - -当未上传视频时,将展示推广图;视频推广图均未上传时将展示详情页顶部图。 - -建议在视频内呈现的内容:游戏新版本/新角色/新玩法素材、节日相关素材等并及时进行视频更新。 - -![](https://capacity-files.lcfile.com/GiJvMAhveKODMnvSYdh58JKR6h58mmMo/Assets-V2en-2.png) - -### 截图 - -请确保每张截图尺寸一致,截图格式必须为 JPG 或 PNG,至少 3 张,请勿上传相同截图; - -截图文件大小不宜过大,可能会导致上传速度及打开游戏详情页时加载速度过慢。 - -横版游戏截图比例建议:16:9,尺寸:1280x720 px 及以上; - -竖版游戏截图比例建议:9:16,尺寸:720x1280 px 及以上。 - -在游戏详情页中,截图将展示在游戏简介上方,游戏名下方。 - -![](https://capacity-files.lcfile.com/hpw19sE7Pux0G5y0hckmyrvRF331KRaG/Assets-V2en-3.png) - -## 详情页顶部图 - -### 用途 - -该图将展示在 TapTap(2.5.0 版本及以上)游戏详情页顶部,**详情页顶部图必须提供**。 - -![](https://capacity-files.lcfile.com/bIvVd5o3nQxFNHbwSN16BlXVc50inGnq/Assets-V2en-4.png) - -### 设计建议 - -详情页顶部图一般用以突出该游戏的品牌形象,建议使用游戏主要角色及游戏 Logo 作为该图主要内容;并且确保素材及 Logo 清晰可见。 - -除了游戏标题外,请勿引用或使用其他文本。 - -请将游戏关键内容控制在安全区(见下方标注)内,以确保关键内容在不同场景下的完整展示。 - -### 尺寸及格式 - -详情页顶部图尺寸为 1920x1080 px(16:9),格式必须为 JPG 或 PNG - -请将宣传的主要内容(如游戏 Logo、主要人物等)控制在安全区域 1760x920px 内。 - -![](/img/Assets-Requirements-8.png) - -## 推广图 - -### 用途 - -该图将用于所有 TapTap 上的游戏卡片,例如游戏合辑、首页智能推荐,**推广图可选择性提供** - -若未上传推广图,展示时,将使用详情页顶部图;已上传视频时,推广图将在视频自动播放前展示。 - -### 设计建议 - -为了达到最佳效果,建议使用用以营销、推广游戏的素材;亦可使用最新版本相关素材,同时也确保及时进行更新;请务必确保素材及 Logo 清晰可见。 - -除了游戏标题外,请勿引用或使用其他文本。 - -### 尺寸及格式 - -详情页顶部图尺寸为 1920x1080 px(16:9),格式必须为 JPG 或 PNG - -## 资质 - -### 授权 - -如果你的游戏涉及到授权,请提供独代授权书扫描件或 IP 类授权书。如果存在多级授权,请提供所有独代授权书扫描件。 - -授权书格式必须为 JPG 或 PNG,每张图片大小不超过 2M,最多可上传 10 张。 - -## Q&A - -### 素材过审有哪些注意点? - -游戏素材请勿含有侵权内容,若游戏素材涉及 IP,请提供授权; - -素材中不得含有涉及政治、赌博、暴力、色情等违反国家法律法规的内容。 diff --git a/.ci/hk/zh-Hans/store/store-notifications.mdx b/.ci/hk/zh-Hans/store/store-notifications.mdx deleted file mode 100644 index aff3dba18..000000000 --- a/.ci/hk/zh-Hans/store/store-notifications.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: 平台自动通知说明 -sidebar_position: 31 ---- - -import { Blue } from "/src/docComponents/doc"; - -TapTap 平台会基于开发者的操作行为,触发自动通知,目前平台会基于以下场景发送自动通知。开发者可根据实际需要自主配置是否接收通知。 - -- 关于发送对象:当前平台通知主要面向厂商超管、游戏主管理员等角色,开发者可前往后台 权限管理 模块自主配置所需角色。 -- 关于发送形式:当前平台支持邮箱、站内信等发送形式,开发者可前往后台 消息中心-接收设置 模块自行管理相关联系方式。 - -## 自动通知类型说明 - -一、开发者入驻与资料管理 - -| 通知名称 | 场景 | 对象 | 发送形式 | -| -------------------------- | ------------------------------------------ | ------------------------ | ------------ | -| 开发者入驻审核通过通知 | 成为认证开发者审核通过 | 提交成为认证开发者的用户 | 邮箱 | -| 开发者入驻审核失败通知 | 成为认证开发者审核失败 | 提交成为认证开发者的用户 | 邮箱 | -| 开发者信息变更审核通过通知 | 已入驻的开发者,提交开发者信息变更审核通过 | 厂商超管 | 邮箱、站内信 | -| 开发者信息变更审核失败通知 | 已入驻的开发者,提交开发者信息变更审核失败 | 厂商超管 | 邮箱、站内信 | - -二、游戏上架 - -| 通知名称 | 场景 | 对象 | 发送形式 | -| ---------------- | ------------------------------------------------ | ---------- | ------------ | -| 游戏审核通过通知 | 开发者提交游戏后,游戏审核通过 | 游戏管理员 | 邮箱、站内信 | -| 游戏审核失败通知 | 开发者提交游戏后,游戏审核失败 | 游戏管理员 | 邮箱、站内信 | -| 游戏定时上线通知 | 游戏设置定时上线,审核通过后,到达定时上线的时间 | 游戏管理员 | 邮箱、站内信 | diff --git a/.ci/hk/zh-Hans/store/store-register.mdx b/.ci/hk/zh-Hans/store/store-register.mdx deleted file mode 100644 index 1ca83d682..000000000 --- a/.ci/hk/zh-Hans/store/store-register.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: 开发者注册 -slug: /store -sidebar_position: 30 ---- - -import { Red, Blue, Black, Gray } from "/src/docComponents/doc"; - -## 申请通道及流程 - -进入 **[开发者中心](https://developer.taptap.io/)** >> 点击 **[申请成为开发者](https://developer.taptap.io/developer-apply/)** >> 选择需要申请的开发者类型(注册开发者/认证开发者)>> 填写资料 >> 提交审核(审核时间:预计 2 个工作日) >> 通过审核后即可进入开发者后台 - -| **入驻类型** | **前提条件** | **必填信息** | **是否需要审核** | **厂商详情页** | -| ---------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------- | -| **注册开发者** | 具备可用的[Tap.io](https://accounts.taptap.io/signup)账号 | 厂商名称、联系邮箱(需要验证) | 无需平台审核 | 在[Tap.io](https://www.taptap.io/)不展示 | -| **认证开发者(推荐)** | 具备可用的[Tap.io](https://accounts.taptap.io/signup)账号 | **个人主体**
    • 厂商名称、开发者姓名、联系地址、身份证正反面
    • 联系邮箱(需要验证)

    **企业主体**
    • 厂商名称、注册主体名称、联系地址、注册主体资格证明文件
    • 联系邮箱(需要验证)
    | 需要平台审核 | 在[Tap.io](https://www.taptap.io/)不展示 | - -![](/img/DC-v2/Register-as-Develper-V2-1.png) - -## 注册开发者/认证开发者区别 - -- **注册开发者** - - 开发者后台 **厂商设置** 处会标记「注册开发者」身份,厂商注册信息支持修改,提交后自动通过审核;页面上增加「成为认证开发者」入口,点击可跳转到「认证开发者填写页面」; - - 注册开发者仅拥有厂商权限(厂商设置、权限管理、工单等)、游戏运营(上架游戏、查看概览、提交资质等)相关权限; -- **认证开发者** - - 开发者后台 **厂商设置** 处会标记「认证开发者」身份,厂商注册信息支持修改,修改后需提交审核; - - 认证开发者拥有厂商权限(厂商设置、权限管理、工单等)和游戏运营(上架游戏、查看概览、提交资质等)相关权限,还拥有财务权限(设置财务主体、游戏服务账单)以及游戏服务(TDS 技术服务功能、数据分析)权限。 - -如果您当前为「注册开发者」,需要在 TapTap 上架付费游戏,点击补充厂商财务主体时,系统就会自动触发跳转注册「认证开发者」界面。 - -如果您当前需要使用「游戏服务」的功能,点击 **游戏配置** 页面,系统也会自动跳转至注册「认证开发者」的界面。 - -## 提交审核 - -认证开发者申请提交后,TapTap 运营团队预计会在 2 个工作日内完成审核,审核结果会在 个人中心-通知 中告知您。 - -您也可以在申请开发者账号的页面中查看当前审核状态: - -- 若状态显示为「审核中」,此时无法重新提交申请,请耐心等待审核结果; -- 若状态显示为「审核失败」,请您按照驳回原因修改后重新提交审核; - -## 后台入口 - -当您的开发者账号审核通过后,点击进入 **开发者中心**,即可进入开发者后台。 - -![](https://img.tapimg.com/market/images/9f2a0358f9f38cdb7bd1da7fe4699a27.png) - -## Q&A - -### 注册开发者与认证开发者身份可以互相转换吗? - -当前 TapTap 暂不支持开发者身份的相互转换,**请根据实际情况选择开发者类型进行申请,我们更建议您申请成为「认证开发者」。** - -### 个人开发者没有公司,如何填写厂商名称? - -我们建议您以团队或工作室名称而非个人名称作为厂商名称,例如:xxx 工作室。 - -### 厂商名称已存在,如何进行厂商账号认领? - -您可以通过 **开发者后台**>>**工单** 联系我们,或将您的需求发送至运营部邮箱 [international_operation@taptap.com](mailto:international_operation@taptap.com),请提供企业运营执照或其他能证明您是该厂商官方人员的材料,并以相同厂商名称提交开发者申请。 - -**邮件格式** - -> **邮件主题** TapTap 厂商账号认领申请 - XXX 厂商名称 -> -> 需认领的厂商名称或厂商页链接: -> -> 营业执照扫描件或相关证明材料(可以附件形式上传) : -> -> 申请人联系方式: - -您的开发者申请审核通过后,即完成开发者账号认领,您将自动成为该厂商主管理员。 - -TapTap 运营人员预计将在 2 个工作日内处理完毕。 - -### 如何修改厂商名称? - -您可以通过 **开发者后台** >> **工单** 联系我们,或将您的需求发送至运营部邮箱 [international_operation@taptap.com](mailto:international_operation@taptap.com),并提供企业营业执照扫描件或其他能证明您是该厂商官方人员的材料。 - -**邮件格式** - -> **邮件主题** -> TapTap 厂商名称变更申请 - XXX 厂商原名称 -> -> 需修改的厂商名称或厂商页链接: -> -> 新厂商名称: -> -> 营业执照扫描件或相关证明材料(可以附件形式上传) : -> -> 申请修改厂商名称原因(简述): -> -> 申请人联系方式: - -TapTap 运营人员预计将在 2 个工作日内处理完毕。 - -### 开发者账号审核通过后需要立即进行游戏入库吗? - -不需要。 - -TapTap 对创建游戏时间无要求,请开发者根据自己的时间安排进行游戏创建;TapTap 建议开发者尽早进行游戏入库,积累预约用户。 diff --git a/.ci/hk/zh-Hans/store/store-test.mdx b/.ci/hk/zh-Hans/store/store-test.mdx deleted file mode 100644 index 973ab17d5..000000000 --- a/.ci/hk/zh-Hans/store/store-test.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: 游戏测试 -sidebar_position: 60 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -## 安卓端测试流程 - -### 测试信息同步 - -确认测试时间后,请将游戏测试信息发送至运营部邮箱[international_operation@taptap.com](mailto:international_operation@taptap.com) - -**邮件格式** - -> **邮件标题]** -> TapTap 游戏开测信息同步 - XXX 游戏名称 -> -> 游戏名称: -> -> 是否为限量测试: -> -> 站内页面链接: -> -> 预下载时间(无预下载需求可不填):X月X日 XX:XX -> -> 开服日期、时间(无服务器写开下载时间):X月X日 XX:XX -> -> 删档/不删档 -> -> 计费/不计费 - -游戏开测后,TapTap 运营人员将主动向游戏安卓预约用户进行通知推送。 - -### 测试状态变更** - -你需要在开测前一天将游戏状态修改为测试,并上传 apk 进行审核,apk 置为当前的时间需要与开放测试的时间保持一致,通过审核后,游戏详情页的按钮会在测试开始的时候变更为「开放下载」。 - -![ ](/img/Game-Testing-1.png) - -### iOS 端测试流程 - -TapTap 仅支持以 TestFlight 形式进行 iOS 端游戏测试。 - -请在邮件中同步本次测试时间和 TestFlight 公开链接,运营同学会帮你将公开链接配置到后台中,随后将 iOS 游戏状态改为「测试 」即可。 - -用户可以通过点击网页版 TapTap 游戏详情页的「下载」按钮申请测试,将会引导他安装 TestFlight。 -若已经安装,会唤起 TestFlight 软件,并且直接获取下载权限。 - -### 安卓端测试形式 - -1. 不限量测试 - - 安卓不限量测试请参考[安卓测试流程],及时上传 apk 并与 TapTap 运营人员同步测试信息。 - -2. 限量测试 - - - 限定人数关闭下载 - - 到达期望下载人数后,可将安卓商店状态修改为「预约」或「敬请期待」,关闭下载。 - - - 激活码限量测试 - - 你需要提前在后台上传激活码,并邮件通知 TapTap 运营同步限量测试具体信息,TapTap 运营将为你进行操作。 - -![ ](/img/Game-Testing-2.png) - -## 测试资源 - -TapTap 运营将会与编辑同步游戏测试信息,TapTap 不会对游戏进行评级,编辑将根据游戏测试时间以及游戏品质安排游戏测试资源位。 - - diff --git a/.ci/hk/zh-Hans/store/store-update.mdx b/.ci/hk/zh-Hans/store/store-update.mdx deleted file mode 100644 index 593a88f18..000000000 --- a/.ci/hk/zh-Hans/store/store-update.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: 游戏更新 -sidebar_position: 55 ---- - -import { Blue } from "/src/docComponents/doc"; - -### 更新位置及入口 - -游戏上架后,建议您时常更新在架游戏详情页素材,这样可以提升游戏的下载转化率和站内的曝光度,您可以在 **开发者后台** >> **商店资料** 位置点击 **构建新版本** 完成更新操作。 - -- 构建的新版本会自动读取已上线版本填写的详情页内容,您可以在此基础上对需要更新的内容进行修改 - -![](/img/DC-v2/update-V2en-1.png) - -更新游戏同样支持定时上线功能,您可以在 **发布设置** 中选择当前更新版本的发布时间 - -- 若发布设置中选择 **「立即发布」** ,新版本素材/APK 通过审核后即会更新到游戏详情页中; -- 若发布设置中选择 **「定时上线」** ,通过审核后,新版本素材/APK 会在设置的时间发布到游戏详情页中。 - -注意:定时上线时间目前仅支持选择当前时间 24h 之后的时间 - -### 草稿功能 - -在新创建的未发布版本中,填写完更新内容后,您可选择保存为草稿或者直接提交审核。 - -- 保存草稿 >> 再次打开游戏页面可以直接读取当前存储的草稿内容。 -- 目前仅可储存一份草稿,再次保存草稿时将会自动覆盖前一版本。 - -![](/img/DC-v2/update-V2en-2.png) - -### 提交审核 - -游戏更新审核提交后,预计将会在 24 小时内得到审核结果,审核结果将会在个人中心-通知中心告诉您。 - -### 查询版本记录 - -您可以在 **开发者后台** >> **版本记录** 中查询所有历史版本的审核状态、操作日志、APK 版本名称、版本号等信息,也可以点击 **版本详情** 查看该版本提交的所有内容。 - -## Q&A - -### 游戏名称可以修改吗? - -可以。 - -修改游戏名称时,请同步修改游戏图标、推广图、截图等资料中的涉及到游戏名称的信息并提交审核。 - -我们建议游戏修改名称后,在站内发布内容帖向玩家说明修改游戏名称的原因,以免给玩家带来不良游戏体验,影响游戏评分。 - -### 游戏包名可以修改吗? - -可以,但是不建议您修改。 - -包名变更会导致之前安装过的玩家出现存档丢失,需要重新安装等情况,如确定需要修改,我们建议您在站内发布内容贴向玩家说明修改游戏包名的原因,并将您的需求提交到 **开发者后台** >> 工单 系统中,或通过运营部邮箱 [international_operation@taptap.com](mailto:international_operation@taptap.com) 联系我们。 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6c973f2c7..6c23c3165 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v2 with: - token: ${{ secrets.VERSION_TOKEN }} + token: ${{ secrets.CHECKOUT_TOKEN }} - name: Deploy to LeanEngine uses: enflo/curl-action@v1.2 diff --git a/docs/aws-partner.mdx b/docs/aws-partner.mdx deleted file mode 100644 index e8e4d7fd2..000000000 --- a/docs/aws-partner.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: TapTap is an AWS software partner for global game developers -slug: /aws-partner/ ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; - -TapTap—China's active mobile game community—has officially become Amazon's software partner. The two parties will aggregate each other's technology and publishing capabilities, starting from 0, to help developers going overseas to solve the problem of early going overseas, reduce developers costs, and provide overseas games more comprehensive support. - -TapTap is a zero-commission app store that lets gamers download gaming applications onto Android mobile phone while content providers are able to take 100% revenue. Meanwhile, TapTap has a robust community of developers and gamers that safely use the application. Since TapTap includes a community of developers that can add their apps to the platform, multiple versions of the same game can be found like TapTap PUBG. - -

    - TapTap -

    - -TapTap is trying to remodel the 30% revenue cut taken by Apple’s App Store and Google Play — announcing a 0% revenue share with game developers. After two years of exploration and development, TapTap International has users in more than 170 countries (regions). In April this year, the new version of TapTap International was launched, serving overseas players with a new look and exciting functions. TapTap Developer Service (TDS) is also gradually opening more functions in the international version to provide support for overseas developers and domestic overseas manufacturers. - -

    - AWS Partner Network -

    - -AWS helps TapTap International Edition to provide Amazon Web Services Cloudfront, Amazon EC2, Amazon RDS, and other services, and the global cloud infrastructure provides support for TapTap International Edition. As the pioneer and leader of cloud computing, AWS provides more than 200 full-featured cloud computing services around the world. AWS provide almost unlimited scalability, so we can scale our application automatically as we continue to grow and add new customers. - -Through cooperation with AWS, Taptap has upgraded the underlying architecture, improved the security of the architecture, expanded international delivery capabilities, and improved the experience of gamers and developers. - -

    - -

    - -Up to now, a number of games such as "Sausage Man" and "Flash Party" have completed the whole process of publishing, testing and launching through the TapTap international version. In addition, TapTap's unique community system also brings novel and effective operation methods for the game's overseas platform distribution. TapTap International's "Sausage Man" launched in July 2021. Its game forum has gathered more than 1.4 million players and published nearly 3,000 discussion posts. Developers hold various activities in the community to enhance user stickiness and retention. - -Benefiting from TapTap's zero-commission business model, well-known game developers such as Epic Games have also partnered with TapTap. The worldwide popular Fortnite is now officially exclusive to TapTap, offering download, installation and forum services. Currently, Fortnite has recorded over 10 million downloads on TapTap. - -

    - Sausage Man -

    - -According to XD’s financial report in 2021, the MAU of the TapTap international version has grown to 12.24 million, and it still maintains a high growth rate. Based on tens of millions of MAUs, in addition to the basic distribution capabilities, TapTap has the ability to provide more platform-based one-stop services for domestic developers going overseas. For example, "bonfire testing" or "beta testing" can help developers acquire seed users and provide a sufficient and diverse population for each level of testing. Users help developers tune the game through the mechanism of community feedback and grow in testing. For global testing, developers generally cannot easily open Google and iOS stores in key countries such as the United States, Germany and France during the testing period. Only some small-volume countries such as the Philippines, Canada, etc. can be opened for testing. With the help of TapTap, we can introduce more users from core countries, especially users in Tier-1 countries. The number of users in TapTap international is quite stable, which help developer solve the problem of user volume during the test period. In global testing process, advertising purchases are often limited by the insufficient cold start speed and the small purchase area. It is difficult to import sufficient number of users on the test day or the next day. - -
    - PUBG: NEW STATE -
    - PUBG: NEW STATE open second alpha test TapTap International -
    -
    - -TapTap Developer Service (TDS) is releasing its service capabilities for TapTap international developers after repeated verifications in China. Taking the first batch of “Flash Party” that received a version number this year as an example, the game launched on “TapTap International” in February 2022, and access the TapTap Developer Service (TDS) Tap Login, friend system, embedded community and other functions open up the social system for "Flash Party", which is convenient for players to exchange games and view strategies. At the same time, various operational activities can directly reach players in the game, improving player retention and duration. - -
    - Flash Party -
    Flash Party Moments
    -
    diff --git a/leancloud/docs/classroom.mdx b/docs/classroom.mdx similarity index 100% rename from leancloud/docs/classroom.mdx rename to docs/classroom.mdx diff --git a/docs/community/advanced.mdx b/docs/community/advanced.mdx deleted file mode 100644 index b5d43cfd3..000000000 --- a/docs/community/advanced.mdx +++ /dev/null @@ -1,329 +0,0 @@ ---- -title: 挑战进阶操作,玩转社区运营 -sidebar_label: 挑战进阶操作 -sidebar_position: 30 ---- - -## 4.1 如何做好一次活动 - -![做好活动](/img/events-keypoints.jpg) - -社区活动是促进论坛活跃、与玩家高效交流的有力途径。TapTap 强活跃高反馈的社区属性,决定了此处的玩家相较于一般游戏发行平台、信息发布渠道、玩家自发聚集地等,拥有更频繁的社交潜力与创作潜力,这就要求游戏开发者要更注重社区活动带来的种种益处。 - - - - - - - - - - - - - - - - - - - - - - -
    核心操作重点解读
    起一个好标题标题是活动发布前需要处理的重中之重,良好的标题设计,可以快速吸引玩家眼球标题的设计可以从版本热点、奖品噱头、参与方式等方面进行入手。
    设计参与方式活动参与方式分回复类、转发类、发帖类、视频类、共创类等。回复、转发活动门槛低参与率高,游戏热度表现一般时比较适合。社区发帖、视频投稿活动门槛高参与率低,相比而言更适合热度较高的游戏。在活动发布后可对优质内容进行转发 / 回帖 / 汇总展示以兹鼓励。
    避免无价值内容许多厂商在设计多平台分发的活动时,习惯于在官网、H5、公众号、短视频平台等渠道,承接一套截图转发的固定分享流程。导致大量用户在 TapTap 论坛发截图刷屏,此时需要开设裂变活动分享的子版块并不予于首页展示或由厂商发布集合分享帖引导用户集中在评论区进行分享。设计此类活动时请谨慎!TapTap 是用户兴趣偏集中的交流型社区,如果论坛充斥过多无价值内容,就会反感和流失,对舆论评价也会造成负面影响。
    - -以下三种参与方式在 TapTap 社区相对常见,但请不要限制你的想象力,玩家是创意度和热情度极高的群体,你的脑洞会决定活动上限。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    参与方式模式说明适用阶段
    发帖参与 -

    发帖可分为图文、纯文字、视频;

    -

    - 内容门槛高价值大,需要玩家具有一定创作能力,对不了解游戏内容的玩家参与门槛较高,预约期慎重举办; -

    -

    长期看对社区价值更高(提高内容沉淀);

    -

    - 此类活动需要较大奖品池以吸引玩家的目光,常见手机、游戏机、现金等噱头奖品。 -

    -
    -

    - 测试阶段: - 内容可设计为分享游戏故事,捏脸比拼,同人二创等方向 -

    - -
    -

    - 开服阶段: - 为了让玩家快速沉淀在社区,需要用奖品刺激玩家分享门槛较低的简单游戏内容,包括装扮分享、昵称分享、答题活动等 -

    - -
    -

    - 长尾运营阶段: - 为产出更多优质UGC内容,需要引导玩家分享游戏攻略、欧皇晒卡、通关竞速、同人二创、版本内容打卡等 -

    - -
    回帖参与 -

    回帖可分为图片、图文、视频;

    -

    - 此类活动参与门槛较低,互动性更强,社区价值较少,适合吸引玩家快速关注,或短期加入活动,不适宜长期留存; -

    -

    简单的回帖互动(建议征集、祝福、玩梗、分享等);

    -

    - 奖品池需求较小,前期以测试资格为主,上线后常见游戏周边、TapTap游戏联名道具/周边、游戏激活码。 -

    -
    -

    - 测试阶段: - 为了引起玩家的关注度,需要引导玩家参与讨论。包括测试资格互动,开发者面对面互动,游戏共创互动,测试分享心得等 -

    - -
    -

    - 开服阶段: - 为了吸引玩家关注游戏动态且有更好的讨论氛围,需要玩家能够快速切入到讨论的话题中。包括道具取名共创、捏脸分享、送祝福、游戏故事等 -

    - -
    -

    - 长尾运营阶段: - 为了活跃核心玩家,需要了解大家讨论的点并整理成回帖分享。包括版本最强组合、皮肤最美比拼、一句话形容XXX等 -

    - -
    互动参与 -

    玩家点赞或转发,参与更简单;

    -

    - 适合配合大型活动的小活动部分或需要快速出具活动的情况,能快速引起玩家强互动,对社区内容价值没有增益,只提高活动周期内论坛活跃度; -

    -

    - 做站外转发有较大门槛,活动参与步骤相对繁琐,但能够渗透私域流量,达成外部曝光,转换率较低; -

    -

    - 做此类活动需要发放若干数量的奖励,常见游戏福利、周边、测试资格等。 -

    -
    -

    - 测试阶段: - 为了吸引玩家的关注度,可开放预约活动对玩家进行引导,预约奖品价值可以偏多走量 -

    - -
    -

    - 开服阶段: - 为了吸引玩家体验游戏,可开放下载送礼、转发朋友圈、对玩家进行引导等 -

    - -
    -

    - 长尾运营阶段: - 为了让社区玩家更加活跃,可发布优质内容帖让玩家转发/点赞/评论互动有奖 -

    - -
    - -## 4.2 如何与玩家互动 - -互动在 TapTap 社区拥有独特而必要的价值,开发者在完全透明的沟通基础上,可以无任何顾虑与玩家一对一平等沟通。 - -社区内的发言全部玩家可见,可检索,可修改(会存有修改记录)。玩家在任何时段,对游戏的总体感受,构成了消费者对产品的信任标签。开发者可以随时参考过往标签,以及新鲜出炉的标签,对产品形态进行深度思考。 - -### 落地到方法,社区互动有如下要点 - -评价的互动,请务必真实,尊重玩家发言,心平气和与玩家沟通,做到不敷衍,不冷漠。真诚的交流会给予玩家极大安全感,此处也往往是很多开发者在运营思路上的雷区。 - -同时,请务必注意在互动时不要复制粘贴发言,极易引起玩家反感。面对长评价、帖子热评、用心的差评时,要及时回复,建立良好形象,官方回复会默认在玩家评论下优先显示,后续阅览的玩家会在阅读热评时看到开发者留言。 - -![官方回复](/img/official-replies.png) - -论坛内的互动,以回帖互动为主,玩家会根据需要在各个子版块发帖,讨论游戏内容和社区内容。因为讨论量大且内容相对庞杂,建议提前从社区论坛规划入手,为玩家建立适当的各类讨论区,然后有针对性地在热门讨论区和热门回帖下进行回复。 - -![回帖互动](/img/reply-to-popular-posts.png) - -在社区发布你的开发动态、制作进展,在 TapTap 社区,玩家很愿意第一时间了解游戏的新进展,通过交流研发心得,一步步将游戏设计的特点展示给你的玩家。介绍游戏背景,解构美术风格,甚至开展一次共创活动!在未来,优秀的游戏将由开发者与玩家共同完成。 - -![宠物蛋设计大赛](https://img.tapimg.com/market/images/70773c5b5fe1e3242228e1095fe72d29.png) - -可参考:https://www.taptap.cn/topic/19447866 - -在与玩家互动的过程中,开发者需要严格遵循[社区规范条例](https://www.taptap.cn/topic/104247),维护社区和谐氛围。同时,作为游戏论坛的管理者,开发者有义务规范并监督已有游戏论坛的玩家氛围,如遇到风险内容、争议内容、垃圾内容,需根据情况进行隐藏或删除处理,或联系 TapTap 社区进行进一步处理。 - -及时清理低价值内容,并做到不引导用户发布无价值信息(可能会导致刷屏的活动、言论等) - -## 4.3 常见舆情分析 - -在运营游戏论坛时,经常会出现各类舆情问题,我们建议善用论坛功能,及时发现问题,快速解决问题。 - -- 版务方面:观察近期论坛帖的讨论方向,针对大量水帖(包括无价值内容的活动参与帖)可使用子版块归类和隐藏,并引导玩家前往固定子版块 / 推荐位发帖。 -- 公关方面:如遇到恶性问题,比如因游戏内容或其他突发状况(如服务器事故等),第一时间发布官方公告,并提供解决方案。 - -用子版块隐藏处理水帖的方法,具体操作流程为:设置水帖子版块 >> 查明水帖并移进子版块 >> 子版块内容隐藏 - -![子版块隐藏](/img/hide-subforum.png) - -除了使用论坛内工具外,可以根据需要设置版主,并定期进行版主操作审核。可前往论坛管理中心查看版主日志,观察舆情处理情况。 - -![查看版主日志](/img/moderation-logs.png) - -在上述操作外,论坛同样支持删帖、禁言操作,遇到严重恶意评论,可以在确认为垃圾内容时进行操作。但请注意,论坛环境为平台、开发者、玩家三方共同建设,删帖、禁言操作会给到玩家相关通知,出于舆情稳定考虑,建议慎用此功能,防止舆情反弹。 - -### 游戏运营过程中常见的舆情问题 - - - - - - - - - - - - - - - - - - - - - - - - - - -
    常见阶段常见问题
    预约期素材抄袭、玩法争论、玩法借鉴、宣传与实物不符等。
    测试期限额测试导致玩家未获取资格、长期预约但未在 TapTap 测试、预下载方式、测试 / 激活码发放、测试包安装、测试服炸服、提前偷跑、测试内容及效果争论、测试服充值未到帐、充值规则误解等。
    上线期预下载方式、炸服、充值、客诉、游戏内容引发的争论、黑客攻击、长时间无法登录等。
    上线后版本更新引发的 bug、玩法添加 / 变更引发的争论等
    - -## 4.4 遇到舆情风险怎么办 - -我们统计了游戏不同阶段会碰到的舆论风险,官方可视情况提前准备舆论方案,同时我们建议处理舆论把握两个关键点: - -**1.快** - -**2.态度真诚且正面回应问题** - -参考案例:《弈剑行》 - -《弈剑行》遇到的问题 1:开服当天遭遇小段时间炸服;玩家对氪金的质疑 - -解决办法:官方在第一时间发布公告说明炸服原因 + 炸服补偿(直面玩家问题,说出对付费设计的思考)。 - -![说明炸服原因](/img/kdUfrlmTi48AlkCDQgHzGg.png) - -处理结果:最终评分维持在 8.8 分 - -《弈剑行》遇到的问题 2:开服次日遭到黑客攻击 - -解决办法:直面问题,第一时间向玩家说明情况 + 吃定心丸,承担玩家全部损失 + 全额退款,反击黑客,树立勇敢且正义的形象。 - -![说明黑客攻击问题](/img/IW_WSrAvhDIl2twTLSnZhA.png) - -处理结果:玩家对官方的信赖度迅速升高,评分从 8.8 上升至 9.4。20 天后单机版上线,评分稳定在 9.4,保持了极高的口碑。 - -总结:当突发情况发生时,官方直面舆论的态度,与迅速处理舆论的方法非常重要,请尽量在状况发生后的一小时内发布公告声明。 diff --git a/docs/community/features.mdx b/docs/community/features.mdx deleted file mode 100644 index ba3b2f1e6..000000000 --- a/docs/community/features.mdx +++ /dev/null @@ -1,237 +0,0 @@ ---- -title: 学习社区模块,掌握基本操作 -sidebar_label: 学习社区模块 -sidebar_position: 10 -toc_max_heading_level: 2 ---- - -import { Anchor } from "/src/docComponents/doc"; -import useBaseUrl from '@docusaurus/useBaseUrl'; - -在运营方面,如何最大化展示游戏特色,并持续提供给玩家新鲜感,是在 TapTap 社区良好运营的核心法则。同时,时刻注意论坛内玩家行为,以此为依据调整并随时更新运营规划。我们将在下述内容中,逐步讲解 TapTap 社区的各个功能模块,及运营思路。 - -## 2.1 沟通入门课:发帖 - -### 发帖前的第一步 - -前往开发者后台填写权限信息,按运营需要对发布官方帖的账号添加多项权限,具体流程如下: - -前往**权限管理** >> **角色配置**,点击**查看角色**,设置该角色的**基础信息**、**角色类型**、**角色权限**,默认角色无法修改权限。 - -对需要发帖的角色,请务必设置「发布官方帖」权限。相关问题请参考[权限管理](/store/)。 - -![gameadmin](/img/a08674151cb066ee2866a9e482d983bb.png) -![gameadmin](https://img.tapimg.com/market/images/89286efad13b588337a4cd381d9da97c.png) - -### 发帖前的第二步 - -为你的账号添加蓝 v 标识,前往**开发者中心** >> 点击**右上角工单**,**创建工单**提交申请为账号**添加蓝 v 认证**。 - -你的申请将于 1-3 个工作日内完成,完成后账号效果如下图显示。 - -蓝 V 认证展示 - -### 如何创作官方帖? - -官方帖是于 TapTap 社区发布信息,与玩家沟通的主要方式。在游戏登录 TapTap 后各个阶段,都需要针对运营需求设计并发布官方帖。 - -建议官方在**无节点**时,在 TapTap 社区**保持周更**的频率,持续与玩家互动;在测试、上线、更新等**重要节点**时,可以**大幅提升**发布频率。 - -官方帖常规类型: - -- 信息告知:内容面面俱到,并根据论坛内玩家反馈实时更新,如开测信息一览,服务器维护公告 & 补偿等。 -- 品宣内容:展示游戏的独特风格,并展示对玩家真诚的态度,如研发进度报告,美术图透等。 -- 社区活动:说明参与条件,并使活动符合游戏调性,如攻略征集,玩家共创等。 - -官方可视自身情况规划栏目的更新频率。 - -标题设计:需要让玩家一眼明确官方想表达的内容,同时强化栏目印象 - -- 通知信息→【重要】游戏将于 1 月 1 日开启付费删档测试! -- 庆祝活动→【内含福利】预约 10w 达成,感谢各位的支持 -- 游戏爆料→【研发日志】我们是怎么在制作模型? - -帖子封面:**长帖的第一张图将作为帖子封面**,在动态流中进行展示。因此这张封面图清晰表达帖子的中心内容,宣传卖点等。封面图的尺寸需要保持在**2.5:1~16:9**之间,长宽不小于**300*300**。如长帖的第一张图不符合尺寸条件,将会自动选用下一张尺寸合适的图片作为封面图。 - -### 发帖后如何有效触达玩家 - -以下功能的使用需**提前设置**相关权限,具体设置方法及使用流程如下 - -#### 帖子推送 - -需前往**开发者中心**,为角色设置**「官方帖消息推送」**权限。查看[权限管理](/store/)说明。 - -获得权限后,前往**开发者中心** >> 点击**新增推送帖**,即可完成推送。 - -![官方帖消息推送](/img/official-posts.png) - -推送完成后,该条信息将显示在玩家的通知栏中,并新增消息提醒。 - -官方帖通知推送 - -#### 论坛置顶 - -需前往**论坛管理中心**添加**版主权限**,并同时为版主设置**「置顶」**权限。查看说明:https://www.taptap.cn/doc/6?item_id=108 - -获得权限后,前往游戏论坛即可对目标帖子进行置顶操作。 - -![置顶](/img/pin-posts.png) - -#### 资讯位设置 - -需前往**开发者中心**,为角色设置**「更新游戏」**权限。[查看说明](/store/)。 - -获得权限后,前往**开发者中心** >> **游戏运营** >> **资讯管理** 设置游戏最新资讯,此功能建议在重要节点定期更新。 - -![资讯管理](/img/news.png) - -#### 论坛推荐位设置 - -需前往**论坛管理中心**添加**版主权限**,并同时为版主设置 **「推荐位管理」** 权限。查看说明:https://www.taptap.cn/doc/6?item_id=106 - -获得权限后,前往**论坛管理中心** >> 点击 **推荐位管理** 即可添加和修改论坛推荐位。 - -![推荐位管理](/img/featured-posts.png) - -#### 开发者的话前两行 - -需前往**开发者中心**,为角色设置 **「更新游戏」** 权限。[查看说明](/store/)。 - -获得权限后,前往**开发者中心** >> **商店** >> **更新游戏** 中设置游戏资料(含**开发者的话**),开发者的话前两行文本在详情页默认露出。 - -开发者的话 - - -## 2.2 强力信息板:推荐位 - -推荐位处于**论坛首页顶部**,官方可将其设置成**帖子、视频、动态、子版块等链接**,使内容进行持久稳定的曝光。 - -推荐位一共可以放 10 个 icon,但安卓端首屏默认展示 5 个。 - -推荐位的 icon 设计:尺寸为**200*200**,设计上建议简洁明了,可以用游戏内的 UI/ 角色头像,如没有合适素材,可[点击这里](pathname:///files/recommends-icons.zip)获取 TapTap 默认素材包。 - -推荐位 - -设置推荐位内需遵循如下逻辑:玩家所需内容>官方想展示内容。 - -官方可结合论坛搜索热词,了解玩家的需求风向(以香肠派对为例)。 - -论坛搜索热词 - -后台数据显示,进入论坛的玩家均为游戏的核心玩家,此类玩家更关注游戏更深度的内容 & 福利,品宣类内容对他们影响较弱。 - -因此,推荐位的内容建议以详细的单项攻略(如角色养成、队伍搭配等)& 游戏福利 / 活动为主。 - -推荐位文案,建议采用如下逻辑:简单直白 + 创意性,不建议使用游戏原创名词。 - -- 下载必看>官方公告 -- 福利活动>官方活动 -- 角色番外>魔女庭院(游戏名词) - -### 具体案例参考 - -《末剑 2》游戏上线当天准备的论坛推荐位如下: - -![《末剑 2》游戏上线当天论坛推荐位](/img/image2021-11-12_17-45-53.png) - -以上版块中【通关攻略】点击率最高,其他版块点击率只有【通关攻略】的 1/5 左右,总点击率处于较低水平。 - -- 分析原因: - -1. 游戏公测后,玩家在论坛的消费刚需为「游戏内容」(无舆情影响下),论坛热搜也集中在攻略上 -2. 文案缺少吸引力,如【官方活动】表述平淡,可优化为【最新活动】 / 【有奖活动】 - -《末剑 2》上线次日将论坛推荐位调整如下: - -![《末剑 2》上线次日论坛推荐位](/img/image2021-11-12_17-46-37.png) - -- 调整思路: - -1. 舍弃官方公告内容,铺设更多细分攻略内容(玩家刚需) -2. 对表述较平淡的文案进行优化,使用「有奖」等增强吸引力的词汇 - -更新后【攻略合集】、【法宝收集】、【迷宫一览】的点击率均上升至较高水平,总点击率提升 5 倍。 - -## 2.3 整理你的家:子版块 - -子版块是 TapTap 社区全部内容(含帖子、视频、动态等)的分类合集,具备划分内容、聚集讨论、内容曝光的功能。可根据运营需要进行设置和更改。 - -子版块 - -### 如何添加子版块权限 - -论坛子版块权限设置流程:进入**论坛管理中心**,点击**论坛版主** >> **论坛版主添加** >> **管理权限** >> **子版块** - -![添加版主](/img/image2021-11-12_16-29-30.png) - -![设置版主权限](/img/image2021-11-12_16-33-46.png) - -### 如何设置子版块 - -论坛子版块设置流程:进入**论坛管理中心**,点击**子版块管理** >> **新增** - -![子版块管理](/img/17.png) - -![编辑子版块](/img/18.png) - -名称需使用 0-4 个字符,2 个字符显示最佳。排序数字越大则展示靠后,数字越小则在前展示。如果你不想让子版块的内容在论坛首页展示,不想让玩家在该子版块发布内容,请都选择否。 - -![排序子版块](/img/19.png) - -### 如何使用子板块功能进行批量管理 - -子版块的管理是社区运营需要持续运营的工作内容,通过运营者的集中操作能够曝光优质内容、快速减少社区舆论扩散、提高玩家浏览社区内容的体验。 - -![批量管理功能](/img/batch-operate-posts.png) - -论坛子版块功能流程:进入**论坛管理中心**,点击**批量管理** >> **选择操作** - -可通过以上筛选条件划分出你所需要管理的内容,进行批量移动、沉底、删除的操作。 - -以下为子版块管理的功能说明: - -

    移动:该功能使用会对所选择内容所属板块进行更新,可用于日常论坛版面的内容进行管理。

    - -

    沉底:该功能使用会对所选择内容进行曝光权重的降低,可对引战、谩骂、羞辱等内容进行处理。

    - -

    删除:该功能使用会对所选择内容进行删除,可对政治言论、违法犯罪、血腥黄色等内容进行处理。

    - -![批量操作](/img/batch-operations.png) - -![批量移动](/img/image2021-11-12_16-50-36.png) - -论坛子版块内容批量移动流程:进入**论坛管理中心**,点击**批量管理** >> 选择**帖子 / 视频 / 动态** >> 选择**子版块** >> **批量移动** - -通过以上的重复操作,将需要删除子版块的帖子 / 视频 / 动态内容批量移动至其他位置,完成全部内容的处理即可对该子版块进行删除操作。 - -论坛子版块删除流程:进入**论坛管理中心**,点击**子版块管理** >> **删除** - -优秀的子版块离不开日常的管理,还需要版规的制定。请参考子版块版规:https://www.taptap.cn/topic/17659287 - -管理子版块遵循如下几点:版规制定、版务处理、氛围引导。 - -版规制定适用于玩家允许发布内容的子版块,在版规制定中明确要求内容发布的准则。在子版块发布与该内容契合的活动,有利于激励玩家在子版块里发布内容,就能够形成子版块专属的氛围情况。 - -## 2.4 多一个帮手:设立版主 - -TapTap 支持赋予玩家账号官方权限,来使用某些特定拥有的功能,如移动帖子、删除帖子等。 - -根据运营需要赋予玩家 / 非官方账号版主权限,协助官方进行社区管理。 - -![添加论坛版主](/img/add-forum-moderator.png) - -同时,官方可通过论坛日志来查询版主的操作记录: - -![日志管理](/img/forum-audit-logs.png) - -只有论坛主管理员可添加、删除版主权限,如需查看论坛主管理员账号,可在 **开发者中心** >> **设置** >> **论坛设置** 查看和调整。 - -## 2.5 数据查询:论坛管理中心 - -论坛管理中心是观察论坛运营数据的重要渠道,点击各模块后,可在下方按日期查询详细数据。 - -![论坛管理中心](/img/forum-admin-center.png) - -点击数据名称旁的问号标识,查看该数据定义,综合利用不同数据指标,了解社区情况。 - -![论坛数据指标](/img/forum-stats.png) \ No newline at end of file diff --git a/docs/community/finale.mdx b/docs/community/finale.mdx deleted file mode 100644 index e6b1afab8..000000000 --- a/docs/community/finale.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: 结束语 -sidebar_label: 写在结尾 -sidebar_position: 40 ---- - -感谢所有阅读至此处的开发者,TapTap 社区是一个开放的游戏交流社区,从创立至今,已经吸引过亿玩家与众多开发者们聚集于此。TapTap 的社区共建源自于所有人的共同努力。作为开发者,了解玩家喜好,了解游戏开发环境变化,对游戏产品的制作和上线至关重要。请使用好社区提供的各项功能,同时真诚细致地与玩家沟通,处理每一个问题。我们相信,在 TapTap 不止发现好游戏,还能发现更辽阔的友谊天地。 \ No newline at end of file diff --git a/docs/community/stages.mdx b/docs/community/stages.mdx deleted file mode 100644 index 9bf53957a..000000000 --- a/docs/community/stages.mdx +++ /dev/null @@ -1,145 +0,0 @@ ---- -title: 区分运营阶段,解决宣发难点 -sidebar_label: 区分运营阶段 -sidebar_position: 20 ---- - -## 3.1 心动的开始:首次曝光 - -欢迎加入 TapTap,初登平台,你首先需要搭建并完善自己的游戏社区,我们已经为你准备了最基本的社区框架,需要填充并规划更多运营内容,完成一次漂亮的品牌曝光,建立玩家对游戏产品的好感度,并吸引第一批核心玩家关注。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    事项介绍
    发布登录公告初次登录平台时,建议优先发布游戏品宣帖,内容涵盖背景、内容、玩法、预计上线 / 测试时间等内容,如果你已开启游戏预约,但未进行过初次曝光,同样建议进行此步骤
    打理子版块可根据需要设置基本子版块,内容涵盖游戏信息、玩法说明、讨论专区
    建立特定人设根据游戏类型,在与玩家交流时,建立一个独特的人设,并围绕此人设一步步输出游戏特点,于玩家间形成良性互动
    举办社区活动适当开展预约奖励活动,提高曝光效率
    建立玩家群在登录阶段,建立并曝光你的玩家群,吸引第一批兴趣玩家入驻
    - -## 3.2 发现优点:阶段测试 - -在阶段测试中,玩家更关注游戏的产品研发进度、基础游戏体验、未来改动计划,希望获得小众参与感。开发者需要提前准备如下内容,应对不同阶段测试时,可能会出现的问题,确保玩家顺利参与。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    事项介绍
    准备测试公告测试参与人数、测试时间、测试类型、测试平台、测试筛选条件等基础信息应对服务器可能出现的状况,需提前准备炸服公告、补偿公告、调试进度公告付费测试时需撰写充值返利规则、机型适配公布、平台账号及历史数据互通等
    FAQ对开测时需要玩家关注的重点问题,以及玩家主要关注的问题进行解答
    举办社区活动适当开展测试体验活动,鼓励玩家在社交平台分享游戏鼓励玩家在论坛分享游戏体验 / 攻略,为之后的测试 / 公测做好攻略储备
    发布 bug & 意见反馈帖请开设专帖收集玩家对游戏的意见,以便对游戏进行调整该阶段,因机型等问题会造成闪退黑屏等 bug 出现,请开设专门的 bug 收集帖
    打理子版块发布不同类型的内容,用子版块将这些内容进行分类,如官方公告、攻略分享、玩家互助、反馈专区等
    - -## 3.3 第一次见面:游戏上线 - -游戏即将上线,期待已久的玩家终于要与作品见面了,我们重视每一款即将上线的游戏产品,在此过程中,你将会得到 TapTap 的一系列资源支持。在 TapTap 社区,我们建议你提早进行运营规划,社区将会提供定制的优化方案,辅助品牌曝光,吸引更多玩家,建立优质的内容交流圈子。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    事项介绍
    发布一系列官方帖需提前准备公测详细信息、上线公告、活动一览(游戏内 + 游戏外)、反馈渠道、FAQFAQ 信息需包括:测试时间、测试类型、机型适配、账号数据、充返规则等可参考:https://www.taptap.cn/topic/18666496
    设置论坛推荐位设置 FAQ、详细的攻略、最新的福利 / 活动以及 bug 反馈
    对子版块排序该阶段,需将官方公告、攻略、玩家交流反馈的版块放在最前面
    做好舆情预案请提前做好舆情预警,对开服时可能出现的风险点做准备,包括撰写关于炸服、充值问题、恶性 Bug、多平台差异等内容的官方公告,以及相关处理方案
    举办社区活动请提前规划并设计社区活动方案,公测前 3 天是最佳的宣发期。经验证,玩家在公测首日进入论坛率高达 70%,此时可持续输出游戏的公关素材,类似 pv、研发心路历程等,提升玩家粘性
    进行舆情维护快速有效的舆情维护,可以有效帮助游戏树立品牌形象,主要回复点在于评价回复、热门论坛帖回复、官方帖楼层回复
    储备攻略在开服前 3-4 天逐步放出基础攻略(基础玩法介绍、职业介绍等),可使用终测攻略,如终测与公测内容有较大差别,需准备新攻略
    - -## 3.4 长久的陪伴:版本更新 - -游戏即将迎来重要的版本更新,对新老玩家都是一次全新的内容展示,在新版本中,玩家不仅期待不同的游戏体验,更愿意针对游戏迭代产生的新创意、新内容、新槽点发布自己的看法,与开发者进一步交流。我们建议你填充如下运营计划,持续优化产品及社区氛围,建立长期价值。 - - - - - - - - - - - - - - - - - - - - - - -
    事项介绍
    发布官方帖请提前准备新版本游戏介绍、活动一览(游戏内 + 游戏外)、以及应对突发问题的说明帖
    设置并更新推荐位版本更新时,推荐位可以有效服务版本内容及社区活动,可尝试利用版本特色设计社区活动,会在推荐位收获良好曝光
    搭配社区活动可参考版本特点设计并发布社区活动,在上线后,社区玩家主要偏向于内容(攻略、创作)需求,建议以此设计活动方向
    \ No newline at end of file diff --git a/docs/community/start.mdx b/docs/community/start.mdx deleted file mode 100644 index d0a2a182f..000000000 --- a/docs/community/start.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: 快速了解社区,分享独特基因 -sidebar_label: 快速了解社区 -sidebar_position: 1 -slug: /community ---- - -## 1.1 欢迎加入 TapTap - -你好,开发者,欢迎加入 TapTap! - -TapTap 社区旨在帮助游戏产品在重要运营节点登录平台、游戏上架、开启测试、游戏上线、版本更新时,完成并完善社区搭建,为玩家提供更优质的内容,收获更真实友好的反馈。 - -对开发者而言,良好的社区氛围与高活跃的玩家密度是助力项目成长的催化剂。在 TapTap,开发者可以全天候无缝对接高粘性品类玩家、海量兴趣玩家、以及优质内容创作者,直面玩家体验,赋予项目无限可能。 - -## 1.2 社区的独特基因 - -「真实评价、平等交流、共同成长」,这里是与众不同的游戏天堂。 - -有别于常见的游戏发行平台、信息发布渠道、玩家自发聚集地,TapTap 社区是将游戏产品、玩家、开发者三者紧密连结的服务型内容社区。游戏产品在 TapTap 社区会更贴近玩家,直面口碑评价。收获真实的友谊,与玩家共同成长是这里的独特基因。 - -在 TapTap,用心是唯一的「手段」,优质的内容征集活动、共创活动,甚至开发者日记,会获得前所未有的玩家关注与热情参与。 - -社区将指导你如何更好的理解并参与到上述优质社区氛围之中,我们希望开发者可以用产品与玩家对话,共同创造理想的「游戏世界」。 - -## 1.3 使用说明 - -为了更好的服务开发者,TapTap 社区设计并提供了诸多常用功能和特定功能,帮助游戏项目在社区运营的各个阶段获得更好的展示和曝光。很多优质项目受限于运营经验,以及对平台属性的了解差异,实际运营效果可能不如预期。 - -《TapTap 社区运营指南》致力于解决上述问题,开发者将在下文了解各模块的基础功能与实用步骤,快速掌握社区后台和开发者功能使用,对细节操作进行实例研究和效果学习,内容会穿插精选优质案例以供参考。希望开发者可以有所收获,为玩家提供最优质的内容,提升其初入论坛的满意度、活动参与效率、品牌好感度。 - -如果你和团队是运营新人,强烈建议仔细阅读第二部分,务必对平台模块进行一次深入了解。如果你和团队是运营老将,也强烈建议阅读进阶部分,了解平台调性是差异化运营和精细化运营的核心要求。相信这份指南一定能为你和团队带来收获。 \ No newline at end of file diff --git a/docs/ddos.mdx b/docs/ddos.mdx deleted file mode 100644 index 5b54593b9..000000000 --- a/docs/ddos.mdx +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: 开发者应对 DDoS 攻击问题的指引文档 ---- - - - - - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -亲爱的开发者伙伴们, - -TapTap 从解决社区入驻开发者所面临游戏运营困难的角度出发,在司法机关的指导下,联合上海多家头部游戏研发企业发起成立了「反网络黑灰产联盟」(以下简称「联盟」),旨在协助开发者有效处置恶意注册、侵犯知识产权、恶意评论、高技术侵害等网络黑灰产问题,其中 DDoS 攻击问题成为了 TapTap 现阶段的重点工作之一。 - -近期,TapTap 依托联盟的力量,为了让开发者在面对 DDoS 攻击威胁时能够从容应对。TapTap 结合自身经验,并且总结曾受 DDoS 攻击开发者的实际案例,形成了一套具有实操作用的指引手册。 - -## DDoS 攻击问题应对指引 - -### 攻击前准备 - -* 提高运营安全意识 - - - 充分重视 DDoS 攻击威胁,将抵抗 DDoS 攻击纳入安全保障考量,尤其是新游上线或重大更新等时间段。 - - - 做好真实 IP 保护工作,切勿随意将真实 IP 信息露出。 - -* 防护准备 - - - 提前与云服务商沟通防护措施,有条件的可以提前接入高防,为抵抗潜在 DDoS 攻击做好准备。 - -### 攻击中应对 - -* 巧妙应对勒索 - - - 尝试从议价等角度入手,尽可能与勒索者周旋,套取更多证据信息,同时为开启防护争取时间。 - - 不建议支付钱款,攻击威胁并不会因为钱款的支付而停止或消失。 - -* 开启防护 - - - 尽快接入/打开高防服务(注意为业务分配新 IP 作为防 DDoS 服务的回源 IP,详见附文[如何应对 DDoS 攻击·应对策略](#应对策略) - -* 证据留存 - - - 受攻击时段的日志记录(体现异常流量)。 - - 勒索者身份关联信息(QQ号、银行账号、IP等)。 - - 抵抗攻击成本(高防成本、技术抵抗成本等)。 - - 对游戏造成的损害(负面评价、评分降低、用户流失、收入损失等)。 - -### 攻击后处置 - -* 证据同步 - - - 将收集到的所有证据同步「反网络黑灰产联盟」(我们将严格保密,在记录、比对和分析后提交司法机关进行进一步处置)。 - -* 提升防护 - - - 结合自身情况尽可能提升防护能力,以应对后续可能出现的 DDoS 攻击问题。 - -如果你的游戏遭遇到了 DDoS 攻击与勒索,请不要慌张,尽快通过邮箱与我们取得联系。 - -**联系方式:SH-ADPL@taptap.com** - -**联盟公众号:** - -![公众号图标](https://img.tapimg.com/market/images/e6947838a61983ba8bd3e2b6f3a4dc85.png) - -## 附文:如何应对 DDoS 攻击 - -### 什么是 DDoS 攻击? - -DDoS (Distributed Denial-of-Service) 攻击是指攻击者通过利用大量被恶意软件感染的计算机、软件的缺陷、互联网协议的弱点等,使得被攻击对象因为从不同来源收到大量非正常流量,从而导致大部分业务流量无法被正常处理。 - -由于网络协议由多层组成,业务流量要能从用户端到达游戏的服务器并被处理,需要各层都能正常工作,包括交换机、路由器、网卡、服务器等各种软硬件。只要其中任何一环失败,就能达成攻击者的目的。所以很多时候 DDoS 的效果并不会直接反映在服务器的负载上。不同的攻击也需要不同的方式应对。 - -### 被攻击时会发生什么? - -通常来说,当机房或云服务商检测到有个别租户被大量恶意流量攻击时,为了保护其他租户不受影响,会修改路由表把目标为被攻击 IP 地址的流量都路由到一个「黑洞」,也就是丢掉这些流量。这时用户就无法正常访问服务了。 - -如果是针对 HTTP 等应用层协议的攻击,可能会导致业务服务器负载过高,从而服务无法访问。 - -### 应对策略 - -如果受到的是应用层攻击(比如 http/https 流量,表现为业务服务器或反向代理负载显著升高),接入云服务商提供的应用层防火墙(WAF,Web Application Firewall)能起到抗攻击的作用。但从过去的情况看,游戏受到的大部分是更低层的攻击,所以以下重点讨论更常见的情况。 - -被攻击时成本最低也能最快完成的方案是换 IP。也就是为服务器分配一个新的 IP,把老的 IP 去掉,然后把域名指向新的 IP。但这多半不能解决问题,因为攻击者发现域名解析变了之后会开始攻击新的 IP。而因为操作快、成本低,可以先尝试,以尽量减少服务不可用的时间。在少数情况下,攻击者是针对 IP 而不是域名进行攻击,可能就彻底解决了(比如你并不是攻击目标,而是被误伤的情况)。 - -通常来说能持续有效的方式是接入高防服务。防 DDoS 攻击的难点是把攻击流量和业务流量区分开,高防服务提供的流量清洗就是用专业的设备和机房来以比较高的准确率隔离攻击流量,让业务流量可以到达业务服务器。各大型云服务商均会提供防 DDoS 攻击服务,也有专门提供这类服务的公司。 - -通常来说,攻击者会找到业务域名对应的 IP,对此 IP 发起攻击: - - - -需要注意的是,简单地启用防 DDoS 服务,并把原 IP 作为回源 IP 是起不到任何作用的,因为攻击者已经知道原 IP,会对它进行持续攻击。 - - - -正确地做法是为业务分配新的 IP 作为防 DDoS 服务的回源 IP(新分配的 IP 不要在任何地方公布,也不要用任何域名指向它),把业务域名指向高防 IP,并把原 IP 从业务服务器解绑。 - -:::caution - -#### 更安全的配置 - -业务服务器应该设置可信的上游 IP,对其他不可行来源 IP 都设置丢弃数据包,否则攻击者还是有可能扫到业务服务 IP 的。 - -对 HTTPS/TLS 服务,要对 SNI 做相关配置 ,避免处理客户端流量,而是需要经过可信高防回源/代理 IP 才提供服务和作出响应。 - -对非 HTTPS/TLS 服务,也要严格设置可信上游,丢弃所有非可信上游来源流量,保证自己不会被攻击者认为业务服务 IP 是游戏的源站 IP。 - -::: - - - -在国内,由于防 DDoS 服务价格较为昂贵,另外流量会经过额外的环节而增加网络延迟,通常只会在受到攻击时,和在开服或上榜等可能被攻击的时间点前接入此类服务。因为临时接入高防需要时间,建议如果能预期到在特定时间点被攻击的可能性较大,就提前接入避免服务中断产生损失。在国外可以选用 Cloudflare 等自带 DDoS mitigation 功能的动态 CDN 来同时达到降低网络延迟和常态防 DDoS 的效果。 diff --git a/leancloud/docs/deeplink.mdx b/docs/deeplink.mdx similarity index 100% rename from leancloud/docs/deeplink.mdx rename to docs/deeplink.mdx diff --git a/docs/demos.mdx b/docs/demos.mdx deleted file mode 100644 index a8e3ce04e..000000000 --- a/docs/demos.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Demos -hide_table_of_contents: true # 因为 Conditional 不会作用于 ToC,对于此类内容不多的页面直接隐藏 ---- -import { Conditional } from "/src/docComponents/conditional"; - -## 游戏场景下使用 TDS 服务的示例 Demo - -1. [UE Demo](https://github.com/taptap/TapSDK-UE-Demo):使用 UE SDK,以 ActionRPG 为模板,接入 TDS 的内建账户、防沉迷、公告、内嵌动态、成就、客服、排行榜和数据埋点。【[安装包下载](https://github.com/taptap/TapSDK-UE-Demo/releases/tag/v1.1.0)】 -2. [Unity Demo](https://github.com/taptap/TDS-Unity-Demo):使用 Unity SDK,集成了 Tap 登录、内建账户、防沉迷、成就、排行榜、动态和公告,支持 Android 和 iOS 两个平台。【[安装包下载](https://github.com/taptap/TDS-Unity-Demo#%E6%B8%B8%E6%88%8F%E4%B8%8B%E8%BD%BD)】 - - -## 生态服务 - -TapTap 登录、TapDB、好友关系与内嵌动态。 - -1. [TapSDK-Unity-Demo](https://github.com/taptap/TapSDK-Unity-Demo/tree/master) 【[Unity 安装包下载](https://capacity-files.lcfile.com/0iB7AQs4NlVpKirz8t94g7HePh03SAl6/demo.apk)】 -2. [TapSDK-Android-Demo](https://github.com/taptap/TapSDK-Android-Demo) 【[Android 安装包下载](https://capacity-files.lcfile.com/MgMrSbWK8LBoYnjB3cBsYEsk63C3E3LV/tapsdk_android_v3.29.0.apk)】 -3. [TapSDK-iOS](https://github.com/TapTap/TapSDK-iOS) - -## 商业变现 - -1. [TapADN Android Demo](https://capacity-files.lcfile.com/Y71RXq7Js6CBb14kRAhKbNsWv43UGGxq/TapADDemo.zip) 【[Android 安装包下载](https://capacity-files.lcfile.com/JEGe4SukTg3FbRI7DN623RexDAxOd0OP/tapaddemo_external-release.apk)】 - -:::tip -TapADN SDK 为 APP 提供广告投放及广告监测归因、反作弊和广告投放统计分析等服务,由易玩(上海)网络科技有限公司开发提供。[SDK 隐私政策](https://developer.taptap.cn/docs/sdk/tap-adn/agreement/)、[SDK 合规使用说明](https://developer.taptap.cn/docs/sdk/tap-adn/adn-compliance/); -::: - -## 数据存储 - -1. [存储快速入门 Demo](https://github.com/leancloud/StorageStarted):包括 iOS 和 Android 示例,拟商品发布的场景,讲解数据存储的基础用法。 -2. 数据存储 Demo([iOS Demo](https://github.com/leancloud/LeanStorageDemo-iOS)、[Android Demo](https://github.com/leancloud/LeanStorageDemo-Android)):展示了 LeanCloud 数据存储 SDK 的各种基础和高级用法,包括用户系统、文件上传下载、子类化、对象复杂查询等。 - - - -## 即时通信 - -1. [Android ChatKit Demo](https://github.com/leancloud/LeanCloudChatKit-Android):基于 Android SDK 开发并封装了简单 UI 的聊天套件。 -2. [C# SDK Demo](https://github.com/leancloud/CSharp-SDK-Unity-Demo):基于 C# SDK 编写的 Demo。 主要展示如何快速开发游戏(Demo 包括如何使用 C# SDK 存储、云引擎、排行榜、即时通信)。 - -## 推送通知 - -[Android 混合推送 Demo](https://github.com/leancloud/mixpush-demos): 使用 Android 混合推送服务的简单 Demo。 - - - -## 云引擎 - -### 示例项目 / 项目骨架 - -我们为每种语言维护了一个示例项目,包含了推荐的骨架代码,建议大家从这几个项目开始新项目的开发: - -- [node-js-getting-started](https://github.com/leancloud/node-js-getting-started/) -- [python-getting-started](https://github.com/leancloud/python-getting-started) -- [slim-getting-started](https://github.com/leancloud/slim-getting-started)(PHP) -- [java-war-getting-started](https://github.com/leancloud/java-war-getting-started) -- [dotNET-getting-started](https://github.com/leancloud/dotNET-getting-started) -- [golang-getting-started](https://github.com/leancloud/golang-getting-started) - -### Node.js 云引擎 Demo 仓库 - -[leanengine-nodejs-demos](https://github.com/leancloud/leanengine-nodejs-demos) 是 LeanEngine Node.js 项目的常用功能和示例仓库。包括了推荐的最佳实践和常用的代码片段,每个文件中都有较为详细的注释,适合云引擎的开发者阅读、参考,也可以将代码片段复制到你的项目中使用。 - -在这个仓库的 README 中有详细的功能列表和介绍。 diff --git a/docs/design/design-login.mdx b/docs/design/design-login.mdx deleted file mode 100644 index 55bfb8569..000000000 --- a/docs/design/design-login.mdx +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: 使用 TapTap 登录的按钮设计规则 -sidebar_label: 登录设计指南 -sidebar_position: 1 -slug: /design ---- - -import { Background, Figure } from "/src/docComponents/doc"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { Red, Blue, Black, Gray } from "/src/docComponents/doc"; - -## 1. 登录按钮的类型 - -TapTap 登录按钮,具有 “TapTap 登录”文字按钮 和 纯标志按钮,2 种形式。 - -![](https://img.tapimg.com/market/images/497ed7588b1b8f786a9825927c856c77.png) - -## 2. “TapTap 登录”文字按钮 - -### 2.1 官方默认按钮 - -如采用“TapTap 登录”文字按钮,官方提供了 3 种颜色,2 种变体供选择:最大圆角半径、默认圆角半径。 **请勿使用最小圆角半径(纯直角)按钮。** 请从下图 6 款内选择,可 [下载官方设计资源包](https://assets.tapimg.com/img/TapTap_Login_Source.zip) 使用。 - -![](https://img.tapimg.com/market/images/331ab8b8aee2933a57250ea54b47be63.png) - -### 2.2 按钮尺寸 - -默认高度为 50 pt,在保证 TapTap 登录按钮宽高比不变的前提下,**可以根据游戏屏幕分配率规则,来调整按钮的尺寸。** - -![](https://img.tapimg.com/market/images/3174489b6242f0f99b78fa42615e89c9.png) - -### 2.3 注意事项 - -使用“TapTap 登录”文字按钮,**该按钮不可更改样式,也不可在官方提供的标准样式上做任何更改,**如:添加描边、渐变、投影、纹理等,均不可调整。 在保证按钮的宽高比不变的前提下,**可以根据游戏屏幕分配率规则,来调整按钮的尺寸。** - - -### 2.4 效果示意图 - -**竖屏游戏** 示意(仅做参考,最终效果以游戏内实装效果为准) - -![](https://img.tapimg.com/market/images/1745e047a07d0595d32ed32a3d41480d.png) - - -**横屏游戏** 示意(仅做参考,最终效果以游戏内实装效果为准) - -![](https://img.tapimg.com/market/images/2e50d673b97a06c472b0303a2d9acd62.png) - -## 3. 纯标志按钮 - -### 3.1 官方默认按钮 - -如采用纯标志按钮,官方提供了 2 种颜色,3 种变体供选择:正圆形、纯直角、圆角矩形。请从下图 6 款内选择,可 [下载官方设计资源包](https://assets.tapimg.com/img/TapTap_Login_Source.zip) 使用。 - -![](https://img.tapimg.com/market/images/41779fce61088c612cef64de3d7d7fda.png) - -### 3.2 按钮尺寸 - -默认尺寸为 40*40 pt,在确保纯标志按钮始终具有 1:1 的宽高比的情况下,**可以根据游戏屏幕分配率规则或搭配其他按钮效果等,来调整按钮的尺寸。** - -![](https://img.tapimg.com/market/images/e9fdf5e51add4f35fe86934ef611826d.png) - -### 3.3 注意事项 - -1.圆角值:多种登录方式同时排列时,如果使用了纯标志型按钮中的圆角矩形样式,其中 **圆角值需保持统一。** - -![](https://img.tapimg.com/market/images/53fd05dc67347cd2f6b6cea3b0a3b3af.png) - -2.边缘形态:多种登录方式同时排列时,**可轻微调整按钮的边缘形态,同时必须与其他登录方式的边缘形态保持统一。** - -![](https://img.tapimg.com/market/images/a513b156c47297d29844b911e9174eab.png) - - -## 4. 最新版资源下载地址 - -点击下载「TapTap 登录」设计资源包 - - - - diff --git a/docs/design/design-moment.mdx b/docs/design/design-moment.mdx deleted file mode 100644 index d1e80936c..000000000 --- a/docs/design/design-moment.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: 内嵌动态设计指南 -sidebar_position: 2 ---- - -import { Background, Figure } from "/src/docComponents/doc"; -import useBaseUrl from "@docusaurus/useBaseUrl"; - -## 导航描述字符规则 - -### 导航描述字符的类型与应用区域 - -导航描述字符分为深色和浅色两种类型,适用于叠加在页面背景上的导航类或注释类字符。 - - -![](https://capacity-files.lcfile.com/s1u6KtHT3keLeI0pufjPzWzRgUVK37wX/11.png) - -![](https://capacity-files.lcfile.com/Rq5yvfk2aLQWDB0VQGIWVd4BjsSsOV95/12.png) - - -### 导航描述字符配置建议 - -字符必须清晰可见。因此,当厂商选取的主题背景为浅色时,建议使用深色描述字符;当主题背景为深色时,建议使用浅色描述字符。 - -![](https://capacity-files.lcfile.com/i0fN5BRoEmwkMrofHcwP5wMSAt4R0VQi/14.png) -![](https://capacity-files.lcfile.com/OiYdR42keXSzFtEJI9SQCcrXjF8dshbG/15.png) - -## 背景图主题规则 - -### 背景图尺寸 - -Tap 动态同时支持横屏和竖屏,因此每个接入游戏需要分别提供横屏和竖屏,共 2 种尺寸的背景图。 - - -
    - - -### 背景图裁切适配方式 - -在小屏手机中体验 TapTap 动态时,背景图会被裁切适配。 - - -
    - - -### 背景图定制化建议 - -#### 背景图风格建议 - -背景图不得干扰内容的呈现,因此建议使用较为简洁或对比度较弱的背景。 - -![](https://capacity-files.lcfile.com/kvnotEeK5jjdqWRkVQszmXbC17H1Fpd8/18.png) - -#### 可追加在背景图中的图形元素风格建议 - -支持在背景图中加入图形元素,但不得干扰内容呈现。 - -![](https://capacity-files.lcfile.com/saTAoWf78fKsFxYg89yLQIBAxGeauifm/19.png) -![](https://capacity-files.lcfile.com/qjYbXWPdNBcIplMKDpEOxl2lw24RaBnX/20.png) -![](https://capacity-files.lcfile.com/uK2VLvWxIKjY8C9Ojv07FYQgXv24hLh7/21.png) -#### 图形元素的置放安全区域 - -为了使被追加的图形元素,也能够呈现完整,因此定义了置放的安全区域。 - -![](https://capacity-files.lcfile.com/iGOqqtJuBvzpcVrbIG9PhFkqnKzkcusy/22.png) -![](https://capacity-files.lcfile.com/dH7XAYve8Gt6UTT63yyX0kiirqpB3H8r/23.png) -![](https://capacity-files.lcfile.com/71Ym7tIqzKIKleXxh1G1djs7UY6PWDSF/24.png) - -#### 吸顶标签栏的背景色规则 - -吸顶标签栏的背景色可进行自定义,建议吸取背景图顶部的色彩,能够使界面风格更统一。 - -![](https://capacity-files.lcfile.com/0e7wFaUhshPN5wcxWnzu2yJ6ldMrbS91/25.png) diff --git a/docs/roles-final.mdx b/docs/roles-final.mdx deleted file mode 100644 index 7471bfeb4..000000000 --- a/docs/roles-final.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -slug: /sdk/tap-support/roles-final/ -title: Tap 客服的权限控制 ---- - - - - - -:::warning WIP -这个文档展示的是客服权限设计的最终状态,并非当前实现。 -::: - -客服模块拥有与开发者中心部分独立的权限系统。权限由角色与范围两部分组成。 - -- 角色:决定用户能使用哪些功能,进行哪些操作; -- 范围:决定用户能访问哪些工单。 - -### 角色 - -按照不同的职责,系统内置了以下几种角色: - -| 用户角色 | 职责 | 访问路径 | 计费 | -| ---------- | :----------------------------------------------------------------------- | ---------- | ------ | -| 厂商管理员 | 维护服务与安全相关配置 | 开发者中心 | 不计费 | -| 管理员 | 日常管理
    拥有除了服务与安全相关配置之外的所有配置权限 | 客服工作台 | 不计费 | -| 客服 | 直接与玩家沟通,处理玩家诉求 | 客服工作台 | 计费 | -| 协作者 | 协助客服处理玩家诉求 | 客服工作台 | 不计费 | -| 开发者 | 查看、维护游戏接入与运营过程中关心的配置 | 客服工作台 | 不计费 | - -厂商管理员是在开发者中心拥有客服权限的 Tap 用户。其他角色的用户是独立的客服系统用户,通过独立于开发者中心的客服工作台登录使用客服模块,我们统称他们为「成员」。成员可以由厂商管理员在开发者中心添加,也可以由管理员在客服工作台添加。一个成员可以拥有一种或多种用户角色,厂商管理员或管理员可以修改成员的角色。 - -详细的角色与权限对应关系如下: - -| | | 厂商管理员 | 管理员 | 客服 | 协作者 | 开发者 | -| ----------------- | ---------------------------------------------------------------------------- | :----------------------------: | :----------------------------: | :--: | :----: | :-------------------------: | -| 服务与安全 | 启用、关闭服务 | ✔️ | | | | | -| | 设置计费成员数量上限 | ✔️ | | | | | -| | 配置客服工作台单点登录 | ✔️ | | | | | -| | 配置自定义域名 | ✔️ | | | | | -| 用户管理 | 查询成员列表与资料 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | -| | 管理成员
    添加、禁用、修改资料 | ✔️ | ✔️ | | | | -| | 查询玩家资料 | | ✔️ | ✔️ | ✔️ | ✔️ | -| | 管理玩家
    添加、修改资料 | | ✔️ | ✔️ | | | -| 工单 | 访问工单
    - 查询、查看工单
    - 提交内部留言
    | | ✔️ | ✔️ | ✔️ | ✔️ | -| | 修改工单属性 | | ✔️ | ✔️ | | | -| | 回复玩家 | | | ✔️ | | | -| | 代玩家提单 | | ✔️ | ✔️ | ✔️ | ✔️ | -| 工单
    视图 | 使用
    - 查看视图
    - 管理用户视图
    | | ✔️ | ✔️ | ✔️ | ✔️ | -| | 管理
    - 管理全局视图 | | ✔️ | | | | -| 工单
    快捷回复 | 使用
    - 查看快捷回复
    - 管理用户快捷回复
    | | ✔️ | ✔️ | | | -| | 管理
    - 管理全局快捷回复 | | ✔️ | | | | -| 知识库 | 查看 | | ✔️ | ✔️ | ✔️ | ✔️ | -| | 管理
    添加、修改内容与发布状态 | | ✔️ | ✔️ | | | -| 设置 | 内容设置
    - 产品与分类
    - 工单字段、表单
    - 动态内容
    | | ✔️ | | | ✔️
    只读 | -| | 客服设置
    - 群组
    - 触发器
    - 通知渠道管理
    | | ✔️ | | | | -| | 开发设置
    - 玩家鉴权方式
    - 自定义域名(只读)
    | | ✔️ | | | ✔️ | -| 审计 | 审计日志 | ✔️
    厂商部分 | ✔️
    成员部分 | | | | - -### 范围 - -如果用户拥有「访问工单」的权限,其能访问的工单有如下三种不同的范围: - -- 所有工单 -- 仅限其所在组的工单 -- 仅限分配给该用户的工单 diff --git a/docs/sdk-api.mdx b/docs/sdk-api.mdx deleted file mode 100644 index fd47af870..000000000 --- a/docs/sdk-api.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: SDK API Reference -hide_table_of_contents: true # 因为 Conditional 不会作用于 ToC,对于此类内容不多的页面直接隐藏 ---- -import { Conditional } from "/src/docComponents/conditional"; - -## TapTap 登录、内嵌动态、好友、成就、云存档、数据分析 - -| 平台 | API 总览 | -| ------- | ----------------------------------------------------------------------- | -| Unity | [Unity-API](https://unity-api-reference.tds1.tdsapps.cn/namespaces) | -| Android | [Android-API](https://taptap.github.io/TapSDK-Android/) | -| iOS | [iOS-API](https://taptap.github.io/TapSDK-iOS/index.html) | - -## 数据存储、即时通讯、推送通知、云引擎、排行榜 - -| 平台 | API 总览 | -| ------- | --------------------------------------------------------------- | -| Unity | [Unity-API](https://leancloud.github.io/csharp-sdk/html) | -| Android | [Android-API](https://leancloud.github.io/java-unified-sdk) | -| iOS | [iOS-API](https://leancloud.github.io/objc-sdk) | diff --git a/docs/sdk/achievement/_category_.json b/docs/sdk/achievement/_category_.json deleted file mode 100644 index caeb7199b..000000000 --- a/docs/sdk/achievement/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "成就系统", - "collapsed": true, - "position": 8 -} diff --git a/docs/sdk/achievement/bestpractice.mdx b/docs/sdk/achievement/bestpractice.mdx deleted file mode 100644 index 92bc6d7e5..000000000 --- a/docs/sdk/achievement/bestpractice.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: 《泰拉瑞亚》 使用 Tap 成就来吸引更多玩家 -sidebar_label: 最佳实践 -sidebar_position: 3 ---- - -《泰拉瑞亚》是一个跨越手机、PC、主机平台的常青树游戏,在全球都有海量的忠实玩家。在中国大陆,中国港澳台发行的手机版《泰拉瑞亚》,目前在 TapTap 上销量也超过了 200 万份,收获了 9.3 的 Tap 评分。 - - -## 支持跨平台的 TDS 成就 -由于泰拉瑞亚可以在多个平台和渠道发行,他们使用了不受平台和引擎的限制的 TDS 的成就系统,不论游戏发布在 iOS AppStore、Android 各大渠道、PC、甚至主机平台,都能帮助游戏实现跨平台的成就系统。 -截止到 2022 年 5 月底,游戏总计触发成就人数超过 200 万人,解锁白金成就(即获得了全成就)的玩家超过 3500 人,游戏社区中也有不少玩家晒自己的成就进度、讨论成就的具体达成方法。 - -![img](https://capacity-files.lcfile.com/zqc1dU4x4cV06hcCok4CwCxFWoowsf4v/achievement_show_on_taptap.png) - - -我们也即将在未来几个月在 TapTap 客户端内增加更多成就系统相关的功能和露出,包括在动态显示好友获得的成就、对比成就、成就专题页等,为喜欢成就的玩家提供更多的实用和分享功能,也能帮助接入了成就的游戏进行更好的宣发和曝光。 - - -## 白金成就 - -我们会为那些获得 TapTap 玩家和 TapTap 编辑认可的游戏,提供白金奖杯,以此来激励玩家冲击游戏全成就,并且白金成就也可作为一种社交资产被玩家所拥有。 - -如果您的游戏符合至少以下条件,就可以在开发者中心后台申请开通白金成就,我们的编辑会来评估您的游戏是否可以开通白金: -- 游戏时长在同类游戏中处于正常水平 -- 免费游戏中,用户不需要付费,仍然可以获得全成就 -- 付费游戏中,用户不需要强制购买主线流程外的额外 DLC 或内购,仍然可以获得白金成就 - -![img](https://capacity-files.lcfile.com/3kBnhO30aI8ukszjt61L4527zHbyAaW5/baijin_achievement.png) - - -## 成就稀有度 -成就的稀有度在一定的冷启动数据后,会开始自动计算。稀有度可以反映这个成就的获得难度,对于稀有度极高的成就,玩家挑战完成会有强烈的成就感。 -![img](https://capacity-files.lcfile.com/C15DpYOKoz9DBFRcUmhy85GDX9Mv8PRT/achievement_rare.png) - - -## 开发者中心的成就配置 -成就后台的配置也较为简单,并且可以看到每一个成就的解锁人数、完成率等数据。 -![img](https://capacity-files.lcfile.com/dTh8PAoMPzbySH1Vw1D6XvF9gswNY6KK/achievement_list.png) - - -## 接入成就的方法 - -目前成就系统提供 SDK 上报成就与服务端上报成就 2 种方式。 - -《泰拉瑞亚》采用了服务端的方式接入了 TDS 成就系统,这样能够比较好的做好防作弊的处理,也能比较及时和稳定的做到成就的同步。 - -如果您的成就已经存储在服务端,那么我们优先建议您使用服务端上报的方式。 - -如果您的游戏是个单机游戏,没有服务端,或者没有存储过用户成就,那么也可以选择接入 TapSDK 来上报玩家的成就。当您设置到成就的触发点后,玩家联网时 SDK 会上报成就数据;若玩家在断网状态,SDK 也会在本地存储成就数据,待网络恢复后上报。 -在后台配置完成就,并确保您的游戏已经接入完成并且测试无误后,您就可以点击发布,将成就发布到 TapTap 上了。 - - -## 将玩家的成就展示在 TapTap - -如果希望在 TapTap 客户端内的成就列表中看到您的游戏,那就需要在游戏中接入 TapTap 登录。您可以把 TapTap 登录作为游戏的一种登录方式,可以 将 TapTap 登录作为游戏账号可以绑定的一个第三方账号,专门用于做成就的同步。 - - -## 立即开始使用 TDS 成就服务 - -如果希望了解如何接入TDS 成就系统,可以访问我们的 [成就产品指南](/sdk/achievement/features/) 和 [成就开发指南](/sdk/achievement/guide/)。整个系统的接入非常简单,有任何问题也欢迎通过 TapTap 开发者中心的工单系统来与我们取得联系。 \ No newline at end of file diff --git a/docs/sdk/achievement/faq.mdx b/docs/sdk/achievement/faq.mdx deleted file mode 100644 index c82a162c6..000000000 --- a/docs/sdk/achievement/faq.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -### 成就编辑完成后在待发布状态,如何进行测试? - -如果需要添加测试人员体验「待发布」状态的成就,请进入开发者中心通过右上角「工单」联系 TDS 技术支持。「工单」中请提供应用的 Client ID 以及测试账号实现 TapTap 登录后返回的 Object ID。 - -### 游戏上线了,还能再申请「白金成就」吗? - -白金成就不受游戏上线的影响。既能在游戏上线前创建好白金成就,也可以在游戏上线后创建。 - -### 「白金成就」审核时,还能发布新的普通成就吗? - -白金成就的申请和创建并不影响普通成就的发布,可以随时提交已经准备好的普通成就。 - -### 在申请「白金成就」资质通过前,玩家已经获得全部成就了,还能补领吗? - -如果有玩家在白金成就发布前,已经获得到全部的普通成就,再为游戏创建白金成就时,玩家依然可以自动获得。 - -### 如果已经是上架的老游戏了,对于已经使用自建账户登录或者第三方登录的用户,怎么接入使用 TDS「内建账户」呢? - -答:针对老用户来说,就使用自建账号/第三方账号登录,登录后需要调用 TapSDK 的绑定接口,对老用户进行绑定。绑定后,在 TDS User(内建账户) 这里都会生成一个玩家的 ID,在使用成就系统后,TapSDK 这边会根据这个 TDS User ID 来确定该玩家身份。如果是新用户的话,直接 TapTap 登录就可以了,就不需要进行绑定这一步骤了。 - -### 如果是已经上架的老游戏接成就系统,那老用户已经完成过的成就,还能再触发吗? - -老用户已完成的数据有两个方式可以同步: -- 通过服务器 API 的方式提前同步一份数据到 TDS 成就服务; -- 若游戏本身记录了成就达成的数据,那么游戏找合适的时间点把以前记录的数据转化为「成就 ID」通过 SDK 达成一次(注:需要临时关一下通知,不然顶部会有多个提醒通知)。 - -### 如果玩家已经达成了全部成就获得「白金成就」,开发者在后台又新增了一个普通成就,玩家的「白金成就」标志会消失吗? - -不会,白金成就的范围仅限在分组为本体成就之内,新创建的普通成就是为拓展成就,将不会影响白金成就。 - -### 游戏有多个区服或可以创建多个角色,如果重复获得成就,SDK 的逻辑是怎样的? - -成就记录跟着账号走,每个成就只记录第一次获得的行为,之后重复获得将不做展示。 - -### 调用初始化数据接口时遇到 `Empty sign or session` 报错,可能的原因是什么? - -因为成就系统是基于内建账户系统(`TDSUser`)的,需要在进行成就系统初始化数据(`[TapAchievement initData];`)之前接入内建账户功能,并且对 `TDSUser` 对象进行实例化。如果 `TDSUser` 为空,会报错 `Empty sign or session`。 - diff --git a/docs/sdk/achievement/features.mdx b/docs/sdk/achievement/features.mdx deleted file mode 100644 index 8dc5db98d..000000000 --- a/docs/sdk/achievement/features.mdx +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: 成就系统功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -开发者可以在开发者中心后台配置并发布游戏成就,玩家在游戏内触发并获得成就,从而提升玩家在游戏中的参与度,鼓励玩家以不同的玩法来玩游戏。TapTap 为游戏增加了点亮「白金成就」荣誉标识,对于那些孜孜不倦完成全部成就的玩家给予奖励。 - - -## 核心优势 - -**对于游戏开发者**: -- 降低开发成本:无需投入开发成本,轻松配置,一键发布。 -- 提高用户生命周期价值:建立用户成长体系(精神激励),提升玩家在游戏中的参与度。 -- 数据验证:以用户分层来完善用户画像,帮助开发者用数据评估游戏内的玩法设计难易度。 -- 支持多端:TapSDK 3.18.2 以上版本支持 iOS、Andriod、Unity、UE4 版本,**同时包含 PC 端展示** - -**对于游戏玩家**: -- 游戏体验:成就内容的趣味性,提升玩家在游戏内的体验。 -- 情绪激发:激发玩家的荣誉感、收集欲望,提升用户粘性。 - - -## 名词解释 - -### 基础信息 - -| 名词 | 释义 | 规则 | -| --- | --- | --- | -| 成就 ID | 向 SDK 上报成就的唯一标识,可以按照游戏自己的要求来定义成就 ID | 只允许英文+数字,最多可以包含 100 个字符 | -| 名称 | 成就的简称,例如「驾车高手」 | 最多可以包含 80 个字符 | -| 简介 | 成就的简要说明,通常用来告诉玩家如何获得成就,例如「驾驶汽车连续躲避 10 个障碍物」 | 最多可以包含 400 个字符 | -| 图标 | 与成就内容相关的方形图标 | 图标 UI 规则:只需要提供成就解锁时的 512×512 PNG 或 JPG 彩色图片,TDS 会自动生成未解锁状态时的灰度版本 | -| 初始状态 | 成就的初始化状态,分为隐藏成就和显示成就 | 一旦初始设置,后续不可再修改 | -| 隐藏成就 | 玩家看不到成就细节 | 选择后,TDS 会在成就处于隐藏状态时为其提供特殊的描述和图标 | -| 显示成就 | 玩家看得到成就细节 | 默认选择,发布后展示开发者配置的内容描述 | -| 成就分组 | 将普通成就进行分组 | 一个普通成就只能归属于一个成就分组,成就待发布状态,可修改成就分组;成就已发布状态,不可修改成就分组 | -| 本体成就 | 游戏发行后最初版本的完整成就,成就分组之一 | 非特殊情况,通常成就数量 ≥10 个,本体成就的数量是固定的 | -| 拓展成就 | 游戏后期推出 DLC 的新成就,成就分组之一 | 成就数量不限,随游戏内容增加而增加 | - -### 分步成就 - -分步成就会让玩家用更长的时间逐步达成成就。随着玩家逐步取得成就,可以向 TDS 报告玩家的进度。TDS 会记录进度信息,并在玩家达到解锁该成就的必要条件时提醒游戏,同时告知玩家成就达成。 - -**数值范围**:创建时必须定义解锁成就所需的步骤总数(此数字必须介于 2 到 100,000,000 之间)。步骤总数达到解锁值后,成就即被解锁(即使它已被隐藏)。开发者无需存储用户的累积进度。 - -**不可重置**:分步成就在游戏进程中是累积的,并且进度无法从游戏内删除或重置。例如,「赢得 20 场比赛」将被视为分步成就。而「连续赢得 5 局游戏」则不行,因为当玩家输掉游戏时,其进度将被重置。「拥有 2,000 个金币」也不符合条件,因为玩家在玩游戏时可能会获得、也会失去金币。对于后两项成就,你可以把成就名称设定为「连续获胜」或「金币总数」,并在玩家达成目标时解锁的标准成就。 - -### 成就稀有度 - -获得成就人数的百分比,值越低,获得人数越少,越稀有。成就稀有度会通过 SDK 面板进行展示,前提是需要达到一定的统计量及人数达到 100 人。TapTap 上展示的成就并不会展示成就稀有度。 - -计算公式 = 完成此成就的人数/初始化成就人数。 -- 普通:50% ≤ Common ≤ 100% -- 稀有:10% ≤ Uncommon < 50% -- 珍贵:1% ≤ Rare < 10% -- 极为珍贵:Ultra Rare < 1% - -成就稀有等级: - -![img](https://capacity-files.lcfile.com/ekmGiWHqhUBcbWxlslbqmptgvUM0YUCh/achievement02.png) - -### 解锁状态 - -当玩家在游戏内触达到开发者设定的目标时,成就状态会发生改变。 -- 未解锁:提交成就发布后的初始化状态,即玩家未达成成就; -- 已达成:玩家已达成成就,分为: - - 非分步成就:当玩家触达成就后,「未解锁」状态直接变为「已达成」状态; - - 分步成就:当玩家触达到成就某步骤数时,「未解锁」状态上会展示完成度百分比,当完成所有步骤后,状态变为「已达成」状态。 - -成就卡片状态: - -![img](https://capacity-files.lcfile.com/Ln6jBCdAHYNMQUYuKgX3tM2HutRwshLk/achievement01.png) - -### 白金成就 - -对于高品质的游戏,当玩家获得全部**本体成就**时,会解锁白金成就标识。 - -定义范围:正式发布的全部**本体成就**,未发布的成就不算在内。 - -
    - -## 功能说明 - -### 创建普通成就 - -要为新游戏创建一个成就,你可以在**开发者中心**、**游戏服务**下找到**云服务**标签。选择**成就**菜单,然后点击**创建普通成就**。 - -填写成就的相关信息,点击**保存**,这个成就进入到**待发布**状态。 - -![img](https://capacity-files.lcfile.com/o4oC1pEkt30fzfo3AyCnzPQbY13U4RtB/achievement04.png) - -### 编辑普通成就 - -成就发布前,要编辑已经创建的成就,请在成就列表里点击**编辑**。此时,可以看到与第一次创建成就时一样的配置页面,根据需要进行编辑。 - -成就发布后,**成就 ID**、**分步成就**、**成就分组**和**初始状态**这四项配置将无法修改。 -![img](https://capacity-files.lcfile.com/xGTAYHEVYKjLwFG2Un6zCc8dtHTp5OHC/achievement05.png) - -### 发布普通成就 - -完成编辑后,成就将处于**待发布**状态,开发者对其进行测试,验收正常后,至少要完成创建 **5** 个普通成就才能发布,点击**发布成就**,所有成就将发布到正式环境,请谨慎操作。 - -![img](https://capacity-files.lcfile.com/pAwJgNF8RCrYzenM8UR4k4QwiUCGDQ1g/achievement06.png) - -### 删除/重置成就 - -成就发布前,通过点击成就列表末尾的按钮,来**删除**和**重置**待发布的成就。 - -成就发布后,**不能被删除和重置**。 - -![img](https://capacity-files.lcfile.com/os9ACohsCtoid66Q5gHPuYvYpEpBQE2J/achievement07.png) - -### 申请白金成就 - -为了确保白金成就的质量,在申请白金成就时审核人员会根据游戏质量是否高于平均水品来判断审核结果。 - -![img](https://capacity-files.lcfile.com/zidlGulEJ61L3OYHOliqXisCqW4elkrW/achievement08.png) - -### 创建白金成就 - -当开发者申请白金成就资质通过后,点击**创建白金成就**按钮,此时完成游戏白金成就创建。白金成就需要配置**图标**、**名称**和**简介**。 - -![img](https://capacity-files.lcfile.com/wAfLQss5HPv3wUkCMtcmpsd1KbOne2Bn/achievement09.png) - -### 发布白金成就 -为了确保白金成就的挑战性,开发者需要至少创建 **10 个本体成就**后,才能发布白金成就。 -另外,发布白金成就后,**无法再创建本体成就**,只能创建拓展成就。 - -
    - -### 多语言设置 - -进入 **开发者中心 > 游戏服务 > 应用配置**,在右上方点击**多语言设置**。 - -![](https://capacity-files.lcfile.com/lmHIKcEAN11FPsoVIhmmz4n68OihdcLE/achievement10.png) - -第二步,根据游戏内的成就勾选需要的语言,点击**确定**生效。另外,在右侧已选框中删除添加错误的语言。 - -![](https://capacity-files.lcfile.com/XfkrmMOj4LlKzBE42mB87zm0pnVDwf7S/achievement11.png) - -完成以上步骤后,在编辑成就页面里点击语言标签右侧的 **+** 号,选择刚刚添加的语言。至此,可以为成就添加**名称**和**简介**的翻译。成就的图标和配置信息将继承默认语言的配置,不需要重复添加。 - -![](https://capacity-files.lcfile.com/KG8bdnj3ThbpI2v7x3J9uajzfBIE9EMG/achievement12.png) - -
    - -### 游戏内成就展示 - -- TapSDK:可以在游戏内展示成就面板,玩家根据开发者设计的入口打开成就界面,查看已达成和未解锁的游戏成就。 -- API:开发者也可以选择使用游戏自己的成就界面,同时调用 API 的方式向 TapTap 发送成就。 - -![img](https://capacity-files.lcfile.com/P7pD6xiO2KlkAxXUFzvwEA5zry2tVa23/achievement13.png) - -### 游戏内成就通知 - -- 当玩家在游戏中触发成就行为,在游戏顶部推送冒泡。界面的成就队列只有 1 条,如同时触发多条成就,则会排队显示。 -- 开发者对于游戏内的成就展示页和冒泡通知可以选择显示或隐藏。 - -![img](https://capacity-files.lcfile.com/DdEv3pd3kitPheTXD47vMhTjF1LpHv5r/achievement14.png) - -![img](https://capacity-files.lcfile.com/DSXcQaiQirx31spBfuQvsjvzQxE2Nyok/achievement15.png) - -### TapTap 上展示成就 -- 游戏需正式上架 TapTap,通过 TapTap 登录游戏:使用 TapTap 登录的玩家可以直接在 「TapTap 客户端-个人主页-关于」查看已解锁和未解锁的游戏成就。 -- 非 TapTap 登录游戏:如果游戏本身存在账号体系,需要游戏自己绘制一个 TapTap 账号绑定入口,将游戏账号和 TapTap 账号关联起来,将成就数据同步到 TapTap 上。 -- 目前只支持上架中国大陆区域的游戏在 TapTap 上展示成就。 - -:::tip -**关于绑定 TapTap 账号的两种场景如何处理** -- 场景 1:用户有一个非 TapTap 登录账号(账号 A),和一个没登录过游戏的 TapTap 号,需要游戏这里做账号绑定,他们是共用一个 TDS user ID,那 TapTap 上展示的是账号 A 的成就数据 -- 场景 2:用户有一个非 TapTap 登录账号(账号 A),和一个已经登录过游戏的 TapTap 号(账号 B),那 TapTap 上展示的是账号 B 的成就数据。那如果用户想要展示账号 A 的数据,那就游戏自身需要有解绑账号的功能,即把账号 B 和 TapTap 账号解除绑定,把 TapTap 账号置换出来和账号 A 进行绑定 -::: - -![img](https://capacity-files.lcfile.com/RxtLqJKJPAbHRz5qNJ3bD0wUx0UF4MY3/achievement_show_on_taptap1.png) - - -## 接入说明 - -### 接入准备 - -1. 入驻成为 TapTap 的开发者; -2. 在 TapTap 开发者中心创建游戏应用,且需要开通「内建账户」服务; -3. 若要给子账号添加权限,请至「权限管理」中给该账号设置「游戏管理员」权限; -4. 下载 TapSDK(最低支持版本 v3.2.0)集成到游戏包内。 - -### 接入流程 - -![img](https://capacity-files.lcfile.com/exAftqivPqahCSJlTjBYNR3vnkjXmYWc/achievement16.png) - -### 接入指南 - -见 **[成就系统 > 开发指南](/sdk/achievement/guide/)**。 - -### 测试验收 - -1. 开发者配置完成就后,可以设置少数**正式环境**的用户成为**测试人员**; -2. 测试人员可以体验到「待发布」状态的成就数据,来进行测试; -3. 在进行测试后发现数据不合理,修改了后想重新测试,只需[「重置进度」或「删除成就」](#删除重置成就)即可; -4. 如果测试人员的数据在发布前,没有进行重置,是会带到正式环境的,和其他用户的数据共同展示(注:是否需要删档测试,取决于运营需求)。 - -:::note -如果需要添加测试人员体验「待发布」状态的成就,请进入开发者中心通过右上角「工单」联系 TDS 技术支持。「工单」中请提供应用的 `Client ID` 以及测试账号实现 TapTap 登录后返回的 `Object ID`。 -::: diff --git a/docs/sdk/achievement/guide.mdx b/docs/sdk/achievement/guide.mdx deleted file mode 100644 index 8f0955879..000000000 --- a/docs/sdk/achievement/guide.mdx +++ /dev/null @@ -1,988 +0,0 @@ ---- -title: 成就系统开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import Languages from '../_partials/languages.mdx'; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -本文介绍如何在游戏中加入成就系统。TDS 推出的成就系统模块,是基于内建账户系统(TDSUser)的,具体请阅读 **[内建账户 > 开发指南](/sdk/authentication/guide/)**。 - -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - - - -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下权限: - -``` - -``` - - - -<> - - - -<> - - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启成就服务、绑定 API 域名; - -## SDK 获取 - -由于成就服务依赖内建账户,所以集成游戏成就服务所需 SDK 依赖库需要在[内建账户依赖库](/sdk/authentication/guide/#sdk-获取)的基础上,在 [下载页](/tap-download) 获得 TapSDK,另外添加 `TapAchievement` 模块: - - - - - - -{`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapAchievement_${sdkVersions.taptap.android}', ext:'aar') // TapTap 成就系统 -}`} - - - -{`// 成就系统 -TapAchievementResource.bundle -TapAchievementSDK.framework`} - - -<> - -{`PublicDependencyModuleNames.AddRange(new string[] { - // 添加 TapTap 成就系统 - "TapAchievement", -}); -`} - - - - - -## SDK 初始化 - -参考[内建账户](/sdk/authentication/guide/#sdk-初始化)的初始化方法,初始化内建账户服务会同步初始化游戏成就服务; - - - -## 模块基础设置 - - -<> - -PC 平台的成就详情界面会显示游戏名称和游戏 Icon。(下图中:左上角图片为应用图标,「人类跌落梦境」为应用名称) -![img](https://img.tapimg.com/market/images/0bee8d2437aaf06d5f145ff1f37897c5.png) - -默认情况下,SDK 会以应用的名字和图标来显示,如果需要*自定义*名字和图标,可以通过以下接口: - -```cs -TapAchievement.SetApplicationName(string applicationName) - -TapAchievement.SetApplicationIcon(Texture2D applicationIcon) -``` - - -<> -Android 不需要设置 - - -<> -iOS 不需要设置 - - -<> - -UE4 的 SDK 需要设置应用的名称和图标,用于在成就详情页面显示。 -(下图中:左上角图片为应用图标,「人类跌落梦境」为应用名称) -![成就移动端横屏详情页面](https://img.tapimg.com/market/images/0bee8d2437aaf06d5f145ff1f37897c5.png) -```cpp -FTapAchievementsPtr AchievementInterface = FTapAchievementModule::GetAchievementInterface(); - -FText ProjectName = /** 你的应用名称 */ -UTexture2D* ProjectTexture = /** 你的应用图标 */ - -AchievementInterface->SetApplicationName(ProjectName); -AchievementInterface->SetApplicationIcon(ProjectTexture); -``` - - - - -## 注册监听回调 -成就 SDK 中包含多个监听回调,分别会在初始化数据成功、初始化数据失败以及成就进度更新时被调用,请特别注意初始化数据成功的回调,这是成就 SDK 正常使用的前提,初始化数据失败时请提示用户或者在合适的时候重新初始化数据。 - - - -<> - -**使用前提** - -使用 TapTap.Achievement 前提是必须依赖 `TapTap.Bootstrap` 库。 - -**命名空间** - -```cs -using TapTap.Achievement; -``` - -注册监听回调: - -```cs -TapAchievement.RegisterCallback(IAchievementCallback callback); - -private class AchievementCallback:IAchievementCallback -{ - public void OnAchievementSDKInitSuccess() - { - // 成就 SDK 初始化成功 - } - - public void OnAchievementSDKInitFail(TapError errorCode) - { - if (errorCode != null) - { - // 初始化失败 - } - } - - public void OnAchievementStatusUpdate(TapAchievementBean bean, TapError errorCode) - { - if (errorCode != null) - { - // 成就状态更新失败 - return; - } - - if (bean != null) - { - // 成就状态更新成功 - } - } -} -``` - - -<> - -```java -TapAchievement.registerCallback(new AchievementCallback() { - @Override - public void onAchievementSDKInitSuccess() { - // 数据加载成功 - } - - @Override - public void onAchievementSDKInitFail(AchievementException exception) { - // 数据加载失败,请重试 - } - - @Override - public void onAchievementStatusUpdate(TapAchievementBean item, AchievementException exception) { - if (exception != null) { - // 成就更新失败 - return; - } - if (item != null) { - // item 更新成功 - } - } -}); -``` - - -<> - - -```objectivec -[TapAchievement registerCallBack:self]; - -- (void)onAchievementSDKInitSuccess { - // 数据加载成功 -} - -// 初始化失败 -- (void)onAchievementSDKInitFail:(nullable NSError *)error { - // 数据加载失败,请重试 -} - -// 成就状态改变 -- (void)onAchievementStatusUpdate:(nullable TapAchievementModel *)achievement failure:(nullable NSError *)error { - if (error) { - // 成就更新失败 - } else { - // achievement 更新成功 - } -} -``` - - - -<> - - -```cpp -FTapAchievementsPtr AchievementInterface = FTapAchievementModule::GetAchievementInterface(); - -AchievementInterface->OnAchievementStatusUpdate.AddLambda([](const FAchievementDescTap* Desc, const FAchievementTap* Achievement, const TSharedPtr& Error) - { - if (Desc && Achievement) - { - //成就更新成功 - } - else if(Error) - { - //成就更新失败 - } - }); -``` - - - - - -## 初始化数据 -由于成就系统会在本地记录用户的成就数据,所以请**在用户登录后初始化数据**。如果用户切换账号时,务必重新调用该接口,不然数据可能会存在账号存储混乱的问题。 - -这个步骤是异步操作,需要确认收到成功回调时才能进行更多操作。 - - - -```cs -TapAchievement.InitData(); -``` - -```java -TapAchievement.initData(); -``` - -```objectivec -[TapAchievement initData]; -``` - -```cpp -AchievementInterface->InitData( - FSimpleDelegate::CreateWeakLambda(this,[this]() - { - //初始化数据成功 - int32 TotalIconNumber = AchievementInterface->AsyncDownloadAllAchievementIcon(FDownloadIconStatus::CreateWeakLambda(this, [this](const FString& DisplayId, int32 LiftNumber) - { - if (LiftNumber == 0) - { - //异步加载图标完成 - } - })); - }), - FTUError::FDelegate::CreateLambda([](const FTUError& Error) - { - //初始化数据失败 - })); -``` - - - -## 获取全部成就数据 -全部成就数据分为本地数据和服务端数据两种:本地数据是记录在玩家本机的数据,本地数据会在调用初始化数据接口成功后被服务端数据刷新。主动调用服务端数据接口时也会更新本地数据。 - -如果在游玩过程中有更新服务端数据且需要实时更新的需求时,可以调用获取服务端数据接口来实现,正常情况下直接获取本地数据即可。 - - - -```cs -// 获取本地数据 -TapAchievement.GetLocalAllAchievementList((list, code) => -{ - if (code != null) - { - // 获取成就数据失败 - } - else - { - // 获取成就数据成功 - }); -} -// 获取服务器数据 -TapAchievement.FetchAllAchievementList((list, code) => -{ - if (code != null) - { - // 获取成就数据失败 - } - else - { - // 获取成就数据成功 - }); -} -``` - -```java -// 本地数据 -List allList = TapAchievement.getLocalAllAchievementList(); - -// 服务端数据 -TapAchievement.fetchAllAchievementList(new GetAchievementListCallBack() { - @Override - public void onGetAchievementList(List achievementList, AchievementException exception) { - if (exception != null) { - switch (exception.errorCode) { - case AchievementException.SDK_NOT_INIT: - // SDK 还未初始化数据 - break; - default: - // 数据获取失败 - } - } else { - // 成功获取数据 - } - } -}); -``` - -```objectivec -// 本地数据 - NSArray *allList = [TapAchievement getLocalAllAchievementList]; - - // 服务端数据 - [TapAchievement fetchAllAchievementList:^(NSArray *_Nullable result, NSError *_Nullable error) { - if (error) { - switch (error.code) { - case 9001: - // SDK 还未成功初始化 - break; - default: - // 数据获取失败 - break; - } - } else { - // 成功获取数据 - } - }]; -``` - -```cpp -// 本地数据 -const TArray& Descriptions = AchievementInterface->GetLocalAllAchievementList(); - -// 服务端数据 -AchievementInterface->FetchAllAchievementList( - FTapAchievementDescriptionResult::CreateWeakLambda(this, [this](const TArray& Descriptions) - { - //获取服务端数据成功 - AchievementInterface->AsyncDownloadAllAchievementIcon(FDownloadIconStatus::CreateWeakLambda(this, [this](const FString& DisplayId, int32 LiftNumber) - { - if (LiftNumber == 0) - { - //异步加载图标完成 - } - })); - }), - FTUError::FDelegate::CreateWeakLambda(this, [this](const FTUError& Error) - { - //获取服务端数据失败 - })); -``` - - - -## 获取当前用户成就数据 -用户成就数据分为本地数据和服务端数据两种:本地数据是记录在玩家本机的数据,本地数据会在调用初始化数据接口成功后和服务端数据合并(对单个成就来说会以步长更高的数据为准)。主动调用服务端数据接口也会和本地数据进行合并。 - -对于用户数据,一般以本地数据为准。服务端数据可能存在上报失败等可能导致数据并非实时。 - - -```cs -// 获取本地数据 -TapAchievement.GetLocalUserAchievementList((list, code) => -{ - if (code != null) - { - // 获取成就数据失败 - } - else - { - // 获取成就数据成功 - }); -} -// 获取服务器数据 -TapAchievement.FetchUserAchievementList((list, code) => -{ - if (code != null) - { - // 获取成就数据失败 - } - else - { - // 获取成就数据成功 - }); -} -``` - -```java -// 本地数据 -List userList = TapAchievement.getLocalUserAchievementList(); - -// 服务端数据 -TapAchievement.fetchUserAchievementList(new GetAchievementListCallBack() { - @Override - public void onGetAchievementList(List achievementList, AchievementException exception) { - if (exception != null) { - switch (exception.errorCode) { - case AchievementException.SDK_NOT_INIT: - // SDK 还未初始化数据 - break; - default: - // 数据获取失败 - } - } else { - // 成功获取数据 - } - } -}); -``` - -```objectivec -// 本地数据 - NSArray *userList = [TapAchievement getLocalUserAchievementList]; - -// 服务端数据 -[TapAchievement fetchUserAchievementList:^(NSArray *_Nullable result, NSError *_Nullable error) { - if (error) { - switch (error.code) { - case 9001: - // SDK 还未成功初始化 - break; - default: - // 数据获取失败 - break; - } - } else { - // 成功获取数据 - } - }]; -``` - -```cpp -// 本地数据 -const TArray& Achievements = AchievementInterface->GetLocalUserAchievementList(); - -// 服务端数据 -AchievementInterface->FetchUserAchievementList( - FTapAchievementProgressResult::CreateWeakLambda(this, [this](const TArray& Achievements) - { - //获取服务端数据成功 - }), - FTUError::FDelegate::CreateWeakLambda(this, [this](const FTUError& Error) - { - //获取服务端数据失败 - })); -``` - - - -## 达成某个成就(直接获得) - - - -```cs -// displayID 是在开发者中心中添加成就时自行设定的 成就 ID -TapAchievement.Reach("displayID"); -``` - -```java -// displayID 是在开发者中心中添加成就时自行设定的 成就 ID -TapAchievement.reach("displayID"); -``` - -```objectivec -// displayID 是在开发者中心中添加成就时自行设定的 成就 ID -[TapAchievement reach:@"displayId"]; -``` - -```cpp -// DisplayId 是在开发者中心中添加成就时自行设定的 成就 ID -if (const FTapAchievementsPtr AchievementInterface = FTapAchievementModule::GetAchievementInterface()) -{ - AchievementInterface->Reach(DisplayId); -} -``` - - - -## 分步成就增长步数 -成就增长步数提供两种方式调用,`growSteps` 中传递当前增量达成的步数(例如:多走了 5 步,则传递 5 即可),`makeSteps` 中传递当前成就已达成的步数(例如:当前已经走了 100 步,则传递 100),调用 `growSteps` 时 SDK 内部会计算当前全量步数。 - - - -```cs -// displayID 是在开发者中心中添加成就时自行设定的 成就 ID -TapAchievement.GrowSteps("displayID", step); -TapAchievement.MakeSteps("displayID", step); -``` - -```java -// displayID 是在开发者中心中添加成就时自行设定的 成就 ID -TapAchievement.growSteps("displayID", 5); -TapAchievement.makeSteps("displayID", 100); -``` - -```objectivec -// displayID 是在开发者中心中添加成就时自行设定的 成就 ID -[TapAchievement growSteps:@"displayID" numSteps:5]; -[TapAchievement makeSteps:@"displayID" numSteps:100]; -``` - -```cpp -// DisplayId 是在开发者中心中添加成就时自行设定的 成就 ID -if (const FTapAchievementsPtr AchievementInterface = FTapAchievementModule::GetAchievementInterface()) -{ - AchievementInterface->GrowSteps(DisplayId, 5); - AchievementInterface->MakeSteps(DisplayId, 100); -} -``` - - - -## 设置冒泡开关 -默认情况下,成就达成时 SDK 会自行展示一个冒泡浮窗提示玩家已达成相应成就。需要关闭请调用如下接口: - - - -```cs -TapAchievement.SetShowToast(bool isShow); -``` - -```java -TapAchievement.setShowToast(false); -``` - -```objectivec -[TapAchievement setShowToast:NO]; -``` - -```cpp -AchievementInterface->SetShowToast(false); -``` - - - -## 打开成就展示页 -SDK 自带一个展示所有成就和已达成成就情况的页面: - - - -```cs -TapAchievement.ShowAchievementPage(); -``` - -```java -TapAchievement.showAchievementPage(); -``` - -```objectivec -[TapAchievement showAchievementPage]; -``` - -```cpp -AchievementInterface->ShowAchievementsUI(); -``` - - - -## 成就相关数据解读 - - - -```cs -public string displayId; // 成就 ID -public int visible = VisibleFalse; // 是否是隐藏成就 -public string title; // 标题 -public string subTitle; // 副标题 -public string achieveIcon; // 图标 -public int step; // 设定步数 -public bool fullReached; // 是否达成 -public int reachedStep; // 达成步数 -public long reachedTime; // 达成时间 -public AchievementStats stats; // 当前成就稀有度指标 -``` - -```java - /*base*/ - private String displayId; // 成就 ID - private int visible = VISIBLE_TRUE; // 是否是隐藏成就 - private String title; // 标题 - private String subTitle; // 副标题 - private String achieveIcon; // 图标 - private int step; // 设定步数 - private AchievementStats stats; // 当前成就稀有度指标 - private int type; // 类型,1 为普通成就,99 为白金成就 - /*user*/ - private boolean fullReached; // 是否已达成 - private int reachedStep; // 当前达成步数 - private long reachedTime; // 当前达成时间 -``` - -```objectivec -@property (nonatomic, copy, readonly) NSString *displayId; // 成就 ID -@property (nonatomic, copy, readonly) NSString *achieveIcon; // 成就图片 -@property (nonatomic, copy) NSString *title; // 成就名称 -@property (nonatomic, copy, readonly) NSString *subTitle; // 成就描述 -@property (nonatomic, assign, readonly) NSNumber *step; // 完成成就总的步数 -@property (nonatomic, strong) TDSAchievementStatus *stats; // 当前成就稀有度指标 -@property (nonatomic, assign) NSInteger type; // 类型,1 为普通成就,99 为白金成就 - -// 用户数据 -@property (nonatomic, assign) BOOL fullReached; // 是否到达成就 -@property (nonatomic, assign) long reachedTime; // 成就达到时间 -@property (nonatomic, assign) NSInteger reachedStep; // 当前完成步数 -``` - -```cpp -FString DisplayId; // 成就 ID -bool bIsHide = false; // 是否是隐藏成就 -int32 CountStep = 0; // 完成成就总的步数 -FString AchievementTitle; // 标题 -FString AchievementSubtitle; // 副标题 -FString AchievementIconUrl; // 成就图标网络地址 -float Rarity = 0.f; // 当前成就稀有度指标 -double RarityD = 0.0; // 当前成就稀有度指标 double -int32 Level = 1; // 当前成就稀有度级别(1~4) -int32 Type = 1; // 类型,1 为普通成就,99 为白金成就 -mutable UTexture2D* IconTexture = nullptr; // 成就图标贴图资源(受引擎内存管理,设置 "IconTexture = nullptr" 引擎定时回收内存,可由 AsyncDownloadAllAchievementIcon 统一异步加载并转化贴图) - -// 用户数据 -FString DisplayId; // 成就 ID -FDateTime CompleteTime; // 成就达成时间 -int32 CompleteStep = 0; // 当前达成步数 -bool bFullCompleted = false; // 是否达成成就 -``` - - - -## 国际化 - -成就支持设置语言: - -:::tip -初始化数据时只会从服务端更新当前的语言对应的成就数据,如果在初始化后切换语言的话,需要重新调用 `fetchAllAchievementList` 和 `fetchUserAchievementList` 接口来更新成就数据的多语言内容。 -::: - - - -## REST API - -下面我们介绍成就相关的 REST API 接口。 -开发者可以自行编写程序或脚本调用这些接口在服务端进行管理性质的操作。 - -### 请求格式 - -POST 请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 - -请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,参数如下表: - -Key|Value|含义 ----|----|--- -`X-TDS-Id`|`{{clientId}}`|游戏的 `Client Id`,可在控制台查看 -`X-TDS-Server-Secret`|`{{serverSecret}}`|游戏的 `Server Secret`,可在控制台查看 - - -参见文档关于[应用凭证](/sdk/storage/guide/setup-dotnet#应用凭证)的说明。 - -除了在 `X-TDS-Id` 这个 HTTP Header 中传入 `Client Id` 外,还需要在 URL 中指定 `Client Id`,两者的值需要一致。 - -获取成就的接口需要在 URL 中指定语言,详见[语言代码列表](#语言代码列表)。 - - -异常时返回 500(HTTP 状态码)错误,例如: - -```json -{ - "code": "500", - "msg": "成就服务忙,稍后请求", -} -``` - -### 全部成就列表 - -获取游戏的全部成就,调用时需在 URL 中指定相应的语言。 - -```sh -curl -X GET \ - -H "X-TDS-Id: {{clientId}}" \ - -H "X-TDS-Server-Secret: {{serverSecret}}" \ - https://tds-tapsdk.cn.tapapis.com/achievement/open/v1/clients/{{clientId}}/achievements/languages/ -``` - -返回数据结构体: - -```json -{ - "success": true, - "data": { - "list": [ - { - "achievement_id": "成就 id", - "client_id": "应用 Client Id", - "achievement_open_id": "成就外部 id(开发者中心创建成就时自定义的成就 ID,向 SDK 上报成就的唯一标识)", - "achievement_type": "成就类型:1-普通成就,99-白金成就", - "is_hide": "是否隐藏:0-不隐藏,1-隐藏", - "count_step": "成就步数,不分步时是 1", - "show_order": "成就顺序,白金成就是 0", - "achievement_config_out_dto": { - "achievement_config_id": "成就配置 id", - "achievement_id": "成就 id", - "language_id": "语言 id", - "achievement_icon": "成就 icon 链接", - "achievement_title": "成就标题", - "achievement_sub_title": "成就副标题" - }, - "achievement_rarity": { - "rarity": "稀有度比率", - "level": "稀有度:1-普通,2-稀有,3-珍贵,4-极为珍贵" - } - } - ] - } -} -``` - -### 玩家成就列表 - -获取某一玩家取得的成就,调用时需在 URL 中指定该玩家对应的 TDS 内建账户的 ObjectId 和语言代码。 - -```sh -curl -X GET \ - -H "X-TDS-Id: {{clientId}}" \ - -H "X-TDS-Server-Secret: {{serverSecret}}" \ - https://tds-tapsdk.cn.tapapis.com/achievement/open/v1/clients/{{clientId}}/users//achievements/languages/ -``` - -返回数据结构体: - -```json -{ - "success": true, - "data": { - "list": [ - { - "achievement_id": "成就 id", - "client_id": "游戏 id", - "achievement_open_id": "成就外部 id(游戏在 DC 新增成就时绑定的 ID)", - "achievement_type": "成就类型:1-普通成就,99-白金成就", - "is_hide": "是否隐藏:0-不隐藏,1-隐藏", - "count_step": "成就步数,不分步时是 1", - "show_order": "成就顺序,白金成就是 0", - "achievement_config_out_dto": { - "achievement_config_id": "成就配置 id", - "achievement_id": "成就 id", - "language_id": "语言 id", - "achievement_icon": "成就 icon 链接", - "achievement_title": "成就标题", - "achievement_sub_title": "成就副标题" - }, - "achievement_rarity": { - "rarity": "稀有度比率", - "level": "稀有度:1-普通,2-稀有,3-珍贵,4-极为珍贵" - }, - "user_achievement_id": "用户成就 id", - "complete_time": "完成时间戳(毫秒级)", - "completed_step": "完成步数", - "full_completed": "是否完全完成,true-是,fasle-否" - } - ] - } -} -``` - -### 提交成就 - -可以调用这一接口提交单个或多个玩家取得的成就,提交的成就会**追加**到玩家已达成的成就列表。 - -```sh -curl -X POST \ - -H "X-TDS-Id: {{clientId}}" \ - -H "X-TDS-Server-Secret: {{serverSecret}}" \ - -H "Content-Type: application/json" \ - -d '{"data": [{"user_id": , "list": - [{ - "achievement_id": "成就 id", - "achievement_open_id": "成就外部 id(开发者中心创建成就时自定义的成就 ID,向 SDK 上报成就的唯一标识)", - "complete_time": "完成时间戳(毫秒级)", - "completed_step": "完成步数" - }] - }] - }' \ - https://tds-tapsdk.cn.tapapis.com/achievement/open/v1/clients/{{clientId}}/achievements -``` - -返回数据结构体: - -```json -{ - "success": true, - "data": { - "list": [ - { - "user_id": "ObjectId", - "result_list": [ - { - "result": "本条数据上报是否成功,true 成功,false 失败", - "code": "成功时,返回 0,失败时,返回对应错误码", - "msg": "成功时,无返回,失败时,返回对应错误信息" - } - ] - } - ] - } -} -``` - -### 语言代码列表 - -使用 ISO 639-1 中定义的双小写字母语言代码(例如,`en` 表示英语,`jp` 表示日语),但: - -1. ISO 639-1 中未包括的语言,使用 ISO 632-2 中定义的三小写字母语言代码(例如,`fil` 表示菲律宾语) -2. 仅使用语言代码无法表示所需语言时,附加 ISO 3166-1 中定义的地区代码(例如,`zh_CN` 表示简体中文) - -当前 REST API 支持的语言代码如下: - -| 代码 | 语言 | -| ------- | ----------- | -| zh_CN | 简体中文 | -| zh_TW | 繁体中文 | -| en_US | 英语(美国) | -| ja_JP | 日文 | -| ko_KR | 韩文 | -| pt_PT | 葡萄牙语 | -| vi_VN | 越南语 | -| hi_IN | 印度语 | -| id_ID | 印尼语 | -| ms_MY | 马来语 | -| th_TH | 泰语 | -| es_ES | 西班牙语 | -| af | 南非荷兰语 | -| am | 阿姆哈拉语 | -| bg | 保加利亚语 | -| ca | 加泰罗尼亚语 | -| hr | 克罗地亚语 | -| cs | 捷克语 | -| da | 丹麦语 | -| nl | 荷兰语 | -| et | 爱沙尼亚语 | -| fil | 菲律宾语 | -| fi | 芬兰语 | -| fr | 法语 | -| de | 德语 | -| el | 希腊语 | -| he | 希伯来语 | -| hu | 匈牙利语 | -| is | 冰岛语 | -| it | 意大利语 | -| lv | 拉脱维亚语 | -| lt | 立陶宛语 | -| no | 挪威语 | -| pl | 波兰语 | -| ro | 罗马尼亚语 | -| ru | 俄语 | -| sr | 塞尔维亚语 | -| sk | 斯洛伐克语 | -| sl | 斯洛文尼亚语 | -| sw | 斯瓦希里语 | -| sv | 瑞典语 | -| tr | 土耳其语 | -| uk | 乌克兰语 | -| zu | 祖鲁语 | - -注意,上表中的部分语言虽然 REST API 支持,但[客户端 SDK 并没有支持](#国际化)。 - - -## 视频教程 - -可以参考视频教程:[TapTap 成就功能讲解及接入](https://www.bilibili.com/video/BV1yH4y1z7F7/),了解如何在 Untiy 项目中接入成就功能。 - -更多视频教程见[开发者学堂](https://developer.taptap.cn/tds-tutorials/list)。因为 SDK 功能在不断完善,视频教程可能出现与新版 SDK 功能不一致的地方,以当前文档为准。 \ No newline at end of file diff --git a/docs/sdk/anti-addiction/_category_.json b/docs/sdk/anti-addiction/_category_.json deleted file mode 100644 index 29a13b08d..000000000 --- a/docs/sdk/anti-addiction/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "合规认证", - "collapsed": true, - "position": 4 -} diff --git a/docs/sdk/anti-addiction/faq.mdx b/docs/sdk/anti-addiction/faq.mdx deleted file mode 100644 index 13eac9550..000000000 --- a/docs/sdk/anti-addiction/faq.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - -## 常见问题 - - - -### 个人/团体的开发者需要接入防沉迷吗 - -按照国家新闻出版署[《关于进一步严格管理切实防止未成年人沉迷网络游戏的通知》](https://www.nppa.gov.cn/xxfb/tzgs/202108/t20210830_666285.html),各游戏出版运营企业均须在游戏内落实游戏实名认证和防沉迷新策略。 -对于没有版号的游戏,可以接入 TDS 推出的 [实名认证防沉迷](/sdk/anti-addiction/features/)。 - -### 防沉迷和登录有什么关系? - -防沉迷依赖于 TapTap 登录 SDK,详情参考 [开发指南](/sdk/anti-addiction/guide/) 。同时,配套使用 TapTap 登录 + 合规认证服务( 3.29.0 及以上版本 SDK),可实现自动静默授权、认证,无需玩家手动授权、输入实名信息等,极大提升流畅度。即使开发者需要使用必须经玩家手动同意的授权项,配套使用 TapTap 登录 + 合规认证服务 也可让玩家通过「快速认证服务」便捷的完成实名。 - - -### 实名认证通过后退出账号,再次启动游戏,会跳过实名认证流程 - -家首次进入游戏会触发实名认证弹窗,让玩家授权游戏获取 TapTap 实名信息或输入身份信息,认证通过后,后面每一次以同样的 `userIdentifier` (唯一标识)调用认证接口都会直接拿到第一次认证的结果,不再触发弹窗。如玩家需要换用其他账号登录,需要主动退出账号。 - - - -### 如何开通实名认证与防沉迷服务 - -可以在 **开发者中心后台 > 游戏服务 > 开发与构建 > 合规认证** 处自助开通服务。目前提供两种方案,游戏开通时选择其中一种: - -* **有版号**。完成控制台提示的前置条件,点击开通,然后配置好 [中宣部参数](/sdk/anti-addiction/features/#注册中宣部实名认证系统): - * 将中宣部系统后台的**游戏备案识别码、应用标识、应用密钥**填写到 TapTap 开发者中心后台对应处。 - * 将 TapTap 开发者中心后台显示的 **IP 白名单地址**复制、填写到中宣部系统后台。 -* **暂无版号**。无版号游戏无法配置中宣部参数,可直接选择开通,等游戏有了版号,可以配置 [中宣部参数](/sdk/anti-addiction/features/#注册中宣部实名认证系统) 并切换到有版号方案。切换后客户端代码不受影响,不需要修改。 - -### SDK 自带哪些用户界面(UI) - -实名认证和防沉迷 SDK 提供的用户界面主要在防沉迷授权阶段,可参考 [功能介绍文档](/sdk/anti-addiction/features/#接入-tds-实名认证和防沉迷服务) 中的界面预览。 - -### 授权失败 - -在实名认证时使用 TapTap 快速认证服务提示「授权失败」,请至后台 [配置签名证书](/sdk/start/quickstart/#配置签名证书)。 - -### 未查询到实名认证配置 - -实名认证时使用手动输入实名信息服务提示「未查询到实名认证配置」,原因是未开启实名认证服务,需要在 开发者中心后台 > 游戏服务 > 开发与构建 > 合规认证 处自助开通服务。 - -### userIdentifier is empty - -调用认证接口时需要传入的玩家唯一标识 userIdentifier 参数值为空,建议开发者对此做非空判断。 - -### 未弹出实名认证窗口/未收到回调 - -这种情况一般是仅调用了初始化防沉迷 UI 模块代码,也就是说只完成了 SDK 的初始化,同时注册防沉迷的消息监听。 - -触发实名认证弹窗**必须调用** [认证接口](/sdk/anti-addiction/guide/#防沉迷认证),之后才会收到回调。 - -### 重复认证 - -我们预期同一个玩家认证过一次之后不再触发弹窗,防沉迷服务直接使用第一次认证的结果,这样用户体验更好。 - -如果出现重复认证,可以按照以下思路排查: - -* 首先确认游戏使用的 [玩家唯一标识 userIdentifier](#玩家唯一标识-useridentifier-参数说明) 符合要求。如果同一个玩家用的 userIdentifier 会发生改变,在防沉迷服务中会被视为不同用户,导致重复认证。这个时候需要游戏传入合适的 userIdentifier,建议接入 [TDS 内建账户系统](/sdk/taptap-login/guide/start/#一键完成-taptap-登录),用 `objectId` 作为玩家唯一标识;或者使用 [单纯 TapTap 用户认证](/sdk/taptap-login/guide/tap-login/#taptap-登录并获取登录结果),传入 `openid` 或 `unionid` 作为玩家唯一标识。 -* 如果是**有版号游戏**,请确认在 开发者中心后台游戏服务 > 开发与构建 > 合规认证 处填写的参数无误。如果参数有问题,请求中宣部接口会失败,导致重复认证。除此之外,还需要确定应用在中宣部是否审核通过、接口测试是否完成。请按照如下步骤进行排查: - * **IP 白名单地址**全部填入中宣部系统后台。 - * **游戏备案识别码、应用标识**和中宣部后台保持一致。 - * **应用密钥**在有效期内(有效期为半年,注意在失效前更新)。 - * **中宣部接口测试**是否完成,完成的状态应该为**已通过**,中宣部目前需要测试的接口用例为 8 个。 - * 检查游戏在中宣部是否处于审核通过状态。 -### iOS 使用快速认证完成后,实名弹窗未自动关闭 - -可以参考 [配置跳转 TapTap 应用](/sdk/taptap-login/guide/start/#配置跳转-taptap-应用)文档,在 info.plist 中添加配置; - -## 注意事项 - -### 玩家唯一标识 userIdentifier 参数说明 - -第一次认证会触发实名认证弹窗,让玩家授权游戏获取 TapTap 实名信息或输入身份信息,认证通过后,后面每一次以同样的 userIdentifier 调用认证接口都会直接拿到第一次认证的结果,不再触发弹窗。 - -因此,同一个用户的唯一标识应该要保证唯一性。 - -### 测试实名认证环境 - -无论是 Android 还是 iOS 项目,不支持在 Unity Editor 环境里调试,请对应打包到真实设备或者移动端的模拟器中进行测试实名认证防沉迷的相关功能。 - -### 使用 TapTap 快速认证报:获取实名信息失败,请稍后重试。 - -- 检查 TapTap 客户端登录的账号是否在 TapTap 客户端进行了实名,自 v3.22.0 版本开始,TapTap 客户端登录的账号未在 TapTap 客户端实名时进行 TapTap 快速认证并不会报该异常,而是跳转到 TapTap 客户端进行实名认证。 -- 检查设备时间是否开启联网同步了,设备时间不准确也会导致该异常发生。 diff --git a/docs/sdk/anti-addiction/features.mdx b/docs/sdk/anti-addiction/features.mdx deleted file mode 100644 index c6d053632..000000000 --- a/docs/sdk/anti-addiction/features.mdx +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: 实名认证和防沉迷功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -合规认证提供实名认证、防沉迷服务,帮助开发者达成国家新闻出版署 [《关于进一步严格管理 切实防止未成年人沉迷网络游戏的通知》](https://www.nppa.gov.cn/xxfb/tzgs/202108/t20210830_666285.html) 要求。 -开发者请按功能指引和开发指南文档规范接入,若恶意绕过防沉迷功能,违反防沉迷要求,开发者将承担全部法律责任,并且相关游戏将被下架。 - -:::tip -暂无版号的游戏也可以使用实名认证和防沉迷功能。 -::: - - -## 准备工作 - -在使用实名认证和防沉迷服务之前,需要在对应的游戏里找到 **游戏服务 > 合规认证** 开通服务。如下图,可选「已有版号」或「暂无版号」方案,然后点击立即开通即可。 - -![](https://img.tapimg.com/market/images/8dfc08c01bb0ae1ea62a2f574acdbc49.png) - -「已有版号」或「暂无版号」方案根据游戏的实际情况选择。 - -### 暂无版号 - -版号还未申请或正在国家新闻出版署审核中的游戏,可选「暂无版号」方案。游戏拿到版号后,可以再切换到有版号方案。「暂无版号」方案只需要点击上图的立刻开通按钮,就操作完成了。 - -### 已有版号 - -#### 注册中宣部实名认证系统 - -有版号游戏需要完成中宣部实名认证系统的注册,无版号游戏暂时不需要注册。注册步骤如下: - -在 [中宣部网络游戏防沉迷实名认证系统](https://wlc.nppa.gov.cn/fcm_company/index.html#/login?redirect=%2F) 完成应用的注册,应用注册后还需要完成中宣部「接口测试」并且游戏需要通过中宣部的审核。 - -获取相关凭证: - -- 游戏备案识别码 `bizId` -- 应用标识 `APPID` -- 应用密钥 `Secret Key` - -![](/img/anti-addiction/biz-id.png) - -![](/img/anti-addiction/secretkey.png) - -游戏在中宣部的审核状态必须为「审核通过」的状态。 - -![](https://capacity-files.lcfile.com/BC7gUR6wfpOh9PHj8XwKybxseeHhl6Fq/anti-examine-success.png) - -游戏在中宣部必须完成接口测试,中宣部目前需要测试的接口用例为 8 个,需要注意的是在中宣部 **测试接口 > 预置参数 > IP 白名单** 这里的「IP 白名单」需要配置游戏侧自己的公网 IP,中宣部接口测试是临时性的,通过之后便不再使用了。 - -![](/img/anti-addiction/testcase.png) - -然后在 TapTap 开发者中心后台完成参数配置,使用 TapTap 开发者中心后台提供的 IP 白名单地址填入中宣部系统后台: - -![](https://img.tapimg.com/market/images/37abdc0ce0f37e0ad199761d370cb8d6.png) - -### 接入 TapTap 登录 -防沉迷模块依赖于 [TapTap 登录模块](https://developer.taptap.cn/docs/sdk/taptap-login/features/),开发者接入防沉迷前应先接入 TapTap 登录相关依赖。 - - -## 接入实名认证和防沉迷服务 - -完成准备工作后,方可接入实名认证和防沉迷服务SDK。 - -按照国家新闻出版署[《关于进一步严格管理切实防止未成年人沉迷网络游戏的通知》](https://www.nppa.gov.cn/xxfb/tzgs/202108/t20210830_666285.html),各游戏出版运营企业均须在游戏内落实游戏实名认证和防沉迷新策略。《通知》中要求,所有用户必须使用真实有效身份信息进行游戏账号注册并登录网络游戏。 - -合规认证服务为开发者自动对接了中宣部网络游戏防沉迷实名认证系统,并以开发者主体进行信息上报,符合中宣部对游戏企业接入的合规要求。 - -### 自动静默认证 - -推荐配套使用 TapTap 登录 + 合规认证服务( 3.29.0 及以上版本 SDK),可实现自动静默授权、认证,无需玩家手动授权、输入实名信息等,极大提升流畅度。 - -### TapTap 快速认证 - -合规认证提供 TapTap 快速认证服务,便于玩家使用 TapTap 账号中通过国家认证的实名信息,快速完成游戏实名认证流程。 - -:::tip -推荐配套使用 TapTap 登录 + 合规认证服务( 3.29.0 及以上版本 SDK),快速认证弹窗步骤也可省略,实现自动静默授权、认证,无需玩家手动授权、输入实名信息等。 -::: - -![](/img/anti-addiction/image2021-10-18_17-57-51.png) - -- 若玩家点击「使用」并同意授权后,将允许使用其 TapTap 账号中通过国家认证的实名信息,快速完成游戏实名认证流程。注意,当玩家使用 TapTap 登录时,通过授权步骤即可快速完成实名认证。若玩家拒绝授权,将出现此快速认证弹窗,此时仅展示「使用」快速认证服务按钮,以便玩家尽快完成实名。 - -![](/img/anti-addiction/image2021-10-19_17-4-12.png) - -- 若用户点击「不使用」,将唤起「手动填写实名信息」弹窗,用户填写并提交身份信息后完成游戏实名认证流程。 - - -## 防沉迷策略 - -《通知》中要求,游戏企业必须严格管理未成年的游戏时间以及付费额度。SDK 封装了相应接口,游戏可通过云端查询未成年玩家是否处于可以游戏的时间段内,以及未成年玩家的某笔消费是否受限。 - -![](/img/anti-addiction/not-allow-play.png) - -### 游戏时间限制 - -仅允许未成年人在**周五、周六、周日和法定节假日的 20:00 至 21:00** 进行游戏。 - -### 充值额度限制 - -- 不满 8 岁,无法在游戏内进行充值。 - -**同一网络游戏企业**所提供的游戏付费服务需要满足: - -- 满 8 岁,不满 16 岁的用户,单次充值 ≤ 50 元 ,单月充值累计 ≤ 200 元 -- 满 16 岁,不满 18 岁的用户,单次充值 ≤ 100 元 ,单月充值累计 ≤ 400 元 - -注意,计算限制时,同一企业的所有网络游戏的充值共同累计。 -例如,假定一家企业名下有两款网络游戏,一个满 8 岁,不满 16 岁的用户某月在其中一款游戏下累计充值了 100 元,那么他在另一款游戏下最多只能再充 100 元。 - - -## 测试账号 - -:::tip -无论是否有版号,都可以使用测试账号。最新的测试账号功能,已经去除了测试模式,同时不强制使用 unionid 作为玩家唯一标识,你可以使用自定义的唯一标识。 -::: - -![](https://img.tapimg.com/market/images/e971df09e5496b35b4c321daa34fa657.png) - -测试账号根据使用场景分为两大类: -- 自测专用:对游戏团队内部,用于进行实名认证防沉迷功能测试的测试账号。 -- 版审专用:提供给版审单位,用于申请游戏版号的测试账号。 - -### 自测专用测试账号 -- 可自主设置账号可玩时段,便于测试防沉迷时间限制 -- 可自主设置账号已充值金额,便于测试防沉迷消费限制 -- 未实名账号测试实名后,可自主重置回未实名状态,便于多次测试账号实名 -- 共提供 12 个不同实名状态、年龄段测试账号 - -### 版审专用测试账号 -- 不可设置可玩时段和已充值金额,避免版审时误操作导致审核驳回 -- 未实名账号测试实名后,可自主重置回未实名状态,用于多次版审时,未实名账号均已实名,需要恢复未实名的场景 -- 提供 30 个标准版审要求测试账号,及 12 个备用测试账号 - - 未实名空账号一组,共 3 个 - - 未成年人账号两组,共 18 个,每组含高、中、低(满 16 周岁未满 18 周岁,满 8 周岁未满 16 周岁,未满 8 周岁)各 3 个 - - 成年人账号一组(满 18 周岁),共 9 个 - - 12 个备用账号包含:满 8 周岁未满 12 周岁账号 6 个;满 18 周岁账号 6 个 - -实名认证防沉迷提供的测试账号强依赖于 TapTap 登录,请确保已完成对应能力接入。 - - -## 可玩年龄限制 - -![](https://img.tapimg.com/market/images/9fbbed39c52b2fbf963106478b73aea9.png) - -开启该功能,可限制未满所配置年龄的玩家进入游戏。 -该功能主要用于:希望根据游戏适龄、审核要求等情况,在《关于进一步严格管理切实防止未成年人沉迷网络游戏的通知》 要求基础上,额外限制「8/12/16/18 周岁以下用户不可游戏」的场景。 - -该功能需在 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 合规认证 > 可玩年龄限制** 中开启使用。对应开发文档请参考 [可玩年龄限制开发指南](/v4/sdk/anti-addiction/guide) diff --git a/docs/sdk/anti-addiction/guide.mdx b/docs/sdk/anti-addiction/guide.mdx deleted file mode 100644 index bf16f960b..000000000 --- a/docs/sdk/anti-addiction/guide.mdx +++ /dev/null @@ -1,997 +0,0 @@ ---- -title: 实名认证和防沉迷开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -:::tip -使用 TDS 实名认证和防沉迷服务之前,需要在 **开发者中心后台 > 游戏服务 > 开发与构建 > 合规认证** 处开通服务,可选择「已有版号」或「暂无版号」方案。 -::: - -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- 支持 UE 4.26.2 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - - - -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于正常网络请求 | 用户首次使用该功能时会申请权限 | -| 获取网络状态权限 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下权限: - -``` - - -``` - - - -<> - - - -<> - - - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启应用配置。 -2. 参考[实名认证和防沉迷功能介绍](/sdk/anti-addiction/features/#准备工作)中准备工作开通防沉迷服务。 -3. 防沉迷模块依赖于 [TapTap 登录模块](/sdk/taptap-login/features),开发者接入防沉迷前应先接入 TapTap 登录 相关依赖。 - -## SDK 配置 - -可以在 [下载页](/tap-download) 获得 TapSDK,引入防沉迷模块。 - - - -<> - - - -iOS 平台配置: - -使用 Xcode 13.0 beta 5 编译,检查 Unity 输出的 Xcode 工程: - -
    -查看 Unity 输出的 Xcode 工程详情配置 - -1. 请确保设置 `Xcode` - `General` - `Frameworks, Libraries, and Embedded Content` 中的 `AntiAddictionService.framework` 和 `AntiAddictionUI.framework` 为 `Do Not Embed`。 - -![](https://capacity-files.lcfile.com/AC0rIAqYHzn7xtPLeSXoA56B1AGEPTGh/anti-framework-donotembed.png) - -2. 如果编译报错找不到头文件或者模块,请确保 `Xcode` - `Build Settings` - `Framework Search Paths` 中的路径以保证 Xcode 正常编译。 - -![](https://capacity-files.lcfile.com/XezAoU67JbFIaNkXXvm2wNCMTgQCzfgv/anti-search-paths.png) - -3. 确保 Xcode 工程的 `Build Settings` 的 `Swift Compile Language` / `Swift Language Version` 为 `Swift5`。 - -![](https://capacity-files.lcfile.com/dU2H5QlDIjX6eDHto0IiEoV7wLeQxXII/anti-swift.png) - -4. 添加依赖库 `libz.tbd`、`libc++.tbd`。 - -![](https://capacity-files.lcfile.com/OAoB4JFxLvsPKLenTeep5qDkwwygmCCI/anti-lib.png) - -5. 开始代码接入。 - -6. 将 `AntiAddiction-Unity/Assets/Plugins/iOS/Resource/AntiAdictionResources.bundle` 拷贝到 Unity 导出的 Xcode 工程目录下(如果 Unity 项目没有正确导入 `AntiAddictionResources.bundle`)。假设你的 Unity 项目名称为 AntiDemo,则默认导出 Xcode 工程名为 antidemoxcode,需要将 `AntiAdictionResources.bundle` 拷贝到 antidemoxcode 目录里。拷贝成功后在项目的 `Build Phases` > `Copy Bundle Resources` 里添加拷贝的 `AntiAdictionResources.bundle`。 - -![](https://capacity-files.lcfile.com/uPSl32k7bmaqHOvBbD8TYlnF1aa2qg22/anti-resources-bundle.png) - -
    - - -<> - - -
      -
    • 防沉迷 SDK AntiAddiction_{sdkVersions.taptap.android}.aar 拷贝到游戏目录下的 src/main/libs 目录中
    • -
    • 防沉迷 SDK AntiAddictionUI_{sdkVersions.taptap.android}.aar 拷贝到游戏目录下的 src/main/libs 目录中
    • -
    • TapCommon_{sdkVersions.taptap.android}.aar 拷贝到游戏目录下的 src/main/libs 目录中
    • -
    • TapLogin_{sdkVersions.taptap.android}.aar 拷贝到游戏目录下的 -src/main/libs 目录中
    • -
    • TapBootstrap_{sdkVersions.taptap.android}.aar 拷贝到游戏目录下的 -src/main/libs 目录中 (可选)
    • -
    - -在游戏目录下 `build.gradle` 文件中添加代码 - - -{`repositories{ - flatDir{ - dirs 'src/main/libs' - } -} -dependencies { - // ... - implementation(name: "AntiAddiction_${sdkVersions.taptap.android}", ext: "aar") // 防沉迷 SDK - implementation(name: "AntiAddictionUI_${sdkVersions.taptap.android}", ext: "aar") // 防沉迷 SDK - implementation(name: "TapCommon_${sdkVersions.taptap.android}", ext: "aar") - implementation(name: "TapLogin_${sdkVersions.taptap.android}", ext: "aar") - implementation(name: "TapBootstrap_${sdkVersions.taptap.android}", ext: "aar") // 可选 - // ... -}`} - - - -<> - -iOS **防沉迷 SDK** 结构: - -- `AntiAddictionService` 防沉迷基础库,源码由 Swift 编写。 -- `AntiAddictionUI` 带 UI 的防沉迷库,依赖 `AntiAddictionService`,源码由 Objective-C 编写。 -- `AntiAddictionResources.bundle` 资源文件 - -其他依赖: - -- `TapCommonSDK.framework` 基础库 -- `TapLoginSDK.framework` 基础库 -- `TapCommonResource.bundle` 资源文件 -- `TapLoginResource.bundle` 资源文件 -- `TapBootstrap.framework` 基础库(可选) - -添加防沉迷库文件: - -- 添加 `AntiAddictionService.framework`、`AntiAddictionUI.framework`、`TapLoginSDK.framework`、`TapBootstrap.framework`(可选) 和 `TapCommonSDK.framework` 静态库。注意添加时选择 Embed 方式为 **Do Not Embed**。 - -- 引用代码: - - ``` objc - // AntiAddictionUI - #import - ``` - -添加系统依赖库: - -请检查项目中是否已自动添加以下依赖项: - -- `libc++.tdb` -- `libz.tdb` - -若运行时遇到相关依赖库加载报错,可改为 Optional 尝试。 - -配置编译选项: - -- 在 Build Setting 中的 Other Link Flag 中添加 `-ObjC` 和 `-Wl -ld_classic`。 - -- 在 Build Setting 中的 Always Embed Swift Standard Libraries 设置为 YES,即始终引入 Swift 标准库,避免 App 启动时报错「无法找到 Swift 标准库之类」。如果项目中找不到,可以建立一个空 Swift 文件,Xcode 会自动建立桥接关系。 - -- 在 Build Setting 中的 Swift Compiler - Language/Swift Language Version 选择 Swift 5。 - - - -<> - -#### 安装插件 - -* 下载 [TapSDK UE4](/tap-download),TapSDK-UE4-xxx.zip 解压后将 `AntiAddiction`、`TapCommon`、`TapLogin`、`TapBootstrap(可选)` 文件夹 Copy 到项目的 `Plugins` 目录中 -* 重启 Unreal Editor -* 打开 编辑 > 插件 > 项目 > TapTap,开启 `AntiAddiction` 模块 - -#### 添加依赖 - -在 Project.Build.cs 中添加所需模块: - -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapLogin", - "TapBootstrap", // 可选 - "AntiAddiction" -}); -``` - -#### 导入头文件 - -```cpp -#include "AntiAddictionUE.h" -``` - -
    - -iOS 打包 Objective-C 和 Swift 的混编解决方案 - -目前有两种解决方案 - -一、防沉迷库替换成动态库 - -优缺点: - -* 优点:可以不用修改引擎的代码 -* 缺点: - * 包体积略微增大 - * 最低支持 iOS 13,低于该系统版本会造成闪退 - -操作步骤: - -1. 下载 TapSDK-iOS 相同版本的[库文件](https://github.com/taptap/TapSDK-iOS/releases) -2. 把 `Plugins/AntiAddiction/Source/AntiAddiction/ios/framework/AntiAddictionService.zip` 中的 `AntiAddictionService.framework` 替换成刚下载到的 `Dylib/AntiAddictionService.framework`(解压缩 -> 替换 -> 压缩) -3. 把 `AntiAddiction.Build.cs` 文件中 - - ```cs - new Framework( - "AntiAddictionService", - "../AntiAddiction/ios/framework/AntiAddictionService.zip" - ) - ``` - - 替换成: - - ```cs - new Framework( - "AntiAddictionService", - "../AntiAddiction/ios/framework/AntiAddictionService.zip", - null, - true - ) - ``` - -4. 重新编译即可 - -二、修改 `UnrealBuildTool` - -1. 修改 `XcodeProject.cs` 文件 - - 路径:`Engine/Source/Programs/UnrealBuildTool/ProjectFiles/Xcode/XcodeProject.cs` - - 在函数: - - ```cpp - private void AppendProjectBuildConfiguration(StringBuilder Content, string ConfigName, string ConfigGuid) - ``` - - 中添加如下代码: - - ```cpp - // Enable Swift - Content.Append("\t\t\t\tCLANG_ENABLE_MODULES = YES;" + ProjectFileGenerator.NewLine); - Content.Append("\t\t\t\tSWIFT_VERSION = 5.0;" + ProjectFileGenerator.NewLine); - Content.Append("\t\t\t\tLIBRARY_SEARCH_PATHS = \"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\";" + ProjectFileGenerator.NewLine); - if (ConfigName == "Debug") - { - Content.Append("\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";" + ProjectFileGenerator.NewLine); - } - Content.Append("\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;" + ProjectFileGenerator.NewLine); - Content.Append("\t\t\t\tEMBEDDED_CONTENT_CONTAINS_SWIFT = YES;" + ProjectFileGenerator.NewLine); - ``` - - 参考如下: - ![](https://img.tapimg.com/market/images/e62c405ba77c57475dfaed7b5e550c5b.jpg) - -2. 修改 `IOSToolChain.cs` 文件 - - 路径:`Engine/Source/Programs/UnrealBuildTool/Platform/IOS/IOSToolChain.cs` - - 在函数: - - ```cpp - string GetLinkArguments_Global(LinkEnvironment LinkEnvironment) - ``` - - 中添加如下代码: - - ```cpp - // 该行代码需要前置(前置的代码位置见下面示例图片) - // Added by uwellpeng: enable swift support, make sure '/usr/lib/swift' goes before '@executable_path/Frameworks' - Result += " -rpath \"/usr/lib/swift\""; - - // enable swift support - Result += " -rpath \"@executable_path/Frameworks\""; - // /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/swift/ - String swiftLibPath = String.Format(" -L {0}Platforms/{1}.platform/Developer/SDKs/{1}{2}.sdk/usr/lib/swift", - Settings.Value.XcodeDeveloperDir, bIsDevice? Settings.Value.DevicePlatformName : Settings.Value.SimulatorPlatformName, Settings.Value.IOSSDKVersion); - Result += swiftLibPath; - Log.TraceInformation("Add swift lib path : {0}", swiftLibPath); - ///Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos - swiftLibPath = String.Format(" -L {0}Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/{1}", - Settings.Value.XcodeDeveloperDir, bIsDevice? Settings.Value.DevicePlatformName.ToLower() : Settings.Value.SimulatorPlatformName.ToLower()); - Result += swiftLibPath; - Log.TraceInformation("Add swift lib path : {0}", swiftLibPath); - ///Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/iphoneos - swiftLibPath = String.Format(" -L {0}Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/{1}", - Settings.Value.XcodeDeveloperDir, bIsDevice? Settings.Value.DevicePlatformName.ToLower() : Settings.Value.SimulatorPlatformName.ToLower()); - Result += swiftLibPath; - // Xcode 12 多了 swiftcompatabiliy51 的库,需要新增以下代码 - if (Settings.Value.IOSSDKVersionFloat >= 14.0f) - { - Result += String.Format(" -lswiftCompatibility51"); - } - ``` - - 需要注意的是 `Result += " -rpath \"/usr/lib/swift\"";` 这段代码需要加在 `@executable_path/Frameworks` 前面 - - 参考: - - ![](https://img.tapimg.com/market/images/84ca51379a9f6c7a69262450fbf89b7b.jpg) - -3. 重新编译 UBT - - 使用 `msbuild` 工具重新编译 `UnrealBuildTool`,即在 `Engine/Source/Programs/UnrealBuildTool` 目录运行 `Terminal` 指令 `msbuild` 来重新编译(如果引擎目录在一些不可编辑的目录下,可以加上 `sudo` 命令,即 `sudo msbuild`)。 - - 完成上述三个步骤即可在解决 UnrealEngine 上 Swift 的混编问题 - -
    - - - -
    - -防沉迷 SDK 需要联网和发送请求数据的权限,请开发者注意在项目中声明相应权限。 - -## 初始化与回调设置 - -### 回调设置 - -因初始化接口依赖或需结合使用防沉迷回调对象参数,开发者应先自定义防沉迷回调对象来处理不同类型事件。 - - -<> - -```cs -Action callback = (code, extra) => { - // 防沉迷回调 - UnityEngine.Debug.LogFormat($"code: {code} error Message: {extra}"); -}; -``` - - -<> - - -```java - -AntiAddictionUICallback callback = new AntiAddictionUICallback() { - @Override - public void onCallback(int code, Map extras) { - // 防沉迷回调 - } -}; -``` - - -<> - -```objc -//设置需要实现以下协议方法来接收回调的`delegate` 对象 -- (void)antiAddictionCallbackWithCode:(AntiAddictionResultHandlerCode)code extra:(NSString * _Nullable)extra { - // 防沉迷回调 -} -``` - - - - -<> - -```cpp -void UAntiAddictionWidget::OnCallBack(AntiAddictionUE::ResultHandlerCode Code, const FString& Extra) { -// 防沉迷回调 - -} - -``` - - - - - -回调参数中 code 用于标识回调类型, extra 为对应提示信息 - -| 回调 code | 回调类型 | 触发逻辑 | -|---|---|---| -| 500 | `LOGIN_SUCCESS` | 玩家未受到限制,正常进入游戏 | -| 1000 | `EXITED` | 退出防沉迷认证及检查,当开发者调用 Exit 接口时或用户认证信息无效时触发,游戏应返回到登录页 | -| 1001 | `SWITCH_ACCOUNT` | 用户点击切换账号,游戏应返回到登录页 | -| 1030 | `PERIOD_RESTRICT` | 用户当前时间无法进行游戏,此时用户只能退出游戏或切换账号 | -| 1050 | `DURATION_LIMIT` | 用户无可玩时长,此时用户只能退出游戏或切换账号 | -| 1100 | `AGE_LIMIT` | 当前用户因触发应用设置的年龄限制无法进入游戏 | -| 1200 | `INVALID_CLIENT_OR_NETWORK_ERROR` | 数据请求失败,游戏需检查当前设置的应用信息是否正确及判断当前网络连接是否正常 -| 9002 | `REAL_NAME_STOP` | 实名过程中点击了关闭实名窗,游戏可重新开始防沉迷认证 | - -### 初始化 - -从 `3.27.0` 版本开始,防沉迷初始化有两种方式,使用 `TapBootstrap` 模块 和 单独调用防沉迷接口初始化,游戏根据需要选择一种即可, `3.27.0` 之前的版本只支持单独调用防沉迷接口初始化。 - - -#### TapBootstrap 初始化(不推荐) - -使用 `TapBootstrap` 初始化时,需要在 `TapConfig` 中设置 `TapAntiAddicitionConfig` 配置, 但不需要再额外调用登录初始化接口,同时***防沉迷回调对象需单独设置***。示例如下: - - -<> - -```cs -var config = new TapConfig.Builder() - .ClientID(clientId) - .ClientToken(clientToken) - .ServerURL(serverUrl) - .RegionType(regionType) - // showSwitchAccount bool 类型 是否显示切换账号按钮; useAgeRange bool 类型 是否需要获取真实年龄段信息 - .AntiAddictionConfig(showSwitchAccount, useAgeRange); -TapBootstrap.Init(config); -// 设置回调, callback 为开发者实现的自定义防沉迷回调对象 -AntiAddictionUIKit.SetAntiAddictionCallback(callback); -``` - - -<> - - -```java - -// TapAntiAddicitonConfig 构造方法中参数为是否显示切换账号,是否使用年龄段 -TapAntiAddictionConfig tapAntiAddictionConfig = new TapAntiAddictionConfig(true, true); - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(activity) - .withClientId(clientId) - .withClientToken(clientToken) - .withServerUrl(serverUrl) - .withRegionType(regionType) - .withAntiAddictionConfig(tapAntiAddictionConfig) - .build(); -TapBootstrap.init(activity, tapConfig); -//设置回调, callback 为开发者实现的自定义防沉迷回调对象 -AntiAddictionUIKit.setAntiAddictionCallback(callback); -``` - - -<> - -```objc - TapConfig *config = [TapConfig new]; - config.clientId = @"your_client_id"; // 必须,开发者中心对应 Client ID - config.clientToken = @"your_client_token"; // 必须,开发者中心对应 Client Token - config.serverURL = @"https://your_server_url"; - config.tapAntiAddictionConfig = [[TapAntiAddictionConfig alloc] init]; - config.tapAntiAddictionConfig.showSwitchAccount = true; - config.tapAntiAddictionConfig.useAgeRange = true; - [TapBootstrap initWithConfig:config]; - //设置回调, callback 为开发者实现的自定义防沉迷回调对象 - [AntiAddiction setDelegate:delegate]; -``` - - - -<> - -```cpp -FTUConfig Config; -Config.ClientID = ETB_Init_ClientID->GetText().ToString(); -Config.ClientToken = ETB_Init_ClientToken->GetText().ToString(); -Config.ServerURL = ETB_Init_ServerURL->GetText().ToString(); -Config.TapAntiAddictionConfig = MakeShared(); -Config.TapAntiAddictionConfig->bShowSwitchAccount = true; -Config.TapAntiAddictionConfig->bUseAgeRange = true; -FTapBootstrap::Init(Config); -// 绑定开发者实现的自定义防沉迷回调对象 AntiAddictionUE::OnCallBack -AntiAddictionUE::OnCallBack.BindUObject(this, &UAntiAddictionWidget::OnCallBack); - -``` - - - - - -##### 参数说明 - -TapAntiAddictionConfig 为防沉迷模块使用的配置,其参数如下: - - - -<> - - - `showSwitchAccount` 是否显示切换账号按钮。如果游戏不支持切换账号功能,应设置为 false;如果游戏支持则设置为 true,当玩家点击切换账按钮后,SDK 会触发 `1001` 回调,游戏可根据这个回调 code 做相应处理。 - - - `useAgeRange` 游戏是否需要获取真实年龄段信息,当设置为 true 时,使用 Tap 实名时会需要用户主动授权,当设置为 false 时,使用 Tap 实名时会进行无 UI 交互的静默授权,未设置时默认为 true - - - - - -<> - -防沉迷初始化参数包括: - -- `showSwitchAccount`:是否显示切换账号按钮。如果游戏不支持切换账号功能,应设置为 false;如果游戏支持则设置为 true,当玩家点击切换账按钮后,SDK 会触发 `1001` 回调,游戏可根据这个回调 code 做相应处理。 -- `useAgeRange` 游戏是否需要获取真实年龄段信息,当设置为 true 时,使用 Tap 实名时会需要用户主动授权,当设置为 false 时,使用 Tap 实名时会进行无 UI 交互的静默授权,未设置时默认为 true - - - -<> - - - - `showSwitchAccount` 是否显示切换账号按钮。如果游戏不支持切换账号功能,应设置为 false;如果游戏支持则设置为 true,当玩家点击切换账按钮后,SDK 会触发 `1001` 回调,游戏可根据这个回调 code 做相应处理。 - - `useAgeRange` 游戏是否需要获取真实年龄段信息,当设置为 true 时,使用 Tap 实名时会需要用户主动授权,当设置为 false 时,使用 Tap 实名时会进行无 UI 交互的静默授权,未设置时默认为 true - - - -<> - - - `ShowSwitchAccount` 是否显示切换账号按钮。如果游戏不支持切换账号功能,应设置为 false;如果游戏支持则设置为 true,当玩家点击切换账按钮后,SDK 会触发 `1001` 回调,游戏可根据这个回调 code 做相应处理。 - - `UseAgeRange` 游戏是否需要获取真实年龄段信息,当设置为 true 时,使用 Tap 实名时会需要用户主动授权,当设置为 false 时,使用 Tap 实名时会进行无 UI 交互的静默授权,未设置时默认为 true - - - - - - - -#### 防沉迷接口单独初始化(推荐) - -因防沉迷模块依赖于 `TapLogin` 模块,所以在防沉迷模块初始化前必须完成 `TapLogin` 模块[初始化](/sdk/taptap-login/guide/tap-login/#初始化)。 -初始化防沉迷 UI 模块,包括设置启动防沉迷功能的配置、注册防沉迷的消息监听。 - - -<> - -```cs -using TapTap.AntiAddiction; -using TapTap.AntiAddiction.Model; - -AntiAddictionConfig config = new AntiAddictionConfig() -{ - gameId = "your_client_id", // TapTap 开发者中心对应 Client ID - showSwitchAccount = false, // 是否显示切换账号按钮 - useAgeRange = true // 是否使用年龄段信息 -}; -//设置配置及回调,callback 为开发者实现的自定义防沉迷回调对象 -AntiAddictionUIKit.Init(config); -AntiAddictionUIKit.SetAntiAddictionCallback(callback); -``` - - -<> - - -```java -// Android SDK 的各接口第一个参数是当前 Activity,以下不再说明 -Config config = new Config.Builder() - .withClientId("your_client_id") // TapTap 开发者中心对应 Client ID - .showSwitchAccount(false) // 是否显示切换账号按钮 - .useAgeRange(true) //是否使用年龄段信息 - .build(); -//设置配置与回调,callback 为开发者实现的自定义防沉迷回调对象 -AntiAddictionUIKit.init(activity, config); -AntiAddictionUIKit.setAntiAddictionCallback(callback); -``` - - -<> - -```objc -AntiAddictionConfig *config = [[AntiAddictionConfig alloc] init]; -config.clientID = @"your_client_id"; // TapTap 开发者中心对应 Client ID -config.showSwitchAccount = YES; -config.useAgeRange = YES; //是否使用年龄段信息 -//设置配置与回调, delegate 为开发者实现的自定义防沉迷回调对象 -[AntiAddiction initWithConfig:config]; -[AntiAddiction setDelegate:delegate]; -``` - - -<> - -```cpp -FAAUConfig Config; -Config.ClientID = TEXT("your_client_id"); // TapTap 开发者中心对应 Client ID -Config.ShowSwitchAccount = false; -Config.UseAgeRange = true; //是否使用年龄段信息 -AntiAddictionUE::Init(Config); -// 绑定 开发者实现的自定义防沉迷回调对象 AntiAddictionUE::OnCallBack -AntiAddictionUE::OnCallBack.BindUObject(this, &UAntiAddictionWidget::OnCallBack); - -``` - - - - - -##### 参数说明 - - - -<> - -- `config` 是防沉迷功能的配置,包括如下参数: - - `gameId` 游戏的 `Client ID`,可以在控制台查看(**开发者中心 > 你的游戏 > 游戏服务 > 应用配置**)。 - - `showSwitchAccount` 是否显示切换账号按钮。如果游戏不支持切换账号功能,应设置为 false;如果支持则设置为 true,当玩家点击切换账按钮(如下图所示)后,SDK 会触发 `1001` 回调,游戏可根据这个回调 code 做相应处理。 - - `useAgeRange` 游戏是否需要获取真实年龄段信息,当设置为 true 时,使用 Tap 实名时会需要用户主动授权,当设置为 false 时,在移动端使用 Tap 实名时会进行无 UI 交互的静默授权,未设置时默认为 true -- `callback` 开发者实现的自定义防沉迷回调对象 - - - - -<> - -防沉迷初始化参数包括: - -- `config` 是防沉迷功能的配置,包括如下参数: - - `clientID` 游戏的 `Client ID`,可以在控制台查看(**开发者中心 > 你的游戏 > 游戏服务 > 应用配置**)。 - - `showSwitchAccount` 是否显示切换账号按钮。如果游戏不支持切换账号功能,应设置为 false;如果支持则设置为 true,当玩家点击切换账按钮(如下图所示)后,SDK 会触发 `1001` 回调,游戏可根据这个回调 code 做相应处理。 - - `useAgeRange` 游戏是否需要获取真实年龄段信息,当设置为 true 时,使用 Tap 实名时会需要用户主动授权,当设置为 false 时,使用 Tap 实名时会进行无 UI 交互的静默授权,未设置时默认为 true - -- `callback` 开发者实现的自定义防沉迷回调对象 - - - - -<> - -- `config` 是防沉迷功能的配置,包括如下参数: - - `clientID` 游戏的 `Client ID`,可以在控制台查看(**开发者中心 > 你的游戏 > 游戏服务 > 应用配置**)。 - - `showSwitchAccount` 是否显示切换账号按钮。如果游戏不支持切换账号功能,应设置为 false;如果支持则设置为 true,当玩家点击切换账按钮(如下图所示)后,SDK 会触发 `1001` 回调,游戏可根据这个回调 code 做相应处理。 - - `useAgeRange` 游戏是否需要获取真实年龄段信息,当设置为 true 时,使用 Tap 实名时会需要用户主动授权,当设置为 false 时,使用 Tap 实名时会进行无 UI 交互的静默授权,未设置时默认为 true - -- `delegate` 开发者实现的自定义防沉迷回调对象 - - - -<> - -- `config` 是防沉迷功能的配置,包括如下参数: - - `ClientID` 游戏的 `Client ID`,可以在控制台查看(**开发者中心 > 你的游戏 > 游戏服务 > 应用配置**)。 - - - `ShowSwitchAccount` 是否显示切换账号按钮。如果游戏不支持切换账号功能,应设置为 false;如果支持则设置为 true,当玩家点击切换账按钮(如下图所示)后,SDK 会触发 `1001` 回调,游戏可根据这个回调 code 做相应处理。 - - `UseAgeRange` 游戏是否需要获取真实年龄段信息,当设置为 true 时,使用 Tap 实名时会需要用户主动授权,当设置为 false 时,在移动端使用 Tap 实名时会进行无 UI 交互的静默授权,未设置时默认为 true - - - - - - -![切换账号界面](/img/anti-addiction/switch-account.png) - - - - -## 开始认证 - -防沉迷开始认证时需传入玩家唯一标识 `userIdentifier`,如果接入 [TDS 内建账户系统](/sdk/taptap-login/guide/start/#一键完成-taptap-登录),可以用玩家的 `objectId`;如果使用[单纯 TapTap 用户认证](/sdk/taptap-login/guide/tap-login/#taptap-登录并获取登录结果)则可以用 `openid` 或 `unionid`。 - - - -<> - -```cs -// 注意唯一标识参数值长度不能超过 64 字符 -string userIdentifier = "玩家的唯一标识"; -AntiAddictionUIKit.StartupWithTapTap(userIdentifier); -``` - -<> - -```java -// 注意唯一标识参数值长度不能超过 64 字符 -String userIdentifier = "玩家的唯一标识"; -AntiAddictionUIKit.startupWithTapTap(activity, userIdentifier); -``` - - -<> - -```objc -// 注意唯一标识参数值长度不能超过 64 字符 -NSString *userIdentifier = @"玩家的唯一标识"; -[AntiAddiction startupWithTapTap:userIdentifier]; -``` - - -<> - -```cpp -AntiAddictionUE::StartupWithTapTap(TEXT("your_userIdentifier"), true); -``` - - - - - - - -## 检查消费上限 - -根据年龄段的不同,未成年玩家的消费金额有不同的上限。开发者需要在未成年玩家消费前检查是否受限,并在成功消费后上报消费金额。 - -游戏在收到玩家的付费请求后,调用以下接口当前玩家的付费行为是否被限制: - - - -```cs -long amount = 100; -AntiAddictionUIKit.CheckPayLimit(amount, - (result) => { - // status 为 1 时可以支付 - int status = result.status; - if (status == 1) { - // 可以进行支付 - } - }, - (exception) => { - // 处理异常 - } -); -``` - -```java -long amount = 100; -AntiAddictionUIKit.checkPayLimit(activity, amount, - new Callback() { - @Override - public void onSuccess(CheckPayResult result) { - // status 为 true 时可以支付,false 则限制消费 - if (result.status) { - } - } - - @Override - public void onError(Throwable throwable) { - // 处理异常 - } - } -); -``` - -```objc -NSInteger amount = 100; -[AntiAddiction checkPayLimit:amount resultBlock:^(BOOL status) { - if (status) { - // 无限制 - } -} failureHandler:^(NSString * _Nonnull error) { - // 处理异常 -}]; -``` - -```cpp -AntiAddictionUE::CheckPayLimit(FCString::Atoi(*AmountTF->Text.ToString()), [](bool Status) { - TUDebuger::DisplayShow(FString::Printf(TEXT("Status: %d"), Status)); -}, [](const FString& Msg) { - TUDebuger::ErrorShow(Msg); -}); -``` - - - -消费金额的单位为分。 - -## 上报消费金额 - -当未成年玩家消费成功后,调用如下接口上报玩家消费金额: - - - -```cs -long amount = 100; -AntiAddictionUIKit.SubmitPayResult(amount, - () => { - // 成功 - }, (exception) => { - // 处理异常 - } -); -``` - -```java -long amount = 100; -AntiAddictionUIKit.submitPayResult(amount, - new Callback() { - @Override - public void onSuccess(SubmitPayResult result) { - // 提交成功 - } - - @Override - public void onError(Throwable throwable) { - // 处理异常 - } - } -); -``` - -```objc -NSInteger amount = 100; -[AntiAddiction submitPayResult:amount callBack:^(BOOL success) { - if (success) { - // 提交成功 - } -} failureHandler:^(NSString * _Nonnull error) { - // 处理异常 -}]; -``` - -```cpp -AntiAddictionUE::SubmitPayResult(FCString::Atoi(*AmountTF->Text.ToString()), [](bool Success) { - TUDebuger::DisplayShow(FString::Printf(TEXT("Success: %d"), Success)); -}, [](const FString& Msg) { - TUDebuger::ErrorShow(Msg); -}); -``` - - - -上报消费金额时,传入的消费金额的单位同样为分。 - -## 退出认证 - -玩家在游戏内退出账号时调用,重置防沉迷状态。 - - - -```cs -AntiAddictionUIKit.Exit(); -``` - -```java -AntiAddictionUIKit.exit(); -``` - -```objc -[AntiAddiction exit]; -``` - -```cpp -AntiAddictionUE::Exit(); -``` - - - -## 其他 -### 获取玩家年龄段 - -开发者可调用如下接口获取玩家所处年龄段: - - - -```cs -int ageRange = AntiAddictionUIKit.AgeRange; -``` - -```java -int ageRange = AntiAddictionUIKit.getAgeRange(); -``` - -```objc -NSInteger ageRange = [AntiAddiction getAgeRange]; -``` - -```cpp -EAAUAgeLimit AgeLimit = AntiAddictionUE::GetAgeRange(); -``` - - - -上例中的 `ageRange` 是一个整数,表示玩家所处年龄段的下限(最低年龄)。 - -| 类型数值 | 含义 | -| - | - | -| -1 | 未知 | -| 0 | 0 到 7 岁 | -| 8 | 8 到 15 岁 | -| 16 | 16 到 17 岁 | -| 18 | 成年玩家 | - -`-1` 表示「未知」,说明该用户还未实名或未授予年龄段权限,通常有以下几个原因: - -1. 开发者在用户未完成实名时调用该接口,应该在收到用户是否可进入游戏的回调时(回调 code 为 500 / 1030 / 1050)时,再进行调用 -2. 在防沉迷初始化时配置的参数 useAgeRange 设置为 false 导致,需设置为 true -3. 该游戏无版号且在 TapPlay 中运行 - - - -### 获取剩余时长 - -获取玩家当前剩余时长: - - - -```cs -int remainingTimeInSeconds = AntiAddictionUIKit.RemainingTime; // 单位:秒 - -int remainingTimeInMinutes = AntiAddictionUIKit.RemainingTimeInMinutes; // 单位:分 -``` - -```java -int remainingTimeInSeconds = AntiAddictionUIKit.getRemainingTime(); // 单位:秒 - -int remainingTimeInMinutes = AntiAddictionUIKit.getRemainingTimeInMinutes(); // 单位:分 -``` - -```objc -NSInteger remainingTimeInSeconds = [AntiAddiction getRemainingTime]; // 单位:秒 - -NSInteger remainingTimeInMinutes = [AntiAddiction getRemainingTimeInMinutes]; // 单位:分 -``` - -```cpp -int RemainingTime = AntiAddictionUE::GetRemainingTime(); // 单位:秒 - -int RemainingTimeInMinutes = AntiAddictionUE::GetRemainingTimeInMinutes(); // 单位:分 -``` - - - -### 设置适龄限制 - -当除了需要满足防沉迷政策外,应用需要对用户年龄有额外限制时,例如只允许 16 周岁以上使用,开发者可在 开发者中心页面配置对应的年龄限制,SDK 将在用户完成实名后 且 根据时长限制规则显示 UI 前检查用户是否符合游戏要求,满足要求时,SDK 会继续进行后续时长业务及回调处理,否则会直接返回 code 为 `1100` 的年龄限制回调通知开发者。 - - -:::tip -合规认证 3.29.0 之前版本提供的 REST API 已不适用于 3.29.0 及其以上版本,如果游戏早期已接入 REST API 且游戏想要升级至 3.29.0 及其以上版本后继续使用 REST API,请于 TapTap 开发者后台提工单联系我们。 -::: \ No newline at end of file diff --git a/docs/sdk/anti-addiction/practice.mdx b/docs/sdk/anti-addiction/practice.mdx deleted file mode 100644 index 429c21c03..000000000 --- a/docs/sdk/anti-addiction/practice.mdx +++ /dev/null @@ -1,470 +0,0 @@ ---- -title: 最佳实践 -sidebar_label: 最佳实践 -sidebar_position: 3 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; - -平台推荐集成 3.29.2 及以上版本的 SDK,配套使用 TapTap 登录 + 合规认证服务,以达到最佳用户体验。 - -## 准备工作 - -### 创建应用获取应用参数 -在 [TapTap 开发者中心](https://developer.taptap.cn) 创建游戏应用,获取应用 Client ID、Client Token 等参数,用于初始化 SDK; - -![](https://capacity-files.lcfile.com/nnQKxgJJzgErOlIOcxnbIHt8Vc1RmGYe/tap_get_ready.png) - -### 开通 TapTap 登录服务 -合规认证服务依赖于 TapTap 登录服务,因此,厂商需要在 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 开启「TapTap 登录」; - -![](https://img.tapimg.com/market/images/168f902edd3de84cf0d5eb5fa640e78d.png) - -### 配置应用包名和签名信息 -Android 签名处填写 MD5 值,详情可参考:[如何获取 MD5 值](/sdk/start/faq/#如何获取-android-应用的-md5-值); - -![](https://img.tapimg.com/market/images/3c725fc6859363f630d90471d0c8929b.png) - -### 开通合规认证服务 -找到 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 开发与构建 > 合规认证**,根据游戏实际情况,选择「已有版号」或「暂无版号」方案,然后点击**立即开通** - -![](https://img.tapimg.com/market/images/90e6d759ba528aa7e9ef29077387edbb.png) - -:::tip -若游戏选择的是「已有版号」方案则还需要完成中宣部实名认证系统的注册以及相应配置,具体的操作请参考 [注册中宣部实名认证系统](/sdk/anti-addiction/features/#已有版号) -::: - -### 可玩年龄限制 -应用需要对用户年龄有额外限制时,可在 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 合规认证 > 可玩年龄限制** 中开启此功能,并配置所需的**最低年龄要求** - -在后续的代码集成中,也需要处理相关的 `1100` 回调 - -![](https://img.tapimg.com/market/images/e7fab2ce4099b792bd32390c04e28986.png) - -## 代码接入 - -下面模拟一个小游戏来进行接入示例,该游戏主要包括两个场景: -- 登录场景 LoginScene: 用于初始化 SDK 、用户登录、切换账号、显示合规认证异常提示 UI 等 -- 游戏商店及设置场景 GameStoreAndSettignsScene: 用于展示充值页面、设置菜单等 - -另外,因合规认证模块在游戏整个生命周期中运行,所以通过全局的单例管理工具类 GameSDKManager 来处理 SDK 的初始化及回调。 - -完整示例代码可参考 [TDS-Unity-Demo](https://github.com/taptap/TDS-Unity-Demo) - -### 导入 SDK 包体 - - - -<> - -在游戏项目的 `Packages/manifest.json` 文件中添加以下依赖: - - - {`"dependencies":{ - "com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.tapsdk.antiaddiction":"https://github.com/TapTap/TapAntiAddiction-Unity.git#${sdkVersions.taptap.unity}", -}`} - - - -在 Unity 顶部菜单中选择 **Window > Package Manager** 可查看已经安装在项目中的包。 - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - - - -### 初始化与设置回调 - -在 GameSDKManager 工具类中完成 SDK 的初始化及全局回调的设置,示例如下: - - - -<> - -```cs -using System; -using TapTap.Login; -using TapTap.AntiAddiction; -using TapTap.AntiAddiction.Model; -using UnityEngine; - -/// -/// SDK 初始化及合规认证回调处理管理类 -/// -public sealed class GameSDKManager -{ - // 游戏在 TapTap 开发者中心对应的 Client ID - private readonly string clientId = "游戏的 Client ID"; - - // 是否已初始化 - private readonly bool hasInit = false; - - // 是否已通过合规认证检查 - public bool hasCheckedAntiAddiction { get; private set; } - - private static readonly Lazy lazy - = new Lazy(() => new GameSDKManager()); - - public static GameSDKManager Instance { get { return lazy.Value; } } - - private GameSDKManager() { } - - // 声明合规认证回调 - private readonly Action AntiAddictionCallback = (code, errorMsg) => - { - // 根据回调返回的参数 code 添加不同情况的处理 - switch (code) - { - - case 500: // 玩家未受限制,可正常进入 - Instance.hasCheckedAntiAddiction = true; - // TODO: 显示开始游戏按钮 - break; - - case 1000: // 防沉迷认证凭证无效时触发 - case 1001: // 当玩家触发时长限制时,点击了拦截窗口中「切换账号」按钮 - case 9002: // 实名认证过程中玩家关闭了实名窗口 - TapLogin.Logout(); // 如果游戏有其他账户系统,此时也应执行退出 - // TODO: 切换到登录页面 例如:SceneManager.LoadScene("Login"); - break; - - case 1100: // 当前用户因触发应用设置的年龄限制无法进入游戏 - // TODO: 游戏应自行绘制适龄限制提示,并引导玩家退出游戏 - break; - - case 1200: // 数据请求失败,应用信息错误或网络连接异常 - // TODO: 引导玩家确认网络连接是否正常,并重新调用开始认证接口 - break; - - default: - Debug.Log("其他可选回调"); - break; - } - - }; - - /// - /// 初始化登录与合规认证 SDK - /// - public void InitSDK() - { - if (!hasInit) - { - // 初始化 TapTap 登录 - TapLogin.Init(clientId); - - // 定义合规认证模块 config - AntiAddictionConfig config = new AntiAddictionConfig() - { - gameId = clientId, // TapTap 开发者中心对应 Client ID - showSwitchAccount = true, // 是否显示切换账号按钮 - useAgeRange = false // 是否使用年龄段信息 - }; - - // 初始化合规认证及设置回调 - AntiAddictionUIKit.Init(config); - AntiAddictionUIKit.SetAntiAddictionCallback(AntiAddictionCallback); - } - } - - /// - /// 开始合规认证检查 - /// - /// 用户唯一标识 - public void StartAntiAddiction(string userIdentifier) - { - hasCheckedAntiAddiction = false; - AntiAddictionUIKit.StartupWithTapTap(userIdentifier); - } -} -``` - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - - -### TapTap 登录 & 开始实名认证 - -登录场景为游戏的第一个场景,在该脚本中需调用 `GameSDKManager` 中初始化接口,并实现 Tap 登录、开始合规认证,示例如下: - -:::tip -TapTap 登录按钮素材需要使用官方提供的[登录按钮素材](https://assets.tapimg.com/img/TapTap_Login_Source.zip); -::: - - - -<> - -```cs -using UnityEngine; -using System; -using TapTap.AntiAddiction; -using TapTap.Login; - -/// -/// 登录场景 -/// -public class LoginScene : MonoBehaviour -{ - /// - /// 初始化 SDK 并判断本地是否已登录,已登录时开始合规认证检查,否则显示登录按钮 - /// - async void Start() - { - // 初始化 SDK - GameSDKManager.Instance.InitSDK(); - - AccessToken currentToken = null; - try - { - // 检查本地是否已存在 TapToken - currentToken = await TapLogin.GetAccessToken(); - } - catch (Exception e) - { - Debug.Log("本地无有效 token"); - } - - if (currentToken == null) - { - // TODO: 显示登录按钮 - } - else - { - // 如果当前还未通过合规认证检查,开始认证 - if (!GameSDKManager.Instance.hasCheckedAntiAddiction) - { - // 开始合规认证检查 - StartAntiAddiction(); - } - } - } - - /// - /// 登录按钮点击后执行 Tap 登录 - /// - public async void OnTapLoginButtonClick() - { - try - { - // 发起 Tap 登录并获取用户信息 - var accessToken = await TapLogin.Login(); - - // 开始合规认证检查 - StartAntiAddiction(); - } - catch (Exception e) - { - // 登录取消或错误,提示用户重新登录 - Debug.Log("用户登录取消或错误"); - } - } - - /// - /// 开启合规认证检查 - /// - public async void StartAntiAddiction() - { - // 获取当前已登录用户的 Profile 信息 - Profile profile = null; - try - { - profile = await TapLogin.GetProfile(); - } - catch (Exception exception) - { - Debug.Log($"获取 Profile 信息出现异常:{exception}"); - } - if (profile == null) - { - // 无法获取 Profile 时,登出并显示登录按钮 - TapLogin.Logout(); - // TODO: 显示登录按钮 - return; - } - - // 使用当前 Tap 用户的 unionid 作为用户标识进行合规认证检查 - string userIdentifier = profile.unionid; - GameSDKManager.Instance.StartAntiAddiction(userIdentifier); - } - -} -``` - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - - -### 限制未成年人消费额度 - -:::tip -每次充值之前,游戏侧请 **务必** 调用 `CheckPayLimit` 接口进行判断,检验当前玩家的充值行为是否被限制。充值金额的单位为分。 -::: - - - -<> - -```cs -// 100 表示 100 分,即 1 元 -long amount = 100; -AntiAddictionUIKit.CheckPayLimit(amount, (result) => - { - // 获取检查状态 - int status = result.status; - - // 当前充值不受限 - if (status == 1) - { - // TODO: 进行后续充值流程,充值完成后调用上报充值金额接口 - } - else - { - // 本次充值触发合规限制,游戏侧需停止后续充值流程 - } - }, - (exception) => - { - // TODO: 处理参数或网络异常后重试 - } -); -``` - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - - - -:::tip -充值成功后,游戏侧请 **务必** 调用 `SubmitPayResult` 接口上报玩家充值金额。上报充值金额时,传入的充值金额的单位同样为分。 -::: - - - -<> - -```cs -// 100 表示 100 分,即 1 元 -long amount = 100; -AntiAddictionUIKit.SubmitPayResult(amount, () => - { - // 提交成功 - }, (exception) => - { - // TODO: 处理参数或网络异常后重试 - } -); -``` - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - diff --git a/docs/sdk/authentication/_category_.json b/docs/sdk/authentication/_category_.json index 34f92f013..5da22988f 100644 --- a/docs/sdk/authentication/_category_.json +++ b/docs/sdk/authentication/_category_.json @@ -1,5 +1,5 @@ { "label": "内建账户", "collapsed": true, - "position": 11 + "position": 5 } diff --git a/docs/sdk/authentication/faq.mdx b/docs/sdk/authentication/faq.mdx index 0570f7667..8110d8eea 100644 --- a/docs/sdk/authentication/faq.mdx +++ b/docs/sdk/authentication/faq.mdx @@ -1,15 +1,23 @@ --- -title: 常见问题 +title: 内建账户常见问题 sidebar_label: 常见问题 -sidebar_position: 5 +sidebar_position: 4 --- -### 内建账户用户名和 TapTap 账户用户名不一致 +### 应用内用户的密码需要加密吗 -目前由于 TapTap 昵称和游戏昵称是分开的两个部分,我们的设计逻辑是,第一次使用 TapTap 登录后会将登录的信息存到内建账户中,但是后续如果修改 TapTap 的昵称,内建账户中原先保存的昵称是不会更新的,如果想要使用更新后的昵称, -可以尝试游戏内新增一个[修改昵称](/sdk/authentication/guide/#设置其他用户属性)的逻辑,用户输入昵称,然后客户端调用内建账户的[设置其他用户属性](/sdk/authentication/guide/#设置其他用户属性)的方法,就可以更新昵称了。 +不需要加密密码,我们的服务端已使用随机生成的 salt,自动对密码做了加密。 如果用户忘记了密码,可以调用 `requestResetPassword` 方法(具体查看 SDK 的 AVUser 用法),向用户注册的邮箱发送邮件,用户以此可自行重设密码。 在整个过程中,密码都不会有明文保存的问题,密码也不会在客户端保存,只是会保存 sessionToken 来标示用户的登录状态。 -### 内建账户修改密码后 403 (Forbidden) +### sessionToken 在什么情况下会失效? -如果在 `开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置` 中勾选了 密码修改后,强制客户端重新登录,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 403 (Forbidden) 错误 +如果在控制台的存储的设置中勾选了「密码修改后,强制客户端重新登录」,则用户修改密码后, sessionToken 会变更,需要重新登录。如果没有勾选这个选项,Token 就不会改变。当新建应用时,这个选项默认是被勾上的。 +### 在 PC 端用手机号登录,在小程序上用微信登录,如何绑定到同一个账号上? + +从逻辑上,在 PC 端登录的账号,与在小程序中用微信登录的账号,他们没有任何可以联系在一起的地方。如果都是独立创建了两个账号,只能在业务层面进行绑定(也就是将一个账号的所有关联对象全都迁移到另一个账号,然后删除原账号)。 + +如果可以在业务上加一些限制,则可以避免上面这种「创建了两个独立的账号」的情况。比如,如果手机号是账号必须设置的信息,那么我们可以在以手机号作为关联项。具体的步骤如下,首先是 `loginWithWeapp` 并带上 `failOnNotExist` 参数,这样如果该微信关联的用户已经存在则照常登录,如果没有则会失败,此时跳转到使用手机号登录/注册的页面,让用户通过手机号登录或注册,成功之后再通过 `associateWithWeapp` 接口关联当前微信账号。 + +### 不通过短信验证能否强制修改 _User 表 mobilePhoneVerified 字段,使其设置为 true? + +可以通过云引擎使用 [master key](/sdk/engine/functions/sdk/#使用超级权限) 来修改 `mobilePhoneVerified` 的值。因为云引擎运行在可信的服务器端环境中,所以可以全局开启超级权限(Master Key),这样云端会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据,当然这种方式也只允许调用一些仅供 Master Key 使用的 API。 diff --git a/docs/sdk/authentication/features.mdx b/docs/sdk/authentication/features.mdx deleted file mode 100644 index 731448b65..000000000 --- a/docs/sdk/authentication/features.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: 内建账户功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -## 业务介绍 - -TDS 内建账户服务致力于帮助开发者快速低成本地构建一个安全可靠的玩家登录系统。支持玩家采用包括游客账号、第三方账号(TapTap、Apple、微信、QQFacebook 等)在内的多种账号来登录你的游戏。你无需关心云端的搭建与实现,只需在工程中调用相关能力提供的 API,通过 TDS User 对象来快速实现它们。 - -## 适用场景 - -- 没有开发过账户系统的开发者,希望能轻松快速地完成游戏内账户系统的搭建。 -- 如果你已经自行构建了账户系统,也可以通过访问 TDS 内建账户来更加方便地使用其他 TDS 服务(比如数据存储、好友、成就等)。 - -### 注册和登录支持如下方式 - -- 游客登录 -- 第三方账号 OAuth 登录(TapTap、Apple、微信、QQFacebook 等) - -## 工作流程 - -1. 开发者在游戏中集成 TapSDK; -2. 开发者访问 SDK 中的接口来登录用户,获取 TDS User; -3. 在开发者中心管理账户。 - -### 获取 TDS User - -访问 TDS 内建账户系统时,需要上报一个玩家唯一身份标识。这个标识可以由开发者自行决定,目的是在 TDS 产生一个 TDS User 与之绑定,以便能使用更多的 TDS 服务。 - -### 提供游客登录 - -向玩家提供游客模式并使用 TDS 的服务,可直接访问内建账户系统的 API 进行上报与验证。 - -### 提供第三方账号登录 - -若要向玩家提供第三方账号登录(TapTap、Apple、微信、QQFacebook 等),需要先完成与第三方的对接,拿到第三方服务颁发的 OAuth 令牌。对于 TapTap 登录,请先按照 TapTap 登录接入流程完成 OAuth 协议流程,接着使用 OAuth 的结果访问 TDS 内建账户系统进行上报与验证,完成与 TDS 内建账户的关联绑定。 - -## 服务端校验 - -使用第三方登录时,为了提高账户的安全性,开发者可以使用 TDS 内建账户系统提供的服务端校验能力。开发者可前往 **TapTap 开发者中心 > 游戏服务 > 内建账户** 开启服务端校验能力。开启后,TDS 会前往对应的平台校验账户的合法性。 - -![第三方集成](https://capacity-files.lcfile.com/bILXg52CgfXumOqUSap0qhawpsg6rJo0/tdsuser-oauth-providers.png) - -## 管理账户 - -TDS 提供了控制台,方便你在开发者中心查看与管理账户。 - -![用户管理](https://capacity-files.lcfile.com/7sr0WH7EobWoqz8eKTCsGgiesQkiGxYz/lc-users-console.png) - -## 哪些 TDS 服务会使用内建账户系统 - -TDS 提供的游戏服务中,属于云服务的能力,均要求使用内建账户系统。你可前往开发者中心查看。 diff --git a/docs/sdk/authentication/guide.mdx b/docs/sdk/authentication/guide.mdx index d2f6c4a1a..865d680d3 100644 --- a/docs/sdk/authentication/guide.mdx +++ b/docs/sdk/authentication/guide.mdx @@ -6,838 +6,1848 @@ sidebar_position: 2 import MultiLang from "/src/docComponents/MultiLang"; import { Conditional } from "/src/docComponents/conditional"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import CodeBlock from "@theme/CodeBlock"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; -从 TapSDK 3.0 开始,我们提供了一个内建账户系统供游戏使用:开发者可以直接用 TapTap OAuth 授权的结果生成一个游戏内的账号(`TDSUser`),同时我们也支持将更多第三方认证登录的结果绑定到该账号上来。 -TapSDK 提供的游戏内好友、成就等服务和功能,也都基于这一账户系统。 +用户系统几乎是每款应用都要加入的功能,我们为此专门提供了一个 `LCUser` 类来方便应用使用各项用户管理的功能。 -## 环境要求 +`LCUser` 是 `LCObject` 的子类,这意味着任何 `LCObject` 提供的方法也适用于 `LCUser`,唯一的区别就是 `LCUser` 提供一些额外的用户管理相关的功能。 - +## 用户的属性 -<> +`LCUser` 相比一个普通的 `LCObject` 多出了以下属性: -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 +- `username`:用户的用户名。 +- `password`:用户的密码。 +- `email`:用户的电子邮箱。 +- `emailVerified`:用户的电子邮箱是否已验证。 +- `mobilePhoneNumber`:用户的手机号。 +- `mobilePhoneVerified`:用户的手机号是否已验证。 - +在接下来对用户功能的介绍中我们会逐一了解到这些属性。 -<> +## 注册 -Android 5.0(API level 21)或更高版本 +用户第一次打开应用的时候,可以让用户注册一个账户。下面的代码展示了一个典型的使用用户名和密码注册的流程: - + <> -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) +```cs +// 创建实例 +LCUser user = new LCUser(); - +// 等同于 user["username"] = "Tom"; +user.Username = "Tom"; +user.Password = "cat!@#123"; -<> +// 可选 +user.Email = "tom@xd.com"; +user.Mobile = "+8619201680101"; -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 +// 设置其他属性的方法跟 LCObject 一样 +user["gender"] = "secret"; +await user.SignUp(); +``` -**支持平台**:Android / iOS / Windows / macOS +新建 `LCUser` 的操作应使用 `SignUp` 而不是 `Save`,但以后的更新操作就可以用 `Save` 了。 - - -## 权限说明 - - - <> - - -<> +```java +// 创建实例 +LCUser user = new LCUser(); -该模块需要如下权限: +// 等同于 user.put("username", "Tom") +user.setUsername("Tom"); +user.setPassword("cat!@#123"); -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | +// 可选 +user.setEmail("tom@xd.com"); +user.setMobilePhoneNumber("+8619201680101"); -该模块将在应用中添加如下权限: +// 设置其他属性的方法跟 LCObject 一样 +user.put("gender", "secret"); +user.signUpInBackground().subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 注册成功 + System.out.println("注册成功。objectId:" + user.getObjectId()); + } + public void onError(Throwable throwable) { + // 注册失败(通常是因为用户名已被使用) + } + public void onComplete() {} +}); ``` - -``` - - -<> +新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 <> - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启内建账户应用服务、绑定 API 域名; +```objc +// 创建实例 +LCUser *user = [LCUser user]; +// 等同于 [user setObject:@"Tom" forKey:@"username"] +user.username = @"Tom"; +user.password = @"cat!@#123"; -## SDK 获取 +// 可选 +user.email = @"tom@xd.com"; +user.mobilePhoneNumber = @"+8619201680101"; - -<> +// 设置其他属性的方法跟 LCObject 一样 +[user setObject:@"secret" forKey:@"gender"]; - - -#### iOS 配置 - -在 `Assets/Plugins/iOS/Resource` 目录下创建 `TDS-Info.plist` 文件,复制以下代码并且**替换其中的 `ClientId`**。如果游戏使用了 TapTap [内嵌动态](/sdk/embedded-moments/features/)或[数据分析](/sdk/tapdb/features/)服务,需要配置相关权限并**替换授权文案**: - -:::tip - -复制使用以下内容时,**请删除空行以及注释**,以免出现 XML 解析时报错,`ApplicationException: expected a key node`。 - -::: - -```xml - - - - - taptap - - client_id - ClientId - - - NSPhotoLibraryUsageDescription - 说明为何应用需要此项权限 - NSCameraUsageDescription - 说明为何应用需要此项权限 - NSMicrophoneUsageDescription - 说明为何应用需要此项权限 - - NSUserTrackingUsageDescription - 说明为何应用需要此项权限 - - +[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + if (succeeded) { + // 注册成功 + NSLog(@"注册成功。objectId:%@", user.objectId); + } else { + // 注册失败(通常是因为用户名已被使用) + } +}]; ``` - -<> +新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 -1. [下载 TapSDK Android](/tap-download),解压后选择需要用到的 SDK 包导入到项目 `project/app/libs` 目录下。 + -2. 打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: +<> - {` -dependencies { - ... - // 导入 libs 目录下所有 aar 的包: - implementation fileTree(dir: 'libs', include: ['*.aar']) - // 如果需要单独导入 libs 目录下的指定包,请按照如下方式: - implementation files('libs/TapBootstrap_${sdkVersions.taptap.android}.aar') // TapTap 启动器 - implementation files('libs/TapCommon_${sdkVersions.taptap.android}.aar') // TapTap 基础库 - implementation files('libs/TapLogin_${sdkVersions.taptap.android}.aar') // TapTap 登录 - implementation 'com.taptap:lc-storage-android:${sdkVersions.leancloud.java}' // 数据存储 - implementation 'com.taptap:lc-realtime-android:${sdkVersions.leancloud.java}' -} -`} +```swift +do { + // 创建实例 + let user = LCUser() -3. 旧版 Android 额外配置 + // 等同于 user.set("username", value: "Tom") + user.username = LCString("Tom") + user.password = LCString("cat!@#123") - 如果 `targetSdkVersion < 29`,还需要添加如下配置: + // 可选 + user.email = LCString("tom@xd.com") + user.mobilePhoneNumber = LCString("+8619201680101") - - `manifest` 节点添加 `xmlns:tools="http://schemas.android.com/tools"` - - `application` 节点添加 `tools:remove="android:requestLegacyExternalStorage"` + // 设置其他属性的方法跟 LCObject 一样 + try user.set("gender", value: "secret") - -<> + _ = user.signUp { (result) in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } + } +} catch { + print(error) +} +``` -#### 导入 SDK - -1. 在 Xcode 选择工程,到 **Build Setting > Other Linker Flags** 添加 `-ObjC` 和 `-Wl -ld_classic`。 - -2. 下载 [TapSDK iOS](/tap-download),解压后选择需要导入的资源文件,直接拖拽到项目目录即可。 - -3. 视需要导入下载的资源文件: - - - 必选:TapTap 启动器、基础库、登录 - - ``` - TapBootstrapSDK.framework - TapCommonSDK.framework - TapLoginSDK.framework - LeanCloudObjc.framework - TapCommonResource.bundle - TapLoginResource.bundle - ``` - -4. 请仔细核对下面依赖库是否都添加成功: - - ``` - // 必选 - WebKit.framework - Security.framework - SystemConfiguration.framework - CoreTelephony.framework - SystemConfiguration.framework - libc++.tbd - - // TapTap 内嵌动态 - AVFoundation.framework - CoreTelephony.framework - MobileCoreServices.framework - Photos.framework - SystemConfiguration.framework - WebKit.framework - - // 数据分析 - // 如果不需要获取 IDFA 则不要添加 `AppTrackingTransparency` 和 `AdSupport` 两个系统库 - AppTrackingTransparency.framework - AdSupport.framework - CoreMotion.framework - Security.framework - SystemConfiguration.framework - libresolv.tbd - libsqlite3.0.tbd - libz.tbd - ``` - -#### 配置权限 - -如果游戏使用了 TapTap [内嵌动态](/sdk/embedded-moments/features/)或[数据分析](/sdk/tapdb/features/)服务,那么需要在 `info.plist` 配置相关权限并**替换授权文案**: - -```xml - -NSPhotoLibraryUsageDescription -说明为何应用需要此项权限 -NSCameraUsageDescription -说明为何应用需要此项权限 -NSMicrophoneUsageDescription -说明为何应用需要此项权限 - -NSUserTrackingUsageDescription -说明为何应用需要此项权限 -``` - -#### 配置跳转 TapTap 应用 - -用户无 TapTap 应用时,默认会通过 WebView 登录。 - -1. 打开 `info.plist`,添加如下配置(请替换 `clientID` 为你在控制台获取的 Client ID): - - ![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` - -2. 配置 openUrl: - - a) 如果项目中有 `SceneDelegate.m`,请先删除,然后请添加如下代码到 `AppDelegate.m` 文件: - - ```objectivec - - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [TDSHandleUrl handleOpenURL:url]; - } - - - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSHandleUrl handleOpenURL:url]; - } - ``` - - b) 删除 `info.plist` 里面的 Application Scene Manifest - - ![](/img/tap_ios_appmanifest.png) - - c) 删除 `AppDelegate.m` 文件中的两个管理 `Scenedelegate` 生命周期代理方法 - - ```objectivec - #pragma mark - UISceneSession lifecycle - - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { - - return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; - } - - - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { - } - ``` - - d) 在 `AppDelegate.h` 中添加 `UIWindow` - - ```objectivec - @property (strong, nonatomic) UIWindow *window; - ``` +新建 `LCUser` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 <> -* 下载 [TapSDK UE4](/tap-download),TapSDK-UE4-xxx.zip 解压后将 `TapBootstrap`、`TapCommon`、`TapLogin` 文件夹 Copy 到项目的 `Plugins` 目录中 -* 重启 Unreal Editor -* 打开 编辑 > 插件 > 项目 > TapTap,开启 `TapBootstrap` 和 `TapLogin` 模块 +```dart +// 创建实例 +LCUser user = LCUser(); -#### 添加依赖 +// 等同于 user['username'] = 'Tom'; +user.username = 'Tom'; +user.password = 'cat!@#123'; -在 Project.Build.cs 中添加所需模块: +// 可选 +user.email = 'tom@xd.com'; +user.mobile = '+8619201680101'; -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapBootstrap", - "TapLogin" -}); -if (Target.Platform == UnrealTargetPlatform.IOS || Target.Platform == UnrealTargetPlatform.Android) -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - // 推送接入 - // "LeanCloudPush", - - "LeanCloudMobile" - } - ); -} -else -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - "LeanCloud" - } - ); -} +// 设置其他属性的方法跟 LCObject 一样 +user['gender'] = 'secret'; +await user.signUp(); ``` -#### 导入头文件 +新建 `LCUser` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 -```cpp -#include "TapBootstrap.h" -``` + -
    +<> -点击展开 iOS 配置 +```js +// 创建实例 +const user = new AV.User(); -在 项目设置 > Platform > iOS > Additional Plist data 中可以填入一个字符串,复制以下代码并且替换其中的 `ClientID` 以及授权文案。 +// 等同于 user.set('username', 'Tom') +user.setUsername("Tom"); +user.setPassword("cat!@#123"); -```xml -CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt{ClientID} - - - -LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - -``` +// 可选 +user.setEmail("tom@xd.com"); +user.setMobilePhoneNumber("+8619201680101"); -如果接入 TapDB 模块,那么还需要加上: +// 设置其他属性的方法跟 AV.Object 一样 +user.set("gender", "secret"); -```xml -NSUserTrackingUsageDescription -{数据追踪权限申请文案} +user.signUp().then( + (user) => { + // 注册成功 + console.log(`注册成功。objectId:${user.id}`); + }, + (error) => { + // 注册失败(通常是因为用户名已被使用) + } +); ``` -
    +新建 `AV.User` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 -
    - -## SDK 初始化 - -初始化 TapSDK 时需传入 `Client ID`、区域等应用配置信息。 - - - <> -```cs -using TapTap.Bootstrap; // 命名空间 -using TapTap.Common; // 命名空间 +```python +# 创建实例 +user = leancloud.User() -var config = new TapConfig.Builder() - .ClientID("your_client_id") // 必须,开发者中心对应 Client ID - .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token - .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 - .ConfigBuilder(); -TapBootstrap.Init(config); -``` +# 等同于 user.set('username', 'Tom') +user.set_username('Tom') +user.set_password('cat!@#123') - -<> +# 可选 +user.set_email('tom@xd.com') +user.set_mobile_phone_number('+8619201680101') -**请确保 TapSDK 的初始化在主线程(UI 线程)中执行。** +# 设置其他属性的方法跟 leancloud.Object 一样 +user.set('gender', 'secret') -```java -TapConfig tdsConfig = new TapConfig.Builder() - .withAppContext(MainActivity.this) // Context 上下文 - .withClientId("your_client_id") // 必须,开发者中心对应 Client ID - .withClientToken("your_client_token") // 必须,开发者中心对应 Client Token - .withServerUrl("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .withRegionType(TapRegionType.CN) // TapRegionType.CN:中国大陆,TapRegionType.IO:其他国家或地区 - .build(); -TapBootstrap.init(MainActivity.this, tdsConfig); +user.sign_up() ``` - -<> - -```objectivec -// 开发者必须至少依赖 `TapBootstrap`、`TapLogin`、`TapCommon` 以及 `LeanCloudObjc` 模块,并按照如下方式完成初始化: -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // 必须,开发者中心对应 Client ID -config.clientToken = @"your_client_token"; // 必须,开发者中心对应 Client Token -config.serverURL = @"https://your_server_url"; // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -config.region = TapSDKRegionTypeCN; // TapSDKRegionTypeCN:中国大陆,TapSDKRegionTypeIO:其他国家或地区 -[TapBootstrap initWithConfig:config]; -``` +新建 `leancloud.User` 的操作应使用 `sign_up` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 <> -`TapBootstrap` 初始化方法会把直接初始化 TapLogin 模块,如果插件中包含 TapDB 并且 `DBConfig.Enable = true`,那么也会完成 [`TapDB` 初始化](/sdk/tapdb/sdk/client-side-integration/#tapsdk-init)。 - -这两个模块无需再次初始化。 +```php +// 创建实例 +$user = new User(); -```cpp -FTUConfig Config; -Config.ClientID = "your_client_id"; // 必须,开发者中心对应 Client ID -Config.ClientToken = "your_client_token"; // 必须,开发者中心对应 Client Token -Config.ServerURL = "https://your_server_url"; // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -Config.RegionType = ERegionType::CN; // ERegionType::CN:中国大陆,ERegionType::Global:其他国家或地区 -FTapBootstrap::Init(Config); -``` +// 等同于 $user->set("username", "Tom") +$user->setUsername("Tom"); +$user->setPassword("cat!@#123"); - +// 可选 +$user->setEmail("tom@xd.com"); +$user->setMobilePhoneNumber("+8619201680101"); - +// 设置其他属性的方法跟 LeanObject 一样 +$user->set("gender", "secret"); -初始化的时候,**必须填入** `client_id`、`client_token` 和 `server_url`,其中: +$user->signUp(); +``` -- `client_id`、`client_token` 信息可在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 查看。 +新建 `User` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 -- `server_url` 请**使用 HTTPS 协议**,参考文档关于 **[域名](/sdk/start/get-ready/#域名)** 的说明。 + +```go +// 注册用户 +user, err := client.Users.SignUp("Tom", "cat!@#123") +if err != nil { + panic(err) +} -## `TDSUser` 和 `LCUser` +// 设置其他属性 +if err := client.Users.ID(user.ID).Set("email", "tom@xd.com", leancloud.UseUser(user)); err != nil { + panic(err) +} +``` -`TDSUser` 类继承自 `LCUser` 类。 -`LCUser` 是 LeanCloud 提供的账户系统,`TDSUser` 基本沿用了其功能和接口,并针对 TDS 的需求进行了细微调整,所以我们推荐大家使用 `TDSUser` 类来构建玩家账户系统。 +
    -## TapTap 登录 +如果收到 `202` 错误码,意味着已经存在使用同一 `username` 的账号,此时应提示用户换一个用户名。除此之外,每个用户的 `email` 和 `mobilePhoneNumber` 也需要保持唯一性,否则会收到 `203` 或 `214` 错误。可以考虑在注册时把用户的 `username` 设为与 `email` 相同,这样用户可以直接 [用邮箱重置密码](#重置密码)。 -直接调用一键登录方法即可,详见[接入 TapTap 登录](/sdk/taptap-login/guide/start/#一键完成-taptap-登录)。 +采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码(云端对密码的长度、复杂度不作限制),并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调 **在客户端,应用切勿再次对密码加密,这会导致 [重置密码](#重置密码) 等功能失效**。 -## 游客登录 +### 手机号注册 -内建账户系统支持玩家在游戏中创建一个游客账号,其调用示例如下: +对于移动应用来说,允许用户以手机号注册是个很常见的需求。实现该功能大致分两步,第一步是让用户提供手机号,点击「获取验证码」按钮后,该号码会收到一个六位数的验证码: - + ```cs -try{ - // 通过 tdsUSer 给出用户唯一标识,如果有的话 - var tdsUser = await TDSUser.LoginAnonymously(); -}catch(Exception e){ - // 登录失败 - Debug.Log($"{e.code} : {e.message}"); -} +await LCSMSClient.RequestSMSCode("+8619201680101"); ``` ```java -TDSUser.logInAnonymously().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - - } - - @Override - public void onNext(TDSUser resultUser) { - // 登录成功,得到一个账户实例 - String userId = resultUser.getObjectId(); - } - - @Override - public void onError(Throwable throwable) { - - } - - @Override - public void onComplete() { - } +LCSMSOption option = new LCSMSOption(); +option.setSignatureName("sign_name"); // 设置短信签名名称 +LCSMS.requestSMSCodeInBackground("+8619201680101", option).subscribe(new Observer() { + @Override + public void onSubscribe(Disposable disposable) { + } + @Override + public void onNext(LCNull avNull) { + Log.d("TAG","Result: succeed to request SMSCode."); + } + @Override + public void onError(Throwable throwable) { + Log.d("TAG","Result: failed to request SMSCode. cause:" + throwable.getMessage()); + } + @Override + public void onComplete() { + } }); ``` -```objectivec -[TDSUser loginAnonymously:^(TDSUser * _Nullable user, NSError * _Nullable error) { - if (user) { - NSString *userId = user.objectId; +```objc +LCShortMessageRequestOptions *options = [[LCShortMessageRequestOptions alloc] init]; +options.templateName = @"template_name"; // 控制台配置好的模板名称 +options.signatureName = @"sign_name"; // 控制台配置好的短信签名名称 +[LCSMS requestShortMessageForPhoneNumber:@"+8619201680101" options:options callback:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + /* 请求成功 */ } else { - NSLog(@"%@", error); + /* 请求失败 */ } }]; ``` -```cpp -FTDSUser::LoginAnonymously(FTDSUser::FCallBackDelegate::CreateLambda([](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - FString UserID = UserPtr->GetObjectId(); - } else { - // 登录失败 Error.msg; +```swift +// templateName 是短信模版名称,signatureName 是短信签名名称。可以在控制台 > 短信 > 设置中查看。 +_ = LCSMSClient.requestShortMessage(mobilePhoneNumber: "+8619201680101", templateName: "template_name", signatureName: "sign_name") { (result) in + switch result { + case .success: + break + case .failure(error: let error): + print(error) } -})); +} ``` - +```dart +await LCSMSClient.requestSMSCode('+8619201680101'); +``` + +```js +AV.Cloud.requestSmsCode("+8619201680101"); +``` -:::info +```python +leancloud.cloud.request_sms_code('+8619201680101') +``` -这里的「游客账户」可以保证玩家在同一个设备上多次登录都得到同一个账户,但是如果玩家卸载游戏重装之后再以「游客」身份登录则无法保证账户的唯一性。 +```php +SMS::requestSmsCode("+8619201680101"); +``` -::: +```go +// 暂不支持 +``` -## 当前用户 + -用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户: +用户填入验证码后,用下面的方法完成注册: - + ```cs -TDSUser currentUser = await TDSUser.GetCurrent(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} +await LCUser.SignUpOrLoginByMobilePhone("+8619201680101", "123456"); ``` ```java -TDSUser currentUser = TDSUser.getCurrentUser(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} +LCUser.signUpOrLoginByMobilePhoneInBackground("+8619201680101", "123456").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 注册成功 + System.out.println("注册成功。objectId:" + user.getObjectId()); + } + public void onError(Throwable throwable) { + // 验证码不正确 + } + public void onComplete() {} +}); ``` ```objc -TDSUser *currentUser = [TDSUser currentUser]; -if (currentUser != nil) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} +[LCUser signUpOrLoginWithMobilePhoneNumberInBackground:@"+8619201680101" smsCode:@"123456" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 注册成功 + NSLog(@"注册成功。objectId:%@", user.objectId); + } else { + // 验证码不正确 + } +}]; ``` -```cpp -TSharedPtr CurrentUser = FTDSUser::GetCurrentUser(); -if (CurrentUser.IsValid()) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} +```swift +_ = LCUser.signUpOrLogIn(mobilePhoneNumber: "+8619201680101", verificationCode: "123456", completion: { (result) in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) + } +}) ``` - - -会话信息会长期有效,直到用户主动登出: - - - -```cs -await TDSUser.Logout(); - -// currentUser 变为 null -TDSUser currentUser = await TDSUser.GetCurrent(); +```dart + await LCUser.signUpOrLoginByMobilePhone('+8619201680101', '123456'); ``` -```java -TDSUser.logOut(); - -// currentUser 变为 null -TDSUser currentUser = TDSUser.getCurrentUser(); +```js +AV.User.signUpOrlogInWithMobilePhone("+8619201680101", "123456").then( + (user) => { + // 注册成功 + console.log(`注册成功。objectId:${user.id}`); + }, + (error) => { + // 验证码不正确 + } +); ``` -```objc -[TDSUser logOut]; - -// currentUser 变为 nil -TDSUser *currentUser = [TDSUser currentUser]; +```python +user = leancloud.User.signup_or_login_with_mobile_phone('+8619201680101', '123456') ``` -```cpp -FTDSUser::Logout(); +```php +User::signUpOrLoginByMobilePhone("+8619201680101", "123456"); +``` -// CurrentUser 变为 nullptr -TSharedPtr CurrentUser = FTDSUser::GetCurrentUser(); +```go +user, err := client.Users.SignUpByMobilePhone("+8619201680101", "123456") +if err != nil { + panic(err) +} ``` -## 设置当前用户 +`username` 将与 `mobilePhoneNumber` 相同,`password` 会由云端随机生成。如果希望让用户指定密码,可以在客户端让用户填写手机号和密码,然后按照上一小节使用用户名和密码注册的流程,将用户填写的手机号作为 `username` 和 `mobilePhoneNumber` 的值同时提交。同时根据业务需求,在**云服务控制台 > 内建账户 > 设置**勾选**未验证手机号码的用户,禁止登录**、**已验证手机号码的用户,允许以短信验证码登录**。 -用户登录后,云端会返回一个 **session token** 给客户端,它会由 SDK 缓存起来并用于日后同一 `TDSUser` 的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个 `TDSUser` 发起的请求了。 +### 手机号格式 -以下是一些应用可能需要用到 session token 的场景: +云端接受的手机号以 `+` 和国家代码开头,后面紧跟着剩余的部分。手机号中不应含有任何划线、空格等非数字字符。例如,`+15559463664` 是一个合法的美国或加拿大手机号(`1` 是国家代码),`+8619201680101` 是一个合法的中国手机号(`86` 是国家代码)。 -- 应用根据以前缓存的 session token 登录(可以通过 `sessionToken` 属性获取到当前用户的 session token;在服务端等受信任的环境下,可以通过 Master Key(即 Server Secret)读取任意用户的 `sessionToken` 字段以获取 session token)。 -- 应用内的某个 WebView 需要知道当前登录的用户。 -- 在服务端登录后,返回 session token 给客户端,客户端根据返回的 session token 登录。 +请参阅官网的[价格](https://www.leancloud.cn/pricing/)页面以了解支持的国家和地区。 -下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效): +## 登录 + +下面的代码用用户名和密码登录一个账户: - + ```cs -await TDSUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf"); +try { + // 登录成功 + LCUser user = await LCUser.Login("Tom", "cat!@#123"); +} catch (LCException e) { + // 登录失败(可能是密码错误) + print($"{e.code} : {e.message}"); +} ``` ```java -TDSUser.becomeWithSessionTokenInBackground("anmlwi96s381m6ca7o7266pzf").subscribe(new Observer() { +LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { public void onSubscribe(Disposable disposable) {} - public void onNext(TDSUser user) { - // 修改 currentUser - TDSUser.changeCurrentUser(user, true); + public void onNext(LCUser user) { + // 登录成功 } public void onError(Throwable throwable) { - // session token 无效 + // 登录失败(可能是密码错误) } public void onComplete() {} }); ``` ```objc -[TDSUser becomeWithSessionTokenInBackground:@"anmlwi96s381m6ca7o7266pzf" block:^(TDSUser * _Nullable user, NSError * _Nullable error) { +[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { if (user != nil) { // 登录成功 } else { - // session token 无效 + // 登录失败(可能是密码错误) } }]; ``` -```cpp -FTDSUser::BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf", FTDSUser::FCallBackDelegate::CreateLambda([](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - FString UserID = UserPtr->GetObjectId(); - } else { - // 登录失败 Error.msg; +```swift +_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) } -})); +} ``` - +```dart +try { + // 登录成功 + LCUser user = await LCUser.login('Tom', 'cat!@#123'); +} on LCException catch (e) { + // 登录失败(可能是密码错误) + print('${e.code} : ${e.message}'); +} +``` -请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。 +```js +AV.User.logIn("Tom", "cat!@#123").then( + (user) => { + // 登录成功 + }, + (error) => { + // 登录失败(可能是密码错误) + } +); +``` -如果在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 `403 (Forbidden)` 错误。 +```python +user = leancloud.User() +user.login(username='Tom', password='cat!@#123') +``` -下面的代码检查 session token 是否有效: +```php +User::logIn("Tom", "cat!@#123"); +``` + +```go +user, err := client.Users.LogIn("Tom", "cat!@#123") +if err != nil { + panic(err) +} +``` - + + +### 邮箱登录 + +下面的代码用邮箱和密码登录一个账户: + + ```cs -TDSUser currentUser = await TDSUser.GetCurrent(); -bool isAuthenticated = await currentUser.IsAuthenticated(); -if (isAuthenticated) { - // session token 有效 -} else { - // session token 无效 +try { + // 登录成功 + LCUser user = await LCUser.LoginByEmail("tom@xd.com", "cat!@#123"); +} catch (LCException e) { + // 登录失败(可能是密码错误) + print($"{e.code} : {e.message}"); } ``` ```java -boolean authenticated = TDSUser.getCurrentUser().isAuthenticated(); -if (authenticated) { - // session token 有效 -} else { - // session token 无效 -} +LCUser.loginByEmail("tom@xd.com", "cat!@#123").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 登录成功 + } + public void onError(Throwable throwable) { + // 登录失败(可能是密码错误) + } + public void onComplete() {} +}); ``` ```objc -TDSUser *currentUser = [TDSUser currentUser]; -NSString *token = currentUser.sessionToken; -[currentUser isAuthenticatedWithSessionToken:token callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // session token 有效 +[LCUser loginWithEmail:@"tom@xd.com" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 登录成功 } else { - // session token 无效 + // 登录失败(可能是密码错误) } }]; ``` -```cpp -TSharedPtr User = FTDSUser::GetCurrentUser(); -if (User.IsValid() && User->IsAuthenticated()) { - // session token 有效 -} else { - // session token 无效 +```swift +_ = LCUser.logIn(email: "tom@xd.com", password: "cat!@#123") { result in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) + } } ``` - - -## 设置其他用户属性 +```dart +try { + // 登录成功 + LCUser user = await LCUser.loginByEmail('tom@xd.com', 'cat!@#123'); +} on LCException catch (e) { + // 登录失败(可能是密码错误) + print('${e.code} : ${e.message}'); +} +``` -开发者可以使用内建账户系统设置 `nickname` 和 `avatar`,比如通过设置 `nickname` 字段来添加昵称: +```js +AV.User.loginWithEmail("tom@xd.com", "cat!@#123").then( + (user) => { + // 登录成功 + }, + (error) => { + // 登录失败(可能是密码错误) + } +); +``` - +```python +user = leancloud.User() +user.login(email='tom@xd.com', password='cat!@#123') +``` -```cs -var currentUser = await TDSUser.GetCurrent(); // 获取当前登录的账户实例 -currentUser["nickname"] = "Tarara"; -await currentUser.Save(); +```php +User::logInWithEmail("tom@xd.com", "cat!@#123"); ``` -```java -TDSUser currentUser = TDSUser.currentUser(); // 获取当前登录的账户实例 -currentUser.put("nickname", "Tarara"); -currentUser.saveInBackground().subscribe(new Observer() { +```go +user, err := client.LoginByEmail("tom@xd.com", "cat!@#123") +if err != nil { + panic(err) +} + +fmt.Println(user) +``` + + + +### 手机号登录 + +如果应用允许用户以手机号注册,那么也可以让用户以手机号配合密码或短信验证码登录。下面的代码用手机号和密码登录一个账户: + + + +```cs +try { + // 登录成功 + LCUser user = await LCUser.LoginByMobilePhoneNumber("+8619201680101", "cat!@#123"); +} catch (LCException e) { + // 登录失败(可能是密码错误) + print($"{e.code} : {e.message}"); +} +``` + +```java +LCUser.loginByMobilePhoneNumber("+8619201680101", "cat!@#123").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 登录成功 + } + public void onError(Throwable throwable) { + // 登录失败(可能是密码错误) + } + public void onComplete() {} +}); +``` + +```objc +[LCUser logInWithMobilePhoneNumberInBackground:@"+8619201680101" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 登录成功 + } else { + // 登录失败(可能是密码错误) + } +}]; +``` + +```swift +_ = LCUser.logIn(mobilePhoneNumber: "+8619201680101", password: "cat!@#123") { result in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +try { + // 登录成功 + LCUser user = await LCUser.loginByMobilePhoneNumber('+8619201680101', 'cat!@#123'); +} on LCException catch (e) { + // 登录失败(可能是密码错误) + print('${e.code} : ${e.message}'); +} +``` + +```js +AV.User.logInWithMobilePhone("+8619201680101", "cat!@#123").then( + (user) => { + // 登录成功 + }, + (error) => { + // 登录失败(可能是密码错误) + } +); +``` + +```python +user = leancloud.User.login_with_mobile_phone('+8619201680101', 'cat!@#123') +``` + +```php +User::logInWithMobilePhoneNumber("+8619201680101", "cat!@#123"); +``` + +```go +user, err := client.LogInByMobilePhoneNumber("+8619201680101", "cat!@#123") +if err != nil { + panic(err) +} + +fmt.Println(user) +``` + + + +默认情况下,云服务允许所有关联了手机号的用户直接以手机号登录,无论手机号是否 [通过验证](#验证手机号)。为了让应用更加安全,你可以选择只允许验证过手机号的用户通过手机号登录。可以在 **控制台 > 内建账户 > 设置** 里面开启该功能。 + +除此之外,还可以让用户通过短信验证码登录,适用于用户忘记密码且不愿重置密码的情况。和 [通过手机号注册](#手机号注册) 的步骤类似,首先让用户填写与账户关联的手机号码,然后在用户点击「获取验证码」后调用下面的方法: + + + +```cs +await LCUser.RequestLoginSMSCode("+8619201680101"); +``` + +```java +LCUser.requestLoginSmsCodeInBackground("+8619201680101").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser requestLoginSmsCode:@"+8619201680101"]; +``` + +```swift +_ = LCUser.requestLoginVerificationCode(mobilePhoneNumber: "+8619201680101") { result in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } +} +``` + +```dart +await LCUser.requestLoginSMSCode('+8619201680101'); +``` + +```js +AV.User.requestLoginSmsCode("+8619201680101"); +``` + +```python +leancloud.User.request_login_sms_code('+8619201680101') +``` + +```php +SMS::requestSmsCode("+8619201680101"); +``` + +```go +if err := client.Users.RequestLoginSMSCode("+8619201680101"); err != nil { + panic(err) +} +``` + + + +用户填写收到的验证码后,用下面的方法完成登录: + + + +```cs +try { + // 登录成功 + await LCUser.SignUpOrLoginByMobilePhone("+8619201680101", "123456"); +} catch (LCException e) { + // 验证码不正确 + print($"{e.code} : {e.message}"); +} +``` + +```java +LCUser.signUpOrLoginByMobilePhoneInBackground("+8619201680101", "123456").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 登录成功 + } + public void onError(Throwable throwable) { + // 验证码不正确 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser logInWithMobilePhoneNumberInBackground:@"+8619201680101" smsCode:@"123456" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 登录成功 + } else { + // 验证码不正确 + } +}]; +``` + +```swift +_ = LCUser.logIn(mobilePhoneNumber: "+8619201680101", verificationCode: "123456") { result in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +try { + // 登录成功 + await LCUser.signUpOrLoginByMobilePhone('+8619201680101', '123456'); +} on LCException catch (e) { + // 验证码不正确 + print('${e.code} : ${e.message}'); +} +``` + +```js +AV.User.logInWithMobilePhoneSmsCode("+8619201680101", "123456").then( + (user) => { + // 登录成功 + }, + (error) => { + // 验证码不正确 + } +); +``` + +```python +user = leancloud.User.signup_or_login_with_mobile_phone('+8619201680101', '123456') +``` + +```php +User::signUpOrLoginByMobilePhone("+8619201680101", "123456"); +``` + +```go +user, err := client.Users.LogInByMobilePhoneNumber("+8619201680101", "123456") +if err != nil { + panic(err) +} +``` + + + +### 测试手机号和固定验证码 + +在开发过程中,可能会因测试目的而需要频繁地用手机号注册登录,然而运营商的发送频率限制往往会导致测试过程耗费较多的时间。 + +为了解决这个问题,可以在 **控制台 > 短信 > 设置** 里面设置一个测试手机号,而云端会为该号码生成一个固定验证码。以后进行登录操作时,只要使用的是这个号码,云端就会直接放行,无需经过运营商网络。 + +测试手机号还可用于将 iOS 应用提交到 App Store 进行审核的场景,因为审核人员可能因没有有效的手机号码而无法登录应用来进行评估审核。如果不提供一个测试手机号,应用有可能被拒绝。 + +可参阅 [短信 SMS 服务使用指南](/sdk/sms/guide/) 来了解更多有关短信发送和接收的限制。 + +### 单设备登录 + +某些场景下需要确保用户的账户在同一时间只在一台设备上登录,也就是说当用户在一台设备上登录后,其他设备上的会话全部失效。可以按照以下方案来实现: + +1. 新建一个专门用于记录用户登录信息和当前设备信息的 class。 +2. 每当用户在新设备上登录时,将该 class 中该用户对应的设备更新为该设备。 +3. 在另一台设备上打开客户端时,检查该设备是否与云端保存的一致。若不一致,则将用户 [登出](#当前用户)。 + +### 账户锁定 + +输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 `{ "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." }`,开发者可在客户端进行必要提示。 + +锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 + +## 验证邮箱 + +可以通过要求用户在登录或使用特定功能之前验证邮箱的方式防止恶意注册。默认情况下,当用户注册或变更邮箱后,`emailVerified` 会被设为 `false`。在应用的 **云服务控制台 > 内建账户 > 设置** 中,可以开启 **启用邮箱验证功能** 选项,这样当用户注册或变更邮箱时,会收到一封含有验证链接的邮件。在同一设置页面还可找到阻止未验证邮箱的用户登录的选项。 + +如果用户忘记点击链接并且在未来某一时刻需要进行验证,可以用下面的代码发送一封新的邮件: + + + +```cs +await LCUser.RequestEmailVerify("tom@xd.com"); +``` + +```java +LCUser.requestEmailVerifyInBackground("tom@xd.com").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser requestEmailVerify:@"tom@xd.com"]; +``` + +```swift +_ = LCUser.requestVerificationMail(email: "tom@xd.com") { result in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } +} +``` + +```dart +await LCUser.requestEmailVerify('tom@xd.com'); +``` + +```js +AV.User.requestEmailVerify("tom@xd.com"); +``` + +```python +leancloud.User.request_email_verify('tom@xd.com') +``` + +```php +User::requestEmailVerify("tom@xd.com"); +``` + +```go +if err := client.Users.RequestEmailVerify("tom@xd.com"); err != nil { + panic(err) +} +``` + + + +用户点击邮件内的链接后,`emailVerified` 会变为 `true`。如果用户的 `email` 属性为空,则该属性永远不会为 `true`。 + +## 验证手机号 + +和 [验证邮箱](#验证邮箱) 类似,应用还可以要求用户在登录或使用特定功能之前验证手机号。默认情况下,当用户注册或变更手机号后,`mobilePhoneVerified` 会被设为 `false`。在应用的 **控制台 > 内建账户 > 设置** 中,可以开启阻止未验证手机号的用户登录的选项。 + +可以用下面的代码发送一条新的验证码(如果相应用户的 `mobilePhoneVerified` 已经为 `true`,那么验证短信不会发送): + + + +```cs +await LCUser.RequestMobilePhoneVerify("+8619201680101"); +``` + +```java +LCUser.requestMobilePhoneVerifyInBackground("+8619201680101").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser requestMobilePhoneVerify:@"+8619201680101" withBlock:^(BOOL succeeded, NSError * _Nullable error) { + if(succeeded){ + // 请求成功 + }else{ + // 请求失败 + } +}]; +``` + +```swift +_ = LCUser.requestVerificationCode(mobilePhoneNumber: "+8619201680101") { result in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } +} +``` + +```dart +await LCUser.requestMobilePhoneVerify('+8619201680101'); +``` + +```js +AV.User.requestMobilePhoneVerify("+8619201680101"); +``` + +```python +leancloud.User.request_mobile_phone_verify('+8619201680101') +``` + +```php +User::requestMobilePhoneVerify("+8619201680101"); +``` + +```go +if err := client.Users.RequestMobilePhoneVerify("+8619201680101"); err != nil { + panic(err) +} +``` + + + +用户填写验证码后,调用下面的方法来完成验证。`mobilePhoneVerified` 将变为 `true`: + + + +```cs +await LCUser.VerifyMobilePhone("+8619201680101", "123456"); +``` + +```java +LCUser.verifyMobilePhoneInBackground("123456").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // mobilePhoneVerified 将变为 true + } + public void onError(Throwable throwable) { + // 验证码不正确 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser verifyMobilePhone:@"123456" withBlock:^(BOOL succeeded, NSError * _Nullable error) { + if(succeeded){ + // mobilePhoneVerified 将变为 true + }else{ + // 验证码不正确 + } +}]; +``` + +```swift +_ = LCUser.verifyMobilePhoneNumber(mobilePhoneNumber: "+8619201680101", verificationCode: "123456") { result in + switch result { + case .success: + // mobilePhoneVerified 将变为 true + break + case .failure(error: let error): + // 验证码不正确 + print(error) + } +} +``` + +```dart +await LCUser.verifyMobilePhone('+8619201680101','123456'); +``` + +```js +AV.User.verifyMobilePhone("123456").then( + () => { + // mobilePhoneVerified 将变为 true + }, + (error) => { + // 验证码不正确 + } +); +``` + +```python +leancloud.User.verify_mobile_phone_number('123456') +``` + +```php +User::verifyMobilePhone("123456"); +``` + +```go +// 暂不支持 +``` + + + +### 绑定、修改手机号之前先验证 + +除了在用户绑定、修改手机号**之后**进行验证,云服务也支持在用户绑定或修改手机号**之前**先通过短信验证。也就是说,绑定手机号或修改手机号时先请求发送验证码(用户需处于登录状态),再凭短信验证码完成绑定或修改操作。 + + + +```cs +await LCUser.RequestSMSCodeForUpdatingPhoneNumber("+8619201680101"); + +await LCUser.VerifyCodeForUpdatingPhoneNumber("+8619201680101", "123456"); +// 更新本地数据 +LCUser currentUser = await LCUser.GetCurrent(); +user.Mobile = "+8619201680101"; +``` + +```java +LCUser.requestSMSCodeForUpdatingPhoneNumberInBackground("+8619201680101",null).subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + } + @Override + public void onNext(@NonNull LCNull lcNull) { + // 成功调用 + } + @Override + public void onError(@NonNull Throwable e) { + // 调用出错 + } + @Override + public void onComplete() { + } +}); + +LCUser.verifySMSCodeForUpdatingPhoneNumberInBackground("123456", "+8619201680101").subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + } + @Override + public void onNext(@NonNull LCNull lcNull) { + // 更新本地数据 + LCUser currentUser = LCUser.getCurrentUser(); + currentUser.setMobilePhoneNumber("+8619201680101"); + } + @Override + public void onError(@NonNull Throwable e) { + // 验证码不正确 + } @Override - public void onSubscribe(@NotNull Disposable d) { + public void onComplete() { + } +}); +``` + +```objc +[LCUser requestVerificationCodeForUpdatingPhoneNumber:@"+8619201680101" withBlock:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + // 请求成功 + } else { + // 请求失败 + } +}]; + +[LCUser verifyCodeToUpdatePhoneNumber:@"+8619201680101" code:@"123456" withBlock:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + // mobilePhoneNumber 变为 +8619201680101 + // mobilePhoneVerified 变为 true + } else { + // 验证码不正确 + } +}]; +``` + +```swift +_ = LCUser.requestVerificationCode(forUpdatingMobilePhoneNumber: "+8619201680101") { result in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } +} + +_ = LCUser.verifyVerificationCode("123456", toUpdateMobilePhoneNumber:"+8619201680101") { result in + switch result { + case .success: + // mobilePhoneNumber 变为 +8619201680101 + // mobilePhoneVerified 变为 true + break + case .failure(error: let error): + // 验证码不正确 + print(error) + } +} +``` + +```dart +await LCUser.requestSMSCodeForUpdatingPhoneNumber('+8619201680101'); + +await LCUser.verifyCodeForUpdatingPhoneNumber('+8619201680101', '123456'); +// 更新本地数据 +LCUser currentUser = await LCUser.getCurrent(); +user.mobile = '+8619201680101'; +``` + +```js +AV.User.requestChangePhoneNumber("+8619201680101"); + +AV.User.changePhoneNumber("+8619201680101", "123456").then( + () => { + // 更新本地数据 + const currentUser = AV.User.current(); + currentUser.setMobilePhoneNumber("+8619201680101"); + }, + (error) => { + // 验证码不正确 + } +); +``` + +```python +User.request_change_phone_number("+8619201680101") + +User.change_phone_number("123456", "+8619201680101") +# 更新本地数据 +current_user = leancloud.User.get_current() +current_user.set_mobile_phone_number("+8619201680101") +``` + +```php +User::requestChangePhoneNumber("+8619201680101"); + +User::changePhoneNumber("123456", "+8619201680101"); +// 更新本地数据 +$currentUser = User::getCurrentUser(); +$user->setMobilePhoneNumber("+8619201680101"); +``` + +```go +if err := client.Users.requestChangePhoneNumber("+8619201680101"); err != nil { + panic(err) +} + +if err := client.Users.ChangePhoneNumber("123456", "+8619201680101"); err != nil { + panic(err) +} +``` + + + +## 当前用户 + +用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户: + + + +```cs +LCUser currentUser = await LCUser.GetCurrent(); +if (currentUser != null) { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```java +LCUser currentUser = LCUser.getCurrentUser(); +if (currentUser != null) { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```objc +LCUser *currentUser = [LCUser currentUser]; +if (currentUser != nil) { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```swift +if let user = LCApplication.default.currentUser { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```dart +LCUser currentUser = await LCUser.getCurrent(); +if (currentUser != null) { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```js +const currentUser = AV.User.current(); +if (currentUser) { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```python +current_user = leancloud.User.get_current() +if current_user is not None: + # 跳到首页 + pass +else: + # 显示注册或登录页面 + pass +``` + +```php +$currentUser = User::getCurrentUser(); +if ($currentUser != null) { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```go +// 暂不支持 +``` + + + +会话信息会长期有效,直到用户主动登出: + + + +```cs +await LCUser.Logout(); + +// currentUser 变为 null +LCUser currentUser = await LCUser.GetCurrent(); +``` + +```java +LCUser.logOut(); + +// currentUser 变为 null +LCUser currentUser = LCUser.getCurrentUser(); +``` + +```objc +[LCUser logOut]; + +// currentUser 变为 nil +LCUser *currentUser = [LCUser currentUser]; +``` + +```swift +LCUser.logOut() + +// currentUser 变为 nil +let currentUser = LCApplication.default.currentUser +``` + +```dart +await LCUser.logout(); + +// currentUser 变为 null +LCUser currentUser = await LCUser.getCurrent(); +``` + +```js +AV.User.logOut(); + +// currentUser 变为 null +const currentUser = AV.User.current(); +``` + +```python +user.logout() + +current_user = leancloud.User.get_current() # None +``` + +```php +User::logOut(); + +// currentUser 变为 null +$currentUser = User::getCurrentUser(); +``` + +```go +// 暂不支持 +``` + + + +## 设置当前用户 + +用户登录后,云端会返回一个 **session token** 给客户端,它会由 SDK 缓存起来并用于日后同一用户的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个用户发起的请求了。 + +以下是一些应用可能需要用到 session token 的场景: + +- 应用根据以前缓存的 session token 登录。 +- 应用内的某个 WebView 需要知道当前登录的用户。 +- 在服务端登录后,返回 session token 给客户端,客户端根据返回的 session token 登录。 + +下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效): + + + +```cs +await LCUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf"); +``` + +```java +LCUser.becomeWithSessionTokenInBackground("anmlwi96s381m6ca7o7266pzf").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 修改 currentUser + LCUser.changeCurrentUser(user, true); + } + public void onError(Throwable throwable) { + // session token 无效 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser becomeWithSessionTokenInBackground:@"anmlwi96s381m6ca7o7266pzf" block:^(LCUser * _Nullable user, NSError * _Nullable error) { + if (user != nil) { + // 登录成功 + } else { + // session token 无效 + } +}]; +``` + +```swift +_ = LCUser.logIn(sessionToken: "anmlwi96s381m6ca7o7266pzf") { (result) in + switch result { + case .success(object: let user): + // 登录成功 + print(user) + case .failure(error: let error): + // session token 无效 + print(error) + } +} +``` + +```dart +await LCUser.becomeWithSessionToken('anmlwi96s381m6ca7o7266pzf'); +``` + +```js +AV.User.become("anmlwi96s381m6ca7o7266pzf").then( + (user) => { + // 登录成功 + }, + (error) => { + // session token 无效 + } +); +``` + +```python +user = leancloud.User.become('anmlwi96s381m6ca7o7266pzf') +``` + +```php +User::become("anmlwi96s381m6ca7o7266pzf"); +``` + +```go +user, err := client.Users.Become("anmlwi96s381m6ca7o7266pzf") +if err != nil { + panic(err) +} +``` + + + +请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。 + +如果在 **控制台 > 内建账户 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 `403 (Forbidden)` 错误。 + +下面的代码检查 session token 是否有效: + + + +```cs +LCUser currentUser = await LCUser.GetCurrent(); +bool isAuthenticated = await currentUser.IsAuthenticated(); +if (isAuthenticated) { + // session token 有效 +} else { + // session token 无效 +} +``` + +```java +boolean authenticated = LCUser.getCurrentUser().isAuthenticated(); +if (authenticated) { + // session token 有效 +} else { + // session token 无效 +} +``` + +```objc +LCUser *currentUser = [LCUser currentUser]; +NSString *token = currentUser.sessionToken; +[currentUser isAuthenticatedWithSessionToken:token callback:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + // session token 有效 + } else { + // session token 无效 + } +}]; +``` + +```swift +if let sessionToken = LCApplication.default.currentUser?.sessionToken?.value { + _ = LCUser.logIn(sessionToken: sessionToken) { (result) in + if result.isSuccess { + // session token 有效 + } else { + // session token 无效 + } + } +} +``` + +```dart +LCUser currentUser = await LCUser.getCurrent(); +bool isAuthenticated = await currentUser.isAuthenticated(); +if (isAuthenticated) { + // session token 有效 +} else { + // session token 无效 +} +``` + +```js +const currentUser = AV.User.current(); +currentUser.isAuthenticated().then((authenticated) => { + if (authenticated) { + // session token 有效 + } else { + // session token 无效 + } +}); +``` + +```python +authenticated = leancloud.User.get_current().is_authenticated() +if authenticated: + # session token 有效 + pass +else: + # session token 无效 + pass +``` + +```php +$authenticated = User::isAuthenticated(); +if ($authenticated) { + // session token 有效 +} else { + // session token 无效 +} +``` + +```go +// 暂不支持 +``` + + + +## 重置密码 +我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了多种重置密码的方法。 + +邮箱重置密码的流程如下: + +1. 用户输入注册的电子邮箱,请求重置密码; +2. 云端向该邮箱发送一封包含重置密码的特殊链接的电子邮件; +3. 用户点击重置密码链接后,一个特殊的页面会打开,让他们输入新密码; +4. 用户的密码已被重置为新输入的密码。 + +首先让用户填写注册账户时使用的邮箱,然后调用下面的方法: + + + +```cs +await LCUser.RequestPasswordReset("tom@xd.com"); +``` + +```java +LCUser.requestPasswordResetInBackground("tom@xd.com").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 } + public void onComplete() {} +}); +``` - @Override - public void onNext(@NotNull LCObject lcObject) { - // 保存成功,currentUser 的属性得到更新 - TDSUser tdsUser = (TDSUser) lcObject; +```objc +[LCUser requestPasswordResetForEmailInBackground:@"tom@xd.com"]; +``` + +```swift +_ = LCUser.requestPasswordReset(email: "tom@xd.com") { (result) in + switch result { + case .success: + break + case .failure(error: let error): + print(error) } +} +``` - @Override - public void onError(@NotNull Throwable e) { +```dart +await LCUser.requestPasswordReset('tom@xd.com'); +``` + +```js +AV.User.requestPasswordReset("tom@xd.com"); +``` + +```python +leancloud.User.request_password_reset('tom@xd.com') +``` + +```php +User::requestPasswordReset("tom@xd.com"); +``` + +```go +if err := client.Users.RequestPasswordReset("tom@xd.com"); err != nil { + panic(err) +} +``` + + + +上面的代码会查询是否有用户的 `email` 属性与前面提供的邮箱匹配。如果有的话,则向该邮箱发送一封密码重置邮件。之前提到过,应用可以让 `username` 与 `email` 保持一致,也可以单独收集用户的邮箱并将其存为 `email`。 + +密码重置邮件的内容可在应用的 **云服务控制台 > 内建账户 > 邮件模版** 中自定义。更多关于自定义邮件模板和验证链接的内容,请参考《自定义邮件验证和重设密码页面》。 + +除此之外,还可以用手机号重置密码: + +1. 用户输入注册的手机号,请求重置密码; +2. 云端向该号码发送一条包含验证码的短信; +3. 用户输入验证码和新密码。 + +下面的代码向用户发送含有验证码的短信: + + + +```cs +await LCUser.RequestPasswordRestBySmsCode("+8619201680101"); +``` + +```java +LCUser.requestPasswordResetBySmsCodeInBackground("+8619201680101").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser requestPasswordResetWithPhoneNumber:@"+8619201680101"]; +``` +```swift +_ = LCUser.requestPasswordReset(mobilePhoneNumber: "+8619201680101") { (result) in + switch result { + case .success: + break + case .failure(error: let error): + print(error) } +} +``` - @Override - public void onComplete() { +```dart +await LCUser.requestPasswordRestBySmsCode('+8619201680101'); +``` + +```js +AV.User.requestPasswordResetBySmsCode("+8619201680101"); +``` + +```python +leancloud.User.request_password_reset_by_sms_code('+8619201680101') +``` + +```php +User::requestPasswordResetBySmsCode("+8619201680101"); +``` + +```go +if err := client.Users.RequestPasswordResetBySmsCode("+8619201680101"); err != nil { + panic(err) +} +``` + + + +上面的代码会查询是否有用户的 `mobilePhoneNumber` 属性与前面提供的手机号匹配。如果有的话,则向该号码发送验证码短信。 + +可以在 **云服务控制台 > 内建账户 > 设置** 中设置只有在 `mobilePhoneVerified` 为 `true` 的情况下才能用手机号重置密码。 + +用户输入验证码和新密码后,用下面的代码完成密码重置: + + + +```cs +await LCUser.ResetPasswordBySmsCode("+8619201680101", "123456", "cat!@#123"); +``` +```java +LCUser.resetPasswordBySmsCodeInBackground("123456", "cat!@#123").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 密码重置成功 + } + public void onError(Throwable throwable) { + // 验证码不正确 } + public void onComplete() {} }); ``` -```objectivec -TDSUser *currentUser = [TDSUser currentUser]; -currentUser[@"nickname"] = @"Tarara"; -[currentUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) { +```objc +[LCUser resetPasswordWithSmsCode:@"123456" newPassword:@"cat!@#123" block:^(BOOL succeeded, NSError *error) { if (succeeded) { - // 保存成功 + // 密码重置成功 } else { - NSLog(@"%@", error); + // 验证码不正确 } }]; ``` -```cpp -TSharedPtr User = FTDSUser::GetCurrentUser(); -User->SetNickName(TEXT("Tarara")); -User->Save(FTDSUser::FCallBackDelegate::CreateLambda([](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - // 保存成功 - } else { - // 登录失败 Error.msg; +```swift +_ = LCUser.resetPassword(mobilePhoneNumber: "+8619201680101", verificationCode: "123456", newPassword: "cat!@#123") { result in + switch result { + case .success: + // 密码重置成功 + break + case .failure(error: let error): + // 验证码不正确 + print(error) } -})); +} ``` - +```dart +await LCUser.resetPasswordBySmsCode('+8619201680101', '123456', 'cat!@#123'); +``` + +```js +AV.User.resetPasswordBySmsCode("123456", "cat!@#123").then( + () => { + // 密码重置成功 + }, + (error) => { + // 验证码不正确 + } +); +``` -内建账户系统只支持内置字段和两个自定义字段: `nickname`(昵称) 以及 `avatar`(头像),添加其他新的字段会报错。 +```python +leancloud.User.reset_password_by_sms_code('123456', 'cat!@#123') +``` -内建账户系统保存了用户的**鉴权信息**,也可能保存了邮箱、手机等**敏感信息**,因此会设置非常严格的权限,以防用户信息泄露。另外,在内建账户系统中保存过多数据,也容易导致慢查询等性能问题。所以,我们限制了自定义字段的使用,如需保存其他的用户信息,建议另外创建专门的 Class(比如 `UserProfile`)。 +```php +User::resetPasswordBySmsCode("123456", "cat!@#123"); +``` -:::tip -建议开发者使用 `nickname` 字段存储昵称信息,[TDS 游戏好友模块](/sdk/friends/guide/) **根据昵称查找好友、好友邀请链接功能都使用了 `nickname` 字段**。 +```go +if err := client.Users.ResetPasswordBySmsCode("+8619201680101", "123456", "cat!@#123"); err != nil { + panic(err) +} +``` -[通过 TapTap OAuth 授权结果直接登录](/sdk/taptap-login/guide/start/#一键完成-taptap-登录)这一方式创建的玩家,SDK 会自动将 `nickname` 字段设置为 TapTap 账户的用户名。 -::: + ## 用户的查询 -`TDSUser` 是 `LCObject` 的子类,所以 `LCObject` 支持的数据增删改查方式,`TDSUser` 也都可用。感兴趣的读者可以参考[数据存储的开发文档](/sdk/storage/features/)了解更多信息。 +使用下面的代码来查询用户: + + + +```cs +LCQuery userQuery = LCUser.GetQuery(); +``` + +```java +LCQuery userQuery = LCUser.getQuery(); +``` + +```objc +LCQuery *userQuery = [LCUser query]; +``` + +```swift +let userQuery = LCQuery(className: "_User") +``` + +```dart +LCQuery userQuery = LCUser.getQuery(); +``` + +```js +const userQuery = new AV.Query("_User"); +``` + +```python +user_query = leancloud.Query('_leancloud.User') +``` + +```php +$userQuery = new Query("_User"); +``` + +```go +userQuery := client.Users.NewUserQuery() +``` + + -不过,为了安全起见,**内建账户系统(`_User` 表)默认关闭了 `find` 权限**,这样每位用户登录后只能查询到自己在 `_User` 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 `find` 查询权限。除此之外,还可以在云引擎里封装用户查询相关的方法。 +为了安全起见,**新创建的应用的 `_User` 表默认关闭了 `find` 权限**,这样每位用户登录后只能查询到自己在 `_User` 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 `find` 查询权限。除此之外,还可以在 [云引擎](/sdk/engine/overview) 里封装用户查询相关的方法。 -可以参见 [用户对象的安全](#用户对象的安全) 来了解 `_User` 表的一些限制,还可以阅读[数据和安全](/sdk/storage/guide/security/)来了解更多 class 级权限设置的方法。 +可以参见 [用户对象的安全](#用户对象的安全) 来了解 `_User` 表的一些限制,还可以阅读[《数据和安全》](/sdk/storage/guide/security)来了解更多 class 级权限设置的方法。 ## 关联用户对象 -关联 `TDSUser` 的方法和 `LCObject` 是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书: +关联用户的方法和对象是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书: - + ```cs LCObject book = new LCObject("Book"); -TDSUser author = await LCUser.GetCurrent(); +LCUser author = await LCUser.GetCurrent(); book["title"] = "我的第五本书"; book["author"] = author; await book.Save(); @@ -850,7 +1860,7 @@ ReadOnlyCollection books = await query.Find(); ```java LCObject book = new LCObject("Book"); -TDSUser author = TDSUser.getCurrentUser(); +LCUser author = LCUser.getCurrentUser(); book.put("title", "我的第五本书"); book.put("author", author); book.saveInBackground().subscribe(new Observer() { @@ -875,7 +1885,7 @@ book.saveInBackground().subscribe(new Observer() { ```objc LCObject *book = [LCObject objectWithClassName:@"Book"]; -TDSUser *author = [TDSUser currentUser]; +LCUser *author = [LCUser currentUser]; [book setObject:@"我的第五本书" forKey:@"title"]; [book setObject:author forKey:@"author"]; [book saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { @@ -888,33 +1898,123 @@ TDSUser *author = [TDSUser currentUser]; }]; ``` -```cpp -暂不支持 +```swift +do { + guard let author = LCApplication.default.currentUser else { + return + } + let book = LCObject(className: "Book") + try book.set("title", value: "我的第五本书") + try book.set("author", value: author) + _ = book.save { result in + switch result { + case .success: + // 获取所有该作者写的书 + let query = LCQuery(className: "Book") + query.whereKey("author", .equalTo(author)) + _ = query.find { result in + switch result { + case .success(objects: let books): + // books 是包含同一作者所有 Book 对象的数组 + break + case .failure(error: let error): + print(error) + } + } + case .failure(error: let error): + print(error) + } + } +} catch { + print(error) +} +``` + +```dart +LCObject book = LCObject('Book'); +LCUser author = await LCUser.getCurrent(); +book['title'] = '我的第五本书'; +book['author'] = author; +await book.save(); + +LCQuery query = LCQuery('Book'); +query.whereEqualTo('author', author); +// books 是包含同一作者所有 Book 对象的数组 +List books = await query.find(); +``` + +```js +const Book = AV.Object.extend("Book"); +const book = new Book(); +const author = AV.User.current(); +book.set("title", "我的第五本书"); +book.set("author", author); +book.save().then((book) => { + // 获取所有该作者写的书 + const query = new AV.Query("Book"); + query.equalTo("author", author); + query.find().then((books) => { + // books 是包含同一作者所有 Book 对象的数组 + }); +}); +``` + +```python +Book = leancloud.Object.extend('Book') +book = Book() +author = leancloud.User.get_current() +book.set('title', '我的第五本书') +book.set('author', author) +book.save() + +# 获取所有该作者写的书 +query = Book.query +query.equal_to('author', author) +book_list = query.find() +``` + +```php +$book = new LeanObject("Book"); +$author = User::getCurrentUser(); +$book->set("title", "我的第五本书"); +$book->set("author", $author); +$book->save(); + +// 获取所有该作者写的书 +$query = new Query("Book"); +$query->equalTo("author", $author); +$books = $query->find(); +``` + +```go +// 暂不支持 ``` ## 用户对象的安全 -`TDSUser` 类自带安全保障,只有通过登录等经过鉴权的方法获取到的 `TDSUser` 才能进行保存或删除相关的操作,保证每个用户只能修改自己的数据。 +用户对象自带安全保障,只有通过经过鉴权的方法获取到的用户对象才能进行更新或删除操作,保证每个用户只能修改自己的数据。 -这样设计是因为 `TDSUser` 中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。 +这样设计是因为用户对象中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。 下面的代码展现了这种安全措施: - + ```cs try { - TDSUser tdsUser = await TDSUser.LoginWithTapTap(); + LCUser user = await LCUser.Login("Tom", "cat!@#123"); // 试图修改用户名 user["username"] = "Jerry"; + // 密码已被加密,这样做会获取到空字符串 + string password = user["password"]; // 可以执行,因为用户已鉴权 await user.Save(); // 绕过鉴权直接获取用户 - LCQuery userQuery = TDSUser.GetQuery(); - TDSUser unauthenticatedUser = await userQuery.Get(user.ObjectId); + LCQuery userQuery = LCUser.GetQuery(); + LCUser unauthenticatedUser = await userQuery.Get(user.ObjectId); unauthenticatedUser["username"] = "Toodle"; // 会出错,因为用户未鉴权 @@ -925,97 +2025,266 @@ try { ``` ```java -TDSUser.loginWithTapTap(MainActivity.this, new Callback() { - @Override - public void onSuccess(TDSUser resultUser) { - Toast.makeText(MainActivity.this, "Logged in with TapTap.", Toast.LENGTH_SHORT).show(); - // 可以修改,因为已经鉴权 - resultUser.put("username", "Toodle"); - // 仅为示意,实际项目中需使用异步方法以免阻塞 - resultUser.save(); +LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 试图修改用户名 + user.put("username", "Jerry"); + // 密码已被加密,这样做会获取到空字符串 + String password = user.getString("password"); + // 可以执行,因为用户已鉴权 + user.save(); + + // 绕过鉴权直接获取用户 + LCQuery query = new LCQuery<>("_User"); + query.getInBackground(user.getObjectId()).subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser unauthenticatedUser) { + unauthenticatedUser.put("username", "Toodle"); + // 会出错,因为用户未鉴权 + unauthenticatedUser.save(); + } + public void onError(Throwable throwable) {} + public void onComplete() {} + }); + } + public void onError(Throwable throwable) {} + public void onComplete() {} +}); +``` + +```objc +[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 试图修改用户名 + [user setObject:@"Jerry" forKey:@"username")]; + // 密码已被加密,这样做会获取到空字符串 + NSString *password = user[@"password"]; + // 保存更改 + [user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + if (succeeded) { + // 可以执行,因为用户已鉴权 + + // 绕过鉴权直接获取用户 + LCQuery *query = [LCQuery queryWithClassName:@"_User"]; + [query getObjectInBackgroundWithId:user.objectId block:^(LCObject *unauthenticatedUser, NSError *error) { + [unauthenticatedUser setObject:@"Toodle" forKey:@"username"]; + [unauthenticatedUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + if (succeeded) { + // 无法执行,因为用户未鉴权 + } else { + // 操作失败 + } + }]; + }]; + } else { + // 错误处理 + } + }]; + } else { + // 错误处理 + } +}]; +``` + +```swift +_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in + switch result { + case .success(object: let user): + // 试图修改用户名 + try! user.set("username", "Jerry") + // 密码已被加密,这样做会获取到空字符串 + let password = user.get("password") + // 可以执行,因为用户已鉴权 + user.save() + + // 绕过鉴权直接获取用户 + let query = LCQuery(className: "_User") + _ = query.get(user.objectId) { result in + switch result { + case .success(object: let unauthenticatedUser): + try! unauthenticatedUser.set("username", "Toodle") + _ = unauthenticatedUser.save { result in + switch result { + .success: + // 无法执行,因为用户未鉴权 + .failure: + // 操作失败 + } + } + case .failure(error: let error): + print(error) + } + } + case .failure(error: let error): + print(error) + } +} +``` + +```dart +try { + LCUser user = await LCUser.login('Tom', 'cat!@#123'); + // 试图修改用户名 + user['username'] = 'Jerry'; + // 密码已被加密,这样做会获取到空字符串 + String password = user['password']; + // 可以执行,因为用户已鉴权 + await user.save(); + + // 绕过鉴权直接获取用户 + LCQuery userQuery = LCQuery('_User'); + LCUser unauthenticatedUser = await userQuery.get(user.objectId); + unauthenticatedUser['username'] = 'Toodle'; + + // 会出错,因为用户未鉴权 + unauthenticatedUser.save(); +} on LCException catch (e) { + print('${e.code} : ${e.message}'); +} +``` + +```js +const user = AV.User.logIn("Tom", "cat!@#123").then((user) => { + // 试图修改用户名 + user.set("username", "Jerry"); + // 密码已被加密,这样做会获取到空字符串 + const password = user.get("password"); + // 保存更改 + user.save().then((user) => { + // 可以执行,因为用户已鉴权 + + // 绕过鉴权直接获取用户 + const query = new AV.Query("_User"); + query.get(user.objectId).then((unauthenticatedUser) => { + unauthenticatedUser.set("username", "Toodle"); + unauthenticatedUser.save().then( + (unauthenticatedUser) => {}, + (error) => { + // 会出错,因为用户未鉴权 + } + ); + }); + }); +}); +``` + +```python +leancloud.User.login('Tom', 'cat!@#123') +current_user = leancloud.User.get_current() + +# 试图修改用户名 +current_user.set('username', 'Jerry') +# 密码已被加密,这样做会获取到空字符串 +password = current_user.get('password') +# 可以执行,因为用户已鉴权 +current_user.save() + +# 绕过鉴权直接获取用户 +query = leancloud.Query('_User') +unauthenticated_user = query.get(current_user.id) +unauthenticated_user.set('username', 'Toodle') +# 会出错,因为用户未鉴权 +unauthenticated_user.save() +``` + +```php +User::logIn("Tom", "cat!@#123"); +$currentUser = User::getCurrentUser(); + +// 试图修改用户名 +$currentUser->set("username", "Jerry"); +// 密码已被加密,这样做会获取到空字符串 +$password = $currentUser->get("password"); +// 可以执行,因为用户已鉴权 +$currentUser->save(); + +// 绕过鉴权直接获取用户 +$query = new Query("_User"); +$unauthenticatedUser = $query->get($currentUser->getObjectId()) +$unauthenticatedUser->set("username", "Toodle"); +// 会出错,因为用户未鉴权 +$unauthenticatedUser->save() +``` + +```go +user, err := client.Users.LogIn("Tom", "cat!@#123") +if err != nil { + panic(err) +} + +// 试图修改用户名,未鉴权将失败 +if err := client.User(user).Set("username", "Jerry"); err != nil { + panic(err) +} + +// 密码已被加密,这样做会获取到空字符串 +password := user.String("password") + +// 可以执行,因为用户已鉴权 +if err := client.User(user).Set("username", "Jerry", leancloud.UseUser(user)); err != nil { + panic(err) +} + +// 绕过鉴权直接获取用户 +unauthenticatedUser := User{} +if err := client.Users.NewUserQuery().EqualTo("objectId", user.ID).First(&unauthenticatedUser); err != nil { + panic(err) +} + +// 会出错,因为用户未鉴权 +if err := client.User(unauthenticatedUser).Set("username", "Toodle"); err != nil { + panic(err) +} +``` + + + +通过调用 [当前用户](#当前用户) 相关方法获取的用户总是经过鉴权的。 + +要查看一个用户对象是否经过鉴权,可以调用如下方法。通过经过鉴权的方法获取到的用户对象无需进行该检查。 + + + +```cs +IsAuthenticated +``` + +```java +isAuthenticated +``` - // 绕过鉴权直接获取用户 - LCQuery query = new LCQuery<>("_User"); - query.getInBackground(user.getObjectId()).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(TDSUser unauthenticatedUser) { - unauthenticatedUser.put("username", "Toodle"); - // 会出错,因为用户未鉴权 - unauthenticatedUser.save(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} - }); - } +```objc +isAuthenticatedWithSessionToken +``` - @Override - public void onFail(TapError error) { - Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show(); - } -}, "public_profile"); +```swift +// 暂不支持 ``` -```objc -[TDSUser loginByTapTapWithPermissions:@[@"public_profile"] callback:^(TDSUser * _Nullable user, NSError * _Nullable error) { - if (user) { - // 试图修改用户名 - [user setObject:@"Jerry" forKey:@"username")]; - // 保存更改 - [user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 可以执行,因为用户已鉴权 +```dart +isAuthenticated +``` - // 绕过鉴权直接获取用户 - LCQuery *query = [TDSUser query]; - [query getObjectInBackgroundWithId:user.objectId block:^(LCObject *unauthenticatedUser, NSError *error) { - [unauthenticatedUser setObject:@"Toodle" forKey:@"username"]; - [unauthenticatedUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 无法执行,因为用户未鉴权 - } else { - // 操作失败 - } - }]; - }]; - } else { - // 错误处理 - } - }]; - } else { - // 错误处理 - } -}]; +```js +isAuthenticated; ``` -```cpp -FTDSUser::LoginWithTapTap({TUType::PermissionScope::Profile, TUType::PermissionScope::Friend}, - FTDSUser::FCallBackDelegate::CreateLambda( - [](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - // 试图修改用户名 - UserPtr->SetUsername("Jerry"); - // 保存更改 - UserPtr->Save(FTDSUser::FCallBackDelegate::CreateLambda( - [](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - // 保存成功 Error.msg; - } - else { - // 保存失败 Error.msg; - } - })); - } - else { - // 登录失败 Error.msg; - } - })); +```python +is_authenticated ``` - +```php +isAuthenticated +``` + +```go +// 暂不支持 +``` -通过 `TDSUser.GetCurrent()` 获取的 `LCUser` 总是经过鉴权的。 + -要查看一个 `TDSUser` 是否经过鉴权,可以调用 `isAuthenticated` 方法。通过经过鉴权的方法获取到的 `TDSUser` 无需进行该检查。 +注意,用户的密码只能在注册的时候进行设置,日后如需修改,只能通过 [重置密码](#重置密码) 的方式进行。密码不会被缓存在本地。如果尝试直接获取已登录用户的密码,会得到 `null`。 ## 其他对象的安全 @@ -1023,40 +2292,39 @@ FTDSUser::LoginWithTapTap({TUType::PermissionScope::Profile, TUType::PermissionS ## 第三方账户登录 -我们在登录功能的开发指南中已经介绍了如何[一键完成 TapTap 登录](/sdk/taptap-login/guide/start#一键完成-taptap-登录)。 - -其实除了 TapTap 外,我们也支持直接使用第三方社交平台(例如 Apple、微信、QQFacebook 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。 - -事实上,TapSDK 采用了开放的接口设计,平台标识和唯一授权信息都由开发者指定,所以是可以兼容所有的第三方账户登录需求的。例如,海外开发者拿到 Facebook 授权信息之后,一样可以调用 `TDSUser.loginWithAuthData` 接口完成玩家账户的登录(平台名字可指定为 `facebook`)。 +云服务支持应用层直接使用第三方社交平台(例如微信、微博、QQ 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。 例如以下的代码展示了终端用户使用微信登录的处理流程: - + ```cs Dictionary thirdPartyData = new Dictionary { - // 可选参数 + // 必须 { "openid", "OPENID" }, { "access_token", "ACCESS_TOKEN" }, { "expires_in", 7200 }, + + // 可选 { "refresh_token", "REFRESH_TOKEN" }, { "scope", "SCOPE" } }; -TDSUser currentUser = await TDSUser.LoginWithAuthData(thirdPartyData, "weixin"); +LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin"); ``` ```java Map thirdPartyData = new HashMap(); -// 可选参数 +// 必须 thirdPartyData.put("expires_in", 7200); thirdPartyData.put("openid", "OPENID"); thirdPartyData.put("access_token", "ACCESS_TOKEN"); +// 可选 thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); thirdPartyData.put("scope", "SCOPE"); -TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "weixin").subscribe(new Observer() { +LCUser.loginWithAuthData(thirdPartyData, "weixin").subscribe(new Observer() { public void onSubscribe(Disposable disposable) { } - public void onNext(TDSUser user) { + public void onNext(LCUser user) { System.out.println("成功登录"); } public void onError(Throwable throwable) { @@ -1069,40 +2337,94 @@ TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "weixin").subscribe(new ```objc NSDictionary *thirdPartyData = @{ - // 可选参数 + // 必须 @"openid":@"OPENID", @"access_token":@"ACCESS_TOKEN", @"expires_in":@7200, + + // 可选 @"refresh_token":@"REFRESH_TOKEN", @"scope":@"SCOPE", }; -TDSUser *user = [TDSUser user]; +LCUser *user = [LCUser user]; LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; option.platform = LeanCloudSocialPlatformWeiXin; [user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { if (succeeded) { NSLog(@"登录成功"); - } else { + }else{ NSLog(@"登录失败:%@",error.localizedFailureReason); } }]; ``` -```cpp -TSharedPtr ThirdPartyData = MakeShared(); -// 可选参数 -ThirdPartyData->SetNumberField("expires_in", 7200); -ThirdPartyData->SetStringField("openid", "OPENID"); -ThirdPartyData->SetStringField("access_token", "ACCESS_TOKEN"); -ThirdPartyData->SetStringField("refresh_token", "REFRESH_TOKEN"); -ThirdPartyData->SetStringField("scope", "SCOPE"); -FTDSUser::LoginWithAuthData("weixin", ThirdPartyData, FTDSUser::FCallBackDelegate::CreateLambda([](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - // 登录成功 - } else { - // 登录失败 Error.msg; +```swift +let thirdPartyData: [String: Any] = [ + // 必须 + "openid": "OPENID", + "access_token": "ACCESS_TOKEN", + "expires_in": 7200, + + // 可选 + "refresh_token": "REFRESH_TOKEN", + "scope": "SCOPE" +] +let user = LCUser() +user.logIn(authData: thirdPartyData, platform: .weixin) { (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) } -})); +} +``` + +```dart +var thirdPartyData = { + // 必须 + 'openid': 'OPENID', + 'access_token': 'ACCESS_TOKEN', + 'expires_in': 7200, + + // 可选 + 'refresh_token': 'REFRESH_TOKEN', + 'scope': 'SCOPE' +}; +LCUser currentUser = await LCUser.loginWithAuthData(thirdPartyData, 'weixin'); +``` + +```js +const thirdPartyData = { + // 必须 + openid: "OPENID", + access_token: "ACCESS_TOKEN", + expires_in: 7200, + + // 可选 + refresh_token: "REFRESH_TOKEN", + scope: "SCOPE", +}; +AV.User.loginWithAuthData(thirdPartyData, "weixin").then( + (user) => { + // 登录成功 + }, + (error) => { + // 登录失败 + } +); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 ``` @@ -1157,8 +2479,8 @@ FTDSUser::LoginWithAuthData("weixin", ThirdPartyData, FTDSUser::FCallBackDelegat - **`lc_apple`**:只有 platform 为 `lc_apple` 时,云服务才会执行 `identity_token` 和 `code` 的逻辑。 - **`uid`**:必填。云服务通过 `uid` 判断是否存在用户。 -- **`identity_token`**:可选。`authData` 中有 `identity_token` 时云端会自动校验 `identity_token` 的有效性。开发者需要在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 -- **`code`**:可选。`authData` 中有 `code` 时云端会自动用该 `code` 向 Apple 换取 `access_token` 和 `refresh_token`。开发者需要在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 +- **`identity_token`**:可选。`authData` 中有 `identity_token` 时云端会自动校验 `identity_token` 的有效性。开发者需要在 **云服务控制台 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 +- **`code`**:可选。`authData` 中有 `code` 时云端会自动用该 `code` 向 Apple 换取 `access_token` 和 `refresh_token`。开发者需要在 **云服务控制台 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 #### 获取 Client ID @@ -1166,7 +2488,7 @@ Client ID 用于校验 `identity_token` 及获取 `access_token`,指的是 App #### 获取 Private Key 及 Private Key ID -Private Key 用于获取 `access_token`。登录 Apple 开发者平台,在左侧的「Certificates, Identifiers & Profiles」中选择「Keys」,添加一个用于 Apple Sign In 的 Private Key,下载 `.p8` 文件,同时在下载 Key 的页面获得 Private Key ID。详情请参考 [Apple 的文档](https://developer.apple.com/cn/help/account/)。 +Private Key 用于获取 `access_token`。登录 Apple 开发者平台,在左侧的「Certificates, Identifiers & Profiles」中选择「Keys」,添加一个用于 Apple Sign In 的 Private Key,下载 `.p8` 文件,同时在下载 Key 的页面获得 Private Key ID。详情请参考 [Apple 的文档](https://help.apple.com/developer-account/#/dev77c875b7e)。 将 Key ID 填写到控制台,将下载下来的 Private Key 文件上传到控制台。控制台只能上传 Private Key 文件,无法查看及下载其内容。 @@ -1178,16 +2500,18 @@ Team ID 用于获取 `access_token`。登录 Apple 开发者平台,在右上 在控制台填写完成所有信息后,使用以下代码登录。 - + ```cs -// 使用 Apple 登录时,uid、identity_token、code 都必传; Dictionary appleAuthData = new Dictionary { + // 必须 { "uid", "USER IDENTIFIER" }, + + // 可选 { "identity_token", "IDENTITY TOKEN" }, { "code", "AUTHORIZATION CODE" } }; -TDSUser currentUser = await TDSUser.LoginWithAuthData(appleAuthData, "lc_apple"); +LCUser currentUser = await LCUser.LoginWithAuthData(appleAuthData, "lc_apple"); ``` ```java @@ -1195,13 +2519,14 @@ TDSUser currentUser = await TDSUser.LoginWithAuthData(appleAuthData, "lc_apple") ``` ```objc -// 使用 Apple 登录时,uid、identity_token、code 都必传; NSDictionary *appleAuthData = @{ + // 必须 @"uid":@"USER IDENTIFIER", + // 可选 @"identity_token":@"IDENTITY TOKEN", @"code":@"AUTHORIZATION CODE", }; -TDSUser *user = [TDSUser user]; +LCUser *user = [LCUser user]; [user loginWithAuthData:appleAuthData platformId:"lc_apple" options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { if (succeeded) { NSLog(@"登录成功"); @@ -1211,18 +2536,51 @@ TDSUser *user = [TDSUser user]; }]; ``` -```cpp -TSharedPtr AppleAuthData = MakeShared(); -AppleAuthData->SetStringField("uid", "USER IDENTIFIER"); -AppleAuthData->SetStringField("identity_token", "IDENTITY TOKEN"); -AppleAuthData->SetStringField("code", "AUTHORIZATION CODE"); -FTDSUser::LoginWithAuthData("lc_apple", AppleAuthData, FTDSUser::FCallBackDelegate::CreateLambda([](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - // 登录成功 - } else { - // 登录失败 Error.msg; +```swift +let appleData: [String: Any] = [ + // 必须 + "uid": "USER IDENTIFIER", + // 可选 + "identity_token": "IDENTITY TOKEN", + "code": "AUTHORIZATION CODE" +] +let user = LCUser() +user.logIn(authData: appleData, platform: .apple) { (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) } -})); +} + +``` + +```dart +var appleData = { + // 必须 + "uid": "USER IDENTIFIER", + // 可选 + "identity_token": "IDENTITY TOKEN", + "code": "AUTHORIZATION CODE" +}; +LCUser currentUser = await LCUser.loginWithAuthData(appleData, 'lc_apple'); +``` + +```js +// 不支持 +``` + +```python +# 不支持 +``` + +```php +// 不支持 +``` + +```go +// 不支持 ``` @@ -1231,8 +2589,6 @@ FTDSUser::LoginWithAuthData("lc_apple", AppleAuthData, FTDSUser::FCallBackDelega 每个用户的 `authData` 是一个以平台名为键名,鉴权信息为键值的 JSON 对象。 - - 一个关联了微信账户的用户应该会有下列对象作为 `authData`: ```json @@ -1280,8 +2636,6 @@ FTDSUser::LoginWithAuthData("lc_apple", AppleAuthData, FTDSUser::FCallBackDelega } ``` - - 理解 `authData` 的数据结构至关重要。一个终端用户通过如下的鉴权信息来登录的时候, ```json @@ -1297,28 +2651,28 @@ FTDSUser::LoginWithAuthData("lc_apple", AppleAuthData, FTDSUser::FCallBackDelega 云端首先会查找账户系统,看看是否存在 `authData.platform.openid` 等于 `OPENID` 的账户,如果存在,则返回现有账户,如果不存在那么就创建一个新账户,同时将上面的鉴权信息写入新账户的 `authData` 属性中,并将新账户的数据当成结果返回。 云端会自动为每个用户的 `authData..` 创建唯一索引,从而避免重复数据。 -`` 在微信等部分云服务内建支持的第三方平台上为 `openid` 字段,在其他第三方平台(包括部分云服务专门支持的第三方平台和所有云服务没有专门支持的第三方平台)上为 `uid` 字段。 +`` 在微信等部分云服务内建支持的第三方平台上为 `openid` 字段,在其他第三方平台(包括部分云服务专门支持的第三方平台和所有云服务没有专门支持的第三方平台)上为 `uid` 字段。 ### 自动验证第三方平台授权信息 为了确保账户数据的有效性,云端还支持对部分平台的 Access Token 的有效性进行自动验证,以防止伪造账户数据。如果有效性验证不通过,云端会返回 `invalid authData` 错误,关联不会被建立。对于云端无法识别的服务,开发者需要自己去验证 Access Token 的有效性。 比如,注册、登录时分别通过云引擎的 `beforeSave` hook、`beforeUpdate` hook 来验证 Access Token 有效性。 -如果希望使用这一功能,则在开始使用前,需要在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置** 配置相应平台的 **应用 ID** 和 **应用 Secret Key**。 +如果希望使用这一功能,则在开始使用前,需要在 **云服务控制台 > 内建账户 > 设置** 配置相应平台的 **应用 ID** 和 **应用 Secret Key**。 -如果不希望云端自动验证 Access Token,可以在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 内建账户 > 设置** 里面取消勾选 **第三方登录时,验证用户 AccessToken 合法性**。 +如果不希望云端自动验证 Access Token,可以在 **云服务控制台 > 内建账户 > 设置** 里面取消勾选 **第三方登录时,验证用户 AccessToken 合法性**。 -配置平台账号的目的在于创建 `TDSUser` 时,云端会使用相关信息去校验请求参数 `thirdPartyData` 的合法性,确保 `TDSUser` 实际对应着一个合法真实的用户,确保平台安全性。 +配置平台账号的目的在于创建用户对象时,云端会使用相关信息去校验请求参数 `thirdPartyData` 的合法性,确保用户对象实际对应着一个合法真实的用户,确保平台安全性。 ### 绑定第三方账户 -如果用户已经登录,也可以在当前账户上绑定或解绑更多第三方平台信息。例如,先用游客身份登录,再绑定 TapTap 或其他第三方账号,那么以后通过 TapTap 或其他第三方授权登录,都可以得到完全相同的账号。 +如果用户已经登录,也可以在当前账户上绑定或解绑更多第三方平台信息。 -绑定成功后,新的第三方账户信息会被添加到 `TDSUser` 的 `authData` 字段里。 +绑定成功后,新的第三方账户信息会被添加到用户对象的 `authData` 字段里。 例如,下面的代码可以关联微信账户: - + ```cs await currentUser.AssociateAuthData(weixinData, "weixin"); @@ -1335,7 +2689,247 @@ user.associateWithAuthData(weixinData, "weixin").subscribe(new Observer( } @Override public void onError(Throwable e) { - System.out.println("绑定失败:" + e.getMessage()); + System.out.println("绑定失败:" + e.getMessage()); + } + @Override + public void onComplete() { + } +}); +``` + +```objc +[user associateWithAuthData:weixinData platformId:LeanCloudSocialPlatformWeiXin options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + NSLog(@"成功"); + } else{ + NSLog(@"失败:%@",error.localizedFailureReason); + } +}]; +``` + +```swift +currentUser.associate(authData: weixinData, platform: .weixin) { (result) in + switch result { + case .success: + // 关联成功 + case .failure(error: let error): + // 关联失败 + } +} +``` + +```dart +await currentUser.associateAuthData(weixinData, 'weixin'); +``` + +```js +user + .associateWithAuthData(weixinData, "weixin") + .then(function (user) { + // 成功绑定 + }) + .catch(function (error) { + console.error("error: ", error); + }); +``` + +```python +user.link_with("weixin", weixin_data) +``` + +```php +$user->linkWith("weixin", $weixinData); +``` + +```go +// 暂不支持 +``` + + + +为节省篇幅,上面的代码示例中没有给出具体的平台授权信息,相关内容请参考上面的[「第三方账户登录」](#第三方账户登录)一节。 + +### 解除与第三方账户的关联 + +类似地,可以解绑第三方账户。 + +例如,下面的代码可以解除用户和微信账户的关联: + + + +```cs +LCUser currentUser = await LCUser.GetCurrent(); +await currentUser.DisassociateWithAuthData("weixin"); +``` + +```java +LCUser user = LCUser.currentUser(); +user.dissociateWithAuthData("weixin").subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + } + @Override + public void onNext(LCUser user) { + System.out.println("解绑成功"); + } + @Override + public void onError(Throwable e) { + System.out.println("解绑失败:" + e.getMessage()); + } + @Override + public void onComplete() { + } +}); +``` + +```objc +[user disassociateWithPlatform:LeanCloudSocialPlatformWeiXin callback:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + NSLog(@"成功"); + } else{ + NSLog(@"失败:%@",error.localizedFailureReason); + } +}]; +``` + +```swift +currentUser.disassociate(authData: .weixin) { (result) in + switch result { + case .success: + // 解除关联成功 + case .failure(error: let error): + // 解除关联失败 + } +} +``` + +```dart +LCUser currentUser = await LCUser.getCurrent(); +await currentUser.disassociateWithAuthData('weixin'); +``` + +```js +user.dissociateAuthData("weixin").then( + (s) => { + // 解除关联成功 + }, + (error) => { + // 解除关联失败 + } +); +``` + +```python +user.unlink_from("weixin") +``` + +```php +$user->unlinkWith("weixin"); +``` + +```go +// 暂不支持 +``` + + + +
    + +扩展:第三方登录时补充完整的用户信息 + +有些产品,新用户在使用第三方账号授权拿到相关信息后,仍然需要补充设置用户名、手机号、密码等重要信息后,才被允许登录成功。 + +这时要使用 `loginWithauthData` 登录接口的 `failOnNotExist` 参数并将其设置为 `true`。服务端会判断是否已存在能匹配上的 `authData`,如果不存在则会返回 `211` 错误码和 `Could not find user` 报错信息。开发者根据这个 `211` 错误码,跳转到要求输入用户名、密码、手机号等信息的页面,实例化一个用户对象,保存上述补充数据,再次调用 `loginWithauthData` 接口进行登录,并 **不再传入 `failOnNotExist` 参数**。示例代码如下: + + + +```cs +try { + Dictionary thirdPartyData = new Dictionary { + // 必须 + { "openid", "OPENID" }, + { "access_token", "ACCESS_TOKEN" }, + { "expires_in", 7200 }, + + // 可选 + { "refresh_token", "REFRESH_TOKEN" }, + { "scope", "SCOPE" } + }; + LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); + option.FailOnNotExist = true; + LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); +} catch (LCException e) { + if (e.code == 211) { + // 不存在 authData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 + } +} + +// 跳转到输入用户名、密码、手机号等业务页面之后 +Dictionary thirdPartyData = new Dictionary { + { "expires_in", 7200 }, + { "openid", "OPENID" }, + { "access_token", "ACCESS_TOKEN" } +}; +try { + LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); + option.FailOnNotExist = true; + LCUser user = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); + user.Username = "Tom"; + user.Mobile = "+8618200008888"; + await user.Save(); +} catch (LCException e) { + //其他报错信息 +} +``` + +```java +Map thirdPartyData = new HashMap(); +thirdPartyData.put("expires_in", 7200); +thirdPartyData.put("openid", "OPENID"); +thirdPartyData.put("access_token", "ACCESS_TOKEN"); +thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); +thirdPartyData.put("scope", "SCOPE"); +Boolean failOnNotExist = true; +LCUser user = new LCUser(); +user.loginWithAuthData(thirdPartyData, "weixin", failOnNotExist).subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + } + @Override + public void onNext(LCUser user) { + System.out.println("存在匹配的用户,登录成功"); + } + @Override + public void onError(Throwable e) { + LCException avException = new LCException(e); + int code = avException.getCode(); + if (code == 211){ + // 跳转到输入用户名、密码、手机号等业务页面 + } else { + System.out.println("发生错误:" + e.getMessage()); + } + } + @Override + public void onComplete() { + } +}); + +// 跳转到输入用户名、密码、手机号等业务页面之后 +LCUser user = new LCUser(); +user.setUsername("Tom"); +user.setMobilePhoneNumber("+8618200008888"); +Boolean failOnNotExist = false; +user.loginWithAuthData(thirdPartyData, "weixin", failOnNotExist).subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + } + @Override + public void onNext(LCUser user) { + System.out.println("登录成功"); + } + @Override + public void onError(Throwable e) { + System.out.println("登录失败:" + e.getMessage()); } @Override public void onComplete() { @@ -1344,88 +2938,164 @@ user.associateWithAuthData(weixinData, "weixin").subscribe(new Observer( ``` ```objc -[user associateWithAuthData:weixinData platformId:LeanCloudSocialPlatformWeiXin options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { +NSDictionary *thirdPartyData = @{ + @"access_token":@"ACCESS_TOKEN", + @"expires_in":@7200, + @"refresh_token":@"REFRESH_TOKEN", + @"openid":@"OPENID", + @"scope":@"SCOPE", + }; +LCUser *user = [LCUser user]; +LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; +option.platform = LeanCloudSocialPlatformWeiXin; +option.failOnNotExist = true; +[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { if (succeeded) { - NSLog(@"成功"); - } else{ - NSLog(@"失败:%@",error.localizedFailureReason); + // 你的逻辑 + } else if ([error.domain isEqualToString:kLeanCloudErrorDomain] && error.code == 211) { + // 不存在 thirdPartyData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 } }]; -``` -```cpp -User->AssociateWithAuthData("weixin", AuthData, FTDSUser::FCallBackDelegate::CreateLambda([](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - // 绑定成功 - } - else { - // 绑定失败 Error.msg Error.msg; +// 跳转到输入用户名、密码、手机号等业务页面之后 +LCUser *user = [LCUser user]; +user.username = @"Tom"; +user.mobilePhoneNumber = @"+8618200008888"; +LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; +option.platform = LeanCloudSocialPlatformWeiXin; +[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + NSLog(@"登录成功"); + }else{ + NSLog(@"登录失败:%@",error.localizedFailureReason); } -})); +}]; ``` - - -为节省篇幅,上面的代码示例中没有给出具体的平台授权信息,相关内容请参考上面的[「第三方账户登录」](#第三方账户登录)一节。 - -### 解除与第三方账户的关联 - -类似地,可以解绑第三方账户。 +```swift +let thirdPartyData: [String: Any] = [ + "access_token": "ACCESS_TOKEN", + "expires_in": 7200, + "refresh_token": "REFRESH_TOKEN", + "openid": "OPENID", + "scope": "SCOPE" +] +let user = LCUser() +user.logIn(authData: thirdPartyData, platform: .weixin, options: [.failOnNotExist]) { (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + if error.code == 211 { + // 不存在绑定了当前 authData 的 User 的实例 + // 跳转到输入用户名、密码、手机号等业务页面 + let user = LCUser() + user.username = "Tom" + user.password = "cat!@#123" + user.mobilePhoneNumber = "+8618200008888" + user.logIn(authData: thirdPartyData, platform: .weixin, completion: { (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) + } + }) + } + } +} +``` -例如,下面的代码可以解除用户和微信账户的关联: +```dart +try { + Map thirdPartyData = { + // 必须 + 'openid': 'OPENID', + 'access_token': 'ACCESS_TOKEN', + 'expires_in': 7200, + + // 可选 + 'refresh_token': 'REFRESH_TOKEN', + 'scope': 'SCOPE' + }; + LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); + option.failOnNotExist = true; + LCUser currentUser = await LCUser.loginWithAuthData(thirdPartyData, 'weixin', option: option); +} on LCException catch (e) { + if (e.code == 211) { + // 不存在 authData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 + } +} - +// 跳转到输入用户名、密码、手机号等业务页面之后 +Map thirdPartyData = { + 'expires_in': 7200, + 'openid': 'OPENID', + 'access_token': 'ACCESS_TOKEN' +}; +try { + LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); + option.failOnNotExist = true; + LCUser user = await LCUser.loginWithAuthData(thirdPartyData, 'weixin', option: option); + user.username = 'Tome'; + user.mobile = '+8618200008888'; + await user.save(); +} on LCException catch (e) { + //其他报错信息 +} +``` -```cs -TDSUser currentUser = await TDSUser.GetCurrent(); -await currentUser.DisassociateWithAuthData("weixin"); +```js +const thirdPartyData = { + access_token: "ACCESS_TOKEN", + expires_in: 7200, + refresh_token: "REFRESH_TOKEN", + openid: "OPENID", + scope: "SCOPE", +}; +AV.User.loginWithAuthData(thirdPartyData, "weixin", { + failOnNotExist: true, +}).then( + (s) => { + // 登录成功 + }, + (error) => { + // 登录失败 + // 检查 error.code == 211,跳转到用户名、手机号等资料的输入页面 + } +); + +const user = new AV.User(); +// 设置用户名 +user.setUsername("Tom"); +// 设置密码 +user.setMobilePhoneNumber("+8618200008888"); +user.setPassword("cat!@#123"); +// 设置邮箱 +user.setEmail("tom@leancloud.rocks"); +user.loginWithAuthData(thirdPartyData, "weixin").then( + (loggedInUser) => { + console.log(loggedInUser); + }, + (error) => {} +); ``` -```java -TDSUser user = TDSUser.currentUser(); -user.dissociateWithAuthData("weixin").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("解绑成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("解绑失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); +```python +# 暂不支持 ``` -```objc -[user disassociateWithPlatform:LeanCloudSocialPlatformWeiXin callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"成功"); - } else{ - NSLog(@"失败:%@",error.localizedFailureReason); - } -}]; +```php +// 暂不支持 ``` -```cpp -User->DisassociateAuthData("weixin", FTDSUser::FCallBackDelegate::CreateLambda( - [](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - // 成功 - } - else { - // 失败 Error.msg; - } - })); +```go +// 暂不支持 ``` - +
    @@ -1485,7 +3155,7 @@ Dictionary thirdPartyData = new Dictionary { LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); option.AsMainAccount = true; option.UnionIdPlatform = "weixin"; -TDSUser currentUser = await TDSUser.LoginWithAuthDataAndUnionId( +LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( thirdPartyData, "wxleanoffice", "unionid4a", option: option); ``` @@ -1496,15 +3166,15 @@ thirdPartyData.put("expires_in", 1384686496); thirdPartyData.put("uid", "officeopenid"); thirdPartyData.put("access_token", "officetoken"); thirdPartyData.put("scope", "SCOPE"); -TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "wxleanoffice", +LCUser.loginWithAuthData(LCUser.class, thirdPartyData, "wxleanoffice", "unionid4a", "weixin", true) // 新增参数,分别表示 uniondId,unionIdPlatform,asMainAccount // 对于 unionIdPlatform,这里使用「weixin」来指代微信平台。 - .subscribe(new Observer() { + .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { } @Override - public void onNext(TDSUser user) { + public void onNext(LCUser user) { System.out.println("登录成功"); } @Override @@ -1525,7 +3195,7 @@ NSDictionary *thirdPartyData = @{ @"scope":@"SCOPE", @"unionid":@"unionid4a" // 新增属性 }; -TDSUser *currentuser = [TDSUser user]; +LCUser *currentuser = [LCUser user]; LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; option.platform = @"weixin"; // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 option.unionId = thirdPartyData[@"unionid"]; @@ -1539,9 +3209,92 @@ option.isMainAccount = true; }]; ``` +```swift +let thirdPartyData: [String: Any] = [ + "access_token": "officetoken", + "expires_in": 1384686496, + "uid": "officeopenid", + "scope": "SCOPE", + "unionid": "unionid4a" // 新增属性 +] +let user = LCUser() +user.logIn( + authData: thirdPartyData, + platform: .custom("wxleanoffice"), + unionID: thirdPartyData["unionid"] as? String, + unionIDPlatform: .weixin, // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 + options: [.mainAccount]) +{ (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +Map thirdPartyData = { + // 必须 + 'uid': 'officeopenid', + 'access_token': 'officetoken', + 'expires_in': 1384686496, + 'unionId': 'unionid4a', // 新增属性 + + // 可选 + 'refresh_token': '...', + 'scope': 'SCOPE' +}; +LCUserAuthDataLoginOption option = LCUserAuthDataLoginOption(); +option.asMainAccount = true; +option.unionIdPlatform = 'weixin'; +LCUser currentUser = await LCUser.loginWithAuthDataAndUnionId( + thirdPartyData, 'wxleanoffice', 'unionid4a', + option: option); +``` + +```js +const thirdPartyData = { + access_token: "officetoken", + expires_in: 1384686496, + uid: "officeopenid", + scope: "SCOPE", +}; + +AV.User.loginWithAuthDataAndUnionId( + thirdPartyData, + "wxleanoffice", + "unionid4a", // 新增参数 + { + unionIdPlatform: "weixin", // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 + asMainAccount: true, + } +).then( + (user) => { + // 绑定成功 + }, + (error) => { + // 绑定失败 + } +); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + -注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 `authData..uid` 的唯一索引,具体可以参考[数据存储 REST API 使用详解](/sdk/storage/guide/rest/)的《连接用户账户和第三方平台》一节。 +注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 `authData..uid` 的唯一索引,具体可以参考《数据存储 REST API 使用详解》的[《连接用户账户和第三方平台》](/sdk/authentication/rest/#连接用户账户和第三方平台)一节。 如果用户 A 是第一次在「云服务通讯」中通过微信登录,那么 `_User` 表中会增加一个新用户(假设其 `objectId` 为 `ThisIsUserA`),其 `authData` 的结果如下: @@ -1587,7 +3340,7 @@ Dictionary thirdPartyData = new Dictionary { LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); option.AsMainAccount = false; option.UnionIdPlatform = "weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -TDSUser currentUser = await TDSUser.LoginWithAuthDataAndUnionId( +LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( thirdPartyData, "wxleansupport", "unionid4a", option: option); ``` @@ -1598,14 +3351,14 @@ thirdPartyData.put("expires_in", 1384686496); thirdPartyData.put("uid", "supportopenid"); thirdPartyData.put("access_token", "supporttoken"); thirdPartyData.put("scope", "SCOPE"); -TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "wxleansupport", "unionid4a", +LCUser.loginWithAuthData(LCUser.class, thirdPartyData, "wxleansupport", "unionid4a", "weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 - false).subscribe(new Observer() { + false).subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { } @Override - public void onNext(TDSUser user) { + public void onNext(LCUser user) { System.out.println("登录成功"); } @Override @@ -1626,7 +3379,7 @@ NSDictionary *thirdPartyData = @{ @"scope":@"SCOPE", @"unionid":@"unionid4a" }; -TDSUser *currentuser = [TDSUser user]; +LCUser *currentuser = [LCUser user]; LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; option.platform = @"weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 option.unionId = thirdPartyData[@"unionid"]; @@ -1640,6 +3393,89 @@ option.isMainAccount = false; }]; ``` +```swift +let thirdPartyData: [String: Any] = [ + "access_token": "supporttoken", + "expires_in": 1384686496, + "uid": "supportopenid", + "scope": "SCOPE", + "unionid": "unionid4a" +] +let user = LCUser() +user.logIn( + authData: thirdPartyData, + platform: .custom("wxleansupport"), + unionID: thirdPartyData["unionid"] as? String, + unionIDPlatform: .weixin, // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 + options: [.mainAccount]) +{ (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +Map thirdPartyData = { + // 必须 + 'uid': 'supportopenid', + 'access_token': 'supporttoken', + 'expires_in': 1384686496, + 'unionId': 'unionid4a', + + // 可选 + 'refresh_token': '...', + 'scope': 'SCOPE' +}; +LCUserAuthDataLoginOption option = LCUserAuthDataLoginOption(); +option.asMainAccount = false; +option.unionIdPlatform = 'weixin'; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 +LCUser currentUser = await LCUser.loginWithAuthDataAndUnionId( + thirdPartyData, 'wxleansupport', 'unionid4a', + option: option); +``` + +```js +const thirdPartyData = { + access_token: "supporttoken", + expires_in: 1384686496, + uid: "supportopenid", + scope: "SCOPE", +}; + +AV.User.loginWithAuthDataAndUnionId( + thirdPartyData, + "wxleansupport", + "unionid4a", + { + unionIdPlatform: "weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 + asMainAccount: false, + } +).then( + (user) => { + // 绑定成功 + }, + (error) => { + // 绑定失败 + } +); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + 与「云服务通讯」中的登录过程相比,在「云服务技术支持」这个应用中,我们在登录时只是将 `asMainAccount` 设为了 `false`。 这时我们看到,本次登录得到的还是 `objectId` 为 `ThisIsUserA` 的 `_User` 表记录(同一个账户),同时该账户的 `authData` 属性中发生了变化,多了 `wxleansupport` 的数据,如下: @@ -1668,11 +3504,11 @@ option.isMainAccount = false; } ``` -在新的登录方式中,当一个用户以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录得到新的 `TDSUser` 后,接下来这个用户以「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 `wxleansupport` 的新用户数据加入到已有账户的 `authData` 里了,不会再创建新的账户。 +在新的登录方式中,当一个用户以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录得到新的用户对象后,接下来这个用户以「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 `wxleansupport` 的新用户数据加入到已有账户的 `authData` 里了,不会再创建新的账户。 -这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个 `TDSUser` 上,实现互通。 +这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个用户对象上,实现互通。 -#### 为 UnionID 建立索引 +### 为 UnionID 建立索引 云端会为 UnionID 自动建立索引,不过因为自动创建基于请求的抽样统计,可能会滞后。 因此,我们推荐自行创建相关索引,特别是用户量(`_User` 表记录数)很大的应用,更需要预先创建索引,否则用户使用 UnionID 账号登录时可能超时失败。 @@ -1682,7 +3518,7 @@ option.isMainAccount = false; - `authData.wxleansupport.uid` - `authData._weixin_unionid.uid` -#### 该如何指定 unionIdPlatform +### 该如何指定 unionIdPlatform 从上面的例子可以看出,使用 UnionID 登录的时候,需要指定 `unionIdPlatform` 的主要目的,就是为了便于查找已经存在的唯一主账号。云端会在主账号对应账户的 `authData` 属性中增加一个 `_{unionIdPlatform}_unionid` 键值对来标记唯一性,终端用户在其他应用中登录的时候,云端会根据参数中提供的 `uniondId` + `unionIdPlatform` 的组合,在 `_User` 表中进行查找,这样来确定唯一的既存主账号。 @@ -1698,11 +3534,11 @@ option.isMainAccount = false; - 微博平台的多个应用,统一使用 `weibo` 作为 `unionIdPlatform`; - 除此之外的其他平台,开发者可以自行定义 `unionIdPlatform` 的名字,只要自己保证多个应用间统一即可。 -#### 主副应用不同登录顺序出现的不同结果 +### 主副应用不同登录顺序出现的不同结果 上面的流程是用户先登录了「云服务通讯」这个主应用,然后再登录「云服务技术支持」这个副应用,所以账号都被通过 UnionID 有效关联起来了。可能有人会想到另外一个问题,如果用户 B 先登录副应用,后登录主应用,这时候会发生什么事情呢? -用户 B 首先登录副应用的时候,提供了「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,并且指定「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `false`」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个 `TDSUser` 对象,该账户 `authData` 结果为: +用户 B 首先登录副应用的时候,提供了「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,并且指定「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `false`」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个用户对象,该账户 `authData` 结果为: ```json { @@ -1717,7 +3553,7 @@ option.isMainAccount = false; } ``` -用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,以及「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `true`」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个 `TDSUser` 对象,该账户 `authData` 结果为: +用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,以及「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `true`」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个用户对象,该账户 `authData` 结果为: ```json { @@ -1737,7 +3573,7 @@ option.isMainAccount = false; 还有更复杂的情况。如果某公司的产品之前就接入了微信登录,产生了很多存量用户,并且分散在不同的子产品中,这时候怎么办?我们接下来专门讨论此时的解决方案。 -#### 存量账户如何通过 UnionID 实现关联 +### 存量账户如何通过 UnionID 实现关联 还是以我们的两个子产品「云服务通讯」(后续以「产品 1」指代)和「云服务技术支持为例」(后续以「产品 2」指代)为例,在接入 UnionID 之前,我们就接入了之前版本的微信平台登录,这时候账户系统内可能存在多种账户: @@ -1796,4 +3632,221 @@ option.isMainAccount = false;
    - +## 匿名用户 + +将数据与用户关联需要首先创建一个用户,但有时你不希望强制用户在一开始就进行注册。使用匿名用户,可以让应用不提供注册步骤也能创建用户。下面的代码创建一个新的匿名用户: + + + +```cs +await LCUser.LoginAnonymously(); +``` + +```java +LCUser.logInAnonymously().subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // user 是新的匿名用户 + } + public void onError(Throwable throwable) {} + public void onComplete() {} +}); +``` + +```objc +[LCUser loginAnonymouslyWithCallback:^(LCUser *user, NSError *error) { + // user 是新的匿名用户 +}]; +``` + +```swift +// 暂不支持 +``` + +```dart +await LCUser.loginAnonymously(); +``` + +```js +AV.User.loginAnonymously().then((user) => { + // user 是新的匿名用户 +}); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + + + +可以像给普通用户设置属性那样给匿名用户设置 `username`、`password`、`email` 等属性,还可以通过走正常的注册流程来将匿名用户转化为普通用户。匿名用户能够: + +- [使用用户名和密码注册](#注册) +- [关联第三方平台](#第三方账户登录),比如微信 + +下面的代码为一名匿名用户设置用户名和密码: + + + +```cs +LCUser currentUser = await LCUser.LoginAnonymously(); +currentUser.Username = "Tom"; +currentUser.Password = "cat!@#123"; + +await currentUser.SignUp(); +``` + +```java +// currentUser 是个匿名用户 +LCUser currentUser = LCUser.getCurrentUser(); + +currentUser.setUsername("Tom"); +currentUser.setPassword("cat!@#123"); + +currentUser.signUpInBackground().subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // currentUser 已经转化为普通用户 + } + public void onError(Throwable throwable) { + // 注册失败(通常是因为用户名已被使用) + } + public void onComplete() {} +}); +``` + +```objc +// currentUser 是个匿名用户 +LCUser *currentUser = [LCUser currentUser]; + +user.username = @"Tom"; +user.password = @"cat!@#123"; + +[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + if (succeeded) { + // currentUser 已经转化为普通用户 + } else { + // 注册失败(通常是因为用户名已被使用) + } +}]; +``` + +```swift +// 暂不支持 +``` + +```dart +LCUser currentUser = await LCUser.loginAnonymously(); +currentUser.username = 'Tom'; +currentUser.password = 'cat!@#123'; + +await currentUser.signUp(); +``` + +```js +// currentUser 是个匿名用户 +const currentUser = AV.User.current(); + +user.setUsername("Tom"); +user.setPassword("cat!@#123"); + +user.signUp().then( + (user) => { + // currentUser 已经转化为普通用户 + }, + (error) => { + // 注册失败(通常是因为用户名已被使用) + } +); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + + + +下面的代码检查当前用户是否为匿名用户: + + + +```cs +LCUser currentUser = await LCUser.GetCurrent(); +if (currentUser.IsAnonymous) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```java +LCUser currentUser = LCUser.getCurrentUser(); +if (currentUser.isAnonymous()) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```objc +LCUser *currentUser = [LCUser currentUser]; +if (currentUser.isAnonymous) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```swift +// 暂不支持 +``` + +```dart +LCUser currentUser = await LCUser.getCurrent(); +if (currentUser.isAnonymous) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```js +const currentUser = AV.User.current(); +if (currentUser.isAnonymous()) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + + + +如果匿名用户未能在登出前转化为普通用户,那么该用户将无法再次登录同一账户,且之前产生的数据也无法被取回。 diff --git a/docs/sdk/authentication/rest.mdx b/docs/sdk/authentication/rest.mdx index c15af7874..f8005f582 100644 --- a/docs/sdk/authentication/rest.mdx +++ b/docs/sdk/authentication/rest.mdx @@ -1,28 +1,119 @@ --- title: 内建账户 REST API sidebar_label: REST API -sidebar_position: 4 +sidebar_position: 3 --- :::tip -请参考 数据存储 REST API 文档中关于 [Base URL](/sdk/storage/guide/rest/#base-url)、[请求格式](/sdk/storage/guide/rest/#请求格式)、[响应格式](/sdk/storage/guide/rest/#响应格式)的说明。 +请参考 数据存储 REST API 文档中关于 [Base URL](sdk/storage/guide/rest/#base-url)、[请求格式](/sdk/storage/guide/rest/#请求格式)、[响应格式](/sdk/storage/guide/rest/#响应格式)的说明。 ::: -| URL | HTTP | 功能 | -| ----------------------------------------------- | ------ | ------------------------------------ | -| /1.1/users | POST | 用户注册
    用户连接 | -| /1.1/usersByMobilePhone | POST | 使用手机号码注册或登录 | -| /1.1/login | POST | 用户登录 | -| /1.1/users/<objectId> | GET | 获取用户 | -| /1.1/users/me | GET | 根据 sessionToken 获取用户信息 | -| /1.1/users/<objectId>/refreshSessionToken | PUT | 重置用户 sessionToken | -| /1.1/users/<objectId>/updatePassword | PUT | 更新密码,要求输入旧密码 | -| /1.1/users/<objectId> | PUT | 更新用户
    用户连接
    验证 Email | -| /1.1/users | GET | 查询用户 | -| /1.1/users/<objectId> | DELETE | 删除用户 | -| /1.1/requestPasswordReset | POST | 请求密码重设 | -| /1.1/requestEmailVerify | POST | 请求验证用户邮箱 | - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    URLHTTP功能
    /1.1/usersPOST用户注册
    用户连接
    /1.1/usersByMobilePhonePOST使用手机号码注册或登录
    /1.1/loginPOST用户登录
    /1.1/users/<objectId>GET获取用户
    /1.1/users/meGET根据 sessionToken 获取用户信息
    /1.1/users/<objectId>/refreshSessionTokenPUT重置用户 sessionToken。
    /1.1/users/<objectId>/updatePasswordPUT更新密码,要求输入旧密码。
    /1.1/users/<objectId>PUT更新用户
    用户连接
    验证Email
    /1.1/usersGET查询用户
    /1.1/users/<objectId>DELETE删除用户
    /1.1/requestPasswordResetPOST请求密码重设
    /1.1/requestEmailVerifyPOST请求验证用户邮箱
    /1.1/requestMobilePhoneVerifyPOST请求发送用户手机号码验证短信
    /1.1/verifyMobilePhone/<code>POST使用"验证码"验证用户手机号码
    /1.1/requestChangePhoneNumberPOST请求发送手机短信验证码以绑定或更新手机号。
    /1.1/changePhoneNumberPOST验证手机短信验证码并绑定或更新手机号。
    /1.1/requestLoginSmsCodePOST请求发送手机号码登录短信。
    /1.1/requestPasswordResetBySmsCodePOST请求发送手机短信验证码重置用户密码。
    /1.1/resetPasswordBySmsCode/<code>PUT验证手机短信验证码并重置密码。
    不仅在移动应用上,还在其他系统中,很多应用都有一个统一的登录流程。通过 REST API 访问用户的账户让你可以简单实现这一功能。 @@ -33,13 +124,13 @@ sidebar_position: 4 注册一个新用户与创建一个新的普通对象之间的不同点在于 username 和 password 字段都是必需的。password 字段会以和其他的字段不一样的方式处理,它在储存时会被加密而且永远不会被返回给任何来自客户端的请求。 -你可以让云服务自动验证邮件地址,做法是进入 **云服务控制台 > 存储 > 设置 > 用户账号**,勾选 **用户注册时,发送验证邮件**。 +你可以让云服务自动验证邮件地址,做法是进入 **云服务控制台 > 内建账户 > 设置**,勾选 **启用邮箱验证功能**。 这项设置启用了的话,所有填写了 email 的用户在注册时都会产生一个 email 验证地址,并发回到用户邮箱,用户打开邮箱点击了验证链接之后,用户表里 `emailVerified` 属性值会被设为 true。你可以在 `emailVerified` 字段上查看用户的 email 是否已经通过验证。 -你还可以在 **云服务控制台 > 存储 > 设置 > 用户账号**,勾选**未验证邮箱的用户,禁止登录**。 +你还可以在 **云服务控制台 > 内建账户 > 设置**,勾选**未验证邮箱的用户,禁止登录**。 -为了注册一个新的用户,需要向 user 路径发送一个 POST 请求,你可以加入一个新的字段,例如,创建一个新的用户有一个电话号码: +为了注册一个新的用户,需要向 user 路径发送一个 POST 请求,你可以加入一个新的字段,例如,创建一个新的用户有一个电话号码: ```sh curl -X POST \ @@ -47,14 +138,14 @@ curl -X POST \ -H "X-LC-Key: {{appkey}}" \ -H "Content-Type: application/json" \ -d '{"username":"tom","password":"f32@ds*@&dsa","phone":"18612340000"}' \ - https://{{host}}/1.1/users + https://API_BASE_URL/1.1/users ``` -当创建成功时,HTTP 返回为 201 Created,Location 头包含了新用户的 URL: +当创建成功时,HTTP返回为 201 Created,Location 头包含了新用户的 URL: ```sh Status: 201 Created -Location: https://{{host}}/1.1/users/55a47496e4b05001a7732c5f +Location: https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f ``` 返回的主体是一个 JSON 对象,包含 objectId、createdAt 时间戳表示创建对象时间,sessionToken 可以被用来认证这名用户随后的请求: @@ -77,44 +168,43 @@ curl -X POST \ -H "X-LC-Id: {{appid}}" \ -H "X-LC-Key: {{appkey}}" \ -d '{"username":"tom","password":"f32@ds*@&dsa"}' \ -https://{{host}}/1.1/login +https://API_BASE_URL/1.1/login ``` 用户也可以通过邮箱地址和密码登录,只需将 body 中的 username 换成 email: ```json -{ "email": "tom@example.com", "password": "f32@ds*@&dsa" } +{"email":"tom@example.com","password":"f32@ds*@&dsa"} ``` 类似地,将 `username` 换成 `mobilePhoneNumber` 可以通过手机号和密码登录: ```json -{ "mobilePhoneNumber": "+86186xxxxxxxx", "password": "f32@ds*@&dsa" } +{"mobilePhoneNumber":"+86186xxxxxxxx","password":"f32@ds*@&dsa"} ``` 返回的主体是一个 JSON 对象包括所有除了 password 以外的自定义字段。它同样包含了 createdAt、updateAt、objectId 和 sessionToken 字段。 ```json { - "sessionToken": "qmdj8pdidnmyzp0c7yqil91oc", - "updatedAt": "2015-07-14T02:31:50.100Z", - "phone": "18612340000", - "objectId": "55a47496e4b05001a7732c5f", - "username": "tom", - "createdAt": "2015-07-14T02:31:50.100Z", - "emailVerified": false, - "mobilePhoneVerified": false + "sessionToken":"qmdj8pdidnmyzp0c7yqil91oc", + "updatedAt":"2015-07-14T02:31:50.100Z", + "phone":"18612340000", + "objectId":"55a47496e4b05001a7732c5f", + "username":"tom", + "createdAt":"2015-07-14T02:31:50.100Z", + "emailVerified":false, + "mobilePhoneVerified":false } ``` 可以将 sessionToken 理解为用户的登录凭证,每个用户的 sessionToken 在同一个应用内都是唯一的, 类似于 Cookie 的概念。 -SDK 在客户端会一直缓存 sessionToken,直到用户登出或重新登录。 正常情况下,用户的 sessionToken 是固定不变的,但在以下情况下会发生改变: -- 客户端调用了忘记密码功能,重设了密码。 -- 开发者在 **云服务控制台 > 存储 > 设置 > 用户账号** 中勾选了 **密码修改后,强制客户端重新登录**,那么在修改密码后 sessionToken 也将强制更换。 -- 调用 [`refreshSessionToken`](#重置登录_sessionToken) 主动重置。 +* 客户端调用了忘记密码功能,重设了密码。 +* 开发者在 **云服务控制台 > 内建账户 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么在修改密码后 sessionToken 也将强制更换。 +* 调用 [`refreshSessionToken`](#重置登录_sessionToken) 主动重置。 在 sessionToken 变化后,已有的登录如果调用到用户相关权限受限的 API,将返回 403 权限错误。 @@ -127,22 +217,22 @@ curl -X PUT \ -H "X-LC-Id: {{appid}}" \ -H "X-LC-Key: {{appkey}}" \ -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/users/57e3bcca67f35600577c3063/refreshSessionToken + https://API_BASE_URL/1.1/users/57e3bcca67f35600577c3063/refreshSessionToken ``` -调用这个 API 要求传入登录返回的 `X-LC-Session` 作为认证,或者使用 `Master Key`。 +调用这个 API 要求传入登录返回的 `X-LC-Session` 作为认证,或者使用 Master Key。 重置成功将返回新的 sessionToken 及用户信息: ```json { - "sessionToken": "5frlikqlwzx1nh3wzsdtfr4q7", - "updatedAt": "2016-10-20T03:10:57.926Z", - "objectId": "57e3bcca67f35600577c3063", - "username": "tom", - "createdAt": "2016-09-22T11:13:14.842Z", - "emailVerified": false, - "mobilePhoneVerified": false + "sessionToken":"5frlikqlwzx1nh3wzsdtfr4q7", + "updatedAt":"2016-10-20T03:10:57.926Z", + "objectId":"57e3bcca67f35600577c3063", + "username":"tom", + "createdAt":"2016-09-22T11:13:14.842Z", + "emailVerified":false, + "mobilePhoneVerified":false } ``` @@ -152,6 +242,10 @@ curl -X PUT \ 锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 +## 使用手机号码注册或登录 + +请参考 [短信服务 REST API 详解 · 使用手机号码注册或登录](/sdk/sms/rest/#使用手机号码注册或登录)。 + ## 验证 Email 设置 email 验证是 app 设置中的一个选项,通过这个标识,应用层可以对提供真实 email 的用户更好的功能或者体验。Email 验证会在 User 对象中加入 `emailVerified` 字段,当一个用户的 email 被新设置或者修改过的话,`emailVerified` 会被重置为 false。云服务会往用户填写的邮箱发送一个验证链接,用户点击这个链接可以让 `emailVerified` 被设置为 true。 @@ -160,9 +254,9 @@ emailVerified 字段有 3 种状态可以参考: 1. **true**:用户已经点击了发送到邮箱的验证地址,邮箱被验证为真实有效。云端保证在新创建用户的时候 emailVerified 一定为 false。 2. **false**:User 对象最后一次被更新的时候,用户并没有确认过他的 email 地址。如果你看到 emailVerified 为 false 的话,你可以考虑刷新 User 对象或者再次请求验证用户邮箱。 -3. **null**:User 对象在 email 验证没有打开的时候就已经创建了,或者 User 没有 email。 +3. **null**:User对象在 email 验证没有打开的时候就已经创建了,或者 User 没有 email。 -邮件模板和验证链接可以在**云服务控制台 > 数据存储 > 用户 > 邮件模板**定制。 +邮件模板和验证链接可以在**云服务控制台 > 内建账户 > 邮件模板**定制。 ## 请求验证 Email @@ -174,7 +268,7 @@ curl -X POST \ -H "X-LC-Key: {{appkey}}" \ -H "Content-Type: application/json" \ -d '{"email":"tom@example.com"}' \ - https://{{host}}/1.1/requestEmailVerify + https://API_BASE_URL/1.1/requestEmailVerify ``` ## 请求密码重设 @@ -187,22 +281,26 @@ curl -X POST \ -H "X-LC-Key: {{appkey}}" \ -H "Content-Type: application/json" \ -d '{"email":"tom@example.com"}' \ - https://{{host}}/1.1/requestPasswordReset + https://API_BASE_URL/1.1/requestPasswordReset ``` 如果成功的话,将返回状态码 `200 OK`。 -邮件模板和验证链接可以在**云服务控制台 > 数据存储 > 用户 > 邮件模板**定制。 +邮件模板和验证链接可以在**云服务控制台 > 内建账户 > 邮件模板**定制。 + +## 手机号码验证 + +请参考 [短信服务 REST API 详解 - 用户账户与手机号码验证](/sdk/sms/rest/#用户账户与手机号码验证)。 ## 获取用户 -和[获取对象](#获取对象)类似,你可以发送一个 GET 请求到 URL 以获取用户的账户信息。比如,为了获取上面创建的用户: +和[获取对象](#获取对象)类似,你可以发送一个 GET 请求到 URL 以获取用户的账户信息。比如,为了获取上面创建的用户: ```sh curl -X GET \ -H "X-LC-Id: {{appid}}" \ -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f + https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f ``` 你还可以通过 sessionToken 获取用户信息。 @@ -213,7 +311,7 @@ curl -X GET \ -H "X-LC-Id: {{appid}}" \ -H "X-LC-Key: {{appkey}}" \ -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/users/me + https://API_BASE_URL/1.1/users/me ``` 返回的 JSON 数据与 [`/login`](#登录) 登录请求所返回的相同。 @@ -233,7 +331,7 @@ curl -X GET \ 为了改动一个用户已经有的数据,需要对这个用户的 URL 发送一个 PUT 请求。任何你没有指定的 key 都会保持不动,所以你可以只改动用户数据中的一部分。 -比如,如果我们想对「tom」的手机号码做出一些改动: +比如,如果我们想对 「tom」 的手机号码做出一些改动: ```sh curl -X PUT \ @@ -242,10 +340,10 @@ curl -X PUT \ -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ -H "Content-Type: application/json" \ -d '{"phone":"18600001234"}' \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f + https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f ``` -返回的 body 是一个 JSON 对象,只有一个 `updatedAt` 字段表明更新发生的时间。 +返回的 body 是一个 JSON 对象,只有一个 `updatedAt` 字段表明更新发生的时间. ```json { @@ -264,11 +362,11 @@ curl -X PUT \ -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ -H "Content-Type: application/json" \ -d '{"old_password":"the_old_password", "new_password":"the_new_password"}' \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f/updatePassword + https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f/updatePassword ``` -- **old_password**:用户的老密码 -- **new_password**:用户的新密码 +* **old_password**:用户的老密码 +* **new_password**:用户的新密码 注意:仍然需要传入 X-LC-Session,也就是登录用户才可以修改自己的密码。 @@ -284,22 +382,22 @@ curl -X PUT \ curl -X GET \ -H "X-LC-Id: {{appid}}" \ -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/users + https://API_BASE_URL/1.1/users ``` 返回的值是一个 JSON 对象包括一个 `results` 字段,值是包含了所有对象的一个 JSON 数组。 ```json { - "results": [ + "results":[ { - "updatedAt": "2015-07-14T02:31:50.100Z", - "phone": "18612340000", - "objectId": "55a47496e4b05001a7732c5f", - "username": "tom", - "createdAt": "2015-07-14T02:31:50.100Z", - "emailVerified": false, - "mobilePhoneVerified": false + "updatedAt":"2015-07-14T02:31:50.100Z", + "phone":"18612340000", + "objectId":"55a47496e4b05001a7732c5f", + "username":"tom", + "createdAt":"2015-07-14T02:31:50.100Z", + "emailVerified":false, + "mobilePhoneVerified":false } ] } @@ -316,7 +414,7 @@ curl -X DELETE \ -H "X-LC-Id: {{appid}}" \ -H "X-LC-Key: {{appkey}}" \ -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f + https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f ``` ## 连接用户账户和第三方平台 @@ -338,6 +436,7 @@ curl -X DELETE \ ``` + [微信](http://open.weixin.qq.com/) 的 authData 内容: ```json @@ -362,31 +461,13 @@ curl -X DELETE \ } ``` -[TapTap](https://www.taptap.cn) 的 authData 内容: - -```json -{ - "taptap": { - "kid": "mac_key id", - "access_token": "当前与 kid 相同", - "token_type": "mac", - "mac_key": "mac 密钥", - "mac_algorithm": "hmac-sha-1", - "openid": "授权用户唯一标识,不同应用的同一用户 openid 不同", - "name": "用户名", - "avatar": "用户头像图片 URL(不保证永久有效)", - "unionid": "授权用户唯一标识,开发者旗下所有应用,同一用户的 unionid 总是相同" - } -} -``` - 其他任意第三方平台: ```json { - "第三方平台名称,例如 facebook": { + "第三方平台名称,例如facebook": { "uid": "在第三方平台上的唯一用户 ID 字符串", - "access_token": "在第三方平台的 `Access Token`" + "access_token": "在第三方平台的 access token", // ……其他可选属性 } } @@ -396,7 +477,7 @@ curl -X DELETE \ 注意: -- 其他第三方平台不支持校验 `Access Token`。 +- 其他第三方平台不支持校验 access token。 - 其他第三方平台不支持后面提到的 UnionID 登录功能,因此也不用设置相应的 `unionid`、`platform`、`main_account` 字段。 - 其他第三方平台请使用 `uid` 字段储存第三方平台的唯一用户 ID 字符串,不要使用 `openid`。 @@ -415,6 +496,7 @@ curl -X DELETE \ 使用一个连接服务来注册用户并登录,同样使用 POST 请求 users,只是需要提供 `authData` 字段。例如,使用 QQ 账户注册或者登录用户: + ```sh curl -X POST \ -H "X-LC-Id: {{appid}}" \ @@ -429,14 +511,14 @@ curl -X POST \ } } }' \ - https://{{host}}/1.1/users + https://API_BASE_URL/1.1/users ``` 云端会检查是否已经有一个用户连接了这个 `authData` 服务。如果已经有用户存在并连接了同一个 `authData`,那么返回 200 OK 和详细信息(包括用户的 `sessionToken`): ```sh Status: 200 OK -Location: https://{{host}}/1.1/users/75a4800fe4b05001a7745c41 +Location: https://API_BASE_URL/1.1/users/75a4800fe4b05001a7745c41 ``` 应答的 body 类似: @@ -462,22 +544,22 @@ Location: https://{{host}}/1.1/users/75a4800fe4b05001a7745c41 ```sh Status: 201 Created -Location: https://{{host}}/1.1/users/55a4800fe4b05001a7745c41 +Location: https://API_BASE_URL/1.1/users/55a4800fe4b05001a7745c41 ``` 应答内容包括 objectId、createdAt、sessionToken 以及一个自动生成的随机 username,例如: ```json { - "username": "ec9m07bo32cko6soqtvn6bko5", - "sessionToken": "tfrvbzmdf609nu9204v5f0tuj", - "createdAt": "2015-07-14T03:20:47.733Z", - "objectId": "55a4800fe4b05001a7745c41" + "username":"ec9m07bo32cko6soqtvn6bko5", + "sessionToken":"tfrvbzmdf609nu9204v5f0tuj", + "createdAt":"2015-07-14T03:20:47.733Z", + "objectId":"55a4800fe4b05001a7745c41" } ``` -云端会自动验证部分平台 `Access Token` 的有效性。 -详见[自动验证第三方平台授权信息](/sdk/storage/guide/dotnet#自动验证第三方平台授权信息)。 +云端会自动验证部分平台 access token 的有效性。 +详见数据存储开发指南《自动验证第三方平台授权信息》章节的说明。 ### UnionID 注册和登录 @@ -498,8 +580,8 @@ Location: https://{{host}}/1.1/users/55a4800fe4b05001a7745c41 使用 UnionID 注册登录,需要提供带有 `unionid` 参数的 `authData`。另外需要配合传递 `platform` 和 `main_account` 这两个字段。 -- `platform`:unionId 对应的注册平台,可由应用自行指定,微信、QQ、微博平台建议设为[这些推荐的值](/sdk/storage/guide/dotnet#该如何指定-unionidplatform)。 -- `main_account`: `main_account` 为 true 时把当前平台的鉴权信息作为主账号。 +* `platform`:unionId 对应的注册平台,可由应用自行指定,微信、QQ、微博平台建议设为《数据存储开发指南·该如何指定 unionIdPlatform》中推荐的值。 +* `main_account`: `main_account` 为 true 时把当前平台的鉴权信息作为主账号。 在服务端进行存储的时候会根据 `platform` 来命名新增的平台,如传入 `"platform" = "weixin"` 时,返回数据中会增加 `_weixin_unionid` 字段存储 `{"uid":"xxxxx"}`。 @@ -539,7 +621,7 @@ curl -X POST \ "main_account":"false" }, }}' \ - https://{{host}}/1.1/users + https://API_BASE_URL/1.1/users ``` 我们看到,在上面的例子中,`wxleanoffice` 和 `wxleansupport` 的 `unionid` 是一样的,`platform` 均指定为 `weixin`,`wxleanoffice` 是主账号,`main_account` 为 `true`。 @@ -548,42 +630,42 @@ curl -X POST \ ```json { - "sessionToken": "v53f0q4oecbrjojn530w89s5f", - "updatedAt": "2018-08-16T08:03:44.203Z", - "objectId": "5b752fe0a22b9d003137e16d", - "username": "vp7szn9ytuaylgtnw14qnjx2u", - "createdAt": "2018-08-16T08:03:44.203Z", - "emailVerified": false, - "authData": { - "wxleanoffice": { - "openid": "oTY851cqL0gk3DqW3xINqG1Q4PTc", - "access_token": "12_b6mz7ujXbTY4vpbqCRaKVa_y0Ij3N9grCeVtM8VJT8KFd4qnQ9lXtBsZVxG6x9c9Nay_oNgvbKK7KYKbn8R2P7uEgA0EhsXMHmxkx-xU-Tk", - "expires_in": 7200, - "refresh_token": "12_71UYUnqHDuIfekimsJsYjBDfY67ilo30fDqrYkqlwZtxNgcBhMmQgDVhT6mJWkRg0mngvX9kXeCGP8kmBWdvUtc5ngRiN5LDTWAau4du838", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account": "true" - }, - "wxleansupport": { - "openid": "ZTY873cOa0gk5aOW5OaaOa3Q6PTc", - "access_token": "34_b6mO7OjXbTY6O-bOaRaaVa_O0aj5a9gOaeVOa8VaT8aad6OnQ9lXO-OZVOa6O9c9aaO_ZagObaa7aYabn8R4P7Oagn0ahOXaamOkO-OU-Tk", - "expires_in": 7200, - "refresh_token": "8-_78UYUnOaaOafekimOaOYj-afY67ilZ40faOOYkOlOZOOagc-hamQgaVhT6maWkRg0mngOX9kXeaaP8km-WdOUOc4ngRia4aaTWnaO4dO848", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account": "false" + "sessionToken": "v53f0q4oecbrjojn530w89s5f", + "updatedAt": "2018-08-16T08:03:44.203Z", + "objectId": "5b752fe0a22b9d003137e16d", + "username": "vp7szn9ytuaylgtnw14qnjx2u", + "createdAt": "2018-08-16T08:03:44.203Z", + "emailVerified": false, + "authData": { + "wxleanoffice": { + "openid": "oTY851cqL0gk3DqW3xINqG1Q4PTc", + "access_token": "12_b6mz7ujXbTY4vpbqCRaKVa_y0Ij3N9grCeVtM8VJT8KFd4qnQ9lXtBsZVxG6x9c9Nay_oNgvbKK7KYKbn8R2P7uEgA0EhsXMHmxkx-xU-Tk", + "expires_in": 7200, + "refresh_token": "12_71UYUnqHDuIfekimsJsYjBDfY67ilo30fDqrYkqlwZtxNgcBhMmQgDVhT6mJWkRg0mngvX9kXeCGP8kmBWdvUtc5ngRiN5LDTWAau4du838", + "scope": "snsapi_userinfo", + "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", + "platform": "weixin", + "main_account": "true" + }, + "wxleansupport": { + "openid": "ZTY873cOa0gk5aOW5OaaOa3Q6PTc", + "access_token": "34_b6mO7OjXbTY6O-bOaRaaVa_O0aj5a9gOaeVOa8VaT8aad6OnQ9lXO-OZVOa6O9c9aaO_ZagObaa7aYabn8R4P7Oagn0ahOXaamOkO-OU-Tk", + "expires_in": 7200, + "refresh_token": "8-_78UYUnOaaOafekimOaOYj-afY67ilZ40faOOYkOlOZOOagc-hamQgaVhT6maWkRg0mngOX9kXeaaP8km-WdOUOc4ngRia4aaTWnaO4dO848", + "scope": "snsapi_userinfo", + "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", + "platform": "weixin", + "main_account":"false" + }, + "_wxleanoffice_unionid": { + "uid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U" + } }, - "_wxleanoffice_unionid": { - "uid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U" - } - }, - "mobilePhoneVerified": false + "mobilePhoneVerified": false } ``` -参见[扩展:接入*UnionID*体系,打通不同子产品的账号系统](/sdk/storage/guide/dotnet#扩展:接入-unionid-体系,打通不同子产品的账号系统)。 +可参见《数据存储开发指南》的《扩展:接入_UnionID_体系,打通不同子产品的账号系统》一节的详细说明。 ### 连接 @@ -605,7 +687,7 @@ curl -X PUT \ } } }' \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f + https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f ``` 完成连接后,你可以使用匹配的 `authData` 来认证他们。 @@ -618,21 +700,21 @@ curl -X PUT \ ```json { "username": "3td7p1nucap1i1p53m1zibwgx", - "authData": { + "authData": { "weixin": { "openid": "oTY851cqL0gk3DqW3xINqG1Q4PTc", "scope": "snsapi_userinfo", "refresh_token": "refresh_token", "platform": "weixin", - "unionid": "unionid", + "unionid": "unionid, "access_token": "access_token", "expires_in": 7200 } - } + }, } ``` -取消微信关联通过删除 `authData.weixin` 来实现: +取消微信关联通过删除 `authData.weixin` 来实现: ```sh curl -X PUT \ @@ -641,15 +723,15 @@ curl -X PUT \ -H "X-LC-Session: 6fehqhr2t2na5mv1aq2om7jgz" \ -H "Content-Type: application/json" \ -d '{"authData.weixin":{"__op":"Delete"}}' \ - https://{{host}}/1.1/users/5b7e53a767f356005fb374f6 + https://API_BASE_URL/1.1/users/5b7e53a767f356005fb374f6 ``` 其返回值类似于: ```json { - "updatedAt": "2018-08-23T06:32:47.633Z", - "objectId": "5b7e53a767f356005fb374f6" + "updatedAt":"2018-08-23T06:32:47.633Z", + "objectId":"5b7e53a767f356005fb374f6" } ``` @@ -659,7 +741,7 @@ curl -X PUT \ ACL 按 JSON 对象格式来表示,JSON 对象的 key 是 objectId 或者一个特别的 key(`*`,表示公共访问权限)。ACL 的值是权限对象,这个 JSON 对象的 key 即是权限名,而这些 key 的值总是 true。 -举个例子,如果你想让一个 id 为 55a47496e4b05001a7732c5f 的用户有读和写一个对象的权限,而且这个对象应该可以被公共读取,符合的 ACL 应该是: +举个例子,如果你想让一个 id 为 55a47496e4b05001a7732c5f 的用户有读和写一个对象的权限,而且这个对象应该可以被公共读取,符合的 ACL 应该是: ```json { diff --git a/docs/sdk/copyright-verification/_category_.json b/docs/sdk/copyright-verification/_category_.json deleted file mode 100644 index 4dfca5ea1..000000000 --- a/docs/sdk/copyright-verification/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "正版验证", - "collapsed": true, - "position": 5 -} diff --git a/docs/sdk/copyright-verification/faq.mdx b/docs/sdk/copyright-verification/faq.mdx deleted file mode 100644 index d8c946aea..000000000 --- a/docs/sdk/copyright-verification/faq.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 3 ---- - -## 在 TapTap 上售卖的游戏是否必须接入正版验证 SDK? - -在 TapTap 上售卖的游戏不强制要求接入正版验证 SDK。 -如果开发者选择不接入正版验证 SDK,那么 TapTap 只能控制未授权的玩家无法从 TapTap 下载游戏,无法在游戏启动时验证玩家是否购买过游戏,包括: - -- 玩家从其他途径下载到游戏安装包后可以安装、启动、进入游戏。 -- 玩家在 TapTap 付费下载游戏后,申请退款。退款后,玩家无法再次下载该游戏,但仍然可以进入之前下载的游戏。 - -如果开发者希望限制未授权的玩家进入游戏,需要自行实现相应的防盗版机制。 -如无特殊需求,我们建议开发者接入正版验证 SDK,节省添加防盗版机制的开发成本。 - -目前,如果没有在开发者中心开启正版验证服务,售卖设置的入口不会显示。 -所以,即使开发者不打算接入正版验证 SDK,仅希望使用 TapTap 的付费下载功能,也需要先在开发者中心申请开启正版验证服务。 -待审核通过,成功开启正版验证后,开发者即可进行售卖设置,无需实际接入正版验证 SDK。 - -## TapTap 分成比例是多少? - -TapTap 不分成。 -不过,国内会有 5% 的支付手续费,海外根据实际沟通确定(第三方支付渠道的手续费和税务成本)。 diff --git a/docs/sdk/copyright-verification/features.mdx b/docs/sdk/copyright-verification/features.mdx deleted file mode 100644 index 6038eb411..000000000 --- a/docs/sdk/copyright-verification/features.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: 正版验证 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { Conditional } from "/src/docComponents/conditional"; - - -TapTap 的正版验证服务适用于买断制游戏,用于检测用户开始游戏时是否完成付费,是否具备下载资格及 DLC 解锁资格。 - -![](https://img.tapimg.com/market/images/e0582de465379e63ba2df5ad3937de4a.png) - -## 付费下载 - -用户需要在 TapTap 完成订单支付后,才可以下载游戏 APK 。同时,如果用户使用一些不正常手段获取游戏 APK 的,也会被正版验证 SDK 拦截,无法进入游戏。 - -在使用付费下载正版验证服务前,请确保您的游戏存在一个用户可见的游戏详情页(可预约、可关注均可)。完成页面上架后,请前往 [开发指南](/sdk/copyright-verification/guide/) 完成开发接入。 - -## DLC - -当您的游戏需开放新章节、新主线等付费的 DLC 内容时,您同样可使用 DLC 正版验证服务完成用户的解锁资格验证。 - -在使用 DLC 正版验证服务前,请确保您的游戏存在一个用户可见的游戏详情页(可预约、可关注均可),并前往开发者中心 > 商店 > 游戏售卖创建 DLC 并提交审核。完成审核后, 请前往 [开发指南](/sdk/copyright-verification/guide/) 完成开发接入。 \ No newline at end of file diff --git a/docs/sdk/copyright-verification/guide.mdx b/docs/sdk/copyright-verification/guide.mdx deleted file mode 100644 index 361107f31..000000000 --- a/docs/sdk/copyright-verification/guide.mdx +++ /dev/null @@ -1,546 +0,0 @@ ---- -title: 正版验证开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import AndroidFaq from "../_partials/android-package-visibility.mdx"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -正版验证适用于在 TapTap 上架的**付费下载**游戏。 - -游戏集成 TapSDK 的正版验证之后,当玩家第一次启动游戏时(包含卸载后再次安装),SDK 会前往 TapTap 查询玩家是否已购买游戏: - -- 如已购买,则可正常进入游戏。 -- 如查询到未购买,将出现「游戏未激活,请前往 TapTap 购买此游戏」弹窗,玩家必须选择「打开 TapTap」购买之后才能游玩。 - -## 付费下载 - -### 权限说明 - - - -<> - - - -<> - -该模块需要访问设备中已安装的 Tap 客户端信息,在 Android 11 且工程 targetVersion > 29 时需要 -开发者在应用 `AndroidManifest.xml` 中添加如下配置: - -``` - - - - - -``` -该模块将在应用中添加如下权限: - -``` - -``` - - -<> - - - -<> - - - - -### SDK 获取 - -可以在 [下载页](/tap-download) 获得 TapSDK,添加以下依赖: - - - -<> - - - - - -<> - -将 SDK 包导入到项目 `project/app/libs` 目录下。打开项目的 `project/app/build.gradle` 文件,添加: - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - implementation name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar' - implementation name:'TapLicense_${sdkVersions.taptap.android}', ext:'aar' -}`} - - - - -<> - -```objc -// 暂不支持 -``` - - - - -<> - -将插件 ** TapLicense ** 、** TapCommon ** 拷贝到项目的插件目录,并在项目模块的 ** build.cs ** 文件中添加依赖: - -```csharp -PublicDependencyModuleNames.AddRange(new string[] { - "Json", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapLicense", -}); -``` - - - - - -### 测试模式设置 - -测试模式开启后,可以模拟线上正版验证流程,方便进行调试。 - -测试模式需要在 License 其他功能调用之前开启。 - -:::info -切记在上线前关闭测试模式 -::: - - - -```cs -// 需要引入 `License` 库 -using TapTap.License; - -// 设置是否开启测试环境 -TapLicense.SetTestEnvironment(bool isTest); -``` - -```java -// 设置是否开启测试环境 -TapLicenseHelper.setTestEnvironment(boolean isTest, Activity activity); -``` - -```objc -// 暂不支持 -``` - -```cpp -// 需要引入 `License` 库 -#include "TapLicense.h" - -// 设置是否开启测试环境 -FTapLicense::SetTestEnvironment(bool bIsTest); -``` - - -### 设置授权回调 - - - -```cs -// 需要引入 `License` 库 -using TapTap.License; - -// 默认情况下 SDK 会弹出不可由玩家手动取消的弹窗来避免未授权玩家进入游戏,如果需要回调来触发流程,请添加如下代码 -TapLicense.SetLicencesCallback(ITapLicenseCallback callback); - -public interface ITapLicenseCallback -{ - // 授权成功回调 - void OnLicenseSuccess(); -} -``` - -```java -// 默认情况下 SDK 会弹出不可由玩家手动取消的弹窗来避免未授权玩家进入游戏,如果需要回调来触发流程,请添加如下代码 -TapLicenseHelper.setLicenseCallback(new TapLicenseCallback() { - @Override - public void onLicenseSuccess() { - // 授权成功回调 - } -}); -``` - -```objc -// 暂不支持 -``` - -```cpp -// 需要引入 `License` 库 -#include "TapLicense.h" - -// 默认情况下 SDK 会弹出不可由玩家手动取消的弹窗来避免未授权玩家进入游戏,如果需要回调来触发流程,请添加如下代码 -FTapLicense::SetLicenseCallback(FSimpleDelegate::CreateLambda([]() { - // 本体授权成功 -})); -``` - - - -### 检查是否购买游戏 - -Check 方法中参数默认值为 false,表示为 SDK 首次触发以及在距离首次触发的第 5 天之后再一次会通过 TapTap 客户端来确认当前登录用户是否购买游戏,如果使用 true 的话,则每次接口调用都会通过 TapTap 客户端来确认当前登录用户是否购买过游戏。 - - - -```cs -TapLicense.Check(); -TapLicense.Check(true); -``` - -```java -TapLicenseHelper.check(Activity activity); -TapLicenseHelper.check(Activity activity, boolean forceCheck); -``` - -```objc -// 暂不支持 -``` - -```cpp -FTapLicense::Check(); -FTapLicense::Check(true); -``` - - - -### Android 11 及更高版本的适配 - - - -## DLC - -### 权限说明 - - - -<> - - - -<> - -该模块需要访问设备中已安装的 Tap 客户端信息,在工程 targetVersion > 29 时需要 -开发者在应用 AndroidManifest.xml 中添加如下配置: - -``` - - - - - -``` - - - -<> - - - -<> - - - - -### 测试模式设置 - -测试模式开启后,可以模拟线上 DLC 的查询与购买,方便进行调试。 - -测试模式需要在 License 其他功能调用之前开启。 - -:::info -切记在上线前关闭测试模式 -::: - - - -```cs -// 需要引入 `License` 库 -using TapTap.License; - -// 设置是否开启测试环境 -TapLicense.SetTestEnvironment(bool isTest); -``` - -```java -// 设置是否开启测试环境 -TapLicenseHelper.setTestEnvironment(boolean isTest, Activity activity); -``` - -```objc -// 暂不支持 -``` - -```cpp -// 需要引入 `License` 库 -#include "TapLicense.h" - -// 设置是否开启测试环境 -FTapLicense::SetTestEnvironment(bool bIsTest); -``` - - - -### DLC 查询和购买 - -可以在 [下载页](/tap-download) 获得 TapSDK,添加以下依赖: - - - -<> - - - - - -<> - -将 SDK 包导入到项目 `project/app/libs` 目录下。打开项目的 `project/app/build.gradle` 文件,添加: - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - implementation name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar' - implementation name:'TapLicense_${sdkVersions.taptap.android}', ext:'aar' -}`} - - - - -<> - -```objc -// 暂不支持 -``` - - - -<> - -将插件 ** TapLicense ** 、** TapCommon ** 拷贝到项目的插件目录,并在项目模块的 ** build.cs ** 文件中添加依赖: - -```csharp -PublicDependencyModuleNames.AddRange(new string[] { - "Json", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapLicense", -}); -``` - - - - - -#### DLC 回调设置 - -DLC 回调包含查询回调和购买回调。 - - - -```cs -public class MyTapDLCCallback:ITapDlcCallback -{ - public void OnQueryCallBack(TapLicenseQueryCode code, Dictionary queryList) - { - // 查询回调 - } - - public void OnOrderCallBack(string sku, TapLicensePurchasedCode status) - { - // 购买回调 - } -} - -TapLicense.SetDLCCallback(new MyTapDLCCallback()); -``` - -```java -TapLicenseHelper.setDLCCallback(new DLCManager.InventoryCallback() { - @Override - public boolean onQueryCallBack(int i, HashMap queryList) { - // 查询回调 - return false; - } - - @Override - public void onOrderCallBack(String s, int i) { - // 购买回调 - } -}); -``` - -```objc -// 暂不支持 -``` - -```cpp -FTapLicense::SetDLCCallback( - FTapLicense::FDLCQueryDelegate::CreateLambda( - [](FTapLicense::EQueryResult Code, const FTapLicense::Map& QueryList) { - // 查询回调 - switch (Code) { - case FTapLicense::EQueryResult::OK: - //查询成功 - break; - case FTapLicense::EQueryResult::Error: - //查询失败 - break; - case FTapLicense::EQueryResult::NotInstallTapTap: - //sdk有相应处理,正常不需要开发者做处理 - break; - } - }), - FTapLicense::FDLCOrderDelegate::CreateLambda([](const FString& Sku, FTapLicense::EOrderStatus Status) { - // 购买回调 - }) -); -``` - - - -#### DLC 查询 - -对应的查询回调会返回具体的查询结果,查询成功时会返回当前 Tap 玩家是否已经购买过对应商品,在查询回调中返回的键值对类型参数 `queryList` 中可以获取,该参数 `key` 为查询的商品 `skuid`,`value` 表示该商品当前查询用户的购买状态:0 表示未购买,1 表示已购买。 - - - -```cs -TapLicense.QueryDLC(string[] skuIds); -``` - -```java -TapLicenseHelper.queryDLC(Activity activity, String[] skuIds); -``` - -```objc -// 暂不支持 -``` - -```cpp -FTapLicense::QueryDLC(const TArray& DLCList); -``` - - - -#### DLC 购买 - - - -```cs -TapLicense.PurchaseDLC(string skuId); -``` - -```java -TapLicenseHelper.purchaseDLC(Activity activity, String skuIds); -``` - -```objc -// 暂不支持 -``` - -```cpp -FTapLicense::PurchaseDLC(const FString& DLC); -``` - - - -#### 参数说明 - -##### TapLicenseQueryCode - -| 回调 | 回调值 | 说明 | -| ------------------------------- | ------ | ------------------------------ | -| QUERY_RESULT_OK | 0 | 查询成功 | -| QUERY_RESULT_NOT_INSTALL_TAPTAP | 1 | 检查测试机未安装 TapTap 客户端 | -| QUERY_RESULT_ERR | 2 | 查询失败 | -| ERROR_CODE_UNDEFINED | 80000 | 未知错误 | - -##### skuId - - - -请前往 [DLC 商品](/store/buyout/dlc-products) 查看如何创建 DLC 商品并获得 skuid。 - - - - -请前往 DLC 商品查看如何创建 DLC 商品并获得 skuid。 - - - - -## 如何测试 - -### SDK 开启测试模式 - -在 License 模块初始化后,调用 TapLicenseHelper.setTestEnvironment(boolean testEnvironment, Activity activity) 开启/关闭测试模式。 - -### 填写测试包名、测试用户 - -在开发者中心 > 游戏服务 > 正版验证添加测试包名,同时将测试用户的 TapTap ID 加入测试白名单。 - -![](https://img.tapimg.com/market/images/02d44a76f74acfde8d7a38f41c84f073.png) - -### 完成支付 -加入测试白名单的用户将正常拉起 TapTap 的支付功能,请正常操作完成购买。 - -:::tip - -- 测试用户所使用的 TapTap 请确保是最新正式版本。 -- 测试环境的购买仅为了模拟正式环境的购买流程,并不会产生真正的付款订单。 -- 单个测试用户在测试环境下仅可完成 1 次订单支付。如需重复测试,请从白名单中移除此测试账号后再加入。 -- 在正式上线前,请关闭测试环境。 - -::: - -![](https://img.tapimg.com/market/images/c22970392e74be4a59bd3d12138d205e.png) \ No newline at end of file diff --git a/docs/sdk/embedded-moments/_category_.json b/docs/sdk/embedded-moments/_category_.json deleted file mode 100644 index c83485109..000000000 --- a/docs/sdk/embedded-moments/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "内嵌动态", - "collapsed": true, - "position": 6 -} diff --git a/docs/sdk/embedded-moments/bestpractice.mdx b/docs/sdk/embedded-moments/bestpractice.mdx deleted file mode 100644 index b64e8aafd..000000000 --- a/docs/sdk/embedded-moments/bestpractice.mdx +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: 内嵌动态最佳实践 -sidebar_label: 最佳实践 -sidebar_position: 3 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; - -![](https://capacity-files.lcfile.com/Fo0jjeOf1F4nIsWsuvGt3zUxqt7CRTvQ/head_cover.png) - ---- - -## 游戏内社区对游戏的价值体现 - -**内容「生产」和「消费」** - -- 内容生产:玩家可以轻松将游戏内容一键分享到社区中 - -- 内容消费:根据算法智能推荐,玩家更容易发现优质的游戏内容 - -**链接「玩家」和「官方」** - -- 内容分发:官方发布的内容会进入玩家的关注流,可在内嵌动态为玩家精准分发内容 - -- 快速反馈:玩家发布的内容,官方可以及时反馈和给予帮助解答 - -**链接「玩家」和「玩家」** - -- 社交需求:在游戏过程中,能与其他玩家进行延时性的交流和互动 - -- 帮助决策:在游戏关卡中,遇到困难,能快速查到游戏攻略和大神解说 - ---- - -## 实用的运营工具能辅助建设良好的社区形态 - -### 快捷登录 —— 玩家登录之后可以深度体验社区玩法 - -**游客模式**:玩家不需要登录也能浏览社区内的帖子 - -**TapTap 登录**:玩家发布动态或者进行互动时,需要进行 TapTap 账号登录 - - - - - - -
    - - 唤起 TapTap 原⽣应⽤授权登录 - - - 本机无客户端时可唤起 Webview 账号登录 -
    - -### 模块详解 —— 如何巧妙运用「一块」「二位」「三页」来快速搭建社区 - -**子版块**:TapTap 论坛内的顶部导航,可以将帖子分类管理,包含「全部」「官方」「精华」「视频」4 个不可删改的固定 Tab,和可配置的子版块包含「攻略」「反馈」。 - -**运营位**:运营位可以帮助开发者展示重要轮播图,如活动和新功能上线 - -**推荐位**:推荐位可以帮助开发者配置快捷导航,此处可以关联不同类型的落地页 - -![](https://capacity-files.lcfile.com/l8BvC3sp42vx4eYN817kkf4vNpPYCP8B/3part_show.png) - - -**索引块**:用以承载结构化内容组织需求,目前有小索引块(5 列)、中索引块(3 列)、大索引块(2 列) - -:::tip - -可用于制作成游戏的「WIKI」「百科」「游戏资料库」 - -::: - -![](https://capacity-files.lcfile.com/qTvpSWM571lS8SG7jDCOOHBBCMMVFqcE/suoyingkuai1.png) - -![](https://capacity-files.lcfile.com/EOd3psIWI1iI23Tbp42k6wGYEFC92tQ9/suoyingkuai2.png) - - -**合集页**:TapTap 论坛内的某一类主题帖子的集合 - -:::tip - -可用于整合专题类的攻略或同人集合,如「大神进阶」「新手教程」「同人漫画」等 - -::: - -![](https://capacity-files.lcfile.com/Ts0THmlavVwrbpYneg3wjuC4b8BgvBSu/hejiye.png) - -**详情页**:TapTap 论坛内的帖子详情页 - -![](https://capacity-files.lcfile.com/47q8lN0dNFwddsOAAku0U8pEMFzUB8NQ/detail_page.png) - -### 对战引导关闭 —— 逛社区玩游戏两不误 - -在匹配对战类游戏中,如 Moba 类或 FPS 类游戏中,可以引导玩家关闭动态回到游戏进行对局。 - -![](https://capacity-files.lcfile.com/3CPL9rC7Em4GBnwKjvnv5F6tNl55yc73/close_moment.png) - -## 优质游戏社区实战技巧分享 - -### 入口红点 —— 触达用户的神兵利器 - -开发者可以在游戏内放置内嵌动态的入口,红点可以引导玩家进入内嵌动态里,入口红点和内嵌动态内「关注」的红点逻辑是一致的,玩家关注的用户发布了新内容将触发消息通知。 - -:::tip - -红点对提升使用率至关重要,建议将入口放在游戏内显眼的位置 - -::: - -![](https://capacity-files.lcfile.com/yIb7UR7pIQW7Ul9AREmVIvjNTRDRsej6/redspot.png) - -| 不接入「红点」前 😖 | 拥有了「红点」之后 😆 | -| -------------------------- | ---------------------------- | -| 使用率降低,社区无人问津 | 社区活跃提升促进用户内容消费 | -| 官方发布新消息用户无法感知 | 官方活动公告一触即达 | -| 入口成了一个摆设 | 活跃的社区可以提升用户留存 | - -### 游戏社区风格 —— 将 TapTap 与游戏风格融会贯通,给玩家更好的沉浸体验 - -入口样式经典案例,好的入口是打开的关键因素 - -![](https://capacity-files.lcfile.com/FvA7941IMNj1kLPRinJo6r0kzi9hXRWv/rukouyangshi.png) - -横屏游戏经典案例,善用空白处给游戏增添品牌效果 - -![](https://capacity-files.lcfile.com/weMWPAcwdNTDsWvmySmvuT7CtlriS3hU/hengpingyangshianli.png) - -竖屏游戏经典案例,运营位、推荐位、背景图需要高度统一 - -![](https://capacity-files.lcfile.com/aUYFPhyCAny748Gk4H96j3kBlbE2DFm4/shupingyangshianli.png) - -### 场景化入口 —— 在游戏内自由跳转页面 - -开发者可以在游戏内某一场景处绘制入口,然后在开发者中心后台配置玩家点击按钮后的落地页,帮助玩家在游戏内遇到困难和问题时给予决策辅助。入口样式最好结合游戏场景去绘制,跳转的落地页根据开发者自身诉求自定义配置。 - -:::tip - -适用场景:玩法介绍、英雄攻略、问题反馈、赛事新闻 - -::: - -![](https://capacity-files.lcfile.com/IVjvBoaoiN4CRSo9odbaT1n53qK02mcE/changjinghuarukou_anli.png) - -### 内嵌攻略站 ——TapTap 助力开发者建设攻略站 - -还在为生产攻略而烦恼吗?还在为找不到攻略而发愁吗?游戏官方需要准备好游戏素材,自备官方攻略或选取社区内玩家的优质攻略进行投稿。 TapTap 会将这些内容进行归类,玩家在游戏内可以轻轻松松地找到它们。 - -:::tip - -为了给玩家更好阅读体验,建议横屏游戏使用自动转屏至竖屏 - -::: - -![](https://capacity-files.lcfile.com/4hHLFMqhfy8erSuwVxCgc7apIRRKsdxD/strategy.png) - -### 一键发布 —— 分享游戏内任意场景截图 - -:::tip - -适用场景:成就分享、战绩分享、段位分享、主页分享,开发者可以结合内嵌动态提供的回调,结合场景基于玩家一些奖励激励玩家进行内容生产 - -::: - -![](https://capacity-files.lcfile.com/XUtgbv5RBjxC2LtURbVfrGfArJca6Tzh/share_data.gif) - -### 为好友列表赋能,轻松了解好友动态 - -![](https://capacity-files.lcfile.com/DMPtG4lr2okFv8D0ehmcs66WJKwsbg6H/friend_moments.gif) - -### 问题反馈 —— 游戏内测阶段的好帮手 - -![](https://capacity-files.lcfile.com/OxpqhhT9Apukr21Vd0MC7ErmpviSGQIz/feedback.gif) diff --git a/docs/sdk/embedded-moments/features.mdx b/docs/sdk/embedded-moments/features.mdx deleted file mode 100644 index de7888b42..000000000 --- a/docs/sdk/embedded-moments/features.mdx +++ /dev/null @@ -1,290 +0,0 @@ ---- -title: 内嵌动态功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -## 产品介绍 - -玩家可以在游戏内进入 TapTap 的社区论坛,查看攻略资讯,分享自己的游戏精彩瞬间,也可以参与其他玩家、官方和大神之间的互动。 - -## 核心优势 - -**对于游戏开发者:** - -- 内容生产:可以通过一键分享,引导玩家分享自己在游戏内的内容 -- 内容触达:官方发布的内容会进入玩家的关注流,可在内嵌动态为玩家精准分发内容 -- 内容反馈:对玩家发布的内容可以及时反馈和给予帮助解答 - -**对于游戏玩家:** - -- 社交需求:在游戏过程中,能与其他玩家进行延时性的交流和互动 -- 帮助决策:在游戏场景中,遇到困难,能快速查到游戏攻略和大神解说 - -## 账号体系 - -当玩家在内嵌动态内发布动态或者进行互动时,需要进行 TapTap 账号登录,故此建议开发者接入 **[TapTap 登录](/sdk/taptap-login/features/)**。 - -若游戏没有接入 TapTap 登录,玩家可以以**游客模式**使用内嵌动态,当触发到需要登录的功能时会在内嵌动态中唤起的登录界面。 -![](https://capacity-files.lcfile.com/ol3k5gyUteul2kh1R0BE2yoaclL5Dmoc/taplogin-moment.png) - -## 动态功能 - -### 游戏论坛 - -玩家可以直接在「游戏」模块中访问 TapTap 论坛: - -![](https://capacity-files.lcfile.com/gOaifpUWFGT6hYUsc84F8sckfu74d67b/game.png) - -其中各模块分别为: - -![](https://capacity-files.lcfile.com/FS5XnJzoKmHqmQyTBw6Vp2V1jgYQCQ22/game-detail.png) - -1. **子版块**:子版块可以帮助用户分类筛选信息流,在 TapTap 论坛和内嵌动态内是互通的,该模块可在「论坛管理者中心-子版块管理」中编辑,过审后即可展示。 - -2. **运营位**:运营位可以帮助开发者展示重要的信息和活动,是内嵌动态独有的模块,该模块可在「游戏服务-内嵌动态-运营位配置」中编辑,过审后即可在内嵌动态中展示。 - -3. **推荐位**:推荐位可以帮助开发者配置快捷导航,此处可以关联「帖子」「子版块」「索引页」,在 TapTap 论坛和内嵌动态内是互通的,该模块可在「论坛管理者中心-推荐位管理」中编辑,过审后即可展示。 - -4. **信息流**:玩家进入论坛时默认查看热门推荐的信息流,右上角可以根据回复时间和发布时间筛选信息流排序。 - -除此之外,还提供以下功能: - -- **发布动态**:玩家可以在论坛内发布图文动态,和发布视频动态 - -![](https://capacity-files.lcfile.com/iTcFOySJpv4c4OxbXDQvaapJqXwpo5aJ/post.png) - -- **社区互动**:玩家可以**点赞、评论、转发**其他玩家的动态 - -![](https://capacity-files.lcfile.com/auTGp3LqRNUNS2le7lIVP8KIoNiApP7m/repost.png) - -### 攻略站 - -#### 工具箱 - -用于放置游戏工具,如帖子、外链等一些链接。 - -![](https://capacity-files.lcfile.com/tIcfpiiADXplIwMJ5iS8BaFIJlrcHIMo/toolbox.png) - -#### 信息板 - -展示热门攻略等内容,或是近期比较常用的攻略。 -![](https://capacity-files.lcfile.com/medcErLrYf6k3mWxyNY5pMWiPMlGo07f/message_board.png) - - -#### 索引块 - -用以承载结构化内容组织需求,目前有小索引块(5 列)、中索引块(3 列)、大索引块(2 列) - -![](https://capacity-files.lcfile.com/tazGj9CpfLvDx5ahC77sh5CJtIvGvHbi/suo_yin_kuai.png) - - -#### 合集页 -TapTap 论坛内的某一类主题帖子的集合,用于归纳整理 - -![](https://capacity-files.lcfile.com/PMDmtQeWLuvLDcPz0zUJKImCAKQacKMz/hejiye1.png) - -#### 攻略站须知 - -##### 为什么我的游戏内嵌动态里没有「攻略」模块,如何开启? - -首先需要游戏论坛内超过 20 个相关的攻略帖子,开发者可以在 **开发者中心后台 - 游戏运营 - 游戏攻略** 进行开启 - -##### TapTap 开发者中心能配置「攻略」吗?怎么配置? - -目前我们仅对部分游戏开放编辑权限,开发这可以前往 **论坛管理中心 - 攻略管理** 进行操作 - -##### 哪些人可以配置攻略? - -游戏开发者、论坛版主(被允许了攻略配置权限的) - - -### 关注流 - -已登录 TapTap 的用户,可在此查看 TapTap 上关注的好友发布的动态和官方动态。当有新发布的内容时,「关注」的导航栏上会有**红点提醒**,让玩家不会错过重要发布的内容。 - -![](https://capacity-files.lcfile.com/oFlBqtty93dB4ok6fdV2OA8uBpRnMahI/follow.png) - -### 个人页 - -玩家可以在「我的」页面找到自己发布过的动态,可以再进行外部分享和内容删除。 - -![](https://capacity-files.lcfile.com/3fjfuorViRYEpfAeoF1wEf7nDW0I6NfO/me.png) - -在右上角的「小闹铃」处可以查询到最新消息,玩家之间的互动会触发通知,有助于玩家之间更好地建立联系和沉淀关系: - -![](https://capacity-files.lcfile.com/75Rdbv9hGFsTEV2lujF67lS8Bcg2Js19/msg.png) - -## SDK 功能 - -### 场景化入口 - -开发者可以在游戏内某一场景处绘制入口,然后在[开发者中心后台配置](#场景化入口配置)玩家点击按钮后的落地页,帮助玩家在游戏内遇到困难和问题时给予决策辅助。 - -:::tip - -1. 入口样式最好结合游戏场景去绘制,TDS 不提供样式规范,目的是为了让玩家进入时不会有违和感。 -2. 跳转的落地页可以配置成某一篇文章或者是某一个模块,根据开发者自身诉求自定义配置。 - -::: - -![](https://capacity-files.lcfile.com/URQeeUTcT801oxSRfKq3sjMC36uxsmKM/scenario-portal.png) - -### 入口红点 - -开发者可以在游戏内放置内嵌动态的入口,红点可以引导玩家进入内嵌动态里。 - -![](https://capacity-files.lcfile.com/nDoYy5JPwia8wyXUUnfDERfXgB7sW7UL/red-dot.png) - -:::tip - -1. 红点对提升使用率至关重要,建议将入口放在游戏内显眼的位置 -2. 入口红点和内嵌动态内「关注」的红点逻辑是一致的,玩家关注的用户发布了新内容将触发消息通知,获取新消息间隔为 1 分钟一次(1 分钟是最小单位,轮询时长开发者可自行调整为 3 分钟、5 分钟等) -3. 当玩家打开内嵌动态后,游戏需要清除小红点展示,再继续下一次轮询请求来展示红点 - -::: - -### 一键发布 - -游戏内任意场景需要截图分享的,TDS 提供一键发布到内嵌动态内,仅支持图文动态发布。 - -![](https://capacity-files.lcfile.com/jSHc1QzSFPTKva5uKWQp8QiT870Y9gaO/share.png) - -### 动态关闭引导弹窗 - -若需要玩家立即退出内嵌动态回到游戏内及时响应的(如即时对战),开发者可以自定义弹窗引导和弹窗触发的节点。 - -![](https://capacity-files.lcfile.com/3DVl39Uz882GKT4Epc29Fd6wTIdo8o7S/popup.png) - -## 后台功能 - -### 主题配置 - -为了更好地结合游戏场景,让玩家不会有割裂感,TDS 提供开发者自定义配置内嵌状态的样式主题,你可以在「游戏服务」-「内嵌动态」-「主题配置」中上传背景图片和设置字体配色。 - -:::tip - -1. 可参考[内嵌动态设计指南](/design/design-moment/)。 -2. 如果游戏仅支持横屏或者竖屏,只需要上传一张即可,若是支持转屏则需要上传两张。 -3. 图片是需要进行人工审核的,一般会在 2 个工作日内完成。 - -::: - -![](https://capacity-files.lcfile.com/Gboeocmn2zmPlN0779P1RHI4vj0AivGH/tap_moment_bg.png) - -### 运营位配置 - -为了更好地帮助游戏进行活动运营,TDS 提供开发者自定义配置内嵌状态的运营位,你可以在「游戏服务」-「内嵌动态」-「运营位配置」中新增运营位,需要提供**标题、图片 Banner**和**链接**。 - -:::tip - -1. 最多同时配置 5 个运营位,TDS 对跳转链接域名不做限制。 -2. 运营位是需要进行人工审核的,一般会在当天内审核完。 - -::: - -![](https://capacity-files.lcfile.com/VcFEYFk5H9OxFCUNpkHgGTFIpWhzKE86/tap-moment-banner-config.png) - -### 场景化入口配置 - -场景化入口可以在「游戏服务」-「内嵌动态」-「场景化入口配置」中创建,开发者提交**入口名称、落地页类型、落地页**后,生成的入口 ID 可以在游戏内使用。该模块是不需要审核的,开发者可以自由变更跳转路径。 - -![](https://capacity-files.lcfile.com/NgUth0N8CfkrDs5VdsPuipJARRkbvOdw/tap-moment-scenario-based-portal-configuration.png) - - -## 论坛管理中心 - -![](https://capacity-files.lcfile.com/K8YfR6iVwtyRSbAC7sBONQasqPhhUu6C/forum-management.png) - -### 论坛数据查看 - -为了更好地体现内嵌动态的价值和内容质量的反馈,我们提供开发者查看内嵌动态内数据的功能,在「游戏服务」-「内嵌动态」-「论坛管理中心」,点击直接跳转查看。(注:查看数据需要开权限) - - -### 子版块配置 - -子版块配置可以从右上角「论坛管理中心」入口进入,前往「子版块管理」提供**子版块名称(包含多语言)**,子版块的新增和修改都是需要人工审核的。 - - -### 推荐位配置 - -推荐位配置可以从右上角「论坛管理中心」入口进入,前往「推荐位管理」提供**标题、图片、跳转路径**,推荐位的新增和修改都是需要人工审核的。 - - -### 攻略配置 - -攻略配置可以从右上角「论坛管理中心」入口进入,前往「攻略管理」提供**封面图片、帖子链接、实体** - -#### 什么是实体词? - -1、「实体」是算法应用中产生的概念,例如在《原神》中,“圣遗物”、“夜兰”、“3.6版本”都可能是一个实体。 - - -2、在攻略配置中,「实体」所指代的是所有与该名词相关的攻略的集合。 - - -3、如果为某一模块配置了「实体」,算法将会自动为其收录添加最新最热的攻略,并进行排序。 - - - -#### 工具箱配置 - -1、点击「新建模块」,选择「工具箱」 - - -2、输入工具箱的标题名称,如“常用攻略集合” - - -3、点击「新增内容」,填写名称,如“近战攻略”,跳转的内容类型包含**论坛帖子、外链、实体** - - -4、需要上传图片尺寸需要满足 **144x144 px**,点击「提交」完成保存 - - -5、最少需要配置 2 个,最多无上限 - - - -![](https://capacity-files.lcfile.com/Loh12C6iq4a07NN2yXUUpOTcOA3dDJM2/toolbox_config.png) - - - -#### 信息板配置 - -1、点击「新建模块」,选择「信息板-banner」或者「信息板-实体」 - - -2、输入信息板的标题名称,如“热门攻略” - - -3、点击「新增内容」,填写名称,如“地图攻略”,跳转的内容类型包含**链接、实体** - - -4、若配置「信息板-Banner」需要上传图片尺寸需要满足 **1000x563 px**,点击「提交」完成保存 - - -5、最少需要配置 1 个,最多 4 个 - - -![](https://capacity-files.lcfile.com/7Hw2JdGk0qgYM2OGpU1fEtpCWzhCasoP/message_config.png) - - -#### 索引块配置 - - -1、点击「新建模块」,选择「索引块-大」或者「索引块-中」或者「索引块-小」 - - -2、输入信息板的标题名称,如“英雄图鉴” - - -3、点击「新增内容」,填写名称,如“投掷道具” - - -4、点击「+」添加一个快,填写名称,如"爱心烟雾弹",跳转的内容类型包含**论坛帖子、外链、实体** - - -5、需要上传图片尺寸需要满足,「索引块-小」**186x186 px**,「索引块-中」**500x280 px**,「索引块-大」**500x280 px**,点击「提交」完成保存 - - -![](https://capacity-files.lcfile.com/5kh14qdvQHky0gqdGzs06sWvIqM2qrtj/suoyinkuai_config.png) diff --git a/docs/sdk/embedded-moments/guide.mdx b/docs/sdk/embedded-moments/guide.mdx deleted file mode 100644 index b4c6a39b6..000000000 --- a/docs/sdk/embedded-moments/guide.mdx +++ /dev/null @@ -1,696 +0,0 @@ ---- -title: 内嵌动态开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import Languages from "../_partials/languages.mdx"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -本文介绍如何在游戏中加入 [TapTap 内嵌动态](/sdk/embedded-moments/features/)。使用内嵌动态功能需依赖 TapTap 登录。由于 TapTap 登录有两种形式,分别为[基于内建账户系统登录](/sdk/taptap-login/guide/start/)和[单纯登录](/sdk/taptap-login/guide/tap-login/),所以内嵌动态 SDK 获取以及初始化根据 TapTap 登录集成方式的不同而有区别。 - -以下先介绍基于内建账户系统登录条件下内嵌动态的集成,**如果使用单纯的 TapTap 登录 并接入内嵌动态,初始化和 SDK 获取请参考:[单纯的内嵌动态初始化](#单纯的内嵌动态初始化)**。 -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - - - -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | -| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | -| 读写存储权限 | 用于发布或下载动态页面内图片、视频 | 下载或使用本地图片发布动态时申请 | - -该模块将在应用中添加如下权限: - -``` - - - - - -``` - - - -<> - - - -<> - - - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启内嵌动态应用配置、绑定 API 域名; - -## SDK 获取 - - -请先参考 [TapTap 登录](/sdk/taptap-login/guide/start/#sdk-获取)完成 SDK 获取,然后在此基础上可以通过 [下载](/tap-download) 获得 TapSDK,添加 `TapMoment` 模块: - - - - - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapMoment_${sdkVersions.taptap.android}', ext:'aar') // TapTap 内嵌动态 -}`} - - - - {`// 内嵌动态 -TapMomentResource.bundle -TapMomentSDK.framework -`} - - -```cs -PublicDependencyModuleNames.AddRange(new string[] { - //... - "TapMoment" -}); -``` - - - -## SDK 初始化 - -请确认已经完成了 [Tap 登录初始化](/sdk/taptap-login/guide/start/#sdk-初始化),则内嵌动态无需再次进行初始化; - -:::info - -- 如果使用 [单纯的 TapTap 登录](/sdk/taptap-login/guide/tap-login/) 并接入内嵌动态,初始化和 SDK 获取请参考:[单纯的内嵌动态初始化](#单纯的内嵌动态初始化)。 - -::: - -## 设置回调 - -设置回调以获取动态的状态变化。 - - - -```cs -TapMoment.SetCallback((code, msg) => { - Debug.Log(code + "---" + msg); -}); -``` - -```java -TapMoment.setCallback(new TapMoment.TapMomentCallback() { - @Override - public void onCallback(int code, String msg) { - - } -}); -``` - -```objectivec -@interface ViewController () -@end - -[TapMoment setDelegate:self]; -- (void)onMomentCallbackWithCode:(NSInteger)code msg:(NSString *)msg -{ - NSLog (@"msg:%@, code:%i", msg, code); -} -``` - -```cpp -FTapMoment::SetCallback(FTapMoment::FDelegate::CreateLambda([](int Code, const FString& Msg) { - -})); -``` - - - -回调方法中 code 表示事件类型,现支持的回调类型如下: - -| 回调 | 回调值 | 说明 | -| -------------------------------- | ------ | ---------------------------------------- | -| CALLBACK_CODE_PUBLISH_SUCCESS | 10000 | 动态发布成功 | -| CALLBACK_CODE_PUBLISH_FAIL | 10100 | 动态发布失败 | -| CALLBACK_CODE_PUBLISH_CANCEL | 10200 | 关闭动态发布页面 | -| CALLBACK_CODE_GET_NOTICE_SUCCESS | 20000 | 获取新消息成功 | -| CALLBACK_CODE_GET_NOTICE_FAIL | 20100 | 获取新消息失败 | -| CALLBACK_CODE_MOMENT_APPEAR | 30000 | 动态页面打开 | -| CALLBACK_CODE_MOMENT_DISAPPEAR | 30100 | 动态页面关闭 | -| CALLBACK_CODE_CLOSE_CANCEL | 50000 | 取消关闭所有动态界面(弹框点击取消按钮) | -| CALLBACK_CODE_CLOSE_CONFIRM | 50100 | 确认关闭所有动态界面(弹框点击确认按钮) | -| CALLBACK_CODE_LOGIN_SUCCESS | 60000 | 动态页面内登录成功 | -| CALLBACK_CODE_SCENE_EVENT | 70000 | 场景化入口回调 | - -## 获取新消息 - -定时调用获取消息通知的接口,有新信息时可以在 TapTap 动态入口显示小红点,提醒玩家查看新动态。 - - - -```cs -TapMoment.FetchNotification(); -``` - -```java -TapMoment.fetchNotification(); -``` - -```objectivec -[TapMoment fetchNotification]; -``` - -```cpp -FTapMoment::FetchNotification(); -``` - - - -获取消息通知的结果会在本文刚开始设置的回调中返回,`code` 为 `CALLBACK_CODE_GET_NOTICE_SUCCESS`(`20000`)表示获取成功,`CALLBACK_CODE_GET_NOTICE_FAIL`(`20100`)表示获取失败。 -获取成功时,`msg` 为新消息数量,`0` 表示没有新消息。 - -:::tip - -为了方便玩家查看好友动态、游戏公告等,我们建议将 TapTap 动态入口放在显眼的位置,**每分钟调用 1 次**获取消息通知的接口。 - -获取消息通知时,如果没有新消息(`msg` 为 `0`),那么游戏需要清除界面上的小红点。 -同样,打开 TapTap 动态页面后,游戏也需要清除界面上的小红点。 - -::: - -## 设置默认屏幕方向 - -在游戏中显示动态页面时,如果游戏屏幕方向始终保持不变,开发者可调用如下接口设置动态默认展示的屏幕方向。 - - - -```cs -TapMoment.SetDefaultOrientation(Orientation.ORIENTATION_LANDSCAPE); -``` - -```java -TapMoment.setDefaultOrientation(TapMoment.ORIENTATION_PORTRAIT); -``` - -```objectivec -TapMomentConfig *mConfig = TapMomentConfig.new; -mConfig.orientation = TapMomentOrientationDefault; -[TapMoment setDefaultConfig:mConfig]; -``` - - -```cpp -//暂不支持 -``` - - -设置方向后,后续打开动态页面接口可选择使用该方向。 - -## 显示动态页面 - -在游戏中显示 TapTap 动态页面,在这个页面,玩家不仅可以查看动态,还能发布新动态。 - - - -```cs -//使用指定屏幕方向 -TapMoment.Open(Orientation.ORIENTATION_LANDSCAPE); -//使用默认方向 -TapMoment.Open(); -``` - -```java -//使用指定屏幕方向 -TapMoment.open(TapMoment.ORIENTATION_PORTRAIT); -//使用默认方向 -TapMoment.open(); -``` - -```objectivec -//使用指定屏幕方向 -TapMomentConfig *mConfig = TapMomentConfig.new; -mConfig.orientation = TapMomentOrientationDefault; -[TapMoment open:mConfig]; -//使用默认方向 -[TapMoment open]; -``` - -```cpp -//使用指定屏幕方向 -FTapMoment::Open(ETapMomentOrientation::PORTRAIT); -``` - - - -:::note - -打开动态页面时,请先屏蔽游戏自身的声音,以免干扰动态内的视频声音。 - -如需要动态能支持横竖屏随设备自动旋转,需要游戏自身能支持横竖屏。 - -如前所述,打开动态页面后别忘了清除动态页面入口处的小红点。 - -::: - -动态页面的背景图可以配置,步骤如下图所示。 -背景图需要人工审核后才能生效,请预留充足的时间。 - -![](https://capacity-files.lcfile.com/Gboeocmn2zmPlN0779P1RHI4vj0AivGH/tap_moment_bg.png) - -## 场景化入口 - -开发者可以[结合游戏场景绘制入口](/sdk/embedded-moments/features/#场景化入口),玩家打开入口跳转到指定的页面。使用前需要在开发者中心后台完成[场景化入口配置](/sdk/embedded-moments/features/#场景化入口配置)。 - - -<> - -```cs -// sceneId 为开发者中心后台创建场景化入口后生成的「入口 ID」 -var sceneDic = new Dictionary() { { TapMomentConstants.TapMomentPageShortCutKey, sceneId } }; -//使用指定屏幕方向 -TapMoment.DirectlyOpen(Orientation.ORIENTATION_DEFAULT, TapMomentConstants.TapMomentPageShortCut, sceneDic); -// 使用默认屏幕方向 -TapMoment.DirectlyOpen(TapMomentConstants.TapMomentPageShortCut, sceneDic); -``` - -#### 参数说明 - -| 参数 | 说明 | -| ----------- | ------------------------------------------------------------------------------------ | -| orientation | 打开方向 | -| page | 固定为 TapMomentConstants.TapMomentPageShortCut | -| Dictionary | 其中 TapMomentConstants.TapMomentPageShortCutKey 固定,第三个参数为需要跳转的页面 id | - - -<> - -```java -Map extras = new HashMap<>(); -// 注意:这里的 key 是固定的,"scene_id";第二个参数是开发者中心后台创建场景化入口后生成的「入口 ID」 -extras.put("scene_id", "xxxx"); -// 使用指定屏幕方向,注意:第二个参数固定为 "tap://moment/scene/" -TapMoment.directlyOpen(TapMoment.ORIENTATION_DEFAULT,"tap://moment/scene/", extras); - -//使用设置的默认方向 -TapMoment.directlyOpen("tap://moment/scene/", extras); - -``` - -#### 参数说明 - -| 参数 | 说明 | -| ----------- | ---------------------------------------------------- | -| orientation | 打开方向 | -| page | 固定为 `tap://moment/scene/` | -| HashMap | 集合中的 Key 固定为 `scene_id` 表示需要跳转的页面 id | - - -<> - -```objectivec -TapMomentConfig *mConfig = TapMomentConfig.new; -mConfig.orientation = TapMomentOrientationDefault; -//使用指定方向 -[TapMoment directlyOpen:mConfig page:TapMomentPageShortcut extras:@{ TapMomentPageShortcutKey: @"sceneid" }]; -//使用设置的默认屏幕方向 -[TapMoment directlyOpen:TapMomentPageShortcut extras:@{ TapMomentPageShortcutKey: @"sceneid" }]; -``` - -#### 参数说明 - -| 参数 | 说明 | -| ----------- | ---------------------------------------------------------- | -| orientation | 打开方向 | -| page | 固定为 TapMomentPageShortcut | -| Dictionary | TapMomentPageShortcutKey 为固定写法,表示需要跳转的页面 id | - - - -<> - -```cpp -TSharedPtr JsonObject = MakeShared(); -// 注意:这里的 key 是固定的,FTapMomentPage::ShortCutKey;第二个参数是开发者中心后台创建场景化入口后生成的「入口 ID」 -JsonObject->SetStringField(FTapMomentPage::ShortCutKey, "xxxx"); -// 注意:第二个参数固定为 FTapMomentPage::ShortCut -FTapMoment::DirectlyOpen(ETapMomentOrientation::DEFAULT, FTapMomentPage::ShortCut, JsonObject); -``` - - -| 参数 | 说明 | -| ----------- | ------------------------------------------------------------------------------------ | -| Orientation | 打开方向 | -| Page | 固定为 FTapMomentPage::ShortCut | -| Extras | 其中 FTapMomentPage::ShortCutKey 固定,第三个参数为需要跳转的页面 id | - - - - - -### 场景化入口回调格式说明 - -**SDK 回调结构** - -| 字段名 | 值类型 | required | 说明 | -| ------------ | ------ | -------- | ----------------------------------------- | -| sceneId | 字符串 | 是 | 场景化入口 ID | -| eventType | 字符串 | 是 | 枚举的事件类型,如 VIEW,FORWARD,VOTE 等 | -| eventPayload | 字符串 | 是 | 根据类型自定义的 JSON 字符串 | -| timestamp | 整数 | 是 | unix 时间戳,ms | - -**事件类型** - -| eventType | eventPayload (未序列化) | 说明 | -| --------- | ----------------------- | ----------------------------------------------- | -| READY | {} | 场景化页面已打开 | -| REPOST | {} | 转发,仅帖子本身 | -| VOTE | { isCancel: boolean } | 点赞(含是否取消),仅帖子本身 | -| FOLLOW | { isCancel: boolean } | 关注(含是否取消),仅帖子本身 | -| COMMENT | {} | 评论,仅帖子本身 | - -## 关闭动态页面 - -玩家可以在动态页面退出。 -但在特定场景下,游戏可能需要主动关闭动态页面。 - -比如,玩家排位等待结束,准备进入对局时提示玩家关闭动态页面,玩家确认后关闭。 - - - -```cs -TapMoment.Close("提示", "匹配成功,进入游戏"); -``` - -```java -TapMoment.closeWithConfirmWindow("提示", "匹配成功,进入游戏"); -``` - -```objectivec -[TapMoment closeWithTitle:@"提示" content:@"匹配成功,进入游戏" showConfirm:YES]; -``` - -```cpp -FTapMoment::Close(TEXT("提示"), TEXT("匹配成功,进入游戏")); -``` - - - - -用户的选择会通过回调返回: - -- `CALLBACK_CODE_CLOSE_CANCEL`(50000),表示玩家点了「取消」,选择不关闭动态页面。 -- `CALLBACK_CODE_CLOSE_CONFIRM`(50100),表示玩家点了「确认」,选择关闭动态页面。 - -如果需要直接关闭动态窗口,不弹出二次确认框: - - - -```cs -TapMoment.Close(); -``` - -```java -TapMoment.close(); -``` - -```objectivec -[TapMoment close]; -``` - -```cpp -FTapMoment::Close(); -``` - - - -## 一键发布 - -:::info - -这是可选功能,请根据项目需求决定是否在游戏中加入这一功能。 - -::: - -我们推荐游戏让玩家直接在动态页面发布新动态。 -不过,SDK 也提供了发布图文动态的 API,以支持「一键发动态」等需求。 -图文动态包括单张或多张图片及相应的文字内容。 - - - -```cs -string content = "我是描述"; -string[] images = {"imgpath01","imgpath02","imgpath03"}; -//使用指定屏幕方向 -TapMoment.Publish(Orientation.ORIENTATION_LANDSCAPE, images, content); -//使用默认方向 -TapMoment.Publish(images, content); -``` - -```java -int orientation = TapMoment.ORIENTATION_PORTRAIT; -String content = "描述"; -String[] imagePaths = new String[]{"content://hello.jpg", "/sdcard/world.jpg"}; -//使用指定屏幕方向 -TapMoment.publish(orientation, imagePaths, content); -//使用默认方向 -TapMoment.publish(imagePaths, content); -``` - -```objectivec -TapMomentConfig * tconfig = TapMomentConfig.new; -tconfig.orientation = TapMomentOrientationDefault; - -TapMomentImageData *postData = TapMomentImageData.new; -postData.images = @[@"file://..."]; -postData.content = @"我是图片描述"; -//使用指定屏幕方向 -[TapMoment publish:tconfig content:(postData)]; -//使用默认方向 -[TapMoment publish:(postData)]; -``` - -```cpp -TArray ImagePaths = {"imgpath01", "imgpath02", "imgpath03"}; -FTapMoment::Publish(ETapMomentOrientation::PORTRAIT, ImagePaths, TEXT("我是描述")); -``` - - - -:::info - -玩家在动态页面可以发布图文动态和视频动态。 -「一键发布」只支持发布图文动态。 - -::: - -## 国际化 - -内嵌动态支持设置语言: - - - -## 单纯的内嵌动态初始化 - -这里 SDK 获取及初始化,仅供使用 [单纯的 TapTap 登录](/sdk/taptap-login/guide/tap-login/) 的开发者参考。 - -1. 请先 [下载](/tap-download) TapSDK,并添加相关依赖。内嵌动态功能依赖 `TapLogin`、`TapCommon` 和 `TapMoment` 模块。 - -TapSDK Unity v3.7.1 及更高版本还需要添加 `com.leancloud.storage` 模块。Android SDK 及 iOS SDK 不需要额外增加依赖。 - - - - - {`"dependencies":{ - "com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.moment":"https://github.com/TapTap/TapMoment-Unity.git#${sdkVersions.taptap.unity}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -}`} - - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') // 必选:TapSDK 基础库 - implementation (name:'TapLogin_${sdkVersions.taptap.android}', ext:'aar') // 必选:TapTap 登录 - implementation (name:'TapMoment_${sdkVersions.taptap.android}', ext:'aar') // TapTap 内嵌动态 -}`} - - - - {`// 基础库 -TapCommonSDK.framework -TapLoginSDK.framework -TapCommonResource.bundle -TapLoginResource.bundle -// 内嵌动态 -TapMomentResource.bundle -TapMomentSDK.framework -`} - - - - {`// 基础库 -TapCommon -TapLogin -// 内嵌动态 -TapMoment -`} - - - - -2. 请确认完成了 [单纯 TapTap 登录的初始化](/sdk/taptap-login/guide/tap-login/#初始化)。 - -3. 完成内嵌动态功能的**初始化**,示例如下: - - -<> - -```cs -// 适用于中国大陆 -TapMoment.Init(string clientID); - -// 适用于其他国家或地区 isCN 为 true 表示中国大陆,false 表示其他国家或地区 -TapMoment.Init(string clientID, boolean isCN); -``` - -**参数说明** - -| 参数 | 描述 | -| -------- | --------------------------------------- | -| clientID | TapTap 开发者中心对应游戏的 Client ID。 | - - -<> - -```java -// 适用于中国大陆 -TapMoment.init(Context context, String clientID); - -// 适用于其他国家或地区 isCN 为 true 表示中国大陆,false 表示其他国家或地区 -TapMoment.init(Context context, String clientID, boolean isCN); -``` - -**参数说明** - -| 参数 | 描述 | -| -------- | --------------------------------------- | -| context | 上下文,一般是当前 Application。 | -| clientID | TapTap 开发者中心对应游戏的 Client ID。 | - - -<> - -```objectivec -// 适用于中国大陆 -[TapMoment initWithClientID:@"your clientId"]; - -// 适用于其他国家或地区 isCN 为 true 表示中国大陆,false 表示其他国家或地区 -[TapMoment initWithClientID:@"your clientId" isCN:isCN]; -``` - -**参数说明** - -| 参数 | 描述 | -| -------- | ------------------------------------- | -| clientId | TapTap 开发者中心对应应用的 Client ID | - - - -<> - -```cpp -FTapMomentConfig Config; -Config.RegionType = ERegionType::Global; -Config.ClientID = "your clientId"; -FTapMoment::Init(Config); -``` - -**参数说明** - -| 参数 | 描述 | -| -------- | --------------------------------------- | -| Config | 其中 ClientID 是 TapTap 开发者中心对应游戏的 Client ID。 | - - - - - -4. 使用本篇文档中提到的其他接口。 diff --git a/docs/sdk/gamesaves/_category_.json b/docs/sdk/gamesaves/_category_.json deleted file mode 100644 index ffa2f1c37..000000000 --- a/docs/sdk/gamesaves/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "云存档", - "collapsed": true, - "position": 15.2 -} diff --git a/docs/sdk/gamesaves/features.mdx b/docs/sdk/gamesaves/features.mdx deleted file mode 100644 index f38d3a68b..000000000 --- a/docs/sdk/gamesaves/features.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: 云存档功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -TDS 云存档为你提供了一种将玩家的游戏进程保存到 TDS 服务器的云服务。你的游戏可以检索已保存的游戏数据,允许玩家从任何设备上的任意一个保存点继续游戏。 - -云存档服务可以跨多个设备同步玩家的游戏数据。例如,如果你有一款安卓上的游戏,你可以使用 TDS 云存档让玩家在其他安卓手机上开始游戏,然后在平板电脑上继续玩,而不丢失任何进度。即使玩家的设备丢失、损坏或购买了新型号,TDS 云存档还是可以确保玩家的游戏从停止的地方继续进行。 - -要了解如何为你的游戏实现云存档,请参阅[开发指南](/sdk/gamesaves/guide/)。 - -## 基础知识 - -游戏的云存档分为两个部分: - -- 一个二进制的文件,即游戏存档,你的游戏负责解析和写入它。 -- [元数据](/sdk/gamesaves/features#云存档保存的游戏元数据),呈现游戏存档中一系列有用的信息。 - -## 存档封面 - -存档封面属于元数据,强烈建议你将代表存档的图片与相应的存档文件相关联。 - -## 存档描述 - -你可以为游戏存档提供简短的文字说明。这个文字说明是直接显示给玩家的,应该示意游戏存档所代表的状态。例如,「在天台与黑衣人决斗」。 - -## 冲突解决 - -使用 TDS 云服务时,你的游戏在尝试保存数据时可能会遇到冲突。当玩家在不同的设备或计算机上运行多个应用程序实例时,这种冲突就有可能发生。你的应用程序必须能够以提供最佳用户体验的方式解决这些冲突。 - -通常,当你的应用程序实例在尝试加载或保存数据时无法访问 TDS 云存档服务时,就会发生数据冲突。避免数据冲突的最佳方法是在应用程序启动或恢复时始终从服务加载最新数据,并以合理的频率将数据保存到服务中。你的应用程序应尽一切努力处理冲突,以便保留玩家的数据并让他们获得良好的体验。 - -## 云存档保存的游戏元数据 - -已保存游戏的结构化元数据包含以下属性: - -| 字段含义 | 字段名 | 是否可选 | 说明 | -| ---------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------- | -| 存档 ID | `objectId` | 自动生成 | 云存档云端为游戏存档生成的唯一字符串。使用此 ID 来引用你游戏客户端中保存的游戏。 | -| 关联用户 | `user` | 自动获取 | 云存档 SDK 自动获取当前用户信息做绑定。 | -| 存档原文件 | `gameFile` | 必须 | 开发者提供的存档原文件 | -| 存档名称 | `name` | 必须 | 开发者为游戏存档提供的简称,存档名称不对玩家展示。特殊情况下存档名称可以自定规则来完成「存档组」的概念。 | -| 存档描述 | `summary` | 必须 | **长度不超过 1000** 的字符串。开发者提供已保存游戏存档的描述,这往往也作为展示给玩家的实际存档名。 | -| 修改时间 | `modifiedAt` | 必须 | 存档原文件的修改时间或添加时间。 | -| 游戏时长 | `playedTime` | 可选 | 开发者提供存档的游戏时间(以毫秒为单位)。 | -| 游戏进度 | `progressValue` | 可选 | 开发者提供的游戏进度信息(整数),例如当前进度是第几关。 | -| 存档封面 | `cover` | 可选 | 开发者提供的[图片](/sdk/gamesaves/features#存档封面),目前 SDK 只允许上传 PNG / JPG 格式的图片。 | diff --git a/docs/sdk/gamesaves/guide.mdx b/docs/sdk/gamesaves/guide.mdx deleted file mode 100644 index a17cc2c28..000000000 --- a/docs/sdk/gamesaves/guide.mdx +++ /dev/null @@ -1,497 +0,0 @@ ---- -title: 云存档开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; - -阅读此文档前请先阅读[云存档功能介绍](/sdk/gamesaves/features/),了解云存档的核心概念及功能。 -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下权限: - -``` - -``` - - - -<> - - - -<> - - - - -## 集成前准备 - -接入云存档的前提是: - -1. 绑定[域名](/sdk/start/get-ready/#域名),包括 API 域名和文件域名。 - -2. 云存档需要依赖于内建账户,因此需要通过开发者中心开通 **TDS 内建账户**。游戏存档会绑定在 TDSUser 下,所以请首先参考[内建账户系统](/sdk/authentication/guide/)集成文档完成[ SDK 获取](/sdk/authentication/guide/#sdk-获取) 、[SDK 初始化](/sdk/authentication/guide/#sdk-初始化)以及登录功能的接入。 - -## 创建存档 - -SDK 会自动获取当前登录玩家(TDSUser)信息,关联到存档上。 -因此,用户已登录时才能创建存档。 - - - -```cs -var gameSave = new TapGameSave -{ - Name = "internal name", - Summary = "description", - ModifiedAt = DateTime.Now.ToLocalTime(), - PlayedTime = 60000L, // ms - ProgressValue = 100, - CoverFilePath = image_local_path, // jpg/png - GameFilePath = dll_local_path -}; -await gameSave.Save(); -``` - -```java -TapGameSave snapshot = new TapGameSave(); -snapshot.setName("internal name"); -snapshot.setSummary("description"); -snapshot.setPlayedTime(60000); // ms -snapshot.setProgressValue(100); -snapshot.setCover(image_local_path); // jpg/png -snapshot.setGameFile(dll_local_path); -snapshot.setModifiedAt(new Date()); -snapshot.saveInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable d) {} - - @Override - public void onNext(@NotNull TapGameSave gameSave) { - System.out.println("存档保存成功:" + gameSave.toJSONString()); - } - - @Override - public void onError(@NotNull Throwable e) { - e.printStackTrace(); - } - - @Override - public void onComplete() {} -}); -``` - -```objc -TapGameSave *gameSave = [TapGameSave new]; -gameSave.name = @"internal name"; -gameSave.summary = @"description"; -gameSave.modifiedAt = [NSDate date]; -gameSave.playedTime = 60000; // ms -gameSave.progressValue = 100; -[gameSave setCoverWithLocalPath:@"image_local_path" error:&error]; // jpg/png -[gameSave setGameFileWithLocalPath:@"dll_local_path" error:&error]; -[gameSave saveInBackgroundWithBlock:^(BOOL succeeded, NSError *_Nullable error) { - if (succeeded) { - NSLog(@"保存成功。objectId:%@", gameSave.objectId); - } else { - // 异常处理 - } -}]; -``` - - - -上面的例子中,存档元信息字段的含义请参考[云存档保存的游戏元数据](/sdk/gamesaves/features#云存档保存的游戏元数据)。 -保存时,SDK 会限制仅当前玩家可以读写存档本身及关联的存档文件、封面文件。 - -## 查询用户存档 - -最常见的场景是获取当前玩家的所有存档: - - - -<> - -```cs -var collection = await TapGameSave.GetCurrentUserGameSaves(); - -foreach(var gameSave in collection){ - var summary = gameSave.Summary; - var modifiedAt = gameSave.ModifiedAt; - var playedTime = gameSave.PlayedTime; - var progressValue = gameSave.ProgressValue; - var coverFile = gameSave.Cover; - var gameFile = gameSave.GameFile; - var gameFileUrl = gameFile.Url; -} -``` - -`gameFile.Url` 是保存在云端的存档文件的下载地址,通过这个 URL 下载的文件格式和上传时的格式保持一致。 - - -<> - -```java -TapGameSave.getCurrentUserGameSaves() - .subscribe(new Observer>() { - @Override - public void onSubscribe(@NotNull Disposable d) {} - - @Override - public void onNext(@NotNull List tapGameSaves) { - for (TapGameSave gameSave : tapGameSaves) { - String summary = gameSave.getSummary(); - Date modifiedAt = gameSave.getModifiedAt(); - double playedTime = gameSave.getPlayedTime(); - int progressValue = gameSave.getProgressValue(); - LCFile cover = gameSave.getCover(); - LCFile gameFile = gameSave.getGameFile(); - } - } - - @Override - public void onError(@NotNull Throwable e) { - e.printStackTrace(); - } - - @Override - public void onComplete() {} -}); -``` - - -<> - -```objc -LCQuery *query = [TapGameSave queryWithCurrentUser]; -[query findObjectsInBackgroundWithBlock:^(NSArray *_Nullable gameSaves, NSError *_Nullable error) { - if (error) { - NSLog(@"test fail because %@", error); - } else { - for (TapGameSave *gameSave in gameSaves) { - NSString *summary = gameSave.summary; - NSDate *modifiedAt = gameSave.modifiedAt; - double playedTime = gameSave.playedTime; - int progressValue = gameSave.progressValue; - LCFile* cover = gameSave.cover; - LCFile* gameFile = gameSave.gameFile; - } - } -}]; -``` - - - - - -当然也可以查询满足特定条件的存档,比如查询当前玩家游戏进度超过第 3 关的存档: - - - -<> - -```cs -TDSUser user = await TDSUser.GetCurrent(); -LCQuery gameSaveQuery = TapGameSave.GetQueryWithUser(user); -gameSaveQuery.WhereGreaterThan("progressValue", 3); -var collections = await gameSaveQuery.Find(); -``` - -查询条件的构造请参考[数据存储指南查询章节的说明](/sdk/storage/guide/dotnet/)。 - - -<> - -```java -LCQuery gameSaveQuery = TapGameSave.getQueryWithUser(); -gameSaveQuery.whereGreaterThan("progressValue", 3); -gameSaveQuery.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List gamesaves) { - // gamesaves 是包含满足条件的云存档对象的数组 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -查询条件的构造请参考[数据存储指南查询章节的说明](/sdk/storage/guide/java/)。 - - -<> - -```objc -LCQuery *query = [TapGameSave queryWithCurrentUser]; -[query whereKey:@"progressValue" greaterThan:@3]; -[query findObjectsInBackgroundWithBlock:^(NSArray *_Nullable gameSaves, NSError *_Nullable error) { - // 略 -}]; -``` - - - - - -注意,**如果之前整个游戏的任意玩家从未保存过存档,那么查询存档会报错** `Class or object doesn't exists.` -这是因为云存档在底层数据库层面的表(Class)只在保存第一个存档时才会创建。 - -## 删除用户存档 - -玩家只能删除自己的存档。 - -删除存档: - - - -```cs -await gameSave.Delete(); -``` - -```java -gameSave.deleteInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) {} - - @Override - public void onNext(LCNull response) { - // Deleted. - } - - @Override - public void onError(@NonNull Throwable e) { - System.out.println("Failed to delete:" + e.getMessage()); - } - - @Override - public void onComplete() {} -}); -``` - -```objc -[gameSave deleteInBackground]; -``` - - - -删除存档时云端会自动删除关联的封面文件和存档原文件。 - -## REST API - -下面我们介绍云存档相关的 REST API 接口。 -开发者可以自行编写程序或脚本调用这些接口在服务端进行管理性质的操作。 - -### 请求格式 - -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 - -请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,参数如下表: - -| Key | Value | 含义 | 来源 | -| -------------- | ---------------- | ----------------------------------------- | -------------- | -| `X-LC-Id` | `{{appid}}` | 当前应用的 `App Id`(即 `Client Id`) | 可在控制台查看 | -| `X-LC-Key` | `{{appkey}}` | 当前应用的 `App Key`(即 `Client Token`) | 可在控制台查看 | -| `X-LC-Session` | `` | 玩家的登录凭证 | | - -管理接口需要使用 `Master Key`:`X-LC-Key: {{masterkey}},master`。 -`Master Key` 即 `Server Secret`,同样可在控制台查看。使用管理接口时无需携带 `sessionToken`。 - -详见文档关于[应用凭证](/sdk/storage/guide/setup-dotnet#应用凭证)的说明。 - -云存档限制只有添加存档的玩家本人可读可写,因此调用云存档的 REST API 接口时均需在 `X-LC-Session` HTTP 头中携带玩家的 `sessionToken` 或使用 `Master Key`,否则请求会因权限不足而失败。 - -### Base URL - -REST API 请求的 Base URL(下文 curl 示例中用 `{{host}}` 表示)即应用绑定的 API 自定义域名,可以在控制台绑定、查看。 -详见文档关于[域名](/sdk/storage/guide/setup-dotnet#域名)的说明。 - -### 接口列表 - -| 接口名称 | 接口请求方法 | 接口地址 | 接口描述 | -| -------- | ------------ | ---------------- | ---------------------- | -| 获取存档 | GET | `/gamesaves/:id` | 根据 id 来获取存档记录 | -| 查询存档 | GET | `/gamesaves` | 根据查询条件查询存档 | -| 添加存档 | POST | `/gamesaves` | 增加新存档 | -| 更新存档 | PUT | `/gamesaves/:id` | 根据 id 更新存档 | -| 删除存档 | DELETE | `/gamesaves/:id` | 根据 id 删除文档 | - -### 获取存档 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - https://API_BASE_URL/1.1/gamesaves/ -``` - -返回示例: - -```json -{ - "updatedAt": "2021-08-16T09:18:30.093Z", - "progressValue": 123, - "name": "dennis", - "objectId": "611a2d65bcf94a3222b6d5f3", - "createdAt": "2021-08-16T09:18:29.761Z", - "gameFile": { - "__type": "Pointer", - "className": "_File", - "objectId": "60d1af149be3180684000002" - }, - "summary": "hello", - "modifiedAt": { - "__type": "Date", - "iso": "2015-06-21T18:02:52.249Z" - }, - "user": { - "__type": "Pointer", - "className": "_User", - "objectId": "5b62c15a9f54540062427acc" - } -} -``` - -各个字段的含义请参考[云存档保存的游戏元数据](/sdk/gamesaves/features#云存档保存的游戏元数据)。 - -### 查询存档 - -可以通过 `where` 参数指定查询条件查询存档: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"progressValue":123}' \ - https://API_BASE_URL/1.1/gamesaves -``` - -返回示例: - -```json -{ - "results": [ - { - "updatedAt": "2021-08-16T09:30:20.643Z", - "name": "dennis", - "createdAt": "2021-08-16T09:30:20.643Z", - "gameFile": { - "__type": "Pointer", - "className": "_File", - "objectId": "60d1af149be3180684000002" - }, - "summary": "hello", - "modifiedAt": { - "__type": "Date", - "iso": "2015-06-21T18:02:52.249Z" - }, - "objectId": "611a302cbcf94a3222b6d687" - } - ] -} -``` - -`where` 的用法详见[数据存储 REST API 指南](/sdk/storage/guide/rest#查询约束) - -### 添加存档 - -添加存档时的必填字段和可选字段请参考[云存档保存的游戏元数据](/sdk/gamesaves/features#云存档保存的游戏元数据)。 - -在调用添加存档接口前,请先创建 `gameFile` 和 `cover` 指向的[文件](/sdk/storage/guide/rest/#创建文件),并**确保文件的 ACL 为仅限当前用户读取。** - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - -d '{ - "progressValue":123, - "playedTime":1283490343, - "name":"dennis", - "gameFile":{"id":"55a39634e4b0ed48f0c1845c", "__type":"File"}, - "cover":{"id": "543cbaede4b07db196f50f3c", "__type": "File"}, - "summary":"hello", - "modifiedAt":{"__type":"Date", "iso":"2015-06-21T18:02:52.249Z"} - }' \ -https://API_BASE_URL/1.1/gamesaves -``` - -成功创建时会返回 objectId 和创建时间: - -``` -{"objectId":"611a3407bcf94a3222b6d789", "createdAt":"2021-08-16T09:46:47.290Z"} -``` - -失败时会报错,例如: - -- `gameFile is required.`:遗漏了必填字段 `gameFile`。 -- `Forbidden to add new fields by class '_GameSave' permissions.`:提交了非法字段,云存档目前暂不支持添加自定义字段。 - -### 删除存档 - -``` -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - https://API_BASE_URL/1.1/gamesaves/ -``` - -会返回 - -```json -{} -``` - -删除存档时也可以附加 `where` 条件,防止误删。 -参见[有条件删除对象](/sdk/storage/guide/rest#有条件删除对象)。 - -删除存档时云端会自动删除关联的封面文件和存档原文件。 - -### 更新存档 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - -d '{"progressValue": 114514}' \ - https://API_BASE_URL/1.1/gamesaves/ -``` - -成功时返回 objectId 和更新时间: - -```json -{ - "updatedAt": "2021-08-16T09:49:49.579Z", - "objectId": "611a34bdbcf94a3222b6d7af" -} -``` - -失败时会报错,例如: - -- `Forbidden to add new fields by class '_GameSave' permissions.`:提交了非法字段,云存档目前暂不支持添加自定义字段。 - -注意,更新封面文件和存档原文件后,原本关联的封面文件和存档原文件并不会一并删除。 -这些文件需要另外[删除](/sdk/storage/guide/rest#删除文件)。 -所以一般建议通过删除存档后重新创建存档的方式「更新」存档,这个更新存档的接口主要供服务端在一些管理场景下使用。 diff --git a/docs/shadow/im/_category_.json b/docs/sdk/im/_category_.json similarity index 100% rename from docs/shadow/im/_category_.json rename to docs/sdk/im/_category_.json diff --git a/docs/shadow/im/best-practice/_category_.json b/docs/sdk/im/best-practice/_category_.json similarity index 100% rename from docs/shadow/im/best-practice/_category_.json rename to docs/sdk/im/best-practice/_category_.json diff --git a/docs/shadow/im/best-practice/hook-text-moderation.mdx b/docs/sdk/im/best-practice/hook-text-moderation.mdx similarity index 100% rename from docs/shadow/im/best-practice/hook-text-moderation.mdx rename to docs/sdk/im/best-practice/hook-text-moderation.mdx diff --git a/leancloud/docs/sdk/im/best-practice/realtime-guide-onoff-status.mdx b/docs/sdk/im/best-practice/realtime-guide-onoff-status.mdx similarity index 100% rename from leancloud/docs/sdk/im/best-practice/realtime-guide-onoff-status.mdx rename to docs/sdk/im/best-practice/realtime-guide-onoff-status.mdx diff --git a/docs/shadow/im/features.mdx b/docs/sdk/im/features.mdx similarity index 100% rename from docs/shadow/im/features.mdx rename to docs/sdk/im/features.mdx diff --git a/.ci/hk/en/sdk/TapPayments/develop/_category_.json b/docs/sdk/im/guide/_category_.json similarity index 100% rename from .ci/hk/en/sdk/TapPayments/develop/_category_.json rename to docs/sdk/im/guide/_category_.json diff --git a/docs/shadow/im/guide/beginner.mdx b/docs/sdk/im/guide/beginner.mdx similarity index 100% rename from docs/shadow/im/guide/beginner.mdx rename to docs/sdk/im/guide/beginner.mdx diff --git a/docs/shadow/im/guide/intermediate.mdx b/docs/sdk/im/guide/intermediate.mdx similarity index 100% rename from docs/shadow/im/guide/intermediate.mdx rename to docs/sdk/im/guide/intermediate.mdx diff --git a/docs/shadow/im/guide/overview.mdx b/docs/sdk/im/guide/overview.mdx similarity index 100% rename from docs/shadow/im/guide/overview.mdx rename to docs/sdk/im/guide/overview.mdx diff --git a/docs/shadow/im/guide/rest.mdx b/docs/sdk/im/guide/rest.mdx similarity index 100% rename from docs/shadow/im/guide/rest.mdx rename to docs/sdk/im/guide/rest.mdx diff --git a/docs/shadow/im/guide/senior.mdx b/docs/sdk/im/guide/senior.mdx similarity index 100% rename from docs/shadow/im/guide/senior.mdx rename to docs/sdk/im/guide/senior.mdx diff --git a/docs/shadow/im/guide/systemconv.mdx b/docs/sdk/im/guide/systemconv.mdx similarity index 100% rename from docs/shadow/im/guide/systemconv.mdx rename to docs/sdk/im/guide/systemconv.mdx diff --git a/leancloud/docs/sdk/im/im-faq.mdx b/docs/sdk/im/im-faq.mdx similarity index 100% rename from leancloud/docs/sdk/im/im-faq.mdx rename to docs/sdk/im/im-faq.mdx diff --git a/leancloud/docs/sdk/im/server/_category_.json b/docs/sdk/im/server/_category_.json similarity index 100% rename from leancloud/docs/sdk/im/server/_category_.json rename to docs/sdk/im/server/_category_.json diff --git a/leancloud/docs/sdk/im/server/python.mdx b/docs/sdk/im/server/python.mdx similarity index 100% rename from leancloud/docs/sdk/im/server/python.mdx rename to docs/sdk/im/server/python.mdx diff --git a/leancloud/docs/sdk/im/ui-library/_category_.json b/docs/sdk/im/ui-library/_category_.json similarity index 100% rename from leancloud/docs/sdk/im/ui-library/_category_.json rename to docs/sdk/im/ui-library/_category_.json diff --git a/leancloud/docs/sdk/im/ui-library/android.mdx b/docs/sdk/im/ui-library/android.mdx similarity index 100% rename from leancloud/docs/sdk/im/ui-library/android.mdx rename to docs/sdk/im/ui-library/android.mdx diff --git a/docs/sdk/leaderboard/faq.mdx b/docs/sdk/leaderboard/faq.mdx deleted file mode 100644 index a31517444..000000000 --- a/docs/sdk/leaderboard/faq.mdx +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 3 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -### 当两个玩家的成绩相同时,如何通过上传的时间进行设置排名? - -针对这个需求,我们在现有接口基础上提供一个实现方案,使用时间戳 + 分数进行编码生成一个新的数值进行提交成绩,查询排行榜成绩时通过解码可以获取成绩和时间戳;实例可以参考[获取指定区间排名](/sdk/leaderboard/guide/#获取指定区间的排名)中的 Android 请求示例; - -### 查询排行榜时,如何获取用户的头像和昵称? - -Unity 版本查询排行榜时可以通过传入 selectKeys 参数指定返回的 Ranking 中的 user 需要包含的属性,Android 版本可以通过传入 selectMemberKeys 参数进行指定,详细的调用方式可以参考[获取指定区间排名](/sdk/leaderboard/guide/#获取指定区间的排名)文档中的描述; \ No newline at end of file diff --git a/docs/sdk/leaderboard/guide.mdx b/docs/sdk/leaderboard/guide.mdx deleted file mode 100644 index 74960cdb8..000000000 --- a/docs/sdk/leaderboard/guide.mdx +++ /dev/null @@ -1,2557 +0,0 @@ ---- -title: 排行榜指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -:::info - -阅读此文档前请先阅读: - -- [排行榜功能介绍](/sdk/leaderboard/features/),了解排行榜的核心概念及功能。 -- [内建账户指南](/sdk/authentication/guide/),排行榜成员类型有 user、object、entity 三种,这里的 user 指内建账户系统中的用户。另外,本文中的 `currentUser` 指内建账户系统的当前登录用户。 - -::: - -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 - -**支持平台**:Android / iOS - - - - - -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下权限: - -``` - -``` - - - -<> - - - -<> - - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启内建账户、排行榜服务、绑定 API 域名; - -## SDK 获取 - -排行榜功能是数据存储 SDK 的一部分,排行榜 SDK 配置与数据存储 SDK 配置一致; - - - -<> - -SDK 可以**通过 Unity Package Manager 导入或手动导入**,二者任选其一。请根据项目需要选择。 - -#### 方法一:使用 Unity Package Manager - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - - - {`"dependencies":{ - "com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-${sdkVersions.leancloud.csharp}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -}`} - - -在 Unity 顶部菜单中选择 **Window > Package Manager** 可查看已经安装在项目中的包。 - -#### 方法二:手动导入 - -1. 在 [下载页](/tap-download) 找到 **LeanCloud C# SDK** 下载地址,下载 `LeanCloud-SDK-Realtime-Unity.zip`。 - -2. 解压后的 `LeanCloud-SDK-Realtime-Unity.zip` 为 Plugins 文件夹,拖拽至 Unity 即可。 - - - - - -<> - -1. 打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: - - {` -dependencies { - ... - implementation 'com.taptap:lc-storage-android:${sdkVersions.leancloud.java}' -} -`} - - - - -<> - -参考 [Objective-C SDK 配置](/sdk/storage/guide/setup-objc)。 - - - -<> - -* 下载 [TapSDK UE4](/tap-download),TapSDK-UE4-xxx.zip 解压后将 `LeanCloud` 文件夹 Copy 到项目的 `Plugins` 目录中 -* 重启 Unreal Editor -* 打开 编辑 > 插件 > 项目 > LeanCloud,开启 `LeanCloud` 模块 - -#### 添加依赖 - -在 Project.Build.cs 中添加所需模块: - -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", -}); -if (Target.Platform == UnrealTargetPlatform.IOS || Target.Platform == UnrealTargetPlatform.Android) -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - "LeanCloudMobile" - } - ); -} -else -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - "LeanCloud" - } - ); -} -``` - -#### 导入头文件 - -```cpp -#include "LCLeaderboard.h" -``` - - - - - -## SDK 初始化 - -[内建账户 SDK](/sdk/authentication/guide/#sdk-初始化) 初始化完成后会同步初始化排行榜功能,因此无需重复初始化; - - -## 快速入门 - -### 创建排行榜 - -创建排行榜有 3 种方式: - -- 在 TapTap 开发者中心的排行榜[控制台](#控制台)创建。 -- 在**服务端等受信任的环境**下[调用 REST API](#创建排行榜-2) 创建。 -- 在**服务端等受信任的环境**下[调用 SDK 的管理接口](#创建排行榜-1)创建。 - -比如,可以在控制台创建一个名称为 `world`,成员类型为「内建账户」,排序为 `descending`,更新策略为 `better`,重置周期为「每月」的世界排行榜。 - -![create leaderboard](/img/create_leaderboard.png) - -### 提交成绩 - -假设玩家已登录(`currentUser`),通过如下代码即可更新成绩: - - - -```cs -var statistic = new Dictionary(); -statistic["world"] = 20.0; -await LCLeaderboard.UpdateStatistics(currentUser, statistic); -``` - -```java -Map statistic = new HashMap<>(); -statistic.put("world", 20.0); -LCLeaderboard.updateStatistic(currentUser, statistic).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCStatisticResult jsonObject) { - // scores saved - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - } - - @Override - public void onComplete() {} -}); -``` - -```objc -NSDictionary *statistic = @{ - @"world" : 20.0, -}; -[LCLeaderboard updateCurrentUserStatistics:statistic, callback:^(NSArray *statistics, NSError *error) { - if (statistics) { - // statistics 是更新后你的最好/最新成绩 - } else if (error) { - // 处理错误 - } -}]; -``` - -```cpp -TMap Statistic; -Statistic.Add("world", 20.0); -FLCLeaderboard::FStatisticsDelegate OnSuccess = FLCLeaderboard::FStatisticsDelegate::CreateLambda([=](TArray Statistics) { - // statistics 是更新后你的最好/最新成绩 -}); -FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) { - // 处理错误 -}); -FLCLeaderboard::UpdateCurrentUserStatistics(Statistic, OnSuccess, OnError); -``` - - - -### 获取名次 - -接下来我们获取排行榜中的前十,由于之前我们只提交了一个玩家(当前玩家)的成绩,所以结果中只包含一个成绩。 - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("world"); -var rankings = await leaderboard.GetResults(limit: 10); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world"); -leaderboard.getResults(0, 10, null, null).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCLeaderboardResult leaderboardResult) { - List rankings = leaderboardResult.getResults(); - // process rankings - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - } - - @Override - public void onComplete() {} -}); -``` - -```objc -LCLeaderboard *leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"world"]; -leaderboard.limit = 10; -[leaderboard getUserResultsWithOption:nil callback:^(NSArray *rankings, NSInteger count, NSError *error) { - // rankings 是排行榜前十的排名信息 -}]; -``` - -```cpp -FLCLeaderboard Leaderboard("world", FLCLeaderboard::User); -Leaderboard.Limit = 10; -FLCLeaderboard::FRankingsDelegate OnSuccess = FLCLeaderboard::FRankingsDelegate::CreateLambda([=](TArray Rankings, int64 Count) { - // process rankings -}); -FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) { - // handle error -}); -Leaderboard.GetResults(OnSuccess, OnError); -``` - - - -看过上面这个最简单的接入排行榜的例子后,下面就详细介绍排行榜的各个接口。 - -## 成绩管理 - -### 更新用户成绩 - -当用户完成了一局游戏后,你可以在客户端使用 SDK 的 `updateStatistic` 方法更新该用户的成绩。 -不过,**从反作弊的角度出发,我们建议在控制台勾选「只允许使用 Master Key 更新分数」,然后在服务端更新成绩。** - - - -```cs -var statistic = new Dictionary { - { "score", 3458.0 }, - { "kills", 28.0 } -}; -await LCLeaderboard.UpdateStatistics(currentUser, statistic); -``` - -```java -Map statistic = new HashMap<>(); -statistic.put("score", 3458.0); -statistic.put("kills", 28.0); -LCLeaderboard.updateStatistic(currentUser, statistic).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCStatisticResult jsonObject) { - // saved - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - } - - @Override - public void onComplete() {} -}); -``` - -```objc -NSDictionary *statistic = @{ - @"score" : 3458.0, - @"kills" : 28.0, -}; -[LCLeaderboard updateCurrentUserStatistics:statistic callback:^(NSArray *statistics, NSError *error) { - if (!error) { - // saved - } -}]; -``` - -```cpp -TMap Statistic; -Statistic.Add("score", 3458.0); -Statistic.Add("kills", 28.0); -FLCLeaderboard::FStatisticsDelegate OnSuccess = FLCLeaderboard::FStatisticsDelegate::CreateLambda([=](TArray Statistics) { - // saved -}); -FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) { - // handle error -}); -FLCLeaderboard::UpdateCurrentUserStatistics(Statistic, OnSuccess, OnError); -``` - - - -更新用户成绩需要玩家登录,玩家只能更新自己的成绩。 -你可以一次性更新多个排行榜的成绩,比如上面的示例中同时更新了得分(`score`)和击杀(`kills`)两个排行榜的成绩。 - -客户端无法更新 object 和 entity 的成绩。 -如需更新其他用户、object、entity 的成绩,需要使用 [REST API](#更新成绩) 或 [SDK 提供的管理接口](#更新排行榜成员成绩)。 - -### 删除用户成绩 - -玩家也可以删除自己的成绩: - - - -```cs -await LCLeaderboard.DeleteStatistics(currentUser, new List { "world" }); -``` - -```java -// 暂未支持 -``` - -```objc -[LCLeaderboard deleteCurrentUserStatistics:@[@"world"], callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // 删除成功 - } else if (error) { - // 处理错误 - } -}]; -``` - -```cpp -// 暂未支持 -``` - - - -和更新成绩一样,玩家只能删除自己的成绩。 -如需删除其他用户、object、entity 的成绩,需要使用 [REST API](#删除成绩) 或 [SDK 提供的管理接口](#删除排行榜成员成绩)。 - -### 查询排行榜成员成绩 - -**已登录用户**可以通过 `GetStatistics` 方法查询其他用户在所有排行榜中的成绩: - - - -```cs -var otherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "5c76107144d90400536fc88b"); -var statistics = await LCLeaderboard.GetStatistics(otherUser); -foreach(var statistic in statistics) { - Debug.Log(statistic.Name); - Debug.Log(statistic.Value); -} -``` - -```java -// 查询排行榜成员成绩 -LCUser otherUser = null; -try { - otherUser = LCUser.createWithoutData(LCUser.class, "5c76107144d90400536fc88b"); -} catch (LCException e) { - e.printStackTrace(); -} -LCLeaderboard.getUserStatistics(otherUser).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCStatisticResult lcStatisticResult) { - List statistics = lcStatisticResult.getResults(); - for (LCStatistic statistic : statistics) { - Log.d(TAG, statistic.getName()); - Log.d(TAG, String.valueOf(statistic.getValue())); - } - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - Toast.makeText(MainActivity.this, "查询失败: " + throwable.getMessage(), Toast.LENGTH_SHORT).show(); - } - - @Override - public void onComplete() {} -}); -``` - -```objc -NSString *otherUserObjectId = @"5c76107144d90400536fc88b"; -[LCLeaderboard getStatisticsWithUserId:otherUserObjectId, statisticNames:nil, callback:^(NSArray * _Nullable *statistics, NSError _Nullable *error) { - if (statistics) { - for (LCLeaderboardStatistic *statistic in statistics) { - NSLog(@"排行榜名称:%@", statistic.name); - NSLog(@"成绩:%f", statistic.value); - } - } else if (error) { - // 处理错误 - } -}]; -``` - -```cpp -FLCLeaderboard::FStatisticsDelegate OnSuccess = FLCLeaderboard::FStatisticsDelegate::CreateLambda([=](TArray Statistics) { - for (FLCLeaderboardStatistic Statistic : Statistics) { - // 排行榜名称:Statistic.Name; - // 成绩:Statistic.Value; - } -}); -FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) { - // 处理错误 -}); - -FString OtherUserObjectId = "5c76107144d90400536fc88b"; -FLCLeaderboard::GetStatistics(OtherUserObjectId, OnSuccess, OnError, {}, FLCLeaderboard::User); -``` - - - -更多的场景下,只关心用户在某个或某几个排行榜中的成绩。 -查询时指定相应排行榜的名称即可: - - - -```cs -var statistics = await LCLeaderboard.GetStatistics(otherUser, new List { "world" }); -``` - -```java -LCLeaderboard.getUserStatistics(otherUser, Arrays.asList("world")).subscribe(/** 略 **/); -``` - -```objc -[LCLeaderboard getStatisticsWithUserId:otherUserObjectId, statisticNames:@[@"world"], callback:^(NSArray * _Nullable statistics, NSError * _Nullable error) { - // 略 -}]; -``` - -```cpp -FLCLeaderboard::GetStatistics(OtherUserObjectId, OnSuccess, OnError, {"world"}, FLCLeaderboard::User); -``` - - - -类似地,可以查询 object 和 entity 的成绩。 - - -<> - -例如,假定武器排行榜是 object 排行榜: - -```cs -var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b"); -var statistics = await LCLeaderboard.GetStatistics(excalibur, new List { "weapons" }); -``` - -如果武器排行榜是 entity 排行榜: - -```cs -var statistics = await LCLeaderboard.GetStatistics("excalibur", new List { "weapons" }); -``` - - -<> - -例如,假定武器排行榜是 object 排行榜: - -```java -String excaliburObjectId = "582570f38ac247004f39c24b"; -LCLeaderboard.getMemberStatistics("Weapon", excaliburObjectId, - Arrays.asList("weapons")).subscribe(/** 略 **/); -``` - -如果武器排行榜是 entity 排行榜: - -```java -LCLeaderboard.getMemberStatistics(LCLeaderboard.MEMBER_TYPE_ENTITY, "excalibur", - Arrays.asList("weapons")).subscribe(/** 略 **/); -``` - -顺便提下,之前提到的 `getUserStatistics` 方法 - -```java -LCLeaderboard.getUserStatistics(otherUser, Arrays.asList("weapons")).subscribe(/** 略 **/); -``` - -等价于: - -```java -LCLeaderboard.getMemberStatistics(LCLeaderboard.LCLeaderboard.MEMBER_TYPE_USER, - otherUser.getObjectId(), - Arrays.asList("weapons")).subscribe(/** 略 **/); -``` - - -<> - -例如,假定武器排行榜是 object 排行榜: - -```objc -NSString *excaliburObjectId = @"582570f38ac247004f39c24b"; -[LCLeaderboard getStatisticsWithObjectId:excaliburObjectId, statisticNames:@[@"weapons"], - option:nil - callback:^(NSArray *statistics, NSError *error) { - // 略 -}]; -``` - -如果武器排行榜是 entity 排行榜: - -```objc -[LCLeaderboard getStatisticsWithEntity:@"excalibur", statisticNames:@[@"weapons"], - callback:^(NSArray * _Nullable *statistics, NSError * _Nullable error) { - // 略 -}]; -``` - - - -<> - -例如,假定武器排行榜是 object 排行榜: - -```cpp -FString ExcaliburObjectId = "582570f38ac247004f39c24b"; -FLCLeaderboard::GetStatistics(ExcaliburObjectId, OnSuccess, OnError, {}, FLCLeaderboard::Object); -``` - -如果武器排行榜是 entity 排行榜: - -```cpp -FLCLeaderboard::GetStatistics("excalibur", OnSuccess, OnError, {}, FLCLeaderboard::Entity); -``` - - - - - -最后,还可以查询一组成员的成绩: - - - -```cs -var otherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "5c76107144d90400536fc88b"); -var anotherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "672a127144a90d00536f3456"); -var statistics = await LCLeaderboard.GetStatistics({otherUser, anotherUser}, new List { "world" }); - -var oneObject = LCObject.CreateWithoutData("abccb27133a90ddd536ffffa"); -var anotherUser = LCObject.CreateWithoutData("672a1279345777005a2b2444"); -var statistics = await LCLeaderboard.GetStatistics({oneObject, anotherObject}, new List { "weapons" }); - -var statistics = await LCLeaderboard.GetStatistics({"Sylgr", "Leiptr"}, new List { "rivers" }); -``` - -```java -// 暂未支持 -``` - -```objc -NSString *otherUserObjectId = @"5c76107144d90400536fc88b"; -NSString *anotherUserObjectId = @"672a127144a90d00536f3456"; -[leaderboard getStatisticsWithUserIds:@[otherUserObjectId, anotherUserObjectId] - callback:^(NSArray * _Nullable statistics, NSError * _Nullable error) { -// 略 -}]; - -NSString *oneObjectId = @"abccb27133a90ddd536ffffa"; -NSString *anotherObjectId = @"672a1279345777005a2b2444"; -[leaderboard getStatisticsWithObjectIds:@[oneObjectId, anotherObjectId] - option:nil - callback:^(NSArray * _Nullable statistics, NSError * _Nullable error) { -// 略 -}]; - -[leaderboard getStatisticsWithEntities:@[@"Sylgr", "Leiptr"] - callback:^(NSArray * _Nullable statistics, NSError * _Nullable error) { -// 略 -}]; -``` - -```cpp -// 暂未支持 -``` - - - -## 获取排行榜结果 - -通过 `Leaderboard#getResults` 方法可以获取排行榜结果。 -最常见的使用场景是获取排名前 N 的用户成绩和获取当前登录用户附近的排名。 - -首先,我们构造一个排行榜实例: - - - -<> - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("world"); -``` - -`LCLeaderboard.CreateWithoutData` 方法接受两个参数: - -```cs -public static LCLeaderboard CreateWithoutData(string statisticName, string memberType = LCLeaderboard.USER_MEMBER_TYPE) -``` - -- `statisticName` 为排行榜名称,这里需要传入云端已经存在的排行榜名称。上例中是 `world`。 -- `memberType` 为排行榜成员类型,传入 `LCLeaderboard.USER_MEMBER_TYPE` 表示成员类型为用户,传入 `LCLeaderboard.ENTITY_MEMBER_TYPE` 表示成员类型为 entity,成员类型为 object 时请传入相应 Class 名称。上例中省略了这一参数,表示使用默认值,成员类型为用户。 - - -<> - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world"); -``` - -`LCLeaderboard.createWithoutData` 方法接受两个参数: - -```java -public static LCLeaderboard createWithoutData(String name, String memberType) -``` - -- `name` 为排行榜名称,这里需要传入云端已经存在的排行榜名称。上例中是 `world`。 -- `memberType` 为排行榜成员类型,传入 `LCLeaderboard.MEMBER_TYPE_USER` 表示成员类型为用户,传入 `LCLeaderboard.MEMBER_TYPE_ENTITY` 表示成员类型为 entity,成员类型为 object 时请传入相应 Class 名称。用户是最常用的排行榜类型,因此 `createWithoutData` 还提供了一个单参数的重载方法,上例中就只传入了排行榜名称,此时成员类型为用户。 - - -<> - -```objc -LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"world"]; -``` - - - -<> - -```cpp -FLCLeaderboard Leaderboard("world", FLCLeaderboard::User); -``` - - `FLCLeaderboard` 初始化方法接受两个参数: - - -- `Name` 为排行榜名称,这里需要传入云端已经存在的排行榜名称。上例中是 `world`。 -- `InMemberType` 为排行榜成员类型,传入 `FLCLeaderboard::User` 表示成员类型为用户,传入 `FLCLeaderboard::Entity` 表示成员类型为 entity,传入 `FLCLeaderboard::Object` 表示成员类型为 object - - - - - -构造排行榜实例后,调用该实例上的相应方法即可获取排名。 - -### 获取指定区间的排名 - -获取排行榜的 Top 10: - - - -<> - -```cs -var rankings = await leaderboard.GetResults(limit: 10); -``` - -`GetResults` 方法可以指定以下参数限制查询范围: - -| 参数名 | 类型 | 说明 | -| :-----------------: | :--------: | -------------------------------------------------------------------- | -| `aroundUser` | `LCUser` | 获取某用户附近的排名,详见下节。 | -| `aroundObject` | `LCObject` | 获取某 object 附近的排名,详见下节 | -| `aroundEntity` | `string` | 获取某 entity 附近的排名,详见下节。 | -| `limit` | `number` | 限制返回的结果数量,默认 10。 | -| `skip` | `number` | 指定从某个位置开始获取,与 `limit` 一起可以实现翻页,默认 0。 | -| `selectKeys` | `string[]` | 指定返回的 `Ranking` 中的 `user` 需要包含的属性,详见下文。 | -| `includeKeys` | `string[]` | 指定返回的 `Ranking` 中的 `user` 需要包含的 Pointer 属性,详见下文。 | -| `includeStatistics` | `string[]` | 指定返回的 `Ranking` 中需要包含的其他成绩,详见下文。 | -| `version` | `number` | 指定返回某个历史版本的成绩。 | - -返回的结果是一个排名数组(`Ranking[]`),`Ranking` 上有如下属性: - -| 属性 | 类型 | 说明 | -| :------------------: | :---------------: | -------------------------------- | -| `Rank` | `int` | 排名,从 0 开始 | -| `User` | `LCUser` | 该成绩的用户(user 排行榜) | -| `Object` | `LCObject` | 该成绩的 object(object 排行榜) | -| `Entity` | `string` | 该成绩的 entity(entity 排行榜) | -| `Value` | `double` | 成绩 | -| `IncludedStatistics` | `List` | 该成员在其他排行榜的成绩 | - - -<> - -```java -leaderboard.getResults(0, 10, null, null).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCLeaderboardResult leaderboardResult) { - List rankings = leaderboardResult.getResults(); - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - } - - @Override - public void onComplete() {} -}); -``` - -`Leaderboard#getResults` 方法可以指定以下参数限制查询范围: - -```java -Observable getResults( - int skip, int limit, - List selectMemberKeys, List includeStatistics) -``` - -- `skip` 指定从某个位置开始获取,与 `limit` 一起可以实现翻页,默认 0 -- `limit` 限制返回的结果数量,默认 20 -- `selectMemberKeys` 指定返回的 LCRanking 中排行榜成员需要包含的属性,详见下文 -- `includeStatistics` 指定返回的 LCRanking 中需要包含的其他成绩,详见下文 - -如需获取历史版本的排行榜信息,可以在排行榜实例上设置版本号: - -```java -int previousVersion = currentVersion - 1; -leaderboard.setVersion(previousVersion); -``` - -LCRanking 提供了以下方法供获取排名信息: - -```java -// 排名,从 0 开始 -int getRank() -// 该成绩的用户(user 排行榜) -LCUser getUser() -// 该成绩的 object(object 排行榜) -LCObject getObject() -// 该成绩的 entity(entity 排行榜) -String getEntityId() -// 成绩 -double getStatisticValue() -// 该成员在其他排行榜的成绩 -List getIncludedStatistics() -``` - - -<> - -```objc -leaderboard.limit = 10; -[leaderboard getUserResultsWithOption:nil, - callback:^(NSArray * _Nullable *rankings, NSInteger count, NSError * _Nullable error) { - // rankings 是排行榜前十的排名信息 -}]; -``` - -Leaderboard 实例上可以设置如下属性限制查询范围: - -```objc -/// 指定从某个位置开始获取,与 limit 一起可以实现翻页,默认 0 -@property (nonatomic) NSInteger skip; -/// 限制返回的结果数量,默认 20 -@property (nonatomic) NSInteger limit; -/// 指定返回的 LCLeaderboardRanking 中需要包含的其他成绩 -@property (nonatomic, nullable) NSArray *includeStatistics; -/// 指定排行榜版本号,默认 0 -@property (nonatomic) NSInteger version; -``` - -`getUserResultsWithOption` 的第一个参数是 `LCLeaderboardQueryOption`,可以指定返回的 `LCLeaderboardRanking` 中排行榜成员需要包含的属性,详见下文。 - -回调中的 `rankings` 是一个 LCLeaderboardRanking 数组。 -LCLeaderboardRanking 包含如下属性: - -```objc -// 排行榜名称 -@property (nonatomic, readonly, nullable) NSString *statisticName; -/// 排名,从 0 开始 -@property (nonatomic, readonly) NSInteger rank; -/// 成绩 -@property (nonatomic, readonly) double value; -/// 该成员在其他排行榜的成绩 -@property (nonatomic, readonly, nullable) NSArray *includedStatistics; -/// 该成绩的用户(user 排行榜) -@property (nonatomic, readonly, nullable) LCUser *user; -/// 该成绩的 object(object 排行榜) -@property (nonatomic, readonly, nullable) LCObject *object; -/// 该成绩的 entity(entity 排行榜) -@property (nonatomic, readonly, nullable) NSString *entity; -``` - -上例我们调用的是 `getUserResultsWithOption` 方法,所以 `user` 属性不为空,`object`、`entity` 属性均为空。 - -如果排行榜是 object 排行榜或 entity 排行榜,那么相应地需要调用 `getObjectResultsWithOption` 及 `getEntityResultsWithCallback` 方法。 -`getObjectResultsWithOption` 的参数和 `getUserResultsWithOption` 相同。 -因为 entity 排行榜的成员为字符串,所以 `getEntityResultsWithCallback` 不支持 `LCLeaderboardQueryOption`,第一个参数就是回调,回调的参数和 `getUserResultsWithOption`、`getObjectResultsWithOption` 相同: - -```objc -- (void)getEntityResultsWithCallback:(void (^)(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error))callback; -``` - - - -<> - -```cpp -Leaderboard.Limit = 10; -FLCLeaderboard::FRankingsDelegate OnSuccess = FLCLeaderboard::FRankingsDelegate::CreateLambda([=](TArray Rankings, int64 Count) { - // 排行榜前十的排名信息 -}); -FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) { - // handle error -}); -Leaderboard.GetResults(OnSuccess, OnError); -``` - -Leaderboard 实例上可以设置如下属性限制查询范围: - -```cpp -/// 指定从某个位置开始获取,与 limit 一起可以实现翻页,默认 0 -int64 Skip = 0; -/// 限制返回的结果数量,默认 20 -int64 Limit = 20; -/// 是否返回排行榜的数量 -bool WithCount = false; -/// 指定排行榜版本号,默认 0 -int64 Version = 0; -/// 指定返回的 LCLeaderboardRanking 中需要包含的其他成绩 -TArray IncludeStatistics; -/// 指定返回的 LCRanking 中排行榜成员需要包含的属性 -TArray SelectMemberKeys; -``` - - -FLCLeaderboardRanking 提供了以下方法供获取排名信息: - -```cpp -// 排名,从 0 开始 -int64 Rank; -// 该成绩的用户(user 排行榜) -TSharedPtr User; -// 该成绩的 object(object 排行榜) -TSharedPtr Object; -// 该成绩的 entity(entity 排行榜) -FString EntityId; -// 成绩 -double Value; -// 该成员在其他排行榜的成绩 -TArray Statistics; -``` - - - - - -默认情况下返回的排行榜结果中的 `user` 是一个指向 `LCUser` 的 Pointer。 -如果想要像下面这个例子一样,在排行榜结果中显示用户名或者其他的用户属性(对应 `_User` 表中的属性),那么需要使用 `selectKeys` 指定需要包含的属性。 - -| 排名 | Username | Score↓ | -| :--: | -------- | :----: | -| 0 | Genji | 3458 | -| 1 | Lúcio | 3252 | -| 2 | D.Va | 3140 | - - - -```cs -var rankings = await leaderboard.GetResults(limit: 10, - selectKeys: new List { "nickname" }); -``` - -```java -List selectKeys = new ArrayList<>(); -selectKeys.add("nickname"); -leaderboard.getResults(0, 10, selectKeys, null).subscribe(/* 略 */); -``` - -```objc -leaderboard.limit = 10; -LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init]; -option.selectKeys = @[@"nickname"]; -[leaderboard getUserResultsWithOption:option, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // 略 -}]; -``` - -```cpp -Leaderboard.Limit = 10; -Leaderboard.SelectMemberKeys = {"nickname"}; -Leaderboard.GetResults(OnSuccess, OnError); -``` - - - -如果想要在排行榜结果中包含用户的其他成绩,可以使用 `includeStatistics`。 -例如,查询得分排行榜时同时返回相应用户的击杀数: - -| 排名 | Username | Score↓ | Kills | -| :--: | -------- | :----: | :---: | -| 0 | Genji | 3458 | 28 | -| 1 | Lúcio | 3252 | 2 | -| 2 | D.Va | 3140 | 31 | - - - -```cs -var rankings = await leaderboard.GetResults(limit: 10, selectKeys: new List { "username" } - includeStatistics: new List { "kills" }); -``` - -```java -List selectKeys = new ArrayList<>(); -selectKeys.add("username"); -List includeStatistics = new ArrayList<>(); -includeStatistics.add("kills"); -leaderboard.getResults(0, 10, selectKeys, includeStatistics).subscribe(/* 略 */); -``` - -```objc -leaderboard.limit = 10; -leaderboard.includeStatistics = @[@"kills"]; -LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init]; -option.selectKeys = @[@"username"]; -[leaderboard getUserResultsWithOption:option, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // 略 -}]; -``` - -```cpp -Leaderboard.Limit = 10; -Leaderboard.SelectMemberKeys = {"username"}; -Leaderboard.IncludeStatistics = {"kills"}; -Leaderboard.GetResults(OnSuccess, OnError); -``` - - - -如果想要包含的属性是 Pointer 类型或文件类型,那么 `selectKeys` 只会返回 Pointer 本身。 -想要一并获取 Pointer 指向的另一个 class 的数据的话,需要额外使用 `includeKeys` 指定。 -例如,假设 `club` 是一个指向 `Club` 类的 Pointer: - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("weapons", "Weapon"); -var rankings = await leaderboard.GetResults(limit: 10, - selectKeys: new List { "name", "club" }, - includeKeys: new List { "club" }); -``` - -```java -// 暂未支持,如有此类需求可在 onNext 方法中额外发起一次查询 -``` - -```objc -LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"weapons"]; -leaderboard.limit = 10; -LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init]; -option.selectKeys = @[@"name", @"club"]; -option.includeKeys = @[@"club"]; -[leaderboard getUserResultsWithOption:option, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // 略 -}]; -``` - -```cpp -// 暂未支持 -``` - - - -注意,排行榜 **不保证成绩并列的成员之间的先后顺序**。 -例如,假设 A、B、C 的成绩为 42、32、32,且排行榜的排序策略为降序,那么查询排行榜的返回结果可能是 A、B、C,也可能是 A、C、B。 - -当成绩相同时,如果需要其他因素去设置排名可以参考如下示例**(示例是通过成绩上传的时间戳去判断排名,当成绩相同时,越早/晚提交,排名越靠前/后)**: -
    -Android 请求示例: - -``` -public class RankingActivity extends AppCompatActivity{ - - // ... - - /** - * 上传/更新成绩 - * - * */ - private void submitScore() { - - double score = 324.45; // 实际的分数 - long ts = System.currentTimeMillis() / 1000; // 时间戳 - double last_score = toEncode( score, ts); // 将实际分数与时间戳组合生成一个新的数据上传到服务器 - - Map statistic = new HashMap<>(); - statistic.put("word", last_score); - - LCLeaderboard.updateStatistic(LCUser.currentUser(), statistic).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCStatisticResult jsonObject) { - Log.e(TAG, "onNext: "+jsonObject.getResults().get(0).toString()); - - } - - @Override - public void onError(@NotNull Throwable throwable) { - ToastUtil.showCus(throwable.getMessage(), ToastUtil.Type.ERROR); - } - - @Override - public void onComplete() {} - }); - - } - - /** - * 查询排行榜列表 - * */ - private void searchRankList() { - // 获取排行榜的实例 - LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("word"); - - leaderboard.getResults(0, 10, null, null).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public void onNext(@NotNull LCLeaderboardResult leaderboardResult) { - List rankings = leaderboardResult.getResults(); - for(int i=0; i> 32); - - /** - * 当排行榜使用降序排列,分数相同时,时间越近,排名靠后,使用这行代码 - */ - int score = (int) (encryptedNewScore >> 32) + 1; - long ts = encryptedNewScore & 0xFFFFFFFFL; - return new int[]{score, (int) ts}; - } - - -} - -``` -
    - -### 获取当前用户附近的排名 - -| 排名 | Username | Score↓ | -| :------: | ---------- | :----: | -| … | | | -| 24 | Bastion | 716 | -| 25 (You) | Widowmaker | 698 | -| 26 | Hanzo | 23 | -| … | | | - - - -<> - -如果想达到类似上表的效果,在调用 `GetResults` 方法时额外传入当前用户即可: - -```cs -var rankings = await leaderboard.GetResults(aroundUser: currentUser, limit: 3, selectKeys: new List { "username" }); -``` - - -<> - -如果想达到类似上表的效果,可以调用 `getAroundResults` 方法: - -```java -List selectKeys = new ArrayList<>(); -selectKeys.add("username"); -leaderboard.getAroundResults(currentUser.getObjectId(), 0, 3, selectKeys, null).subscribe(/* 略 */); -``` - -`getAroundResults` 方法的第一个参数为排行榜成员的 ID(user 或 object 为 `objectId`、entity 为字符串),其他参数和 `getResults` 相同。 - - -<> - -如果想达到类似上表的效果,可以调用 `getUserResultsAroundUser` 方法: - -```objc -leaderboard.limit = 3; -[leaderboard getUserResultsAroundUser:currentUser.objectId, - option:nil, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // 略 -}]; -``` - - - -<> - -如果想达到类似上表的效果,可以调用 `GetAroundResults` 方法: - -```cpp -Leaderboard.Limit = 3; -Leaderboard.GetAroundResults(CurrentUser->GetObjectId();, OnSuccess, OnError); -``` - -`GetAroundResults` 方法的第一个参数为排行榜成员的 ID(user 或 object 为 `objectId`、entity 为字符串),其他参数和 `GetResults` 相同。 - - - - - -上面的代码示例中,`limit` 的值为 3,表示获取与当前玩家相邻的前后两个玩家的排名,当前玩家会在结果的中间位置。 -如仅需获取当前玩家排名,可指定 `limit` 为 1。 - -类似地,可以获取某个 object 或 entity 附近的排名。 -例如,获取武器榜上和某件武器排名相近的武器,同时获取这些武器的名称、攻击力、等级: - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("weapons", "Weapon"); -var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b"); -var rankings = await leaderboard.GetResults(aroundObject: excalibur, limit: 3, selectKeys: new List { "name", "attack", "level" }); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world", "Weapon"); -String excaliburObjectId = "582570f38ac247004f39c24b"; -List selectKeys = new ArrayList<>(); -selectKeys.add("name"); -selectKeys.add("attack"); -selectKeys.add("level"); -leaderboard.getAroundResults(excaliburObjectId, 0, 3, selectKeys, null).subscribe(/* 略 */); -``` - -```objc -LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"weapons"]; -leaderboard.limit = 3; -LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init]; -option.selectKeys = @[@"name", @"attack", @"level"]; -NSString *excaliburObjectId = @"582570f38ac247004f39c24b"; -[leaderboard getObjectResultsAroundObject:excaliburObjectId, - option:option, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // 略 -}]; -``` - -```cpp -FLCLeaderboard Leaderboard("weapons", FLCLeaderboard::Object); -Leaderboard.Limit = 3; -Leaderboard.SelectMemberKeys = {"name", "attack", "level"}; -FString ExcaliburObjectId = "582570f38ac247004f39c24b"; -Leaderboard.GetAroundResults(ExcaliburObjectId, OnSuccess, OnError); -``` - - - -上面的例子中假定武器排行榜是通过 object 排行榜实现的,存储武器信息的 Class 为 `Weapon`。 -如果武器排行榜是通过 entity 排行榜实现的,且只需获取武器的名称,那么相应的代码为: - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("weapons", LCLeaderboard.ENTITY_MEMBER_TYPE); -var rankings = await leaderboard.GetResults(aroundEntity: "excalibur", limit: 3); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world", LCLeaderboard.ENTITY_MEMBER_TYPE); -leaderboard.getAroundResults("excalibur", 0, 3, null, null).subscribe(/* 略 */); -``` - -```objc -LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"weapons"]; -leaderboard.limit = 3; -[leaderboard getEntityResultsAroundEntity:@"excalibur", - callback:^(NSArray^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // 略 -}]; -``` - -```cpp -FLCLeaderboard Leaderboard("weapons", FLCLeaderboard::Entity); -Leaderboard.Limit = 3; -Leaderboard.GetAroundResults("excalibur", OnSuccess, OnError); -``` - - - - -## 控制台 - -在排行榜控制台(游戏服务 > 云服务 > 排行榜),你可以: - -- 新建、重置、编辑、删除排行榜。 -- 查看排行榜当前版本的数据、删除成绩、下载排行榜的历史数据归档文件。 -- 设置是否允许客户端查询前一版本,是否只允许使用 Master Key 更新分数。 - -## SDK 管理接口 - -除了通过控制台管理排行榜外,C# SDK 和 Java SDK 也提供了管理接口,可以在**服务端等受信任的环境**下使用。 -另外,也可以直接调用 REST API 提供的管理接口。 - -下面我们先介绍 SDK 提供的管理接口。 - -:::caution - -请注意,调用管理接口 API 需要在 SDK 初始化时使用 masterKey,因此**只能在服务端等可信任的环境中调用,不得在客户端使用。** - -::: - - - -```cs -LCApplication.Initialize({{appid}}, {{appkey}}, "https://xxx.example.com", {{masterkey}}); -LCApplication.UseMasterKey = true; -``` - -```java -LeanCloud.setMasterKey({{masterkey}}); -``` - -```objc -// 不支持 -``` - -```cpp -// 不支持 -``` - - - -### 创建排行榜 - - - -<> - -```cs -var leaderboard = await LCLeaderboard.CreateLeaderboard("time", order: LCLeaderboardOrder.ASCENDING); -``` - -可以指定的参数类型和默认值如下: - -```cs -public static async Task CreateLeaderboard(string statisticName, - LCLeaderboardOrder order = LCLeaderboardOrder.Descending, - LCLeaderboardUpdateStrategy updateStrategy = LCLeaderboardUpdateStrategy.Better, - LCLeaderboardVersionChangeInterval versionChangeInterval = LCLeaderboardVersionChangeInterval.Week, - string memberType = LCLeaderboard.USER_MEMBER_TYPE) -``` - -- `statisticName` 所排名的成绩名字。 -- `order` 排序,可以传入 `LCLeaderboardOrder.Descending` 或 `LCLeaderboardOrder.Ascending`。 -- `updateStrategy` 成绩更新策略,可以传入 `LCLeaderboardUpdateStrategy.Better`、`LCLeaderboardUpdateStrategy.Last`、`LCLeaderboardUpdateStrategy.Sum`。 -- `versionChangeInterval` 自动重置周期,可以传入 `LCLeaderboardVersionChangeInterval.Never`、`LCLeaderboardVersionChangeInterval.Day`、`LCLeaderboardVersionChangeInterval.Week`、`LCLeaderboardVersionChangeInterval.Month`。 -- `memberType` 成员类型,传入 `LCLeaderboard.USER_MEMBER_TYPE` 表示成员类型为用户,传入 `LCLeaderboard.ENTITY_MEMBER_TYPE` 表示成员类型为 entity,成员类型为 object 时请传入相应 Class 名称。 - - -<> - -```java -LCLeaderboard.createWithMemberType(LCLeaderboard.MEMBER_TYPE_USER, "time", - LCLeaderboard.LCLeaderboardOrder.Ascending, - LCLeaderboard.LCLeaderboardUpdateStrategy.Last, - LCLeaderboard.LCLeaderboardVersionChangeInterval.Day).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull final LCLeaderboard lcLeaderboard) { - System.out.println("leaderboard created"); - } - - @Override - public void onError(@NotNull Throwable throwable) { - System.out.println("failed to create leaderboard. Cause " + throwable); - } - - @Override - public void onComplete() {} -}); -``` - -你可以指定以下参数: - -```java -public static Observable createWithMemberType(String memberType, String name, - LCLeaderboardOrder order, - LCLeaderboardUpdateStrategy updateStrategy, - LCLeaderboardVersionChangeInterval versionChangeInterval) -``` - -- `memberType` 成员类型,传入 `LCLeaderboard.MEMBER_TYPE_USER` 表示成员类型为用户,传入 `LCLeaderboard.MEMBER_TYPE_ENTITY` 表示成员类型为 entity,成员类型为 object 时请传入相应 Class 名称。 -- `name` 所排名的成绩名字。 -- `order` 排序,可以传入 `LCLeaderboard.LCLeaderboardOrder.Descending`(默认值)或 `LCLeaderboard.LCLeaderboardOrder.Ascending`。 -- `updateStrategy` 成绩更新策略,可以传入 `LCLeaderboard.LCLeaderboardUpdateStrategy.Better`(默认值)、`LCLeaderboard.LCLeaderboardUpdateStrategy.Last`、`LCLeaderboard.LCLeaderboardUpdateStrategy.Sum`。 -- `versionChangeInterval` 自动重置周期,可以传入 `LCLeaderboard.LCLeaderboardVersionChangeInterval.Never`、`LCLeaderboard.LCLeaderboardVersionChangeInterval.Day`、`LCLeaderboard.LCLeaderboardVersionChangeInterval.Week`(默认值)、`LCLeaderboard.LCLeaderboardVersionChangeInterval.Month`。 - -默认值指相应参数传入 `null` 时对应的取值。 - - - -<> - -不支持 - - - -<> - -不支持 - - - - - -### 手动重置排行榜 - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("score"); -await leaderboard.Reset(); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("score"); -leaderboard.reset().subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) { - } - - @Override - public void onNext(@NotNull Boolean aBoolean) { - if (aBoolean) { // aBoolean should always be true - System.out.println("leaderboard reset"); - } - } - - @override - public void onerror(@notnull throwable throwable) { - system.out.println("Failed to reset leaderboard. Cause " + throwable); - } - - @override - public void oncomplete() {} -}); -``` - -```objc -// 不支持 -``` - -```cpp -// 不支持 -``` - - - -### 获取排行榜属性 - -通过以下接口可以获取当前排行榜的属性,例如:重置周期、版本号、更新策略等。 - - - -```cs -var leaderboardData = await LCLeaderboard.GetLeaderboard("world"); -``` - -```java -LCLeaderboard.fetchByName("world").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(LCLeaderboard lcLeaderboard) { - int v =lcLeaderboard.getVersion(); - String statisticName = lcLeaderboard.getStatisticName(); - Log.d(TAG, String.valueOf(v)); - Log.d(TAG, statisticName); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } -}); -``` - -```objc -// 不支持 -``` - -```cpp -// 不支持 -``` - - - -### 修改排行榜属性 - -排行榜创建后,仅可修改自动重置周期和成绩更新策略,其他属性无法修改。 - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("equip"); -await leaderboard.UpdateVersionChangeInterval(LCLeaderboardVersionChangeInterval.Week); -await leaderboard.UpdateUpdateStrategy(LCLeaderboardUpdateStrategy.Last); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("equip"); -leaderboard.updateVersionChangeInterval(LCLeaderboard.LCLeaderboardVersionChangeInterval.Week) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) { - } - - @Override - public void onNext(@NotNull Boolean aBoolean) { - if (aBoolean) { // aBoolean should always be true - System.out.println("version update interval updated"); - } - } - - @override - public void onerror(@notnull throwable throwable) { - system.out.println("Failed to change version update interval. Cause: " + throwable); - } - - @override - public void oncomplete() {} -}); -leaderboard.updateUpdateStrategy(LCLeaderboard.LCLeaderboardUpdateStrategy.Last) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) { - } - - @Override - public void onNext(@NotNull Boolean aBoolean) { - if (aBoolean) { // aBoolean should always be true - System.out.println("update strategy updated"); - } - } - - @override - public void onerror(@notnull throwable throwable) { - system.out.println("Failed to change update strategy. Cause: " + throwable); - } - - @override - public void oncomplete() {} -}); -``` - -```objc -// 不支持 -``` - -```cpp -// 不支持 -``` - - - -### 删除排行榜 - - - -```cs -var leaderboard = lcleaderboard.createwithoutdata("equip"); -await leaderboard.destroy(); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("equip"); -leaderboard.destroy().subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) { - } - - @Override - public void onNext(@NotNull Boolean aBoolean) { - if (aBoolean) { // aBoolean should always be true - System.out.println("leaderboard deleted"); - } - } - - @override - public void onerror(@notnull throwable throwable) { - system.out.println("Failed to delete leaderboard. Cause " + throwable); - } - - @override - public void oncomplete() {} -}); -``` - -```objc -// 不支持 -``` - -```cpp -// 不支持 -``` - - - -**删除排行榜会删除该排行榜的所有数据,包括当前数据及所有历史版本归档。** - -### 更新排行榜成员成绩 - -有些情况下,你会想要绕过排行榜的更新策略强制更新用户的成绩,你可以使用 `overwrite` 参数实现这一需求: - - - -```cs -var statistic = new Dictionary { - { "score", 0.0 } -}; -await LCLeaderboard.UpdateStatistics(user, statistic, overwrite: true); -``` - -```java -Map statistic = new HashMap<>(); -statistic.put("world", 0.0); -LCLeaderboard.updateStatistic(currentUser, statistic, true).subscribe(/** 略 **/); -``` - -```objc -// 不支持 -``` - -```cpp -// 不支持 -``` - - - -object 和 entity 排行榜只支持在服务端使用 Master Key 更新,但仍会遵循排行榜的更新策略: - - - -```cs -var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b"); -await LCLeaderboard.UpdateStatistics(excalibur, statistic); -``` - -```java -// 暂不支持,如有需求可以调用 REST API 接口更新 -``` - -```objc -不支持 -``` - -```cpp -不支持 -``` - - - -如果需要强制更新,同样需要指定 `overwrite` 为真: - - - -```cs -await LCLeaderboard.UpdateStatistics("Vimur", statistic, overwrite: true); -``` - -```java -// 暂不支持,如有需求可以调用 REST API 接口更新 -``` - -```objc -// 不支持 -``` - -```cpp -// 不支持 -``` - - - -### 删除排行榜成员成绩 - -在服务端使用 Master Key 可以删除任意用户、object、entity 的成绩: - - - -```cs -var otherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "5c76107144d90400536fc88b"); -await LCLeaderboard.DeleteStatistics(otherUser, new List { "world" }); - -var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b"); -await LCLeaderboard.DeleteStatistics(excalibur, new List { "weapons" }); - -await LCLeaderboard.DeleteStatistics("Vimur", new List { "rivers" }); -``` - -```java -// 暂不支持,如有需求可以调用 REST API 接口更新 -``` - -```objc -// 不支持 -``` - -```cpp -// 不支持 -``` - - - -## REST API - -下面我们介绍排行榜相关的 REST API 接口。 -开发者可以自行编写程序或脚本调用这些接口在服务端进行管理性质的操作。 - -### 请求格式 - -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 - -请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,参数如下表: - -| Key | Value | 含义 | 来源 | -| ---------- | ------------ | ----------------------------------------- | -------------- | -| `X-LC-Id` | `{{appid}}` | 当前应用的 `App Id`(即 `Client Id`) | 可在控制台查看 | -| `X-LC-Key` | `{{appkey}}` | 当前应用的 `App Key`(即 `Client Token`) | 可在控制台查看 | - -管理接口需要使用 `Master Key`:`X-LC-Key: {{masterkey}},master`。 -`Master Key` 即 `Server Secret`,同样可在控制台查看。 - -详见文档关于[应用凭证](/sdk/storage/guide/setup-dotnet#应用凭证)的说明。 - -### Base URL - -REST API 请求的 Base URL(下文 curl 示例中用 `{{host}}` 表示)即应用的 API 自定义域名,可以在控制台绑定、查看。详见文档关于[域名](/sdk/storage/guide/setup-dotnet#域名)的说明。 - -### 管理排行榜 - -#### 创建排行榜 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"statisticName": "world", "memberType": "_User", "order": "descending", "updateStrategy": "better", "versionChangeInterval": "month"}' \ - https://{{host}}/1.1/leaderboard/leaderboards -``` - -| 参数 | 约束 | 说明 | -| ----------------------- | ---- | --------------------------------------------------------------------------------------------- | -| `statisticName` | 必须 | 排行榜的名称,创建后不可修改。 | -| `memberType` | 必须 | 排行榜的成员类型,创建后不可修改。可填写 `_Entity`、`_User` 及其他已有的 class 名称。 | -| `order` | 可选 | 排行榜的排序策略,创建后不可修改。可选项有 `ascending` 或 `descending`,默认为 `descending`。 | -| `updateStrategy` | 可选 | 可选项有 `better`、`last`、`sum`,默认为 `better`。 | -| `versionChangeInterval` | 可选 | 可选项有 `day`、`week`、`month`、`never`,默认为 `week`。 | - -返回的主体是一个 JSON 对象,包含创建排行榜时传入的所有参数,同时包括以下信息: - -- `version` 为排行榜当前版本号。 -- `expiredAt` 为下次过期(重置)时间。 -- `activatedAt` 当前版本的开始时间。 - -```json -{ - "objectId": "5b62c15a9f54540062427acc", - "statisticName": "world", - "memberType": "_User", - "versionChangeInterval": "month", - "order": "descending", - "updateStrategy": "better", - "version": 0, - "createdAt": "2018-08-02T08:31:22.294Z", - "updatedAt": "2018-08-02T08:31:22.294Z", - "expiredAt": { - "__type": "Date", - "iso": "2018-08-31T16:00:00.000Z" - }, - "activatedAt": { - "__type": "Date", - "iso": "2018-08-02T08:31:22.290Z" - } -} -``` - -#### 获取排行榜属性 - -通过这个接口来查看当前排行榜的属性,例如更新策略、当前版本号等。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/leaderboard/leaderboards/ -``` - -返回的 JSON 对象包含该排行榜的所有信息: - -```json -{ - "objectId": "5b0b97cf06f4fd0abc0abe35", - "statisticName": "world", - "memberType": "_User", - "order": "descending", - "updateStrategy": "better", - "version": 5, - "versionChangeInterval": "day", - "expiredAt": { "__type": "Date", "iso": "2018-05-02T16:00:00.000Z" }, - "activatedAt": { "__type": "Date", "iso": "2018-05-01T16:00:00.000Z" }, - "createdAt": "2018-04-28T05:46:58.579Z", - "updatedAt": "2018-05-01T01:00:00.000Z" -} -``` - -#### 修改排行榜属性 - -这个接口可以用来修改排行榜的 `updateStrategy` 和 `versionChangeInterval` 属性,其他属性不可修改。可以只更新某一个属性,例如只修改 versionChangeInterval: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"versionChangeInterval": "day"}' \ - https://{{host}}/1.1/leaderboard/leaderboards/ -``` - -返回的 JSON 对象包含更新的属性信息及 `updatedAt` 字段。 - -```json -{ - "objectId": "5b0b97cf06f4fd0abc0abe35", - "versionChangeInterval": "day", - "updatedAt": "2018-05-01T08:01:00.000Z" -} -``` - -#### 重置排行榜 - -无论排行榜的重置策略是什么,你都可以通过这个方法重置排行榜。重置时当前版本的数据清空,同时会归档到 csv 文件以供下载,排行榜的 `version` 会自动加一。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/leaderboard/leaderboards//incrementVersion -``` - -返回的 JSON 会显示重置后的当前版本号,下一次过期时间 `expiredAt`,当前版本的开始时间 `activatedAt`: - -```json -{ - "objectId": "5b0b97cf06f4fd0abc0abe35", - "version": 7, - "expiredAt": { "__type": "Date", "iso": "2018-06-03T16:00:00.000Z" }, - "activatedAt": { "__type": "Date", "iso": "2018-05-28T06:02:56.169Z" }, - "updatedAt": "2018-05-28T06:02:56.185Z" -} -``` - -#### 获取历史数据归档文件 - -因为每个排行榜最多保存 60 个归档文件,我们建议定期使用这个接口获取归档文件后另行备份保存。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - https://{{host}}/1.1/leaderboard/leaderboards//archives -``` - -返回的对象会按照 `createdAt` 降序排列。其中 `file_key` 是文件的名称,`url` 是文件的下载地址,`status` 包含以下状态: - -- `scheduled`:进入归档任务队列,还未归档,这个状态通常极短。 -- `inProgress`:正在归档中。 -- `failed`:归档失败,出现这种情况请提交工单联系技术支持。 -- `completed`:归档已完成。 - -```json -{ - "results": [ - { - "objectId": "5b0b9da506f4fd0abc0abe6e", - "statisticName": "wins", - "version": 9, - "status": "completed", - "url": "https://lc-paas-files.cn-n1.lcfile.com/yK5s6YJztAwEYiWs.csv", - "file_key": "yK5s6YJztAwEYiWs.csv", - "activatedAt": { "__type": "Date", "iso": "2018-05-28T06:11:49.572Z" }, - "deactivatedAt": { "__type": "Date", "iso": "2018-05-30T06:11:49.951Z" }, - "createdAt": "2018-05-01T16:00.00.000Z", - "updatedAt": "2018-05-28T06:11:50.129Z" - } - ] -} -``` - -#### 删除排行榜 - -**这个接口将删除排行榜的所有数据**,包括当前版本的数据及所有归档文件,删除后无法恢复,请谨慎操作。 - -删除时只需要指定排行榜的名称 statisticName。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/leaderboard/leaderboards/ -``` - -删除成功时返回空 JSON 对象: - -```json -{} -``` - -### 管理成绩 - -#### 更新成绩 - -使用 Master Key 可以更新任意成绩,但更新成绩时仍然遵循排行榜的 `updateStrategy` 属性。 - -更新用户成绩时需指定相应用户的 `objectId`: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 5}, {"statisticName": "world", "statisticValue": 91}]' \ - https://{{host}}/1.1/leaderboard/users//statistics -``` - -返回的数据是服务端当前使用的分数: - -```sh -{ - "results": [ - { - "statisticName": "wins", - "version": 0, - "statisticValue": 5 - }, - { - "statisticName": "world", - "version": 2, - "statisticValue": 91 - } - ] -} -``` - -类似地,更新 object 成绩时需指定相应 object 的 `objectId`: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 5}, {"statisticName": "weapons","statisticValue": 91}]' \ - https://{{host}}/1.1/leaderboard/objects//statistics -``` - -更新 entity 成绩时则需指定相应的字符串: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 5}, {"statisticName": "cities","statisticValue": 91}]' \ - https://{{host}}/1.1/leaderboard/entities//statistics -``` - -当前用户可以更新自己的成绩,这个不属于管理接口,不需要 `Master Key`,但需要传入当前用户的 `sessionToken`(客户端 SDK 更新当前用户的成绩封装了这一接口): - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 5}, {"statisticName": "world", "statisticValue": 91}]' \ - https://{{host}}/1.1/leaderboard/users/self/statistics -``` - -#### 强制更新成绩 - -附加 `overwrite=1` 会无视更新策略 better 及 sum,强制使用 last 策略更新用户的成绩。 -比如,发现某个用户存在作弊行为时,可以使用这个接口强制更新用户的成绩。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 10}]' \ - https://{{host}}/1.1/leaderboard/users//statistics?overwrite=1 -``` - -返回的数据是当前服务端使用的分数: - -```json -{ "results": [{ "statisticName": "wins", "version": 0, "statisticValue": 10 }] } -``` - -类似地,附加 `overwrite=1` 可以强制更新 object 成绩和 entity 成绩。 - -#### 删除成绩 - -如果不希望某个用户出现在榜单中,可以使用该接口删除用户的成绩以及在榜单中的排名(仅删除当前排行榜的成绩,不能删除历史版本的成绩)。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/leaderboard/users//statistics?statistics=wins,world -``` - -成功删除时返回空对象: - -``` -{} -``` - -类似地,可以删除 object 的成绩: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - --data-urlencode 'statistics=weapons,equipments' \ - https://{{host}}/1.1/leaderboard/objects//statistics -``` - -以及 entity 的成绩: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - --data-urlencode 'statistics=cities' \ - https://{{host}}/1.1/leaderboard/entities//statistics -``` - -同样,当前用户可以删除自己的成绩,这个不属于管理接口,不需要 `Master Key`,但需要传入当前用户的 `sessionToken`(客户端 SDK 删除当前用户的成绩封装了这一接口): - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/leaderboard/users/self/statistics?statistics=wins,world -``` - -### 查询成绩 - -通过 REST API 可以查询成绩,这些接口不属于管理接口,不需要 `Master Key`: - -#### 查询某个成绩 - -指定用户的 objectId 即可获取该用户的成绩。 -你可以在请求 url 中指定多个 `statistics` 来获得多个排行榜中的成绩,排行榜名称用英文逗号 `,` 隔开,如果不指定将会返回该用户参与的所有排行榜中的成绩。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - --data-urlencode 'statistics=wins,world' \ - https://{{host}}/1.1/leaderboard/users//statistics -``` - -返回结果: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 5, - "version": 0, - "user": { - "__type": "Pointer", - "className": "_User", - "objectId": "60d950629be318a249000001" - } - }, - { - "statisticName": "world", - "statisticValue": 91, - "version": 0, - "user": {...} - } - ] -} -``` - -类似地,指定 object 的 objectId 可以查询该 object 参与的排行榜的成绩: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - --data-urlencode 'statistics=wins,world' \ - https://{{host}}/1.1/leaderboard/objects//statistics -``` - -返回示例: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 5, - "version": 0, - "object": { - "__type": "Pointer", - "className": "Weapon", - "objectId": "60d1af149be3180684000002" - } - }, - { - "statisticName": "world", - "statisticValue": 91, - "version": 0, - "object": { - "__type": "Pointer", - "className": "Weapon", - "objectId": "60d1af149be3180684000002" - } - } - ] -} -``` - -获取某个 entity 成绩时则需指定该 entity 的字符串: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - --data-urlencode 'statistics=wins,world' \ - https://{{host}}/1.1/leaderboard/entities//statistics -``` - -返回示例: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 5, - "version": 0, - "entity": "1a2b3c4d" - }, - { - "statisticName": "world", - "statisticValue": 91, - "version": 0, - "entity": "1a2b3c4d" - } - ] -} -``` - -#### 查询一组成绩 - -通过这个接口可以一次性拉取多个 user 的成绩,最多不超过 200 个。在请求中,需要在 body 中传入 user 的 `objectId` 数组。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '["60d950629be318a249000001", "60d950629be318a249000000"]' - https://{{host}}/1.1/leaderboard/users/statistics/ -``` - -查询一组成绩的返回结果与[查询单个成绩](#查询某个成绩)类似: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 1, - "version": 0, - "user": { - "__type": "Pointer", - "className": "_User", - "objectId": "60d950629be318a249000001" - } - }, - { - "statisticName": "wins", - "statisticValue": 2, - "version": 0, - "user": { - "__type": "Pointer", - "className": "_User", - "objectId": "60d950629be318a249000000" - } - } - ] -} -``` - -类似地,传入 object 的 `objectId` 数组可以一次性获取多个 object 的成绩(最多不超过 200 个): - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '["60d950629be318a249000001", "60d950629be318a249000000"]' - https://{{host}}/1.1/leaderboard/objects/statistics/ -``` - -传入 entity 的字符串数组则可以一次性获取多个 entity 的成绩(最多不超过 200 个): - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '["Vimur", "Fimbulthul"]' - https://{{host}}/1.1/leaderboard/entities/statistics/ -``` - -### 查询排行榜 - -#### 获取区间排名 - -你可以使用这个接口来获取 Top 排名。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=20' \ - --data-urlencode 'selectKeys=username,club' \ - --data-urlencode 'includeKeys=club' \ - --data-urlencode 'includeStatistics=wins' \ - https://{{host}}/1.1/leaderboard/leaderboards/user//ranks -``` - -| 参数 | 约束 | 说明 | -| ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| startPosition | 可选 | 排行头部起始位置,默认为 0。 | -| maxResultsCount | 可选 | 最大返回数量,默认为 20。 | -| selectKeys | 可选 | 返回用户在 `_User` 表的其他字段,支持多个字段,用英文逗号 `,` 隔开。出于安全性考虑,在非 masterKey 请求下不返回敏感字段 `email` 及 `mobilePhoneNumber`。 | -| includeKeys | 可选 | 返回用户在 `_User` 表的 pointer 字段的详细信息,支持多个字段,用英文逗号 `,` 隔开。为确保安全,在非 masterKey 请求下不返回敏感字段 `email` 及 `mobilePhoneNumber`。 | -| includeStatistics | 可选 | 返回该用户在其他排行榜中的成绩,如果传入了不存在的排行榜名称,将会返回错误。 | -| version | 可选 | 返回指定 version 的排行结果,默认返回当前版本的数据。 | -| count | 可选 | 值为 1 时返回该排行榜中的成员数量,默认为 0。 | - -返回 JSON 对象: - -```json -{ - "results": [ - { - "statisticName": "world", - "statisticValue": 91, - "rank": 0, - "user": { - "__type": "Pointer", - "className": "_User", - "updatedAt": "2021-07-21T03:08:10.487Z", - "username": "zw1stza3fy701rvgxqwiikex7", - "createdAt": "2020-09-04T04:23:04.795Z", - "club": { - "objectId": "60f78f98d9f1465d3b1da12d", - "name": "board games", - "updatedAt": "2021-07-21T03:08:08.692Z", - "createdAt": "2021-07-21T03:08:08.692Z", - }, - "objectId": "5f51c1287628f2468aa696e6" - } - }, - {...} - ], - "count": 500 -} -``` - -查询 object 排行榜的 Top 排名的接口与之类似,只是将 `user` 替换为 `object`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=2' \ - --data-urlencode 'selectKeys=name,image' \ - --data-urlencode 'includeKeys=image' \ - --data-urlencode 'count=1' \ - https://{{host}}/1.1/leaderboard/leaderboards/object//ranks -``` - -返回结果: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 4, - "rank": 0, - "object": { - "__type": "Pointer", - "className": "Weapon", - "name": "sword", - "image": { - "bucket": "test_files", - "provider": "leancloud", - "name": "sword.jpg", - "url": "https://example.com/sword.jpg", - "objectId": "60d2f3a39be3183377000002", - "__type": "File" - }, - "objectId": "60d2f22f9be318328b000007" - } - }, - { - "statisticName": "wins", - "statisticValue": 3, - "rank": 1, - "object": {...} - } - ], - "count": 500 -} -``` - -同理,URL 中的 `user` 替换为 `entity` 可查询 entity 排行榜的 Top 排名: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=2' \ - --data-urlencode 'count=1' \ - https://{{host}}/1.1/leaderboard/leaderboards/entity//ranks -``` - -返回结果: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 4, - "rank": 0, - "entity": "1234567890" - }, - { - "statisticName": "wins", - "statisticValue": 3, - "rank": 1, - "entity": "2345678901" - } - ], - "count": 500 -} -``` - -#### 获取附近排名 - -在 URL 末端附加相应的 objectId 可获取某用户或 object 附近的排名。 - -获取某用户附近的排名: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=20' \ - --data-urlencode 'selectKeys=username,club' \ - --data-urlencode 'includeKeys=club' \ - https://{{host}}/1.1/leaderboard/leaderboards/user//ranks/ -``` - -参数含义参见上面[获取区间排名](#获取区间排名)一节。 -获取附近排名的返回结果与[获取区间排名](#获取区间排名)类似。 - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 3, - "rank": 2, - "user": {...} - }, - { - "statisticName": "wins", - "statisticValue": 2.5, - "rank": 3, - "user": { - "__type": "Pointer", - "className": "_User", - "username": "kate", - "club": { - "objectId": "60f78f98d9f1465d3b1da12d", - "name": "board games", - "updatedAt": "2021-07-21T03:08:08.692Z", - "createdAt": "2021-07-21T03:08:08.692Z", - }, - "objectId": "60d2faa99be3183623000001" - } - }, - { - "statisticName": "wins", - "statisticValue": 2, - "rank": 4, - "user": {...} - } - ], - "count": 500 -} -``` - -获取某 object 附近的排名: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=2' \ - --data-urlencode 'selectKeys=name,image' \ - --data-urlencode 'includeKeys=image' \ - --data-urlencode 'count=1' \ - https://{{host}}/1.1/leaderboard/leaderboards/object//ranks/ -``` - -同理,在 URL 末端附加 entity 字符串即可获取该 entity 附近的排名: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=2' \ - --data-urlencode 'count=1' \ - https://{{host}}/1.1/leaderboard/leaderboards/entity//ranks/ -``` - -## 视频教程 - -可以参考视频教程:[如何接入 TapTap 排行榜功能](https://www.bilibili.com/video/BV1ZN411s7Jj/),了解如何在 Untiy 项目中接入排行榜功能。 - -更多视频教程见[开发者学堂](https://developer.taptap.cn/tds-tutorials/list)。因为 SDK 功能在不断完善,视频教程可能出现与新版 SDK 功能不一致的地方,以当前文档为准。 \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/guide/_category_.json b/docs/sdk/leaderboard/guide/_category_.json similarity index 100% rename from i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/guide/_category_.json rename to docs/sdk/leaderboard/guide/_category_.json diff --git a/leancloud/docs/sdk/leaderboard/guide/cs.mdx b/docs/sdk/leaderboard/guide/cs.mdx similarity index 100% rename from leancloud/docs/sdk/leaderboard/guide/cs.mdx rename to docs/sdk/leaderboard/guide/cs.mdx diff --git a/leancloud/docs/sdk/leaderboard/guide/js.mdx b/docs/sdk/leaderboard/guide/js.mdx similarity index 100% rename from leancloud/docs/sdk/leaderboard/guide/js.mdx rename to docs/sdk/leaderboard/guide/js.mdx diff --git a/leancloud/docs/sdk/leaderboard/guide/objc.mdx b/docs/sdk/leaderboard/guide/objc.mdx similarity index 100% rename from leancloud/docs/sdk/leaderboard/guide/objc.mdx rename to docs/sdk/leaderboard/guide/objc.mdx diff --git a/leancloud/docs/sdk/leaderboard/guide/rest.mdx b/docs/sdk/leaderboard/guide/rest.mdx similarity index 100% rename from leancloud/docs/sdk/leaderboard/guide/rest.mdx rename to docs/sdk/leaderboard/guide/rest.mdx diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/start/_category_.json b/docs/sdk/leaderboard/quick-start/_category_.json similarity index 100% rename from i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/start/_category_.json rename to docs/sdk/leaderboard/quick-start/_category_.json diff --git a/leancloud/docs/sdk/leaderboard/quick-start/cs.mdx b/docs/sdk/leaderboard/quick-start/cs.mdx similarity index 100% rename from leancloud/docs/sdk/leaderboard/quick-start/cs.mdx rename to docs/sdk/leaderboard/quick-start/cs.mdx diff --git a/leancloud/docs/sdk/leaderboard/quick-start/js.mdx b/docs/sdk/leaderboard/quick-start/js.mdx similarity index 100% rename from leancloud/docs/sdk/leaderboard/quick-start/js.mdx rename to docs/sdk/leaderboard/quick-start/js.mdx diff --git a/leancloud/docs/sdk/other/_category_.json b/docs/sdk/other/_category_.json similarity index 100% rename from leancloud/docs/sdk/other/_category_.json rename to docs/sdk/other/_category_.json diff --git a/leancloud/docs/sdk/other/error-code.mdx b/docs/sdk/other/error-code.mdx similarity index 100% rename from leancloud/docs/sdk/other/error-code.mdx rename to docs/sdk/other/error-code.mdx diff --git a/leancloud/docs/sdk/other/openview/_category_.json b/docs/sdk/other/openview/_category_.json similarity index 100% rename from leancloud/docs/sdk/other/openview/_category_.json rename to docs/sdk/other/openview/_category_.json diff --git a/leancloud/docs/sdk/other/openview/status_system.mdx b/docs/sdk/other/openview/status_system.mdx similarity index 100% rename from leancloud/docs/sdk/other/openview/status_system.mdx rename to docs/sdk/other/openview/status_system.mdx diff --git a/leancloud/docs/sdk/other/tool_tips.mdx b/docs/sdk/other/tool_tips.mdx similarity index 100% rename from leancloud/docs/sdk/other/tool_tips.mdx rename to docs/sdk/other/tool_tips.mdx diff --git a/docs/shadow/push/_category_.json b/docs/sdk/push/_category_.json similarity index 100% rename from docs/shadow/push/_category_.json rename to docs/sdk/push/_category_.json diff --git a/docs/shadow/push/features.mdx b/docs/sdk/push/features.mdx similarity index 100% rename from docs/shadow/push/features.mdx rename to docs/sdk/push/features.mdx diff --git a/docs/shadow/push/guide/Unreal.mdx b/docs/sdk/push/guide/Unreal.mdx similarity index 100% rename from docs/shadow/push/guide/Unreal.mdx rename to docs/sdk/push/guide/Unreal.mdx diff --git a/.ci/hk/zh-Hans/sdk/TapPayments/develop/_category_.json b/docs/sdk/push/guide/_category_.json similarity index 100% rename from .ci/hk/zh-Hans/sdk/TapPayments/develop/_category_.json rename to docs/sdk/push/guide/_category_.json diff --git a/docs/shadow/push/guide/android-mixpush.mdx b/docs/sdk/push/guide/android-mixpush.mdx similarity index 100% rename from docs/shadow/push/guide/android-mixpush.mdx rename to docs/sdk/push/guide/android-mixpush.mdx diff --git a/docs/shadow/push/guide/android.mdx b/docs/sdk/push/guide/android.mdx similarity index 100% rename from docs/shadow/push/guide/android.mdx rename to docs/sdk/push/guide/android.mdx diff --git a/leancloud/docs/sdk/push/guide/flutter.mdx b/docs/sdk/push/guide/flutter.mdx similarity index 100% rename from leancloud/docs/sdk/push/guide/flutter.mdx rename to docs/sdk/push/guide/flutter.mdx diff --git a/docs/shadow/push/guide/ios-cert.mdx b/docs/sdk/push/guide/ios-cert.mdx similarity index 100% rename from docs/shadow/push/guide/ios-cert.mdx rename to docs/sdk/push/guide/ios-cert.mdx diff --git a/docs/shadow/push/guide/ios.mdx b/docs/sdk/push/guide/ios.mdx similarity index 100% rename from docs/shadow/push/guide/ios.mdx rename to docs/sdk/push/guide/ios.mdx diff --git a/docs/shadow/push/guide/overview.mdx b/docs/sdk/push/guide/overview.mdx similarity index 100% rename from docs/shadow/push/guide/overview.mdx rename to docs/sdk/push/guide/overview.mdx diff --git a/docs/shadow/push/guide/push-faq.mdx b/docs/sdk/push/guide/push-faq.mdx similarity index 100% rename from docs/shadow/push/guide/push-faq.mdx rename to docs/sdk/push/guide/push-faq.mdx diff --git a/docs/shadow/push/guide/rest.mdx b/docs/sdk/push/guide/rest.mdx similarity index 100% rename from docs/shadow/push/guide/rest.mdx rename to docs/sdk/push/guide/rest.mdx diff --git a/leancloud/docs/sdk/push/guide/setup-flutter.mdx b/docs/sdk/push/guide/setup-flutter.mdx similarity index 100% rename from leancloud/docs/sdk/push/guide/setup-flutter.mdx rename to docs/sdk/push/guide/setup-flutter.mdx diff --git a/docs/shadow/push/guide/unity.mdx b/docs/sdk/push/guide/unity.mdx similarity index 100% rename from docs/shadow/push/guide/unity.mdx rename to docs/sdk/push/guide/unity.mdx diff --git a/leancloud/docs/sdk/sms/_category_.json b/docs/sdk/sms/_category_.json similarity index 100% rename from leancloud/docs/sdk/sms/_category_.json rename to docs/sdk/sms/_category_.json diff --git a/leancloud/docs/sdk/sms/faq.mdx b/docs/sdk/sms/faq.mdx similarity index 100% rename from leancloud/docs/sdk/sms/faq.mdx rename to docs/sdk/sms/faq.mdx diff --git a/leancloud/docs/sdk/sms/guide.mdx b/docs/sdk/sms/guide.mdx similarity index 100% rename from leancloud/docs/sdk/sms/guide.mdx rename to docs/sdk/sms/guide.mdx diff --git a/leancloud/docs/sdk/sms/rest.mdx b/docs/sdk/sms/rest.mdx similarity index 100% rename from leancloud/docs/sdk/sms/rest.mdx rename to docs/sdk/sms/rest.mdx diff --git a/docs/sdk/start/agreement.mdx b/docs/sdk/start/agreement.mdx deleted file mode 100644 index 9632533a8..000000000 --- a/docs/sdk/start/agreement.mdx +++ /dev/null @@ -1,642 +0,0 @@ ---- -title: TapSDK 隐私政策 -sidebar_position: 8 ---- - -更新日期:2024 年 3 月 29 日 -生效日期:2024 年 3 月 29 日 - -易玩(上海)网络科技有限公司(以下简称“TapTap”或“我们”)通过 TapSDK 向开发者提供多种服务,开发者可以根据自身需求在其应用中接入其中任意一项或多项服务。本文档将向开发者和其用户(以下或称“玩家”)说明 TapSDK 的隐私安全信息,包括 TapSDK 各项服务处理的个人信息范围、处理目的、权限使用情况等。 - -**如果您是开发者,在接入、使用 TapSDK 和服务前,请按照《中华人民共和国个人信息保护法》等法律法规以及《TapTap 平台开发者协议》等平台规则的要求在您的隐私政策中向您的用户告知 TapSDK 相关信息,并获取用户的同意或取得其他合法性基础。** - -**如果您是用户,在您使用集成了 TapSDK 的应用前,请务必仔细阅读相关应用的隐私政策及本隐私政策,确保您充分理解和同意之后再开始使用。** - -## 一、我们如何收集和使用用户的个人信息 - -在用户使用 TapSDK 提供的服务过程中,我们将根据合法、正当、必要的原则收集信息。 - -**1.1 TapSDK 功能** - -为满足开发者的多种业务需求,TapSDK 提供更新唤起、TapTap 登录、合规认证、正版验证、DLC、内嵌动态、TapTap Connect(悬浮窗)、成就系统、内建账户、客服、数据分析、诊断接入、数据存储等扩展功能,具体功能介绍详见下文,关于扩展功能的配置方式详见[《TapSDK 合规使用说明》](https://developer.taptap.cn/docs/sdk/start/compliance/)。 - -**1.2 功能介绍及对应所需用户信息与权限** - -**1.2.1 更新唤起** - -1)功能介绍:更新唤起服务主要应用于在 TapTap 国内商店分发的游戏包体更新场景。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限系统版本为了确保设备系统兼容、定位解决问题
    设备型号
    设备 CPU 信息
    网络类型
    AndroidID
    设备内存信息
    设备指定应用信息获取当前已安装 TapTap 客户端信息
    网络权限用于访问网络数据
    安装 APK 权限用于安装 TapTap 客户端
    可选个人信息/权限/
    - -**1.2.2 TapTap 登录** - -1)功能介绍:提供 TapTap 登录方式,玩家可以通过 TapTap 授权快速开始游戏。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限系统版本为帮助用户保障登录功能的正常生效和安全稳定
    设备版本
    网络权限用于访问网络数据
    可选个人信息/权限/
    - -**1.2.3 合规认证** - -1)功能介绍:基于 TapTap 账号的快速实名认证功能,对使用 TapTap 账号登录游戏的玩家,在经过玩家同意授权之后,允许玩家使用在 TapTap 里已经通过国家认证的实名信息快速完成游戏中的认证流程。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限WiFi 信息为了确保设备系统兼容、定位解决问题
    系统版本
    网络权限用于访问网络数据
    获取网络状态用于检测当前网络连接是否有效
    可选个人信息/权限/
    - -**1.2.4 正版验证、DLC** - -1)功能介绍:TapTap 的正版验证服务适用于付费下载的游戏,当玩家在使用付费游戏时,校验玩家是否已经成功购买付费游戏。TapTap 开发者服务,支持付费的可下载内容(DLC),让玩家不离开游戏便能浏览、购买、拥有新内容。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限设备指定应用信息获取已安装的 TapTap 客户端信息
    可选个人信息/权限/
    - -**1.2.5 内嵌动态** - -1)功能介绍:玩家可以在游戏内访问 TapTap 的社区论坛(官方公告、游戏攻略、问题反馈、热门话题等),同时也可以看到 TapTap 好友的游戏动态,并参与其他玩家、官方和大神之间的互动。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限WiFi 信息为了确保设备系统兼容、定位解决问题
    系统版本
    设备版本
    AndroidID
    手机样式
    设备内存
    网络权限用于访问网络数据
    获取网络状态用于检测当前网络连接是否有效
    读写存储权限用于发布或下载动态页面内图片、视频
    可选个人信息/权限/
    - -**1.2.6 TapTap Connect(悬浮窗)** - -1)功能介绍:TapTap Connect(以下简称“悬浮窗”)是连接 TapTap、游戏和玩家的工具。开发者启用该功能后,成功登录 TapTap 账号的玩家可在悬浮窗内进行浏览游戏动态、评价游戏、分享游戏下载链接等操作,来帮助游戏提升留存并带来新用户。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限系统版本为了确保设备系统兼容、定位解决问题
    设备型号
    设备 CPU 信息
    网络类型
    AndroidID
    设备内存信息
    设备方向传感器信息为了确保悬浮窗口在设备中正常显示
    网络权限用于访问网络数据
    可选个人信息/权限/
    - -**1.2.7 成就系统** - -1)功能介绍:可以在游戏中设置「普通成就」和「白金成就」,增加玩家在游戏中的参与度,鼓励玩家以不同的玩法来玩游戏。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限设备名称用于本地数据加密处理
    网络权限用于访问网络数据
    可选个人信息/权限/
    - -**1.2.8 内建账户** - -1)功能介绍:内建账户服务致力于帮助开发者快速低成本地构建一个安全可靠的玩家登录系统。支持玩家采用包括游客账号、第三方账号(TapTap、Apple、微信、QQ 等)在内的多种账号来登录游戏。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限系统版本为帮助用户保障登录功能的正常生效和安全稳定
    设备型号
    设备 CPU 信息
    网络类型
    AndroidID
    设备内存信息
    网络权限用于访问网络数据
    可选个人信息/权限/
    - -**1.2.9 客服** - -1)功能介绍:游戏客服能帮助游戏运营团队更快更好地解决玩家遇到的问题。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限系统版本为了确保设备系统兼容、定位解决问题
    设备版本
    网络类型
    手机样式
    设备内存
    AndroidID
    网络权限用于访问网络数据
    获取网络状态用于检测当前网络连接是否有效
    读取存储权限用于反馈工单时添加本地图片、视频附件
    可选个人信息/权限/
    - -**1.2.10 数据分析** - -1)功能介绍:提供了一套专注于解决游戏项目数据需求的分析工具,通过简单的接入就可以获得丰富实用的数据看板和广告追踪能力,让数据分析和广告投放变得轻松易操作,同时也可以用于分析人群画像,帮助开发者更好地理解用户。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限WiFi 信息供开发者进行数据分析,是业务需求之一
    系统版本
    设备版本
    手机样式
    传感器列表
    AndroidID
    网络权限用于访问网络数据
    获取网络状态用于检测当前网络连接是否有效
    可选个人信息/权限读写存储权限用于存储用户标识
    读取电话状态获取移动端设备网络类型
    - -**1.2.11 诊断接入** - -1)功能介绍:诊断接入是向开发者提供的应用安全模块,主要是通过 APK 加固和反作弊检测等能力,来为游戏应用程序的安全进行保驾护航。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限系统版本为了确保设备系统兼容、定位解决问题
    设备型号
    网络类型
    设备内存信息
    设备 CPU 信息
    手机样式
    设备名称
    网络权限用于访问网络数据
    可选个人信息/权限/
    - -**1.2.12 数据存储** - -1)功能介绍:数据存储服务能够高效存取海量级 JSON 对象、二进制文件、地理位置等数据。其内置的行级 ACL 权限控制,以及通用的用户及角色管理体系,可以帮助你快速实现安全而灵活的数据访问。 - -2)收集信息/获取权限 - - - - - - - - - - - - - - - - -
    个人信息/权限类型个人信息/权限名称使用目的
    必要个人信息/权限网络权限用于访问网络数据
    可选个人信息/权限/
    - - -1.3 我们仅为实现 TapSDK 产品和/或服务功能,对所收集的用户个人信息进行处理。若需要将收集的个人信息用于其他目的,我们会以合理方式告知用户,并在获得用户的同意后进行使用。 - -**请理解,在下列情形中,根据法律法规及相关国家标准,我们处理用户的个人信息无需事先征得用户的授权同意:** -1)为订立、履行用户作为一方当事人的合同所必需; -2)为履行法定职责或者法定义务所必需; -3)为应对突发公共卫生事件,或者紧急情况下为保护自然人的生命健康和财产安全所必需; -4)为公共利益实施新闻报道、舆论监督等行为,在合理的范围内处理个人信息; -5)依照本法规定在合理的范围内处理个人自行公开或者其他已经合法公开的个人信息; -6)法律、行政法规规定的其他情形。 - -**特别说明:根据法律规定,如信息无法单独或结合其他信息识别到特定个人的,其不属于个人信息**。当信息可以单独或结合其他信息识别到用户时,或我们把没有与任何特定个人建立联系的数据与用户的个人信息结合使用时,我们会将其作为用户的个人信息,按照本隐私政策进行处理与保护。 - - -## 二、我们如何存储用户的个人信息 - -2.1 存储信息的地点 - -我们遵守中国法律的规定,将中国境内获取的个人信息存储于中国境内。如果用户的个人信息存储地点需要从中国境内转移到境外的,我们将严格依照法律法规的要求执行。 - -2.2 存储信息的期限 - -一般而言,我们仅在为实现目的所必需的最短时间内保留用户的个人信息。但在下列情况下,我们有可能会调整个人信息的存储时间: -1)为遵守适用的法律法规等有关规定,或我们有理由确信需要遵守法律法规等有关规定; -2)为遵守法院判决、裁定或其他法律程序的规定; -3)为遵守相关政府机关或法定授权组织的要求; -4)为执行相关服务协议或本隐私政策、维护社会公共利益,为保护我们的客户、我们或我们的关联公司、其他用户或雇员的人身财产安全或其他合法权益所合理必需的用途。 - -在超出必要存储期限后,我们将依照法律法规的要求删除或匿名化处理用户的个人信息。 - -当我们的产品或服务发生停止运营的情形时,我们将通过第三方开发者的应用或推送通知、公告等形式通知用户,并在合理的期限内删除或匿名化处理用户的个人信息。 - -## 三、我们如何共享、转让、披露用户的个人信息 - -3.1 共享 - -通常情况下,除非获得用户的明确同意,我们不会与任何公司、组织和个人共享用户的个人信息,但以下情况除外: -1)与我们的关联公司共享 -我们只会与关联公司共享必要的个人信息,且关联公司亦受本隐私政策声明目的的约束。如关联公司要改变个人信息的使用及处理目的,将再次征求用户的授权同意。 -2)与合作伙伴共享 -为了向用户提供完善的产品和服务,我们的某些服务或技术将由合作伙伴提供。请知悉,我们仅会出于合法、正当、必要、特定、明确的目的共享用户的个人信息,且会与合作伙伴签署严格的保密协议,要求他们严格按照我们的说明、隐私政策以及其他任何相关的保密政策并采取安全措施来处理用户的个人信息。 - -3.2 转让 - -通常情况下,除非获得用户的明确同意,我们不会将用户的个人信息转让给任何公司、组织和个人,但以下情况除外: -在涉及合并、收购、资产转让或类似的交易时,如涉及到个人信息转让,我们会要求新的持有用户个人信息的公司、组织以不低于本隐私政策所要求的标准继续保护用户的个人信息,否则,我们将要求该公司、组织重新向用户征求授权同意。 - -3.3 披露 - -我们不会主动公开用户未自行公开的信息,除非遵循国家法律法规规定所必须或者获得用户的同意。 - -## 四、用户如何管理其个人信息 - -4.1 查阅、复制和修改个人信息 - -当用户所使用的某项功能由 TapSDK 提供,用户可以在使用第三方开发者提供的产品或服务过程中,通过第三方开发者的隐私政策中的指引查阅、复制、修改相关的个人信息,也可按照本隐私政策第八条所述方式与我们联系。 - -4.2 删除个人信息 - -在以下情形下,我们将删除与用户相关的个人信息: -1)处理目的已实现、无法实现或者为实现处理目的不再必要; -2)我们停止提供服务,或者信息保存期限已届满; -3)我们收到用户的撤回同意的请求; -4)用户认为我们的处理违反法律、行政法规或者违反约定处理个人信息; -5)法律、行政法规规定的其他情形。 - -我们将会自行进行内部评估,和/或在收到用户的删除请求后进行评估,若满足相应法律规定的情形时,我们将会采取包括技术手段在内的相应步骤进行处理。 如法律、行政法规规定的保存期限未届满,或者删除个人信息从技术上难以实现的,我们将停止除存储和采取必要的安全保护措施之外的处理。 - -4.3 撤回同意 - -基于用户的同意而进行的个人信息处理活动,用户有权撤回该同意。第三方开发者应当提供关闭 TapSDK 产品和/或服务的能力,用户可以联系第三方开发者,要求其关闭 TapSDK 产品和/或服务,停止收集和处理用户的个人信息。 - -由于我们与用户无直接的交互对话界面,用户可以直接联系第三方开发者或在设备操作系统主张撤回同意或关闭已开启的权限,也可通过本隐私政策的联系方式联系我们,主张撤回同意的权利。 - -当用户撤回其对个人信息处理的同意,或关闭 TapSDK 产品和/或服务,或关闭已开启的权限后,我们将无法继续为用户提供撤回(关闭)所对应的功能和服务,也不再处理用户相应的个人信息。但用户撤回同意的决定,不会影响撤回前基于用户同意的,已进行的个人信息处理活动的效力。 - -4.4 账号注销服务 - -如用户希望注销在第三方开发者的具体产品或服务内注册的账号时,用户可以根据第三方开发者的具体产品或服务的隐私政策申请注销相应账号。 - -4.5 限制或者拒绝他人对个人信息的处理 - -如用户希望限制或拒绝他人对其个人信息进行处理,用户可以根据第三方开发者的具体产品或服务的隐私政策的指引操作,也可以按照本隐私政策第八条所述方式与我们联系。 - -4.6 获取个人信息副本 - -在符合法律规定的情况下,用户可以请求获取个人信息副本。用户可以根据第三方开发者的具体产品或服务的隐私政策的指引操作,也可以按照本隐私政策第八条所述方式与我们联系。 - -4.7 在以下情形中,我们可能基于法律法规的规定将无法响应用户的请求,但我们将尽最大努力尽快向用户进行反馈。 -1)为订立、履行用户作为一方当事人与我们之间的合同所必需; -2)为履行法定义务所必需; -3)为应对突发公共卫生事件,或者紧急情况下为保护用户的生命健康和财产安全所必需; -4)为公共利益实施新闻报道、舆论监督等行为,在合理的范围内处理个人信息; -5)依照法律规定在合理的范围内处理用户自行公开或者其他已经合法公开的个人信息; -6)法律、行政法规规定的其他情形。 - -如用户对第三方开发者或我们如何实现上述权利存在疑问,可以根据本隐私政策第八条所述方式与我们联系。 - -## 五、我们如何处理儿童个人信息 - -我们非常重视未成年人的个人信息保护工作。根据相关法律法规的规定,若用户是 18 周岁以下的未成年人,在使用我们的服务前,应在用户监护人的监护、指导下共同阅读并同意本隐私政策。若用户的监护人对所监护未成年人的个人信息有相关疑问时,请通过本隐私政策第八条所述方式与我们联系。 - -特别地,若用户是 14 周岁以下的儿童,我们还专门制定了[《儿童个人信息保护规则》](https://www.taptap.cn/doc/children-privacy/),儿童及其监护人在使用我们的服务前,应仔细阅读[《儿童个人信息保护规则》](https://www.taptap.cn/doc/children-privacy/)。只有在取得监护人对[《儿童个人信息保护规则》](https://www.taptap.cn/doc/children-privacy/)的同意后,14 周岁以下的儿童方可使用我们的服务。 - -如果经我们发现第三方开发者未获得儿童的监护人同意向我们提供儿童个人信息的,我们将尽快删除该儿童的个人信息并确保不对其进一步处理。 - -## 六、我们如何保护用户的个人信息 - -我们会为用户的信息安全提供保障,以防止信息的丢失、不当使用、未经授权访问或披露。为此,我们将采取合理的安全措施(主要包括技术方面和管理方面)来保护用户的个人信息。 - -6.1 技术措施 - -1)使用加密技术以确保数据传输的保密性; -2)提供多种安全功能以保护用户的账号安全; -3)使用去标识化、匿名化等合理可行的手段保护用户的个人信息; -4)使用受信赖的保护机制防止数据遭到恶意攻击。 - -6.2 管理措施 - -1)建立数据处理和访问制度,以防未经授权的人员访问我们的系统; -2)对员工进行安全教育与培训,签署保密协议,加强员工对于保护个人信息安全重要性的认识; -3)成立专门的数据安全部门,并制定个人信息安全事件的应急预案; -4)定期组织安全应急预案演练,预防此类安全事件发生; -5)若发生个人信息泄露、损毁、丢失等安全事件,我们会启动应急预案,阻止安全事件扩大。安全事件发生后,我们会及时以推送通知、邮件等形式向第三方开发者告知安全事件的基本情况、我们即将或已经采取的处置措施和补救措施,以及我们对用户的应对建议以及法律法规要求披露的其他事项。如您是第三方开发者,您应当按法律规定告知用户。 - -**但请用户理解,互联网并非绝对安全的环境,任何安全措施都无法做到无懈可击。我们建议用户采取积极措施保护个人信息的安全,包括但不限于使用复杂密码、定期修改密码、不将自己的账号密码等个人信息透露给他人等**。 - -## 七、本隐私政策如何更新 - -为给第三方开发者和/或用户提供更好的服务,以及随着 TapSDK 产品和/或服务的不断发展与变化,我们可能会根据产品或服务的更新情况及法律法规的相关要求适时更新本隐私政策。 - -当本隐私政策的条款发生变更时,我们会以页面显著位置提示等合理方式进行通知,并说明生效日期。如果更新后的本隐私政策对处理用户的个人信息情况有重大变化的,如您是第三方开发者,您应当适时更新隐私政策,并以弹窗等形式通知用户并且获得其同意,如果用户不同意接受本隐私政策,请禁止该用户使用我们的服务。 - -## 八、联系我们 - -如您对本隐私政策的内容及相关事宜有任何疑问、意见或建议,您可以通过以下方式与我们联系。 - -TapSDK 个人信息保护负责人邮箱:`privacy@taptap.com` -联系地址:上海市静安区灵石路 718 号 B1 北楼 TapTap 隐私与数据合规中心(收) -邮编:200072 - -我们将在 15 天内予以回复。 - -本页面内容具有多种语言版本,若其他语言版本与简体中文版本发生冲突,应以简体中文版本为准。 \ No newline at end of file diff --git a/docs/sdk/start/compliance.mdx b/docs/sdk/start/compliance.mdx index 1fc303fce..77d7757b4 100644 --- a/docs/sdk/start/compliance.mdx +++ b/docs/sdk/start/compliance.mdx @@ -1,656 +1,71 @@ --- -title: TapSDK 合规使用说明 -sidebar_position: 9 +title: SDK 合规使用说明 +sidebar_position: 3 --- import CodeBlock from "@theme/CodeBlock"; import sdkVersions from "/src/docComponents/sdkVersions"; -更新日期:2024 年 3 月 28 日 +发布日期:2023 年 10 月 13 日 -生效日期:2024 年 3 月 28 日 +生效日期:2023 年 10 月 13 日 -易玩(上海)网络科技有限公司(以下简称 “TapTap” 或“我们”)将通过本文档向开发者和其用户介绍 TapSDK 的标准使用方式以及相关建议。 -## **一、TapSDK 数据收集合规步骤** +为有效治理 App 强制授权、过度索权、超范围收集个人信息等现象,落实《网络安全法》《消费者权益保护法》的要求,保障个人信息安全,2019 年 1 月,中央网信办、工信部、公安部、市场监管总局等四部委发布了《关于开展 App 违法违规收集使用个人信息专项治理的公告》,在全国范围组织开展 App 违法违规收集使用个人信息专项治理,并陆续出台完善了《App 违法违规收集使用个人信息行为认定方法》、《GB/T 35273-2020 信息安全技术 个人信息安全规范》等标准规范。 -TapSDK 提供延迟初始化的方式来满足合规。开发者应在用户同意 《隐私政策》后,初始化 SDK 进行数据收集。具体参考如下步骤: +2020 年以来,随着工信部纵深推进 App 专项整治行动,以及陆续检测通报 App 违规情况,监管部门、各行业参与方、终端用户都越来越关注 App 和 SDK 的安全问题。 -``` -if(未同意隐私协议){ - //展示隐私协议弹窗相关逻辑 - if(用户同意隐私协议){ - TapBootstrap.init(); - } -}else{ - TapBootstrap.init(); -} -``` +为帮助 LeanCloud SDK的使用者们更清楚地了解监管要求、高效落实个人信息保护相关事宜,LeanCloud(公司全称:美味书签(北京)信息技术有限公司)特编写了《LeanCloud SDK合规指南》(以下简称“指南”),供开发者们参考。 +## 一、App 个人信息保护的合规要求 -## **二、TapSDK 基本业务功能** +1. App 首先需制定一份《隐私政策》,并确保在产品界面中显著展示。《隐私政策》须单独成文,而不是作为用户协议、用户说明等文件中的一部分存在。App 应在《隐私政策》中明示收集使用个人信息的目的、方式和范围,并确保《隐私政策》链接正常有效,易于访问和阅读。 +2. App 应在《隐私政策》中将收集个人信息的业务功能以及每个业务功能所收集的个人信息类型进行逐项列举,不应使用“等、例如” 等方式概括说明;同时,App 须对个人敏感信息类型进行显著标识(如字体加粗、标星号、下划线、斜体、颜色等)。如果通过嵌入第三方代码、插件等方式将个人信息传输至第三方服务器,应通过弹窗提示等方式明确告知用户。 -业务功能介绍:由于开发者可以根据自身需求接入 TapSDK 的任意功能,因此根据定义,无基本业务功能。 -## **三、TapSDK 扩展业务功能** +## 二、App 使用 LeanCloud SDK 时的合规指引 +1. 您应确保在 App 首次运行时通过明显方式提示终端用户阅读您的《隐私政策》,并取得终端用户的合法授权后,再初始化 SDK 进行信息收集与处理。如果终端用户不同意您的隐私政策,则不能初始化 LeanCloud 的各项 SDK,也无法使用相应 SDK 对应功能。 +2. 如果您的 App 在终端用户首次运行时,需要注册用户账号才能使用,则可以在账号注册环节提示终端用户同意您的《隐私政策》,之后完成注册;如果您的 App 并不一定需要终端用户注册用户账号才能使用,那么如果终端用户不同意您的《隐私政策》,按照最新合规政策,您不应停止让终端用户使用您的 App,仍需保留用户的基本使用权利。若终端用户将使用到需收集相关个人信息才能使用的功能(比如混合推送服务),可以再次提醒终端用户需要同意您的《隐私政策》才能正常使用相关功能,若终端用户仍不同意,则无法提供对应功能,可在终端用户下次需要使用时再提示同意您的《隐私政策》。 -### 1. 更新唤起 +### 对混合推送的特别说明 -- 功能介绍 +若您使用了 LeanCloud 混合推送 SDK,针对各类厂商的推送 SDK,建议您在 APP 自身隐私政策中添加各项 SDK 的隐私政策文本模版如下: -更新唤起服务主要应用于在 TapTap 国内商店分发的游戏包体更新场景。 +| SDK 名称 | SDK 厂商 | 合作目的 | 收集个人信息 | SDK 隐私政策链接 | +| ----------------- | ----------------- | ----------------- | ---------------- | ---------------- | +| 华为推送 SDK | 华为软件技术有限公司 | 用于华为手机的消息推送 | 设备信息【包括:设备型号、设备名称、SIM卡序列号、设备唯一标识符(IMEI、IMSI、AndroidID、IDFA、OAID),以下同】、网络信息 | https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/sdk-data-security-0000001050042177 | +| 荣耀推送 SDK | 荣耀终端有限公司 | 用于荣耀手机的消息推送 | 设备信息、网络信息 | https://www.hihonor.com/cn/privacy/privacy-policy/ | +| 小米推送 SDK | 北京小米移动软件有限公司 | 用于小米手机的消息推送 | 设备标识符(如 Android ID、OAID、GAID)、设备信息 | https://dev.mi.com/console/doc/detail?pId=1822 | +| Oppo 推送 SDK | 广东欢太科技有限公司 | 用于 Oppo 手机的消息推送 | 设备标识符(如 IMEI、ICCID、IMSI、Android ID、GAID)、应用信息(如应用包名、版本号和运行状态)、网络信息(如 IP 或域名连接结果,当前网络类型) | https://open.oppomobile.com/wiki/doc#id=10288 | +| Vivo 推送 SDK | 维沃移动通信有限公司 | 用于 Vivo 手机的消息推送 | 设备信息 | https://www.vivo.com.cn/about-vivo/privacy-policy | +| 魅族推送 SDK | 珠海市魅族通讯设备有限公司 | 用于魅族手机的消息推送 | 设备标识信息、位置信息、网络状态信息、运营商信息 | https://i.flyme.cn/privacy | -- 合规调用时机 +### LeanCloud 隐私政策模版 +在使用 LeanCloud 各项 SDK 产品时,开发者需在 App《隐私政策》的 “与授权合作伙伴共享”条款中,将 LeanCloud 的用户隐私政策 加入其中,并向终端用户逐一明示您嵌入的 SDK 收集使用个人信息的目的、方式和范围。 -玩家打开游戏后对该功能进行初始化和调用。 +在 APP 自身隐私政策中添加 LeanCloud 各项 SDK 的隐私政策文本模版如下: -- 需要权限 +| SDK 名称 | SDK 厂商 | 合作目的 | 收集个人信息 | SDK 隐私政策链接 | +| ----------------- | ----------------- | ----------------- | ---------------- | ---------------- | +| LeanCloud 存储 SDK | 美味书签(北京)信息技术有限公司 | 为 App 用户提供结构化数据存储技术服务 | 无 | /sdk/privacy.html | +| LeanCloud RTM SDK | 美味书签(北京)信息技术有限公司 | 为 App 用户提供实时聊天技术服务 | 无 | /sdk/privacy.html | +| LeanCloud 混合推送 SDK | 美味书签(北京)信息技术有限公司 | 为 App 用户提供消息推送技术服务 | 除厂商收集信息外,本 SDK 不收集个人信息 | /sdk/privacy.html | +| LeanCloud 多人对战 SDK | 美味书签(北京)信息技术有限公司 | 为 App 用户提供多人对战技术服务 | 无 | /sdk/privacy.html | -| 权限 | 使用目的 | 权限申请时机 | -| ------ | ------| ------ | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | -| 安装 APK 权限 | 用于安装 TapTap 客户端 | 用户首次使用该功能时会申请权限 | +## 三、合规文件指引 +为了更及时高效地落实合规要求,我们建议各位开发者充分了解现有以及陆续将发布的有关个人信息保护的法律、法规、政策、标准等,以下资料供您参考: -- 关闭功能的配置方式 +- [《GB/T 35273-2020 信息安全技术 个人信息安全规范》](http://c.gb688.cn/bzgk/gb/showGb?type=online&hcno=4568F276E0F8346EB0FBA097AA0CE05E) +- [《网络安全标准实践指南—移动互联网应用程序(App)个人信息保护常见问题及处置指南》](https://www.tc260.org.cn/front/postDetail.html?id=20200918162332) +- [《网络安全实践指南—移动互联网应用基本业务功能必要信息规范》](https://www.tc260.org.cn/front/postDetail.html?id=20190531230315) +- [《网络安全标准实践指南—移动互联网应用程序(App)收集使用个人信息自评估指南(征求意见稿)》](https://www.tc260.org.cn/front/postDetail.html?id=20200319113609) +- [《网络安全标准实践指南—移动互联网应用程序(App)个人信息安全防范指引(征求意见稿)》](https://www.tc260.org.cn/front/postDetail.html?id=20200330091643) +- [《网络安全标准实践指南—移动互联网应用程序(App)系统权限申请使用指南》](https://www.tc260.org.cn/front/postDetail.html?id=20200918163359) +- [《网络安全标准实践指南—移动互联网应用程序(App)中的第三方软件开发工具包(SDK)安全指引(征求意见稿)》](https://www.tc260.org.cn/front/postDetail.html?id=20200918155732) +- [《App 违法违规收集使用个人信息行为认定方法》](http://www.cac.gov.cn/2019-12/27/c_1578986455686625.htm) -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/update/guide/#tapsdk-%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: -``` -// TapUpdate.Init("clientId", "clientToken"); +如有任何问题,您可通过以下方式与我们联系: -``` +邮箱:leancloud-support@xd.com -或者移除掉统一初始化入口(详见[文档](https://developer.taptap.cn/docs/sdk/start/quickstart/#%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// var config = new TapConfig.Builder() -// .ClientID("your_client_id") // 必须,开发者中心对应 Client ID -// .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token -// .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -// .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 -// .ConfigBuilder(); -// TapBootstrap.Init(config); - -``` -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| 系统版本 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 显示及UI 交互时触发一次 | -| 设备型号 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 显示及UI 交互时触发一次 | -| 设备 CPU 信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 显示及UI 交互时触发一次 | -| 网络类型 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | -| Android ID | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 初始化及用户发起授权时获取一次 | -| 设备内存信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | -| 设备指定应用信息 | 获取当前已安装 TapTap 客户端信息 | 当设备未安装时,引导用户完成 TapTap 客户端 | 每次应用冷启动获取一次 | - -- 可选个人信息 - -无 - - -### 2. TapTap 登录 - -- 功能介绍 - -提供 TapTap 登录方式,玩家可以通过 TapTap 授权快速开始游戏。 - -- 合规调用时机 - -玩家点击「TapTap 登录」按钮时进行初始化和调用。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/taptap-login/guide/tap-login/#%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// TapLogin.Init(string clientID); - -try -{ - // 在 iOS、Android 系统下,会唤起 TapTap 客户端或以 WebView 方式进行登录 - // 在 Windows、macOS 系统下显示二维码(默认)和跳转链接(需配置) -// var accessToken = await TapLogin.Login(); -// Debug.Log($"TapTap 登录成功 accessToken: {accessToken.ToJson()}"); -} -``` -或者移除掉统一初始化入口(详见[文档](https://developer.taptap.cn/docs/sdk/start/quickstart/#%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// var config = new TapConfig.Builder() -// .ClientID("your_client_id") // 必须,开发者中心对应 Client ID -// .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token -// .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -// .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 -// .ConfigBuilder(); -// TapBootstrap.Init(config); -``` - -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| 系统版本 | 为帮助用户保障登录功能的正常生效和安全稳定 | 使用网页登录 | 用户使用网页登录时获取一次 | -| 设备版本 | 为帮助用户保障登录功能的正常生效和安全稳定 | 用户授权及验证登录有效性 | 初始化及用户发起授权时获取一次 | - -- 可选个人信息 - -无 - -### 3. 合规认证 - -- 功能介绍 - -基于 TapTap 账号的快速实名认证功能,对使用 TapTap 账号登录游戏的玩家,在经过玩家同意授权之后,允许玩家使用在 TapTap 里已经通过国家认证的实名信息快速完成游戏中的认证流程。 - -- 合规调用时机 - -玩家实名认证或游戏检查当前登录用户是否合规时进行初始化和调用。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | -| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | - - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/anti-addiction/guide/#sdk-%E9%85%8D%E7%BD%AE)),示例如下: - - {` -"dependencies":{ - ... -// "com.tapsdk.antiaddiction":"https://github.com/taptap/TapAntiAddiction-Unity.git#${sdkVersions.taptap.unity}", -} -`} - -``` -// AntiAddictionConfig config = new AntiAddictionConfig() -// { -// gameId = "your_client_id", // TapTap 开发者中心对应 Client ID -// showSwitchAccount = false, // 是否显示切换账号按钮 -// }; - -// AntiAddictionUIKit.Init(config, callback); -``` - -或者移除掉统一初始化入口(详见[文档](https://developer.taptap.cn/docs/sdk/start/quickstart/#%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// var config = new TapConfig.Builder() -// .ClientID("your_client_id") // 必须,开发者中心对应 Client ID -// .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token -// .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -// .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 -// .ConfigBuilder(); -// TapBootstrap.Init(config); -``` - -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| WiFi 信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 仅网络请求异常时获取状态一次 | -| 系统版本 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | - -- 可选个人信息 - -无 - -### 4. 正版验证、DLC - -- 功能介绍 - -TapTap 的正版验证服务适用于付费下载的游戏,当玩家在使用付费游戏时,校验玩家是否已经成功购买付费游戏。 -TapTap 开发者服务,支持付费的可下载内容(DLC),让玩家不离开游戏便能浏览、购买、拥有新内容。 - -- 合规调用时机 - -玩家使用游戏内购及触发游戏版本验证时。 - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的调用代码(详见[文档](https://developer.taptap.cn/docs/sdk/copyright-verification/guide/)),示例如下: - -``` -// TapLicense.Check(); -// TapLicense.QueryDLC(string[] skuIds); - -``` -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| 设备指定应用信息 | 获取已安装的 TapTap 客户端信息 | 使用 TapTap 客户端完成游戏版本验证及内购 | 在该功能调用对应版本验证及付费时配置 | 每次应用冷启动获取一次 | - -- 可选个人信息 - -无 - - -### 5. 内嵌动态 - -- 功能介绍 - -玩家可以在游戏内访问 TapTap 的社区论坛(官方公告、游戏攻略、问题反馈、热门话题等),同时也可以看到 TapTap 好友的游戏动态,并参与其他玩家、官方和大神之间的互动。 - -- 合规调用时机 - -玩家打开内嵌动态或者开始接收动态通知时进行初始化和调用。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | -| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | -| 读写存储权限 | 用于发布或下载动态页面内图片、视频 | 下载或使用本地图片发布动态时申请 | - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/embedded-moments/guide/#%E5%8D%95%E7%BA%AF%E7%9A%84%E5%86%85%E5%B5%8C%E5%8A%A8%E6%80%81%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -//TapMoment.Init(string clientID); - -``` - -或者移除掉统一初始化入口(详见[文档](https://developer.taptap.cn/docs/sdk/start/quickstart/#%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// var config = new TapConfig.Builder() -// .ClientID("your_client_id") // 必须,开发者中心对应 Client ID -// .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token -// .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -// .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 -// .ConfigBuilder(); -// TapBootstrap.Init(config); -``` - -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| WiFi 信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示动态页面时获取一次 | -| 系统版本 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示动态页面时获取一次 | -| 设备版本 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示动态页面时获取一次 | -| Android ID | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示动态页面时获取一次 | -| 手机样式 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示动态页面时获取一次 | -| 设备内存 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示动态页面时获取一次 | - - - -- 可选个人信息 - -无 - -### 6. TapTap Connect(悬浮窗) - -- 功能介绍 - -TapTap Connect(以下简称悬浮窗)是连接 TapTap、游戏和玩家的工具。开发者启用该功能后,成功登录 TapTap 账号的玩家可在悬浮窗内进行浏览游戏动态、评价游戏、分享游戏下载链接等操作,来帮助游戏提升留存并带来新用户。 - -- 合规调用时机 - -玩家 Tap 账号登录成功后。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/taptap-connect/guide/#tapsdk-%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// TapConnect.Init("your_client_id", "your_client_token", (bool)isCN); - -``` -或者移除掉统一初始化入口(详见[文档](https://developer.taptap.cn/docs/sdk/start/quickstart/#%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// var config = new TapConfig.Builder() -// .ClientID("your_client_id") // 必须,开发者中心对应 Client ID -// .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token -// .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -// .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 -// .ConfigBuilder(); -// TapBootstrap.Init(config); -``` - -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| 系统版本 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 悬浮窗 UI 发生变化时获取一次 | -| 设备型号 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 悬浮窗 UI 发生变化时获取一次 | -| 设备 CPU 信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 悬浮窗 UI 发生变化时获取一次 | -| 网络类型 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | -| Android ID | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 初始化及用户发起授权时获取一次 | -| 设备内存信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | -| 设备方向传感器信息 | 为了确保悬浮窗口在设备中正常显示 | 悬浮窗显示 | 每次应用冷启动获取一次 | -- 可选个人信息 - -无 - - -### 7. 成就系统 - -- 功能介绍 - -可以在游戏中设置「普通成就」和「白金成就」,增加玩家在游戏中的参与度,鼓励玩家以不同的玩法来玩游戏。 - -- 合规调用时机 - -玩家查看游戏成就时进行初始化和调用。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/achievement/guide/#sdk-%E8%8E%B7%E5%8F%96)),示例如下: - - - {` -"dependencies":{ - ... -// "com.taptap.tds.achievement": "https://github.com/TapTap/TapAchievement-Unity.git#${sdkVersions.taptap.unity}", -} -`} - -或者移除掉统一初始化入口(详见[文档](https://developer.taptap.cn/docs/sdk/start/quickstart/#%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// var config = new TapConfig.Builder() -// .ClientID("your_client_id") // 必须,开发者中心对应 Client ID -// .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token -// .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -// .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 -// .ConfigBuilder(); -// TapBootstrap.Init(config); -``` - -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| 设备名称 | 用于本地数据加密处理 | 保障用户数据在本地的安全性 | 仅在首次使用时收集一次 | - -- 可选个人信息 - -无 - - -### 8. 内建账户 - -- 功能介绍 - -TDS 内建账户服务致力于帮助开发者快速低成本地构建一个安全可靠的玩家登录系统。支持玩家采用包括游客账号、第三方账号(TapTap、Apple、微信、QQ 等)在内的多种账号来登录你的游戏。 - -- 合规调用时机 - -通过 [TapTap] 或其他登录方式使用内建账户初始化时调用。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除掉统一初始化入口(详见[文档](https://developer.taptap.cn/docs/sdk/start/quickstart/#%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// var config = new TapConfig.Builder() -// .ClientID("your_client_id") // 必须,开发者中心对应 Client ID -// .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token -// .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -// .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 -// .ConfigBuilder(); -// TapBootstrap.Init(config); -``` - -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | ------ | -| 系统版本 | 为帮助用户保障登录功能的正常生效和安全稳定 | 用户发起登录 | 用户登录授权时获取一次 | -| 设备型号 | 为帮助用户保障登录功能的正常生效和安全稳定 | 用户发起登录 | 用户登录授权时获取一次 | -| 设备 CPU 信息 | 为帮助用户保障登录功能的正常生效和安全稳定 | 用户发起登录 | 用户登录授权时获取一次 | -| 网络类型 | 为帮助用户保障登录功能的正常生效和安全稳定 | 用户发起登录 | 每次应用冷启动获取一次 | -| Android ID | 为帮助用户保障登录功能的正常生效和安全稳定 | 用户发起登录 | 初始化及用户发起授权时获取一次 | -| 设备内存信息 | 为帮助用户保障登录功能的正常生效和安全稳定 | 用户发起登录 | 每次应用冷启动获取一次 | - -- 可选个人信息 - -无 - -### 9. 客服 - -- 功能介绍 - -Tap 游戏客服能帮助游戏运营团队更快更好地解决玩家遇到的问题。 - -- 合规调用时机 - -玩家打开客服页面时。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | -| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | -| 读取存储权限 | 用于反馈工单时添加本地图片、视频附件 | 上传附件时申请 | - - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/tap-support/guide/#sdk-%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: -``` -// TapSupport.Init("https://please-replace-with-your-customized.domain.com", "产品 ID"); -``` - -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| 系统版本 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示客服页面时获取一次 | -| 设备版本 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示客服页面时获取一次 | -| 网络类型 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示客服页面时获取一次 | -| 手机样式 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示客服页面时获取一次 | -| 设备内存 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 展示客服页面时获取一次 | -| Android ID | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 初始化及用户发起授权时获取一次 | -- 可选个人信息 - -无 - -### 10. 数据分析 - -- 功能介绍 - -提供了一套专注于解决游戏项目数据需求的分析工具,通过简单的接入就可以获得丰富实用的数据看板和广告追踪能力,让数据分析和广告投放变得轻松易操作,同时也可以用于分析人群画像,帮助开发者更好地理解用户。 - -- 合规调用时机 - -玩家触发开发者配置的事件时进行初始化和调用。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | -| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | -| 读写存储权限(可选) | 用于存储用户标识 | 开发者根据业务需求选择是否需要申请 | -| 读取电话状态(可选) | 获取移动端设备网络类型 | 开发者根据业务需求选择是否需要申请 | - -开发者如果不需要上述可选权限,无需额外处理,SDK 不会主动申请;当开发者需要时,需在工程中添加如下: - -``` -//检查及申请权限 -if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(MainActivity.this, new String[] { Manifest.permission.READ_PHONE_STATE }, READ_PHONE_STATE_PERMISSION_CODE); -} - -// 接收授权结果回调 -@Override -public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - if (requestCode == READ_PHONE_STATE_PERMISSION_CODE) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // 用户授权 - } else { - // 用户拒绝授权 - } - } -} - -``` - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/tapdb/sdk/client-side-integration/#tapdb-%E5%8D%95%E7%8B%AC%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// TapDB.Init("clientId", "taptap", "gameVersion", true); -``` - -或者移除掉统一初始化入口(详见[文档](https://developer.taptap.cn/docs/sdk/start/quickstart/#%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// var config = new TapConfig.Builder() -// .ClientID("your_client_id") // 必须,开发者中心对应 Client ID -// .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token -// .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -// .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 -// .ConfigBuilder(); -// TapBootstrap.Init(config); -``` - -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| WiFi 信息 | 供开发者进行数据分析,是业务需求之一 | 开发者可根据该信息来进行数据分析 | 应用冷启动获取一次 | -| 系统版本 | 供开发者进行数据分析,是业务需求之一 | 开发者可根据该信息来进行数据分析 | 应用冷启动获取一次 | -| 设备版本 | 供开发者进行数据分析,是业务需求之一 | 开发者可根据该信息来进行数据分析 | 应用冷启动获取一次 | -| 手机样式 | 供开发者进行数据分析,是业务需求之一 | 开发者可根据该信息来进行数据分析 | 应用冷启动获取一次 | -| 传感器列表 | 供开发者进行数据分析,是业务需求之一 | 开发者可根据该信息来进行数据分析 | 应用冷启动获取一次 | -| Android ID | 供开发者进行数据分析,是业务需求之一 | 开发者可根据该信息来进行数据分析 | 应用冷启动获取一次 | - -- 可选个人信息 - -无 - - -### 11. 诊断接入 - -- 功能介绍 - -TapTap 开发者服务提供的应用安全模块,主要是通过 APK 加固和反作弊检测等能力,来为游戏应用程序的安全进行保驾护航。 - -- 合规调用时机 - -玩家触发开发者配置的事件上报时机时。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -- 关闭功能的配置方式 -- 默认启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/tapdb/sdk/client-side-integration/#%E5%90%AF%E7%94%A8%E8%AE%BE%E7%BD%AE)),示例如下: - -``` -// TapDB.EnableThemis(false); - -``` - -- 必要个人信息 - -| 必要个人信息 | 使用目的 | 场景 | 收集频次 | -| ------ | ------ | ------ | ------ | -| 系统版本 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | -| 设备型号 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | -| 网络类型 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | -| 设备内存信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | -| 设备 CPU 信息 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | - 手机样式 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | - 设备名称 | 为了确保设备系统兼容、定位解决问题 | 遇到服务故障时针对性进行排查和优化 | 每次应用冷启动获取一次 | -- 可选个人信息 - -无 - -### 12. 数据存储 - -- 功能介绍 - -数据存储服务能够高效存取海量级 JSON 对象、二进制文件、地理位置等数据。其内置的行级 ACL 权限控制,以及通用的用户及角色管理体系,可以帮助你快速实现安全而灵活的数据访问。 - -- 合规调用时机 - -用户登录或注册账号时。 - -- 需要权限 - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -- 关闭功能的配置方式 - -默认不启动,由开发者按需调用。若要关闭,可在应用中移除该功能相应的初始化代码(详见[文档](https://developer.taptap.cn/docs/sdk/storage/guide/setup-java/#android-%E5%B9%B3%E5%8F%B0%E5%88%9D%E5%A7%8B%E5%8C%96)),示例如下: - -``` -// LeanCloud.initialize(this, "your-client-id", "your-client-token", "https://your_server_url"); - -``` - -### 13. 其他服务 - -以下功能为云服务,都是基础的技术服务,不收集用户个人信息: - -- 云存档 -- 排行榜 -- 云引擎 - - -## **四、向最终用户披露 TapSDK 条款** - -在接入TapSDK 后,开发者应当向最终用户披露 TapSDK 条款,具体建议如下: - -1. 在用户同意游戏的《隐私协议》后再进行 TapSDK 初始化 -2. 在隐私协议中披露接入 TapSDK 的情况 -> 名称:TapSDK -> -> 公司名称:易玩(上海)网络科技有限公司 -> -> 收集个人信息类型:网络状态、WiFi 信息、设备版本、系统版本、设备型号、操作系统等 -> -> 使用目的:TapTap 登录、合规认证、正版验证等 -> -> 隐私政策链接:[TapSDK 隐私政策 | TapTap 开发者文档](https://developer.taptap.cn/docs/sdk/start/agreement/) - - -示例:TapTap 在隐私协议中披露使用的第三方 SDK 清单。 - - - ![privacy_overview](/img/tapsdk-compliance/privacy_overview.png)![sdk_info_list](/img/tapsdk-compliance/sdk_info_list.png) - -3. 在申请具体权限前,向用户说明需要权限的具体原因(需提供同意和拒绝按钮 ),当用户同意后,再请求系统权限。示例: -![request_first](/img/tapsdk-compliance/permission_request_1.png) ![request_second](/img/tapsdk-compliance/permission_request_2.png) diff --git a/leancloud/docs/sdk/start/dashboard-guide.mdx b/docs/sdk/start/dashboard-guide.mdx similarity index 100% rename from leancloud/docs/sdk/start/dashboard-guide.mdx rename to docs/sdk/start/dashboard-guide.mdx diff --git a/docs/sdk/start/faq.mdx b/docs/sdk/start/faq.mdx index 9a592e3c6..d436f65a3 100644 --- a/docs/sdk/start/faq.mdx +++ b/docs/sdk/start/faq.mdx @@ -1,120 +1,151 @@ --- -title: 常见问题 -sidebar_position: 6 +title: 账户和控制台常见问题 +sidebar_label: 常见问题 +sidebar_position: 5 --- -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; +:::tip +这篇文档仅介绍**账户和控制台**相关的常见问题,使用具体的服务遇到问题可参考对应文档目录下列出的常见问题,如[数据存储常见问题](/sdk/storage/faq/)、[云引擎常见问题](/sdk/engine/faq/)等。 +::: + +## 平台 + +### LeanCloud 部署在哪个云平台上 + +LeanCloud 部署在国内多个云计算平台上,并采用在双线机房内同时使用虚拟机和实体机的混合部署策略,来保证应用的访问体验和可靠性。 + +### 哪里获取平台的更新信息 + +通常情况下,我们新版本的更新周期为一到两周。获取更新信息可以通过: -## 综合 +* [官方博客](https://leancloudblog.com/)(每次更新的详细信息都会发布在那里) +* [官方微博](https://weibo.com/avoscloud) +* 官方微信公众号:LeanCloud通讯 +* 每月初,我们会将每月的更新摘要发送到您的注册邮箱。 +* 在控制台页面的右上方有消息中心,请注意查看新通知。 -### TDS 云服务带高防吗? +## 节点选择 -TDS 可以防护小规模的攻击,开发者无需为此承担额外的费用。 -独占游戏在独占期间 TDS 免费提供高防服务,开发者需要事先和 TDS 方面沟通配置细节。 -其他游戏,TDS 可以协助接入 IaaS 服务商的高防,开发者需要承担相应的费用(IaaS 服务商会按高防带宽收费),详询商务。 +### 华北节点、华东节点与国际版如何选择,各节点的区别是什么? -### 请问同一身份证实名认证的账号是否有上限 +如果用户主体在国内,可以选择国内的华北和华东节点;如果用户主体在国外,则可以选择国际版。跨节点访问(例如国内访问国际版,或者国外访问国内节点)会有网络延迟,所以建议根据用户所在地选择节点。 -目前 TapTap 的要求是同一个身份证号最多只能实名 5 个 TapTap 账号,对于游戏账号实名没有限制。 +华北节点和华东节点在国内不同地区的访问速度基本没有差别。目前华北节点的功能更完善一些,有一些功能在华东还不支持,例如,华东节点暂不支持在控制台自助绑定独立 IP、暂时没有 API 访问日志功能、混合推送中不支持 FCM 推送。如果需要使用这些服务可以选择华北节点。 -### 如何获取 Android 应用的 MD5 值? +### 应用如何切换节点? -#### 一、通过 APP 工具获取 +应用不支持一键切换节点,「应用转让」功能也仅限在相同节点转让。如果一定要切换节点,可以通过数据导入导出的方式实现,即在另外的节点新建一个应用,将当前节点应用数据导出后导入到新创建的应用。 -当只有 APK 文件包时,为了正确填写签名 MD5 值,可以使用如下的工具进行获取:[**GenSignatureMD5**](https://capacity-files.lcfile.com/vW65JxH2b2KwDS8JcbVUfiwLHSeHTlD5/tds_getsign.apk),工具使用方式:使用正式的签名证书对游戏应用进行签名打包,然后将 APK 包安装到手机上。与此同时,将 GenSignatureMD5 工具也安装到同一部手机上,然后打开该工具输入游戏包名就可以得到签名 MD5 值。 +需要注意: -#### 二、通过 Android Studio 获取 +* 只有数据存储服务的结构化数据支持导出与导入。 +* 即时通信服务的会话记录与聊天记录无法导出。 +* 云引擎需要重新部署。 +* 短信签名与模板需要重新审核。 +* 推送厂商、开发证书、敏感词库等应用维度的设置信息都需要重新配置。 -通过 Android Studio Terminal 输入以下命令获取: +## 技术支持 -```sh -keytool -exportcert -alias {alias} -keystore {storefile} | openssl dgst -md5 +### 获取客服支持有哪些途径 -或者使用如下命令获取: +* 到免费的 [用户社区](https://forum.leancloud.cn/) 进行提问。 +* 商用版应用的所有者可以进入 [工单系统](https://leanticket.cn) 来提交问题。 +* 账号相关事项,可发送邮件到 获取帮助。 +* 紧急故障拨打客服电话:+8618625038918。 +* 售前咨询请致电 +8613011098244。 -./gradlew signingReport -``` +## 费用 +### 如何付费 -就可以在命令窗口看到签名文件的信息,包括了 SHA1 值和 MD5 值。 +* 支付宝充值 -除了以上方法还可以使用 Android Studio 自带的 Gradle Tasks 查看,双击下图中的 signingReport 后调试窗口会输出 MD5 值。 + 进入 **控制台 > 财务 > 概览**,点击「余额充值」按钮时将会出现「支付宝充值」窗口。我们将每天自动从您的账户余额里扣除前一日的费用。每次扣费优先使用充值金额,其次是赠送金额。 -![](https://capacity-files.lcfile.com/y7fcVDW6cUFKfG4ATDXj8KKE9L2jWprB/%E8%8E%B7%E5%8F%96MD5%E7%A4%BA%E4%BE%8B1.jpeg) +* 对公账户汇款 + + 账户信息请通过 **控制台 > 财务 > 概览** 查看(点击「余额充值」按钮后显示)。 :::caution +请务必在汇款附言里中注明以下信息,以便我们账务确认汇款的来源和用途,及时入账。 + +1. 您的 LeanCloud 用户名 +2. (或)注册邮箱 +::: + +### 如何申请开具发票 + +请参考[控制台指南 > 账单和发票](/sdk/start/dashboard/#账单和发票)一节的说明。 + +### 如果没有缴费会怎么样 -注意,运行 signingReport 调试窗口输出的 MD5 值带冒号分隔符,绑定到开发者中心时需要手动删除冒号。 -> TapTap 开发者中心绑定 MD5 格式举例: -> -> 正确格式:6EB4347CF9C098BE1C8D965D539C42E2 -> -> 错误格式:6E:B4:34:7C:F9:C0:98:BE:1C:8D:96:5D:53:9C:42:E2 +账户余额小于 0 时,账户服务将被停止,即云端会拒绝所有的请求,因此应用的统计数据也无法生成;应用数据被置于不可见模式,但仍会在 LeanCloud 云端保留 30 天。如需要恢复服务和访问应用数据,请登录控制台充值。 +:::caution +欠费超过 30 天,应用内的数据(包括文件)会被删除且无法恢复。 ::: -如果右侧 Gradle 面板没有 Gradle Tasks 选项卡,在设置中关掉下图所示选项,重新 Sync Gradle,即可看到 Gradle Tasks 选项卡。 +你可以在控制台设置告警余额,当账户余额小于设置的值时,我们会发送短信、邮件通知。请开发者务必关注账户的余额情况,以免对业务造成影响。 + +### 欠费期间应用产生的数据和请求能否找回 + +不能。因为欠费时应用处于禁用阶段,所有的请求都会被云端丢弃,与统计相关的数据也不会生成,所以当应用服务恢复后也无法找回这一期间的数据。为避免统计数据出现断档,请在控制台中设置告警余额,及时充值,保证服务的持续。 + +## 隐私政策 + +### LeanCloud SDK 收集哪些数据 + +LeanCloud SDK 不采集个人信息,例如,不会收集设备 Mac 地址,不会采集唯一设备识别码(如 IMEI / android ID / IDFA / OPENUDID / GUID、SIM 卡 IMSI 信息等)对用户进行唯一标识,不会访问其他应用的数据信息。 + +* [LeanCloud Android SDK 个人信息采集说明](https://www.leancloud.cn/privacy/sdk-data-collection/android/) +* [LeanCloud Objective-C SDK Data Collection Practices](https://www.leancloud.cn/privacy/sdk-data-collection/objc/) +* [LeanCloud Swift SDK Data Collection Practices](https://www.leancloud.cn/privacy/sdk-data-collection/swift/) + +LeanCloud SDK 都是开源的,开发者可以审查代码以确定 SDK 是否有其他收集用户数据的行为。 + +## 数据存储 + +### 如何重命名 Class? + +LeanCloud 不支持重命名 Class。 +你可以新建一个 Class,利用 [数据导入导出](/sdk/storage/guide/rest/#数据导出-api) 功能从旧 Class 导出数据,然后导入到新 Class。 +导出导入期间不要往旧 Class 写数据,或者在旧 Class 和新 Class 双写数据,以避免数据不一致。 + +### 如何导入或者导出数据? -![](https://capacity-files.lcfile.com/8dEVF81X34JFtUE50tnqj6OIoxxDdXsU/%E8%8E%B7%E5%8F%96MD5%E7%A4%BA%E4%BE%8B2.jpeg) +* [导入数据](/sdk/start/dashboard/#导入数据) +* [导出数据](/sdk/start/dashboard/#导出数据) +### 导出数据后没收到邮件? -### Android 上 TapSDK 和 B 站 SDK 引用的 okhttp 版本冲突,怎么办? +首先请确认邮箱能够正常接收邮件。 +如果导出数据的数据量较大,导出任务可能需要一定时间才能完成,请耐心等待。 +另外,12 点之后无法导出数据,指的是 12 点后无法提交数据导出任务。 +也就是说,在 12 点前提交的数据导出任务,可能在 12 点之后才实际完成导出。 -TapSDK 现在自动包含了 LeanCloud 核心 SDK,LeanCloud SDK 依赖如下几个基础库: -- com.squareup.okhttp3:okhttp:4.7.2 -- com.squareup.retrofit2:retrofit:2.9.0 -- io.reactivex.rxjava2:rxjava:2.2.19 +### 如何在 App 邮件内完全使用自己的品牌 -有开发者给我们反馈,B 站游戏 SDK 是以 aar 形式提供,里面附带了 3.9.0 版本的 okhttp library(至少在 5.4.0 版之前是如此),与 TapSDK 的依赖产生了冲突,会导致程序启动时报如下错误: -`Caused by: java.lang.NoSuchMethodError: No static method get(Ljava/lang/String;)Lokhttp3/HttpUrl; in class Lokhttp3/HttpUrl; or its super classes (declaration of 'okhttp3.HttpUrl' appears in /data/app/` +请参考 [自定义邮件验证和重设密码页面](/sdk/storage/guide/custom-reset-verify-page/)。 + +### 创建唯一索引失败 -由于 B 站游戏 SDK 固定死了 okhttp 网络库版本,解决这个问题则需要 TapSDK 这里对 okhttp/retrofit/rxjava 等基础库进行降级处理。 -开发者可以拷贝如下配置到应用的 build.gradle 的 `dependencies` section 中: +请确认想要创建索引的列没有已经存在的重复值。 -
    +### 如何上传文件 -build.gradle 的配置 +任何一个 Class 如果有 File 类型的列,就可以直接在 **数据** 管理平台中将文件上传到该列。如果没有,请自行创建列,类型指定为 File。 - {` - implementation('com.taptap:lc-realtime-android:${sdkVersions.leancloud.java}'){ - exclude group: 'com.taptap', module: 'lc-storage-android' - exclude group: 'com.taptap', module: 'lc-realtime-core' - exclude group: 'com.taptap', module: 'lc-storage-core' - } - implementation('com.taptap:lc-storage-android:${sdkVersions.leancloud.java}'){ - exclude group: 'com.taptap', module: 'lc-storage-core' - } - implementation('com.taptap:lc-realtime-core:${sdkVersions.leancloud.java}') { - exclude group: 'com.taptap', module: 'lc-storage-core' - } - implementation('com.taptap:lc-storage-core:${sdkVersions.leancloud.java}') { - exclude group: 'com.squareup.okhttp3', module: 'okhttp' - exclude group: 'com.squareup.retrofit2', module: 'retrofit' - exclude group: 'com.squareup.retrofit2', module: 'adapter-rxjava2' - exclude group: 'com.squareup.retrofit2', module: 'converter-gson' - exclude group: 'io.reactivex.rxjava2', module: 'rxjava' - } - implementation("com.squareup.retrofit2:retrofit:2.3.0") - implementation("com.squareup.retrofit2:adapter-rxjava2:2.3.0") - implementation("com.squareup.retrofit2:converter-gson:2.3.0") - implementation("io.reactivex.rxjava2:rxjava:2.0.0") - implementation("com.google.code.gson:gson:2.8.6") - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'\n - configurations { - all*.exclude group: 'com.squareup.okhttp3' - } -`} +## 应用 -
    +### 如何在应用之间共享数据 -### application id is empty +请参考 [应用间数据共享](/sdk/storage/guide/app-data-share/) 绑定 Class。 -请确保 TapTap SDK 初始化代码在主线程,以避免此问题以及后续会出现的功能异常,例如登录过程中无法访问网络。 +### 应用什么情况下会自动归档? +连续 30 天无 API 请求的开发版应用会自动归档,应用归档后,存储以及依赖存储的服务不可用。云引擎服务仍可以正常访问。归档的应用可以在控制台 > 查看所有应用页面手动激活。如果开发版应用连续 30 天无 API 请求但应用仍在计费(例如数据存储空间使用超过免费额度 1 GB),则不会被归档。 -### TapTap 客户端用户信息修改后不会通知游戏内用户信息修改。 +### 海外用户访问应用速度很慢 -TapTap 客户端不支持同步更新,但是当用户通过客户端登录成功后会返回 `access_token`、`mac_key`, 游戏可以本地缓存该份登录数据, -之后用户每次进入游戏的时候可以调用[服务端认证接口](/sdk/taptap-login/guide/taptap-oauth/)进行一次查询用来同步用户信息。 \ No newline at end of file +由于众所周知的原因,许多海外地区访问国内服务的网络速度不尽如人意。 +如果应用主要面向海外用户,建议使用 [LeanCloud 国际版](https://leancloud.app/)(机房位于北美)。如果应用同时面向国内用户和海外用户,可以考虑购买专线方案改善海外用户访问速度。专线方案仅面向商用版应用,如有需要,可以提交工单联系我们。 diff --git a/docs/sdk/start/get-ready.mdx b/docs/sdk/start/get-ready.mdx deleted file mode 100644 index 4cbec2504..000000000 --- a/docs/sdk/start/get-ready.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: 准备工作 -sidebar_position: 3 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import DomainBinding from "../_partials/setup-domain.mdx"; - -为了能使用 TapTap 开发者服务(TapTap Developer Services,简称 TDS 服务),你需要完成前期的配置工作。 - -## 创建应用 - -在使用 TDS 服务之前,你需要创建一个应用,来完成对接前的准备工作。创建应用请参考[商店指南](/store/)。 - -## 开启应用配置 - -依次进入 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 应用配置**,点击「立即开启」,获得当前应用的基本信息。 - -### 基本信息 - -`Client ID` 是一个应用实体包在 TapTap 开发者中心的唯一身份标识,TapTap 通过 `Client ID` 来鉴别应用的身份。每个应用仅能拥有一个 `Client ID`,如同一个应用区分测试服与正式服,需要创建两个不同的应用,分别开启应用配置。 - -### 适用地区 - - - -一个 client 仅能对应一个地区。这是由于在 TapTap 的账号系统内,将中国大陆用户与全球用户做了隔离区分,互不相通。 - -![](https://capacity-files.lcfile.com/nnQKxgJJzgErOlIOcxnbIHt8Vc1RmGYe/tap_get_ready.png) - - - - - -适用于中国大陆以外的国家和地区。 - -![](https://capacity-files.lcfile.com/SaYP7m4TEQQTpuvy5n2r0GjxAzgim624/io_tap_get_ready.png) - - - -## 域名 - - - -## 隐私声明 - -集成账号服务的功能,需要先签订[《TapTap 平台开发者协议》](/store/store-devagreement/)。使用 TDS 服务,视为你同意前述所有协议,且你将基于这些协议承担相应的法律责任与义务。 diff --git a/leancloud/docs/sdk/start/guide.mdx b/docs/sdk/start/guide.mdx similarity index 100% rename from leancloud/docs/sdk/start/guide.mdx rename to docs/sdk/start/guide.mdx diff --git a/docs/sdk/start/overview.mdx b/docs/sdk/start/overview.mdx deleted file mode 100644 index a0844254c..000000000 --- a/docs/sdk/start/overview.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: 概览 -slug: /sdk -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -TapTap 开发者服务(TapTap Developer Services,简称 TDS 服务)旨在帮助开发者降低游戏研发、运营维护等阶段投入的精力和成本。TDS 整合了各项服务,让开发者能够聚焦在游戏核心乐趣的创造上,创作更优秀的游戏,进而促进游戏行业生态的良性循环,最终让开发者与玩家双双受益。 - - - -:::info - -2022 年 3 月 25 日 0 点之后,在 [TapTap 开发者中心](https://developer.taptap.cn/) 创建的新游戏已经不再支持发布到除中国大陆外的其他国家/地区。 - -出海游戏请前往 [**TapTap.io 开发者中心**](https://developer.taptap.io/) 创建游戏、开启游戏服务、使用国际版文档。 - -::: - - - -TDS 提供以下服务,开发者可以通过在游戏中集成 TapSDK 来开启使用: - -- **[更新唤起](/sdk/update/guide/)**:当游戏 apk 有更新时,支持玩家从游戏内直接跳转至 TapTap 进行游戏 apk 更新。 - -- **[TapTap 登录](/sdk/taptap-login/features/)**:提供 TapTap 登录方式,玩家可以通过 TapTap 授权快速开始游戏。 - - - -- **[TapTap 好友](/sdk/tap-friend/features/)**:玩家通过 TapTap 账号登录后,可以获取玩同一款游戏的 TapTap 互关好友列表。 - - - - - - -- **[合规认证](/sdk/anti-addiction/features/)**:基于 TapTap 账号的快速实名认证功能,对使用 TapTap 账号登录游戏的玩家,在经过玩家同意授权之后,允许玩家使用在 TapTap 里已经通过国家认证的实名信息快速完成游戏中的认证流程。 - - - - -- **[正版验证](/sdk/copyright-verification/features/)**:帮助买断制游戏验证付费下载资格和 DLC 解锁资格。 - -- **[内嵌动态](/sdk/embedded-moments/features/)**:玩家可以在游戏内访问 TapTap 的社区论坛(官方公告、游戏攻略、问题反馈、热门话题等),同时也可以看到 TapTap 好友的游戏动态,并参与其他玩家、官方和大神之间的互动。 - -- **[TapTap Connect (悬浮窗)](/sdk/taptap-connect/features/)**:提供连接 TapTap、游戏和玩家的工具。 - - - - -- **[成就系统](/sdk/achievement/features/)**:可以在游戏中设置「普通成就」和「白金成就」,增加玩家在游戏中的参与度,鼓励玩家以不同的玩法来玩游戏。 - - - - -- **[礼包系统](/sdk/tds-gift/features/)**:TDS 全套礼包系统,旨在帮助开发者快速生成、核销礼包码。 - - - -- **[TapLink](/sdk/taplink/features/)**:帮助玩家从 TapTap 跳转到游戏领取礼包。 - - - -- **[内建账户](/sdk/authentication/features/)**:帮助开发者快速低成本地构建一个安全可靠的玩家登录系统,支持玩家采用包括游客账号、第三方账号(TapTap、Apple、微信、QQFacebook 等)在内的多种账号来登录你的游戏。 - -- **[客服](/sdk/tap-support/features/)**:接入 Tap 游戏客服能帮助游戏运营团队更快更好地解决玩家遇到的问题。 - -- **[数据分析](/sdk/tapdb/features/)**:提供了一套专注于解决游戏项目数据需求的分析工具,通过简单的接入就可以获得丰富实用的数据看板和广告追踪能力,让数据分析和广告投放变得轻松易操作,同时也可以用于分析人群画像,帮助开发者更好地理解用户。 - - - -- **[应用安全](/sdk/taptap-appsafety/features/)**:TapTap 开发者服务提供的应用安全模块,主要是通过 APK 加固和反作弊检测等能力,来为游戏应用程序的安全进行保驾护航。 - - - -- **[数据存储](/sdk/storage/features/)**:数据存储服务能够高效存取海量级 JSON 对象、二进制文件、地理位置等数据。其内置的行级 ACL 权限控制,以及通用的用户及角色管理体系,可以帮助你快速实现安全而灵活的数据访问。 - -- **[排行榜](/sdk/leaderboard/features/)**:基于内建账户系统在游戏中设立排行榜功能,可以推动玩家之间的趣味性竞争,从而帮助提升游戏的玩家活跃度。 - -- **[云存档](/sdk/gamesaves/features/)**:将玩家的游戏进程保存到 TDS 服务器,游戏可以检索已保存的游戏数据,允许玩家从任何设备上的任意一个保存点继续游戏。 - -- **[云引擎](/sdk/engine/overview/)**:你专属的容器云计算平台。它既可以被简单地用来托管静态网站,又可以接受任意程序语言的定制开发来动态处理外来请求,满足业务定制化需求。不用自搭服务器,一样开发后端系统。 - - - -- **[多人在线对战](/sdk/multiplayer/features/)**:依赖云服务轻松实现游戏内玩家匹配、在线对战消息同步等功能。 - - - - - -- **[TapPlay](/sdk/tap-play/features/)**:TapPlay 服务利用沙盒技术实现,旨在帮助开发者实现低成本、高效率的游戏开发,并提高游戏分发的转化率。 - -- **[TapCanary](/sdk/tap-canary/features/)**:将游戏应用的早期版本,发布给内部测试人员或受信任的用户进行封闭式测试,支持云玩模式和 TapPlay(沙盒)模式。 - - - -- **[TapADN](/sdk/tap-adn/features/)**:开发者接入 Tap 联盟 SDK,将游戏内广告流量提供给 Tap 联盟,用户发起广告请求时展示 Tap 联盟广告。 - -使用对应的服务请先完成[开发者注册](https://developer.taptap.cn/)[开发者注册](https://developer.taptap.io/),之后登录开发者中心开启「游戏服务」。 diff --git a/leancloud/docs/sdk/start/privacy.mdx b/docs/sdk/start/privacy.mdx similarity index 100% rename from leancloud/docs/sdk/start/privacy.mdx rename to docs/sdk/start/privacy.mdx diff --git a/docs/sdk/start/quickstart.mdx b/docs/sdk/start/quickstart.mdx index 6e4758be0..639ed8c69 100644 --- a/docs/sdk/start/quickstart.mdx +++ b/docs/sdk/start/quickstart.mdx @@ -1,542 +1,84 @@ --- -title: TapSDK 快速开始 +title: 快速开始 sidebar_label: 快速开始 -sidebar_position: 4 +slug: /sdk/start/quickstart/ +sidebar_position: 1 --- +## 注册账号 -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; +### 账号体系 -本文介绍如何快速接入 TapSDK 并实现 **[TapTap 登录](/sdk/taptap-login/guide/start/)** 功能。 +LeanCloud 国内版()和国际版()独立运营,使用独立的账号系统。 -:::note +国内版、国际版的功能大体相同,使用哪个版本主要取决于应用面向的用户。 +如果你的应用主要面向国内用户,我们建议在国内版注册账号;相应地,如果应用主要面向海外用户,建议在国际版注册账号。 +另外,根据有关部门规定,国内版需要绑定手机、实名认证、绑定已备案的域名才能正常使用。 -[下载](/tap-download) 页面提供了 Unity、Android、iOS 示例项目,可供参考。 -::: - -## 创建应用 - -请登录 [TapTap 开发者中心](https://developer.taptap.cn/)[TapTap 开发者中心](https://developer.taptap.io/) 注册为开发者并创建应用。 - -## 下载 TapTap 应用 - -在测试设备中下载 [TapTap 客户端](https://www.taptap.cn/mobile)[TapTap 客户端](https://www.taptap.io/mobile),测试时会唤起 TapTap 客户端授权登录。若用户设备中未安装 TapTap 客户端,则会打开 WebView 进行登录。 - -## 环境要求 - - -<> +### 注册邮箱 -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 +注册邮箱用于重置密码、受让应用、接收导出数据、接收余额告警等重要通知,十分关键,请**确保使用长期有效、能正常接收邮件的邮箱**注册账号。 +如代表公司注册账号,建议使用公司邮箱注册,以免因离职时遗忘交接而导致不必要的麻烦。 - -<> +使用邮箱注册账号后,会收到欢迎邮件,请点击邮件中的「验证邮箱」按钮完成邮箱验证。 +注册邮箱每个月会收到 LeanCloud 月报(包括产品进展、常见问题等内容),如不想接收此类邮件,可以点击欢迎邮件底部的「立即退订」,或在「控制台 > 账号设置 > Email」修改「邮件选项」。 +重大变更、余额告警等重要邮件无法退订。 -- Android 5.0(API level 21)或更高版本 +## 实名认证 - -<> +注册国内版账号后,控制台会自动跳转至实名认证页面,点击「开始认证」按钮按照提示完成认证即可。 +请根据实际情况选择实名认证类型(个人认证、企业认证)。 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) +个人认证在填写姓名和身份证号后会显示二维码,通过支付宝应用扫描二维码即可完成认证。 +企业认证请根据控制台提示提交相应的企业信息和文件,我们会在 2 个工作日内审核,审核通过后会收到邮件通知。 - +## 账号安全 -<> +为保证账号安全,邮箱、手机发生变更时,请及时在控制台更新。 +在 **控制台 > 账号设置 > 开发者信息**可以修改手机号,**控制台 > 账号设置 > Email**可以修改邮箱。 -* 安装 UE 4.26 及以上版本 -* iOS 12 或更高版本 -* Android 5.0(API level 21)或更高版本 +为增强账号安全性,建议定期更换密码并开启二次认证。 +在**控制台 > 账号设置 > 账号安全**可以修改密码及开启二次认证。 +开启二次认证后,登录控制台以及进行敏感操作时需要提供 Google Authenticator 等应用生成的验证码。 -**支持平台**:Android / iOS +如你选择开启二次认证,**务必妥善保管恢复码**,以免因为手机丢失等原因无法访问账号。 - +## 创建应用 - +无论你打算使用 LeanCloud 的哪些服务,首先都需要创建一个应用。 +点击控制台左上角的 LeanCloud 图标,即可返回控制台首页。 +在控制台首页点击**创建应用**按钮,在创建应用对话框中填写应用名称并选择应用类型即可新建应用。 :::caution - -- 下面的**项目配置**以及**初始化**部分,预设开发者使用基于[内建账户](/sdk/authentication/features/)系统的 TDS 服务。 -- 如果游戏已经有了完整的账户系统,仅需要接入 TapTap 登录、内嵌动态,且不需要 TDS 更多云服务,则不必参考下面的配置和初始化方式,可跳转至 [单纯的 TapTap 登录开发指南](/sdk/taptap-login/guide/tap-login/)、[内嵌动态开发指南](/sdk/embedded-moments/guide/)。 -- 请慎重选择,如果之后需要其他 TDS 服务,升级需要一定的开发成本。 - -::: - -## 项目配置 - - -<> - - - -#### iOS 配置 - -在 `Assets/Plugins/iOS/Resource` 目录下创建 `TDS-Info.plist` 文件,复制以下代码并且**替换其中的 `ClientId`**。如果游戏使用了 TapTap [内嵌动态](/sdk/embedded-moments/features/)或[数据分析](/sdk/tapdb/features/)服务,需要配置相关权限并**替换授权文案**: - -:::tip - -复制使用以下内容时,**请删除空行以及注释**,以免出现 XML 解析时报错,`ApplicationException: expected a key node`。 - +每个账号最多可以创建 50 个应用。未验证手机号的账号无法创建应用。 ::: -```xml - - - - - taptap - - client_id - ClientId - - - NSPhotoLibraryUsageDescription - 说明为何应用需要此项权限 - NSCameraUsageDescription - 说明为何应用需要此项权限 - NSMicrophoneUsageDescription - 说明为何应用需要此项权限 - - NSUserTrackingUsageDescription - 说明为何应用需要此项权限 - - -``` - - -<> - -1. [下载 TapSDK Android](/tap-download),解压后选择需要用到的 SDK 包导入到项目 `project/app/libs` 目录下。 - -2. 打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: - - {` -dependencies { - ... - // 导入 libs 目录下所有 aar 的包: - implementation fileTree(dir: 'libs', include: ['*.aar']) - // 如果需要单独导入 libs 目录下的指定包,请按照如下方式: - implementation files('libs/TapBootstrap_${sdkVersions.taptap.android}.aar') // TapTap 启动器 - implementation files('libs/TapCommon_${sdkVersions.taptap.android}.aar') // TapTap 基础库 - implementation files('libs/TapLogin_${sdkVersions.taptap.android}.aar') // TapTap 登录 - ... - // 数据存储 - implementation 'com.taptap:lc-storage-android:${sdkVersions.leancloud.java}' - // 即时通讯 - implementation 'com.taptap:lc-realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' -} -`} - -3. 在 `AndroidManifest.xml` 添加网络权限: - - ```java - - ``` - -4. 旧版 Android 额外配置 - - 如果 `targetSdkVersion < 29`,还需要添加如下配置: - - - `manifest` 节点添加 `xmlns:tools="http://schemas.android.com/tools"` - - `application` 节点添加 `tools:remove="android:requestLegacyExternalStorage"` - - -<> - -#### 导入 SDK - -1. 在 Xcode 选择工程,到 **Build Setting > Other Linker Flags** 添加 `-ObjC` 和 `-Wl -ld_classic`。 - -2. 下载 [TapSDK iOS](/tap-download),解压后选择需要导入的资源文件,直接拖拽到项目目录即可。 - -3. 视需要导入下载的资源文件: - - - 必选:TapTap 启动器、基础库、登录 - - ``` - TapBootstrapSDK.framework - TapCommonSDK.framework - TapLoginSDK.framework - TapCommonResource.bundle - TapLoginResource.bundle - LeanCloudObjc.framework - ``` - -4. 请仔细核对下面依赖库是否都添加成功: - - ``` - // 必选 - WebKit.framework - Security.framework - SystemConfiguration.framework - CoreTelephony.framework - SystemConfiguration.framework - libc++.tbd - - // TapTap 内嵌动态 - AVFoundation.framework - CoreTelephony.framework - MobileCoreServices.framework - Photos.framework - SystemConfiguration.framework - WebKit.framework - - // 数据分析 - // 如果不需要获取 IDFA 则不要添加 `AppTrackingTransparency` 和 `AdSupport` 两个系统库 - AppTrackingTransparency.framework - AdSupport.framework - CoreMotion.framework - Security.framework - SystemConfiguration.framework - libresolv.tbd - libsqlite3.0.tbd - libz.tbd - ``` - -#### 配置权限 - -如果游戏使用了 TapTap [内嵌动态](/sdk/embedded-moments/features/)或[数据分析](/sdk/tapdb/features/)服务,那么需要在 `info.plist` 配置相关权限并**替换授权文案**: - -```xml - -NSPhotoLibraryUsageDescription -说明为何应用需要此项权限 -NSCameraUsageDescription -说明为何应用需要此项权限 -NSMicrophoneUsageDescription -说明为何应用需要此项权限 - -NSUserTrackingUsageDescription -说明为何应用需要此项权限 -``` - -#### 配置跳转 TapTap 应用 - -用户无 TapTap 应用时,默认会通过 WebView 登录。 - -1. 打开 `info.plist`,添加如下配置(请替换 `clientID` 为你在控制台获取的 Client ID): - - ![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` - -2. 配置 openUrl: - - a) 如果项目中有 `SceneDelegate.m`,请先删除,然后请添加如下代码到 `AppDelegate.m` 文件: - - ```objectivec - - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [TDSHandleUrl handleOpenURL:url]; - } - - - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSHandleUrl handleOpenURL:url]; - } - ``` - - b) 删除 `info.plist` 里面的 Application Scene Manifest - - ![](/img/tap_ios_appmanifest.png) - - c) 删除 `AppDelegate.m` 文件中的两个管理 `Scenedelegate` 生命周期代理方法 - - ```objectivec - #pragma mark - UISceneSession lifecycle - - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { - - return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; - } - - - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { - } - ``` - - d) 在 `AppDelegate.h` 中添加 `UIWindow` +新创建的应用会以卡片形式显示在控制台,卡片左上角显示应用名称,右上角显示应用类型(开发版、商用版),下方显示用户数、昨日 API 请求数、当月 API 请求数三项统计数据,点击中间的图标则可直达相应的服务:结构化数据存储、云引擎、即时通讯、推送、短信、游戏、设置。 - ```objectivec - @property (strong, nonatomic) UIWindow *window; - ``` +点击应用名称会进入应用概览页面,这个页面会显示应用的基本信息数据,例如请求数、新增用户数、消息数等。 - +左栏菜单显示各项服务,我们会在后文具体说明。 +对于刚刚创建的应用,通常最先查看的页面是**控制台 > 设置 > 应用凭证**。 +在初始化 SDK 时需要用到其中的 App ID、App Key、服务器地址等信息。 +根据有关部门规定,使用 LeanCloud 国内版的服务需要绑定域名。 +我们建议你首先[绑定域名](/sdk/domain/guide/),这样设置页面显示的服务器地址就会是你绑定的域名,SDK 初始化时可以直接使用,后续无需修改。 +如果暂不绑定域名(比如域名正在办理备案),设置页面会显示供测试使用的临时共享域名,该域名无可用性保证,可能被回收,正式上线前需要改用自有域名。 -<> +创建应用、绑定域名后,就可以[参考文档](/sdk/storage/overview/)基于 SDK 或 REST API 上手开发项目了。 -* 下载 [TapSDK UE4](/tap-download),TapSDK-UE4-xxx.zip 解压后将 `TapBootstrap`、`TapCommon`、`TapLogin` 文件夹 Copy 到项目的 `Plugins` 目录中 -* 重启 Unreal Editor -* 打开 编辑 > 插件 > 项目 > TapTap,开启 `TapBootstrap` 和 `TapLogin` 模块 +## 财务信息 -#### 添加依赖 +控制台首页右侧会显示财务信息,包括账户余额和最近消费情况,请多加留意,以免因为欠费而导致服务停止。 -在 Project.Build.cs 中添加所需模块: +余额旁有**充值**、**告警**两个链接。 -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapBootstrap", - "TapLogin" -}); -if (Target.Platform == UnrealTargetPlatform.IOS || Target.Platform == UnrealTargetPlatform.Android) -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - // 推送接入 - // "LeanCloudPush", - - "LeanCloudMobile" - } - ); -} -else -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - "LeanCloud" - } - ); -} -``` +* 点击**充值**即可通过支付宝、银行汇款两种方式进行充值,其中以银行汇款方式充值需要一定时间才能到账(2 个工作日或更久),请提前安排。 +* 点击**告警**可以设置告警余额,账户余额小于设定值时会发送告警短信和告警邮件。 -#### 导入头文件 - -```cpp -#include "TapBootstrap.h" -``` - -
    - -点击展开 iOS 配置 - -在 项目设置 > Platform > iOS > Additional Plist data 中可以填入一个字符串,复制以下代码并且替换其中的 `ClientID` 以及授权文案。 - -```xml -CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt{ClientID} - - - -LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - -``` - -如果接入 TapDB 模块,那么还需要加上: - -```xml -NSUserTrackingUsageDescription -{数据追踪权限申请文案} -``` - -
    - - - -
    - -## 初始化 - -初始化 TapSDK 时需传入 `Client ID`、区域等应用配置信息。 - - - -<> - -```cs -using TapTap.Bootstrap; // 命名空间 -using TapTap.Common; // 命名空间 - -var config = new TapConfig.Builder() - .ClientID("your_client_id") // 必须,开发者中心对应 Client ID - .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token - .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 - .ConfigBuilder(); -TapBootstrap.Init(config); -``` - - -<> - -**请确保 TapSDK 的初始化在主线程(UI 线程)中执行。** - -```java -TapConfig tdsConfig = new TapConfig.Builder() - .withAppContext(MainActivity.this) // Context 上下文 - .withClientId("your_client_id") // 必须,开发者中心对应 Client ID - .withClientToken("your_client_token") // 必须,开发者中心对应 Client Token - .withServerUrl("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .withRegionType(TapRegionType.CN) // TapRegionType.CN:中国大陆,TapRegionType.IO:其他国家或地区 - .build(); -TapBootstrap.init(MainActivity.this, tdsConfig); -``` - - -<> - -```objectivec -// 开发者必须至少依赖 `TapBootstrap`、`TapLogin`、`TapCommon` 以及 `LeanCloudObjc` 模块,并按照如下方式完成初始化: -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // 必须,开发者中心对应 Client ID -config.clientToken = @"your_client_token"; // 必须,开发者中心对应 Client Token -config.serverURL = @"https://your_server_url"; // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -config.region = TapSDKRegionTypeCN; // TapSDKRegionTypeCN:中国大陆,TapSDKRegionTypeIO:其他国家或地区 -[TapBootstrap initWithConfig:config]; -``` - - - -<> - -`TapBootstrap` 初始化方法会把直接初始化 TapLogin 模块,如果插件中包含 TapDB 并且 `DBConfig.Enable = true`,那么也会完成 [`TapDB` 初始化](/sdk/tapdb/sdk/client-side-integration/#tapsdk-init)。 - -这两个模块无需再次初始化。 - -```cpp -FTUConfig Config; -Config.ClientID = "your_client_id"; // 必须,开发者中心对应 Client ID -Config.ClientToken = "your_client_token"; // 必须,开发者中心对应 Client Token -Config.ServerURL = "https://your_server_url"; // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -Config.RegionType = ERegionType::CN; // ERegionType::CN:中国大陆,ERegionType::Global:其他国家或地区 -FTapBootstrap::Init(Config); -``` - - - - - -初始化的时候,**必须填入** `client_id`、`client_token` 和 `server_url`,其中: - -- `client_id`、`client_token` 信息可在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 查看。 - -- `server_url` 请**使用 HTTPS 协议**,参考文档关于 **[域名](/sdk/start/get-ready/#域名)** 的说明。 - - -## 接入功能 - -TapSDK 提供了众多功能。请在初始化 SDK 后,根据项目需要,参考相应功能的文档,接入相应功能。 -绝大多数游戏都会接入 TapTap 登录,所以我们推荐从这一功能开始。 - -### 接入 TapTap 登录 - -请根据开发者指南:[快速上手,接入 TapTap 一键登录](/sdk/taptap-login/guide/start) 完成操作。 - -### 配置签名证书 - -Android 和 iOS 应用需要在 TapTap 开发者中心进入你的游戏,依次选择 **游戏服务 > 开发与构建 > TapTap 登录** 配置应用的相关信息(如下图所示),否则 Android 应用测试登录功能时会返回 `signature not match` 报错信息,iOS 会返回 `sdk_not_matched` 报错信息,无法正常使用 TapTap 登录功能。 - -Android 签名处填写 MD5 值,详情可参考:[如何获取 MD5 值](/sdk/start/faq/#如何获取-android-应用的-md5-值)。 - - - -![](https://capacity-files.lcfile.com/YmcgwIbUzC7dRKunBT5jQ1lYEO2hedWG/start_getready_info.png) - - - - - -![](https://capacity-files.lcfile.com/MM13UMrcN5n1WSJyClE7QQHb5f9ue4o6/io-login-config.png) - - - -接下来,就可以打包应用,测试 TapTap 登录功能了。 - -### Android 代码混淆 - -TapSDK 已经做了混淆处理,再次混淆会导致不可预期的错误,请在项目的混淆脚本中添加如下配置,跳过对 TapSDK 的混淆操作: - -```java --keep class com.tds.** { *;} --keep class com.taptap.** { *;} --keep class com.tapsdk.** { *;} --keep class tds.androidx.** { *;} -``` - -如果使用到基于**数据存储**的云服务,比如**内建账户**方式登录则需要额外添加 **[数据存储](/sdk/storage/guide/setup-java/#android-代码混淆)** 相关的混淆代码。 - -## 打包 - -Android 或 iOS 请按通常的 Android APK 或者 iOS 应用打包流程操作即可。这里介绍一下 Unity 打包流程: - -### 打包 APK - -第一步,配置 package name 和签名文件: - -![](https://capacity-files.lcfile.com/qooIRbr5qtLrnhsP0hWjOSnBYW12eNg6/tap_unity_android_build.png) - -第二步,检查 **File > Build Settings > Player Settings > Other Settings > Target API Level** 版本,当 API Level 小于 29 时,需要配置 manifest,在 `application` 节点添加: - -``` -tools:remove="android:requestLegacyExternalStorage" -``` - -这是因为 SDK 内部默认配置了 `android:requestLegacyExternalStorage = true`,当 `targetSdkVersion < 29` 时会报错 `Android resource linking failed`。 - -### 导出 Xcode 工程 +:::info +* 名下无商用版应用的用户,余额告警的默认值为 0 +* 名下有商用版应用的用户,余额告警的默认值为 100 +::: -需要配置 icon 和 `BundleID`: +当前账号绑定邮箱、手机始终会收到告警短信、邮件,还可以额外添加接受告警的邮件地址和手机号,比如添加负责财务的同事的联系方式。 -![](https://capacity-files.lcfile.com/Nke4QO6zdEz5mRd2Kwd8R9ydyP8QYaJy/tap_ios_build.png) diff --git a/docs/sdk/start/release-notes/_category_.json b/docs/sdk/start/release-notes/_category_.json deleted file mode 100644 index a7e2616f3..000000000 --- a/docs/sdk/start/release-notes/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapSDK 更新日志", - "collapsed": true, - "position": 11 -} diff --git a/docs/sdk/start/release-notes/android.mdx b/docs/sdk/start/release-notes/android.mdx deleted file mode 100644 index 87800a14b..000000000 --- a/docs/sdk/start/release-notes/android.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Android -sidebar_position: 2 ---- -import ReleaseNote from '../../../../src/docComponents/ReleaseNote/index.tsx'; - - - - \ No newline at end of file diff --git a/docs/sdk/start/release-notes/ios.mdx b/docs/sdk/start/release-notes/ios.mdx deleted file mode 100644 index 0977868d8..000000000 --- a/docs/sdk/start/release-notes/ios.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: iOS -sidebar_position: 3 ---- - -import ReleaseNote from '../../../../src/docComponents/ReleaseNote/index.tsx'; - - - - \ No newline at end of file diff --git a/docs/sdk/start/release-notes/ue.mdx b/docs/sdk/start/release-notes/ue.mdx deleted file mode 100644 index 1b7e3616a..000000000 --- a/docs/sdk/start/release-notes/ue.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: UE4 -sidebar_position: 4 ---- -import ReleaseNote from '../../../../src/docComponents/ReleaseNote/index.tsx'; - - - - \ No newline at end of file diff --git a/docs/sdk/start/release-notes/unity.mdx b/docs/sdk/start/release-notes/unity.mdx deleted file mode 100644 index 9d80abf73..000000000 --- a/docs/sdk/start/release-notes/unity.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Unity -sidebar_position: 1 ---- -import ReleaseNote from '../../../../src/docComponents/ReleaseNote/index.tsx'; - - - - \ No newline at end of file diff --git a/docs/sdk/start/ver-lifetime.mdx b/docs/sdk/start/ver-lifetime.mdx deleted file mode 100644 index 7e36b0675..000000000 --- a/docs/sdk/start/ver-lifetime.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: TapSDK 版本与维护周期 -sidebar_position: 10 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -## 版本划分规则 - -TapSDK 是我们提供的众多服务的总称,为了方便开发者按需接入,我们将每一个子服务使用一个独立模块来对外提供服务,例如:`TapTap 登录`对应 `TapLogin` 模块,`公告系统`对应 `TapBillboard` 模块,等。 - -我们使用[语义版本号](https://semver.org/lang/zh-CN/)来为 TapSDK 规划版本,版本格式主要分为三部分:`主版本号.次版本号.修订号`,其中: - -- 主版本号:标记一系列功能和 API 接口的集合,如果某次更新引入了不兼容的 API 修改,我们会升级主版本号; -- 次版本号:针对向下兼容的功能性新增,我们会升次版本号。例如在原来 3.4.0 的基础上,我们新加入了客服模块,那么就会以 3.5.0 来发布新版本; -- 修订号:我们修复了 SDK 的 bug 或者对内部实现进行了优化,并且保持对外接口不变时,会升级修订号; -- 先行版本,以及因为部分平台的特殊限制我们有时也会把平台信息加到「主版本号.次版本号.修订号」的后面,作为延伸。 - -目前我们开发的主线是 **3.x 版本**,各平台的 SDK 版本和变更列表,可以参考如下链接: - -- Unity Engine SDK:https://github.com/taptap/TapSDK-Unity/releases -- Unreal Engine SDK:https://github.com/taptap/TapSDK-UE4/releases -- Android 原生 SDK:https://github.com/taptap/TapSDK-Android/releases -- iOS 原生 SDK:https://github.com/taptap/TapSDK-iOS/releases - -## 版本维护生命周期 - -每一个大版本,如果是我们开发/维护的主线版本,那么会**持续维护——目前 3.x 版本的 SDK 即是如此**。如果不是主线版本,则我们会**在停止新功能开发之后继续维护两年**。假如现在最新的版本是 3.16.5,而下一次迭代,我们改变了对外接口,整个开发主线切换到 4.x 版本,那么从 4.0.0 发布之时起算,我们还会继续维护 3.x 两年的时间——期间只改 bug,不新增功能;两年之后 3.x 版本就彻底停止维护。 - -对于当前主线下的小版本(例如 3.1.x/3.2.x 版本),如果发现了 bug,我们会在当前最新的版本上进行修复(而不是在 bug 被发现的小版本上进行修复),由于次版本号之间接口完全兼容,并且子功能大都是采用独立模块来发布(模块之间无影响),所以开发者**可以无负担地升级到最新次版本号**。 - -我们可以看一个实际的例子。例如某开发者使用了实名认证和防沉迷功能,主要是接入了 `TapLogin/BootStrap/AntiAddiction` 三个模块,接入时的版本是 `3.5.3`,而当前最新的版本已迭代到了 `3.10.4`。后来: - -- 开发者发现并报告 SDK 的一个新 bug,我们跟进修复并发布 `3.10.5` 版本,开发者可以直接将对应模块版本从 `3.5.3` 升级到 `3.10.5`; -- 而假如开发者发现并报告的问题是一个已经被修复的 bug(例如从 `3.8.0` 之后该问题已经不复存在),那么开发者可以直接升级到我们当前的最新版 `3.10.4`。 - -建议开发者定期更新 TapSDK,保持合理的更新频率以保证客户端的稳定以及更好的用户体验。同时,在有接口的不兼容更新发生前,我们会预先发布 SDK 更新计划;对于即将退出维护周期的 SDK,我们也会提醒开发者及时进行升级。更新计划和升级通知会通过站内信的方式通知各位开发者,在收到站内信通知后,对于需要升级 SDK 的开发者需要尽快更新,避免带来功能或者收益上的影响。 - diff --git a/leancloud/docs/sdk/storage/best-practice/app-data-share.mdx b/docs/sdk/storage/best-practice/app-data-share.mdx similarity index 100% rename from leancloud/docs/sdk/storage/best-practice/app-data-share.mdx rename to docs/sdk/storage/best-practice/app-data-share.mdx diff --git a/leancloud/docs/sdk/storage/best-practice/custom-reset-verify-page.mdx b/docs/sdk/storage/best-practice/custom-reset-verify-page.mdx similarity index 100% rename from leancloud/docs/sdk/storage/best-practice/custom-reset-verify-page.mdx rename to docs/sdk/storage/best-practice/custom-reset-verify-page.mdx diff --git a/leancloud/docs/sdk/storage/best-practice/network-connectivity-diagnosis.mdx b/docs/sdk/storage/best-practice/network-connectivity-diagnosis.mdx similarity index 100% rename from leancloud/docs/sdk/storage/best-practice/network-connectivity-diagnosis.mdx rename to docs/sdk/storage/best-practice/network-connectivity-diagnosis.mdx diff --git a/docs/sdk/storage/features.mdx b/docs/sdk/storage/features.mdx deleted file mode 100644 index 1d72c3724..000000000 --- a/docs/sdk/storage/features.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: 数据存储功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -数据存储服务能够高效存取海量级 JSON 对象、二进制文件、地理位置等数据。其内置的行级 ACL 权限控制,以及通用的用户及角色管理体系,可以帮助你快速实现安全而灵活的数据访问。 - -## 初衷 - -大部分的产品都是数据驱动的,它们有一个最大的特点,就是对后端的需求在模式上其实是比较统一的: - -- 前端负责数据展现和用户交互处理,与后端的 app server 通过网络来交换需要的数据; - -- app server 负责业务逻辑处理,生成核心数据存储到 data server,或者聚合 data server 查询到的数据返回给客户端; - -- data server 负责核心数据的存储和备份。 - -这样的模式适合互联网上绝大部分产品,虽然数据结构有差异、业务逻辑不一样,但是前后端交互的主体——「数据」,抽象来看是一致的,后端的架构(譬如 LAMP)也是大同小异的,而且同样的系统在一遍一遍地被重复开发,极大浪费了我们宝贵的技术资源。 - -## 实用功能 - -开发者无需担心数据规模的大小和访问流量的多少,可以将我们的数据存储服务看成是一个面向对象的海量数据库来使用。 - -### 结构化数据存储 - -存储任意类型的 JSON 对象,支持对象之间的关联映射,同时提供完整的增删改查操作接口。 - -### 文件存储 - -存储图片,文档,音视频等二进制文件,自动提供弹性空间和多副本的冗余备份策略,默认支持多个 CDN 加速节点。 - -### ACL 权限控制 - -支持在整表、单行、单列三个维度上,进行读写分离的权限控制。严密的 ACL 机制,确保数据安全。 - -### 多端实时同步 - -数据可在云端与多个客户端之间实时同步,支持跨设备进行实时互动协作。 - -### 大数据分析处理 - -提供通用的 SQL 查询接口,可实时并行处理数据,专为数据挖掘、OLAP 以及商业智能而建。 - -## 优势和特色 - -- 多副本分布保存,提供 99.999% 的数据可靠性和超高并发访问能力。完善的备份机制确保数据万无一失。 - -- 扩展功能组件,支持朋友圈、动态消息等常见社区功能,让业务开发更简单便捷。 - -- 每天支撑超过 10 亿次请求,峰值压力堪比电商秒杀。自动应对流量高峰,服务稳定可靠。 diff --git a/leancloud/docs/sdk/storage/flutter-guide/_category_.json b/docs/sdk/storage/flutter-guide/_category_.json similarity index 100% rename from leancloud/docs/sdk/storage/flutter-guide/_category_.json rename to docs/sdk/storage/flutter-guide/_category_.json diff --git a/leancloud/docs/sdk/storage/flutter-guide/flutter.mdx b/docs/sdk/storage/flutter-guide/flutter.mdx similarity index 100% rename from leancloud/docs/sdk/storage/flutter-guide/flutter.mdx rename to docs/sdk/storage/flutter-guide/flutter.mdx diff --git a/leancloud/docs/sdk/storage/flutter-guide/setup-flutter.mdx b/docs/sdk/storage/flutter-guide/setup-flutter.mdx similarity index 100% rename from leancloud/docs/sdk/storage/flutter-guide/setup-flutter.mdx rename to docs/sdk/storage/flutter-guide/setup-flutter.mdx diff --git a/leancloud/docs/sdk/storage/friendship.md b/docs/sdk/storage/friendship.md similarity index 100% rename from leancloud/docs/sdk/storage/friendship.md rename to docs/sdk/storage/friendship.md diff --git a/leancloud/docs/sdk/storage/go-guide/_category_.json b/docs/sdk/storage/go-guide/_category_.json similarity index 100% rename from leancloud/docs/sdk/storage/go-guide/_category_.json rename to docs/sdk/storage/go-guide/_category_.json diff --git a/leancloud/docs/sdk/storage/go-guide/go.md b/docs/sdk/storage/go-guide/go.md similarity index 100% rename from leancloud/docs/sdk/storage/go-guide/go.md rename to docs/sdk/storage/go-guide/go.md diff --git a/leancloud/docs/sdk/storage/go-guide/setup-go.md b/docs/sdk/storage/go-guide/setup-go.md similarity index 100% rename from leancloud/docs/sdk/storage/go-guide/setup-go.md rename to docs/sdk/storage/go-guide/setup-go.md diff --git a/leancloud/docs/sdk/storage/java-guide/sdk-introduction.mdx b/docs/sdk/storage/java-guide/sdk-introduction.mdx similarity index 100% rename from leancloud/docs/sdk/storage/java-guide/sdk-introduction.mdx rename to docs/sdk/storage/java-guide/sdk-introduction.mdx diff --git a/leancloud/docs/sdk/storage/java-guide/setup-android-securely.mdx b/docs/sdk/storage/java-guide/setup-android-securely.mdx similarity index 100% rename from leancloud/docs/sdk/storage/java-guide/setup-android-securely.mdx rename to docs/sdk/storage/java-guide/setup-android-securely.mdx diff --git a/leancloud/docs/sdk/storage/overview.md b/docs/sdk/storage/overview.md similarity index 100% rename from leancloud/docs/sdk/storage/overview.md rename to docs/sdk/storage/overview.md diff --git a/leancloud/docs/sdk/storage/php-guide/_category_.json b/docs/sdk/storage/php-guide/_category_.json similarity index 100% rename from leancloud/docs/sdk/storage/php-guide/_category_.json rename to docs/sdk/storage/php-guide/_category_.json diff --git a/leancloud/docs/sdk/storage/php-guide/php.mdx b/docs/sdk/storage/php-guide/php.mdx similarity index 100% rename from leancloud/docs/sdk/storage/php-guide/php.mdx rename to docs/sdk/storage/php-guide/php.mdx diff --git a/leancloud/docs/sdk/storage/php-guide/setup-php.mdx b/docs/sdk/storage/php-guide/setup-php.mdx similarity index 100% rename from leancloud/docs/sdk/storage/php-guide/setup-php.mdx rename to docs/sdk/storage/php-guide/setup-php.mdx diff --git a/leancloud/docs/sdk/storage/py-guide/_category_.json b/docs/sdk/storage/py-guide/_category_.json similarity index 100% rename from leancloud/docs/sdk/storage/py-guide/_category_.json rename to docs/sdk/storage/py-guide/_category_.json diff --git a/leancloud/docs/sdk/storage/py-guide/python.mdx b/docs/sdk/storage/py-guide/python.mdx similarity index 100% rename from leancloud/docs/sdk/storage/py-guide/python.mdx rename to docs/sdk/storage/py-guide/python.mdx diff --git a/leancloud/docs/sdk/storage/py-guide/setup-python.mdx b/docs/sdk/storage/py-guide/setup-python.mdx similarity index 100% rename from leancloud/docs/sdk/storage/py-guide/setup-python.mdx rename to docs/sdk/storage/py-guide/setup-python.mdx diff --git a/leancloud/docs/sdk/storage/swift-guide/_category_.json b/docs/sdk/storage/swift-guide/_category_.json similarity index 100% rename from leancloud/docs/sdk/storage/swift-guide/_category_.json rename to docs/sdk/storage/swift-guide/_category_.json diff --git a/leancloud/docs/sdk/storage/swift-guide/setup-swift.mdx b/docs/sdk/storage/swift-guide/setup-swift.mdx similarity index 100% rename from leancloud/docs/sdk/storage/swift-guide/setup-swift.mdx rename to docs/sdk/storage/swift-guide/setup-swift.mdx diff --git a/leancloud/docs/sdk/storage/swift-guide/swift.mdx b/docs/sdk/storage/swift-guide/swift.mdx similarity index 100% rename from leancloud/docs/sdk/storage/swift-guide/swift.mdx rename to docs/sdk/storage/swift-guide/swift.mdx diff --git a/docs/sdk/tap-adn/## Interface Update.ini b/docs/sdk/tap-adn/## Interface Update.ini deleted file mode 100644 index 55babbedc..000000000 --- a/docs/sdk/tap-adn/## Interface Update.ini +++ /dev/null @@ -1,45 +0,0 @@ -## Interface Update -### 开屏广告的交互监听新增点击回调 -(文档链接)[https://developer.taptap.cn/docs/sdk/tap-adn/tds-tapad/#%E6%92%AD%E6%94%BE] `AdInteractionListener.onAdClick()` - -### 自渲染信息流广告新增广告转化类型返回 -(文档链接)[https://developer.taptap.cn/docs/sdk/tap-adn/tds-tapad/#%E8%8E%B7%E5%8F%96%E5%B9%BF%E5%91%8A-3] `TapFeedAd.getInteractionType()` - -## New Feature -### 新增模板染信息流广告 -(文档链接)[https://developer.taptap.cn/docs/sdk/tap-adn/tds-tapad/#%E6%A8%A1%E6%9D%BF%E6%9F%93%E4%BF%A1%E6%81%AF%E6%B5%81%E5%B9%BF%E5%91%8A] - -## Internal Change -### 支持新的预算 -### 控制 TapADN SDK 内部获取 oaid 频率 - -``` -// 示例代码 -TapAdConfig tapADConfig = new TapAdConfig.Builder() - ... - .withData(getData(enableCustomRecommend, enableOaidStrictMode)) - .build(); -private String getData(boolean enablePersonalized, boolean enableOaidStrictMode) { - if (!enablePersonalized && enableOaidStrictMode) return ""; - try { - JSONArray jsonArray = new JSONArray(); - if (enablePersonalized) { - JSONObject personalObject = new JSONObject(); - personalObject.put("name", "personal_ads_type"); - personalObject.put("value", "0"); - jsonArray.put(personalObject); - } - // 配置 oaid 获取频次 - if (enableOaidStrictMode) { - JSONObject oaidControllerObject = new JSONObject(); - oaidControllerObject.put("name", "enable_oaid_strict_mode"); - oaidControllerObject.put("value", 1); - jsonArray.put(oaidControllerObject); - } - return jsonArray.toString(); - } catch (Exception e) { - e.printStackTrace(); - } - return ""; -} -``` \ No newline at end of file diff --git a/docs/sdk/tap-adn/_category_.json b/docs/sdk/tap-adn/_category_.json deleted file mode 100644 index 04d3c9e17..000000000 --- a/docs/sdk/tap-adn/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapADN", - "collapsed": true, - "position": 22 -} diff --git a/docs/sdk/tap-adn/adn-compliance.mdx b/docs/sdk/tap-adn/adn-compliance.mdx deleted file mode 100644 index 5821ab25b..000000000 --- a/docs/sdk/tap-adn/adn-compliance.mdx +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: 合规使用说明 -sidebar_position: 10 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -根据《个人信息保护法》、《数据安全法》、《网络安全法》等法律法规和监管部门规章要求,App 开发运营者(以下简称为“开发者”)在提供网络产品服务时应尊重和保护最终用户的个人信息,不得违法违规收集使用个人信息,保证和承诺就个人信息处理行为获得最终用户的授权同意,遵循最小必要原则,且应当采取有效的技术措施和组织措施确保个人信息安全。为帮助开发者在使用 TapADN SDK 的过程中更好地落实用户个人信息保护相关要求,避免出现侵害最终用户个人信息权益的情形,特制定本合规使用说明,供开发者在接入使用 TapADN SDK 服务时参照自查和合理配置,不断提升个人信息保护水平。 - - -## TapADN SDK 配置能力说明 - -### SDK扩展业务功能的配置说明 - -**要求内容**:《SDK 合规使用说明》应详细说明 SDK 各项扩展业务功能介绍及对应关闭的配置方式、示例。

    -**接入说明**: TapADN SDK 提供的主要扩展业务功能为个性化广告推荐,TapADN SDK 为开发者提供退出个性化广告能力的接口,开发者可以调用接口,向最终用户提供退出个性化广告的能力。退出后,最终用户看到的广告数量不变,相关度会降低。开发者需遵守相关法律法规的要求,在 App 内为最终用户提供退出个性化广告的功能,保证在最终用户点击退出功能后调用 TapADN SDK 的能力接口。配置文档链接:[个性化推荐设置](/sdk/tap-adn/adn-personlized/) - -### SDK可选个人信息的配置说明 -**要求内容**:《SDK 合规使用说明》应详细说明SDK各项可选个人信息使用目的、场景及对应关闭的配置方式、示例。

    -**接入说明**: 对于 TapADN SDK 可选收集的个人信息的控制,开发者可以参考如下配置文档的内容进行配置操作。因相关信息的不收集将会对其对应的功能造成影响,请开发者结合业务实际需要进行合理配置。配置文档链接:[SDK 接入指南](/sdk/tap-adn/tds-tapad/) - -TapADN SDK 可选个人信息的说明

    - -| 可选个人信息类型 | 字段 | 使用目的 | 使用场景 | -|---------------|----|---------|---------| -| 设备信息 | 「仅 Android 」设备标识符(如 IMEI、AndroidId )| 广告投放及广告反作弊。其中,IMEI 设备型号还会用于广告监测归因。| 在进行广告投放和广告投放效果分析时使用 | -| 应用信息 | 「仅 Android 」软件列表信息 | 广告投放、反作弊 | 在进行广告投放和广告投放效果分析时使用 | -| 位置信息 | 「仅 Android 」精确位置信息、粗略位置信息 | 广告定向投放及广告反作弊 | 在进行广告投放和广告投放效果分析时使用 | - - -### SDK 可按照不同频次、精度收集个人信息的配置说明 - -**要求内容**:如果 SDK 可按照不同频次、精度收集个人信息的,《SDK 合规使用说明》应说明不同频次、精度的使用目的、场景及对应选择的配置方式、示例。 - -**接入说明**:收集频次方面,TapADN SDK 的数据采集仅在 App 调用/最终用户触发相关功能时触发,不涉及定时逻辑等频次控制选项。收集精度方面,主要涉及定位相关功能,主要通过权限进行控制,TapADN SDK 通过可选权限让 App 可以控制是否申请精确地理位置权限或粗略地理位置权限。如果您需要对定位权限进行配置,可参考配置文档链接:[ SDK 接入指南](/sdk/tap-adn/tds-tapad/) - -### SDK 申请系统权限的说明 -**要求内容**:《SDK 合规使用说明》应详细说明 SDK 所需的系统权限与各业务功能间的关系,并说明权限申请时机。 - -**接入说明**:对于 TapADN SDK 可选申请的系统权限,您可以参考相关如下表格的内容,详细了解相关权限与各业务功能的关系及其申请时机,因相关权限的不申请将会对其对应的功能造成影响,您可以结合业务实际需要进行合理配置。配置文档链接:[SDK 接入指南](/sdk/tap-adn/tds-tapad/) -

    -安卓操作系统应用权限列表: - -| 权限 | 权限功能说明 | 用途和目的 | 申请时机 | -|---------|--------------|-------------|---------------------| -| READ_PHONE_STATE 读取电话状态(设备 IMEI 号)| 「可选」读取手机设备标识等信息 | 进行广告投放及广告监测归因、反作弊 | 开发者在调用需要该权限的 SDK 功能时进行调用。例如进行广告投放、监测归因与反作弊。| -| ACCESS_COARSE_LOCATION 访问粗略位置 | 「可选」获取粗略地理位置信息 | 进行广告投放及反作弊 | 开发者在调用需要该权限的 SDK 功能时进行调用,例如根据粗略位置信息投放广告、广告反作弊。 | -| ACCESS_FINE_LOCATION 访问精准定位 | 「可选」获取精确地理位置信息 | 进行广告投放及反作弊 | 开发者在调用需要该权限的 SDK 功能时进行调用,例如根据精确位置信息投放广告、广告反作弊。 | -| QUERY_ALL_PACKAGES 应用软件列表 | 「可选」获取应用软件列表 | 进行广告投放及反作弊 | 开发者在调用需要该权限的 SDK 功能时进行调用。例如根据应用软件列表情况进行广告投放、广告反作弊。 | -| REQUEST_INSTALL_PACKAGES 应用请求安装软件包 | 「可选」允许应用请求安装软件包 | 游戏广告包安装 | 媒体发布的时候需要带上此权限。 | -| WRITE_EXTERNAL_STORAGE 写入外置存储器 | 「可选」允许应用程序写入外部存储 | 应用下载广告投放及广告素材存储 | 开发者在调用需要该权限的SDK功能时进行调用。例如下载 App 包体等功能。 | -| READ_EXTERNAL_STORAGE 读取外置存储器 | 「可选」允许应用程序读取外部存储 | 应用下载广告投放及广告素材存储 | 开发者在调用需要该权限的SDK功能时进行调用。例如下载 App 包体等功能。 | - - -### SDK 初始化及业务功能调用时机 -**要求内容**:《SDK 合规使用说明》应详细说明 SDK 初始化及各项业务功能接口合规调用时机。App 应当在 App 登录注册页面及 App 首次运行时,通过弹窗、文本链接及附件等简洁明显且易于访问的方式,向最终用户告知涵盖个人信息处理主体、处理目的、处理方式、处理类型、保存期限等内容的个人信息处理规则,并且获得最终用户授权同意后才能处置最终用户数据。 - -**接入说明**:请务必在用户同意您 App 中的隐私政策后,再进行 TapADN SDK 的初始化。用户同意隐私政策之前,避免动态申请涉及用户个人信息的敏感设备权限;用户同意隐私政策前,您应避免私自采集和上报个人信息。当您的App未向用户提供服务时,例如 App 在后台运行时,请勿请求 TapADN SDK 的相关服务。具体的初始化时机可以详细查阅相关接入文档的内容。具体配置文档链接:[SDK 接入指南](/sdk/tap-adn/tds-tapad/) - - -### SDK 隐私政策披露要求与示例 -**要求内容**:《SDK 合规使用说明》应提供 App 向最终用户披露SDK 隐私政策条款的示例,包括 SDK 名称、公司名称、处理个人信息种类及目的、采集方式、隐私政策链接等内容。 - -**接入说明**: 开发者在 App 集成 TapADN SDK 后,TapADN SDK 的正常运行会收集必要的最终用户信息用于广告投放及效果优化。 请开发者根据集成 TapADN SDK 的实际情况,在您 App 的隐私政策中,对 TapADN SDK 名称、公司名称、处理个人信息种类及目的、采集方式、隐私政策链接等内容进行披露。建议:确认您所接入的 TapADN SDK 版本和功能模块;根据上述版本和模块,从隐私政策中确定与 TapADN SDK 交互的数据内容;在您 App 的隐私政策中,以文字或列表的方式向公众披露 TapADN SDK 的相关信息。 -

    -披露示例(仅供参考,请以实际合作情况为准) - -SDK名称:TapADN SDK - -涉及个人信息:设备标识符(Android如 IMEI、Android ID、OAID,IP 地址,,精确位置信息、粗略位置信息,设备传感器信息(加速度传感器、陀螺仪传感器、线性加速度传感器、磁场传感器、旋转矢量传感器),设备品牌、型号、软件系统版本、屏幕密度、屏幕分辨率、设备语言、设备时区、CPU 信息、可用存储空间大小、手机系统重启时间、磁盘总空间、系统总内存空间、运营商信息、Wi-Fi 状态、网络信号强度、软件列表、应用包名、运行中的进程信息、版本号、应用前后台状态 - -合作方主体:易玩(上海)网络科技有限公司 - -使用目的:广告投放及监测归因、反作弊、统计分析、减少 App 崩溃、提供可靠稳定的服务 - -使用场景:向最终用户投放广告时使用 - -收集方式:SDK 自行采集 - -官网链接:https://ssp.taptap.cn - -[隐私政策链接](/sdk/tap-adn/agreement/) - -### 最终用户同意方式的示例 -**要求内容**:《SDK 合规使用说明》应详细说明 App 获取最终用户授权同意的建议方式,其中需要取得最终用户单独同意的,应显著提示并给出示例。 - -**接入说明**:App 首次运行时应当有隐私弹窗,隐私弹窗中应公示简版隐私政策内容并附完整版隐私政策链接,并明确提示最终用户阅读并选择是否同意隐私政策;隐私弹窗应提供同意按钮和拒绝同意的按钮,并由最终用户主动选择。 -

    - -![披露示例](/img/tap-ad/privacy_agreement.png) - -### 最终用户行使权利的配置说明 -**要求内容:** 最终用户对其个人信息的处理享有知情、决定、查阅、复制、补充、更正、撤回授权同意、删除、注销账号等权利。以嵌入接口形式向最终用户提供行使权利的,应提供接口调用方式、示例。 - -**接入说明:** 开发者在其 App 中集成 TapADN SDK 后,TapADN SDK 的正常运行会收集必要的最终用户信息用于广告投放及效果优化目的。开发者应根据相关法律法规为最终用户提供行使个人信息主体权利的路径或功能, 需要 TapADN SDK 配合的,请与 TapADN SDK 及时进行联系,我们将与开发者协同妥善解决最终用户的诉求。 - -### 广告一键关闭 -- **标准要求**:应用在向用户展示广告和信息弹窗,或者用户在浏览广告和信息弹窗过程中,都需要存在「关闭 X 」或者「跳过」按钮,并且点击后立即关闭广告展示。 - -- **接入指南**:开发者使用自渲染方式展示广告时,请务必保证具备广告关闭选项且保证一键关闭。 - -### 自渲染广告需配置明确广告来源标识 -- **标准要求**:通过程序化购买广告方式发布的互联网广告,广告需求方平台经营者应当清晰标明广告来源。 - -- **接入指南**:开发者使用自渲染方式展示广告时,请务必保证广告中有明确的广告和来源标识,在获取到来源字段后需要在前端渲染出广告内容; -获取广告来源代码如下: -``` - String getAdLogIconUrl(); -``` - -### 应用下载广告和开屏广告合规要求 -- **标准要求**:应用下载广告需明示 APP 名称、开发者名称、隐私政策、权限列表和版本信息,并且严格落实点击「下载」按钮方可下载。开屏广告页面需限制点击跳转的区域,并提供显著并且有效的「跳过/关闭」按钮。 - -- **接入指南**:开发者需升级到新版SDK(3.16.3.20 版本以上),在客户端通过 TapADN 提供的广告配置接口进行应用下载广告的合规配置,包含应用下载六要素展示,具体链接可以参考平台接入文档:[自渲染获取应用下载六要素信息及注册直接下载区域说明](/sdk/tap-adn/adn-selfrending/) diff --git a/docs/sdk/tap-adn/adn-personlized.mdx b/docs/sdk/tap-adn/adn-personlized.mdx deleted file mode 100644 index 56c656e14..000000000 --- a/docs/sdk/tap-adn/adn-personlized.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: 个性化推荐设置 -sidebar_position: 9 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -发布日期:2024 年 3 月 16 日 - -为落实个人信息保护相关的规定,TapADN SDK 为开发者提供退出个性化广告能力的接口,开发者可以调用接口,向用户提供退出个性化广告的能力。退出后,看到的广告数量不变,相关度会降低。开发者需遵守相关法律法规的要求,在开发者应用内为用户提供退出个性化广告的功能,保证在用户点击退出功能后调用 TapADN SDK 的能力接口。 - - -## 方法简介: -开发者可以在 data 这个自定义字段中增加新的传入 key personal_ads_type,当用户选择拒绝个性化广告时上报 0,当用户同意时上报1; - -| Parameter | Type | Description | Value | -|--------------|-------------|-------------|-------------------------------| -| personal_ads_type | String | 是否屏蔽个性化广告 | 不传或传空没任何影响,默认不屏蔽

    0,屏蔽个性化推荐广告;

    1,不屏蔽个性化推荐广告; - -## 实现路径: -根据用户在应用内的配置进行参数传递,用户默认不屏蔽则不需要任何配置。 -若用户选择屏蔽个性化推荐广告,则需要开发者通过 updateAdConfig 接口进行参数更新。 - -## 支持版本: -Android:3.16.3.26 及以上 - -## Android 实现: -通过 TapADN SDK 提供的控制是否屏蔽个性化推荐广告接口进行设置。 - -``` -TapAdSdk.updateAdConfig(tapAdConfig) //参数类型为 TapAdConfig -``` - -demo示例: -``` - /** - * @param personalTypeValue 个性化推荐广告开关 - */ - private static void updateData(String personalTypeValue) { - TapAdConfig tapAdConfig = new TapAdConfig.Builder() - .withData(getData(personalTypeValue)) - .build(); - TapAdSdk.updateAdConfig(tapAdConfig); - } - - private static String getData(String personalTypeValue) { - try { - JSONArray jsonArray = new JSONArray(); - JSONObject personalObject = new JSONObject(); - personalObject.put( name , personal_ads_type ); - personalObject.put( value , personalTypeValue); - jsonArray.put(personalObject); - return jsonArray.toString(); - } catch (Exception e) { - e.printStackTrace(); - } - return ""; - } -``` - - - - - - - - - diff --git a/docs/sdk/tap-adn/adn-selfrending.mdx b/docs/sdk/tap-adn/adn-selfrending.mdx deleted file mode 100644 index 5f0088ad3..000000000 --- a/docs/sdk/tap-adn/adn-selfrending.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: 自渲染获取应用下载六要素信息及注册直接下载区域说明 -sidebar_position: 11 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -## 说明 - -发布日期:2024 年 3 月 16 日 - -TapADN SDK在 3.16.3.20 及以上版本针对自渲染信息流广告提供了获取应用的下载六项信息:应用名称、开发者公司名称、应用版本、隐私协议超链、权限列表超链 、产品功能的接口,并将自渲染广告的两种点击区域配置方式调整为两种: -- 非创意区域:点击后进入落地页; -- 创意区域: 即可点区域,点击进入落地页或触发下载; - -## 支持版本 -3.16.3.20 及以上 - -## 开发者实践路径 -### 信息流自渲染广告新增应用下载要素获取 API---获取广告六要素信息展示 -``` -private void bindData(View convertView, final AdViewHolder adViewHolder,List images, TapFeedAd ad) { - ... - ComplianceInfo complianceInfo = ad.getComplianceInfo(); - if (complianceInfo != null) { - String appName = complianceInfo.getAppName(); - String appVersion = complianceInfo.getAppVersion(); - String appDeveloperName = complianceInfo.getDeveloperName(); - String appPrivacyUrl = complianceInfo.getPrivacyUrl(); - String appFunctionDescUrl = complianceInfo.getFunctionDescUrl(); - String permissionUrl = complianceInfo.getPermissionUrl(); - } - // 渲染广告view... - - ad.registerViewForInteraction(activity, (ViewGroup) itemView, clickViewList, creativeViewList, describeViewList, privacyViewList, permissionViewList, new TapFeedAd.AdInteractionListener() { - ... - }); -``` - -### 接口定义 -``` -public interface TapFeedAd { - /** - * 注册可点击的 view,click/show 会在内部完成 - * - * @param activity 渲染广告所在的 activity - * @param container 渲染广告最外层的 ViewGroup - * @param clickViews 可点击的视图 - * @param creativeViews 可点击的创意视图(广告下载的按钮) - * @param describeViews 可点击的介绍详情视图 - * @param privacyViews 可点击的隐私协议视图 - * @param permissionViews 可点击的权限视图 - * - */ - void registerViewForInteraction(Activity activity, ViewGroup container, List clickViews, List creativeViews, - List describeViews, List privacyViews, List permissionViews, AdInteractionListener listener); -} - -``` -``` -public interface ComplianceInfo { - - /** - * 获取应用名称 - * - * @return - */ - String getAppName(); - - /** - * 获取应用版本号 - * - * @return - */ - String getAppVersion(); - - /** - * 获取开发者公司名称 - * - * @return - */ - String getDeveloperName(); - - /** - * 获取隐私协议地址 - * - * @return - */ - String getPrivacyUrl(); - - /** - * 获取产品功能url - * - * @return - */ - String getFunctionDescUrl(); - - /** - * 获取权限名称及权限描述地址 - * - * @return - */ - String getPermissionUrl(); - - /** - * 广告来源地址 - * - * @return - */ - String getAdLogIconUrl(); -} -``` - -## 使用注意事项 -- 如确定使用六要素外侧披露方案,则需要针对下载类自渲染广告在广告层展示应用的六要素 -- 点击后想要出合规弹窗/直接下载 可以调用”创意区域",若点击了想要进入落地页可以调用“非创意区域”。 - -## 渲染样式参考 - -![渲染样式参考](/img/tap-ad/self_render_reference.png) - - - - - - - - - - diff --git a/docs/sdk/tap-adn/adn-shakeenabled.mdx b/docs/sdk/tap-adn/adn-shakeenabled.mdx deleted file mode 100644 index b85613969..000000000 --- a/docs/sdk/tap-adn/adn-shakeenabled.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: 摇一摇使用配置 -sidebar_position: 12 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -## 说明 - -发布日期:2024 年 3 月 16 日 - -## 功能介绍 -- 激励视频、Banner、开屏、插屏场景的流量已支持配置“摇一摇”的创意交互形式。完成配置后,用户在展示“摇一摇”样式的广告上可以根据提示摇动手机触发交互,丰富用户体验。 -- 根据历史广告实验数据,增加摇一摇交互可对 CPM 提升 20%+。 - -## 使用方式 -1. Android SDK 版本在 3.16.3.27 以上 -2. 开发者可在初始化 SDK 时进行配置, 详见 -[【TapADN 接入文档-初始化】](/sdk/tap-adn/tds-tapad/#初始化)。 - - -``` -TapAdConfig config = new TapAdConfig.Builder() - ... - .shakeEnabled(false) // 可选参数,是否开启摇一摇。 true 开启;false 关闭。 默认 true 开启。 - ... -.build(); -``` - -## 注意事项 -- 开发者需要创建信息流、激励视频广告位才可使用摇一摇功能 -- 只有当摇一摇广告完整展示时,用户摇动才会触发点击。在一屏展示多个广告的场景,用户摇动后,仅离屏幕中心区域最近的单条广告会触发点击。 -- 当前版本摇一摇配置项全局生效,暂不支持开发者对单个推广位自行配置。摇一摇样式的广告将全程展示摇一摇交互,期间用户按提示语摇动均触发广告点击和跳转。当广告 view 被隐藏时不会触发。 diff --git a/docs/sdk/tap-adn/agreement.mdx b/docs/sdk/tap-adn/agreement.mdx deleted file mode 100644 index 3c58952e4..000000000 --- a/docs/sdk/tap-adn/agreement.mdx +++ /dev/null @@ -1,278 +0,0 @@ ---- -title: TapADN SDK 隐私政策 -sidebar_label: 隐私政策 -sidebar_position: 5 ---- - -# **TapADN SDK 隐私政策** - -发布日期:2024 年 03 月 22 日 - -生效日期:2024 年 03 月 22 日 - -作为 TapADN SDK 产品和/或服务的提供方,易玩(上海)网络科技有限公司及其关联方(以下简称“我们”)高度重视个人信息的保护。TapADN SDK 为开发者提供广告投放及效果优化服务,本隐私政策所称之 TapADN SDK 产品和/或服务,以下统称“本服务”。在最终用户(以下简称“您”)使用开发者开发和/或运营的网站或应用软件(包括 APP、小程序、快应用、网页等,以下简称“开发者应用”)时,如果开发者集成了本服务后,我们将通过开发者应用向您提供相关功能或服务,我们承诺按照本隐私政策及法律法规的规定妥善处理您的个人信息,保护您的个人信息及隐私安全。 - -**特别声明:** - -**1. 本隐私政策不能替代开发者应用的隐私政策。开发者应就其应用收集处理个人信息的情况向您披露隐私政策,以向您声明其如何处理及保护您的个人信息。如果您寻求行使个人信息主体权利,请与相应开发者进行联系。** - -**2. 您通过开发者应用所使用的本服务,由开发者根据其应用所需自行选择配置,并可能因为您所使用的开发者应用版本不同而有所差异。如果开发者应用版本中不包括我们的某些功能或服务,则本隐私政策中涉及的功能或服务及相关个人信息的处理内容将不适用。** - -**3. 开发者在接入 TapADN SDK 前应详细阅读并同意遵守本隐私政策、[《TapADN SDK 合规使用说明》](/sdk/tap-adn/adn-compliance/)等内容,并根据合规说明内容进行相关合规配置。** - -**4. 请开发者在接入 TapADN SDK 前,务必仔细阅读本隐私政策,特别是以加粗提示的条款应重点阅读,并确认在已经充分理解本隐私政策内容的前提下,按照本隐私政策,作出适当的选择。同时,开发者应向您告知开发者应用集成 TapADN SDK 的情况,包括 TapADN SDK 名称、主体名称、处理个人信息种类及目的、本隐私政策链接等内容,以便于您了解相关内容并获取您的同意。一旦您接受或使用 TapADN SDK 提供的服务,即表示您已充分理解并同意本政策。除非另有说明,如果开发者或您不同意本隐私政策或其更新,开发者或您应立即停止接入和/或使用本服务。** - -## 本《隐私政策》将帮助您了解以下内容: - -我们希望通过本《隐私政策》向您清晰、准确且完整地说明,您在使用集成了 TapADN SDK 的开发者应用时,我们如何处理和保护您的个人信息。 - -一、我们如何收集和使用个人信息 - -二、我们如何存储个人信息 - -三、我们如何共享、转让、披露您的个人信息 - -四、您如何管理个人信息 - -五、我们如何保护您的个人信息 - -六、未成年人保护 - -七、本政策如何更新 - -八、联系我们 - -九、其他 - -## 一、我们如何收集和使用用户的个人信息 - -在您使用 TapADN SDK 提供的服务过程中,我们将根据合法、正当、必要的原则收集信息。 - -### 1、TapADN SDK 的功能介绍 - -(1)基本功能:TapADN SDK 的基本业务功能为在开发者应用中进行广告投放活动,包括广告展示、监测、归因及投放效果分析与优化。 - -(2)扩展功能:TapADN SDK 的扩展业务功能主要为提供个性化推荐服务,关于扩展功能的配置方式详见[《TapADN SDK 合规使用说明》](/sdk/tap-adn/adn-compliance/)。 - - -### 2、个人信息的采集 - -如您使用集成 TapADN SDK 的开发者应用,TapADN SDK 会通过开发者应用采集下列信息并申请相应权限。开发者应向您告知 TapADN SDK 名称、主体名称、处理个人信息种类及目的、隐私政策等内容,取得您的同意后方可使用 TapADN SDK 开展相关业务功能。由于不同 SDK 版本采集的信息字段与是否可选可能存在一定差异,具体采集情况以您实际使用的开发者应用所接入的 SDK 版本为准。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    SDK 名称 功能描述 个人信息类型 个人信息名称 用途和目的
    TapADN SDK 广告投放合作 设备信息 必选信息:
    【仅 Android】设备基础信息(设备品牌、型号、软件系统版本、屏幕密度、屏幕分辨率、设备语言、设备时区、CPU 信息、可用存储空间大小)、OAID

    可选信息:
    【仅 Android】设备标识符(IMEI、AndroidID、AAID、VAID)
    广告投放及广告反作弊。
    其中,IMEI、AndroidID、OAID、AAID、VAID、设备型号还会用于广告监测归因。
    网络信息 必选信息:
    【仅 Android】Wi-Fi 状态、网络信号强度
    保证网络服务有效性及稳定性。
    其中,Wi-Fi 状态还会用于广告投放。
    必选信息:
    【仅 Android】IP 地址
    广告投放、广告监测归因、广告反作弊
    应用信息 必选信息:
    【仅 Android】应用包名、运行中的进程信息、版本号、应用前后台状态

    可选信息:
    【仅 Android】软件列表信息
    广告投放、广告反作弊
    传感器信息 必选信息:
    【仅 Android】线性加速度传感器、磁场传感器、旋转矢量传感器、加速度传感器、陀螺仪传感器
    广告投放(包括摇一摇、扭一扭)、广告反作弊
    广告信息 必选信息:
    【仅 Android】交互数据(对广告的展示、点击及转化数据)
    广告监测归因、广告投放统计分析、广告反作弊
    位置信息 可选信息:
    【仅 Android】精确位置信息、粗略位置信息(WiFi 列表、WLAN 接入点(SSID、BSSID)、基站)
    广告定向投放及广告反作弊
    性能数据 必选信息:
    【仅 Android】崩溃数据、性能数据
    减少应用崩溃、提供稳定可靠的服务
    - - -**特别提示您注意,我们不会要求您主动提交个人信息。我们采集的信息不能单独识别特定自然人的身份,并且基于本 SDK 的技术特性,其在运行过程客观上无法获取任何能够单独识别特定自然人身份的信息。** - -**我们可能会对 TapADN SDK 的功能和提供的服务有所调整变化,但请您知悉并了解,未经开发者主动集成或同意,我们不会自行变更开发者已设置的各项业务功能及个人信息配置状态。根据开发者所集成的 SDK 版本不同,本服务功能及个人信息处理情况存在差异。当您使用集成了本服务的开发者应用时,建议您仔细阅读并理解开发者所提供的隐私政策,以便做出适当的选择。** - - -### 3、权限申请 - -| **权限【仅 Android】** | **权限功能说明** | **用途和目的** | **申请时机** | -| --------------------------------------------- | -------------------------------- | ---------------------------------- | ------------------------------------------------------------ | -| READ_PHONE_STATE 读取电话状态 | 「可选」读取设备标识符(IMEI) | 进行广告投放及广告监测归因、反作弊 | 开发者在调用需要该权限的 SDK 功能时进行调用 | -| ACCESS_COARSE_LOCATION 访问粗略位置 | 「可选」获取粗略地理位置信息 | 根据粗略位置信息进行广告投放及反作弊 | 开发者在调用需要该权限的 SDK 功能时进行调用 | -| **ACCESS_FINE_LOCATION 访问精准定位** | 「可选」获取精确地理位置信息 | 根据精确位置信息进行广告投放及反作弊 | 开发者在调用需要该权限的 SDK 功能时进行调用 | -| QUERY_ALL_PACKAGES 应用软件列表 | 「可选」获取应用软件列表 | 根据应用软件列表情况进行广告投放及反作弊 | 开发者在调用需要该权限的 SDK 功能时进行调用 | -| REQUEST_INSTALL_PACKAGES 应用请求安装软件包 | 「可选」允许应用请求安装软件包 | 游戏广告包安装 | 媒体发布的时候需要带上此权限 | -| WRITE_EXTERNAL_STORAGE 写入外置存储器 | 「可选」允许应用程序写入外部存储 | 应用下载广告投放及广告素材存储 | 开发者在调用需要该权限的 SDK 功能时进行调用 | -| READ_EXTERNAL_STORAGE 读取外置存储器 | 「可选」允许应用程序读取外部存储 | 应用下载广告投放及广告素材存储 | 开发者在调用需要该权限的 SDK 功能时进行调用 | - - -### 4、提供个性化推荐服务 - -为了展示、推荐相关性更高的信息,提供更契合您的服务,TapADN SDK 会收集、使用您的个人信息并通过计算机算法模型自动计算、预测您的偏好,以匹配您可能感兴趣的信息或服务,以下我们将详细说明该个性化推荐服务的运行机制以及您实现控制的方式。 - -a)个性化推荐服务的适用范围:我们提供个性化推荐服务的范围包括投放广告,展示图文或视频/直播内容,推荐商品或者服务。 - -b)个性化推荐服务收集的字段:为了提供个性化的服务,我们可能会收集、使用您的设备信息(硬件型号、操作系统版本、设备标识符(OAID、AndroidID、AAID、VAID))、您的操作信息(广告浏览、点击、转化相关操作记录)、应用程序的总体安装使用情况以及经您授权由其他合作方提供的其他信息。系统会对上述信息进行自动分析和计算,预测您的偏好特征,根据计算结果提供个性化推荐服务。 - -c)个性化推荐服务的训练和干预:我们会根据您在使用过程中的浏览相关行为对推荐模型进行实时训练和反馈,不断调整优化推荐结果。为了满足您的多元需求,避免同类型内容过于集中,我们会综合运用多样化技术对内容进行自动化处理,更好地提供优质内容和服务。 - -请您放心,我们的个性化推荐服务并不会识别特定自然人的真实身份,在向您进行个性化推荐的过程中,我们会努力妥善保护您的个人隐私。 - -请您理解,由于我们与您并无直接交互对话界面,如您希望退出个性化广告推荐功能,您可以在开发者应用内关闭。我们已向开发者提供退出个性化广告推荐服务的接口,并要求开发者确保在您选择关闭时调用该接口,以确保个性化广告推荐服务真实退出。 - -请开发者注意,如果您向最终用户提供 TapADN SDK 提供的个性化广告推荐功能,您应在 APP 隐私政策中向最终用户告知相应的功能,详细查阅[《TapADN SDK 合规使用说明》](/sdk/tap-adn/adn-compliance/)并根据我们提供配置方式进行相应配置,确保在最终用户选择关闭个性化广告推荐功能时调用相关接口,停止向最终用户继续提供个性化推荐广告。 - - -### 5、其他用途 - -我们可能会对您提供或我们收集的信息进行去标识化研究、统计分析和预测,用于改善和提升 TapADN SDK 的服务,为营销决策提供产品或技术服务支撑。 - -我们还可能为了履行服务内容所需而收集您的其他信息,例如您与我们的客户服务团队联系时提供的相关信息。 - -### 6、征得授权同意的例外 - -请您理解,在下列情形中,根据法律法规及相关国家标准,我们处理您的个人信息无需事先征得您的授权同意: -a. 为订立、履行您作为一方当事人的合同所必需; -b. 为履行法定职责或者法定义务所必需; -c. 为应对突发公共卫生事件,或者紧急情况下为保护自然人的生命健康和财产安全所必需; -d. 为公共利益实施新闻报道、舆论监督等行为,在合理的范围内处理个人信息; -e. 依照本法规定在合理的范围内处理个人自行公开或者其他已经合法公开的个人信息; -f. 法律、行政法规规定的其他情形。 - -**特别说明:根据法律规定,如信息无法单独或结合其他信息识别到特定个人的,其不属于个人信息**。当信息可以单独或结合其他信息识别到您时,或我们把没有与任何特定个人建立联系的数据与您的个人信息结合使用时,我们会将其作为您的个人信息,按照本隐私政策进行处理与保护。 - - -## 二、我们如何存储个人信息 - -我们非常重视信息安全,并遵循严格的安全标准,使用符合业界标准的安全措施保护您提供的信息,采用各种合理的技术、运营和管理方面的安全措施来保护我们所采集信息的安全。防止信息遭到未经授权的访问、公开披露、使用、修改、损坏或丢失。 - -### 1、信息存储的地点 - -我们依照法律法规的规定,将在中华人民共和国境内运营过程中收集和产生的个人信息存储于境内。我们不会将上述信息传输至境外,如果开发者将您的个人信息传输至境外,开发者应履行数据出境相关的合规义务,向最终用户告知境外接收方的名称或者姓名、联系方式、处理目的、处理方式、个人信息的种类以及个人向境外接收方行使本法规定权利的方式和程序以及其他法律要求事项,并应征得最终用户的单独同意,履行法定程序,并满足法律法规所规定的其他条件。 - -### 2、存储期限 - -我们仅在为开发者提供服务之目的所必需的期间内保留您的信息。超出与开发者约定的存储期限后,或者接到开发者的相应指令后,我们将依照法律法规的要求对您的个人信息进行删除或匿名化处理,但法律法规另有规定的除外。 - - -## 三、我们如何共享、转让、披露您的个人信息 - - -### 1、共享 - -#### (1)基本原则 - -a.合法原则:与合作方合作过程中涉及数据使用活动的,必须具有合法目的、符合法定要求的合法性基础。如果合作方使用信息不再符合合法原则,则其不应再使用您的个人信息,或在获得相应合法性基础后再行使用。 - -b.正当与最小必要原则:数据使用必须具有正当目的,且应以达成目的必要为限。 - -c.安全审慎原则:我们将审慎评估合作方使用数据的目的,对这些合作方的安全保障能力进行综合评估,并要求其遵循合作协议与相关条款。我们会对合作方获取信息的软件工具开发包(SDK)、应用程序接口(API)进行严格的安全监测,以保护数据安全。 - -#### (2)合作的场景 - -a.为实现相关功能或服务与关联方共享 - -当您在使用 TapADN SDK 及其关联方产品或服务期间,为保障您在我们及关联方提供的产品间所接受服务的一致性,并方便统一管理您的信息,我们会将您去标识化后的个人信息与这些关联方共享。 - -b.我们可能会与广告合作方共享 - -我们可能与委托我们进行推广和广告投放的合作伙伴和其他合作伙伴,如广告主、代理商、广告监测服务商、进行广告投放与优化合作的关联方等(合称“广告合作方”)共享分析去标识化或匿名化处理的设备信息(硬件型号、操作系统版本、设备标识符(OAID、AndroidID、AAID、VAID))、统计信息、标签信息或其他由 TapADN SDK 所采集的必要信息。这些信息难以或无法与您的真实身份相关联。这些信息将帮助我们分析、衡量、优化广告和相关服务的有效性,提升广告有效触达率。 - -1. 广告推送与投放:进行推送/推广和广告投放或提供相关服务的广告合作方可能需要使用您的设备信息(硬件型号、操作系统版本、设备标识符(OAID、AndroidID、AAID、VAID))、网络信息、渠道信息以及标签信息;与广告主合作的广告推送/推广、投放/分析服务的广告合作方可能需要使用前述信息,以实现广告投放、优化与提升广告有效触达率。 -2. 广告统计分析:提供广告统计分析服务的广告合作方可能需要使用您的设备信息(硬件型号、操作系统版本、设备标识符(OAID、AndroidID、AAID、VAID))、网络信息、广告点击、浏览、展示以及广告转化数据用于分析、衡量、优化广告和相关服务的有效性。 -3. 广告合作方对信息的使用:广告合作方可能将上述信息与其合法获取的其他数据相结合,以优化广告投放效果,我们会要求其对信息的使用遵循合法、正当、必要原则,保障您合法权利不受侵犯。 -4. 广告留资信息:您在广告中主动填写、提交的联系方式、地址等相关信息,可能被广告主或其委托的合作方收集并使用。 - -c.实现安全与统计分析 - -1. 保障使用安全:我们非常重视产品和服务的安全性,为保障您的正当合法权益免受不法侵害,我们的合作方可能会使用必要的设备、帐号及日志信息。 -2. 分析产品情况:为分析 TapADN SDK 服务的稳定性,提供分析服务的合作方可能需要使用服务情况(崩溃、闪退记录)、设备标识信息、应用总体安装使用情况。 -3. 学术科研:为提升相关领域的科研能力,促进科技发展水平,我们在确保数据安全与目的正当的前提下,可能会与合作的科研院所、高校等机构使用去标识化或匿名化的数据。 - -### 2、转让 - -随着业务的持续发展,我们将有可能进行合并、收购、资产转让,您的个人信息有可能因此而被转移。在发生前述变更时,我们将按照法律法规及不低于本隐私政策所载明的安全标准要求继受方保护您的个人信息,继受方变更原先的处理目的、处理方式的,我们将要求继受方重新征得您的授权同意。 - -### 3、披露 - -我们不会主动公开您未自行公开的信息,除非遵循国家法律法规规定所必须或者获得您的同意。 - - - -## 四、您如何管理个人信息 - -### 1、用户权利行使 - -我们非常重视您对个人信息管理的权利,包括查阅、复制、更正、补充、删除、撤回授权同意等权利,我们将尽全力帮助您管理您的个人信息,以使您有能力保障自身的隐私和信息安全。 - -**您作为最终用户,请您知悉:由于您不是我们的直接用户,与我们并无直接的交互对话界面,为保障您的权利实现,我们已要求集成我方服务的开发者承诺,应为您提供便于操作的用户权利实现方式。请您知悉并理解,因开发者独立开发和运营其应用并与您直接发生交互,我们无法控制或全面掌握开发者应用中交互界面设计及其对个人信息权益的响应情况。** - -作为直接面向最终用户提供应用程序服务的开发者,在集成 TapADN SDK 后为最终用户提供服务时,开发者应根据使用的开发者平台提供的功能设置,为最终用户提供并明确其个人信息管理的路径,并及时响应最终用户个人信息管理请求。 - -在开发者集成使用本服务过程中,如果最终用户提出管理其个人信息的请求,且开发者已确定该等请求涉及到本 SDK 产品处理的个人信息、需要我们协助处理时,开发者应当及时通过本隐私政策联系方式联系我们,并附上必要的最终用户请求的书面证明材料。我们将及时核验相关材料,并按照相关法律法规及本隐私政策,为开发者响应最终用户的行权请求提供相应的支持与配合。 - -### 2、停止运营 - -如果我们停止运营 TapADN SDK 的服务,将及时停止继续收集您的个人信息。我们会向开发者发送通知并要求开发者以公告或其他适当的方式向您发送停止运营的告知,并对我们所持有的与已关停的产品或服务相关的个人信息进行删除。 - -## 五、我们如何保护您的个人信息 - -我们会为您的信息安全提供保障,以防止信息的丢失、不当使用、未经授权访问或披露。为此,我们将采取合理的安全措施(主要包括技术方面和管理方面)来保护您的个人信息。**但请您理解,互联网并非绝对安全的环境,任何安全措施都无法做到无懈可击。** - -### 1、技术措施 - -1. 使用加密技术以确保数据传输的保密性; -2. 提供多种安全功能以保护您的账号安全; -3. 使用去标识化、匿名化等合理可行的手段保护您的个人信息; -4. 使用受信赖的保护机制防止数据遭到恶意攻击; - - -### 2、管理措施 - -1. 建立数据处理和访问制度,以防未经授权的人员访问我们的系统; -2. 对员工进行安全教育与培训,签署保密协议,加强员工对于保护个人信息安全重要性的认识; -3. 成立专门的数据安全部门,并制定个人信息安全事件的应急预案; -4. 定期组织安全应急预案演练,预防此类安全事件发生; -5. 若发生个人信息泄露等安全事件,我们会启动应急预案,阻止安全事件扩大,并通过推送通知、公告等形式告知您。 - - -## 六、未成年人保护条款 - -我们非常重视未成年人的个人信息保护工作,若您是 18 周岁以下的未成年人,在使用我们的服务前,应在您监护人的监护、指导下共同阅读并同意本隐私政策。若您的监护人对相关未成年人的个人信息有疑问时,可以联系 SDK 所接入的应用开发者,或通过本隐私政策第八条所述方式与我们联系。 - -特别地,若您是 14 周岁以下的儿童,我们还专门制定了[《儿童个人信息保护规则》](https://www.taptap.cn/doc/children-privacy/),儿童及其监护人在使用我们的服务前,应仔细阅读[《儿童个人信息保护规则》](https://www.taptap.cn/doc/children-privacy/)。如开发者应用涉及处理不满 14 周岁儿童的个人信息,开发者应当向不满 14 周岁儿童的监护人详细告知处理儿童个人信息的情况,并就该等处理事项获得儿童监护人的授权同意,同时开发者还应当针对不满 14 周岁儿童制定专门的个人信息处理规则。只有在取得监护人对相关个人信息处理规则的同意后,14 周岁以下的儿童方可使用对应服务。 - -如果经我们发现第三方开发者未获得儿童的监护人同意向我们提供儿童个人信息的,我们将尽快删除该儿童的个人信息并确保不对其进一步处理。 - -## 七、本政策如何更新 - -(一)为了提供更好的服务,TapADN SDK 及相关服务将不时更新与变化,我们会适时对本隐私政策进行修订,这些修订构成本隐私政策的一部分。未经您明确同意,我们不会削减您依据当前生效的隐私政策所应享受的权利。 - -(二)本隐私政策更新后,我们会在官网隐私政策公示页面及其他相关页面公布更新版本,以便开发者与您及时了解本隐私政策的最新版本。我们会向开发者告知本隐私政策更新的内容,由开发者向您告知并取得您的授权同意。若您继续使用开发者及我们的服务,即表示已经充分阅读、理解并同意受更新后的隐私政策约束。 - - -## 八、联系我们 - -如果您对个人信息保护及本隐私政策的内容有任何疑问、意见或建议,您可以通过以下方式与我们联系。 - -TapADN 个人信息保护负责人邮箱:`privacy@taptap.com` - -联系地址:上海市静安区灵石路 718 号 B1 北楼 TapADN 隐私与数据合规中心(收) - -邮编:200072 - -我们将在 15 天内予以回复。 - - -## 九、其他 - -本隐私政策适用于 TapADN SDK。需要特别说明的是,本政策不适用于其他第三方向您提供的服务,也不适用于我们提供的已另行独立设置隐私政策的 SDK 产品或服务。 - -请您了解,本政策中所述的 TapADN SDK 及相关服务可能会根据您所使用的设备型号、系统版本、软件应用程序版本、移动客户端等因素而有所不同。最终处理个人信息的情况以您使用的开发者应用所实际集成的 TapADN SDK 版本为准。(完) - diff --git a/docs/sdk/tap-adn/faq.mdx b/docs/sdk/tap-adn/faq.mdx deleted file mode 100644 index 809531c0a..000000000 --- a/docs/sdk/tap-adn/faq.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -### 1、有展现量没有预估收益 /eCPM 波动大? - -独占接入 TapADN 广告,计费点如下: - -* 开屏广告:点击跳转 h5,在 h5 内下载; - -* 激励视频、banner、信息流:下载 apk; - -下载即计费,无下载的游戏无收益。 - -一般展现量 5k 以上数据较为可靠,收益较低的游戏可尝试聚合接入。 - -### 2、新建正式媒体找不到预约游戏 - -可在上线前,建测试媒体和测试推广位,提前接好 SDK; - -上线后将媒体和广告位转为正式直接开启计费; - -### 3、聚合模式由于没有回传 oaid 无数据 - -聚合接入(bidding 和瀑布流)必须回传 oaid,没有 oaid 的广告不参竞,建议回传 oaid,回传方式见[获取 oaid 并回传](/sdk/tap-adn/scheme/#3获取-oaid-并回传强烈建议)。 - -### 4、未上架 Tap 的媒体如何接入 TapADN? - -联系对接人,提供以下信息进行资质审核,平台方手动开户。 - -(1)提交资质文件:营业执照、软著、ICP 证,若上述证件主体不一致,需提供授权书。 - -(2)与财务确认是否可以开具广告费。 - -(3)提供公司名称、统一社会信用代码、TapTap 用户 id 作为超管用户。 - -### 5、如何获取 SHA1 - -不同签名文件的 SHA1 值不同,可以参考下面三种获取 SHA1 值的方式: - -**一、无法获取 keystore 的情况,获取 SHA1 的方法(优先推荐)** - -**获取 SHA1 方法源码请:[点击跳转](https://lf6-ttcdn-tos.pstatp.com/obj/ad-tetris-site/AppSigning.java)** - -![源码](https://capacity-files.lcfile.com/bEe1uaMYUu54KM15Nx3GGbKnKKPFiNNG/157634260ac5d68ba1cd6a705bef285d.png) - -**二、通过 Eclipse 编译器获取** - -使用 adt 22 以上版本,可以在 Eclipse 中直接查看。 - -Windows:依次在 Eclipse 中打开 Window -> Preferences -> Android -> Build - -Mac:依次在 Eclipse 中打开 Eclipse/ADT -> Preferences -> Android -> Build - -在弹出的 Build 对话框中 「SHA1 fingerprint」 中的值即为 Android 签名证书的 SHA1 值,如图所示: - -![Eclipse 页面](https://capacity-files.lcfile.com/KhIkBPGze6Y8yjnfAlcEdEBaUlckuUjB/22.png) - -**三、通过 keytool 即 jdk 自带工具获取** - -按照如下步骤进行操作: - -1、运行,进入控制台 - -![cmd](https://capacity-files.lcfile.com/WwtnCLs9omH3Rhv9kV1wPIcc3YECO1FE/cmd.png) - -2、 在控制台窗口中输入 `cd .android` ,然后定位到 `.android` 文件夹 - -![cmd_exe](https://capacity-files.lcfile.com/jXjKy7NlKGKwA5uEklQzjjf74urX6Oz6/cmd_exe.png) - -3、继续在控制台输入命令。 - -debug.keystore:命令为: -``` -keytool -list -v -keystore debug.keystore -``` -自定义的 keystore:命令为: -``` -keytool -list -v -keystore apk 的 keystore -``` -如图所示: - -![keytool](https://capacity-files.lcfile.com/XzDOuLRMMBB84XBI5uAfwvccpwO39oAg/keytool.png) - -提示输入密钥库密码,编译器提供的 debug keystore 默认密码是 Android,请自行填写自定义签名文件的密码。 - -输入密钥后回车(如果没设置密码,可直接回车),此时可在控制台显示的信息中获取 SHA1 值,keystore 文件为 Android 签名证书文件。 - -如下图所示: - -![SHA1 值](https://capacity-files.lcfile.com/YwQKTQjoj7ooJ2eADXfmfWRJyH6J43XV/333.png) - -### 6、每日观看次数限制是否有限制 - -平台未设置单个用户的观看上限。但是单个客户每日多次观看广告 ecpm 会有边际递减的效应,所以建议控制单个用户每日观看广告在十次以内。 - diff --git a/docs/sdk/tap-adn/features.mdx b/docs/sdk/tap-adn/features.mdx deleted file mode 100644 index 0c2fd2a0d..000000000 --- a/docs/sdk/tap-adn/features.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: TapADN 功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 2 ---- - -import {Conditional} from '/src/docComponents/conditional'; - - -## 功能介绍 - -### 广告类型 -当前版本支持的广告类型:激励视频、开屏、Banner - -| 广告类型 | 特点 | 使用场景 | 转化方式 | 请求逻辑 | 备注 | -|-----------|-------------|-----------------------------------|---------|---------|-----| -| 激励视频 | 1、用户在完整观看完视频后(或者观看至少 30s),才可获得一定的奖励,如游戏内获取道具/角色复活/解锁关卡等;

    2、激励视频不可跳过(如有特殊需求,可进行配置,如果配置跳过,一定会有弹窗提示未观看完整视频,无法获取奖励,并且会对点击率、eCPM 有负向影响) | 1、进入视频的入口处对用户有明确的提示,完整观看视频后会获取某一种奖励;

    2、用户主动选择观看广告 | 直接下载 APK、Deeplink 跳转落地页 、 跳转小程序/小游戏 | 视频广告加载时间相对较长,建议开发者做好预请求,即在用户触达广告前一定时间进行提前请求;

    **注意点 1:**需开发者体验用户整个广告触达的流程,尽可能保证在请求到观看广告之间的时间越短越好; 

    **注意点 2:**不建议开发者再做缓存(SDK 内有缓存逻辑,如开屏) | 人均观看次数过高严重影响 eCPM,建议严格控制人均次数(一般建议控制在 7 次以内)

    激励视频由于广告带有奖励性质,有一定的成本,因此在设计场景奖励及次数数需要核算成本和收入 | -| 开屏 | 1、展示时间为 5s,点击“跳过”按钮可直接跳过

    2、素材样式:图片&视频 | 用户进入应用前展示 

    **冷启动:**用户首次进入应用,或结束进程后重新进入 

    **热启动:**用户在未结束进程的情况下退出应用后,再次进入 | Deeplink 跳转落地页 、 跳转小程序/小游戏 | TapADN SDK 对所有开屏推广位均有缓存机制,无需开发者再做缓存,**每次请求 1 条即可** | 建议将 SDK 收到请求到广告渲染完成时间(广告等待/返回时长)设置为 3.5 秒,短于 3.5 秒可能会影响展示率 | -| Banner | 1、常用样式,无特殊需求

    2、可以设置为轮播 | tab 页面底部/顶部 | 直接下载 APK , Deeplink 跳转落地页 , 跳转小程序/小游戏 | / | / | -| 信息流 | 为可自定义布局的信息流广告,包含大图和视频两种基本样式类型,应用下载、跳转到落地页和跳转到浏览器三种交互类型。 | 自定义 | 直接下载 APK , Deeplink 跳转落地页 , 跳转小程序/小游戏 | / | / | -| 插屏 | 支持「全屏视频广告」、「全屏图文广告」和「半屏图文广告」等形式,具体广告展示形式在后台配置 | 自定义 | 直接下载 APK , Deeplink 跳转落地页 , 跳转小程序/小游戏 | / | / | - - -## 接入规范 - -为了优化广告效果,对广告展示(广告出现场景/广告元素渲染)制定如下要求: -1. **每种广告位只能用于特定场景,不能混用** -2. **可点区域,为避免误点,广告应与应用内容明确区分** - 1. 广告边框清晰可见 - 2. 广告周边应留出空白区域 - 3. 广告与应用内其他可点区域之间保持距离 - 4. 除开屏广告位之外,其他广告位广告可点区域仅限于广告图片、标题、按钮等素材,不应将广告背景设置为可点区域 -3. **请勿遮挡、扭曲、模糊广告图片和内容。** -4. **有效的广告展示:展示超过 20% 像素,观看时间超过 1s** -5. **各个类型代码位广告实施规范** - 1. **开屏广告:**开屏广告场景在应用启动时进行展示,展示完毕后自动关闭并进入应用的主界面。开屏代码位只能用于应用开屏的位置。 - 2. **Banner 横幅广告**:横幅广告是在内容底部或顶部显示的小条形广告。不应将横幅广告放置于文本、图片和应用的其他可点击部分,避免误点。 - 3. **激励视频广告** - 1. 激励视频广告的展示场景是用户在需要游戏复活、解锁游戏关卡、获得游戏道具等享受应用中的某些功能时由用户选择观看激励视频广告并获得相应激励。 - 2. 展示广告前向用户说明激励广告规则,明确告知用户看完视频广告后能获得相应奖励 - -## SDK 新增特点 -1. ** SDK 支持电商类广告和小程序/小游戏广告** -:::info -小程序整体接入流程: - - ·进入微信开放平台创建移动应用并获取到相应的微信 AppID; - - ·在移动端嵌入最新版 OpenSDK(仅嵌入即可,无需额外开发工作),并确认版本为 Android v5.3.1 以上; - - ·在adn开发者平台,将微信开放平台填写的 AppID 与当前应用进行关联; - - ·嵌入更新 Tapadn SDK 到 3.16.3.22 以上版本 -::: -小程序广告, 优先支持上述类型, 也支持微信 url scheme 形式打开小程序(后者无需媒体方任何操作, 但小程序广告转化率可能会低于前者方式)。 diff --git a/docs/sdk/tap-adn/scheme.mdx b/docs/sdk/tap-adn/scheme.mdx deleted file mode 100644 index d87b5a4c3..000000000 --- a/docs/sdk/tap-adn/scheme.mdx +++ /dev/null @@ -1,211 +0,0 @@ ---- -title: TapADN 合作方案 -sidebar_label: 合作方案 -sidebar_position: 1 ---- - -import {Conditional} from '/src/docComponents/conditional'; - -## 一、TapADN 介绍 - -### 什么是 TapADN - -开发者接入 Tap 联盟 SDK,将游戏内广告流量提供给 Tap 联盟,用户发起广告请求时展示 Tap 联盟广告。 - -### 1、独占接入 - -Tap 联盟为开发者提供广告流量结算收益。 - -Tap 根据开发者提供的广告流量价值,按比例扶持开发者在 Tap 内的曝光,帮助开发者进行用户增长(流量扶持必要条件:一是独占接入,指流量不通过竞价的方式筛选后进入 Tap 联盟,切流或全量均可,二是游戏为 Tap 上架游戏)。 -![TapADN 合作方案流程](https://capacity-files.lcfile.com/s80cObaAERALxuawFmUqf0wmvGLiHi4x/%E7%8B%AC%E5%8D%A0%E6%8E%A5%E5%85%A5.png) - -### 2、聚合接入 - -Tap 联盟成为开发者变现广告的来源之一,为开发者提供广告流量结算收益,目前支持 TapADN 的聚合平台有 Tobid、Topon、TradPlus(排名不分先后)。 - -## 二、TapADN 的优势 - -### 1、高额的广告收入回报 - -- **0 分成,开发者可得到 100% 的现金分成比例;** - -- 相较于大部分竞媒的 eCPM,TapADN 的 eCPM 相对较高; - -### 2、高质量广告供给,对游戏留存更好 - -- TapADN 不存在 p2p、贷款、交友等低俗广告,对游戏留存和用户体验更佳; -- **Tap 广告将自动安装权限完全交由开发者,Tap 不会主动开启自动安装**,对玩家游戏体验和评分维护更好; - -### 3、流量扶持助力游戏用户增长 - -为了扶持开发者,TapTap 侧会按比例对开发者给的流量做 Tap 平台曝光扶持(走聚合平台接入且以竞价形式露出无流量扶持)具体扶持策略如下:可折算为开发者每变现 1 块钱配套给到 4-6 个 TapTap 曝光(曝光位置为首页推荐);TapADN 系统自动补量,「D+1结算」,即当日产生的收益,次日系统自动消耗补量,具体补量展现可在后台查看 -(**仅独占接入且在 Tap 上架的游戏可享有**)。 - -![TapADN 补量扶持](https://capacity-files.lcfile.com/sN9tlceaAkzSh6JV3wzdRbYdgmd8EMg3/TapADN%E8%A1%A5%E9%87%8F%E6%89%B6%E6%8C%81.png) - -### 4、TapADN 展示的广告不存在为 TapTap 导流的行为 - -展示广告后可直接点击下载 APK ,无需跳转 TapTap,不存在导流行为。 - -根据《互联网广告管理暂行办法(2016)》第十三条第二款,通过程序化购买广告方式发布的互联网广告,广告需求方平台经营者应当清晰标明广告来源。故 TapTap 侧必须添加广告标识,但 TapTap 广告标识将最小化展示,基本不会引起用户注意。 - -![广告展示](https://capacity-files.lcfile.com/wTCsjquDgoiaKagvmvpOgM0RrFTl4jNS/%E5%B9%BF%E5%91%8A%E5%B1%95%E7%A4%BA.png) - -## 三、接入流程 - -登录 [TapADN 后台](https://ssp.taptap.cn/)开始接入流程; - -![接入流程](https://capacity-files.lcfile.com/51b40dNzTb9mt2zt6rcx7WIuSbufYCna/%E6%8E%A5%E5%85%A5%E6%B5%81%E7%A8%8B.png) - -### 1、接入形式 - -#### 聚合接入 - -目前支持 TapADN 的聚合平台有 Tobid、Topon、TradPlus(排名不分先后)。 - -#### 独占切流接入(建议以该模式接入,提供流量扶持) - -按照 UV 随机切流量,一部分走 TapADN 一部分走其他,对比两者的 eCPM 高低即对用户设备 ID 用均匀随机哈希函数,举个例子,比如想分 50:50 的两个流量:用 mumuhash 做 hash,然后对 2 取余,结果为 1 的分一个桶,结果为 0 的分一个桶,把结果为 1 的用户的请求全部发送给 Tap 联盟,0 的全部发送给其他;如果给 Tap 联盟 20% 就可以对 10 取余,结果是 0 和 1 的给 Tap 联盟,以此类推。 - -#### 独占全量接入(建议以该模式接入,提供流量扶持) - -将全部流量接入 TapADN。 - -### 2、接入前准备 - -#### (1)明确签约主体 - -**TapADN 资质复用开发者中心认证,即 TapADN 后台的主体信息需与 TapTap 开发者中心主体信息保持一致** - -如游戏之前挂靠下 A 主体下,目前需要使用 B 主体进行广告变现,则需要先在 TapTap 开发者中心用 B 主体资质注册新的主体 B,然后通过游戏主体转移的形式,将游戏从 A 主体转移到 B 主体,再在 TapADN 后台将游戏与 B 主体绑定:[游戏主体转移 | TapTap 开发者文档](https://developer.taptap.cn/docs/store/release/store-creategame/#4-游戏主体可以进行转移吗)。 - -- 如厂商在开发者中心认证为企业,开具增值税专用发票,按企业规模大小,税率为 1-6% 不等; - - 开票类目:广告费; - -- 如厂商在开发者中心认证为个人,需自行开具发票,以当地税务局要求为准,按劳务报酬分类由企业代扣代缴,税率为 20%,可开具增值税专用发票、增值税普通发票。 - - 开票类目:广告费; - -#### (2)确认有无广告费发票开具的经营资质 - -TapADN 业务属于广告范畴,**财务侧严格要求必须开具「广告费」**,如厂商由于经营资质无法开具,建议修改经营资质,10 天左右可修改完成。 - -#### (3)获取 Oaid 并回传(强烈建议) - -oaid 可以帮助 TapADN 侧获取用户特征,是 TapTap 平台侧和广告主串后链路数据的唯一必要 id,广告主投放只看激活、付费这些后链路效果,如果没有 oaid,付费和激活数据会被白白浪费,广告主观客到的数据变差,就会降价停投,最终导致 eCPM 暴跌,oaid 是最重要的回传数据,目前观测到的数据看,没有回传 oaid 或者覆盖率不高的媒体 eCPM 比回传高的低 50-90%。 - -**oaid 获取方式:** - -中国移动联盟官网有详细说明,先注册账号然后接入移动官方联盟的 SDK,申请 oaid 证书,即可获取,链接如下: - -[移动安全工作委员会](https://msa-alliance.cn/col.jsp?id=120) - -![oaid 相关](https://capacity-files.lcfile.com/Ec2obP3eo0LyvXTLCrWzePbIR1QcfErI/oaid%E7%9B%B8%E5%85%B3.png) - -#### (4)使用 TapTap 登陆获取 Tapid(非强制) - -Tapid (使用 Tap 登录即可获得)可以帮助 Tap 联盟侧获取用户特征,增强模型千人千面的广告推荐能力,从而提升 eCPM,增加收益。 - -### 3、接入流程 - -登陆 [TapADN 后台](https://ssp.taptap.cn/)按照后台流程操作。 - -#### (1)绑定厂商→填写账户信息→签署协议 - -![创建账户](https://capacity-files.lcfile.com/JDhUguQu07iBLO7zUEc0Rqo3BiDvB34p/download_image%20%281%29.png) - -#### (2)新建媒体(流量管理-媒体管理-新建媒体) - -![新建媒体](https://capacity-files.lcfile.com/EMyTD9E0WGsnv0TyOnJPFsCugp9q5fqq/Frame%202.png) - -1. **获取媒体秘钥** - -![媒体密钥](https://capacity-files.lcfile.com/T5flcDmCKvn0Cw3mRl02cxxqOnbUtYEB/557AE20A-D403-465A-A307-8E8DB2FFA40F.png) - -2. **Access Token 获取** - -![Access Token 获取](https://capacity-files.lcfile.com/m2vEFCE9VIWj5wp3w7vYTl3AjAoh1QGv/Frame%202.png) - -3. **流量主 id 获取** - -![流量主 id 获取](https://capacity-files.lcfile.com/IymjB4T0nXXsuUCtEomlMBTywEdd89d6/Frame%203.png) - -#### (3)新建推广位(流量管理-推广位管理-新建推广位) - -1. 首次新建推广位时,需选择测试,以便接入时测试广告位的接入情况。 - - 若测试无误,后续**游戏正式上线前需将测试推广位转为正式推广位,测试推广位无计费、无收益。** - -2. 「是否用于第三方平台聚合」 - - 瀑布流及竞价「bidding」模式接入选择「是」。 - - 直接切流及走聚合平台非竞价模式接入选择「否」。 - - 该选项不作为实际评估是否有流量补贴收益的凭证,仅作为与 TapADN 侧的信息同步。 - -3. 期望 CPM - - 设置则为瀑布流模式 接入的预设底价。 - - 不设置则为「bidding」模式。 - -![CPM 设置](https://capacity-files.lcfile.com/qe5agrPNGJTVBNL6tEVRIWCGzJzSgBr1/Frame%204.png) - - -#### (4)技术接入 - -- TapADN SDK 下载:可以从 [TapADN 后台](https://ssp.taptap.cn/)中的**接入中心**下载 SDK。 -- TapADN 接入文档:可以参考 [TapADN SDK 接入指南](/sdk/tap-adn/tds-tapad/)。 - -#### (5)上线前的接入测试 - -**TapADN 后台测试选项:位置:流量管理-测试工具** - -「测试数据」测试广告位的数据都会在这里呈现,仅统计过去 24 小时的数据,有数据正常返回则表示调试成功「测试设备」名单里的用户,点击广告不计费,可以避免被平台误判为黑产。 - -### 4、聚合接入文档指引 - -#### Tobid - -![Tobid](https://capacity-files.lcfile.com/Gt0D40UOD4O3d2md6QjpRTEWAWvdmRqA/Frame%203-2.png) - -#### Topon - -[TapTap 接入 Topon 指南](https://docs.toponad.com/#/zh-cn/android/NetworkAccess/taptap/taptap_cn) - -![Topon](https://capacity-files.lcfile.com/o0eabdurEFUGsiYNXmeeK5RzJ6ldPpwq/%E6%96%B0%E5%BB%BA%E9%A1%B9%E7%9B%AE%20%281%29.png) - -#### TradPlus - -TradPlus 后台官方集成文档:[Android 集成说明 | TradPlus Knowledge Center](https://docs.tradplusad.com/docs/doc_u3d/u3d_integration/setting_info/setting_info_android/) - -### 5、结算流程 - -#### (1)TapADN 后台数据呈现时效 - -流量收益:T+1 天更新,主要呈现预估收益(现金收益); - -补量扶持:T+1 结算,主要提供扶持数据查询,扶持金额满 200 元扶持曝光系统自动下发,开发者无需操作。 - -#### (2)结算时间 - -每月 10 号系统生成结算单,15 号之前开发者需给齐结算材料,30 个工作日内付款。一般 20-25 号即可收到款项; - -15 号若为周末,材料截止日延至首个工作日; - -15 号若为工作日,材料截止日即为当天超期晚到的材料顺延下个月付款; - -#### (3)结算单获取 - -位置:财务结算-结算管理; - -**结算单需要盖公章/合同章,不可盖财务专用章;** - -![结算单](https://capacity-files.lcfile.com/qFCS71TP22dw7tj10USmzVkdE5K4elCE/Frame%202-8.png) - - -## 附件 - -[TapADN Demo 源码](https://github.com/taptap/TapADN-Demo) diff --git a/docs/sdk/tap-adn/tds-tapad.mdx b/docs/sdk/tap-adn/tds-tapad.mdx deleted file mode 100644 index 2c99de327..000000000 --- a/docs/sdk/tap-adn/tds-tapad.mdx +++ /dev/null @@ -1,2003 +0,0 @@ ---- -title: TapADN SDK 接入指南 -sidebar_label: 接入指南 -sidebar_position: 3 ---- - - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import { Conditional } from "/src/docComponents/conditional"; - -## SDK 配置 - - - -<> - -Unity 模块是通过引入 Android 模块后增加桥接文件打包出的 `.unitypackage`,方便以 Unity 开发的游戏直接引入。其他引擎/平台的游戏可以通过 Android 原生的方式接入,详见 Android 接入文档。 - -**开发环境要求**: - -- Unity 2019.4 或更高版本 -- Android 5.0(API level 21)或更高版本 - -:::tip -Unity 项目可在 `TapTap/TapAD/Demo` 找到示例工程来了解其实现过程,注意** Demo 工程需要到真机中才能看到效果,Editor 环境并不会实际生效**。 -::: - -
    - -点击展开 Unity 2020.3.15 之前的版本升级 Gradle 版本 - -在 `Project Settings` -> `Player` -> `Android Tab` -> `Publish Settings` -> `Build`,然后勾选**Custom Base Gradle Template** - -将以下更改应用于生成的这个文件: -**Assets/Plugins/Android/baseProjectTemplate.gradle** - -如果存在,请移除文件顶部的以下注释: - -```groovy -// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN -``` - -修改文件内容: - -```groovy -dependencies { - // If you are changing the Android Gradle Plugin version, make sure it is compatible with the Gradle version preinstalled with Unity - // See which Gradle version is preinstalled with Unity here https://docs.unity3d.com/Manual/android-gradle-overview.html - // See official Gradle and Android Gradle Plugin compatibility table here https://developer.android.com/studio/releases/gradle-plugin#updating-gradle - // To specify a custom Gradle version in Unity, go do "Preferences > External Tools", uncheck "Gradle Installed with Unity (recommended)" and specify a path to a custom Gradle version - // highlight-start - // 将 3.x.0 版本修改为 4.0.1 - //classpath 'com.android.tools.build:gradle:3.x.0' - classpath 'com.android.tools.build:gradle:4.0.1' -} -``` - -同时,为了将 [Gradle 版本和 Android Gradle Plugin 版本对应](https://developer.android.com/studio/releases/gradle-plugin#expandable-1),需要更新 Gradle 版本,下载 [6.1.1 版本的 Gradle](https://services.gradle.org/distributions/gradle-6.1.1-bin.zip),解压后放到自定义的文件夹中,同时**不**勾选 Unity 中的 `Preferences` -> `External Tools`-> `Android` -> `Gradle Installed with Unity(recommend)`,改为选择解压后 Gradle 文件夹的位置,如 /gradle-6.1.1。 - -
    - -无论 Unity 版本都添加一些原生依赖库:在 `Project Settings` -> `Player` -> `Android Tab` -> `Publish Settings` -> `Build`,勾选**Custom Main Gradle Template** - -将以下更改应用于生成的这个文件: -**Assets/Plugins/Android/mainTemplate.gradle** - -如果存在,请移除文件顶部的以下注释: - -```groovy -// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN -``` - -修改文件内容: -:::tip -- **TapADN SDK 从 3.16.3.10 版本开始更新了 glide 的依赖,glide 版本从 4.0.0 更新到了 4.9.0。** -- ** 3.16.3.17 加入 Android Dependencies 文件(位于 TapAD/Editor/TapAdDependencies.xml) 方便接入 EDM4U(https://github.com/googlesamples/unity-jar-resolver) 的项目方解决 Android 依赖。** -::: -```groovy -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - // highlight-start -// 加入的依赖库-开始 - implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' - implementation 'io.reactivex.rxjava2:rxjava:2.0.1' - implementation 'com.squareup.okhttp3:okhttp:3.12.1' - implementation "com.android.support:appcompat-v7:28.0.0" - implementation "com.android.support:support-annotations:28.0.0" - implementation "com.android.support:support-v4:28.0.0" - implementation "com.github.bumptech.glide:glide:4.9.0" - implementation 'com.android.support:recyclerview-v7:28.0.0' -// 加入的依赖库-结束 - // highlight-end -// 下面这行是 Unity 的 mainTemplate.gradle 自带的,帮助定位插入位置 -// **DEPS** -} -``` - - - -<> - -最低支持 Android 5.0(API level 21),编译环境为 Android Studio。 - -

    -将 TapAD_{sdkVersions.tapadn.android}.aar 拷贝到游戏目录下的 `src/main/libs` 目录中。 -

    - -在游戏目录下 `build.gradle` 文件中添加代码: -:::tip -**TapADN SDK 从 3.16.3.10 版本开始更新了 glide 的依赖,glide 版本从 4.0.0 更新到了 4.9.0。** -::: - -{`repositories{ - flatDir{ - dirs 'src/main/libs' - } -} -dependencies { - // ... - implementation(name: "TapAD_${sdkVersions.tapadn.android}", ext: "aar") // 广告 SDK - implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' - implementation 'io.reactivex.rxjava2:rxjava:2.0.1' - implementation 'com.squareup.okhttp3:okhttp:3.12.1' - implementation "com.android.support:appcompat-v7:28.0.0" - implementation "com.android.support:support-annotations:28.0.0" - implementation "com.android.support:support-v4:28.0.0" - implementation "com.github.bumptech.glide:glide:4.9.0" - implementation 'com.android.support:recyclerview-v7:28.0.0' - // ... -}`} - - - - -
    - -### 权限申请 - - - -<> - -无论 Unity 版本都需加入 Android 相关权限申请,在 `Project Settings` -> `Player` -> `Android Tab` -> `Publish Settings` -> `Build`,勾选**`Custom Main Manifest`**。 - -将以下更改应用于生成的这个文件: -**Assets/Plugins/Android/AndroidManifest.xml** - -如果存在,请移除文件顶部的以下注释: -``` xml -// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN -``` - -修改文件内容: - -```xml - - - - // highlight-start - - - - - - - - - - - - - - - - - - - - - // highlight-end - ... -``` - - - -<> - -配置 AndroidManifest.xml - -```xml - - - - - - - - - - - - - - - - -``` - - - - - -## 获取广告权限 - -在获取 imei 地理位置时,需要用户授予权限。建议先询问权限,再进行初始化工作。 - - - -<> - -```cs -using TapTap.TapAd -TapAdSdk.RequestPermissionIfNecessary(); -``` - - - -<> - -```java -TapAdManager.get().requestPermissionIfNecessary(activity); -``` - - - - - -## 初始化 - - - -<> - -```cs -using TapTap.TapAd - -TapAdConfig config = new TapAdConfig.Builder() - .MediaId(your_media_id) // 必选参数,为 TapADN 注册的媒体 ID - .MediaName(your_media_name) // 必选参数,为 TapADN 注册的媒体名称 - .MediaKey(your_media_key) // 必选参数,媒体密钥,可以在 TapADN 后台查看(用于传输数据的解密) - .MediaVersion("1") // 必选参数,默认值 "1" - .Channel(your__channel) // 必选参数,渠道 - .TapClientId(your_tap_client_id) // 可选参数,TapTap 开发者中心的游戏 Client ID - .EnableDebugLog(false) // 可选参数,是否打开原生 debug 调试信息输出:true 打开、false 关闭。默认 false 关闭 - .ShakeEnabled(false) // 可选参数,是否开启摇一摇: true 打开、false 关闭。 - .Build(); - -// CustomControllerWrapper 为实现了 TapTap.TapAd.ICustomController 的类 -// onInitedCallback 为可选回调函数,类型是 System.Action, -TapAdSdk.Init(config, new CustomControllerWrapper(this), onInitedCallback); - -// ICustomController 接口说明 -public interface ICustomController -{ - // 是否允许 SDK 主动使用地理位置信息 - bool CanUseLocation { get; } - - // 当 isCanUseLocation=false 时,可传入地理位置信息,TapAd 使用您传入的地理位置信息 - TapAdLocation GetTapAdLocation { get; } - - // 是否允许 SDK 主动使用手机硬件参数,如 imei - bool CanUsePhoneState { get; } - - // 当 isCanUsePhoneState=false 时,可传入 imei 信息,TapAd 使用您传入的 imei 信息 - string GetDevImei { get; } - - // 是否允许 SDK 主动使用 ACCESS_WIFI_STATE 权限 - bool CanUseWifiState { get; } - - // 是否允许 SDK 主动使用 WRITE_EXTERNAL_STORAGE 权限 - bool CanUseWriteExternal { get; } - - // 开发者可以传入 oaid - // 信通院 OAID 的相关采集——如何获取 OAID: - // 1. 移动安全联盟官网 http://www.msa-alliance.cn/ - // 2. 信通院统一 SDK 下载 http://msa-alliance.cn/col.jsp?id=120 - string GetDevOaid { get; } - - // 是否允许 SDK 主动获取设备上应用安装列表的采集权限 - bool Alist { get; } - - // 是否允许 SDK 主动获取 ANDROID_ID - bool CanUseAndroidId { get; } - - // 用户信息(为了提高广告的推送效率,开发者可以帮助传入一些用户的画像信息) - CustomUser ProvideCustomer(); -} - -// 地理位置说明 -public sealed class TapAdLocation -{ - // 纬度 - public double latitude; - - // 经度 - public double longitude; - - // 精度 - public double accuracy; - - // 设置经纬度 - public TapAdLocation(double latitude, double longitude, double accuracy) - { - //... - } -} - -// 用户信息 -public sealed class CustomUser -{ - public int realAge; - // 0.男;1.女 - public int realSex; - // 角色性别 0.男;1.女 - public int avatarSex; - // 角色等级 - public int avatarLevel; - // 是否新玩家 0.否;1.是 - public int newUserStatus; - // 是否为付费用户 0.否;1.是 - public int payedUserStatus; - // 是否通过新手教程 0.否;1.是 - public int beginMissionFinished; - // 角色当前付费道具数量 - public int avatarPayedToolCnt; -} -``` - - - -<> - -```java -TapAdConfig config = new TapAdConfig.Builder() - .withMediaId(your_media_id) // 必选参数。为 TapADN 注册的媒体 ID - .withMediaName(your_media_name) // 必选参数。为 TapADN 注册的媒体名称 - .withMediaKey(your_media_key) // 必选参数。媒体密钥,可以在TapADN后台查看 - .withMediaVersion("1") // 必选参数。默认值 "1" - .withTapClientId(your_tap_client_id) // 可选参数。TapTap 开发者中心的游戏 Client ID - .enableDebug(true) // 可选参数,是否打开 debug 调试信息输出:true 打开、false 关闭。默认 false 关闭 - .withGameChannel(your_app_channel) // 必选参数,渠道 - .shakeEnabled(false) // 可选参数,是否开启摇一摇: true 打开、false 关闭。默认 true 开启。 - .withCustomController(new TapAdCustomController() { - - // 是否允许 SDK 主动使用地理位置信息 - @Override - public boolean isCanUseLocation() { - return enableGetLocation; - } - - // 当 isCanUseLocation=false 时,可传入地理位置信息,TapAd 使用您传入的地理位置信息 - @Override - public TapAdLocation getTapAdLocation() { - return new TapAdLocation(longitude, latitude, accuracy); - } - - // 是否允许 SDK 主动使用手机硬件参数,如 imei - @Override - public boolean isCanUsePhoneState() { - return enableGetPhoneState; - } - - // 当 isCanUsePhoneState=false 时,可传入 imei 信息,TapAd 使用您传入的 imei 信息 - @Override - public String getDevImei() { - return imei; - } - - // 是否允许 SDK 主动使用 ACCESS_WIFI_STATE 权限 - @Override - public boolean isCanUseWifiState() { - return enableGetWifiState; - } - - // 是否允许 SDK 主动使用 WRITE_EXTERNAL_STORAGE 权限 - @Override - public boolean isCanUseWriteExternal() { - return enableWriteExternal; - } - - // 开发者可以传入 oaid - // 信通院 OAID 的相关采集——如何获取 OAID: - // 1. 移动安全联盟官网 http://www.msa-alliance.cn/ - // 2. 信通院统一 SDK 下载 http://msa-alliance.cn/col.jsp?id=120 - @Override - public String getDevOaid() { - return oaid; - } - - // 是否允许 SDK 主动获取设备上应用安装列表的采集权限 - @Override - public boolean alist() { - return enableGetAppList; - } - - // 是否允许 SDK 主动获取 ANDROID_ID - @Override - public boolean isCanUseAndroidId() { - return enableGetAndroidId; - } - - @Override - public CustomUser provideCustomUser() { - return new CustomUser.Builder() - .withRealAge(age) // 年龄 - .withRealSex(sex) // 性别 0:男 1:女 - .withAvatarSex(avatarSex) // 角色性别 0:男 1:女 - .withAvatarLevel(avatarLevel) // 角色等级 - .withNewUserStatus(status) // 是否新玩家 0:否;1:是 - .withPayedUserStatus(status) // 是否付费用户 0:否;1:是 - .withBeginMissionFinished(finished) // 是否通过新手教程 0:否 1:是 - .withAvatarPayedToolCnt(cnt) // 角色当前付费道具数量 - .build(); - } - }) - .build(); - -TapAdSdk.init(this, config); -``` - - - - - -## 上报用户行为 - -为了获取更优质的广告内容,需要游戏额外提供一些玩家在游戏中的行为数据作为补充。 - - - -<> - -```cs -using TapTap.TapAd -var userActions = new UserAction[3]; -var jan1st1970 = new DateTime - (1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); -for (int i = 0 ; i < 3; i ++) { - var tmp = new UserAction(actionType: i, actionTime: (long)(DateTime.UtcNow - jan1st1970).TotalMilliseconds, - amount: i * 1000, winStatus: i % 2); - userActions[i] = tmp; -} -// highlight-start -// CustomUserAction 为实现了 IUserAction 的类 -// highlight-end -TapAdSdk.UploadUserAction(userActions, new CustomUserAction(this)); -// 用户行为数据类说明 -public class UserAction -{ - // 行为动作类型,可自定义 - public int ActionType { get; } - - // 行为发生时间(单位:秒) - public long ActionTime { get; } - - // 付费金额 - public int Amount{ get; } - - // 是否胜利.0-失败;1-胜利 - public int WinStatus{ get; } - public UserAction(int actionType, long actionTime, int amount, int winStatus) - { - ... - } -} -// 上报接口说明 -public interface IUserAction -{ - // 上报成功 - void OnSuccess(); - - // 上报失败 - void OnError(int code, string message); -} -``` - - - -<> - -```java -UserAction[] userActions = new UserAction[3]; - -for (int i = 0 ; i < 3; i ++) { - UserAction tmp = new UserAction.Builder() - .withActionType(i) - .withActionTime(System.currentTimeMillis()) - .withAmount(i * 1000) - .withWinStatus(i % 2) - .build(); - userActions[i] = tmp; -} -TapAdManager.get().uploadUserAction(userActions, new Callback() { - @Override - public void onSuccess() { - // 上报成功 - } - - @Override - public void onError(AdException exception) { - // 上报失败 - } -}); -``` - - - - - -## 开屏广告 - -开屏广告为用户在进入 App 时展示的全屏广告。开屏广告为一个 `View`,宽高默认为 `match_parent`。 - -:::info -竖屏开屏广告 `view` 要求 width = 屏幕宽,height 需要 >= 75% 屏幕高,否则会影响计费。 - -横屏开屏广告 `view` 要求 width = 屏幕宽,height 需要 = 屏幕高,否则会影响计费。 -::: - -建议业务逻辑: - -1. 每次启动应用的时候调用 SDK 接口请求开屏广告。 -2. SDK 会根据一定规则从候选队列里选取一个返回给游戏,获取成功就可以展示,获取失败的话本次不展示广告。 - -### 获取广告 - - - -<> - -```cs -using TapTap.TapAd -TapSplashAd _tapSplashAd = null; -if (TapAdSdk.IsInited == false) -{ - Debug.Log("TapAd 需要先初始化!"); - return; -} -// 释放之前的广告 -if (_tapSplashAd != null) -{ - _tapSplashAd.Dispose(); - _tapSplashAd = null; -} -int adId = YOUR_AD_ID; -// create AdRequest -var request = new TapAdRequest.Builder() - .SpaceId(adId) - .Build(); -_tapSplashAd = new TapSplashAd(request); -// highlight-start -// SplashAdLoadListener 为实现了 ISplashAdLoadListener 的类 -// highlight-end -_tapSplashAd.SetLoadListener(new SplashAdLoadListener(this)); -_tapSplashAd.Load(); -// Splash 加载接口说明 -public interface ISplashAdLoadListener : ICommonLoadListener -{ - // 当 Splash 加载完毕 - void OnSplashAdLoad(TapSplashAd ad); -} -// 通用加载接口说明 -public interface ICommonLoadListener -{ - // 加载出错回调 - void OnError(int code, string message); -} -``` - - - -<> - -```java -TapAdNative tapAdNative = TapAdManager.get().createAdNative(activity); -// 广告后台获取广告位 id -int spaceId = "your_space_id"; -tapAdNative.loadSplashAd(new AdRequest.Builder() - .withSpaceId(spaceId) - .build(), new TapAdNative.SplashAdListener() { - - @Override - public void onError(int code, String message) { - // 获取失败 - } - - @Override - public void onSplashAdLoad(TapSplashAd splashAd) { - // 获取成功,可以在这时候播放广告 - - } -}); -``` - - - - - -### 播放 - - - -<> - -```cs -using TapTap.TapAd -if (TapAdSdk.IsInited == false) -{ - Debug.Log("TapAd 需要先初始化!"); - return; -} -if (_tapSplashAd != null) -{ - // highlight-start - // SplashInteractionListener 为实现了 ISplashAdInteractionListener 的类 - // highlight-end - _tapSplashAd.SetInteractionListener(new SplashInteractionListener(this)); - _tapSplashAd.Show(); -} -else -{ - Debug.LogErrorFormat($"[Unity::AD] 未加载好视频,无法播放!"); -} -// Splash 播放回调接口说明 -public interface ISplashAdInteractionListener : ICommonInteractionListener -{ - // 点击跳过 - void OnAdSkip(TapSplashAd ad); - - // 广告时间到 - void OnAdTimeOver(TapSplashAd ad); - - // 点击广告回调 3.16.3.30 新增 - void OnAdClick(); -} -// 通用播放回调接口说明 -public interface ICommonInteractionListener -{ - -} -``` - - - -<> - -```java -// 注册开屏广告交互事件监听 -splashAd.setSplashInteractionListener(new TapSplashAd.AdInteractionListener() { - @Override - public void onAdSkip() { - // 点击了跳过,可以进入游戏 - } - - @Override - public void onAdTimeOver() { - // 开屏时间到(开屏页已关闭),可以进入游戏 - } - - @Override - public void onAdClick() { - // 开屏广告点击了转化按钮,会跳转到落地页 3.16.3.30 新增 - } -}); -// 展示开屏页 -splashAd.show(this); -``` - - - - - -### 关闭开屏广告 -:::tip -注意 **3.16.3.23 版本开始 开屏广告移除了自动关闭逻辑,需要媒体手动关闭**。 -::: - - - -<> - -```cs -using TapTap.TapAd -// 场景 1 媒体直接关闭开屏广告 -if (_tapSplashAd != null) -{ - _tapSplashAd.Dispose(); -} - -// 场景 2 在收到开屏广告交互事件后选择关闭 -public void OnAdSkip(TapSplashAd ad) { - if (ad != null) ad.Dispose(); -} - -public void OnAdTimeOver(TapSplashAd ad) { - if (ad != null) ad.Dispose(); -} -... - -``` - - - -<> - -```java -// 场景 1 媒体展示开屏中直接关闭广告 -if (splashAd != null) -{ - splashAd.dispose(); - splashAd.destroyView(); -} - -// 场景 2 在收到开屏广告交互事件后选择关闭 -@Override -public void onAdSkip() { - if (splashAd != null) - { - splashAd.dispose(); - splashAd.destroyView(); - } -} - -@Override -public void onAdTimeOver() { - if (splashAd != null) - { - splashAd.dispose(); - splashAd.destroyView(); - } -} -``` - - - - - - -## 激励视频广告 - -激励视频是一种全屏播放的视频广告,用户可以在观看完整的视频后获取奖励,视频广告播放结束后会显示结束页面,引导用户进行后续动作。目前激励视频广告的表现形式为:视频播放完展示 Endcard 页面。 - -使用场景包括但不限于: - -1. 游戏等应用内观看视频广告获得游戏内金币等。 -2. 积分类应用接入。 - -:::info 支持的广告尺寸 -- 全屏横屏(宽高比 16:9) -- 全屏竖屏(宽高比 9:16) -- Android 端暂不支持重力旋转。 -::: - -### 获取广告 - - - -<> - -```cs -using TapTap.TapAd -TapRewardVideoAd _tapRewardAd = null; -if (TapAdSdk.IsInited == false) -{ - Debug.Log("TapAd 需要先初始化!"); - return; -} -if (_tapRewardAd != null) -{ - _tapRewardAd.Dispose(); - _tapRewardAd = null; -} -int adId = YOUR_AD_ID; -// create AdRequest -var request = new TapAdRequest.Builder() - .SpaceId(adId) - .RewardName("your_reward_name") - .RewardCount(your_reward_count) - .Build(); -_tapRewardAd = new TapRewardVideoAd(request); -// highlight-start -// RewardVideoAdLoadListener 为实现了 IRewardVideoAdLoadListener 的类 -// highlight-end -_tapRewardAd.SetLoadListener(new RewardVideoAdLoadListener(this)); -_tapRewardAd.Load(); -// 激励视频广告加载回调接口说明 -public interface IRewardVideoAdLoadListener : ICommonLoadListener -{ - // 当激励视频被 Cache 完毕 - void OnRewardVideoAdCached(TapRewardVideoAd ad); - - // 当激励视频被 Load 完毕 - void OnRewardVideoAdLoad(TapRewardVideoAd ad); -} -// 通用广告加载回调接口说明 -public interface ICommonLoadListener -{ - // 加载出错回调 - void OnError(int code, string message); -} -``` - - - -<> - -```java -// 注意,一个 Activity 中只需要创建一个 TapAdNative 对象 -TapAdNative tapAdNative = TapAdManager.get().createAdNative(activity); - -AdRequest adRequest = new AdRequest.Builder() - .withSpaceId("your_space_id") // 广告后台获取广告位id - .withRewordName("your_reward_name") // 奖品名称 - .withRewardAmount("your_reward_amount") // 奖品数量 - .withExtra1("your_extra_info") // 游戏如果要通过 s2s 验证激励广告有效性的时候需要传入一些辅助信息来验证本次激励活动是否有效 - .withUserId("game_user_id") // 游戏如果要通过 s2s 验证激励广告有效性的时候需要传入游戏中的玩家 id,来验证是否可以对当前用户发放奖励 - .build(); - -tapAdNative.loadRewardVideoAd(adRequest, new TapAdNative.RewardVideoAdListener() { - @Override - public void onError(int code, String message) { - // 获取失败 - } - - @Override - public void onRewardVideoAdLoad(TapRewardVideoAd rewardVideoAd) { - // 获取广告成功,可以展示广告 - } - - @Override - public void onRewardVideoCached(TapRewardVideoAd rewardVideoAd) { - // 获取广告素材成功,可以展示广告,如果没有选择在 RewardVideoAdLoad 时展示广告。(更建议在这个回调中展示广告,体验更好) - } -}); -``` - - - - - -### 播放 - - - -<> - -```cs -using TapTap.TapAd -if (TapAdSdk.IsInited == false) -{ - Debug.Log("TapAd 需要先初始化!"); - return; -} -if (_tapRewardAd != null) -{ - // highlight-start - // RewardVideoInteractionListener 为实现了 IRewardVideoInteractionListener 的类 - // highlight-end - _tapRewardAd.SetInteractionListener(new RewardVideoInteractionListener(this)); - _tapRewardAd.Show(); -} -else -{ - Debug.LogErrorFormat($"[Unity::AD] 未加载好视频,无法播放!"); -} -// 激励视频广告播放回调接口说明 -public interface IRewardVideoInteractionListener : ICommonInteractionListener -{ - // 当广告出现 - void OnAdShow(TapRewardVideoAd ad); - - // 当广告关闭 - void OnAdClose(TapRewardVideoAd ad); - - // 当视频完成 - void OnVideoComplete(TapRewardVideoAd ad); - - // 视频出错 - void OnVideoError(TapRewardVideoAd ad); - - // 视频播放完毕,奖励确认可以发放 - void OnRewardVerify(TapRewardVideoAd ad, bool rewardVerify, int rewardAmount, string rewardName, int code, string msg); - - // 当跳过视频 - void OnSkippedVideo(TapRewardVideoAd ad); - - // 点击事件 - void OnAdClick(TapRewardVideoAd ad); -} -// 通用广告播放回调接口说明 -public interface ICommonInteractionListener -{ - -} -``` - - - -<> - -```java -// 注册激励广告交互事件监听 -rewardAd.setRewardAdInteractionListener(new TapRewardVideoAd.RewardAdInteractionListener() { - @Override - public void onAdShow() { - // 激励广告已显示 - } - - @Override - public void onAdClose() { - // 激励广告已经关闭 - } - - @Override - public void onVideoComplete() { - // 视频播放结束 - } - - @Override - public void onVideoError() { - // 视频出错 - } - - @Override - public void onRewardVerify(boolean rewardVerify, int rewardAmount, String rewardName, int code, String msg) { - // 激励任务已完成,游戏可以选择在此时进行玩家奖励,或者进一步通过 s2s 的流程来确认是否可以对当前玩家发放奖励 - } - - @Override - public void onSkippedVideo() { - // 激励广告中玩家点击了跳过视频的按钮 - } - - @Override - public void onAdClick() { - // 激励广告点击事件 - } -}); -rewardAd.showRewardVideoAd(activity); -``` - - - - - -## Banner 广告 - -模版渲染 Banner:开发者不用自行对广告样式进行编辑和渲染,可直接调用相关接口进行广告展示。 - -:::tip -不支持开发者在 view 添加按钮及对广告拦截处理。 -::: - -### 获取广告 - - - -<> - -```cs -using TapTap.TapAd - -TapBannerAd _tapBannerAd = null; -if (TapAdSdk.IsInited == false) -{ - Debug.Log("TapAd 需要先初始化!"); - return; -} -if (_tapBannerAd != null) -{ - _tapBannerAd.Dispose(); - _tapBannerAd = null; -} -int adId = YOUR_AD_ID; -// create AdRequest -var request = new TapAdRequest.Builder() - .SpaceId(adId) - .Build(); -_tapBannerAd = new TapBannerAd(request); -// highlight-start -// BannerAdLoadListener 为实现了 IBannerAdLoadListener 的类 -// highlight-end -_tapBannerAd.SetLoadListener(new BannerAdLoadListener(this)); -_tapBannerAd.Load(); -// banner 广告加载回调接口说明 -public interface IBannerAdLoadListener : ICommonLoadListener -{ - // 当 Banner 加载完毕 - void OnBannerAdLoad(TapBannerAd ad); -} -// 通用广告加载回调接口说明 -public interface ICommonLoadListener -{ - // 加载出错回调 - void OnError(int code, string message); -} -``` - - - -<> - -```java -// 注意,一个 Activity 中只需要创建一个 TapAdNative 对象 -TapAdNative tapAdNative = TapAdManager.get().createAdNative(activity); -// 广告后台获取广告位 id -int spaceId = "your_space_id"; -tapAdNative.loadBannerAd(new AdRequest.Builder() - .withSpaceId(spaceId) - .build(), new TapAdNative.BannerAdListener() { - - @Override - public void onError(int code, String message) { - // 获取广告失败 - } - - @Override - public void onBannerAdLoad(TapBannerAd bannerAd) { - // 获取广告成功,可以调用播放广告接口进行播放(**注意** banner 广告的获取和播放需要在同一个 activity 中) - } -}); -``` - - - - - -### 播放 - - - -<> - -```cs -using TapTap.TapAd -if (TapAdSdk.IsInited == false) -{ - Debug.Log("TapAd 需要先初始化!"); - return; -} -// highlight-start -// BannerInteractionListener 为实现了 IBannerAdInteractionListener 的类 -// highlight-end -if (_tapBannerAd != null) -{ - _tapBannerAd.SetInteractionListener(new BannerInteractionListener(this)); - _tapBannerAd.Show(); -} -else -{ - Debug.LogErrorFormat($"[Unity::AD] 未加载好视频,无法播放!"); -} -// banner 广告播放回调接口说明 -public interface IBannerAdInteractionListener : ICommonInteractionListener -{ - // 当广告曝光 - void OnAdShow(TapBannerAd ad); - - // 当广告关闭 - void OnAdClose(TapBannerAd ad); - - // 当点击广告 - void OnAdClick(TapBannerAd ad); - - // 当点击下载 - void OnDownloadClick(TapBannerAd ad); -} -// 通用广告播放回调接口说明 -public interface ICommonInteractionListener -{ - -} -``` - - - -<> - -```java -bannerAd.setBannerInteractionListener(new TapBannerAd.BannerInteractionListener() { - - @Override - public void onAdShow() { - // 广告已曝光 - } - - @Override - public void onAdClose() { - // 广告已关闭(玩家可以手动点击关闭 banner 广告) - } - - @Override - public void onAdClick() { - // 玩家点击了 banner 广告 - } - - @Override - public void onDownloadClick() { - // 玩家点击了下载按钮 - } -}); - -FrameLayout parentLayout = "your_layout"; -parentLayout.addView(bannerAd.getBannerView()); -``` - - - - - -## 自渲染信息流广告 - -:::tip -- **TapADN SDK 从 3.16.3.28 版本开始自渲染信息流视频广告对外暴露视频控制接口。** -::: - -为可自定义布局的信息流广告,包含大图和视频两种基本样式类型,应用下载、跳转到落地页和跳转到浏览器三种交互类型。 - -:::tip -支持的广告尺寸: -- 大图(宽高比:1.78 的图片) -- 视频(宽高比:1.78 的视频) -::: - -### 获取广告 - - - -<> - -```cs -// 暂不支持 -``` - - - -<> - -```java -// 注意,一个 Activity/Fragment 中只需要创建一个 TapAdNative 对象 -TapAdNative tapAdNative = TapAdManager.get().createAdNative(activity); -// 广告后台获取广告位 id -int spaceId = "your_space_id"; -tapAdNative.loadFeedAd(new AdRequest.Builder() - .withQuery("{query}") // 搜索词,可选 - .withSpaceId(spaceId) - .build(), new TapAdNative.FeedAdListener() { - @Override - public void onError(int code, String message) { - // 获取广告失败 - } - - @Override - public void onFeedAdLoad(TapFeedAd feedAd) { - - } -}); - -// TapFeedAd 接口说明 -public interface TapFeedAd { - - /** - * 广告标题 - * - * @return - */ - String getTitle(); - - /** - * 广告描述 - * - * @return - */ - String getDescription(); - - /** - * 广告图标 - * - * @return - */ - String getIconUrl(); - - /** - * 得到 Feed 广告图片模式 - * - * @return 1:大图 2:视频 - */ - int getImageMode(); - - /** - * 广告封面图片 Image list - * - * @return - */ - List getImageInfoList(); - - /** - * 获取 TapTap 得分 - * - * @return - */ - double getScore(); - - /** - * 如果是直接下载类型,返回 apk 包大小,否则返回 “0B” 或者 “”。 - */ - public String getApkSize(); - - /** - * 获取广告的 view,如视频广告的 view,当前版本为自动播放且默认打开声音,用户可以手动关闭声音。 - * - * @return - */ - View getAdView(); - - /** - * @return 0: onKnown 1: 下载类广告 2:直接跳转外部App 3: 通过浏览器打开落地页 - * 4: 小程序跳转类广告 3.16.3.30 新增 - */ - int getInteractionType(); - - /** - * 获取合规信息 - * - * @return - */ - ComplianceInfo getComplianceInfo(); - - /** - * 注册可点击的 view,click/show 会在内部完成 - * - * @param container 渲染广告最外层的 ViewGroup - * @param clickViews 可点击的视图 - * @param creativeViews 可点击的创意视图(广告下载的按钮) - * @param describeViews 可点击的介绍详情视图 - * @param privacyViews 可点击的隐私协议视图 - * @param permissionViews 可点击的权限视图 - * - */ - @Deprecated - void registerViewForInteraction(ViewGroup container, List clickViews, List creativeViews, - List describeViews, List privacyViews, List permissionViews, AdInteractionListener listener); - - /** - * 注册可点击的 view,click/show 会在内部完成 - * - * @param activity 渲染广告所在的 activity - * @param container 渲染广告最外层的 ViewGroup - * @param clickViews 可点击的视图 - * @param creativeViews 可点击的创意视图(广告下载的按钮) - * @param describeViews 可点击的介绍详情视图 - * @param privacyViews 可点击的隐私协议视图 - * @param permissionViews 可点击的权限视图 - * - */ - void registerViewForInteraction(Activity activity, ViewGroup container, List clickViews, List creativeViews, - List describeViews, List privacyViews, List permissionViews, AdInteractionListener listener); - - /** - * 设置下载监听器 - */ - void setDownloadListener(TapAppDownloadListener listener); - - /** - * 设置可以控制自渲染信息流视频,比如播放,暂停,音量键显隐等,v3.16.3.28可用 - */ - void setVideoPlaySelfController(boolean isSelfController); - - /** - * 设置自渲染信息流广告视频回调接口,v3.16.3.28可用 - */ - void setVideoAdListener(VideoAdListener videoAdListener); - - /** - * 暂停自渲染信息流广告视频播放,v3.16.3.28可用 - */ - void stopVideoPlay(); - - /** - * 恢复自渲染信息流广告视频自动播放,v3.16.3.28可用 - */ - void startVideoPlay(); - - /** - * 设置自渲染信息流广告音量键显隐,v3.16.3.28可用 - */ - void setVolumeVisible(boolean isVisible); - - /** - * 打开自渲染信息流广告视频音量(全局),v3.16.3.28可用 - */ - void openVideoVolume(); - - /** - * 关闭自渲染信息流广告视频音量(全局),v3.16.3.28可用 - */ - void closeVideoVolume(); -} - -// 应用合规信息 -public interface ComplianceInfo { - /** - * 获取应用名称 - * - * @return - */ - String getAppName(); - - /** - * 获取广告 - * - * @return - */ - String getAppVersion(); - - /** - * 开发者名称 - * - * @return - */ - String getDeveloperName(); - - /** - * 应用隐私协议地址 - * - * @return - */ - String getPrivacyUrl(); - - /** - * 应用描述地址 - * - * @return - */ - String getFunctionDescUrl(); - - /** - * 应用权限地址 - * - * @return - */ - String getPermissionUrl(); - -} - -public interface AdInteractionListener { - - // 广告卡片点击 - void onAdClicked(View view, TapFeedAd ad); - - // 广告创意交互部分点击 - void onAdCreativeClick(View view, TapFeedAd ad); - - // 广告卡片曝光 - void onAdShow(TapFeedAd ad); - - // 广告卡片单次(第一次)曝光 - void onDistinctAdShow(TapFeedAd ad); -} - -// 自渲染信息流广告视频回调接口 3.16.3.28 版本开始 -interface VideoAdListener { - void onVideoPrepared(TapFeedAd ad); - void onVideoStart(TapFeedAd ad); - void onVideoPause(TapFeedAd ad); - void onVideoVolumeOpen(TapFeedAd ad); - void onVideoVolumeClose(TapFeedAd ad); -} - -// 广告包下载进度监听 -public interface TapAppDownloadListener { - - void onDownloadStart(); - - void onDownloadComplete(); - - void onUpdateDownloadProgress(int percent); - - void onDownloadError(); - - void onInstalled(); -} - -``` - - - - - -### 广告行为监听 - -:::info -AdInteractionListener 涉及到广告计费,必须正确调用,convertView 必须使用 ViewGroup。 -::: - -在加载到信息流广告后,接入方需要注册在信息流广告中可以点击的 View,即 `TapFeedAd.registerViewForInteraction()` 方法,以实现广告的功能交互及计。包含图文点击区域的注册,附加创意按钮点击区域的注册,隐私协议区域的注册,应用权限的注册。点击附加创意区域会进行应用下载操作。 - -注意: 如果需要点击图文区域也能进行下载,请将图文区域的 view 传入 creativeViews。示例代码如下: - - - -<> - -```cs -// 暂不支持 -``` - - - -<> - -```java -public class FeedAdapter extends RecyclerView.Adapter { - ... - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - ... - if (holder instanceof OtherViewHolder) { - ... - } else if (holder instanceof adViewHolder) { - /** - * 注册可点击的 View,click/show 会在内部完成 - * @param container 渲染广告最外层的 ViewGroup - * @param clickViews 可点击的 View 的列表 - * @param creativeViews 用于下载的 View - * @param - */ - // 可以被点击的 view, 也可以把 convertView 放进来意味整个 item 可被点击,点击会跳转到落地页 - List clickViewList = new ArrayList(); - clickViewList.add(convertView); - // 创意点击区域的 view 点击根据不同的创意进行下载动作 - // 如果需要点击图文区域也能进行下载动作,请将图文区域的 view 传入 creativeViewList - List clickViewList = Collections.singletonList(adViewHolder.adImageView); - List creativeViewList = Collections.singletonList(adViewHolder.creativeTextView); - List privacyViewList = Collections.singletonList(adViewHolder.privacyTextView); - List describeTextView = Collections.singletonList(adViewHolder.describeTextView); - List permissionViewList = Collections.singletonList(adViewHolder.permissionTextView); - - // 注册普通点击区域,创意点击区域。重要! 这个涉及到广告计费及交互,必须正确调用。convertView 必须使用 ViewGroup。 - ad.registerViewForInteraction((ViewGroup) convertView, images, clickViewList, creativeViewList, describeViewList, privacyViewList, permissionViewList, new TapFeedAd.AdInteractionListener() { - - // 点击普通区域的回调 - @Override - public void onAdClicked(View view, TapFeedAd ad) { - - } - - // 点击创意区域的回调 - @Override - public void onAdCreativeClick(View view, TapFeedAd ad) { - - } - - // 广告曝光展示的回调 - @Override - public void onAdShow(TapFeedAd ad) { - - } - }); - } - } -} - -``` - - - - - -### 广告下载事件监听 - - - -<> - -```cs -// 暂不支持 -``` - - - -<> - -```java -TapAppDownloadListener downloadListener = new TapAppDownloadListener() { - - // 未开始下载 - @Override - public void onIdle() { - } - - // 下载中 - @Override - public void onDownloadStart() { - } - - // 下载完成 - @Override - public void onDownloadComplete() { - } - - /** - * 下载进度更新回调 - * @param percent 取值范围[0,100] 当前下载进度 - */ - @Override - public void onUpdateDownloadProgress(int percent) {; - } - - // 下载出错 (网络/文件存储错) - @Override - public void onDownloadError() { - } - - // 安装成功 - @Override - public void onInstalled() { - } -}; - -``` - - - - - -### Feed 流广告启动/暂停 - -:::info -如果 Feed 流是在 Activity 中使用,需要在 Activity 的 onResume 和 onPause 调用相关方法; - -如果 Feed 流是在Fragment中使用,除了要考虑 onResume 和 onPause 的系统回调,还要注意 Fragment 的 setUserVisibleHint 回调。 -::: - - - -<> - -```cs -// 暂不支持 -``` - - - -<> - -```java -// 启动 -tapAdNative.resume(); - -// 暂停 -tapAdNative.pause(); -``` - - - - - -### Feed 流广告销毁 - -:::info -如果是在 Activity 中创建,请在 Activity 中调用销毁方法 - -如果是在 Fragment 中创建,请在 Fragment 中调用销毁方法 -::: - - - -<> - -```cs -// 暂不支持 -``` - - - -<> - -```java -tapAdNative.dispose(); -``` - - - - - -## 模板染信息流广告 - -支持图文和视频样式,开发者不用自行对广告样式进行编辑和渲染,可直接调用相关接口获取广告view去进行展示, 开发者可以随时对 TapADN 平台上勾选的模板进行样式上的调整。降低了接入成本的前提下,不会影响cpm的水平,仍然可以保持高的竞争力。 - -:::info -支持的广告样式:开发者在 TapADN 平台上可以进行多模板的勾选。模板渲染信息流广告支持开发者调整、编辑。**注意**:不允许开发者在view添加按钮及对广告拦截处理。 -::: - -### 获取广告 - - - -<> - -```cs -// 暂不支持 -``` - - - -<> - -```java -TapAdNative tapAdNative = TapAdManager.get().createAdNative(activity); -// 广告后台获取广告位 id -int spaceId = "your_space_id"; -tapAdNative.loadFeedAd(new AdRequest.Builder() - .withQuery("{query}") // 搜索词,可选 - .withSpaceId(spaceId) - .build(), new TapAdNative.FeedAdListener() { - @Override - public void onError(int code, String message) { - // 获取广告失败 - } - - @Override - public void onFeedAdLoad(TapFeedAd feedAd) { - - } -}); - -// 模板渲染信息流 接口说明 -/** - * 注册模板信息流广告的交互监听 - * - */ - void setExpressRenderListener(ExpressRenderListener listener); - -/** - * 开始绘制广告 View。**注意** 请确保已经调用了`TapFeedAd.setExpressRenderListener`接口,否则无法收到广告渲染的结果。 - * @param activity 需要展现广告的 activity - * @param feedOption 模版卡片的参数。包括卡片的宽、高,以及视频配置 - */ -void render(Activity activity, FeedOption feedOption); - - -// 模板渲染信息流广告视频回调接口 -interface VideoAdListener { - void onVideoPrepared(TapFeedAd ad); - void onVideoStart(TapFeedAd ad); - void onVideoPause(TapFeedAd ad); - void onVideoVolumeOpen(TapFeedAd ad); - void onVideoVolumeClose(TapFeedAd ad); -} - -// 模板渲染广告交互接口 -interface ExpressRenderListener { - void onRenderSuccess(TapFeedAdView adView); - void onRenderFail(TapFeedAdView adView, TapFeedAd feedAd, int errorCode, String msg); - void onAdShow(TapFeedAdView adView); - void onAdClicked(TapFeedAdView view); - void onAdClosed(TapFeedAdView view); -} -``` - - - - - -### 广告行为监听 -在加载到模板渲染信息流广告后,接入方需要调用 `TapFeedAd.render()` 接口进行模版广告的绘制 同时注册广告交互接口 `TapFeedAd.setExpressRenderListener()` 方法,以实现广告渲染以及其它广告行为的监听。 - - - -<> - -```cs -// 暂不支持 -``` - - - -<> - -```java -public class FeedAdapter extends RecyclerView.Adapter { - ... - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - ... - if (holder instanceof OtherViewHolder) { - ... - } else if (holder instanceof adViewHolder) { - tapFeedAd.setExpressRenderListener(new TapFeedAd.ExpressRenderListener() { - @Override - public void onRenderSuccess(TapFeedAdView adView) { - adViewHolder.itemView.removeAllViews(); - adViewHolder.itemView.addView(adView); - } - - @Override - public void onRenderFail(TapFeedAdView adView, TapFeedAd feedAd, int errorCode, String msg) { - - } - - @Override - public void onAdShow(TapFeedAdView adView) { - - } - - @Override - public void onAdClicked(TapFeedAdView view) { - - } - - @Override - public void onAdClosed(TapFeedAdView view) { - - } - }); - - // 模板广告请求方法需要开发者设置 expressWidth、expressHeight。 参数单位是 px, 高度设置`ViewGroup.LayoutParams.WRAP_CONTENT`, 宽度根据开发者实际需求传入(宽度过小会影响广告体验) - // videoOption.AutoPlayPolicy 分为 VideoOption.AutoPlayPolicy.ALWAYS(视频永远自动播放) / VideoOption.AutoPlayPolicy.WIFI(视频仅在 WIFI 情况下自动播放) - FeedOption feedOption = new FeedOption.Builder() - .expressWidth(ViewGroup.LayoutParams.MATCH_PARENT) - .expressHeight(ViewGroup.LayoutParams.WRAP_CONTENT) - .videoOption(new VideoOption.Builder().autoPlayPolicy(VideoOption.AutoPlayPolicy.ALWAYS).build()) - .build(); - tapFeedAd.render(ExpressFeedViewActivity.this, feedOption); - } - } -} -``` - - - - - - -## 插屏广告 - -插屏广告:支持「全屏视频广告」、「全屏图文广告」和「半屏图文广告」等形式,具体广告展示形式在后台配置。 - -:::tip - 插屏广告分横竖屏形式,强烈建议在后台配置后正确使用横屏或竖屏广告,若用错配置的 orientation,可能会出现 UI 兼容异常问题。 -::: - -### 获取广告 - - - -<> - -```cs -if (TapAdSdk.IsInited == false){ - ShowText("TapAd 没有初始化!将自动初始化"); - Init(); - while (TapAdSdk.IsInited == false){ - await Task.Yield(); - } - ShowText("TapAd 初始化完毕"); -} -if (_tapInterstitialAd != null){ - _tapInterstitialAd.Dispose(); - _tapInterstitialAd = null; -} - -int adId = isHorizontal ? LANDSCAPE_INTERSTITIAL_ID : PORTRAIT_INTERSTITIAL_ID; -// create AdRequest -var request = new TapAdRequest.Builder() - .SpaceId(adId) - .Build(); -_tapInterstitialAd = new TapInterstitialAd(request); -_tapInterstitialAd.SetLoadListener(new InterstitialAdLoadListener(this)); -_tapInterstitialAd.Load(); -``` - - -<> - -```java -AdRequest adRequest = new AdRequest.Builder() - .withSpaceId(adId) - .build(); -tapAdNative.loadInterstitialAd(adRequest,new TapAdNative.InterstitialAdListener() { - @Override - public void onInterstitialAdLoad(TapInterstitialAd interstitialAd) { - // 获取广告成功 - } - - @Override - public void onError(int code, String message) { - // 获取广告失败 - } -}); -``` - - - - -### 播放 - - - -<> - -```cs -if (TapAdSdk.IsInited == false) -{ - ShowText("TapAd 需要先初始化!"); - return; -} -if (_tapInterstitialAd != null) -{ - _tapInterstitialAd.SetInteractionListener(new InterstitialAdInteractionListener(this)); - _tapInterstitialAd.Show(); -} -else -{ - Debug.LogErrorFormat($"[Unity::AD] 未加载好视频,无法播放!"); -} -``` - - -<> - -```java -interstitialAd.setInteractionListener(new TapInterstitialAd.InterstitialAdInteractionListener() { - - @Override - public void onAdShow() { - Log.d(TAG,"onShow"); - } - - @Override - public void onAdClose() { - Log.d(TAG,"onAdClose"); - } - - @Override - public void onAdError() { - Log.d(TAG,"onAdError"); - } -}); -interstitialAd.show(activity); -``` - - - - - -## Android 代码混淆 - -为了保证 SDK 在代码混淆后能正常运作,需要将如下防混淆代码添加到混淆文件中: - -:::tip -Android 原生项目混淆文件位于 `app/proguard-rules.pro`,Unity 项目需要先通过 `Project Settings -> Player -> Publishing Settings -> 勾选 Custom Proguard File`, 然后混淆文件位于项目该路径下 `Assets/Plugins/Android/proguard-user.txt` -::: -``` --dontwarn com.tapadn.** --keep class com.tapadn.** { *;} --dontshrink -``` - -## 激励视频奖励发放说明 - -### 奖励发放条件 - -一般视频总时长为 10~60s,30s 以上的视频播放到 30s 时会显示“跳过”按钮。**低于30s的激励视频观看达到 90% 即会回调奖励验证接口,高于 30s 激励视频观看到第 27s 的时候即会回调奖励验证接口。** - -### 奖励发放主要流程 - -- **客户端回调的优势:**对接简单、高效,通过接口回调结果在客户端完成奖励是否发放即可。 -- **服务端回调的优势:**开发者可在服务端进行二次校验,保证广告有效性后做出奖励发放,反作弊能力强。 - -![](/img/tap-ad/tap_adn_uml.jpg) - -### 奖励验证服务端回调 (S2S 验证) - -:::tip -S2S 验证需要开发者提供奖励发放的**回调 URL,Security Key** 由 TapADN 服务端生成并提供给开发者,现阶段通过 KA 单点对接提供 - -::: - -- **服务器回调模式不是必须的**,只是增加了一次第三方服务器的验证判断。具体的奖励发放由客户端完成。 -- 服务端回调逻辑:TapADN 根据“奖励发放条件“,先通过 “TapADN 服务端”访问“开发者媒体服务端”向开发者确认是否进行奖励发放,再依据“开发者服务端”返回的 true/false,在客户端给出是/否发放奖励的回调。 -- TapADN 服务端只是透传验证请求,不会在中间过程添加校验逻辑。为了保障开发者利益和用户体验,开发者可以在验证环节增加自己的校验逻辑。 - -#### SDK 奖励发放回调接口 - -##### 接口说明 - -```java -//视频播放完成后,奖励验证回调,rewardVerify:是否有效,code:错误码,msg:错误信息 -void onRewardVerify(boolean rewardVerify, int rewardAmount, String rewardName,int code,String msg) -``` - -##### 透传参数 - -SDK 传给 TapADN 服务端 - -| 字段名称 | 字段定义 | 字段类型 | 备注 | -|------|-------|------|------|-------| -| user_id | 用户id | string | 调用SDK透传,应用对用户的唯一标识 | -| extra | 其他信息 | string | 调用SDK传入并透传,如无需要则为空 | - -#### 奖励验证方式 - -1. 同步验证 - 1. 满足“奖励发放条件” - 2. 调用“奖励验证回调接口”,并等待返回 - 3. 调用onRewardVerify - 1. rewardVerify=回调接口返回的isValid - 2. code=回调接口异常时的错误码 - 3. msg=回调接口异常时的错误信息 - 4. 开发者可根据rewardVerify决定是否发放奖励 -2. 异步验证 - 1. 满足“奖励发放条件” - 2. 调用“奖励验证回调接口”,不等待返回 - 3. 调用onRewardVerify - 1. rewardVerify=true(满足“奖励发放条件”) - 2. trans_id - 4. 开发者可通过trans_id查询服务端结果,决定是否发放奖励 - -#### 奖励验证回调接口 - -##### 接口请求方式 - -- 接口请求方式: GET -- 服务端会以 GET 方式请求第三方服务的回调链接,并拼接以下参数回传: - -``` -pid=%s&user_id=%s&trans_id=%s&extra=%s&sign=%s -``` - -##### 请求参数定义 - -| 字段名称 | 字段定义 | 字段类型 | 备注 | -|--------|-------|-------|--------| -| pid | 广告位 ID | string | | -| user_id | 用户 ID | string | 调用 SDK 透传,应用对用户的唯一标识 | -| trans_id | 交易 ID | string | 完成观看的唯一交易 ID,唯一标识该次激励广告 | -| extra | 其他信息 | string | 调用 SDK 传入并透传,如无需要则为空 | -| sign | 签名 | string | 签名 | - -##### 签名生成方式 - -- 获得 SecurityKey,在申请配置激励广告位页面,设置服务端验证回调接口时获取 -- 将 trans_id 和 SecurityKey 用 ':' 符号连接 -- 使用 sha256 生成签名,用小写 16 进制 - -``` -sign = fmt.Sprintf("%x", sha256.Sum256(trans_id:SecurityKey)) -``` - -##### 返回值约定 - -- 返回值格式: json -- 返回示例: - -```json -{"isValid":true} -``` - -##### 返回值定义 - -|字段名称 |字段定义 |字段类型|备注| -|---|-----|-----|-----| -|isValid |校验结果 | bool|判定结果,是否发放奖励 | - - - - -## SDK 常见错误码汇总 - -| 错误码 | 错误信息 | 原因 | -|---|--------|-------------| -|**3001**| 初始化错误 | SDK 未初始化,或者初始化异常 | -|**3002**| 网络异常错误 | | | -|**3003**| 插屏广告应用横竖方向参数与广告位支持方向不匹配| 插屏广告设置的横竖屏与设置的方向不一致| -|**9999**| 未获取到广告素材:未知错误 | | -|**9000**| 内部错误 | 错误信息和内部错误码一般会上报 | -|**10003**| Request MediaInfo Miss | | -|**10004**| Request AppInfo Miss | | -|**10005**| Request AdnSdkInfo Miss | | -|**10007**| Request AdSpace Miss | | -|**10008**| Request AdSpace Not Match| | -|**10009**| Request Invalid DeviceId | | - - diff --git a/docs/sdk/tap-canary/_category_.json b/docs/sdk/tap-canary/_category_.json deleted file mode 100644 index 377983e41..000000000 --- a/docs/sdk/tap-canary/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapCanary", - "collapsed": true, - "position": 19 -} diff --git a/docs/sdk/tap-canary/features.mdx b/docs/sdk/tap-canary/features.mdx deleted file mode 100644 index 4837a0126..000000000 --- a/docs/sdk/tap-canary/features.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: TapCanary 功能介绍 -sidebar_label: 功能介绍 ---- - -## 产品简介 - -通过 TapCanary 可以将游戏应用的早期版本,发布给内部测试人员或受信任的用户进行封闭式测试,从而收集技术或用户体验方面的宝贵意见与反馈,以便在游戏上架商店面对玩家之前对其进行改进。TapCanary 目前支持云玩模式和沙盒模式,解决包体泄露外流的困扰,保证包体的安全性。 - -### 适用场景: - -1. 开发者将 beta 版本的游戏,开放给**指定用户**进行测试; -2. 开发者在其公司内部做小范围封闭测试; -3. 创建多条测试计划,对游戏内容做 A/B 测试。 - -## 开始使用 - -### 创建测试计划 - -在 TapCanary [控制台](https://canary.developer.taptap.cn/dashboard/plan-list),创建一个测试计划,按照步骤提示上传你的应用(`.apk`),并填写相关的测试信息。 - -![](/img/canary/create-plan.png) - -### 添加指定测试用户 - -你可以在第一步创建测试计划时就添加用户,也可以通过再次编辑测试计划来补充或修改测试用户。如测试计划已经开启,被添加的测试账号在 TapCanary APP 中,可以看见并访问应用。 - -每个测试计划至多支持 100 个测试用户。 - -![](/img/canary/manage-participants.jpg) - -#### 单次少量添加 - -你可以选择单次少量添加用户,在编辑模式下,找到 **测试人员 > 手动添加**,输入用户的 TapID。 - -#### 批量添加 - -当测试人员较多时,可以选择通过 CSV 导入的方式批量添加。请注意,CSV 文件中,TapID 每行一个按要求填入。 - -### 开启测试计划 - -在合适的时间,点击**开启**测试计划,测试用户可在 TapCanary APP 访问应用。已开启的测试计划不能切换测试模式,如不支持从云玩模式切换到沙盒模式,反之亦然。 - -![](/img/canary/start.png) - -## 测试应用 - -被开启的测试计划会在 TapCanary APP 中展示并访问。前往 TapTap 商店直接[下载 TapCanary APP](https://www.taptap.cn/app/222711),或前往 TapCanary 官网[扫码下载](https://canary.developer.taptap.cn/)。 - -## 更新应用版本 - -你可以在测试计划的编辑模式中,上传应用的新版 APK。提交成功后,TapCanary APP 中会立即自动获取到最新的应用。请注意填写新版本的更新日志,让测试用户知晓应用的新特性。 - -![](/img/canary/edit-plan.png) - -## 多模式测试 - -一个测试计划同时仅支持一种模式。如果你想开启不同模式的测试,或者想分发给不同的人群,可以创建多个测试计划来达到多模式测试的目的。 - -## 结束测试 - -测试计划会按照预先设置的测试时间自动结束测试。你也可以提前结束测试计划,在控制台中找到 **终止计划** 并点击执行。测试计划结束或终止后,应用将不可被再次下载、更新、访问。 - -![](/img/canary/end.png) diff --git a/docs/sdk/tap-friend/_category_.json b/docs/sdk/tap-friend/_category_.json deleted file mode 100644 index fd09068d2..000000000 --- a/docs/sdk/tap-friend/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapTap 好友", - "collapsed": true, - "position": 3.1 -} diff --git a/docs/sdk/tap-friend/features.mdx b/docs/sdk/tap-friend/features.mdx deleted file mode 100644 index 1f8eff6b3..000000000 --- a/docs/sdk/tap-friend/features.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: TapTap 好友功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -## 产品简介 - -通过接入此功能,开发者可快速获取玩家当前在 TapTap 互关好友列表。 - -## 产品优势 - -游戏可依靠 TapTap 社交生态,快速引入社交关系至游戏内,完成社交冷启或用于丰富游戏的社交关系链,完成上线开黑、组队约玩等游戏社交体验。 - -## 接入流程 - -前往**「开发者中心」**→**「游戏服务」**→**「应用配置」**→开通**「TapTap 登录」**。 - -![](https://capacity-files.lcfile.com/5gSGnf3O8YIU4vjkzc0c59NBG4mlgui5/tap-friend-open-auth.png) - - -点击**「权限和功能」**→**「联系我们」**,填写功能,申请获取 TapTap 互关好友的接口和权限。 - -![](https://capacity-files.lcfile.com/Nt0DhKATg0lE282gnkh9SSKV8esgJ3cz/tap-friend-scope.png) - - -## 收费方案 - -- TapTap 平台好友服务当前免费,开通此接口需要提交工单申请。 - -## 支持版本 - -iOS / Android / Unity 最低支持版本 TapSDK 3.11.1 \ No newline at end of file diff --git a/docs/sdk/tap-friend/guide.mdx b/docs/sdk/tap-friend/guide.mdx deleted file mode 100644 index 9764d32dc..000000000 --- a/docs/sdk/tap-friend/guide.mdx +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: TapTap 好友指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import Languages from '../_partials/languages.mdx'; -import { Conditional } from "/src/docComponents/conditional"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -## 权限说明 - - - -<> - - - -<> -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下权限: - -``` - -``` - - - -<> - - - -<> - - - - - -## SDK 配置 - -可以在 [下载页](/tap-download) 获得 TapSDK,引入 TapTap 登录模块。 - - - -<> - -**Unity 开发环境要求**:Unity 2019.4 或更高版本 - -支持版本: - -- Android:最低支持 5.0 -- iOS:最低支持 iOS 11.0,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - - - -<> - - -{`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - implementation (name:'TapLogin_${sdkVersions.taptap.android}', ext:'aar') - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') -}`} - - - - -<> - - -{` -TapCommonResource.bundle -TapLoginResource.bundle -TapLoginSDK.framework -TapCommonSDK.framework -`} - - - - - - - - -## 获取好友列表 -调用前请确保登录时已申请好友权限,即使用 TapTap 登录接口时 `permissions` 参数添加 `user_friends`。 - - - -```cs -TapFriendResult result = await TapFriends.QueryMutualList(cursor, size); -if (result?.FriendList?.Count > 0) { - foreach (TapFriendInfo friendInfo in result.FriendList) { - Debug.Log($"{friendInfo.Name}, {friendInfo.OpenId}, {friendInfo.Avatar}"); - } -} -Debug.Log($"下一游标:{result.Cursor}"); -``` - -```java -TapFriends.queryMutualList(cursor, size, new TapFriendsCallback() { - @Override - public void onSuccess(TapFriendResult tapFriendResult) { - toast(" queryMutualList success = " + tapFriendResult); - } - - @Override - public void onFail(Throwable throwable) { - toast("queryMutualList fail error = " + throwable); - } -}); -``` - -```objectivec -TapFriendQueryOption *option = [TapFriendQueryOption new]; -option.size = 20; -option.cursor = nil; -[TapFriends queryMutualListWithOption:option - callback:^(TapFriendResult *_Nullable result, NSError *_Nullable error) { - if (error) { - NSLog(@"失败 error = %@",error); - } else { - NSMutableArray *userInfos = [NSMutableArray array]; - for (TapFriendInfo *friendInfo in result.data) { - [userInfos addObject:[NSString stringWithFormat:@"openid = %@ name = %@ avatar = %@", - friendInfo.openid, - friendInfo.name, - friendInfo.avatar]]; - } - NSLog(@"成功 result = %@ cursor = %@", userInfos, result.cursor); - } - }]; -``` - - - -上述参数中 `cursor` 为每页游标,第一页为 null,后续页游标从上一次返回结果中获取;`size` 为结果数量。 - -该接口返回结果 Result 包括好友信息集合和下一页游标 `cursor`,好友信息集合中每条信息包含的字段说明如下表: - -| 字段名 | 描述 | -| ---- | ---- | -| name | Tap 昵称 | -| avatar | Tap 头像 | -| openid | Tap 用户 openid | - -当游标 `cursor` 为 null 时,表示为最后一页数据。 diff --git a/docs/sdk/tap-play/_category_.json b/docs/sdk/tap-play/_category_.json deleted file mode 100644 index d1ad8b5b6..000000000 --- a/docs/sdk/tap-play/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapPlay", - "collapsed": true, - "position": 18 -} diff --git a/docs/sdk/tap-play/faq.mdx b/docs/sdk/tap-play/faq.mdx deleted file mode 100644 index a99565cba..000000000 --- a/docs/sdk/tap-play/faq.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 30 ---- - -## 常见问题 - -### TapPlay 内是否可以支持微信 / QQ 登录? - -- 目前 TapPlay 暂不支持微信、QQ 登录方式,推荐开发者接入 TapTap 登录 或使用手机验证码登录。 - -### TapPlay 内是否可正常微信 / 支付宝支付? - -- TapPlay 支持微信/支付宝以 JSAPI 方式进行接入,但是不支持纯 APP 方式接入。 - -### 如果关闭 TapPlay 功能, TapPlay 的用户还能更新游戏吗? - -- TapPlay 的玩家可能后续无法再更新包体,只能通过游戏内热更进行游戏更新。 - -### TapPlay 可以定向屏蔽一些机型和安卓版本吗? - -- 可以,请使用开发者[工单](https://developer.taptap.cn/user-center/create-ticket)联系运营自定义屏蔽。 - -### TapPlay 存档是否与本地互通?或卸载后本地使用? - -- TapPlay 的存档只能 TapPlay 使用,并不互通。 - -### TapPlay 测试不通过,怎么办? - -- 未接入防沉迷的游戏,若沙盒自动化检测也不通过,则会被卡住审核,收到审核驳回通知+系统检测报告通知。已接入防沉迷的游戏,不会卡住审核,只会收到系统检测通知,并且实际线上不会生效或暂时关闭沙盒。防沉迷不合规的游戏,若需求线上开测,只能全量进入 TapPlay。 - -### 没有 APK 但有小游戏能否创建游戏接入 TapPlay? - -- TapPlay 只支持以 APK 方式接入。 \ No newline at end of file diff --git a/docs/sdk/tap-play/features.mdx b/docs/sdk/tap-play/features.mdx deleted file mode 100644 index f0b2384e4..000000000 --- a/docs/sdk/tap-play/features.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: TapPlay 功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 10 ---- - -## TapPlay 是什么 - -TapPlay 是指在 TapTap 客户端内通过「沙箱环境」运行游戏,实现免安装、即点即玩的游戏体验。 - -## 产品线上形态 - -### 线上入口 - -![](https://capacity-files.lcfile.com/Yjsf2LVKJzHda12p1f4v8ODXkafhaO6A/image-20240321-080618.png) - -### 启动流程 -开始游戏后进入游戏加载状态,加载完毕即进入游戏,免去安装拦截等问题。 - -左侧悬浮窗提供「社区内容」、「性能监控」、「游戏添加到桌面」等信息与调节功能。 - -![](https://capacity-files.lcfile.com/Eeph91vhk2dqjINjnMWzDwDigUHcPXXF/image-20240321-081432%20%281%29.png) - -## 核心优势 - -### 提高激活,降低流失 - -- 无需开发游戏的防沉迷系统,无须修改代码,TapPlay 已集成了完整合规的**青少年防沉迷机制**。 -- 游戏首次启动时,无需重新进行繁琐的权限申请,玩家可快速进入游戏。 -- 提升游戏体验,玩家可直接利用在 TapTap 的实名信息进入游戏。 -- 开发者后台集成准确有效的数据监控工具,实时有效地监控线上游戏数据。 - -![](https://capacity-files.lcfile.com/IcU8V9AccWVYBA5B7p6XEcfUJLriyKuk/image-20240321-082704.png) - -### 免费兼容测试 - -![](https://capacity-files.lcfile.com/hqayARMo7LyutXadsg3gzqqPT5IHxsP4/image-20240321-082816.png) - -### 防抓包,反破解 - -- 玩家无法主动获取游戏的 APK 安装包,游戏文件安全系数高,不会被反编译,不会被恶意传播。 -- 接入 Themis,限制游戏仅可在 TapPlay 环境运行,防破解、极大保障包体安全性。 - -## 站内曝光增益 - -小游戏专区 / 热玩榜 / 联合投放等都将额外成为游戏分发的核心助力。 - -![](https://capacity-files.lcfile.com/cWK0uDadAhu5adPMx8efs0exGlnfOJST/image-20240321-083603.png) - -## TapPlay接入 - -游戏 APK 设置中勾选 TapPlay 模式授权即可,无需修改代码零成本接入。 - -TapPlay 游戏支持游戏内自定义引导创建桌面快捷方式,能够有效提升游戏次留,实现游玩-桌面图标二次启动的本地流程。 - -![](https://capacity-files.lcfile.com/JB8FkvT0xe7jfRtLYB6rxjb0ULldWRDw/image-20240321-084602.png) diff --git a/docs/sdk/tap-play/input.mdx b/docs/sdk/tap-play/input.mdx deleted file mode 100644 index f55a33e11..000000000 --- a/docs/sdk/tap-play/input.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: TapPlay 上架流程 -sidebar_label: 上架流程 -sidebar_position: 20 ---- - -## 通过 TapPlay 上架游戏的流程 - -![](/img/tap-play/flow-submission.png) - -## 上架 TapPlay 注意事项 - -### 必须上传 32/64 位 均兼容的 APK - -32 位 APK 运行在 TapPlay 环境内需用户在游戏下载过程中阻断型地额外安装 TapPlay 游戏助手,严重影响游戏转化率,且硬件联盟已明确指出,2023 年底,硬件将仅支持 64 位 APK,32 位应用无法在终端上运行。 - -### 可以使用什么登录方式 - -TapPlay 支持 [TapTap 登录](/sdk/taptap-login/features/)、手机号登录及游客登录,请确保您的游戏不支持微信、QQ 或其他第三方登录方式。 - -如对 TapTap 登录接入存疑,可提交工单咨询 TapTap 技术支持人员,我们将尽快给予响应。 - -### 内购游戏可以使用什么支付方式 - -1. 选择接入 TapTap 支付 SDK(即将上线),实践流程如下: - - ![](/img/tap-play/flow-pay.svg) - -2. 选择自行接入[微信官方 JSAPI](https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_1) 或 [支付宝官方 JSSDK](https://opendocs.alipay.com/open/203/105285) - -以上方式必选其一,同时需保证支付相关功能的接入完整度需符合国家规范。 - -### 游戏未做实名及青少年防沉迷怎么办 - -游戏通过 TapPlay 上架后,无需额外接入防沉迷 SDK 或游戏内实名认证。 - -### 加固方案的选用 - -推荐网易易盾 SDK,如选用其他加固方案,请通过工单告知我们。 - -![](/img/tap-play/ticket.png) - -### 更多常见问题 - -1. 目前 TapPlay 支持点击游戏桌面图标后通过 TapTap 启动游戏,但部分机型由于硬件限制 TapTap 无法帮助用户创建桌面图标,用户需启动 TapTap 后才可启动游戏 -2. 游戏与其他第三方 SDK 可能存在冲突,建议开发者优先接入 TDS 提供的[开发者服务](/)。 -3. 请保证 TapTap 商店内为游戏的最新资源包,通过 WebView 的方式实现更新可能导致游戏更新文件安装到本地,与 TapPlay 环境内文件发生冲突 -4. 为避免用户卸载游戏导致存档丢失,TapPlay 现已支持存档备份功能,详情请前往 DC 后台 - 商店 - TapPlay 查看 - -## 自测确保稳定性 - -提交 APK 前,建议使用 TapCanary 进行稳定性自测,以发现潜在的问题。 - -## 自测流程 - -请前往 [TapCanary 配置后台](https://canary.developer.taptap.cn/)创建测试计划 → 上传游戏安装包 → 配置测试计划详情及测试用户 → 下载 TapCanary 并登录 → 进行游戏自测 → Debug (如遇问题可通过工单咨询我们) - -- [自测用例](pathname:///files/tapplay-test-cases.xlsx) -- [测试报告模版](pathname:///files/tapplay-test-report-template.pdf) - -## Bug 分级标准 - -当你的游戏出现以下级别的 bug 时候,可能影响游戏的上架,请及时解决 - -### P1 bug( 严重) - -- 严重问题,关键的主流程错误或者无法忍受的性能问题。这个严重问题不会被较大部分玩家接受,通常导致影响游戏的所有或主要功能无法正常使用。严重问题没有解决前,游戏禁止上线。 - -例如: - -- 必现的主流程上(几乎所有玩家均受影响)的阻断性缺陷(操作无法响应、崩溃、ANR、黑白屏等) - -### P2 bug(重要) - -- 重要问题,影响玩家想要使用的功能;如果经过测试和产品等相关人评估风险后,可以有选择地上线。 - -例如: - -- 偶现的主流程阻断性缺陷(操作无响应、崩溃、ANR、黑白屏等) -- 必现的严重发热或卡顿 -- 在较大范围(例如所有 Android5 设备)的设备上必现阻断性缺陷 -- 非主流程复杂组合场景(小部分玩家受到影响)操作后必现的崩溃 -- UI 显示错误,但不影响游戏进程 -- 游戏无法通过免安装启动 -- 大部分机型游戏启动之后持续变卡顿,20 分钟以内导致无法正常进行游戏 - -### P3 bug(次要) - -- 次要问题,问题妨碍但不阻塞玩家想要使用的功能,不过这可能引起一些用户产生抱怨。用户可能忽略该问题或者找到一个代替的解决方法;如果经过测试和产品认可后可以正常上线。 - -例如: - -- 低端机型游戏启动之后持续变卡顿,20 分钟以内导致无法正常进行游戏 -- 游戏中的非全屏广告(横幅广告等)无法关闭 -- 游戏中分享不可用 -- 视频广告奖励逻辑错误(播放后无法获取奖励或未播放即可获得奖励) -- 广告中应用无法下载、安装或启动等 -- 偶现的严重的发热或卡顿 diff --git a/docs/sdk/tap-support/_category_.json b/docs/sdk/tap-support/_category_.json deleted file mode 100644 index 2dbf5a042..000000000 --- a/docs/sdk/tap-support/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "客服", - "collapsed": true, - "position": 12 -} diff --git a/docs/sdk/tap-support/features.mdx b/docs/sdk/tap-support/features.mdx deleted file mode 100644 index d36be3122..000000000 --- a/docs/sdk/tap-support/features.mdx +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Tap 客服功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -Tap 游戏客服能帮助游戏运营团队更快更好地解决玩家遇到的问题。 - -## 核心优势 - -### 丰富的工具提升客服效率 - -Tap 游戏客服由工单与知识库两大核心系统构成,提供了包括公告、常见问题、表单等一系列可自由组合的功能模块用来解决玩家的问题与诉求。 - -- 工单内建基于分类的客服自动分配功能,也可以通过自定义触发器实现更复杂的分配机制,将工单第一时间分配到最合适的客服手上。 -- 工单支持自定义字段,既可以通过表单由玩家填写,也可以由开发者主动上报,为客服提供详细的上下文。 -- 支持多种通知方式,保证你不错过任何一条玩家回复。 - -### 多维度的分析能力助力运营 - -Tap 游戏客服在帮助解决玩家问题的同时,也希望能协助运营发现问题、提炼沉淀有价值的内容、提升游戏本身。 - -- 工单拥有灵活的搜索与筛选能力,并支持按照分类、字段、状态聚合与分析。也支持数据导出,方便使用第三方工具进一步的分析。 -- 知识库文章提供了用户反馈的功能,可以在后台看到哪些内容是高效的,哪些是没有解决玩家问题的。 - -### 简单强大的管理功能 - -Tap 游戏客服提供了符合直觉的基于角色的权限控制系统,除了预置的角色也支持自定义角色满足更灵活的需求。 - -系统为管理者提供了详细的数据评估客服的工作数量与质量,包括但不限于处理工单的数量、回复玩家的及时程度、玩家的满意度等。 - -### 为玩家设计的用户体验 - -作为专注于游戏场景的客服,Tap 游戏客服提供了游戏内的客服体验。玩家使用客服功能时无需切换 app,在客服回复后能即时在游戏内收到提示。 - -此外,SDK 还为横屏场景进行了视觉与交互优化,向开发者提供了控制 API 降低 SDK 对游戏核心流程性能的影响。 - -### 开箱即用的开发体验 - -Tap 游戏客服的客户端 SDK 覆盖了常见的游戏开发平台,提供了包含用户界面、登录、上报数据、小红点维护等功能。高度集成的功能提供了开箱即用的开发体验。 - -客服支持多种登录方式,如果游戏已经在使用 TDS 内建账户功能可以直接使用 TDS 内建账户登录,也支持游戏自有的账户系统登录。 - -:::info 仍在开发中 -目前 Tap 客服正在公开测试阶段,部分开发中的功能可能未对所有厂商开放。 -::: diff --git a/docs/sdk/tap-support/features/_category_.json b/docs/sdk/tap-support/features/_category_.json deleted file mode 100644 index 79e50ccfd..000000000 --- a/docs/sdk/tap-support/features/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "功能指南", - "position": 2 -} diff --git a/docs/sdk/tap-support/features/roles.mdx b/docs/sdk/tap-support/features/roles.mdx deleted file mode 100644 index 9969c6ed4..000000000 --- a/docs/sdk/tap-support/features/roles.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Tap 客服的权限控制 -sidebar_label: 客服权限 -sidebar_position: 2 ---- - -客服模块拥有与开发者中心部分独立的权限系统。权限由角色与范围两部分组成。 - -- 角色:决定用户能使用哪些功能,进行哪些操作; -- 范围:决定用户能访问哪些工单。 - -### 角色 - -按照不同的职责,用户角色分成以下几种: - -| 用户角色 | 职责 | 访问路径 | 计费 | -| ---------- | :----------------------------------------------------------------------- | ---------- | ------ | -| 厂商管理员 | 维护服务与安全相关配置 | 开发者中心 | 不计费 | -| 管理员 | 日常管理
    拥有除了服务与安全相关配置之外的所有配置权限 | 客服工作台 | 计费 | -| 客服 | 直接与玩家沟通,处理玩家诉求 | 客服工作台 | 计费 | - -厂商管理员是在开发者中心拥有客服权限的 Tap 用户。其他角色的用户是独立的客服系统用户,通过独立于开发者中心的客服工作台登录使用客服模块,我们统称他们为「成员」。成员可以由厂商管理员在开发者中心添加,也可以由管理员在客服工作台添加。一个成员可以拥有一种或多种用户角色,厂商管理员或管理员可以修改成员的角色。 - -详细的角色与权限对应关系如下: - -| | | 厂商管理员 | 管理员 | 客服 | 协作者 | 开发者 | -| ----------------- | ---------------------------------------------------------------------------- | :----------------------------: | :----------------------------: | :--: | :----: | :-------------------------: | -| 服务与安全 | 启用、关闭服务 | ✔️ | | | | | -| | 配置自定义域名 | ✔️ | | | | | -| 用户管理 | 查询成员列表与资料 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | -| | 管理成员
    添加、禁用、修改资料 | ✔️ | ✔️ | | | | -| | 查询玩家资料 | | ✔️ | ✔️ | ✔️ | ✔️ | -| | 管理玩家
    添加、修改资料 | | ✔️ | ✔️ | | | -| 工单 | 访问工单
    - 查询、查看工单
    - 提交内部留言
    | | ✔️ | ✔️ | ✔️ | ✔️ | -| | 修改工单属性 | | ✔️ | ✔️ | | | -| | 回复玩家 | | | ✔️ | | | -| 知识库 | 查看 | | ✔️ | ✔️ | ✔️ | ✔️ | -| | 管理
    添加、修改内容与发布状态 | | ✔️ | ✔️ | | | -| 设置 | 内容设置
    - 产品与分类
    - 工单字段、表单
    - 动态内容
    | | ✔️ | | | ✔️
    只读 | -| | 客服设置
    - 群组
    - 触发器
    - 通知渠道管理
    | | ✔️ | | | | -| | 开发设置
    - 玩家鉴权方式
    - 自定义域名(只读)
    | | ✔️ | | | ✔️ | -| 审计 | 审计日志 | ✔️
    厂商部分 | ✔️
    成员部分 | | | | - -### 范围 - -如果用户拥有「访问工单」的权限,其能访问的工单有如下三种不同的范围: - -- 所有工单 -- 仅限其所在组的工单 -- 仅限分配给该用户的工单 diff --git a/docs/sdk/tap-support/features/setup.mdx b/docs/sdk/tap-support/features/setup.mdx deleted file mode 100644 index bfcbc2ea3..000000000 --- a/docs/sdk/tap-support/features/setup.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: 开通 Tap 客服 -sidebar_label: 开通流程 -sidebar_position: 1 ---- - -要使用 Tap 客服,需要在 TapTap 开发者中心开通。 - -### 开通服务 - -游戏客服是厂商维度的服务,开发者可以在 **开发者中心厂商页面 > 游戏服务 > 游戏客服** 处自助开通服务。 - - - -:::info 权限要求 -访问开发者中心游戏客服模块需要厂商权限中的 **客服管理员** 权限。 -::: - -### 价格方案 - -客服系统按照激活的客服数量收费。 - -:::info -公测期间不计费,我们会在正式收费前提前发布通知。 -::: - -### 添加客服 - -启用客服服务后,首先需要添加客服。 - -你可以切换到 **客服管理** 标签页添加客服。添加客服需要提供客服的手机号,添加后客服会收到邀请短信。客服通过手机加短信验证码登录客服后台。 - -客服收到的短信中会带有客服工作台的链接,你也可以在开发者中心游戏客服模块右上角找到客服工作台的跳转入口。 - -添加客服时需要指定客服的角色与范围,不同的角色的职责与具体权限参见[《客服的权限控制》](/sdk/tap-support/features/roles/) 。 - -:::info 成为第一个客服吧 -厂商管理员在 DC 后台仅可以进行账户与安全相关的配置,具体的客服业务相关配置需要在客服工作台进行。要进行业务配置,你可以将自己添加为管理员。 -::: diff --git a/docs/sdk/tap-support/guide.mdx b/docs/sdk/tap-support/guide.mdx deleted file mode 100644 index d3e703855..000000000 --- a/docs/sdk/tap-support/guide.mdx +++ /dev/null @@ -1,950 +0,0 @@ ---- -title: Tap 客服开发指南 -sidebar_label: 开发指南 -sidebar_position: 3 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -本文介绍如何在游戏中接入 TDS 提供的客服系统。 - -## 权限说明 - - - -<> - - - -<> - -该模块需要权限如下: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 访问网络数据 | 用户首次使用该功能时会申请权限 | -| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | -| 读取存储权限 | 用于反馈工单时添加本地图片、视频附件 | 上传附件时申请 | - -该模块将在应用 `AndroidManifest.xml` 中添加如下权限: - -``` - - - - - -``` - - - -<> - - - -<> - - - - - - -## SDK 初始化 - -在 [下载页](/tap-download) 获得 TapSDK,引入 `TapSupport` 模块: - - -<> - - - - -<> - -TapSupport SDK 依赖 TapCommon 模块: - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar' - implementation (name:'TapSupport_${sdkVersions.taptap.android}', ext:'aar') -}`} - - -确认 `AndroidManifest.xml` 已添加网络权限: - -```java - -``` - - -<> - -TapSupport SDK 依赖 TapCommon 模块: - -```objc -TapCommonSDK.framework -TapSupportSDK.framework -``` - - - -<> - -TapSupport SDK 依赖 TapCommon 模块: - -```csharp -PublicDependencyModuleNames.AddRange( - new string[] - { - "TapCommon", - "TapSupport" - // ... add other public dependencies that you statically link with here ... - } - ); -``` - - - - - -在初始化 SDK 之前,有一些准备工作需要完成: - -1. 请厂商管理员或客服管理员邀请你成为客服管理员。 -2. 每个厂商会被分配一个专属域名,需要记下待用。你可以在客服工作台的 **设置 > 开发者信息** 中找到可用的域名。如果你想要使用自己的域名,可以联系厂商管理员在开发者中心游戏客服模块中绑定。 -3. 在客服工作台,为你的游戏创建一个产品(**设置 > 管理 > 产品与分类**),记下这个产品的 ID。 - -然后使用以下代码初始化 TapSupport 模块: - - - -```cs -using TapTap.Support; - -TapSupport.Init("https://please-replace-with-your-customized.domain.com", "产品 ID"); -``` - -```java -import com.tds.tapsupport.TapSupport; -import com.tds.tapsupport.TapSupportCallback; -import com.tds.tapsupport.TapSupportConfig; - -TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "产品 ID", new TapSupportCallback() { - @Override - public void onUnreadStatusChanged(boolean hasUnread) { - // 我们会在 §未读消息通知 讨论 callback 的用法 - } -}) -TapSupport.setConfig(this, config); -``` - -```objc -#import - -TapSupportConfig *config = [TapSupportConfig new]; -config.server = @"https://please-replace-with-your-customized.domain.com"; -config.productID = @"产品 ID"; -config.callback = self; -[TapSupport shareInstance].config = config; -``` - -```cpp -#include "TapUESupport.h" - -FTapSupportConfig Config; -Config.ServerUrl = TEXT("https://please-replace-with-your-customized.domain.com"); -Config.ProductID = TEXT("产品 ID"); -TapUESupport::Init(Config); -``` - - - -上述代码示例中,`please-replace-with-your-customized.domain.com` 就是准备工作 2 中获取或绑定的域名。`产品 ID` 是在准备工作 3 中新建的产品的 ID。 - -## 登录 - -为了确保玩家提交的表单等信息只有该玩家能访问,客服系统需要登录才能使用。我们提供三种不同的登录方式: - -- TDS 内建账户(TDSUser)登录 -- 游戏自有账户系统关联登录 -- 匿名登录 - -:::note -这里的登录指的是游戏玩家在游戏内使用客服系统功能时认证身份的过程(authentication)。这一步主要是游戏侧与客服系统之间的交互,通常玩家是无感知的,和游戏本身的登录是两回事(比如这里的匿名登录与游戏的游客登录无关)。 -::: - - - -### TDS 内建账户登录 - -如果你的游戏使用了 TDS 内建账户服务,可以直接在客户端使用已登录的 TDSUser 授权登录客服系统。 - -:::info -TDS 内建账户服务是 TDS 提供的支持多种登录方式的用户系统。如果你的游戏已经在使用 TapTap 登录、好友、成就、排行榜等服务,你很可能已经在使用 TDS 内建账户了。更多介绍,请参考[《内建账户功能介绍》](/sdk/authentication/features/)。 -::: - -要使用 TDSUser 授权登录客服系统,先要使用一个已登录的 TDS 用户账号请求授权 token,然后使用该 token 调用客服模块的 TDS 登录接口: - - - -```cs -try { - string token = await TDSUser.RetrieveShortToken(); - await TapSupport.LoginWithTDSCredential(token); - Debug.Log("登录 TDSUser JWT 完成"); -} catch (TapException e) { - Debug.LogError($"{e.Code} : {e.Message}"); -} catch (Exception e) { - Debug.Log(e); -} -``` - -```java -TDSUser.retrieveShortTokenInBackground(tdsUser.getSessionToken()).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(JSONObject jsonObject) { - String credential = jsonObject.getString("identityToken"); - TapSupport.loginWithTDSCredential(credential, new TapSupport.LoginCallback() { - @Override - public void onComplete(boolean success, Throwable error) { - if (success) { - // 登录成功 - } else { - // 登录失败 - Log.e("TapSupportActivity", "login:error:" + error.toString()); - } - } - }); - } - - @Override - public void onError(Throwable error) { - // 请求授权 token 失败 - } - - @Override - public void onComplete() { - - } -}); -``` - -```objc -[TDSUser retrieveShortTokenWithCallback:^(NSString *_Nullable token, NSError *_Nullable error) { - if (error) { - // 请求授权 token 失败 - } else { - [TapSupport loginWithTDSCredential:token - handler:^(BOOL succcess, NSError *_Nullable error) { - if (succcess) { - // 登录成功 - } else { - // 登录失败 - } - }]; - } - }]; -``` - -```cpp - -// Todo 1 - -if (TSharedPtr User = FTDSUser::GetCurrentUser()) -{ - User->RetrieveShortToken( - FStringSignature::CreateLambda([](const FString& Credential) - { - /** 获取Credential成功 */ - TapUESupport::LoginWithTDSCredential( - Credential, - FSimpleDelegate::CreateLambda([]() - { - /*** 登录成功 */ - }), - FTUError::FDelegate::CreateLambda([](const FTUError& Error) - { - /*** 登录失败 */ - })); - }), - FLCError::FDelegate::CreateLambda([](const FLCError& Error) - { - /** 获取Credential失败 */ - })); -} - -``` - - - -#### 异常处理 - -如果 SDK 在登录以及后续流程中发生了异常,开发者可以通过回调得到通知。 - -在这些异常中,我们需要特别处理 token 过期(`EXPIRED_CREDENTIAL`)异常,重新执行一遍上面的「请求授权 token,调用登录接口」的流程重新登录客服。 - -对于其他种类的异常,建议直接向玩家展示。由于客服功能异常通常不影响玩家的游玩体验,可以考虑仅在客服入口处提示玩家客服不可用,并提供手动触发重试的交互。 - - - -```cs - -if (e is TapException ex && ex.Code == 9006) { - // 登录过期 - } else { - // 其他异常 -} -``` - -```java -if (e instanceof ServerException) { - try { - org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody); - if(errorResponse.getInt("numCode")==9006){ - // 登录过期 - } - } catch (JSONException ex) { - // ignore - } -} -``` - -```objc -if (error) { - if (error.code == 9006) { - // 重新登录 - } else { - // 其他异常 - -using TapTap.Support; - -TapSupport.Init("https://please-replace-with-your-customized.domain.com", "分类 ID", new TapSupportCallback -{ - OnGetUnreadStatusError = (exception) => { - if (e is TapException ex && ex.Code == 9006) { - // 登录过期,重新执行 TDSUser.RetrieveShortToken 以及 TapSupport.LoginWithTDSCredential - } else { - // 其他异常 - } - } -}); -``` - -```java -import com.tds.tapsupport.TapSupport; -import com.tds.tapsupport.TapSupportCallback; -import com.tds.tapsupport.TapSupportConfig; - -TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "分类 ID", new TapSupportCallback() { - @Override - public void onGetUnreadStatusError(Throwable e) { - if (e instanceof ServerException) { - try { - org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody); - if(errorResponse.getInt("numCode")==9006){ - // 登录过期,重新执行 TDSUser.retrieveShortTokenInBackground 以及 TapSupport.loginWithTDSCredential - } else { - // 其他异常 - } - } catch (JSONException ex) { - // ignore - } - } - } -}); -TapSupport.setConfig(this, config); -``` - -```objc -- (void)onGetUnreadStatusError:(nonnull NSError *)error { - if (error) { - if (error.code == 9006) { - // 登录过期,重新执行 TDSUser retrieveShortTokenWithCallback 以及 TapSupport loginWithTDSCredential - } else { - // 其他异常 - } - } -} -``` - -```cpp - -// Todo 2 -// 处理 EXPIRED_CREDENTIAL 异常(9006),重试登录流程 -// 提示开发者向用户展示其他异常 - -TapUESupport::OnErrorCallBack.BindLambda([](const FTUError& Error) -{ - if (Error.code == 9006) - { - /*** 登录过期,重新执行 User->RetrieveShortToken 以及 TapUESupport::LoginWithTDSCredential */ - } - else - { - /*** 其他错误 */ - } -}); - -``` - - - -### 游戏自有账户系统关联登录 - -如果你的游戏使用了其他用户系统,客服系统也支持使用经过签名的用户信息来登录。 - -:::caution 需要服务端 -对用户信息进行签名需要使用 secret,因此必须有服务端配合才能使用该方式登录。 -::: - -开发者首先需要在客服工作台的 **设置 - 开发者设置 - 玩家鉴权** 中生成一个 auth secret。然后**在服务端**使用这个 secret 用 HS256 算法对玩家信息进行 JWT 签名。玩家信息(payload)应该包含用户的唯一标识与展示用的名称,结构如下: - -```json -{ - "sub": "U1234567", // 唯一标识 - "name": "Dash" // 展示在工单、客服后台的名称 -} -``` - -
    -JWT 签名示例 -输入: - -- 算法: HS256 -- payload: `{"sub": "U1234567", "name": "Dash"}` -- secret: `44a23a3701955756301768bbb5dd1e1ea51500b556fb73201de76d5365150653` - -输出 JWT: - -``` -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJVMTIzNDU2NyIsIm5hbWUiOiJEYXNoIn0.OV2LCP-7cLVTfjJlx21q9O1Tj_LlM0LFyW3OOu7CeNk -``` - -
    - -最后将得到的 JWT 返回给客户端,客户端调用 SDK 的第三方账号登录接口完成登录: - - - -```cs -TapSupport.LoginWithCustomCredential(jwt); -``` - -```java -TapSupport.loginWithCustomCredential("User JWT"); -``` - -```objc -[TapSupport loginWithCustomeCredential:@"User JWT"]; -``` - -```cpp -// Todo 3 -TapUESupport::LoginWithCustomCredential(TEXT("User JWT")); -``` - - - -出于安全考虑,我们通常还会给 JWT 的 payload 增加 `exp` 字段来指定其过期时间,客服系统会尊重 JWT 的过期时间设置: - -```json -{ - "sub": "U1234567", - "name": "Dash", - "exp": 1676546560 // Unix epoch -} -``` - -综合考虑安全性与性能,我们建议将 JWT 的过期时间设为(当前时间往后) 1-3 天。 - -#### 异常处理 - -如果 SDK 在登录以及后续流程中发生了异常,开发者可以通过回调得到通知。 - -在这些异常中,我们需要特别处理 JWT 过期(`EXPIRED_CREDENTIAL`)异常,重新执行一遍上面的「请求 JWT,调用登录接口」的流程重新登录客服。 - -对于其他种类的异常,建议直接向玩家展示。由于客服功能异常通常不影响玩家的游玩体验,可以考虑仅在客服入口处提示玩家客服不可用,并提供手动触发重试的交互。 - - - -```cs - -if (e is TapException ex && ex.Code == 9006) { - // 登录过期 -} else { - // 其他异常 -} -``` - -```java -if (e instanceof ServerException) { - try { - org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody); - if(errorResponse.getInt("numCode")==9006){ - // 登录过期 - } - } catch (JSONException ex) { - // ignore - } -} -``` - -```objc -if (error) { - if (error.code == 9006) { - // 重新登录 - } else { - // 其他异常 - -using TapTap.Support; - -TapSupport.Init("https://please-replace-with-your-customized.domain.com", "分类 ID", new TapSupportCallback -{ - OnGetUnreadStatusError = (exception) => { - if (e is TapException ex && ex.Code == 9006) { - // 登录过期,重新获取新的 JWT 并执行 TapSupport.LoginWithCustomCredential(jwt) - } else { - // 其他异常 - } - } -}); -``` - -```java -import com.tds.tapsupport.TapSupport; -import com.tds.tapsupport.TapSupportCallback; -import com.tds.tapsupport.TapSupportConfig; - -TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "分类 ID", new TapSupportCallback() { - @Override - public void onGetUnreadStatusError(Throwable e) { - if (e instanceof ServerException) { - try { - org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody); - if(errorResponse.getInt("numCode")==9006){ - // 登录过期,重新获取新的 JWT 并执行 TapSupport.loginWithCustomCredential("new JWT"); - } else { - // 其他异常 - } - } catch (JSONException ex) { - // ignore - } - } - } -}); -TapSupport.setConfig(this, config); -``` - -```objc -- (void)onGetUnreadStatusError:(nonnull NSError *)error { - if (error) { - if (error.code == 9006) { - // 登录过期,重新获取新的 JWT 并执行 [TapSupport loginWithCustomeCredential:@"new JWT"]; - } else { - // 其他异常 - } - } -} -``` - -```cpp -// Todo 2 -// 处理 EXPIRED_CREDENTIAL 异常(9006),重试登录流程 -// 提示开发者向用户展示其他异常 -TapUESupport::OnErrorCallBack.BindLambda([](const FTUError& Error) -{ - if (Error.code == 9006) - { - /*** 登录过期,重新获取新的 JWT 并执行 TapUESupport::LoginWithCustomCredential(TEXT("new JWT")); */ - } - else - { - /*** 其他错误 */ - } -}); -``` - - - -### 匿名登录 - -匿名登录是指游戏使用一个只有当前玩家能拿到的字符串作为匿名身份标识(ID)登录客服系统。 - - - -```cs -TapSupport.LoginAnonymously("uuid"); -``` - -```java -TapSupport.loginAnonymously("uuid"); -``` - -```objc -[TapSupport loginAnonymously:@"uuid"]; -``` - -```cpp -TapUESupport::LoginAnonymously("uuid"); -``` - - - -匿名登录可以用来在游戏没有账号系统或者玩家未登录的场景下按照设备区分玩家,也可以在玩家登录游戏账号后按账号区分玩家。 - -#### 与设备关联 - -游戏客户端为设备生成并持久化一个 UUID,然后使用该 ID 调用匿名登录接口即可实现玩家与设备关联。 - -此时,设备上的匿名 ID 是唯一的身份凭证,如果因为删除应用或清除本地数据等原因丢失了该 ID,玩家将无法访问历史工单等数据。 - -#### 与游戏账号关联 - -类似的,要与游戏账号关联,需要使用一个与账号一对一关系的 ID 调用匿名登录接口。 - -匿名登录机制非常灵活。举个例子,一个厂商的多个游戏通过「通行证」账号统一登录,如果希望一个玩家在每个游戏的客服账号是独立的,可以给每个游戏分配一个独立的匿名 ID;如果希望跨游戏的追踪玩家,就用通行证级别的匿名 ID。这种方式中客服系统是完全不知道这个 ID 意味着什么的,这也是「匿名」这个名字的由来。 - -灵活的代价是安全。如果使用不当,匿名登录方式可能导致数据泄露。客服系统不关心这个匿名 ID 的来源,也无法进行任何的校验,这意味着泄露了这个 ID 玩家的历史工单数据也会被泄露。因此,**「只有当前玩家能拿到」需要游戏侧自行保证**。更具体的说,匿名 ID 需满足以下条件: - -- 玩家唯一且不会变 -- 无法被猜测或推导、无法枚举 -- 非公开,不会被玩家通过分享或截图等方式泄露 - -
    -正确的匿名 ID 示例 - -- ✅ `b3a59993-c659-49f9-9f51-f2c808a472a0`:游戏用户系统在创建新玩家时生成一个 UUID 作为该玩家的客服系统匿名 ID。玩家登录后才能拿到该 ID 登录客服系统。 -- ✅ `sha1('2436234209' + secret)`:在服务端对玩家 UID append 一个固定的 secret 然后哈希。玩家登录后才能拿到该 ID 登录客服系统。 - -
    - -
    -不安全的匿名 ID 示例 - -简单的说,纯客户端的方案都不安全: - -- ❌ `'2436234209'` 玩家 UID:这通常是公开信息,玩家在游戏中可见且可能被分享,并且纯数字 ID 很容易枚举。 -- ❌ `sha1('2436234209')` :单纯的哈希,被猜到算法后依然很容易枚举。 - -
    - -为了降低被误用的风险,匿名登录接口限制了匿名 ID 的最小长度为 32。 - -### 清除登录状态 - - - -```cs -TapSupport.Logout(); -``` - -```java -TapSupport.logout(); -``` - -```objc -[TapSupport logout]; -``` - -```cpp -TapUESupport::Logout(); -``` - - - -## 打开客服页面 - - - -```cs -TapSupport.OpenSupportView(); -``` - -```java -TapSupport.openSupportView(); -``` - -```objc -[TapSupport openSupportView]; -``` - -```cpp -// TODO 4: 确认是否可以省略参数 -TapUESupport::OpenSupportView("/", nullptr, nullptr); -TapUESupport::OpenSupportView(); -``` - - - -:::info -如果打开的页面没有内容,可以先在客服工作台为该游戏分类配置一些子分类。 -::: - -### 场景化入口 - -除了落地页,SDK 也支持在特定的场景下直接打开具体的页面。 - - - -```cs -TapSupport.OpenSupportView(); -``` - -```java -TapSupport.openSupportView("/path"); -``` - -```objc -[TapSupport openSupportViewWithPath:@"/path"]; -``` - -```cpp -// TODO 5: 确认是否可以省略参数 -TapUESupport::OpenSupportView("/path", nullptr, nullptr); -TapUESupport::OpenSupportView(TEXT("/path")); -``` - - - -不同的页面通过不同的 path 参数区分。当前支持的页面有: - -| path | 说明 | -| ------------------------------- | ------------------------------------------ | -| `/tickets/new?category_id={id}` | 提交新工单页面,需要指定分类的 id。 | -| `/tickets` | 玩家工单列表页,可以看到玩家所有历史工单。 | -| `/articles/{id}` | 知识库文章页。 | - -### 上报信息 - -工单模块支持开发者通过自定义字段收集设备、玩家等额外信息。开发者可以在打开客服页面(openSupportView)时带上额外的信息。SDK 会在提交工单时将这些信息记录到工单字段中。这些信息可以在客服工作台供客服查看、分析。 - -在上报数据前,首先需要在客服工作台(**设置 > 管理 > 工单字段**)定义字段。这些字段需要设置为「仅限客服」访问。创建完成后需要记下该字段的 ID,我们会在 SDK 中用到。 - - - - - -```cs -Dictionary fields = new Dictionary(); -fields.Add("243", "iOS 15.1"); // 243 是在后台创建的「OS」字段的 ID -fields.Add("244", "Dash"); // 244 是在后台创建的「角色名」字段的 ID - -TapSupport.OpenSupportView("/", fields); -``` - -```java -Map fields = new HashMap<>(); -fields.put("243", "iOS 15.1"); // 243 是在后台创建的「OS」字段的 ID -fields.put("244", "Dash"); // 244 是在后台创建的「角色名」字段的 ID - -TapSupport.openSupportView("/", fields); -``` - -```objc -NSDictionary *fields = @{@"243":@"iOS 15.1", @"244":@"WiFi"}; // 243 是在后台创建的「OS」字段的 ID,244 是在「角色名」字段的 ID -[TapSupport openSupportViewWithPath:@"/" fieldsData:fields]; -``` - -```cpp -// TODO 6: 确认是否可以省略参数 -TSharedPtr Fields = MakeShareable(new FJsonObject); -Fields->SetStringField("243", "iOS 15.1"); -Fields->SetStringField("244", "Dash"); -TapUESupport::OpenSupportView("/", Fields); -``` - - - -除了在打开客服页面时设置,开发者也可以通过以下接口设置全局默认的字段信息: - - - -```cs -TapSupport.SetDefaultFieldsData(fields: fields); -``` - -```java -TapSupport.setDefaultFieldsData(fields); -``` - -```objc -[TapSupport shareInstance].defaultFieldsData = fields; -``` - -```cpp -TapUESupport::SetDefaultFieldsData(Fields); -``` - - - -全局字段可以在设置后更新: - - - -```cs -TapSupport.DefaultFields = new Dictionary { - { "key", "value" } -}; -``` - -```java -TapSupport.updateDefaultField("key", "value"); -``` - -```objc -[TapSupport updateDefaultFieldWithValue:@"value" forKey:@"key"]; -``` - -```cpp -// TODO 7 -TSharedPtr Value = MakeShared(TEXT("Value")); -TapUESupport::UpdateDefaultField(TEXT("key"), Value); -``` - - - -全局设置的字段会在 OpenSupportView 时与传入的 `fields` 参数合并。`OpenSupportView` 方法时传入的字段比全局字段的优先级高,也就是说如果有相同的字段,全局字段不会生效。 - -### 关闭客服页面 - -玩家可以在客服页面内点击关闭按钮退出。但在特定场景下,游戏可能需要主动关闭客服页面: - - - -```cs -TapSupport.CloseSupportView(); -``` - -```java -TapSupport.closeSupportView(); -``` - -```objc -[TapSupport closeSupportView]; -``` - -```cpp -TapUESupport::CloseSupportView(); -``` - - - -## 未读消息通知 - -当提交的工单有了新的进展(例如有了新的客服回复)时,玩家会产生未读消息。通常在游戏内会在客服入口使用小红点等形式提示玩家有未读消息。SDK 会自动通过轮询获取是否有维度消息,当未读消息状态发生变化时——从无到有或从有到无—— SDK 会通过回调通知开发者: - - - -```cs -using TapTap.Support; - -TapSupport.Init("https://please-replace-with-your-customized.domain.com", "分类 ID", new TapSupportCallback -{ - UnReadStatusChanged = (hasUnRead, exception) => - { - Debug.Log($"hasUnRead:{hasUnRead} exception:{exception}"); - } -}); -``` - -```java -import com.tds.tapsupport.TapSupport; -import com.tds.tapsupport.TapSupportCallback; -import com.tds.tapsupport.TapSupportConfig; - -TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "分类 ID", new TapSupportCallback() { - @Override - public void onUnreadStatusChanged(boolean hasUnread) {} -}); -TapSupport.setConfig(this, config); -``` - -```objc -#import - -// callback 需要实现 TapSupportDelegate -TapSupportConfig *config = [TapSupportConfig new]; -config.server = @"https://please-replace-with-your-customized.domain.com"; -config.productID = @"分类 ID"; -config.callback = self; -[TapSupport shareInstance].config = config; -``` - -```cpp -TapUESupport::OnUnreadStatusChanged.BindUObject(this, &YourUObject::OnUnreadStatusChanged); -``` - - - -:::info -开发者无需额外在点击客服入口时清除本地的未读通知状态(小红点)。因为点开了客服入口不意味着玩家查看了所有未读的工单。如果玩家查看了所有未读工单,SDK 会及时获取到最新的状态并通过上文提到的回调通知开发者。 -::: - -### 暂停轮询 - -SDK 内建的轮询机制会智能的调整频率获取未读消息状态。但在有些场景下这些请求依然是没有必要的开销,例如玩家在游戏对局期间(此时界面上都没有展示小红点的地方)希望能暂停一切不必要的后台请求。SDK 为此提供了一对 API 来控制轮询: - -- `Pause`:暂停轮询 -- `Resume`:恢复轮询 - - - -```cs -TapSupport.Resume(); -TapSupport.Pause(); -``` - -```java -TapSupport.resume(); -TapSupport.pause(); -``` - -```objc -[TapSupport resume]; -[TapSupport pause]; -``` - -```cpp -TapUESupport::Resume(); -TapUESupport::Pause(); -``` - - - -
    -SDK 的轮询策略 - -- 初始时立即发起一次请求,设定下一次请求间隔 10s。 - - 如果某一次未读消息状态与当前状态对比没有变化,增加 10s 间隔时间,直到最大间隔时间 300s。如果发生了变化,重置间隔时间为 10s。 - - 如果玩家从未打开过客服页面(WebView),间隔时间增加并恒定为 3600s。 -- 调用 `Resume` 方法会重置轮询为初始状态。 - -
    - - diff --git a/docs/sdk/tapdb/_category_.json b/docs/sdk/tapdb/_category_.json deleted file mode 100644 index 5777a944c..000000000 --- a/docs/sdk/tapdb/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "数据分析", - "collapsed": true, - "position": 13 -} diff --git a/docs/sdk/tapdb/changelog/_category_.json b/docs/sdk/tapdb/changelog/_category_.json deleted file mode 100644 index 23ab30266..000000000 --- a/docs/sdk/tapdb/changelog/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "功能更新", - "position": 8 -} diff --git a/docs/sdk/tapdb/changelog/android.mdx b/docs/sdk/tapdb/changelog/android.mdx deleted file mode 100644 index 3defff371..000000000 --- a/docs/sdk/tapdb/changelog/android.mdx +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: Android -sidebar_position: 3 ---- - -## 2.2.0 | 2022-07-14 发布 -**优化**
    - -1. 调整已冻结项目的界面展示 - - -## 2.1.0 | 2022-04-30 发布 -**新增**
    - -1. 新增多账号切换功能,开发者可添加多个国内或海外账号,并在之间进行切换。 - -**优化**
    - -1. 优化部分体验 - - -## 2.0.0 | 2022-01-13 发布 - -**新增**
    - -1. 新增看板查看功能:可通过客户端查看 web 端发布的看板
    - -**优化**
    - -1. 修复广告模块的数据显示 - -## 1.9.0 | 2021-10-29 发布 - -**新增**
    - -1. 增加 TapTap 登录
    - -**优化**
    - -1. 修复了一些 bug,优化了体验 - -## 2019-10-08 V1.7.4 发布 - -**新增** - -1. 新添加了支持运营事件功能 - -**优化** - -1. 优化了正在变化的数据的显示问题 -2. 优化了图标的样式 -3. 改变了图表线条颜色 - ---- - -## 2019-08-20 V1.7.3 发布 - -**优化** - -1. 修复了广告排序后显示错误的问题 -2. 长按复制后提示复制成功 -3. 优化了多语言的翻译问题 -4. 广告自定义列避免重复命名 -5. 优化了留存中的日周月变化 - ---- - -## 2019-08-12 V1.7.2 发布 - -**新增** - -1. 用户 LTV 图表变为累积 LTV 走势图 -2. 支持长按复制功能 - -**优化** - -1. 修正了概览中切换对比日出现日期错误的数据显示 -2. 优化了部分按钮 -3. 优化了图表排版设计 -4. 全新的更新弹窗设计 -5. 对比日的自定义时间支持双向选择 - ---- - -## 2019-06-25 V1.7.0 发布 - -**新增** - -1. 支持广告效果查询 -2. 优化表格排版 -3. 增加 8 - 13 日 LTV、DAU - 14 日用户 - -**优化** - -1. ARPU、ARPPU、LTV 显示 2 位小数 - ---- - -## 2019-05-20 V1.6.2 发布 - -**新增** - -1. 支持多语言 - -**优化** - -1. 修复二级表格默认排序问题 -2. 优化过滤条件无法生效时提示机制 - ---- - -## 2019-04-11 V1.6.1 发布 - -**优化** - -1. 过滤条件区服名称调整为首次区服 -2. 付费数据下过滤条件新增事件区服 -3. 已选择的过滤条件置顶 -4. 支持截屏 - ---- - -## 2019-04-03 V1.6.0 发布 - -**新增** - -1. 支持对比日期 - -**优化** - -1. 表格总计/平均行固定在底部 -2. 表格为机型主维度时加载优化 -3. 更新「付费方式」,「商品名称」以条形图形式展现 -4. 增加安全验证密码错误提示 -5. 活跃账号/活跃设备下,增加非日期维度提示信息 -6. 修复登录界面无法查看错误提示问题 -7. 优化概览页面自动刷新机制 -8. 优化付费下不同类型数据以双轴折线图展现 - ---- - -## 2019-03-04 V1.5.11 发布 - -**新增** - -1. 可以使用指纹解锁了 -2. 支持查看活跃设备数据 - -**优化** - -1. 多次点击,防止重复进同一个页面 -2. 消息详情页支持下载文件 -3. 游戏列表页刷新时,可点击进入概览页面 - -**修复** - -1. 消息中心链接返回页面错误 -2. 修复无网/弱网状态下登录丢失问题 - ---- - -## 2019-01-28 V1.5.9 发布 - -**新增** - -1. 表格支持排序 -2. 首页支持对比日选择 -3. 长按表头可显示完整信息 -4. 付费-用户模块新增 2、4、5、6 日 LTV 数据 -5. 活跃-行为模块支持查看小时数据 -6. 付费下增加 120 日贡献与 150 日贡献 -7. 登录页面新增 DEMO 入口 - -**优化** - -1. 整体设计样式优化与调整 -2. 活跃下实时在线支持查看七天以上数据 -3. 过滤条件筛选优化交互体验 -4. 修复了过滤条件中不包含未生效的问题 -5. 权限更改后在首页刷新即可生效 -6. 饼图数据显示优化 -7. 设置按钮位置调整到右上角 -8. 优化网络环境较差时的交互与提示 -9. 优化自定义日期控件样式 diff --git a/docs/sdk/tapdb/changelog/ios.mdx b/docs/sdk/tapdb/changelog/ios.mdx deleted file mode 100644 index 4210efa27..000000000 --- a/docs/sdk/tapdb/changelog/ios.mdx +++ /dev/null @@ -1,362 +0,0 @@ ---- -title: iOS -sidebar_position: 2 ---- - -## 2.2.0 | 2022-07-18 发布 -**优化**
    - -1. 优化 iPad 横屏体验 -2. 调整已冻结项目的界面展示 -3. 修复周活跃、月活跃日期显示不准确问题 - - -## 2.1.0 | 2022-04-30 发布 -**新增**
    - -1. 新增多账号切换功能,开发者可添加多个国内或海外账号,并在之间进行切换。 - -**优化**
    - -1. 优化部分体验 - - -## 2.0.0 | 2022-01-13 发布 - -**新增**
    - -1. 新增看板查看功能:可通过客户端查看 web 端发布的看板
    - -**优化**
    - -1. 修复广告模块的数据显示 - -## 1.9.0 | 2021-10-29 发布 - -**新增**
    - -1. 增加 TapTap 登录
    - -**优化**
    - -1. 修复了一些 bug,优化了体验 - -## 2019-10-08 V1.7.4 发布 - -**新增** - -1. 新添加了支持运营事件功能 - -**优化** - -1. 优化了正在变化的数据的显示问题 -2. 优化了图标的样式 -3. 改变了图表线条颜色 - ---- - -## 2019-08-20 V1.7.3 发布 - -**优化** - -1. 修复了广告排序后显示错误的问题 -2. 长按复制后提示复制成功 -3. 优化了多语言的翻译问题 -4. 广告自定义列避免重复命名 -5. 优化了留存中的日周月变化 - ---- - -## 2019-08-12 V1.7.2 发布 - -**新增** - -1. 用户 LTV 图表变为累积 LTV 走势图 -2. 支持长按复制功能 - -**优化** - -1. 修正了概览中切换对比日出现日期错误的数据显示 -2. 优化了部分按钮 -3. 优化了图表排版设计 -4. 全新的更新弹窗设计 -5. 对比日的自定义时间支持双向选择 - ---- - -## 2019-06-25 V1.7.0 发布 - -**新增** - -1. 支持广告效果查询 -2. 优化表格排版 -3. 增加 8 - 13 日 LTV、DAU - 14 日用户 - -**优化** - -1. ARPU、ARPPU、LTV 显示 2 位小数 - ---- - -## 2019-05-21 V1.6.2 发布 - -**新增** - -1. 支持多语言 -2. 截屏时,新增谨慎分享提示 -3. 增加版本更新提醒 - -**优化** - -1. 修复二级表格默认排序问题 -2. 优化过滤条件无法生效时提示机制 - ---- - -## 2019-04-11 V1.6.1 发布 - -**优化** - -1. 过滤条件区服名称调整为首次区服 -2. 付费数据下过滤条件新增事件区服 -3. 已选择的过滤条件置顶 - ---- - -## 2019-04-03 V1.6.0 发布 - -**新增** - -1. 支持对比日期 - -**优化** - -1. 表格总计/平均行固定在底部 -2. 表格为机型主维度时加载优化 -3. 更新「付费方式」,「商品名称」以条形图形式展现 -4. 增加安全验证密码错误提示 -5. 活跃账号/活跃设备下,增加非日期维度提示信息 -6. 修复登录界面无法查看错误提示问题 -7. 优化概览页面自动刷新机制 -8. 修复无法使用 Face ID 解锁或重复使用其解锁问题 -9. 优化付费下不同类型数据以双轴折线图展现 -10. 修复在来源/活跃/留存/付费页面白屏问题 - ---- - -## 2019-02-14 V1.5.10 发布 - -**新增** - -1. 支持查看活跃设备数据了 - ---- - -## 2019-01-22 V1.5.9 发布 - -**新增** - -1. 表格支持排序 -2. 长按表头可显示完整信息 -3. 付费下增加 120 日贡献与 150 日贡献 - -**优化** - -1. 活跃下实时在线支持查看七天以上数据 -2. 修复 iPad 闪退问题 -3. 饼图数据显示优化 -4. 游戏列表星标位置调整 -5. 优化密码解锁体验 -6. 优化日历以及自定义日期标签样式 - ---- - -## 2018-12-07 V1.5.7 发布 - -**修复** - -1. 修复部分时区下对比日错误的问题 - ---- - -## 2018-12-04 V1.5.6 发布 - -**修复** - -1. 修复 iOS 11 及以下系统部分设备闪退问题 -2. 修复部分项目「在线」下点击过滤按钮无法收起列表的问题 - ---- - -## 2018-12-03 V1.5.5 发布 - -**新增** - -1. 支持保存常用的过滤条件 -2. 首页支持选择日期和对比日期 -3. 活跃-周活跃下新增 N 周活跃,细分周活跃数据 -4. 支持拼音搜索 - -**优化** - -1. 修复了过滤条件中不包含未生效的问题 -2. 优化概览页自定义日期页左滑返回的交互 -3. 优化使用 1Password 登录时的交互体验 -4. 权限更改后在首页刷新即可生效 -5. 修复使用搜索功能时键盘无法收起的问题 -6. 优化进入首页 loading 时默认 icon 显示错误的问题 - ---- - -## 2018-10-26 V1.5.4 发布 - -**优化** - -1. 修复概览页零点对比日「昨日」更新延迟问题 -2. 修复公告播放不连贯的问题 -3. 修复横屏下公告缺失的问题 -4. 优化自定义日期控件样式 - ---- - -## 2018-10-22 V1.5.3 发布 - -**新增** - -1. 首页支持对比日选择 - -**优化** - -1. 适配 iPhone XS Max -2. LTV 动态数据标红显示 -3. 修复无网络时 DEMO 无反馈的问题 - ---- - -## 2018-09-06 V1.5.2 发布 - -**新增** - -1. 可以在消息中心接收到最新的通知 -2. 如果碰到问题,可以在设置-问题反馈中向我们反馈 -3. 付费数据新增推广费用曲线 -4. 来源下过滤条件筛选支持付费数据筛选 - -**优化** - -1. 游戏列表昨日数据根据项目时区变化 -2. 修复概览页面今日柱状图时间错位问题 -3. 过滤条件筛选交互优化 -4. 常用货币增加国家 logo -5. 优化网络环境较差时的交互与提示 - ---- - -## 2018-08-09 V1.5.1 发布 - -**新增** - -1. 新增 DEMO 入口 - -**优化** - -1. 修复部分场景下进入客户端闪退的问题 - ---- - -## 2018-07-31 V1.5.0 发布 - -**新增** - -1. 项目根据项目所在时区显示数据 -2. 当你所在时区和项目时区不一致时,增加提示信息 - -**优化** - -1. 整体布局以及细节样式优化 -2. 优化数据显示样式,对未满足天数标红;今日数据标绿 -3. 下拉刷新体验优化 -4. 过滤条件页面支持右滑返回 -5. 无网络连接提示更新 -6. 修复表格全屏时样式问题 -7. 修复登录页 1Password 与清除图标重合的问题 -8. 解决 TouchID/FaceID 锁定状态下的问题 -9. 修复唤醒 App 后卡在开屏图的问题 - ---- - -## 2018-06-11 V1.4.4 发布 - -**修复** - -1. 修复在来源、活跃、付费下不能下拉刷新的 bug - ---- - -## 2018-06-11 V1.4.3 发布 - -**新增** - -1. 付费-用户模块新增了 2、4、5、6 日 LTV 数据 -2. 付费-周期模块改版,支持多日数据对比查看 -3. 付费下支持按「付费方式」「商品名称」查看数据 -4. 活跃-行为模块支持查看小时数据 -5. 新增「关于 TapDB」,可查看当前版本以及功能介绍 - -**优化** - -1. 设置按钮位置调整到右上角 -2. 指纹解锁在开启后显示 -3. 修复 iOS10,iPhone6 闪退问题 -4. 表头内容显示优化 - ---- - -## 2018-05-14 V1.4.2 发布 - -**新增** - -1. 支持将当前设备加入测试白名单,用于测试新增数据 -2. 可以按照日、周、月查看活跃数据 -3. 增加顶部公告 - -**优化** - -1. 全面优化结构,提高 App 响应速度 -2. 概览支持右滑返回上一级界面 -3. 图表支持长按查看数据 -4. 已标记项目按照收入排序 - ---- - -## 2017-12-22 V1.3.0 发布 - -**新增** - -1. 游戏支持置顶,让你更快的找到常用游戏 -2. 来源中支持按照累计付费、首日付费、付费次数进行数据筛选 -3. 可以在设置中提交问题和建议 - -**优化** - -1. 游戏列表按照收入排序 -2. 界面布局优化 -3. 修复了过滤条件内搜索不全的 bug -4. 显示货币支持越南盾 - ---- - -## 2017-11-15 V1.1.0 发布 - -**优化** - -1. 适配 iPhoneX - ---- - -## 2017-10-20 V1.0.0 发布 - -**新增** - -1. 支持在概览页以及活跃下查看实时在线数据 -2. 可按照不同的货币类型查看收入数据 diff --git a/docs/sdk/tapdb/changelog/web.mdx b/docs/sdk/tapdb/changelog/web.mdx deleted file mode 100644 index c0fe1334c..000000000 --- a/docs/sdk/tapdb/changelog/web.mdx +++ /dev/null @@ -1,614 +0,0 @@ ---- -title: 网页版 -sidebar_position: 1 ---- - -## 2.17.0 | 2024-07-01 发布 -**功能**更新 -1. 分析模块中,维度支持重命名 -2. 新增数据预警功能,触发预警后,可发送到企业微信、Slack、邮箱 -3. 显示游戏在 TapTap 的具体状态,如测试服、先行服,方便区分同名项目 -4. TapTap 广告渠道支持自定义开启回传配置 -5. 支持全渠道的广告回调日志查询 -6. 新增 31 - 60 日留存 -7. 游戏列表页支持筛选活跃的项目 -8. 支持隐藏项目,隐藏后不再游戏列表显示 -9. 过滤条件现在可以共享给项目成员了 -10. 付费新增退款数据 -11. 用户价值(LTV)支持显示每日的 LTV 相比首日 LTV 的倍数 -12. 新增全局观察者角色,该角色有企业下所有项目的权限(含新项目),但是没有管理权限 -13. 概览的收入模块增加周、月推广费用 - -## 2.16.0 | 2022-12-16 发布 -**功能**更新 -1. 新增用户精查功能 -2. 国际版支持在分析模型中切换查询时区 - -## 2.15.2 | 2022-09-08 发布 -**体验**优化 -1. 诊断:崩溃分析、错误分析问题详情页,新增打开新窗口按钮 - - -## 2.15.1 | 2022-09-01 发布 -**体验**优化 -1. 崩溃分析、错误分析可通过链接分享部分筛选条件 - - -## 2.15.0 | 2022-08-26 发布 -**功能**更新 -1. 新增诊断模块:包含崩溃分析、错误分析、符号表管理、标签管理 4 个功能。 -2. 使用诊断模块需要接入 TapSDK 3.14.0 或更新版本。 - -**功能**优化 - -1. 为了提升分析能力,我们为所有项目提供了手机硬件相关的一些字段,可以在「分析」模块中使用。 - -| 字段名 | 显示名 | -| --- | --- | -| product_name | 产品名 | -| release_year | 发售年份 | -| chipset_brand | 芯片组品牌 | -| chipset_model | 芯片组型号 | -| single_core_score | 单核跑分 | -| multi_core_score | 多核跑分 | -| price_1_median 1 | 当前市价 | -| price_2_median 2 | 当前二手价 | - - -## 2.14.3 | 2022-08-11 发布 -**体验**优化 -1. 优化了「配置模块 - 预警管理」的操作体验 - - -## 2.14.2 | 2022-07-28 发布 -**体验**优化 -1. 优化了配置模块大部分功能的的操作体验 - -## 2.14.1 | 2022-07-14 发布 -**体验**优化 -1. 优化了「配置 - 用户标签」的操作体验 - -## 2.14.0 | 2022-06-29 发布 - -**自定义分析**更新 - -1. 事件分析不再需要预先选择分析主体,现在支持同时对多个指标选择不同的分析主体 -2. SQL 分析中简化了表名 -3. SQL 分析中添加了表字段的描述,方便大家使用时同步查阅 - -## 2.13.0 | 2022-06-09 发布 - -**多语言支持**升级 -1. 将语言切换到英文时,菜单支持英文显示了 - -## 2.12.0 | 2022-05-26 发布 - -**可视化**升级 - -1. 自定义分析可视化支持添加对比日期后的可视化 -2. 自定义分析支持更多种可视化选项 -3. 看板同步支持新的可视化选项 - -## 2.11.4 | 2022-04-21 发布 - -**体验**优化 - -1. 小时、分钟颗粒度的时间选择器体验优化 - -## 2.11.3 | 2022-04-07 发布 - -**体验**优化 - -1. 维度表上传时,不再丢弃未匹配到属性值的行 -2. 留存分析的全局筛选中「相对事件时间」修改为「相对初始事件时间」与「相对回访事件时间」 -3. 修复下载报表时指标名与系统中报表展示不一致的 bug - -## 2.11.2 | 2022-03-03 发布 - -**功能**更新 - -1. 优化「事件分析」中的「总计」逻辑,所有总计均为在数据明细层面的计算 - -## 2.11.1 | 2022-02-17 发布 - -**功能**更新 - -1. 「事件分析」与「看板」可视化中新增「分组的筛选排序」功能:可以查询结果按照指标值、分组名进行排序并进行筛选 -2. 优化了部分功能体验 - -**文档**更新 - -1. 更新「[看板](/sdk/tapdb/features/custom-event/kanban)」文档,以便于大家了解「分组的筛选排序」功能 - -## 2.11.0 | 2022-01-20 发布 - -**功能**更新 - -1. 新增「预警管理」功能:大家可以通过预警管理功能实时监控关键运营指标的变化情况 -2. 优化了部分功能体验 - -**文档**更新 - -1. 新增「[预警管理](/sdk/tapdb/features/custom-event/alert)」文档,以便于大家更方便的使用预警管理功能 - -## 2.10.0 | 2022-01-13 发布 - -**功能**更新 - -1. 新增「用户标签」功能:大家可以通过用户标签功能将用户划分为不同群体,便于对用户深度分析 - -**文档**更新 - -1. 新增「[用户标签](/sdk/tapdb/features/custom-event/user-tag)」文档,以便于大家更方便的使用用户标签功能 - -## 2.9.2 | 2021-12-16 发布 - -**功能**更新 - -1. 「测试设备」、「数据导出」和「数据大屏」功能下线 -2. 点击「工具」可以看到新的「测试设备」功能入口 - -**文档**更新 - -1. 调整「测试设备」文档,以便于大家更方便的使用测试设备功能 - -## 2.9.1 | 2021-12-09 发布 - -**自定义事件分析**更新 - -1. 时间筛选器优化 - -**文档**更新 - -1. 在[接入问题](/sdk/tapdb/faq/tran)新增接入方面的常见问题,以便于大家更方便的接入 SDK - -## 2.9.0 | 2021-11-25 发布 - -**自定义事件分析**更新 - -1. 新增「虚拟事件」功能:大家可以将事件进行拆解或者组合形成新的「虚拟事件」,提高分析效率 -2. 优化了很多功能体验 - -**文档**更新 - -1. 新增「[虚拟事件](/sdk/tapdb/features/custom-event/virtual-event)」文档,以便于大家更好的了解和使用虚拟事件功能 -2. 新增「[广告管理](/sdk/tapdb/features/custom-event/ad)」文档,以便大家更好的了解和使用广告功能 - -## 2.8.0 | 2021-11-11 发布 - -**自定义事件分析**更新 - -1. 新增「SQL 查询」功能:大家可以使用标准 SQL 对 TapDB 的所有数据进行查询 -2. 优化了部分功能体验 - -**文档**更新 - -1. 新增「[SQL 查询](/sdk/tapdb/features/custom-event/sqlide)」文档,以便于大家更好的了解和使用 SQL 查询功能 - -## 2.7.1 | 2021-10-29 发布 - -**新增**功能
    - -1. 增加 TapTap 登录
    - -## 2.7.0 | 2021-10-21 发布 - -**自定义事件分析**更新 - -1. 新增「维度表」功能:大家可以从系统外部引入维度表对现有属性进行映射加工,丰富分析维度信息 -2. 优化了「分布分析」:支持通过公式构建指标 -3. 优化了很多功能体验 - -**文档**更新 - -1. 新增「[维度表](/sdk/tapdb/features/custom-event/dimension-table)」文档,以便于大家更好的了解和使用维度表功能 - -## 2.6.1 | 2021-10-14 发布 - -**自定义事件分析**更新 - -1. 优化了「分布分析」功能:支持对分析结果创建用户分群 - -## 2.6.0 | 2021-09-29 发布 - -**自定义事件分析**更新 - -1. 新增「属性分析」功能,帮助大家分析玩家群体的属性特征 - -**文档**更新 - -1. 新增「[属性分析](/sdk/tapdb/features/custom-event/property-analyse)」文档,以便于大家更好的了解和使用属性分析功能 - -## 2.5.0 | 2021-09-16 发布 - -**自定义事件分析**更新 - -1. 新增「分布分析」功能:分析用户行为的总次数或属性值按用户聚合后的分布情况 -2. 对「漏斗分析」、「查询主体」切换、「虚拟属性」进行了使用体验优化 - -**文档**更新 - -1. 新增「[分布分析](/sdk/tapdb/features/custom-event/distribution-analyse)」文档,以便于大家更好的了解和使用分布分析功能 -2. 在「[接入问题](/sdk/tapdb/faq/tran)」里新增了一些问题说明 -3. 新增搜索框,方便大家更快捷的查找问题答案 - -## 2.4.0 | 2021-09-02 发布 - -**自定义事件分析**更新 - -1. 新增「留存分析」可视化趋势图,帮助大家更直观的进行留存分析 -2. 优化了「元数据管理」的信息展示 -3. 优化了埋点入库的判断逻辑,以便于更准确的监控埋点上报状况 - -**运营报表**优化 - -1. 优化了运营报表的过滤条件,显示更完整的过滤条件值 - -**文档**更新 - -1. 对「[留存分析](/sdk/tapdb/features/custom-event/retention-analyse)」内容进行修改,帮助大家更好的了解和使用留存分析功能 - -## 2.3.0 | 2021-08-20 发布 - -**自定义事件分析**更新 - -1. 新增「虚拟属性」功能:用于对事件属性和用户属性二次加工,产生一个新的属性值 -2. 在「上报明细」增加监测开关 - -**运营报表**优化 - -1. 对「运营」-「付费」-「付费数据」报表的付费人数、活跃人数在跨天计算时进行了去重优化 - -**文档**更新 - -1. 新增「[虚拟属性](/sdk/tapdb/features/custom-event/virtual)」,以便于大家更好的了解和使用虚拟属性功能 -2. 在「[埋点管理](/sdk/tapdb/features/custom-event/tracking-management)」补充了监测开关使用说明 - -## 文档更新 | 2021-08-12 发布 - -**文档**更新 - -1. 新增「[关于我们](/sdk/tapdb/features)」,以便于大家更好的了解 TapDB -2. 新增「[埋点设计指南](/sdk/tapdb/features/custom-event/data-model)」,用于指导大家更好的设计自定义事件 -3. 新增「[服务端接入文档](/sdk/tapdb/sdk/server-side-integration)」,介绍如何进行服务端接入 -4. 对「[快速接入]、「[数据模型](/sdk/tapdb/sdk/data-spec)」和「[事件分析](/sdk/tapdb/features/custom-event/event-analyse)」的内容进行了修改,帮助大家更好的理解接入过程、基础概念和事件分析功能 -5. 调优了文档结构 - ---- - -## 2.2.0 | 2021-07-29 发布 - -**埋点管理**上线 - -1. 这是一个测试项目数据上报的功能,用于校验埋点的正确性 - -**文档**更新 - -1. 新增「[埋点管理](/sdk/tapdb/features/custom-event/tracking-management)」、「[用户分群](/sdk/tapdb/features/custom-event/cluster)」的使用文档 -2. 调优了文档结构 -3. 优化了基础指标的说明,以便于大家更准确地理解指标含义 - -部分功能下线 - -1. 调试模式下线:其功能已被埋点管理所覆盖 -2. cocos-2d SDK 下线 - ---- - -## 2.1.0 | 2021-07-15 发布 - -**自定义事件分析**更新 - -1. 新增「**结果分群**」功能:可以直接在报表中将查询 「触发用户数」 的指标结果快捷保存为用户分群 -2. 优化了针对自定义事件分析的权限分配,以保证数据安全和防止误改配置信息 -3. 针对上传 ID 创建用户分群的效率进行了调优 - -**SDK**更新 - -1. 调优了 GAID(谷歌广告 ID) 的获取方式 -2. 调优了在 iOS 上获取 & 生成设备 ID 的方式,使其更加稳定了 - ---- - -## 2.0.0 | 2021-06-17 发布 - -**自定义事件分析**功能限时免费中,欢迎大家体验! - -**自定义事件分析**更新 - -1. 新增「**看板**」功能:用于将关注的核心指标和报表放到一屏内快速查看,使用方法详见**自定义事件分析 → 看板** - -**TapDB Demo** 更新:现在可以在 Demo 中体验**看板**功能了! - ---- - -## 1.9.1 | 2021-05-13 发布 - -**自定义事件分析**更新 - -1. 新增「**事件分析**」可视化图表:用于查看趋势和进行对比 - -**TapDB Demo** 更新:现在可以在 Demo 中体验更完整的 TapDB 功能了! - -1. 新增**自定义事件分析**模块 -2. 新增**广告**模块 -3. 新增**工具**模块 - ---- - -## 1.9.0 | 2021-04-15 发布 - -新的分析模块:新增了自定义事件分析,现在可以更自由地查询更多的用户行为了! - -关于新功能的使用和接入建议详细阅读使用手册 - -**自定义事件分析** - -1. 新增「**事件分析**」:分析用户行为的次数、人数、地区分布等 -2. 新增「**留存分析**」:分析广义留存(请查看使用手册或体验产品)用户的行为 -3. 新增「**漏斗分析**」:分析用户逐步( 2~N 步)转化的转化率,步骤间有严格的先后顺序 - -**配置工具** - -1. 新增「**事件管理**」:用于管理事件,所有自定义事件上报前需要在此进行预登记 -2. 新增「**事件属性管理**」:用于管理事件属性,所有事件可以自由配置事件属性 -3. 新增「**用户属性管理**」:用于管理用户属性,所有事件可以自由配置用户属性 -4. 新增「**用户分群**」:设定条件生成用户分群,用户分群可以作为筛选条件应用于自定义事件分析 - ---- - -## 2019-01-18 更新日志 - -**新功能** - -1. 在图表上,对**周末**进行了**标记**,方便查看数据趋势 -2. 增加**活跃设备**数据 -3. **在线分析**支持查询**7 天以上**的数据 -4. 支持在图表上直接编辑**运营事件** - ---- - -## 2018-11-23 更新日志 - -**新功能** - -1. 新增**企业运营报告**,可以在右上角企业中查看,按自然月查询企业所有项目的关键指标 -2. 数据导出功能增加**分包渠道**数据导出 - ---- - -## 2018-08-03 更新日志 - -**新功能** - -1. 新增**原始付费数据**导出,可以在工具-数据导出中下载 - -**修复** - -1. 过滤条件在部分场景下的搜索排序问题 -2. 外部成员中复制链接的 flash 提示问题 -3. 收起侧边栏时的图表渲染问题 - ---- - -## 2018-07-27 更新日志 - -**新功能** - -1. 新版报表中心,下载同时可以进入其他页面看数据,未来将支持更多原始数据导出 -2. 新增特殊用户归类,可以将内部用户、刷榜用户、特殊用户标记为特殊广告渠道,方便查询和过滤 -3. 广点通支持授权模式 -4. 概览支持按日、周、月查询数据 -5. 广告默认查询有数据的广告,提高响应速度 -6. 广告增加硬件平台筛选 - -**修复** - -1. LTV 指标优化,更精准地描述指标定义 -2. 修复部分场景下选择一日,没有显示小时的问题 -3. 搜索优先出精确匹配的结果 -4. 广告下使用设备、账号相关的过滤条件(如机型、地区)时,不展现点击信息,避免误解 - ---- - -## 2018-06-08 更新日志 - -**新功能** - -1. 首页游戏列表支持选择**对比日期** - -2. 付费支持按照**商品**查询数据 - ![图片描述](/img/changelog/2018_06_08_01.png) - -3. **官网**全面更新 - -**修复** - -1. 部分场景下,总计显示乱码的问题 -2. 部分场景下,图表没有渲染的问题 - ---- - -## 2018-05-18 更新日志 - -**新功能** - -1. **用户价值(LTV)** 支持查看第 2、4、5、6 日的数据 -2. TapDB 开始支持多种语言了,目前支持 **简体中文**、 **繁体中文** 、**英语**,即将支持 **韩语** 、**日语** -3. **设备白名单** ,如果需要测试新的分包渠道,新的广告是否调通,可以将设备加入白名单,这样可以在一台设备上反复测试不同渠道。 - -![图片描述](/img/changelog/2018_05_21_01.png) - -**修复** - -1. 广告中的作弊主维度下,修复了存疑广告会将自己计算在内的问题 - ---- - -## 2018-05-04 更新日志 - -**新功能** - -1. **防作弊基础版** ,提供 3 种基础防作弊规则:点击 IP 离散作弊、激活 IP 离散作弊、点击激活时长作弊。 - -2. 支持 2 个新的广告平台: **陌陌** 、 **百度信息流 ocpc** - -3. 按 **支付渠道** 查询收入 - ![图片描述](/img/changelog/2018_05_08_01.png) - -4. **隐藏渠道** ,可以将不常用、测试、传递错误的渠道归类到隐藏渠道 - ![图片描述](/img/changelog/2018_05_08_02.png) - -**修复** - -1. 概览下的新增设备未显示运营事件 - ---- - -## 广告概览 - -###### 更新日期 2018-04-12 - -广告概览可以帮助你 - -- 监控今日的点击、新增、转化是否正常 - -![](/img/changelog/2018_04_12_1.png) - -- 了解今日投放量 Top10 的广告平台 - -![](/img/changelog/2018_04_12_2.png) - -- 分析近 30 日的新用户获取趋势、支出和费用趋势 - -![](/img/changelog/2018_04_12_3.png) - ---- - -## 导出细分数据 - -###### 更新日期 2018-01-11 - -广告监测支持导出广告平台、广告、标签的细分数据,导出后方便查看每个广告下每天的数据 - -![](/img/changelog/2018_01_11_01.jpeg) - ---- - -## 小时活跃、周活跃和月活跃 - -###### 更新日期 2017-12-15 - -1. 现在可以查看不同日期粒度下的活跃了: - -- **小时活跃**(HAU):日期选择某一天 -- **日活跃**(DAU):日期选择某一段时间 -- **自然周活跃**(WAU):活跃页面下切换到「周活跃」 -- **自然月活跃**(MAU):活跃页面下切换到「月活跃」 - -其中周活跃、月活跃可以细分不同的分包渠道来查看渠道活跃明细。 - -![](/img/changelog/2017_12_15_1.png) - -2. 概览中增加月活跃曲线(MAU) - ![](/img/changelog/2017_12_15_2.png) - ---- - -## 选择时区 - -###### 更新日期 2017-12-06 - -创建项目的时候可以选择不同的时区,方便不同地区发行的游戏更好的分析数据 - -![](/img/changelog/2017_12_06_01.jpeg) - ---- - -## 显示货币 - -###### 更新日期 2017-10-13 - -现在可以按照不同的货币类型来查看收入数据了 - -![](/img/changelog/2017_10_17_01.jpeg) - ---- - -## 数据导出 - -###### 更新日期 2017-07-20 - -在工具中使用,可以根据广告平台或广告标签导出新增用户的具体信息,比如新增帐号,IDFA 等 - -![](/img/changelog/2017_07_25_6.png) - ---- - -## 操作日志 - -###### 更新日期 2017-07-13 - -可在企业设置里查看操作日志,目前已上线项目、权限、广告三种日志类型,方便您更加快捷地了解项目状态、人员及权限变化、广告投放等情况。 - -![](/img/changelog/2017_07_25_5.png) - ---- - -## 实时在线 - -###### 更新日期 2017-07-12 - -「实时在线」功能可帮你进行实时在线分析,通过分钟级在线量展现实时现数据动态。 -您可在[【工具-拓展功能】](//tapdb.com/dm/m/t/extensions)或[【在线分析】](//tapdb.com/dm/m/g/active/online)中自主开启并使用了。 - -![](/img/changelog/2017_07_25_4.png) - ---- - -## 180/360 日 LTV - -###### 更新日期 2017-07-07 - -运营和广告下增加 180/360 日 LTV - -- 运营统计中可在付费-用户价值中查看 -- 广告监测中可在自定义列中添加并查看 - ---- - -## 对比日期 - -###### 更新日期 2017-06-02 - -对比日期,在运营和广告模块增加对比日期功能,可以对不同时间段的数据进行对比,在图和表中均可查看对比数据的详情 - -![](/img/changelog/2017_07_25_3.png) - ---- - -## 现在可以钉住常用的对比日了 - -###### 更新日期 2017-05-25 - -在选择游戏概览中的对比日期时,新增了历史日记录功能,最多记录您最近 5 次选择的日期,您可以钉住其中常用的日期,并为之特殊命名,钉住的日期将一直保存,方便您下次使用。 - -![](/img/changelog/2017_07_25_2.jpeg) - ---- - -## 为渠道命名 - -###### 更新日期 2017-04-16 - -可以对分包渠道的名称进行备注,在工具栏里可以使用 - -![](/img/changelog/2017_07_25_1.jpeg) - ---- - -## 存疑新增 - -###### 更新日期 2017-04-16 - -广告页面增加存疑新增功能,标记了匹配了多个广告新增设备 diff --git a/docs/sdk/tapdb/faq/_category_.json b/docs/sdk/tapdb/faq/_category_.json deleted file mode 100644 index 9af634a73..000000000 --- a/docs/sdk/tapdb/faq/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "常见问题", - "position": 6 -} diff --git a/docs/sdk/tapdb/faq/ads.mdx b/docs/sdk/tapdb/faq/ads.mdx deleted file mode 100644 index 58367722e..000000000 --- a/docs/sdk/tapdb/faq/ads.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: 广告相关问题 -sidebar_position: 3 ---- - -### Q:如何创建广告计划? - -A:进入 TapDB,广告页面,点击广告管理,选择右上角的新增广告活动,选择广告平台,注意看下匹配方式,填写信息,生成投放链接。 - -### Q:什么是 IP 匹配? - -A:IP 匹配是通过用户点击广告时的 IP 和打开游戏时的 IP 是否一致进行匹配的,适用所有广告。创建广告活动时如果没有发现对应平台可以创建自定义广告。 - -### Q:什么是设备匹配? - -A:设备匹配是 TapDB 和广告平台已经对接完成,广告平台将点击广告的数据返回 TapDB,通过设备信息来匹配新增用户的,十分精准,但仅使用于和 TapDB 对接完成的广告平台。 - -### Q:IP 匹配和设备匹配怎么投放? - -A:TapDB 的广告匹配分为 IP 匹配和设备匹配: - -IP 匹配: 在新增广告活动时,选择广告平台,IP 匹配的广告平台都有标注,且 IP 匹配的广告需要填写下载链接,生成链接后,直接将 IP 匹配生成的投放链接填入广告平台的落地页 URL 即可。 - -设备匹配: 设备匹配填写相应参数后可以直接生成回调链接,填入广告平台的第三方监控链接(名称可能不同)中即可,落地页 URL 填写游戏本身的下载地址。 - -### Q:不能使用 TapDB 的链接投放广告时怎么办? - -A:比如说游戏的域名是 a.b.c,需要把 a.b.c 以 CNAME 的形式解析到 [l.tapdb.net](http://l.tapdb.net),然后如果 TapDB 给出的连接是 `https://l.tapdb.net/xxxx` ,直接替换 [l.tapdb.net](http://l.tapdb.net) 为 a.b.c,https 改为 http,变成 `http://a.b.c/xxxx` ,使用该链接投递广告。 - -### Q:为什么投放广告匹配到其他系统的玩家? - -A:广告匹配到其他系统的玩家可能有以下原因: - -(1)投放在 web 上,iOS 和 Android 用户都会被匹配。 - -(2)iOS 的投放链接,Android 用户(能看到该广告)可能会点击后主动去搜索该 app 下载,就会被匹配上,同理 Android 广告也可能被 iOS 用户匹配。 - -### Q:如何开启 TapTap 广告渠道的数据回调? - -A:前往广告->广告管理->渠道设置,可以为 TapTap 广告渠道开启数据回调,目前支持回调激活、注册、付费、次留数据 \ No newline at end of file diff --git a/docs/sdk/tapdb/faq/tran.mdx b/docs/sdk/tapdb/faq/tran.mdx deleted file mode 100644 index 83886edcd..000000000 --- a/docs/sdk/tapdb/faq/tran.mdx +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: 接入问题 -sidebar_position: 1 ---- - -### Q:SDK 初始化成功,为什么没有新增数据? - -A:在排查之前,你要粗略了解数据上报的流程: - -一、游戏 App 调 SDK 的相关接口上报数据; - -二、DB 平台收到上报的数据,并在「上报明细」展示; - -排查方法也是基于这个流程一步一步进行。 - -打开「配置」-「上报明细」,然后进行排查: - -1、检查初始化是否是最早调用?若没有最早调用,请调整后重试; - -2、观察「上报明细」功能里是否显示上报的数据;如果显示数据,则表明数据正常上报;若不显示数据,则进入 3; - -3、打开手机抓包工具,测试设备是否正常上报数据; - -### Q:SDK 初始化成功,为什么新增数据不正确? - -A:你可以使用「上报明细」功能来验证埋点上报准确性。 - -### Q:如何查看上报的数据? - -A:你可以通过「上报明细」和「埋点管理」来查看上报的数据 - -「上报明细」:一般在 SDK 的接入调试阶段以及在埋点测试时,你可以使用「上报明细」查看实时上报的埋点数据; - -「埋点管理」:可以查看最近 7 日项目内数据接收情况,快速了解埋点上报整体情况,以及错误上报详情与抽样示例; - -### Q:为什么 SDK 初始化失败? - -可以尝试对 gameversion 字段调试,若无值,需填入值。 - -### Q:单机游戏的 userId 如何储存? - -A:单机游戏的 userId 需要注意以下几点: - -(1)iOS 得自己生成一个 ID 存到证书空间,iOS 有个存储空间,一个是应用,一个是证书(企业)。 - -(2)安卓尽量存到 SD 卡,用一个用户操作或者 1 分钟后调用 `setUser`。 - -(3)随机生成一个唯一用户 ID,并保存到本地。 - -### Q:为什么 SDK 接入后没有新增数据? - -A:请检查 SDK 初始化是否调用成功,另外初始化应该最早调用。若初始化失败,则根据失败日志做对应处理。 - -### Q:服务端和客户端都需要传递充值数据么? - -A:在服务端和客户端选择一种方式传递充值数据即可。若同时传递,则充值数据会翻倍。 - -### Q:为什么服务端传递充值数据 TapDB 页面没有显示收入或者收入比实际要多? - -A:首先服务端和客户端的接口只能用其中一个(若都使用的话会展示双倍充值金额,并确保充值成功再发送充值数据),其次服务端「identify」: 「user_id」里的 user_id 和文档「纪录一个玩家」中的 setUser 里的「userId」需要保持一致。 - -### Q:为什么实时在线数据发送后 TapDB 显示没有数据? - -A:没有显示数据可能有以下几种原因: - -(1)检查文件格式,参数类型(返回 400 报错说明格式不对) - -(2)注意时间戳单位(秒),并且只能发送近 7 日数据,太早的数据不会保存 - -**(3)注意:是否有必须头信息:`Content-Type: application/json`** - -### Q:可以为空的参数能不填么? - -A:不能,可以为空的参数填 `null`。 - -### Q:为什么填了 channel 在 TapDB 里找不到这个分包渠道? - -A:必须要有一个新增数据 TapDB 才能接收到这个渠道,才能在这个页面里显示这个分包渠道。 - -### Q:TapDB 上报事件时,事件是大小写敏感的吗? - -A:所有的 key **不区分**大小写,所有的 value **区分**大小写。事件名是 value,属性名是 key,属性值是 value。 - -### Q:上报自定义事件时,属性类型必须和登记的完全一致吗?是否有什么兼容策略? - -A:必须完全一致,无兼容策略。错误类型的会被 agent 直接当做脏数据抛弃 - -### Q:能否进行私有化部署? - -A:暂时不支持私有化部署。但我们会保证你的数据安全。 - -### Q:如何设置权限? - -A:在「企业设置」-「权限管理」-「编辑成员」可以对相应账号进行编辑和权限去除操作。 - -### Q:自定义事件上报后,后台查不到数据 - -A:请先检查代码中是否设置了 [user_id](/sdk/tapdb/sdk/client-side-integration/#设置账号-id) 。如果不确定的话可以去后台使用 SQL 查询,在结果中可以看到曾经上报过的 user_id。 - -![ SQL 查询地址](/img/数据分析-Sql查询.png) - -```SQL -SELECT * FROM hive_saas1.tapdb."users" -WHERE user_id LIKE'%dTJTp6sA+OWsZ7Jf0JmGg==%' -LIMIT 100 -``` -检查上报的 user_id 里有没有需要上报的用户的 user_id。 - - diff --git a/docs/sdk/tapdb/faq/user.mdx b/docs/sdk/tapdb/faq/user.mdx deleted file mode 100644 index 839427a35..000000000 --- a/docs/sdk/tapdb/faq/user.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: 使用问题 -sidebar_position: 2 ---- - -### Q:如何查看各种维度下的数据? - -A:在来源,在线,留存,付费四个页面里的表格中都有主维度的切换选项,每个维度下显示的数据都不同,可以根据不同情况来查看各个维度下的数据。 - -### Q:什么是分包渠道? - -A:分包渠道即为上报数据时其中的 channel 字段信息,方便大家对数据进行拆分。接入方法详见 [TapSDK 初始化](/sdk/tapdb/sdk/client-side-integration#tapsdk-init)。 - -### Q:如何使用过滤条件? - -A:过滤条件可以根据不同的选项,不同的范围来选择 TapDB 表格内展现的数据,方便区分各种类型的用户。可以添加多个过滤条件来精确筛选满足条件的数据,也可以保存设定好的过滤条件方便下次使用。 - -### Q:为什么来源的收入比实际的收入要少? - -A:来源只显示新增用户的付费收入情况,老用户的付费情况请在付费页面查看。 - -### Q:为什么游戏的转化率很低? - -A:转化率低可能有以下几种原因: - -(1)技术人员确保 SDK 在用户登录的时候调用了 `setUser`。 - -(2)运营人员注意是否有大量的新增设备,可能有作弊机器在刷新增。 - -### Q:如何查询项目接入了什么版本的 TapDB SDK? - -A:在事件分析中,使用「事件发生时的 SDK 版本」作为维度,「任意事件」作为指标查询即可。 - -### Q:如何验证埋点上报数据准确性? - -A:使用埋点管理验证埋点准确性。 diff --git a/docs/sdk/tapdb/features.mdx b/docs/sdk/tapdb/features.mdx deleted file mode 100644 index 6f35f6beb..000000000 --- a/docs/sdk/tapdb/features.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: TapDB 功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -TapDB 是一套专注于解决游戏项目数据需求的分析工具,致力于帮开发者实现低成本、高效率的接入与查询体验。 - -## 初衷 - -同为游戏开发者,我们深知做游戏的不易。思考如何打磨游戏产品就足够令人头疼了,选择一套适合自己的数据工具并正确地用起来可能会耗费相当的精力且效果一般。于是我们相信提供一套低门槛又足够好用、精确的工具一定是有高价值的。 - -在做游戏的这些年,我们积累了一些自己认可的经验和方法,希望可以通过 TapDB 这个产品将这些经验和能力共享给大家。我们坚信 TapTap 和良好的行业生态具有共生性:帮助开发者创造优质的游戏就是在帮助 TapTap 成长。 - -## 实用功能 - -TapDB 服务提供的主要功能有: - -### 基础 BI - -非常实用、无门槛上手的四大数据报表,沉淀着我们数十年的游戏从业经验。 - -### 衍生事件 - -为了开发者的使用体验,我们通过一系列衍生事件将基础 BI 的查询时间缩短至 1/10 不到,确保你能快速访问所需要的数据。 - -### 广告投放跟踪 - -对接了全球主流广告投放系统(如巨量引擎、AMS、AppsFlyer 等),轻松完成广告投放追踪、回调以及数据分析。 - -### 权限角色 - -给每个使用者配备独立账号并单独控制权限,确保数据安全。 - -### 自定义事件分析 - -- 自由定制你所想要的事件,关注那些你最关心的行为。 - -- 多维透视,快速拆解数据,助你更高效地找到问题根源。 - -- 日志级查询能力,用户做了什么了如指掌。 - -## 优势和特色 - -- 低门槛接入:接入基础事件非常简单,而这已足够让你获得非常完善的分析和广告投放能力。 - -- 完全无延迟:上报后可以立刻查到数据,时间就是生命。 - -- 保持更新的游戏分析框架:我们会将最新的经验和方法持续地分享给你。 - -- 结合 TapTap 生态的数据,为开发者提供全链路的游戏数据分析能力。 - -- 免费使用。 - -:::tip -如需使用自定义事件分析和看板功能,请通过工单联系我们申请开通。 -::: diff --git a/docs/sdk/tapdb/features/_category_.json b/docs/sdk/tapdb/features/_category_.json deleted file mode 100644 index 875bf03cc..000000000 --- a/docs/sdk/tapdb/features/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "功能指南", - "position": 4 -} diff --git a/docs/sdk/tapdb/features/custom-event/_category_.json b/docs/sdk/tapdb/features/custom-event/_category_.json deleted file mode 100644 index 38c4107a3..000000000 --- a/docs/sdk/tapdb/features/custom-event/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "自定义事件", - "position": 1 -} diff --git a/docs/sdk/tapdb/features/custom-event/ad.mdx b/docs/sdk/tapdb/features/custom-event/ad.mdx deleted file mode 100644 index 59cfd3270..000000000 --- a/docs/sdk/tapdb/features/custom-event/ad.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: 广告管理 -sidebar_position: 16 ---- - -## 1. 广告概览 - -对于互联网产品,每天的新增用户的数量是个非常受关注的指标。同时,围绕着新增用户这个指标,还可以有很多更加深入的分析,例如: - -- 分析不同渠道带来的新增用户的数量,从而优化后续的投放策略; -- 分析不同渠道带来的新增用户的留存以及转化,从而考察不同渠道带来的用户的质量; -- 分析每天的收入,由不同渠道带来的贡献的比例; - -在广告概览里,可以看到所有广告下的概览数据。也可以看到各个广告平台下各个广告链接的投放数据。 - -![](/img/customEvent/ad/广告概览new.png) - -## 2. 广告管理 - -你可以在广告管理功能里,对广告进行管理,包含广告设置、成本管理、归因设置、查看已删除广告。 - -### 2.1 广告设置 - -你可以对广告活动进行新增、编辑、查询和删除操作。具体功能如图: - -![](/img/customEvent/ad/广告管理.png) - -A:新增广告活动
    -点击后你可以创建广告,输入广告名称、选择要投放的广告平台、选择广告标签、选择子维度。 -点击「生成投放链接」,系统会自动生成投放链接。 - -![](/img/customEvent/ad/新增广告.png) - -B:标签管理
    -广告标签是你为每个广告活动做的备注和标记,可以让你快捷的筛选出具备某个特点的广告活动。在标签管理里可查看已有的标签及标签对应的广告活动,还可以创建新的标签,同时可删除不需要的标签。 - -![](/img/customEvent/ad/标签管理.png) - -C:搜索广告
    -输入搜索词,可以基于广告活动名称进行搜索。 - -D:创建类似广告
    -当你一次性需要创建多个相似广告进行监测时,可以点击创建类似广告,对每个广告活动进行标记。 - -E:编辑广告
    -对广告活动进行编辑 - -F:删除广告
    -已删除的广告活动会显示在这里,你可以恢复广告活动,删除超过 7 天的广告将自动清除。 - -### 2.2 成本管理 - -在成本管理页面可以看到每个广告活动每天的成本,同时广告概览页面里和成本相关的指标也会基于这里的数据进行计算。前提是你要先将成本录入系统。你可以直接在成本管理界面录入,也可以点击「成本导入」以模板的形式批量导入成本。 - -![](/img/customEvent/ad/成本管理.png) - -![](/img/customEvent/ad/上传成本.png) - -### 2.3 归因设置 - -可设置归因「默认窗口期」,同时针对每个平台可以设置单独的归因窗口期。当广告平台未设置单独的归因窗口期时,归因窗口期将跟随系统的窗口期。 - -![](/img/customEvent/ad/归因设置.png) - -### 2.4 已删除广告 - -勾选已删除的广告活动后点击「恢复」,可以恢复对应的广告活动。 - -![](/img/customEvent/ad/恢复广告.png) - -### 2.5 工具-创建链接 - -在「工具」-「创建链接」功能里,你可以把广告数据分享给其他人。你可以选择分享给普通外部成员和广告代理商: - -1. 分享给普通外部成员:对方只能查询广告相关数据; -2. 分享给广告代理商:对方可以查询广告相关数据、在标签内创建修改广告以及管理标签内广告成本。 - -![](/img/customEvent/ad/创建链接.png) diff --git a/docs/sdk/tapdb/features/custom-event/alert.mdx b/docs/sdk/tapdb/features/custom-event/alert.mdx deleted file mode 100644 index 9bf4abeb1..000000000 --- a/docs/sdk/tapdb/features/custom-event/alert.mdx +++ /dev/null @@ -1,173 +0,0 @@ ---- -title: 预警管理 -sidebar_position: 18 ---- - -## 1. 概述 - -通过设置指标和筛选分组项,构建一组时间序列数据。按照时间颗粒度定时查询并与历史数据进行比较,触发预警规则后通知项目组成员。 - -目前支持以「事件分析」的形式设置指标,以邮件方式进行预警通知。 - -![概述](/img/customEvent/alert_1.png) - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ---------- | ------------------------------------- | -| 分析师 / 业务人员 | 对重点或异常数据进行预警监控,实时掌握产品动态,并可在第一时间发现异常问题 | - -## 3. 新建数据预警 - -点击预警列表右上角的新建预警。 - -![新建数据预警](/img/customEvent/alert_2.png) - -### 3.1 基础信息 - -在「基础信息」部分依次录入或选择预警名、查询主体与预警指标。 - -![填写基础信息](/img/customEvent/alert_3.png) - -预警名展示在预警列表中,是识别一个预警的依据。 - -查询主体可选择「账号」或「设备」,与「事件分析」中查询主体类似。 - -### 3.2 预警规则 - -#### 3.2.1 添加预警规则 - -在预警规则中,可选择分组维度,并通过多选分组值同时构造多组时间序列数据,当无分组维度时,默认分组项为「总体」。 - -![预警规则](/img/customEvent/alert_4.png) - -可选预警时间粒度、比较基准与参数类型的关系如下表: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    时间粒度比较基准参数类型
    固定值数值
    上一天、上周同一天数值、百分比
    过去7天均值、过去30天均值数值、百分比、标准差
    小时固定值数值
    上一小时、昨天同一小时数值、百分比
    过去24时均值数值、百分比、标准差
    - -每个预警下可同时设置多条预警规则,每个预警规则下可添加多个分组。 - -#### 3.2.2 预警规则详情 - -**数值** - -固定值作为比较基准时: - -高:指标实际值 > 数值 - -低:指标实际值 < 数值 - -非固定值作为比较基准时: - -高:指标实际值 > 比较基准 + 数值 - -低:指标实际值 < 比较基准 - 数值 - -**百分比** - -高:指标实际值 > 比较基准 * (1 + 百分比) - -低:指标实际值 < 比较基准 * (1 - 百分比) - -**标准差** - -高:指标实际值 > 比较基准 + 参数 * 标准差 - -低:指标实际值 < 比较基准 - 参数 * 标准差 - -其中,标准差为比较基准均值对应的数量与时间颗粒度周期内的所有指标实际值的标准,如「过去 7 天均值」对应的标准差为,过去 7 天每天的指标实际值的标准差。 - -### 3.3 通知设置 - -目前支持邮件,[企业微信群](https://open.work.weixin.qq.com/help2/pc/14931?person_id=1&is_tencent),[Slack](https://api.slack.com/messaging/webhooks) 渠道进行预警通知。 - -![通知设置](https://capacity-files.lcfile.com/skDpU63nDH6cMru3StmXsvcmsid3pXtp/push.png) - -## 4. 预警通知 - -根据每条预警规则设置的时间颗粒度,系统会在每天或每小时结束后对预警指标按照设置的分组进行查询。 - -触发预警规则后将对通知设置中的渠道进行告警,如图: - -![邮件示例](https://capacity-files.lcfile.com/J7DoenoNMw4NbW0Qq8nTseY65aeKIt5l/push_1.png) - -![企业微信群示例](https://capacity-files.lcfile.com/An78t1qdEnvrWzENHBgLHXY9s9wMdLcN/push_2.png) - -![Slack 示例](https://capacity-files.lcfile.com/1W2n3oKOqeu4KDVH8IsC1lqY0hCIxNpe/push_3.png) - - -## 5. 预警的管理与详情 - -### 5.1 预警列表管理 - -已创建的数据预警会以列表的形式展示在数据预警页,可对预警进行详情查看、启动 / 暂停、编辑、复制、删除等操作。 - -![预警列表管理](/img/customEvent/alert_7.png) - -数据预警的使用进度以「预警实例」作为最基本的单位,预警规则下的一个分组为一个「预警实例」,上限为 30,可通过删除预警、预警规则或勾除分组释放使用进度。 - -### 5.2 预警详情 - -点击预警列表中的预警名跳转至该预警的详情页,点击预警规则跳转至该预警详情页,并筛选相应的预警规则。 - -![预警详情_1](/img/customEvent/alert_8.png) - -页面左侧可看到该预警的「基础信息」、「预警规则」与「通知设置」。 - -页面右侧可看到当前筛选的预警规则与分组数据的历史趋势图与数据表,有预警发生的时刻以红点形式标记在趋势图中,可点击下载按钮下载数据表。 - -![预警详情_2](/img/customEvent/alert_9.png) - -## 6. 最佳实践 - -### 6.1 通过「固定值」预警监控业绩指标 - -对于数值确定的业绩目标,如:充值金额、活跃用户数,可通过「固定值」预警监控其是否完成业绩目标。 - -### 6.2 通过「百分比」预警监控指标环比、同比变化 - -对于日常运营指标,如:活跃用户数、新增用户数,可通过「百分比」预警密切关注其变化趋势,一旦出现异常变化趋势,便于及时关注并干预。 - -### 6.3 通过「标准差」预警监控具有长期变化趋势的指标的短期异常变化 - -对于具有长期增长或衰减的指标,「标准差」预警可消除长期变化趋势对于短期变化的影响,从而监控该指标的短期异常变化情况。 - -### 6.4 多规则预警监控预警区间 - -对于某些重要指标,如:活跃用户数,可创建多条预警规则,同时监控是否完成业绩目标、变化趋势情况等,同时可以对同类监控规则创建「高」、「低」2 条规则,对指标的变化区间进行监控。 - -### 6.5 多分组预警探查异常数据维度 - -对于某些发生了异常变化的指标,可通过拆分分组,找到引起变化的主要维度,便于进一步进行下钻分析。 diff --git a/docs/sdk/tapdb/features/custom-event/cluster.mdx b/docs/sdk/tapdb/features/custom-event/cluster.mdx deleted file mode 100644 index f2d65ffd4..000000000 --- a/docs/sdk/tapdb/features/custom-event/cluster.mdx +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: 用户分群 -sidebar_position: 9 ---- - -## 1. 概述 - -将符合一定属性和行为特征的用户划分为一个群体,并对该群体进行研究和分析的方式,即用户分群。 - -TapDB 的分析模型中分别以「账号」、「设备」作为查询主体进行查询,在用户分群中同样支持分别以「账号」、「设备」作为主体进行分群。 - -目前提供「条件分群」、「ID 分群」、「结果分群」3 种分群方式。 - -![概述](/img/customEvent/cluster_summary.png) - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ---------- | ---------------------------- | -| 分析师 / 业务人员 | 划分特定用户群体,用以聚焦分析、排除干扰或导出用户列表等 | - -## 3. 创建用户分群 - -### 3.1 条件分群 - -点击分群列表右上角的新建分群,选择「新建条件分群」;此处请注意分群使用进度。 - -新建条件分群页分为「基础信息」、「分群规则」两部分。 - -![新建条件分群](/img/customEvent/cluster_create_condition_cluster1.png) - -在「基础信息」页,依次录入或选择「分群名、分群主体、分群代码、更新方式、备注」。 - -分群名展示在分群列表、分析模型中,是业务人员识别分群的依据。 - -分群主体,支持「账号 ID」或「设备 ID」,根据业务场景做选择。 - -分群代码是分群存储在系统后台的唯一标识,为方便数据分析人员直接查询数据库表,可命名为带有业务含义的参数名。 - -更新方式分为「手动更新」与「自动更新」。「手动更新」指在完成首次计算后,系统不会自动更新用户群,用户需要手动进行更新;「自动更新」会在每日 0 点后,以前一日作为基准进行用户群更新,可以设置更新延迟以确保所有前一日的数据都接收到,保证数据完整性。 - -![新建条件分群](/img/customEvent/cluster_create_condition_cluster2.png) - -在「分群规则」页,规则分为「属性」、「行为」两部分,两部分之间可以切换「且或」关系。 - -在「属性」规则部分,基于选择的分群主体,设置相应的属性条件,各个属性条件可以切换「且或」关系。 - -在「行为」规则部分,可分为「未做过事件」、「做过事件」两类,两类条件均可多次添加,行为条件可以切换「且或」关系。 - -「未做过事件」即,用户在选定的时间段内,未做过该行为。 - -「做过事件」即,用户在选定的事件段内,做过该行为,同时可以对行为发生的结果进行筛选。 - -### 3.2 ID 分群 - -点击分群列表右上角的新建分群,选择「新建 ID 分群」。 - -![新建 ID 分群](/img/customEvent/cluster_create_id_cluster.png) - -「分群名、分群主体、分群代码」与条件分群相同,不再赘述。 - -在新建 ID 分群页,上传 ID 文件,系统将把文件中的 ID 按照选择的「分群主体」与系统中已有的用户数据进行关联,找到符合条件的用户。 - -文件格式要求:ID 文件中每一行记录一个 ID 字段,使用 UTF-8 编码的 CSV 文本格式记录。若上传内容中存在未匹配项(即项目中不存在 ID 对应的用户),则该项会直接跳过,不会被纳入分群中。可下载模板作为参考。 - -### 3.3 结果分群 - -在各个分析模型中,如果指标是用户数(如事件分析中的「触发用户数」、留存或漏斗分析中某环节的留存或流失用户),则在结果报表中可点击「创建结果分群」来创建分群。 - -![新建结果分群](/img/customEvent/cluster_create_result_cluster1.png) - -创建分群时,可设置结果分群的名称以及备注,形成对结果分群的描述。 - -![新建结果分群](/img/customEvent/cluster_create_result_cluster2.png) - -结果分群不能修改创建规则以及更新,只能修改分群的名称以及备注。 - -## 4. 对用户分群的各类操作 - -创建的分群会以列表形式展示在用户分群页 - -![对用户分群的各类操作](/img/customEvent/cluster_operation.png) - -用户可以对分群进行查看、编辑、删除、更新、下载、复制操作,如下: - -| 操作 | 位置 | 效果 | -| -- | --- | ---------------- | -| 查看 | 分群名 | 查看分群详情 | -| 编辑 | 操作栏 | 进入编辑分群弹窗 | -| 删除 | 操作栏 | 删除分群 | -| 更新 | 操作栏 | 手动启动分群计算,并更新分群结果 | -| 下载 | 操作栏 | 下载当前分群结果的用户列表 | -| 复制 | 操作栏 | 新建一个与当前各参数相同的分群 | - -## 5. 使用用户分群 - -### 5.1 聚焦或排除部分用户 - -聚焦符合特定条件的高价值用户,如:付费金额大于 100 的用户,从而在分析模型中对强付费能力用户进行定向分析,了解其行为特征; - -排除符合特定条件的可疑用户,如:在同一设备上活跃过 3 个账号以上的设备,从而将可以的工作室设备进行排除,不再对正常用户的分析结果。 - -### 5.2 用户数据导入与输出 - -将外部用户数据导入系统创建分群,如:公司在其他游戏项目中已有一批高付费能力的用户、设备,因此可将其导入,探索其在本项目中的活跃、付费情况,更好的引导潜在高付费用户的付费。 - -下载分群结果作为其他系统的操作依据,如:导出疑似工作室设备、账号,从而在游戏运营系统中,对其进行处罚、封禁。 - -### 5.3 分析结果的下钻分析 - -结果分群基于分析模型的结果报表,因而非常适合作为下钻分析的基础。 - -例如,通过漏斗分析计算用户浏览商品、发起订单、支付订单的漏斗转化情况,发现大量用户在发起订单后便流失了。此时便可对该步骤的流失用户进行结果分群,通过各分析模型分析该分群用户的各属性、行为,探索其发起订单但却未成功支付的原因。 diff --git a/docs/sdk/tapdb/features/custom-event/data-model.mdx b/docs/sdk/tapdb/features/custom-event/data-model.mdx deleted file mode 100644 index 5c446b6c5..000000000 --- a/docs/sdk/tapdb/features/custom-event/data-model.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: 埋点设计指南 -sidebar_position: 2 ---- - -## 1. 从案例开始 - -你是否有过这种困扰:好不容易让一些用户接触到了自己的游戏,但在他们登录账号前就因为一些技术问题流失了不少。下面是一个我们利用自定义事件分析完成「用户从打开 App 到真正创建角色」过程中流失的真实案例。 -我们当时按照以下思路设计了分析步骤: - -**1. 确定分析目标**:了解用户在创建角色之前的流失情况; - -**2. 明确具体流程**:对用户打开 App 到创建角色的流程进行拆解: - -- 点击游戏 icon -- Unity 初始化 -- 出现安卓存储权限允许界面 -- SDK 初始化 (新增设备记录点) -- 弹出隐私协议确认框【选「否」会退出即 SDK 初始化失败】 -- 检查版本 -- 确认下载按钮(4G 环境下)/ WIFI 环境下自动下载 -- 开始下载资源 -- 资源下载中 -- 资源下载完成,进入登录界面 -- 点击 TapTap 登录按钮 -- 跳出弹窗,可选 Tap 打开或者 Tap 加速器打开 -- 选择完成,跳转唤起登录授权 -- 点击同意,返回游戏 -- TapTap 登录完成 (转化设备记录点) -- 游戏服务器认证用户 -- 登录游戏服务器 -- 输入昵称创角 -- 创建成功,进入大厅 - -**3. 定义分析指标**:根据上述流程,我们以漏斗思维,设计的几个关键指标为: - -- 资源确认下载率(确认下载的用户 / 更新弹窗的曝光用户); -- 资源下载成功率(下载成功的用户 / 开始下载资源的用户); -- TapTap 登录率(登录成功的用户 / 点击登录的用户); -- 角色创建率(角色创建成功的用户 / 登录成功的用户); - -**4. 明确事件**:一般来说,事件 Event 会有三个类型: - -| 事件类型 | 描述 | -| ---- | ----------------- | -| 曝光 | XX 页面的曝光、XX 弹窗的曝光 | -| 点击 | XX 按钮的点击 | -| 系统事件 | 初始化、检查版本、资源下载等 | - -在上一步,我们确定了分析指标。接下来我们明确了统计哪些事件可以得到以上数据,初步确定了事件名称和事件类型: - -- 曝光:【更新提示弹窗】 -- 点击:【更新提示弹窗 - 确认下载按钮】 -- 系统事件:【更新下载成功】 -- 点击:【首页 - TapTap 登录按钮】 -- 系统事件:【登录成功】 -- 系统事件:【成功创建角色】 - -**5. 定义事件属性**: -基于事件名和事件类型,我们整理了更完整的埋点文档如下: - -| 事件名 | 事件显示名 | 属性名 | 属性显示名 | 属性值 | 触发时机 | -| ---------------- | ---------------- | ----------- | ----- | ---------------- | --------- | -| pv_download | 更新提示弹窗 | #ts | 时间戳 | | 弹窗展示时触发 | -| click_download | 更新提示弹窗 - 确认下载按钮 | #networtype | 网络类型 | WiFi、4g、5g、3g、2g | 点击按钮时触发 | -| download_success | 更新下载成功 | #ts | 时间戳 | | 系统后台触发 | -| login_start | 首页 - TapTap 登录按钮 | #ts | 时间戳 | | 点击按钮时触发 | -| login_success | 登录成功 | #ts | 时间戳 | | 系统后台触发 | -| create_role | 创建角色 | #ts | 时间戳 | | 创建角色成功时触发 | - -通过以上步骤,我们就完成了埋点的设计。 -随后我们将准确无误的埋点信息(事件名、事件显示名、属性名、属性显示名)录入 TapDB 的「**配置**」-「**事件管理**」。 -研发根据的埋点文档进行埋点开发,开发完成后进行数据校验。 -埋点上线后,我们使用「事件」和「看板」功能对「用户从打开 App 到真正创建角色」过程流失用户进行分析,如下图所示: - -![](/img/customEvent/49e3e7c0d12cd20cdd4f4aed2c8d0044.png) - -「更新下载成功(步骤 3)」到「首页 - TapTap 登录按钮(步骤 4)」的转化率非常低,接下来就需要进行详细的数据分析。点击 [TapDB 使用指南](/sdk/tapdb/features/custom-event/event-analyse) 查看详细数据分析方法。 - -## 2. 埋点设计思路 - -我们根据这个案例总结了以下思路,建议参考以下步骤进行埋点设计: -**1. 确定分析场景**; -**2. 明确具体流程**; -**3. 定义分析指标**; -**4. 明确事件:设计 Event,确定其类型,名称等细节**; -**5. 定义事件属性:明确所需要的信息,通过事件属性完成上报**; - -## 3. 其他设计技巧 - -### 3.1 Event 的同类抽象 - -在进行事件设计时,可能会遇到以下问题: - -1. 要统计三个关卡 A、B、C 的通关情况,对每个关卡设计一个通过事件吗? -2. 在设计「申请验证码」的功能埋点时,用户注册时、用户登录时、修改密码时等多场景都会下「申请验证码」,对每个场景设计一个「申请验证码」事件吗? -关于不同场景下的行为是否设置为同一个 Event,我们可以基于同类抽象判断原则: - -- 重要的事件行为或者特别关注的事件行为,可以单独将事件行为作为单独 Event 进行跟踪和属性设置; -- 重要程度一般的行为,比如只需要分析参与次数、参与人数的行为,可以将多个相似类型的行为设置成一个 Event,通过属性的方式标识具体的行为; - -1. 不同关卡通关情况的事件设计:如果简单统计三个关卡 A、B、C 的通关情况时,不需要做成 「A 关卡通关」、「B 关卡通关」、「C 关卡通关」 三个事件,只需设计成 「关卡通关」 事件,将 A、B、C 三个关卡名以属性 「关卡名称」 进行标识。 -2. 「申请验证码」的功能埋点:可将其定义为一个事件「申请验证码」,并在属性字段增加 「场景」,借此从场景来区分用户究竟在什么情况下申请验证码。 - -### 3.2 Event 的命名规范 - -在设计事件显示名时,注意确保事件没有二义性。按照 页面名-模块名-具体事件名的方式来命名,可以帮助分析师通过名字与备注准确理解事件所代表的用户行为。要避免因为说明的不准确而引发错误的分析结论。 -对 App 页面和模块命名进行统一的梳理和维护是有意义的准备工作,它帮助我们确保了所有人的理解是一致的。 diff --git a/docs/sdk/tapdb/features/custom-event/dimension-table.mdx b/docs/sdk/tapdb/features/custom-event/dimension-table.mdx deleted file mode 100644 index 46a5387fc..000000000 --- a/docs/sdk/tapdb/features/custom-event/dimension-table.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: 维度表属性 -sidebar_position: 13 ---- - -## 1 概述 - -对于已经上报的事件属性和用户属性,可以通过上传维度表将原先上传的数据映射为另一种展示值或计算值,使初始埋点时的属性值不与展示值相同。 - -维度表属性相比于虚拟属性,可从系统外部引入原先上传的数据中未包含的信息,而非仅基于系统内数据的逻辑转化。 - -## 2 适用角色与用途 - -| 角色 | 用途 | -| :------------------------ | :------------------------------- | -| 埋点设计人员(数据产品经理 / 分析师) | 避免埋点设计中的过度冗余字段,提高数据模型范式度和埋点设计灵活度 | -| 数据分析人员(数据产品经理 / 分析师 / 运营) | 自助引入系统外部数据作为分析维度,满足更多个性化分析需求 | - -## 3 创建维度表属性 - -可在事件属性管理、用户属性管理中「维度表」列,创建该属性的维度表属性。 - -维度表属性可基于预置属性、自定义属性和虚拟属性创建,但不包括以下类型的虚拟属性: -1. 基于维度表属性创建的虚拟属性; -2. 基于用户属性创建的虚拟事件属性。 - -![创建维度表属性](/img/customEvent/dimension_table_1.png) - -### 3.1 上传入口 - -在需要添加维度表的基础属性字段上选择「上传」维度表属性。 - -![上传入口](/img/customEvent/dimension_table_2.png) - -### 3.2 文件编码要求 {#encoding} - -系统支持 `UTF-8` 或 `UTF-8 with Bom` 编码格式的 `CSV` 文件,文件大小不超过 2G。 - -可使用 Excel 在保存文档时选择「**CSV UTF-8(逗号分隔符)(.csv)**」格式; - -![Excel](/img/customEvent/dimension_table_7.png) - -或使用 WPS 在保存文档时选择「**CSV (逗号分隔符)(*.csv)**」格式。 - -![WPS](/img/customEvent/dimension_table_8.png) - -同样可使用 Sublime、NotePad ++ 等文本编辑工具,将文件以 `UTF-8` 或 `UTF-8 with Bom` 编码保存。 - -![Sublime、NotePad ++ 等](/img/customEvent/dimension_table_9.png) - -### 3.3 文件格式要求 - -首行内容将作为维度表属性的字段名称,需要以英文开头,英文、数字或 `_` 组成。 - -首列内容将和原始属性进行关联,取值需要和原始属性值对应,并保证值唯一,如遇到重复,则以首条为准,后续重复信息将被抛弃。 - -维度表属性不超过 10 列,内容会与录入的该属性的数据类型进行适配,规则如下: - -| 所选数据类型 | 文件内容 | -| :----- | :----------------------------------------------------------- | -| 文本 | 任何内容均可 | -| 数值 | 数字均可,0 开头的,把 0 抹掉 | -| 时间 | 时间戳,或 yyyy-MM-dd HH:mm:ss.SSS、yyyy-MM-dd HH:mm:ss、yyyy-MM-dd | -| 布尔 | true 或 false | - -对于所选数据类型与文件内容类型不一致的行,该行将被完全抛弃,请注意保持数据类型一致。 - -### 3.4 填写显示名和字段类型 - -根据需求填写映射后的维度表字段显示名和数据类型。 - -![填写显示名和字段类型](/img/customEvent/dimension_table_3.png) - -### 3.5 解析结果 - -展示解析总行数、成功行数、错误丢弃行数与错误丢弃原因。 - -![解析结果](/img/customEvent/dimension_table_4.png) - -### 3.6 替换 - -若解析结果不符合预期,可更新文件后进行替换。 - -![替换](/img/customEvent/dimension_table_5.png) - -## 4 维度表属性的使用 - -### 4.1 管理维度表属性 - -可在事件属性管理或用户属性管理页面中,对维度表属性进行管理。 - -维度表属性折叠在基础属性列中,展开后可进行进一步操作。 - -![管理维度表属性](/img/customEvent/dimension_table_6.png) - -### 4.2 模型中使用的注意事项 - -维度表属性在使用上和通常的属性一致,根据类型决定其计算逻辑以及筛选条件。 - -事件属性的维度表属性可以在其基础属性关联的事件中被使用。 - -用户属性的维度表属性,可用场景等同一般的用户属性。 - -## 5 最佳实践 - -### 5.1 提高埋点设计灵活度 - -采集用户的游戏商城浏览、下单信息时,可只采集商品 ID,商品名称、售价等信息可通过基于「商品 ID」创建维度表属性满足分析需求。 - -| 商品 ID(基础属性) | (维度表属性) | 售价(维度表属性) | -| :---------- | :------ | :-------- | -| 1 | 补签券 | 50 | -| 2 | 世界频道喇叭 | 10 | - -### 5.2 引入外部信息作为分析维度 - -系统中采集了用户设备的机型信息,结合通过爬虫收集各机型在电商平台的售价信息,从而得到各类机型的档次,用以对辨别用户价值。 - -| 机型(基础属性) | 售价(维度表属性) | 用户价值(维度表属性) | -| :------- | :-------- | :---------- | -| model_1 | 999 | 低 | -| model_2 | 1299 | 低 | -| model_3 | 1999 | 中 | -| model_4 | 4999 | 高 | diff --git a/docs/sdk/tapdb/features/custom-event/distribution-analyse.mdx b/docs/sdk/tapdb/features/custom-event/distribution-analyse.mdx deleted file mode 100644 index c66598e5e..000000000 --- a/docs/sdk/tapdb/features/custom-event/distribution-analyse.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: 分布分析 -sidebar_position: 6 ---- - -## 一、引入案例 - -玩家付费是一个非常重要的游戏内行为,我们希望了解最近一周玩家在游戏内的付费总金额分布情况,例如:0-100 元,101-200 元,201-300 元 ..., 的用户数量分别是多少。这时候,我们可以用「分布分析」的功能来呈现。 - -**1、设置事件**:我们选择事件「用户付费」,设置自定义区间,以 100 作为分段; - -![](/img/customEvent/distribution/fenbu-1-1.png) - -**2、设置维度**:我们关注的每天的金额分布变化情况,所以在维度里选择「事件发生时间」; - -![](/img/customEvent/distribution/fenbu-1-2-2.png) - -**3、设置展示结果**:设置时间段和其他参数,并点击查询查看分析结果; - -![](/img/customEvent/distribution/fenbu-1-2-3.png) - -**4、保存报表**:将查询结果保存为报表。再将报表转变为看板,就可以每天方便快捷的看分布分析。 - -![](/img/customEvent/distribution/fenbu-1-4.png) - -## 二、什么是分布分析 - -分布分析功能,主要用来了解不同区间事件发生频次,不同事件计算变量加和,以及不同页面浏览时长等区间的用户数量分布。 - -## 三、分布分析的支持场景 - -分布分析可以帮助揭示以下问题:
    - -- 新版本上线后,用户每天玩游戏的次数是否增加? -- 不同广告渠道来源带来的用户,在不同金额区间,例如:0-100 元,101-200 元,201-300 元 ..., 的用户数量有什么差异; -- 不同层级的玩家(新玩家、普通玩家、重度玩家)玩游戏的时长分布有什么差异; - -## 四、如何做分布分析 - -如案例所述,分布分析可以分为 4 个步骤:**设置事件、设置维度、设置展示结果、保存报表**。 - -后面三个步骤在**事件分析**里均有比较详细的介绍,所以这里重点介绍设置事件。 - -在分布分析页面,点击「选择指标」,可以看到「指标选择」界面。指标的具体设置方式,你可点击事件分析查看。点击「设置」按钮,可设置计算结果的展示类型「离散」、「默认区间」和「自定义区间」:
    -**「离散」**:系统将展示每个数字下的分布值;
    -**「默认区间」**:系统会根据计算结果展示默认区间分布;
    -**「自定义区间」**:你可以自己设置每个区间段的起始值和结束值; - -![](/img/customEvent/distribution/fenbu-1-1.png) - -## 五、分布分析的计算原理 - -分布分析有 2 种统计方法,按次数统计和按事件属性的统计指标:
    -**按次数统计**:统计用户在一天 / 周 / 月中,进行某项操作的次数,发生一次就记录一次。
    -**按事件属性的统计指标统计**:统计用户在一天 / 周 / 月中,发生事件的某属性的统计指标值。属性的统计指标与事件分析一致,有总和、均值、最大值、最小值、去重数。 diff --git a/docs/sdk/tapdb/features/custom-event/event-analyse.mdx b/docs/sdk/tapdb/features/custom-event/event-analyse.mdx deleted file mode 100644 index e4fadf539..000000000 --- a/docs/sdk/tapdb/features/custom-event/event-analyse.mdx +++ /dev/null @@ -1,266 +0,0 @@ ---- -title: 事件分析 -sidebar_position: 3 ---- - -## 一、引入案例 - -新增用户和 DAU 作为常用指标,经常需要分析和展示,我们可以用「事件分析」的功能来对场景指标进行分析和呈现。 - -**1、确定指标和筛选条件**:要分析的是账号下的 DAU 和新增用户,分 iOS 和 Android 两端显示。 - -点击「选择指标」,在弹出的窗口里我们选择事件和筛选条件,并且对指标重命名(我们也可以在事件分析的主界面进行筛选条件的设置)。 - -![](/img/customEvent/event/event-2.png) - -**2、确定维度**:iOS 和 Android 就是我们分析指标的维度,所以我们在维度选择里选择「设备系统类型」。 - -![](/img/customEvent/event/event-3.png) - -**3、选择时间段**:我们需要关注最近 30 天的数据变化,所以在日期选择器里选择「近 30 日」 - -![](/img/customEvent/event/event-4.png) - -**4、选择对比时间段**:我们用最近 30 天的数据变化,对比前一个月。 - -![](/img/customEvent/event/event-5.png) - -条件已经设置完成,点击「查询」,输出结果。 - -![](/img/customEvent/event/event-6.png) - -**5、保存报表**:我们将查询结果保存为报表。再将报表转变为看板,就可以每天方便快捷的看最终报表数据。 - -![](/img/customEvent/event/event-7.png) - -**以上就是我们通过事件分析来查看分系统的日活用户的步骤**。 - -## 二、什么是事件分析 - -事件分析本质上就是对事件的 what、how、when、where 进行分析。 - -## 三、事件分析的支持场景 - -1、人数:有多少人触发了某一事件 - -- 比如 UV、DAU 指标趋势; -- 比如说同一个按钮的文案做了调整,调整前文案为 A,调整后文案为 B,进行对照试验,分析 A 和 B 的点击情况; - -2、次数:某一事件触发了多少次,比如对关卡每天的通关次数; - -3、人均次数:某一事件(行为)平均触发多少次; - -4、活跃比:在一个时间区间内,触发某一事件的人数占当前时间段内所有活跃人数的比; - -5、事件细分维度下的数据:查看按钮点击事件,我按照设备类型展开,我就可以得到不同的设备触发的任意事件次数,这样我们就可以知道不同设备上的事件发生情况; - -6、复杂指标的四则运算:当我们想要的数据没有在上报的数据里,怎么办?我们可以通过加减乘除进行指标计算得到一个新的指标,这种方式就叫自定义指标,比如 ROI,ARPU 值; - -## 四、如何做事件分析 - -就像案例里提到的 5 个步骤: - -**1、选择指标和筛选条件;** - -**2、选择维度;** - -**3、选择时间段;** - -**4、选择对比时间段;** - -**5、保存看板;** - -### 4.1 选择指标 - -在确定指标时,同时需确定是否开启「近似计算」。 - -#### 4.1.1 近似计算 - -开启之后仅查询部分样本的数据,准确率 99.9%,查询速度更快。 - -![](/img/customEvent/event/event-2-1.png) - -任意设置的调整,需点击「查询」按钮才能生效。 - -#### 4.1.2 选择指标界面 - -在选择指标界面,包括「选择事件」、「选择属性」、「重命名」、「切换指标公式」和「筛选」功能,如下图: - -![](/img/customEvent/event/event-2-2.png) - -「**选择事件**」:**下拉选项里可选择所有预置事件和自定义事件**; - -「**选择属性或指标名**」:下拉选项里展示事件的分析角度以及属性,如下图: - -![](/img/customEvent/event/event-2-3.png) - -A:分析指标:任何事件都至少有这三个分析指标:总次数,触发用户数,人均次数; - -B:属性:事件属性; - -C:属性的分析指标:根据属性值类型,可存在以下分析指标: - -| 数值类型 | 分析角度 | -| ------------------------------------------------------------- | ------------------------- | -| 数值型 | 总和、中位数,均值、最大值、最小值、人均值、去重数 | -| 列表型 | 列表去重数、列表元素去重数 | -| 布尔型 | 去重数、为真数、为假数、为空数、不为空数 | -| 非数值、布尔型 | 去重数 | -| 时间(支持的格式为 `yyyy-MM-dd HH:mm:ss` 或者 `yyyy-MM-dd HH:mm:ss.SSS`) | 去重数 | - -**「重命名」**:将待分析的指标重命名,注意使用自己能理解同时别人也能理解的描述; - -**「切换指标公式」**:点击后切换为四则运算型指标。这个功能主要用于某些特殊的比例型分析场景,我们来列举两个例子: - -- 当天的活跃用户数占当月活跃用户数的比率,此场景下会将当前日期所在月份的活跃用户数作为分母参与计算; -- 当天的付费用户数占选定时间范围的活跃用户数的比率,此场景下会将所选时间范围的总活跃用户数作为分母参与计算; - -比如我们通过指标公式来构建付费率指标 - -- 我们自定义指标名为「付费率」; -- 我们设置事件和指标公式「用户付费 - 触发用户数」/「账号登录 - 触发用户数」; -- 在设置好指标计算公式后,可以选择「百分比」、「两位小数」、「整数」三种展现样式,我们选择两位小数; - -![](/img/customEvent/event/event-2-4.png) - -#### 4.1.3「筛选」 - -点击后设置指标的限制条件。可在【指标选择】界面设置单个指标的筛选,也可在事件分析主界面,设置全局筛选。 - -![](/img/customEvent/event/event-2-5.png) - -##### 4.1.4.1 筛选的数据类型 - -共有 5 种,分别对应支持不同的数学逻辑: - -1、数值。比如:充值金额。支持数学逻辑:等于、不等于、小于、大于、有值、无值、区间 - -2、字符串。比如:事件发生的城市。支持数学逻辑:等于、不等于、包含、不包含、有值、无值、正则匹配 - -3、列表 ID。 比如:名单。支持数学逻辑:存在元素、不存在元素、元素位置、有值、无值 - -4、时间。比如:注册时间(yyyy-MM-dd HH:mm:ss.SSS 或 yyyy-MM-dd HH:mm:ss)。支持数学逻辑:绝对时间、相对当前时间、相对事件发生时刻、有值、无值 - -5、布尔。比如:Wifi 使用。支持数学逻辑:为真、为假、有值、无值 - -##### 4.1.4.2 等于 / 不等于和包含 / 不包含的区别 - -等于 / 不等于:筛选项目中严格符合所选值才可被过滤,比如筛选条件:次大陆等于美洲,则仅查询美洲的数据。 - -包含 / 不包含:筛选项目中包含所选值即可被过滤,比如次大陆包含美洲,则美洲,北美洲,南美洲的数据都可以被过滤出。 - -绝对时间:指客观的现实时间。比如 2021-03-02 19:24:52 至 2021-03-08 19:24:52,前一时间必须在后一时间之前。点击「选择时间」可以精确到选择秒。 - -![](/img/customEvent/event_analyse_time_filter.png) - -相对当前时间:指相对现在来说,过去 n 天。 - -![](/img/customEvent/event_analyse_relative_time_1.png) - -相对事件时间:指相对所选的事件的前后 n 天。此处可选负数,比如 -1 天,则含义为所选事件发生之前的一天,如果所选为正数,比如 1 天,含义为所选事件发生之后的一天。 - -![](/img/customEvent/event_analyse_relative_time_2.png) - -##### 4.1.4.3 「且」「或」逻辑 - -可添加多个筛选条件。当筛选条件至少 2 个或以上时,会出现且 / 或切换按钮,默认是「且」。「且」即为多个筛选条件取交集,「或」为多个筛选条件取并集。 - -选择完筛选条件以后,点击查询按钮生效。 - -### 4.2 选择维度 - -#### 4.2.1 维度介绍 - -维度是对事实的分解,它将指标进行分割以观察规律: - -「事件发生的国家 / 地区」是一个维度,它的作用是将事实按照其所发生的国家 / 地区进行分组,从而观察指标的规律; - -「用户群的年龄」是一个维度,查看不同年龄段的用户的付费数据; - -「渠道包」是一个维度,查看不同渠道包下用户的留存数据; - -时间是最基础的维度。 - -#### 4.2.2 维度的分类 - -维度下拉框里可选择事件属性、用户属性、用户分群: - -- 事件属性:描述事件发生时候的状态,比如:用户在美国充了多少钱?这里查询在美国(充钱的用户现在可能不在美国)发生的付费事件。 -- 用户属性:描述触发事件的用户的状态。比如:现在在美国的用户充了多少钱?这里查询现在在美国的用户(包含曾经不在美国)触发的付费事件。 -- 用户分群:即用户属于所选中的分群里的用户。 - -![](/img/customEvent/event/event-2-6.png) - -其次阐述二者在数据逻辑上的区别: - -- 事件属性:即每一条事件所带的参数。比如通过 SDK 传「中秋节」事件,该事件带有 3 个属性,「中秋道具个数」、「粽子种类」和「用户购买龙舟数量」。在选择事件属性的维度时,可选的维度受到所选事件的约束,比如事件选定为「中秋节」的情况下,事件属性只能选择这 3 个。 -- 用户属性:即用户表里的字段。用户属性不受所选事件的约束。 - -当所选维度为数值类型的字段时,比如付费金额,会出现区间的选择,离散区间即按照该字段所具有的所有数值进行聚合查询,默认区间为系统按照某规则设定聚合区间,自定义区间为客户可以自己定义区间段来聚合数据。 - -![](/img/customEvent/event_analyse_custom_range.png) - -当所选维度有「事件发生时间」时,可选择时间的颗粒度,支持按天,小时,分钟,周,月。 - -![](/img/customEvent/event_analyse_time_granularity.png) - -### 4.3 选择日期 - -选择我们关注的日期。 - -![](/img/customEvent/event/event-2-7.png) - -### 4.4 选择对比日期 - -对比日期与所选日期区间范围要保持一致,比如下图例子中,所选日期为 2021-3-2 到 2021-3-8,间隔为 6 天,则对比日期间隔也为 6 天。在选择对比日期时,只需要选择一个日期(比如 2021-02-23),系统会自动往后推算 6 天(至 2021-03-01)。 - -![](/img/customEvent/event_analyse_time_compare.png) - -### 4.5 保存报表 - -选择完主维度和指标后,点击保存报表将该报表保存下来,输入名字后点击确认,则该报表被保存至「已保存报表」中。 - -![](/img/customEvent/event_analyse_save_report_1.png) - -首次保存某报表后,再次打开该报表即会出现「另存为」按钮,点击后另存为一张新的报表,若点击「保存报表」,则是更新之前保存的报表。 - -![](/img/customEvent/event_analyse_save_report_2.png) - -被保存 / 另存为的报表展现在屏幕右侧「我的报表」,可以点击自己保存的报表直接查询。 - -![](/img/customEvent/event_analyse_save_report_3.png) - -## 五、透视表 - -透视表是一种可交互的数据报表形态,其特点为所进行的计算与数据跟数据透视表中的排列有关。之所以称为数据透视表,是因为可动态改变数据的聚合维度的先后顺序,从而使得客户可以用多重角度观察数据。下面我们对比以下两张透视表: - -![](/img/customEvent/event_analyse_pivot_table_1.png) - -![](/img/customEvent/event_analyse_pivot_table_2.png) - -上下两张透视表(分别简称上表和下表,以下同)。 - -上表中,想要查询中国各省份当中,各设备系统及使用各网络运营商的用户的充值金额总和,即以「省份」视角透视拆分「设备系统」数据,进而又透视拆分「网络运营商」数据。 - -下表中,想要查询中国各网络运营商当中,各设备系统及各省份的用户的充值金额总和,即以「网络运营商」视角透视拆分「设备系统」数据,进而又透视拆分「省份」数据。 - -### 5.1 可拖拽 - - 透视表的每一列均可以用鼠标拖拽从而改变顺序。其中,维度被拖拽改变前后顺序以后,数据重新进入查询,即如同上表下表改变了数据透视拆分的角度。灵活运用拖拽可提高数据查询的效率。目前,透视表可最多勾选 5 个维度和 20 个指标。 - -### 5.2 搜索 - -透视表的维度表头处,可以点击搜索输入文字,快速筛选出符合条件的项目,这里为结果筛选,筛选后不会重新进入查询。 - -![](/img/customEvent/event_analyse_table_filter.png) - -### 5.3 排序 - -透视表的默认排序逻辑: -首先,对第一列主维度对应的所要排序指标的总计数据做从大到小排序; -进而,对第二列主维度对应的所要排序指标的总计数据做从大到小排序; -以此类推,对第 N 列主维度对应的所要排序指标的总计数据做从大到小排序; - -### 5.4 下载数据 - -透视表右侧点击下载按钮,可下载平铺数据,下载结果为当前查询结果,可以实现所见即所得。提供 CSV / PDF / 图片三种格式。 diff --git a/docs/sdk/tapdb/features/custom-event/funnel-analyse.mdx b/docs/sdk/tapdb/features/custom-event/funnel-analyse.mdx deleted file mode 100644 index f849d1430..000000000 --- a/docs/sdk/tapdb/features/custom-event/funnel-analyse.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: 漏斗分析 -sidebar_position: 5 ---- - -## 一、引入案例 - -在[埋点设计指南](/sdk/tapdb/features/custom-event/data-model)里,我们基于「用户从打开 App 到真正创建角色」的转化过程进行了埋点设计,在这里我们用「漏斗分析」功能对这个转化过程来进行分析。 - -**1、设置步骤**:将用户从打开 App 到真正创建角色的全过程创建事件,并按照用户操作顺序设置为漏斗步骤; -![](/img/customEvent/funnel/案例-1.png) - -**2、设置维度**:不同系统类型可能会对转化情况有影响,所以在维度里选择「设备系统类型」; - -![](/img/customEvent/funnel/案例-2.png) - -**3、设置漏斗周期**:我们将漏斗窗口期设置为 7 天; - -![](/img/customEvent/funnel/案例-3.png) - -**4、设置展示结果**:设置时间段和其他参数,并点击查询查看分析结果; - -![](/img/customEvent/funnel/案例-4.6.png) - -**5、保存报表**:将查询结果保存为报表。再将报表转变为看板,就可以每天方便快捷的看漏斗分析。 - -![](/img/customEvent/funnel/案例-5.2.png) - -## 二、什么是漏斗分析 - -漏斗分析是分析从起点到终点各阶段用户的转化率情况的分析模型,可以衡量每个节点的转化效果。在理想情况下,用户会沿着产品设计的路径到达最终目标事件,但实际情况是用户的行为路径是多种多样。通过埋点事件配置关键业务路径,可以分析多种业务场景下转化和流失的情况,不仅找出产品潜在问题的位置,还可以定位每个环节流失用户,通过产品手段或营销手段促进转化。 - -## 三、漏斗分析的支持场景 - -漏斗分析支持的场景很多,比较典型的场景如下: - -- 游戏流量很大,但注册用户很少,是过程中哪个环节除了问题? -- 用户从「注册 – 创建角色 - 体验游戏 - 付费」 总体转化率如何? -- 不同地区的用户支付转化率有什么差异? -- 两个推广渠道带来不同的用户,哪个渠道的注册转化率高? - -## 四、如何做漏斗分析 - -如案例所述,漏斗分析可以分为 5 个步骤: - -**1、设置步骤;** - -**2、设置维度;** - -**3、设置漏斗周期;** - -**4、设置展示结果;** - -**5、保存报表;** - -### 4.1 设置漏斗 - -在漏斗分析页面,点击「设置步骤」,可以看到「添加漏斗步骤」界面。 - -![](/img/customEvent/funnel/exp/1-漏斗分析-设置漏斗.png) - -在「添加漏斗步骤」界面,可以选择漏斗步骤和添加漏斗步骤: - -1、步骤:由一个事件(可添加一个或者多个筛选条件)组成,表示一个转化流程中的一个关键性的步骤。 - -一个漏斗中至少包含 2 个步骤,每个步骤对应一个事件。可增加更多步骤,拖动步骤前的序号可以改变步骤顺序。 - -同样可对步骤设置筛选条件。也可在漏斗分析主界面设置全局筛选。 - -2、添加步骤:给要分析的漏斗增加更多步骤。 - -![](/img/customEvent/funnel/exp/2-漏斗分析-设置漏斗.png) - -### 4.2 设置维度 - -在维度下拉框,展示的内容包括事件属性(步骤 1)、用户属性和用户分群。 - -![](/img/customEvent/funnel/exp/3-漏斗分析-选择维度.png) - -### 4.3 设置漏斗周期 - -**漏斗周期**:用户触发步骤 1 起,在窗口期内完成后续步骤,算作后续步骤的转化。 - -![](/img/customEvent/funnel/exp/4-漏斗分析-漏斗窗口期.png) - -**限制窗口期在时间区间内**:表示只有在这个时间范围内,用户从第一个步骤,行进到最后一个步骤,才被视为一次完整的漏斗转化。 - -### 4.4 设置展示结果 - -可选择进行分析的步骤范围,并根据「对比 / 趋势」、「转化 / 流失」设置,能够得到 4 类报表:转化对比表、流失对比表、转化趋势表、流失趋势表。 - -![展示结果](/img/customEvent/funnel_analyse_result_type.png) - -转化对比表:用以分析从步骤一到后续步骤的累计的转化率 - -![转化对比表](/img/customEvent/funnel_analyse_table_1.png) - -流失对比表:用以分析每个步骤之间的流失率 - -![流失对比表](/img/customEvent/funnel_analyse_table_2.png) - -转化趋势表:用以分析不同日期的转化率变化趋势 - -![转化趋势表](/img/customEvent/funnel_analyse_table_3.png) - -流失趋势表:用以分析不同日期的流失率变化趋势 - -![流失趋势表](/img/customEvent/funnel_analyse_table_4.png) - -### 4.5 保存到看板 - -将设置好的查询结果保存为报表,再基于报表创建看板,漏斗分析结果一触即达。 - -![](/img/customEvent/funnel/exp/5-漏斗分析-保存报表.png) - -## 五. 漏斗分析原理 - -接下来将会描述漏斗分析原理,尤其是有分组和筛选情况时,计算原理就会显得较为复杂,此处将详细说明。 - -### 5.1. 基本计算原理 - -假设一个由步骤 1、2、3、4、5 构成的漏斗,选择的时间范围为 2021 年 3 月 1 日到 2021 年 3 月 7 日,窗口期是 3 天,如果用户在 2021 年 3 月 1 日到 2021 年 3 月 7 日触发了步骤 1,并且在步骤 1 发生的 3 天内,依顺序依次触发了步骤 2、3、4、5,则认为该用户完成了一次完整的漏斗转化,若依次触发了步骤 1 > 2 > 4 > 5,则该用户仅完成了步骤 1 > 2 的转化。 - -如果步骤中间夹杂了一些其他的步骤,如用户的行为顺序是 1 > X > 2 > X > 3 > 4 > X > 5(其中 X 代表其他事件),则依然认为该用户完成了一次完整的漏斗转化。 - -当一个用户在所选时段内有多个事件都符合某个转化步骤的定义,则会优先选择更靠近最终转化目标的事件作为转化事件,并在第一次达到最终转化目标时停止转化计算。 - -假设一个漏斗的步骤定义为:浏览商城、选择道具、生成订单、支付成功,那么不同用户的行为序列及实际转化步骤(加粗部分)如下: - -例 1:**浏览商城** > 选择道具(道具 B ) > **选择道具(道具 A )** > **生成订单** > 支付成功 - -例 2:浏览商城 > 选择道具(道具 B ) > **浏览商城** > **选择道具(道具 A )** > **生成订单** > **支付成功** - -例 3:浏览商城 > 选择道具(道具 B ) > **浏览商城** > **选择道具(道具 A )** > **生成订单** > **支付成功** > 选择道具(道具 A ) > 生成订单 > 支付成功 - -漏斗分析中展示的数字代表转化 / 流失的独立用户数,而非触发的事件次数。在该时间范围内,即使一个用户多次完成漏斗,也仅计数一次。 - -### 5.2 分组与筛选 - -漏斗分析的分组与筛选,均基于完成转化 / 流失的用户。 -基于用户属性、用户分群的分组与筛选:在完成转化 / 流失的用户的基础上,根据用户属性、用户分群进行分组与筛选。 -基于事件属性的分组与筛选:在完成转化 / 流失的用户的基础上,以该用户在步骤 1 的事件属性进行分组与筛选。 diff --git a/docs/sdk/tapdb/features/custom-event/kanban.mdx b/docs/sdk/tapdb/features/custom-event/kanban.mdx deleted file mode 100644 index 9c16754b5..000000000 --- a/docs/sdk/tapdb/features/custom-event/kanban.mdx +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: 看板 -sidebar_position: 8 ---- - -## 1. 概述 - -看板是多张报表的集合,将构建的指标、留存、漏斗等保存为报表后,可将报表添加至看板中,方便日常数据的监控。 - -![概述](/img/customEvent/kanban_summary.png) - -## 2. 使用看板进行数据分析与协作 - -在该部分将以 demo 项目为例,演示从新建一个看板,到在团队中通过看板进行数据协作的全流程。 - -### 2.1 进入看板页 - -![进入看板](/img/customEvent/kanban_layout.png) - -数据看板由 「看板目录、看板设置、报表展示」 三部分构成: - -- 1 看板目录:创建看板 / 文件夹,查看自建看板或团队成员的共享看板; - -- 2 看板设置:包括添加看板报表、设置看板共享、调整看板设置、看板刷新、为看板报表设置全局筛选等; - -- 3 报表展示:展示每张看板报表的信息,支持看板报表拖拽排序,自定义大小及图表展示类型。 - -### 2.2 新建看板 - -现在,我们需要将一些核心指标形成日报进行每日汇报,因而决定将日常核心指标的相关报表汇总在一个看板中。 - -![新建看板](/img/customEvent/kanban_create_1.png) - -![新建看板](/img/customEvent/kanban_create_2.png) - -我们在看板左侧栏点击右上角「+」,选择「新建看板」,并命名为「日报看板」。 - -### 2.3 编辑、重命名、删除看板 - -![编辑、重命名、删除看板](/img/customEvent/kanban_operation.png) - -对于已经存在的看板,我们可以编辑该看板的相关信息,或删除该看板。 - -### 2.4 使用文件夹对看板进行管理 - -文件夹用以收纳看板,我们可新建文件夹,并进行重命名、删除,系统内置「未分组」文件夹和「共享给我的」文件夹。 - -![文件夹](/img/customEvent/kanban_create_folder.png) - -在看板左侧栏点击右上角「+」,选择「新建文件夹」,并命名为「游戏运营」,用以收纳游戏运营的相关看板。 - -![移动](/img/customEvent/kanban_move.png) - -此时,我们可以将之前新建的看板「日报看板」移动到文件夹「游戏运营」中。 - -### 2.5 将报表添加到看板 - -报表是组成看板的基本元素,我们可在事件分析、留存分析、漏斗分析等分析模型功能中新建报表。 - -为了满足日报汇报需求,我们在事件分析中构建出登录账号数、App 启动设备数等报表,在留存分析中构建出用户 App 启动 7 日留存等报表,现在我们将这些报表添加到看板中。 - -![添加](/img/customEvent/kanban_add_report_1.png) - -点击看板右上方的「报表」按钮,点击「+」添加报表。 - -![添加](/img/customEvent/kanban_add_report_2.png) - -鼠标移入右侧的待添加报表中的报表行,点击「+」可将报表添加至当前看板。 - -### 2.6 设置报表 - -![设置](/img/customEvent/kanban_setting_1.png) - -![设置](/img/customEvent/kanban_setting_2.png) - -对于添加到看板中的报表,可以调整报表的大小尺寸、可视化方式、时间筛选、展示指标、展示分组等信息。 - -展示分组可按分组对应的指标值或分组名进行排序,并设置为选中「前 N 项目」,看板后续将根据查询时的数据进行排序并动态变更选中的分组。 - -![看板](/img/customEvent/kanban_function.png) - -将活跃账号数、活跃设备数等报表窗口尺寸设置为小,便于我们快速浏览当日的流量情况。 - -将「各国家活跃设备数」窗口尺寸设置为中,图表类型选择趋势图,便于观察各个国家活跃设备数在近期的变化趋势。 - -将「各系统活跃用户分布」窗口尺寸设置为中,图表类型选择分布图,便于观察各个系统的用户分布。 - -将「App 启动 30 日留存」窗口尺寸设置为大,便于同时看到更多期群的留存数据。 - -### 2.7 设置看板 - -将报表添加到看板之后,我们可以对看板的更新和是否近似计算进行设置。 - -![设置看板](/img/customEvent/kanban_refresh.png) - -看板默认为定时更新,系统将每天定时为看板刷新计算结果,缓存该结果以便下次查看,对于看板中加载报表较多或计算量较大的场景建议打开定时更新,以提高展示效率。 - -看板可被设置为实时更新,即每次刷新计算时都对看板进行更新,适合需要实时刷新的指标,如当日的广告投放数据。 - -看板默认为精确计算,即每次计算都将按照条件算出准确值。如果对查询效率有需求,可以选择「近似计算」选项。开启后,对触发用户数、人均次数、人均值、去重数等数据,都将采取近似算法,可以极大减少性能开销并减少计算时间。 - -对于本次创建的「日常看板」,我们更关注近期的主要指标变化趋势,因此我们选择每日凌晨 1 点定时更新并开启近似计算。 - -### 2.8 共享看板 - -在工作中我们希望团队其他成员也可以查看新建的看板,甚至共同维护更新该看板,因此我们可以将看板权限共享给团队其他成员。 - -共享权限分为共享、可见两类,两类权限互相独立,可分别授予,用户可将共享、可见权限授予「全部用户」,任何成员加入该项目后即拥有该权限,不必频繁更新。 - -![共享看板](/img/customEvent/kanban_share.png) - -作为整个团队都关注的「日常看板」且并不包含敏感的营收等敏感收入,因此我们将可见权限授予「全部用户」,同时我们希望另一位运营同事与我们一同维护、更新该看板,因此我们将协作权限共享给该同事。 - -### 2.9 看板订阅 - -在某些需要定期查看数据的场景下,频繁打开看板会带来使用流程上的不便,为此我们提供「看板订阅」功能。 - -![](https://capacity-files.lcfile.com/o70Y2aNmVeloTAPRh6KsVhDOeNJ5lSVu/tap_db_1.png) - -作为看板的「所有者」/「协作者」,你可以通过入口访问订阅设置界面,并基于本身业务设定订阅规则。 - -![](https://capacity-files.lcfile.com/VBWQ9kY882IkRrxqr2RDIHUEwXNXPMQC/tap_db_2.png) - -- 订阅标题:即此条订阅的标题内容; - -- 订阅说明:即此条订阅的详细说明内容; - -- 订阅推送:即此条订阅的推送时间,支持按「天」「周」「月」进行定期推送,时区固定为 UTC+8 ; - -- 订阅方式:即订阅推送的渠道,目前支持添加邮箱,[企业微信群](https://open.work.weixin.qq.com/help2/pc/14931?person_id=1&is_tencent),[Slack](https://api.slack.com/messaging/webhooks) 渠道; - -- 订阅状态:即订阅任务的当前状态,若需要暂停/开启订阅,可以手动更改状态; - -- 订阅操作:除了基础「保存」操作外,可以基于当前内容「立即推送」一条订阅,用于测试验证订阅任务。 - -附:订阅推送效果示例图 - -![](https://capacity-files.lcfile.com/R4MzASkfE5GotyeUte1ct7MY6mEJ5BNR/image-1.png) - -![](https://capacity-files.lcfile.com/q6NzsismFRQ9SfD1NM54SbAKTjbhF0sd/image-2.png) - -![](https://capacity-files.lcfile.com/xrqRfvksMadial52GrcjvumE3XAbwE4r/image-3.png) - - - - - diff --git a/docs/sdk/tapdb/features/custom-event/meta-data.mdx b/docs/sdk/tapdb/features/custom-event/meta-data.mdx deleted file mode 100644 index 0b45aeefe..000000000 --- a/docs/sdk/tapdb/features/custom-event/meta-data.mdx +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: 元数据管理 -sidebar_position: 10 ---- - -## 1. 概述 - -元数据管理是系统中的数据管理模块,是用户用以统一管理元数据的地方。 - -TapDB 采用元数据预登记模式,在接收 SDK 数据前,必须将需要收集的事件、属性登记到元数据管理相应的功能模块中,系统在接收数据时,需按照预登记的元数据进行校对,对于符合条件的数据进行入库,不符合要求的数据将被拒绝。 - -该模式可以有效地提高数据的准确性,从根本上解决数据报告和存储不正确的问题。 - -元数据管理由事件管理、事件属性管理和用户属性管理 3 部分构成。 - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ----------- | -------------- | -| 管理员 | 录入埋点方案,管理系统元数据 | -| 业务人员 | 查看元数据,了解数据业务含义 | -| 客户端 / 前端工程师 | 查看埋点需求 | - -## 3. 事件管理 {#event-manage} - -「事件」是系统中各类数据分析模型的基本分析对象,系统内置「预置事件」,并提供上报「自定义事件」的功能。 - -在「事件管理」功能中,可在对「自定义事件」在上报前进行预登记,并从多方面对「事件」进行管理。 - -前端工程师通过查看「曾否上报」为「否」的预登记自定义事件,进行埋点开发。 - -![事件管理](/img/customEvent/metadata_event_overview.png) - -### 3.1 概念解释 - -「事件」相关的「概念」及其「解释」如下: - -| 概念 | 解释 | -| ---- |-----------------------------------------------------------------------------------------------------------------------------------------| -| 事件名 | 事件的唯一标识 | -| 显示名 | 事件在分析模型中的显示名称 | -| 说明 | 描述埋点的各方面信息,如:描述触发时机,帮助技术人员更准确埋点;描述业务内涵,帮助业务人员更深入理解 | -| 事件类型 | 目前分为「预置」、「自定义」两类:
    **预置**:系统内置的事件,广泛适用于各类游戏项目,仅需在 SDK 中打开埋点开关即可上报
    **自定义**:自定义创建的事件,满足游戏项目的个性化需求,上报前需在事件管理中录入 | -| 曾否上报 | 事件是否有过上报记录 | -| 接收开关 | 系统接收事件上报与否的开关 | -| 状态 | 目前分为「正常」、「隐藏」、「删除中」、「已删除」四种状态:
    **正常**:处于正常状态的事件
    **隐藏**:不在分析模型中展示
    **删除中**:已上报数据的事件在被删除后将进入「删除中」状态,72 小时内可撤回,在此期间依然接收数据,但不展示在分析模型中
    **已删除**:已上报数据的事件在被最终删除后,将以「已删除」状态记录在元数据管理中,不再接收数据与展示在分析模型中,且不占用自定义事件数据使用进度 | -| 事件属性 | 事件所需采集的属性信息,在新建、编辑事件中可选择绑定相应的事件属性。目前事件属性分为「预置」、「自定义」两类:
    **预置**:新建的事件默认处于「调试」状态,不在分析模型中展示
    **自定义**:自定义创建的事件属性,满足游戏项目的个性化需求。 | -| 数据限制 | 为保证系统性能,处于「正常」、「隐藏」、「删除中」状态的自定义事件数不可超过 100。「已删除」的自定义事件不占用使用进度,可通过删除自定义事件释放使用进度。 | - -### 3.2 创建自定义事件 - -![创建自定义事件](/img/customEvent/metadata_event_create2.png) - -填写事件的基本信息,此时系统默认关联所有的预置属性,且默认不关联所有的自定义属性。 - -选择需要关联的自定义属性,若不满足需求可新建自定义事件属性,以及解绑不需要关联的预置属性,即完成自定义事件的创建。 - -### 3.3 查看、编辑、删除事件 - -![查看、编辑、删除事件](/img/customEvent/metadata_event_edit.png) - -点击事件名可查看到此事件的基本信息,以及所有关联属性。 - -在操作栏,可编辑事件的基本信息以及变更与事件属性的关联关系,对于已发生上报的事件,「事件名」不可再被编辑。 - -在操作栏,可对已有的自定义事件进行删除操作。未上报数据的事件将被彻底删除,不留下任何记录;已发生上报的事件,将进入「删除中」状态,72 小时内可进行撤销操作,否则将进入「已删除」状态,不再接收数据与展示在分析模型中,同时释放自定义事件数据使用进度限制。 - -### 3.4 Q&A - -Q1:为什么有的事件被删除后不保留任何记录,而有的事件被删除后以「已删除」状态留在元数据管理中? - -A1:未发生上报的事件不保留记录,已发生上报的事件则会保留。在埋点的设计过程中,业务人员会因为需求变更、需求合并处理等问题对埋点设计进行多次变动,因而在没有进行上报行为之前,可以认为所有的录入行为都是在「打草稿」,我们希望用户在此过程中可以无所顾忌,不用担心把事件管理列表弄得一团糟;而一旦数据发生上报,相当于定稿,我们希望系统可以忠实的记录项目曾经采用的所有数据采集方案。 - -## 4. 事件属性管理 {#event-props} - -「事件属性」是用来描述「事件」的属性信息,TapDB 内置「预置事件属性」,并提供上报「自定义事件属性」功能。 - -在「事件属性管理」功能中,可在对「自定义事件属性」进行预登记,并从多方面对「事件属性」进行管理。 - -前端工程师通过查看「曾否上报」为「否」的预登记自定义事件属性,进行埋点开发。 - -![事件属性管理](/img/customEvent/metadata_event_prop_overview.png) - -### 4.1 概念解释 - -「事件属性」相关的「概念」及其「解释」如下: - -| 概念 | 解释 | -| ---- | ---------------------------------------------------------------------------------------------------------- | -| 属性名 | 事件属性的唯一标识 | -| 显示名 | 事件属性在分析模型中的显示名称 | -| 说明 | 描述属性的各方面信息,如:描述业务内涵,帮助业务人员更深入理解 | -| 数据类型 | 属性的数据类型,类型相符的数据方可入库 | -| 单位 | 统计值的单位,在分析模型、报表中作相应展示 | -| 属性类型 | 目前分为「预置」、「自定义」两类:
    **预置**:系统内置的事件属性,广泛适用于游戏项目中的各个事件,仅需在 SDK 中打开埋点开关即可上报:
    **自定义**:自定义创建的事件属性,满足游戏项目的个性化需求,上报前需在事件属性管理中录入 | -| 曾否上报 | 事件属性是否有过上报记录 | -| 接收开关 | 系统接收事件属性上报与否的开关 | -| 状态 | 目前分为「正常」、「隐藏」两种状态:
    **正常**:处于正常状态的事件
    **隐藏**:不在各个分析模型中展示 | -| 数据限制 | 为保证系统性能,自定义事件属性数不可超过 300 | - -### 4.2 创建 - -![事件属性创建](/img/customEvent/metadata_event_prop_create.png) - -填写事件属性的基本信息,即完成自定义属性的创建。可在「事件管理」中将其与事件进行关联。 - -### 4.3. 查看与编辑 - -![事件属性查看与编辑](/img/customEvent/metadata_event_prop_edit.png) - -点击属性名可查看到此属性的基本信息,以及所有关联事件。 - -在操作栏,可编辑除「属性名」、「数据类型」之外的其他事件属性基本信息。 - -### 4.4 Q&A - -Q1:为什么不能删除未进行任何数据上报行为的自定义事件属性,而自定义事件却可以被删除? - -A1:增加一个新事件在数据表中仅为一条数据记录,而增加一个新属性则需要在数据表中增加新的一列,改变原有的数据结构,两者在实现上难度相差很大。因而在初版中暂不支持删除自定义属性功能,后续会迭代删除自定义属性功能,敬请期待。 - -## 5. 用户属性管理 {#user-props} - -「用户属性」是用来描述于「用户」的属性信息,TapDB 内置「预置用户属性」,并提供上报「自定义用户属性」功能。 - -TapDB 分别以「账号」、「设备」作为用户标识对不同主体进行分析,在用户属性管理中,「账号」、「设备」也将分别作为用户标识的组成部分 - -在「用户属性管理」功能中,可在对「自定义用户属性」进行预登记,并从多方面对「用户属性」进行管理。 - -前端工程师通过查看「曾否上报」为「否」的预登记自定义用户属性,进行埋点开发。 - -![用户属性管理](/img/customEvent/metadata_user_prop_overview.png) - -### 5.1 概念解释 - -「用户属性」相关的「概念」及其「解释」如下: - -| 概念 | 解释 | -| ---- | ---------------------------------------------------------------------------------------------------------- | -| 用户标识 | 用户属性的标识之一,分为「账号」、「设备」两类 | -| 属性名 | 用户属性的标识之一,与「用户标识」共同组成唯一标识 | -| 显示名 | 用户属性在分析模型中的显示名称 | -| 说明 | 描述属性的各方面信息,如:描述触发时机,帮助技术人员更准确埋点;描述业务内涵,帮助业务人员更深入理解 | -| 数据类型 | 属性的数据类型,类型相符的数据方可入库 | -| 单位 | 统计值的单位,在分析模型、报表中作相应展示 | -| 属性类型 | 目前分为「预置」、「自定义」两类:
    **预置**:系统内置的用户属性,广泛适用于游戏项目中的各个事件,仅需在 SDK 中打开埋点开关即可上报
    **自定义**:自定义创建的用户属性,满足游戏项目的个性化需求,上报前需在用户属性管理中录入 | -| 曾否上报 | 用户属性是否有过上报记录 | -| 接收开关 | 系统接收用户属性上报与否的开关 | -| 状态 | 目前分为「正常」、「隐藏」两种状态:
    **正常**:处于正常状态的事件
    **隐藏**:不在分析模型中展示 | -| 数据限制 | 为保证系统性能,自定义用户属性数不可超过 100 | - -### 5.2 创建 - -![创建](/img/customEvent/metadata_user_prop_create.png) - -填写用户属性的基本信息,即完成自定义属性的创建。目前,自定义用户属性一旦被创建便不可被删除。 - -### 5.3 查看、编辑与复制 - -![查看、编辑与复制](/img/customEvent/metadata_user_prop_edit.png) - -点击属性名可查看到此属性的基本信息。 - -在操作栏,可编辑除「属性名」、「数据类型」之外的其他事件属性基本信息。 - -在操作栏,可通过复制功能新建一个各项信息相同但切换了用户标识的自定义用户属性,方便快速对设备、账号两个用户识别体快速同步创建自定义用户属性。 - -### 5.4 Q&A - -Q1:为什么需要区分「设备」、「账号」两个主体? - -A1:在不同场景中,以不同的主体来标识用户,从而更能切合业务需求。如广告投放相关业务中,设备是更好的用户标识,而分析用户具体游玩行为时,账号可能是更好的用户标识。另外,可善用复制操作,对两类标识的用户属性进行同步创建。 diff --git a/docs/sdk/tapdb/features/custom-event/overview.mdx b/docs/sdk/tapdb/features/custom-event/overview.mdx deleted file mode 100644 index 317f83f0b..000000000 --- a/docs/sdk/tapdb/features/custom-event/overview.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: 自定义事件分析 -sidebar_position: 1 ---- - -## 为什么需要自定义事件分析 - -我们为 TapDB SDK 预设了一些上报事件,从而构造出了非常强大且完整的运营模块。但随着大家对数据分析的深入,我们发现完全模板化、预置化的分析模式会有明显的局限。为此我们设计了自定义事件分析,让大家能更自由地上报与查询所需要的数据。它有一定的接入和理解门槛,但通过自定义事件分析深挖玩家行为的上限是明显高于预置的报表模板的,因此我们强烈建议你认真学习并用好它。 - -## 事件分析 - -事件分析是最基础的分析模型,它的分析对象是事件 - -- 查看事件相关的所有信息 - - 「进行 PVP 对战」事件的触发总次数 / 触发用户数 / 人均次数 - - 「购买礼包」事件的购买金额总数 / 人均次数 - - 最近 7 天,「抽卡」事件在不同省份发生的人均次数对比 - - 每天的「账号登录」事件中,有多少比例是 TapTap 登录,有多少比例是微信登录 - -## 留存分析 - -留存分析是对用户广义留存行为进行分析的模型,它的分析对象是设备 / 账号 - -- 查看那些触发了 A 事件又触发了 B 事件用户的情况,初始事件和回访事件可以是同一个 - - 「付费」的账号,在之后 7 日内「登录」的情况 - - 「升到 10 级」的账号,在之后 7 日内「付费」的情况 - - 「购买月卡」的账号,在之后 30 日内「领取奖励」的情况 - - 首次触发「登录」的账号,在之后 90 日内「登录」的情况:这是我们通常说「账号留存」 - -## 漏斗分析 - -漏斗分析是对按顺序触发特定事件的用户进行分析的模型,它的分析对象是设备 / 账号 - -- 查看那些严格按照漏斗步骤转化的用户的信息,可以设置漏斗窗口期 - - 「完成签到」的账号数,以及在「完成签到」之后,又在 30 分钟内「参与 PVP」的账号数 - - 「购买月卡」的账号数,以及在「购买月卡」之后,又在 7 天内「购买新手礼包」的账号数 - -## 用户分群 - -用户分群是自定义事件分析中极其强大的一个功能。它的使用思路为 - -- 通过观察数据,找到一批待分析用户,如:留存率极低 / ARPPU 很高 / 持续签到但等级很低的用户,将其保存为一个用户分群 -- 使用这个用户分群作为条件,单独观察 / 筛除其行为数据,从而尝试找到一些行为特征,如:留存率极低的用户 Android 版本都低于 7,那推测很可能是因为其中有大量的模拟器用户 - -用户分群打通了寻找特征到持续深挖的链路,是通过自定义事件分析问题必备的技能 - -## 看板 - -顾名思义,看板是非常适合用于观察数据的一个功能。它往往会用在「组建核心指标日报」,「持续观察某个特定问题」的场景下。当你在分析模型中将某个报表保存下来后,就可以将其添加到看板中。 -看板的一大核心能力是分享:你可以将自己构建的看板分享给其他用户,从而大幅降低沟通成本。注意构建看板时要思考其核心主题,一个优秀的看板应该是围绕一个特定且明确的主题的,要避免将不相关的信息组合到一个看板中,否则反而会造成干扰。 diff --git a/docs/sdk/tapdb/features/custom-event/property-analyse.mdx b/docs/sdk/tapdb/features/custom-event/property-analyse.mdx deleted file mode 100644 index 52c5bd7e3..000000000 --- a/docs/sdk/tapdb/features/custom-event/property-analyse.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: 属性分析 -sidebar_position: 7 ---- - -## 一、什么是属性分析 - -经常会遇到分析玩家在各省的分布情况、玩家的年龄分布情况等,通过这些属性的分析,可以快速描绘出整体用户群的用户画像,而这些分析可以用属性分析功能来实现。
    -属性分析是一种专门分析用户属性的统计与分布情况的模型,模型按照用户属性进行归类,可以同时查看不同分组下用户的统计情况。 -通过属性分析功能,可以帮助开发者多角度、全方位的掌握指定玩家群体的特征,宏观上把握整体玩家的组成与偏好,从而为精细化运营提供依据。 - -## 二、属性分析的支持场景 - -属性分析可以帮助揭示以下问题:
    - -- 所有不同会员等级的用户的平均消费金额是多少; -- 不同省份玩家的分布情况; - -## 三、如何做属性分析 - -属性分析可以分为 4 个步骤:**设置指标、设置维度、设置展示结果、保存报表**。 -后面三个步骤在**事件分析**里均有比较详细的介绍,所以这里重点介绍**设置指标**。 -在属性分析页面,点击「选择指标」,可以看到「指标选择」界面。 - -![](/img/customEvent/character/character1.png) - -对于所有类型的属性都可以将「去重数」作为分析指标,对于数值类型的属性可以将「总和」「均值」「最大值」「最小值」作为分析指标。 - -1. **用户数:** 所有用户数。 -2. **去重数:** 在所有用户中,该属性出现的独立去重个数。 -3. **总和:** 在所有用户中,该属性的取值求和。 -4. **均值:** 在所有用户中,该属性取值的算术平均值。 -5. **最大值:** 在所有用户中,该属性取值的最大值。 -6. **最小值:** 在所有用户中,该属性取值的最小值。 diff --git a/docs/sdk/tapdb/features/custom-event/retention-analyse.mdx b/docs/sdk/tapdb/features/custom-event/retention-analyse.mdx deleted file mode 100644 index fb671912b..000000000 --- a/docs/sdk/tapdb/features/custom-event/retention-analyse.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: 留存分析 -sidebar_position: 4 ---- - -## 一、引入案例 - -游戏经常会进行更新或改版,那改版之后的留存率有什么变化,我们可以用「留存分析」的功能进行分析和呈现。 - -**1、设置事件**:由于我们聚焦在用户对游戏的整体留存情况,所以我们将用户的初始事件和回访事件设定为「账号登录」。 - -![](/img/customEvent/retention/LC-1-1.png) - -**2、设置维度**:由于我们关注的是版本更新对留存是否有影响,所以在维度里选择「App 版本号」; -![](/img/customEvent/retention/LC-1-2.png) - -**3、设置展示结果**:设置时间区间和其他参数; -![](/img/customEvent/retention/LC-1-3.png) - -**4、保存报表**:查看分析结果,并将分析结果保存为报表。 -![](/img/customEvent/retention/LC-1-4.png) - -## 二、什么是留存分析 - -留存,就是玩家在你的游戏中留下来、持续使用。 - -只有做好了留存,才能保障新玩家在注册后不会白白流失。有时候我们仅看日活(DAU),会觉得数据不错,但有可能是因为近期有密集的运营拉新活动,注入了大量的新用户,但是留下来的用户不一定在增长,可能在减少,只不过被新用户数掩盖了所以看不出来。这就好像一个不断漏水的篮子,如果不去修补底下的裂缝,而只顾着往里倒水,是很难获得持续的增长的。 - -一般我们讲的留存率,是指「目标玩家」在一段时间内「回到游戏中完成某个行为」的比例。常见的指标有次日留存率、七日留存率、次周留存率等。比如:某个时间获取的「新玩家」的「次日留存率」常用来度量拉新效果。 - -## 三、留存分析的支持场景 - -1. 完成登录(初始事件)的玩家中,有多少玩家在接下来一个月进行了充值操作(回访事件); -2. 升级到 VIP 9 级(初始事件)的玩家在接下来一个月购买了多少礼包(回访事件); -3. 想判断某项游戏改动是否奏效,如新增了一个英雄角色,观察是否有人因新增角色而多使用游戏几个月; - -## 四、如何做留存分析 - -如案例所述,留存分析可以分为 4 个步骤: - -**1、设置事件**; - -**2、设置维度;** - -**3、设置展示结果;** - -**4、保存报表;** - -### 4.1 设置事件 - -#### 4.1.1 设置初始事件和回访事件 - -在漏斗分析页面,点击「选择事件」,在「选择事件」界面分别选择初始事件以及回访事件,可以分别对其做筛选。值得注意的是,针对初始 / 回访的筛选条件里, 只允许选择事件属性,如果想要选择用户属性的过滤,可以在全局筛选中进行。 -![](/img/customEvent/retention/LC-2-1.png) - -#### 4.1.2 设置「同时展示」 - -设计该功能的主要目的是对触发回访事件的用户进行深入分析。比如: -我想统计完成登录的用户中有多少用户完成了付费,并且,我还想统计这些完成付费的用户的充值总金额。 -在下图中,就是使用「同时展示」功能对付费用户再做一次付费金额阶段累计总和的统计。 - -![](/img/customEvent/retention/LC-2-2-1.png) - -与事件分析模版不同,同时展示功能中,针对数值类型的属性,分析角度由「总和、中位数、均值、最大值、最小值、人均值、去重数」变为了「总和、人均值、阶段累计总和、阶段累计人均」。而且,此处仅可选数值类型和布尔类型的属性,不能选时间、字符串、列表类型的属性,因为这些类型的属性不能统计「阶段累计」。利用「同时展示」功能,我们可以分析完成回访事件的用户在接下来一段时间的某某属性值的阶段累计总和 / 人均,比如 LTV,n 日付费,累计副本伤害值,累计人均购买礼包数等等。 - -| 指标描述 / 数据类型 | 分析角度 | -| ----------- | -------------------- | -| 数值型 | 总和、人均值、阶段累计总和、阶段累计人均 | -| 布尔型 | 为真数、为假数、为空数、不为空数 | - -**除上表列出的各数值类型的分析角度之外,任意事件任意数值类型都具备的默认分析角度为:总次数、触发用户数、人均次数。** -**「阶段累计」是同时展示功能的核心价值体现。同时展示最多只能做一项分析。** - -### 4.2 设置维度 - -留存分析里「事件发生时间」为必选维度,因为留存天然与时间关联。除了时间维度外,还可再选最多 4 个维度。 - -![](/img/customEvent/retention/LC-2-3.png) - -### 4.3 设置展示结果 - -留存分析的报表依然是透视表形态,与事件分析报表不同的是,「事件发生时间」这一个维度是不能拖拽改变先后聚合顺序的,只能在第一列。 - -![](/img/customEvent/retention/LC-2-4.png) - -#### 4.3.1 选择留存的分析期限 - -![](/img/customEvent/retention/LC-2-5.png) - -留存的分析期限默认为 7 日。点击之后下拉框可选:当日、次日、7 日、14 日、30 日、当周、次周、4 周、8 周、16 周、当月、次月、3 月、6 月、12 月。 - -除上述时间以外,客户也可以自己手动填写 n 日,n 周,n 月。 - -如果所选留存的分析期限过长,比如选了 180 天,会因为数据量过大导致计算缓慢,此时可以使用「仅显示关键日期」, - -- 日的关键日期为:1、7、14、21、30、60、90、120、150、180、360 日; -- 周的关键日期为:1、4、8、16、24、32、40、48、52 周; -- 月的关键日期为:1、3、6、12、24 月; - -勾选「仅显示关键日期」后,仅查询上述时间点的留存情况,从而可以缩短查询时间。当所选留存期限大于 90 天时,系统会自动勾选「仅显示关键日期」。 - -#### 4.3.2 留存 / 流失 - -![](/img/customEvent/retention/LC-2-6.png) - -n 日留存的判断逻辑:触发了初始事件的玩家(假设数量为 a)在第 n 日有 b 人触发了回访事件,「留存百分比」即为 b/a。 -n 日流失的判断逻辑:触发了初始事件的玩家(假设数量为 a)在之后的第 1 日至第 n 日(持续时间)有 b 人没有触发回访事件,b 即为第 n 日的「流失用户」数量。「流失百分比」即为 b/a。 - -#### 4.3.3 显示数量 / 百分比 - -报表右侧可选择显示全部 / 仅显示百分比 / 仅显示数量。 - -![](/img/customEvent/retention/LC-2-7-2.png) - -### 4.4 保存报表 - -![](/img/customEvent/retention/LC-2-7-3.png) - -将设置好的查询结果保存为报表,再基于报表创建看板,留存分析结果一触即达。 diff --git a/docs/sdk/tapdb/features/custom-event/sqlide.mdx b/docs/sdk/tapdb/features/custom-event/sqlide.mdx deleted file mode 100644 index cff7d52d3..000000000 --- a/docs/sdk/tapdb/features/custom-event/sqlide.mdx +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: SQL 查询 -sidebar_position: 14 ---- - -## 1 概述 - -使用 SQL 查询可以实现自由编写 SQL 代码查询项目内所有数据,满足 TapDB 固有分析模型中无法满足的个性化取数、分析需求。 - -可从「分析」模块下的「SQL 查询」进入 SQL 查询功能,页面由语句「编写框」、「标签页」构成,其中标签页由「表结构」、「查询历史」、「语句书签」和「查询结果」构成。 - -![](/img/customEvent/sql/sql_1.png) - -## 2 适用角色与用途 - -| 角色 | 用途 | -| :-------- | :------------------------------------------------- | -| 管理员 / 分析师 | 了解项目当前数据资产。 | -| 分析师 | 自由编写 SQL 查询项目所有数据,满足 TapDB 固有分析模型中无法满足的个性化取数、分析需求。 | -| 业务人员 | 替换分析师 SQL 代码中的动态参数,满足持续取数、分析需求。 | - -## 3 用表范围与注意事项 - -### 3.1 用表范围 - -在 TapDB SQL 查询功能中,可查询库表范围如下: - -| 表 | 库名 | 表名 | -| :---: | :-------: | :---------------------: | -| 事件表 | tapdb | view_{{项目 ID}}_events | -| 设备表 | tapdb | view_{{项目 ID}}_devices | -| 用户表 | tapdb | view_{{项目 ID}}_users | -| 用户分群表 | tapdb | view_{{项目 ID}}_cluster | -| 维度属性表 | tapdb_dim | view_{{项目 ID}}_{{维度表名}} | - -建议通过「数据表列表」中的「复制表名」功能,将复制该表的表名至剪切板后粘贴至语句编写框,详见本文 5.1.2 部分。 - -### 3.2 使用用户分群表的注意事项 - -所有用户分群数据均存储在同一张表 view_{{项目 ID}}_cluster 中。 - -![](/img/customEvent/sql/sql_2.png) - -可通过筛选分群名,并选择相应分群主体字段,得到该分群下的用户 ID,如下: - -分群主体为账号的分群: - -```sql -select - user_id -from hive.tapdb.view_{{项目 ID}}_cluster -where cluster_name = ‘{{cluster_name}}’ -``` - -分群主体为设备的分群: - -```sql -select - device_id -from hive.tapdb.view_{{项目 ID}}_cluster -where cluster_name = ‘{{cluster_name}}’ -``` - -## 4 编写与执行 SQL 语句 - -SQL 语句的编写与执行主要在语句编写框内进行。 - -![](/img/customEvent/sql/sql_3.png) - -### 4.1 基本语法 - -TapDB 采用 Presto 查询引擎,适用标准 SQL 语法,但仅可以使用 select 语句以及 with 子句,可以访问 [presto 文档](https://trino.io/docs/332/functions.html) 获取 Presto 的语法以及函数的使用方法。 - -数据表中的字段名建议使用双引号 `" "` 括起,也可以缺省,但如果查询字段名带有特殊符号(如 `$`、`#` 等),则必须使用双引号, -字符串必须使用单引号 `' '` 括起。 - -### 4.2 分区与时区 - -查询事件表时,必须使用分区键 `$part_date` 进行条件筛选,避免全表扫描。 - -![](/img/customEvent/sql/sql_4.png) - -建议使用以下类型的分区限制条件: - -```sql -"$part_date" = '2021-11-01' -"$part_date" in ('2021-11-01', '2021-11-02, '2021-11-03') -"$part_date" between '2021-11-01' and '2021-11-11' -``` - -SQL 查询功能默认按照东 8 区对时间类型的字段进行转化展示,如事件表中的 `time`,设备表、账号表中的 `activation_time`、`last_login_time`、`first_charge_time`、`last_charge_time`。 - -若项目不处于东 8 区,则可使用时间函数对其进行转化: - -```sql -format_datetime("time" at time zone 'America/Chicago', 'yyyy-MM-dd') -``` - -`part_date`、`$part_date` 为按照项目时区进行转化后的字符串格式的日期,若无精准查询时、分、秒的需求,建议使用分区键的筛选满足时间筛选需求。 - -### 4.3 动态参数 - -使用动态参数功能可对查询语句中的参数进行替换,后续查询时只需在参数输入框下方输入参数值即可满足新的查询需求。 - -动态参数的表达式规则为 `${参数名}`,参数名需以英文字母开头,可包括英文字母、数字和下划线,参数名相同的参数视为一个参数,可以创建多个参数变量,下方输入框按照各参数首次出现的顺序与动态参数对应,多个同一参数名的参数仅对应一个参数输入框。 - -![](/img/customEvent/sql/sql_5.png) - -### 4.4 工具栏操作 - -工具栏位于输入框下方,可执行以下操作: - -格式化:将查询语句进行格式化 - -复制语句:将输入框内的查询语句复制到剪切板 - -添加书签:将查询语句保存为书签,方便后续进行查询或进行修改 - -![](/img/customEvent/sql/sql_6.png) - -### 4.5 快捷操作 - -光标位于输入框时,可执行以下快捷操作: - -Ctrl + Enter:执行计算 - -Ctrl + Shift + F:格式化当前查询语句 - -Ctrl + Z:撤销上一步操作 - -Ctrl + Y:恢复上一步操作 - -### 4.6 执行查询 - -完成 SQL 语句编写后,可点击「计算」按钮,或者快捷键 Ctrl + Enter ,发起数据查询。 - -默认单次查询最多 10000 条数据,系统会为查询语句自动添加「limit 10000」,对查询行数进行限制,前端最多展示 500 行,可通过「查询历史」、「查询结果」中的下载功能查看所有数据。 - -## 5 标签页 - -标签页由「表结构」、「查询历史」、「语句书签」和「查询结果」构成。 - -![](/img/customEvent/sql/sql_8.png) - -### 5.1 表结构 - -表结构页可查看数据库、数据表、表字段的详细信息,从左至右由数据库列表、数据表列表、表字段列表 3 部分构成。 - -![](/img/customEvent/sql/sql_9.png) - -#### 5.1.1 数据库列表 - -数据库列表中可查看项目下的数据库,点击列表中的库名,右侧将会展示该库下的数据表列表。 - -![](/img/customEvent/sql/sql_10.png) - -#### 5.1.2 数据表列表 - -数据表列表中可查看选中的数据库下的数据表,点击列表中的表名,右侧将会展示该表下的字段列表。 - -点击「复制表名」按钮,将复制该表的表名至剪切板。 - -![](/img/customEvent/sql/sql_11.png) - -#### 5.1.3 表字段列表 - -表字段列表中可查看选中表的所有字段的信息,包括字段名、数据类型、解释。 - -![](/img/customEvent/sql/sql_12.png) - -### 5.2 查询历史 - -查询历史页中可查看执行过的查询语句,包括语句的完成时间、计算耗费时间、查询语句等信息,并可对查询语句进行搜索,快速找到相应语句。 - -![](/img/customEvent/sql/sql_13.png) - -点击「查询 ID」,可跳转至相应查询结果; - -点击「查询语句」右上角「键入」按钮,可将该语句将替换至输入框中; - -点击「下载」,将该查询结果以 csv 格式文件的形式下载到本地,查询结果页面展示上限为 500 条,超过上限的数据可以使用下载功能下到本地后进行进一步查看与分析,数据下载上限为 10000 条。 - -![](/img/customEvent/sql/sql_14.png) - -### 5.3 语句书签 - -语句书签页中可查看已保存的语句书签。 - -点击「设置」,书签中的内容将替换语句输入框中的内容,点击「删除」,可删除该书签。 - -![](/img/customEvent/sql/sql_15.png) - -### 5.4 查询结果 - -查询结果页中可查看历史查询结果。 - -点击「格式化」,将查询结果中的 json、map 等类型的对象进行格式化展示; - -点击「下载」,将该查询结果以 csv 格式文件的形式下载到本地,同「查询历史」中的「下载」功能。 - -![](/img/customEvent/sql/sql_16.png) - -## 6 最佳实践 - -### 6.1 日志导出 - -导出用户表或事件表,如导出近 7 日用户的所有事件日志: - -```sql -select - * -from hive.tapdb.view_{{项目 ID}}_events -where "$part_date" between '2021-11-05' and '2021-11-11' -``` - -### 6.2 数据清洗与提取 - -提取复杂字段,如:url、json、map 中的关键信息,如提取 url 中后 10 位数字的商品 ID: - -```sql -select - substring("#url", -10) as product_id -from hive.tapdb.view_{{项目 ID}}_events -where "$part_date" between '2021-11-05' and '2021-11-11' -``` - -### 6.3 个性化取数与分析 - -各种 TapDB 现有分析模型无法满足的个性化分析需求。 diff --git a/docs/sdk/tapdb/features/custom-event/tracking-management.mdx b/docs/sdk/tapdb/features/custom-event/tracking-management.mdx deleted file mode 100644 index 814f7fc59..000000000 --- a/docs/sdk/tapdb/features/custom-event/tracking-management.mdx +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: 埋点管理 -sidebar_position: 11 ---- - -## 1. 概述 - -埋点管理为数据接入测试和日常使用过程中提供的数据管理功能,包括埋点信息、上报明细 2 个子模块,各子模块功能说明如下: - -埋点信息页面可查看最近 7 日项目内数据接收情况,方便快速了解埋点上报整体情况,以及错误上报详情与抽样示例。 - -上报明细页面可查看「监测开关」开启期间近 1000 条的埋点上报明细,实时查看埋点上报日志,帮助用户快速测试、验收埋点开发。 - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ---------------------------- | -------------------------------------------- | -| 埋点设计人员(数据产品经理 / 分析师) | 测试与验收新埋点需求开发的上报结果,日常了解埋点整体运行情况,及时发现埋点错误 | -| 埋点开发人员(前端开发 / 客户端开发 / 测试工程师) | 实时查看埋点上报日志,测试埋点效果,快速定位埋点开发环节中的错误(建议在测试项目中启用) | - -## 3. 埋点信息 - -埋点信息由「埋点信息主页」、「错误详情页」2 部分构成。 - -埋点信息主页 - -![埋点信息主页](/img/customEvent/tracking_manager_1.png) - -错误详情页 - -![错误详情页](/img/customEvent/tracking_manager_2.png) - -### 3.1 埋点信息主页 - -埋点信息支持查看最近 7 日项目内数据接收情况。 - -埋点信息主页由查询设置、区间数据、数据详情区域构成。 - -![埋点信息主页](/img/customEvent/tracking_manager_3.png) - -#### 3.1.1 查询设置 - -查询时间筛选:查询时间指数据的接收时间。支持自定义查看最近 7 日任一时间段的数据接收情况,默认统计今日的数据。 - -属性名、显示名搜索:根据关键词对属性名、显示名进行过滤展示。 - -![查询设置](/img/customEvent/tracking_manager_4.png) - -#### 3.1.2 区间数据 - -展示所筛选时间段内所有数据的已接收、已入库、错误入库和入库失败数。 - -![区间数据](/img/customEvent/tracking_manager_5.png) - -#### 3.1.3 数据详情 - -数据名称:事件的埋点上报为相应的事件名,设置用户属性的埋点上报为「用户属性」,无法识别为以上两类的则为「未知数据」; - -显示名:数据名称对应的显示名; - -已接收:系统接收的数据; - -已入库:包括正确入库和错误入库数据; - -错误入库:包含如事件属性名不合法、属性类型不合法等属性级别错误的数据,该类数据可正常入库,错误属性置空; - -入库失败:因数据格式不合法、数据名不合法导致无法入库的数据。 - -错误详情:点击进入该数据的错误详情页,查看按照错误原因展示的错误详情。 - -![数据详情](/img/customEvent/tracking_manager_6.png) - -### 3.2 错误详情页 - -数据详情页展示各事件或用户属性下的错误入库和入库失败数据,错误信息按照错误原因展示。 - -错误详情页由查询设置、区间数据、数据详情区域构成。 - -![错误详情页](/img/customEvent/tracking_manager_7.png) - -#### 3.2.1 数据详情 - -错误条数:指含有该错误原因的数据条数。 - -错误类型、处理结果:指该错误的所属错误类型与 TapDB 对其处理的结果,便于用户对具体错误原因进行筛选。 - -查看抽样:查看符合该错误原因的上报数据的详细内容,每条上报数据均可选择格式化查看与一键复制。 - -![数据详情](/img/customEvent/tracking_manager_8.png) - -#### 3.2.2 错误条数的计算原理 - -「错误条数」是对触发该错误原因的上报数据进行计数得到的结果,而埋点管理中的「错误原因」,是发生在「属性」层面的。 - -当仅上报 1 条某事件的日志时,其中字段 1 属性名不合法,字段 2 属性类型不合法,则该日志会同时触发 2 个错误原因,分别在两类错误原因下计算错误条数,因而数据详情汇总的错误条数的加和存在大于错误日志条数的可能。 - -## 4. 上报明细 {#realtime} - -上报明细展示最近 1000 条「监测开关」开启期间的上报日志明细,并展示其入库的处理结果。 - -![上报明细](/img/customEvent/tracking_manager_9.png) - -「监测开关」开启时,系统接收的埋点日志将实时展示在上报明细中,开启时长达到 1 小时,则自动关闭,不再对数据进行实时展示,任何时候重新开启开关都将重置 1 小时的时间进度。建议在测试、验收埋点开发前,手动开启「监测开关」。 - -点击刷新按钮,可刷新页面获取最新数据。 - -每行上报日志明细都可进行格式化查看、复制操作。 - -## 5. 使用埋点管理 - -### 5.1 及时发现埋点错误 - -使用埋点信息页面,掌握埋点整体运行情况,当出现未知数据,或部分数据错误入库、入库失败时,可在错误详情页定位错误,防止数据资产流失。 - -### 5.2 使用上报日志对埋点需求进行测试、验收 - -在埋点开发人员完成开发后,埋点设计人员可模拟用户在游戏中的各类点击行为,而后在「上报日志」中查看实时上报日志,检查上报时机与日志明细是否符合埋点设计方案,从而对埋点需求进行测试、验收。 diff --git a/docs/sdk/tapdb/features/custom-event/user-seq.mdx b/docs/sdk/tapdb/features/custom-event/user-seq.mdx deleted file mode 100644 index 05b1afac5..000000000 --- a/docs/sdk/tapdb/features/custom-event/user-seq.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: 用户精查 -sidebar_position: 18 ---- - -## 1. 什么是用户精查 - -用户精查用于精细排查符合某一类特征或某一确切用户的行为表现。 - -## 2. 用户精查支持的场景 - -* 通过漏斗分析找到了用户在登录或新手教程阶段发生流失,找出在流失前用户产生了哪些行为。 -* 回顾用户在 Crash 或问题前的行为序列,找出问题场景。 - -## 3. 如何使用用户精查 - -通过分析模型得出分析结果若为人群(触发账号数、触发设备数),点击分析结果即可展示符合条件的用户列表及这些用户的详细信息。也可在用户精查界面中,直接使用用户属性作为条件筛选匹配的用户。点击列表中的用户可获取该用户在某一时间段内上报的行为分布、行为序列、用户属性等。 - -### 3.1 用户搜索 - -点击右上方「用户精查」,开启用户精查界面: - -![](/img/customEvent/user/user_seq_1.png) - -选择主体并设置查询条件,可以找出匹配的设备或账号,也可通过账号或设备 ID 进行精确搜索。 - -![](/img/customEvent/user/user_seq_2.png) - -### 3.2 用户列表 - -从分析模型或用户精查可进入到用户列表: - -![](/img/customEvent/user/user_seq_3.png) - -### 3.3 用户行为序列 - -在用户列表中点击用户 ID 可进入用户行为序列查询: -![](/img/customEvent/user/user_seq_4.png) - -**行为事件总量** - -柱状图将展示筛选时间范围内的上报事件量趋势,开启右侧「展示事件分布」可打开行为分布饼状图。 - - -**事件明细** - -* 该用户的行为序列将被按照时间顺序展示。 -* 点击展开按钮可以查看该事件的全部事件属性。 -* 当事件被展开后,鼠标悬停在事件属性上时,可将其设置为外显属性。设置为外显属性的属性,无需展开事件也可在事件名后面查看到该事件属性。 - - -**用户属性** -右侧用户属性中将展示当前用户的属性,可以使用「自定义属性」功能配置这些属性的显隐。 \ No newline at end of file diff --git a/docs/sdk/tapdb/features/custom-event/user-tag.mdx b/docs/sdk/tapdb/features/custom-event/user-tag.mdx deleted file mode 100644 index c53db4f71..000000000 --- a/docs/sdk/tapdb/features/custom-event/user-tag.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: 用户标签 -sidebar_position: 17 ---- - -## 1. 概述 - -用户标签如同用户属性,将用户某类特征作为「用户标签」,用户在该特征下的具体表现作为「标签值」,用以将用户划分为多个不同的群体,便于在各种分析模型中使用标签进行维度分组或筛选。 - -TapDB 的分析模型中分别以「账号」、「设备」作为查询主体进行查询,在用户标签中同样支持分别以「账号」、「设备」作为主体创建标签。 - -目前支持通过「指标值」创建用户标签,后续将开放更多标签创建方式。 - -![概述](/img/customEvent/userTag_1.png) - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ---------- | --------------------------------- | -| 分析师 / 业务人员 | 计算用户在一段时间范围内的行为指标,在分析模型对用户进行分组、筛选 | - -## 3. 创建用户标签 - -点击标签列表右上角的创建标签,选择「指标值标签」。 - -![创建用户标签_1](/img/customEvent/userTag_2.png) - -新建指标值标签页分为「基础信息」、「指标值配置」两部分。 - -![创建用户标签_2](/img/customEvent/userTag_3.png) - -### 3.1 基础信息 - -在「基础信息」部分,依次录入或选择「标签名」、「标签主体」、「标签显示名」、「更新方式」、「备注」。 - -![基础信息](/img/customEvent/userTag_4.png) - -标签显示名展示在分群列表、分析模型中,是业务人员识别标签的依据。 - -标签主体,支持「账号」或「设备」,根据业务场景做选择。 - -标签名是分群存储在系统后台的唯一标识,为方便数据分析人员直接查询数据库表,可命名为带有业务含义的参数名。 - -更新方式分为「手动更新」与「自动更新」。「手动更新」指在完成首次计算后,系统不会自动更新用户标签,用户需要手动进行更新;「自动更新」会在每日 0 点后,以前一日作为基准进行用户标签更新。 - -### 3.2 指标值配置 - -指定时段内,用户完成事件的聚合指标,作为标签值。 - -![指标值配置](/img/customEvent/userTag_5.png) - -完成事件的用户将属于标签,未完成该事件的用户无标签值。 - -通过构建指标确定用户的标签值,在「事件分析」创建指标方法的基础上,增加「事件」的「天数」、「小时数」,同样支持属性筛选与编辑公式。 - -## 4. 用户标签的管理与使用 - -### 4.1 管理用户标签 - -创建的标签会以列表形式展示在用户标签页,用户可以对分群进行查看、编辑、删除、更新、下载、复制操作。 - -![管理用户标签](/img/customEvent/userTag_6.png) - -其中,「数量」代表在该标签下有值的用户数,「数据更新时间」为标签最近一次进行计算的时间。 - -### 4.2 用户标签详情 - -由于指标值标签的值为离散且数量众多的数值,因而在标签详情中,可对指标值的展示分布情况进行设置,以便进行阅读,但不改变指标值本身。 - -![用户标签详情](/img/customEvent/userTag_7.png) - -### 4.3 在分析模型中使用用户标签 - -与用户属性相同,用户标签可作为筛选条件。 - -![在分析模型中使用用户标签_1](/img/customEvent/userTag_8.png) - -同样,用户标签可作为分组维度。 - -![在分析模型中使用用户标签_2](/img/customEvent/userTag_9.png) - -## 5. 最佳实践 - -在「指标值」标签中可计算每个用户在某个时间段内行为的指标,比如登录天数、付费金额等。这些指标值标签可以作为事件分析、属性分析中的分组维度对用户进行分组分析,或作为筛选项,对用户进行进一步的下钻分析。 diff --git a/docs/sdk/tapdb/features/custom-event/virtual-event.mdx b/docs/sdk/tapdb/features/custom-event/virtual-event.mdx deleted file mode 100644 index 2301765db..000000000 --- a/docs/sdk/tapdb/features/custom-event/virtual-event.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: 虚拟事件 -sidebar_position: 15 ---- - -## 1. 概述 - -可以将多个业务含义相似的事件组成虚拟事件,任一基础事件被触发即视为该虚拟事件被触发; - -也可将某一事件通过不同的筛选条件拆分为多个事件,符合筛选条件的基础事件被触发视作该虚拟事件触发。 - -可在事件管理中新建、管理虚拟事件。 - -![概述](/img/customEvent/virtualEvent_1.png) - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ------------------------- | ------------------------------------ | -| 埋点设计人员(数据产品经理 / 分析师) | 对埋点事件进行转化,降低埋点方案设计的复杂度,弥补埋点设计、开发中的缺陷 | -| 数据分析人员(数据产品经理 / 分析师 / 运营) | 对相似业务含义的事件进行组合,或对某一事件进行拆分,提高日常分析使用效率 | - -## 3. 新建虚拟事件 - -点击事件管理右上角「新建事件」,选择「虚拟事件」。 - -![新建虚拟事件](/img/customEvent/virtualEvent_2.png) - -### 3.1 填写基础信息 - -填入虚拟事件名、虚拟事件显示名和说明,事件名默认以「#ve@」开头。 - -![填写基础信息](/img/customEvent/virtualEvent_4.png) - -### 3.2 编辑虚拟事件的定义规则 - -构成虚拟事件的基础事件之间为「或」的关系,任一基础事件被触发(若对基础事件进行筛选则此处需满足筛选条件)即视为该虚拟事件被触发。 - -当虚拟事件由 2 个或以上基础事件构成时,可对其进行全局筛选,全部基础事件均需满足该筛选条件。 - -![编辑虚拟事件的定义规则](/img/customEvent/virtualEvent_3.png) - -## 4. 虚拟事件的管理与使用 - -### 4.1 管理虚拟事件 - -可在事件管理页面中,对虚拟事件进行管理,虚拟事件可创建数量上限为 300,可以进行编辑、复制和删除操作。 - -![管理虚拟事件](/img/customEvent/virtualEvent_5.png) - -### 4.2 分析模型中使用的注意事项 - -虚拟事件在使用上和预置、自定义事件一致,其所关联的事件属性为所有基础属性的关联事件属性的交集。 - -虚拟事件的触发用户数含有对基础事件联合去重的逻辑,因而在部分场景中,虚拟事件的触发用户数会小于所有基础事件触发用户数的直接加和。 - -## 5. 最佳实践 - -### 5.1 将多个业务含义相似的事件合并为单个事件 - -埋点设计中,将「大世界战斗」、「副本战斗」、「PVP 战斗」作为 3 种不同的事件上报。 - -现需对用户所有战斗行为进行统计,因而可在虚拟事件中将该 3 个事件共同作为构成虚拟事件「战斗」的基础事件,用户触发任意类型的战斗行为便视作触发「战斗」,从而实现对用户的所有战斗行为进行统计。 - -### 5.2 将单个事件拆分为业务含义更具体的多个事件 - -埋点设计中,将所有页面的浏览统一上报为「页面浏览」,并通过事件属性「页面类型」来对各页面进行区分。 - -日常分析中,首页、商店页、充值页的分析需求较为频繁,因而可在虚拟事件中将「页面浏览」作为基础事件并对其「页面类型」进行筛选,构造出 3 个虚拟事件「首页浏览」、「商店页浏览」、「充值页浏览」,这样可以实现快速统计并满足日常分析需求。 diff --git a/docs/sdk/tapdb/features/custom-event/virtual.mdx b/docs/sdk/tapdb/features/custom-event/virtual.mdx deleted file mode 100644 index 035381a7b..000000000 --- a/docs/sdk/tapdb/features/custom-event/virtual.mdx +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: 虚拟属性 -sidebar_position: 12 ---- - -## 1. 概述 - -对于已经上报的事件属性和用户属性,可以通过设置虚拟属性将原先上传的数据映射为另一种展示值或计算值,使初始埋点时的属性值不与展示值相同,并可在后期可对数据进行加工,提高灵活性。 - -虚拟属性通过 SQL 表达式对基础属性字段进行计算,得到一个新的属性字段。 - -可在事件属性管理中新建、管理虚拟事件属性,在用户属性管理中创建、管理虚拟用户属性。 - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ------------------------- | ------------------------------------ | -| 埋点设计人员(数据产品经理 / 分析师) | 对埋点字段进行转化,降低埋点方案设计的复杂度,弥补埋点设计、开发中的缺陷 | -| 数据分析人员(数据产品经理 / 分析师 / 运营) | 自助从现埋点数据中提取部分低频或少量场景使用的分析维度,快速响应分析需求 | - -## 3. 新建虚拟属性 - -点击事件属性管理、用户属性管理右上角「新建事件属性」或「新建用户属性」,选择「新建虚拟属性」。 - -![新建虚拟属性](/img/customEvent/virtual_1.png) - -### 3.1 填写基础信息 - -#### 3.1.1 通用基础信息 - -填入或选择属性名、显示名、数据类型、单位、说明,属性名默认以 `#vp@` 开头。 - -![通用基础信息](/img/customEvent/virtual_2.png) - -#### 3.1.2 虚拟事件属性的独有基础信息 - -选择「关联主体」,不同的选择对应不同的基础属性*(预置属性、自定义属性统称为基础属性)*范围,以及该虚拟属性应用范围: - -| 关联主体 | 基础属性范围 | 虚拟属性应用范围 | -| ---- |-----------------------| ------------- | -| 无主体 | 事件属性 | 以设备或账号作为查询主体时 | -| 账号 | 事件属性 & 账号属性
    账号属性 | 以账号作为查询主体时 | -| 设备 | 事件属性 & 设备属性
    设备属性 | 以设备作为查询主体时 | - -选择「关联事件」,不同的选择对应不同的关联逻辑,如下: - -- 自动识别:关联 SQL 代码所涉及基础事件属性所关联的事件的并集 - -- 全部事件:关联全部事件,若新增事件则自动关联 - -- 指定事件:关联指定事件 - -![事件属性独有](/img/customEvent/virtual_3.png) - -#### 3.1.3 虚拟用户属性的独有基础信息 - -选择「用户标识」,不同的选择对应不同的基础属性范围,以及该虚拟属性应用范围: - -| 用户标识 | 基础属性范围 | 虚拟属性应用范围 | -| ---- | ------ | --------- | -| 账号 | 账号属性 | 以账号为查询主体时 | -| 设备 | 设备属性 | 以设备为查询主体时 | - -![户属性独有基础信息](/img/customEvent/virtual_4.png) - -### 3.2 编辑虚拟属性的创建规则 - -在左侧列表中可以快速获取当前可用的基础属性的信息,在文本框输入 SQL 表达式。 - -![创建规则](/img/customEvent/virtual_5.png) - -虚拟属性的 SQL 表达式使用 presto 语法,可以访问 [presto 文档](https://trino.io/docs/332/functions.html) 获取 presto 的语法以及函数的使用方法。 - -可通过「插入模板」功能,插入常用的应用场景所需的代码片段,通过替换字段名和修改逻辑即可简单快捷实现虚拟属性的创建。 - -### 3.3 逻辑校验 - -输入内容后,可点击「校验」检查基础属性范围、SQL 语法,校验成功则会在「调试」板块出现属性的值输入框。 - -输入校验值后查看结果,可以校验结果是否符合需求。如果符合需求,可以点击「下一步」完成属性创建,如果不符合预期则可继续修改创建规则,并重复校验。 - -![逻辑校验](/img/customEvent/virtual_6.png) - -## 4. 虚拟属性的管理与使用 - -### 4.1 管理虚拟属性 - -可在事件属性管理或用户属性管理页面中,对虚拟属性进行管理,虚拟事件属性、虚拟用户属性可创建数量上限均为 300,可以进行编辑、删除操作。 - -![管理虚拟属性](/img/customEvent/virtual_7.png) - -### 4.2 模型中使用的注意事项 - -虚拟属性在使用上和通常的属性一致,根据类型决定其计算逻辑以及筛选条件。 - -虚拟事件属性可以在计算其关联的事件时被使用,关联关系可以自定义。 - -虚拟用户属性,可用场景等同一般的用户属性。 - -## 5. 最佳实践 - -### 5.1 弥补埋点开发中的字段类型上报错误 - -错误将用户年龄字段「age」上报为文本,在重新发版前,现暂时紧急需对用户年龄进行分析: - -```sql -cast("age" as int) -``` - -### 5.2 计算用户生命周期 - -根据用户的激活时间「activation_time」和事件发生时间「time」,现需计算用户行为发生时的生命周期: - -```sql -date_diff('day', date("activation_time"), date("time")) -``` - -### 5.3 通过算术运算完成单位转化 - -已有以「分」为单位的支付金额字段「amount」,现需要将其转化为以「元」为单位: - -```sql -"amount" / 100 -``` - -### 5.4 标识独立角色或其他所希望进行分析的主体 - -一个用户可在不同服务器创建不同角色,根据用户身份标识字段「user_id」和服务器标识字段「server_id」生成标识角色的字段: - -```sql -concat("server_id", "user_id") -``` - -### 5.5 截取字段获得复杂字段中的关键维度信息 - -根据论坛帖子的 ID「post_id」*(实例:10086202012310001)*截取发帖月份: - -```sql -substring("post_id", 6, 6) -``` - -### 5.6 将页面按照功能模块分类 - -埋点记录了用户访问的 url 地址,现需要了解用户使用的功能模块的情况: - -```sql -case - - when "url" like '%home%' then '首页' - - when "url" like '%store%' then '商城' - - when "url" like '%stage%' then '剧情' - - else '其他' - -end -``` diff --git a/docs/sdk/tapdb/features/diagnosis.md b/docs/sdk/tapdb/features/diagnosis.md deleted file mode 100644 index b7b6b194c..000000000 --- a/docs/sdk/tapdb/features/diagnosis.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: 诊断 -sidebar_position: 3 ---- - -## 1.概述 - -「诊断」是基于 TapTap 自研的崩溃监测 SDK - Themis 监控到的数据生成报表,并提供相关的日志信息帮助开发者高效定位并解决问题的 TapDB 功能模块 - -## 2.如何使用「诊断」? - -「诊断」功能面向开放 TapPlay 的游戏及接入了 TapSDK 并启用了 Themis 功能的游戏,如何在接入过程中调整配置,请参考:[接入指南 - 诊断模块](/sdk/tapdb/sdk/client-side-integration/#themis) - -## 3.「诊断」能够覆盖什么场景? - -- 监控游戏过程中产生的崩溃、闪退、报错,获取用户行为日志 - -## 4.「诊断」包含哪些模块? - - - 崩溃分析:监控游戏过程中产生的崩溃闪退,提供定位条件及报表,可具体到某一特定用户的崩溃信息 - - - 错误分析:监控游戏过程中产生的报错、自定义日志等信息,提供定位条件及报表,可具体到某一特定用户的上报信息 - - - 符号表管理:上传对应的符号表可以对 APP 发生 Crash 的堆栈进行解析和还原,快速并准确地定位用户 APP 发生 Crash 的代码位置 - -![「诊断」包含哪些模块?](/img/customEvent/diagnosis/diagnosis-1.png) - -## 5.崩溃分析及错误分析如何使用? - -*「崩溃分析」及「错误分析」功能一致,仅是监测的条件不同,故一并说明* - -### 通过「概览」观测程序的运行稳定性 - -1. 观察选定日期内程序的运行稳定性趋势,通过报错率、上报趋势图确定选定日期内是否存在问题 - -![观测程序的运行稳定性](/img/customEvent/diagnosis/diagnosis-2.png) - -2. 通过「高占比统计」柱状分布图快速定位问题可能存在的场景(如游戏接入了 TapDB 且开放了 TapPlay,则会额外提供游戏在 TapTap 应用版本的上报分布图) - -![高占比统计](/img/customEvent/diagnosis/diagnosis-3.png) - -3. 通过「Top 10 问题列表」的详细信息进入报错最多的问题详情,快速锚定问题 - -![Top 10 问题列表](/img/customEvent/diagnosis/diagnosis-4.png) - -### 如何定位具体问题原因? - -1. 通过「平台」筛选您要查看的应用所属平台为 Andorid 还是 IOS,默认选中安卓 - -2. 通过「数据」筛选您要查看的是哪个 SDK 上报的数据,包含 TapDB 及 TapPlay - -3. 通过筛选器设置条件定位特定条件的报错人群,设置条件后,将重新加载数据筛选出符合条件的信息 - -![通过筛选器设置条件定位特定条件的报错人群](/img/customEvent/diagnosis/diagnosis-5.png) - -4. 通过设置的过滤条件将筛选出符合条件的报错详细信息,不同用户的相同报错会根据特定特征进行归类,合并为同一个问题 ID - - - a. 点击问题 ID 即可进入该类问题的详情页,可查看该类问题的上报趋势及分布情况 - - - b. 点击上报 ID,侧边栏弹出特定用户的报错详情,开发者可根据「出错堆栈」、「跟踪数据」等定位问题原因 - -![筛选出符合条件的报错详细信息](/img/customEvent/diagnosis/diagnosis-6.png) - -## 6.「符号表管理」如何使用? - -「符号表管理」用于管理开发者所要上传的符号表,向开发者提供上传,筛选、删除等能力,详细使用流程可见下图,如有疑问,可向我们提交工单咨询 - -![符号表管理](/img/customEvent/diagnosis/diagnosis-7.png) diff --git a/docs/sdk/tapdb/features/exchange-rate.mdx b/docs/sdk/tapdb/features/exchange-rate.mdx deleted file mode 100644 index 031d75b9e..000000000 --- a/docs/sdk/tapdb/features/exchange-rate.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: 汇率表 -sidebar_position: 6 ---- - -import { ExchangeTable } from "/src/docComponents/ExchangeTable"; - - diff --git a/docs/sdk/tapdb/features/subcontinent.mdx b/docs/sdk/tapdb/features/subcontinent.mdx deleted file mode 100644 index 8e555a295..000000000 --- a/docs/sdk/tapdb/features/subcontinent.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: 次大陆 -sidebar_position: 4 ---- - -次大陆是按照联合国的规定进行划分。每个次大陆包含的国家或地区如下所示 - -## 非洲 - -**东非** - -布隆迪 科摩罗 吉布提 厄立特里亚 埃塞俄比亚 肯尼亚 马达加斯加 马拉维 毛里求斯 马约特 莫桑比克 留尼汪 卢旺达 塞舌尔群岛 索马里 南苏丹 坦桑尼亚 乌干达 赞比亚 津巴布韦 法属南部领土 英属印度洋领地 - -**中非** - -安哥拉 喀麦隆 中非共和国 乍得 刚果 扎伊尔 赤道几内亚 加蓬 圣多美和普林西比 - -**北非** - -阿尔及利亚 埃及 阿拉伯利比亚民众国 摩洛哥 苏丹 突尼斯 西撒哈拉 - -**南非** - -博茨瓦纳 斯威士兰 莱索托 纳米比亚 南非 - -**西非** - -贝宁 布基纳法索 佛得角 象牙海岸 冈比亚 加纳 几内亚 几内亚比绍 利比里亚 马里 毛里塔尼亚 尼日尔 尼日利亚 塞内加尔 塞拉利昂 圣赫勒拿 多哥 - -## 美洲 - -**加勒比海地区** - -安圭拉 安提瓜和巴布达 阿鲁巴 巴巴多斯 巴哈马 英属维京群岛 博奈尔岛、圣尤斯达蒂斯和萨巴 开曼群岛 古巴 库拉索 多米尼加 多米尼加共和国 格林纳达 瓜德罗普岛 海地 马提尼克群岛 蒙塞拉特群岛 波多黎各 圣马丁岛 圣巴泰勒米 圣基茨和尼维斯 圣卢西亚 圣马丁 圣文森特和格林纳丁斯 特立尼达和多巴哥 特克斯和凯科斯群岛 美属维京群岛 牙买加 - -**中美洲** - -伯利兹 哥斯达黎加 危地马拉 萨尔瓦多 洪都拉斯 墨西哥 尼加拉瓜 巴拿马 - -**北美洲** - -百慕大 加拿大 格陵兰 圣皮埃尔和密克隆 美国 - -**南美洲** - -阿根廷 玻利维亚 布维特岛 巴西 智利 哥伦比亚 厄瓜多尔 福克兰群岛 法属圭亚那 圭亚那 巴拉圭 秘鲁 南乔治亚岛和南桑威齐群岛 苏里南 乌拉圭 委内瑞拉 - -## 亚洲 - -**中亚** - -哈萨克斯坦 吉尔吉克斯坦 塔吉克斯坦 土库曼斯坦 乌兹别克斯坦 - -**东亚** - -中国大陆 中国香港特别行政区 日本 中国澳门特别行政区 蒙古 朝鲜民主共和国 大韩民国 中国台湾地区 - -**东南亚** - -文莱 柬埔寨 印度尼西亚 老挝人民民主共和国 马来西亚 缅甸 菲律宾 新加坡 泰国 越南 东帝汶 - -**南亚** - -阿富汗 孟加拉 不丹 印度 伊朗伊斯兰共和国 马尔代夫 尼泊尔 巴基斯坦 斯里兰卡 - -**西亚** - -亚美尼亚 阿塞拜疆 巴林 塞浦路斯 格鲁吉亚 伊拉克 以色列 约旦 科威特 黎巴嫩 阿曼 - -巴勒斯坦领土 卡塔尔 沙特阿拉伯 叙利亚 土耳其 阿拉伯联合酋长国 也门 - -## 欧洲 - -**东欧** - -白俄罗斯 保加利亚 捷克共和国 匈牙利 摩尔多瓦共和国 波兰 罗马尼亚 俄罗斯 斯洛伐克共和国 乌克兰 - -**北欧** - -奥兰群岛 丹麦 爱沙尼亚 法罗群岛 芬兰 格恩西岛 冰岛 爱尔兰 曼岛 泽西岛 拉脱维亚 立陶宛 挪威 斯瓦尔巴特和扬马延 瑞典 英国 - -**南欧** - -阿尔巴尼亚 安道尔 波斯尼亚和黑山共和国 克罗地亚 直布罗陀 希腊 意大利 科索沃 马耳他 黑山共和国 前南斯拉夫马其顿共和国 葡萄牙 圣马力诺 塞尔维亚 斯洛文尼亚 西班牙 圣座(梵蒂冈) - -**西欧** - -奥地利 比利时 法国 德国 列支敦士登 卢森堡 摩纳哥 荷兰 瑞士 - -## 大洋洲 - -**澳大拉西亚** - -澳大利亚 圣诞岛 科科斯群岛 赫德与麦克唐纳群岛 新西兰 诺福克岛 - -**美拉尼西亚** - -斐济 新喀里多尼亚 巴布亚新几内亚 所罗门群岛 瓦努阿图 - -**密克罗尼西亚** - -关岛 基里巴斯 马绍尔群岛 密克罗尼西亚 瑙鲁 北马里亚纳群岛 帕劳 美国边远小岛 - -## 大洋洲海外地区 - -南极洲 - -**波利尼西亚** - -美属萨摩亚 库克群岛 法属波利尼西亚 纽埃 皮特凯恩群岛 托克劳 汤加 图瓦卢 瓦利斯和富图纳 萨摩亚 diff --git a/docs/sdk/tapdb/sdk/_category_.json b/docs/sdk/tapdb/sdk/_category_.json deleted file mode 100644 index 361b849f7..000000000 --- a/docs/sdk/tapdb/sdk/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "开发指南", - "position": 3 -} diff --git a/docs/sdk/tapdb/sdk/client-side-integration.mdx b/docs/sdk/tapdb/sdk/client-side-integration.mdx deleted file mode 100644 index 80a6d5440..000000000 --- a/docs/sdk/tapdb/sdk/client-side-integration.mdx +++ /dev/null @@ -1,1780 +0,0 @@ ---- -title: 客户端接入 -sidebar_label: 客户端接入 -sidebar_position: 3 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import {Conditional} from '/src/docComponents/conditional'; -import UnitySDKInstallation from "../../_partials/unity-sdk-installation.mdx"; - -## 介绍 - -TapSDK 提供了一套可供游戏开发者收集账号数据的 API。 -系统会收集账号数据并进行分析,最终形成数据报表,帮助游戏开发者分析账号行为并优化游戏。 - -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - - - -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | -| 读写存储权限(可选) | 用于存储用户标识 | 用户首次使用该功能时由开发者申请权限| -| 读取电话状态(可选) | 用于更加精确地描述用户画像 | 用户首次使用该功能时由开发者申请权限 | - -对于可选权限,SDK 不会主动发起申请,只在用户已授权的情况下获取对应数据,需要由开发者决定是否申请。 - -该模块将在应用中添加如下权限: - -``` - - - - - - - ``` - - - -<> - - - -<> - - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启应用配置开通数据分析服务、绑定 API 域名; - - -## SDK 集成 - -请先[下载](/tap-download) TapSDK,并添加相关依赖。 - - - -<> - -如果只需要单独使用 TapDB,可以只导入依赖 `common` 和 `tapdb`。 - -Unity v3.7.1 及更高版本,还需要导入 `com.leancloud.storage`。 - - - - -<> - -如果只需要单独使用 TapDB,可以只导入依赖 `common` 和 `tapdb`。 - - -{`repositories { - flatDir { - dirs 'libs' - } -} -dependencies { - // ... - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') // 必选:TapSDK 基础库 - implementation (name:'TapDB_${sdkVersions.taptap.android}', ext:'aar') // 数据统计 - implementation 'com.taptap:lc-storage-android:${sdkVersions.leancloud.java}' -}`} - - - -<> - -如果只需要单独使用 TapDB,可以只导入依赖 `common` 和 `tapdb`。 - - -{`// 登录 -TapBootstrapSDK.framework -TapCommonSDK.framework -TapLoginSDK.framework -LeanCloudObjc.framework -TapCommonResource.bundle -TapLoginResource.bundle -//TapDB -TapDB.framework`} - - -在 `Build Settings` 中的 `link` -> `Other Linker Flags` 中加入: `-ObjC` 和 `-Wl -ld_classic` - -需要为 Xcode 工程引入下列依赖的框架或库 - -名词 | 含义 | 备注 ---- | --- | --- -AdSupport.framework | 用来获取设备广告标识,跟踪设备 -iAd.framework | 广告框架 | 设置为 optional -AdServices.framework | 广告框架 | 设置为 optional -AppTrackingTransparency.framework | iOS 14 新增 app 追踪框架(若无需在 iOS 14 以上追踪 IDFA 可不添加该依赖) | 设置为 optional -SystemConfiguration.framework | -CoreMotion.framework | -CoreTelephony.framework | -Security.framework | 用来持久化存储设备 ID -libc++.tdb | -libresolv.tbd | -libz.tbd | -libsqlite3.0.tbd | - - - - -<> - -#### 安装插件 - -* 下载 [TapSDK UE4](/tap-download),TapSDK-UE4-xxx.zip 解压后将 `TapDB`、`TapCommon` 文件夹 Copy 到项目的 `Plugins` 目录中 -* 重启 Unreal Editor -* 打开 编辑 > 插件 > 项目 > TapTap,开启 `TapDB` 模块 - -#### 添加依赖 - -在 Project.Build.cs 中添加所需模块: - -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapDB" -}); -``` - - - - - -:::info -如果你曾使用 `<3.6.3` 的 TapSDK 的数据分析功能,且初始化时区域选择为国际(`IO`),那么升级 SDK 至 `>=3.6.3` 的 TapSDK 前需要先迁移数据。 -请提交工单联系我们迁移数据后再升级。 -::: - - -## 初始化 SDK - - -初始化 SDK 并上报一个设备登录( `device_login` )事件,调用这个接口是使用其他接口的先决条件,需要尽早调用。 - - -:::info -以下两种初始化方式结合使用场景任选其一即可。 -::: - -### TapSDK 初始化 {#tapsdk-init} - -如果项目中同时集成[内建账户系统的 TapTap 登录](/sdk/taptap-login/guide/start/#sdk-初始化)并完成了初始化,需要在此基础上添加 TapDBConfig 配置,即可同步初始化 TapDB。代码如下: - - - -<> - - - -```cs -using TapTap.Bootstrap; // 命名空间 - -var config = new TapConfig.Builder() - .ClientID("your_client_id") // 必须,开发者中心对应 Client ID - .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token - .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 - .TapDBConfig(true, "gameChannel", "gameVersion", true) // TapDB 会根据 TapConfig 的配置进行自动初始化 - .ConfigBuilder(); - -TapBootstrap.Init(config); -``` - - - - - -```cs -using TapTap.Bootstrap; // 命名空间 - -var config = new TapConfig.Builder() - .ClientID("your_client_id") // 必须,开发者中心对应 Client ID - .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token - .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .RegionType(RegionType.IO) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 - .TapDBConfig(true, "gameChannel", "gameVersion", true) // TapDB 会根据 TapConfig 的配置进行自动初始化 - .ConfigBuilder(); - -TapBootstrap.Init(config); -``` - - - -TapDBConfig 说明 - -```cs -public Builder TapDBConfig(bool enable, string channel, string gameVersion, bool advertiserIDCollectionEnabled) -``` - -参数 | 可为空 | 说明 ---- | --- | --- -enable | 否 | 是否开启 TapDB -channel | 是 | 分包渠道,长度不大于 256 -version | 是 | 游戏版本,为空时,自动获取游戏安装包的版本,长度不大于 256 -advertiserIDCollectionEnabled | 否 | IDFA 开关,请参考 [收集设备指纹](/sdk/tapdb/sdk/client-side-integration#idfaios) - - - -<> - - - -```java -TapDBConfig tapDBConfig = new TapDBConfig(); -tapDBConfig.setEnable(true); //是否开启 TapDB -tapDBConfig.setChannel("gameChannel"); //分包渠道,长度不大于 256 -tapDBConfig.setGameVersion("1.0.0"); //游戏版本,为空时,自动获取游戏安装包的版本,长度不大于 256 - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(getApplicationContext()) - .withClientId("your_client_id") // 开发者中心对应 Client ID - .withClientToken("your_client_token") // 开发者中心对应 Client Token - .withServerUrl("https://your_server_url") // 开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .withRegionType(TapRegionType.CN) // TapRegionType.CN: 中国大陆 TapRegionType.IO: 其他国家或地区 - .withTapDBConfig(tapDBConfig) - .build(); -TapBootstrap.init(MainActivity.this, tapConfig); -``` - - - - - -```java -TapDBConfig tapDBConfig = new TapDBConfig(); -tapDBConfig.setEnable(true); //是否开启 TapDB -tapDBConfig.setChannel("gameChannel"); //分包渠道,长度不大于 256 -tapDBConfig.setGameVersion("1.0.0"); //游戏版本,为空时,自动获取游戏安装包的版本,长度不大于 256 - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(getApplicationContext()) - .withClientId("your_client_id") // 开发者中心对应 Client ID - .withClientToken("your_client_token") // 开发者中心对应 Client Token - .withServerUrl("https://your_server_url") // 开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .withRegionType(TapRegionType.IO) // TapRegionType.CN: 中国大陆 TapRegionType.IO: 其他国家或地区 - .withTapDBConfig(tapDBConfig) - .build(); -TapBootstrap.init(MainActivity.this, tapConfig); -``` - - - - - -<> - - - -```objectivec -// 初始化 SDK -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // 开发者中心对应 Client ID -config.clientToken = @"your_client_token"; // 开发者中心对应 Client Token -config.region = TapSDKRegionTypeCN; // TapSDKRegionTypeCN: 中国大陆 TapSDKRegionTypeIO: 其他国家或地区 -config.serverURL = @"https://your_server_url"; // 开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - -TapDBConfig * dbConfig = [[TapDBConfig alloc]init]; -dbConfig.enable = YES; //是否开启 TapDB -dbConfig.channel=@"taptap"; //分包渠道,长度不大于 256 -dbConfig.gameVersion=@"1.0.0"; //游戏版本,为空时,自动获取游戏安装包的版本,长度不大于 256 -dbConfig.advertiserIDCollectionEnabled=YES; //IDFA 开关,请参考 「收集设备指纹」章节 -config.dbConfig = dbConfig; - -config.region = TapSDKRegionTypeCN; -[TapBootstrap initWithConfig:config]; -``` - - - - - -```objectivec -// 初始化 SDK -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // 开发者中心对应 Client ID -config.clientToken = @"your_client_token"; // 开发者中心对应 Client Token -config.region = TapSDKRegionTypeIO; // TapSDKRegionTypeCN: 中国大陆 TapSDKRegionTypeIO: 其他国家或地区 -config.serverURL = @"https://your_server_url"; // 开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - -TapDBConfig * dbConfig = [[TapDBConfig alloc]init]; -dbConfig.enable = YES; //是否开启 TapDB -dbConfig.channel=@"taptap"; //分包渠道,长度不大于 256 -dbConfig.gameVersion=@"1.0.0"; //游戏版本,为空时,自动获取游戏安装包的版本,长度不大于 256 -dbConfig.advertiserIDCollectionEnabled=YES; //IDFA 开关,请参考 「收集设备指纹」章节 -config.dbConfig = dbConfig; - -[TapBootstrap initWithConfig:config]; -``` - - - - - -<> - -使用这种方式初始化需要导入 `TapBootstrap` 包、开启 `TapBootstrap` 模块,并将 `TapBootstrap` 添加到 `Project.Build.cs` 文件中。 - -导入头文件: - -```cpp -#include "TapUEBootstrap.h" -#include "TapUECommon.h" -#include "TapUEDB.h" -``` - - - -```cpp -FTUConfig Config; -Config.ClientID = ClientID; // 开发者中心对应 Client ID -Config.ClientToken = ClientToken; // 开发者中心对应 Client Token -Config.ServerURL = ServerURL; // 开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -Config.RegionType = ERegionType::CN; -Config.DBConfig.Enable = true; -Config.DBConfig.Channel = Channel; -Config.DBConfig.GameVersion = GameVersion; -Config.DBConfig.AdvertiserIDCollectionEnabled = AdvertiserIDCollectionEnabled; -TapUEBootstrap::Init(Config); -``` - - - - - -```cpp -FTUConfig Config; -Config.ClientID = ClientID; // 必须,开发者中心对应 Client ID -Config.ClientToken = ClientToken; // 开发者中心对应 Client Token -Config.ServerURL = ServerURL; // 开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -Config.RegionType = ERegionType::IO; -Config.DBConfig.Enable = true; -Config.DBConfig.Channel = Channel; -Config.DBConfig.GameVersion = GameVersion; -Config.DBConfig.AdvertiserIDCollectionEnabled = AdvertiserIDCollectionEnabled; -TapUEBootstrap::Init(Config); -``` - - - -#### DBConfig 说明 - -参数 | 可为空 | 说明 ---- | --- | --- -Enable | 否 | 是否开启 TapDB -Channel | 是 | 分包渠道,长度不大于 256 -Version | 是 | 游戏版本,为空时,自动获取游戏安装包的版本,长度不大于 256 -AdvertiserIDCollectionEnabled | 否 | IDFA 开关,请参考 [收集设备指纹](/sdk/tapdb/sdk/client-side-integration#idfaios) - - - - - -### TapDB 单独初始化 - -在单独使用 TapDB 功能时(即不接登录功能时,不导入 `TapBootstrap` 包时),可以通过以下方式初始化 TapDB。 - - - -<> - - - -```cs -public static void Init(string clientId, string channel, string gameVersion, bool isCN) - -TapDB.Init("clientId", "taptap", "gameVersion", true); -``` - - - - - -```cs -public static void Init(string clientId, string channel, string gameVersion, bool isCN) - -TapDB.Init("clientId", "taptap", "gameVersion", false); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -clientId | 否 | Client Id 可以在控制台获取 -channel | 是 | 分包渠道 -version | 是 | 游戏版本,为空时,自动获取游戏安装包的版本 -isCN | 是 | 区域,true 表示中国大陆,false 表示中国大陆以外的国家或地区。 - - - -<> - - - -```java -public synchronized static void init(final Context context, - final String clientId, - final String channel, - final String gameVersion, - final boolean isCN) - -public synchronized static void init(final Context context, - final String clientId, - final String channel, - final String gameVersion, - final boolean isCN, - final JSONObject properties) - -TapDB.init(getApplicationContext(), "clientId", "taptap", "gameVersion", true); -``` - - - - - -```java -public synchronized static void init(final Context context, - final String clientId, - final String channel, - final String gameVersion, - final boolean isCN) - -public synchronized static void init(final Context context, - final String clientId, - final String channel, - final String gameVersion, - final boolean isCN, - final JSONObject properties) - -TapDB.init(getApplicationContext(), "clientId", "taptap", "gameVersion", false); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -context | 否 | 当前 Application 或 Activity 的 Context 对象 -clientId | 否 | Client Id 可以在控制台获取 -channel | 是 | 分包渠道,长度不大于 256 -version | 是 | 游戏版本,长度不大于 256,为空时,自动获取游戏安装包的版本, -isCN | 是 | 区域,true 表示中国大陆,false 表示中国大陆以外的国家或地区。 -properties | 是 | 设备登录( `device_login` )的事件属性,可以传入预置属性覆盖 SDK 的默认取值,也可以传入在后台配置过的自定义属性 - - - -<> - - - -```objectivec -+ (void)onStartWithClientId:(NSString *)clientId channel:(nullable NSString *)channel version:(nullable NSString *)gameVersion isCN:(BOOL)isCN; - -[TapDB onStartWithClientId:@"clientid" channel:@"taptap" version:@"gameVersion" isCN:YES]; -``` - - - - - -```objectivec -+ (void)onStartWithClientId:(NSString *)clientId channel:(nullable NSString *)channel version:(nullable NSString *)gameVersion isCN:(BOOL)isCN; - -[TapDB onStartWithClientId:@"clientid" channel:@"taptap" version:@"gameVersion" isCN:NO]; -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -appId | 否 | 创建游戏时获得的APPID -channel | 是 | 分包渠道,长度不大于 256 -version | 是 | 游戏版本,长度不大于 256,为空时,自动获取游戏安装包的版本( Xcode 配置中的 Version ) -isCN | 是 | YES 表示中国大陆,NO 表示中国大陆以外的国家或地区 - - - -<> - -导入头文件: - -```cpp -#include "TapUEDB.h" -``` - -初始化 TapDB: - - - -```cpp -FTUDBConfig Config; -Config.ClientId = TEXT("your client id"); -Config.RegionType = ERegionType::CN; -Config.Channel = TEXT("分包渠道名称,可为空"); -Config.GameVersion = TEXT("游戏版本"); -TapUEDB::Init(Config); -``` - - - - - -```cpp -FTUDBConfig Config; -Config.ClientId = TEXT("your client id"); -Config.RegionType = ERegionType::IO; -Config.Channel = TEXT("分包渠道名称,可为空"); -Config.GameVersion = TEXT("游戏版本"); -TapUEDB::Init(Config); -``` - - - -#### DBConfig 说明 - -参数 | 可为空 | 说明 ---- | --- | --- -ClientId | 否 | 开发者中心对应 Client ID -RegionType | 否 | 对应 开发者中心 > 应用配置 > 适用地区 -Channel | 是 | 分包渠道,长度不大于 256 -GameVersion | 是 | 游戏版本,为空时,自动获取游戏安装包的版本,长度不大于 256 - - - - -## 设置账号 - -### 设置账号 ID - -调用该 API 记录一个账号,当账号登录时调用。 - -调用后会上报一个账号登录( `user_login` )事件,并将这个设备的是否有用户注册过( `has_user` )属性置为 `true`。 - -在重启应用或调用清除账号 ID( `clearUser` )前,上报的事件都会带有该账号 ID。 - - - -<> - -```cs -public static void SetUser(string userId) - -TapDB.SetUser("userId"); -``` - -| 字段 | 可为空 | 说明 | -| --- | --- | --- | -| userId | 否 | 账号的唯一字符串,字符串长度不大于 256,只能包含数字、大小写字母、下划线(`_`)、短横(`-`)
    开发者需要保证不同账号的 `userId` 均不相同。 | - - - -<> - -```java -public static void setUser(final String userId) - -public static void setUser(final String userId, final JSONObject properties) - -TapDB.setUser("userId"); - -// 同时传递事件属性 -JSONObject properties = new JSONObject(); -properties.put("currentPoints", 10); -TapDB.setUser("userId", properties); -``` - -| 字段 | 可为空 | 说明 | -| --- | --- | --- | -| userId | 否 | 账号的唯一字符串,字符串长度不大于 256,只能包含数字、大小写字母、下划线(`_`)、短横(`-`)
    开发者需要保证不同账号的 `userId` 均不相同。 | -| properties | 是 | 账号登录( `user_login` )的事件属性 - - - -<> - -```objectivec -+ (void)setUser:(NSString *)userId; - -+ (void)setUser:(NSString *)userId properties:(nullable NSDictionary *)properties; - -[TapDB setUser:@"userId"]; - -// 同时传递事件属性 -[TapDB setUser:@"userId" properties:@{@"#currentPoints":@10}]; -``` - -| 字段 | 可为空 | 说明 | -| --- | --- | --- | -| userId | 否 | 账号的唯一字符串,字符串长度不大于 256,只能包含数字、大小写字母、下划线(`_`)、短横(`-`)
    开发者需要保证不同账号的 `userId` 均不相同 | -| properties | 是 | 账号登录( `user_login` )的事件属性 - - - -<> - -```cpp -FString UserId = TEXT("userId"); -FString LoginType = TUDBType::LoginType::TapTap; -TapUEDB::SetUserWithLoginType(UserId, LoginType); -``` - -| 字段 | 可为空 | 说明 | -| --- | --- | --- | -| userId | 否 | 账号的唯一字符串,字符串长度不大于 256,只能包含数字、大小写字母、下划线(`_`)、短横(`-`)
    开发者需要保证不同账号的 `userId` 均不相同 | -| LoginType | 是 | 账号登录方式,参考 `TUDBType::LoginType` - - - -
    - - -### 清除账号 ID - -当用户进行登出时,可调用 `clearUser` 清除当前 SDK 中保存的账号 ID,后续上报的事件将不会带有账号 ID,调用该接口不会上报任何事件。 - - - -```cs -public static void ClearUser() - -TapDB.ClearUser(); -``` - -```java -public static void clearUser() - -TapDB.clearUser(); -``` - -```objectivec -+ (void)clearUser; - -[TapDB clearUser]; -``` - -```cpp -TapUEDB::ClearUser(); -``` - - - -### 设置账号名称 - -在用户进行账号登录后,可调用该接口设置该账号的名称,调用后将更新账号的账号名称( `user_name` )属性。 - - - -```cs -public static void SetName(string name) - -TapDB.SetName("Tarara"); -``` - -```java -public static void setName(final String name) - -TapDB.setName("Tarara"); -``` - -```objectivec -+ (void)setName:(NSString *)name; - -[TapDB setName:@"Tarara"]; -``` - -```cpp -FString Name = TEXT("Tarara"); // 用户游戏昵称 -TapUEDB::SetName(Name); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -name | 否 | 长度大于 0 并小于等于 256,账号名 - - -### 设置账号等级 - -在用户进行账号登录后,可调用该接口设置该账号的等级,调用将更新账号的账号等级( `level` )属性。 - - - -```cs -public static void SetLevel(int level) - -TapDB.SetLevel(5); -``` - -```java -public static void setLevel(final int level) - -TapDB.setLevel(5); -``` - -```objectivec -+ (void)setLevel:(NSInteger)level; - -[TapDB setLevel:5]; -``` - -```cpp -int32 Level = 5; -TapUEDB::SetLevel(Level); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -level | 否 | 账号等级 - -### 设置账号区服 - -在用户进行账号登录后,可调用该接口设置该账号的区服信息,调用将初始化账号的首次区服( `first_server` )属性、更新账号的当前区服( `current_server` )属性。 - - - -```cs -public static void SetServer(string server) - -TapDB.SetServer("1 区"); -``` - -```java -public static void setServer(final String server) - -TapDB.setServer("1 区"); -``` - -```objectivec -+ (void)setServer:(NSString *)server; - -[TapDB setServer:@"1 区"]; -``` - -```cpp -FString Server = TEXT("1 区"); -TapUEDB::SetServer(Server); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -server | 否 | 账号服务器 - - -## 上报充值记录 - -在用户进行充值后,可调用该接口上报充值信息,调用后将上报 `charge` 事件,并将传入的参数作为事件的属性。 - - - -```cs -public static void OnCharge(string orderId, string product, long amount, string currencyType, string payment) - -public static void OnCharge(string orderId, string product, long amount, string currencyType, string payment, - string properties) - -TapDB.OnCharge("0xueiEns", "轩辕剑", "100", "CNY", "wechat", "{\"on_sell\":true}"); -``` - -```java -public static void onCharge(final String orderId, final String product, final long amount, - final String currencyType, final String payment) - -public static void onCharge(final String orderId, final String product, final long amount, - final String currencyType, final String payment, final JSONObject properties) - -JSONObject info = new JSONObject(); -info.put("on_sell": true); -TapDB.onCharge("0xueiEns", "轩辕剑", "100", "CNY", "wechat", info); -``` - -```objectivec -+ (void)onChargeSuccess:(nullable NSString *)orderId product:(nullable NSString *)product amount:(NSInteger)amount currencyType:(nullable NSString *)currencyType payment:(nullable NSString *)payment; - -+ (void)onChargeSuccess:(nullable NSString *)orderId product:(nullable NSString *)product amount:(NSInteger)amount currencyType:(nullable NSString *)currencyType payment:(nullable NSString *)payment properties:(nullable NSDictionary *)properties; - -[TapDB onChargeSuccess:@"0xueiEns" product:@"轩辕剑" amount:100 currencyType:@"CNY" payment:@"wechat", properties:@{@"on_sell":YES}]; -``` - -```cpp -TapUEDB::OnCharge(OrderId, Product, Amount, CurrencyType, Payment, Properties); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -orderId | 否 | 订单 ID -product | 是 | 产品名称 -amount | 否 | 充值金额(单位分,即无论什么币种,都需要乘以 100) -currencyType | 是 | 货币类型,遵循 ISO 4217 标准。参考:人民币 CNY,美元 USD;欧元 EUR -payment | 是 | 支付方式,如:支付宝 -properties | 是 | 充值( `charge` )的事件属性 - - -**注意:在条件允许的情况下推荐使用服务端充值统计接口,请参考 [服务端接入文档](/sdk/tapdb/sdk/server-side-integration#2.2.上报充值记录)** - -## 自定义事件 - -### 上报事件 - -在 SDK 初始化完成后可使用该接口上报事件 - - - -```cs -public static void TrackEvent(string eventName, string properties) - -TapDB.TrackEvent("eventName", "{\"weapon\":\"axe\"}"); -``` - -```java -public static void trackEvent(final String eventName, final JSONObject properties) - -JSONObject properties = new JSONObject(); -properties.put("#weapon", "axe"); -properties.put("#level", 10); -properties.put("#map", "atrium"); -TapDB.trackEvent("#battle", properties); -``` - -```objectivec -+ (void)trackEvent:(NSString *)eventName properties:(NSDictionary *)properties; - -NSDictionary* dic = @{@"aaa":@"xxx",@"bbb":@"yyy"}; -[TapDB trackEvent:@"testEvent2" properties:dic]; -``` - -```cpp -TapUEDB::TrackEvent(EventName, Properties); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -eventName | 否 | 事件的名称 -properties | 是 | 事件的属性 - - -**注意:** - -* 事件名支持上报预置事件和自定义事件,其中自定义事件应以 `#` 开头 -* 事件属性的 key 值为属性的名称,支持 NSString 类型 -* 事件属性的 value 值为属性的名称,支持 NSString(最大长度 `256` )、NSNumber(取值区间为 `[-9E15, 9E15]` )类型 -* 事件属性支持上报预置属性和自定属性,其中自定义属性应以 `#` 开头 -* 事件属性传入预置属性时,SDK 默认采集的预置属性将被覆盖 - - -### 设置通用事件属性 - -对于一些所有事件都要携带的属性,建议使用通用事件属性实现。 - -#### 添加静态通用事件属性 - - - -```cs -public static void RegisterStaticProperties(string staticProperties) - -//当设置了静态通用事件属性 #current_channel,值固定为 TapDB 后使用事件上报时,等效于在事件属性中添加了 #current_channel -string properties = "{\"#current_channel\":\"TapDB\"}"; -TapDB.RegisterStaticProperties(properties); -``` - -```java -public static void registerStaticProperties(final JSONObject staticProperties) - -//当设置了静态通用事件属性 #current_channel,值固定为 TapDB 后使用事件上报时,等效于在事件属性中添加了 #current_channel -JSONObject commonProperties = new JSONObject(); -commonProperties.put("#current_channel", "TapDB"); -TapDB.registerStaticProperties(commonProperties); -``` - -```objectivec -+ (void)registerStaticProperties:(NSDictionary *)staticProperties; - -//当设置了静态通用事件属性 #current_channel,值固定为 TapDB 后使用事件上报时,等效于在事件属性中添加了 #current_channel -[TapDB registerStaticProperties:@{@"#current_channel":@"TapDB"}]; -``` - -```cpp -TapUEDB::RegisterStaticProperties(Properties); -``` - - - -字段 | 可为空 | 说明 ---- | --- | --- -staticProperties | 否 | 静态通用事件属性字典 - - -#### 删除单个静态通用事件属性 - - - -```cs -public static void UnregisterStaticProperty(string propertyName) - -TapDB.UnregisterStaticProperty("#current_channel"); -``` - -```java -public static void unregisterStaticProperty(string propertyName) - -TapDB.unregisterStaticProperty("#current_channel"); -``` - -```objectivec -+ (void)unregisterStaticProperty:(NSString *)propertyName; - -[TapDB unregisterStaticProperty:@"#current_channel"]; -``` - -```cpp -TapUEDB::UnregisterStaticProperty(Key); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -propertyName | 否 | 静态通用属性名 - - -#### 清空全部静态通用属性 - - - -```cs -public static void ClearStaticProperties() - -TapDB.ClearStaticProperties(); -``` - -```java -public static void clearStaticProperties() - -TapDB.clearStaticProperties(); -``` - -```objectivec -+ (void)clearStaticProperties; - -[TapDB clearStaticProperties]; -``` - -```cpp -TapUEDB::ClearStaticProperties(); -``` - - - -#### 注册动态通用事件属性 - -对于可能随时发生变化的通用事件属性,可以注册动态通用事件属性回调,该回调会在每次调用时被触发,将计算好的属性添加到本次上报事件属性中。 - - - -```cs -public static void RegisterDynamicProperties(IDynamicProperties properties) - -// 后续上报的事件都将携带 #currentLevel 属性,值为变量 level 在事件上报时刻的值 -public class TapDBDynamicPropertiesImpl : IDynamicProperties -{ - public Dictionary GetDynamicProperties() - { - Dictionary dic = new Dictionary(); - dic["#currentLevel"] = level; - return dic; - } -} -TapDB.RegisterDynamicProperties(new TapDBDynamicPropertiesImpl()); -``` - -```java -public static void registerDynamicProperties( - final TapDBDataDynamicProperties dynamicProperties) - -// 后续上报的事件都将携带 #currentLevel 属性,值为变量 level 在事件上报时刻的值 -TapDB.registerDynamicProperties( - () -> { - JSONObject properties = new JSONObject(); - // getCurrentLevel 在这里仅作为案例,表示用户任何的自有逻辑实现 - long level = getCurrentLevel(); - properties.put("#currentLevel", level); - return properties; - } -); -``` - -```objectivec -+ (void)registerDynamicProperties:(NSDictionary* (^)(void))dynamicPropertiesCaculator; - -// 后续上报的事件都将携带 #currentLevel 属性,值为变量 level 在事件上报时刻的值 -[TapDB registerDynamicProperties:^NSDictionary *_Nonnull { - return @{ - @"#currentLevel": level - }; - }]; -``` - -```cpp -TapUEDB::RegisterDynamicProperties(PropertiesBlock); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -dynamicProperties | 否 | 动态通用事件属性计算回调 - -**注意:** - -* 在上报事件或通用属性中使用相同属性名会出现属性覆盖的现象,属性覆盖的优先级从高到低依次为:事件属性、动态通用事件属性、静态通用事件属性、预置属性(例如 `trackEvent` 中设置的事件属性将覆盖动态通用事件属性、静态通用事件属性、预置属性中的同名属性) - - -## 修改用户属性 - -TapDB 支持两种用户主体:设备和账号,你可以通过如下接口对这两种用户的属性进行操作。 - -### 修改设备属性 - -#### 设备属性初始化 - -对于需要保证只有首次设置时有效的属性,可以使用该接口进行赋值操作,仅当前值为空时赋值操作才会生效,如当前值不为空,则赋值操作会被忽略。 - - - -```cs -public static void DeviceInitialize(string properties) - -string properties = "{\"firstActiveServer\":\"server1\"}"; -TapDB.DeviceInitialize(properties); -// 此时设备表的 "#firstActiveServer" 字段值为 "server1" - -string properties = "{\"firstActiveServer\":\"server2\"}"; -TapDB.DeviceInitialize(properties); -// 此时设备表的 "#firstActiveServer" 字段值还是为 "server1" -``` - -```java -public static void deviceInitialize(final JSONObject properties) - -JSONObject properties = new JSONObject(); -properties.put("firstActiveServer", "server1"); -TapDB.deviceInitialize(properties); -// 此时设备表的 "#firstActiveServer" 字段值为 "server1" - -properties.put("firstActiveServer", "server2"); -TapDB.deviceInitialize(properties); -// 此时设备表的 "#firstActiveServer" 字段值还是为 "server1" -``` - -```objectivec -+ (void)deviceInitialize:(NSDictionary *)properties; - -[TapDB deviceInitialize:@{@"firstActiveServer":@"server1"}]; -// 此时设备表的 "#firstActiveServer" 字段值还是为 "server1" - -[TapDB deviceInitialize:@{@"firstActiveServer":@"server2"}]; -// 此时设备表的 "#firstActiveServer" 字段值还是为 "server1" -``` - -```cpp -TapUEDB::DeviceInitialize(Properties); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -properties | 否 | 属性字典 - - -#### 设备属性更新 - -对于常规的设备属性,可使用该接口进行赋值操作,新的属性值将会直接覆盖旧的属性值。 - - - -```cs -public static void DeviceUpdate(string properties) - -string properties = "{\"currentPoints\":10}"; -TapDB.DeviceUpdate(properties); -// 此时设备表的 "currentPoints" 字段值为 10 - -properties = "{\"currentPoints\":42}"; -TapDB.DeviceUpdate(properties); -// 此时设备表的 "currentPoints" 字段值为 42 -``` - -```java -public static void deviceUpdate(final JSONObject properties) - -JSONObject properties = new JSONObject(); -properties.put("currentPoints", 10); -TapDB.deviceUpdate(properties); -// 此时设备表的 "currentPoints" 字段值为 10 - -properties.put("currentPoints", 42); -TapDB.deviceUpdate(properties); -// 此时设备表的 "currentPoints" 字段值为 42 -``` - -```objectivec -+ (void)deviceUpdate:(NSDictionary *)properties; - -[TapDB deviceUpdate:@{@"currentPoints":@10}]; -// 此时设备表的 "currentPoints" 字段值为 10 - -[TapDB deviceUpdate:@{@"currentPoints":@42}]; -// 此时设备表的 "currentPoints" 字段值为 42 -``` - -```cpp -TapUEDB::DeviceUpdate(Properties); -``` - - - -参数 | 可为空 | 说明 ---- | --- | --- -properties | 否 | 属性字典 - - -#### 设备属性累加 - -对于数值类型的属性,可以使用该接口进行累加操作,调用后 TapDB 将对原属性值进行累加后保存结果值 - - -<> - -```cs -public static void DeviceAdd(string properties) - -string properties = "{\"totalPoints\":10}"; -TapDB.DeviceAdd(properties); -// 此时设备表的 "totalPoints" 字段值为 10 - -properties = "{\"totalPoints\":-2}"; -TapDB.DeviceAdd(properties); -// 此时设备表的 "totalPoints" 字段值为 8 -``` - -账号 | 可为空 | 说明 ---- | --- | --- -properties | 否 | 属性字典,value 仅支持数值类型 - - - - -<> - -```java -public static void deviceAdd(final JSONObject properties) - -JSONObject properties = new JSONObject(); -properties.put("totalPoints", 10); -TapDB.deviceAdd(properties); -// 此时设备表的 "totalPoints" 字段值为 10 - -properties.put("totalPoints", -2); -TapDB.deviceAdd(properties); -// 此时设备表的 "totalPoints" 字段值为 8 -``` - -字段 | 可为空 | 说明 ---- | --- | --- -properties | 否 | 属性字典,value 仅支持数值类型 - - - -<> - -```objectivec -+ (void)deviceAdd:(NSDictionary *)properties; - -[TapDB deviceAdd:@{@"totalPoints":@10}]; -// 此时设备表的 "totalPoints" 字段值为 10 - -[TapDB deviceAdd:@{@"totalPoints":@(-2)}]; -// 此时设备表的 "totalPoints" 字段值为 8 -``` - - -参数 | 可为空 | 说明 ---- | --- | --- -properties | 否 | 属性字典,value 仅支持 NSNumber 类型 - - - -<> - -```cpp -TapUEDB::DeviceAdd(Properties); -``` - - - - - -上述代码示例中,属性值为整数。 -累加操作也支持浮点数,不过浮点数相加有精度问题,开发者还需留意。 - -### 修改账号属性 - -#### 账号属性初始化 - -使用方法同设备属性初始化操作 - - - -```cs -public static void UserInitialize(string properties) - -TapDB.UserInitialize(properties); -``` - -```java -public static void userInitialize(final JSONObject properties) - -TapDB.userInitialize(properties); -``` - -```objectivec -+ (void)userInitialize:(NSDictionary *)properties; - -[TapDB userInitialize:@{@"firstActiveServer":@"server1"}]; -``` - -```cpp -TapUEDB::UserInitialize(Properties); -``` - - - - -#### 账号属性更新 - -使用方法同设备属性更新操作 - - - -```cs -public static void UserUpdate(string properties) - -TapDB.UserUpdate(properties); -``` - -```java -public static void userUpdate(final JSONObject properties) - -TapDB.userUpdate(properties); -``` - -```objectivec -+ (void)userUpdate:(NSDictionary *)properties; - -[TapDB userUpdate:@{@"currentPoints":@10}]; -``` - -```cpp -TapUEDB::UserUpdate(Properties); -``` - - - -#### 账号属性累加 - -使用方法同设备属性累加操作 - - - -```cs -public static void UserAdd(string properties) - -TapDB.UserAdd(properties); -``` - -```java -public static void userAdd(final JSONObject properties) - -TapDB.userAdd(properties); -``` - -```objectivec -+ (void)userAdd:(NSDictionary *)properties; - -[TapDB userAdd:@{@"totalPoints":@10}]; -``` - -```cpp -TapUEDB::UserAdd(Properties); -``` - - - - -## 收集设备指纹 - -允许 SDK 采集设备指纹用于辅助数据分析、广告归因,将使统计结果更加精确, - -:::info -请在权限申请、设置 IDFA 开关等操作结束后初始化 SDK,以保证设备指纹能够正常上报。 -::: - -### OAID(Android) - -> 注意:SDK 版本 3.28.2 及以上支持 OAID 版本为 1.0.5 ~ 2.4.0; 3.15.0 ~ 3.28.0 支持 OAID 版本为 1.0.5 ~ 2.1.0; 3.14.0 及以下支持 OAID 版本为 1.0.5 ~ 1.0.25 - - -TapDB SDK 在应用接入 OAID 第三方库时,会在发送相关事件中携带该参数(key 为 `device_id4`)。现支持该第三方库版本为 1.0.5 ~ 2.4.0,因不同版本变更较大,所以针对不同版本接入的说明如下: - -对于 1.0.5 ~ 1.0.25 不需要额外配置,只需应用添加对应第三方库的依赖即可。 - - -对于 1.0.26 ~ 2.4.0 除添加对应第三方库外,需要添加如下处理: - -#### 1. 设置证书信息及配置文件 - -证书信息为应用通过 移动安全联盟邮箱 申请的 `.cert.pem` 文件内容, 该文件与包名对应。现支持两种设置方式: - -1. 将 `cert.pem` 文件拷贝到应用 `assets` 目录,并注意该文件名应设置为 `packageName.cert.pem` , `packageName` 为当前应用包名。 -2. 通过 SDK 的接口 `setOAIDCert` 将证书文件的内容进行设置 - -以上两种方式选择一种即可,当两种同时使用时,优先使用通过接口设置的证书信息。 - -配置文件为 `supplierconfig.json`, 应用需要将内部 appid 对应的内容修改为应用在对应应用市场的应用 ID,其他部分不需要修改,并将修改后的文件拷贝到 `assets` 目录下。 - -#### 2. 在应用工程中加载对应库文件 - -不同版本 OAID 第三方库对应的库文件名称如下: - -| 版本号 | 库名称 | -| ---- | ---- | -| 1.0.30 ~ 2.4.0 | msaoaidsec | -| 1.0.29 | nllvm1632808251147706677 | -| 1.0.27 | nllvm1630571663641560568 | -| 1.0.26 | nllvm1623827671 | - -在 Android 项目工程自定义 `Application` 类的 `onCreate` 方法中添加加载第三方库代码,例如当应用接入的 OAID 版本为 1.2.1 时如下: - -```java -System.loadLibrary("msaoaidsec"); -``` - -#### 常见问题处理 - -当项目中已引入 OAID 库但上报时仍未发现设备 OAID 信息时,请检查以下几项 - -1. 设备时间是否正常 -2. 对于 1.0.26 及以上版本证书所对应的包名是否和当前包名对应 -3. 对于 1.0.26 及以上版本是否加载了其库文件以及库文件名称是否和版本对应 -4. 应用在 Android 12 报错 `java.lang.UnsatisfiedLinkError`且 应用 minSdkVersion 大于等于 23 ,建议在 AndroidManifest.xml 文件 application 标签中添加 `android:extractNativeLibs="true"` - - - - - - - - ### IMEI(Android) - -在 `AndroidManifest.xml` 增加如下条目,且用户同意权限的申请后,SDK 将自动采集 Android IMEI。 - - - -<> - -```xml - -``` - - - -<> - -```xml - -``` - - -<> - -```xml -iOS 平台不适用 -``` - -```cpp -// UE4 SDK 未提供此方法 -``` - - - - - -### IDFA(iOS) - - -由于 `iOS14.5` 以上系统,获取 IDFA 需要弹出窗口有用户确认,故 SDK 默认不获取 IDFA,可以调用接口开启 IDFA 获取。 - - - -<> - -请确保 `info.plist` 中添加了权限请求描述文字,SDK 在初始化时将自动弹出权限请求窗口。 - -``` -NSUserTrackingUsageDescription -此标识符将用于向您推荐个性化广告(或其他描述) -``` - -若使用 TapSDK 初始化,请在 `TapDBConfig` 中的 `advertiserIDCollectionEnabled` 传入 `true` 开启 IDFA 采集开关。 - -若单独使用 TapDB SDK 请调用如下接口开启 IDFA 采集开关,***为保证数据准确性,该接口应在初始化接口之前调用*** - -```cs -TapDB.AdvertiserIDCollectionEnabled(true); -``` - - - -<> - -```java -// Android 平台不适用 -``` - - - -<> - -请确保 `info.plist` 中添加了权限请求描述文字,SDK 在初始化时将自动弹出权限请求窗口。 - -``` -NSUserTrackingUsageDescription -此标识符将用于向您推荐个性化广告(或其他描述) -``` - -若使用 TapSDK 初始化,请在 `TapDBConfig` 中的 `advertiserIDCollectionEnabled` 传入 `YES` 开启 IDFA 采集开关。 - -若单独使用 TapDB SDK 请调用如下接口开启 IDFA 采集开关 - -```objective-c -[TapDB setAdvertiserIDCollectionEnabled:YES]; -``` - - - -<> - -iOS 独占方法 - -```cpp -TapUEDB::AdvertiserIDCollectionEnabled(true); -``` - - - - - - -## 诊断接入 {#themis} - -:::info -TapSDK 3.14.0 及以上可接入此功能。 - -UE4 SDK 暂不支持诊断接入。 -::: - -### 添加依赖 - - - -<> - -SDK 可以**通过 Unity Package Manager 导入或手动导入**,二者任选其一。 - -如果选择 UPM 导入,可以在项目的 `Packages/manifest.json` 文件中添加: - - -{`"dependencies":{ - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.tapdb": "https://github.com/TapTap/TapDB-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.themis": "https://github.com/taptap/TapThemis-Unity.git#3.2.3-3", -}`} - - -如果选择手动导入: - -* 在 [下载页](/tap-download) 找到 TapSDK Unity 下载地址,下载 TapSDK-UnityPackage.zip 然后解压,导入其中的 `TapTap_Common`,`TapTap_TapDB`模块。 - - - -<> - -```cs -repositories { - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'THEMIS-release*.*.*', ext:'aar') -} -``` - - - -<> - -```cs -// TapDB SDK 自动调用,不需要额外接入 -``` - - - - - -### 命名空间 - - - -```cs -using TapTap.Themis; -``` - -```cs -// TapDB SDK 自动调用,不需要额外接入 -``` - -```cs -// TapDB SDK 自动调用,不需要额外接入 -``` - - - -### 启用设置 - -在集成 Themis 对应依赖后,TapDB 模块默认会调用 Themis 相关接口,如果游戏需要禁止该调用,可在初始化 TapDB 接口前调用本接口进行关闭。 - - - -```cs -TapDB.EnableThemis(false); -``` - -```java -TapDB.enableThemis(false); -``` - -```objective-c -[TapDB enableThemis: NO]; -``` - - - - - -### 接口描述 - -#### 初始化 - - - -```cs -TapThemis.InitTHEMIS(); -``` - -```cs -// TapDB SDK 自动调用,不需要额外接入 -``` - -```cs -// TapDB SDK 自动调用,不需要额外接入 -``` - - - -***注意:该接口需要在 TapDB 初始化接口之后调用*** - -#### 设置日志上报等级 - - - -```cs -TapThemis.U3d_ConfigAutoReportLogLevel(TapTap.Themis.LogSeverity.LogError); -``` - -```cs -// 暂不支持 -``` - -```cs -// 暂不支持 -``` - - - -默认等级为 `LogError`,当应用日志等级高于设置的等级时会自动上报。 - -#### 设置异常时是否退出 - - - -```cs -TapThemis.U3d_ConfigAutoQuitApplication(true); -``` - -```cs -// 暂不支持 -``` - -```cs -// 暂不支持 -``` - - - -设置当发生未捕获的异常时是否自动退出。 - -#### 注册日志监听 - - - -```cs -TapThemis.U3d_UnregisterLogCallback(logcallback); -public void logcallback(string condition, string statckTrace, LogType type ){ - -} -``` - -```cs -// 暂不支持 -``` - -```cs -// 暂不支持 -``` - - - -注册应用日志监听,当应用输出日志时,调用对应回调处理。 - -#### 移除日志监听 - - - -```cs -TapThemis.U3d_UnregisterLogCallback(logcallback); -``` - -```cs -// 暂不支持 -``` - -```cs -// 暂不支持 -``` - - - -#### 上报异常 - - - -```cs -TapThemis.ReportException(new Exception("Themis crash test from unity"),"crashMessage from untiy"); -``` - -```cs -// 暂不支持 -``` - -```cs -// 暂不支持 -``` - - - -主动上报异常信息。 diff --git a/docs/sdk/tapdb/sdk/data-spec.mdx b/docs/sdk/tapdb/sdk/data-spec.mdx deleted file mode 100644 index 0a6840949..000000000 --- a/docs/sdk/tapdb/sdk/data-spec.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: 数据规范 -sidebar_position: 2 ---- - -## 设备 - -### 设备 ID (device_id) - -SDK 在初始化时,将为该终端生成唯一的 ID,我们称之为设备 ID。 - -#### 生成规则 - -| SDK 类型 | 生成规则 | -| --- | --- | -| Android | 依次尝试获取本地存储保存过的设备 ID、Android ID,如果都无法获取到正确结果则随机生成 UUID,最后获取或生成的设备 ID 会保存于本地存储 | -| iOS | 尝试获取本地钥匙串保存过的设备 ID,获取失败则随机生成 UUID,最后获取或生成的设备 ID 会保存于本地 | - -#### 设备属性 - -设备属性表示设备的不变的属性以及最新状态。可以按照设备的某个属性或特征与事件进行关联,来分析这些用户的行为。 - -#### 常见问题 - -**设备 ID 会发生变化么?** - -Android:在能获取到 Android ID 的情况下,设备 ID 是比较稳定的(参考 [Google 唯一标识符最佳做法](https://developer.android.com/training/articles/user-data-ids))。如果无法获取到 Android ID,设备 ID 将有随着重装应用或重置 Google 广告 ID 而改变的可能。 - -iOS:获取或生成好的设备 ID 保存于设备的钥匙串中,可以保证在不重置系统的情况下,保持设备 ID 的稳定。 - -## 账号 - -### 账号 ID (user_id) - -通常情况下是你的注册用户 ID,当用户在你的系统中发生注册或登录行为后,你可以将该用户在你的系统中的唯一 ID 直接或加密后设置到 SDK 中,这个 ID 会被作为今后用户在各个平台使用你的产品的身份识别 ID。 - -#### 账号属性 - -账号属性表示账号的不变的属性以及最新状态。可以按照账号的某个属性或特征与事件进行关联,来分析这些用户的行为。 - -## 事件 - -[什么是事件](/sdk/tapdb/sdk/user-event-model) - -### 预置事件 - -| 事件名 | 名称 | 说明 | -| --- | --- | --- | -| device_login | App 启动 | 调用 SDK 初始化接口时会上报此事件,首次上报一个设备 ID 时将在设备表产生一条记录 | -| user_login | 账号登录 | 调用 SDK SetUser 接口时会上报此事件,首次上报一个账号 ID 时将在账号表产生一条记录 | -| play_game | 游玩时长 | SDK 会以应用进入前台作为计时起点,置于后台时,上报此时间段的时长 | -| charge | 用户付费 | 调用 SDK Charge 接口时会上报此事件,通常情况下建议使用服务端 REST API 进行上报 | - -### 衍生事件 - -在上报预置事件时,TapDB 也会同时记录一些特殊事件,这类特殊的事件我们称之为衍生事件。衍生事件无法通过 API 直接上报,只会由预置事件上报后触发。 - -| 事件名 | 名称 | 说明 | -| --- | --- | --- | -| dau_device | App 当日首次次启动 | App 在每日首次上报 `device_login` 时触发,可用于快速查询设备 DAU | -| dvau_device | App 当日首次启动(按版本)| App 的不同版本在每日首次上报 `device_login` 时触发,可用于快速查询分版本的设备 DAU | -| wau_device | App 当周首次启动 | App 在每周首次上报 `device_login` 时触发,可用于快速查询设备 WAU | -| mau_device | App 当月首次启动 | App 在每月首次上报 `device_login` 时触发,可用于快速查询设备 MAU | -| dau_user | 账号当日首次登录 | 账号每日首次上报 `user_login` 时触发,可用于快速查询账号 DAU | -| wau_user | 账号当周首次登录 | 账号每周首次上报 `user_login` 时触发,可用于快速查询账号 WAU | -| mau_user | 账号当月首次登录 | 账号每月首次上报 `user_login` 时触发,可用于快速查询账号 MAU | - -### 自定义事件 - -除了预置事件和衍生事件外,也可以在事件管理中建立更多自定义事件。 - -## 数据规则 - -TapDB 的 REST API 支持传入数据格式为 URLEncode 后的 JSON 对象, -如果你直接使用 TapDB 的 REST API 则需要按照此格式进行上报。 -如果你使用 SDK 接入,数据也会转化成该格式进行上报。 - -### 事件数据 - -记录一个事件及其属性 - -```js -{ - ["index" | "client_id"]: ["APPID" | "ClientID"], - "device_id": "DeviceID", - "user_id": "UserID", - "type": "track", - "name": "EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue", - "event_uuid": "550e8400-e29b-41d4-a716-446655440000" - } -} -``` - -#### 系统字段 - -properties 同层级的字段为系统字段 - -| 名称 | 数据类型 | 说明 | -| --- | --- | --- | -| index | string | 项目的 APPID,在 TapDB 后台可以查看该 ID | -| client_id | string | 项目的 TapTap Client ID,在 TapTap 开发者中心可以查看该 ID | -| type | string | 数据类型,上报事件时传入 track | -| device_id | string | 事件发生时的设备 ID | -| user_id | string | 事件发生时的账号 ID | -| name | string | 事件名,可传入预置事件或自定义事件 | - -注意: - -- 预置事件 `device_login` 必须传入 `device_id` -- 预置事件 `user_login`、`play_game` 必须传入 `device_id`和 `user_id` -- 预置事件 `charge` 必须传入有效的 `user_id`(即上报过 `user_login` 的 `user_id`) - -#### 属性字段 - -properties 内的字段为属性字段,这些传入的字段会被当做事件的属性,你可以通过自定义属性进行扩展。 - -其中 SDK 预置事件属性: - -| 名称 | 类型 | 说明 | -| --- | --- | --- | -| os | string | 操作系统(支持传入 Android、iOS、Windows、Mac)| -| device_id1 | string | 预留设备 ID 槽位(iOS SDK 传入 IDFA,Android SDK 传入 IMEI)| -| device_id2 | string | 预留设备 ID 槽位(Android SDK 传入 Google 广告 ID)| -| device_id3 | string | 预留设备 ID 槽位(Android SDK 传入 Android ID)| -| device_id4 | string | 预留设备 ID 槽位(Android SDK 传入 OAID)| -| width | number | 屏幕宽度 | -| height | number | 屏幕高度 | -| device_model | string | 设备型号(设备厂商+设备型号)| -| os_version | string | 操作系统版本 | -| network | string | 网络类型(WiFi 传入 2、未知传入 3、2G 传入 4、3G 传入 5、4G 传入 6)| -| channel | string | 分包渠道 | -| app_version | string | App 版本 | -| sdk_version | string | SDK 版本 | -| event_uuid | string | 每条日志的唯一 ID,通常传入 UUID | - -### 属性操作 - -账号属性操作 - -```js -{ - ["index" | "client_id"]: ["APPID" | "ClientID"], - "user_id": "UserID", - "type": ["initialise" | "update" | "add"], - "properties": { - "level": 15, - "#custom": "custom" - } -} -``` - -设备属性操作 - -```js -{ - ["index" | "client_id"]: ["APPID" | "ClientID"], - "device_id": "DeviceID", - "type": ["initialise" | "update" | "add"], - "properties": { - "level": 15, - "#custom": "custom" - } -} -``` - -#### 系统字段 - -properties 同层级的字段为系统字段 - -| 名称 | 数据类型 | 说明 | -| --- | --- | --- | -| client_id | string | 项目的 TapTap Client ID,在 TapTap 开发者中心可以查看该 ID | -| type | string | 数据类型,属性操作支持 `initialise`、`update`、`add` | -| device_id | string | 进行属性操作的设备 ID | -| user_id | string | 进行属性操作的账号 ID | - -type 字段传入值的含义 - -| 名称 | 说明 | -| --- | --- | -| initialise | 对于需要保证只有首次设置时有效的属性,可以使用 `initialise` 进行赋值操作,仅当前值为空时赋值操作才会生效,如当前值不为空,则赋值操作会被忽略 | -| update | 对于常规的设备属性,可使用该接口进行赋值操作,新的属性值将会直接覆盖旧的属性值 | -| add | 对于数值类型的属性,可以使用该接口进行累加操作,调用后 TapDB 将对原属性值进行累加后保存结果值 | - -注意: - -- 进行属性操作时,`device_id` 和 `user_id` 只能二选一传入。传入 `device_id` 则对该设备 ID 的属性进行操作,传入 `user_id` 则对该账号 ID 的属性进行操作 - -#### 属性字段 - -`properties` 内的字段为属性字段,这些字段将被视为要操作的字段,按照 `type` 的值进行处理。 diff --git a/docs/sdk/tapdb/sdk/server-side-integration.mdx b/docs/sdk/tapdb/sdk/server-side-integration.mdx deleted file mode 100644 index 24b84619d..000000000 --- a/docs/sdk/tapdb/sdk/server-side-integration.mdx +++ /dev/null @@ -1,289 +0,0 @@ ---- -title: 服务端接入 -sidebar_label: 服务端接入 -sidebar_position: 4 ---- - -import {Conditional} from '/src/docComponents/conditional'; - -你可以直接使用 REST API 进行接入,可以在不依赖 SDK 的情况下直接将数据上报到 TapDB。 - -## 上报事件和属性 - -数据传输的格式和含义请参考 [数据规则](/sdk/tapdb/sdk/data-spec#数据规则)。 - -如果返回 Response Code 为 200,则代表数据上报成功,请在埋点管理中进一步查看事件的写入情况。 - -### 单条上报 - - - -`POST` `https://e.tapdb.net/v2/event` - - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/event` - - - -`Content-Type: application/json` - -请求内容示例: -```json -{ - "client_id": "test_appid", - "device_id": "test_device_id", - "user_id": "test_user_id", - "type": "track", - "name": "#EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue", - "event_uuid": "7656c71f-6d73-488e-b740-3ab370c6f3db" - } -} -``` - -### 批量上报 - - - -`POST` `https://e.tapdb.net/v2/batch` - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/batch` - - - -`Content-Type: application/json` - -请求内容示例: -```json -{ - "data": [ - { - "client_id": "test_appid", - "device_id": "test_device_id", - "user_id": "test_user_id", - "type": "track", - "name": "#EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue", - "event_uuid": "7656c71f-6d73-488e-b740-3ab370c6f3db" - } - }, - { - "client_id": "test_appid", - "device_id": "test_device_id", - "user_id": "test_user_id", - "type": "track", - "name": "#EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue", - "event_uuid": "1c11f92e-6f18-417d-8aed-ffbe668a9feb" - } - } - ] -} - -``` - - - -### 常见问题 - -- 若当前事件的主体并非设备或账号,device_id 和 user_id 可以传入任意一个固定值 -- 为了保证服务端上报的事件也能使用设备维度进行分析,建议在客户端调用 SDK 的 `GetDeviceID` 接口取得 SDK 为该设备生产的唯一 ID 并上报到 App 的服务端 - -## 特殊类型事件上报 - -### 在线人数 - -由于 SDK 无法推送准确的在线数据,这里提供服务端在线数据推送接口。游戏服务端可以以小于 1 分钟的间隔自行统计在线人数(间隔 0.5 分钟为最佳),通过接口推送到 TapDB。TapDB 按照 2 分钟的颗粒度汇总展现。 - -*注意:在线人数使用 json 格式上报,这与其他通用事件上报的格式有差别,请注意区分。* - - - -`POST` `https://se.tapdb.net/tapdb/online` - - - - - -`POST` `https://se.tapdb.ap-sg.tapapis.com/tapdb/online` - - - -`Content-Type: application/json` - -请求内容: - -| 参数 | 参数类型 | 参数说明 | -| --- | --- | --- | -| client_id | string | 游戏的 ClientID | -| onlines | array | 多条在线数据(最多 100 条) | - -其中 onlines 数组的结构为 - -| 参数 | 参数类型 | 参数说明 | -| --- | --- | --- | -| server | string | 服务器。TapDB 对同一服务器每一个自然 1 分钟仅接受一次数据 | -| online | number | 在线人数 | -| timestamp | number | 当前统计数据的时间戳(秒)。TapDB 会按照自然 2 分钟进行数据对齐 | - -示例: - -```json -{ - "client_id":"ClientID", - "onlines":[{ - "server":"s1", - "online":123, - "timestamp":1489739590 - },{ - "server":"s2", - "online":188, - "timestamp":1489739560 - }] -} -``` - - -#### 常见问题 - -*Q:统计数据如何按 2 分钟对齐* - -A:在线人数图表展示的横坐标每个点间隔 2 分钟,分别是每个小时的 0、2、4 至 58 分,实际数据统计周期为该时间点前 0.5 分钟到后 0.5 分钟、后 0.5 分钟到后 1.5 分钟。 -以图表 10 点 04 分的点为例,分别统计的是 10 点 03 分 30 秒至 10 点 04 分 30 秒、 10 点 04 分 30 秒至 10 点 05 分 30 秒这两个时间范围(指上报数据内容的 timestamp 字段,而非实际请求接口的时间)内,各 server 在线人数的总和。这个点大约在 10 点 04 分 30 秒至 10 点 05 分 30 秒时显示前一个时间段(10 点 03 分 30 秒至 10 点 04 分 30 秒)的统计数据,10 点 05 分 30 秒之后显示后一个时间段( 10 点 04 分 30 秒至 10 点 05 分 30 秒)的统计数据。 - -*Q:在线人数曲线有缺口是什么原因* - -A:请检查上报数据的频率是否低于 1 分钟一次。为了保证图表的展示的曲线平滑,可直接将上报频率调整为每分钟 2 次,并在上报失败时进行重试,直至上报成功。 - - -### 充值记录 - -由于 SDK 推送可能会不准确,建议直接使用服务端充值推送接口。注意需要停止客户端 SDK 中充值信息的上报,防止重复统计。 - - - -`POST` `https://e.tapdb.net/v2/event` - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/event` - - - -`Content-Type: application/json` - -```json -{ - "name": "charge", // 事件名,固定为 charge - "client_id": "ClientID", // 必需。注意 ClientID 需要被替换成游戏的 ClientID - "user_id": "userId", // 必需。用户 ID。必须和 SDK 的 setUser 接口传递的 userId 一样,并且该用户已经通过 SDK 接口进行过推送 - "type": "track", // 必需。数据类型,请确保传入的值为 track - "properties": { - "ip": "8.8.8.8", // 可选。充值用户的 IP - "order_id": "100000", // 必需。长度大于 0 并小于等于 256。订单 ID。 - "amount": 100, // 必需。大于 0 并小于等于 100000000000。充值金额。单位分,即无论什么币种,都需要乘以 100 - "virtual_currency_amount": 100, //获赠虚拟币数量,必传,可为 0 - "currency_type": "CNY", // 可选。货币类型。国际通行三字母表示法,为空时默认 CNY。参考:人民币 CNY,美元 USD;欧元 EUR - "product": "item1", // 可选。长度大于 0 并小于等于 256。商品名称 - "payment": "alipay" // 可选。长度大于 0 并小于等于 256。充值渠道 - } -} -``` - -*TapDB 并不会对上报的充值数据进行去重,当接口返回 200 时则代表 TapDB 已经接受到该条日志数据,请到埋点概览中查看该条数据后续的落盘情况。反复上报相同订单记录将会导致数据被重复统计* - - -### 退款记录 - -对于需要进行退款分析的游戏,这里提供服务端退款数据推送接口。该接口的属性与付费事件的属性基本一致,但额外增加了一个 origin_order_id 字段,用于追踪原始订单的 ID。 - - - -`POST` `https://e.tapdb.net/v2/event` - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/event` - - - -`Content-Type: application/json` - -```json -{ - "name": "refund", // 事件名,固定为 refund - "client_id": "ClientID", // 必需。注意 ClientID 需要被替换成游戏的 ClientID - "user_id": "userId", // 必需。用户 ID。必须和 SDK 的 setUser 接口传递的 userId 一样,并且该用户已经通过 SDK 接口进行过推送 - "type": "track", // 必需。数据类型,请确保传入的值为 track - "properties": { - "ip": "8.8.8.8", // 可选。充值用户的 IP - "timestamp": 1718101321234, // 可选。毫秒级时间戳。退款事件发生时间。 - "order_id": "100000", // 必需。长度大于 0 并小于等于 256。订单 ID。 - "origin_order_id": "200000", // 可选。长度大于 0 并小于 256。原始充值订单 ID。 - "amount": 100, // 必需。大于 0 并小于等于 100000000000。充值金额。单位分,即无论什么币种,都需要乘以 100 - "virtual_currency_amount": 100, //获赠虚拟币数量,必传,可为 0 - "currency_type": "CNY", // 可选。货币类型。国际通行三字母表示法,为空时默认 CNY。参考:人民币 CNY,美元 USD;欧元 EUR - "product": "item1", // 可选。长度大于 0 并小于等于 256。商品名称 - "payment": "alipay" // 可选。长度大于 0 并小于等于 256。充值渠道 - } -} -``` - - diff --git a/docs/sdk/tapdb/sdk/user-event-model.mdx b/docs/sdk/tapdb/sdk/user-event-model.mdx deleted file mode 100644 index 69823375d..000000000 --- a/docs/sdk/tapdb/sdk/user-event-model.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: User - Event 模型 -sidebar_position: 1 ---- - -我们很清楚,用户在玩游戏的过程中,会在游戏内产生各种不同类型的行为。比如: - -- 对 RPG 游戏而言,用户在游戏内会有打怪、升级、买装备等行为事件; -- 对 PVP 游戏而言,用户在游戏内会有添加好友等行为事件; -- 对卡牌游戏而言,用户在游戏内会有购买卡牌、使用卡牌等行为事件; - -在这里,**我们把「用户」定义为 「User」,把「行为事件」定义为 「Event」**。这样我们会发现数据分析就是对 **User** 和 **Event** 两个主体的分析。 - -用户在触发各类行为事件时,事件会有 Who、When、What、Where、How 等相关信息: - -- **Who**:触发 Event 的用户。 -- **What**:Event 本身的具体内容。 -- **When**:触发 Event 的时间。 -- **Where**:触发 Event 时的 IP、国家、省、市、区等位置信息。 -- **How**:用户是具体如何触发的 Event,比如用户使用的设备类型、操作系统类型、操作系统版本号、设备品牌、设备型号、设备分辨率、游戏 App 的版本等信息。 - -**其中 What、When、Where、How 都是 Event 的属性状态**。 - -以某对战竞技类游戏为例,「用户组队参加一场对战」可以看做是一个事件名为「对战」的 Event: - -- **Who**:即参加对战的用户有哪些? -- **What**:是什么类型的对战? -- **When**:什么时候进行的对战? -- **Where**:对战发生时的 IP、国家、省、市、区等位置信息是什么? -- **How**:对战发生时的用户使用的是苹果手机还是安卓手机?系统版本号是多少?用户手机分辨率是多少? - -以上就是 **Event** 模型(也叫事件模型)及 **Event** 属性信息。**游戏里的每个待分析的行为都可以定义为 Event,也应该定义为 Event,这是做好数据分析的前提。** - -Event 信息可在 TapDB 的「配置」-「事件管理」里录入。 - -每个 **User** 代表一个用户。**User 也会有很多信息,比如注册时间、注册地址、注册渠道、用户级别、设备类型、性别、年龄等信息,这些信息都是 User 的属性状态**。通过 User 的属性,您可以在用户行为分析时快速筛选出您要分析的用户,比如您想分析付费用户的活跃情况,则只需要分析「累计付费金额」这一用户属性的值大于 0 的用户即可。 - -同样以某对战竞技类游戏为例,「用户组队参加一场对战」可以看做是「事件 event」里的 who: - -- 参加对战的用户有哪些? -- 累计玩 游戏 多长时间? -- 用户得分多少? -- 用户玩过哪些英雄? -- 用户的性别是什么? -- 用户的年龄是什么? -- 用户的注册时间是什么时候? -- 用户的注册渠道是什么? -- 用户的累计付费金额是多少? - -以上就是 User 模型。 - -**Event 模型和 User 模型合称 User - Event 模型**。 - -基于 User - Event 模型设计埋点文档并采集信息,你可以: - -- PVP 游戏:分析不同段位下,使用不同角色的参战次数; -- 卡牌游戏:分析不同 VIP 等级的玩家参与中秋节活动的参与情况; -- SLG:分析用户在进入游戏前 7 天,被其他玩家掠夺资源是如何影响留存率; -- RPG:查看和分析关键等级的通过率,以关注关键流失点; -- SLG:查看最近 7 日,不同等级区间玩家的资源积累和消耗情况,以便确定该如何投放礼包。最好能可视化呈现净流入 / 流出; diff --git a/docs/sdk/taplink/_category_.json b/docs/sdk/taplink/_category_.json deleted file mode 100644 index e9927a2e2..000000000 --- a/docs/sdk/taplink/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapLink", - "collapsed": true, - "position": 10 -} diff --git a/docs/sdk/taplink/features.mdx b/docs/sdk/taplink/features.mdx deleted file mode 100644 index 293d00ad3..000000000 --- a/docs/sdk/taplink/features.mdx +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: TapLink 功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; - -## 能力介绍 - -- TapLink 是一种基于 deep linking 跳转机制的礼包分发能力。 -- 如果玩家在 TapTap 客户端点击「领取礼包」时已安装了您的游戏,TapLink可立即打开并跳转游戏,并直接进行道具兑换或将用户引导至已有的礼包领取页面。 -- 如果玩家在 TapTap 客户端点击「领取礼包」时未安装您的游戏,TapLink会引导玩家停留在原页面进行礼包码复制。 - -## 工作流程 - -![](https://capacity-files.lcfile.com/8HTgi9a15nTfqPvIiFARb4v1rMFSeI9f/0048bea7-49ae-45a4-aecc-9f1a334105ee.png) - -## 产品优势 - -### 礼包领取兑换体验提升,激发玩家兴趣,提高黏性 - -相较于传统礼包码复制,兑换的领取路径,TapLink 在用户体验上做到了自助跳转,从 **TapTap→游戏**一站式领取礼包,大幅缩短用户体验路径,操作简易,减少用户流失,提升领取率。 - -### 平台流量导入游戏,助力游戏促活拉新 - -TapTap 平台礼包入口一键跳转游戏礼包入口,助力游戏日活提升的同时将 TapTap 玩家成功转化为游戏新用户。 - -### 接入流程简易,降低开发成本 - -开发者仅需通过 API 解析相关URL,即可开发完成,无需增加额外的开发工作。 - -### 落地形式灵活,游戏可根据自身需求场景自由设计兑换方式 - -游戏可根据自身需求将设计落地形式,可直接将 TapLink 所携带的礼包信息解析后,直接下发道具给玩家;也可接入预设好的礼包领取落地页。 - -## 接入方式 - -开发者需自行集成 TapLink 链接,方可使用该能力。 - -![](https://capacity-files.lcfile.com/ky6P3MASLHWh9b40SsEmlpBnElbWiqJN/taplink.png) - -TapLink 格式为:`tds{clientID}://gifts?code={code}`(注意这里 `{clientID}` 和 `{code}` 表示变量)。具体的值和示例开发者可以登录控制台查看。一个完整的 URL Scheme 协议格式由 scheme、host、port、path 和 query 组成,其结构为:`://:/?`。游戏接入 TapLink 功能只需要在移动端配置 scheme 和 host 即可。需要注意的是 scheme 是大小写敏感的,配置时请务必严格和控制台给出的`链接配置`里的 scheme 保持一致。 - -
    -Unity-Android scheme 配置示例 - -```xml -假设控制台给出的「链接配置」为:tdsFwFdCIr6u71WQDQwQN://gifts?code={code} 则 scheme 为:tdsFwFdCIr6u71WQDQwQN - - - - - - - - - - - - - - - - - -``` - -
    - -
    -原生 Android scheme 配置示例 - -```xml -假设控制台给出的「链接配置」为:tdsFwFdCIr6u71WQDQwQN://gifts?code={code} 则 scheme 为:tdsFwFdCIr6u71WQDQwQN - - - - - - - - - - - - - - - -``` - -
    - -针对 Android 的包体可以在本地通过 ADB 命令来检测包体中 scheme 的配置是否正确,如果 ADB 命令可以在本地的设备上唤起应用则说明配置没有问题。 -```shell -# scheme 为控制台展示的 scheme, package 为应用的包名。注意:最终的命令里不包含大括号。 -adb shell am start \ - -W -a android.intent.action.VIEW \ - -d "{scheme}://gifts" {packageName} -``` - -游戏客户端可按照如下文档解析 TapLink,获得礼包码: -- [Unity: Deep linking on iOS](https://docs.unity3d.com/Manual/deep-linking-ios.html) -- [Unity: Deep linking on Android](https://docs.unity3d.com/Manual/deep-linking-android.html) -- [Android: 创建指向应用内容的深层链接](https://developer.android.com/training/app-links/deep-linking) -- [Deep linking and URL scheme in iOS](https://benoitpasquier.com/deep-linking-url-scheme-ios/) - -由于 deep linking 是一种标准的技术方案,开发者也可以自行搜索获取其解析方法。游戏客户端获取礼包码之后,去自己的服务端进行核销即可,这样就完成了一次礼包码的发放流程。 - -## 应用场景 - -当前支持:TapTap 签到活动 diff --git a/docs/sdk/taptap-appsafety/_category_.json b/docs/sdk/taptap-appsafety/_category_.json deleted file mode 100644 index 7e8d5a321..000000000 --- a/docs/sdk/taptap-appsafety/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "应用安全", - "collapsed": true, - "position": 14 -} diff --git a/docs/sdk/taptap-appsafety/features.mdx b/docs/sdk/taptap-appsafety/features.mdx deleted file mode 100644 index 9c0b0e301..000000000 --- a/docs/sdk/taptap-appsafety/features.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: 应用安全功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -## 应用安全功能介绍 -TapTap 开发者服务提供的应用安全模块,主要是通过 APK 加固和反作弊检测等能力,来为游戏应用程序的安全进行保驾护航。 - -## 使用规则 - -- 适用游戏:加入薪火计划,并正常运行的游戏,或在 TapTap 平台独家发行的游戏。 -- 功能入口:开发者中心 — 选择游戏 — 游戏服务 — 应用安全 (如下图) - -![](https://capacity-files.lcfile.com/XFKCH21Nw56UDX7B2VGXTtevPl6Eh9DY/app-safety-introduce.png) \ No newline at end of file diff --git a/docs/sdk/taptap-appsafety/guide.mdx b/docs/sdk/taptap-appsafety/guide.mdx deleted file mode 100644 index ab6aaeceb..000000000 --- a/docs/sdk/taptap-appsafety/guide.mdx +++ /dev/null @@ -1,230 +0,0 @@ ---- -title: 应用安全接入指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -本文介绍如何在游戏中加入 TapTap 推出的应用安全功能。 - -## APK 加固功能说明 - -### 功能特点 - -| 功能名称 | 描述 | -| --- | --- | -| SO 加密保护 | 对 SO 文件进行代码、函数、导出表加密混淆,防止 HOOK 攻击,有效提高脱壳难度。 | -| global-metadata 加密 | 针对 IL2CPP 游戏,对 global-metadata 实现加固,提高破解门槛。 | -| AAB 加固 | Android App Bundle 加固方案支持。 | -| 加固方案自保护 | 对加固方案源码进行了混淆,极大的提高了外挂作者对加固方案的逆向分析门槛,进一步提高了游戏的破解难度。 | -| ROOT 环境检测 | 减少风险环境作弊的可能性。 | -| 反调试 | 检测 IDA、FRIDA 等调试工具,有效提高动态分析门槛。 | -| 模拟器识别 | 基于样本库,精确识别模拟器用户,为游戏隔离匹配提供准确依据。 | -| 云手机识别 | 基于真机数据与外网多种云手机样本,多维度断定云手机环境。 | -| 防二次打包 | 保持加固前后签名一致的情况下,可以开启签名校验,防止二次打包。 | -| 恶意软件对抗 | 防框架插件、内存修改器、多开等软件。 | - -### 操作说明 - -#### 功能入口 - -- 未加入薪火,且在 TapTap 独家发型的游戏,直接通过「应用安全」功能入口进入即可,如没有该入口,可联系对应的商务或者运营申请。 - -- 薪火计划的游戏,则直接在薪火页面,选择 APK 加固权益,点击立即使用即可(如下图)。 - -![](https://capacity-files.lcfile.com/wKzxfJfVfKOxDKRlEgiQvAnQ69OdKiak/app-safety-jiagu.png) - -#### 使用规则 - -- 每个游戏单个自然月加固成功次数不允许超过5次,开发者请勿滥用; -- 有多渠道需求的使用方,若须先加固再进行二次打包,请确保项目处于正常运行状态; -- 加固文件大小不超过 2G; -- 加固不支持自动签名,加固后需要重新签名; -- 每次加固,最多加固 2 个文件。 - -## 反作弊检测功能说明 - -反作弊检测功能主要是游戏通过加固后,平台将监测并捕获 App 运行时非法操作及环境的数据进行展示。通过展示作弊设备及相关详情,让开发者深入了解安全现状、精准把握安全态势。 - -![](https://capacity-files.lcfile.com/XJgCXueaiMHdcKDHjom7yBel6fqL8zD7/app-safety-resist.png) - -### 页面字段说明 - -- UUID:运行设备标记 -- 用户ID:游戏侧上报 -- 特征编号:显示值为十六进制 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    特征编码 ID 检测类型 详细信息
    1000000000000000+id 0 root 类 root 系统,测试签名模式
    1 root root、magisk、TESTSIGNING
    2000000000000000+id 0 调试类 调试器
    1 android debug ida、frida、java 信息
    3 android ADB adb enabled
    3000000000000000+id 0 hook 类 被 hook
    1 android hooked, iOS inline hook api flag
    2 android hook 框架,iOS fh hook hook frame
    3 android java hooked java hook flag
    5 android hook 框架(多开器)
    4000000000000000+id 0 内存类 运行时内存被外部修改
    1 判断内存设备被打开
    5000000000000000+id 0 资源修改类 资源、签名被修改
    1 android 签名,iOS 异常签名团队 apk or so 签名信息
    6000000000000000+id 0 模拟器类 使用模拟器运行
    any 模拟器 类型
    7000000000000000+id 0 云手机类 使用云手机运行
    any 云手机 类型
    8000000000000000+id 0 通用外挂、外设类
    any 通用外挂、外设类型值 外设
    B000000000000000+id 0 沙盒类
    any 沙盒检测类型 类型
    - -- 检测类型 - -| index | 类型 | 描述 | -| --- | --- | --- | -| 1 | root 类 | android and ios:运行环境是 root 环境,windows:测试签名环境 | -| 2 | 调试类 | 运行进程被调试 | -| 3 | hook 类 | 被 hook | -| 4 | 内存修改类 | 运行内容被修改 | -| 5 | 资源修改类 | 资源签名被修改 | -| 6 | 模拟器类 | 模拟器运行 | -| 7 | 云真机类 | 云真机运行 | -| 8 | 通用外挂类 | 已知各类作弊软件检测 | -| 9 | 特殊外挂类 | 外设输入捕获计算结果 | -| 10 | 注入类 | -作弊框架或其他注入 | -| 11 | 沙盒类 | 沙盒环境运行 | - -- 检测结果 - -1、对应检测类型实行处理策略。 - -2、检测到退出或者不做额外操作 - -- 详细信息 - -1、检测类型详细信息 - - - -### 功能使用规则 - -#### 图表操作 - -- 提供基于上报 ID、用户 ID、检测类型、检测结果以及时间的筛选功能。 -- 提供报表下载功能,支持对检测的数据下载到本地。 - -![](https://capacity-files.lcfile.com/skGGCntpLlMUqjeQOMESC2mHQs7dOG9q/app-safety-diagrams.png) - -### 反作弊策略配置 - -- 提供反作弊策略配置,开发者自主配置作弊打击策略。 -- 配置默认开启,支持配置,开发者可按需关闭,关闭后,则相应配置策略不生效。 - -![](https://capacity-files.lcfile.com/ItAilmNzJsHD2usODnWMLHIqahLDt9Ow/app-safety-cheat.png) diff --git a/docs/sdk/taptap-connect/_category_.json b/docs/sdk/taptap-connect/_category_.json deleted file mode 100644 index 58fb01dea..000000000 --- a/docs/sdk/taptap-connect/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapTap Connect(悬浮窗)", - "collapsed": true, - "position": 7 -} diff --git a/docs/sdk/taptap-connect/features.mdx b/docs/sdk/taptap-connect/features.mdx deleted file mode 100644 index 6de324346..000000000 --- a/docs/sdk/taptap-connect/features.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: 悬浮窗功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -## 产品简介 -TapTap Connect(以下简称悬浮窗)是连接 TapTap、游戏和玩家的工具。开发者启用该功能后,成功登录 TapTap 账号的玩家可在悬浮窗内进行浏览游戏动态、评价游戏、分享游戏下载链接等操作,来帮助游戏提升留存并带来新用户。 - -![](https://capacity-files.lcfile.com/6CKik33HXTOx9AQwk0c50wnfwxz5VmO2/xuanfuchuang-00.png) - -![](https://capacity-files.lcfile.com/G4fyGhT7kBgIxKTsLIEj152vhVrX59fX/xuanfuchuang-01.png) - -## 玩家交互流程 - -- 玩家使用 TapTap 登录游戏后,将出现悬浮窗入口,该入口默认为半隐藏状态,支持任意拖拽以及停留后就近贴边。 -- 玩家点击入口,即可打开悬浮窗面板,悬浮窗内展示该玩家的 TapTap 个人账号信息(头像、昵称以及 ID),同时也支持玩家从悬浮窗内打开内嵌动态、分享游戏、跳转到 TapTap 游戏详情页内评价游戏等功能。(评价功能仅支持安卓系统) -- 玩家点击悬浮窗面板外空白处,将关闭悬浮窗,入口曝光3秒后,自动恢复半隐藏状态。 - -## 功能优势 - -将 TapTap 社区内容,评价分享等能力进行了整合,玩家无需跳出游戏内即可进行相关操作。 - -- 一是可以帮助游戏提升留存并带来新用户; - -- 二是减少了开发者的接入开发量,接入悬浮窗后,就无需再次单独接入内嵌动态、打开评论区、分享等功能。 - - -## 使用说明 - -### 接入前操作 -- 游戏需接入 TapTap 登录功能,并开启内嵌动态服务。 - -- 在开发者中心【游戏服务 — TapTap 登录 — 悬浮窗配置】模块,启用悬浮窗功能(悬浮窗功能默认关闭,需要操作启用后,才会对玩家展示)。 - - -## 主题样式配置说明 - -### 1、支持对悬浮窗入口进行自定义 - -- 默认直接使用平台提供的图标 - -- 可直接使用玩家的 TapTap 头像作为入口图标 - -- 开发者按照设计规范,自行设计与游戏匹配的入口图标 - -### 2、支持对悬浮窗面板的用户信息区域背景进行自定义 - -- 默认直接使用平台提供的背景 - -- 开发者按照设计规范,自行设计与游戏匹配的背景 - -![](https://capacity-files.lcfile.com/I15RUvaL0Nk58mDTAvQWNhXtr5dh2ba4/xuanfuchuang-02.png) diff --git a/docs/sdk/taptap-connect/guide.mdx b/docs/sdk/taptap-connect/guide.mdx deleted file mode 100644 index 3dace685c..000000000 --- a/docs/sdk/taptap-connect/guide.mdx +++ /dev/null @@ -1,241 +0,0 @@ ---- -title: 悬浮窗开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import Languages from "../_partials/languages.mdx"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -本文介绍如何在游戏中加入 [TapTap 悬浮窗](/sdk/taptap-connect/features/)。使用悬浮窗功能需依赖 TapTap 登录(TapLogin)以及 TapTap 内嵌动态功能(TapMoment)。 - -## 环境要求 - - - -<> - -- TapSDK **3.21.0** 及以上版本 -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -- TapSDK **3.21.0** 及以上版本 -- Android 5.0(API level 21)或更高版本 - - - -<> - -- TapSDK **3.21.0** 及以上版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- TapSDK **3.22.0** 及以上版本 -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - - - -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下权限: - -``` - -``` - - - -<> - - - -<> - - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启悬浮窗相应设置; - -## SDK 获取 - - -请先参考 [TapTap 登录](/sdk/taptap-login/guide/start/#sdk-获取) 和 [内嵌动态](/sdk/embedded-moments/guide/##sdk-获取)完成 SDK 获取,然后在此基础上可以通过 [下载](/tap-download) 获得 TapSDK,添加 `TapConnect` 模块: - - - - - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapConnect_${sdkVersions.taptap.android}', ext:'aar') // TapTap 悬浮窗 - implementation (name:'TapMoment_${sdkVersions.taptap.android}', ext:'aar') // TapTap 悬浮窗依赖内嵌动态-必选 - implementation (name:'TapLogin_${sdkVersions.taptap.android}', ext:'aar') // TapTap 悬浮窗依赖登录模块-必选 - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') // 必选:TapSDK 基础库 - implementation (name:'TapBootstrap_${sdkVersions.taptap.android}', ext:'aar') // 可选:TapSDK 基础库 -}`} - - - - {`// 悬浮窗 -TapConnectResource.bundle -TapConnectSDK.framework -// 悬浮窗依赖内嵌动态和登录模块-必选 -TapMomentResource.bundle -TapMomentSDK.framework -TapLoginSDK.framework -TapLoginResource.bundle -// 基础模块-必选 -TapCommonResource.bundle -TapCommonSDK.framework -// 基础模块-可选 -TapBootstrapSDK.framework -`} - - -```cs -PublicDependencyModuleNames.AddRange(new string[] { - //... - "TapConnect" //必选 - "TapLogin" //必须-悬浮窗依赖登录模块 - "TapMoment" //必选-悬浮窗依赖内嵌动态 - "TapCommon" //必选-基础模块 - "TapBootstrap"//可选-基础模块 -}); -``` - - - -## 初始化 - -:::info -以下两种初始化方式任选其一。 -::: - -### TapSDK 初始化 - -如果你已经完成[内建账户系统 Tap 登录](/sdk/taptap-login/guide/start/#sdk-初始化)的初始化,这里只需要引入悬浮窗模块,不需要其他额外处理。 - -### 悬浮窗单独初始化 - -如果游戏不通过上面提供的 `TapBootstrap` 方法初始化 TapSDK,悬浮窗可以单独初始化,因为悬浮窗依赖单纯 Tap 认证和内嵌动态, - -:::tip - -单独初始化悬浮窗需要优先完成`单纯 TapTap 认证` 和 `内嵌动态` 的初始化动作。关于单纯 TapTap 认证初始化[参考](/sdk/taptap-login/guide/tap-login/#初始化),关于内嵌动态初始化[参考](/sdk/embedded-moments/guide/#单纯的内嵌动态初始化), - -::: -下面是单独初始化悬浮窗的示例代码: - - - -```cs -using TapTap.Connect; // 命名空间 - -TapConnect.Init("your_client_id", "your_client_token", (bool)isCN); -``` - -```java -//activity 为当前 Activity 实例 -TapConnect.init(activity, "clientId", "clientToken", isCN); -``` - -```objc -[TapConnect initWithClientId:@"clientId" clientToken:@"clientToken" isCN:YES]; -``` - -```cpp -FTapConnect::Init("clientId", "clientToken", true); -``` - - - -### 参数说明 - -* `client_id`、`client_token` 信息可在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 查看, isCN 表示是否为中国大陆地区的应用。 - -## 设置悬浮窗入口显示隐藏 - -有时开发者希望在某些场景下直接控制悬浮窗入口的显示隐藏,比如仅在部分场景显示悬浮窗入口,这时可以调用下面的接口: - - - -```cs -TapConnect.SetEntryVisible(bool visible) -``` - -```java -TapConnect.setEntryVisible(boolean visible); -``` - -```objc -[TapConnect setEntryVisible:YES]; -``` - -```cpp -FTapConnect::SetEntryVisible(true); -``` - - - -## 国际化 - -悬浮窗支持设置语言: - - diff --git a/docs/sdk/taptap-login/_category_.json b/docs/sdk/taptap-login/_category_.json deleted file mode 100644 index 08fcbcda1..000000000 --- a/docs/sdk/taptap-login/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapTap 登录", - "collapsed": true, - "position": 3 -} diff --git a/docs/sdk/taptap-login/best-practice.mdx b/docs/sdk/taptap-login/best-practice.mdx deleted file mode 100644 index 6a6406cb2..000000000 --- a/docs/sdk/taptap-login/best-practice.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: TapTap 登录最佳实践 -sidebar_label: 最佳实践 -sidebar_position: 3 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -## 登录流程 - -玩家在登录时,操作的步骤越少,路径越短,则转化率越高。建议使用相对简短的引导,保留必要的步骤,让玩家能快速进入游戏。如图所示: - - - -### 登录界面 - -为玩家提供 TapTap 登录按钮,按照[登录设计指南](/design/)绘制,并参考[功能介绍](/sdk/taptap-login/features/) 中单个登录与多个登录的展现方式。 - -### 使用玩家公开信息 - -玩家在游戏内创建角色时,可以直接使用玩家登录后授权给游戏的公开信息,包括玩家的 TapTap 头像、昵称等,帮助玩家自动完成填写流程。 - -使用内建账户搭建游戏账户系统,可以参考[设置其他用户属性](/sdk/authentication/guide/#设置其他用户属性)一节设置用户信息。 - -仅使用单纯的 TapTap 登录,可以参考下面的流程图: - - - -*参考上一篇「开发指南」文档。 - -### 提供切换账号功能 - -建议游戏为玩家提供切换账号的功能。 - -- 玩家切换账号时,务必调用登出接口,以保证登录账号与其他游戏服务(内嵌动态)账号保持一致。 -- 当玩家已经完成了登出的操作,为玩家自动显示出登录的界面,让玩家可以使用另一个账号登录。 - -### 提供账号绑定功能 - -建议在游戏中添加账号绑定功能,为玩家提供多种登录方式。 - -使用内建账户搭建游戏账户系统,可以参考[绑定第三方账户](/sdk/authentication/guide/#绑定第三方账户)一节。 - -如果游戏有自己的账户系统,仅使用单纯 TapTap 登录,则需要游戏自行实现账号绑定功能,方便玩家绑定游戏账号与 TapTap 登录后返回的用户唯一标识。 - -## Checklist - -向玩家提供登录功能前,开发者需要测试登录流程是否正常完成,检查以下事项: - -* 游戏是否达到 [SDK 环境要求](/sdk/start/quickstart/#环境要求)。 -* 开发者是否了解 TapSDK 中两种 TapTap 登录方式,并选择了适合游戏的一种。参考[接入 TapTap 登录](/sdk/taptap-login/guide/start/)。 -* 是否在 TapTap 开发者后台填写了 Android 平台或 iOS 平台相关配置。参考[配置签名证书](/sdk/start/quickstart/#配置签名证书)和[格式要求](/sdk/taptap-login/features/#配置签名证书)。 -* 在未安装 TapTap 客户端的设备上打开游戏,是否能以 WebView 方式完成登录流程,是否能获取玩家授权的基本信息。 -* 在安装了最新版 TapTap 客户端的设备上打开游戏,是否能拉起 TapTap 客户端完成登录流程,是否能获取玩家授权的基本信息。 -* 登录授权完成后,退出游戏再次进入,是否可以[静默登录](/sdk/taptap-login/features/#实现静默登录)。 -* 登录授权未完成就退出游戏,或者点了取消,再次进入游戏,是否能重新开始登录流程。 diff --git a/docs/sdk/taptap-login/faq.mdx b/docs/sdk/taptap-login/faq.mdx deleted file mode 100644 index 0b72d8db0..000000000 --- a/docs/sdk/taptap-login/faq.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -### TapTap 登录报:`请求的 scope(compliance)不匹配任何被允许的 scope` 异常提示。 -检查是否导入了实名认证防沉迷 SDK 的包体但是并没有对此进行初始化,如果不需要实名认证功能的话,则项目中不要导入实名认证的包体。 - -### TapTap For iOS 登录报 `accessToken: sdk_not_matched` 异常 -检查 TapTap 开发者后台的应用是否配置了应用的 Bundle ID。 - -### TapTap 登录报 `signature not match` 异常 -接入了 TapTap 登录但没有在开发者中心后台配置签名或者配置错了都会提示该异常。有的开发者可能没有调试出异常信息,可以通过这种方式进行验证:如果将 TapTap 客户端卸载,测试登录功能时会弹出 WebView 授权,而测试设备安装了 TapTap 客户端则无法拉起客户端授权,这种情况基本上就是因为签名配置问题导致的,请参考[文档](/sdk/start/quickstart/#配置签名证书)完成配置。 - -### TapTap 登录提示 `暂无测试资格或不在测试期间,无法登录游戏` 异常提示。 -**TapTap 开发者中心 > 商店 > 版本发布 > 内部测试** 检查是否开启了内部测试功能,如果开启了检查是否没有将当前 TapTap ID 账号添加进测试用户中。 - -### TapTap 登录报 `state not equal` 异常 -检查当前设备系统时间是否已经开启联网同步、检查当前设备的 TapTap 客户端版本是否过低。 - -### TapTap 登录报 `java.lang.NoSuchFieldException: CACHE_ELSE_NETWORK` 异常 -由于 Android 项目开启了混淆操作,TapSDK 已经做了混淆处理,需要跳过对 TapSDK 的混淆操作。具体请参考 [Android 代码混淆](/sdk/start/quickstart/#android-代码混淆)。针对 Android 原生目前暂不支持开启资源混淆操作,如果项目开启了资源混淆操作请关闭掉,`shrinkResources false`。 - -### TapTap 登录报 `{"code":36869,"error_description":"Unauthorized."}` 异常 -检查 TapSDK 初始化的 Client ID、Client Token 以及 ServerURL 参数赋值是否有误,此外,ServerURL 请**使用 HTTPS 协议**,参考文档关于 **[域名](/sdk/start/get-ready/#域名)** 的说明。 - -### TapTap 登录报 `Chain validation failed` 异常 -可以按照以下步骤排查: -1、检查设备是否连接代理并没有正常安装证书; -2、检查手机系统时间是否有修改。 - -### TapTap 登录报 `application id is empty` 异常 -可以按照以下步骤排查: -1、检查 TapSDK 的初始化操作是否在安卓 UI 线程也就是 main 线程中执行; -2、确保 TapSDK 的初始化已经完成,避免在 TapSDK 初始化后紧接着调用 TapTap 登录功能,建议 TapSDK 的初始化前置。 - -### 单纯 TapTap 用户认证 For Unity 集成报如下异常: -``` -Assembly 'Assets/TapTap/Common/Plugins/TapTap.Common.dll' will not be loaded due to errors: -Unable to resolve reference 'LC.Newtonsoft.Json'. Is the assembly missing or incompatible with the current platform? -``` -报错原因是因为使用 TapSDK Unity v3.7.1 及更高版本时并没有在项目的 `Packages/manifest.json` 文件中添加 `com.leancloud.storage` 模块导致的,参考[文档](/sdk/taptap-login/guide/tap-login/#sdk-获取)添加即可。 - -### 登录提示: this app is not allowed for this domain - -1. 检查开发者后台应用配置中是否开通了 TapTap 登录服务; -2. 检查项目初始化代码中 ClientId、ClientToken、ServerUrl (需要以 `https://` 开头)是否和开发者后台保持一致。 - - -![](https://dc-file.leanticket.cn/VIFonJ9YOJ4SAXb3WdVhMYWlF84xGKkN/CF18E02C-C819-4940-9C9B-60050062EDD0.png) - -### 玩家通过 TapTap 登录后,开发者能否获得玩家的手机号? - -手机号属于玩家的隐私信息,目前暂不支持开发者获取登录玩家的手机号。 - -### unity 项目集成 TapSDK 然后导出 Xcode 项目,运行时报错 `NullReferenceException: Object reference not set to an instance of an object TapTap.Login.Editor. TapLoginlOSProcessor.OnPostprocessBuild ...` - -请参考文档 [iOS 配置](https://developer.taptap.cn/docs/sdk/taptap-login/guide/start/#ios-%E9%85%8D%E7%BD%AE)描述,检查是否有创建 `TDS-Info.plist` 文件。(注意复制配置内容时,请删除注释行) - -### iOS 应用中 TapSDK 内使用的 protobuf 版本是 30004,如果和你应用中版本产生冲突如何解决? - -在使用 `protobuf` 时,如果你的应用中存在版本冲突,可以通过将 `protobuf` 编译为一个动态框架(Dynamic Framework)来隔离不同版本的依赖。这种方法能够有效地避免版本冲突,特别是在大型项目或者多模块项目中。 diff --git a/docs/sdk/taptap-login/features.mdx b/docs/sdk/taptap-login/features.mdx deleted file mode 100644 index 7ae591c30..000000000 --- a/docs/sdk/taptap-login/features.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: TapTap 登录功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { Conditional } from "/src/docComponents/conditional"; - -为了访问 TapTap Developer Services(以下简称 TDS)的相关服务功能,你的用户需要拥有一个 TapTap 账号。如果用户未使用 TapTap 账号,你的应用在调用 TDS 服务 API 时可能会遇到错误。本文档介绍了如何在你的应用中实现 TapTap 登录。 - -## 业务介绍 - -TapTap 账号服务是基于标准的 OAuth 2.0 协议构建的授权登录系统,为开发者提供了简单、安全、快速的账号登录授权功能,为用户免去输入账号密码的繁琐步骤,让用户只需一键通过 TapTap 账号授权,即可使用你的应用。 - -在取得用户授权之后,开发者可以通过接口调用的方式获得 TapTap 用户的相关公开信息,包括用户昵称、头像等,可用于提高应用的用户体验。 - -## 前期工作 - -请确认已经在 **TapTap 开发者中心 > 应用配置** 完成了开启操作。可参照入门指南中的[准备工作](/sdk/start/get-ready/)。 - -### 配置签名证书 - -为了更高的安全性,TapTap 登录服务需要校验你的游戏。你需要提交游戏的 package name(Android 包名)、Bundle ID(iOS 包名)以及 Android 签名。 - -:::tip - -1. Android 的包名请使用符合 Android 规范的命名方式。参考文档:[Android 开发者 - 设置应用 ID](https://developer.android.com/studio/build/application-id) - -2. Android 的签名为 Keystore 文件中的 MD5 字符串(32 位),填入时请去除特殊符号。 - -3. iOS 的 Bundle ID 请使用符合苹果规范的命名方式。参考文档:[Property List Key - CFBundle 标识符](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier) - -::: - -## 实现交互式登录 - -检查当前如果没有用户登录状态时,需要为用户提供一个可视化点击交互的登录界面。TapTap 审核团队会在应用上架 TapTap 商店时审核你的登录界面,请务必参照[《登录按钮设计规范》](/design/)进行绘制。 - -### 单个登录方式 - -当应用中仅提供 TapTap 一种登录方式时,建议在开始游戏的主界面,绘制一个可交互的登录按钮。按钮的范围大小、按钮上的文案使用,均不能误导、不能阻碍用户的正常顺畅点击。 - -登录按钮的设计样式,在[《登录按钮设计规范》](/design/)允许的范围内,可适当添加与游戏气质相符的风格元素。此外,TDS 也为你准备了不同场景下 TapTap 登录按钮的设计图标,帮助你快速实现登录流程。请点击[登录按钮素材](/tap-download/#登录按钮素材)下载资源。 - - - - - - - - - - - - - -### 多种登录方式 - -如果游戏还有其他登录方式同时存在,应为用户提供合理布局的登录界面,尽可能地从外观明显区分每种登录方式的不同,让用户可以快速找到目标。 - - - -## 实现静默登录 - -静默登录可以帮助用户跳过登录的流程,通常用于用户下一次启动游戏时,仍需之前登录状态的场景。 - -当用户启动游戏时,你可以尝试检查用户是否已经在当前设备上登录、登录信息是否仍有效。 - -- 使用内建账户方式,参考[检查登录状态](/sdk/taptap-login/guide/start/#检查登录状态)。 -- 使用单纯 TapTap 登录,参考[检查登录状态和用户信息](/sdk/taptap-login/guide/tap-login/#检查登录状态和用户信息)。 - -这样可以尝试在不显示登录按钮或界面的情况下帮用户完成登录过程。 - -## 登录授权 - -移动应用的 TapTap 账号服务需要与 TapTap 移动客户端配合使用。TapSDK 会根据用户设备中 TapTap 客户端的安装情况来自动选择使用合适的登录流程。 - - - -[点击此处](https://www.taptap.cn/mobile) 下载 TapTap 移动客户端。 - - - - - -[点击此处](https://www.taptap.io/mobile) 下载 TapTap 移动客户端。 - - - -### 唤起 TapTap 客户端授权登录 - -当用户单击 TapTap 登录按钮时,如果 TapSDK 检测到用户设备中已经安装了 TapTap 客户端,会自动唤起设备中的 TapTap 客户端,并识别客户端中的登录信息,进行授权登录。 - - - - - - - - - - - - - - - -### 打开 WebView 授权登录 - - -当用户单击 TapTap 登录按钮时,如果 TapSDK 检测到用户设备中未安装 TapTap 客户端,则会打开 WebView 进行登录流程。 - - - - - - - -当用户点击 TapTap 登录按钮时,如果 TapSDK 检测到用户设备未安装 TapTap 客户端,则会提示用户下载 TapTap 客户端。 - - - - diff --git a/docs/sdk/taptap-login/guide/_category_.json b/docs/sdk/taptap-login/guide/_category_.json deleted file mode 100644 index 3446da847..000000000 --- a/docs/sdk/taptap-login/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 2 -} diff --git a/docs/sdk/taptap-login/guide/start.mdx b/docs/sdk/taptap-login/guide/start.mdx deleted file mode 100644 index 22d7e53dc..000000000 --- a/docs/sdk/taptap-login/guide/start.mdx +++ /dev/null @@ -1,905 +0,0 @@ ---- -title: 接入 TapTap 登录 -sidebar_label: 功能接入 -sidebar_position: 0 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import Languages from "../../_partials/languages.mdx"; -import Profiles from "../../_partials/tap-login-profile.mdx"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import UnitySDKInstallation from "../../_partials/unity-sdk-installation.mdx"; - - -接入 TapTap 登录有两种方式: - -1. 基于[内建账户系统](/sdk/authentication/features)接入 TapTap 登录;(不推荐) -2. [单纯 TapTap 用户认证](/sdk/taptap-login/guide/tap-login/)。(推荐) - -第一种方式一般适用于以下场景: - -- 希望直接使用 TapSDK 提供的账户系统 -- 希望将更多第三方账号(比如 QQ、微信、Apple 等)绑定到玩家账号上 -- 希望使用 TapSDK 的好友、成就等基于内建账户系统的服务和功能 - -:::tip -内建账户系统:需要游戏侧进行[自定义 API 域名](/sdk/start/get-ready/#绑定-api-域名)绑定,需要游戏进行备案;此外,如果游戏需要兼容 Android 7.1 以下版本设备,需自行购买 SSL 证书并进行手动管理。因此,推荐使用 `单独 TapTap 用户认证`方式。 -::: - -相反,如果你的游戏自己实现了账户系统,也不打算使用`排行榜`、`云存档`、`成就`功能,那么可以考虑使用第二种方式。 - -首先介绍第一种方式,然后介绍[第二种方式](/sdk/taptap-login/guide/tap-login/)。 - -无论使用哪种方式,首先都需要在 **开发者中心 > 游戏服务 > 功能接入** 开启「TapTap 登录」。 - -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - - - -## 权限说明 - - - -<> - - - -<> -开发者需在应用 `AndroidManifest.xml` 中添加如下权限: - -``` - -``` - - - -<> - - - -<> - - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启应用配置、绑定 API 域名; -2. 参考 [快速开始](/sdk/start/quickstart/#配置签名证书) 配置包名与签名证书; - - -## SDK 获取 - - -<> - - - -#### iOS 配置 - -在 `Assets/Plugins/iOS/Resource` 目录下创建 `TDS-Info.plist` 文件,复制以下代码并且**替换其中的 `ClientId`**。如果游戏使用了 TapTap [内嵌动态](/sdk/embedded-moments/features/)或[数据分析](/sdk/tapdb/features/)服务,需要配置相关权限并**替换授权文案**: - -:::tip - -复制使用以下内容时,**请删除空行以及注释**,以免出现 XML 解析时报错,`ApplicationException: expected a key node`。 - -::: - -```xml - - - - - taptap - - client_id - ClientId - - - NSPhotoLibraryUsageDescription - 说明为何应用需要此项权限 - NSCameraUsageDescription - 说明为何应用需要此项权限 - NSMicrophoneUsageDescription - 说明为何应用需要此项权限 - - NSUserTrackingUsageDescription - 说明为何应用需要此项权限 - - -``` - -#### 配置跳转 TapTap 应用 - -用户无 TapTap 应用时,默认会通过 WebView 登录。 - -1. 打开 `info.plist`,添加如下配置(请替换 `clientID` 为你在控制台获取的 Client ID): - - ![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` - - -<> - -1. [下载 TapSDK Android](/tap-download),解压后选择需要用到的 SDK 包导入到项目 `project/app/libs` 目录下。 - -2. 打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: - - {` -dependencies { - ... - // 导入 libs 目录下所有 aar 的包: - implementation fileTree(dir: 'libs', include: ['*.aar']) - // 如果需要单独导入 libs 目录下的指定包,请按照如下方式: - implementation files('libs/TapBootstrap_${sdkVersions.taptap.android}.aar') // TapTap 启动器 - implementation files('libs/TapCommon_${sdkVersions.taptap.android}.aar') // TapTap 基础库 - implementation files('libs/TapLogin_${sdkVersions.taptap.android}.aar') // TapTap 登录 - implementation 'com.taptap:lc-storage-android:${sdkVersions.leancloud.java}' // 数据存储 - implementation 'com.taptap:lc-realtime-android:${sdkVersions.leancloud.java}' // 即时通讯 -} -`} - -3. 在 `AndroidManifest.xml` 添加网络权限: - - ```java - - ``` - -4. 旧版 Android 额外配置 - - 如果 `targetSdkVersion < 29`,还需要添加如下配置: - - - `manifest` 节点添加 `xmlns:tools="http://schemas.android.com/tools"` - - `application` 节点添加 `tools:remove="android:requestLegacyExternalStorage"` - - -<> - -#### 导入 SDK - -1. 在 Xcode 选择工程,到 **Build Setting > Other Linker Flags** 添加 `-ObjC` 和 `-Wl -ld_classic`。 - -2. 下载 [TapSDK iOS](/tap-download),解压后选择需要导入的资源文件,直接拖拽到项目目录即可。 - -3. 视需要导入下载的资源文件: - - - 必选:TapTap 启动器、基础库、登录 - - ``` - TapBootstrapSDK.framework - TapCommonSDK.framework - TapLoginSDK.framework - LeanCloudObjc.framework - TapCommonResource.bundle - TapLoginResource.bundle - ``` - -4. 请仔细核对下面依赖库是否都添加成功: - - ``` - // 必选 - WebKit.framework - Security.framework - SystemConfiguration.framework - CoreTelephony.framework - SystemConfiguration.framework - libc++.tbd - libsqlite3.tbd - - // TapTap 内嵌动态 - AVFoundation.framework - CoreTelephony.framework - MobileCoreServices.framework - Photos.framework - SystemConfiguration.framework - WebKit.framework - - // 数据分析 - // 如果不需要获取 IDFA 则不要添加 `AppTrackingTransparency` 和 `AdSupport` 两个系统库 - AppTrackingTransparency.framework - AdSupport.framework - CoreMotion.framework - Security.framework - SystemConfiguration.framework - libresolv.tbd - libsqlite3.0.tbd - libz.tbd - ``` - -#### 配置权限 - -如果游戏使用了 TapTap [内嵌动态](/sdk/embedded-moments/features/)或[数据分析](/sdk/tapdb/features/)服务,那么需要在 `info.plist` 配置相关权限并**替换授权文案**: - -```xml - -NSPhotoLibraryUsageDescription -说明为何应用需要此项权限 -NSCameraUsageDescription -说明为何应用需要此项权限 -NSMicrophoneUsageDescription -说明为何应用需要此项权限 - -NSUserTrackingUsageDescription -说明为何应用需要此项权限 -``` - -#### 配置跳转 TapTap 应用 - -用户无 TapTap 应用时,默认会通过 WebView 登录。 - -1. 打开 `info.plist`,添加如下配置(请替换 `clientID` 为你在控制台获取的 Client ID): - - ![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` - -2. 配置 openUrl: - - a) 如果项目中有 `SceneDelegate.m`,请先删除,然后请添加如下代码到 `AppDelegate.m` 文件: - - ```objectivec - - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [TDSHandleUrl handleOpenURL:url]; - } - - - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSHandleUrl handleOpenURL:url]; - } - ``` - - b) 删除 `info.plist` 里面的 Application Scene Manifest - - ![](/img/tap_ios_appmanifest.png) - - c) 删除 `AppDelegate.m` 文件中的两个管理 `Scenedelegate` 生命周期代理方法 - - ```objectivec - #pragma mark - UISceneSession lifecycle - - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { - - return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; - } - - - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { - } - ``` - - d) 在 `AppDelegate.h` 中添加 `UIWindow` - - ```objectivec - @property (strong, nonatomic) UIWindow *window; - ``` - - - -<> - -* 下载 [TapSDK UE4](/tap-download),TapSDK-UE4-xxx.zip 解压后将 `TapBootstrap`、`TapCommon`、`TapLogin` 文件夹 Copy 到项目的 `Plugins` 目录中 -* 重启 Unreal Editor -* 打开 编辑 > 插件 > 项目 > TapTap,开启 `TapBootstrap` 和 `TapLogin` 模块 - -#### 添加依赖 - -在 Project.Build.cs 中添加所需模块: - -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapBootstrap", - "TapLogin" -}); -if (Target.Platform == UnrealTargetPlatform.IOS || Target.Platform == UnrealTargetPlatform.Android) -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - // 推送接入 - // "LeanCloudPush", - - "LeanCloudMobile" - } - ); -} -else -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - "LeanCloud" - } - ); -} -``` - -#### 导入头文件 - -```cpp -#include "TapBootstrap.h" -``` - -
    - -点击展开 iOS 配置 - -在 项目设置 > Platform > iOS > Additional Plist data 中可以填入一个字符串,复制以下代码并且替换其中的 `ClientID` 以及授权文案。 - -```xml -CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt{ClientID} - - - -LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - -``` - -如果接入 TapDB 模块,那么还需要加上: - -```xml -NSUserTrackingUsageDescription -{数据追踪权限申请文案} -``` - -
    - - - -
    - -## SDK 初始化 - -初始化 TapSDK 时需传入 `Client ID`、区域等应用配置信息。 - - - -<> - -```cs -using TapTap.Bootstrap; // 命名空间 -using TapTap.Common; // 命名空间 - -var config = new TapConfig.Builder() - .ClientID("your_client_id") // 必须,开发者中心对应 Client ID - .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token - .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .RegionType(RegionType.CN) // 非必须,CN 表示中国大陆,IO 表示其他国家或地区 - .ConfigBuilder(); -TapBootstrap.Init(config); -``` - - -<> - -**请确保 TapSDK 的初始化在主线程(UI 线程)中执行。** - -```java -TapConfig tdsConfig = new TapConfig.Builder() - .withAppContext(MainActivity.this) // Context 上下文 - .withClientId("your_client_id") // 必须,开发者中心对应 Client ID - .withClientToken("your_client_token") // 必须,开发者中心对应 Client Token - .withServerUrl("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .withRegionType(TapRegionType.CN) // TapRegionType.CN:中国大陆,TapRegionType.IO:其他国家或地区 - .build(); -TapBootstrap.init(MainActivity.this, tdsConfig); -``` - - -<> - -```objectivec -// 开发者必须至少依赖 `TapBootstrap`、`TapLogin`、`TapCommon` 以及 `LeanCloudObjc` 模块,并按照如下方式完成初始化: -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // 必须,开发者中心对应 Client ID -config.clientToken = @"your_client_token"; // 必须,开发者中心对应 Client Token -config.serverURL = @"https://your_server_url"; // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -config.region = TapSDKRegionTypeCN; // TapSDKRegionTypeCN:中国大陆,TapSDKRegionTypeIO:其他国家或地区 -[TapBootstrap initWithConfig:config]; -``` - - - -<> - -`TapBootstrap` 初始化方法会把直接初始化 TapLogin 模块,如果插件中包含 TapDB 并且 `DBConfig.Enable = true`,那么也会完成 [`TapDB` 初始化](/sdk/tapdb/sdk/client-side-integration/#tapsdk-init)。 - -这两个模块无需再次初始化。 - -```cpp -FTUConfig Config; -Config.ClientID = "your_client_id"; // 必须,开发者中心对应 Client ID -Config.ClientToken = "your_client_token"; // 必须,开发者中心对应 Client Token -Config.ServerURL = "https://your_server_url"; // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API -Config.RegionType = ERegionType::CN; // ERegionType::CN:中国大陆,ERegionType::Global:其他国家或地区 -FTapBootstrap::Init(Config); -``` - - - - - -初始化的时候,**必须填入** `client_id`、`client_token` 和 `server_url`,其中: - -- `client_id`、`client_token` 信息可在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 查看。 - -- `server_url` 请**使用 HTTPS 协议**,参考文档关于 **[域名](/sdk/start/get-ready/#域名)** 的说明。 - - -## 检查登录状态 - -SDK 会在本地缓存当前用户的登录信息,所以如果一个玩家在游戏内登录之后,下次启动用户通过调用如下方法可以得到之前登录的账户实例。 -缓存不会自动清除。此时玩家无需再次登录,可以直接进入游戏,实现静默登录。 - -如果玩家在游戏内进行了登出或者玩家手动清除了游戏的存储数据,则本地缓存的登录信息也会被删除,下次进入游戏时调用如下方法会返回一个 null 对象,玩家需要[登录](#一键完成-taptap-登录)之后再进入游戏。 - - - -```cs -var currentUser = await TDSUser.GetCurrent(); -if (null == currentUser) -{ - Debug.Log("当前未登录"); - // 开始登录 -} -else -{ - Debug.Log("已登录"); - // 进入游戏 -} -``` - -```java -if (null == TDSUser.currentUser()) { - // 未登录 -} else { - // 已登录,进入游戏 -} -``` - -```objectivec -TDSUser *currentUser = [TDSUser currentUser] -if (currentUser == nil) { - // 未登录 -} else { - // 已登录,进入游戏 -} -``` - -```cpp -TSharedPtr UserPtr = FTDSUser::GetCurrentUser(); -if (UserPtr.IsValid()) { - // 已登录,进入游戏 -} else { - // 未登录 -} -``` - - - - -## 一键完成 TapTap 登录 - -对于「TapTap 用户登录」,TapSDK 提供了特别的支持,以帮助开发者以最便捷的方式和最少的时间完成接入。 - -你可以直接调用 `TDSUser.loginWithTapTap` 方法来一键登录,例如: - - - -```cs -try -{ - // 在 iOS、Android 系统下会唤起 TapTap 客户端或以 WebView 方式进行登录 - // 在 Windows、macOS 系统下显示二维码(默认)和跳转链接(需配置) - var tdsUser = await TDSUser.LoginWithTapTap(); - Debug.Log($"login success:{tdsUser}"); - // 获取 TDSUser 属性 - var objectId = tdsUser.ObjectId; // 用户唯一标识 - var nickname = tdsUser["nickname"]; // 昵称 - var avatar = tdsUser["avatar"]; // 头像 -} -catch (Exception e) -{ - if (e is TapException tapError) // using TapTap.Common - { - Debug.Log($"encounter exception:{tapError.code} message:{tapError.message}"); - if (tapError.code == (int)TapErrorCode.ERROR_CODE_BIND_CANCEL) // 取消登录 - { - Debug.Log("登录取消"); - } - } -} -``` - -```java -TDSUser.loginWithTapTap(MainActivity.this, new Callback() { - @Override - public void onSuccess(TDSUser resultUser) { - Toast.makeText(MainActivity.this, "succeed to login with Taptap.", Toast.LENGTH_SHORT).show(); - // 开发者可以调用 resultUser 的方法获取更多属性。 - String userId = resultUser.getObjectId(); // 用户唯一标识 - String avatar = (String) resultUser.get("avatar"); // 头像 - String nickName = (String) resultUser.get("nickname"); // 昵称 - } - - @Override - public void onFail(TapError error) { - Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show(); - } -}, "public_profile"); -``` - -```objectivec -[TDSUser loginByTapTapWithPermissions:@[@"public_profile"] callback:^(TDSUser * _Nullable user, NSError * _Nullable error) { - if (user) { - // 开发者可以调用 user 的方法获取更多属性。 - NSString *userId = user.objectId; - NSString *username = user[@"nickname"]; - NSString *avatar = user[@"avatar"]; - } else { - NSLog(@"%@", error); - } -}]; -``` - -```cpp -FTDSUser::LoginWithTapTap({TUType::PermissionScope::Profile}, - FTDSUser::FCallBackDelegate::CreateLambda( - [](const TSharedPtr& UserPtr, const FTUError& Error) { - if (UserPtr.IsValid()) { - FString UserID = UserPtr->GetObjectId(); - FString UserName = UserPtr->GetUsername(); - FString Avatar = UserPtr->GetAvatar(); - } - else { - // 登录失败 Error.msg; - } - })); - -// 授权可选项:Profile、Friend、BasicInfo -``` - - - -以上接口调用后会拉起 TapTap 客户端或以 WebView 方式开始登录流程,用户授权之后 SDK 使用 TapTap OAuth 授权结果完成 TDS 内建账户系统的登录。 - -`TDSUser` 即是当前玩家的账户,登录成功之后开发者可以: - -- 通过访问 `objectId` 得到玩家在账户系统的 id(用户唯一标识),可用于游戏服务端内玩家与 TDS 内建账户的绑定或匹配。 -- 通过访问 `nickname` 属性来获得 TapTap 账号的用户名。 -- 通过访问 `avatar` 属性来获得 TapTap 账号的头像。 - -开发者还可以在 TapTap 开发者中心后台 [查看或管理账户系统](/sdk/authentication/features/#管理账户)。 - -### 不同的授权范围 - -TapTap 授权登录接口支持选择不同的授权范围且支持任意组合,但必须包含 `public_profile` 和 `basic_info` 中的一个,不然无法获得 openid 和 unionid。TapTap 授权登录接口可以传递 String 类型的数组作为参数,开发者可以根据自己业务需求添加不同的权限为数组的元素。 - - - -| 权限 | 说明 | TapSDK 最早支持版本 | -|---|---|---| -| `public_profile` | 获得 openid、unionid、用户昵称、用户头像 | v2.0.0 | -| `user_friends` | 获得访问 TapTap 好友相关数据的权限 | v2.0.0 | -| `basic_info` | 获得 openid 和 unionid | v3.13.0 | - -:::tip -若游戏发起 TapTap 授权登录时只请求 `basic_info` 的权限,则用户可享受无感登录的特性,即用户不需要手动确认授权即可自动授权完成登录。 -::: - - - - - -| 权限 | 说明 | TapSDK 最早支持版本 | -|---|---|---| -| `public_profile` | 获得 openid、unionid、用户昵称、用户头像 | v2.0.0 | -| `user_friends` | 获得访问 TapTap 好友相关数据的权限 | v2.0.0 | -| `basic_info` | 获得 openid 和 unionid | v3.13.0 | -| `email` | 获得 email 和 emailVerified 数据 | v3.13.0 | - - - - -### 获取用户信息 - -TapTap 用户登录成功之后,开发者可以通过如下方式获取到 TapTap 授权结果的详细信息: - - - -```cs -var profile = await TapLogin.FetchProfile(); -Debug.Log($"profile: {profile.ToJson()}"); -``` - -```java -Profile profile = TapLoginHelper.getCurrentProfile(); -``` - -```objectivec -[TapLoginHelper currentProfile] -``` - -```cpp -// 注意:如果登录的权限没有:TUType::Profile,那么获取不到用户信息 -TSharedPtr Profile = TapUELogin::GetProfile(); -if (Profile.IsValid()) -{ - // 获取个人信息成功 -} -else -{ - // 获取个人信息失败 -} -``` - - - - - - -## 登出当前账户 - -账户的登出非常简单,调用 `logOut` 方法即可。 - - - -```cs -await TDSUser.Logout(); -``` - -```java -TDSUser.logOut(); -``` - -```objectivec -[TDSUser logOut]; -``` - -```cpp -FTDSUser::Logout(); -``` - - - -## Unity PC 登录配置 - -:::tip - -Unity SDK 自 3.5.2 起支持在 Windows、macOS 下让玩家扫码或跳转网页浏览器完成 TapTap 登录。 - -SDK **默认支持扫码登录**。 - -跳转浏览器登录需要额外配置,具体参考以下两节。 - -::: - - - -![PC 登录](https://capacity-files.lcfile.com/BGyFDAQUNUrw9EuMcx8SyP7pek4BKz5u/taptap-login-pc.png) - - - - - -![PC 登录](https://capacity-files.lcfile.com/GI0d6OOdTq4Xaphumdiayqi16JBgbmMg/taptap-login-pc.png) - - - -### Windows 平台 - -如果想要在 Windows 使用跳转网页浏览器登录功能,需要在注册表添加相应配置: - -``` -Windows Registry Editor Version 5.00 - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}] -@="{游戏名称}" -"URL Protocol"="{程序.exe 安装路径}}" - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}] -@="{游戏名称}" - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}] - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}\Shell\Open] - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}\Shell\Open\Command] -@="\"{程序.exe 安装路径}\" \"%1\"" -``` - -### macOS 平台 - -在 macOS 平台下,SDK 会自动配置 `CFBundleURLTypes`。 - -请通过以下步骤确认配置无误: - -- 在 Unity 下打开 `BuildSetting` 选择 `PC、Mac & Linux Standalone` Platform,`Target Platform` 选择 `macOS`。 -- 勾选 `Create XCode Project`,选择输出 `XCode` 工程进行编译。 -- 打开输出的 `XCode Project`,选择 `Target`,点击 `Info`,展开 `URL Types`,检查是否自动添加以下 `URL Scheme`:`TapWeb : open-taptap-{clientId}`,如未添加,则手动添加: - -```xml -CFBundleURLTypes - - - CFBundleURLName - TapWeb - CFBundleURLSchemes - - open-taptap-{client_id} - - - -``` - -## 国际化 - -TapTap 登录支持设置语言: - - - -## 时长统计 -从 TapSDK Android / iOS / Unity 3.20.0 版本 、UE4 3.21.0 版本开始,为了在 Tap 客户端中展示用户的游戏时长,TapSDK 默认会收集当前用户的时长数据,开发者可**在调用初始化接口前**使用如下接口进行设置是否收集。 - - - -<> - -```cs -using TapTap.Common; // 命名空间 - -TapCommon.SetDurationStatisticsEnabled(true); -``` - - -<> - -```java -TapCommon.setDurationStatisticsEnabled(true); -``` - - -<> - -```objectivec -[TDSCommonService setDurationStatisticsEnabled:@YES]; -``` - - - -<> - -```cpp -TapUECommon::setDurationStatisticsEnabled(true); -``` - - - - - - -## 更多功能 - -请阅读[内建账户指南](/sdk/authentication/guide/)了解内建账户系统的更多功能。 - -## 视频教程 - -可以参考视频教程:[如何在游戏中集成 TapTap 登录功能](https://www.bilibili.com/video/BV1YX4y1b7TB/),了解如何在 Untiy 项目中接入登录功能。 - -更多视频教程见[开发者学堂](https://developer.taptap.cn/tds-tutorials/list)。因为 SDK 功能在不断完善,视频教程可能出现与新版 SDK 功能不一致的地方,以当前文档为准。 \ No newline at end of file diff --git a/docs/sdk/taptap-login/guide/tap-login.mdx b/docs/sdk/taptap-login/guide/tap-login.mdx deleted file mode 100644 index 39108ac8b..000000000 --- a/docs/sdk/taptap-login/guide/tap-login.mdx +++ /dev/null @@ -1,851 +0,0 @@ ---- -title: 单纯 TapTap 用户认证 -sidebar_label: 单纯认证 -sidebar_position: 1 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import {Conditional} from '/src/docComponents/conditional'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import Profiles from "../../_partials/tap-login-profile.mdx"; - -如果你仅仅只需要接入 TapTap 这一种登录方式,确认不使用 TDS 其他云服务,可以看本篇文档。请注意,如果刚开始只选择接入「TapTap 登录」,后面又需要使用其他云服务的话,后期可能有一定的升级成本。 - - - -使用原来 TapSDK v1.x 版本的开发者,也可以参考 [1.x 升级 3.x 指南](../upgrade1.x) 完成 TapSDK 的升级。 - - - -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - - - -## 权限说明 - - - -<> - - - -<> - -该模块依赖如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下配置: - -``` - -``` - - - -<> - - - -<> - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启应用配置、绑定 API 域名; -2. 参考 [快速开始](/sdk/start/quickstart/#配置签名证书) 配置包名与签名证书; - -## SDK 获取 - -单纯接入「TapTap 登录」,需要依赖 `TapLogin` 和 `TapCommon` 模块。请先 [下载](/tap-download) TapSDK,并添加相关依赖。 - - - -<> - -SDK 可以**通过 Unity Package Manager 导入或手动导入**,二者任选其一。请根据项目需要选择。 - -#### 方法一:使用 Unity Package Manager - -从 3.29.1 版本开始, SDK 修改 JSON 解析库为 `Newtonsoft-json`,如果当前工程已接入该依赖库,则不需额外处理,否则需在 `Packages/manifest.json` 添加如下依赖: - -``` -"com.unity.nuget.newtonsoft-json":"3.2.1" -``` - - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - - - {`"dependencies":{ - "com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", -}`} - - -在 Unity 顶部菜单中选择 **Window > Package Manager** 可查看已经安装在项目中的包。 - -#### 方法二:手动导入 - -1. 在 [下载页](/tap-download) 找到 **TapSDK Unity** 下载地址,下载 `TapSDK-UnityPackage.zip`。 - -2. 在 Unity 项目中依次转到 **Assets > Import Packages > Custom Packages**,从解压后的 `TapSDK-UnityPackage.zip` 中,选择希望在游戏中使用的 TapSDK 包导入,其中: - - - `TapTap_Common.unitypackage` TapSDK 基础库,必选。 - - `TapTap_Login.unitypackage` TapTap 登录,必选。 - -3. 如果当前项目已集成 Newtonsoft.Json 依赖,则忽略该步骤,否则在 NuGet.org [ Newtonsoft.Json ](https://www.nuget.org/packages/Newtonsoft.Json) 页面中通过点击右侧 Download package 下载库文件,并将下载的文件后缀从`.nupkg` 修改为 `.zip`,同时解压该文件并复制内部的 `Newtonsoft.Json.dll` 文件拷贝到工程 `Assets` 的 `Plugins` 目录下,另外为了避免导出 IL2CPP 平台时删除必要数据,需在 `Assets` 目录下创建 link.xml 文件(如果已有该文件,则添加如下内容),其内容如下: - -``` - - - - - - -``` - -:::tip - -如果是手动下载 `unitypackage` 进行 SDK 导入,需要将 `Assets/TapTap/Common/Plugins/iOS/TapTap.Common.dll` 设置为只支持 iOS。 - -::: - -#### iOS 配置 - -在 `Assets/Plugins/iOS/Resource` 目录下创建 `TDS-Info.plist` 文件,复制以下代码并且**替换其中的 `ClientId`**。如果游戏使用了 TapTap [内嵌动态](/sdk/embedded-moments/features/)或[数据分析](/sdk/tapdb/features/)服务,需要配置相关权限并**替换授权文案**: - -:::tip - -复制使用以下内容时,**请删除空行以及注释**,以免出现 XML 解析时报错,`ApplicationException: expected a key node`。 - -::: - -```xml - - - - - taptap - - client_id - ClientId - - - NSPhotoLibraryUsageDescription - 说明为何应用需要此项权限 - NSCameraUsageDescription - 说明为何应用需要此项权限 - NSMicrophoneUsageDescription - 说明为何应用需要此项权限 - - NSUserTrackingUsageDescription - 说明为何应用需要此项权限 - - -``` - - -<> - -1. [下载 TapSDK Android](/tap-download),解压后选择需要用到的 SDK 包导入到项目 `project/app/libs` 目录下。 - -2. 打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: - - {`dependencies { - ... - // 导入 libs 目录下所有 aar 的包: - implementation fileTree(dir: 'libs', include: ['*.aar']) - // 如果需要单独导入 libs 目录下的指定包,请按照如下方式: - implementation files('libs/TapCommon_${sdkVersions.taptap.android}.aar') // TapTap 基础库 - implementation files('libs/TapLogin_${sdkVersions.taptap.android}.aar') // TapTap 登录 -} -`} - -3. 旧版 Android 额外配置 - - 如果 `targetSdkVersion < 29`,还需要添加如下配置: - - - `manifest` 节点添加 `xmlns:tools="http://schemas.android.com/tools"` - - `application` 节点添加 `tools:remove="android:requestLegacyExternalStorage"` - - -<> - -#### 导入 SDK - -1. 在 Xcode 选择工程,到 **Build Setting > Other Linker Flags** 添加 `-ObjC` 和 `-Wl -ld_classic`。 - -2. 下载 [TapSDK iOS](/tap-download),解压后选择需要导入的资源文件,直接拖拽到项目目录即可。 - -3. 视需要导入下载的资源文件: - - - 必选:TapTap 启动器、基础库、登录 - - ``` - TapBootstrapSDK.framework - TapCommonSDK.framework - TapLoginSDK.framework - LeanCloudObjc.framework - TapCommonResource.bundle - TapLoginResource.bundle - ``` - -4. 请仔细核对下面依赖库是否都添加成功: - - ``` - // 必选 - WebKit.framework - Security.framework - SystemConfiguration.framework - CoreTelephony.framework - SystemConfiguration.framework - libc++.tbd - - // TapTap 内嵌动态 - AVFoundation.framework - CoreTelephony.framework - MobileCoreServices.framework - Photos.framework - SystemConfiguration.framework - WebKit.framework - - // 数据分析 - // 如果不需要获取 IDFA 则不要添加 `AppTrackingTransparency` 和 `AdSupport` 两个系统库 - AppTrackingTransparency.framework - AdSupport.framework - CoreMotion.framework - Security.framework - SystemConfiguration.framework - libresolv.tbd - libsqlite3.0.tbd - libz.tbd - ``` - -#### 配置权限 - -如果游戏使用了 TapTap [内嵌动态](/sdk/embedded-moments/features/)或[数据分析](/sdk/tapdb/features/)服务,那么需要在 `info.plist` 配置相关权限并**替换授权文案**: - -```xml - -NSPhotoLibraryUsageDescription -说明为何应用需要此项权限 -NSCameraUsageDescription -说明为何应用需要此项权限 -NSMicrophoneUsageDescription -说明为何应用需要此项权限 - -NSUserTrackingUsageDescription -说明为何应用需要此项权限 -``` - -#### 配置跳转 TapTap 应用 - -用户无 TapTap 应用时,默认会通过 WebView 登录。 - -1. 打开 `info.plist`,添加如下配置(请替换 `clientID` 为你在控制台获取的 Client ID): - - ![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` - -2. 配置 openUrl: - - a) 如果项目中有 `SceneDelegate.m`,请先删除,然后请添加如下代码到 `AppDelegate.m` 文件: - - ```objectivec - - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [TDSHandleUrl handleOpenURL:url]; - } - - - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSHandleUrl handleOpenURL:url]; - } - ``` - - b) 删除 `info.plist` 里面的 Application Scene Manifest - - ![](/img/tap_ios_appmanifest.png) - - c) 删除 `AppDelegate.m` 文件中的两个管理 `Scenedelegate` 生命周期代理方法 - - ```objectivec - #pragma mark - UISceneSession lifecycle - - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { - - return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; - } - - - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { - } - ``` - - d) 在 `AppDelegate.h` 中添加 `UIWindow` - - ```objectivec - @property (strong, nonatomic) UIWindow *window; - ``` - - -<> - -#### 安装插件 - -* 下载 [TapSDK UE4](/tap-download),TapSDK-UE4-xxx.zip 解压后将 `TapLogin`、`TapCommon` 文件夹 Copy 到项目的 `Plugins` 目录中 -* 重启 Unreal Editor -* 打开 编辑 > 插件 > 项目 > TapTap,开启 `TapLogin` 模块 - -#### 添加依赖 - -在 Project.Build.cs 中添加所需模块: - -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapLogin" -}); -``` - -#### 导入头文件 - -```cpp -#include "TapUELogin.h" -#include "TapUECommon.h" -``` - -
    - -点击展开 iOS 配置 - -在 项目设置 > Platform > iOS > Additional Plist data 中可以填入一个字符串,复制以下代码并且替换其中的 `ClientID` 以及授权文案。 - -```xml -CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt{ClientID} - - - -LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - -``` - -
    - - - -
    - -## 初始化 - - - -:::info -游戏 [**适用地区**](/sdk/start/get-ready/#适用地区) 在开启应用配置时选定。 - -* [TapTap 开发者中心](https://developer.taptap.cn/)适用地区为中国大陆。 - -* 出海游戏请前往 [**TapTap.io 开发者中心**](https://developer.taptap.io/) 开启游戏服务,适用地区为其他国家或地区。 - -**单纯 TapTap 登录初始化时需要区分**适用于中国大陆还是其他国家或地区。 -::: - - - - -<> - - - -```cs -// 适用于中国大陆 -TapLogin.Init(string clientID); - -// 适用于其他国家或地区 -TapLogin.Init(string clientID, bool isCn, bool roundCorner); -``` - - - - - -```cs -TapLogin.Init(string clientID, bool isCn, bool roundCorner); -``` - - - -**参数说明** - -参数 | 描述 -| ------ | ------ | -clientID | TapTap 开发者中心对应应用的 Client ID -isCn | 中国大陆为 true,其他国家或地区为 false -roundCorner | 网页登录时边框是否使用圆角,使用圆⻆边框为 true,使用直⻆边框为 false - - -<> - - - -```java -// 适用于中国大陆 -TapLoginHelper.init(Context context, String clientID); - -// 适用于其他国家或地区 -LoginSdkConfig config = new LoginSdkConfig(); -config.regionType = RegionType.IO; -TapLoginHelper.init(Context context, String clientID, config); -``` - - - - - -```java -LoginSdkConfig config = new LoginSdkConfig(); -config.regionType = RegionType.IO; -TapLoginHelper.init(Context context, String clientID, config); -``` - - - -**参数说明** - -参数 | 描述 -| ------ | ------ | -context | 上下文,一般是当前 Application -clientID | TapTap 开发者中心对应应用的 Client ID - - -<> - - - -```objectivec -// 适用于中国大陆 -[TapLoginHelper initWithClientID:clientID]; - -// 适用于其他国家或地区 -TTSDKConfig *config = [[TTSDKConfig alloc] init]; -config.regionType = RegionTypeIO; -config.roundCorner = YES; -[TapLoginHelper initWithClientID:clientID config:config]; -``` - - - - - -```objectivec -TTSDKConfig *config = [[TTSDKConfig alloc] init]; -config.regionType = RegionTypeIO; -config.roundCorner = YES; -[TapLoginHelper initWithClientID:clientID config:config]; -``` - - - -**参数说明** - -参数 | 描述 -| ------ | ------ | -clientID | TapTap 开发者中心对应应用的 Client ID -regionType | 适用地区。适用于中国大陆为 `RegionTypeCN`,适用于其他国家或地区为 `RegionTypeIO` -roundCorner | 是否为圆角 - -**配置跳转 TapTap 应用** - - -用户无 TapTap 应用时,默认会打开 WebView 登录 - -打开 info.plist,添加如下配置,然后请替换 clientID 为你在控制台获取的 clientID - -```xml -CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - tt[clientID] - - - - -LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - -``` - - -如果项目中有 SceneDelegate.m,请先删除,然后添加如下代码到 AppDelegate.m 文件中。 - -```objectivec -#import -- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [TDSHandleUrl handleOpenURL:url]; -} - -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSHandleUrl handleOpenURL:url]; -} -``` - -并在 AppDelegate.h 中添加 UIWindow,然后删除 info.plist 里面的 Application Scene Manifest - -```objectivec -@property (strong, nonatomic) UIWindow *window; -``` - - - -<> - -```cpp -FTULoginConfig Config; -Config.ClientID = ClientID; -Config.RegionType = RegionType; -TapUELogin::Init(Config); -``` - -**参数说明** - -参数 | 描述 -| ------ | ------ | -clientID | TapTap 开发者中心对应应用的 Client ID -RegionType | 适用地区。适用于中国大陆为 `ERegionType::CN`,适用于其他国家或地区为 `ERegionType::Global` - - - - - - -## TapTap 登录并获取登录结果 - -:::tip -当用户启动游戏时,可以先检查[用户登录状态](#检查登录状态和用户信息),如果玩家已经登录,则不显示登录按钮,让玩家直接进入游戏。 -::: - - - - -```cs -try -{ - // 在 iOS、Android 系统下,会唤起 TapTap 客户端或以 WebView 方式进行登录 - // 在 Windows、macOS 系统下显示二维码(默认)和跳转链接(需配置) - var accessToken = await TapLogin.Login(); - Debug.Log($"TapTap 登录成功 accessToken: {accessToken.ToJson()}"); -} -catch (Exception e) -{ - if (e is TapException tapError) // using TapTap.Common - { - Debug.Log($"encounter exception:{tapError.code} message:{tapError.message}"); - if (tapError.code == (int)TapErrorCode.ERROR_CODE_BIND_CANCEL) // 取消登录 - { - Debug.Log("登录取消"); - } - } -} - -// 获取 TapTap Profile 可以获得当前用户的一些基本信息,例如名称、头像。 -var profile = await TapLogin.FetchProfile(); -Debug.Log($"TapTap 登录成功 profile: {profile.ToJson()}"); -``` - -```java -// 实例化监听 -TapLoginHelper.TapLoginResultCallback loginCallback = new TapLoginHelper.TapLoginResultCallback() { - @Override - public void onLoginSuccess(AccessToken token) { - Log.d(TAG, "TapTap authorization succeed"); - // 开发者调用 TapLoginHelper.getCurrentProfile() 可以获得当前用户的一些基本信息,例如名称、头像。 - Profile profile = TapLoginHelper.getCurrentProfile(); - } - - @Override - public void onLoginCancel() { - Log.d(TAG, "TapTap authorization cancelled"); - } - - @Override - public void onLoginError(AccountGlobalError globalError) { - Log.d(TAG, "TapTap authorization failed. cause: " + globalError.getMessage()); - } -}; -// 注册监听 -TapLoginHelper.registerLoginCallback(loginCallback); -// 登录 -TapLoginHelper.startTapLogin(MainActivity.this, TapLoginHelper.SCOPE_PUBLIC_PROFILE); -``` - -```objectivec -[TapLoginHelper registerLoginResultDelegate:delegator]; -if ([TapLoginHelper currentProfile]) { - // 当前已经登录 -} else { - [TapLoginHelper startTapLogin:@[@"public_profile"]]; -} - -// delegator -- (void)onLoginCancel { - // 登录取消 -} - -- (void)onLoginError:(nonnull NSError *)error { - // 登录失败 -} - -- (void)onLoginSuccess:(nonnull TTSDKAccessToken *)token { - // 登录成功 -} -``` - -```cpp -// 权限参考 TUType::PermissionScope -TapUELogin::Login({TUType::PermissionScope::Profile}, [](const TUAuthResult& Result) { - if (Result.GetType() == TUAuthResult::Success) { - // 登录成功 - TSharedPtr Profile = TapUELogin::GetProfile(); - } - else if (Result.GetType() == TUAuthResult::Cancel) { - // 登录取消 - } - else { - // 登录失败 - } -}); -``` - - - - - -开发者可以合理使用这些信息。 - -参见[TapTap OAuth 接口文档](/sdk/taptap-login/guide/taptap-oauth/)。 - -## 检查登录状态和用户信息 - -登录状态和用户信息存在本地缓存中,重新登录将会重置,登出将会清除。 - - - -```cs -// 获取登录状态 -try -{ - var accesstoken = await TapLogin.GetAccessToken(); - Debug.Log("已登录"); - // 直接进入游戏 -} -catch (Exception e) -{ - Debug.Log("当前未登录"); - // 开始登录 -} - -// 获取用户信息 -await TapLogin.GetProfile(); - -// 获取实时更新的用户信息 -await TapLogin.FetchProfile(); -``` - -```java -// 获取登录状态 -TapLoginHelper.getCurrentAccessToken(); - -// 获取用户信息 -TapLoginHelper.getCurrentProfile(); - -// 获取实时更新的用户信息 -TapLoginHelper.fetchProfileForCurrentAccessToken(new ApiCallback() { - @Override - public void onSuccess(Profile data) { - - } - - @Override - public void onError(Throwable error) { - - } -}); -``` - -```objectivec -// 获取登录状态 -[TapLoginHelper currentAccessToken]; - -// 获取用户信息 -[TapLoginHelper currentProfile]; - -// 获取实时更新的用户信息 -[TapLoginHelper fetchProfileForCurrentAccessToken:^(TTSDKProfile *_Nonnull profile, NSError *_Nonnull error) {}]; -``` - -```cpp -// 获取登录状态 -TSharedPtr AccessToken = TapUELogin::GetAccessToken(); -if (AccessToken.IsValid()) -{ - // 已登录,直接进入游戏 -} -else -{ - // 未登录 -} - -// 获取用户信息 -// 注意:如果登录的权限没有 TUType::Profile,那么获取不到用户信息 -TSharedPtr Profile = TapUELogin::GetProfile(); -if (Profile.IsValid()) -{ - // 获取成功 -} -else -{ - // 获取失败 -} - -// 获取实时更新的用户信息 -// 注意:如果登录的权限没有 TUType::Profile,那么获取不到用户信息 -TapUELogin::FetchProfile([](TSharedPtr ModelPtr, const FTUError& Error) { -if (ModelPtr.IsValid()) -{ - // 请求成功 -} -else -{ - // 请求失败 -} -}); -``` - - - -## 登出 - - - -```cs -TapLogin.Logout(); -``` - -```java -TapLoginHelper.logout(); -``` - -```objectivec -[TapLoginHelper logout]; -``` - -```cpp -TapUELogin::Logout(); -``` - - - -## Unity PC 登录配置 - -Unity SDK 自 3.5.2 起支持在 Windows、macOS 下让玩家扫码或跳转网页浏览器完成 TapTap 登录。 - -SDK **默认支持扫码登录**,跳转浏览器登录需要[额外配置](/sdk/taptap-login/guide/start/#unity-pc-登录配置)。 - -## 升级到内建账户系统 - -前面说过,如果前期开发时只把「TapTap 登录」作为一个第三方渠道进行了接入,后期要使用内建账户系统,或者老的 v1.x 版本的游戏要升级到 3.x 版本并使用其他服务,这时候会有「一定的开发成本」。这里我们就来具体说说这种情况下该如何处理。 - -1. 首先按照前述[初始化](#初始化)和[一键完成 TapTap 登录](#一键完成-taptap-登录)的提示,完成内建账户系统的 TapTap 用户登录,这时候开发者可以得到一个 TDSUser 实例。 - -2. 登录成功后获得当前授权用户的 `Profile` 信息。请注意,这里的 `Profile` 信息和游戏之前得到的 `Profile` 信息应该是完全一样的,游戏开发者应该可以据此找到游戏服务器上持久化保存的玩家信息,也可以将当前的 TDSUser 与原来的游戏玩家信息绑定在一起。对于游戏来说,最后是否需要将 TDSUser 与游戏内玩家账户进行绑定,是完全由开发者自己决定的: - - - 不绑定是可行的,因为 TapSDK 内部会缓存当前用户的登录状态,需要的时候总能得到之前[登录的 TDS 账户](/sdk/taptap-login/guide/start/#检查登录状态); - - 绑定带来的好处则是使用上更加简单,同时也可以将 TDS 账户信息扩展到更多的第三方平台。 diff --git a/docs/sdk/taptap-login/guide/taptap-oauth.mdx b/docs/sdk/taptap-login/guide/taptap-oauth.mdx deleted file mode 100644 index 75ad09143..000000000 --- a/docs/sdk/taptap-login/guide/taptap-oauth.mdx +++ /dev/null @@ -1,1170 +0,0 @@ ---- -title: TapTap OAuth 接口 -sidebar_position: 4 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; -import {Conditional} from '/src/docComponents/conditional'; - -## 概述 - -TapTap OpenAPI 采用统一的 Mac Token 头部签算来传递用户授权信息。 - -开发者接入 SDK [单纯 TapTap 用户认证方式](/sdk/taptap-login/guide/tap-login/),在用户授权你的应用程序后,将会生成访问令牌(Access Token)。这个访问令牌在加密后会生成 Mac Token 字符串。Access Token 长期有效,只有在用户更新其账户安全信息或解除对当前应用的授权时才会失效。开发人员应妥善管理 Access Token 在其服务器上,用作与 TapTap 服务器进行后续通讯的标识。 - -Mac Token 生成算法见文档中的 [MAC Token 算法](#mac-token-算法) 部分。 - - - -以下接口均为国内示例。当移动端初始化为海外时,登录即为海外,以下服务端文档流程不变,将示例中的请求域名 `open.tapapis.cn` 更换为海外域名 `open.tapapis.com` 即可。 - - - -## 流程 - -1. 移动端用 SDK 的 TapTap 登录,可以 [获取 AccessToken](/sdk/taptap-login/guide/tap-login/#检查登录状态和用户信息),里面包含: - - ```java - public String kid; - public String access_token; - public String token_type; - public String mac_key; - public String mac_algorithm; - public Set scopeSet; - ``` - -2. 再把移动端获取的参数发到游戏服务器,服务端签算 mac token。 -3. 请求 `https://open.tapapis.cn/account/profile/v1``https://openapi.tap.io/account/profile/v1` , header 携带 `mac token`。 - -注意:当前实际返回的 `kid` 和 `access_token` 值相等,建议使用 `kid`。 - -## API -当 SDK 只请求 basic_info 的权限时,请使用基础信息接口,请求 public_profile 时,请使用详细信息接口。 - -### 获取当前账户基础信息 - -> GET https://open.tapapis.cn/account/basic-info/v1?client_id=xxxhttps://openapi.tap.io/account/basic-info/v1?client_id=xxx
    Authorization mac token - -#### 请求参数 - -| 字段 | 类型 | 说明 | -| --------- | ------ | ------ | -| client_id | string | 该应用的 `Client ID`,应与约定相同 | - -#### 响应参数 - -字段 | 类型 | 说明 ---------------- | ------------- | ------------ -openid | string | 授权用户唯一标识,每个玩家在每个游戏中的 openid 都是不一样的,同一游戏获取同一玩家的 openid 总是相同 -unionid | string | 授权用户唯一标识,一个玩家在一个厂商的所有游戏中 unionid 都是一样的,不同厂商 unionid 不同 - -#### 请求示例 - -替换其中的 `MAC id` 和 `Client ID` 为自己签算的 mac token 和控制台的 `Client ID`。 - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://open.tapapis.cn/account/basic-info/v1?client_id=" -``` - - - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://openapi.tap.io/account/basic-info/v1?client_id=" -``` - - - - -### 获取当前账户详细信息 - - - -> GET https://open.tapapis.cn/account/profile/v1?client_id=xxxhttps://openapi.tap.io/account/profile/v1?client_id=xxx
    Authorization mac token - - -#### 请求参数 - -| 字段 | 类型 | 说明 | -| --------- | ------ | ------ | -| client_id | string | 该应用的 `Client ID`,应与约定相同 | - -#### 响应参数 - -字段 | 类型 | 说明 ---------------- | ------------- | ------------ -name | string | 用户名 -avatar | string | 用户头像图片地址 -openid | string | 授权用户唯一标识,每个玩家在每个游戏中的 openid 都是不一样的,同一游戏获取同一玩家的 openid 总是相同 -unionid | string | 授权用户唯一标识,一个玩家在一个厂商的所有游戏中 unionid 都是一样的,不同厂商 unionid 不同 - -#### 请求示例 - -替换其中的 `MAC id` 和 `Client ID` 为自己签算的 mac token 和控制台的 `Client ID`。 - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://open.tapapis.cn/account/profile/v1?client_id=" -``` - - - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://openapi.tap.io/account/profile/v1?client_id=" -``` - - - -## 其他 - -### MAC Token 算法 - -MAC Token 包含以下字段: - -| 字段 | 类型 | 说明 | -| ------------- | ------ | ------------------------------- | -| kid | string | mac_key id, The key identifier. | -| access_token | string | 该字段暂无作用 | -| token_type | string | Token 类型,如 mac | -| mac_key | string | mac 密钥 | -| mac_algorithm | string | mac 计算的算法名称 hmac-sha-1 | - -使用 Mac Token 签算一个接口: - - -
    -Node.js 请求示例 - - - -```javascript -const http = require('http'); -const https = require('https'); -const crypto = require('crypto'); - -function getAuthorization(requestUrl, method, keyId, macKey) { - const url = new URL(requestUrl); - const time = Math.floor(Date.now() / 1000).toString().padStart(10, '0'); - const randomStr = getRandomString(16); - const host = url.hostname; - const uri = url.pathname + url.search; - const port = url.port || (url.protocol === 'https:' ? '443' : '80'); - const other = ''; - const sign = signData(mergeData(time, randomStr, method, uri, host, port, other), macKey); - - return `MAC id="${keyId}", ts="${time}", nonce="${randomStr}", mac="${sign}"`; -} - -function getRandomString(length) { - return crypto.randomBytes(length).toString('base64'); -} - -function mergeData(time, randomCode, httpType, uri, domain, port, other) { - let prefix = - `${time}\n${randomCode}\n${httpType}\n${uri}\n${domain}\n${port}\n`; - - if (!other) { - prefix += '\n'; - } else { - prefix += `${other}\n`; - } - - return prefix; -} - -function signData(signatureBaseString, key) { - const hmac = crypto.createHmac('sha1', key); - hmac.update(signatureBaseString); - return hmac.digest('base64'); -} - -const client_id = "hskc**********kklm"; -const keyId = "1/VLDoiGUhNCIpUq827L**************zAJ-i8hT_w9vuPtPgdaPkWDv6K4eVe_yZnKz************EYep-T4ki5w3kyYACVnM61JJqDEKfpNnHoTZU********************iUArkgPsWEwOpZGxva7FnqbTwmpLT0a28UtiR5gyr4XXutbnE5tb4A-iSqRpqqtgABXBZd34U5Th3iJ1C666iYQFvuQL9uC-Zv7-xKCNjyPonBqU4ZWZnKLFf2mzprU5vJCA8q5by1SZxY63kZBQieHYxFjyOCQdJ-25gDlxiqDbNq08kmSdY6TB1qtQ68V37L6a8nIzyVHooX9uc2Yw"; -const macKey = 'VPDalRmxtBqi******************tH937GNKIvj3'; -const requestUrl = 'https://open.tapapis.cn/account/profile/v1?client_id='+ client_id ; -const method = 'GET'; - - -const authorization = getAuthorization(requestUrl, method, keyId, macKey); -console.log(authorization); - -const options = new URL(requestUrl); -const client = options.protocol === 'https:' ? https : http; - -const req = client.request({ - hostname: options.hostname, - port: options.port, - path: options.pathname + options.search, - method: 'GET', - headers: { - 'Authorization': authorization - } -}, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - console.log(data); - }); -}); - -req.end(); - - -``` - - - - - -```javascript -const http = require('http'); -const https = require('https'); -const crypto = require('crypto'); - -function getAuthorization(requestUrl, method, keyId, macKey) { - const url = new URL(requestUrl); - const time = Math.floor(Date.now() / 1000).toString().padStart(10, '0'); - const randomStr = getRandomString(16); - const host = url.hostname; - const uri = url.pathname + url.search; - const port = url.port || (url.protocol === 'https:' ? '443' : '80'); - const other = ''; - const sign = signData(mergeData(time, randomStr, method, uri, host, port, other), macKey); - - return `MAC id="${keyId}", ts="${time}", nonce="${randomStr}", mac="${sign}"`; -} - -function getRandomString(length) { - return crypto.randomBytes(length).toString('base64'); -} - -function mergeData(time, randomCode, httpType, uri, domain, port, other) { - let prefix = - `${time}\n${randomCode}\n${httpType}\n${uri}\n${domain}\n${port}\n`; - - if (!other) { - prefix += '\n'; - } else { - prefix += `${other}\n`; - } - - return prefix; -} - -function signData(signatureBaseString, key) { - const hmac = crypto.createHmac('sha1', key); - hmac.update(signatureBaseString); - return hmac.digest('base64'); -} - -const client_id = "5enu******wfy"; -const keyId = "1/JFZi8****IiumsGZI31iJH1q*****UKZ-eKA"; -const macKey = 'LMbNcKox*******kfmk7oWXbuRz'; -const requestUrl = 'https://openapi.tap.io/account/profile/v1?client_id='+ client_id ; -const method = 'GET'; - -const authorization = getAuthorization(requestUrl, method, keyId, macKey); -console.log(authorization); - -const options = new URL(requestUrl); -const client = options.protocol === 'https:' ? https : http; - -const req = client.request({ - hostname: options.hostname, - port: options.port, - path: options.pathname + options.search, - method: 'GET', - headers: { - 'Authorization': authorization - } -}, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - console.log(data); - }); -}); - -req.end(); - - -``` - - - -
    - - -
    -Java 请求示例 - - - -```java -package com.taptap; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.*; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -public class Authorization { - public static void main(String[] args) throws IOException { - String client_id = "0RiAlMny7jiz086FaU"; - String kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"; // kid - String mac_key = "mSUQNYUGRBPXyRyW"; // mac_key - String method = "GET"; - String request_url = "https://open.tapapis.cn/account/profile/v1?client_id=" + client_id; // - String authorization = getAuthorization(request_url, method, kid, mac_key); - System.out.println(authorization); - URL url = new URL(request_url); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - // Http - conn.setRequestProperty("Authorization", authorization); - conn.setRequestMethod("GET"); - BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String line; - StringBuilder result = new StringBuilder(); - while ((line = rd.readLine()) != null) { - result.append(line); - } - rd.close(); - System.out.println(result.toString()); - } - /** - * @param request_url - * @param method "GET" or "POST" - * @param key_id key id by OAuth 2.0 - * @param mac_key mac key by OAuth 2.0 - * @return authorization string - */ - public static String getAuthorization(String request_url, String method, String key_id, String - mac_key) { - try { - URL url = new URL(request_url); - String time = String.format(Locale.US, "%010d", System.currentTimeMillis() / 1000); - String randomStr = getRandomString(16); - String host = url.getHost(); - String uri = request_url.substring(request_url.lastIndexOf(host) + host.length()); - String port = "80"; - if (request_url.startsWith("https")) { - port = "443"; - } - String other = ""; - String sign = sign(mergeSign(time, randomStr, method, uri, host, port, other), mac_key); - return "MAC " + getAuthorizationParam("id", key_id) + "," + getAuthorizationParam("ts", time) - + "," + getAuthorizationParam("nonce", randomStr) + "," + getAuthorizationParam("mac", - sign); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - return null; - } - private static String getRandomString(int length) { - byte[] bytes = new byte[length]; - new SecureRandom().nextBytes(bytes); - String base64String = Base64.getEncoder().encodeToString(bytes); - return base64String; - } - private static String mergeSign(String time, String randomCode, String httpType, String uri, - String domain, String port, String other) { - if (time.isEmpty() || randomCode.isEmpty() || httpType.isEmpty() || domain.isEmpty() || port.isEmpty()) - { - return null; - } - String prefix = - time + "\n" + randomCode + "\n" + httpType + "\n" + uri + "\n" + domain + "\n" + port - + "\n"; - if (other.isEmpty()) { - prefix += "\n"; - } else { - prefix += (other + "\n"); - } - return prefix; - } - private static String sign(String signatureBaseString, String key) { - try { - SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(signingKey); - byte[] text = signatureBaseString.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = mac.doFinal(text); - signatureBytes = Base64.getEncoder().encode(signatureBytes); - return new String(signatureBytes, StandardCharsets.UTF_8); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new IllegalStateException(e); - } - } - private static String getAuthorizationParam(String key, String value) { - if (key.isEmpty() || value.isEmpty()) { - return null; - } - return key + "=" + "\"" + value + "\""; - } -} -``` - - - - - -```java -package com.taptap; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.*; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -public class Authorization { - public static void main(String[] args) throws IOException { - String client_id = "0RiAlMny7jiz086FaU"; - String kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"; // kid - String mac_key = "mSUQNYUGRBPXyRyW"; // mac_key - String method = "GET"; - String request_url = "https://openapi.tap.io/account/profile/v1?client_id=" + client_id; // - String authorization = getAuthorization(request_url, method, kid, mac_key); - System.out.println(authorization); - URL url = new URL(request_url); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - // Http - conn.setRequestProperty("Authorization", authorization); - conn.setRequestMethod("GET"); - BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String line; - StringBuilder result = new StringBuilder(); - while ((line = rd.readLine()) != null) { - result.append(line); - } - rd.close(); - System.out.println(result.toString()); - } - /** - * @param request_url - * @param method "GET" or "POST" - * @param key_id key id by OAuth 2.0 - * @param mac_key mac key by OAuth 2.0 - * @return authorization string - */ - public static String getAuthorization(String request_url, String method, String key_id, String - mac_key) { - try { - URL url = new URL(request_url); - String time = String.format(Locale.US, "%010d", System.currentTimeMillis() / 1000); - String randomStr = getRandomString(16); - String host = url.getHost(); - String uri = request_url.substring(request_url.lastIndexOf(host) + host.length()); - String port = "80"; - if (request_url.startsWith("https")) { - port = "443"; - } - String other = ""; - String sign = sign(mergeSign(time, randomStr, method, uri, host, port, other), mac_key); - return "MAC " + getAuthorizationParam("id", key_id) + "," + getAuthorizationParam("ts", time) - + "," + getAuthorizationParam("nonce", randomStr) + "," + getAuthorizationParam("mac", - sign); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - return null; - } - private static String getRandomString(int length) { - byte[] bytes = new byte[length]; - new SecureRandom().nextBytes(bytes); - String base64String = Base64.getEncoder().encodeToString(bytes); - return base64String; - } - private static String mergeSign(String time, String randomCode, String httpType, String uri, - String domain, String port, String other) { - if (time.isEmpty() || randomCode.isEmpty() || httpType.isEmpty() || domain.isEmpty() || port.isEmpty()) - { - return null; - } - String prefix = - time + "\n" + randomCode + "\n" + httpType + "\n" + uri + "\n" + domain + "\n" + port - + "\n"; - if (other.isEmpty()) { - prefix += "\n"; - } else { - prefix += (other + "\n"); - } - return prefix; - } - private static String sign(String signatureBaseString, String key) { - try { - SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(signingKey); - byte[] text = signatureBaseString.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = mac.doFinal(text); - signatureBytes = Base64.getEncoder().encode(signatureBytes); - return new String(signatureBytes, StandardCharsets.UTF_8); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new IllegalStateException(e); - } - } - private static String getAuthorizationParam(String key, String value) { - if (key.isEmpty() || value.isEmpty()) { - return null; - } - return key + "=" + "\"" + value + "\""; - } -} -``` - - - -
    - -
    - -PHP 请求示例 - - - -```php - -``` - - - - -```php - -``` - - -
    - -
    - -Python3 请求示例 - - - -```python -import base64 -import hmac -import random -import string -import time -from hashlib import sha1 - - -def get_mac_token_signature(host, request_url, method, mac_key, kid): - mac_token_pattern = 'MAC id="{kid}",ts="{ts}",nonce="{nonce}",mac="{mac}"' - timestamp = str(int(time.time())) - nonce = ''.join(random.choices(string.ascii_lowercase + string.ascii_uppercase, k=16)) - sign_array = [timestamp, nonce, method, request_url, host, '443', ''] - seperator = '\n' - sign_input = seperator.join(sign_array) + seperator - hmac_code = hmac.new(mac_key.encode('UTF-8'), sign_input.encode('UTF-8'), sha1) - mac_str = base64.b64encode(hmac_code.digest()).decode('UTF-8') - return mac_token_pattern.format(kid=kid, ts=timestamp, nonce=nonce, - mac=mac_str) - -if __name__ == '__main__': - kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" - mac_key = "mSUQNYUGRBPXyRyW" - client_id = "0RiAlMny7jiz086FaU" - signature = get_mac_token_signature('open.tapapis.cn', '/account/profile/v1?client_id=' + client_id, - 'GET', mac_key, kid) - print(signature) -``` - - - - -```python -import base64 -import hmac -import random -import string -import time -from hashlib import sha1 - - -def get_mac_token_signature(host, request_url, method, mac_key, kid): - mac_token_pattern = 'MAC id="{kid}",ts="{ts}",nonce="{nonce}",mac="{mac}"' - timestamp = str(int(time.time())) - nonce = ''.join(random.choices(string.ascii_lowercase + string.ascii_uppercase, k=16)) - sign_array = [timestamp, nonce, method, request_url, host, '443', ''] - seperator = '\n' - sign_input = seperator.join(sign_array) + seperator - hmac_code = hmac.new(mac_key.encode('UTF-8'), sign_input.encode('UTF-8'), sha1) - mac_str = base64.b64encode(hmac_code.digest()).decode('UTF-8') - return mac_token_pattern.format(kid=kid, ts=timestamp, nonce=nonce, - mac=mac_str) - -if __name__ == '__main__': - kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" - mac_key = "mSUQNYUGRBPXyRyW" - client_id = "0RiAlMny7jiz086FaU" - signature = get_mac_token_signature('openapi.tap.io', '/account/profile/v1?client_id=' + client_id, - 'GET', mac_key, kid) - print(signature) -``` - - -
    - -
    - -Go 请求示例 - - - -```go -package main - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "fmt" - "io" - "net/http" - "strconv" - "time" -) - -func main() { - // 替换 clientId、accessToken、macKey 参数 - // clientId 参数在 TapDC 后台查看 - clientId := "请替换为控制台的 `Client ID`" - // TapTap 登录成功后 TapSDK 返回的 access_token - accessToken := "1/jvsVxFC6-PIUiXvZVVtv1hogX5q9Z1y_rp-AtVjE3iyHikXXfd_2h-i0wLmc9UjLJwhH6fQ8cvGrklONdvy2J5YfoqzV0ewGPMSLkQIkRv_xaLaYPariWbrkP1MtG2b4CzR1KHvuSCJHewCmTFZmsyNGojTJr5t75f5Nc8j-jjCYeDtFO0-XFI_J7kzktswzzsmISt7cx49QVess-VbaQcU31pEDb_OA03I28H5ehIvqQ0CQdf1LieLyONcH97l1IEU39AirioF_KGJccVG64QsgWmzxLPwmfTurw4cwBPo04yuXnas4YI5haE2UxtckNCpagP19drtGW57-HaAdww" - // TapTap 登录成功后 TapSDK 返回的 mac_key - macKey := "fTCuDUDDmNny7a36EWbhUDLaqpoDMQu2hCi9qAJ5" - - // 随机数,正式上线请替换 - nonce := "8IBTHwOdqNKAWeKl7plt66==" - // 时间戳转换成字符串 - timestamp := strconv.FormatInt(time.Now().Unix(), 10) - // 请求 url 相关 - reqHost := "open.tapapis.cn" - reqURI := "/account/profile/v1?client_id=" + clientId - reqURL := "https://" + reqHost + reqURI - - macStr := timestamp + "\n" + nonce + "\n" + "GET" + "\n" + reqURI + "\n" + reqHost + "\n" + "443" + "\n\n" - mac := hmacSha1(macStr, macKey) - authorization := "MAC id=" + "\"" + accessToken + "\"" + "," + "ts=" + "\"" + timestamp + "\"" + "," + "nonce=" + "\"" + nonce + "\"" + "," + "mac=" + "\"" + mac + "\"" - - client := http.Client{} - req, err := http.NewRequest(http.MethodGet, reqURL, nil) - if err != nil { - fmt.Println(err.Error()) - return - } - - // 添加请求头 - req.Header.Add("Authorization", authorization) - // 发送请求 - resp, err := client.Do(req) - if err != nil { - fmt.Println(err.Error()) - return - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Println(err.Error()) - return - } - fmt.Println(string(respBody)) -} - -/* -HMAC-SHA1 签名 -*/ -func hmacSha1(valStr, keyStr string) string { - key := []byte(keyStr) - mac := hmac.New(sha1.New, key) - mac.Write([]byte(valStr)) - - // 进行 Base64 编码 - return base64.StdEncoding.EncodeToString(mac.Sum(nil)) -} -``` - - - - -```go -package main - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "fmt" - "io" - "net/http" - "strconv" - "time" -) - -func main() { - // 替换 clientId、accessToken、macKey 参数 - // clientId 参数在 TapDC 后台查看 - clientId := "请替换为控制台的 `Client ID`" - // TapTap 登录成功后 TapSDK 返回的 access_token - accessToken := "1/jvsVxFC6-PIUiXvZVVtv1hogX5q9Z1y_rp-AtVjE3iyHikXXfd_2h-i0wLmc9UjLJwhH6fQ8cvGrklONdvy2J5YfoqzV0ewGPMSLkQIkRv_xaLaYPariWbrkP1MtG2b4CzR1KHvuSCJHewCmTFZmsyNGojTJr5t75f5Nc8j-jjCYeDtFO0-XFI_J7kzktswzzsmISt7cx49QVess-VbaQcU31pEDb_OA03I28H5ehIvqQ0CQdf1LieLyONcH97l1IEU39AirioF_KGJccVG64QsgWmzxLPwmfTurw4cwBPo04yuXnas4YI5haE2UxtckNCpagP19drtGW57-HaAdww" - // TapTap 登录成功后 TapSDK 返回的 mac_key - macKey := "fTCuDUDDmNny7a36EWbhUDLaqpoDMQu2hCi9qAJ5" - - // 随机数,正式上线请替换 - nonce := "8IBTHwOdqNKAWeKl7plt66==" - // 时间戳转换成字符串 - timestamp := strconv.FormatInt(time.Now().Unix(), 10) - // 请求 url 相关 - reqHost := "openapi.tap.io" - reqURI := "/account/profile/v1?client_id=" + clientId - reqURL := "https://" + reqHost + reqURI - - macStr := timestamp + "\n" + nonce + "\n" + "GET" + "\n" + reqURI + "\n" + reqHost + "\n" + "443" + "\n\n" - mac := hmacSha1(macStr, macKey) - authorization := "MAC id=" + "\"" + accessToken + "\"" + "," + "ts=" + "\"" + timestamp + "\"" + "," + "nonce=" + "\"" + nonce + "\"" + "," + "mac=" + "\"" + mac + "\"" - - client := http.Client{} - req, err := http.NewRequest(http.MethodGet, reqURL, nil) - if err != nil { - fmt.Println(err.Error()) - return - } - - // 添加请求头 - req.Header.Add("Authorization", authorization) - // 发送请求 - resp, err := client.Do(req) - if err != nil { - fmt.Println(err.Error()) - return - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Println(err.Error()) - return - } - fmt.Println(string(respBody)) -} - -/* -HMAC-SHA1 签名 -*/ -func hmacSha1(valStr, keyStr string) string { - key := []byte(keyStr) - mac := hmac.New(sha1.New, key) - mac.Write([]byte(valStr)) - - // 进行 Base64 编码 - return base64.StdEncoding.EncodeToString(mac.Sum(nil)) -} -``` - - -
    - -
    -C# 请求示例 - - - -```cs -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -using System; -using System.Net.Http; -using System.Threading.Tasks; -using System.Text; - -public class TapLoginOAuth : MonoBehaviour -{ - - private string host = "open.tapapis.cn"; - private string clientId = "应用的 Client id"; - private string macKey = "客户端登录返回的 macKey 值"; - private string kid = "客户端登录返回的 kid/access_token 值"; - - - // Start is called before the first frame update - void Start() - { - // 发起网络请求 - StartCoroutine(SendRequest()); - } - - // Update is called once per frame - void Update() - { - - } - - IEnumerator SendRequest() - { - // 构建请求 URL - string requestUrl = $"/account/profile/v1?client_id={clientId}"; - - // 生成签名 - string signature = GetMacTokenSignature(host, requestUrl, "GET", macKey, kid); - Debug.Log("Generated Signature: " + signature); - - // 发送 GET 请求到服务器 - using (var httpClient = new HttpClient()) - { - var uri = new Uri($"https://{host}{requestUrl}"); - var request = new HttpRequestMessage(HttpMethod.Get, uri); - request.Headers.Add("Authorization", signature); - - // 发送请求并等待响应 - var response = httpClient.SendAsync(request).Result; - var responseBody = response.Content.ReadAsStringAsync().Result; - - // 输出服务器响应 - Debug.Log("Server Response: " + responseBody); - } - - yield return null; - } - string GetMacTokenSignature(string host, string requestUrl, string method, string macKey, string kid) - { - string macTokenPattern = "MAC id=\"{0}\",ts=\"{1}\",nonce=\"{2}\",mac=\"{3}\""; - string timestamp = ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString(); - string nonce = GenerateNonce(16); - string[] signArray = { timestamp, nonce, method, requestUrl, host, "443", "" }; - string separator = "\n"; - string signInput = string.Join(separator, signArray) + separator; - - using (var hmac = new System.Security.Cryptography.HMACSHA1(Encoding.UTF8.GetBytes(macKey))) - { - byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signInput)); - string macStr = Convert.ToBase64String(hash); - return string.Format(macTokenPattern, kid, timestamp, nonce, macStr); - } - } - - string GenerateNonce(int length) - { - const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - var random = new System.Random(); - var nonce = new char[length]; - - for (int i = 0; i < length; i++) - { - nonce[i] = chars[random.Next(chars.Length)]; - } - - return new string(nonce); - } -} - -``` - - - - - -```cs - -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -using System; -using System.Net.Http; -using System.Threading.Tasks; -using System.Text; - -public class TapLoginOAuth : MonoBehaviour -{ - private string host = "openapi.tap.io"; - private string clientId = "应用的 Client id"; - private string macKey = "客户端登录返回的 macKey 值"; - private string kid = "客户端登录返回的 kid/access_token 值"; - - - // Start is called before the first frame update - void Start() - { - // 发起网络请求 - StartCoroutine(SendRequest()); - } - - // Update is called once per frame - void Update() - { - - } - - IEnumerator SendRequest() - { - // 构建请求 URL - string requestUrl = $"/account/profile/v1?client_id={clientId}"; - - // 生成签名 - string signature = GetMacTokenSignature(host, requestUrl, "GET", macKey, kid); - Debug.Log("Generated Signature: " + signature); - - // 发送 GET 请求到服务器 - using (var httpClient = new HttpClient()) - { - var uri = new Uri($"https://{host}{requestUrl}"); - var request = new HttpRequestMessage(HttpMethod.Get, uri); - request.Headers.Add("Authorization", signature); - - // 发送请求并等待响应 - var response = httpClient.SendAsync(request).Result; - var responseBody = response.Content.ReadAsStringAsync().Result; - - // 输出服务器响应 - Debug.Log("Server Response: " + responseBody); - } - - yield return null; - } - string GetMacTokenSignature(string host, string requestUrl, string method, string macKey, string kid) - { - string macTokenPattern = "MAC id=\"{0}\",ts=\"{1}\",nonce=\"{2}\",mac=\"{3}\""; - string timestamp = ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString(); - string nonce = GenerateNonce(16); - string[] signArray = { timestamp, nonce, method, requestUrl, host, "443", "" }; - string separator = "\n"; - string signInput = string.Join(separator, signArray) + separator; - - using (var hmac = new System.Security.Cryptography.HMACSHA1(Encoding.UTF8.GetBytes(macKey))) - { - byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signInput)); - string macStr = Convert.ToBase64String(hash); - return string.Format(macTokenPattern, kid, timestamp, nonce, macStr); - } - } - - string GenerateNonce(int length) - { - const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - var random = new System.Random(); - var nonce = new char[length]; - - for (int i = 0; i < length; i++) - { - nonce[i] = chars[random.Next(chars.Length)]; - } - - return new string(nonce); - } -} - -``` - - -
    - -
    -脚本请求示例 - -可用此脚本验证直接替换参数,用来验证自己服务端签算的 mac token 是否正确。 - -CLIENT_ID 替换为控制台获取的 `Client ID`,ACCESS_TOKEN 和 MAC_KEY 为客户端登录成功后的 `access_token`、`mac_key`: - - - -``` -#!/usr/bin/env bash - -# 客户端 ID -CLIENT_ID="请替换为控制台的 `Client ID`" -# SDK 获取的 access_token -ACCESS_TOKEN="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" -# SDK 获取的 mac_key -MAC_KEY="mSUQNYUGRBPXyRyW" - -# 随机数,正式上线请替换 -NONCE="8IBTHwOdqNKAWeKl7plt8g==" -# 当前时间戳 -TS=$(date +%s) - -# 请求方法 -METHOD="GET" -# 请求地址 (带 query string) -REQUEST_URI="/account/profile/v1?client_id=${CLIENT_ID}" -# 请求域名 -REQUEST_HOST="open.tapapis.cn" - -MAC=$(printf "%s\n%s\n%s\n%s\n%s\n443\n\n" "${TS}" "${NONCE}" "${METHOD}" "${REQUEST_URI}" "${REQUEST_HOST}" | openssl dgst -binary -sha1 -hmac ${MAC_KEY} | base64) - -AUTHORIZATION=$(printf 'MAC id="%s",ts="%s",nonce="%s",mac="%s"' "${ACCESS_TOKEN}" "${TS}" "${NONCE}" "${MAC}") - -curl -s -H"Authorization:${AUTHORIZATION}" "https://${REQUEST_HOST}${REQUEST_URI}" -``` - - - - - - - -``` -#!/usr/bin/env bash - -# 客户端 ID -CLIENT_ID="请替换为控制台的 `Client ID`" -# SDK 获取的 access_token -ACCESS_TOKEN="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" -# SDK 获取的 mac_key -MAC_KEY="mSUQNYUGRBPXyRyW" - -# 随机数,正式上线请替换 -NONCE="8IBTHwOdqNKAWeKl7plt8g==" -# 当前时间戳 -TS=$(date +%s) - -# 请求方法 -METHOD="GET" -# 请求地址 (带 query string) -REQUEST_URI="/account/profile/v1?client_id=${CLIENT_ID}" -# 请求域名 -REQUEST_HOST="openapi.tap.io" - -MAC=$(printf "%s\n%s\n%s\n%s\n%s\n443\n\n" "${TS}" "${NONCE}" "${METHOD}" "${REQUEST_URI}" "${REQUEST_HOST}" | openssl dgst -binary -sha1 -hmac ${MAC_KEY} | base64) - -AUTHORIZATION=$(printf 'MAC id="%s",ts="%s",nonce="%s",mac="%s"' "${ACCESS_TOKEN}" "${TS}" "${NONCE}" "${MAC}") - -curl -s -H"Authorization:${AUTHORIZATION}" "https://${REQUEST_HOST}${REQUEST_URI}" -``` - - - -
    - -### 通用接口错误信息 - -**统一格式** - -| 字段 | 类型 | 说明 | -| ----------------- | ------ | ---------------------------------------------------- | -| code | int | 预留字段,用于以后追踪问题 | -| error | string | 错误码,代码逻辑判断时使用 | -| error_description | string | 错误描述信息,开发的时候用来帮助理解和解决发生的错误 | - - -**错误响应** - -| 错误码 | 详细描述 | -| ------------------| ------------------------------------------------------------ | -| invalid_request | 请求缺少某个必需参数,包含一个不支持的参数或参数值,或者格式不正确 | -| invalid_time | MAC Token 算法中,ts 时间不合法,**应请求服务器时间重新构造** | -| invalid_client | client_id 参数无效 | -| access_denied | 授权服务器拒绝请求 **这个状态出现在拿着 token 请求用户资源时,如出现,客户端应退出本地的用户登录信息,引导用户重新登录** | -| forbidden | 用户没有对当前动作的权限,**引导重新身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交** | -| not_found | 请求失败,请求所希望得到的资源未被在服务器上发现。**在参数相同的情况下,不应该重复请求** | -| server_error | 服务器出现异常情况 **可稍等后重新尝试请求,但需有尝试上限,建议最多 3 次,如一直失败,则中断并告知用户** | - diff --git a/docs/sdk/taptap-login/guide/upgrade1.x.mdx b/docs/sdk/taptap-login/guide/upgrade1.x.mdx deleted file mode 100644 index 1cdf79f75..000000000 --- a/docs/sdk/taptap-login/guide/upgrade1.x.mdx +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: 1.x 升级 3.x 指南 -sidebar_label: 1.x 升级 3.x -sidebar_position: 2 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - -因新版接口与旧版接口有明显差别,所以需将旧版接口删除,通过如下方式重新接入以便使用新版本接口。 - -## TapSDK 3.x 版本的导入 - -去除 1.x 方式导入的 SDK 相关包和文件,引入 3.x SDK,详情请参考[项目配置](/sdk/start/quickstart/#项目配置)。 - -## 初始化 - -该接口应尽早并在调用 TapSDK 的其他接口前调用,示例如下: - - -<> - -```cs -TapLogin.Init(string clientID); -``` - -**参数说明** - -参数 | 描述 -| ------ | ------ | -clientID | TapTap 开发者中心对应应用的 Client ID - - -<> - -```java -LoginSdkConfig loginSdkConfig = new LoginSdkConfig(true, true, RegionType.CN); -TapLoginHelper.init(Context context, String clientID, LoginSdkConfig config); -``` - -**参数说明** - -参数 | 描述 -| ------ | ------ | -context | 上下文 -clientID | TapTap 开发者中心对应应用的 Client ID -config | TapSDK 初始化相关配置 - - -<> - -```objectivec -[TapLoginHelper initWithClientID:@"your clientId"]; -``` -**参数说明** - -参数 | 描述 -| ------ | ------ | -clientId | TapTap 开发者中心对应应用的 Client ID - - - - - - - -## TapTap 登录并获取登录结果 - - - - -```cs -// 唤起 TapTap 网页 或者 TapTap 客户端进行登录 -try{ - //登录成功 - var loginTask = TapLogin.Login(); - var accessToken = await loginTask; - if(loginTask.IsCanceled){ - // 登录取消 - } - if(accessToken !=null){ - // 登录成功 - } -}catch(Exception e){ - //TODO 登录失败 - if(e is TapException tapError){ - Debug.Log($"code:{tapError.code} message:{tapError.message}") - } -} -// 获取 TapTap Profile 可以获得当前用户的一些基本信息,例如名称、头像。 -var profile = await TapLogin.FetchProfile(); -// 补充: 通过 profile 获取用户的:昵称、头像、唯一标识 查看 -profile.name -profile.avatar -profile.openId -profile.unionid -``` - -```java -TapLoginHelper.TapLoginResultCallback loginCallback = new TapLoginHelper.TapLoginResultCallback() { - @Override - public void onLoginSuccess(AccessToken token) { - Log.d(TAG, "TapTap authorization succeed"); - // 开发者调用 TapLoginHelper.getCurrentProfile() 可以获得当前用户的一些基本信息,例如名称、头像。 - Profile profile = TapLoginHelper.getCurrentProfile(); - } - - @Override - public void onLoginCancel() { - Log.d(TAG, "TapTap authorization cancelled"); - } - - @Override - public void onLoginError(AccountGlobalError globalError) { - Log.d(TAG, "TapTap authorization failed. cause: " + globalError.getMessage()); - } -}; -TapLoginHelper.registerLoginCallback(loginCallback); -TapLoginHelper.startTapLogin(MainActivity.this, TapLoginHelper.SCOPE_PUBLIC_PROFILE); -``` - -```objectivec -[TapLoginHelper registerLoginResultDelegate:delegator]; -if ([TapLoginHelper currentProfile]) { - // 当前已经登录 -} else { - [TapLoginHelper startTapLogin:@[@"public_profile"]]; -} - -// delegator -- (void)onLoginCancel { - // 登录取消 -} - -- (void)onLoginError:(nonnull NSError *)error { - // 登录失败 -} - -- (void)onLoginSuccess:(nonnull TTSDKAccessToken *)token { - // 登录成功 -} -``` - - \ No newline at end of file diff --git a/docs/sdk/taptap-login/guide/upgrade2.x.mdx b/docs/sdk/taptap-login/guide/upgrade2.x.mdx deleted file mode 100644 index 0fb2dbd6f..000000000 --- a/docs/sdk/taptap-login/guide/upgrade2.x.mdx +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: 2.x 升级 3.x 指南 -sidebar_label: 2.x 升级 3.x -sidebar_position: 3 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - -因新版接口与旧版接口有明显差别,所以需将旧版接口删除,通过如下方式重新接入以便使用新版本接口。 - -## TapSDK 3.x 版本的导入 - -去除 2.x 方式导入的 SDK 相关包和文件,引入 3.x SDK,详情请参考[项目配置](/sdk/start/quickstart/#项目配置)。 - -## 初始化 - -TapSDK 3.x 的初始化方法较 TapSDK 2.x 只多了一个参数的设置,即 ServerUrl,具体初始化方式请参考[初始化参数](/sdk/start/quickstart/#初始化)。 - - -## TapTap 登录并获取登录结果 - -TapSDK 2.x 获取用户信息后会得到用户唯一标识 `userID`,该唯一标识和 TapSDK 3.x 授权后得到的 `userID` 是一样的。 - -### TapSDK 2.x 版本登录后获取用户信息方式 - -获取当前登录用户的 `userID`、昵称、头像等基本信息。 - - - -```cs -TapBootstrap.GetUser((user, error) => { - Debug.Log(user.ToJSON()); // 获取用户唯一标识 userID -}); -``` - -```java -TapBootstrap.getUser(new Callback() { - @Override - public void onSuccess(TapUser tapUser) { - String userID = tapUser.userId; // 获取用户唯一标识 userID - } - - @Override - public void onFail(TapError tapError) { - - } -}); -``` - -```objectivec -[TapBootstrap getUser:^(TapUser * _Nullable userInfo, NSError * _Nullable error) { - if (error) { - NSLog(@"获取用户信息失败 %@", error); - } else { - NSLog(@"获取用户信息成功 %@", userInfo); // 获取用户唯一标识 userID - } -}]; -``` - - - -### TapSDK 3.x 版本登录后获取用户信息方式 - -`TDSUser` 即是当前玩家的账户系统,登录成功之后开发者可以: - -- 通过访问 `nickname` 属性来获得 TapTap 账户的用户名; -- 通过访问 `avatar` 属性来获得 TapTap 账户的头像; -- 通过访问 `objectId` 来得到该账户系统的 `userID`,该 `userID` 和 TapSDK 2.x 版本授权后得到的 `userID` 是一样的;该 `userID` 可用于游戏服务器内玩家与 TDS 内建账户的绑定或匹配。 - - - -```cs -try -{ - var tdsUser = await TDSUser.LoginWithTapTap(); // 通过访问 `objectId` 来得到该账户系统的 userID - Debug.Log($"login sucess:{tdsUser}"); -} -catch (Exception e) -{ - if (e is TapException tapError) // using TapTap.Common - { - Debug.Log($"encounter exception:{tapError.code} message:{tapError.message}"); - if (tapError.code == TapErrorCode.ERROR_CODE_BIND_CANCEL) // 取消登录 - { - Debug.Log("登录取消"); - } - } -} -``` - -```java -TDSUser.loginWithTapTap(MainActivity.this, new Callback() { - @Override - public void onSuccess(TDSUser resultUser) { - Toast.makeText(MainActivity.this, "succeed to login with Taptap.", Toast.LENGTH_SHORT).show(); - // 开发者可以调用 resultUser 的方法获取更多属性。 - String userId = resultUser.getObjectId(); // 用户唯一标识 userID - String avatar = (String) resultUser.get("avatar"); // 头像 - String nickName = (String) resultUser.get("nickname"); // 昵称 - } - - @Override - public void onFail(TapError error) { - Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show(); - } -}, "public_profile"); -``` - -```objectivec -[TDSUser loginByTapTapWithPermissions:@[@"public_profile"] callback:^(TDSUser * _Nullable user, NSError * _Nullable error) { - if (user) { - // 开发者可以调用 user 的方法获取更多属性。 - NSString *userId = user.objectId; // 用户唯一标识 userID - NSString *username = user[@"nickname"]; - NSString *avatar = user[@"avatar"]; - } else { - NSLog(@"%@", error); - } -}]; -``` - - - diff --git a/docs/sdk/tds-gift/_category_.json b/docs/sdk/tds-gift/_category_.json deleted file mode 100644 index c1f75ec93..000000000 --- a/docs/sdk/tds-gift/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "礼包系统", - "collapsed": true, - "position": 9 -} diff --git a/docs/sdk/tds-gift/faq.mdx b/docs/sdk/tds-gift/faq.mdx deleted file mode 100644 index bcb0bab2f..000000000 --- a/docs/sdk/tds-gift/faq.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -### 兑换参数完备,但请求兑换时返回 403。 - -请检查您请求的 header 是否有带上特殊的 User Agent。 - -### 二次校验是需要游戏方请求两次兑换接口吗? - -使用二次校验需要您按照规范开发校验接口并将地址配置到后台中,由兑换系统发起请求。 - -### 无服务器返回值的 content 参数无法解析? - -返回值的 content 是一段嵌套的 json 字符串,可以先读出字符串再进行进一步 json 解析,除此之外还新增了 content_obj 字段,可以有选择的去使用。 - -### 如何判断兑换是否成功? - -可以根据返回值中的 error 字段是否为 0 来判断。 - -### 无服务器兑换的 sign 字段如何使用? - -可以参考文档的[签名部分](/sdk/tds-gift/guide/#签名)。 - -### Unity 请求无服务器兑换接口报错 HTTP/1.1 422 Unprocessable Entity - -可以通过 **开发者后台 -> 礼包服务** 检查活动开关是否打开,以及礼包码是否失效。 - -### 礼包生成时出错,时间不能超过 2040 年 - -当前最大礼包时间为 2038-01-19 11:14:07。 - -### 三种兑换场景的区别是什么? - -| 兑换方式 | 所需接口 | 特点 | -| --------------- | ------------- |------------- | -| 游戏方校验 | 兑换接口、二次校验接口、礼包发放接口 | 游戏方需要维护校验与发放两个接口;可根据礼包领取条件自行判断是否符合,并可以将礼包条件校验逻辑与礼包发放逻辑分开维护处理 | -| 游戏方发送 | 兑换接口、礼包发放接口 | 游戏方仅需维护一个发放接口;可以通过发放接口进行领取条件的判断以及礼包的发放 | -| 无服务器兑换 | 兑换接口 | 仅判断兑换码是否有效以及码量,兑换码有效即返回礼包信息;流程更简单,无需自行维护接口 | - -### 调用礼包兑换接口报:{"error":100016,"message":"该礼包码无效码","info":{"dev_message":"invalid code","hint":"gift code is invalid"}} 异常。 - -检查接口中传递的礼包码是否错误,礼包码可以在 **Tap 开发者中心 > 你的游戏 > 运营工具 > 礼包 > 礼包活动 > 数据 > 导出** 进行导出。点击 `导出` 按钮后会生成 `.csv` 文件,可以在该文件中查看礼包码。 - -### 调用 `游戏方发送` 礼包兑换接口报:{"error":100015,"message":"发送道具失败"} 异常。 - -该异常说明调用接口时传递的参数正确、签算也正确,可能是游戏侧服务端的发送道具的接口出了异常,需要游戏侧检查自己的发送道具接口是否正常。 \ No newline at end of file diff --git a/docs/sdk/tds-gift/features.mdx b/docs/sdk/tds-gift/features.mdx deleted file mode 100644 index 563b74da7..000000000 --- a/docs/sdk/tds-gift/features.mdx +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: 礼包系统功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -## 产品简介 - -TDS 全套礼包系统,旨在帮助开发者快速生成、核销礼包码。 - -### 产品定位 - -- 开发者无须投入大量开发成本设计礼包券码生成+核销系统,接入相关能力,1 天内即可完成礼包系统开发,快速生成礼包码进入分发运营阶段。 -- 通过礼包系统生成的券码可直接通过现有兑换接口,实现在游戏客户端内或者网页内快速兑换核销。 - -### 产品优势 - -- 支持游戏生成不同类型券码:通兑码或唯一码,覆盖常见通用运营场景。 -- 支持券码定制,自定义兑换条件,道具灵活配置等个性化运营配置,满足丰富活动场景。 -- 降本提效,为开发者节省海量券码产生的存储费用;根据厂商需求及市场变化不断优化迭代,降低产品设计及维护成本。 -- 提供整体活动、单条券码的消费数据情况,运营可清晰查看当前活动券码消耗及库存,判断活动效果。 - -## 礼包系统使用流程 - -![](/img/tds-gift/gift01.png) - -## 礼包系统使用指引 - -### 礼包系统入口 - -进入 **「开发者中心」**→**「游戏服务」**→**「运营工具」**→**「礼包系统」** - -### 创建/编辑礼包活动 - -#### 配置区服 - -- 未配置区服,请先前往 **「服务器管理」** 配置区服信息 -- 已配置区服,根据实际发放需求,勾选区服信息 - -![](/img/tds-gift/gift02.png) - -#### 配置礼包基础信息 - -- 礼包活动名称 - - 填写礼包名称(该名称不做显示用途) -- 礼包活动有效期 - - 填写礼包活动时间,礼包码的生效周期等于该活动时间,超时礼包码失效,无法兑换 -- 礼包码类型 - - 通兑码:一个码支持多用户兑换;通兑码支持定制,长度限制最长为 13 个字符,仅限数字、英文 - - 唯一码:一个码仅支持一个用户兑换 -- 礼包码生成规则 - - 随机生成:13 位礼包码均由系统随机生成 - - 输入前缀(仅针对唯一码):唯一码支持定制前缀,为了保证生成券码数量,前缀长度建议不超过 6 位 -- 自定义条件 - - 游戏在基础条件限制上新增自定义条件,自定义条件可以叠加,自定义条件最大数量不超过 10 个。未有自定义条件可不勾选 - - 条件:填写限制条件名称(赛季、注册时间等);判断条件:等于,小于,小于等于,大于,大于等于;值:填写限制条件的值 - - 例如:注册时间为 2022 年的用户才可领取该礼包:注册时间(条件)等于(判断逻辑)2022(值) -- 礼包码物品及数量 - - 物品名称:实际发放物品名称,例如原石、甜甜鸡 - - 数量:实际发放物品数量 - -:::tip - -**注意**:每次仅能新增一个物品,切勿将多个物品填写进同一行。新增物品种类最多不超过 10 个。 - -::: - -#### 上架礼包活动 - -前往 **「礼包活动」** 列表,点击开关,上架礼包活动使券码生效 - -![](/img/tds-gift/gift03.png) - -> **注意**:礼包活动上架后礼包码才可被成功兑换,活动上线时务必打开此开关。 - -#### 编辑礼包活动 - -点击 **「编辑」** 进入编辑礼包活动页面,仅「区服」、「礼包活动名称」、「礼包活动有效期」支持编辑 - -### 活动补码/补量 - -**唯一码补码** - -点击「补码」,输入新增礼包码个数,点击「生成并导出」系统将自动下载新增的券码至本地 - -**通兑码补量** - -点击「补量」,输入通兑码新增兑换次数,点击「确认」新增数量,同时系统将再次自动导出该通兑码至本地 - -:::tip - -**注意**:通兑码不会新生成礼包码,仅在原兑换码上新增兑换次数,如需新增礼包码,前往「创建礼包码」新增活动 - -::: - -### 历史记录 - -前往礼包系统首页,点击 **「数据」** 查看历史生成记录 - -![](/img/tds-gift/gift04.png) - -### 服务器配置 - -- 服务器名称 - - 填写游戏服务器名称 -- 服务器 code - - 道具发放通知接收地址 -- 接受道具下发通知地址 - - check_url -- 接收二次校验结果通知地址 - - 开启二次校验服务需填写此字段,如不开启,则不需要 - -### 数据查询 - -**单一 CDKEY 或 单一用户数据** - -- 输入「CDKEY」即可查询到对应礼包码消费状态 -- 输入「uid」即可查询单个用户消费礼包码历史记录 - -![](/img/tds-gift/gift05.png) - -**礼包活动数据** - -- 入口:礼包活动首页 -- 输入任意以下筛选条件,即可查询礼包活动整体消费库存情况 - - 礼包码类型 - - 礼包码活动 - - 礼包活动 ID 或名称 - -![](/img/tds-gift/gift06.png) - -## 常见 FAQ - -Q:提示报错「保存用户 client id 失败」? - -A:为了确保礼包服务能正常使用,请先前往:开发者中心 →「游戏服务」→「应用配置」,开启服务,获取 client id。 - -Q:为什么看不见「游戏服务」tab? - -A:游戏管理员新增用户角色时,需在权限设置中,勾选应用配置权限,才可看到「游戏服务」tab 及其内容。 diff --git a/docs/sdk/tds-gift/guide.mdx b/docs/sdk/tds-gift/guide.mdx deleted file mode 100644 index 9f9e47f1e..000000000 --- a/docs/sdk/tds-gift/guide.mdx +++ /dev/null @@ -1,681 +0,0 @@ ---- -title: 礼包系统开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -:::tip -接入礼包系统之前,需完成应用配置。在 **游戏服务 - 应用配置** 中可获取 **Client ID 和 Server Secret**。请妥善保管这些信息,勿传给他人。 -::: - -## 开始 - -礼包系统接入适用于有请求接口能力的游戏,目前尚未提供完全离线的兑换码服务。 - -根据您的使用场景,您可以灵活对接以下不同兑换接口之一完成兑换流程。 - -| 方式 | 接口名 | 需游戏方校验后台设置的兑换条件 | 需游戏方提供发送道具接口 | -| -------------- | -------------------------------- | :----------------------------- | :----------------------- | -| 游戏方校验 | /api/v1.0/cdk/game/submit-check | ✓ | ✓ | -| 游戏方发送 | /api/v1.0/cdk/game/submit-send | ✗ | ✓ | -| 无服务器兑换 | /api/v1.0/cdk/game/submit-simple | ✗ | ✗ | -| 游戏方校验 Web | /api/v1.0/cdk/page/submit-check | ✓ | ✓ | -| 游戏方发送 Web | /api/v1.0/cdk/page/submit-send | ✗ | ✓ | - -### 接入顺序 - -- 完成应用配置获取 **Client ID 和 Server Secret** 信息。 -- 进入 **运营工具-礼包系统-服务器配置** 选择性填写游戏方服务器信息并在其中配置发送通知接收地址用来发送相应道具。 -- 创建礼包活动导出兑换码,礼包配置完成准备测试。 -- 根据实际业务场景使用对应兑换接口完成兑换。 - -### 兑换流程 - -兑换流程大致是: - -1. 根据接口文档提交 **兑换码** 与 **配置信息等参数** 给兑换接口。 -2. 兑换接口校验数据合法性。 -3. 兑换系统根据业务场景选择是否调用 **游戏方提供的二次校验接口**。(可选,当后台配置二次校验接口时会调用) -4. 校验全部通过后根据不同接口判断是否调用 **游戏方提供的发送道具接口** 完成兑换。(可选,当后台配置发送道具接口时会调用) - -有三种不同的兑换接口可以进行兑换,整体对接流程只要对接其一接口即可完成: - -| 名称 | 描述 | -| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| 游戏方校验 | 如果您将使用服务器对接兑换系统,可以安全地保存密钥信息,并希望每次兑换时能通过您的应用程序来校验是否允许兑换,调用您的接口来发送道具,请使用此流程。 | -| 游戏方发送 | 如果您将使用服务器对接兑换系统,可以安全地保存密钥信息,并希望调用您的应用程序接口来发送每次兑换的道具,请使用此流程。 | -| 无服务器兑换 | 如果您希望不开发接口完成兑换流程,调用兑换接口后根据返回值来判断是否兑换失败,请使用此流程。 | - -### 三种兑换场景 - -#### 游戏方校验 - -当您请求兑换码 submit-check 接口进行兑换时,兑换系统会核对库存以及提交数据是否正确,兑换系统核对通过后会调用您在后台配置的二次校验接口并将后台设置的条件作为参数传给您来校验。 -当您的接口校验通过后会继续将后台配置的奖品内容作为参数调用您的发送道具接口,当发送道具成功完成后本次兑换完成。 -![游戏方校验泳道图](/img/tds-gift/submit_check.png) - -#### 游戏方发送 - -当您请求兑换码 submit-send 接口进行兑换时,兑换系统会核对库存以及提交数据是否正确,核对完成后会继续将后台配置的奖品内容作为参数调用您的发送道具接口,当发送道具成功完成后本次兑换完成。 -![游戏方发送泳道图](/img/tds-gift/submit_send.png) - -#### 无服务器兑换 - -当您请求兑换码 submit-simple 接口进行兑换时,兑换系统会核对库存以及提交数据是否正确,兑换信息验证通过后本次兑换完成并将后台配置的道具信息返回给您。 -![无服务器兑换泳道图](/img/tds-gift/submit_simple.png) - -## 对接兑换接口 - -### 游戏方二次校验规范 - -礼包活动在后台配置了自定义兑换条件后,使用带二次校验能力的接口进行兑换时,兑换系统会将此礼包活动的自定义兑换条件发送给游戏方进行二次校验,下图配置会对您在后台配置的接口发起如下请求: - -![自定义兑换条件](/img/tds-gift/custom_redemption.png) - -``` -curl --location -g --request POST <后台配置的游戏方二次校验接口地址> \ ---header 'Content-Type: application/json' \ ---data-raw '{"activity_id":<后台活动ID>,"character_id":<用户ID>,"content":"[{\"condition\":\"level\",\"operate":\"gt\",\"value\":\"10\"}]","content_obj": [{"condition":"level","operate":"gt","value":"10"}],"ext":<兑换提交的ext参数>,"gift_code":<礼包码>,"nonce_str":"abcdc","server_code": <后台配置的code>,"sign":"42d2b58a8cd58b5e63d90524aaf63ef97e04f223","timestamp":1658825322,"custom":{<自定义维度>:[<自定义维度字段1>,<自定义维度字段2>]}}' -``` - -请在校验完成后返回带有特定字段的 JSON 返回值告诉兑换系统校验结果。 - -```json -{ - "status": true, - "errorCode": "字符串类型的错误code" -} -``` - -如果您希望返回字段命名风格保持一致也可以使用下划线命名风格返回信息。 - -```json -{ - "status": true, - "error_code": "字符串类型的错误code" -} -``` - -如果您并不要求兑换系统知晓兑换的错误信息可以直接返回 **非 200 HTTP code** 表示兑换失败,或者返回 **HTTP code 200** 表示兑换成功。 -这样就无需按兑换系统的格式来返回信息。 - -| 请求参数 | 类型 | 含义 | -| --------------------- | ----------------------------- | -------------------- | -| activity_id | 字符串 | 礼包活动 ID | -| character_id | 字符串 | 兑换的用户 ID | -| content | 字符串(嵌套 JSON 字符串) | 后台配置的自定义条件 | -| content.condition | 字符串 | 自定义条件-key | -| content.operate | 字符串 | 关系-详情见其他 | -| content.value | 字符串 | 自定义条件-value | -| content_obj | 对象数组(和 content 含义相同) | 后台配置的自定义条件 | -| content_obj.condition | 字符串 | 自定义条件-key | -| content_obj.operate | 字符串 | 关系-详情见其他 | -| content_obj.value | 字符串 | 自定义条件-value | -| ext | 字符串 | 请求接口时传递的数值 | -| gift_code | 字符串 | 用户此次兑换的礼包码 | -| nonce_str | 字符串 | 此次请求的随机字符串 | -| server_code | 字符串 | 服务器 code | -| sign | 字符串 | 签名 | -| timestamp | 数字 | 秒级时间戳 | -| custom | 对象 | 维度信息 | -| custom.xxx | 字符数组 | 自定义维度 | - -| 返回参数 | 类型 | 含义 | -| --------- | -------------- | ----------------------------------------------- | -| errorCode | 字符串 | 错误类型,error_code 含义相同,优先取 errorCode | -| ext | 字符串(可选) | 需要透传给发送道具接口的字符串 | -| status | 布尔值 | 是否校验成功,true 为成功,false 为失败 | - -### 游戏方发送道具规范 - -后台设置的物品信息,在兑换系统完成兑换校验后会带上配置的参数请求 **游戏方的发送道具接口**。下图配置会对您在后台配置的接口发起如下请求: -![自定义兑换条件](/img/tds-gift/custom_prizes.png) - -``` -curl --location -g --request POST <后台配置的游戏方发送道具接口地址> \ ---header 'Content-Type: application/json' \ ---data-raw '{"activity_id": <后台活动ID>,"character_id":"uid","content":"[{\"name\": \"奖品名称\", \"number\": 2}]","content_obj": [{"name": "奖品名称", "number": 2}],"ext":<兑换提交的ext>,"gift_code":"gift_code","nonce_str":"abcdc","server_code":<后台配置的code>,"sign":"fbe23e6f6f5193355b29e9ea2913b1992af56346","check_ext":<二次兑换接口返回的ext>,"timestamp":1658827492,"custom":{<自定义维度>:[<自定义维度字段1>,<自定义维度字段2>]}}' -``` - -同样请在发送完成后返回带有特定字段的 JSON 返回值告诉兑换系统发送道具结果,也可以使用下划线命名返回字段。 - -```json -{ - "status": true, - "errorCode": "字符串类型的错误code" -} -``` - -如果您并不要求兑换系统知晓兑换的错误信息可以直接返回 **非 200 HTTP code** 表示兑换失败,或者返回 **HTTP code 200** 表示兑换成功。 -这样就无需按兑换系统的格式来返回信息。 - -| 请求参数 | 类型 | 含义 | -| ------------------ | ----------------------------- | ---------------------------- | -| activity_id | 字符串 | 礼包活动 ID | -| character_id | 字符串 | 兑换的用户 ID | -| server_code | 字符串 | 服务器 code | -| content | 字符串(嵌套 JSON 字符串) | 后台配置的自定义奖品 | -| content.name | 字符串 | 奖品名称 | -| content.number | 数字 | 奖品数量 | -| content_obj | 对象数组(和 content 含义相同) | 后台配置的自定义奖品 | -| content_obj.name | 字符串 | 奖品名称 | -| content_obj.number | 整型 | 奖品数量 | -| ext | 字符串 | 请求接口时传递的数值 | -| gift_code | 字符串 | 用户此次兑换的礼包码 | -| nonce_str | 字符串 | 此次请求的随机字符串 | -| sign | 字符串 | 签名 | -| check_ext | 字符串 | 二次校验透传过来的字符串参数 | -| timestamp | 数字 | 秒级时间戳 | -| custom | 对象 | 维度信息 | -| custom.xxx | 字符数组 | 自定义维度 | - -| 返回参数 | 类型 | 含义 | -| --------- | ------ | ----------------------------------------------- | -| errorCode | 字符串 | 错误类型,error_code 含义相同,优先取 errorCode | -| status | 布尔值 | 是否校验成功,true 为成功,false 为失败 | - -### 无服务器兑换 - -使用无服务器兑换接口进行兑换时无需游戏方开发校验与发送接口。 - -但要注意在 **后台配置礼包时需创建无服务器礼包**。 - -兑换系统在接收到兑换请求后直接将兑换结果返回,游戏根据返回结果判断是否发放道具。 - -接口参数的详细说明请看文档下面的 [三种兑换接口 - 无服务器兑换接口](/sdk/tds-gift/guide/#无服务器兑换接口) - -### 维度配置 - -在礼包后台中可以配置自定义维度信息,配置完成后创建活动礼包时可以勾选上相应维度信息。这些勾选上维度的礼包在二次校验,发送道具,以及无服务器返回结果里会带上勾选的维度参数,方便您对礼包做进一步分类业务的处理。 - -### 请求域名 - -| TapTap 版本 | 域名 | -| --------------- | ------------------------- | -| 国内 taptap.cn | https://poster-api.xd.cn | -| 海外 taptap.io | https://poster-api.xd.com | - -### 签名 - -游戏方与兑换系统的接口通讯时将使用签名参数作为基础的安全验证,其中 **Secret 为后台 游戏服务 - 应用配置 中 Server Secret**。 - -:::tip -为了保证安全性,请勿将 Server Secret 信息放在客户端中使用以防敏感信息泄漏。 -::: - -计算签名伪代码如下: - -``` -sign == sha1(timestamp + nonce_str + secret) -``` - -您也可以通过单元测试验证签名计算过程正确与否,如下是 golang 示例: - -```go -func TestMakeSign(t *testing.T) { - timestamp := 1655724586 - nonceStr := "abcde" - secret := "abc" - sign := MakeSign(int64(timestamp), nonceStr, secret) - assert.Equal(t, sign, "3cb8c38833fa742e7873378faddcbe5b56088482") - //output: 3cb8c38833fa742e7873378faddcbe5b56088482 -} -``` - -为了 Secret 不存放在客户端中,无服务器兑换返回的签名通过 **Client ID 代替 Secret 进行验签**。 - -``` -sign == sha1(timestamp + nonce_str + client_id) -``` - -``` -c_sign == sha1(timestamp + content + client_id) -``` - -### 二次校验兑换接口 - -POST /api/v1.0/cdk/game/submit-check - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------ | ------ | -------- | ------------------------------------------------- | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| server_code | 字符串 | 必传 | 后台配置的服务器 code | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| sign | 字符串 | 必传 | 签名 | -| timestamp | 数字 | 必传 | 时间戳,单位为秒,有效期为一分钟 | -| ext | 字符串 | 非必传 | 该字段会原封不动出现在 ext 中传给游戏二次校验接口 | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -请求示例: - -``` -curl --location --request POST '/api/v1.0/cdk/game/submit-check' \ ---header 'Content-Type: application/json' \ ---data-raw '{"client_id":<后台ClientId>,"gift_code":<这次使用的兑换码>,"server_code":<服务器code>,"character_id":<游戏用户ID>,"nonce_str":<随机字符串>,"sign":<签名>,"timestamp":<时间戳>,"ext": <透传字段>}' -``` - -### 无二次校验兑换接口 - -POST /api/v1.0/cdk/game/submit-send - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------ | ------ | -------- | -------------------------------- | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| server_code | 字符串 | 必传 | 后台配置的服务器 code | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| sign | 字符串 | 必传 | 签名 | -| timestamp | 数字 | 必传 | 时间戳,单位为秒,有效期为一分钟 | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -请求示例: - -``` -curl --location --request POST '/api/v1.0/cdk/game/submit-send' \ ---header 'Content-Type: application/json' \ ---data-raw '{"client_id":<后台ClientId>,"gift_code":<这次使用的兑换码>,"server_code":<服务器code>,"character_id":<游戏用户ID>,"nonce_str":<随机字符串>,"sign":<签名>,"timestamp":<时间戳>,"ext": <透传字段>}' -``` - -### 无服务器兑换接口 - -POST /api/v1.0/cdk/game/submit-simple - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------ | ------ | -------- | ----------------------------------- | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| sign | 字符串 | 必传 | 签名,该签名用 ClientId 代替 Secret | -| timestamp | 整型 | 必传 | 时间戳(秒) | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -返回参数: - -| 返回参数 | 类型 | 含义 | -| ------------------ | ----------------------------- | -------------------------------- | -| activity_id | 字符串 | 礼包活动 ID | -| nonce_str | 字符串 | 随机字符串 | -| timestamp | 整型 | 时间戳 | -| content | 字符串(嵌套 JSON 字符串) | 后台配置的自定义奖品 | -| content.name | 字符串 | 奖品名称 | -| content.number | 数字 | 奖品数量 | -| content_obj | 数组对象(含义与 content 相同) | 后台配置的自定义奖品 | -| content_obj.name | 字符串 | 奖品名称 | -| content_obj.number | 整型 | 奖品数量 | -| sign | 字符串 | 返回签名,验证返回数据没有被修改 | -| c_sign | 字符串 | 内容签名,验证返回数据没有被修改 | -| error | 整形 | 0 为成功,其他为失败码 | -| success | 布尔 | 是否成功 | -| custom | 对象 | 维度信息 | -| custom.xxx | 字符数组 | 自定义维度 | - -请求示例: - -``` -curl --location --request POST '/api/v1.0/cdk/game/submit-simple' \ ---header 'Content-Type: application/json' \ ---data-raw '{"client_id":<后台ClientId>,"gift_code":<这次使用的兑换码>,"character_id":<游戏用户ID>,"nonce_str":<随机字符串>,"sign":<签名>,"timestamp":<时间戳>}' -``` - -
    -Shell 请求示例: - -``` -#! /usr/bin/env bash - -# TapDC 后台应用的 Client ID -client_id="请替换为控制台的 `Client ID`" -# 本次兑换的兑换码 在“礼包活动面板-数据-导出” 然后导出的文件里面可以看到格式如 S0LLC8ICB2MP2 的兑换码 -gift_code="请替换为礼包面板中的兑换码" - -# 随机字符串,建议五位随机字符串 -nonce_str="A2B3Z" -# 游戏用户 ID -character_id="6347de128b****3ee825e029" -# 签名,该签名用 ClientId 代替 Secret -signed=$(echo -n $(date +%s)$nonce_str$client_id |openssl sha1) -RESULT_REQUEST=`curl --location --request POST 'https://poster-api.xd.cn/api/v1.0/cdk/game/submit-simple' \ ---header 'Content-Type: application/json' \ ---data-raw "{\"client_id\":\"$client_id\",\"gift_code\":\"$gift_code\",\"character_id\":\"$character_id\",\"nonce_str\":\"$nonce_str\",\"sign\":\"$signed\",\"timestamp\":$(date +%s)}"` - -echo $RESULT_REQUEST -``` - -
    - -
    -Android 请求示例: - -可以参考[ Android Demo ](https://github.com/taptap/TapSDK-Android-Demo/blob/main/app/src/main/java/com/tds/demo/fragment/GiftFragment.java) 中礼包系统功能 - -
    - -
    -C# 请求示例: - -``` -using UnityEngine.Networking; -using System; -using System.Text; -using System.Security.Cryptography; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -public class APIClient : MonoBehaviour -{ - private const string apiUrl = "https://poster-api.xd.cn/api/v1.0/cdk/game/submit-simple"; - private const string contentType = "application/json"; - private const string clientId = "hskcocvse6x1cgkklm"; - private const string giftCode = "NZ4mp2cztRMXH"; - private const string characterId = "879a0cdd9nnb917055ee"; - private const string nonceStr = "RFG7U"; - - public GUISkin demoSkin; - - - void Start() - { - - } - - - private void OnGUI() - { - - GUI.skin = demoSkin; - float scale = 1.0f; - - - float btnWidth= Screen.width / 5 * 2; - float btnWidth2 = btnWidth + 80 * scale; - - float btnHeight = Screen.height / 25; - float btnTop = 30 * scale; - float btnGap = 20 * scale; - - GUI.skin.button.fontSize = Convert.ToInt32(13 * scale); - - var style = new GUIStyle(GUI.skin.button) { fontSize = 20 }; - var labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 30 }; - - if (GUI.Button(new Rect((Screen.width - btnGap) / 2 - btnWidth, btnTop, btnWidth /2, btnHeight), "返回", style)) - { - UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(0); - - } - - btnTop += btnHeight + 20 * scale; - - if (GUI.Button(new Rect((Screen.width - btnGap) / 2 - btnWidth, btnTop, btnWidth, btnHeight), "无服务器兑换", style)) - { - StartCoroutine(StartRequest()); - } - - - } - - private IEnumerator StartRequest() - { - // 获取当前时间戳(秒) - int timestamp = (int)(System.DateTime.UtcNow.Subtract(new System.DateTime(1970, 1, 1))).TotalSeconds; - - // 拼接并加密 sign 参数 - string sign = GetSign(timestamp, nonceStr, clientId); - - // 构建请求参数 - string requestBody = "{\"client_id\":\"" + clientId + "\",\"gift_code\":\"" + giftCode + "\",\"character_id\":\"" + characterId + "\",\"nonce_str\":\"" + nonceStr + "\",\"timestamp\":" + timestamp + ",\"sign\":\"" + sign + "\"}"; - - // 创建 UnityWebRequest 对象 - UnityWebRequest request = new UnityWebRequest(apiUrl, "POST"); - - // 设置请求头 - request.SetRequestHeader("Content-Type", contentType); - - // 设置请求体 - byte[] bodyRaw = Encoding.UTF8.GetBytes(requestBody); - request.uploadHandler = new UploadHandlerRaw(bodyRaw); - request.downloadHandler = new DownloadHandlerBuffer(); - - // 发送请求 - yield return request.SendWebRequest(); - - // 处理响应 - if (request.result == UnityWebRequest.Result.Success) - { - Debug.Log("API request succeeded"); - Debug.Log(request.downloadHandler.text); - } - else - { - - Debug.Log("API request failed: " + request.error); - Debug.Log(request.downloadHandler.text); - - } - } - - private string GetSign(int timestamp, string nonceStr, string clientId) - { - // 拼接参数并进行 SHA1 加密 - string signString = timestamp.ToString() + nonceStr + clientId; - byte[] signBytes = Encoding.UTF8.GetBytes(signString); - byte[] signHash = new SHA1CryptoServiceProvider().ComputeHash(signBytes); - string sign = BitConverter.ToString(signHash).Replace("-", "").ToLowerInvariant(); - - return sign; - } -} - - -``` - -
    - -返回示例: - -```json -{ - "activity_id":"TDS20220928151122U9H", - "content":"[{\"name\": \"奖品\", \"number\": 2}]", - "content_obj":[ - { - "name":"奖品名称", - "number":2 - } - ], - "nonce_str":"0DD4B", - "sign":"5895f87d3dfb5e0918c1b195c015d9284609d122", - "timestamp":1664354023, - "success":true, - "error":0, - "c_sign":"12c6fc06c99a462375eeb3f43dfd832b08ca9e17", - "custom":{ - "seasons":[ - "101", - "102" - ] - } -} -``` - -#### 返回格式 - -成功返回 - -```json -{ - "success": true, - "messages": "成功", - "error": 0 -} -``` - -错误返回 - -| 返回参数 | 含义 | -| ---------------- | ------------ | -| error | 错误码 | -| message | 错误大致信息 | -| info | 详细信息 | -| info.dev_message | 开发提示 | -| info.hint | 详细提示 | - -```json -{ - "error": 100001, - "message": "输入有误", - "info": { - "dev_message": "", - "hint": "test error" - } -} -``` - -### Web 嵌入兑换接口 - -兑换系统提供了一套支持简易图形验证码的兑换接口,用来支持类似 Web 活动页的接入,与上述兑换接口流程相同但区别在于部分请求参数的改变。 - -#### 获取验证码 - -:::tip -验证码有效期为五分钟,失效后请重新获取新的验证码。 -::: - -GET /api/v1.0/cdk/page/captcha-img - -Header Content-Type:application/json - -| 返回参数 | 含义 | -| -------- | -------------------------------- | -| img | base64 格式的图片信息 | -| key | 验证码 key,提交兑换时会需要带上 | - -返回示例: - -```json -{ - "img": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAA8CAYAAAAjW/WRAAANc0lEQVR4nOydCXAUV3qA/z7mHs2t0bWSkJCEECCO5Ta2w5GFmMCGrGHNemuXXZwYluxhQrEVkrWzIkUS29hlOxgfYFypJBATbONgG7ABGxwOg7GQQAJJCB0gCWkOzT3T09Od6haSR6PRXJpL0vuqKKZfv379uvt9+t97/UYihdWf/AEQCERQ8FRXAIFIZ5AgCEQIkCAIRAiQIAhECJAgCEQIkCAIRAiQIAhECJAgCEQIkCAIRAjIVFcgnTAUaqpSXQcOXavp2VTXAdHHuBMkXSQIRag6InmSy5gVZDSIEAvBrgtJkzjGjCBjVYhIQNIkjlEpSKJkSJdGFY/rCywjXa5ttIGNluXu8ZJitDcUdB+SS9oKMtKGMJ4aQDykGU/3KxrSSpCRPOgx+YBZwHLqjeqcelO2ssupETm80uofTrzcNUltDndoOt5LpdWare61lEvdrmLSS+twlpWwADQlFN4zqtUXujN1txJx3uPz1w17Lw7985Ihae8u3jRw/SkVJNaHOCZl8OPx7efWYwyLszjG2LVio6lA0WXOk5szDC75xPMd3z9atfAQJSXpaMpM5b1WWa05uZ1dj3HtzSaX19rksrsOqdRIkyQl8nhkOpO5XGs2L7VmyC/fKSg4NdLzhRICgkjhL0QgSR+kIynC4xWTLtzHkAIXLXVoxBbAAPJuGPKVnU49J46iyyE1FCut0ZQZeP8ifQ7++WJ5BlqTqSjLYFjcmaU/aVap2gP3u8Vi293cnMu9SsXtiS2tT2caja09Wm1TqDI3nHljoE5P/N3psHVYcfG9mNtOUgSJRYrxJEQg3SXKNhbDWMCA1Tf1FiruO3N6ipVNGMMQDo3YSEkFUUWPYMQiTCyy6A3GR4xq9flgcvhjl8lMZqXyrNZknr+y5vDPhssXToiRyBAM7Ml1k9nhdh7esTvmkyEpYkfXbFEUX+qa4FSLHPnVPSViGyW36aWGa39eXG0oUlhZHBv2mcWDaJ9dqOc2/UbdDqNaffJeTta3ABirNxhKVRbrzAynrQJnfVBgawC518Ln9RBiaFJOhymmS/x2oqNDJAwag6zd9bcR35hg8sTzxiL6WLzn2kMZ3c7Mrsma5q+fmFST7PPH+kz7u0Gdsgkg9LnAKtSCm5QChYsH8hbabkKXtADKzVf5ccCxRT+p+uOr22Hnr3YFLTvRMgQj4kF6NPLsffPAsPuQFNHBCSI3uLQdU7RN3zxeWpfKuvjLsu3d4I04EK6f2JZRBr2iTGABGJdEUmdUq7555y9/83O3WASv/OMm+MMzL/J5W0oL4H+fWAG/3vkWv50KIQIZdgxSfP9qVlnHhTKZx6JgMJzp/uGMD6+UrLrhEKk8EHCzNj/9i0HHBm77sxagaiRdt3BgHpK/Jlbg8wHOsuDDccxDEIRVJMJtQhFVajIm6twJgWUxWkhQMqNLkczT+g+E+9kWIv+LG3YMu+/05DVwdcF0mHnxGq4026YCAPcPGqaWwKwLfUGRk6HsdvOP3CJR54qL752Py0XEgSERRGu7K3/s6r+t1lnbygMzC4USWPHo38C0SUvDFuwfKUbadYsKBsN4MYJAGCUS8bdZhY5lLTdHdI4k8sjbtfNENq/UkiszxruLFUyC4Qg1FRrYDbuSsyxkWZwMOpOpKNNgfLSxuOi/aJKk5A6Hpqi1bUPdpLLXfAThjbReiWaQIApnj2T9uX/4Kyll03kEst6zFT858umcP91YXXcCTp/fz+fBcQKe2fjfIBbJhxQWS/cp7vI8EERYr8tUHaxYjzkEGm+hpcb0myvvc7vV+2Ysd1d217vmdrRFW9dUMPfgrUpVhz2TITDf57+b9UU0x0YjAISRYDjCvXPgmN35+aDtoy0dcPLufTiqz37eLRLZubTyxqYnzSrllfuZmQl5WRgrg7pYq668vJqTg8Vw36/X7lZVaSZs5NIXzFoLnd0NUN90DgSkCAh8cM9sJOOKUI0+UJ5QMg2U8yB6ZBwr+QHmJFWOZS1HOGnILrmczrbbvbm2TtENXeloEcSlFDp1LT4BsCAM3JcMAQIJJ4T/uCHYAP/V603QbnfCC/MrYQ9JbOfSDt1uh5cF5MV0kwP8I0hl66mipTX7+cHDjIrlsHLJM4MyUl4X1DeehbzsctBpClM+2B5OFm5QiAELF41nQCM8C2rhV3B22+p/0r4895c9z371hvTLglLx9czJpi3ffJT8WkcHJwBmEABRJwegMaD/xBTyS9LxEMCfSKJDpANpTpY365qhy+WGv581GUgMA5plYc/1JtCKRfDT0oKBvKluW/7wgnCV/+DELqhrPMsn/nTNv0Jh3vQhmdOp4sOBm8XiH+3dsuOcoRrK5M8CidvASU+AFudv4WHdDLB6Z8Bd18+gQrGVz5/ICYNICBcFMDsBxGUlL8a+59Y9x9ufIKKJDlGCldy5s1LgpTVfLJtbQmAYmD0UvHCtAdYU5cKcTE3Qg9KhvfXN+LAMNLZ8zSdgGAZ52ZP5zxZbN6y6dn6fXax2tmVOM6S2qpFBWERirtFnb4Wq6qmbPnHN7ryl2TtrC2AgOLxj93Mrq16volnVQP5wY6CRCjTSbpDARZMrL3292iMROOVGl8iuk7hHUp9+4hkdwlHYfneRyEPl3SqZeCCrzUxxaRW3Gp4SUVTB5W7ToLw4hsEUtQJeWjh9xMtc4gEviNHcDl5v332XSdVw8/ZXcPDLN7ulHot+OcBTXLpDpLx/ZtqGDxpz5nWkoqKRgnkJgvufxYBlZF6X/ETxQ5gPE/oyqG4u/fjmbf+i2TN1w+Edu18Pdny0455EjwO8EpKmZCQnh1bTblPEKkgCo0NYVFbrwi595oc0SVL9aXWTyvb55/GX4Y265iFl9O9Ptii8IHbndxbbHSY4evJ5EBLCDLM857aYsqsklE0r81iy/uzq67+g5wjfvqOf2Z3MSkYFjfO9dFbgc/q0LovsdOEaPjnf1sj9z8i8XozBhn3/Exgx/AVwXGgclJeTxxFhOdGib+pVVpxsnSozuVU4zRD3puluVh5rnjv/P+rVDo3Y+Onv5xxnSJwJdmwyo0MksBgwDI77QuXxb/hb3dSw9U92VOEbyqZbNf/5MMCT/YltmVMvHZv9uxMeUkpzl7fu/M7Vecab3ycYr2hJzYG/2L9s5luJrljMPJjF8kw1nJedzV+EsYDTWlcrK/DxLzgJk0QC7OChbqRRQLagdNgIENOMWwhmv9ewsHledj0lFdzRtVh1tBCn75eqWyU2qierwVySU2/S3Jum47u9qYwOkeASSxrlDkd+uFW6/Zww9La/1Gp6O9wyl2TIwgtCCaQDoc8jkFqOzN/x8XdZMPazyqc+//kX22dgLEMoXIbvlXVcyGvIXXAvERUaKayI5le6+jQui+RqVhn3eUrWkUKCdRUWnDm5xGwqh1q2AB4LIUUss0Hxnq4mKZ9w2vGWh2gBQblUol5um5KQ7lUnPn7Uiwvgx5v/fZKcDr7iPdVCBGJWKmv0BsNiADgTJiuGsQyOMQy/YMu/0adKFl4Qi1RnH0jwUZLATGZ5rsMq0d5TOnv4ubgJ3TWF6SSIfwTweJQgumOB6rqt0I1/D8QSA6jUN0Gp7vvhxQlixzWGdxdvejVZ9YtFHoKRgdCjh0eutgPB0ECwfT0UAUMBBgz0y5FuMgTDoNXcyenuXjWz9npEkdqSkfF/gWn9jT6aZfnxEIUXpF07xcjghBdnfAKCoYUqR5e0V5bt9M/oEUgHJBJ77bKRnjgaohkIswzZFwEWc9X/kk9rBu6H12K+e6X9bN4WVkx7ElnfaPCXZ2hXaeiKmP2r8x986nsE/UKlero6DGzt5PJX4lFQsqMKLwiDk4xFqm9T2zsnctslnZfzr5SsGvRWU0i7pf2fXcIMeywnC0W81gXhvWIx+CicMEgkPr3TCT4ME3TKM8TVWUXiK9mLcDehoLIcQ6dJUsBIxg7RdNsg/QWKiWTIMvAmfX7DkSkLbh35Mfe5U11afWjRH9/vzySmbIK//uxXvycYH7/c4YN5219r0c/oieZikroswofjGR+VzBQ1aCpwm0iHeXEJ0LgACMZL5ziaLOvrPqGz7XGXPBTJnllK6gLRNCOa77CEk2VAEJyh8Y2nfvu03G3OYQFjT1X+8kBt4dIWbt/yb/cuqrh77gfc5y7VxJqDD+/8n8CCUrEuKF1Jt2nWQMaLPPH4At+g1bw55gbV4xd2beYG6iyG+wyK/AacoUmt7V4pt58mMsAuqQQWiLAnG8sCBJLu06zRMFbliVWWId8HKeip1S2tfWe1ynF/Qn8ai5HgEeSCS1gw8CdFxpMA/qR7dEgkY0WeSGXhJBn2K7dZvc1KnbVN5RQp3K2ZlT3cQD7uNR0FjKXokEhGqzzh/tREWv1mxVQznqNDIolGHkihQMF+4fe4FgRFh/QgHaMPJ8u4EgRFh9FJquUZs4Kg6DD2ScbL0TEhCIoOiEQxKv/CFIoOiGSR9oKg6IBIJWknCIoOiHQipYKg6IBId5ImCJIBMRpJmCCoq4QYC8RFEBQdEGOVmARB0QExXggrCIoOiPHMEEFQdEAgvoMXJJQUSAjEeGZMrMVCIBLF/wcAAP//ettpiq6bhWEAAAAASUVORK5CYII=", - "key": "XfNTN1gQyvA2AcNOu1UN" -} -``` - -#### 二次校验网页版 - -POST /api/v1.0/cdk/page/submit-check - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------------- | ------ | -------- | ------------------------------------------------- | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| server_code | 字符串 | 必传 | 后台配置的服务器 code | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| verify_captcha_key | 字符串 | 必传 | 验证码 KEY | -| verify_captcha_code | 字符串 | 必传 | 用户提交的验证码答案 | -| ext | 字符串 | 非必传 | 该字段会原封不动出现在 ext 中传给游戏二次校验接口 | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -#### 无二次校验网页版 - -POST /api/v1.0/cdk/page/submit-send - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------------- | ------ | -------- | ------------------------------ | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| server_code | 字符串 | 必传 | 后台配置的服务器 code | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| verify_captcha_key | 字符串 | 必传 | 验证码 KEY | -| verify_captcha_code | 字符串 | 必传 | 用户提交的验证码答案 | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -## 其他 - -### 错误码 - -| 自定义状态码 | 含义 | 复现场景 | -| ------------ | ---------------------------------------------- | ---------------------------------------- | -| 100001 | 输入有误 | 发起请求的参数错误或者缺失部分必要参数 | -| 100004 | 验证码输入错误,请重试 | 校验图形验证码错误 | -| 100005 | 参数错误 | 提交的 ClientId 或者 ServerCode 错误 | -| 100006 | 礼包码次数已达上限 | 通兑码可领取次数达到上限(正常情况) | -| 100025 | 礼包码已经兑换过了 | 唯一码已经被兑换过了 | -| 100008 | 礼包码已经过期了 | 礼包活动已过期 | -| 100009 | 礼包活动暂未开放 | 礼包活动暂停兑换,还没到活动开始时间 | -| 100010 | 超出服务使用范围 | 提交的 ServerCode 错误,不在礼包设置的范围 | -| 100011 | 点击过快,请稍候再试 | 唯一码连续提交 | -| 100012 | 礼包码次数已达上限,请稍候再试 | 通兑码库存不足(并发兑换时可能触发) | -| 100013 | 校验 cdkey 未通过 | 二次校验游戏方返回了不通过结果 | -| 100014 | 操作的人太多了,服务器正在拼命处理中 | 兑换系统已达流量极限 | -| 100015 | 发送道具失败 | 发送道具游戏方返回了失败结果 | -| 100016 | 该礼包码无效 | 异常兑换码的提交 | -| 100017 | 礼包码次数已达上限 | 相同用户使用礼包中超过礼包设置的兑换数量 | -| 100018 | 该活动无法使用此兑换接口 | 礼包无服务器设置错误 | -| 100022 | 该国家地区不支持此兑换码 | 非此 ClientId 的唯一兑换码 | -| 500001 | 服务器故障 | 礼包系统出现通用性错误 | -| 500002 | 外部接口校验错误 | 与游戏方接口通讯发生了异常 | - -### 多语言 Lang 参数 -| Lang | 语言 | -|---------------|-------------| -| ar_AR | 阿拉伯语 | -| de_DE | 德语 | -| zh_TW | 中文(繁體)| -| zh_CN | 中文(简体)| -| en_US | 英语 | -| es_ES | 西班牙语 | -| fr_FR | 法语 | -| id_ID | 印度尼西亚语 | -| it_IT | 意大利语 | -| ja_JP | 日语 | -| ko_KR | 韩语 | -| pt_PT | 葡萄牙语 | -| ru_RU | 俄语 | -| tr_TR | 土耳其语 | -| vi_VN | 越南语 | - -### 自定义条件关系表 - -| 关系 code | 含义 | -| --------- | -------- | -| lt | 小于 | -| le | 小于等于 | -| eq | 等于 | -| ne | 不等于 | -| ge | 大于等于 | -| gt | 大于 | - -### 兑换系统出口 IP - -如果您配置的接口对请求 IP 有安全限制,请根据需要允许兑换系统的 IP 访问。 - -| TapTap 版本 | IP | -| --------------- | ------------- | -| 国内 taptap.cn | 59.110.228.98 | -| 海外 taptap.io | 8.214.95.148 | diff --git a/docs/sdk/tds-gift/no-server-guide.mdx b/docs/sdk/tds-gift/no-server-guide.mdx deleted file mode 100644 index 0211f92e6..000000000 --- a/docs/sdk/tds-gift/no-server-guide.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: 无服务器兑换开发指南 -sidebar_label: 无服务器兑换 -sidebar_position: 3 ---- - -:::tip -这篇是说明无服务器兑换方式的接入流程,如果需要完整开发内容请看 [**开发指南**](/sdk/tds-gift/guide/)。 -::: - -## 介绍 - -对接礼包兑换码需要游戏方自行维护一到两个 Web 接口是比较繁琐的,固我们简化兑换的流程为: - -**游戏方请求兑换接口进行兑换,根据接口返回的信息判断兑换是否成功。** - -这样游戏方只需调用一个接口就能完成对接。 - -:::tip -客户端直接收到兑换结果是可以被截取的,所以此方式安全系数没有其他方式高。 -::: - -## 对接 - -### 兑换接口 - -域名按开发者中心分为两个地址,两个地址的礼包数据是不互通的: - -| TapTap 版本 | 域名 | -| --------------- | ------------------------- | -| 国内 taptap.cn | https://poster-api.xd.cn | -| 海外 taptap.io | https://poster-api.xd.com | - -兑换接口参数在[**开发指南 - 无服务器兑换**](/sdk/tds-gift/guide/#无服务器兑换接口)中有详细介绍。 - -发起兑换时需要生成签名,无服务兑换接口签名是由 clientId 等信息计算出来的,签名计算过程见[**文档**](/sdk/tds-gift/guide/#签名)。 - -### 返回值 - -#### 结果 - -根据返回值中的 error 字段是否为 0 来判断兑换行为是否成功,当 error 字段判断不为 0 时可以根据[**错误码**](/sdk/tds-gift/guide/#错误码)来查看具体原因。 - -#### 验证 - -为了防止返回信息未被窜改,建议您做以下措施: -1. 为防止重放攻击您需要校验下返回时间戳是否与客户端时间相差过多; -2. 根据返回值计算出签名,验证是否与返回值里的签名相同,签名计算过程见[**文档**](/sdk/tds-gift/guide/#签名); -2. 建议奖品内容的 name 可以是加密后的名称。 - -#### 奖品内容 - -根据返回值中的 content 或者 content_obj 字段都可以获取该兑换码对应的奖品内容,而区别是 content 是一段嵌套的 json 字符串,直接按对象方式解析可能会报错。 - -其中奖品内容设置是不限中英文的,为了方便开发判断建议最好维护一份道具表。 - -## 其他 -### 开发环境 - -使用无服务器兑换接口区分测试正式环境需在开发者中心新建不同应用来完成。 - -如果无法分应用处理环境问题建议使用[无二次校验兑换接口](/sdk/tds-gift/guide/#无二次校验兑换接口)通过配置服务器信息,配置服务器 code 转发到不同环境完成兑换。 - diff --git a/docs/sdk/update/_category_.json b/docs/sdk/update/_category_.json deleted file mode 100644 index c950de2f0..000000000 --- a/docs/sdk/update/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "更新唤起", - "collapsed": true, - "position": 2 -} diff --git a/docs/sdk/update/faq.mdx b/docs/sdk/update/faq.mdx deleted file mode 100644 index 999183a19..000000000 --- a/docs/sdk/update/faq.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 3 ---- - -### 弹窗提示『请求失败,请重试』 - -可以参考[集成前准备](/sdk/update/guide/#集成前准备)检查是否已经提交 APK 并通过审核,以及发布设置是否为立即上线状态。 - - -### 从 v3.23.0 以前的版本更新到新版本的升级流程 -将原先 `updateGame` 相关的接口代码删除,并重新按 [sdk-获取](/sdk/update/guide/#sdk-获取)的流程接入 - -### Unity 唤醒更新会增加哪些 Android 依赖库 -首先 Unity 唤醒更新只会在 **Android** 平台生效,其次唤醒更新新加的依赖库会在打包过程中自动添加到 [Unity Gradle Template](https://docs.unity3d.com/Manual/gradle-templates.html) 中,您可以到 `唤醒更新模块/Mobile/Editor/TapTapUpdateDependencies.xml` 中查看到,具体来说会增加 3 个依赖库: -1. com.squareup.okhttp3:okhttp:3.12.1 -2. androidx.core:core:1.6.0 -3. com.google.android.flexbox:flexbox:3.0.0 - -## 单机游戏没有服务器,如何接入该功能? - -单机游戏暂不需要接入更新唤起功能。 - -## 更新唤起功能支持 iOS 版本吗? - -受限于苹果政策,iOS 平台的 TapTap 客户端无法提供游戏更新功能,更新唤起能力仅支持 Android 平台使用。 - -## 更新唤起功能是否只适用于强制更新的游戏版本? - -游戏在强更场景必须使用「更新唤起」服务,**当游戏需要包体强更时,请求 SDK 更新接口,唤起 TapTap 更新即可**,同时平台也不会干涉游戏自身的热更操作,如果游戏在非强更场景,也希望用户更新的话,可以尝试在公告等场景对用户进行提醒。 - -## 接入更新唤起功能后,如何进行测试? - -建议在测试环境中验收接入效果,具体测试流程如下: - -1. 需模拟游戏实际更新场景,测试的包体版本号(Version Code)需低于游戏在服务器上设置的最新版本; -2. 当打开版本号较低的测试包体时,游戏需自行获取服务器上的版本号并能与当前测试包体的版本号进行判断对比; -3. 判断当前测试包体版本号较低的情况下,能成功调起更新唤起功能接口; -4. 功能验证完成。 - -## 接入更新唤起游戏打包报 `Android resource linking failed,AAPT:error:resource android:attr/xx/xx not found.` 异常 - -检查项目打包时 compileSdkVersion 版本号,该异常时 Gradle 版本和 compileSdkVersion 匹配问题导致,建议将 compileSdkVersion 升级到 28 或更高版本。 - -## 接入更新唤起游戏打包报 `getUpdateInfo error: org.json.JSONExcetion: {"date": "{xxxx}"}` 异常 - -检查项目是否开启了混淆操作,TapSDK 已经做了混淆处理,再次混淆会导致不可预期的错误,请在项目的混淆脚本中添加如下配置,跳过对 TapSDK 的混淆操作: -```java --keep class com.tds.** { *;} --keep class com.taptap.** { *;} --keep class com.tapsdk.** { *;} --keep class tds.androidx.** { *;} -``` - -### 引入 TapUpdate 模块后闪退报错:Unable to get provider com.taptap.services.update.TapUpdateFileProvider,具体表现是弹出「更新游戏,需安装 TapTap 客户端」页面,点击「安装 TapTap」没有相应。 - -检查游戏是否存在二次打包的情况,检查二次打包后 APK 中的 AndroidManifest.xml 文件中如下的 `provider` 标签里面的 `android:authorities` 字段值是否为 `项目包名.com.taptap.services.update.fileProvider`,如果不是的话,请手动更正。 - -```xml - - - - -``` \ No newline at end of file diff --git a/docs/sdk/update/features.mdx b/docs/sdk/update/features.mdx deleted file mode 100644 index a8c75de91..000000000 --- a/docs/sdk/update/features.mdx +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: 更新唤起 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -## 功能介绍 - - -- 更新唤起服务主要应用于在 TapTap 国内商店分发的游戏包体更新场景,目前仅支持安卓。 -- 使用更新唤起功能,需接入 TapSDK,开发者在判断游戏需要强更后调用 TapSDK 更新接口,就会唤起 TapTap 客户端,引导用户前往 TapTap 完成更新。 - -## 接入优势 -- **节省开发及流量成本**:游戏接入更新唤起功能后,无需自行开发 APK 包更新流程,同时也可以节省下载新版本所需要付出的流量成本。 - -- **帮助玩家规范游戏更新流程,提升游戏热度**:TapSDK 为玩家提供了标准化的更新流程,与苹果应用商店等更新流程体验保持一致。同时游戏的更新下载热度是影响 TapTap 热门榜单的因素之一,用户前往 TapTap 更新后,在包体强更的节点时将有机会在热门榜中露出,获得更多流量曝光,帮助游戏吸引新老用户的新增与回流。 - - -## 接入要求 - -- 游戏需保持全网更新方式的统一性,平台不会干涉游戏自身的热更操作,**游戏只需要在包体强更时,请求 SDK 更新接口,唤起 TapTap 更新即可**。 - -- 开发者需确保**在 TapTap 商店的游戏包体是最新版本**,否则会导致用户打开 TapTap 后发现商店版本较低,影响用户体验。 - -- 针对已上架的游戏,开发者需确保**更新资料版本中的包名和已上架的游戏包名保持一致**,否则会导致玩家因包名不一致而更新失败。 - -- 针对新游戏,开发者需要在 TapTap 上线一个包含 APK 包**(用于平台获取游戏包名,包名需与后续更新的包名保持一致)**的商店资料版本,如果 APK 包当前无法对外,可将发布状态设置为「敬请期待」或「预约」。 - - -## 游戏接入建议 -**图示 UI 仅供参考,非 TapSDK 提供** - -1、**包体强更场景**:玩家必须更新到最新包体才可以玩游戏。 -- 游戏判断当前包体需强更时,在 APP 启动或者用户登陆成功之后,打开强更弹窗(如下图),告知玩家当前版本过低,当用户点击「更新」按钮时,则直接调用 TapSDK 更新接口。点击「取消」时,则关闭游戏或者提示玩家“不更新至最新版本,游戏无法玩”。 -![](https://capacity-files.lcfile.com/ESiXBqnxhUDN8r7IteGSnPGcuFk14pyq/01.png) - -2、**普通更新场景**:游戏内支持热更,或者玩家无需更新到最新包体也可以玩游戏。 -- 平台不要求开发者在普通更新场景去请求 TapSDK 更新接口,对玩家进行强制更新引导,开发者如果需要玩家更新,可在公告等其他场景进行弱引导提醒即可。 - - - -## 更新唤起流程说明 - -游戏调用 TapSDK 更新接口后,TapSDK 会检查当前设备是否已安装 TapTap 客户端来进入不同的更新流程。 - -### 玩家已安装 TapTap 客户端 - -- TapSDK 会直接唤起 TapTap 客户端,并跳转至游戏更新页面,引导用户完成更新。在唤起 TapTap 客户端的时候,设备可能会有打开第三方 APP 的提示(不同设备提示样式会有差异,下图仅为参考),用户需同意后,才可打开 TapTap,然后根据页面引导完成游戏更新即可。 - -![](https://capacity-files.lcfile.com/7TN6cV9vEljCHNOpIa01oBbPk4m3za1D/02.png) - -### 玩家未安装 TapTap 客户端 - -- TapSDK 如果检查当前设备未安装 TapTap 客户端,首先会询问用户“是否需要使用 TapTap 更新”(见下图,该弹窗由 TapSDK 提供)。 - -![](https://capacity-files.lcfile.com/Tc9HkqxzgmWUVq5tcqbsece4R0QBMUd4/03.png) - -- 用户确认更新后,则会开始直接下载 TapTap 客户端,下载完成后会自动向设备请求安装,用户根据设备引导完成安装即可(见下图)。 - -![](https://capacity-files.lcfile.com/05g1cDq1tLjDnHmmODMmbKUFHy0NzyS0/update-03.png) - -- TapTap 客户端下载并安装成功后,将会继续跳转 TapTap 更新游戏页面,有部分安卓设备会引导用户返回游戏,SDK 内也会提示用户“打开 TapTap 去更新“(见下图,该弹窗由 TapSDK 提供)。用户打开 TapTap 后,根据页面引导完成游戏更新即可。 - -![](https://capacity-files.lcfile.com/wuprYiOr9iBSe79U5pyG3NOuEakVTxc3/update-04.png) - -- 针对以上流程,用户如果选择关闭弹窗或取消等中断更新游戏的行为,TapSDK 会提供相关的回调接口,方便开发者接管用户后续行为。比如当用户取消「使用 TapTap 更新」TapSDK 会告知游戏更新失败。 - -## 最佳实践 - -### 月圆之夜 -游戏启动后,告知玩家需要更新到新版本,点击更新,唤起 TapTap 的流程。 -
    - -
    - -### 香肠派对 - -通过公告形式提醒玩家强制更新,但是更新后会给玩家相应奖励,鼓励玩家去更新。 -![](https://capacity-files.lcfile.com/9OslzFsggaPsGkp5BEObRyqbhxgmATkf/04.png) - -### 史莱姆连接 - -检测到玩家未安装 TapTap 客户端时,玩家选择更新,下载并安装 TapTap 客户端完成更新的流程。 - -
    - -
    - diff --git a/docs/sdk/update/guide.mdx b/docs/sdk/update/guide.mdx deleted file mode 100644 index aef1523aa..000000000 --- a/docs/sdk/update/guide.mdx +++ /dev/null @@ -1,495 +0,0 @@ ---- -title: 更新唤起开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; -import AndroidFaq from "../_partials/android-package-visibility.mdx"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -:::info -本文档只适用于国内版本,海外版本请参考 [文档](/tap-update-old-guide/) -::: -## 环境要求 - - - -<> - -- TapSDK **3.23.0** 及以上版本 -- Unity 2019.4 或更高版本 -- Android 5.0(API level 21)或更高版本 - -
    - -点击展开 Unity 2022.3 之前的版本升级 Gradle 版本 - -为了将 [Gradle 版本和 Android Gradle Plugin 版本对应](https://developer.android.com/studio/releases/gradle-plugin#expandable-1),需要更新 Gradle 版本,下载 [6.7.1 版本的 Gradle](https://services.gradle.org/distributions/gradle-6.7.1-bin.zip),解压后放到自定义的文件夹中,同时**不**勾选 Unity 中的 `Preferences` -> `External Tools`-> `Android` -> `Gradle Installed with Unity(recommend)`,改为选择解压后 Gradle 文件夹的位置,如 /gradle-6.7.1。 - -另外, Unity 更新唤醒模块会**自动**更新项目中使用的 Android Gradle Plugin 版本,如果您需要手动更改或者查看,可以在 `Project Settings` -> `Player` -> `Android Tab` -> `Publish Settings` -> `Build`,然后勾选**Custom Base Gradle Template** - -将以下更改应用于生成的这个文件: -**Assets/Plugins/Android/baseProjectTemplate.gradle** - -修改文件内容: - -```groovy -dependencies { - // 将版本修改至少为 4.2.0 - classpath 'com.android.tools.build:gradle:4.2.0' -} -``` - -
    - - - -<> - -- TapSDK **3.23.0** 及以上版本 -- Android 5.0(API level 21)或更高版本 - - - -<> - -- 受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能 - - - -<> - -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - -
    - -## 权限说明 - - - -<> - - - -<> - -该模块依赖权限如下: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于正常网络访问 | 用户首次使用该功能时会申请权限 | -| 安装 APK 权限 | 用于安装 Tap 客户端 | 用户首次使用该功能时会申请权限 | - -同时该模块也会访问设备已安装的 Tap 客户端信息,所以接入 SDK 后将在应用 `AndroidManifest.xml` 中添加如下配置: - -```xml - - - - - - -``` - - - -<> - - - -<> - - - - -## 集成前准备 - -使用更新唤起功能前提需要通过 **TapTap 开发者中心 > 商店 > 游戏资料 > 商店资料** 中已经上传 APK, 发布设置为 **立即上线** 并通过 **审核**(开发者包如果暂时不想对外,发布状态选 **敬请期待** 或者 **预约**)。 - - - -## SDK 获取 - - - -<> - -SDK 可以**通过 Unity Package Manager 导入或手动导入**,二者任选其一。请根据项目需要选择。 - -#### 方法一:使用 Unity Package Manager - -从 3.29.1 版本开始, SDK 修改 JSON 解析库为 `Newtonsoft-json`,如果当前工程已接入该依赖库,则不需额外处理,否则需在 `Packages/manifest.json` 添加如下依赖: - -``` -"com.unity.nuget.newtonsoft-json":"3.2.1" -``` - - -##### NPMJS 安装 - -从 3.25.0 版本开始,TapSDK 支持了 NPMJS 安装,优势是只需要配置版本号,并且支持嵌套依赖。 - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - - - {`"dependencies":{ - "com.taptap.tds.update":"${sdkVersions.taptap.unity}", - "com.taptap.tds.common":"${sdkVersions.taptap.unity}", -}`} - - -但需要注意的是,要在 `Packages/manifest.json` 中 `dependencies` 同级下声明 `scopedRegistries`: - - -{`"scopedRegistries": [ - { - "name": "NPMJS", - "url": "https://registry.npmjs.org/", - "scopes": ["com.tapsdk", "com.taptap"] - } - ]`} - - -##### GitHub 安装 - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - - - {`"dependencies":{ - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.update":"https://github.com/TapTap/TapUpdate-Unity.git#${sdkVersions.taptap.unity}", - "com.tapsdk.androiddependencyresolver":"${sdkVersions.taptap.adr}", -} -"scopedRegistries": [ - { - "name": "TapSDK", - "url": "https://nexus.tapsvc.com/repository/npm-registry/", - "scopes": [ - "com.tapsdk", - "com.taptap" - ] - } -]`} - - -在 Unity 顶部菜单中选择 **Window > Package Manager** 可查看已经安装在项目中的包。 - -#### 方法二:手动导入 - -1. 在 [**下载页**](/tap-download) 找到 **TapSDK Unity** 下载地址,分别下载 `TapSDK-UnityPackage.zip` 。 - - 然后请[**点击下载**](https://github.com/taptap/android_dependency_resolver/releases) `TapTap_AndroidDependencyResolver.unitypackage` 。 - -2. 在 Unity 项目中依次转到 **Assets > Import Packages > Custom Packages**,从解压后的 `TapSDK-UnityPackage.zip` 中,选择希望在游戏中使用的 TapSDK 包导入,其中: - - - `TapTap_Common.unitypackage` TapSDK 基础库,必选。 - - `TapTap_Update.unitypackage` TapTap 唤起更新库,必选。 - - `TapTap_AndroidDependencyResolver.unitypackage` 必选。 - -3. 如果当前项目已集成 `Newtonsoft.Json` 依赖,则忽略该步骤,否则在 `NuGet.org` [ Newtonsoft.Json ](https://www.nuget.org/packages/Newtonsoft.Json) 页面中通过点击右侧 「Download package」 下载库文件,并将下载的文件后缀从`.nupkg` 修改为 `.zip`,同时解压该文件并复制内部的 `Newtonsoft.Json.dll` 文件拷贝到工程 `Assets` 的 `Plugins` 目录下,另外为了避免导出 IL2CPP 平台时删除必要数据,需在 `Assets` 目录下创建 `link.xml` 文件(如果已有该文件,则添加如下内容),其内容如下: - -``` - - - - - - -``` - - - -<> - -在 [下载页](/tap-download) 获得 TapSDK,添加 `TapUpdate` 和 `TapCommon` 模块。 - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapUpdate_${sdkVersions.taptap.android}', ext:'aar') - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') - implementation 'com.squareup.okhttp3:okhttp:3.12.1' - implementation 'androidx.core:core:1.6.0' - implementation 'com.google.android.flexbox:flexbox:3.0.0' -}`} - - - - -<> - -受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能 - - - -<> - -#### 安装插件 - -* 下载 [TapSDK UE4](/tap-download),TapSDK-UE4-xxx.zip 解压后将 `TapUpdate`、`TapCommon` 文件夹 Copy 到项目的 `Plugins` 目录中 -* 重启 Unreal Editor -* 打开 编辑 > 插件 > 项目 > TapTap,开启 `TapUpdate` 模块 - -#### 添加依赖 - -在 Project.Build.cs 中添加所需模块: - -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapUpdate", -}); -``` - -#### 导入头文件 - -```cpp -#include "TapUpdate.h" -``` - - - - - -## 初始化 - -:::info -以下两种初始化方式任选其一。 -::: - -### TapSDK 初始化 - -如果你已经完成[内建账户系统 Tap 登录](/sdk/taptap-login/guide/start/#sdk-初始化)的初始化,这里只需要引入更新唤起模块,不需要其他额外处理。 - -### 更新唤起单独初始化 -根据在开发者中心的应用配置,开发者调用如下接口: - - - -```cs -using TapTap.Update - -TapUpdate.Init("clientId", "clientToken"); -``` - -```java -import com.taptap.services.update.TapUpdate; - -TapUpdate.init(activity, "clientId", "clientToken"); -``` - - -受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能 - -```cpp -FTapUpdate::Init(TEXT("clientId"), TEXT("clientToken")); -``` - - - -**参数说明** - -参数 | 描述 -| ------ | ------ | -clientId | TapTap 开发者中心对应应用的 Client ID -clientToken | TapTap 开发者中心对应应用的 Client Token - -## 开始更新 - -:::tip -TapSDK 如果检查当前设备未安装 TapTap 客户端,首先会弹窗询问用户「是否需要使用 TapTap 更新」,当点击「取消」按钮时取消更新的回调方法才会执行。 -::: - - - -<> - -```cs -using TapTap.Update - -TapTap.Update.TapUpdate.UpdateGame(() => { - // 取消更新的事件 -}); -``` - - -<> - -由于更新唤起 UI 依赖于开发者设置的 Activity 实例,所以为了避免因 Activity 发生重建导致更新唤起功能不可用,开发者应确保在屏幕旋转、配置修改时当前 Activity 不会发生重建,具体设置方式参考 [限制 activity 重新创建](https://developer.android.com/guide/topics/resources/runtime-changes?hl=zh-cn#restrict-activity) - -```java -import com.taptap.services.update.TapUpdate; -import com.taptap.services.update.TapUpdateCallback; - -TapUpdate.updateGame(DemoUpdateActivity.this, new TapUpdateCallback() { - @Override - public void onCancel() { - // 取消更新的事件 - } -}); -``` - - -<> - -受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能 - - - -<> - -```cpp -FTapUpdate::UpdateGame(FSimpleDelegate::CreateLambda([]() { - // Update cancel -})); -``` - - - - - -## 打开游戏评论区 - -如果玩家设备已经安装 TapTap 客户端,会打开游戏所在 TapTap 客户端对应的评论区页面,如果玩家设备没有安装 TapTap 客户端,则打开游戏评论区失败,需要游戏侧自行处理相关逻辑。 - - - -<> - - - -```cs -// 适用于中国大陆 -TapCommon.OpenReviewInTapTap(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("打开游戏评论区成功"); - } else { - Debug.Log("打开游戏评论区失败"); - } -}); - -// 适用于其他国家或地区 -TapCommon.OpenReviewInTapGlobal(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("打开游戏评论区成功"); - } else { - Debug.Log("打开游戏评论区失败"); - } -}); -``` - - - - - -```cs -TapCommon.OpenReviewInTapGlobal(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("打开游戏评论区成功"); - } else { - Debug.Log("打开游戏评论区失败"); - } -}); -``` - - - - - -<> - - - -```java -// 适用于中国大陆 -if(TapGameUtil.openReviewInTapTap(this,"appid")){ - Log.d(TAG, "打开评论区成功"); -} else { - Log.d(TAG, "打开评论区失败"); -} - -// 适用于其他国家或地区 -if(TapGameUtil.openReviewInTapGlobal(this,"appid")){ - Log.d(TAG, "打开评论区成功"); -} else { - Log.d(TAG, "打开评论区失败"); -} -``` - - - - - -```java -if(TapGameUtil.openReviewInTapGlobal(this,"appid")){ - Log.d(TAG, "打开评论区成功"); -} else { - Log.d(TAG, "打开评论区失败"); -} -``` - - - - - -<> - -```objc -// 未支持 -``` - - - - - -appid:游戏在 TapTap 商店的唯一身份标识。 -例如:`https://www.taptap.cn/app/187168``https://www.taptap.io/app/187168`,其中 `187168` 是 `appid`。 - - - -## 测试 -为了保证上线后,游戏对于用户是否正常使用更新唤起功能,请务必按照以下说明完成自测。 - -### 上传 APK - -新应用需要上传测试的 APK 至开发者中心,并通过审核。已上架的游戏,需确保更新资料版本中的 APK 包名和已上架的 APK 包名保持一致。 - -### 应用上线 - -针对已上架的游戏,开发者需确保**更新资料版本中的包名和已上架的游戏包名保持一致**,否则会导致玩家因包名不一致而更新失败。 - -针对新游戏,开发者需要在 TapTap 上线一个包含 APK 包并且通过审核**(用于平台获取游戏包名,包名需与后续更新的包名保持一致)**的商店资料版本,如果 APK 包当前无法对外,可将发布状态设置为「敬请期待」或「预约」。 - -### 开始测试 - -触发更新唤起功能后正常状态是可以唤起应用在 TapTap 商店的详情页面。 - - - - diff --git a/docs/shadow/billboard/_category_.json b/docs/shadow/billboard/_category_.json deleted file mode 100644 index 657b5a2e6..000000000 --- a/docs/shadow/billboard/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "公告系统", - "collapsed": true, - "position": 8 -} diff --git a/docs/shadow/billboard/design-billboard.mdx b/docs/shadow/billboard/design-billboard.mdx deleted file mode 100644 index 0e986bb9e..000000000 --- a/docs/shadow/billboard/design-billboard.mdx +++ /dev/null @@ -1,424 +0,0 @@ ---- -title: 公告系统设计指南 -sidebar_position: 3 -slug: /sdk/billboard/design-billboard/ ---- - - - - - -import { Background, Figure } from "/src/docComponents/doc"; -import useBaseUrl from "@docusaurus/useBaseUrl"; - -## 公告模板的类型 -![](/img/design-billboard/1.1.png) - - -## A.导航公告 - -### 横屏配置切图尺寸 - -定制横屏公告系统需上传顶部导航栏背景、左侧标签栏卡片背景和弹窗背景图切图,切图均为三倍图 @3X 大小,文件类型:JPG. PNG. - - -
    - - -### 横屏配置定制化建议 -字符必须清晰可见。因此,当背景图为浅色时,建议使用深色描述字符;当背景图为深色时,建议使用浅色描述字符。 - - -
    - 文字颜色与背景颜色对比明确,便于阅读,同时采用了强调色和辅助色,使得整体结构清晰。 - - } - imgSrc={useBaseUrl("/img/design-billboard/2.2.png")} - imgAlt="" - /> -
    - 文字颜色与背景颜色对比模糊,不便阅读,主次颜色较为相近,整体结构层次不够明确。 - - } - imgSrc={useBaseUrl("/img/design-billboard/2.3.png")} - imgAlt="" - /> - - -### 竖屏配置切图尺寸 - -定制竖屏公告系统需上传顶部导航栏背景、标签栏卡片背景、弹窗背景图和返回上一级按钮切图,切图均为三倍图 @3X 大小,文件类型:JPG. PNG. - - -
    - - -### 竖屏配置定制化建议 -字符必须清晰可见。因此,当背景图为浅色时,建议使用深色描述字符;当背景图为深色时,建议使用浅色描述字符。 - - -
    - 文字颜色与背景颜色对比明确,便于阅读,同时采用了强调色和辅助色,使得整体结构清晰。 - - } - imgSrc={useBaseUrl("/img/design-billboard/2.5.png")} - imgAlt="" - /> -
    - 文字颜色与背景颜色对比模糊,不便阅读,主次颜色较为相近,整体结构层次不够明确。 - - } - imgSrc={useBaseUrl("/img/design-billboard/2.6.png")} - imgAlt="" - /> - - -
    -返回上一级按钮上的文字图标均为按钮切图的一部分,切图时需注意: - - -
    - 按钮切图上需包含文字或图标 icon 内容。 - - } - imgSrc={useBaseUrl("/img/design-billboard/2.7.png")} - imgAlt="" - /> -
    - 只有按钮的背景切图,按钮含义不清晰。 - - } - imgSrc={useBaseUrl("/img/design-billboard/2.8.png")} - imgAlt="" - /> - - -### 全局文本类型和字体建议 -全局文本类型按照颜色配置可分为 4 种,分别为默认文字、辅助文字、公告高亮文字和公告链接文字,字体可自行上传配置,版权问题由厂商自行承担。 - - -
    - - - -
    - 正文文字颜色清晰易读,辅助文字和公告高亮文字颜色对比明确,公告链接文字颜色使用正确。 - - } - imgSrc={useBaseUrl("/img/design-billboard/2.10.png")} - imgAlt="" - /> -
    - 正文文字颜色放在错误的背景颜色上,不易辨认,辅助文字和公告高亮文字颜色对比不够强烈,公告链接文字没有可点击跳转感。 - - } - imgSrc={useBaseUrl("/img/design-billboard/2.11.png")} - imgAlt="" - /> - - -### 全局更多可配置区域与切图尺寸 -除了字体和文字颜色,全局还有更多可定制细节如关闭按钮、小红点、空状态插图和公告状态标签,切图均为三倍图 @3X 大小,文件类型:JPG. PNG. - - -
    - - -
    - - -
    - - -
    - - -## B.开屏公告 - -### 横屏配置切图尺寸 - -定制横屏公告系统需上传**顶部导航栏背景**和**弹窗背景图**切图,切图均为三倍图 @3X 大小,文件类型:JPG. PNG. - - -
    - - -### 横屏配置定制化建议 -标题颜色与背景颜色对比明确,配置的内容图片和文本比例和谐,横屏游戏推荐图片高度不超过弹窗高度的二分之一,以便在首屏展示更多公告信息。 - - -
    - 文字颜色与背景颜色对比明确,便于阅读,同时采用了强调色和辅助色,使得整体结构清晰。 - - } - imgSrc={useBaseUrl("/img/design-billboard/3.2.png")} - imgAlt="" - /> -
    - 标题颜色与背景颜色对比模糊,不便阅读;配置的内容图片高度过高,重要文本信息无法首屏展示。 - - } - imgSrc={useBaseUrl("/img/design-billboard/3.3.png")} - imgAlt="" - /> - - -### 竖屏配置切图尺寸 - -定制竖屏公告系统需上传**顶部导航栏背景**和**弹窗背景图**切图,切图均为三倍图 @3X 大小,文件类型:JPG. PNG. - - -
    - - -### 竖屏配置定制化建议 -字符必须清晰可见。因此,当背景图为浅色时,建议使用深色描述字符;当背景图为深色时,建议使用浅色描述字符。 - - -
    - 文字颜色与背景颜色对比明确,便于阅读,同时采用了强调色和辅助色,使得整体结构清晰。 - - } - imgSrc={useBaseUrl("/img/design-billboard/3.5.png")} - imgAlt="" - /> -
    - 文字颜色与背景颜色对比模糊,不便阅读,主次颜色较为相近,整体结构层次不够明确。 - - } - imgSrc={useBaseUrl("/img/design-billboard/3.6.png")} - imgAlt="" - /> - - -### 全局文本类型和字体建议 -全局文本类型按照颜色配置可分为 4 种,分别为默认文字、辅助文字、公告高亮文字和公告链接文字,字体可自行上传配置,版权问题由厂商自行承担。 - - -
    - - - -
    - 正文文字颜色清晰易读,辅助文字和公告高亮文字颜色对比明确,公告链接文字颜色使用正确。 - - } - imgSrc={useBaseUrl("/img/design-billboard/3.10.png")} - imgAlt="" - /> -
    - 正文文字颜色放在错误的背景颜色上,不易辨认,辅助文字和公告高亮文字颜色对比不够强烈,公告链接文字没有可点击跳转感。 - - } - imgSrc={useBaseUrl("/img/design-billboard/3.11.png")} - imgAlt="" - /> - - -### 全局更多可配置区域与切图尺寸 - -除了字体和文字颜色,全局需配置**空状态插图**作为空白公告兜底图,切图为三倍图 @3X 大小,文件类型:JPG. PNG. - - -
    - - - -## C.跑马灯公告 - -### 跑马灯背景风格 - -跑马灯背景长度可配置为:**自适应铺满**和**自定义长度**,两者呈现风格也不相同。如图例: - - -
    -
    - - -### 自适应铺满可配置内容 - -可配置滚屏区域长度和跑马灯显示位置。 - -
    - - -
    - - -### 自定义长度可配置内容 - -可配置滚屏区域长度和跑马灯显示位置。 - -
    - - -
    - - -### 全局更多可配置区域与切图尺寸 - -除了位置,跑马灯还可定制文字颜色、背景颜色和公告图标,自定义公告图标需上传切图,切图为三倍图 @3X 大小,文件类型:JPG. PNG. - - -
    - - -字符必须清晰可见。自定义文字颜色和背景颜色时,需考虑到两者的对比度是否便于阅读。 - - -
    - 文字颜色与背景颜色对比明确,便于阅读。 - - } - imgSrc={useBaseUrl("/img/design-billboard/4.8.png")} - imgAlt="" - /> -
    - 文字颜色与背景颜色对比模糊,不便阅读。 - - } - imgSrc={useBaseUrl("/img/design-billboard/4.9.png")} - imgAlt="" - /> - - -## D.图片公告 - -相关配置文档敬请期待~ diff --git a/docs/shadow/billboard/faq.mdx b/docs/shadow/billboard/faq.mdx deleted file mode 100644 index 09c9cc65d..000000000 --- a/docs/shadow/billboard/faq.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 3 -slug: /sdk/billboard/faq/ ---- - - - - - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -### 可以在同一个游戏内使用多个公告模板吗? - -可以,同一个游戏可以使用「导航公告」、「跑马灯公告」等 - - -### 跳转链接是否可以在游戏内打开? - -支持 - -### 分发维度选择「平台」为「iOS」,地区为「中国大陆」,区服为「S1 区」「S2 区」,那用户侧的效果是什么样的? - -这条公告只有同时满足 iOS + 国服 + S1 和 S2 的用户才能看到,Android 的用户看不到,S3 和 S4 的用户看不到 - - -### 现在只提供 3 种公告模板吗? - -后续会推出更多常用的公告模板,如果需要定制化的,建议开发者使用 API 的方式接入 - - -### 跑马灯公告和其他模板同时展示,会不会互相覆盖? - -TDS 提供了关闭跑马灯的接口,对于两者能否需要同时展示,游戏可以自行处理 - - -### 跑马灯公告在播放时,用户关闭游戏进程怎么办?那用户要怎么关闭跑马灯公告? - -游戏可以在切换场景时,自动关闭跑马灯公告。开发者可以在后台,把这条公告直接隐藏或是删除。 - - -### 公告类型可以自定义配置吗? - -目前暂不支持 diff --git a/docs/shadow/billboard/features.mdx b/docs/shadow/billboard/features.mdx deleted file mode 100644 index ed7bc48d9..000000000 --- a/docs/shadow/billboard/features.mdx +++ /dev/null @@ -1,254 +0,0 @@ ---- -title: 公告系统功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 -slug: /sdk/billboard/features/ ---- - - - - - -import { Conditional } from "/src/docComponents/conditional"; - -## 产品介绍 - -开发者在开发者中心发布编辑公告内容,玩家在打开游戏时可以收到公告通知。 - -## 核心优势 - -- 节省成本:减少开发成本 -- 运营工具:游戏运营日常高频使用到的内容发布工具 -- 个性配置:可以自行上传主题样式,配置多维度分发内容,不需要二次研发 -- 兼容性强:WebView 界面比游戏引擎绘制的界面更好地兼容图文排版 -- 维护方便:不依赖于其他服务支持,且更新不需要升级 SDK - -## 接入准备 - -- 开通权限:请至「权限管理」中给该账号设置「游戏管理员」权限 -- 依赖服务:开发者需要给公告配置一个自定义域名,确保公告可以被正常访问到 -- 设计样式:需要设计师根据 **[《公告系统设计指南》](../design-billboard/)** 准备好公告面板的素材 -- 支持版本:iOS / Android 最低支持版本 TapSDK 3.17.0,Unity 和 UE 最低支持版本 TapSDK 3.17.0,**同时支持移动端和 PC 端** - -## 功能介绍 - -### 业务流程 - -![](/img/billboard/liuchengtu.png) - -### 开通服务 - -开发者可以在 **开发者中心 > 游戏服务 > 应用配置** 找到公告系统,点击立即开通服务。 - -![](https://capacity-files.lcfile.com/Nbfo8oQW6K7M1wOFTpJ0Osy8p6Lwj9CA/open_billboard.png) - - - -### 域名配置 - -为了确保公告系统正常访问和安全运作,TDS 建议开发者绑定自己公司的域名,绑定域名流程可查阅 **[《域名配置指南》](/sdk/domain/guide/)**。 - -![](https://capacity-files.lcfile.com/Az9ieoDfeykXfNUIjWcUyxoTFxYuRBmk/bond_url.png) - - - -### 模板配置 - -TDS 为开发者提供了多种公告模板,可以在游戏内任意位置唤起不同样式的公告面板,结合游戏自身画风自定义上传样式进行 UI 换肤。 - -- **导航公告**:适用于公告数量较多和发布频次较高的游戏 - -- **开屏公告**:游戏维护和停服期间,打开游戏后立即可见,且不能手动关闭 - -- **跑马灯公告**:游戏主界面上的滚屏通知,不会干扰用户玩游戏 - -- **图片公告**:适用于公告数量较少和需 Banner 设计的游戏 **(暂未开放)** - -:::tip - -TDS 提供了一套默认风格,并支持恢复到默认配置,样式设计可以参考 **[《公告系统设计指南》](../design-billboard/)** - -::: - -![](https://capacity-files.lcfile.com/cdC8h0yFuvMT968oJQLrnQOu3yobs2tx/bond_des.png) - -### 导航公告 -导航公告可配置如下 -- 全局通用配置:默认文字颜色、辅助文字颜色、高亮文字颜色、链接文字颜色、字体、小红点、关闭按钮、空状态、导航分类标签 -- 横屏游戏必配:顶栏背景图、内容背景图、标签栏默认背景图、标签栏选中背景图 -- 竖屏游戏必配:顶栏背景图、内容背景图、标签栏默认背景图、返回按钮 - -:::tip - -1、导航公告也能作为开屏公告使用,可满足用户进入游戏后直接拉起公告面板 - -2、上传的切图大小尽量控制在 **100 KB** 以内,可以加快公告打开速度 - -3、上传的字体大小尽量控制在 **5 MB** 以内,可以快速加载字体 - -::: - -![](https://capacity-files.lcfile.com/g5Cbic22VtBfa4BhkzKOlD3IJPDhtpbx/edit_mod.png) - -**关于导航分类标签配置** - -为了区分不同公告类型的优先级程度,开发者可以自定义配置公告标签样式,TDS 默认提供了 6 种样式可以在不同的场景下使用。此处配置不强行要求,可以为空展示。 - -![](/img/billboard/billboard_label.png) - - -### 开屏公告 -开屏公告可配置如下 - -- 全局通用配置:标题文字颜色、默认文字颜色、高亮文字颜色、链接文字颜色、字体、空状态 -- 横屏游戏需配:滚屏区域长度、内容背景长度、公告显示位置 -- 竖屏游戏需配:滚屏区域长度、内容背景长度、公告显示位置 - -:::tip - -1、开屏公告没有关闭按钮,仅满足在特殊场景下打开,请谨慎使用 - -2、上传的切图大小尽量控制在 100 KB 以内,可以加快公告打开速度 - -3、上传的字体大小尽量控制在 5 MB 以内,可以快速加载字体 - -::: - -![](https://capacity-files.lcfile.com/qSeRQovHTQqlPLafYmYz21uJaVpWhRJu/billboard_screen.png) - - -### 跑马灯公告 -跑马灯公告可配置如下 - -- 全局通用配置:默认文字颜色、内容背景颜色、链接文字颜色、字体、公告图标、循环次数 -- 横屏游戏必配:顶栏背景图、内容背景图 -- 竖屏游戏必配:顶栏背景图、内容背景图 - -:::tip - -1、跑马灯可自定义长度,长度范围是最小 150 px,最大 500 px - -2、跑马灯可以在界面上任意位置,可以通过调整偏移距离来控制 - -3、上传的字体大小尽量控制在 2 MB 以内,利于引擎快速加载字体 - -4、关于字体兼容 Unity PC 版本,需要使用与游戏构建相同的 Unity Editor 版本,避免不同版本的 AssetBundle 不兼容的问题 - -::: - -![](https://capacity-files.lcfile.com/LtI6SDLyPBmTpVaPU4w03vKjgUq4b8X4/billboard_paomadeng.png) - - -### 维度配置 - -维度配置是为了帮助开发者更好地进行**公告分发**,开发者可以根据游戏的发行维度需求,自定义分发维度。 - -- 预设维度:初始提供 2 个预设维度「渠道」和「地区」,不可编辑和删除。 - - 「渠道」初始默认预设字段「iOS」「Andriod-TapTap」共 2 个 - - 「地区」初始默认预设字段「中国大陆」「中国澳门」「中国香港」「中国台湾」「日本」「美国」「韩国」「印度尼西亚」「泰国」共 9 个 -- 自定义维度:最多添加 10 个自定义维度,可以编辑和删除,维度的参数和描述不可重命名。 - - 每个维度下,可以自定义添加字段 - - 字段的参数和描述不可重命名 -- 推荐维度:「版本号」「区服」「环境」「包体」「平台」 - -:::caution - -1、字段参数支持数字、英文、下划线,区分大小写,长度为 20 个字符 - -2、字段作为前后端传参的依据,建议规范命名,不可重命名 - -3、已创建的维度和字段不能修改参数,建议游戏封包前全面测试,游戏上线后切勿随意删改 - -::: - -![](https://capacity-files.lcfile.com/RrcOGH1moiD01XnT7GwLyMOHmiaF7nCU/weidu_config.png) - -### 发布公告 - -发布公告需要填写公告内容和公告设置,支持立即**提交发布**,也能**保存草稿**。草稿状态下的公告不会对外展示。另外,已发布的公告不可存为草稿,但可以单独对已发布的公告进行**显示**或**隐藏**。 - -公告内容需要填写:长标题、短标题(仅在导航模板中展示)、公告正文,并支持多语言配置 - -![](https://capacity-files.lcfile.com/Ia6CpGTfD2Ae6f0JJ97sqlVUEzO067ht/release_billboard_content.png) - -公告设置需要选择:可见范围、公告类型、跳转位置、分发维度、定时设置 - -- 可见范围:可以将该条公告内容关联到某一个模板,目前可选导航模板 -- 公告类型:可以选择更新公告、维护公告、上新公告、活动公告、玩法公告、测试公告、停服公告 -- 跳转位置:**公告正文**、**链接地址**、**游戏内模块** -- 分发维度:可以**不限制维度**分发,也可以根据需求选择**多个维度**,每个维度下可以选择多个选项 -- 定时设置:可以设置公告**定时发布**和**定时隐藏** - -:::tip - -1、从公告跳转到游戏内模块,开发者可以自定义回调地址,自行处理,公告系统不会限制游戏跳转的业务场景 - -2、公告发布后可以修改维度 - -3、已隐藏的公告再重新提交发布后会自动显示 - -::: - -![](https://capacity-files.lcfile.com/N2FWeddK8DMVFNl7djj21KDVTxWQoMwJ/release_billboard_config.png) - -### 公告排序 - -**公告排序**可以通过编辑数字来控制公告在 SDK 侧边导航展示的顺序,数字越大排序越靠前 - -可以通过编号、状态、可见范围、地区、渠道、长标题和公告发布时间段来**筛选公告** - -![](https://capacity-files.lcfile.com/j0jtOJSNvdI3S1LC0J47Aoj10s0Uv8nx/billboard_list.png) - -### 复制公告 - -开发者可以将现有已编辑完的**公告复制**到本游戏或者其他游戏,作为草稿使用,便于重复运用模板,另外公告下的维度配置是不支持复制的,复制后会默认置为不限维度 - -![](https://capacity-files.lcfile.com/WYFIrx6FOjwbHby3pfxWLWHeKpeSLQhY/copy_billboard.png) - -### 展示公告 - -#### 导航公告 - -红点逻辑:「游戏公告 Tab」和「活动公告 Tab」下至少有一条未读消息时会显示红点,若公告消息都为已读状态,则红点消失 - -数据上限:导航模板下会展示最新发布的 20 条公告 - -公告详情:可高亮展示文案,展示图片,播放视频,文字链跳转。横屏状态会自动展开第一条公告详情,竖屏状态会先展示公告列表,进入公告详情可返回到公告列表 - -跳转说明:链接支持外部浏览器打开页面,或是跳转到游戏内模块,或是跳转到公告详情 - -![](https://img.tapimg.com/market/images/a002825ac59f0c0456ff180afe9d6899.png) - -![](https://img.tapimg.com/market/images/ff9b021f99609e6d45445e92ab59e6bb.png) - - -#### 开屏公告 - -展示逻辑:展示「可见范围」为开屏维护公告的数据,且状态为「已显示」,若存在多条数据,则按照「公告排序」数值最大的展示,且仅展示一条公告数据。公告标题会拉去「长标题」字段数据,公告正文会拉去「正文」字段数据,并渲染、视频和链接 - -展示次数:不限制开屏公告的展示次数 - -关闭逻辑:开屏公告显示时间范围是「发布时间」到「结束之间」内,超过结束时间后,弹窗自动关闭。公告弹窗要覆盖在游戏主界面上,且不可以被用户手动关闭,游戏可以自己控制关闭公告 - -数据更新:调用一次数据后,不会实时更新内容数据。如,开发者在后台修改公告内容,用户需要关闭进程,重新进入游戏才会拉取新的数据。若开发者在后台修改了公告显示时间,用户看到开屏公告自动关闭,用户点击进入游戏,此时需要注意,游戏的服务端需要返回一个结果,如「游戏正在维护中」 - -![](https://capacity-files.lcfile.com/FQe8hAK4GknzfRS779MtfqObDP5BvIPP/open_mod.png) - -![](https://capacity-files.lcfile.com/3F05L7i4MTWO8LasBFbMfOixbJkAeNTY/open_mod_shuping.png) - - -#### 跑马灯公告 - -展示逻辑:展示「可见范围」为跑马灯公告的数据,且状态为「已显示」,若存在多条数据,则每条按照「公告排序」数值从大到小依次循环播放,单条公告按照轮询次数播放。公告正文会拉去「正文」字段数据,不会渲染图片,视频,链接,纯文本数据。 - -轮询次数:跑马灯公告的轮询次数随模版走,而不是随单条公告。一个游戏只有一个跑马灯公告配置。当开发者修改了跑马灯公告模板配置时,下次初始化才会拉取最新的模版配置。举个例子,当运营配置了轮播此时为 「5」,此时发布了公告 A 和公告 B,公告 A 单条播放 5 次后公告 B 才会单挑播放 5 次 - -关闭逻辑:显示时间是「发布时间」(存在轮询延迟,本地通过轮询方式,间隔 5 min 拉取,后端对应接口获取数据),跑马灯播放完后会需要自动关闭(后台设置的隐藏时间不会影响,但是隐藏状态的跑马灯无法被展示出来),游戏可以控制关闭跑马灯公告,譬如一些进入游戏对局后切换的场景。跑马灯公告不可以被用户手动关闭。 - -![](https://capacity-files.lcfile.com/pxHQH19aBjk0NXBD5Y4wRIFdHjNzfefm/paomadeng_mod.png) - - - -#### 图片模板 - -目前暂未开放 diff --git a/docs/shadow/billboard/guide.mdx b/docs/shadow/billboard/guide.mdx deleted file mode 100644 index 0a9aff7e3..000000000 --- a/docs/shadow/billboard/guide.mdx +++ /dev/null @@ -1,1285 +0,0 @@ ---- -title: 公告指南 -sidebar_label: 开发指南 -sidebar_position: 2 -slug: /sdk/billboard/guide/ ---- - - - - - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import Languages from '../../sdk/_partials/languages.mdx'; -import { Conditional } from "/src/docComponents/conditional"; -import UnitySDKInstallation from "../../sdk/_partials/unity-sdk-installation.mdx"; - -开发者在开发者中心发布编辑公告内容,玩家在打开游戏时可以收到公告通知。 - -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - - - -## 集成前准备 - -1. 接入 TDS 公告服务 SDK 之前,请参考[《功能介绍》文档](../features/#功能介绍)完成**公告系统开通、域名配置**。 - - -## SDK 配置 - -可以在 [下载页](/tap-download) 获得 TapSDK,引入公告模块。 - - - -<> - - - -如需支持 PC 平台,需要额外下载 [3D WebView for Windows and macOS (Web Browser)](https://assetstore.unity.com/packages/tools/gui/3d-webview-for-windows-and-macos-web-browser-154144) 插件及公告模块 PC 平台支持插件([Tap SDK](https://github.com/taptap/TapSDK-Unity/releases) 中 TapTap_Billboard_XXX_Standalone.unitypackage,XXX 为版本号) - - - -<> - - -{`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - - implementation (name:'TapBillboard_${sdkVersions.taptap.android}', ext:'aar') // TapTap 公告系统 - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') -}`} - - - - -<> - - -{`// 公告系统 -TapBillboardResource.bundle -TapCommonResource.bundle -TapBillboardSDK.framework -TapCommonSDK.framework -`} - - - - -<> - -#### 安装插件 - -* 下载 [TapSDK UE4](/tap-download),TapSDK-UE4-xxx.zip 解压后将 `TapCommon`、`TapBillboard` 文件夹 Copy 到项目的 `Plugins` 目录中 -* 重启 Unreal Editor -* 打开 编辑 > 插件 > 项目 > TapTap,开启 `TapBillboard` 模块 - -#### 添加依赖 - -在 Project.Build.cs 中添加所需模块: - -```cs -PublicDependencyModuleNames.AddRange(new string[] { - // ... ... 添加你依赖的其他模块 - "TapCommon", - "TapBillboard" -}); -``` - -#### 导入头文件 - -```cpp -#include "TapBillboardModule.h" -#include "TapBillboardCommon.h" - -``` - - - - - - - -## 初始化 - -:::info -以下两种初始化方式任选其一。 -::: - -### TapSDK 初始化 - -如果你已经完成[内建账户系统 Tap 登录](/sdk/taptap-login/guide/start/#sdk-初始化)的初始化,这里只需要引入公告模块,然后添加下面初始化代码中高亮的部分到项目中即可。 - - -**注意:公告配置中 ServerUrl 必须为完整的包含 https:// 的链接地址,不能只设置域名。** - - - -<> - -```cs -using TapTap.Common; -using TapTap.Bootstrap; -// highlight-next-line -using TapTap.Billboard; - -// highlight-start -var dimensionSet = new HashSet>(); -KeyValuePair platformPair = new KeyValuePair("platform", "TapTap"); -KeyValuePair locationPair = new KeyValuePair("location", "CN"); -dimensionSet.Add(platformPair); -dimensionSet.Add(locationPair); -var templateType = "navigate"; // 可选 -var billboardServerUrl = "https://your-billboard-server-url"; // 开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置 > 公告 , 这里必须包含 https:// -// highlight-end - -var config = new TapConfig.Builder() - .ClientID("your_client_id") // 必须,开发者中心对应 Client ID - .ClientToken("your_client_token") // 必须,开发者中心对应 Client Token - .ServerURL("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > 云服务 API - .RegionType(RegionType.CN) // 可选项,CN 表示中国大陆,IO 表示其他国家或地区 - // highlight-next-line - .TapBillboardConfig(dimensionSet, templateType, billboardServerUrl) - .ConfigBuilder(); -TapBootstrap.Init(config); -``` - - - -<> - -**注意:Android SDK 初始化需要放在 Android 主线程中进行。** - - - -国内版初始化: - -```java -// highlight-start -Set> dimensionSet = new HashSet<>(); -dimensionSet.addAll(Arrays.asList(Pair.create("location", "CN"), Pair.create("platform", "TapTap"))); -String billboardServerUrl = "https://your-billboard-server-url"; // 开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置 > 公告, 这里必须包含 https:// - -TapBillboardConfig billboardCnConfig = new TapBillboardConfig.Builder() - .withDimensionSet(dimensionSet) // 可选 - .withServerUrl(billboardServerUrl) // 必须,公告的自定义域名地址,这里需为包含 https:// 的完整地址 - .build(); -// highlight-end - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(gameMainActivity) // 必须,传入游戏主 Activity - .withClientId("your_client_id") // 必须,开发者中心应用配置里的 Client ID - .withClientToken("your_client_token") // 必须,开发者中心应用配置里的 Client Token - .withServerUrl("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > 云服务 API - // highlight-next-line - .withBillboardConfig(billboardCnConfig) // 必须 - .withRegionType(TapRegionType.CN) - .build(); -TapBootstrap.init(gameMainActivity, tapConfig); -``` - -海外版初始化: - - - -```java -// highlight-start -Set> dimensionSet = new HashSet<>() -dimensionSet.addAll(Arrays.asList(Pair.create("location", "XX"), Pair.create("platform", "TapTap"))); - -TapBillboardConfig billboardIntlConfig = new TapBillboardConfig.Builder() - .withDimensionSet(dimensionSet) // 可选 - .build(); -// highlight-end - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(gameMainActivity) // 必须,传入游戏主 Activity - .withClientId("your_client_id") // 必须,开发者中心应用配置里的 Client ID - .withClientToken("your_client_token") // 必须,开发者中心应用配置里的 Client Token - .withServerUrl("https://your_server_url") // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > 云服务 API - // highlight-next-line - .withBillboardConfig(billboardIntlConfig) // 必须 - .withRegionType(TapRegionType.IO) - .build(); -TapBootstrap.init(gameMainActivity, tapConfig); -``` - - - -<> - - - -国内版初始化: - -```objectivec -// highlight-start -NSMutableSet *dimensionSet = [[NSMutableSet alloc] init]; -[dimensionSet addObject:[NSArray arrayWithObjects:@"platform", @"ios", nil]]; -[dimensionSet addObject:[NSArray arrayWithObjects:@"location", @"CN", nil]]; - -TapBillboardConfig *billboardCnConfig = [TapBillboardConfig new]; -billboardCnConfig.diemensionSet = dimensionSet; // 可选项 -billboardCnConfig.serverUrl = @"https://your-billboard-server-url"; // 开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置 > 公告, 这里必须包含 https:// -// highlight-end - -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // 必须,开发者中心对应 Client ID -config.clientToken = @"your_client_token"; // 必须,开发者中心对应 Client Token -config.region = TapSDKRegionTypeCN; // TapSDKRegionTypeCN:中国大陆,TapSDKRegionTypeIO:其他国家或地区 -config.serverURL = "https://your_server_url"; // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > 云服务 API -// highlight-next-line -config.tapBillboardConfig = billboardCnConfig; -[TapBootstrap initWithConfig:config]; -``` - -海外版初始化: - - - -```objectivec -// highlight-start -NSMutableSet *dimensionSet = [[NSMutableSet alloc] init]; -[dimensionSet addObject:[NSArray arrayWithObjects:@"platform", @"ios", nil]]; -[dimensionSet addObject:[NSArray arrayWithObjects:@"location", @"XX", nil]]; - -TapBillboardConfig *billboardIntlConfig = [TapBillboardConfig new]; -billboardIntlConfig.diemensionSet = dimensionSet; // 可选项 -// highlight-end - -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // 必须,开发者中心对应 Client ID -config.clientToken = @"your_client_token"; // 必须,开发者中心对应 Client Token -config.region = TapSDKRegionTypeIO; // TapSDKRegionTypeCN:中国大陆,TapSDKRegionTypeIO:其他国家或地区 -config.serverURL = "https://your_server_url"; // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > 云服务 API -// highlight-next-line -config.tapBillboardConfig = billboardIntlConfig; -[TapBootstrap initWithConfig:config]; -``` - - - -<> - -```cpp -#include "ServiceWidgetBillboard.h" -#include "ServiceWidgetBootstrap.h" -#include "TapBillboardCommon.h" -#include "TapUECommon.h" - -FTapBillboardPtr TapBillboard = FTapBillboardModule::GetTapBillboardInterface(); - -FTUConfig Config; -Config.ClientID = "your_client_id"; //开发者中心应用 ID -Config.ClientToken = "your_client_token"; //开发者中心对应 Client Token -Config.RegionType = ERegionType::CN; //区域:国内或者海外 -Config.ServerURL = ""; // 开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > 云服务 API -Config.BillboardConfig = MakeShared(); -Config.BillboardConfig->BillboardUrl = "";// 开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置 > 公告, 这里必须包含 https:// -Config.BillboardConfig->Dimensions = {{TEXT("location"), TEXT("CN")}} // 开发者中心 > 你的游戏 > 游戏服务 > 运营工具 > 公告 > 维度配置 -TapBillboard->Init(Config); - -``` - - - - - -### 公告系统单独初始化 - -如果游戏不通过上面提供的 `TapBootstrap` 方法初始化 TapSDK,仅希望初始化公告系统,可以这么做: - -- **复制上面提供的第一种初始化方式代码**。 -- **修改最后一行**为: - - - -```cs -TapBillboard.Init(config); -``` - -```java -TapBillboard.init(tapConfig); -``` - -```objectivec -[TapBillboard initWithConfig:config]; -``` - -```cpp -FTapBootstrap::Init(Config); -``` - - - -### 参数说明 - - - -* 初始化需要两个域名,分别是 API 域名和公告域名。参考文档关于[域名](/sdk/domain/guide/)的说明,在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置** 处完成配置之后填入。 - - - -* `dimensionSet` 参考[公告系统维度配置](../features/#维度配置),这里使用了两个预设维度:渠道(platform)和地区(location)。 - -* `client_id`、`client_token`、`RegionType` 信息可在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 查看。 - -## 打开公告 - - - -```cs -TapBillboard.OpenPanel((any, error) => -{ - if (error != null) - { - // 打开公告失败,可以根据 error.code 和 error.errorDescription 来判断错误原因 - } else - { - // 打开公告成功 - } -}, () => { - // 公告已关闭 -}); -``` - -```java -TapBillboard.openPanel(BillboardActivity.this, new Callback() { - @Override - public void onError(TapBillboardException tapBillboardException) { - // 打开公告失败,可以根据 tapBillboardException.code 和 tapBillboardException.message 来判断错误原因 - } - - @Override - public void onSuccess(Void result) { - // 打开公告成功 - } -},new PanelShowStateListener() { - @Override - public void onClose() { - // 公告已关闭,如果有 UI 更新,请切换到 Android 主线程 - } -}); -``` - -```objectivec -[TapBillboard openPanel:^(bool _, NSError *_Nullable error) { - if (error) { - // 打开公告失败,可以根据 error.code 和 error.errorDescription 判断错误原因 - } else { - // 打开公告成功 - } -} closeCallback:^(void){ - // 公告已关闭 -}]; -``` -```cpp -TapBillboard->OpenPanel( - FSimpleDelegate::CreateLambda([](){ /** 打开成功 */}), - FTapFailed::CreateLambda([](const FTUError& Error){ /** 打开失败 */}), - FSimpleDelegate::CreateLambda([](){ /** 公告关闭 */})); - -``` - - - -## 打开开屏公告 -开屏公告用户无法主动关闭,当时间到达开发者配置的关闭时间时会自动关闭,或者开发者可调用关闭开屏公告接口主动关闭。 - - - -```cs - TapBillboard.OpenSplashPanel(async (result, err) => { - if (result) { - Debug.Log("打开开屏公告成功"); - } else { - Debug.Log($"打开开屏公告失败: {err.code}, {err.errorDescription}"); - } - }, () => { - Debug.Log("关闭开屏公告"); - }); -``` - -```java - TapBillboard.openSplashPanel(this, new Callback() { - @Override - public void onSuccess(Void result) { - // 打开开屏公告成功 - } - - @Override - public void onError(TapBillboardException tapBillboardException) { - // 打开开屏公告失败,可以根据 tapBillboardException.code 和 tapBillboardException.message 来判断错误原因 - - }, new PanelShowStateListener() { - @Override - public void onClose() { - //开屏公告已关闭 - } - }); - -``` - -```objectivec - [TapBillboard openSplashPanel:^(bool result, NSError * _Nullable error) { - if(error){ - NSLog(@"open splash fail error = %@", error.description); - }else{ - NSLog(@"open splash success "); - } - } closeCallback:^{ - NSLog(@"close splash"); - }]; -``` - -```cpp -TapBillboard->OpenSplashPanel( - FSimpleDelegate::CreateLambda([](){ /** 打开成功 */}), - FTapFailed::CreateLambda([](const FTUError& Error){ /** 打开失败 */}), - FSimpleDelegate::CreateLambda([](){ /** 公告关闭 */})); - -``` - - - -## 关闭开屏公告 -当开屏公告展示时,开发者可调用该接口主动关闭。 - - - -```cs -TapBillboard.CloseSplashPanel(); -``` - -```java -TapBillboard.closeSplashPanel(); -``` - -```objectc -[TapBillboard closeSplashPanel]; -``` - -```cpp -TapBillboard->CloseSplashPanel(); -``` - - - -## 获取小红点 - -可以用来获取是否有新的公告信息。SDK 内部对小红点信息进行了缓存,如果两次请求间隔小于 5 分钟,会直接返回上一次成功结果。 - - - -```cs -TapBillboard.QueryBadgeDetails((badgeDetails, error) => -{ - if (error != null) - { - // 获取小红点信息失败,可以根据 error.code 和 error.errorDescription 来判断错误原因 - } - else - { - // 获取小红点信息成功 - if (badgeDetails.showRedDot == 1) { - // 有新的公告信息 - } else { - // 没有新的公告信息 - } - } -}); -``` - -```java -TapBillboard.getBadgeDetails(new Callback() { - @Override - public void onError(TapBillboardException tapBillboardException) { - // 获取小红点信息失败,可以根据 tapBillboardException.code 和 tapBillboardException.message 来判断错误原因 - } - - @Override - public void onSuccess(BadgeDetails badgeDetails) { - if (badgeDetails.showRedDot == 1) { - // 有新的公告信息 - } else { - // 没有新的公告信息 - } - } -}); -``` - -```objectivec -[TapBillboard getBadgeDetails:^(BadgeDetails * _Nullable result, NSError *_Nullable error) { - if (error) { - // 获取小红点信息失败,可以根据 error.code 和 error.errorDescription 来判断错误原因 - } else { - if ([result.showRedDot intValue] == 1) { - // 有新的公告信息 - } else { - // 没有新的公告信息 - } - } -}]; -``` - -```cpp -TapBillboard->GetBadgeDetails( - FTapBadgeDetailsResult::CreateLambda([](const FBadgeDetails& BadgeDetails){ /** 获取成功 */}), - FTapFailed::CreateLambda([](const FTUError& Error){ /** 获取失败 */})); - -``` - - - -## 注册自定义事件监听 - -当[游戏发布的公告配置的跳转链接为「游戏内模块」](../features/#发布公告),或者游戏正文添加了链接并且链接类型为「游戏内模块」的时候,可以通过监听自定义事件来获取消息,并做相应处理。 - - - -```cs -TapBillboard.RegisterCustomLinkListener(url => -{ - // 这里返回的 url 地址和游戏在公告系统内配置的地址是一致的 -}); -``` - -```java -TapBillboard.registerCustomLinkListener(new CustomLinkListener() { - @Override - public void onCustomUrlClick(String url) { - // 这里返回的 url 地址和游戏在公告系统内配置的地址是一致的 - // 注意如果有 UI 操作需要切换到 Android 主线程处理 - } -}); -``` - -```objectivec -[TapBillboard registerCustomLinkListener:^(NSString * _Nullable customUrl) { - // 这里返回的 url 地址和游戏在公告系统内配置的地址是一致的 -}]; -``` - -```cpp -FDelegateHandle CustomUrlClickedHandle = TapBillboard->RegisterCustomLinkListener(FOnCustomLinkClicked::FDelegate::CreateLambda([](const FString& Url){ /** 自定义Url点击 */})); - - -``` - - - -## 解除已注册的自定义事件监听 - -如果不想再接收公告的自定义事件监听可以调用下面的接口: - - - -```cs -// 已注册的回调对象需要游戏保存,取消注册的时候要把对象传给 SDK -TapBillboard.UnRegisterCustomLinkListener(registerdListener); -``` - -```java -// 已注册的回调对象需要游戏保存,取消注册的时候要把对象传给 SDK -TapBillboard.unRegisterCustomLinkListener(registerdListener); -``` - -```objectivec -// 已注册的回调对象需要游戏保存,取消注册的时候要把对象传给 SDK -[TapBillboard unRegisterCustomLinkListener:registerdListener]; -``` - -```cpp -// 已注册的回调对象需要游戏保存,取消注册的时候要把对象传给 SDK -TapBillboard->UnregisterCustomLinkListener(CustomUrlClickedHandle); -``` - - - -## 展示跑马灯数据 - -开始获取跑马灯数据,当请求到内容时,会按照开发者配置的位置及样式进行展示。内部请求采用轮询方式,每 5 分钟发起一次请求。 - - - -```cs -TapBillboard.StartFetchMarqueeData(); -``` - -```java -//参数为展示跑马灯所在页面的 Activity 对象 -TapBillboard.startFetchMarqueeData(this); -``` - -```objectivec -[TapBillboard startFetchMarqueeData]; -``` - -```cpp -TapBillboard->StartFetchMarqueeData(); -``` - - - -## 停止获取跑马灯数据 - -停止轮询获取跑马灯数据,参数为当展示时是否立刻关闭跑马灯窗口。 - - - -```cs -TapBillboard.StopFetchMarqueeData(closeNow); -``` - -```java -TapBillboard.stopFetchMarqueeData(true); -``` - -```objectivec -[TapBillboard stopFetchMarqueeData:YES]; -``` - -```cpp -TapBillboard->StopFetchMarqueeData(true);; -``` - - - -## 注册公告内容输出状态监听 - -当公告展示时输出及关闭音频时,通过该回调通知开发者。 - - - -```cs -TapBillboard.RegisterOutputStateListener(new OutputStateListener { - OnPlayVoice = () => { - Debug.Log("公告页面播放声音"); - }, - OnStopVoice = () => { - Debug.Log("公告页面关闭声音"); - } -}); -``` - -```java -TapBillboard.registerOutputStateListener(new OutputStateListener() { - @Override - public void onPlayVoice() { - Toast.makeText(BillboardActivity.this, "公告音频开始输出",Toast.LENGTH_SHORT).show(); - } - - @Override - public void onStopVoice() { - Toast.makeText(BillboardActivity.this, "公告音频停止输出",Toast.LENGTH_SHORT).show(); - } - }); - - -``` - -```objectivec -TapBillboard.RegisterOutputStateListener(new OutputStateListener { - OnPlayVoice = () => { - Debug.Log($"公告页面播放声音"); - }, - OnStopVoice = () => { - Debug.Log($"公告页面关闭声音"); - } - }); - -``` - -```cpp -TapBillboard->RegisterOutputStateListener(FAudioOutputStateChanged::FDelegate::CreateLambda([](bool bPlaying){ /** 公告音频输出状态改变 */})); - -``` - - - -## 解除公告内容输出状态监听 - - - -```cs -TapBillboard.UnRegisterOutputStateListener(); -``` - -```java -TapBillboard.unRegisterOutputStateListener(); -``` - -```objectivec -[TapBillboard unRegisterOutputStateListener]; -``` - -```cpp -TapBillboard->UnregisterOutputStateListener(); -``` - - - - - -## 国际化 - -公告支持设置语言: - - - -## 错误码 - -| `code` | 场景 | -|---|---| -| 400 | 初始化参数错误 | -| 403 | 用户无权限访问该服务,需检查开发者中心是否开通了公告服务 | -| 50x | 服务端内部错误,错误内容可以参考 description | -| 19999 | 其他错误,内容可以参考 description | - -## REST API - -下面我们介绍公告系统相关的 REST API 接口。 - -### 请求格式 - -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 - -请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,参数如下表: - -| Key | Value | 含义 | 来源 | -| ---------- | ------------ | ----------------------------------------- | -------------- | -| `X-LC-Id` | `{{appid}}` | 当前应用的 `App Id`(即 `Client Id`) | 可在控制台查看 | -| `X-LC-Sign` | `{{appSign}}` | 由 `sign,timestamp` 组成的字符串。其中 timestamp 为客户端产生本次请求的 unix 时间戳(UTC),精确到毫秒。`sign` 是 timestamp 拼接 `App Key`(即 `Client Token`)组成的字符串,再对它做 MD5 签名后的结果。| 参考下方的计算方式 | - -使用 App Key 来计算 `appSign`: - -```sh -md5( timestamp + App Key ) -= md5(1453014943466UtOCzqb67d3sN12Kts4URwy8) -= d5bcbb897e19b2f6633c716dfdfaf9be - --H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" -``` - -除了在 `X-LC-Id` 这个 HTTP Header 中传入 `Client Id` 外,还需要在 URL 中指定 `Client Id`,两者的值需要一致。 - -异常时返回 400 (HTTP 状态码)错误,例如: - -```json -{ - "success": false, - "data": { - "code": 0, - "error": "invalid_request", - "msg": "invalid params", - "error_description": "" - }, - "now": 1659084246 -} -``` - -### Base URL - -REST API 请求的 Base URL(下文 curl 示例中用 `{{host}}` 表示)即应用的 API 自定义域名,可以在控制台绑定、查看。详见文档关于[域名](/sdk/domain/guide/)的说明。 - -### 获取模版 - -| 参数 | 约束 | 说明 | -|---|---|---| -| `client_id` | 必须 | **开发者中心后台游戏服务 > 应用配置** 中的 `Client ID` | -| `template` | 必须 | 公告模版类型,目前仅支持导航模版。可填写:导航模版-`navigate`、图片模版-`image`。 | - -```sh -curl -X GET - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - https://{{host}}/billboard/rest-api/v1/pattern/detail?client_id={{appid}}&template={{template}} -``` - -返回的主体是一个 JSON 对象,包含创建模版时传入的所有参数: - -```json -{ - "success": true, - "data": { - "empty_img": { - "url": "https://tds-billboard.tds1.tapfiles.cn/20220727/aWkG63mpT2WeFrtT0Dxojj4QLfabWHh3.png" - }, - "highlight_text_color": "#00D9C5", - ... - }, - "now": 1659085552 -} -``` - -### 获取公告列表 - -| 参数 | 约束 | 说明 | -|---|---|---| -| `client_id` | 必须 | **开发者中心后台游戏服务 > 应用配置** 中的 `Client ID` | -| `lang` | 必须 | 详见[语言代码列表](#语言代码列表) | -| `template` | 必须 | 公告模版类型,目前仅支持导航模版。可填写:导航模版-`navigate`、图片模版-`image`、开屏模版-`splash`、跑马灯模版-`marquee`。 | -| `dimension_list` | 可选 | `[{"维度参数":"维度字段参数"},...]`,示例:`[{"platform":"ios"},{"location":"CN"}]` | -| `type` | 必须 | 公告类型。可填写:活动公告-`activity`、其他统称游戏公告-`game`。| -| `uuid` | 选填 | 设备唯一 id 字符串,用于支持数据分析。示例:"4e4105c7-781e-45c0-92ea-d595c75a3c2c" | - -```sh -curl -X POST - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - -H 'Content-Type: application/json' \ - -d '{ - "lang":{{lang}}, - "template":{{template}}, - "type":{{type}}, - "dimension_list":{{dimension_list}} - }' \ - https://{{host}}/billboard/rest-api/v1/announcement/list?client_id={{appid}}&uuid={{uuid}} -``` - -返回的 JSON 对象包含公告列表 `list` 和最新发布的一条公告详情 `lastest`。 - -| 参数 | 说明 | -|---|---| -| `id` | 公告 id | -| `type` | 公告类型:更新公告-update、维护公告-maintenance、上新公告-new、活动公告-activity、玩法公告-play_method、测试公告-test、停服公告-discontinued | -| `expire_time` | 过期时间:秒级时间戳,0-表示不过期 | -| `short_title` | 短标题 | -| `jump_location` | 跳转位置:content-正文、link-跳转链接、game-游戏内模块 | -| `jump_link` | 跳转位置选择「跳转链接」时为对应的链接,选择「游戏内模块」时为对应的模块参数 | -| `publish_time` | 发布时间:秒级时间戳 | - -```json -{ - "success": true, - "data": { - "list": [ - { - "id": 2, - "type": "activity", - "expire_time": 0, - "short_title": "公告跳转链接地址", - "jump_location": "link", - "jump_link": "https://www.taptap.cn", - "publish_time": 1659077524 - }, - ... - ], - "lastest": { - "id": 2, - "content": "正文", - "short_title": "公告跳转链接地址", - "long_title": "测试情报|「起着测试」4月14日开启" - } - }, - "now": 1659085756 -} -``` - -### 获取公告详情 - -| 参数 | 约束 | 说明 | -|---|---|---| -| `client_id` | 必须 | **开发者中心后台游戏服务 > 应用配置** 中的 `Client ID` | -| `lang` | 必须 | 详见[语言代码列表](#语言代码列表) | -| `id` | 必须 | 公告 id | -| `uuid` | 选填 | 设备唯一 id 字符串,用于支持数据分析。示例:"4e4105c7-781e-45c0-92ea-d595c75a3c2c" | - -```sh -curl -X GET - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - https://{{host}}/billboard/rest-api/v1/announcement/detail?client_id={{appid}}&lang={{lang}}&id={{id}}&uuid={{uuid}} -``` - -返回的 JSON 对象 - -| 参数 | 说明 | -|---|---| -| `id` | 公告 id | -| `content` | 正文 | -| `long_title` | 长标题 | -| `short_title` | 短标题 | - -```json -{ - "success": true, - "data": { - "id": 82, - "content": "正文", - "short_title": "短标题", - "long_title": "长标题" - }, - "now": 1659087990 -} -``` - -### 获取小红点 - -| 参数 | 约束 | 说明 | -|---|---|---| -| `client_id` | 必须 | **开发者中心后台游戏服务 > 应用配置** 中的 `Client ID` | -| `uuid` | 必须 | 设备唯一 id 字符串,示例:"4e4105c7-781e-45c0-92ea-d595c75a3c2c" | -| `template` | 必须 | 公告模版类型,目前仅支持导航模版。可填写:导航模版-`navigate`、图片模版-`image`。 | -| `dimension_list` | 可选 | `[{"维度参数":"维度字段参数"},...]`,示例:`[{"platform":"ios"},{"location":"CN"}]` | - -```sh -curl -X POST \ - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - -H 'Content-Type: application/json' \ - -d '{ - "uuid": {{uuid}}, - "template":{{template}}, - "dimension_list":{{dimension_list}} - }' \ - 'https://{{host}}/billboard/rest-api/v1/dot/read?client_id={{client_id}}' -``` - -返回的 JSON 对象 - -| 参数 | 说明 | -|---|---| -| `show_red_dot` | 是否展示小红点:0-不展示 1-展示 | -| `close_button_img` | 从导航接口中得到的关闭按钮 URL | - -```json -{ - "success": true, - "data": { - "show_red_dot": 0, - "close_button_img": "https://tds-billboard.tds1.tapfiles.cn/20220727/aMHoqDTHT4zrXYPNprkZjXkdLK6vFg8E.png" - }, - "now": 1658896487 -} -``` - -### 提交小红点 - -| 参数 | 约束 | 说明 | -|---|---|---| -| `client_id` | 必须 | **开发者中心后台游戏服务 > 应用配置** 中的 `Client ID` | -| `uuid` | 必须 | 设备唯一id字符串,示例:"4e4105c7-781e-45c0-92ea-d595c75a3c2c" | -| `read_all` | 必须 | 标记公告全部已读:true-是,false-否 | - -```sh -curl -X POST \ - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - -H 'Content-Type: application/json' \ - -d '{ - "uuid": {{uuid}}, - "read_all":{{read_all}} - }' \ - 'https://{{host}}/billboard/rest-api/v1/dot/submit?client_id={{client_id}}' -``` - -返回的 JSON 对象 -```json -{ - "success": true, - "data": { - "msg": "ok" - }, - "now": 1658895192 -} -``` - -### 批量查询公告详情 - -| 参数 | 约束 | 说明 | -|---|---|---| -| `client_id` | 必须 | **开发者中心后台游戏服务 > 应用配置** 中的 `Client ID` | -| `uuid` | 必须 | 设备唯一id字符串,示例:"4e4105c7-781e-45c0-92ea-d595c75a3c2c" | -| `ids` | 必须 | 公告唯一id字符串,用逗号分隔。示例:"1,2" | -| `lang` | 必须 | 语言。示例:"zh_CN" | - -```sh -curl -X GET \ - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - 'https://{{host}}/billboard/rest-api/v1/announcement/detail/multi?client_id={{client_id}}&ids={ids}&uuid={uuid}&lang={lang}' -``` - -返回的 JSON 对象 - -| 参数 | 说明 | -|---|---| -| `id` | 公告 id| -| `content` | 公告正文 | -| `type` | 公告类型:更新公告-update、维护公告-maintenance、上新公告-new、活动公告-activity、玩法公告-play_method、测试公告-test、停服公告-discontinued | -| `template` | 公告模版类型 | -| `short_title` | 短标题 | -| `long_title` | 长标题 | -| `jump_location` | 跳转位置:content-正文、link-跳转链接、game-游戏内模块 | -| `jump_link` | 跳转位置选择「跳转链接」时为对应的链接,选择「游戏内模块」时为对应的模块参数 | -| `publish_time` | 发布时间:秒级时间戳,0-表示不存在 | -| `expire_time` | 过期时间:秒级时间戳,0-表示不过期 | - -```json -{ - "success": true, - "data": { - "list": [ - { - "id": 1, - "content": "[{\"type\":\"paragraph\",\"children\":[{\"text\":\"测试公告排序哈哈哈哈 \"}]},{\"type\":\"block-link\",\"info\":{\"url\":\"test://action\",\"type\":\"game\",\"noLinkTitle\":false},\"children\":[{\"text\":\"测试内部跳转1\"}]},{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]},{\"type\":\"block-link\",\"info\":{\"url\":\"test://action2\",\"type\":\"game\",\"noLinkTitle\":false},\"children\":[{\"text\":\"测试内部跳转2\"}]}]", - "type": "", - "template": "", - "short_title": "测试公告排序", - "long_title": "测试公告排序", - "jump_location": "", - "jump_link": "", - "publish_time": 0, - "expire_time": 0 - }, - { - "id": 2, - "content": "", - "type": "", - "template": "", - "short_title": "", - "long_title": "", - "jump_location": "", - "jump_link": "", - "publish_time": 0, - "expire_time": 0 - } - ] - }, - "now": 1676010120 -} -``` - -### 未读公告-设备维度 - -| 参数 | 约束 | 说明 | -|---|---|---| -| `client_id` | 必须 | **开发者中心后台游戏服务 > 应用配置** 中的 `Client ID` | -| `uuid` | 必须 | 设备唯一id字符串,示例:"4e4105c7-781e-45c0-92ea-d595c75a3c2c" | -| `template` | 必须 | 公告模版类型,目前仅支持导航模版。可填写:开屏模版-`splash`、跑马灯模版-`marquee`。 | -| `dimension_list` | 可选 | `[{"维度参数":"维度字段参数"},...]`,示例:`[{"platform":"ios"},{"location":"CN"}]` | - -```sh -curl -X POST \ - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - -H 'Content-Type: application/json' \ - -d '{ - "uuid": {{uuid}}, - "template":{{template}}, - "dimension_list":{{dimension_list}} - }' \ - 'https://{{host}}/billboard/rest-api/v1/announcement/unread?client_id={clientId}' -``` - -返回的 JSON 对象 - -| 参数 | 说明 | -|---|---| -| `id` | 公告 id| -| `publish_time` | 发布时间:秒级时间戳,0-表示不存在 | -| `expire_time` | 过期时间:秒级时间戳,0-表示不过期 | - -```json -{ - "success": true, - "data": { - "list": [ - { - "id":1, - "publish_time": 1669203905, - "expire_time": 1670672278 - }, - { - "id":2, - "publish_time": 1669203905, - "expire_time": 1670672278 - } - ] - }, - "now": 1666853298 -} -``` - -### 标记已读-设备维度 - -注:该设备中,未读公告接口中不在返回已标记的公告id。ids当前仅限跑马灯类型的公告,否则返回异常信息 - -| 参数 | 约束 | 说明 | -|---|---|---| -| `client_id` | 必须 | **开发者中心后台游戏服务 > 应用配置** 中的 `Client ID` | -| `uuid` | 必须 | 设备唯一id字符串,示例:"4e4105c7-781e-45c0-92ea-d595c75a3c2c" | -| `ids` | 必须 | 公告唯一id数组。示例:[1,3] | - -```sh -curl -X POST \ - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - -H 'Content-Type: application/json' \ - -d '{ - "uuid": {{uuid}}, - "ids":{{ids}} - }' \ - 'https://{{host}}/billboard/rest-api/v1/announcement/mark?client_id={clientId}' -``` - -返回的 JSON 对象 - -```json -{ - "success": true, - "data": { - "msg": "ok" - }, - "now": 1670318366 -} -``` - - -### 取消已读标记-设备维度 - -注:该设备中,维度公告接口中再次返回取消标记的公告id - -| 参数 | 约束 | 说明 | -|---|---|---| -| `client_id` | 必须 | **开发者中心后台游戏服务 > 应用配置** 中的 `Client ID` | -| `uuid` | 必须 | 设备唯一id字符串,示例:"4e4105c7-781e-45c0-92ea-d595c75a3c2c" | - -```sh -curl -X POST \ - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - -H 'Content-Type: application/json' \ - -d '{ - "uuid": {{uuid}}, - }' \ - 'https://{{host}}/billboard/rest-api/v1/announcement/unmark?client_id={clientId}' -``` - -返回的 JSON 对象 - -```json -{ - "success": true, - "data": { - "msg": "ok" - }, - "now": 1670318366 -} -``` - -### 语言代码列表 - -使用 ISO 639-1 中定义的双小写字母语言代码(例如,`en` 表示英语,`jp` 表示日语),但: - -1. ISO 639-1 中未包括的语言,使用 ISO 632-2 中定义的三小写字母语言代码(例如,`fil` 表示菲律宾语) -2. 仅使用语言代码无法表示所需语言时,附加 ISO 3166-1 中定义的地区代码(例如,`zh_CN` 表示简体中文) - -当前 REST API 支持的语言代码如下: - -| 代码 | 语言 | -| ------- | ----------- | -| zh_CN | 简体中文 | -| zh_TW | 繁体中文 | -| en_US | 英语(美国) | -| ja_JP | 日文 | -| ko_KR | 韩文 | -| pt_PT | 葡萄牙语 | -| vi_VN | 越南语 | -| hi_IN | 印度语 | -| id_ID | 印尼语 | -| ms_MY | 马来语 | -| th_TH | 泰语 | -| es_ES | 西班牙语 | -| af | 南非荷兰语 | -| am | 阿姆哈拉语 | -| bg | 保加利亚语 | -| ca | 加泰罗尼亚语 | -| hr | 克罗地亚语 | -| cs | 捷克语 | -| da | 丹麦语 | -| nl | 荷兰语 | -| et | 爱沙尼亚语 | -| fil | 菲律宾语 | -| fi | 芬兰语 | -| fr | 法语 | -| de | 德语 | -| el | 希腊语 | -| he | 希伯来语 | -| hu | 匈牙利语 | -| is | 冰岛语 | -| it | 意大利语 | -| lv | 拉脱维亚语 | -| lt | 立陶宛语 | -| no | 挪威语 | -| pl | 波兰语 | -| ro | 罗马尼亚语 | -| ru | 俄语 | -| sr | 塞尔维亚语 | -| sk | 斯洛伐克语 | -| sl | 斯洛文尼亚语 | -| sw | 斯瓦希里语 | -| sv | 瑞典语 | -| tr | 土耳其语 | -| uk | 乌克兰语 | -| zu | 祖鲁语 | - -注意,上表中的部分语言虽然 REST API 支持,但[客户端 SDK 并没有支持](#国际化)。 - -## 视频教程 - -可以参考视频教程:[如何接入 TapTap 公告服务](https://www.bilibili.com/video/BV1tp4y1L7Fe/),了解如何在 Untiy 项目中接入公告功能。 - -更多视频教程见[开发者学堂](https://developer.taptap.cn/tds-tutorials/list)。因为 SDK 功能在不断完善,视频教程可能出现与新版 SDK 功能不一致的地方,以当前文档为准。 \ No newline at end of file diff --git a/docs/shadow/fire-test/_category_.json b/docs/shadow/fire-test/_category_.json deleted file mode 100644 index defdffc7e..000000000 --- a/docs/shadow/fire-test/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "薪火计划", - "collapsed": true, - "position": 1.5 -} diff --git a/docs/shadow/fire-test/exchange.mdx b/docs/shadow/fire-test/exchange.mdx deleted file mode 100644 index 08a65d703..000000000 --- a/docs/shadow/fire-test/exchange.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: 分享薪火置换资源规则 -sidebar_label: 分享置换资源 -sidebar_position: 1.5 -slug: /sdk/fire-test/exchange ---- - -本文主要介绍分享薪火置换资源的使用规则。对于参加薪火计划的游戏,可在薪火主页面通过扫描二维码,将参与薪火计划的H5页面分享至第三方平台,邀请其他厂商的游戏来加入薪火计划。对于扫码分享的游戏,TapTap 平台也会根据分享效果给予开发者一定的奖励。奖励规则如下: - -**1、兑换薪火流量加速权益** - -通过参加薪火分享的游戏,平台将根据分享落地页的曝光及邀请数据,为开发者提供兑换薪火流量加速权益的机会。 - -- 以自然月为时间周期,每个月「分享落地页的曝光用户量」超过 100,即可兑换 1 次「流量加速权益」,需当月兑换,过期未兑换则失效。兑换入口,可见下图。 -- 分享落地页的「邀请码」可邀请「其他厂商的新游戏」加入薪火计划,根据成功邀请游戏数量进行「流量加速权益」兑换,单个邀请码最多可被使用5次。 - - 比如成功邀请了3款其他厂商的新游戏加入薪火,则可以兑换3次薪火流量加速权益。 - - 其中邀请成功的标准是:其他厂商的新游戏加入薪火计划,并且使用了您游戏的邀请码完成1次流量权益兑换。 -- 通过分享或者邀请码兑换的「流量加速权益」有效期为 6 个月,且不受当月使用次数限制。 - -![](https://capacity-files.lcfile.com/zGiiRxhiKmNNnqpQkgLLxkhQpwcVlvC5/image-20230627-084055.png) - -**2、在「TapTap资源置换平台」置换流量资源** - -对于参加薪火计划并分享成功的游戏,薪火计划与 [TapTap资源置换平台](https://rep.taptap.cn/)进行了联动,开发者可根据下文的规则,在 [TapTap资源置换平台](https://rep.taptap.cn/) 领取相应流量奖励。 - -- 分享成功后的游戏,其落地页的「游戏跳转链接」会在 [TapTap资源置换平台](https://rep.taptap.cn/) 自动生成 1 条「效果资源」,可根据「游戏跳转链接」的访问数据置换相关资源。 -- 首次完成分享的游戏,也可前往 [TapTap资源置换平台](https://rep.taptap.cn/) 获得价值「1000元的优选流量包」,一个厂商最多可领取5次。 -- 如成功邀请其他厂商的新游戏加入薪火计划,并成为 「TapTap 资源置换平台的新用户」,可再次获得价值「1000 元的优选流量包」。 - - TapTap 资源置换平台的新用户指的是:从未在TapTap资源置换平台领取过奖励的厂商。 - -**3、新加入薪火游戏的奖励规则** - -对于新加入薪火计划的游戏,可以在薪火功能页面的流量权益入口,进行邀请码兑换操作,入口如下图。 - -- 开发者输入邀请码进行兑换,兑换成功后即可获得1次薪火流量加速权益,有效期为6个月。 -- 如果当前厂商也是 [TapTap资源置换平台](https://rep.taptap.cn/) 的新用户,也可在资源置换平台领取价值5000元以上新手流量大礼包。 - -![](https://capacity-files.lcfile.com/xI8Jtu74cJTT9tjD8Bhbdqf6z9o9l0Pv/image-20230627-090947.png) diff --git a/docs/shadow/fire-test/fire-service-doc.mdx b/docs/shadow/fire-test/fire-service-doc.mdx deleted file mode 100644 index 85646e9fa..000000000 --- a/docs/shadow/fire-test/fire-service-doc.mdx +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: TapTap 平台薪火计划服务协议 -sidebar_label: 服务协议 -sidebar_position: 2 -slug: /sdk/fire-test/fire-service-doc ---- - -更新日期:2022年12月7日 - -生效日期:2022年12月7日 - - - -欢迎您加入TapTap平台薪火计划! - -如您选择加入 TapTap平台薪火计划,您应当阅读并遵守《TapTap平台薪火计划服务协议》(以下简称“本协议”)、《TapTap平台开发者协议》等相关协议、规则。请您务必审慎阅读、充分理解各条款内容,**特别是限制或免除责任的条款**,**以及开通或使用某项服务的单独协议、规则。限制或免责条款可能以加粗形式提示您重点注意。** - -除非您已阅读并接受本协议及相关协议、规则等的所有条款,否则,您无权使用本服务。一旦您勾选“同意”(具体措辞以TapTap平台最终展示为准),或您以任何方式使用本服务,即视为您已阅读并同意上述协议、规则等的约束。 - -当您有违反本协议的任何行为时,TapTap有权依照您的违反情况依据自身的判断,随时单方采取限制、中止或终止向您提供服务等措施,并有权追究您相关责任。 - - - -## **一、定义** - -1. TapTap平台:包括但不限于域名为taptap.com、taptap.io、taptap.cn的网站平台以及由易玩(上海)网络科技有限公司及其关联公司开发或运营的名称为“TapTap”的适用于iOS、Android及其他操作系统的移动应用平台。 - -2. 合作游戏:指审核通过并成功加入TapTap平台薪火计划的特定游戏产品,包括商务游戏与普通游戏。 - -3. 商务游戏:指与TapTap平台另行签署《TapTap平台游戏合作协议》(该协议需现行有效,具体协议名称可能有所变化)且协议中“独占情况”为“独占”的游戏。您理解并同意,商务游戏应同时遵守《TapTap平台游戏合作协议》及本协议的约定。 - -4. 普通游戏:指合作游戏中的非商务游戏。 - -5. 正式上架:是指您以免费或付费形式在合作区域内正式将合作游戏全面公开给终端用户进行下载使用,同时向用户正式开放游戏内或其他形式的收费服务,收费服务包括但不限于向用户出售道具、虚拟游戏币等形式。 - - - -## **二、合作内容** - -1. **合作期限:** - **1)普通游戏:自您同意接受本协议之日起至合作游戏在TapTap平台正式上架之日起3个自然月。如您未在上述期限届满前通过系统终止合作,则本协议将在上述期限届满后自动续约1个自然月,续约次数不限。** - **2)商务游戏:与《TapTap平台游戏合作协议》(具体协议名称可能有所变化)约定的合作期限相同,《TapTap平台游戏合作协议》到期或终止的,商务游戏自动转为普通游戏,合作期限按照普通游戏计算。** - -2. 合作区域:中华人民共和国大陆地区(不包括港澳台地区)。 - -3. 合作语言:以合作游戏实际提供的语言版本为准。 - -4. 合作版本:适用于各类终端的非iOS操作系统(包括但不限于Android、鸿蒙和未来上线的操作系统,以及前述所有操作系统的全部版本)的游戏版本。 - -5. **合作要求:在合作期限内,您仅能通过TapTap平台提供合作游戏的下载(含试玩、预约下载等)与更新。未经TapTap平台事先书面同意,就本协议约定的服务或合作,您不得直接或间接地从任意第三方获得任何与本协议相同或类似的服务,且不得与任意第三方就本协议所述事项建立任何类似的合作关系(包括但不限于您与第三方签订与本协议相同或类似的协议、在合作区域内将合作游戏上传至其他游戏平台等)。** - - - -## **三、权利义务** - -1. 您保证是符合法律法规规定的主体,已经取得必要经营资质,有权利和能力签订和履行本协议,您提交的一切资料(包括但不限于注册信息、身份资质、经营资质、授权文件等)真实、合法、有效。 - -2. 若您选择加入薪火计划,**则您必须取消合作游戏在非TapTap平台的上架或预约等状态,在合作游戏中接入TapTap登录功能,同时保证合作期限内满足本协议合作要求**,否则TapTap平台有权拒绝您加入或继续使用薪火计划,并要求您承担对应的违约责任。 - -3. **当您加入薪火计划后,将获得TapTap平台提供的针对合作游戏的额外曝光资源,并可免费使用开发者中心提供的付费功能(短信功能除外)(以下统称“各项权益与功能”,具体以相关页面说明为准)**。但薪火计划内所包含的各项权益与功能仅适用于合作游戏的合理使用,您不得随意滥用(如不合理使用、将各项权益与功能用于非合作游戏等)。TapTap有权自行判断您的使用是否为滥用,一旦认定为滥用,您应按照TapTap要求进行代码整改等操作。 - -4. 合作到期终止后,您可重新开通薪火计划,TapTap将从您重新开通的次日为您提供各项权益与功能并依据第2.1条重新计算合作期限。 - -5. **若您的合作游戏为商务游戏,您理解并同意,TapTap将自动为您开通TapTap平台维权服务,您需配合平台提供相应的维权材料,包括但不限于维权授权书、计算机软件著作权登记证书、合法权利来源的证明等。** - -6. **若您的合作游戏为普通游戏,您理解并同意,在合作期限内,TapTap将对合作游戏进行全网监测,若发现合作游戏在非TapTap平台上架或预约等,TapTap将向您发送违规通知,您应当在7个自然日内 - 1)确保合作游戏满足本协议项下合作要求;或 - 2)依据提示上传资料以开通TapTap平台维权服务,授权TapTap帮助您维权 - 否则,TapTap有权立即停用您的各项权益与功能且不承担任何责任。在合作游戏重新满足合作要求或您开通TapTap平台维权服务后24小时内,TapTap将恢复您的各项权益与功能。** - -7. 您理解并同意,由于各个第三方平台的要求不同、法律法规的更新与迭代等,TapTap不对维权结果做出任何保证。 - -8. **您理解并同意,若您违反本协议等相关平台规则的约定或法律法规的要求,TapTap有权视情节的严重程度采取下列任一种或几种措施追究您的责任: - 1)解除本协议; - 2)终止合作游戏在TapTap平台的上架; - 3)不再允许您加入薪火计划; - 4)要求您支付累计免费金额(以TapTap平台统计为准)并承担5万元人民币的违约金。** - - -## **四、保密条款** - -1. 双方为了本协议之目的,已经或将会提供或披露某些保密信息。保密信息指由协议一方持有的与其业务、经营、技术及权利等事项相关的,非公开的信息(包括但不限于违规处理方案、诉讼情况等)、资讯、数据、资料。其中,披露信息的一方为“披露方”,而接受信息的一方为“接受方”。 - -2. 除本协议另有规定的情况外,未经披露方事先书面同意,接受方不可为其自身业务目的或其他目的使用或向任何第三方披露任何披露方的保密信息。双方应保证本方雇员履行上述义务。 - -3. 保密义务不适用于以下各项: - 1)一方为本协议之目的向其关联方或专业顾问进行的信息披露。 - 2)由一方独立开发或从有权批露该等信息的第三方获得或非因违反本协议而为公众所知的。 - 3)法律、法规或具有管辖权的任何法院、监管机构或其他政府部门作出的具有约束力的判决、命令或要求。 - 4)在其他任何监管或政府程序之进程中要求作出的信息披露。 - -4. 保密义务不因本协议的解除、终止或撤销而失效。 - - - -## **五、不可抗力及免责条款** - -1. 因受不可抗力影响而不能履行或不能完全履行本协议的一方无需承担违约责任。但是,遇有不可抗力事件的一方,应立即将事件情况书面通知对方,并应于5个工作日内出示有效证明。双方按照事件对协议的履行的影响程度,再行协商决定是否继续履行本协议或终止协议。 - -2. 鉴于互联网之特殊性质,TapTap的免责事由亦包括但不限于下列任何影响TapTap平台正常运营之情形,TapTap应及时与相关单位配合进行修复: - 1)黑客攻击、计算机病毒侵入或发作。 - 2)基础运营商或主管部门技术调整导致之影响。 - 3)由于您原因(包括但不限于操作失误、系统故障等)导致的。 - 4)计算机系统遭到破坏、瘫痪或无法正常使用导致TapTap未能依约提供服务。 - 5)因法律、法规、政策调整或政府管制(政府专项行动等)而造成的暂时性关闭、服务调整等。 - 6)其他非TapTap过错造成的原因等。 - - - -## **六、法律适用及争议解决** - -1. 本协议签订地为中华人民共和国上海市静安区。 - -2. 本协议的订立、履行、变更、终止、解除等一切问题均适用中华人民共和国大陆地区法律(不包括冲突法)。 - -3. 若双方之间发生任何纠纷或争议,首先应友好协商解决;协商不成的,双方在此完全同意将纠纷或争议提交协议签订地有管辖权的人民法院解决。 - - - -## **七、其他** - -1. 本协议内容包括协议正文及所有与TapTap平台相关的已经发布的或将来可能发布的各类TapTap平台规则。上述内容为本协议不可分割的一部分,与本协议正文具有同等法律效力。您在加入TapTap平台薪火计划后,应予遵守。 - -2. 本协议有效期自动涵盖合作期限,除非双方另有约定或重新签订同类协议,本协议在您使用TapTap平台薪火计划服务期间持续有效。 - -3. **为给您提供更好的服务或因国家法律法规、监管政策、技术条件、产品功能等变化需要,我们可能会修订本协议的内容,修订后的协议内容一经在相关页面公布即代替原来的协议。同时,我们将以适当的方式(包括但不限于短信、邮件、站内通知、网站公告等)提醒您关注更新后的协议内容,以便您及时了解本协议的最新版本。如您对更新后的协议有异议,请立即停止使用本服务;如您继续使用本服务,则视为您对更新后协议的认可。** - -4. 本协议具有多种语言版本,若其他语言版本与简体中文版本发生冲突,应以简体中文版本为准。本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据;本协议条款无论因何种原因部分无效或不可执行,其余条款仍有效,对双方具有约束力。 diff --git a/docs/shadow/fire-test/fire.mdx b/docs/shadow/fire-test/fire.mdx deleted file mode 100644 index e87432e26..000000000 --- a/docs/shadow/fire-test/fire.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: 薪火计划 -sidebar_label: 产品介绍 -sidebar_position: 1 -slug: /sdk/fire-test/fire ---- - -## 介绍 - -薪火计划旨在帮助开发者在游戏开发、游戏预热、上线曝光、下载转化等各个阶段降低成本,提高收益。 - -加入薪火计划,您将获得 - -- [免费的 TDS 服务](/sdk/):合作期内,可享受免费的技术服务。如云存储、云存档、排行榜等,短信服务除外。 -- 额外的曝光流量:合作期内,每个自然月均可获得 1 次曝光流量倾斜的机会,自定义使用时间段,预计可增加 30%-50% 的曝光流量 -- 免费的APK加固服务:合作期内,游戏可以享受免费的APK加固服务,每个自然月加固成功次数不允许超过5次。 - -## 如何加入及相关要求 - -1. 通过开发者中心的「游戏服务」-「薪火计划」,根据要求提交申请资料。 -2. 游戏必须已接入 TapTap 登录功能。 -3. 合作期内,游戏仅在 TapTap 上线,不可主动提交上架到其他平台,否则 TapTap 将保留追责的权利。 -4. 更多细节,可查看 [TapTap 平台薪火计划服务协议](/sdk/fire-test/fire-service-doc/) - -## 常见问题及指南 - -### 薪火计划合作期限说明 - -- 加入薪火计划的游戏,默认3个月的合作期限,到达合作结束时间会进行自动续约,每次续约时长为1个自然月。 -- 开发者在游戏加入薪火计划1个月后,支持主动取消自动续约,取消自动续约后,则到达合作结束时间,薪火合作结束。如需立刻终止薪火合作,请通过工单联系客服处理。 -- 已经取消自动续约的游戏,可重新开通薪火计划,重新开通后,薪火计划次日生效。 - -### TDS 免费服务的使用攻略 - -- 薪火计划合作期内,开发者可免费享受 TDS 的收费服务(除短信服务),进行正常接入后即可直接使用。如被平台发现游戏涉嫌违规操作,TapTap 平台有权立即终止权益。 -- 通过工单咨询对应服务,我们的技术支持同事将为您提供全方位的服务。 - -### 薪火计划的额外曝光权益使用攻略 - -- 加入薪火计划后,可直接获取 3 次额外曝光流量权益,同一个自然月内最多可使用两次加速权益; -- 获得权益后如何使用? - - 在薪火计划详情页会实时展示当前的权益可用次数,点击「使用流量权益」,可配置曝光流量计划。 - - 确认使用后,该次流量权益则视为"已使用"状态,系统将在 72 小时后返回使用结果。 - - 使用成功并生效的权益将统计该次曝光量,未在 72h 内生效的权益,将自动恢复,开发者可再次使用。 -- 什么时候使用曝光权益可以获得更好的效果? - - 额外曝光权益是基于游戏原有的曝光权重进行合理加权,提升曝光权重,故在游戏本身的基础流量较高时,可以获得更好效果。建议在游戏首发、重大版本更新、重大运营活动时使用。 - -### 涉嫌违规操作怎么办? - -#### 违规操作包括不限于以下行为: - -- 游戏在非 TapTap 的应用平台上架; -- 同一个加入薪火计划的游戏 ID 被用于多个游戏的服务接入; -- 游戏内容违反相关法律法规; - -#### 涉嫌违规操作会有什么影响? - -- 如游戏涉嫌违规操作,TapTap 平台会立即终止薪火权益,包括 TDS 免费游戏服务和额外的流量权益。在 14 个工作日的处理周期内未按要求完成整改,将停用 TDS 免费服务的使用权,且处理周期内及之后产生的费用将由开发者自行承担,同时对游戏进行下架隐藏处理,并将按照「**TapTap 平台薪火计划服务协议**」中规定的内容,进行追责。 - -#### 涉嫌违规操作如何整改? - -- 如涉嫌上架到其他平台,请尽快联系对应平台的工作人员,下架您的游戏 -- 如违规使用 TDS 免费服务,请立即停止超规的使用行为 diff --git a/docs/shadow/friends/_category_.json b/docs/shadow/friends/_category_.json deleted file mode 100644 index e777c9648..000000000 --- a/docs/shadow/friends/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "游戏好友", - "collapsed": true, - "position": 6 -} diff --git a/docs/shadow/friends/features.mdx b/docs/shadow/friends/features.mdx deleted file mode 100644 index 29f7cc51a..000000000 --- a/docs/shadow/friends/features.mdx +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: 好友功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 -slug: /sdk/friends/features ---- - - - - - - -import { Conditional } from "/src/docComponents/conditional"; - -## 产品简介 - -TapTap 开发者服务(简称 TDS)为游戏提供了完整的好友解决方案,支持游戏灵活调用各类功能接口,帮助游戏快速形成社交网络,助力游戏运营。 - -## 产品优势 - -- **一站式接入好友能力,提升开发效率** - - 好友产品为游戏开发者提供完整的添加、删除、查找好友的功能接口,游戏无需自建好友系统,大大降低开发成本。 - -- **数据实时追踪,精细化运营提升体验** - - 开发者后台提供丰富的管理功能,支持开发者查询玩家的好友关系、接口调用等数据,帮助开发者实时管理玩家数据,跟踪好友接口使用情况。 - -- **连接 TapTap 生态,基于社交关系带动游戏活跃** - - 游戏可获取到 TapTap 的互相关注好友关系,并用于丰富游戏的社交关系链,完成上线开黑、组队约玩、社交冷启等提升游戏体验。 - -## 概念解释 - -**游戏好友** - -基于 TDS 内建账户系统,提供添加、删除、查找好友关系等接口。游戏好友关系模型分为「关注模式」和「好友模式」。 - -- 关注模式 - - 玩家 A 可自主关注玩家 B,此时 A 为 B 的粉丝,B 为 A 的关注。此时如果 B 回关了 A,那么 A 和 B 称为互相关注关系。 - -- 好友模式 - - 当玩家 A 添加玩家 B 为好友时,需要先发出加好友的申请添加好友申请,玩家 B 同意好友申请,AB 即为好友关系。 - -开发者仅可选择「关注模式」、「好友模式」其中一种好友模型接入,不可二者同时接入。 - - - -**TapTap 平台好友** - -针对使用 TapTap 账号登录用户,支持获取 TapTap 同玩互关好友列表,作为游戏好友来源。详情请参考 [TapTap 好友](/sdk/tap-friend/features/)。 - - - -:::tip -使用游戏好友需要接入 TDS 内建账号系统。 -::: - -## 能力说明 - -### 游戏好友 - -#### 关注模式 - -| **能力名称** | **应用场景** | -| -------------------- | ------------------------------------------------------------------------------------------------------------------ | -| 获取关注列表 | 提供获取用户关注列表接口,支持开发者查询当前用户的关注列表 | -| 获取互关列表 | 提供获取用户互关列表接口,支持开发者查询当前用户的关注列表 | -| 获取黑名单列表 | 提供获取用户黑名单列表接口,支持开发者查询当前用户的黑名单列表 | -| 添加关注 | 提供关注好友的接口,支持玩家在主页社交列表、游戏结算等场景直接关注对方 | -| 取消关注 | 提供取消关注的功能接口,支持玩家取消关注来解除关系 | -| 添加拉黑 | 提供添加拉黑的接口,支持玩家在主页社交列表、游戏结算等场景直接拉黑对方(当前黑名单最大限制为 100,暂不支持可配置) | -| 取消拉黑 | 提供取消拉黑的接口,支持玩家从黑名单中取消拉黑对方 | -| 查询并关注玩家 | 支持通过搜索昵称、好友码(每个玩家拥有唯一的 6 位纯小写好友码)、`objectId` 来查找并关注玩家 | -| 分享邀请关注 | 支持用户分享邀请关注链接至三方社交平台,邀请他人在游戏内关注自己 | -| 获取粉丝列表 | 提供获取用户粉丝列表接口,支持开发者查询当前用户的粉丝列表 | -| 富信息 | 富信息用于呈现玩家状态等信息,如在线状态、正在使用哪个英雄、正处于哪个游戏模式等 | -| 支持按照在线状态排序 | 关注列表,互关列表支持根据用户在线状态进行排序。(粉丝列表暂不支持) | - -#### 好友模式 - -| **能力名称** | **应用场景** | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| 获取好友列表 | 提供获取好友列表接口,支持开发者查询当前用户的好友列表 | -| 查询好友关系 | 提供查询好友关系接口,支持开发者在游戏的不同场景内查询不同玩家之间是否为好友 | -| 添加好友 | 提供发送申请添加好友的接口,支持游戏在玩家主页、游戏结算等场景直接申请添加对方为好友。同时支持玩家查看自己收到的添加好友申请玩家列表 | -| 查询并添加好友 | 支持通过搜索昵称、好友码(每个玩家拥有唯一的 6 位纯小写好友码)、`objectId` 来查找并添加好友 | -| 分享邀请添加好友 | 支持用户分享邀请好友链接至三方社交平台,邀请他人在游戏内成为自己的好友 | -| 删除好友 | 提供删除好友的功能接口,支持玩家删除好友来解除关系 | -| 查询好友申请列表 | 提供查询好友申请列表接口,支持开发者获取某个玩家收到好友申请的所有玩家列表 | -| 同意成为好友 | 提供同意成为好友接口,支持玩家收到好友申请后,同意成为好友 | -| 拒绝成为好友 | 提供拒绝成为好友接口,支持玩家收到好友申请后,拒绝成为好友 | -| 获取黑名单列表 | 提供获取用户黑名单列表接口,支持开发者查询当前用户的黑名单列表 | -| 添加拉黑 | 提供添加拉黑的接口,支持玩家在主页社交列表、游戏结算等场景直接拉黑对方(当前黑名单最大限制为 100,暂不支持可配置) | -| 取消拉黑 | 提供取消拉黑的接口,支持玩家从黑名单中取消拉黑对方 | -| 按照在线状态排序 | 好友列表支持根据用户在线状态进行排序 | -| 设置好友数量上限 | 可在控制台(开发者中心 > 游戏服务 > 云服务 > 好友 > 设置)配置「游戏内支持玩家最多添加的好友数量」,最大可配置为 2000 | -| 富信息 | 富信息用于呈现玩家状态等信息,如在线状态、正在使用哪个英雄、正处于哪个游戏模式等 | - - -## 收费方案 - -- 游戏好友服务根据接口请求次数计费,接口请求次数合并统计至数据存储服务,共享每天 3 万次免费额度,超出部分 1.0 元 / 万次。 - -## 常见 Q&A - -Q:可以同时接入「关注模式」和「好友模式」两种好友模型吗? - -A:不可以,开发者仅能选择一种好友模型接入,接入前请谨慎选择。 - -Q:「关注模式」下有关注上限吗? - -A:有,关注模式下单个游戏最多支持关注 5000 人,关注限制暂不支持调整以及后台配置。 - -Q:分享链接邀请文案可支持自由配置吗? - -A:支持;建议「关注模式」下文案为「xx 邀请你关注 Ta」,「好友模式」下文案为「xx 邀请你成为好友」。 - -如上述 FAQ 不能解答你的问题,欢迎前往开发者中心导航栏右上角提交工单联系我们。 diff --git a/docs/shadow/friends/follow.mdx b/docs/shadow/friends/follow.mdx deleted file mode 100644 index 15ce8a7d9..000000000 --- a/docs/shadow/friends/follow.mdx +++ /dev/null @@ -1,1140 +0,0 @@ ---- -title: 关注模式 -sidebar_position: 4 -slug: /sdk/friends/follow ---- - - - - - - -import MultiLang from "/src/docComponents/MultiLang"; - -阅读本文前请先[完成 SDK 初始化](/sdk/friends/guide/)。 - -## 好友通知设置 -好友模块默认会向游戏推送好友状态及申请的通知,如果游戏需要关闭,可调用如下接口: - - - -```cs -try { - await TDSFollows.DisableFriendNotification(); - TapLog("关闭推送通知成功"); - } catch (Exception e) { - TapLog($"关闭推送通知失败: ${e}"); - } - -``` - -```java -TDSFollows.disableFriendNotification(new Callback() { - @Override - public void onSuccess(Void result) { - toast("消息推送关闭成功"); - } - - @Override - public void onFail(TDSFriendError error) { - toast("消息推送关闭失败 " + error.detailMessage); - } - }); -``` - -```objc - void (^callback)(BOOL succeeded, NSError * error) = ^(BOOL succeeded, NSError * _Nullable error){ - if (succeeded) { - [LogHelper log:LogInfoTypeDisplay :[NSString stringWithFormat:@"设置成功"]]; - } else { - [LogHelper log:LogInfoTypeError :[NSString stringWithFormat:@"设置失败"]]; - } - }; - -[TDSFollows disableFriendNotificationWithCallback:callback]; - -``` - - - -当关闭通知后,如果需要再次开启,可调用如下接口: - - - -```cs - try { - await TDSFollows.EnableFriendNotification(); - TapLog("关闭推送通知成功"); - } catch (Exception e) { - TapLog($"关闭推送通知失败: ${e}"); - } - -``` - -```java -TDSFollows.enableFriendNotification(new Callback() { - @Override - public void onSuccess(Void result) { - toast("消息推送开启成功"); - } - - @Override - public void onFail(TDSFriendError error) { - toast("消息推送开启失败 " + error.detailMessage); - } - }); -``` - -```objc - void (^callback)(BOOL succeeded, NSError * error) = ^(BOOL succeeded, NSError * _Nullable error){ - if (succeeded) { - [LogHelper log:LogInfoTypeDisplay :[NSString stringWithFormat:@"设置成功"]]; - } else { - [LogHelper log:LogInfoTypeError :[NSString stringWithFormat:@"设置失败"]]; - } - }; - -[TDSFollows enableFriendNotificationWithCallback:callback]; -``` - - - - -## 响应好友变化通知 - -好友模块支持客户端监听好友状态变化,在游戏中实时给玩家提示。 -你需要在调用上线接口前注册好友状态变更监听实例,这样,玩家上线后就能收到相应通知: - - - -```cs -TDSFollows.FriendStatusChangedDelegate = new TDSFriendStatusChangedDelegate { - // 当前玩家成功上线(长连接建立成功) - OnConnected = () => {}, - // 当前玩家长连接断开,SDK 会自动重试,开发者通常无需额外处理 - OnDisconnected = () => {}, - // 当前连接异常 - OnConnectionError = (code, message) => {}, -}; -``` - -```java -TDSFollows.registerFriendStatusChangedListener(new FriendStatusChangedListener() { - // 当前玩家成功上线(长连接建立成功) - @Override - public void onConnected() {} - - // 当前玩家长连接断开,SDK 会自动重试,开发者通常无需额外处理 - @Override - public void onDisconnected() {} - - // 当前连接异常 - @Override - public void onConnectError(int code, String msg){}); -} -``` - -```objc -[TDSFollows registerNotificationDelegate:self]; - -// 当前玩家成功上线(长连接建立成功) -- (void)onConnected {} - -// 当前玩家长连接断开,SDK 会自动重试,开发者通常无需额外处理 -- (void)onDisconnected {} - -// 当前连接异常 -- (void)onDisconnectedWithError:(NSError * _Nullable)error {} -``` - - - -如果想要停止监听: - - - -```cs -TDSFollows.FriendStatusChangedDelegate = null; -``` - -```java -TDSFollows.removeFriendStatusChangedListener(); -``` - -```objc -[TDSFollows unregisterNotificationDelegate]; -``` - - - -## 玩家上线 - -玩家成功登录后,需要调用该接口建立和好友服务云端的长连接。 -长连接建立后,如果网络临时中断,SDK 会在网络恢复后自动重连。 - - -<> - -```cs -await TDSFollows.Online(); -``` - - -<> - -```java -TDSFollows.online(new Callback() { - @Override - public void onSuccess(Void result) { - // 成功 - } - - @Override - public void onFail(TDSFriendError error) { - // 处理异常 - } -}); - -``` - -建立长连接后,如果玩家通过好友邀请链接打开游戏,那么 Android SDK 也会自动发送对应的好友申请。 - - -<> - -```objc -[TDSFollows online]; -``` - - - - -## 玩家下线 - -玩家登出后,需要调用此接口断开和云端的长连接。 - - - -```cs -await TDSFollows.Offline(); -``` - -```java -TDSFollows.offline(); -``` - -```objc -[TDSFollows offline]; -``` - - - -## 根据昵称查询好友 - -在不知道玩家 objectId 的情况下,可以通过玩家昵称查询好友。 -例如,搜索昵称为 `Tarara` 的好友: - - - -```cs -ReadOnlyCollection friendInfos = await TDSFollows.SearchUserByName("Tarara"); -foreach (TDSFriendInfo info in friendInfos) { - // 玩家信息 - TDSUser user = info.User; - // 富信息数据,详见后文 - Dictionary richPresence = info.RichPresence; - // 好友是否在线 - bool online = info.Online; -} -``` - -```java -TDSFollows.searchUserByName("Tarara", new ListCallback() { - @Override - public void onSuccess(List friendInfoList) { - for (TDSFriendInfo info : friendInfoList) { - // 玩家信息 - TDSUser user = info.getUser(); - // 富信息数据,详见后文 - TDSRichPresence richPresence = info.getRichPresence(); - // 好友是否在线 - boolean online = info.isOnline(); - } - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed search friend by nickname" + error.detailMessage); - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFollows searchUserWithNickname:@"Tarara" option:option -callback:^(NSArray * _Nullable friendInfos, NSError * _Nullable error) { - if (friendInfos) { - for (TDSFriendInfo *info in friendInfos) { - // 玩家信息 - TDSUser *user = info.user; - // 富信息数据,详见后文 - NSDictionary *richPresence = info.richPresence; - // 好友是否在线 - BOOL online = info.online; - } - } else if (error) { - // 处理错误 - } -}]; -``` - - - -注意,**使用这一功能的前提是内建账户系统中设置了 `nickname`(昵称)字段**。 -参见[内建账户系统文档](/sdk/authentication/guide/#设置其他用户属性)。 - -## 好友码 - -每个已登录玩家都有一个好友码,可以分享给其他玩家用于添加好友。 - -访问 `TDSUser` 的 `shortId` 属性可获取好友码: - - - -```cs -// currentUser 是已登录的 TDSUser -string shortId = currentUser["shortId"]; -``` - -```java -String shortId = currentUser.getString("shortId"); -``` - -```objc -NSString *shortId = currentUser[@"shortId"]; -``` - - - -可以通过好友码查询玩家: - - - -```cs -TDSFriendInfo friendInfo = await TDSFollows.SearchUserByShortCode(shortId); -``` - -```java -TDSFollows.searchUserByShortCode(shortId, new Callback() { - @Override - public void onSuccess(TDSFriendInfo friendInfo) { /* 略(参见上节) */ } - - @Override - public void onFail(TDSFriendError error) { /* 略(参见上节) */ } -}); -``` - -```objc -[TDSFollows searchUserWithShortCode:shortId -callback:^(TDSFriendInfo * _Nullable friendInfo, NSError * _Nullable error) { - // 略(参见上节) -}]; -``` - - - -## 根据 objectId 查询好友 - -除了昵称、好友码外,还可以根据 objectId 查找好友。 - -查询 objectId 为 `5b0b97cf06f4fd0abc0abe35` 的好友: - - - -```cs -TDSFriendInfo friendInfo = await TDSFollows.SearchUserById("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFollows.searchUserById("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(TDSFriendInfo friendInfo) { /* 略 */ } - - @Override - public void onFail(TDSFriendError error) { /* 略 */ } -}); -``` - -```objc -[TDSFollows searchUserWithObjectId:@"5b0b97cf06f4fd0abc0abe35" -callback:^(TDSFriendInfo * _Nullable friendInfo, NSError * _Nullable error) { - // 略 -}]; -``` - - - -## 富信息 - -富信息用于呈现玩家状态等信息,如在线状态、正在使用哪个英雄、正处于哪个游戏模式等。 - -在控制台添加富信息相关配置后,可以根据已配置的富信息字段,设置对应的富信息内容: - - - -```cs -await TDSFollows.SetRichPresence("score", "60"); -``` - -```java -TDSFollows.setRichPresence("score", "60", new Callback() { - @Override - public void onSuccess(Void result) { - toast("Succeed to set rich presence."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to set rich presence: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFollows setRichPresenceWithKey:@"score" value:@"60" - callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Succeed to set rich presence. - } else if (error) { - // Failed to set rich presence. - } -}]; -``` - - - -这里 `score` 是在控制台配置的富信息字段。 -富信息的字段有两种类型: - -- `variable` 类型,值是字符串。例如,之前的代码实例中,`score` 在控制台配置为 `variable` 类型,因此客户端设置富信息字段的值时填了 `60`,云端返回给客户端的富信息为 `"score": "60"`,在游戏界面该玩家的好友会看到当前玩家的富信息显示为「得分 60」之类。这里,开发者需要自行实现将 `score` 显示为「得分」等本地化内容的逻辑。 - -- `token` 类型,值是以 `#` 开头的字符串。例如,下面的代码实例中 `display` 字段的类型是 `token`,客户端设置富信息字段的值时填了 `#matching`,这个值在云端会进行多语言匹配,返回给客户端的富信息会直接替换为本地化的内容:`"display": "匹配中"`。注意,如果多语言匹配失败则会返回空字符串(`"display": " "`)。 - -需要一次性配置多个字段时,可以传入一组字段: - - - -```cs -Dictionary info = new Dictionary(); -info.Add("score", "60"); -info.Add("display", "#matching"); -await TDSFollows.SetRichPresences(info); -``` - -```java -Map info = new HashMap<>(); -info.put("score", "60"); -info.put("display", "#matching"); -TDSFollows.setRichPresence(info, new Callback() { - // 略 -}); -``` - -```objc -[TDSFollows setRichPresencesWithDictionary:@{ - @"score" : @"60", - @"display" : @"#matching", -} callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略 -}]; -``` - - - -控制台**最多配置 20 个富信息字段**,字段名(key)长度不超过 128 bytes,字段值(value)长度不超过 256 bytes。 - -如需清除当前玩家的某项富信息,可以调用以下接口: - - - -```cs -TDSFollows.ClearRichPresence("score"); -``` - -```java -TDSFollows.clearRichPresence("score", new Callback() { - // 略 -}); -``` - -```objc -[TDSFollows clearRichPresenceWithKey:@"score" -callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略 -}]; -``` - - - -同样,可以批量清除一组富信息: - - - -```cs -IEnumerable keys = new string[] {"score", "display"} -await TDSFollows.ClearRichPresences(keys); -``` - -```java -List keys = new ArrayList<>(); -keys.add("score"); -keys.add("display"); -TDSFollows.clearRichPresence(keys, new Callback() { - // 略 -}); -``` - -```objc -[TDSFollows clearRichPresencesWithKeys:@[@"score", @"display"] -callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略 -}); -``` - - - -设置和清除富信息接口有调用频率限制,每 30s 最多各触发一次。 - -富信息提供了 [REST API 接口](/sdk/friends/guide/#富信息-rest-api)。 -开发者可以自行编写程序或脚本调用这些接口在服务端进行管理性质的操作。 - -## 关注 - -可以通过指定[好友码](/sdk/friends/guide/#好友码)关注相应玩家: - - - -```cs -await TDSFollows.FollowByShortCode(shortId); -``` - -```java -TDSFollows.followByShortCode(shortId, new Callback() { - @Override - public void onSuccess(HandleResult result) { - // 成功关注 - } - - @Override - public void onFail(TDSFriendError error) { - // 处理错误 - } -}); -``` - -```objc -[TDSFollows followWithShortCode:shortId callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - if (error) { - // handle error - } else { - // handle result - } -}]; -``` - - - -关注玩家时可以指定额外的属性,例如,将对方放入 `coworkers`(同事)分组: - - - -```cs -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFollows.FollowByShortCode(shortId); -``` - -```java -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFollows.followByShortCode(shortId, attrs, new Callback() { - // 略 -}); -``` - -```objc -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFollows followWithShortCode:shortId attributes:attributes callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -此外,也可以通过指定某个 `TDSUser` 的 `objectId` 来关注他。 -比如,假设 Tarara 的 `objectId` 是 `5b0b97cf06f4fd0abc0abe35`,可以通过以下代码添加她: - - - -```cs -await TDSFollows.Follow("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFollows.follow("5b0b97cf06f4fd0abc0abe35", new Callback() { - // 略 -}); -``` - -```objc -[TDSFollows followWithUserId:@"5b0b97cf06f4fd0abc0abe35" -callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -通过 `objectId` 关注玩家同样可以指定额外属性: - - - -```cs -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFollows.Follow("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFollows.follow("5b0b97cf06f4fd0abc0abe35", attrs, new Callback() { - // 略 -}); -``` - -```objc -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFollows followWithUserId:@"5b0b97cf06f4fd0abc0abe35" attributes:attributes -callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -注意,一个玩家 **最多关注 5000** 个玩家。 - -## 取关 - -可以通过指定 `objectId` 或好友码来取消关注之前关注的玩家。 -例如,之前关注了 Tarara,当前玩家后来改变主意,不想关注 Tarara 了: - - - -```cs -await TDSFollows.UnFollow("5b0b97cf06f4fd0abc0abe35"); - -await TDSFollows.UnFollowByShortCode(shortIdOfTarara); -``` - -```java -TDSFollows.unfollow("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(HandleResult result) { - // 成功取关 - } - - @Override - public void onFail(TDSFriendError error) { - // 处理错误 - } -}); - -TDSFollows.unfollowByShortCode(shortIdOfTarara, new Callback() { - // 略 -}); -``` - -```objc -[TDSFollows unfollowWithUserId:@"5b0b97cf06f4fd0abc0abe35" callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; - -[TDSFollows unfollowWithShortCode:shortIdOfTarara, callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -## 拉黑 - -玩家可以拉黑其他玩家,使其他玩家无法关注自己。 -拉黑时会解除彼此之间的关注关系。 -和关注、取关类似,可以通过 objectId 或好友码来指定想要拉黑的玩家。 -例如,假设 Tarara 的 objectId 是 `5b0b97cf06f4fd0abc0abe35`,可以这样拉黑 Tarara: - - - -```cs -await TDSFollows.Block("5b0b97cf06f4fd0abc0abe35"); - -await TDSFollows.BlockByShortCode(shortIdOfTarara); -``` - -```java -TDSFollows.block("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(HandleResult result) { - // 成功拉黑 - } - - @Override - public void onFail(TDSFriendError error) { - // 处理错误 - } -}); - -TDSFollows.blockByShortCode(shortIdOfTarara, new Callback() { - // 略 -}); -``` - -```objc -[TDSFollows blockWithObjectId:@"5b0b97cf06f4fd0abc0abe35" callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; - -[TDSFollows blockWithShortCode:shortIdOfTarara callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -玩家也可以取消拉黑之前拉黑的玩家: - - - -```cs -await TDSFollows.Unblock("5b0b97cf06f4fd0abc0abe35"); - -await TDSFollows.UnblockByShortCode(shortIdOfTarara); -``` - -```java -TDSFollows.unblock("5b0b97cf06f4fd0abc0abe35", new Callback() { - // 略 -}); - -TDSFollows.unblockByShortCode(shortIdOfTarara, new Callback() { - // 略 -}); -``` - -```objc -[TDSFollows unblockWithObjectId:@"5b0b97cf06f4fd0abc0abe35" callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; - -[TDSFollows unblockWithShortCode:shortIdOfTarara callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -如果玩家之前存在关注关系,**取消拉黑后不会自动恢复关注**。 - -玩家可以查询自己拉黑了哪些人: - - - -```cs -FriendResult result = await TDSFollows.QueryBlockList(cursor, limit, sortCondition); -``` - -```java -TDSFollows.queryBlockList(config, cursor, new Callback() { - // 略 -} -``` - -```objc -[TDSFollows queryBlockListWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - -传入的查询参数的含义参见[查询互关列表](#查询互关列表)一节。 - - - -注意: - -- 黑名单上限 100 人。也就是说,一个玩家最多拉黑 100 个玩家。 -- 玩家甲拉黑玩家乙后,不仅玩家乙无法关注玩家甲,玩家甲也无法关注玩家乙。如果玩家甲之前已经关注了玩家乙,或玩家乙之前已经关注了玩家甲,在玩家甲拉黑玩家乙时都会取消关注。 - -## 查询互关列表 - -玩家可以查询哪些玩家和自己是「互相关注」关系。 -该接口除了会返回好友列表外,还会返回游标。 -指定游标和返回数量,可以实现翻页功能。 -同时,支持根据在线状态获取排序后的结果(当前在线的玩家在前): - - - -```cs -// 首次查询 -string cursor = null; -// 默认 50,最大 500 -int limit = 50; -// 根据在线状态排序 -SortCondition sortCondition = SortCondition.OnlineCondition -FriendResult result = await TDSFollows.QueryMutualList(cursor, limit, sortCondition); - -ReadOnlyCollection friendInfos = result.FriendList; -foreach (TDSFriendInfo info in friendInfos) { - // 玩家信息 - TDSUser user = info.User; - // 富信息数据 - Dictionary richPresence = info.RichPresence; - // 玩家是否在线 - bool online = info.Online; -} - -// 翻页 -string cursor = result.Cursor; -FriendResult more = await TDSFollows.QueryMutualList(cursor, limit, sortCondition); -``` - -```java -FriendRequestConfig config = new FriendRequestConfig.Builder() - .pageSize(50) /* 默认 50,最大 500 */ - .sortCondition(SortCondition.getOnlineCondition()) /* 根据在线状态排序 */ - .build(); -// 首次查询 -TDSFollows.queryMutualList(config, null, new Callback() { - @Override - public void onSuccess(FriendResult result) { - List friendInfoList = result.getFriendList(); - for (TDSFriendInfo info : friendInfoList) { - // 玩家信息 - TDSUser user = info.getUser(); - // 富信息数据 - TDSRichPresence richPresence = info.getRichPresence(); - // 玩家是否在线 - boolean online = info.isOnline(); - } - - // 翻页 - String cursor = result.getCursor(); - TDSFollows.queryMutualList(config, cursor, new Callback() { - /* 略 */ - } - } - @Override - public void onFail(TDSFriendError error) { - toast("query error = " + error.code + " msg = " + error.detailMessage); - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.limit = 50;// 默认 50,最大 500 -__block NSString *cursor; // 游标 -TDSFriendQuerySortCondition *sort = [TDSFriendQuerySortCondition new]; -[sort append:TDSFriendQuerySortTypeOnline error:nil]; -option.sortCondition = sort; - -[TDSFollows queryMutualListWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - for (TDSFriendInfo *info in result.friendList) - // 玩家信息 - TDSUser *user = info.user; - // 富信息数据 - NSDictionary *richPresence = info.richPresence; - // 玩家是否在线 - BOOL online = info.online; - } - cursor = result.cursor; -}]; - -// 翻页 -option.from = cursor; -[TDSFollows queryMutualListWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -注意,可以根据在线状态排序的前提是游戏正确地接入了好友服务,包括在玩家登录后调用[上线接口](#玩家上线),玩家登出后调用[下线接口](#玩家下线)。 -否则,由于游戏未正确上报玩家的在线状态,好友服务无从得知玩家的在线状态,查询结果自然也无法支持根据在线状态排序。 - -## 查询关注列表 - -类似地,玩家可以查询自己关注了哪些玩家,接口与查询互关列表类似,也同样支持根据在线状态排序: - - - -```cs -FriendResult result = await TDSFollows.QueryFolloweeList(cursor, limit, sortCondition); -``` - -```java -TDSFollows.queryFolloweeList(config, cursor, new Callback() { - // 略 -} -``` - -```objc -[TDSFollows queryFolloweeWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -## 查询粉丝列表 - -玩家还可以查询哪些玩家关注了自己,也就是自己的粉丝,接口与查询互关、关注列表类似,**但不支持根据在线状态排序**: - - - -```cs -FriendResult result = await TDSFollows.QueryFollowerList(cursor, limit, sortCondition); -``` - -```java -TDSFollows.queryFollowerList(config, cursor, new Callback() { - // 略 -} -``` - -```objc -[TDSFollows queryFollowerWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -如果查询时指定了根据在线状态排序,**云端会忽略这一条件**,仍然返回未排序的查询结果。 - -## 分享链接 - -### 落地页 - -使用分享链接功能需要首先部署落地页网站。 -落地页网站可以部署在[云引擎](/sdk/engine/overview/)或其他支持部署纯静态网站的服务器上。 -如果计划部署在云引擎上,需注意云引擎的免费实例会自动休眠,请购买标准实例使用。 - -我们提供了[开源的落地页示例项目][repo],修改相应配置后可直接构建、部署、使用。 -注意,示例项目中的 `GAME_ANDROID_LINK` 环境变量格式为 `scheme://host/path`。 -`host` 和 `path` 的值需和 Android 的 `AndroidManifest.xml` 中的值保持一致。 - -[repo]: https://github.com/taptap/TapFriends-landing-page - -例如,假设 `AndroidManifest.xml` 中的相关配置如下: - -```xml - - - - - - - - - - - -``` - -那么落地页项目中 `GAME_ANDROID_LINK` 的值为 `tapsdk://游戏应用ID/friends`。 - -落地页网站的地址需要在客户端配置: - -```cs -TDSFollows.SetShareLink("https://please-replace-with-your-domain.example.com"); -``` - -如果落地页部署在云引擎网站上,那么地址就是 `https://你的云引擎自定义域名`。 - -### 生成链接 - -部署完落地页网站并在客户端配置好相应地址后,调用以下接口即可生成好友邀请页网址: - - - -```cs -string inviteUrl = await TDSFollows.GenerateFriendInvitationLink(); -``` - -```java -TDSFollows.generateFriendInvitationLink(new Callback() { - @Override - public void onSuccess(String inviteUrl) { - System.out.println("share this link to invite your friends: " + inviteUrl); - } - - @Override - public void onFail(TDSFriendError error) { - System.out.println("Failed to generate invite link: " + error.detailMessage); - } -}); -``` - -```objc -NSError *error; -NSString *inviteUrl = [TDSFollows generateFriendInvitationLinkWithError:&error]; -``` - - - -分享链接中传递的用户名称默认为玩家昵称(`nickname`),因此,默认情况下,使用分享链接的前提是内建账户系统中设置了 `nickname`(昵称)字段。 -参见[内建账户系统文档](/sdk/authentication/guide/#设置其他用户属性)。 -如果希望使用其他名称,可以在调用上述接口时另行指定。 -另外,还可以传入其他信息,这些信息会作为 URL 查询参数拼接在好友邀请页网址后面。 -例如,昵称为 Tarara 的玩家,邀请好友时希望使用「她姥姥」这个名称,邀请链接希望附加 `ref=taptap` 这个参数,可以这样调用: - - - -```cs -Dictionary parameters = new Dictionary { - { "ref", "taptap" } -}; -string inviteUrl = await TDSFollows.GenerateFriendInvitationLink("她姥姥", parameters); -``` - -```java -Map parameters = new HashMap(); -parameters.put("ref", "taptap"); -TDSFollows.generateFriendInvitationLink("她姥姥", parameters, new Callback() { - // 略 -}); -``` - -```objc -NSError *error; -TDSFriendLinkOption *option = [TDSFriendLinkOption new]; -option.roleName = @"她姥姥"; -option.queries = @{ - @"ref" : @"taptap", -}; -NSString *inviteUrl = [TDSFollows generateFriendInvitationLinkWithOption:option error:&error]; -``` - - - -### 处理链接 - -玩家通过邀请链接打开游戏后,需要调用 SDK 提供的接口关注邀请者。 - - - -```cs -public class DeepLinkManager : MonoBehaviour -{ - // 略 - private async void onDeepLinkActivated(string url) { - await TDSFollows.HandleFriendInvitationLink(url); - } -} -``` - -```java -public void onHandleLink(View view) { - TDSFollows.handFriendInvitationLink(url, new Callback() { - @Override - public void onSuccess(HandleResult result) { - // 略 - } - - @Override - public void onFail(TDSFriendError error) { - // 略 - } - }); -} -``` - -```objc -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSFollows handleFriendInvitationLink:url - callback:^(TDSFriendHandleResult * _Nullable result, TDSFriendsLinkInfo * _Nullable linkInfo, NSError * _Nullable error) { - // 略 - }]; - /* 如果需要延迟发送关注请求,可调用该接口, delay 单位为秒 - return [TDSFollows handleFriendInvitationLink:url delay: 1 - callback:^(TDSFriendHandleResult * _Nullable result, TDSFriendsLinkInfo * _Nullable linkInfo, NSError * _Nullable error) { - // 略 - }];*/ - -} -``` - - - -开发者也可以通过 SDK 提供的接口解析链接,获取玩家的 objectId、名称、传入的其他参数,定制相应的逻辑。 - - - -```cs -public class DeepLinkManager : MonoBehaviour -{ - // 略 - private async void onDeepLinkActivated(string url) { - TDSFriendLinkInfo invitation = TDSFollows.ParseFriendInvitationLink(url); - string userObjectId = invitation.Identity; - string name = invitation.RoleName; - Dictionary parameters = invitation.Queries; - await TDSFollows.Follow(userObjectId); - } -} -``` - -```java -TDSFriendLinkInfo linkInfo = TDSFollows.parseFriendInvitationLink("url"); -String userObjectId = linkInfo.getIdentity(); -String name = linkInfo.getRoleName(); -Map parameters = linkInfo.getQueries(); -``` - -```objc -TDSFriendLinkInfo *linkInfo = [TDSFollows parseFriendInvitationLink:(NSURL *)url]; -NSString *userObjectId = linkInfo.identity; -NSString *name = linkInfo.roleName; -NSDictionary *parameters = linkInfo.queries; -``` - - - -注意: - -落地页示例项目默认使用好友模式,需要设置环境变量 `INVITE_TYPE` 为 `follow` 切换为关注模式。 diff --git a/docs/shadow/friends/guide.mdx b/docs/shadow/friends/guide.mdx deleted file mode 100644 index bc88800d7..000000000 --- a/docs/shadow/friends/guide.mdx +++ /dev/null @@ -1,380 +0,0 @@ ---- -title: 好友指南 -sidebar_label: 开发指南 -sidebar_position: 2 -slug: /sdk/friends/guide ---- - - - - - - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import UnitySDKInstallation from "../../sdk/_partials/unity-sdk-installation.mdx"; - -TDS 提供了两种好友模型: - -- [好友模式](/sdk/friends/mutual/) -- [关注模式](/sdk/friends/follow/) - -开发者可根据游戏项目需求,选择其中一种模型。 -注意: - -- 只能选择一种模型,**同一游戏无法混用两种模型**。 -- 选定一种模型后,**后续无法变更为另一种模型**。 - -此外,还支持获取[第三方平台的好友关系](/sdk/friends/third-party/)(此功能需要通过工单申请开通)。 - -我们建议开发者按照以下顺序入手: - -- 了解 [TDS 内建账户系统](/sdk/authentication/features/),好友功能依赖内建账户,下文中的玩家、用户均指 `TDSUser`。 - -- 阅读本文,了解如何初始化 SDK。 - -- 根据游戏项目需求,选定游戏将采用的好友模型,然后阅读相应的开发指南: - - - [好友模式](/sdk/friends/mutual/) - - [关注模式](/sdk/friends/follow/) - -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- 安装 UE 4.26 及以上版本 -- iOS 12 或更高版本 -- Android 5.0(API level 21)或更高版本 -- macOS 10.14.0 或更高版本 -- Windows 7 或更高版本 - -**支持平台**:Android / iOS / Windows / macOS - - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启好友服务、绑定 API 域名; - -## SDK 获取 - -由于好友功能依赖内建账户,所以集成好友功能所需 SDK 依赖库需要在[内建账户依赖库](/sdk/authentication/guide/#sdk-获取)的基础上,在 [下载页](/tap-download) 获得 TapSDK,另外添加 `TapFriends` 模块: - - - - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapFriend_${sdkVersions.taptap.android}', ext:'aar') -}`} - - - - {`TapFriendResource.bundle -TapFriendSDK.framework -TapFriendUISDK.framework`} - - - - -## SDK 初始化 - -参考[内建账户](/sdk/authentication/guide/#sdk-初始化)的初始化方法,初始化内建账户服务会同步初始化游戏好友服务; - - -:::tip - -如需使用[富信息功能](#富信息),请先在 **开发者中心 > 游戏服务 > 云服务 > 好友 > 设置** 开启 **富信息接口数据实时同步**。 -并调用 [配置富信息字段](#配置富信息字段) 的 REST API 接口设置需要使用的富信息字段。 - -::: - -## 富信息 REST API - -下面我们介绍富信息相关的 REST API 接口。 -开发者可以自行编写程序或脚本调用这些接口在服务端进行管理性质的操作。 - -### 请求格式 - -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 - -请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,参数如下表: - -| Key | Value | 含义 | 来源 | -| ---------- | ------------ | ----------------------------------------- | -------------- | -| `X-LC-Id` | `{{appid}}` | 当前应用的 `App Id`(即 `Client Id`) | 可在控制台查看 | -| `X-LC-Key` | `{{appkey}}` | 当前应用的 `App Key`(即 `Client Token`) | 可在控制台查看 | - -管理接口需要使用 `Master Key`:`X-LC-Key: {{masterkey}},master`。 -`Master Key` 即 `Server Secret`,同样可在控制台查看。 - -详见文档关于[应用凭证](/sdk/storage/guide/setup-dotnet#应用凭证)的说明。 - -### Base URL - -REST API 请求的 Base URL(下文 curl 示例中用 `{{host}}` 表示)即应用绑定的 API 自定义域名,可以在控制台绑定、查看。 -详见文档关于[域名](/sdk/storage/guide/setup-dotnet#域名)的说明。 - -### 获取富信息配置 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: {{sessionToken}}" \ - https://{{host}}/friend/v1/rich-presence/config -``` - -返回结果示例: - -```json -{ - "clientId": "YOUR CLIENT ID", - "enabled": 1, - "richMsgEnabled": true, - "onlineMsgEnabled": true, - "webSocketUrl": "wss://XXX.ws.tds1.tapapis.cn/ws/leancloud/v1", - "richPresenceFields": [ - { - "key": "display", - "type": "token" - }, - { - "key": "leadboard", - "type": "token" - }, - { - "key": "inviteable", - "type": "variable" - }, - { - "key": "score", - "type": "variable" - }, - { - "key": "rank", - "type": "variable" - } - ], - "richPresenceMultiLang": [ - { - "key": "display", - "lang": "zh_CN", - "content": { - "#playing": "游戏中", - "#idle": "在线", - "#room": "准备中", - "#matching": "组队中" - } - }, - { - "key": "display", - "lang": "en_US", - "content": { - "#playing": "Playing", - "#idle": "Idle", - "#room": "Room", - "#matching": "Matching" - } - }, - { - "key": "leadboard", - "lang": "zh_CN", - "content": { - "#score": "%score% 分,排名为 %rank%", - "#rank": "%rank% 名" - } - }, - { - "key": "leadboard", - "lang": "en_US", - "content": { - "#score": "%score% score", - "#rank": "%rank% rank" - } - } - ] -} -``` - -### 配置富信息字段 - -可以通过 REST API 设置游戏的富信息字段,需要指定各字段的名称和类型。 -这是管理接口,鉴权需要 `Master Key`: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "enableRichMsg":true, - "enableOnlineMsg":true, - "richPresenceFields":[ - {"key":"display","type":"token"}, - {"key":"leadboard","type":"token"}, - {"key":"inviteable","type":"variable"}, - {"key":"score","type":"variable"}, - {"key":"rank","type":"variable"} - ] - }' \ - https://{{host}}/friend/v2/rich-presence/config/base-info -``` - -`enableRichMsg` 和 `enableOnlineMsg` 分别用于设置是否启用富信息通知、是否启用好友上下线通知,建议两者均指定为 `true`。 -注意,**这个接口 URL 地址中的版本号是 `v2`**,和其他接口的 `v1` 不同。 - -返回结果示例: - -```json -{ - "appId": "YOUR CLIENT ID" -} -``` - -请求失败返回结果示例: - -```json -{ - "code": 400, - "error": "Missing request header" -} -``` - -### 配置多语言内容 - -配置富信息字段的多语言内容。 -这是管理接口,鉴权需要 `Master Key`: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "action":"save", - "config":[ - { - "key": "display", - "lang": "zh_CN", - "content": { - "#playing": "游戏中", - "#idle": "在线", - "#room": "准备中", - "#matching": "组队中" - } - }, - { - "key": "display", - "lang": "en_US", - "content": { - "#playing": "Playing", - "#idle": "Idle", - "#room": "Room", - "#matching": "Matching" - } - }, - { - "key": "leadboard", - "lang": "zh_CN", - "content": { - "#score": "%score% 分,竞技排名为 %rank%", - "#rank": "%rank% 名" - } - }, - { - "key": "leadboard", - "lang": "en_US", - "content": { - "#score": "%score% score", - "#rank": "%rank% rank best" - } - } - ] - }' - https://{{host}}/friend/v1/rich-presence/config/lang-info -``` - -返回结果同上节的[配置富信息字段](#配置富信息字段)接口。 - -### 获取玩家的富信息 - -可以一次性获取多个玩家的富信息(传入逗号分隔的 objectId 列表): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: {{sessionToken}}" \ - -G --data-urlencode 'ids={userObjectId,anotherUserObjectId}' - https://{{host}}/friend/v1/rich-presence/users -``` - -返回结果示例: - -```json -{ - "results": [ - { - "online": false, - "richPresence": { - "score": "15", - "leadboard": "15 分,排名为 150", - "rank": "150" - } - }, - { - "online": false, - "richPresence": {} - } - ] -} -``` diff --git a/docs/shadow/friends/mutual.mdx b/docs/shadow/friends/mutual.mdx deleted file mode 100644 index 744a1deda..000000000 --- a/docs/shadow/friends/mutual.mdx +++ /dev/null @@ -1,1476 +0,0 @@ ---- -title: 好友模式 -sidebar_position: 3 -slug: /sdk/friends/mutual ---- - - - - - - -import MultiLang from "/src/docComponents/MultiLang"; - -阅读本文前请先[完成 SDK 初始化](/sdk/friends/guide/)。 - -## 好友通知设置 -好友模块默认会向游戏推送好友状态及申请的通知,如果游戏需要关闭,可调用如下接口: - - - -```cs - try { - await TDSFriends.DisableFriendNotification(); - TapLog("关闭推送通知成功"); - } catch (Exception e) { - TapLog($"关闭推送通知失败: ${e}"); - } - -``` - -```java -TDSFriends.disableFriendNotification(new Callback() { - @Override - public void onSuccess(Void result) { - toast("消息推送关闭成功"); - } - - @Override - public void onFail(TDSFriendError error) { - toast("消息推送关闭失败 " + error.detailMessage); - } - }); -``` - -```objc -void (^callback)(BOOL succeeded, NSError * error) = ^(BOOL succeeded, NSError * _Nullable error){ - if (succeeded) { - [LogHelper log:LogInfoTypeDisplay :[NSString stringWithFormat:@"设置成功"]]; - } else { - [LogHelper log:LogInfoTypeError :[NSString stringWithFormat:@"设置失败"]]; - } - }; - -[TDSFriends disableFriendNotificationWithCallback:callback]; -``` - - - -当关闭通知后,如果需要再次开启,可调用如下接口: - - - -```cs -try { - await TDSFriends.EnableFriendNotification(); - TapLog("开启推送通知成功"); - } catch (Exception e) { - TapLog($"开启推送通知失败: ${e}"); - } - -``` - -```java -TDSFriends.enableFriendNotification(new Callback() { - @Override - public void onSuccess(Void result) { - toast("消息推送关闭成功"); - } - - @Override - public void onFail(TDSFriendError error) { - toast("消息推送关闭失败 " + error.detailMessage); - } - }); -``` - -```objc - void (^callback)(BOOL succeeded, NSError * error) = ^(BOOL succeeded, NSError * _Nullable error){ - if (succeeded) { - [LogHelper log:LogInfoTypeDisplay :[NSString stringWithFormat:@"设置成功"]]; - } else { - [LogHelper log:LogInfoTypeError :[NSString stringWithFormat:@"设置失败"]]; - } - }; - -[TDSFriends enableFriendNotificationWithCallback:callback]; - -``` - - - - -## 响应好友变化通知 - -好友模块支持客户端监听好友状态变化,在游戏中实时给玩家提示。 -你需要在调用上线接口前注册好友状态变更监听实例,这样,玩家上线后就能收到相应通知: - - - -```cs -TDSFriends.FriendStatusChangedDelegate = new TDSFriendStatusChangedDelegate { - // 新增好友(触发时机同「已发送的好友申请被接受」) - OnFriendAdd = friendInfo => {}, - // 新增好友申请 - OnNewRequestComing = req => {}, - // 已发送的好友申请被接受 - OnRequestAccepted = req => {}, - // 已发送的好友申请被拒绝 - OnRequestDeclined = req => {}, - // 好友上线 - OnFriendOnline = userId => {}, - // 好友下线 - OnFriendOffline = userId => {}, - // 好友富信息变更 - OnRichPresenceChanged = (userId, richPresence) => {}, - // 当前玩家成功上线(长连接建立成功) - OnConnected = () => {}, - // 当前玩家长连接断开,SDK 会自动重试,开发者通常无需额外处理 - OnDisconnected = () => {}, - // 当前连接异常 - OnConnectionError = (code, message) => {}, -}; -``` - -```java -TDSFriends.registerFriendStatusChangedListener(new FriendStatusChangedListener() { - // 新增好友(触发时机同「已发送的好友申请被接受」) - @Override - public void onFriendAdd(TDSFriendInfo friendInfo) {} - - // 新增好友申请 - @Override - public void onNewRequestComing(TDSFriendshipRequest request) {} - - // 通过分享链接进入游戏时触发此回调 - // 开发者可以在此回调中直接调用 handFriendInvitationLink - // 或通过 parseFriendInvitationLink 解析链接,获取相关参数再执行自定义的逻辑 - @Override - public void onReceivedInvitationLink(String url) {} - - // 已发送的好友申请被接受 - @Override - public void onRequestAccepted(TDSFriendshipRequest request) {} - - // 已发送的好友申请被拒绝 - @Override - public void onRequestDeclined(TDSFriendshipRequest request) {} - - // 好友上线 - @Override - public void onFriendOnline(String userId) {} - - // 好友下线 - @Override - public void onFriendOffline(String userId) {} - - // 好友富信息变更 - @Override - public void onRichPresenceChanged(String userId, TDSRichPresence richPresence) {} - - // 当前玩家成功上线(长连接建立成功) - @Override - public void onConnected() {} - - // 当前玩家长连接断开,SDK 会自动重试,开发者通常无需额外处理 - @Override - public void onDisconnected() {} - - // 当前连接异常 - @Override - public void onConnectError(int code, String msg){}); -} -``` - -```objc -[TDSFriends registerNotificationDelegate:self]; - -// 新增好友(触发时机同「已发送的好友申请被接受」) -- (void)onFriendAdd:(TDSFriendInfo *)info {} - -// 新增好友申请 -- (void)onNewRequestComing:(TDSFriendshipRequest *)request {} - -// 已发送的好友申请被接受 -- (void)onRequestAccepted:(TDSFriendshipRequest *)request {} - -// 已发送的好友申请被拒绝 -- (void)onRequestDeclined:(TDSFriendshipRequest *)request {} - -// 好友上线 -- (void)onFriendOnline:(NSString *)userId {} - -// 好友下线 -- (void)onFriendOffline:(NSString *)userId {} - -// 好友富信息变更 -- (void)onRichPresenceChanged:(NSString *)userId dictionary:(NSDictionary * _Nullable)dictionary {} - -// 当前玩家成功上线(长连接建立成功) -- (void)onConnected {} - -// 当前玩家长连接断开,SDK 会自动重试,开发者通常无需额外处理 -- (void)onDisconnected {} - -// 当前连接异常 -- (void)onDisconnectedWithError:(NSError * _Nullable)error {} -``` - - - -上述事件中的「好友」,均指「好友模式」下的「好友」。 -目前 SDK 暂不支持监听关注模式下的事件。 - -如果想要停止监听: - - - -```cs -TDSFriends.FriendStatusChangedDelegate = null; -``` - -```java -TDSFriends.removeFriendStatusChangedListener(); -``` - -```objc -[TDSFriends unregisterNotificationDelegate]; -``` - - - -## 玩家上线 - -玩家成功登录后,需要调用该接口建立和好友服务云端的长连接。 -长连接建立后,如果网络临时中断,SDK 会在网络恢复后自动重连。 - - -<> - -```cs -await TDSFriends.Online(); -``` - - -<> - -```java -TDSFriends.online(new Callback() { - @Override - public void onSuccess(Void result) { - // 成功 - } - - @Override - public void onFail(TDSFriendError error) { - // 处理异常 - } -}); -``` - -建立长连接后,如果玩家通过好友邀请链接打开游戏,那么 Android SDK 也会自动发送对应的好友申请。 - - -<> - -```objc -[TDSFriends online]; -``` - - - - -## 玩家下线 - -玩家登出后,需要调用此接口断开和云端的长连接。 - - - -```cs -await TDSFriends.Offline(); -``` - -```java -TDSFriends.offline(); -``` - -```objc -[TDSFriends offline]; -``` - - - -## 根据昵称查询好友 - -在不知道玩家 objectId 的情况下,可以通过玩家昵称查询好友。 -例如,搜索昵称为 `Tarara` 的好友: - - - -```cs -ReadOnlyCollection friendInfos = await TDSFriends.SearchUserByName("Tarara"); -foreach (TDSFriendInfo info in friendInfos) { - // 玩家信息 - TDSUser user = info.User; - // 富信息数据,详见后文 - Dictionary richPresence = info.RichPresence; - // 好友是否在线 - bool online = info.Online; -} -``` - -```java -TDSFriends.searchUserByName("Tarara", new ListCallback() { - @Override - public void onSuccess(List friendInfoList) { - for (TDSFriendInfo info : friendInfoList) { - // 玩家信息 - TDSUser user = info.getUser(); - // 富信息数据,详见后文 - TDSRichPresence richPresence = info.getRichPresence(); - // 好友是否在线 - boolean online = info.isOnline(); - } - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed search friend by nickname" + error.detailMessage); - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends searchUserWithNickname:@"Tarara" option:option -callback:^(NSArray * _Nullable friendInfos, NSError * _Nullable error) { - if (friendInfos) { - for (TDSFriendInfo *info in friendInfos) { - // 玩家信息 - TDSUser *user = info.user; - // 富信息数据,详见后文 - NSDictionary *richPresence = info.richPresence; - // 好友是否在线 - BOOL online = info.online; - } - } else if (error) { - // 处理错误 - } -}]; -``` - - - -注意,**使用这一功能的前提是内建账户系统中设置了 `nickname`(昵称)字段**。 -参见[内建账户系统文档](/sdk/authentication/guide/#设置其他用户属性)。 - -## 好友码 - -每个已登录玩家都有一个好友码,可以分享给其他玩家用于添加好友。 - -访问 `TDSUser` 的 `shortId` 属性可获取好友码: - - - -```cs -// currentUser 是已登录的 TDSUser -string shortId = currentUser["shortId"]; -``` - -```java -String shortId = currentUser.getString("shortId"); -``` - -```objc -NSString *shortId = currentUser[@"shortId"]; -``` - - - -可以通过好友码查询玩家: - - - -```cs -TDSFriendInfo friendInfo = await TDSFriends.SearchUserByShortCode(shortId); -``` - -```java -TDSFriends.searchUserByShortCode(shortId, new Callback() { - @Override - public void onSuccess(TDSFriendInfo friendInfo) { /* 略(参见上节) */ } - - @Override - public void onFail(TDSFriendError error) { /* 略(参见上节) */ } -}); -``` - -```objc -[TDSFriends searchUserWithShortCode:shortId -callback:^(TDSFriendInfo * _Nullable friendInfo, NSError * _Nullable error) { - // 略(参见上节) -}]; -``` - - - -## 根据 objectId 查询好友 - -除了昵称、好友码外,还可以根据 objectId 查找好友。 - -查询 objectId 为 `5b0b97cf06f4fd0abc0abe35` 的好友: - - - -```cs -TDSFriendInfo friendInfo = await TDSFriends.SearchUserById("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFriends.searchUserById("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(TDSFriendInfo tdsFriendInfo) { - /* 略(参见上节) */ - } - @Override - public void onFail(TDSFriendError tdsFriendError) { - /* 略(参见上节) */ - } -}); -``` - -```objc -[TDSFriends searchUserWithObjectId:@"5b0b97cf06f4fd0abc0abe35" -callback:^(TDSFriendInfo * _Nullable friendInfo, NSError * _Nullable error) { - // 略(参见上节) -}]; -``` - - - -## 富信息 - -富信息用于呈现玩家状态等信息,如在线状态、正在使用哪个英雄、正处于哪个游戏模式等。 - -在控制台添加富信息相关配置后,可以根据已配置的富信息字段,设置对应的富信息内容: - - - -```cs -await TDSFriends.SetRichPresence("score", "60"); -``` - -```java -TDSFriends.setRichPresence("score", "60", new Callback() { - @Override - public void onSuccess(Void result) { - toast("Succeed to set rich presence."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to set rich presence: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFriends setRichPresenceWithKey:@"score" value:@"60" - callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Succeed to set rich presence. - } else if (error) { - // Failed to set rich presence. - } -}]; -``` - - - -这里 `score` 是在控制台配置的富信息字段。 -富信息的字段有两种类型: - -- `variable` 类型,值是字符串。例如,之前的代码实例中,`score` 在控制台配置为 `variable` 类型,因此客户端设置富信息字段的值时填了 `60`,云端返回给客户端的富信息为 `"score": "60"`,在游戏界面该玩家的好友会看到当前玩家的富信息显示为「得分 60」之类。这里,开发者需要自行实现将 `score` 显示为「得分」等本地化内容的逻辑。 - -- `token` 类型,值是以 `#` 开头的字符串。例如,下面的代码实例中 `display` 字段的类型是 `token`,客户端设置富信息字段的值时填了 `#matching`,这个值在云端会进行多语言匹配,返回给客户端的富信息会直接替换为本地化的内容:`"display": "匹配中"`。注意,如果多语言匹配失败则会返回空字符串(`"display": " "`)。 - -需要一次性配置多个字段时,可以传入一组字段: - - - -```cs -Dictionary info = new Dictionary(); -info.Add("score", "60"); -info.Add("display", "#matching"); -await TDSFriends.SetRichPresences(info); -``` - -```java -Map info = new HashMap<>(); -info.put("score", "60"); -info.put("display", "#matching"); -TDSFriends.setRichPresence(info, new Callback() { - // 略 -}); -``` - -```objc -[TDSFriends setRichPresencesWithDictionary:@{ - @"score" : @"60", - @"display" : @"#matching", -} callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略 -}]; -``` - - - -控制台**最多配置 20 个富信息字段**,字段名(key)长度不超过 128 bytes,字段值(value)长度不超过 256 bytes。 - -如需清除当前玩家的某项富信息,可以调用以下接口: - - - -```cs -TDSFriends.ClearRichPresence("score"); -``` - -```java -TDSFriends.clearRichPresence("score", new Callback() { - // 略 -}); -``` - -```objc -[TDSFriends clearRichPresenceWithKey:@"score" -callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略 -}]; -``` - - - -同样,可以批量清除一组富信息: - - - -```cs -IEnumerable keys = new string[] {"score", "display"} -await TDSFriends.ClearRichPresences(keys); -``` - -```java -List keys = new ArrayList<>(); -keys.add("score"); -keys.add("display"); -TDSFriends.clearRichPresence(keys, new Callback() { - // 略 -}); -``` - -```objc -[TDSFriends clearRichPresencesWithKeys:@[@"score", @"display"] -callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略 -}); -``` - - - -设置和清除富信息接口有调用频率限制,每 30s 最多各触发一次。 - -富信息提供了 [REST API 接口](/sdk/friends/guide/#富信息-rest-api)。 -开发者可以自行编写程序或脚本调用这些接口在服务端进行管理性质的操作。 - -## 添加好友 - -可以通过指定[好友码](/sdk/friends/mutual/#好友码)添加相应玩家为好友。 - - - -```cs -await TDSFriends.AddFriendByShortCode(shortId); -``` - -```java -TDSFriends.addFriendByShortCode(shortId, null, new Callback() { - @Override - public void onSuccess(Void result) { - toast("Applied or added."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to add a friend: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFriends addFriendWithShortCode:shortId callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Applied or added. - } else if (error) { - // Failed to add a friend. - } -}]; -``` - - - -如果当前玩家已经在对方的好友列表中,那么对方会直接成为当前玩家的好友。 -否则,会向对方发送好友申请。 - -添加好友时可以指定额外的属性,例如,将对方放入 `coworkers`(同事)分组: - - - -```cs -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFriends.AddFriendByShortCode(shortId, attrs); -``` - -```java -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFriends.addFriendByShortCode(shortId, attrs, new Callback() { - // 略(参见上面的例子) -}); -``` - -```objc -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFriends addFriendWithShortCode:shortId attributes:attributes -callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略(参见上面的例子) -}]; -``` - - - -此外,也可以通过指定某个 `TDSUser` 的 `objectId` 来添加他为好友。 -比如,假设 Tarara 的 `objectId` 是 `5b0b97cf06f4fd0abc0abe35`,可以通过以下代码添加她为好友: - - - -```cs -await TDSFriends.AddFriend("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFriends.addFriend("5b0b97cf06f4fd0abc0abe35", new Callback() { - // 略(参见上面的例子) -}); -``` - -```objc -[TDSFriends addFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" -callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略(参见上面的例子) -}]; -``` - - - -通过 `objectId` 添加好友同样可以指定额外属性: - - - -```cs -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFriends.AddFriend("5b0b97cf06f4fd0abc0abe35", attrs); -``` - -```java -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFriends.addFriend("5b0b97cf06f4fd0abc0abe35", attrs, new Callback() { - // 略(参见上面的例子) -}); -``` - -```objc -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFriends addFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" attributes:attributes callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略(参见上面的例子) -}]; -``` - - - -## 删除好友 - -成为好友的两个玩家,之后也可以单方面删除好友。 -例如,和 Tarara 成为好友后,当前玩家又改变主意,不想和 Tarara 做朋友了: - - - -```cs -await TDSFriends.DeleteFriend("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFriends.deleteFriend("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(Boolean ok) { - toast("Deleted."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to delete a friend: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFriends deleteFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Deleted. - } else if (error) { - // Failed to delete a friend. - } -}]; -``` - - - -## 拉黑 - -### 添加黑名单用户 - -将用户加入黑名单,无论双方是否是好友,都可以进行该操作。拉黑后,双方之间进行中的好友申请都会被删除,且双方无法再发起以及接受对方的好友申请。查询好友列表时也无法查到已在黑名单中的好友,黑名单用户最多 100 人。 - -假设 Tarara 的 objectId 是 `5b0b97cf06f4fd0abc0abe35`,可以这样将 Tarara 加到黑名单: - - -<> - -```cs -await TDSFriends.BlockFriend("5b0b97cf06f4fd0abc0abe35"); -``` - - -<> - -```java -TDSFriends.blockFriend("5b0b97cf06f4fd0abc0abe35", new Callback(){ - @Override - public void onSuccess(Void result) { - // block user succeed. - } - @Override - public void onFail(TDSFriendError error) { - // Failed to block. - } -}); -``` - - -<> - -```objc -[TDSFriends blockFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // block user succeed. - } else if (error) { - // Failed to block. - } -}]; -``` - - - - - -### 移除黑名单用户 - -从黑名单移除用户,如果目标用户曾经是当前用户的好友,则恢复进入当前用户好友列表。 - - -<> - -```cs -await TDSFriends.UnblockFriend("5b0b97cf06f4fd0abc0abe35"); -``` - - -<> - -```java -TDSFriends.unblockFriend("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(Void result) { - // unblock succeed. - } - - @Override - public void onFail(TDSFriendError error) { - // Failed to unblock. - } -}); -``` - - -<> - -```objc -[TDSFriends unblockFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // unblock succeed. - } else if (error) { - // Failed to unblock. - } -}]; -``` - - - - - -### 查询黑名单列表 - -分页查询黑名单用户列表。 - - -<> - -```cs -var from = 0; -var limit = 100; -ReadOnlyCollection friendInfos = await TDSFriends.QueryBlockList(from, limit); -foreach (TDSFriendInfo info in friendInfos) { - // 玩家信息 - TDSUser user = info.User; - // 富信息数据 - Dictionary richPresence = info.RichPresence; - // 好友是否在线 - bool online = info.Online; -} -``` - -其中: - -- `from` 为获取列表的起始位置,第一页为 0,下一页为上一次获取数据的总数量。 -- `limit` 为每页获取数据的数量。 - - -<> - -```java -TDSFriends.queryBlockList(0, 10, new ListCallback() { - @Override - public void onSuccess(List result) { - System.out.println("query blockList data, data = " + result); - } - - @Override - public void onFail(TDSFriendError error) { - System.out.println("query blockList failed, error = " + error); - } -}); -``` - -其中: - -- `from` 为获取列表的起始位置,第一页为 0,下一页为上一次获取数据的总数量。 -- `limit` 为每页获取数据的数量。 -- `callback` 为异步处理回调,包含黑名单用户列表的数据信息。 - - -<> - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends queryBlockListWithOption:option - callback:^(NSArray * _Nullable friendInfos, NSError * _Nullable error) { - if (friendInfos) { - for (TDSFriendInfo *info in friendInfos) { - // 玩家信息 - TDSUser *user = info.user; - // 富信息数据 - NSDictionary *richPresence = info.richPresence; - // 好友是否在线 - BOOL online = info.online; - } - } else if (error) { - // 处理错误 - } -}]; -``` - -其中: - -- `option` 为查询时的条件配置,包括:`from` 每页的开始位置;`limit` 查询数据数量。 -- `callback` 为异步处理结果回调。 - - - - - -## 查询好友申请列表 - -好友申请有三种状态: - -- `pending`,对方没有回应,还处于等待中。好友申请创建之后默认是此状态。 -- `accepted`,对方已经接受,现在双方成为好友。 -- `declined`,对方已经拒绝。 - -SDK 提供了查询好友申请的接口。 -例如,查询处于 `pending` 状态的前 20 条申请: - - -<> - -```cs -var from = 0; -var limit = 100; -ReadOnlyCollection requests = await TDSFriends.QueryFriendRequestList ( - LCFriendshipRequest.STATUS_PENDING, from, limit -); -``` - -`LCFriendshipRequest.STATUS_PENDING` 即表示好友申请状态为 `pending`。 -类似地,`LCFriendshipRequest.STATUS_ACCEPTED` 和 `LCFriendshipRequest.STATUS_DECLINED` 分别表示好友申请状态为 `accepted` 和 `declined`。 -`LCFriendshipRequest.STATUS_ANY` 则表示任意状态。 - - -<> - -```java -int from = 0; -int limit = 100; -TDSFriends.queryFriendRequestList(LCFriendshipRequest.STATUS_PENDING, from, limit, - new ListCallback(){ - - @Override - public void onSuccess(List requests) { - // requests 就是处于 pending 状态中的好友申请列表 - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to query friendship requests: " + error.detailMessage); - } -}); -``` - -上述代码示例中的 `LCFriendshipRequest.STATUS_PENDING` 即表示好友申请状态为 `pending`。 -类似地,`LCFriendshipRequest.STATUS_ACCEPTED` 和 `LCFriendshipRequest.STATUS_DECLINED` 分别表示好友申请状态为 `accepted` 和 `declined`。 -`LCFriendshipRequest.STATUS_ANY` 则表示任意状态。 - - -<> - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends queryFriendRequestWithStatus:TDSUserFriendshipRequestStatusPending - option:option - callback:^(NSArray * _Nullable requests, NSError * _Nullable error) { - // requests 就是处于 pending 状态中的好友申请列表 -}]; -``` - -上述代码示例中的 `TDSUserFriendshipRequestStatusPending` 即表示好友申请状态为 `pending`。 -类似地,`TDSUserFriendshipRequestStatusAccepted` 和 `TDSUserFriendshipRequestStatusDeclined` 分别表示好友申请状态为 `accepted` 和 `declined`。 -`TDSUserFriendshipRequestStatusAny` 则表示任意状态。 - - - - - -如果希望返回结果中携带好友申请发起人的富信息,那么可以使用以下接口: - - - -```cs -ReadOnlyCollection requests = await TDSFriends.QueryFriendRequestWithFriendStateList ( - LCFriendshipRequest.STATUS_PENDING, from, limit -); -foreach (TDSFriendshipRequest request in requests) { - // 好友申请(参见 QueryFriendRequestList) - LCFriendshipRequest req = request.FriendshipRequest; - // 申请发起人的富信息 - TDSFriendInfo info = request.FriendInfo; -} -``` - -```java -TDSFriends.queryFriendRequestWithFriendStateList(LCFriendshipRequest.STATUS_PENDING, - from, limit, new ListCallback() { - @Override - public void onSuccess(List requests) { - for (TDSFriendshipRequest request : requests) { - // 好友申请(参见 queryFriendRequestList) - LCFriendshipRequest req = request.getLcFriendshipRequest(); - // 申请发起人的富信息 - TDSFriendInfo info = request.getFriendInfo(); - } - } - - @Override - public void onFail(TDSFriendError error) { - // 处理错误 - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends queryFriendRequestAndStateWithStatus:TDSUserFriendshipRequestStatusPending - option:option - callback:^(NSArray * _Nullable requests, NSError * _Nullable error) { - for (TDSFriendshipRequest *request in requests) { - // 好友申请(参见 queryFriendRequestWithStatus) - LCFriendshipRequest *req = request.lcFriendshipRequest; - // 申请发起人的富信息 - TDSFriendInfo *info = request.friendInfo; - } -}]; -``` - - - -## 处理好友申请 - -对于新的好友请求,玩家可以同意或者拒绝,也可以什么都不做,无视这些请求,甚至直接删除。 - - - -```cs -// LCFriendshipRequest request - -// 接受 -await TDSFriends.AcceptFriendshipRequest(request); -// 接受并添加额外属性 -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFriends.AcceptFriendshipRequest(request, attrs); - -// 拒绝 -await TDSFriends.DeclineFriendshipRequest(request); -// 删除 -await request.Delete(); -``` - -```java -// LCFriendshipRequest request - -// 接受 -TDSFriends.acceptFriendRequest(request, new Callback() { - @Override - public void onSuccess(Void result) { - toast("Accepted."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to delete a friend: " + error.detailMessage); - } -}); -// 接受并添加额外属性 -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFriends.acceptFriendRequest(request, attrs, new Callback() { - // 略 -}); - -// 拒绝 -TDSFriends.declineFriendRequest(request, new Callback() { - // 略 -}); -// 删除 -TDSFriends.deleteFriendRequest(request, new Callback() { - // 略 -}); -``` - -```objc -// LCFriendshipRequest request - -// 接受 -[TDSFriends acceptFriendRequest:request attributes:nil -callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Accepted. - } else if (error) { - // Failed to accept a friend request. - } -}]; -// 接受并添加额外属性 -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFriends acceptFriendRequest:request attributes:attributes -callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略 -}]; - -// 拒绝 -[TDSFriends declineFriendRequest:request -callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略 -}]; - -[TDSFriends deleteFriendRequest:request -callback:^(BOOL succeeded, NSError * _Nullable error) { - // 略 -}]; -``` - - - -注意: - -1. 对方拒绝了当前玩家发起的好友申请之后,玩家通过之前接口的添加好友接口再次发送申请时会收到报错,表明对方不想和当前玩家成为好友。 -2. 对方删除了当前玩家发起的好友请求后,当前玩家还可以再次发起申请。 - -## 查询好友列表 - -玩家可以查询自己的好友列表。查询时可以限定返回结果数量及起始位置: - - - -```cs -var from = 0; -var limit = 100; -ReadOnlyCollection friendInfos = await TDSFriends.QueryFriendList(from, limit); -foreach (TDSFriendInfo info in friendInfos) { - // 玩家信息 - TDSUser user = info.User; - // 富信息数据 - Dictionary richPresence = info.RichPresence; - // 好友是否在线 - bool online = info.Online; -} -``` - -```java -int from = 0; -int limit = 100; -TDSFriends.queryFriendList(from, limit, - new ListCallback(){ - - @Override - public void onSuccess(List friendInfoList) { - for (TDSFriendInfo info : friendInfoList) { - // 玩家信息 - TDSUser user = info.getUser(); - // 富信息数据 - TDSRichPresence richPresence = info.getRichPresence(); - // 好友是否在线 - boolean online = info.isOnline(); - } - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to query friend list" + error.detailMessage); - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends queryFriendWithOption:option - callback:^(NSArray * _Nullable friendInfos, NSError * _Nullable error) { - if (friendInfos) { - for (TDSFriendInfo *info in friendInfos) { - // 玩家信息 - TDSUser *user = info.user; - // 富信息数据 - NSDictionary *richPresence = info.richPresence; - // 好友是否在线 - BOOL online = info.online; - } - } else if (error) { - // 处理错误 - } -}]; -``` - - - -## 查询是否好友 - -可以通过指定某个 `TDSUser` 的 `objectId` 来查询他是否是当前玩家的好友。 -比如,假设 Tarara 的 `objectId` 是 `5b0b97cf06f4fd0abc0abe35`: - - - -```cs -bool isFriend = await TDSFriends.CheckFriendship("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFriends.checkFriendship("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(Boolean isFriend) { - if (isFriend) { - toast("Tarara is my friend."); - } else { - toast("Tarara is not my friend."); - } - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to query friendship: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFriends checkFriendshipWithUserId:@"5b0b97cf06f4fd0abc0abe35" - callback:^(NSNumber * _Nullable isFriend, NSError * _Nullable error) { - if (error) { - // 处理错误 - } - if (isFriend.boolValue) { - NSLog(@"Tarara is my friend."); - } else { - NSLog(@"Tarara is not my friend."); - } -}]; -``` - - - -## 分享链接 - -### 落地页 - -使用分享链接功能需要首先部署落地页网站。 -落地页网站可以部署在[云引擎](/sdk/engine/overview/)或其他支持部署纯静态网站的服务器上。 -如果计划部署在云引擎上,需注意云引擎的免费实例会自动休眠,请购买标准实例使用。 - -我们提供了[开源的落地页示例项目][repo],修改相应配置后可直接构建、部署、使用。 -注意,示例项目中的 `GAME_ANDROID_LINK` 环境变量格式为 `scheme://host/path`。 -`host` 和 `path` 的值需和 Android 的 `AndroidManifest.xml` 中的值保持一致。 - -[repo]: https://github.com/taptap/TapFriends-landing-page - -例如,假设 `AndroidManifest.xml` 中的相关配置如下: - -```xml - - - - - - - - - - - -``` - -那么落地页项目中 `GAME_ANDROID_LINK` 的值为 `tapsdk://游戏应用ID/friends`。 - -落地页网站的地址需要在客户端配置: - - - -```cs -TDSFriends.SetShareLink("https://please-replace-with-your-domain.example.com"); -``` - -```java -TDSFriends.setShareLink("https://please-replace-with-your-domain.example.com"); -``` - -```objc -[TDSFriends setShareLink:@"https://please-replace-with-your-domain.example.com"]; -``` - - - -如果落地页部署在云引擎网站上,那么地址就是 `https://你的云引擎自定义域名`。 - -### 生成链接 - -部署完落地页网站并在客户端配置好相应地址后,调用以下接口即可生成好友邀请页网址: - - - -```cs -string inviteUrl = await TDSFriends.GenerateFriendInvitationLink(); -``` - -```java -TDSFriends.generateFriendInvitationLink(new Callback() { - @Override - public void onSuccess(String inviteUrl) { - System.out.println("share this link to invite your friends: " + inviteUrl); - } - - @Override - public void onFail(TDSFriendError error) { - System.out.println("Failed to generate invite link: " + error.detailMessage); - } -}); -``` - -```objc -NSError *error; -NSString *inviteUrl = [TDSFriends generateFriendInvitationLinkWithError:&error]; -``` - - - -分享链接中传递的用户名称默认为玩家昵称(`nickname`), -因此,默认情况下,使用分享链接的前提是内建账户系统中设置了 `nickname`(昵称)字段。 -参见[内建账户系统文档](/sdk/authentication/guide/#设置其他用户属性)。 -如果希望使用其他名称,可以在调用上述接口时另行指定。 -另外,还可以传入其他信息,这些信息会作为 URL 查询参数拼接在好友邀请页网址后面。 -例如,昵称为 Tarara 的玩家,邀请好友时希望使用「她姥姥」这个名称,邀请链接希望附加 `ref=taptap` 这个参数,可以这样调用: - - - -```cs -Dictionary parameters = new Dictionary { - { "ref", "taptap" } -}; -string inviteUrl = await TDSFriends.GenerateFriendInvitationLink("她姥姥", parameters); -``` - -```java -Map parameters = new HashMap(); -parameters.put("ref", "taptap"); -TDSFriends.generateFriendInvitationLink("她姥姥", parameters, new Callback() { - // 略 -}); -``` - -```objc -NSError *error; -TDSFriendLinkOption *option = [TDSFriendLinkOption new]; -option.roleName = @"她姥姥"; -option.queries = @{ - @"ref" : @"taptap", -}; -NSString *inviteUrl = [TDSFriends generateFriendInvitationLinkWithOption:option error:&error]; -``` - - - -### 处理链接 - -玩家通过邀请链接打开游戏后,开发者需要调用该接口。 -调用该接口后,SDK 会自动向对应的玩家发起好友申请。 - - - -```cs -public class DeepLinkManager : MonoBehaviour -{ - // 略 - private async void onDeepLinkActivated(string url) { - await TDSFriends.HandleFriendInvitationLink(url); - } -} -``` - -```java -public class FriendsActivity extends AppCompatActivity { -// 略 - public void onHandleLink(View view) { - TDSFriends.handFriendInvitationLink(url, new Callback() { - @Override - public void onSuccess(Void result) { - // 略 - } - - @Override - public void onFail(TDSFriendError error) { - // 略 - } - }); - } -} -``` - -```objc -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSFriends handleFriendInvitationLink:url - callback:^(BOOL succeeded, TDSFriendsLinkInfo * _Nullable linkInfo, NSError * _Nullable error) { - if (error) { - // handle error - } - }]; - /* 如果需要延迟发送好友申请请求,可调用该接口, delay 单位为 秒 - return [TDSFriends handleFriendInvitationLink:url delay: 1 - callback:^(BOOL succeeded, TDSFriendsLinkInfo * _Nullable linkInfo, NSError * _Nullable error) { - if (error) { - // handle error - } - }];*/ - -} -``` - - - -开发者也可以通过 SDK 提供的接口解析链接,获取玩家的 objectId、名称、传入的其他参数,定制相应的逻辑。 - - - -```cs -public class DeepLinkManager : MonoBehaviour -{ - // 略 - private async void onDeepLinkActivated(string url) { - TDSFriendLinkInfo invitation = TDSFriends.ParseFriendInvitationLink(url); - string userObjectId = invitation.Identity; - string name = invitation.RoleName; - Dictionary parameters = invitation.Queries; - await TDSFriends.Follow(userObjectId); - } -} -``` - -```java -TDSFriendLinkInfo linkInfo = TDSFriends.parseFriendInvitationLink("url"); -String userObjectId = linkInfo.getIdentity(); -String name = linkInfo.getRoleName(); -Map parameters = linkInfo.getQueries(); -``` - -```objc -TDSFriendLinkInfo *linkInfo = [TDSFriends parseFriendInvitationLink:(NSURL *)url]; -NSString *userObjectId = linkInfo.identity; -NSString *name = linkInfo.roleName; -NSDictionary *parameters = linkInfo.queries; -``` - - diff --git a/docs/shadow/friends/third-party.mdx b/docs/shadow/friends/third-party.mdx deleted file mode 100644 index 99f382e41..000000000 --- a/docs/shadow/friends/third-party.mdx +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: 第三方平台好友 -sidebar_label: 第三方好友 -sidebar_position: 5 -slug: /sdk/friends/third-party ---- - - - - - - -import { Conditional } from "/src/docComponents/conditional"; -import MultiLang from "/src/docComponents/MultiLang"; - -阅读本文前请先了解[好友模块的通用接口](/sdk/friends/guide/)。 - -## 查询第三方平台好友 - -调用以下接口可以查询当前玩家在第三方平台(比如 TapTap)上的同玩好友(互相关注的用户)。 -该接口除了会返回好友列表外,还会返回游标。 -指定游标和返回数量,可以实现翻页功能。 -同时,支持根据在线状态获取排序后的结果(当前游戏在线的玩家在前)。 - -返回的第三方平台好友列表中会包括玩家在第三方平台的 ID、昵称、头像。 -如前所述,第三方平台的好友列表只会返回同玩好友,也就是已经使用第三方平台账号登录过该游戏的好友。 -通常情况下,同玩好友也是基于内建账户系统使用第三方平台账号登录游戏,因此第三方平台的好友列表还会返回相应的 `TDSFriendInfo`。 - -特殊情况下,如果同玩好友通过其他方式,而非基于内建账户系统使用第三方平台账号登录游戏,那么第三方平台的好友列表仍会返回该玩家,但相应的 `TDSFriendInfo` 会是 `null`。 -例如,假设游戏有两个渠道包,其中一个渠道包 a 基于内建账户系统使用 F 平台账号登录游戏,同时支持了好友功能,渠道包 b 自行实现了 F 平台账号的登录,并未支持好友功能。玩家 d 和 e 在 F 平台上互为好友,玩家 d 安装了渠道包 a 并通过它使用 F 平台账号登录游戏,玩家 e 则安装了渠道包 b 并通过它使用 F 平台账号登录游戏,那么玩家 d 调用以下接口查询第三方平台 F 的好友,返回列表中会包含 e,但 e 的 `TDSFriendInfo` 会是 `null`。 - - - -```cs -// 首次查询 -string platform = "taptap"; -string cursor = null; -// 默认 50,最大 500 -int limit = 50; -// 根据在线状态排序 -SortCondition sortCondition = SortCondition.OnlineCondition -ThirdPartyFriendResult result = await TDSFriends.QueryThirdPartyFriendList(platform, cursor, limit, condition: sortCondition); - -ReadOnlyCollection friends = result.FriendList; -foreach (ThirdPartyFriend friend in friends) { - string thirdPartyId = friend.Id; - string thirdPartyNickName = friend.Name; - string thirdPartyAvatarUrl = friend.Avatar; - TDSFriendInfo info = friend.FriendInfo; -} - -// 翻页 -string cursor = result.Cursor; -ThirdPartyFriendResult more = await TDSFriends.QueryThirdPartyFriendList(platform, cursor, limit, condition: sortCondition); -``` - -```java -ThirdPartyFriendRequestConfig config = new ThirdPartyFriendRequestConfig.Builder() - .platform(ThirdPartyFriendRequestConfig.PLATFORM_TAPTAP) - .pageSize(50) /* 默认 50,最大 500 */ - .sortCondition(SortCondition.getOnlineCondition()) /* 根据在线状态排序 */ - .build(); -// 首次查询 -TDSFollows.queryThirdPartyMutualList(config, null, new Callback() { - @Override - public void onSuccess(ThirdPartyFriendResult result) { - List friends = result.getFriendList(); - for (ThirdPartyFriend friend : friends) { - String thirdPartyId = friend.getUserId(); - String thirdPartyNickName = friend.getUserName(); - String thirdPartyAvatarUrl = friend.getUserAvatar(); - TDSFriendInfo info = friend.getTdsFriendInfo(); - } - - // 翻页 - String cursor = result.getCursor(); - TDSFollows.queryThirdPartyMutualList(config, cursor, new Callback() { - /* 略 */ - } - } - @Override - public void onFail(TDSFriendError error) { - toast("query error = " + error.code + " msg = " + error.detailMessage); - } -}); -``` - -```objc -TDSThirdPartyFriendQueryOption *option = [TDSThirdPartyFriendQueryOption new]; -option.platform = TDSThirdPartyFriendPlatformTaptap; -option.limit = 50;// 默认 50,最大 500 -__block NSString *cursor; // 游标 - -[TDSFriends queryThirdPartyFriendListWithOption:option -callback:^(TDSThirdPartyFriendResult * _Nullable result, NSError * _Nullable error) { - for (TDSThirdPartyFriend* friend in result.friendList) { - NSString *thirdPartyId = friend.userId; - NSString *thirdPartyNickName = friend.userName; - NSString *thirdPartyAvatarUrl = friend.userAvatar; - TDSFriendInfo *info = friend.tdsFriendInfo; - } - cursor = result.cursor; -}]; - -// 翻页 -option.from = cursor; -[TDSThirdPartyFriend queryThirdPartyFriendListWithOption:option -callback:^(TDSThirdPartyFriendResult * _Nullable result, NSError * _Nullable error) { - // 略 -}]; -``` - - - -默认情况下,SDK 优先从本地缓存中查询,以避免不必要的网络开销。 -游戏如果希望总是从网络获取查询结果,可以在查询时指定缓存策略。 -无论查询时是否指定缓存策略,SDK 总是会缓存查询的结果。 -换句话说,缓存策略只决定是否读缓存,不决定是否写缓存。 - - - -```cs -ThirdPartyFriendResult result = await TDSFriends.QueryThirdPartyFriendList(platform, cursor, limit, - TDSFriends.ThirdPartyFriendRequestCachePolicy.OnlyNetwork, sortCondition); -``` - -```java -ThirdPartyFriendRequestConfig config = new ThirdPartyFriendRequestConfig.Builder() - .platform(ThirdPartyFriendRequestConfig.PLATFORM_TAPTAP) - .sortCondition(SortCondition.getOnlineCondition()) - .cachePolicy(ThirdPartyFriendRequestConfig.CachePolicy.ONLY_NETWORK) - .pageSize(50) - .build(); -``` - -```objc -option.cachePolicy = TDSThirdPartyFriendCachePolicyOnlyNetwork; -``` - - - -目前支持的第三方平台(platform)如下: - - - -- `taptap`(需要提交工单联系我们开通) - - - - - -- `taptap`(需要提交工单联系我们开通) -- `facebook`(游戏需要支持 Facebook 登录) -- `twitter`(游戏需要支持 Twitter 登录) - - diff --git a/docs/shadow/game_account/_category_.json b/docs/shadow/game_account/_category_.json deleted file mode 100644 index d196e1bc6..000000000 --- a/docs/shadow/game_account/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "游戏官方号", - "collapsed": true, - "position": 18 -} diff --git a/docs/shadow/game_account/features.mdx b/docs/shadow/game_account/features.mdx deleted file mode 100644 index 03df392ed..000000000 --- a/docs/shadow/game_account/features.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: 什么是游戏官方号 -sidebar_position: 1 -slug: /store/game_account/features/ ---- - - - - - -游戏官方号是TapTap为开发者提供的官方认证账号,用于代表开发者官方身份来进行内容发布、粉丝互动和品牌运营。 - -该功能目前处于邀请测试阶段,将逐步开放,敬请期待。 - -## 游戏官方号为厂商及游戏带来的核心优势 -- **账号轻松涨粉**:游戏官方号绑定游戏,实现新增粉丝共享,账号、游戏双双涨粉。 -- **企业统一管理**:实现公私账号隔离,内容和粉丝不再分散在各小编个人账号。 -- **多人便捷运营**:多运营子账号协同为游戏官方号代发内容,持续产出官方内容。 -- **带动内容传播**:依托官方号关注关系,内容在动态流、主页更大范围触达目标用户。 - - -## 游戏官方号的专享权益 -- **粉丝共享**:与绑定游戏同步新增粉丝,实现游戏、账号粉丝双增长。 -- **关联曝光**:主页拥有专属游戏专栏,旗下游戏获更多曝光途径。 -- **主页置顶**:主页额外游戏置顶展示位,置顶主推游戏享受主页流量导入。 -- **蓝 V 标识**:专属认证内容建立玩家信任,强化官方权威。 -- **唯一昵称**:游戏官方号之间不可重名,确保官方唯一性。 -- **账号矩阵**:围绕 IP 建立不同定位官方号,多个运营子账号协同经营,做强主账号。 -- **官方动态**:无需额外权限即可发布官方帖,运营子账号便捷为官方号代发官方动态。 -- **官方回复**:评价回复展示官方标识,拉进与玩家距离。 -- **互动消息**:消息中心接收玩家互动消息,即时互动,维持粉丝活跃。 - -![](https://img.tapimg.com/market/images/257f35e47b12f8dd6c54081a0f3362ea.png) - - -## 使用游戏官方号的要求 -需完善厂商资料,通过平台审核,成为认证开发者。[认证开发者说明](/store/#1-厂商创建通道及流程) - - -## 游戏官方号与游戏的关系 -### 绑定游戏 -游戏官方号绑定一个游戏,代表它即是游戏的内容、账号运营主体。 - -一旦绑定游戏: -- 它拥有上述介绍的所有权益。可与绑定游戏**同步新增粉丝**。 -- 系统将把该游戏的存量粉丝,在接下来的2小时内,逐步同步给该官方号。 -- 该游戏的官方帖消息推送,将以该游戏官方号为主体。 -- 绑定关系是**一对一**的,一个游戏官方号只能绑定一个游戏,反之亦然。并且绑定关系不可解除。 - -绑定游戏的官方号一般与游戏同名,以确保官方性和品牌一致性。 - -例如:绑定【火炬之光:无限】的游戏官方号昵称即为【火炬之光:无限】,负责官方新闻的发布。具体适用场景可参考[游戏官方号运营手册](/store/game_account/operational_guidance/) - -### 关联游戏 -游戏官方号关联游戏,代表它参与该游戏的运营,拥有相关的账号定位。 - -一旦关联游戏: -- 它拥有以上除「粉丝共享」「子账号代发」以外的所有权益。 -- 关联关系是多对多的,一个游戏官方号可以关联多个游戏,反之亦然。 - -关联游戏的官方号一般使用与游戏有关的名称,以确保关联性。 - -例如:关联【火炬之光:无限】的游戏官方号之一【火炬-暗鸦】,负责官方活动、攻略的发布和论坛互动。具体适用场景可参考[游戏官方号运营手册](/store/game_account/operational_guidance/) \ No newline at end of file diff --git a/docs/shadow/game_account/guide.mdx b/docs/shadow/game_account/guide.mdx deleted file mode 100644 index 1d5131fbd..000000000 --- a/docs/shadow/game_account/guide.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: 如何创建、管理游戏官方号 -sidebar_position: 2 -slug: /store/game_account/guide/ ---- - - - - - -## 创建游戏官方号 -1. 在「游戏官方号」模块选择「新增官方号」。 - -![](https://img.tapimg.com/market/images/e3194a5ed463efaf165ebc2777124d52.png) - -2. 根据表单中各项信息的提示,填写信息,完成创建。不确定绑定、关联、游戏如何填写,可根据运营目标参考[游戏官方号运营手册](/store/game_account/operational_guidance/) - -注意事项: -- 绑定游戏一旦绑定,不可变更,请谨慎选择。 -- 若遇到游戏同名的官方号昵称已被使用,请进入厂商首页提交工单申诉。 - -## 将已有账号转换为游戏官方号 -1. 开发者与账号原拥有者协商一致,同意进行账号转换,将账号所有权转移给厂商。 -2. 厂商管理员将该账号添加为开发者中心「成员」。 - -![](https://img.tapimg.com/market/images/71fcbae7e824fc72518f8feacc8c394f.png) - -3. 开发者中心登录该账号,在「游戏官方号」模块选择「当前账号转换为官方号」,确认信息,提交申请。 - -![](https://img.tapimg.com/market/images/a848de62d0addd44e6b105b504213e35.png) - -4. 厂商管理员在「游戏官方号」模块查看「官方号转换申请」,同意并确认、填写信息,完成转换。转换后的游戏官方号可在「管理官方号」中查看。不确定绑定、关联、游戏如何填写,可暂时不填,后续根据运营目标参考[游戏官方号运营手册](/store/game_account/operational_guidance/) - -![](https://img.tapimg.com/market/images/6c0864c74398e0e5308d180d5cb5b3ef.png) - - -请注意,转换为游戏官方号的操作不可逆,转换后,会清除的资产、数据包括: -- 游戏与网站授权(将无法使用该账号登录游戏,也无法通过该 TapTap 账号找回游戏账号) -- 已购游戏、服务 -- 原登录方式、实名信息 -- 成就、论坛等级、徽章、头像挂件 -- 私信记录 - -## 修改游戏官方号资料 - -![](https://img.tapimg.com/market/images/fcd56b5bbff714ba2557df4bab9117d8.png) - -## 更换登录方式 -鼠标移至「登录方式」上,点击出现的编辑按钮 - -![](https://img.tapimg.com/market/images/1dfe6383a9c2088fa1cee39b994b6d51.png) - -## 像普通账号一样登录使用 TapTap 客户端或网页 -使用所配置的登录方式,即可使用 TapTap 客户端或网页登录游戏官方号。登录后即可便捷的发布动态、互动回复。 - -若配置的是邮箱登录,可在下图所示位置找到邮箱登录入口 - -![](https://img.tapimg.com/market/images/f18d02c73a1a1addd47c0c8e8ad7a08e.png) - - -## 游戏官方号的使用限制 -为保证游戏官方号的官方性,及避免运营中可能出现的账号所有物归属权的争议,游戏官方号在使用中会受到一些限制: -- **账号**:不可修改登录方式(厂商管理员在开发者中心修改除外)、注销账号、授权登录游戏/网站。 -- **资产**:不可购买游戏/服务、兑换游戏、获得活动奖励、获取创作者收益。 -- **数据**:不可对外展示成就、徽章、头像挂件、游戏时长、战绩、论坛等级、收藏、玩过游戏。 -- **功能**:不可评价游戏、云玩、获取测试资格。 -- **权限**:不可拥有开发者中心厂商权限。 - - -## 游戏官方号数量控制 -创建过多无用的游戏官方号可能会被限制曝光和限制厂商功能。由于游戏官方号不可注销,请确保按需创建。 \ No newline at end of file diff --git a/docs/shadow/game_account/operational_guidance.mdx b/docs/shadow/game_account/operational_guidance.mdx deleted file mode 100644 index a39588cc8..000000000 --- a/docs/shadow/game_account/operational_guidance.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: 游戏官方号运营手册 -sidebar_position: 3 -slug: /store/game_account/operational_guidance/ ---- - - - - - - -游戏官方号在运营时具备较强灵活性,以下将针对不同运营场景的配置、功能使用给出一些例子,开发者可以结合实际情况参考。 - -| 厂商 | 有重点运营游戏的厂商 | 有系列游戏的厂商 | 小工作室 | -|----------|-----------------------|-------------------|----------| -| **情况** | - 有4个运营人员
    - 期望多个官方号从不同方向积累粉丝
    - 以旗下游戏「大作A」为例| - 有1个运营人员
    - 期望能以系列IP名义做积累
    - 以旗下系列游戏「解谜1」「解谜2」为例| - 无专职运营
    - 能发布重要的官方消息、和玩家互动即可
    - 以旗下游戏「独立作品A」为例| -| **官方号设置** | - 绑定「大作A」的同名官方号:「大作A」
    - 负责「大作A」活动发布的:「大作A福利官」
    - 负责「大作A」相关话题内容下互动的:「大作A互动使」 | - 绑定「解谜1」的同名官方号:「解谜1」
    - 绑定「解谜2」的同名官方号:「解谜2」
    - 解谜系列IP的官方号:「解谜」 | 绑定「独立作品A」的同名官方号:「独立作品A」 | -|**绑定游戏设置** | - 「大作A」绑定同名游戏
    - 「大作A福利官」「大作A互动使」不绑定游戏 | - 「解谜1」「解谜2」分别绑定自身同名游戏
    - 「解谜」不绑定游戏 | - 唯一官方号绑定同名游戏 | -|**关联游戏设置** | - 以上3个官方号均关联「大作A」 | - 「解谜1」「解谜2」分别关联自身同名游戏
    - 「解谜」同时关联两个游戏 | - 唯一官方号关联同名游戏 | -|**主页置顶设置** | - 以上3个官方号均置顶「大作A」 | - 「解谜1」「解谜2」分别置顶自身同名游戏
    - 「解谜」置顶系列游戏中最新作「解谜2」 | - 唯一官方号置顶同名游戏 | -|**运营人员分配** | - 「大作A」2人运营
    - 其他两个官方号各1人运营 | - 3个官方号由1人运营 | - 开发者1人运营 | -|**登录方式设置** | - 「大作A」绑定邮箱,2名运营均可登录邮箱查收验证码
    - 其他2个官方号分别绑定2个邮箱,2名运营分别持有 | - 3个官方号分别绑定邮箱,唯一运营持有3个邮箱 | - 绑定开发者自己方便的邮箱或手机号 | -|**发布官方帖** | - 3个官方号各自登录,选择「大作A」论坛,以登录官方号名义直发
    - 或为「大作A」绑定号代发 | - 登录「解谜」,选择「解谜1」或「解谜2」论坛,以 「解谜」名义发布官方帖 | - 登录「独立作品A」,选择同名论坛发布官方帖 | -|**内容倾向** | - 「大作A」发布官方新闻、版本情报、公告
    - 「大作A福利官」发布各类运营活动、开奖帖,号召福利党关注
    - 「大作A互动使」在论坛解答玩家疑惑,鼓励玩家二创,在论坛外各相关话题下与潜在玩家互动玩梗 | - 「解谜」在两个游戏论坛承担主要的官方内容发布 | - 「独立作品A」发布官方内容、开发者故事 | -|**评价官方回复** | 可登录官方号回复,也可继续使用「社区运营」权限的客服账号回复,均为官方回复 | 同左 | 同左 | -|**互动消息** | 登录官方号后,消息中心查看、回复 | 同左 | 同左 | diff --git a/docs/shadow/im/guide/_category_.json b/docs/shadow/im/guide/_category_.json deleted file mode 100644 index 3446da847..000000000 --- a/docs/shadow/im/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 2 -} diff --git a/docs/shadow/push/guide/_category_.json b/docs/shadow/push/guide/_category_.json deleted file mode 100644 index 3446da847..000000000 --- a/docs/shadow/push/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 2 -} diff --git a/docs/shadow/rtc/_category_.json b/docs/shadow/rtc/_category_.json deleted file mode 100644 index 576865718..000000000 --- a/docs/shadow/rtc/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "实时语音", - "collapsed": true, - "position": 17 -} diff --git a/docs/shadow/rtc/features.mdx b/docs/shadow/rtc/features.mdx deleted file mode 100644 index 067dc277c..000000000 --- a/docs/shadow/rtc/features.mdx +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: 实时语音功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 -slug: /sdk/rtc/features/ ---- - - - - - -一站式语音解决方案,提供实时语音、语音合规服务,覆盖 FPS、MOBA、MMORPG、休闲对战、线上桌游等多种游戏玩法类型,一次接入即可满足所有语音需求。 - -## 核心优势 - -- 开箱即用:通用框架全覆盖,一次接入即可满足多样化的语音需求。 -- 低时延、高质量的语音质量:实现两人或多人语音实时音频通话,低时延、高音质,在弱网环境下也能表现亮眼。 -- 支持中英文的语音合规:覆盖各类违规内容,智能识别实时语音及音频文件中的涉黄、暴力、谩骂、广告及其他各类敏感或不良信息。 -- 服务全球可用:全球部署海量加速节点,覆盖全球主流国家和地区,实现玩家就近接入,提供低延时不卡顿的实时语音服务。 - -## 功能说明 - -### 实时语音通话 - -支持最多 6 个人同时说话,超低延迟,适用于多人组队开黑等竞技游戏场景。并支持: - -- 房间内屏蔽他人语音 -- 成员进入房间和发言的事件回调,游戏可用于绘制玩家状态 -- 启用/关闭耳返 - -### 语音合规 - -支持中英文的违规内容过滤,游戏可以按照不同的切片时长将语音上报给合规模块。当检测到违规时,将回调给游戏方,游戏可自行决定将玩家踢出房间、禁言或不做任何处理。 -注:目前语音合规仅支持普通话与部分方言,其他语言将在后续版本支持。 - -### 3D 语音 - -3D 语音可以将无方位感的声音处理成带有声源方位感的声音,从而虚拟出空间中任意位置的声源对人耳造成的感觉,适合为大逃杀、FPS 等游戏打造沉浸式听觉体验。 - -:::info - -3D 语音功能处于 Beta 阶段,如在试用过程中遇到问题,或有任何建议,欢迎通过工单联系我们。 - -::: - -### 开发者中心 - -我们在开发者中心为你提供了以下基本功能: - -- 开通和配置实时语音服务 -- 远程开启/关闭实时语音服务 -- 查询实时语音的活跃用户量、最高同时在线人数、玩家语音时长数等数据 -- 实时语音的用量和计费详情 - -## 接入说明 - -### 接入准备 - -1. 入驻成为 TapTap 的开发者; - -2. 在 TapTap 开发者中心创建游戏应用; - -3. 点击「游戏服务」-「实时语音」,开启服务; - -4. 配置合规服务 - - ![](https://capacity-files.lcfile.com/f7lyeyQrmn9YIwIBBlWq7HIVTqLdndQA/rtc-console.png) - - 1. 设置语音切片时长:较短的时长切片,会牺牲一部分上下文语义换取相对较快的回调速度,你可以根据自己的实际场景来选择切片长度; - 2. 设置回调地址:用于接受语音合规识别结果的回调地址; - -5. 下载 TapSDK(最低支持版本 v3.5.0)集成到游戏包内; - -6. 在游戏的测试包内进行测试验收。 - diff --git a/docs/shadow/rtc/guide.mdx b/docs/shadow/rtc/guide.mdx deleted file mode 100644 index 25f6e9fe1..000000000 --- a/docs/shadow/rtc/guide.mdx +++ /dev/null @@ -1,1490 +0,0 @@ ---- -title: 实时语音开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 -slug: /sdk/rtc/guide/ ---- - - - - - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import Mermaid from '/src/docComponents/Mermaid'; -import {Conditional} from '/src/docComponents/conditional'; - -## SDK 获取 - -可以在 [下载页](/tap-download) 获得 TapSDK,引入 `TapRTC` 模块: - - - -<> - -请先确保系统已安装 [git-lfs],然后通过 UPM 方式添加依赖: - -[git-lfs]: https://git-lfs.github.com - - -{`"dependencies":{ - ... - "com.taptap.tds.rtc":"https://github.com/TapTap/TapRTC-Unity.git#${sdkVersions.taptap.unity}", -}`} - - - -<> - -{`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapRTC_${sdkVersions.taptap.rtc}', ext:'aar') -}`} - - - -<> - -1. 在 Xcode 选择工程,到 Build Setting > Other Linker Flags 添加 `-ObjC` 和 `-Wl -ld_classic`。 -2. 拖拽 `TapRTC_SDK` 目录到项目目录。 -3. `TapRTC.framework` 和 `GMESDK.framework` 文件拖进项目,选择 `Do Not Embed`。 -4. 将资源文件 TapRTC.bundle 拖进项目。 -5. 添加 SDK 依赖的系统库:libz、libresolv、libiconv、libc++、CoreMedia.framework、CoreAudio.framework、AVFoundation.framework、SystemConfiguration.framework、UIKit.framework、AudioToolbox.framework、OpenAL.framework、Security.framework。 - - -TapRTC.framework - - - - - - -## 注意事项 - -* RTC 使用前需要在开发者中心进行相关配置。 -* 开发者需要在自己的服务器上实现[相应的签名鉴权服务](#服务端鉴权)。 -* C# SDK 需要周期性的调用 `TapRTC.Poll` 接口,才能触发相关的事件回调。 - -Android 平台需要申请网络、音频相关权限: - -```xml - - - - - - - -``` - -iOS 平台需要申请麦克风权限(`Privacy - Microphone Usage Description`)。 - -游戏通常没有后台通话的需求,玩家一般边玩游戏边语音,游戏保持在前台。 -如果游戏类型比较特殊,需要支持后台通话,那么还需申请后台播放权限: -在 target 中配置 **Capability > Background Modes > Audio, AirPlay, and Picture in Picture**。 - -## 核心接口 - -RTC 通过 `TapRTCConfig` 来进行初始化配置。初始化的过程是异步的,需要等待初始化结果返回之后,才能进行下一步的操作。 - -* `ClientId`、`ClientToken`、`ServerUrl`,参见关于[应用凭证](/sdk/storage/guide/setup-dotnet#应用凭证)的说明。 -* `UserId`: 开发者自定义的用户 Id,用于标记玩家 -* `DeviceId`: 开发者自定义的设备 Id,用于标记设备 -* `AudioPerfProfile`: 音频质量配置(`LOW`、`MID`、`HIGH`,默认为 `MID`) - - - -```cs -using TapTap.RTC; - -var config = new TapRTCConfig.Builder() - .ClientID("ClientId") - .ClientToken("ClientToken") - .ServerUrl("ServerUrl") - .UserId("UserId") - .DeviceId("DeviceId") - .AudioProfile(AudioPerfProfile.MID) - .ConfigBuilder(); - -ResultCode code = await TapRTC.Init(config); - -if (code == ResultCode.OK) { - // 初始化成功 -} else { - // 失败 -} -// SDK 的 RTC 模块中,返回 ResultCode 的接口均以 ResultCode.OK 表示操作成功。 -``` - -```java -import android.app.Application; -import com.taptap.taprtc.Config; -import com.taptap.taprtc.Config.AudioPerfProfile; -import com.taptap.taprtc.DeviceID; -import com.taptap.taprtc.UserID; -import com.taptap.taprtc.TapRTCEngine; - -public class MyApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - Config config = new Config(); - config.appId = "AppId"; - config.appKey = "AppKey"; - config.serverUrl = "ServerUrl"; // 请去掉 http/https 前缀,直接传入自定义域名或者形如「xxx.cloud.tds1.tapapis.cn」之类的域名即可。 - config.userId = new UserID("UserId"); - config.deviceId = new DeviceID("DeviceId"); - // 如需使用范围语音功能,此项必须设为 LOW - config.profile = AudioPerfProfile.MID; - try { - TapRTCEngine.get().init(this, config, resultCode -> { - if (resultCode == ResultCode.OK) { - // 初始化成功 - } else { - // 初始化失败 - } - }); - } catch (TapRTCException e) { - throw new RuntimeException(e); - } - } -} -// SDK 的 RTC 模块中,返回 ResultCode 的接口均以 ResultCode.OK 表示操作成功。 -``` - -```objc -TapRTCConfig *config = [[TapRTCConfig alloc] initWithAppId:@"AppId" -appKey:@"AppKey" serverUrl:@"ServerUrl" -userId:@"UserId" deviceId:@"DeviceId" -profile:AudioPerfProfileMID]; -TapRTCEngine *engine = [TapRTCEngine defaultEngine]; -[engine initializeWithConfig:config resultBlock:^(NSError * _Nullable error) { - if (error) { - // handle error - } -})]; -``` - - - -### 触发回调事件 - -C# SDK 需要在 `Update` 方法中调用 `Poll` 方法触发事件回调。如果不调用该方法,会导致 SDK 运行异常。 - -```cs -public void Update() -{ - ResultCode code = TapRTC.Poll(); - if (code == ResultCode.OK) { - // 成功触发回调 - } else { - // 失败 - } -} -``` - -Java SDK、Objective-C SDK 无需定期调用 `Poll` 方法。 - -### 恢复系统 - - - -```cs -ResultCode code = TapRTC.Resume(); -if (code == ResultCode.OK) { - // 成功恢复 -} else { - // 失败 -} -``` - -```java -import com.taptap.taprtc.TapRTCEngine; - -ResultCode code = TapRTCEngine.get().resume(); -``` - -```objc -TapRTCResultCode resultCode = [engine resume]; -if (resultCode == TapRTCResultCode_Success) { - // resumed -} else { - // failed to resume -} -``` - - - -### 暂停系统 - - - -```cs -ResultCode code = TapRTC.Pause(); -if (code == ResultCode.OK) { - // 成功暂停 -} else { - // 失败 -} -``` - -```java -import com.taptap.taprtc.TapRTCEngine; - -ResultCode code = TapRTCEngine.get().pause(); -``` - -```objc -TapRTCResultCode resultCode = [engine pause]; -if (resultCode == TapRTCResultCode_Success) { - // paused -} else { - // failed to pause -} -``` - - - -## 房间相关接口 - -### 创建房间 - -初始化成功之后,SDK 在创建房间之后,才可以进行实时语音通话。 -创建房间时需指定房间号(`roomId`)。 -[是否启用范围语音](#范围语音)也需在创建房间时设置,C# SDK 默认不启用,Java SDK 创建时必须指定是否启用范围语音,Objective-C SDK 使用单独的接口创建范围语音房间。 - - - -```cs -bool enableRangeAudio = false; -var room = await TapRTC.AcquireRoom("roomId", enableRangeAudio); -``` - -```java -import com.taptap.taprtc.TapRTCEngine; -import com.taptap.taprtc.RoomID; - -RoomId roomId = new RoomID("roomId"); -boolean enableRangeAudio = false; -TapRTCRoom room = TapRTCEngine.get().acquireRoom(roomId, enableRangeAudio); -``` - -```objc -TapRTCRoom *room = [engine acquireRoomWithRoomId:@"roomID"]; -``` - - - -### 注册房间相关的回调事件 - - - -```cs -room.RegisterEventAction(new TapRTCEvent() -{ - OnDisconnect = (code, message) => { label.text += "\n" + $"断开连接 code:{code} msg:{e}"; }, - OnEnterFailure = s => { label.text += "\n" + $"进入房间失败:{s}"; }, - OnEnterSuccess = () => { label.text += "\n" + $"进入房间成功"; }, - OnExit = () => { label.text += "\n" + $"退出房间成功"; }, - OnUserEnter = userId => { label.text += "\n" + $"玩家{userId}进入房间"; }, - OnUserExit = userId => { label.text += "\n" + $"玩家{userId}退出房间"; }, - OnUserSpeaker = (userId, volume) => { label.text += "\n" + $"玩家{userId}在房间说话,音量{volume}"; }, - OnUserSpeakEnd = userId => { label.text += "\n" + $"玩家{userId}在房间说话结束"; }, - // 返回切换后的音质,参见下文「切换音频质量」一节 - OnRoomTypeChanged = (i) => { label.text += "\n" + $"房间音质改为{i}"; }, - OnRoomQualityChanged = (weight, loss, delay) => - { - Debug.Log($"音频质量:{weight} 丢包率:{loss}% 延迟:{delay}ms"); - }, - -}); -``` - -```java -import com.taptap.taprtc.UserID; -import com.taptap.taprtc.TapRTCRoom; - -room.registerCallback(new TapRTCRoom.Callback() { - // 进入房间成功 - @Override public void onEnterSuccess() {} - - // 进入房间失败 - @Override public void onEnterFailure(String msg) {} - - // 连接断开 - @Override public void onDisconnect() {} - - // 重连成功 - @Override public void onReConnected() {} - - // 当前玩家退出房间 - @Override public void onExit() {} - - // 其他玩家进入房间 - @Override public void onUserEnter(UserID userId) {} - - // 其他玩家退出房间 - @Override public void onUserExit(UserID userId) {} - - // 玩家说话开始 - @Override public void onUserSpeakStart(UserID userId, int volume) {} - - // 玩家说话结束 - @Override public void onUserSpeakEnd(UserID userId) {} - - // 音频质量变化 - @Override public void onRoomQualityChanged(int weight, double loss, int delay) {} -}); -``` - -```objc -// 需实现 TapRTCRoomDelegate - -// 进入房间成功 -- (void)onEnterSuccess; - -// 进入房间失败 -- (void)onEnterFailure:(NSError *)error; - -// 其他玩家进入房间 -- (void)onUsersEnter:(NSString *)userId; - -// 其他玩家退出房间 -- (void)onUsersExit:(NSString *)userId; - -// 当前玩家退出房间 -- (void)onExit; - -// 连接断开 -- (void)onDisconnect; - -// 重连成功 -- (void)onReconnected; - -// 玩家说话开始 -- (void)onUsersSpeakStart:(NSString *)userId volume:(NSInteger)volume; - -// 玩家说话结束 -- (void)onUsersSpeakEnd:(NSString *)userId; - -// 音频质量变化(返回音频质量、丢包率、延迟) -- (void)onQualityCallBackWithWeight:(int)weight loss:(float)loss delay:(int)delay; -``` - - - -### 加入房间 - -使用[服务端生成的鉴权信息](#服务端鉴权)进入房间。 - -进入房间成功之后,会通过 `TapRTCEvent` 中的 `OnEnterSuccess` 进行回调。 - - - -```cs -ResultCode code = await room.Join("authBuffer"); -if (code == ResultCode.OK) { - // 成功加入 -} -if (code == ResultCode.ERROR_ALREADY_IN_ROOM) { - // 玩家已经在此房间内 -} -``` - -```java -import com.taptap.taprtc.Authority; - -Authority authBuffer = new Authority("authBuffer"); -ResultCode code = room.join(authBuffer); -if (code == ResultCode.OK) { - // 成功加入 -} -if (code == ResultCode.ERROR_ALREADY_IN_ROOM) { - // 玩家已经在此房间内 -} -``` - -```objc -[room joinWithAuth:@"authBuffer"]; -``` - - - -`authBuffer` 是在服务端生成的鉴权信息,详见下文[服务端鉴权](#服务端鉴权)一节的说明。 - -### 退出房间 - -退出房间之后,会通过 `TapRTCEvent` 中的 `OnExit` 进行回调。 - - - -```cs -ResultCode code = room.Exit(); -``` - -```java -ResultCode code = room.exit(); -``` - -```objc -TapRTCResultCode resultCode = [room exit]; -if (resultCode == TapRTCResultCode_Success) { - // exited -} else { - // failed exit -} -``` - - - -### 收听某人语音(默认收听) - - - -```cs -ResultCode code = room.EnableUserAudio("userId"); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_USER_NOT_EXIST) { - // 玩家不存在 -} -``` - -```java -import com.taptap.taprtc.UserID; - -UserId userId = new UserID("userId"); -ResultCode code = room.enableUserAudio(userId); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_USER_NOT_EXIST) { - // 玩家不存在 -} -``` - -```objc -TapRTCResultCode resultCode = [room enableUserAudioWithUserId:@"userId"]; -``` - - - -### 拒收某人语音 - - - -```cs -ResultCode code = room.DisableUserAudio("userId"); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_USER_NOT_EXIST) { - // 玩家不存在 -} -``` - -```java -import com.taptap.taprtc.UserID; - -UserId userId = new UserID("userId"); -ResultCode code = room.disableUserAudio(userId); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_USER_NOT_EXIST) { - // 玩家不存在 -} -``` - -```objc -TapRTCResultCode resultCode = [room disableUserAudioWithUserId:@"userId"]; -``` - - - -### 打开/关闭语音 - -这个接口设置音频下行的开关。 -一般来说,推荐游戏使用[打开/关闭扬声器的接口](#打开关闭扬声器)。 - - - -```cs -// 开启 -ResultCode code = room.EnableAudioReceiver(true); - -// 关闭 -ResultCode code = room.EnableAudioReceiver(false); -``` - -```java -// 开启 -ResultCode code = room.enableAudioReceiver(true); - -// 关闭 -ResultCode code = room.enableAudioReceiver(false); -``` - -```objc -TapRTCResultCode resultCode = [room enableAudioReceiver:YES]; -``` - - - -### 切换音频质量 - -音频质量分为 LOW、MID、HIGH 三档。 - -进入房间后,可以切换音频质量。 - - - -```cs -room.ChangeRoomType(AudioPerfProfile.LOW); -room.ChangeRoomType(AudioPerfProfile.MID); -room.ChangeRoomType(AudioPerfProfile.HIGH); -``` - -```java -// 暂未支持 -``` - -```objc -// 暂未支持 -``` - - - -切换音频质量会触发 `OnRoomTypeChanged` 回调。 - -### 获取当前房间的用户 - - - -```cs -HashSet userIdList = room.Users; -``` - -```java -List userIdList = room.getUsers(); -``` - -```objc -[room getUsers:^(NSArray*userIDs, NSError * _Nullable error) { - if (error) { - // 处理错误 - } else { - // userIDs 是房间内的用户 ID 列表 - } -})]; -``` - - - -## 音频控制相关接口 - -### 打开/关闭麦克风 - - - -```cs -// 打开 -ResultCode code = TapRTC.GetAudioDevice().EnableMic(true); - -// 关闭 -ResultCode code = TapRTC.GetAudioDevice().EnableMic(false); -``` - -```java -// 打开 -boolean ok = TapRTCEngine.get().getAudioDevice().enableMic(true); - -// 关闭 -boolean ok = TapRTCEngine.get().getAudioDevice().enableMic(false); -``` - -```objc -// 打开 -TapRTCResultCode code = [engine.audioDevice enableMic:YES]; - -// 关闭 -TapRTCResultCode code = [engine.audioDevice enableMic:NO]; -``` - - - -### 打开/关闭扬声器 - - - -```cs -// 打开 -ResultCode code = TapRTC.GetAudioDevice().EnableSpeaker(true); - -// 关闭 -ResultCode code = TapRTC.GetAudioDevice().EnableSpeaker(false); -``` - -```java -// 打开 -boolean ok = TapRTCEngine.get().getAudioDevice().enableSpeaker(true); - -// 关闭 -boolean ok = TapRTCEngine.get().getAudioDevice().enableSpeaker(false); -``` - -```objc -// 打开 -TapRTCResultCode code = [engine.audioDevice enableSpeaker:YES]; - -// 关闭 -TapRTCResultCode code = [engine.audioDevice enableSpeaker:NO]; -``` - - - -### 设置/获取音量 - -音量是 0 到 100 之间的整数。 - - - -```cs -int vol = 60; - -// 设置麦克风音量 -ResultCode code = TapRTC.GetAudioDevice().SetMicVolume(vol); -// 设置扬声器音量 -ResultCode code = TapRTC.GetAudioDevice().SetSpeakerVolume(vol); - -// 获取麦克风音量 -int micVolume = TapRTC.GetAudioDevice().GetMicVolume(); -// 获取扬声器音量 -int speakerVolume = TapRTC.GetAudioDevice().GetSpeakerVolume(); -``` - -```java -int vol = 60; - -// 设置麦克风音量 -TapRTCEngine.get().getAudioDevice().setMicVolume(vol); -// 设置扬声器音量 -boolean ok = TapRTCEngine.get().getAudioDevice().setSpeakerVolume(vol); - -// 获取麦克风音量 -int micVolume = TapRTCEngine.get().getAudioDevice().getMicVolume(); -// 获取扬声器音量 -int speakerVolume = TapRTCEngine.get().getAudioDevice().getSpeakerVolume(); -``` - -```objc -int vol = 60; - -// 设置麦克风音量 -TapRTCResultCode code = [engine.audioDevice setMicVolume:vol]; -// 设置扬声器音量 -TapRTCResultCode code = [engine.audioDevice setSpeakerVolume:vol]; - -// 获取麦克风音量 -int micVolume = [engine.audioDevice getMicVolume]; -// 获取扬声器音量 -int speakerVolume = [engine.audioDevice getSpeakerVolume]; -``` - - - -### 打开/关闭播放设备 - - - -```cs -// 打开 -ResultCode code = TapRTC.GetAudioDevice().EnableAudioPlay(true); -// 关闭 -ResultCode code = TapRTC.GetAudioDevice().EnableAudioPlay(false); -``` - -```java -// 打开 -boolean ok = TapRTCEngine.get().getAudioDevice().enableAudioPlay(true); -// 关闭 -boolean ok = TapRTCEngine.get().getAudioDevice().enableAudioPlay(false); -``` - -```objc -// 打开 -TapRTCResultCode code = [engine.audioDevice enableAudioPlay:YES]; - -// 关闭 -TapRTCResultCode code = [engine.audioDevice enableAudioPlay:NO]; -``` - - - -### 打开/关闭耳返 - - - -```cs -// 打开 -ResultCode code = TapRTC.GetAudioDevice().EnableLoopback(true); -// 关闭 -ResultCode code = TapRTC.GetAudioDevice().EnableLoopback(false); -``` - -```java -// 打开 -boolean ok = TapRTCEngine.get().getAudioDevice().enableLoopback(true); -// 关闭 -boolean ok = TapRTCEngine.get().getAudioDevice().enableLoopback(false); -``` - -```objc -// 打开 -TapRTCResultCode code = [engine.audioDevice enableLoopback:YES]; - -// 关闭 -TapRTCResultCode code = [engine.audioDevice enableLoopback:NO]; -``` - - - -## 范围语音 - -范围语音功能可以支持以下特性: - -- 玩家附近一定范围内的其他小队玩家也能听到该玩家的语音; -- 在同一房间内,支持大量用户同时打开麦克风进行语音通话。 - -要使用范围语音功能,首先创建房间时需要指定启用范围语音功能: - - - -```cs -bool enableRangeAudio = true; -var room = await TapRTC.AcquireRoom("roomId", enableRangeAudio); -``` - -```java -import com.taptap.taprtc.TapRTCEngine; -import com.taptap.taprtc.RoomID; - -RoomId roomId = new RoomID("roomId"); -boolean enableRangeAudio = true; -TapRTCRoom room = TapRTCEngine.get().acquireRoom(roomId, enableRangeAudio); -``` - -```objc -TapRTCRoom *room = [engine acquireRangeAudioRoomWithRoomId:@"roomID"]; -``` - - - -其次,玩家进入房间前,需要**切换音频质量为 LOW**: - - - -```cs -room.ChangeRoomType(AudioPerfProfile.LOW); -``` - -```java -// Java SDK 未提供切换音频质量的接口。 -// Java SDK 下,如需使用范围语音功能,**SDK 初始化时音频质量需设置为 LOW**。 -``` - -```objc -// Objective-C SDK 在进入范围语音房间时会自动把音质设为 LOW -``` - - - -接着设定小队号,并指定语音模式: - -- 世界模式:距离当前玩家[一定范围](#设置语音接收范围)内的其他小队成员也可以听到该玩家的语音; -- 小队模式:仅小队成员之间可以互相通话。 - -无论哪种模式下,小队成员之间都能互相通话,不论距离远近。 - - - -```cs -int teamId = 12345678; -ResultCode code = room.GetRtcRangeAudioCtrl().SetRangeAudioTeam(teamId); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} - -// 世界模式 -ResultCode resultCode = room.GetRtcRangeAudioCtrl().SetRangeAudioMode(RangeAudioMode.WORLD); -if (resultCode == ResultCode.OK) { - // 成功 -} -if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} -// 小队模式 -ResultCode resultCode = room.GetRtcRangeAudioCtrl().SetRangeAudioMode(RangeAudioMode.TEAM); -if (resultCode == ResultCode.OK) { - // 成功 -} -if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} -``` - -```java -import com.taptap.taprtc.TapRTCRangeAudioCtrl.RangeAudioMode; - -int teamId = 12345678; -ResultCode code = room.rangeAudioCtrl().setRangeAudioTeam(new TeamID(teamId)); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} - -// 世界模式 -ResultCode resultCode = room.rangeAudioCtrl().setRangeAudioMode(RangeAudioMode.WORLD); -if (resultCode == ResultCode.OK) { - // 成功 -} -if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} - -// 小队模式 -ResultCode resultCode = room.rangeAudioCtrl().setRangeAudioMode(RangeAudioMode.TEAM); -if (resultCode == ResultCode.OK) { - // 成功 -} -if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} -``` - -```objc -int teamId = 12345678; -TapRTCResultCode resultCode = [room setRangeAudioTeamId:teamId]; - -// 世界模式 -TapRTCResultCode code = [room setRangeAudioMode:TapRTCRangeAudioModeWorld]; -TapRTCResultCode code = [room setRangeAudioMode:TapRTCRangeAudioModeTeam]; -``` - - - -然后进入房间,并[设置语音接收范围](#设置语音接收范围)、[更新声源方位](#更新声源方位朝向),范围语音即可生效。 - -进入房间后,如需切换语音模式,可再次调用 `SetRangeAudioMode`。 - -### 设置语音接收范围 - -语音接收范围控制世界模式下其他小队成员能否听到声音,在进房后调用,一般仅需设置一次。 - - - -```cs -int range = 300; -ResultCode code = room.GetRtcRangeAudioCtrl().UpdateAudioReceiverRange(range); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} -``` - -```java -int range = 300; -ResultCode code = room.rangeAudioCtrl().updateAudioReceiverRange(range); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} -``` - -```objc -int range = 300; -TapRTCResultCode code = [room updateAudioReceiverRange:range]; -``` - - - -范围(`range`)之外的其他小队成员无法听到。 - -如果同时[启用 3D 语音](#开关-3d-语音),那么距离远近还会影响音量大小: - -| 音源距离 | 音量衰减 | -| - | - | -| `N < range/10` | 1.0(无衰减)| -| `N >= range/10` | `range/10/N` | - -### 更新声源方位朝向 - -成功进入房间后,需要在 Unity 的 Update 方法中调用该接口更新声源方位、朝向,范围语音才能生效。 -方位通过世界坐标系的前、右、上三个坐标指定,朝向通过自身坐标系的前、右、上轴的单位向量指定。 - - - -```cs -int x = 1; -int y = 2; -int z = 3; -Position position = new Position(x, y, z); - -float[] axisForward = new float[3] {1.0, 0.0, 0.0}; -float[] axisRight = new float[3] {0.0, 1.0, 0.0}; -float[] axisUp = new float[3] {0.0, 0.0, 1.0}; -Forward forward = new Forward(axisForward, axisRight, axisUp); -ResultCode code = room.GetRtcRangeAudioCtrl().UpdateSelfPosition(position, forward); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} -``` - -```java -import com.taptap.taprtc.TapRTCRangeAudioCtrl.Position; -import com.taptap.taprtc.TapRTCRangeAudioCtrl.Forward; - -int x = 1; -int y = 2; -int z = 3; -Position position = new Position(x, y, z); - -float[] axisForward = {1.0, 0.0, 0.0}; -float[] axisRight = {0.0, 1.0, 0.0}; -float[] axisUp = {0.0, 0.0, 1.0}; -Forward forward = new Forward(axisForward, axisRight, axisUp); - -ResultCode code = room.rangeAudioCtrl().updateSelfPosition(position, forward); -if (code == ResultCode.OK) { - // 成功 -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // 该房间未启用范围语音 -} -``` - -```objc -TapRTCPosition position; -position.x = 1.0; -position.y = 2.0; -position.z = 3.0; - -TapRTCAxis axisForward; -axisForward.x = 1.0; -axisForward.y = 0.0; -axisForward.z = 0.0; -TapRTCAxis axisRight; -axisRight.x = 0.0; -axisRight.y = 1.0; -axisRight.z = 0.0; -TapRTCAxis axisUp; -axisUp.x = 0.0; -axisUp.y = 0.0; -axisUp.z = 1.0; - -TapRTCForward forward; -forward.forward = axisForward; -forward.rightward = axisRight; -forward.upward = axisUp; -TapRTCResultCode code = [room updateSelfPosition:position forward:forward]; -``` - - - -朝向不影响是否能听到语音,因此如未[启用 3D 语音](#开关-3d-语音),更新声源方位朝向时,朝向参数可随意设置。 -但启用 3D 语音时,需正确设置朝向,这样才能得到准确的 3D 音效。 - -### 开关 3D 语音 - -开启 3D 语音,可以将无方位感的声音处理成带有声源方位感的声音,以增加玩家的沉浸感。 -这个接口接受两个参数,第一个参数指定当前玩家是否可以听到 3D 音效,第二个参数指定 3D 语音是否作用于[小队内部](#范围语音)。 - - - -```cs -bool enable3D = true; -bool applyToTeam = true; -ResultCode code = TapRTC.GetAudioDevice().EnableSpatializer(enable3D, applyToTeam); -``` - -```java -boolean enable3D = true; -boolean applyToTeam = true; -boolean ok = TapRTCEngine.get().getAudioDevice().enableSpatializer(enable3D, applyToTeam); -``` - -```objc -TapRTCResultCode code = [engine.audioDevice EnableSpatializer:YES applyTeam:YES]; -``` - - - - -## 错误码 - -上述文档中的部分操作会返回 ResultCode,代码示例中给出了部分常见的错误类型对应的错误码。 -完整的错误码列表如下: - - - -```cs -namespace TapTap.RTC -{ - public enum ResultCode - { - OK = 0, - ERROR_UNKNOWN = 1, - ERROR_UNIMPLEMENTED = 2, - ERROR_NOT_ON_MAIN_THREAD = 3, - ERROR_INVAIDARGS = 4, - ERROR_NOT_INIT = 5, - ERROR_CONFIG_ERROR = 11, - ERROR_NET = 21, - ERROR_NET_TIMEOUT = 22, - ERROR_USER_NOT_EXIST = 101, - ERROR_ROOM_NOT_EXIST = 102, - ERROR_DEVICE_NOT_EXIST = 103, - ERROR_TEAM_ID_NOT_NULL = 104, - ERROR_ALREADY_IN_ROOM = 105, - ERROR_NO_PERMISSION = 106, - ERROR_AUTH_FAILED = 107, - ERROR_LIB_ERROR = 108, - ERROR_NOT_RANGE_ROOM = 109, - } -} -``` - -```java -public enum ResultCode { - OK(0, "Success"), - ERROR_UNKNOWN(1, "Unknown Error"), - ERROR_UNIMPLEMENTED(2, "Unimplemented Functionality"), - ERROR_NOT_ON_MAIN_THREAD(3, "Not running on the main thread"), - ERROR_INVALID_ARGUMENT(4, "Invalid parameter"), - ERROR_NOT_INIT(5, "Uninitialized"), - ERROR_CONFIG_ERROR(11, "Configuration error"), - ERROR_NET(21, "Network error"), - ERROR_NET_TIMEOUT(22, "Network request timeout"), - ERROR_USER_NOT_EXIST(101, "User does not exist"), - ERROR_ROOM_NOT_EXIST(102, "Room does not exist"), - ERROR_DEVICE_NOT_EXIST(103, "Device does not exist"), - ERROR_TEAM_ID_NOT_NULL(104, "TeamID cannot be Zero"), - ERROR_ALREADY_IN_ROOM(105, "It's already in the other room"), - ERROR_NO_PERMISSION(106, "TapRTCCode_Error_NoPermission\tNo Permission"), - ERROR_AUTH_FAILED(107, "Authorization failure"), - ERROR_LIB_ERROR(108, "Service provider library error"), - ERROR_NOT_RANGE_ROOM(109, "Not Support Range Room"), -} -``` - -```objc -FOUNDATION_EXPORT NSString * const TapRTCNetworkErrorDomain; -FOUNDATION_EXPORT NSString * const TapRTCResultErrorDomain; - -typedef NS_ENUM(NSInteger, TapRTCResultCode) { - TapRTCCode_OK = 0, - TapRTCCode_Error_Unknown = 1, - TapRTCCode_Error_Unimplemented = 2, - TapRTCCode_Error_NotOnMainThread, - TapRTCCode_Error_InvaidArgs, - TapRTCCode_Error_NotInit, - - TapRTCCode_ConfigError_Error = 11, - TapRTCCode_ConfigError_AppID, - TapRTCCode_ConfigError_AppKey, - TapRTCCode_ConfigError_ServerUrl, - TapRTCCode_ConfigError_UserID, - TapRTCCode_ConfigError_DeviceID, - - TapRTCCode_NetError_Error = 21, - TapRTCCode_NetError_Timeout, - - TapRTCCode_Error_UserNotExist = 101, - TapRTCCode_Error_RoomNotExist, - TapRTCCode_Error_DeviceNotExist, - TapRTCCode_Error_TeamIDNotBeZero, - TapRTCCode_Error_AlreadyInOtherRoom, - TapRTCCode_Error_NoPermission, - TapRTCCode_Error_AuthFailed, - TapRTCCode_LibError, -}; -``` - - - -## 服务端 - -为了保证聊天通道的安全性,实时语音服务需要搭配游戏自己的鉴权服务器使用。 -此外,游戏自己的服务器还用于响应合规回调和调用剔除玩家的接口。 - -### 服务端鉴权 - -客户端加入房间前,需要先通过开发者自己的鉴权服务器获取签名,之后实时语音云端会验证该签名,只有附带有效签名的请求才会被执行,非法请求会被阻止。 - ->游戏鉴权服务器: 1. 加入房间前请求签名 -游戏鉴权服务器-->>客户端: 2. 生成签名返回给客户端 -客户端->>实时语音服务云端: 3. 将签名编码到请求中发给实时语音服务器 -实时语音服务云端-->>客户端: 4. 对请求的内容和签名进行验证,执行后续操作 -`} /> - -1. 客户端加入房间前,向游戏自己的鉴权服务器请求签名; -2. 鉴权服务器根据下文所述的[鉴权密钥算法](#鉴权密钥算法)生成签名返回给客户端; -3. 客户端获得签名后,编码到请求中,发给实时语音服务器; -4. 实时语音服务器对请求的内容和签名做一遍验证,验证通过后执行后续的实际操作。 - -签名采用 **HMAC-SHA1** 算法和 Base64 结合。 -针对不同的请求,开发者生成不同的签名(参见后续格式说明),总体上,签名就是使用特定的密钥(在这里我们使用应用的 Master Key),对玩家和房间信息进行签名。 - -#### 鉴权密钥算法 - -鉴权所用到的签名产生过程涉及到**明文**、**密钥**和**加密算法**。 - -##### 明文 - -明文为以下字段的 json 字符串(字段顺序无关) - - - -| 字段 | 类型/长度 | 说明 | -| :--------- | :------------- | :----------------------------------------------------------- | -| `userId` | `string` | 进入语音聊天房间的用户标识 | -| `appId` | `string` | 游戏的 Client ID | -| `expireAt` | `unsigned int/4` | 过期时间(当前时间+有效期(单位:秒, 建议值:300s)) | -| `roomId` | `string` | 房间 ID | - -##### 密钥 - -游戏的 `Master Key`(即 `Server Secret`)。 -`Client ID` 和 `Server Secret` 都可在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 查看。 - -##### 加密算法 - -加密算法采用 **HMAC-SHA1** 算法和 Base64 结合,类似 JWT 的格式。 -生成的结果包含 payload(明文)和 sign(加密串)两部分。 - -1. 按照上面表格中字段构造 JSON 字符串。 - -2. Base64 编码上一步的 JSON 字符串得到 payload。 - -3. 通过 **HMAC-SHA1** 对 payload 用 **密钥** 生成 sign。 - -4. 使用 `.` (英文半角句号)拼接 payload 和 sign。 - -注意:JSON 字符串本身是字段顺序无关的,但是**传递的 payload 和用于生成 sign 的 payload 的字段顺序必须相同**,否则无法通过实时语音服务云端的校验。 - -下面给出 Java 和 Go 的示例代码供参考: - -
    -Java 示例 - -```java -import com.google.gson.Gson; -import org.junit.Test; -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import static org.junit.Assert.*; - -import java.time.Instant; -import java.util.Base64; - -public class JUnitTestSuite { - - private static final String MAC_NAME = "HmacSHA1"; - - @Test - public void testToken() throws Exception { - String masterKey = "masterKey"; - Token t = new Token(); - t.appId = "appId"; - t.userId ="user_test"; - t.roomId ="room_test";; - - int expTime = (int) Instant.now().getEpochSecond() + 5 * 60; - t.expireAt = expTime; - - // server authBuff to your SDK Client - String authBuff = genToken(t, masterKey); - assertNotNull(authBuff); - } - - private String genToken(Token token, String key) throws Exception { - Gson gson = new Gson(); - String t = gson.toJson(token); - String payload = Base64.getEncoder().encodeToString(t.getBytes(StandardCharsets.UTF_8)); - byte[] pEncryptOutBuf = hmacSHA1Encrypt(payload.getBytes(StandardCharsets.UTF_8), key); - String sign = Base64.getEncoder().encodeToString(pEncryptOutBuf); - return payload + "." + sign; - } - - - byte[] hmacSHA1Encrypt(byte[] text, String key) throws Exception { - byte[] data = key.getBytes(StandardCharsets.UTF_8); - SecretKey secretKey = new SecretKeySpec(data, MAC_NAME); - Mac mac = Mac.getInstance(MAC_NAME); - mac.init(secretKey); - return mac.doFinal(text); - } - - class Token { - String userId; - String appId; - String roomId; - long expireAt; - } -} -``` - -
    - -
    -Go 示例 - -```go -package configs - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - - -func TestToken(t *testing.T) { - assert := assert.New(t) - t1 := &Token{ - UserId: "appId", - AppId: "user_test", - RoomId: "roomId_test", - ExpireAt: time.Now().Unix() + 5*60, - } - authBuff := GenToken(t1, "masterKey") - assert.NotEmpty(authBuff) - fmt.Println(authBuff) -} - - -const ( - sep = "." -) - -func GenToken(t *Token, masterKey string) string { - b, err := json.Marshal(t) - if err != nil { - return "" - } - payload := base64.StdEncoding.EncodeToString(b) - sign := base64.StdEncoding.EncodeToString(HmacSHA1(masterKey, payload)) - return payload + sep + sign - -} - - -func HmacSHA1(key string, data string) []byte { - mac := hmac.New(sha1.New, []byte(key)) - mac.Write([]byte(data)) - return mac.Sum(nil) -} - -type Token struct { - UserId string `json:"userId,omitempty"` - AppId string `json:"appId,omitempty"` - RoomId string `json:"roomId,omitempty"` - ExpireAt int64 `json:"expireAt,omitempty"` -} -``` - -
    - -#### 部署方式 - -由于加密密钥使用了 `Server Secret`,加密算法的逻辑需在服务端实现,**切勿在客户端部署加密方案**。 - -#### 使用方法 - -游戏自己的鉴权服务器生成加密串后,下发给客户端,客户端调用[加入房间的接口](#加入房间)时传入相应的鉴权信息。 - -C# SDK 还提供了一个 `GenToken` 工具方法,供客户端接入 SDK 测试开发时使用。 -比如,客户端开发人员可以在等待服务端开发人员实现、部署相应接口前先行在客户端测试加入房间的功能。 -再比如,客户端开发人员可以比较 SDK 自带的 `GenToken` 生成的加密串和服务端生成的加密串是否一致,以便验证服务端正确实现了加密算法。 - -```cs -var authBuffer = AuthBufferHelper.GenToken(appId, roomId, userId, masterKey); -``` - -注意,由于这个方法需要传入 `Server Secret` 作为参数,**因此仅供内部测试开发时使用,切勿在对外提供的代码或安装包中使用**。 -如果担心因为人为失误导致对外提供的代码或安装包中泄漏 `Server Secret`,或者出于安全考虑希望尽可能少的内部开发人员接触到 `Server Secret`,建议不使用 SDK 提供的 `GenToken` 方法,内部测试开发时也同样通过游戏自己的鉴权服务器生成加密串。 - -### 合规回调 - - -开发者在实时语音服务管理后台(**开发者中心 > 你的游戏 > 游戏服务 > 实时语音 > 设置**)设置回调地址后,语音内容有违规时会调用设置的回调地址。 - -回调地址需要是一个 HTTP(S) 协议接口的 URL,需要支持 POST 方法,传输数据编码采用 UTF-8。 - -回调的 POST body 示例: - -```json -{ - "HitFlag":true, - "Msg":"不堪入目的消息内容", - "ScanFinishTime":1634893736, - "ScanStartTime":1634893734, - "Scenes":[ - "default" - ], - "VoiceFilterPiece":[ - { - "Duration":14000, - "HitFlag":true, - "Info":"不堪入目的消息内容", - "MainType":"abuse", - "Offset":0, - "PieceStartTime":1634893734, - "RoomId":"1234", - "UserId":"123456", - "VoiceFilterDetail":[ - { - "EndTime":0, - "KeyWord":"不堪入目的关键词", - "Label":"abuse", - "Rate":"0.00", - "StartTime":0 - } - ] - } - ] -} -``` - -开发者可以在回调的 Header 中拿到 `sign` 字段,以便校验请求是否来自实时语音服务云端。 - -#### 合规回调校验算法 - -1. 在回调的 POST body 前附加 `POST` 前缀,得到 payload: - - ``` - POST{"HitFlag":true,"Msg":"不堪入目的消息内容",/* 略 */} - ``` - - 注意: - - - **请直接从 HTTP 请求体中读取 JSON 内容**,反序列化到程序语言的数据结构可能会改变字段顺序,导致校验失败。 - - POST 和 body 直接连接,中间无空格。 - -2. 对 payload 进行 HMAC-SHA1 加密,密钥为游戏应用的 `Server Secret`。 - -3. 对上一步的结果进行 BASE64 编码,得到 sign。 - -4. 和回调的 HTTP 头中的 sign 字段的值进行比较,相同则表明请求来自实时语音服务云端。 - -下面给出 Go 的示例代码供参考: - -
    -Go 示例 - -```go -package main - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "github.com/labstack/echo/v4" - "io/ioutil" - "net/http" -) - - -func testCallback(c echo.Context) error { - sign := c.Request().Header.Get("sign") - body, _ := ioutil.ReadAll(c.Request().Body) - checkGMESign(sign, "yourMasterKey", string(body)) - return c.NoContent(http.StatusOK) -} - - -func checkGMESign(signature, secretKey, body string) bool { - sign := genSign(secretKey, body) - return sign == signature -} - -func genSign(secretKey, body string) string { - content := "POST" + body - a := hmacSHA1(secretKey, content) - return base64.StdEncoding.EncodeToString(a) -} - -func hmacSHA1(key string, data string) []byte { - mac := hmac.New(sha1.New, []byte(key)) - mac.Write([]byte(data)) - return mac.Sum(nil) -} -``` - -
    - -### 剔除玩家 - -在一些场景下,游戏可能有从房间剔除玩家(踢人)的需求,比如涉及违规内容时。 -开发者可以在自己的服务端调用实时语音服务的 REST API 接口实现这一需求。 - -#### 请求格式 - -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 - -请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,参数如下表: - -Key|Value|含义|来源 ----|----|---|--- -`X-LC-Id`|`{{appid}}`|当前应用的 `App Id`(即 `Client Id`)|可在控制台查看 -`X-LC-Key`|`{{masterkey}},master`|当前应用的 `Master Key`(即 `Server Secret`)|可在控制台查看 - -#### Base URL - -REST API 请求的 Base URL(下文 curl 示例中用 `{{host}}` 表示)即应用的 API 自定义域名,可以在控制台绑定、查看。详见文档关于[域名](/sdk/storage/guide/setup-dotnet#域名)的说明。 - -#### REST API - -```sh -curl -X DELETE \ --H "Content-Type: application/json" \ --H "X-LC-Id: {{appId}}" \ --H "X-LC-Key: {{masterKey}},master" \ --d '{"roomId":"YOUR-ROOM-ID", "userId":"YOUR-USER-ID"}' \ -https://{{host}}/rtc/v1/room/member -``` - -成功剔除时响应的 HTTP 状态码为 `200`,出错时响应的 HTTP 状态码为相应的错误码,例如没有权限时 HTTP 状态码为 401. - -注意,**剔除玩家后,玩家可以重新加入房间,加入房间后又可以发言。** -**游戏还需在自己的鉴权服务器上实现对应的封禁逻辑**,相应玩家重新加入房间时不下发签名,以阻止玩家通过重新加入房间的方式绕开限制。 diff --git a/docs/shadow/tap-update-old-guide.mdx b/docs/shadow/tap-update-old-guide.mdx deleted file mode 100644 index b6edd2573..000000000 --- a/docs/shadow/tap-update-old-guide.mdx +++ /dev/null @@ -1,679 +0,0 @@ ---- -title: 唤起更新开发指南 -slug: /tap-update-old-guide/ ---- - - - - - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; -import AndroidFaq from "../sdk/_partials/android-package-visibility.mdx"; - -TapTap 开发者服务为游戏和玩家提供唤起 TapTap 客户端更新游戏的功能。 - -当游戏发布了新版本,且需要玩家进行更新才能体验新版本时,在游戏内绘制一个界面告知玩家并提供「更新」按钮。 - -玩家点击按钮调用 `updateGameAndFailToWebInTapTap` 接口,此时跳转到 TapTap 客户端内的游戏详情页,玩家在 TapTap 内完成更新。 - -:::info -唤起更新只提供「唤起 TapTap 客户端,跳转至游戏详情页」这一简单功能。 - -TapTap 上游戏的**版本号**来自开发者在构建新版本时上传的 APK,唤起更新功能并不提供「检查 TapTap 商店是否有新版本」接口,游戏需自行实现判断版本的功能。 -::: - -## SDK 获取 - - - -<> - -SDK 可以**通过 Unity Package Manager 导入或手动导入**,二者任选其一。 - -如果选择 UPM 导入,可以在项目的 `Packages/manifest.json` 文件中添加: - - - {`"dependencies":{ - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -}`} - - -如果选择手动导入: - -* 在 [下载页](/tap-download) 找到 TapSDK Unity 下载地址,下载 TapSDK-UnityPackage.zip 然后解压,导入其中的 `TapTap_Common` 模块。 -* 下载 [LeanCloud-SDK-Storage-Unity.zip](https://github.com/leancloud/csharp-sdk/releases),解压后为 Plugins 文件夹,拖拽至 Unity 即可。 - - - - -<> - -在 [下载页](/tap-download) 获得 TapSDK,添加 `TapCommon` 模块。 - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') -}`} - - - -<> - -在 [下载页](/tap-download) 获得 TapSDK,添加 `TapCommon` 模块。 - -{`TapCommonSDK.framework`} - - - - - -## 唤起 TapTap 检查更新 - -:::tip -自 TapSDK 3.3.0 版本开始,针对更新功能做了逻辑优化,唤起 TapTap 客户端更新游戏失败时可以跳转到自定义网页。TapSDK 3.3.0 版本对之前的版本向下兼容。 -该版本一般情况下不需要在使用以下 API 前特别检查是否安装 TapTap 客户端,自 TapSDK 3.3.0 版本开始推荐使用新接口。 -::: - - -<> - -在 TapTap 客户端更新游戏,失败时通过浏览器打开 TapTap 网站对应的游戏页面: - - - -```cs -// 适用于中国大陆 -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapTap(string appId); - -// 适用于其他国家或地区 -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapGlobal(string appId); -``` - - - - - -```cs -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapGlobal(string appId); -``` - - - -唤起 TapTap 客户端更新游戏失败,跳转到自定义网页: - - - -```cs -// 适用于中国大陆 -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapTap(string appId, string webUrl); - -// 适用于其他国家或地区 -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapGlobal(string appId, string webUrl); -``` - - - - - -```cs -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapGlobal(string appId, string webUrl); -``` - - - - -<> - -在 TapTap 客户端更新游戏,失败时通过浏览器打开 TapTap 网站对应的游戏页面: - - - -```java -// 适用于中国大陆 -TapGameUtil.updateGameAndFailToWebInTapTap(context, "your app id"); - -// 适用于其他国家或地区 -TapGameUtil.updateGameAndFailToWebInTapGlobal(context, "your app id"); -``` - - - - - -```java -TapGameUtil.updateGameAndFailToWebInTapGlobal(context, "your app id"); -``` - - - -唤起 TapTap 客户端更新游戏失败,跳转到自定义网页: - - - -```java -// 适用于中国大陆 -TapGameUtil.updateGameAndFailToWebInTapTap(context, "your app id", "your website url"); - -// 适用于其他国家或地区 -TapGameUtil.updateGameAndFailToWebInTapGlobal(context, "your app id", "your website url"); -``` - - - - - -```java -TapGameUtil.updateGameAndFailToWebInTapGlobal(context, "your app id", "your website url"); -``` - - - - -<> - -```objc -// 受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能 -``` - - - - - - -
    -点击展开 TapSDK 3.3.0 之前的相关接口 - -**TapSDK 3.3.0 及之后版本不必参考这一节**,推荐使用上面「唤起 TapTap 检查更新」的 API。 - -**检查 TapTap 客户端是否安装**: - - - -<> - -```cs -// 适用于中国大陆 -TapCommon.IsTapTapInstalled(installed => -{ - if (installed) { - Debug.Log("TapTap 已经安装"); - } -}); - -// 适用于其他国家或地区 -TapCommon.IsTapTapGlobalInstalled(installed => -{ - if (installed) { - Debug.Log("TapTap 已经安装"); - } -}); -``` - - -<> - -需要导入 `TapCommon.aar` 包,接口在 `TapGameUtil` 中。 - -```java -import com.tds.common.utils.TapGameUtil; - -// 适用于中国大陆 -if(TapGameUtil.isTapTapInstalled(this)){ - Log.d(TAG, "已经安装 TapTap 客户端"); -} -// 适用于其他国家或地区 -if(TapGameUtil.isTapGlobalInstalled(this)){ - Log.d(TAG, "已经安装 TapTap 客户端"); -} -``` - - -<> - -需要导入 `TapCommon.framework` 库文件,在 `info.plist` 里配置 `LSApplicationQueriesSchemes` 增加 `tapsdk`、`taptap` 和 `tapiosdk`。 - -```objc -#import -// 适用于中国大陆 -BOOL isInstalled = [TapGameUtil isTapTapInstalled]; -// 适用于其他国家或地区 -BOOL isInstalled = [TapGameUtil isTapGlobalInstalled]; -``` - - - - - -**唤起 TapTap 更新游戏**: - - - -```cs -TapCommon.UpdateGameInTapTap("appid", callSuccess => -{ - if (callSuccess) { - Debug.Log("TapTap 唤起成功"); - } -}); -``` - -```java -if(TapGameUtil.updateGameInTapTap(this,"appid")){ - Log.d(TAG, "唤起 TapTap 客户端成功"); -} -``` - -```objc -// 受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能 -``` - - - -**常见问题:Android 11 或更高版本无法拉起 TapTap 客户端** - - - -
    - -
    - -## 打开游戏评论区 - - - -<> - - - -```cs -// 适用于中国大陆 -TapCommon.OpenReviewInTapTap(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("打开游戏评论区成功"); - } -}); - -// 适用于其他国家或地区 -TapCommon.OpenReviewInTapGlobal(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("打开游戏评论区成功"); - } -}); -``` - - - - - -```cs -TapCommon.OpenReviewInTapGlobal(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("打开游戏评论区成功"); - } -}); -``` - - - - - -<> - - - -```java -// 适用于中国大陆 -if(TapGameUtil.openReviewInTapTap(this,"appid")){ - Log.d(TAG, "打开评论区成功"); -} - -// 适用于其他国家或地区 -if(TapGameUtil.openReviewInTapGlobal(this,"appid")){ - Log.d(TAG, "打开评论区成功"); -} -``` - - - - - -```java -if(TapGameUtil.openReviewInTapGlobal(this,"appid")){ - Log.d(TAG, "打开评论区成功"); -} -``` - - - - - -<> - -```objc -// 未支持 -``` - - - - - -appid:游戏在 TapTap 商店的唯一身份标识。 -例如:`https://www.taptap.cn/app/187168``https://www.taptap.io/app/187168`,其中 `187168` 是 `appid`。 - -## 常见问题 - -### 未接入 TapSDK,如何唤起 TapTap 客户端更新游戏 - -受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能,以下方案仅限于 Android 平台使用。 - -未接入 TapSDK、使用旧版 TapSDK 难以升级的游戏,可以通过以下方案进行手动唤起 TapTap 客户端更新游戏: - -根据玩家设备是否安装 TapTap 客户端来对应打开 URL: - -- 如果玩家设备安装 TapTap 客户端则直接唤起 TapTap 客户端到游戏详情页进行更新; -- 如果玩家设备没有安装 TapTap 客户端,则以 Web 形式打开游戏详情页,根据页面底部提示引导玩家下载 TapTap 客户端,安装成功后打开 TapTap 客户端,玩家根据提示选择在 TapTap 客户端里打开游戏详情页进行更新。 - -未安装 TapTap 客户端对应的 URL: - - - -- 适用于中国大陆:`https://l.taptap.cn/5d1NGyET?subc1=AppID` -- 适用于其他国家或地区:`https://l.taptap.io/GNYwFaZr?subc1=AppID` - - - - -- `https://l.taptap.io/GNYwFaZr?subc1=AppID` - - - -已安装 TapTap 客户端对应的 URL: - - - -- 适用于中国大陆:`taptap://taptap.cn/app?app_id=AppID&source=outer|update` -- 适用于其他国家或地区:`tapglobal://taptap.tw/app?app_id=游戏商店id&source=outer|update` - - - - -- `tapglobal://taptap.tw/app?app_id=游戏商店id&source=outer|update` - - - -注意替换其中的 `AppID`。`AppID` 是游戏在 TapTap 商店的唯一身份标识,例如:`https://www.taptap.cn/app/187168``https://www.taptap.io/app/187168`,其中 `187168` 是 AppID。 - -注意,除了打开 URL 外,还需要检测设备是否已经安装 TapTap 客户端,以及处理唤起失败的逻辑,这些代码都需要自行编写。 - -下面提供 TapSDK 唤起更新的代码供参考。 - -
    -参考代码 - - - -适用于中国大陆: - -```java -package com.tds.common.utils; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; -import java.util.Locale; - -public class TapGameUtil { - - private static final String TAG = TapGameUtil.class.getName(); - - public static final String PACKAGE_NAME_TAPTAP = "com.taptap"; - - public static final String CLIENT_URI_TAPTAP = "taptap://taptap.cn"; - - public static final String DEFAULT_CLIENT_DOWNLOAD_URL_TAPTAP = "https://l.taptap.cn/5d1NGyET"; - - // 这里更新的时候不检查 Tap 客户端,一是因为特定 schema 没被应用注册的话大概率是直接返回 error 的,在这里被 try catch 后返回 false 可以近似等于客户端不存在 - // 二是因为 Android 11 开始检查客户端需要游戏做特殊配置,这个配置无法在 SDK 内做好,因为和编译工具版本强绑定,无法做前后版本兼容。 - public static boolean updateGameAndFailToWebInTapTap(Activity activity, String appId) { - return updateGameInTapTap(activity, appId) || openWebDownloadUrlOfTapTap(activity, appId); - } - - public static boolean updateGameAndFailToWebInTapTap(Activity activity, String appId, String webUrl) { - if (TextUtils.isEmpty(webUrl)) { - return updateGameAndFailToWebInTapTap(activity, appId); - } - return updateGameInTapTap(activity, appId) || openWebDownloadUrl(activity, webUrl); - } - - public static boolean isTapTapInstalled(Context context) { - return isTapClientInstalled(context, PACKAGE_NAME_TAPTAP); - } - - public static boolean isTapClientInstalled(Context context, String clientPackageName) { - if (context != null && !TextUtils.isEmpty(clientPackageName)) { - boolean TapTapInstalled = false; - try { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo(clientPackageName, 0); - if (null != packageInfo) { - TapTapInstalled = true; - } - } catch (Exception e) { - Log.e(TAG, clientPackageName + " isInstalled=false"); - } - return TapTapInstalled; - } - return false; - } - - public static boolean updateGameInTapTap(Activity activity, String appId) { - return updateGameInTapClient(activity, appId, CLIENT_URI_TAPTAP); - } - - public static boolean updateGameInTapClient(Activity activity, String appId, String clientUri) { - if (activity != null && !TextUtils.isEmpty(appId) && !TextUtils.isEmpty(clientUri)) { - try { - Uri uri = Uri.parse(String.format(Locale.US, - "%s/app?app_id=%s&source=outer|update", clientUri, appId)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, clientUri + "updateGameInTapTap failed"); - return false; - } - return true; - } - Log.e(TAG, clientUri + "updateGameInTapTap failed"); - return false; - } - - public static boolean openReviewInTapTap(Activity activity, String appId) { - return openReviewInTapClient(activity, appId, CLIENT_URI_TAPTAP); - } - - public static boolean openReviewInTapClient(Activity activity, String appId, String clientUri) { - if (activity != null && !TextUtils.isEmpty(appId) && !TextUtils.isEmpty(clientUri)) { - try { - Uri uri = Uri.parse(String.format(Locale.US, - "%s/app?tab_name=review&app_id=%s", clientUri, appId)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, clientUri + "openTapTapReview failed"); - return false; - } - return true; - } - Log.e(TAG, clientUri + "openTapTapReview failed"); - return false; - } - - public static boolean openWebDownloadUrlOfTapTap(Activity activity, String appId) { - return openWebDownloadUrl(activity, String.format(Locale.US, DEFAULT_CLIENT_DOWNLOAD_URL_TAPTAP + "?subc1=%s", appId)); - } - - public static boolean openWebDownloadUrl(Activity activity, String url) { - if (activity != null && !TextUtils.isEmpty(url)) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setData(Uri.parse(url)); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, "openWebUrl fail"); - return false; - } - return true; - } - return false; - } - -} -``` - -适用于其他国家或地区: - - - -```java -package com.tds.common.utils; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; -import java.util.Locale; - -public class TapGameUtil { - - private static final String TAG = TapGameUtil.class.getName(); - - public static final String PACKAGE_NAME_TAPTAP_GLOBAL = "com.taptap.global"; - - public static final String CLIENT_URI_TAPTAP_GLOBAL = "tapglobal://taptap.tw"; - - public static final String DEFAULT_CLIENT_DOWNLOAD_URL_TAPTAP_GLOBAL = "https://l.taptap.io/GNYwFaZr"; - - // 这里更新的时候不检查 Tap 客户端,一是因为特定 schema 没被应用注册的话大概率是直接返回 error 的,在这里被 try catch 后返回 false 可以近似等于客户端不存在 - // 二是因为 Android 11 开始检查客户端需要游戏做特殊配置,这个配置无法在 SDK 内做好,因为和编译工具版本强绑定,无法做前后版本兼容。 - - public static boolean updateGameAndFailToWebInTapGlobal(Activity activity, String appId) { - return updateGameInTapGlobal(activity, appId) || openWebDownloadUrlOfTapGlobal(activity, appId); - } - - public static boolean updateGameAndFailToWebInTapGlobal(Activity activity, String appId, String webUrl) { - if (TextUtils.isEmpty(webUrl)) { - return updateGameAndFailToWebInTapGlobal(activity, appId); - } - return updateGameInTapGlobal(activity, appId) || openWebDownloadUrl(activity, webUrl); - } - - public static boolean isTapGlobalInstalled(Context context) { - return isTapClientInstalled(context, PACKAGE_NAME_TAPTAP_GLOBAL); - } - - public static boolean isTapClientInstalled(Context context, String clientPackageName) { - if (context != null && !TextUtils.isEmpty(clientPackageName)) { - boolean TapTapInstalled = false; - try { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo(clientPackageName, 0); - if (null != packageInfo) { - TapTapInstalled = true; - } - } catch (Exception e) { - Log.e(TAG, clientPackageName + " isInstalled=false"); - } - return TapTapInstalled; - } - return false; - } - - public static boolean updateGameInTapGlobal(Activity activity, String appId) { - return updateGameInTapClient(activity, appId, CLIENT_URI_TAPTAP_GLOBAL); - } - - public static boolean updateGameInTapClient(Activity activity, String appId, String clientUri) { - if (activity != null && !TextUtils.isEmpty(appId) && !TextUtils.isEmpty(clientUri)) { - try { - Uri uri = Uri.parse(String.format(Locale.US, - "%s/app?app_id=%s&source=outer|update", clientUri, appId)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, clientUri + "updateGameInTapTap failed"); - return false; - } - return true; - } - Log.e(TAG, clientUri + "updateGameInTapTap failed"); - return false; - } - - public static boolean openReviewInTapGlobal(Activity activity, String appId) { - return openReviewInTapClient(activity, appId, CLIENT_URI_TAPTAP_GLOBAL); - } - - public static boolean openReviewInTapClient(Activity activity, String appId, String clientUri) { - if (activity != null && !TextUtils.isEmpty(appId) && !TextUtils.isEmpty(clientUri)) { - try { - Uri uri = Uri.parse(String.format(Locale.US, - "%s/app?tab_name=review&app_id=%s", clientUri, appId)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, clientUri + "openTapTapReview failed"); - return false; - } - return true; - } - Log.e(TAG, clientUri + "openTapTapReview failed"); - return false; - } - - public static boolean openWebDownloadUrlOfTapGlobal(Activity activity, String appId) { - return openWebDownloadUrl(activity, String.format(Locale.US, DEFAULT_CLIENT_DOWNLOAD_URL_TAPTAP_GLOBAL + "?subc1=%s", appId)); - } - - public static boolean openWebDownloadUrl(Activity activity, String url) { - if (activity != null && !TextUtils.isEmpty(url)) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setData(Uri.parse(url)); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, "openWebUrl fail"); - return false; - } - return true; - } - return false; - } - -} -``` - -
    \ No newline at end of file diff --git a/docs/shadow/text-moderation/text-moderation-best-practice.mdx b/docs/shadow/text-moderation/text-moderation-best-practice.mdx deleted file mode 100644 index 3988fe58f..000000000 --- a/docs/shadow/text-moderation/text-moderation-best-practice.mdx +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: 文本检测最佳实践 -slug: /sdk/text-moderation/best-practice/ ---- - - - - - -## 游戏昵称检测 - -根据《互联网信息服务管理办法》《网络信息内容生态治理规定》等法律法规,及国家网信办等相关部门的要求,游戏开发运营者需保证玩家发布的信息同样复核内容安全规定。其中玩家昵称由于具有长期持有、出现频繁等特点,易引发明显的违规风险,因此文本检测提供对游戏违规昵称提供识别能力。 - -### 前提条件 - - - 已[开启文本检测服务](/sdk/text-moderation/features#开启文本检测服务) - - 已阅读[开发指南](/sdk/text-moderation/guide/) - -### 应用场景 - -游戏创建人物角色,玩家起名字时,可以利用文本检测 API 进行识别,逻辑处理可参考以下: - -1. 玩家输入昵称:「游戏不好玩」,点击创建角色 -2. 服务端收到了创建角色的请求,在服务端中使用文本检测接口对玩家昵称进行检测,根据返回的结果可以处理以下几种情况: - - 1. **通过**:识别为健康的内容,允许创建该昵称 - - 2. **复核**:由于是复核情况,游戏可以执行上述「通过」的逻辑,后续根据记录在由人工定夺,是否需要修改名字 - - 3. **拒绝**:存在违规风险,不符合规定,不允许创建该昵称 - -3. 通过上述的文本检测结构就能达到满足于昵称场景下的识别 - - -### 代码示例 - -**Golang** - -``` go -rs := client.Check("游戏不好玩") -if rs.result == 1 { - // todo: reject create user -} else if rs.result == 2 { - // todo: can create user, log result for auditor review -} else { - // todo: get pass, can create user -} -``` - - -## 聊天检测 - -大多数联机游戏中都会有即时通讯,广播,聊天室等场景,在这些场景中可能会出现违规违法,低俗色情,诱导推广信息的内容问题。为了降低开发者负担,利用文本检测可以解决该场景下的内容生态一系列问题,保证内容健康积极,打击违法违规信息,制止传播低俗色情内容,屏蔽诱导推广信息。 - -### 前提条件 - - - 已[开启文本检测服务](/sdk/text-moderation/features#开启文本检测服务) - - 已阅读[开发指南](/sdk/text-moderation/guide/) - -### 应用场景 - -玩家进行聊天,传播内容时,可以通过文本检测 API 进行识别过滤,逻辑处理可参考如下: - -#### 涉政内容过滤 -1. 玩家输入聊天内容:「G 市偷井盖,真讨厌」,并发送到公屏 -2. 服务端收到了发送公屏的请求,在服务端中使用文本检测接口对内容进行检测,根据返回的过滤内容结果可以处理以下几种情况: - - 1. **通过**:识别为健康的内容,允许发送到公屏 - - 2. **复核**:由于是复核情况,游戏可以执行上述「通过」的逻辑,后续根据记录在由人工定夺,进行禁言/封号处理 - - 3. **拒绝**:存在涉证内容,不符合规定,可以不允许发送公屏聊天,或者通过接口的已过滤后的文本「*,真讨厌」进行发送 - -#### 代码示例 - -**Golang** - -``` go -rs := client.Check("G 市偷井盖,真讨厌") -if rs.result == 1 && rs.filtered_text != "" { - // todo: can send 'rs.filterd_text' in chat room -} else if rs.result == 2 { - // todo: can send msg, log result for auditor review -} else if rs.result == 0 { - // todo: get pass, can send msg -} else { - // todo: warning info send to user -} -``` - -#### 广告内容 - -1. 玩家输入聊天内容:「工作室,+v xxxx」,并发送到公屏 -2. 服务端收到了发送公屏的请求,在服务端中使用文本检测接口对内容进行检测,根据返回的过滤结果可以处理以下几种情况 : - - 1. **通过**:识别为健康的内容,允许发送到公屏 - - 2. **复核**:由于是复核情况,游戏可以执行上述「通过」的逻辑,后续根据记录在由人工定夺,进行禁言/封号处理 - - 3. **拒绝**:识别类型为广告(Adv),需要进行屏蔽,不发送到公屏聊天 - -#### 代码示例 - -**Golang** - -``` go -rs := client.Check("工作室,+v xxxx") -if rs.result == 1 && rs.type == "Adv" { - // todo: reject send to chat room -} else if rs.result == 2 { - // todo: can send msg, log result for auditor review -} else { - // todo: get pass, can send msg -} -``` - - - diff --git a/docs/shadow/text-moderation/text-moderation-features.mdx b/docs/shadow/text-moderation/text-moderation-features.mdx deleted file mode 100644 index 3cbd2c7bd..000000000 --- a/docs/shadow/text-moderation/text-moderation-features.mdx +++ /dev/null @@ -1,200 +0,0 @@ ---- -title: 文本检测功能介绍 -slug: /sdk/text-moderation/features/ ---- - - - - - -## 服务简介 -### 产品概述 - -文本检测为昵称、聊天、个性签名等场景,提供实时、智能、个性化的风险文本检测服务。 -基于 AI 及多重识别策略,及时、准确、高效地抵御政治、暴恐、色情、辱骂等违规内容风险。 - -### 应用场景 - -- 用户资料检测:昵称、公会名、简介、个性签名等。 -- 即时交流检测:聊天室、IM 消息等。 -- 非即时交流检测:帖子、弹幕、评论、动态等。 - -### 服务能力 - -- 识别检测:识别政治、色情、暴恐、辱骂、违禁品、犯罪、邪教等敏感违规文本。 -- 控制台:自定义词库、场景识别策略控制、检测记录查询、在线实时检测等。控制台详细介绍参考 [控制台操作](#控制台操作) -- 词库维护:专业运营持续更新默认词库,紧跟政策要求。 - -### 接入方式 - -提供便捷灵活的 API 接入方式,详细 API 参考 [开发指南](/sdk/text-moderation/guide/) - - -## 开启文本检测服务 -1. 开通:点击开发者中心右上角「工单」按钮,「创建问题」时分类选择「游戏服务」-「文本检测」-「开通申请」,填写联系方式,简要描述使用场景。提交后将有专人提供服务开通咨询。 -2. 获取开通资格后,使用管理员账号进入开发者中心对应的游戏。 -3. 点击顶部「游戏服务」菜单。 -4. 若此前未开启游戏服务功能,需先开启。 -5. 找到文本检测服务,点击「开启」,完成服务开启。 - -后续使用时,通过开发者中心游戏项目内路径:游戏服务 > 云服务 > 文本检测,即可访问服务控制台。 - - -## 控制台操作 -接下来先介绍文本检测控制台中的核心概念,将有助于你理解控制台的日常配置和使用。 - -### 概念定义 -#### 「场景」 -在文本检测中,「场景」是面向业务、模块的,聚合多种检测策略的核心对象。 - -可以简单理解为文本经过「场景」完成检测,输出检测结果。 - -业务上,往往一个「场景」对应一类业务场景,比如聊天场景、昵称场景等。相似的业务场景可共用一个「场景」,比如聊天、留言、弹幕、评论等,都可以使用默认提供的「聊天检测」场景。 - -详细说明请见下文 [场景配置](#控制台---场景配置) - -#### 「词库」 -「词库」用于放置需要屏蔽、放通的黑、白名单关键词,「场景」中与词相关的检测逻辑依赖于词库。 - -「词库」分为默认词库和自定义词库: - -- 默认词库:由专业运营人员持续维护,紧跟最新网络安全要求。 - -- 自定义词库:由游戏运营者自定义 - -详细说明请见下文 [自定义词库配置](#控制台---自定义词库配置) - - -### 控制台概览 - -在文本检测控制台,可修改检测配置、查看检测统计数据、查询检测记录等。 - -文本检测控制台顶部功能菜单: -- 「统计」页:用于查看检测统计数据。 -- 「检测记录」页:用于查询近期检测记录。 -- 「在线检测」页:用于在线输入文本,检测是否违规。 -- 「场景」页:用于配置检测策略,获取场景 ID 用于 API 调用。 -- 「自定义词库」页:用于配置自定义黑、白名单词。 - -### 控制台 - 统计 -![](/img/textsafety1.png) - -在该页可查看近期检测核心数据指标。 - -通过选择场景、日期查询,可查看对应条件下的图表。 - -- 「整体检测统计」图表:其中柱表示通过率,折线分别表示检测数、通过数、不通过数。 -- 「不通过识别类型分布统计」图表:其中折线表示各识别类型数量。 - -### 控制台 - 检测记录 -![](/img/textsafety2.png) - -在该页可查询近期每一条检测记录详情,包括检测文本、检测结果、识别类型、时间等。 - -- 你可以通过多维度的筛选条件,定向查询需要的记录。 -- 将鼠标移至文本上,会显示完整文本气泡,若检测为不通过,则气泡中将以红字显示可能违规的词句。 - -### 控制台 - 在线检测 -![](/img/textsafety3.png) - -在该页可在线输入文本,使用所选的场景进行检测,并实时输出检测结果。 - -- 在线检测经常用于调整了场景配置后,快速测试场景效果,以及游戏运营公告等官方文本的自检。 -- 在线检测的文本,在记录中将默认带上 admin_000000 的用户标识,你可通过此项来区分记录中哪些是在线检测生成的记录。 - -### 控制台 - 场景配置 -![](/img/textsafety4.png) - -在该页可对场景进行策略配置,包括启用的识别类型、使用的词库。此处可获取场景 ID ,用于 API 请求时指定场景进行检测,场景参数详见 [开发指南](/sdk/text-moderation/guide/) - -#### 1. 场景介绍 -开启服务即默认提供两个场景: - -- 「昵称用户资料」场景:推荐用于用户昵称、个性签名、队伍名、公会名、公会宣言等命名、代表角色或团体的文本场景。 -- 「聊天检测」场景:推荐用于用户聊天、留言、评论、邮件、弹幕等广泛的 UGC 文本场景。 - -> 由于「聊天检测」场景具备极强的泛用性,大部分情况下,昵称用户资料之外的文本都可以使用「聊天检测」场景进行检测。 - -#### 2. 修改启用识别类型 -在修改弹窗中,勾选某识别类型,意为将该类敏感词纳入检测范围。若不勾选,则文本中即使出现该类敏感词,也不会识别。 - -> 启用所有识别类型,此配置方式几乎适用所有游戏。 - -#### 3. 修改使用词库 -- 在修改弹窗中,左侧陈列所有可供使用的词库,其中包含系统提供的默认词库,和自定义词库,勾选即可使用该词库。 -- 弹窗右侧陈列所有已使用的词库,可拖拽修改词库排序。词库排序决定优先级,越前者优先级越高。 -- 优先级决定了:当多个词库中有同一个词时,以优先级高的词库中的词类型为准。 - -> 一般将自定义词库优先级配置为高于默认词库 - -### 控制台 - 自定义词库配置 -![](/img/textsafety5.png) - -在该页可管理自定义词库。自定义词库在被场景使用后,即可在该场景中发挥作用,识别词库中的词。 - -#### 1. 管理自定义词库 -目前最大支持同时存在 5 个自定义词库,可以根据运营需要维护多个词库供场景使用。 - -#### 2. 管理词库中的词 -![](/img/textsafety6.png) - -在增删词弹窗中,可新增词、删除词、修改词的识别类型。 - -- 词的识别类型与「场景」配置中的启用识别类型对应,若「场景」不勾选某识别类型,则即使在词库中添加了该类型的词,检测时也不会识别。 -- 新增的词若已存在,同样会新增成功,并替换该词的原识别类型。 -- 当词的识别类型为「健康」时,类似白名单,意为该词无条件通过,请谨慎使用。 - -注意:「健康」词仅对完整包含其的文本部分起放通作用。 - -举例:假设有敏感词「武器」「抢夺」,在词库中添加健康词「掉落武器」,则: -- 句子「怪掉落武器」检测通过; -- 句子「落武器」检测不通过(因为包含敏感词「武器」,且不完整匹配健康词「掉落武器」); -- 句子「抢夺掉落武器」检测不通过(「掉落武器」部分被放通,但「抢夺」为敏感词,因此整句不通过)。 - -健康词不区分大小写、简繁体,添加时使用小写字母、简体中文即可,对应的大写字母和繁体中文也将被放通。 - - -:::caution -为保证「健康」词在场景中生效,请确保包含「健康」词的词库,在场景的使用词库配置中,排序优先于系统默认词库。 -::: - - -## 控制台建议配置方案 -以下配置方案适用于绝大部分游戏,建议初始遵循此配置。后续有精细化运营需要,并充分熟悉系统后,可再逐步调整。 - -### 词库配置 - -> 通用、常见、关键的屏蔽词已包含在默认词库中,一般无需在自定义词库中维护大量屏蔽词,建议根据实际使用效果,有选择的添加自定义屏蔽词。 - -#### 1. 新建词库「白名单」 - -所有希望放通的词,均添加至该词库。加词时注意识别类型选择「健康」。 - -#### 2. 新建词库「通用屏蔽词」 -所有希望额外屏蔽的词,均添加至该词库。加词时根据该词的期望分类选择对应识别类型,合理的识别类型将有助于后续运营管理和追踪数据。 - - -:::caution -新建词库后,需要在对应场景的「使用词库」勾选才能生效。 -::: - -### 场景配置 -#### 1. 配置「昵称用户资料检测」场景 -1. 「启用识别类型」保持默认全选。 -2. 「使用词库」弹窗左侧,保持两个「默认」词库的勾选,并勾选自定义词库「白名单」「通用屏蔽词」。 -3. 「使用词库」弹窗右侧,将使用词库的排序调整为:白名单 > 通用屏蔽词 > 「默认」昵称词库 > 「默认」基础通用词库。 -4. 提交修改,完成设置。 - -#### 2. 配置「聊天检测」场景 -1. 「启用识别类型」保持默认全选。 -2. 「使用词库」弹窗左侧,保持两个「默认」词库的勾选,并勾选自定义词库「白名单」「通用屏蔽词」。 -3. 「使用词库」弹窗右侧,将使用词库的排序调整为:白名单 > 通用屏蔽词 > 「默认」聊天词库 > 「默认」基础通用词库。 -4. 提交修改,完成设置。 - -### 场景参数选择 -- 昵称、个性签名、队伍名、公会名、公会宣言等命名模块,调用服务时,使用「昵称用户资料检测」的场景 ID 作为参数。 - -- 以上模块之外的 UGC 文本模块,调用服务时,使用「聊天检测」的场景 ID 作为参数。 - -详细 API 使用见 [开发指南](/sdk/text-moderation/guide/) - diff --git a/docs/shadow/text-moderation/text-moderation-guide.mdx b/docs/shadow/text-moderation/text-moderation-guide.mdx deleted file mode 100644 index 210a556b3..000000000 --- a/docs/shadow/text-moderation/text-moderation-guide.mdx +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: 文本检测开发指南 -slug: /sdk/text-moderation/guide/ ---- - - - - - -:::info -**目前需要联系运营团队申请开通文本检测功能。** -**申请方式详见[开启文本检测服务](/sdk/text-moderation/features#开启文本检测服务)** -::: - -## 介绍 - -文本检测为昵称、聊天、个性签名等场景,提供实时、智能、个性化的风险文本检测服务。 -基于 AI 及多重识别策略,及时、准确、高效地抵御政治、暴恐、色情、辱骂等违规内容风险。 - -### API 列表 - -国内域名:https://whisper.cn.tapapis.com - -海外域名:https://whisper-sg.intl.tapapis.com - -API 路径 | 介绍 ---- | --- -/v2/text/check | 文本智能识别检测 API,提供准确的判定和类型识别结果 - -### API 协议 - -API 以 HTTP 协议对外提供,对于 POST 和 PUT 请求,请求主体必须是 JSON 格式,并且 HTTP Header 的 Content-Type 需要设置为 application/json - -## API 鉴权 - -API 鉴权通过 HTTP Header 设置键值参数进行授权,参数列表如下: - -Key | Value | 描述 ---- | --- | --- -X-Client-ID | ${client_id} | 国内为 TDS 客户标识(开发者中心 `Client ID`),海外需平台研发侧提供 -X-Server-Secret | ${server_secret} | 国内为 TDS 服务密钥(开发者中心 `Server Secret`),海外需平台研发侧提供 - -## API 详细说明 - -### 接口描述 - -提供场景进行区分识别检测效果,可利用多个词库组合,配置多种智能算法进行识别检测。API 会根据聚合后的结果返回一个准确的检测结果,分别是以下三种状态,用户可以根据响应的结果进行过滤: - -- 通过:表示文本内容是健康的,允许放行的,可供记录的内容。 -- 不通过:表示文本内容存在非法文本,不可对内容放行以及记录。 -- **复核:表示文本内容疑似非法,建议昵称场景按不通过处理,聊天场景按通过处理** - -> 提示:单次请求数据不得超过 5 MB 大小,文本长度超过 1 万字将会进行截断处理 - -### 请求方式 - -``` -POST https://{{domain}}/v2/text/check -``` - -### 请求参数 - -#### 基本参数 -参数 | 类型 | 必须 | 最大长度 | 说明 | 示例值 ---- | --- | --- | --- | --- | --- -scene | string | 是 | 32 | 业务场景,通过配置不同场景实现多种识别检测规则。请前往过滤场景中获取 | c1hh9bttqehouf41di00 -data | object | 是 | - | 文本数据提供进行内容识别检测 | [文本信息参数](#文本信息参数) -opt | object | 否 | - | 检测可选项,助力一些业务方需求 | [检测选项参数](#检测选项参数) - -#### 文本信息参数 - -参数 | 类型 | 必须 | 最大长度 | 说明 | 示例值 ---- | --- | --- | --- | --- | --- -data_id | string | 是 | 128 | 数据唯一标识 | 6pYJGpgcPz2ugv -text | string | 是 | 4096 | 进行检测的文本内容 | 文本检测助力游戏厂商识别违规非法政治内容 -user_id | string | 是 | 128 | 用户 ID,区分用户昵称、宠物昵称、聊天等具体场景 | nickname:123456 -ip | string | 否 | 128 | 用户请求合法 IP,非必填但需要推送 | 223.161.60.210 -publish_time | int64 | 否 | 13 | 内容创建时间戳(毫秒),默认当前请求时间 | 1717140728000 -nickname | string | 否 | 128 | 用户昵称,辅助校验和数据查询用途 | 小明 -server_code | string | 否 | 128 | 服务器编码,辅助校验 | prod_cn -group_id | string | 否 | 32 | 公聊:频道聊天,辅助校验 | A10 -room_id | string | 否 | 32 | 组团:组队、公会、旅团聊天,辅助校验 | 1U56O47 -receive_uid | string | 否 | 64 | 私聊:一对一聊天,接受玩家 UID,辅助校验 | 132447454 -register_time | int64 | 否 | 13 | 用户注册时间戳(毫秒),辅助校验 | 1685518328000 -level | int | 否 | 4 | 角色等级,辅助校验 | 2 - -#### 检测选项参数 -参数 | 类型 | 必须 | 最大长度 | 说明 | 示例值 ---- | --- | --- | --- | --- | --- -replacement | string | 否 | 1 | 对命中关键词进行替换的符号,默认值"*" | * - -### 响应结果 - -#### 检测结果数据结构 - -参数 | 类型 | 必须 | 说明 | 示例值 ---- | --- | --- | --- | --- -request_id | string | 是 | 请求唯一标识,后续可用于查询数据 | c1i1955tqehouf41di20 -result | int | 是 | 返回检测结果
    0: 通过
    1: 拒绝
    2: 复核 | 1 -type | string | 是 | 返回识别类型:
    健康: Health
    广告:Adv
    政治:Politics
    辱骂:Abuse
    犯罪:Crime
    邪教:Heresy
    恐怖主义:Terrorism
    色情:Porn
    赌博:Gamble
    违禁品:Contraband
    突发敏感事件:SensitiveEvent
    违规网站:IllegalWebsite
    | Politics -filtered_text | string | 否 | 通过命中关键词返回过滤后的文本内容(text) | 小明你好*,*就要多读书 -hint | object | 是 | 检测线索 | [线索数据结构](#线索数据结构) - -> **提示:filtered_text 字段可能为空串,响应结果为拒绝且 filtered_text 为空代表该内容需要屏蔽。** - -#### 线索数据结构 -参数 | 类型 | 必须 | 说明 | 示例值 ---- | --- | --- | --- | --- -hit_words | array[object]| 是 | 命中的关键词 | [命中词数据结构](#命中词数据结构) - -#### 命中词数据结构 -参数 | 类型 | 必须 | 说明 | 示例值 ---- | --- | --- | --- | --- -word | string | 是 | 关键词 | xxx -type | string | 是 | 识别类型(同上)| Adv -positions | object | 是 | 关键词位置信息 | [关键词位置结构](#关键词位置结构) - -#### 关键词位置结构 -参数 | 类型 | 必须 | 说明 | 示例值 ---- | --- | --- | --- | --- -start_index | int64 | 是 | 起始下标(从 0 开始)| 0 -end_index | int64 | 是 | 结束下标 | 3 - -### 请求示例 -``` -curl --location --request POST 'https://whisper.cn.tapapis.com/v2/text/check' \ ---header 'Content-Type: application/json' \ ---header 'X-Client-ID: *' \ ---header 'X-Server-Secret: *' \ ---data-raw '{ - "data": { - "data_id": "49a12d8d-7dc1-41cb-968f-31ddbd2ab3f2", - "text": "你好,欢迎使用文本检测", - "ip": "223.161.60.210", - "user_id": "f2e9172e5172", - "nickname": "小明" - }, - "scene": "c1eqbi0e3piuseb840fg", - "opt": { - "replacement": "*" - } -}' -``` - -### 响应示例 -拒绝情况示例 -```json -{ - "result": 1, - "type": "Politics", - "request_id": "c1i1955tqehouf41di20", - "filtered_text": "小明你好*,*就要多读书", - "hint": { - "hit_words": [ - { - "word": "xxx", - "type": "Adv", - "positions": { - "start_index": 4, - "end_index": 7 - } - }, - { - "word": "ooo", - "type": "Politics", - "positions": { - "start_index": 8, - "end_index": 11 - } - } - ] - } -} -``` - -健康情况示例 -```json -{ - "result": 0, - "type": "Health", - "request_id": "c1i1asltqehouf41di2g", - "filtered_text": "", - "hint": { - "hit_words": [] - } -} -``` - -## API 错误响应 -当 HTTP 返回码不等于 200 时,即遇到了错误情况将返回以下信息。 - -参数名称 | 类型 | 必须 | 描述 ----|---|---|--- -code | int | 是 | 接口错误码,返回码见 (https://developers.google.com/maps-booking/reference/grpc-api/status_codes) -message | string | 是 | 错误消息 -details | object | 否 | 详情信息,具体结构根据不同 code 表示 - -```json -// 响应 body 为 json 格式 -{ - "code": 5, - "message": "Text checker is not exists for 'c1exxxxe3pi1useb840fg' scene", - "details": [] -} -``` diff --git a/docs/store/buyout/_category_.json b/docs/store/buyout/_category_.json deleted file mode 100644 index 7199c0c1f..000000000 --- a/docs/store/buyout/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "买断制游戏", - "collapsed": true, - "position": 6.5 -} diff --git a/docs/store/buyout/copyright-verification.mdx b/docs/store/buyout/copyright-verification.mdx deleted file mode 100644 index 8408faeac..000000000 --- a/docs/store/buyout/copyright-verification.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: 买断制游戏介绍 -sidebar_position: 1 ---- - - -## **什么是买断制游戏?** - -买断制游戏是指用户在一次性购买游戏付费项如付费下载/游戏完整版后,即可完整体验游戏内容,买断制游戏无后续的持续付费项。 - -## **什么是 DLC 商品** - -DLC 是指游戏发行后额外提供的下载内容,通常包含新的游戏内容、地图、章节、角色等。DLC 可以扩展游戏的玩法、增加游戏内容,或者提供额外的故事情节和挑战。 - -TapTap 现支持买断制游戏的开发者设置游戏 DLC 商品: -开发者可以通过创设 DLC 商品销售游戏的额外付费内容,且 DLC 销售额将被记入热卖榜。 - -平台推荐的使用 DLC 商品来提供付费内容的两种商品类型包含: -- 完整版:用户需要解锁完整版完整体验游戏内容。此类商品在游戏详情页的下载内容推荐为游戏免费试玩demo。 -- DLC 版:是否解锁扩展包不会对用户的基本游戏体验带来影响。用户可选购此类商品以解锁游戏附加内容,例如新增地图/新增关卡/特殊玩法等。 -详细 DLC 商品功能,请查看 [DLC 商品](/store/buyout/dlc-products),商品创建后请前往 [开发指南](/sdk/copyright-verification/guide/) 完成开发接入。 -用户可以通过购买买断制游戏来获得完整的游戏体验,通过购买 DLC 来扩展买断制游戏游戏的内容和玩法。 \ No newline at end of file diff --git a/docs/store/buyout/discounts-coupons.mdx b/docs/store/buyout/discounts-coupons.mdx deleted file mode 100644 index 1bd2e4386..000000000 --- a/docs/store/buyout/discounts-coupons.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: 折扣与优惠券 -sidebar_position: 3 ---- - - -## **1. 买断制游戏的折扣** - -### **1.1 概览** - -- TapTap 支持游戏开发者对付费游戏的各类游戏商品设置折扣; -- 有效的折扣策略能够扩大游戏的受众群体并使您的游戏在 TapTap 平台上取得长期成功; -- 通过设置折扣,您不仅可以推出个性化的促销活动,还可以参与官方周期性举办的特卖活动。 - -### **1.2 配置步骤** - -付费下载 -- 开发者后台 >> 商店 >> 买断制游戏 >> 付费下载设置,开启付费下载折扣并设置折扣信息后提交审核; -- 审核通过后,您创建的付费下载折扣价格将在折扣期间生效。 - -DLC 商品 -- 您可以前往 **商店 >> 买断制游戏 >> 折扣与优惠券 >> 创建折扣**,选择希望打折的游戏商品进行折扣设置。 - - - -## **2. 买断制游戏的优惠券** - -### **2.1 概览** - -- TapTap 支持游戏开发者对付费游戏及其 DLC 商品生成专享优惠券,专享优惠券将支持与平台折扣同享; -- 开发者可以通过创设该付费游戏下不同使用范围的优惠券,将优惠券作为一类运营活动奖品类型向参与活动用户或是指定用户发放。 - -### **2.2 配置步骤(自行配置)** - -- 您可以前往商店 >> 买断制游戏 >> 折扣与优惠券 >> 创建优惠券,完成优惠券表单配置后生成一批优惠券。 -- 请检查您的优惠券表单配置内容,优惠券开始发放后将不支持修改任何优惠券表单内容,如遇到了优惠券表单配置错误的问题,您可以选择将该条优惠券“暂停发放”,但是平台不建议这样做。 -- 优惠券类型(详见 2.3 优惠券类型)一经选定不支持修改。 - -:::tip -- 每条您创建的优惠券链接活动,每位用户限领一次; -- 优惠券仅限获得优惠券的平台账号使用,用户获得优惠券后无法转让或“赠送好友”使用; -- 对于买断制游戏,同一个账号无法重复购买。如果领券用户已经购买了所有适用该优惠券的商品,将会导致领取的优惠券无法使用。 -::: - -### **2.3 优惠券类型** - -TapTap 平台的开发者后台目前支持支持两种优惠券类型: -- 先到先得链接活动; -- 兑换码。 - -先到先得链接活动 -- 平台将为您创建一条先到先得优惠券活动的链接,您可以将链接分享至论坛或粉丝群发起优惠券活动; -- 链接活动的优惠券将直接发放至用户的平台账号; -- 链接活动类型的优惠券创建后,您可以随时补充优惠券库存。 - -兑换码 -- 平台将为您生成一批优惠券兑换码,您可以将兑换码分享给指定用户,或是将兑换码作为活动奖品举行平台活动,如论坛抽奖/签到; -- 用户获得兑换码后需在平台兑换后使用,请注意同个 TapTap 账号仅支持兑换 1 次您创建的本条优惠券; -- 兑换码优惠券创建后不支持补充库存。 - -### **2.4 优惠券对账** - -目前平台支持两种优惠券:游戏专享券,平台通用券 -- 游戏专享券:上述说明中,您可以在开发者后台自行配置的优惠券为“游戏专享券”,这类优惠券属于您自行发起的优惠活动。对账单不会计入这部分优惠券抵扣的金额,对账单内的“付款金额”为平台收到的用户实际付款金额; -- 平台优惠券:TapTap 平台将周期性举办买断制游戏活动,在此期间平台将发放一批无门槛且全平台买断制游戏通用的优惠券。这部分优惠券的实际抵扣金额将作为平台补贴计入对账单内的“活动金额”。注意:如这批优惠券订单发生跨月度退款,活动金额将扣除上月平台已补贴的优惠券金额,因此您的活动金额可能出现负数。 \ No newline at end of file diff --git a/docs/store/buyout/dlc-products.mdx b/docs/store/buyout/dlc-products.mdx deleted file mode 100644 index 95e90d6d1..000000000 --- a/docs/store/buyout/dlc-products.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: DLC 商品 -sidebar_position: 2 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -- TapTap 现支持游戏开发者设置 DLC 商品。 -- 开发者可以通过创设DLC商品销售游戏的额外付费内容,且DLC销售额将被记入热卖榜。 - -## **如何创建 DLC 商品** - -- 在商店 >> 付费下载 >> DLC商品设置 >> 新建商品,使用配置表单创建一件 DLC 商品。 -- DLC 商品创建完成后生成的商品 id 即为商品 skuid,商品创建后可前往 [开发指南](/sdk/copyright-verification/guide/) 继续完成 DLC 商品的开发接入。 -- 创建的 DLC 商品通过平台审核后,即可在指定时间上线。 - - -## ** DLC 商品的信息填写建议** - -其中 \* 标注为必须提交的资料 - -### 1. 商品封面 \* 必填 -- 用途:游戏详情页、商品详情页、用户购买游戏时的购物抽屉将使用此图标展示 DLC 商品信息 -- 尺寸:1920x1080px -- 格式:png/jpg -- 要求:建议使用 DLC 商品的主要场景/内容/角色为主要内容。请确保素材及 Logo 清晰可见,且除游戏标题外,勿引用或使用其他文本。 - -### 2. 商品类型 \* 必填 - -商品类型包括完整版以及 DLC 版,不同的商品类型在用户侧将有不同的展示标识。 - -- 完整版:用户需要解锁完整版获得完整游戏体验。此类商品的游戏详情页通常为免费试玩 demo。 -- DLC 版:DLC 版的解锁不影响用户基本游戏体验,解锁后支持体验如新地图/关卡/玩法/新角色等游戏内容。 - -:::tip -- 平台不支持以完整版或 DLC 版的商品形式进行消耗类道具售卖,任何因道具类型 DLC 商品产生的纠纷(如退款用户的道具扣除问题),平台不予处理。如果有特殊商品售卖需要请联系平台运营确认后单独处理。 -::: - -### 3. 商品价格 \* 必填 - -- 内容:请提供整数定价,且商品价格不得为 0 元。 -- 运营建议:平台站内买断制游戏的定价范围通常在 1 元至 48 元之间,其中常见的定价有 1 元、6 元、8 元、12 元、18 元、36 元和 48 元,数据表明平台用户对于价格在 6 元至 18 元的买断制游戏定价接受度更高,平台建议综合考虑游戏品质进行定价。 - -### 4. 商品名称 \* 必填 -- 内容:请提供一个 14 个字以内的商品名称,名称不允许使用 emoji 表情。 -- 要求:完整版商品名称可以为游戏名称,DLC 版商品名称不能包含游戏名称。 -- 运营建议:为了用户体验,不建议创建名称相同的多件商品。 - -### 5. 商品卖点 \* 必填 -- 内容:请提供一个 10 个字以内的商品卖点,不允许使用 emoji 表情。 -- 运营建议:商品卖点建议不包含游戏名称,请简洁明介绍商品对用户最有吸引力的部分,如:全新的可玩角色,新章节解锁。 - -:::tip -- 为避免影响用户端展示效果,商品卖点请勿与游戏名称雷同。 -::: - -### 6. 付费内容介绍 \* 必填 - -- 内容:请提供 100 字以内的商品详细介绍,内容名称不允许使用 emoji 表情。 -- 运营建议:建议将主要内容控制在前 50 个字,有利于加深用户对游戏的第一印象。 \ No newline at end of file diff --git a/docs/store/events/_category_.json b/docs/store/events/_category_.json deleted file mode 100644 index 57583481f..000000000 --- a/docs/store/events/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "正式上线期", - "collapsed": true, - "position": 6 -} diff --git a/docs/store/events/events-handbook.mdx b/docs/store/events/events-handbook.mdx deleted file mode 100644 index 7234bff27..000000000 --- a/docs/store/events/events-handbook.mdx +++ /dev/null @@ -1,431 +0,0 @@ ---- -title: 首发上线期运营手册 -sidebar_position: 1 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -游戏首发上线期期间,首发游戏还将再次自动获得平台首页推荐流量扶持,具体流量倾斜情况依据游戏的预约/下载量变化(预约/下载量越高,给予的曝光扶持越多)。同时,开发者还可以根据指南,利用社区模块发布社区活动,获得更多流量。 - -## 一、 首发上线期如何获得免费资源位 -### 1. 今日游戏(自行配置) -为了尽可能让所有好游戏都能够获得足够的曝光,TapTap 在首页新增了「今日游戏」栏目以替代之前的「即将上线」。今日游戏能够承载游戏生命周期里的首曝、测试、首发、更新、折扣等各个关键节点的游戏曝光需求,信息更加全面,与用户的关联性更强。开发者仅需要按规范操作即可自动获得「今日游戏」栏目的基础曝光。 - -更多详细信息与申请指南:[如何获得「今日游戏」栏目的曝光](https://developer.taptap.cn/docs/store/operations-skills/today/) - - - - -### 2. 首页推荐扶持(算法分发) -首次开放预约以及首发(预约➡️下载)的游戏,算法会自动帮助进行首页推荐曝光流量倾斜 -- 申请方式:无需申请,机器自动配置 -- 扶持逻辑:曝光倾斜程度依据游戏的曝光转化率动态调整,相同曝光下,预约/下载的人数越多,算法给予的流量倾斜越多(整个过程算法自动动态调整) -- 扶持时长:整个扶持流程持续 3 天,3 天后进入自然推荐流程,不再做额外倾斜 -- 注意:冷启动阶段的流量倾斜仅在首次开放预约和首发各进行一次,版本更新或重复发布无法再次参与 - - - -:::info Tips - -详情页的配置丰富度会影响游戏最终的曝光转化率,如何配置优质详情页请见:[详情页优化建议](https://developer.taptap.cn/docs/store/operations-skills/suggestions) - -::: - - - -### 3. 官方 UGC 征集活动:新游安利局(需申请) - -TapTap 为鼓励站内内容创作者为近期上线/大更新的游戏创作内容,增加曝光,定期举办 新游安利局 活动,为内容创作者提供现金奖励。该活动有固定的选游范围,厂商可申请参加。 -- 活动举办周期:每 15 个自然日举办一次(实际举办时间可能会根据实际站内游戏上线情况动态调整) -- 游戏筛选周期:近一个月上线或进行大版本更新的游戏 -- 申请时机:请在当期活动开始前至少 7 个工作日内发起申请,且游戏上线时间须在当期活动的游戏筛选周期内 -- 审核标准:编辑根据游戏质量和站内预约量级综合评定 -- 申请方式:请与对接运营/商务沟通发起申请 -- 案例展示:[新游安利局 Vol.4 0531-0620](https://www.taptap.cn/activity/135) -![](https://capacity-files.lcfile.com/V85p1fsCg47XTkVYI2s6TJlV86zFmOIn/%E6%96%B0%E6%B8%B8%E5%AE%89%E5%88%A9%E5%B1%80.png) - - -## 二、 如何更好地运营游戏并与 TapTap 运营合作——首发上线期 - -### 1. 基于 TapREP 与 TapTap 的运营合作 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    资源置换形式置换说明重点置换形式示例创建指南价值计算方式预期可获资源价值
    游戏内跳转 -

    安卓官包+iOS官包,游戏内资源位跳转TapTap对应活动页面

    -
    -

    拍脸图

    -

    跳转到TapTap链接

    -

    至少放置3天

    -

    用户每日登录时可见

    -

    覆盖安卓官包+iOS官包

    -
    -

    -
    -

    TapREP:创建效果资源

    -
    -

    按实际拉新拉活效果次日结算

    -

    大盘单个拉新用户价值100-300元

    -

    大盘单个拉活用户价值10-30元

    -
    -

    按覆盖用户量350万,覆盖7天预估

    -

    预估价值50万

    -

    大盘单个拉新用户价值100-300元

    -

    大盘单个拉活用户价值10-30元

    -
    -

    公告/邮件/站内信

    -

    跳转到TapTap链接

    -

    至少放置3天

    -

    覆盖安卓官包+iOS官包

    -
    -

    -
    -

    首页入口

    -

    跳转到TapTap链接

    -

    需覆盖整体活动周期

    -

    覆盖安卓官包+iOS官包

    -
    -

    -
    品牌标记内容媒体(微博/微信/b站/小红书/抖音/快手)宣发时文案/内容带TapTap---官号/KOL合作均可 -

    品牌标记示例

    -

    bilbili:文案带TapTap示例

    -

    抖音:视频内容带TapTap示例

    -

    快手:文案带TapTap示例

    -

    微博:文案带TapTap示例

    -

    小红书:文案带TapTap示例

    -

    直播:

    -

    -
    -

    TapREP:创建品牌资源

    -
    -

    以实际互动量结算,互动量定义为转评赞投币收藏等各个平台的互动行为

    -
      -
    • 抖音/快手/小红书/微博 统计自上传且审核通过之日起7天的增量互动数据
    • -
    • bilibili 统计自上传且审核通过之日起30天的增量互动数据
    • -
    -
    -

    根据实际转化结算:

    -

    目前数据平均一万赞视频价值在3500元左右(仅预估数据),结算实际还是以所有互动行为为结算依据

    -

    不同热度的视频价值示例:

    - -
    官网置换+品牌专区 -

    官网+品牌专区主入口挂TapTap游戏详情页+媒体渠道挂TapTap论坛首页

    -
    -

    渠道增加TapTap专区,引导至论坛

    -
    -

    -
    -

    step1: TapREP:创建效果资源

    -

    step2: 在更新完效果链接后,在TapREP后台上传品牌挂架

    -
    -

    一次性资源奖励+持续拉新拉活价值收益

    -

    一次性资源奖励阶梯奖励,单次最高收益1万元,根据放的位置不同,收益不同

    -

    -

    实际拉新拉活效果次日结算

    -

    大盘单个拉新用户价值100-300元

    -

    大盘单个拉活用户价值10-30元

    -
    -

    一次性资源奖励1000~10000元

    -


    -

    效果根据实际转化结算:

    -

    预约期游戏,站内预约超50万的游戏,首月预估资源价值20万,结算实际依赖官网带转化效果

    -
    -

    主入口加上TapTap下载

    -
    -

    -
    自媒体引流 -

    媒体通稿+微博、微信等自媒体带TapTap详情页效果链接

    -
    -

    媒体通稿带tap预约链接

    -
    -

    -
    -

    TapREP:创建效果资源

    -
    -

    按实际拉新拉活效果次日结算

    -

    大盘单个拉新用户价值100-300元

    -

    大盘单个拉活用户价值10-30元

    -
    -

    效果根据实际转化结算:

    -

    预约期游戏,站内预约超50万的游戏,首月预估资源价值5万,结算实际依赖各个资源位带转化效果

    -
    -

    微博/微信等内容带tap预约链接

    -
    -

    -
    -

    微信公众号阅读原文跳转tap预约链接

    -
    -

    -
    -

    微信公众号导航栏跳转tap预约链接

    -
    -

    -
    运营资源 -

    在TapTap站内开启论坛活动和签到活动

    -
    -
      -
    1. 签到活动示例
    2. -
    3. 论坛活动示例
    4. -
    -

    - 抽奖活动 - {" "} - 回复活动 - {" "} - 话题活动 -

    -
    -

    TapREP:创建运营资源

    -
    -

    平台将根据活动曝光、参与情况折算价值,并根据论坛dau这一核心指标,作为加权和降权,形成最终价值。

    -

    对厂商侧来说:

    -
      -
    • 帖子/活动的曝光越高,拿到的价值越多
    • -
    • 帖子/活动的互动越多,拿到的价值越多
    • -
    • 论坛dau越高,拿到的价值越多
    • -
    -
    -
      -
    • 单次签到活动价值在1000~1.8W不等,由论坛DAU和具体活动数据决定
    • -
    • 单次论坛活动价值在1250~7500不等,由论坛DAU和具体活动数据决定
    • -
    • 每月可做多次签到和论坛活动
    • -
    -
    线下合作 -

    线下和TapTap联办漫展/市集/快闪等

    -
    -

    - - -

    -
    -

    需特殊申请开白,REP资源置换后台提工单,联系对接商务或发送邮件到tapop@xd.com发起申请

    -
    -
      -
    • 根据人流量和展台露出程度给到固定金额的品牌价值结算
    • -
    • 根据实际转化次日结算
    • -
    -
    -
      -
    • 按CP29人流量,高露出核心展台预估,预估价值5万
    • -
    -
    素材授权 -

    提供游戏素材授权给taptap,允许在站外推广时使用游戏相关素材,一次性给到1000元流量金

    -
    -

    计算机软著

    -
    -

    -
    -

    TapREP:上传素材授权

    -
    -

    单次素材授权,审核通过后,发放1000元流量包

    -
    对易玩的授权书
    -
    - - - - -:::info 注意 - - -表格中所提到的“元”为等价流量包 - -::: - - - - -:::info **推荐** - -特别地,对于 **IOS 渠道** 的资源露出跳转,目前在TapTap能够置换到的流量价值更高,并且可以直接用于TapTap安卓端的站内投放: - -- TapTap 目前对于 IOS 端的资源置换合作所带来的用户设定的单价更高,单个用户价值为**100-300元**左右 - -- TapTap 对于 IOS 端的效果资源除了基础的流量包奖励外,还将根据游戏品质和热度(需经编辑审核),给予 TapTap IOS 端**开屏**和**首页置顶**的专属资源倾斜 - -::: - - - -### 2. 论坛配置及优秀实践分享 -TapTap 除了作为商店为厂商吸引玩家外,还是一个拥有巨大用户群的玩家社区。它允许玩家在其中讨论,分享游戏相关内容,并帮助开发者与玩家直接进行沟通。因此定期的论坛活动有助于提高玩家对于游戏的忠诚度,并帮助开发者及时收获真实反馈。 - -- 社区发帖操作:[学习社区模块,掌握基本操作](https://developer.taptap.cn/docs/community/features/) - -- 社区活动设计指南:[挑战进阶操作,玩转社区运营](https://developer.taptap.cn/docs/community/advanced/) - -- 论坛公告置顶:[学习社区模块,掌握基本操作 2.1-论坛置顶](https://developer.taptap.cn/docs/community/features/) - -- 优秀案例分享: - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    活动类型案例展示操作方式注意事项
    -

    意见收集

    -
    -

    【深度冻结】bug反馈&建议征集专用帖!

    - -
    -

    论坛后台发帖

    -
    -

    -
    -

    社区互动

    -
    -

    【有奖活动】星穹会客厅 | 景元:运筹帷幄的云骑将军

    - -
    -

    论坛后台发帖

    -
    -

    -
    -

    评论抽奖

    -
    -

    【内含周边抽奖】《深空之眼》太一·庚辰角色PV「夙夜长愿」

    - -
    -

    论坛后台发帖

    -
      -
    • TapTap社区内的评论抽奖活动支持自动化组件,只需在发帖时,在发布设置中选择【添加评论抽奖活动】并按照指引填写相关信息即可。目前支持多种奖品设置(实物/兑换码),定时自动开奖,自动去除bot等功能
    • -
    -
    -
      -
    1. 活动发起方务必在帖子内清楚、准确描述活动规则、活动奖品、开奖时间。
    2. -
    3. 中奖用户需要在 7 个自然日内完成收件信息填写,活动发起方请务必在开奖后 30 个自然日内完成奖品发放。
    4. -
    5. 开奖时将自动过滤机器人用户。
    6. -
    7. 使用抽奖功能组建需要开通版主运营权限
    8. -
    -
    -
    diff --git a/docs/store/integration/_category_.json b/docs/store/integration/_category_.json deleted file mode 100644 index bacdd4921..000000000 --- a/docs/store/integration/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "与 TapTap 深度结合", - "collapsed": true, - "position": 17 -} diff --git a/docs/store/integration/cloud-gaming.mdx b/docs/store/integration/cloud-gaming.mdx deleted file mode 100644 index 3b0acc881..000000000 --- a/docs/store/integration/cloud-gaming.mdx +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: TapTap 云玩功能介绍 -sidebar_position: 1 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -## 1. 云玩是什么 - -云玩是指通过实时互动云技术让更多用户能够免下载免安装、无视配置,实现即点即玩的云游戏体验。 - -## 2. 产品形态 - -### 2.1 线上入口 - -![gameadmin](/img/云玩线上入口.png) - -### 2.2 云玩流程 - -2.2.1 点击「在线玩」开始游戏加载 - -![gameadmin](/img/云玩流程1.jpeg) - -2.2.2 进入登录界面开始游戏 - -![gameadmin](/img/云玩流程2.jpeg) - -2.2.3 左侧悬浮窗提供「网络状态」、「画质」等信息与调节功能 - -![gameadmin](/img/云玩流程3.jpeg) - -## 3. 云玩核心优势 - -### 3.1 全链路生态闭环 - -将云业务找用户-用户转化-消费内容-社区生态链路跑通,实现业务闭环。 - -### 3.2 无缝云游戏原生体验 - -超低延迟高画质,保障流畅体验; - -全方位接近原生游戏体验,粘贴板、支付等功能流畅使用,避免玩家的跳出感。 - -### 3.3 资源调度定制化 - -放开云业务资源调度权限,深度定制以厂商目标进行资源管控。 - -### 3.4 释放上云压力,分摊业务成本 - -标准对接流程,解放上云压力,开发者可以把重心投入在产出好内容中; - -针对开发者特性以及个性化需求进行深度定制。 - -## 4. 场景应用 - -### 4.1 业务场景 - -4.1.1 游戏使用门槛:设备性能 / 存储要求 -4.1.2 游戏游玩体验:大版本更新;多端游戏体验;游戏内测 / 封测 / DEMO体验 - -### 4.2 用户场景 - -4.2.1 能玩:3A大作游戏免下载安装体验 -4.2.2 轻松玩:点击即玩,无视配置 -4.2.3 原生体验:画质可选,付费还原本机 - -## 5. 云玩接入 - -### 5.1 云支持 - -5.1.1 X86架构—VOS容器方案 -此方案适用于 PC 游戏以及端游。在 X86 架构的服务器或者虚拟系统内运行容器,容器本身占用资源较低,资源消耗即游戏本身消耗,所有游戏都将动态,使用服务器资源。游戏启动速度快,提高服务器资源使用率,能够支持 GPU 密集型的云游戏运行稳定且确保资源的合理分配,提高编码效率,降低网络负载,从而保证低延迟,好体验。 - -5.1.2 ARM架构—板卡方案 -此方案适用于移动端游戏。基于高通 IOT 旗舰芯片的 ARM 阵列服务器,服务器基于 CPU+GPU 的 SoC 阵列结构,在多路并行 GPU 渲染方面具备很大的性能优势。 - -### 5.2 业务上云 - -5.2.1 游戏授权适配 -提前3天提供绿色免 launcher 的游戏包体进行全链路验收测试。 - -5.2.2 风控规避 -针对云游戏固定 ip 的情况,适当的调整游戏的风控策略避免影响云游戏体验。 - -### 5.3 云支付 - -5.3.1 SDK接入 -提供全终端 SDK 接入,降低本身研发投入的同时,实现平台功能的快速对齐,对接当天可测试。 - -5.3.2 独家H5支付 -云游戏独家 H5 支付解决方案,不与平台绑定,支付流程基本还原本地状态。 - -### 5.4 安全策略 - -提供终端root识别、防脚本、防外挂能力,可根据游戏需求自主开启。 - -## 6. 云玩案例 - -![gameadmin](/img/云玩案例.png) - -## 7. 如何申请云玩 - -开发者可以根据游戏性能要求以及云玩契合程度于开发者后台进行云玩授权;因服务器成本无法支持全部游戏开通云玩,我们将进行游戏选品后适配验收最终上架。 - -## 8. 常见问题 - -### 8.1 云适配 - -全链路验收测试后将会提供测试报告,如遇黑屏、白屏、闪退等异常问题,需要开发者自测处理。 - -### 8.2 游戏登录 - -目前云玩无法通过微信登录进入游戏,如游戏内仅有微信登录一种登录方式,建议接入 Tap 登录或手机登录。 - -### 8.3 云设备性能 - -根据全链路测试结果进行云设备性能评估进行线上配置。 - -### 8.4 云支付 - -支持自主打开/关闭,具体接入请参阅 5.3 云支付 。 \ No newline at end of file diff --git a/docs/store/integration/tap-play.mdx b/docs/store/integration/tap-play.mdx deleted file mode 100644 index 29338668e..000000000 --- a/docs/store/integration/tap-play.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: 使用 TapPlay 提高游戏分发的转化率 -sidebar_position: 3 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -TapPlay 是利用沙盒技术实现的,可以让用户直接通过 TapTap 玩游戏的功能服务,旨在帮助开发者实现低成本、高效率的游戏开发,并提高游戏分发的转化率。 - -与传统方式相比,TapPlay 具备以下核心优势: - - - 算法自动倾斜更多流量,首页专题额外曝光; - - 免费市场 Top 300 主流机型自动化稳定性测试,发现更多兼容适配问题; - - 防抓包、反破解; - - 无需开发者修改代码,**零成本**接入; - - 无需开发游戏的防沉迷系统,TapPlay 已**集成了完整合规的青少年防沉迷机制**; - - 提升游戏体验,玩家可直接利用在 TapTap 的实名信息进入游戏; - - 游戏首次启动时,无需重新进行繁琐的权限申请,玩家可快速进入游戏; - - 提升游戏的安装成功率,有效提高游戏在 TapTap 上的下载**转化率**; - - 集成准确有效的数据监控工具,实时有效地监控线上游戏运营及用户反馈; - - 玩家无法主动获取游戏的 APK 安装包,游戏文件**安全系数高**,不会被反编译,不会被恶意传播。 - ---- - -如需了解更多关于 TapPlay 的信息,请移步官方文档:[TapTap 开发者文档 - TapPlay](https://developer.taptap.cn/docs/sdk/tap-play/features/) \ No newline at end of file diff --git a/docs/store/integration/tapad.mdx b/docs/store/integration/tapad.mdx deleted file mode 100644 index 78f4cc98a..000000000 --- a/docs/store/integration/tapad.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: 利用 TapAD 进行广告投放,获得精准流量 -sidebar_position: 5 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -![gameadmin](/img/TapAD.png) - -TapAD 是 TapTap 站内推广投放平台,游戏管理者通过 TapAD 创建、优化、管理推广效果,实现推广投放撬动全站价值。大数据算法会把推广内容展现给更感兴趣的用户,帮助不同类型的游戏找到匹配的玩家群体,提升投放效率。 - -未来 TapAD 将会在更多原生位置开拓推广位,可选择的投放分端也会逐步完善。 - -已开放推广场景: - - - TapTap 客户端:首页信息流、搜索、标签页、发现页专题 - - TapTap Web 端:首页信息流、搜索 - - --- - -如需了解更多关于 TapAD 的信息,请移步以下内容: - -TapAD 官网:[TapTap 推广中心](https://biz.taptap.com/) -TapAD 文档:[TapTap 投放平台文档中心 - 关于 TapAD](https://biz.taptap.com/docs/tapad-about) \ No newline at end of file diff --git a/docs/store/integration/taprep.mdx b/docs/store/integration/taprep.mdx deleted file mode 100644 index cc4c7a8a3..000000000 --- a/docs/store/integration/taprep.mdx +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: 利用 TapREP 平台免费获得流量 -sidebar_position: 2 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -![gameadmin](/img/TapREP.png) - -在 TapTap 和游戏厂商之间,存在大量以资源置换为条件的合作。 - -为降低合作过程中的沟通成本,提升置换效率,公平化透明化地实现资源置换,TapREP 平台将提供全流程服务,公开可置换资源,提供线上置换路径,以达成货币化的资源置换。 - ---- - -如需了解更多关于 TapREP 的信息,请移步以下内容: - -TapREP 官网:[TapTap 资源置换平台](https://rep.taptap.cn/) -TapREP 文档:[TapTap 资源置换文档中心](https://rep.taptap.cn/docs/taprep-about) \ No newline at end of file diff --git a/docs/store/integration/tds.mdx b/docs/store/integration/tds.mdx deleted file mode 100644 index 3dfd83675..000000000 --- a/docs/store/integration/tds.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: 利用 TDS 服务降低游戏研运成本 -sidebar_position: 4 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -![gameadmin](/img/TDS.png) - -TapTap 开发者服务(TapTap Developer Services,简称 TDS 服务)旨在帮助开发者降低游戏研发、运营维护等阶段投入的精力和成本。 - -TDS 整合了各项服务,让开发者能够聚焦在游戏核心乐趣的创造上,创作更优秀的游戏,进而促进游戏行业生态的良性循环,最终让开发者与玩家双双受益。 - ---- - -如需了解更多关于 TDS 服务的信息,请移步官方文档:[TapTap 开发者文档 - 概览](https://developer.taptap.cn/docs/sdk/) \ No newline at end of file diff --git a/docs/store/long-termops/_category_.json b/docs/store/long-termops/_category_.json deleted file mode 100644 index 33fbfb37d..000000000 --- a/docs/store/long-termops/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "长线运营期", - "collapsed": true, - "position": 7 -} diff --git a/docs/store/long-termops/long-term.mdx b/docs/store/long-termops/long-term.mdx deleted file mode 100644 index 5cf90ef28..000000000 --- a/docs/store/long-termops/long-term.mdx +++ /dev/null @@ -1,477 +0,0 @@ ---- -title: 长线运营期运营手册 -sidebar_position: 1 ---- -import {Red, Blue, Black, Gray, Popup} from '/src/docComponents/doc'; - -游戏正式上线后,厂商可以根据自己的运营节奏,上线日常运营活动以拉新促活(论坛活动,签到活动,礼包发放等);每项活动每次可申请一次活动中心曝光,活动中心页面总体每日可获得约 6 万次曝光,各板块曝光情况不同,最终实际曝光情况与游戏热度强相关。如遇重大版本更新,站内支持申请新版本详情页模块,以单独的 SKU 形式在站内分发。 - -## 一、 长线运营期如何获得免费资源位 -### 1. 每日签到(自行配置) -每日签到允许厂商可通过签到送奖励的方式,促进用户活跃、新增以及回流,一定程度上延长用户生命周期。根据我们的测试与数据回收,签到活动能极大的提高游戏论坛的活跃人数与用户留存。 -- 签到页曝光量:TapTap 站内每天有平均超过 **15 万**的用户是签到活动的活跃用户 -- 申请频次:无正在进行中的签到活动即可立即申请;有正在进行中的签到活动需等待当前活动结束后,方可继续申请 -- 审核周期:所有活动申请,自提交审核成功后 2 个工作日内处理,请及时查看审核状态 -- 审核标准:对于明确抄袭的游戏不予通过;每档位奖励数量 < 1000,不予通过 -其余无要求,但过往经验及数据显示,奖品价值越高,用户参与度越高,活动效果越好。 -- 福利中心签到活动展示逻辑:福利中心的签到活动依据审核通过时间先后排序,最新通过的活动排位最靠前 -- 其他相关信息&申请方式:[游戏每日签到 | TapTap 开发者文档](https://developer.taptap.cn/docs/store/operations-skills/game-sign/) -- 资源位位置: - -
    - - - - - - - - - - - - - - - - - - - -
    我的游戏活动中心论坛推荐位游戏签到页面效果说明
    -
      -
    • 我的游戏、活动中心-新的签到福利上线了为系统自动配置
    • -
    • 论坛推荐位请按需自行配置
    • -
    -
    -
    - -### 2. 活动中心(需申请) -活动中心可以帮助玩家一站式获取游戏招募、礼包、签到、社区活动等福利相关信息,促进用户下载及回流,延长其生命周期。**签到活动,论坛活动以及日常普发礼包都可以申请在此位置曝光。** - -- **活动中心曝光量:** 整个页面每日约** 6 万次**曝光(实际曝光情况与游戏热度与页面位置相关) - -- **申请频次:** 每项活动每次可申请一次 - -- **审核周期:** 所有活动申请,自提交审核成功后 2 个工作日内处理,请及时查看审核状态 - -- **审核标准:** - -> a. 对于评分低于 5 分(不含 5 分)的游戏,不做通过 -> -> b. 对于预约数< 15 万,月下载/月更新均< 10 万的游戏,不做通过 -> -> c. 对于存在争议、用户口碑极差的游戏,不做通过 -> -> d. 对于有强烈舆论问题的游戏,舆论期内不做通过 -> -> e. 纯游戏内活动、引导评价式活动、活动形式不在 TapTap 内落地的活动不做通过 -> -> f. 纯实物奖励活动,总获奖人数低于 10 人不做通过 -> -> g. 一等奖总价值低于 200 元,总价值低于 500 元的活动,不做通过 - -- **活动中心资源展示逻辑:** 签到位与礼包位按审核通过时间先后排序;banner 头图与热门活动由编辑审核,按活动质量高低排序 - -- **隐藏扶持:** 高质量活动将获得 banner 头图,push,首页推荐等资源扶持 - -- **其他相关信息&申请方式:** [上“活动中心”栏目 | TapTap 开发者文档](https://developer.taptap.cn/docs/store/operations-skills/event-center/) - -### 3. 攻略 tab 配置(自行配置) -**攻略 tab 允许三类人进行配置:** -- 游戏开发者 -- 论坛版主(被允许了攻略配置权限的) -- 论坛超管 - -**目前攻略tab支持配置三类组件:** -- 工具箱:主要用以放置游戏工具,支持实体、帖子、外部链接 -- 信息板:用以承载热点内容,支持实体、帖子 -- 索引块:用以承载 wiki 等结构化组织需求,支持实体、帖子 - -**攻略配置指南:** -[攻略配置教程](https://www.taptap.cn/doc/guide/how-to-use-guide.html) - -![](https://capacity-files.lcfile.com/2JeApj7u2M2XhH5V3WOyDtHS0c42Ym5P/%E6%94%BB%E7%95%A5tab.png) - -### 4. 新版本详情页(自行配置) -新版本详情页是我们在首页、我的游戏页、游戏详情页新增的资源推广位,会将游戏的新版本以单独 SKU 的形式分发,吸引玩家预约游戏新版本。并在新版本上线后,通过我的游戏页悬浮窗、Push、微信服务号提醒等方式拉动已预约新版本的玩家回流及下载。 - -- **申请方式:** 新版本详情页由开发者自行填写,提交基础审核通过后方可上线 - -- **推送方式:** 我的游戏页悬浮窗、Push、微信服务号 - -- **其他相关信息&填写指南:** [利用「新版本」功能,吸引更多玩家关注或回流 | TapTap 开发者文档](https://developer.taptap.cn/docs/store/operations-skills/in-game-events/) - - - -:::info 注意 - -基础审核通过后,新版本详情页的卡片将出现在游戏详情页中。新版本上线当日,「我的游戏」页面中游戏图标处会新增悬浮窗提醒。 - -此外,通过 TapTap 编辑筛选及二次审核的游戏,在新版本上线前将按照 TapTap 推荐逻辑在首页分发。新版本上线当日,游戏还将在我的游戏页置顶。 - -::: - - - -
    - - - - - - - - - - - - - - - -
    游戏详情页展示位新版本详情页内容示例搜索品牌专区展示位
    -
    - - -### 5. 今日游戏(自行配置) - -为了尽可能让所有好游戏都能够获得足够的曝光,TapTap 在首页新增了「今日游戏」栏目以替代之前的「即将上线」。今日游戏能够承载游戏生命周期里的首曝、测试、首发、更新、折扣等各个关键节点的游戏曝光需求,信息更加全面,与用户的关联性更强。开发者仅需要按规范操作即可自动获得「今日游戏」栏目的基础曝光。 - -更多详细信息与申请指南:[如何获得「今日游戏」栏目的曝光](https://developer.taptap.cn/docs/store/operations-skills/today/) - - - - -## 二、 如何更好地运营游戏并与 TapTap 运营合作——长线运营期 -### 1. 基于 TapREP 与 TapTap 的运营合作 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    推荐程度资源置换形式置换说明重点置换形式示例创建指南价值计算方式 & 预期可获资源价值
    ⭐️⭐️⭐️⭐️⭐️ -

    游戏内跳转

    -
    -

    安卓官包 + iOS 官包,游戏内资源位跳转 TapTap 对应活动页面。

    -
    -

  • 拍脸图
  • -

    跳转到 TapTap 链接

    -

    至少放置 3 天

    -

    用户每日登录时可见

    -

    覆盖安卓官包 + iOS 官包

    -
    -

    -
    -

    TapREP_创建效果价值

    -
    -

    按实际拉新拉活效果次日结算

    -

    大盘:

    -

  • 单个拉新用户价值区间 10 元 - 50 元;
  • -

  • 单个拉活用户价值 3 元 - 30 元
  • -

    举例:按覆盖用户量 350 万,覆盖 7 天预估价值 10 万 - 50 万(若同步上传 iOS 官包,可获取总价值预估提升 50%)

    -
    -

  • 公告/邮件/站内信
  • -

    跳转到 TapTap 链接

    -

    至少放置 3天

    -

    覆盖安卓官包 + iOS 官包

    -
    -

    -
    -

  • 首页入口
  • -

    跳转到 TapTap 链接

    -

    需覆盖整体活动周期

    -

    覆盖安卓官包 + iOS 官包

    -
    -

    -
    ⭐️⭐️⭐️⭐️⭐️

    品牌标记

    内容媒体(微博/微信/b 站/小红书/抖音/快手)宣发时文案/内容带TapTap---官号/KOL 合作均可。 -

    品牌标记示例

    -

    bilibili:文案带 TapTap 示例

    -

    抖音:视频内容带 TapTap 示例

    -

    快手:文案带 TapTap 示例

    -

    微博:文案带 TapTap 示例

    -

    小红书:文案带 TapTap 示例

    -

    直播:

    -

    -
    -

    TapREP_创建品牌资源

    -
    -

    品牌资源价值根据单条内容的:互动量、粉丝量、官方蓝 V 等因素进行综合加权计算:

    -

  • 抖音/快手/小红书/微博:统计自上传且审核通过之日起 7 天的增量互动数据;
  • -

  • bilibili:统计自上传且审核通过之日起 30 天的增量互动数据。
  • -

    按以闪抖音合作为预估:

    -

    以闪抖音 kol 合作

    -

    内容示例和预估

    -

    预估价值 1-5 万

    -
    ⭐️⭐️⭐️⭐️⭐️官网置换 -

    在官网、新版本活动页等位置露出 TapTap 下载入口,并更换为 REP 平台所提供的监测链接。

    -
    -

    -
    -

    主入口加上TapTap下载。

    -
    -

    step1: TapREP_创建效果价值

    -

    step2: TapREP_上传官网品牌挂件

    -
    -

    官网品牌挂件价值为一次性奖励 + 效果结算两部分组成;

    -

    一次性资源奖励阶梯奖励,单次最高收益 3 万元,根据放的位置不同,收益不同,具体规则如下图:

    -

    -

    实际拉新拉活效果次日结算

    -

    大盘:

    -

  • 单个拉新用户价值区间 10-50 元;
  • -

  • 单个拉活用户价值 3-30 元;
  • -

    (实际价值受游戏等级影响存在波动。)

    -
    -

    -
    -

    渠道增加TapTap专区,引导至论坛。

    -
    ⭐️⭐️⭐️⭐️⭐️自媒体引流 -

    媒体通稿 + 微博、微信等自媒体带 TapTap 详情页效果链接。

    -
    -

    -

    媒体通稿带 Tap 预约链接。

    -
    -

    -

    微博/微信等内容带 Tap 预约链接。

    -
    -

    TapREP_创建效果价值

    -
    -

    实际拉新拉活效果次日结算

    -

    大盘:

    -

  • 单个拉新用户价值区间 10-50 元;
  • -

  • 单个拉活用户价值 3-30 元;
  • -

    (实际价值受游戏等级影响存在波动)

    -

    举例:按覆盖用户量 350 万,覆盖 7 天预估价值 10 万 - 50 万。

    -
    -

    -

    微信公众号阅读原文跳转 Tap 预约链接。

    -
    -

    -

    微信公众号导航栏跳转 Tap 预约链接。

    -
    ⭐️⭐️⭐️⭐️

    运营资源

    在 TapTap 站内开启论坛活动和签到活动。 -
      -
    1. 签到活动示例
    2. -
    3. 论坛活动示例
    4. -
    -

    - 抽奖活动 - {" "} - 回复活动 -

    -
    -

    TapREP_创建运营资源

    -
    -

    根据活动曝光、参与情况折算价值,并根据论坛等级这一核心指标,作为加权和降权。

    -

    运营资源总价值无月度上限,分拆到三个模块的价值为: ① 每周任务为固定金额,每月上限 2000 元 ② 论坛活动价值有单月上限,金额与论坛 DAU 强相关,且至少需要做 2 次论坛活动方可拿到上限 ③ 签到活动价值有单次上限,金额与论坛 DAU 强相关,无单月上限。

    -

    -
    ⭐️⭐️⭐️⭐️

    线下合作

    线下和 TapTap 联办漫展/市集/快闪等。 -

    -
    -

    -
    -

    需特殊申请开白,REP 资源置换后台提工单,联系对接商务或发送邮件到 tapop@xd.com 发起申请。

    -
    -

  • 根据人流量和展台露出程度给到固定金额的品牌价值结算。
  • -

  • 根据实际转化次日结算。
  • -

    按 CP29 人流量,高露出展台预估。

    -

    预估价值 1-4 万。

    -
    ⭐️⭐️⭐️

    软著授权

    提供游戏的软著授权给 TapTap,允许 TapTap 在站外买量时使用。 -

    -

    计算机软著

    -
    -

    -

    对易玩的授权书

    -
    -

    TapREP_上传软著授权

    -
    -

    审核通过后,将按照授权时间和游戏等级给予最高 1.2 万元的一次性奖励。

    -

    -
    资源合计:预估最高可获 50-100 万流量金(预估合计曝光 400 万)
    -
    - - - - -:::info 注意 - -表格中所提到的“元”为等价流量包。 - -::: - - - - -:::info - -增加 iOS 渠道,预估总收益将增加 50%。 - -::: - - - -### 2. 论坛配置及优秀实践分享 -TapTap 除了作为商店为厂商吸引玩家外,还是一个拥有巨大用户群的玩家社区。它允许玩家在其中讨论,分享游戏相关内容,并帮助开发者与玩家直接进行沟通。因此定期的论坛活动有助于提高玩家对于游戏的忠诚度,并帮助开发者及时收获真实反馈。 - -- 社区发帖操作:[学习社区模块,掌握基本操作](https://developer.taptap.cn/docs/community/features/) - -- 社区活动设计指南:[挑战进阶操作,玩转社区运营](https://developer.taptap.cn/docs/community/advanced/) - -- 优秀案例分享: - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    活动类型案例展示操作方式注意事项
    -

    意见收集

    -
    -

    【深度冻结】bug 反馈&建议征集专用帖!

    - -
    -

    论坛后台发帖

    -
    -

    -
    -

    社区互动

    -
    -

    【有奖活动】星穹会客厅 | 景元:运筹帷幄的云骑将军

    - -
    -

    论坛后台发帖

    -
    -

    -
    -

    评论抽奖

    -
    -

    【内含周边抽奖】《深空之眼》太一·庚辰角色 PV「夙夜长愿」

    - -
    -

    论坛后台发帖

    -
      -
    • TapTap 社区内的评论抽奖活动支持自动化组件,只需在发帖时,在发布设置中选择【添加评论抽奖活动】并按照指引填写相关信息即可。目前支持多种奖品设置(实物/兑换码),定时自动开奖,自动去除 bot 等功能
    • -
    -
    -
      -
    1. 活动发起方务必在帖子内清楚、准确描述活动规则、活动奖品、开奖时间。
    2. -
    3. 中奖用户需要在 7 个自然日内完成收件信息填写,活动发起方请务必在开奖后 30 个自然日内完成奖品发放。
    4. -
    5. 开奖时将自动过滤机器人用户。
    6. -
    7. 使用抽奖功能组建需要开通版主运营权限
    8. -
    -
    -
    diff --git a/docs/store/operations-skills/_category_.json b/docs/store/operations-skills/_category_.json deleted file mode 100644 index 0fd1fbe27..000000000 --- a/docs/store/operations-skills/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "通用运营技能", - "collapsed": true, - "position": 16 -} diff --git a/docs/store/operations-skills/astroturfers.mdx b/docs/store/operations-skills/astroturfers.mdx deleted file mode 100644 index 5e1616197..000000000 --- a/docs/store/operations-skills/astroturfers.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: TapTap 诱导好评定义及处理规则 -sidebar_position: 10 ---- - -TapTap 致力于维护评价内容的真实性,拒绝开发者利用奖励干预玩家对游戏的真实点评。 -玩家通过游戏方「诱导好评」行为发布的评价,均属于 TapTap 不提倡的评价内容。 - -## 1. 什么是「诱导好评」行为 - -如下行为将被 TapTap 平台视为「诱导好评」: -- 开发者在游戏内、QQ 群、论坛等地,发布带「好评」「五星」等字样的评价邀请; -- 以非 TapTap 官方([TapTap评价君](https://www.taptap.cn/user/83544660))的身份,在游戏内、QQ 群、论坛等地,将奖励和评价引导整合在同一公告中发布; -- 在评分里程碑分数未达成前,设置评分里程碑奖励。 - -以下是部分错误示范和最佳实践示例: - -| **「诱导好评」定义** | **错误示范** | **最佳实践** | -|:----------------------------------------------------:|:--------------------------------------------------------------------------------:|:-----------------------------------------------------------------------:| -| 开发者在游戏内 / QQ 群 / 论坛等地,发布带 **「好评」「五星」** 字样的评价邀请 | 某游戏在游玩过程中,弹窗提醒玩家 **「给个好评」** | 某游戏在游戏内发布,引导玩家「去 TapTap 评价游戏」 | -| 开发者在游戏内 / QQ 群 / 论坛等地,将奖励和评价引导整合在同一公告中发布 | **某游戏**在 TapTap 论坛发布活动,称「全服奖励礼包码在此,希望大家来 TapTap 评价区多提意见」;或在玩家 QQ 群发布公告,称「凭 TapTap 评价截图,找群管理**领奖励**」 | 某游戏**和 TapTap 官方合作**,通过「TapTap评价君」账号发布评价征集活动,邀请玩家客观评价游戏,**并由 TapTap 官方评选出最终结果** | -| 在评分里程碑分数**未达成前**,设置评分里程碑奖励 | 某游戏在 TapTap 评分 **6.8** 分时举办运营活动,称「TapTap 评分达到 **7** 分,将发放全服礼包」 | 某游戏**庆祝** TapTap 评分**已到达** 8 分,发放全服礼包,且**未暗示「到达更高分数会有其他奖励」** | - -## 2. 「诱导好评」事件的处理流程和规则 - -如发现「诱导好评」行为,将适用以下处理流程: - -1. 核实诱导好评行为后,平台将清理引导期间的相关好评,并回溯游戏评分。 -2. 每周三,平台会通过开发者后台发送上一周的诱导好评通报,确认有诱导好评行为的开发者将会收到整改通知。请注意:收到整改通知,意味着游戏评价将进入重点审核名单,该名单不会对游戏的流量与分发产生直接影响。 -3. 如同一游戏多次产生诱导好评行为并拒不整改,平台将根据诱导出现频次升级处罚,包括但不限于降低流量,停止分发,在游戏详情页进行标注等等,并通过开发者后台同步发送处罚通知。 diff --git a/docs/store/operations-skills/event-center.mdx b/docs/store/operations-skills/event-center.mdx deleted file mode 100644 index 714047152..000000000 --- a/docs/store/operations-skills/event-center.mdx +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: 申请活动中心栏目 -sidebar_position: 5 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; - -## 「活动中心」是什么 - -TapTap 新推出的一个游戏服务栏目,帮助玩家一站式获取游戏招募、礼包、签到、社区活动等福利相关信息,促进用户下载及回流,延长其生命周期。 - -**功能:**展示所有可参与的活动信息,通过福利内容,促进用户活跃、新增以及回流。 - -**优势:** - -1. 通过「活动中心」,开发者可以更好的为不同生命周期的游戏宣传,触达更多新老用户,吸引游戏新增与回流。 -2. 每个游戏活动都能得到充分的曝光,降低用户信息获取门槛,提高各类活动参与度,延长用户生命周期。 - -## 「活动中心」使用须知 - -### 活动支持类型 - - - - - - - - - - - - - - - - - - - - - - - - - -
    活动类型签到活动礼包福利论坛活动游戏招募
    创建方式开发者中心创建开发者中心创建(无需申请)论坛发帖论坛发帖
    示例 - - - - - - - - -
    - -### 您将获得的支持 - -#### 1)活动中心资源曝光 - - - - - - - - - -
    - - - - - 顶部 Banner 位、签到位、礼包位、热门活动 Banner - 位(高热度/高福利签到/测试招募活动也可放在热门活动中) -
    - -#### 2)其他曝光支持 - -高质量的活动,将有机会获得首页推荐及 push 等额外支持。 - -### 您需要准备的素材 - -#### 1)内容 - -承载活动相关信息的 TapTap 站内链接及奖品清单(含奖品明细、数量及奖品价值)。 - -#### 2)图片 - -尺寸:1920\*1080px - -格式:JPG 或 PNG - -大小:< 2M - -要求:图片需确保美观清晰,含有大小适中的游戏 Logo 且位于图片左上/右上;可添加推广语,推广语需与活动形式或奖品强相关。 - -![](/img/event-center/picture-sample.jpeg) - -## 如何申请 - -开发者可于【开发者中心 - 工单 - 商店运营服务支持 - TapTap 新功能申请/咨询 - TapTap 活动中心申请/咨询】中提交相关图片/文案进行申请。 - -**操作示例:** - - - -## Q&A - -**Q: 什么样的活动形式和内容无法通过审核?** - -**A:** - -1. 对于评分低于 5 分(不含 5 分)的游戏,不做通过。 -2. 对于预约数< 15 万,月下载/月更新均< 10 万的游戏,不做通过。 -3. 对于存在争议、用户口碑极差的游戏,不做通过。 -4. 对于有强烈舆论问题的游戏,舆论期内不做通过。 -5. 纯游戏内活动、引导评价式活动、活动形式不在 TapTap 内落地的活动不做通过。 -6. 纯实物奖励活动,总获奖人数低于 10 人不做通过。 -7. 一等奖总价值低于 200 元,总价值低于 500 元的活动,不做通过。 - -**Q: 活动奖品的价值,如何计算?** - -**A:** 虚拟道具价值按照游戏内售价折算;手办、钥匙扣、充电宝、抱枕等周边,有官方商城/淘宝旗舰店的,按旗舰店价格折算,无以上渠道的,按行价预估。 - -**Q: 什么样的活动可以获得资源倾斜?** - -**A:** 热门游戏活动或奖品数量多或奖品价值高的活动,会给予一定资源倾斜。 diff --git a/docs/store/operations-skills/event-notice.mdx b/docs/store/operations-skills/event-notice.mdx deleted file mode 100644 index af1cf402c..000000000 --- a/docs/store/operations-skills/event-notice.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: 如何利用「活动与公告」板块获得曝光 -sidebar_position: 11 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; -import Link from "@docusaurus/Link"; -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -## 什么是「活动与公告」板块? - -「活动与公告」板块是 TapTap 对现有详情页内容进行整合迭代后的全新板块,一站式为用户展示站内活动(签到,H5 活动,礼包,社区活动),官方公告,新版本预约页等。为这些零散的官方内容提供了统一的展示窗口,帮助用户快速获取信息,也帮助厂商集中扩大曝光。
    - -## 「活动与公告」如何对内容进行展示? - -展示形式可以大致分为三种: - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    置顶 1/2 位置顶 2/2 位板块内
    Tap 客户端详情页展示创建方式 - - - - - -
    开发者中心 - - - - - -
    置顶逻辑首次置顶的内容在完成 1 位置顶之后,再次置顶的内容未执行置顶操作的内容
    - -## 「活动与公告」如何进行操作? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    项目操作方式DC 后台位置前端展示(详情页)前端展示(板块内)
    公告手动添加链接游戏运营-活动与公告
    - - - -
    反馈 - - - -
    新版本新版本配置页配置新版本预约页配置好后会自动收录在游戏运营-活动与公告中

    点击右侧编辑按钮就可操作置顶
    - - - -
    活动手动添加链接游戏运营-活动与公告
    - - - -
    抽奖

    征稿
    签到签到配置页配置签到和礼包配置好后会自动收录在游戏运营-活动与公告中

    点击右侧编辑按钮就可操作置顶

    礼包礼包配置页配置
    - -## 常见问题 Q&A - -**Q:为什么我之前配置过的活动或公告,现在在「活动与公告」没有展示,且开发者中心的活动与公告列表里也找不到?** - -**A:功能刚刚上线存在一定的过往数据延迟,只需要对内容进行一次内容更新,即可在后台以及前端进行展示,更新行为包括下架后重新上架,修改活动名称,修改内容标题等。 - -**Q:为什么我无法在“新建一个活动或公告“里面,新建签到,新版本,发放礼包?** - -**A:签到活动,新版本预约和礼包发放请在开发者中心对应页面进行创建,创建成功后会自动被收录在「活动与公告」板块,并可以通过开发者中心的「活动与公告」后台进行置顶操作。 \ No newline at end of file diff --git a/docs/store/operations-skills/game-sign.mdx b/docs/store/operations-skills/game-sign.mdx deleted file mode 100644 index 503c00e91..000000000 --- a/docs/store/operations-skills/game-sign.mdx +++ /dev/null @@ -1,259 +0,0 @@ ---- -title: 利用签到活动,拉动玩家活跃 -sidebar_position: 7 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import Link from "@docusaurus/Link"; - -## 游戏签到是什么 - -TapTap 新推出的一个游戏服务功能,厂商可通过签到送奖励的方式,促进用户活跃、新增以及回流,一定程度上延长用户生命周期。根据我们的测试与数据回收,签到活动能极大的提高游戏论坛的活跃人数与用户留存。 - -## 您将获得的资源位支持 - -### 系统自动配置 -当您在开发者后台创建的签到活动过审之后,将会获得以下系统自动配置的资源: - -【活动与公告】:创建的签到活动过审后,系统会自动展示于游戏详情页—活动与公告处; - -【我的游戏】:签到活动将自动显示在【我的游戏】列表中; - -【福利中心—新的签到福利上线了】:系统自动配置; - -这边也建议您将签到活动自行配置在论坛推荐位。 - -### 位置展示 - - - - - - - - - - - - - - - - - - -
    活动与公告我的游戏福利中心游戏论坛游戏签到页面效果
    - - - - - - - - - -
    - -## 「签到活动」创建方式 - -### 一、活动素材要求 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    素材内容说明效果展示
    活动基本信息活动标题建议突出奖励内容,如非必要请不要填写游戏名
    签到开始时间 -
  • 用户可以开始签到行为的时间点,部分资源位根据时间点自动露出
  • -
  • 已上线签到活动的开始时间无法修改,如有变动需提工单或联系对接运营下架当前已过审签到活动,开发者后台重新配置
  • -
    签到结束时间 - 签到结束的时间点,此时用户无法进行签到行为,但仍可进行查看礼包码、兑换等行为 -
    活动结束时间 - 此活动结束,不能进行任何操作。 -
    - 建议此时间晚于签到结束时间至少 7 日,方便用户兑换奖励等操作 -
    礼包兑换方式-前往游戏兑换(2选1) - 礼包码在游戏中的兑换引导文案,方便用户快速兑换奖励。 -
    - 例:进入游戏后,依次点击福利-兑换码-输入兑换码即可 -
    礼包兑换方式-前往网页兑换(2 选 1)提供玩家兑换礼包码的网页地址即可
    礼包码 -
  • 礼包码请按天数准备对应个数礼包文件,如签到奖励领取天数为 1/3/5/7,则准备 4 个兑换码文件
  • -
  • 支持 xlsx、xls、csv 格式
  • -
    内容推荐(选填)可以设置一个想要导流的 TapTap 论坛帖子
    图片活动头图 - 签到活动头图将直接使用当前游戏的宣传图(16:9) - - - 部分图片将被遮挡,遮盖范围见下,参考效果。 - -
    累计签到奖品图片 - 尺寸:144*144 -
    - 格式:PNG 透明底 -
    - 需提供配套文案,用以向用户说明奖励内容 -
    - 不同道具用中文顿号「、」隔开,奖品与数量之间用星号「*」隔开,不可手动换行。 -
    - 示例:金币 *1000、原石 *1 -
    - -
    集卡卡片图片(选填) - 尺寸:342*426 -
    - 格式:PNG -
    - 需提供配套文案,用以向用户说明奖励内容 -
    - 不同道具用中文顿号「、」隔开,奖品与数量之间用星号「*」隔开,不可手动换行。 -
    集卡奖品图片(选填) - 尺寸:216*216 -
    - 格式:PNG 透明底 -
    - 需提供配套文案,用以向用户说明奖励内容 -
    - 不同道具用中文顿号「、」隔开,奖品与数量之间用星号「*」隔开,不可手动换行。 -
    - 示例:金币 *1000、原石 *1 -
    - -### 二、 普通签到活动创建方式 - -开发者可于【开发者中心 - 游戏运营 - 签到系统 -创建签到活动】中提交相关图片/文案进行创建与审核。 - -操作步骤一:点击创建签到活动,提交相关信息并保存 - -操作步骤二:在生成的内容中点击配置礼包,提交相关信息并保存 - -确认内容无误后,提交审核(请仔细检查内容,如出现错误将影响你的审核结果) - -**操作示例:** - -![](https://img.tapimg.com/market/images/2dec1d7e1716e453f7cbcf3ebe8c4b6d.jpg) - -### 三、新功能「签到集卡」创建方式 - -TapTap 签到功能已对前端整体展示效果进行了优化,调整了部分素材图的尺寸要求。 - -新推出的“签到集卡”功能允许厂商向参与签到的用户额外提供 1~2 种较高价值的抽奖奖品(如:京东卡/实物奖品/游戏兑换码等),活动期间的抽奖奖品将以抽卡形式按指定概率随机发放。 - -#### 操作示例: - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    内容配置教程配置表单活动效果参考
    集卡功能开启前往 【游戏运营-游戏福利-签到系统】,点击【创建签到活动】后,在“配置签到活动”表单内的”集卡活动启用状态“处勾选”启用“进行功能开启。
    请注意集卡活动要求每张奖品卡数量不小于 5 件,如需关闭请点击“配置活动”修改配置。
    配置活动卡片数量 - 集卡功能开启后,点击活动的操作栏的“集卡”,在弹出的“配置集卡”表单内进行卡片配置。
    默认配置 5 张活动卡片( 3 张基础卡和 2 张奖品卡),允许删除至多 1 张奖品卡,奖品卡删除后无法重新添加。 -
    - -
    配置活动卡片信息/数量 - 点击卡片操作栏的“编辑”进行卡片信息配置,完成奖品卡的基础信息配置后,点击“补充库存”进行库存填写/上传,奖品卡数量即为实际奖品数量。
    奖品卡将以配置的抽卡概率为真实概率进行发放。
    参考概率数据:奖品卡配置概率=该奖品卡数量/将奖品卡紫和奖品卡金全部领完的期望用户数量 *100
    例:配置抱枕 40 件和立牌 200 个,期望 5 万用户参与并领取所有奖励,则抱枕配置 80 (实际概率 0.08% ),立牌配置 400 (实际概率 0.4% )。
    - 注意: -
  • 活动开始后无法进行库存以及卡片信息的修改,请谨慎配置。
  • -
  • 由于概率的随机性,活动期间卡片提前发完/未发完均属正常情况。
  • -
  • 配置“京东卡”奖品的厂商请于活动结束后向平台提单进行未发放奖品退还。
  • -
    -
    - -### Q&A - -**Q:如何确认是否申请成功?** - -**A:**申请审核状态,请于活动创建页面审核状态处查看。 - -**Q:我想举办签到活动,需要提前多久提交活动申请以便审核?** - -**A:**所有活动申请,自提交审核成功后 2 个工作日内处理,烦请及时查看审核状态,以便活动及时上线。 - -**Q:活动对于游戏以及奖品有什么要求?** - -**A:** -- 对于明确抄袭的游戏不予通过 -- 每档位奖励数量 < 1000,不予通过 -- 其余无要求,但过往经验及数据显示,奖品价值越高,用户参与度越高,活动效果越好。 - -**Q:关于签到活动我还有其他问题?** - -**A:**如有任何问题请提工单选择分类「商店运营服务支持」-「TapTap 新功能申请/咨询」-「游戏签到系统问题咨询」进行咨询处理。 \ No newline at end of file diff --git a/docs/store/operations-skills/in-game-events.mdx b/docs/store/operations-skills/in-game-events.mdx deleted file mode 100644 index d3bbe5296..000000000 --- a/docs/store/operations-skills/in-game-events.mdx +++ /dev/null @@ -1,255 +0,0 @@ ---- -title: 如何更好运营游戏更新和游戏内活动 -sidebar_position: 6 -toc_max_heading_level: 6 ---- - -## 新版本详情页是什么? - -新版本详情页是我们在首页、我的游戏页、游戏详情页新增的资源推广位,会将游戏的新版本以单独 SKU 的形式分发,吸引玩家预约游戏新版本。并在新版本上线后,通过我的游戏页悬浮窗、Push、微信服务号提醒等方式拉动已预约新版本的玩家回流及下载。 - -新版本详情页可清晰展示版本重点更新内容与重要信息,如图文视频、上线日期、福利活动等,更易触动玩家。 - -## 如何申请开启新版本详情页? - -当开发者确认了游戏新版本的核心内容(包括但不限于新玩法、新地图、新职业、新剧情等)后,即可自主创建新版本详情页,填写内容。 - -## 如何填写新版本详情页的内容? - -新版本详情页的配置入口为:**开发者中心——游戏运营——新版本运营。** - -
    -点击查看后台配置界面 - -<> - -![新版本详情页配置界面](/img/in-game-events-configure.png) - - - -
    - -进入配置页面后,开发者们可以参看以下游戏的新版本详情页,以及[我们的详细文档](#新版本详情页详细填写指南),了解新版本详情页的结构设计与内容填写规范。 - -:::note 点击查看 - -- [《江南百景图》扬州府新版本详情页](https://www.taptap.cn/game-event/20) -- [《原神》2.4「飞彩镌流年」新版本详情页](https://www.taptap.cn/game-event/10) -- [《哈利波特》MA5赛季新版本详情页](https://www.taptap.cn/game-event/21) -- [《光•遇》潜海季新版本详情页](https://www.taptap.cn/game-event/24) -- [《决战平安京》鬼灭之刃联动新版本详情页](https://www.taptap.cn/game-event/9) -- [《仙境传说RO》七王室之谜新版本详情页](https://www.taptap.cn/game-event/12) - -::: - -## 填写好新版本详情页后,如何申请曝光资源? - -在确保内容按规则填写完成后,即可提交审核。 - -基础审核通过后,新版本详情页的卡片将出现在游戏详情页中。新版本上线当日,我的游戏页面中游戏图标处会新增悬浮窗提醒。 - -此外,通过 TapTap 编辑筛选及二次审核的游戏,在新版本上线前将按照 TapTap 推荐逻辑在首页分发。新版本上线当日,游戏还将在我的游戏页置顶。 - -## 新版本详情页详细填写指南 - -### 后台配置页面一对一填写指南 - -建议开发者打开后台配置页面,从上至下对照查看,加 \* 为必填项。 - -
    -点击查看后台配置界面 - -<> - -![新版本详情页配置界面](/img/in-game-events-configure.png) - - - -
    - -#### 版本名* - -文案建议使用版本号 + 版本名,如:2.4 飞彩镌流年。请勿包含游戏名,不可以使用「」、【】、“”、()等标点符号作为版本名的开头和结尾 - -#### 新版本卖点* - -总结最核心的卖点,如「新英雄天使上线,S3 赛季结束」,字数限制为 20 字内。 - -#### 新版本头图* - -为新版本详情页内的顶部图片。通常来说,上传该版本的海报图、KV 图即可。支持 JPG、PNG 格式,建议比例为 16:9,分辨率为 1280*720px 及以上。 - -#### 新版本推广图* - -为新版本详情页在游戏详情页、首页的展示卡片图,可上传该版本海报图,KV 图,**但图片中仅可附带游戏名或版本名,其余元素需去掉**。支持 JPG、PNG 格式,建议比例为 16:9,分辨率为 1920*1080px 及以上。 - -#### 新版本搜索品牌专区图* - -为新版本详情页在搜索的品牌专区的展示卡片图,可上传该版本海报图、KV 图,但图片中仅可附带游戏名或版本名,其余元素需去掉。支持 JPG、PNG 格式,建议比例为 2.5:1,分辨率为 1920*768px 及以上。 - -#### 模块标题(设置)* - -新版本详情页的主体结构便是模块,至多可以设置3个模块,每个模块下可以设置 5 个划重点。 - -我们整理了 3 套模板,每套模板都配有优秀案例,此外还有两大需要注意的原则,点击展开查看。 - -
    -模板一:适用于内容较多,重点较多的中大型更新 - -<> - -新版本详情页中所要填写的内容为**重要的新增游戏内容、重点新活动、以及其他有吸引力的内容这三类。**那么在更新内容较多时我们就可以将该版本的所有更新内容汇总为这三类,设置 - -1. **「重要新增游戏内容」** -2. **「重点新活动」** -3. **「其他更新内容」** - -这三个模块(模块名可一定程度自定义)。那么,汇总到每一类下的每一个小点,则对应模块下的划重点。更新详情则对应划重点下的正文与图片视频素材。 - -:::note 参考案例 - -- [点击查看《原神》2.4新版本「飞彩镌流年」新版本详情页](https://www.taptap.cn/game-event/10) -- [点击查看《江南百景图》扬州府新版本详情页](https://www.taptap.cn/game-event/20) - -::: - - - -
    - -
    - -模板二:适用于内容适中,重点较少的中型更新 - -<> - -可能存在这种情况,某一版本的更新内容并不多,重点内容仅有 2–3 个,继续使用模板一,可能导致为数不多的重点被录收在了一个大模块之下,既不直观,内容展示得也不够多。 - -如果遇到这种情况,则可以直接将为数不多的重点作为模块,直接展示清楚。若某一重点需要拓写多条(如更新了 4 个角色),则在该模块下再加几个划重点即可,基本也可容纳。 - -:::note 参考案例 - -- [点击查看《哈利波特》MA5 赛季更新新版本详情页](https://www.taptap.cn/game-event/21) -- [点击查看《光•遇》潜海季新版本详情页**](https://www.taptap.cn/game-event/24) - -::: - -**但模板二的缺点也较为明显,**因为模块最多设置3个。除非确定仅有3大重点需要展示,否则不建议使用模板二。 - - - -
    - -
    -模版三:适用于内容适中,需特别强调某一重点的中型更新 - -模板三是模板一与模板二的结合,主要考虑的是这种情况。 - -某一版本的更新内容适中,按照模板一的做法,游戏内容与活动用到了 1–2 个模块,还余下 1–2 模块可用,但并不想往其中填入价值较低的 BUG 修复等内容。而在游戏内容与活动的模块中却有 1–2 个非常值得详细说明、强调的重点。那么则可以将这 1–2 个重点提取出来,直接作为单独模块。 - -
    - -
    -两大原则:同一性、排序自定义 - -<> - -无论使用哪一套模板,都需要注意以下两条原则。 - -1. **单个模块下必须是同一类型的内容** - - 无论使用哪一个模板,都必须遵守这一原则,即单个模块下必须是同一类型的内容,不可将游戏内容、活动内容、其他更新内容混写在一个模块之下。举例: - - - 若模块的标题为「春节版本重要游戏更新」,则其下的重点只能是新角色、新地图、新任务、新时装等等,不能包含新活动; - - - 若模块的标题为「春节版本新增英雄」,则其下的重点只能是几位新英雄,不仅不能包含新活动,也不能包含同为游戏内容的新地图、新任务、新时装等等。 - -2. **模块的顺序和数量都可以根据重要程度自行决定** - - 无论使用哪一个模板,其中模块的顺序和数量都可以根据重要程度自行决定。举例: - - - 如某一版本大量的活动才是重点,则可将活动内容填在第一个模块; - - 如某一版本以大量新增游戏内容与优化修复为主,没有活动内容,则可以仅设置两个模块。 - - - -
    - -如选用模板一,模块标题则在 **「重要新增游戏内容」「重点新活动」「其他更新内容」**的基础上修改。如 **「一周年版本重要更新」「万圣节活动」「BUG修复与优化」。**字数限制为15字内。 - -如选用模板二,模块标题则自定义,精准概况内容即可。 - -#### 公告链接 - -每个模块下都可以添加一个官方公告链接,链接需为 TapTap 站内长帖,长帖内容需与模块内容强相关。如: - -- 万圣节活动(模块)——万圣节活动一图流(链接长帖) -- 一周年重要更新(模块)——12月25日更新公告(链接长帖) - -#### 划重点标题* - -准确概况该重点的核心内容,如:春节新时装上线、新增限定角色、累登送自选礼包、角色贴图修复等等。字数限制为 15 字内。 - -#### 划重点正文 - -展开说明重点内容,如: - -- 时装(划重点)——时装的售价,属性,穿戴条件(正文) -- 角色(划重点)——角色的品级,使用武器,获取方式(正文) -- 活动(划重点)——活动的参与方式,开启时间,奖励(正文) -- 优化(划重点)——具体优化的项目,优化前与优化后的对比(正文) - -#### 重点摘要的视频 & 图片 - -图片与视频素材需与该重点的标题正文强相关,我们建议图片使用专门的展示图,而不是游戏内截图。因为游戏截图缺少文字、设计元素补充,突出重点的效果略弱,且会让版面显得单调。此外,一定不可有血腥色情暴力等违规内容。 - -简单举例,如: - -- 角色(划重点)——角色的立绘、3D 展示图、角色专属视频 -- 活动(划重点)——活动的海报图,活动奖励展示图 - -视频仅支持 MP4 格式,图片支持 JPG、PNG 格式。视频可以上传 1 个,图片可上传 0–5 张。横图建议比例为 16:9,尺寸为 1280x720 px 及以上,竖图建议比例为 9:16,尺寸为 720x1280 px 及以上,**更推荐使用横图。** - -图片与视频可以都上传,会以横滑方式展示。 - -#### 上线日期* - -选择新版本的上线日期即可,建议尽量选择准确日期。若初稿选择了「日期待定」,后续请记得修改为准确时间。 - -#### 新版本标签* - -在现有标签中选择最符合情况的标签,最多三个。 - -#### 新版本视频 - -若有为新版本专门制作视频,则可上传。上传后,版本头图部分将优先展示新版本视频。视频仅支持 MP4 格式。 - -#### 是否显示历史版本* - -意为是否新版本详情页底部显示上一个新版本详情页的入口,建议选择「显示」。 - -:::tip 文字内容填写小贴士 - -文字内容部分的原则是精准简洁、概括性强、重点突出,说玩家能看懂的话。 - -我们不建议太多抒情,追求文采的词句。如介绍一名新角色,建议文案优先介绍新角色的属性、技能、品级,其次再考虑介绍角色的故事背景与人设性格。 - -::: - -### 后台操作重要注意事项 - -#### 如何保存草稿 - -首先,后台暂时没有实时自动保存功能,因此我们建议先在本地准备一份草稿。填写内容过程中,随时可以点击右上角的「保存草稿」存档。存档后,即可在列表页看到草稿。 - -#### 再次编辑草稿时,如何避免草稿丢失 - -点击编辑即可再次进入编辑页面。但需注意,此时先前的草稿内容不会自动填充,需点击顶部弹出的「读取」。因此,**千万不要在二次进入编辑页面,尚未读取草稿内容,页面全为空时点击保存,这会导致草稿内容丢失。** - -#### 如何预览效果 - -需要在所有带红色 * 号的必填项都填入内容后,才可以使用右上角的预览功能。点击预览时,系统会自动保存当前页面内容为最新草稿。 - -#### 如何提交审核 - -在确保内容结构按规则填写,素材无遗漏后,即可在列表页提交审核。若审核不通过,可以按上文的步骤再次编辑提审。审核通过后,也可继续修正或丰富内容,二次提审。 diff --git a/docs/store/operations-skills/information-dissemination.mdx b/docs/store/operations-skills/information-dissemination.mdx deleted file mode 100644 index 83ae4150e..000000000 --- a/docs/store/operations-skills/information-dissemination.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: 如何让官方信息更好地触达到玩家 -sidebar_position: 1 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -确保官方信息更好地触达玩家对于游戏开发者来说至关重要。玩家渴望及时了解游戏更新、活动和重要公告等关键信息,而作为开发者,您有责任确保这些信息准确、及时地传达给他们。 - -为了让官方信息更好地触达到玩家,以下是一些有效的运营手段。 - - -## 1.重视「官方帖推送」 - -### 1.1 什么是「官方帖推送」 - -![gameadmin](/img/信息触达2.png) - -官方帖推送是触达更高的信息传达方式,玩家可在消息中心通过红点提醒获得强感知。 - -开发者可在 游戏运营 >> 内容管理 >> 官方帖消息推送 中对推送进行管理。 - -### 1.2 创建「官方帖推送」有哪些须知 - -1.2.1 推送时间不可为凌晨 1:00 - 7:00。 -1.2.2 每个自然周最多可发送 1 次推送,次数将于下个自然周重置。 -1.2.3 「定时发送」可选择 5 分钟后的今、明两天内任意时刻定时群发。 -1.2.4 定时推送创建后不可修改,但可在发送前取消。 -1.2.5 被取消的定时推送,不占用本周发送次数。 \ No newline at end of file diff --git a/docs/store/operations-skills/maelstrom.mdx b/docs/store/operations-skills/maelstrom.mdx deleted file mode 100644 index acdb58103..000000000 --- a/docs/store/operations-skills/maelstrom.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: 游戏异常事件应对 -sidebar_position: 8 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -每一位游戏人,都经历或见证过游戏异常事故带来的评价轰炸,比如炸服、恶性 bug、运营事故等,每当异常事件发生,评价区总是首当其冲地成为玩家发泄不满情绪的地方。 - -这类事件的通常表现为:**短时间出现大量报复性差评,目的是降低游戏的评分,使开发者利益受损。** - -这很棘手,尤其当开发者的事故处理经验不足时,这些报复性的情绪差评会长期影响游戏的评分。 - -而我们认为,游戏评分的目的是起到决策辅助作用,让潜在的玩家决定是否要下载游玩,「报复性的情绪差评」 实际上同样影响了评分的真实性,不该被计入分数。 - -**因此,我们决定增设「异常事件评价处理」通道,协助诸位伙伴更好地度过舆情危机。** - -## 1. 最佳实践 -- 了解引起玩家舆情的真正原因,并尝试解决它。这有利于将舆论风波控制在最小范围。 -- 不要与玩家作正面冲突对抗,即便您不认可他们的观点。这通常会扩大影响面,让事态更加棘手。 -- 积极解决事件,等待舆情反转或消退,评价区轰炸结束后,您可以发起异常评价处理申请。 -- 核实事故后,我们会根据您的申请理由逐一浏览评价,并对满足处理标准的评价进行事故标记,事故评价将被降权且不计入总分。您可以在 概览 >> 评分评价数据模块 查看被标记的评价数量。 - -## 2. 常见的异常事件评价处理类型 -- 游戏临时故障评价:当您的游戏程序异常,出现无法登录游戏、无法正常游玩等问题时,针对此项内容发表的评价。 -- 游戏运营事故评价:如果游戏运营人员因工作失误,造成游戏活动、游戏奖励异常,导致大量玩家不满时,或因为一些突发的运营事件被有规模、有组织地轰炸时,针对此项内容发表的评价。 -- 非正式版评价:如果您的游戏在先行服更新了一些内容,玩家对此表示不满,但是您已经在正式版上修改了此内容时,针对此项内容发表的评价。 - -## 3. 申请条件 -- 您的游戏最近 30 天内遭受过异常事件评价轰炸,且事故已解决或风波已平息; -- 您的游戏最近 90 天内没有刷好评/违规引导好评行为; -- 申请 CD: - - 通常情况下,异常事件评价处理每隔 **30** 天可申请一次; - - 为了降低游戏开服突发情况而集中爆发负面舆论带来的影响,**游戏首次开放下载后**申请的 CD 将被重置;此时无需间隔 30 天亦可直接申请; - - 如遇特殊情况,请与对应 TapTap 运营人员申请后再评估。 - -满足以上条件,请在开发者中心的 商店 >> 评分评价 >> 异常评价处理 中进行申请。 - -## 4. 处理范围与规则 -舆情期间内容主要讨论事故相关的评价,将会被降权并标记为「事故评价」,事故评价不计入游戏评分。 -申请通过后,我们会在 7 个工作日内处理您的事故评价并通知结果。 - -## 5. 常见问题 -Q:舆情期间新发布的评价都会被处理吗? -A:不会。TapTap 的工作人员将会审阅舆情期间的每一条评价,只处理舆情相关的评价,讨论游戏其他内容的评价,不会被视为事故评价处理。 - -Q:为什么处理结束后,事故评价没有被删除? -A:事故评价并不完全等同于违规内容,我们只对其进行降权和标记,使其不影响您的评价区观感和游戏评分,除非这条评价同时违反了《[TapTap 评价区发布规则](https://poster.taptap.cn/r/NB5ZEgOTUgDk.html?f=1)》,则会视为违规评价处理。 - -Q:我没有受到规模性的评价轰炸,但有个别玩家对我进行了恶意差评怎么办? -A:对于非「规模性」的恶意差评,可以在开发者中心的 商店 >> 评价处理和投诉 中进行恶意评价投诉,现已支持批量投诉功能,TapTap 的审核人员将在 60 分钟内处理您的投诉。 \ No newline at end of file diff --git a/docs/store/operations-skills/review-management.mdx b/docs/store/operations-skills/review-management.mdx deleted file mode 100644 index 6c978d466..000000000 --- a/docs/store/operations-skills/review-management.mdx +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: 如何运营游戏的评分评价? -sidebar_position: 2 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -对游戏开发者而言,评分评价至关重要。它们不仅提供了宝贵的用户反馈和改进机会,还直接影响着玩家的选择和游戏的市场表现。 - -为了更好地在 TapTap 平台维护您的游戏评分和评价,您可以采取以下运营策略。 - -## 1. 积极回复玩家评价,反馈版本更新/研发状况 - -保持积极回复玩家评价有助于建立良好的开发者形象,增强玩家信任和忠诚度,并促进积极的口碑传播,进而提升游戏的品牌价值和用户体验。 - -当保持积极回复玩家评价时,可以采取以下具体措施: - -### 1.1 及时回复 - -尽量在24小时内回复玩家的评价,展现出对玩家反馈的关注,并让玩家感到被听到和重视。 - -*"感谢您的评价!我们非常重视玩家的反馈,我们会尽快回复您的问题。"* - -*"非常抱歉让您感到困扰,我们已经收到了您的评价,并会尽快给您一个满意的答复。"* - -### 1.2 真诚倾听 - -认真阅读玩家的评价,并理解他们的关注点和问题。展现出对玩家意见的真诚倾听,表达对他们的感谢和理解。 - -*"非常感谢您对我们游戏的支持和宝贵的意见,我们会认真倾听您的建议并不断改进游戏体验。"* - -*"我们真诚感谢您的反馈,我们会认真考虑您的意见,并努力提供更好的游戏体验。"* - -### 1.3 积极态度 - -保持积极的态度回复玩家评价,无论是积极的赞美还是负面的批评。用友善和礼貌的语言回应,展现出对玩家的尊重和关心。 - -*"非常感谢您对我们游戏的赞美,您的支持是我们前进的动力!"* - -*"我们非常重视您的反馈,我们会努力改进游戏,确保每位玩家都能享受到优质的游戏体验。"* - -### 1.4 提供解决方案 - -针对玩家的问题或关注点,给予具体的解决方案或建议,展现出对问题的认真处理和解决意愿。如果无法立即解决问题,说明将采取行动并向玩家保持更新。 - -*"非常抱歉您遇到了这个问题,我们会尽快修复并更新包体。请您耐心等待并感谢您的理解。"* - -*"感谢您的反馈,我们会考虑您的建议并争取在未来的版本中增加这个功能。"* - -### 1.5 避免冲突和争执 - -遇到负面评价或抱怨时,避免与玩家发生冲突或争执。保持冷静和专业,通过积极的沟通和解释,寻求共同的解决方案。 - -*"我们很抱歉您对游戏的体验感到不满意,我们愿意倾听您的问题并寻找共同的解决方案。"* - -*"我们理解您的不满,我们会努力改善游戏中的问题,并确保您和其他玩家都能获得更好的游戏体验。"* - -### 1.6 学习和改进 - -将玩家的评价视为宝贵的反馈和学习机会。借此机会反思游戏的不足之处,并采取适当的措施进行改进,以提升游戏质量和玩家满意度。 - -*"感谢您的反馈,我们会认真分析并加以改进,以提升游戏质量和玩家满意度。"* - -*"我们会将您的意见作为宝贵的学习机会,不断改进我们的游戏,以更好地满足玩家的期待和需求。"* - -## 2. 使用开发者身份,对评价进行处理和投诉 - -您可以以开发者身份对评价发起投诉,从而获得更高的关注度、更快的响应速度、以及更透明的信息反馈,并最终维护游戏声誉和用户体验,保护您的的权益和利益。 - -您可在开发者中心的 商店 >> 评分评价 >> 评价处理和投诉 ,使用开发者身份参与到评价区的维护治理中。 - -![gameadmin](/img/开发者评价投诉.png) - -### 2.1 使用须知 - -进行评价治理前,请知悉 [TapTap 评价发布规则](https://poster.taptap.cn/r/NB5ZEgOTUgDk.html?f=1) 。 - -### 2.2 处理明细 - -以开发者身份被投诉的评价,将在 60 分钟内得到响应,并展示处理结果。 - -若内容被处理, - - 将展示绿色对勾,并显示处理结果 - - 该内容将从评价列表被移除或折叠 - - 该内容不计入评分 - - 您将会收到该内容被处理的站内信通知 - -若内容未被处理, - - 将展示红色叉,并显示处理结果 - -### 2.3 意见反馈 - -如在使用过程中有任何建议,可以点击链接进行反馈:[开发者调研——TapTap 评分评价相关功能](https://jinshuju.net/f/igjJpY) - -## 3. 遵守平台规则,不诱导好评/请水军刷评 - -TapTap 致力于维护评价内容的真实性,拒绝涉及第三方利益相关的评价。受第三方利益诱导发布的水军评价,以及受开发者竞争对手指使产出的恶意水评,均属于 TapTap 不提倡的评价内容。 - -这类对社区生态有害的内容,我们会根据情节轻重进行折叠或删除操作,减少或停止对其的分发。 - -以上评价均不会计入评分;事后查明有违反平台规则的开发者,也会受到包括但不限于流量屏蔽,降低分发等处罚。 - -## 4. 使用「异常事件评价处理」功能 - -如果您的游戏因异常事件影响、曾被轰炸评价区,您可等待事件结束后,在开发者中心的 商店 >> 评分评价 >> 异常事件评价处理 中发起异常评价处理申请来处理上述情况。 - -发起申请时,您需要选择事件影响的评价时间范围,并详细说明事件的前因后果。 - -我们的审核人员将在收到申请后的 7 个工作日内逐条查看评价,并对满足条件的评价进行标记,以确保它们不会对您的评分产生负面影响。 - -更详细的处理策略,请移步 [开发者服务|TapTap开发者文档](https://developer.taptap.cn/docs/store/operations-skills/store-gameglitch/) - -### 4.1 「异常事件评价处理」申请须知 - -- 您的游戏最近 30 天内遭受过异常事件评价轰炸,且事故已解决或风波已平息; -- 您的游戏最近 90 天内没有刷好评/违规引导好评行为; -- 申请 CD:原则上 **30** 天一次,有特殊情况跟对应运营申请再评估。 \ No newline at end of file diff --git a/docs/store/operations-skills/store-gameglitch.mdx b/docs/store/operations-skills/store-gameglitch.mdx deleted file mode 100644 index a05377c68..000000000 --- a/docs/store/operations-skills/store-gameglitch.mdx +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: 游戏故障应对 -sidebar_position: 9 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -当游戏发生故障,用户无法正常进入游戏时,游戏故障处理功能将成为您的得力助手。 - -此功能能覆盖小型故障到更大范围的故障: - -- 对于小型故障,您可以展示公告告知进入游戏页的用户; -- 如果故障范围扩大,您可以选择屏蔽自然分发或搜索。 - -一旦故障被修复,您可以提交 异常事件评价处理 并选择游戏故障类型。提交后,我们的审核人员会逐条审核游戏故障的评价。如果评价仅讨论了本次故障,审核人员将标记其为游戏故障评价,且不计分。请注意,只要是讨论游戏故障,不论好/中/差评,均会一视同仁处理。 - -此外,为确保用户能够随时享受到高质量的游戏体验,游戏故障处理功能每隔 **60** 天可使用 **1** 次。 \ No newline at end of file diff --git a/docs/store/operations-skills/suggestions.mdx b/docs/store/operations-skills/suggestions.mdx deleted file mode 100644 index 04b9153d4..000000000 --- a/docs/store/operations-skills/suggestions.mdx +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: 如何在不同运营节点提高运营效率 -sidebar_position: 3 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -该申请的资源位都申请了,可以参与的运营合作也都参与了,开发者还可以从哪些方面优化运营策略,来提高自己的运营效率呢?TapTap依据过往的成功运营案例,为各位开发者整理了在游戏各个阶段整理了可以提高运营效率的建议,帮助开发者提升在TapTap游戏社区的活动质量,并在与TapTap的运营合作中尽可能获得更多的流量奖励。 - -## 1. 预约 -### 1.1 详细的游戏详情页配置 -**详情页顶部图:** -- 建议使用以突出该游戏的品牌形象,建议使用游戏主要角色及游戏 Logo 作为该图主要内容 -- 并且确保素材及 Logo 清晰可见 - -**视频及图片:** -- 视频可使用版本更新介绍/CG 宣传/游戏劲爆玩法/人物讲解等方式第一时间抓住玩家的眼球 -- 图片可使用稀有人物介绍/打斗截图/刷怪截图/技能截图搭配相应文案吸引玩家进行下载游戏 -- 除品宣图外,游戏实机画面更能吸引到玩家下载;商业气息浓厚的宣传图及氪金元素的内容易招致玩家的反感,缺乏真实画面的纯 CG 式的概念图容易让玩家无法判断游戏的品质而降低对游戏的认可度 - -**游戏简介:** -- 详尽描述游戏主题,玩法,剧情以及特色等信息 -- 简介中文字请勿空行,以免影响在客户端的展示 -- 请注意排版的美观性,冗长且缺乏分段的文案会令玩家厌烦 - -**开发者的话:** -- 可以分享设计理念,开发历程,游戏故事,游戏彩蛋以及其他真诚的,趣味性较强的内容 -- 附加游戏官网链接,官方社群链接/账号,官方社媒等 - - -具体素材要求可参考:[游戏物料要求|TapTap开发者中心 ](https://developer.taptap.cn/docs/store/release/store-material/) - -优秀案例参考:[火力苏打(T3)| TapTap](https://www.taptap.cn/app/202516) -![](https://capacity-files.lcfile.com/LHkWoy5MGwNQSEP7hY6hf9uxSXqhK1E0/%E8%AF%A6%E6%83%85%E9%A1%B5%E9%85%8D%E7%BD%AE.jpg) - -### 1.2 拓展尽可能多的场景入口 -对于官网和站外宣发两种引流方式,可以尽可能多地露出站内链接,增加站内详情页/活动页入口 -- 官网下载地址更换为效果资源链接。平台侧对官网下载地址更换有特殊扶持,更换为资源置换平台提供的 APK 下载地址即可获得 1 万元流量包奖励;同时官网累计的用户价值有算法加权,相较其他资源能够获得更多的价值 -- 平台提供的链接支持通过 TapTap 下载,同时支持官方包直接下载的形式,因此适配的场景较多;除官网以外,社交媒体也可以作为场景入口,如:微信公众号文章/导航/阅读原文、微博内容发布时带链接. -![](https://capacity-files.lcfile.com/V3PLPHxMaUuBLOiAbCbo1NhPlXjJeUb3/%E5%9C%BA%E6%99%AF%E5%85%A5%E5%8F%A3.jpg) - -### 1.3 站外官号预约/下载帖文置顶 -- 在站外平台完成过认证的官方账号,在通过 REP 后台与 TapTap 进行运营合作,计算内容价值时权重更高,可以置换到更多流量奖励 -- 贴文置顶,高亮等加权行为也将帮助品牌资源在算法计算中获得更高价值,置换到更多流量奖励 -![](https://capacity-files.lcfile.com/9zYfRMjMytq6lLJX4G7W4PQFhI07Utfl/%E7%AB%99%E5%A4%96%E7%BD%AE%E9%A1%B6.png) - -### 1.4 组合外露不同的资源链接 -- 在社交媒体等平台发布内容时,文案内提及 TapTap 下载并同时搭配平台提供的效果链接(如果社区活动,签到活动等) -- 两类资源同时使用时,在价值计算上将由额外加成,组合外露可置换获得更多流量奖励 -![](https://capacity-files.lcfile.com/Sf7LWtitsfcduOn3vYIRcFg72qfgA6qz/%E7%BB%84%E5%90%88%E5%A4%96%E9%9C%B2.png) - -### 1.5 在流量高峰期进行活动发布和流量包投放 -相同的活动内容或相同金额的流量包,在流量高峰期进行投放往往能获得更多自然曝光 -- **工作日流量高峰:10:00-24:00** -- **周末流量高峰:10:00-次日 1:00** - -### 1.6 设计高质量的社区活动 -社区活动是游戏运营中最重要的部分之一,在游戏各个运营阶段的运营动作都需要通过社区活动官宣或配合社区活动进行。因此,越高的社区活动质量,越能为游戏带来更多流量 - -**具体社区活动运营指南:** [TapTap 开发者中心|挑战进阶操作,玩转社区运营](https://developer.taptap.cn/docs/community/advanced/) - -![](https://capacity-files.lcfile.com/GEL9eoP62NBPdJf8V1AvsyNuQKOURsJC/%E8%AE%BE%E8%AE%A1%E5%A5%BD%E6%B4%BB%E5%8A%A8.png) - - -## 2. 测试 -### 2.1 评分维护 -通常在测试发布后会正式解禁游戏评分,除了需要根据测试用户的反馈对游戏进行优化外,适当的评分维护也是运营的重点。评分维持在 **7 分以上**是一款游戏的健康评分区间;**越高的游戏评分,在首页推荐的算法分发权重也越高**。开发者可以通过以下方式进行评分维护: - -- **及时的舆情处理:** 面对测试中出现的舆情问题,如黑客攻击,服务器崩溃,严重bug等,及时正确地公开处理可以帮助提升玩家好感度,提高评分。例如游戏内及站内论坛**发布处理公告并及时更新处理进度**,**公布解决方案**,**公布补偿方案**等。 - -- **日常社区维护:** 在游戏运营的各个阶段,周期性地发布**游戏内容分享**,**社区福利活动**和**发布进度**有助于提升玩家对于游戏的好感度,进而提高评分(包括但不限于游戏资料分享,互动抽奖,测试/首发/新版本发布公告等) - -- **对于优质评价和走心长评及时互动回复**:安排了解游戏的制作人或策划人员参与讨论交流更容易赢取玩家的认可,尽可能**回复长篇评测**以及容易激发玩家共鸣的**高质量评价**,「客服式」的复制黏贴会显得毫无诚意和敷衍 - -- **恶评投诉:** TapTap 开发者中心近期上线了恶评投诉功能,针对一些对游戏的恶意评价,不实评价,水军刷评等行为,可以投诉上报,经核查属实后,评论将被删除。**具体路径:开发者服务 - 商店 - 评分评价 - 评价处理和投诉 - 每条评论右下角❗️- 选择投诉原因** - -- **请勿人为刷评分**:任何评分维护都是基于提升游戏品质和玩家友好度的,TapTap 对刷评分和刷榜行为零容忍,严禁任何形式的刷评价行为。一经确认,游戏将全站限制露出,甚至**要求下架**! - -**舆情处理示例:** - -[bug 处理及补偿](https://www.taptap.cn/moment/345315663721530975) - -**社区维护示例:** - -[互动活动:共创技能](https://www.taptap.cn/moment/293062693554750078);[社区活动](https://www.taptap.cn/moment/408927240529643393);[角色资料](https://www.taptap.cn/moment/410107418626752941) - -![](https://capacity-files.lcfile.com/UhHrQJtexucv8OWyv8k1lHIzz4Ykdf3b/%E4%BC%98%E8%B4%A8%E5%9B%9E%E5%A4%8D.jpg) - - - -:::info 注意 - -单个评价仅可投诉一次,投诉处理结果将通过通知和 UI 显示。 -- 未受理的评价将直接显示投诉未受理在开发者中心 - 评价处理和投诉 - 投诉功能处 -- 已被处理的评价将直接被折叠或者隐藏,处理结果将发通知给到投诉人。 - -::: - - - -### 2.2 游戏故障保护&事后评论维护 -当游戏发生故障,用户无法进入游戏时,可使用 **TapTap 开发者后台**提供的**游戏故障处理**来及时维护 -此功能能覆盖小型故障到更大范围的故障: -- 对于小型故障,您可以展示公告告知进入游戏页的用户故障原因以及修复进度,在设置的时间自动结束展示或提前手动结束; -- 如果故障范围扩大,您可以选择屏蔽自然分发或搜索。 -一旦故障被修复,您可以提交 异常事件评价处理 - 游戏故障类型。提交后,我们的审核人员会逐条审核游戏故障的评价。如果评价仅讨论了本次故障,审核人员将标记其为游戏故障评价,且不计分。请注意,只要是讨论游戏故障,不论好/中/差评,均会一视同仁处理。 - -**注意:为确保用户能够随时享受到高质量的游戏体验,游戏故障处理功能每隔 60 天可使用 1 次。* - -**操作流程指引:** [游戏故障应对](https://developer.taptap.cn/docs/store/operations-skills/store-gameglitch/) - - -## 3. 首发 -### 3.1 评分维护 -首发阶段将会有大批玩家体验游戏并作出评价,因此此阶段的评分维护亦非常重要。详细见 2.1. - -### 3.2 尝试缩小包体大小 -过大的下载包体可能会导致玩家下载完成率低,可通过转换成小包体提升下载完成率 - -**包体大小与下载完成率的关系参考:** - -包体大小/MB|下载完成率 ----|--- -500-800|80.10% -800-1000|70.30% -1000-1200|63.20% -1200-1400|58.30% -1400-1600|54.20% -1600-1800|50.70% - -### 3.3 认真详尽的攻略 tab -- **提前进行攻略储备:** 在正式发布之前就搭建好攻略站,开发者可以邀请版主一起产出一部分优质攻略做兜底,以吸引更多玩家使用攻略 tab 并产出更多攻略内容 -- **尽量使用所有攻略 tab 组件**(工具箱,信息板,索引块),充分发挥攻略站功能 -- **多样化攻略形式:** 视频与图文结合 -- **明确的攻略分类:** 关卡,角色,任务,道具等都是很好的分类方式 -- **攻略tab优质案例:** [香肠派对攻略 | TapTap 发现好游戏](https://www.taptap.cn/app/58881/strategy) -![](https://capacity-files.lcfile.com/Czr2beRXDT9lKo06Mf7N3di8a8Ji8UO3/%E6%94%BB%E7%95%A5tab%E9%85%8D%E7%BD%AE.png) - -### 3.4 首发福利活动打包推送 -首发是游戏运营的重要节点,因此在此阶段的运营动作和福利活动宜组合打包进行 -- 普发礼包、签到活动、社区活动等首发活动应组合上线,以实现多维度,多渠道吸引玩家 -- 上述福利可以通过集合为福利合集,作为推荐位在论坛版块置顶。具体操作流程请见:[首发社区运营](https://developer.taptap.cn/docs/store/events/events-handbook/)以及[2.2 强力信息板:推荐位](https://developer.taptap.cn/docs/community/features/) -- 为福利板块申请资源位曝光,详见:[上“活动中心”栏目 | TapTap 开发者文档](https://developer.taptap.cn/docs/store/operations-skills/event-center/) -- 福利打包案例:[火炬之光:无限 - TapTap](https://www.taptap.cn/app/172664/topic) -![](https://capacity-files.lcfile.com/TkwAXUJsnJzwp4dXSkK9hxhzU9wwn5GS/%E6%89%93%E5%8C%85.png) - - -## 4. 长线运营 -### 4.1 加强签到活动的外部宣发 -签到活动的价值由论坛日度活跃、签到活动数据、奖励力度共同决定;多在活动的基础上进行外部宣发,能获得更高的活动价值,置换获得更多流量奖励 -![](https://capacity-files.lcfile.com/yJlLp4eh4HyK0udexcOd1yd0cxLAODmn/%E7%AD%BE%E5%88%B0%E5%AE%A3%E5%8F%91.png) - -### 4.2 素材更新更加频繁 -针对游戏内容变化频繁地更新详情页素材,有助于提高游戏的下载转化 - -### 4.3 常规活动流量循环投入 -由签到活动,社区活动等效果资源置换获得的流量包,可以再次投放在常规活动中,实现良性循环 - -### 4.4 详尽的新版本详情页 -站内新版本详情页页面不仅提供了多种模块功能供开发者选择,更是曝光量极高的资源位之一。因此版本更新时优质的的新版本详情页配置是帮助游戏获得更多流量的重要策略 - -- **新版本内容介绍:** 通常包含新版本涉及的所有更新(地图,角色,玩法,道具等) - -- **新版本内容 highlight:** 对于新版本内容中最有吸引力的,最期望推广的内容进行单独阐述介绍 - -- **新版本活动介绍:** 版本更新通常会配合新版本预约/上线活动,因此可以在版本详情页中附上活动链接并简述活动内容 - -- **过往版本回顾:** 对于过往版本更新的合集,方便玩家查看 - -- **多样化内容素材:** 图文与视频结合 - -- **新版本详情页配置指南:** [长线运营手册:一、4 新版本详情页](https://developer.taptap.cn/docs/store/long-termops/long-term/) - -- **优秀新版本详情页案例:** [香肠派对-新版本详情页](https://www.taptap.cn/game-event/1513);[古魂-新版本详情页](https://www.taptap.cn/game-event/1449);[原神-新版本详情页](https://www.taptap.cn/game-event/1448) - -![](https://capacity-files.lcfile.com/A2ANqKCT4Am4KBl5HsylkqWDCTblk6mn/%E6%96%B0%E7%89%88%E6%9C%AC%E8%AF%A6%E6%83%85%E9%A1%B5%E9%85%8D%E7%BD%AE.png) - diff --git a/docs/store/operations-skills/today.mdx b/docs/store/operations-skills/today.mdx deleted file mode 100644 index a59f6d82c..000000000 --- a/docs/store/operations-skills/today.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: 如何获得「今日游戏」栏目的曝光 -sidebar_position: 4 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -## 什么是「今日游戏」? - -为了尽可能让所有好游戏都能够获得足够的曝光,TapTap 在首页新增了「今日游戏」栏目以替代之前的「即将上线」。今日游戏能够承载游戏生命周期里的首曝、测试、首发、更新、折扣等各个关键节点的游戏曝光需求,信息更加全面,与用户的关联性更强。开发者仅需要按规范操作即可自动获得「今日游戏」栏目的基础曝光。 - - - -## 我的游戏如何才能展示在「今日游戏」? - -游戏在各运营节点完成对应的操作流程即可自动展示在「今日游戏」栏目中并获得基础曝光量。 - -TapTap编辑将根据游戏品质调整游戏对外展示的卡片大小以及展示排序。 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    游戏运营节点操作流程备注
    开放预约游戏详情页正式上架且开放预约状态后,将自动展示在「今日游戏」,所有用户可见
    封闭式测试 -

    测试招募

    -
      -
    • 开发者后台完成新建测试计划和招募计划后,在招募开始时游戏卡片将自动展示在「今日游戏」
    • -
    • 将展示给所有用户,招募期结束后卡片自动下架
    • -
    -
    -
      -
    • 您配置的封闭测试计划(测试招募、先到先得)和开放式测试计划在90天内最多只能在「今日游戏」展示一次;若游戏在90天内有多次测试计划,系统将默认外显第一次的测试信息,如需调整,请提前发起工单申请或联系对接运营
    • -
    -
    -

    指定用户封闭测试

    -
      -
    • 开发者后台完成新建测试计划并发放资格后,游戏卡片将自动展示在「今日游戏」
    • -
    • 仅展示给拥有测试资格的用户,测试期结束后卡片自动下架
    • -
    -
    -

    先到先得测试

    -
      -
    • 开发者后台完成新建测试计划后,在测试开始时游戏卡片将自动展示在「今日游戏」
    • -
    • 将展示给所有用户,资格发放完毕后卡片自动下架
    • -
    • 已获得资格的用户仍可见该卡片
    • -
    -
    开放式测试 -
      -
    • 开发者后台完成新建测试计划后,在测试开始时游戏卡片将自动展示在「今日游戏」
    • -
    • 将展示给所有用户,测试期结束后卡片自动下架
    • -
    -
    正式上线 -
      -
    • 若为定时发布版本:游戏在开发者后台完成开放下载审核并设置定时上架后,详情页卡片自动展示在上架日的「今日游戏」栏目
    • -
    • 若为立即上架版本:游戏在开发者后台完成提包并审核上架后,详情页卡片将立即自动展示在当日的「今日游戏」栏目
    • -
    • 将展示给所有用户
    • -
    -
    版本更新 -
      -
    • 开发者后台完成配置新版本详情页并被审核通过后,游戏卡片将在「今日游戏」栏目中展示给关注过游戏的用户
    • -
    • 累计下载量 ≥100万 的游戏每年有 2次 申请全量展示版更卡片的机会(两次更新全量展示间隔时间应 > 3 个月)
    • -
    -
    付费下载游戏折扣 -
      -
    • 目前仅支持运营人工更新,如有需求请提前发起工单申请上架折扣信息或联系对接运营
    • -
    -
    -
    - -## 什么样的游戏会被「今日游戏」下架? - -- 传奇、素材质量极差,与 TapTap 调性极为不符的游戏; -- 游戏详情页仅有 5 图或图片不全,素材质量较差,不包含任何实机,完全不展示游戏玩法; -- 素材物料纯 AI,并且和实机内容完全不符的; -- 违反 TapTap 游戏上架规则,例如刷分等恶劣情况。 - -## 常见问题Q&A - -**Q:我已经按照要求进行了后台操作,为什么我的游戏卡片还是没有展示在「今日游戏」?** - -**A:**未来事件编辑会根据品质决定是否提前露出,并保证已预约,已关注,已获得资格用户一定露出,因此建议开发者在重大运营节点至少提前一天完成后台操作,否则可能没有办法及时获得「今日游戏」的曝光 - -「今日游戏」仅能够展示当日 T-15 至 T+15 的游戏卡片(T为当日),如果游戏事件外显时间在未来 15 天后,则无法在游戏日历展示,直至游戏事件上线日进入 T+15 范畴 (即将上线子栏目可最多展示至 T+30) - -个别事件游戏日历读取后台数据有一定的延迟,请稍等5-10分钟后刷新查看 - -**Q:暂不方便传包更新后台状态,但希望提前展示游戏状态,该怎么办?** - -**A:**在新游宣发期内,如果希望在未来时能显示游戏的状态(仅限首发游戏),但当下还没有在开发者后台提交包体变更的。可以通过开发者后台的工单系统申请提前在「今日游戏」栏目展示游戏的首发信息,运营会统一审核。 - -**请注意,务必在约定的时间完成包体变更,否则事件将会失效。** - -**Q:申请「今日游戏」栏目需要提交新的素材吗?** - -**A:**不需要,「今日游戏」自动读取游戏详情页的推广图素材。值得注意的是,部分展示卡片会对推广图进行 4:3 的剪裁,因此在上传素材时,请尽量按照图示安全区进行设计 - -![](https://capacity-files.lcfile.com/brQGppHlXQk3MxwDJGY7LogbAIuIUcgM/image.png) - -**Q:曾经上架过预约,但游戏因为更换厂商/厂商重组等原因重新开放预约的,可以上架「今日游戏」吗?** - -**A:**可以,如有需求请提前发起工单申请或联系对接运营。 - -**Q:「今日游戏」上线后,我还能继续申请「即将上线」资源位吗?** - -**A:**不可以,「今日游戏」栏目将代替「即将上线」资源位,「即将上线」栏目在最新版本TapTap App已不可见,邮件申请通道也已经关闭,发送邮件将会收到如下回复: - -> **开发者朋友你好:** -> -> 感谢你对「即将上线」栏目的支持,我们现已将该栏目升级为「今日游戏」,关于「今日游戏」如何上架等事宜请点击开发者文档-今日游戏查看,「即将上线」邮件通道现已关闭,不再受理后续申请,此邮件为自动回复,再次感谢你对 TapTap 的支持! -> -> **TapTap 编辑团队** diff --git a/docs/store/permissions.mdx b/docs/store/permissions.mdx deleted file mode 100644 index c699df74e..000000000 --- a/docs/store/permissions.mdx +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: TapTap 权限说明 -sidebar_position: 1.1 ---- - -**读取手机状态和身份** - -允许应用访问设备的电话功能。此权限可让应用确定本机号码和设备ID、是否正处于通话状态以及拨打的号码。 - -**获取某一程序占用空间容量** - -允许一个程序获取应用存储空间容量 - -**查看WLAN状态** - -允许程序访问WLAN网络状态信息 - -**读取系统日志文件** - -允许程序读取底层系统日志文件 - -**检索当前运行的应用程序** - -允许应用检索近期和当前运行的任务信息。此权限可让该应用了解设备上使用了哪些应用 - -**完全的网络访问权限** - -允许该应用创建网络套接字和使用自定义网络协议。浏览器和其他某些应用提供了向互联网发送数据的途径,因此应用无需该权限即可向互联网发送数据 - -**防止手机休眠** - -允许该应用阻止手机进入休眠状态 - -**写入外部存储** - -允许程序写入外部存储,如SD卡上写文件 - -**控制震动** - -允许应用控制振动设备 - -**查看网络状态** - -允许应用程序查看所有网络的状态。例如存在和连接的网络 - -**拍摄照片和视频** - -允许访问摄像头进行拍照或录制视频 - -**访问大致位置信息(基于网络进行定位)** - -允许应用根据网络来源(例如基站和 WLAN 网络)获取您的位置信息。您的手机必须支持并开启这些位置信息服务,此应用才能使用这些服务 - -**访问精确位置(基于GPS和网络)** - -允许应用根据GPS或网络来源(例如基站和WLN网络)获取您的位置信息。您的手机必须支持并开启这些位置信息服务,应用才能使用这些服务。这可能会增加耗电量 - -**获取额外的位置信息提供程序命令** - -允许该应用程序使用其他位置信息提供程序命令。此权限使该应用可以干扰GPS或其他位置信息源的运作 - -**访问模拟定位信息** - -创建用于测试的模拟位置源,这允许应用程序覆盖其他位置源(如GPS或位置提供商)返回的位置或状态 - -**与蓝牙设备配对** - -允许该应用查看手机上的蓝牙配置,以及与配对设备建立连接或接受其连接请求 - -**访问蓝牙设置** - -允许应用配置本地蓝牙手机,并允许其查找远程设备且与之配对 - -**发送持久广播** - -允许应用程序发送持久广播,在广播结束后仍然保留。过度使用可能会导致手机使用过多的内存,从而使手机变慢或不稳定 - -**更改用户界面设置** - -允许应用程序更改当前配置,例如语言设置或整体的字体大小 - -**允许接受WLAN多播** - -允许应用程序使用多播地址接收发送到WLAN网络上所有设备(而不仅仅是您的手机)的数据包 - -**停用屏幕锁定** - -允许应用程序禁用密钥锁和任何相关的密码安全性。例如,当接到来电时,手机会禁用钥匙锁,然后在通话结束后重新启用钥匙锁 - -**闪光灯** - -允许访问闪光灯 - -**更改音频设置** - -允许应用程序修改全局音频设置,例如音量和用于输出的扬声器 - -**读取同步设置** - -允许应用程序读取帐户的同步设置。例如,这可以确定应用程序是否与帐户同步 - -**录音** - -允许该应用使用麦克风录制音频 - -**结束后台进程** - -允许应用程序结束其他应用程序的后台进程。这可能会导致其他应用程序停止运行 - -**修改系统设置** - -允许应用程序修改系统的设置数据 - -**更改WLAN状态** - -允许应用程序连接到WLAN接入点以及与WLAN接入点断开连接,并对配置的WLAN网络进行更改 - -**更改网络连接** - -允许该应用更改网络连接的状态 - -**结束后台进程** - -允许应用结束其他应用的后台进程。此权限可导致其他应用停止运行 - -**对正在运行的应用重新排序** - -允许应用将任务移动到前台和后台。该应用可能不经您的命令自行执行此操作 - -**让应用程序始终运行** - -允许应用程序使其自身的某些部分在内存中始终运行。这会限制其他应用程序的可用内存,从而降低手机的速度 - -**装载和卸载文件** - -允许安装和卸载文件系统可移动存储文件 - -**写入同步设置** - -允许应用程序修改帐户的同步设置。例如,这可用于启用人员应用程序与帐户的同步 diff --git a/docs/store/release/_category_.json b/docs/store/release/_category_.json deleted file mode 100644 index dc4a52296..000000000 --- a/docs/store/release/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "上架游戏须知", - "collapsed": true, - "position": 3 -} diff --git a/docs/store/release/bundle.mdx b/docs/store/release/bundle.mdx deleted file mode 100644 index b36225a61..000000000 --- a/docs/store/release/bundle.mdx +++ /dev/null @@ -1,271 +0,0 @@ ---- -title: TapTap 打包规范 -sidebar_position: 5 ---- - -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; - -## 支持 64 位架构 - -自 **2022 年 7 月 27 日**起,您在 TapTap 上发布的应用必须支持 ARM 64 位架构。 64 位 CPU 能够为您的用户提供更快、更丰富的体验。添加 64 位的应用版本不仅可以提升性能、为未来创新创造条件,还能针对仅支持 64 位架构的设备做好准备。 - -本指南参考 Google Play 的指引,介绍了 32 位应用如何支持 64 位设备,供您随时采用。 - -## 评估您的应用 - -如果您的应用仅使用以 Java 编程语言或 Kotlin 编写的代码(包括所有库或 SDK),那么就表示该应用已经能支持 64 位设备。如果您的应用使用了任何原生代码,或者您不确定应用是否使用了这类代码,那么您需要评估应用并采取措施。 - -### 您的应用是否使用了原生代码? - -首先需要检查您的应用是否使用了任何原生代码。 如果您的应用符合以下情况,便是使用了原生代码: - -- 使用了任何 C/C++(原生)代码。 -- 与任何第三方原生库关联。 -- 通过使用原生库的第三方应用构建程序构建而成。 - -### 应用是否包含 64 位库? - -若要确定应用是否包含 64 位库,最简单的方法就是检查 APK 文件的结构。在构建时,APK 会与应用所需的所有原生库打包在一起。原生库会根据 [ABI](https://developer.android.com/ndk/guides/abis?hl=zh-cn#sa) 存储在不同的文件夹中。您的应用不一定要支持所有 64 位架构,但对于支持的每种原生 32 位架构,应用都必须包含相应的 64 位架构。 - -对于 ARM 架构,32 位库位于 **armeabi-v7a** 中。 对应的 64 位库则位于 **arm64-v8a** 中。 - -对于 x86 架构,32 位库位于 **x86** 中,64 位库则位于 **x86_64** 中。 - -首先要确保这两个文件夹中都有原生库。总结如下: - -| 平台 | 32 位库文件夹 | 64 位库文件夹 | -| ---- | ----------------- | --------------- | -| ARM | `lib/armeabi-v7a` | `lib/arm64-v8a` | -| x86 | `lib/x86` | `lib/x86_64` | - -请注意,每个文件夹中的一套库可能完全相同,也可能不完全相同,这取决于应用的具体情况。您应达到的目标是确保您的应用能够在仅支持 64 位架构的环境中正常运行。 - -通常情况下,同时针对 32 位和 64 位架构构建的 APK 或软件包会具有这两种 ABI 的文件夹,每个文件夹中都有一套相应的原生库。如果您的应用不支持 64 位架构,那么您很可能会看到 32 位 ABI 文件夹,但没有 64 位文件夹。 - -### 使用 APK 分析器查找原生库 - -[APK 分析器](https://developer.android.com/studio/debug/apk-analyzer?hl=zh-cn)是一款可用于对所构建的 APK 进行各方面评估的工具。针对我们目前所讨论的情况,我们将使用该工具查找原生库,以确定是否具备 64 位库。 - -1. 打开 Android Studio,然后**打开任一项目**。 -2. 从菜单中依次选择 **Build > Analyze APK…** -3. 选择您要评估的 APK。 -4. 查看 **lib** 文件夹,您可以在其中找到“.so”文件。如果在您的应用中找不到任何“.so”文件,则说明该应用已支持 64 位架构,您无需采取进一步措施。如果您看到 **armeabi-v7a** 或 **x86**,则说明您有 32 位库。 -5. 检查是否 **arm64-v8a** 或 **x86_64** 文件夹中有类似的“.so”文件。 -6. 如果您没有任何 **arm64-v8a** 或 **x86_64** 库,则需要更新构建流程以开始构建并打包 APK 中的这些工件。 - -### 通过解压缩 APK 查找原生库 - -APK 文件的结构类似于 ZIP 文件,可以像 ZIP 文件一样解压缩。 如果您更喜欢使用命令行或任何其他解压缩工具,也可以采用解压缩 APK 的方法。 - -只需解压缩 APK 文件(根据您使用的解压缩工具,您可能需要将其重命名为 .zip),然后按照上文中的指南浏览解压缩后的文件,即可确定您的应用是否已经为支持 64 位设备做好准备了。 - -例如,您可以从命令行中运行如下命令: - -``` -:: Command Line > zipinfo -1 YOUR_APK_FILE.apk | grep \.so$ lib/armeabi-v7a/libmain.so lib/armeabi-v7a/libmono.so lib/armeabi-v7a/libunity.so lib/arm64-v8a/libmain.so lib/arm64-v8a/libmono.so lib/arm64-v8a/libunity.so -``` - -请注意,此示例中存在 **armeabi-v7a** 库和 **arm64-v8a** 库,这表明该应用支持 64 位架构。 - -## 使用 64 位库构建应用 - -下面针对构建 64 位库做出了相关的说明。不过,需要指出的是,以下内容仅介绍了如何构建在源代码的基础上可构建的代码和库。 - -如果您使用任何外部 SDK 或库,请确保按照[上文所述的步骤](https://developer.android.com/distribute/best-practices/develop/64-bit?hl=zh-cn#assess-your-app)使用 64 位版本。如果没有 64 位版本可用,请与相应 SDK 或库的所有者联系,并在规划支持 64 位设备的方案时将这一点考虑在内。 - -### 使用 Android Studio 或 Gradle 进行构建 - -大多数 Android Studio 项目都使用 Gradle 作为底层构建系统,因此本部分适用于使用这两种工具进行构建的情况。针对原生代码进行构建很简单,只需将 **arm64-v8a** 和/或 **x86_64**(视您要支持的架构而定)添加到应用的“build.gradle”文件中的 [ndk.abiFilters](https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.NdkOptions.html) 设置中即可: - - - - -``` -// Your app's build.gradle -plugins { - id 'com.android.app' -} - -android { - compileSdkVersion 27 - defaultConfig { - appId "com.google.example.64bit" - minSdkVersion 15 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - ndk.abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64' -// ... -``` - - - - -``` -// Your app's build.gradle -plugins { - id("com.android.app") -} - -android { - compileSdkVersion(27) - defaultConfig { - appId = "com.google.example.64bit" - minSdkVersion(15) - targetSdkVersion(28) - versionCode = 1 - versionName = "1.0" - ndk { - abiFilters += listOf("armeabi-v7a","arm64-v8a","x86","x86_64") - } -// ... -``` - - - - -### 使用 CMake 进行构建 - -如果您的应用是使用 [CMake](https://developer.android.com/ndk/guides/cmake?hl=zh-cn#options) 构建的,那么您可以通过将 **arm64-v8a** 传递到“-DANDROID_ABI”参数来针对 64 位 ABI 进行构建: - -``` -:: Command Line > cmake -DANDROID_ABI=arm64-v8a … or > cmake -DANDROID_ABI=x86_64 … -``` - -在使用 `externalNativeBuild` 时,此方法无效。请参阅[使用 Gradle 进行构建](https://developer.android.com/distribute/best-practices/develop/64-bit?hl=zh-cn#building_with_android_studio_or_gradle)部分。 - -### 使用 ndk-build 进行构建 - -如果您的应用是使用 [ndk-build](https://developer.android.com/ndk/guides/ndk-build?hl=zh-cn) 构建的,那么您可以使用 `APP_ABI` 变量修改“[Application.mk](https://developer.android.com/ndk/guides/application_mk?hl=zh-cn)”文件,从而针对 64 位 ABI 进行构建: - -``` -APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 -``` - -在使用 `externalNativeBuild` 时,此方法无效。请参阅[使用 Gradle 进行构建](https://developer.android.com/distribute/best-practices/develop/64-bit?hl=zh-cn#building_with_android_studio_or_gradle)部分。 - -### 将 32 位代码移植到 64 位架构 - -如果您的代码已经可以在桌面或 iOS 平台上运行,那么您无需针对 Android 做额外的工作。如果这是第一次针对 64 位系统构建您的代码,那么您需要解决的主要问题是指针不再适合 `int` 这样的 32 位整数类型。您将需要更新以 `int`、`unsigned` 或 `uint32_t` 等类型存储指针的代码。在 Unix 系统上,`long` 对应的是指针大小,但在 Windows 上并非如此,因此您应该改用释意类型 `uintptr_t` 或 `intptr_t`。使用 `ptrdiff_t` 类型来存储两个指针之间的差异。 - -您应该始终选择使用 [``](https://en.cppreference.com/w/c/types/integer) 中定义的特定固定宽度整数类型,而不是 `int` 或 `long` 等传统类型,即便对于非指针也应如此。 - -使用以下编译器标记来捕捉代码在指针和整数之间转换不正确的情况: - -``` --Werror=pointer-to-int-cast -Werror=int-to-pointer-cast -Werror=shorten-64-to-32 -``` - -具有 `int` 字段(包含指向 C/C++ 对象的指针)的 Java 类也有同样的问题。在 JNI 源代码中搜索 `jint`,并确保切换到 `long`(Java 端)和 `jlong`(C++ 端)。 - -**注意**:因指针被截断而引起的崩溃将表现为 SIGSEGV,其中错误地址的前 32 位全部为零。 - -对于 64 位代码而言,隐式函数声明的危险性要高得多。C/C++ 假定隐式声明的函数(即编译器未检测到声明的函数)的返回值类型为 `int`。如果函数的实际返回值类型是指针,那么在 32 位系统上是可行的,因为在 32 位系统中指针的类型为 `int`,但在 64 位系统中,编译器会丢弃指针的前半部分。例如: - -``` -// This function returns a pointer: -// extern char* foo(); - -// If you don't include a header that declares it, -// when the compiler sees this: -char* result = foo(); - -// Instead of compiling that to: -result = foo(); - -// It compiles to something equivalent to: -result = foo() & 0xffffffff; - -// Which will then cause a SIGSEGV if you try to dereference `result`. -``` - -以下编译器标记会将隐式函数声明警告变成错误,以便您能够更轻松地查找和解决此问题: - -``` --Werror=implicit-function-declaration -``` - -如果您有内联汇编程序,您需要重新编写该程序或使用普通的 C/C++ 实现。 - -如果您对类型大小进行了硬编码(例如,8 或 16 字节),请使用等效的 `sizeof(T)` 表达式(例如 `sizeof(void*)`)来替换它们。 - -如果需要有条件地编译不同于 64 位的 32 位代码,则对于一般性的 32/64 差异,您可以使用 `#if defined(__LP64__)`;对于 Android 支持的具体架构,可以使用 `__arm__`、`__aarch64__` (arm64)、`__i386__` (x86) 和 `__x86_64__`。 - -您需要调整类似 `printf` 或 `scanf` 的函数的格式字符串,因为如果使用传统的格式说明符,您无法以一种对 32 位和 64 位设备都正确的方式来指定 64 位类型。您可利用 [``](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/inttypes.h.html) 中的 `PRI` 和 `SCN` 宏来解决此问题,`PRIxPTR` 和 `SCNxPTR` 分别用于写入/读取十六进制指针,`PRId64` 和 `SCNd64` 分别用于以可移植的方式写入/读取 64 位值。 - -在移位时,您可能需要使用 `1ULL` 来获取要移位的 64 位常数,而不能使用仅支持 32 位的 `1`。 - -### 游戏开发者 - -我们知道,迁移第三方游戏引擎是一个耗费人力的过程,并且需要很长的准备时间。庆幸的是,三大最常用的引擎目前都支持 64 位架构: - -- Unreal(自 2015 年起) -- Cocos2d(自 2015 年起) -- Unity(自 2018 年起) - -### Unity 开发者 - -#### 升级到支持的版本 - -Unity 自版本 [2018.2](https://blogs.unity3d.com/2018/07/10/2018-2-is-now-available/) 和 [2017.4.16](https://unity3d.com/unity/whatsnew/unity-2017.4.16) 开始提供 64 位支持。 - -如果您发现自己使用的 Unity 版本不支持 64 位架构,请确定要升级到的版本,并按照 Unity 提供的[指南](https://docs.unity3d.com/Manual/UpgradeGuides.html)迁移您的环境,确保将您的应用升级到可构建 64 位库的版本。Unity 建议您[升级到该编辑器的最新 LTS 版本](https://blogs.unity3d.com/2018/04/09/new-plans-for-unity-releases-introducing-the-tech-and-long-term-support-lts-streams/),以获取最新的功能和更新。 - -下面的图表概述了 Unity 的各个版本以及您应该采取的措施: - -| Unity 版本 | 版本是否支持 64 位? | 建议采取的措施 | -| ------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------- | -| 2022.x | ✔️ | 确保您的构建设置能够输出 64 位库。 | -| 2021.x | ✔️ | 确保您的构建设置能够输出 64 位库。 | -| 2020.x | ✔️ | 确保您的构建设置能够输出 64 位库。 | -| 2019.x | ✔️ | 确保您的构建设置能够输出 64 位库。 | -| 2018.4 (LTS) | ✔️ | 确保您的构建设置能够输出 64 位库。 | -| 2018.3 | ✔️ | 确保您的构建设置能够输出 64 位库。 | -| 2018.2 | ✔️ | 确保您的构建设置能够输出 64 位库。 | -| 2018.1 | ➖ | 提供实验性的 64 位支持。 | -| 2017.4 (LTS) | ✔️ | 自 [2017.4.16](https://unity3d.com/unity/whatsnew/unity-2017.4.16) 开始支持。 确保您的构建设置能够输出 64 位库。 | -| 2017.3 | ✖️ | 升级到支持 64 位的版本。 | -| 2017.2 | ✖️ | 升级到支持 64 位的版本。 | -| 2017.1 | ✖️ | 升级到支持 64 位的版本。 | -| <=5.6 | ✖️ | 升级到支持 64 位的版本。 | - -#### 更改构建设置以输出 64 位库 - -如果您使用的 Unity 版本支持 64 位的 Android 库,那么您可以通过调整构建设置来生成 64 位版本的应用。您还需要使用 IL2CPP 后端作为 Scripting Backend。要为构建 64 位架构而设置 Unity 项目,请按以下步骤操作: - -1. 转到 **Build Settings**,然后确认 Unity 标志是否显示在 **Platform** 下的 **Android** 旁边,以确保您是在针对 Android 进行构建。 - - 如果 Unity 标志未显示在 Android 平台旁边,请选择 **Android**,然后点击 **Switch Platform**。 -2. 点击 **Player Settings**。 -3. 依次转到 **Player Settings Panel > Settings for Android > Other Settings > Configuration** -4. 将 **Scripting Backend** 设为 **IL2CPP**。 -5. 依次选择 **Target Architecture > ARM64** 复选框。 -6. 照常构建! - -请注意,针对 ARM64 进行构建需要您专门针对该平台构建您的所有资产。请按照 Unity 的[指南](https://docs.unity3d.com/Manual/ReducingFilesize.html)来缩减 APK 大小,同时考虑利用 [Android App Bundle](https://developer.android.com/platform/technology/app-bundle?hl=zh-cn) 功能来减小大小增加量。 - -## 合并 APK 和 64 位合规性 - -如果您要使用 Google Play 的[合并 APK 支持](https://developer.android.com/google/play/publishing/multiple-apks?hl=zh-cn)来发布应用,请注意在版本层面评估是否符合 64 位要求。不过,如果 APK 或 app bundle 不会分发给搭载 Android 9 Pie 或更高版本的设备,则不适用 64 位要求。 - -如果您的某个 APK 被标记为不合规,但该 APK 比较老旧且无法使其合规,一种策略是在该 APK 清单的 [`uses-sdk`](https://developer.android.com/guide/topics/manifest/uses-sdk-element?hl=zh-cn) 元素中添加 `maxSdkVersion="27"` 属性。这样一来,此 APK 将不会被分发给搭载 Android 9 Pie 或更高版本的设备,因而也就不会再妨碍合规。 - -## RenderScript 和 64 位合规性 - -如果您的应用使用 RenderScript 并且是通过较低版本的 Android 工具构建的,该应用可能会存在 64 位合规性问题。使用版本低于 21.0.0 的构建工具时,编译器可能会将生成的位码放到外部 `.bc` 文件中。64 位架构不再支持这些旧的 `.bc` 文件,因此,如果您的 APK 中有这类文件,就会造成合规性问题。 - -要解决此问题,请移除项目中的所有 `.bc` 文件,将环境升级到 `build-tools-21.0.0` 或更高版本,并将 Android Studio 中的 [`renderscriptTargetApi`](https://developer.android.com/guide/topics/renderscript/compute?hl=zh-cn) 设为 21+,以指示编译器不要生成 `.bc` 文件。然后,重新构建您的应用,检查是否有 `.bc` 文件,再将应用上传到 Play 管理中心。 - -## 在 64 位硬件上测试应用 - -64 位版本的应用应提供与 32 位版本相同的质量和功能集。请对您的应用进行测试,以确保使用最新的 64 位设备的用户能够在您的应用中获得优质的体验。 - -要开始测试您的应用,您要有支持 64 位架构的设备。时下有很多支持 64 位架构的热门设备,例如 Google 的 Pixel 以及其他旗舰设备。 - -最简单的 APK 测试方法就是使用 adb 安装该应用。大多数情况下,您可以提供 `--abi` 作为参数,用以指示要将哪些库安装到设备上。这样在设备上安装该应用时便会仅包含 64 位库。 - -``` -:: Command Line # A successful install: > adb install --abi armeabi-v7a YOUR_APK_FILE.apk Success # If your APK does not have the 64-bit libraries: > adb install --abi arm64-v8a YOUR_APK_FILE.apk adb: failed to install YOUR_APK_FILE.apk: Failure [INSTALL_FAILED_NO_MATCHING_ABIS: Failed to extract native libraries, res=-113] # If your device does not support 64-bit, an emulator, for example: > adb install --abi arm64-v8a YOUR_APK_FILE.apk ABI arm64-v8a not supported on this device -``` - -安装成功后,请照常对应用进行测试,以确保其质量与 32 位版本相同。 diff --git a/docs/store/release/metrics.mdx b/docs/store/release/metrics.mdx deleted file mode 100644 index 80c57e9e3..000000000 --- a/docs/store/release/metrics.mdx +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: 商店数据指标定义 -sidebar_position: 6 ---- - -该模块主要展示游戏在 TapTap 平台中涉及商店业务的关键指标数据。 - -## 实时数据总量 - -指标名称|定义 --|- -浏览量|游戏详情页在 TapTap 平台(含客户端、Web 端)的总体曝光浏览量 -下载请求量|游戏在 TapTap 平台的 APK 下载请求量和 iOS 转化量的累计之和。(该数据经过平台风控过滤、以及对设备进行去重处理) -预约量|游戏在 TapTap 平台的预约量累计之和。(该数据经过平台风控过滤、以及对用户进行去重处理) -广告下载和预约量|游戏在 TapTap 客户端(不含 Web 端数据)的投放效果数据,此处仅统计近半年的数据,更详细数据请前往 TapAD 查看。 - -## 浏览量 - -指标名称|定义 --|- -浏览量|游戏详情页在 TapTap 平台(含客户端、Web 端)的总体曝光浏览量,该指标可区分平台内不同位置。 - -## 下载请求量 - -游戏在 TapTap 平台的下载请求数据。 - -来源|指标|定义 --|-|- -安卓|APK 新增下载请求量|游戏 APK 在 TapTap 安卓端的新增下载请求量(不包含 Web 端的下载请求) -安卓|APK 新增下载完成量|游戏 APK 在 TapTap 安卓端的新增下载完成量 -安卓|APK 更新下载请求量|游戏 APK 在 TapTap 安卓端的更新下载请求量 -安卓|APK 更新下载完成量|游戏 APK 在 TapTap 安卓端的更新下载完成量 -iOS|iOS 转化量|游戏在 TapTap iOS 端唤起苹果商店游戏详情页的转化量(包含 Web 端的苹果商游戏详情页店唤起量) -全部|总体新增下载请求量|游戏在 TapTap 平台的新增 APK 下载请求量和 iOS 转化量之和。 - -说明:上述指标也同步提供平台处理后(经过平台风控过滤、对设备进行去重处理)的数据。 - -## 预约量 - -游戏在 TapTap 平台(含安卓端、iOS 端和 Web 端)的预约数据。 - -来源|指标|定义 --|-|- -安卓|预约量(安卓)|游戏在 TapTap 安卓端的预约量(包含 Web 端预约游戏安卓版本的数量) -安卓|取消预约量(安卓)|游戏在 TapTap 安卓端的取消预约量(包含 Web 端取消预约游戏安卓版本的数量) -iOS|预约量(iOS)|游戏在 TapTap iOS 端的预约量(包含 Web 端预约游戏 iOS 版本的数量) -iOS|取消预约量(iOS)|游戏在 TapTap iOS 端的取消预约量(包含 Web 端取消预约游戏 iOS 版本的数量) -其他|预约量(其他)|游戏在 TapTap 平台活动、直播等页面产生的预约量 -其他|取消预约量(其他)|游戏在 TapTap 平台活动、直播等页面产生的取消预约量 -全部|预约总量|游戏在 TapTap 平台的总体预约量。 -全部|取消预约总量|游戏在 TapTap 平台的取消预约总量。 - -说明:上述指标也同步提供平台处理后(经过平台风控过滤、对用户进行去重处理)的数据。 - -## 转化效果数据 - -游戏在 TapTap 安卓客户端和 iOS 端内的转化数据,此处不包含非 TapTap 平台内的转化场景数据。 - -### 指标定义 - -来源|指标|定义 --|-|- -安卓|曝光量|游戏素材(icon、大图、标签等)在 TapTap 安卓端的曝光量。 -安卓|点击量|点击游戏素材进入游戏详情页的数量 -安卓|点击率|点击量/曝光量 -安卓|详情页转化量|仅在游戏详情页曝光后的转化量,在首页推荐的位置,详情页转化量约等于转化总量。(由于 TapTap 客户端存在一个历史版本,支持在首页列表直接转化,所以数据存在轻微差异) -安卓|详情页转化率|详情页转化量/点击量 -安卓|转化总量|游戏在 TapTap 安卓端的总体转化量(转化行为包括预约和下载,但不含 Web 端扫码转化量)。 -iOS|曝光量|游戏素材(icon、大图、标签等)在 TapTap iOS 端的曝光量。 -iOS|点击量|点击游戏素材进入游戏详情页的数量 -iOS|点击率|点击量/曝光量 -iOS|详情页转化量|仅在游戏详情页曝光后的转化量 -iOS|详情页转化率|详情页转化量/点击量 -iOS|转化总量|游戏在 TapTap iOS 端的总体转化量(转化行为包括预约和唤起苹果商店,但不含 Web 端扫码转化量)。 - -### 位置说明 - -来源|位置 --|- -安卓|全部、首页推荐、即将上线、排行榜、找游戏、搜索、Tap加速器、其他(TapTap安卓端内除上述位置之外的数据) -iOS|全部、首页推荐、搜索、排行榜、其他(TapTap iOS端内除上述位置之外的数据) - -## 排行数据 - -游戏在 TapTap 平台(含客户端、Web 端)的榜单排名数据,安卓榜单最多150名,iOS榜单最多50名。 - -来源|榜单类别 --|- -安卓|热门榜、预约榜、热玩榜、新品榜 -iOS|热门榜、预约榜 - -## 广告投放数据 - -游戏在 TapAD 的投放数据,此处仅统计近半年的数据,更详细数据请前往 TapAD 查看 - -指标|定义 --|- -广告展示量|游戏在 TapTap 平台广告位的展示量 -广告点击量|游戏在 TapTap 平台广告位的点击 -广告下载/预约量|游戏在 TapTap 平台广告位的转化量(包含下载和预约) \ No newline at end of file diff --git a/docs/store/release/store-agree.mdx b/docs/store/release/store-agree.mdx deleted file mode 100644 index cadbf93d8..000000000 --- a/docs/store/release/store-agree.mdx +++ /dev/null @@ -1,236 +0,0 @@ ---- -title: TapTap 游戏审核规范细则 -sidebar_position: 3 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - - -请确保您在开发者后台提交的游戏资料符合《 TapTap 游戏审核规范细则 》,违规游戏可能被处罚或下架。 - -## **1. 基本说明** -### **1.1 法律法规** -1.1.1 根据国家有关规定,未经国家新闻出版广电总局批准的移动游戏,不得在大陆地区上线运营,即没有版号的游戏不得有任何形式的内购计费内容。 -1.1.2 根据国家有关规定,游戏资质不允许互相套用。如版号出版时间早于软著出版时间,该类游戏将不予以收录。 -1.1.3 游戏不得含有违法内容或其他敏感信息,包括但不限于涉政、涉赌、暴力、血腥、虐待动物或儿童、淫秽、色情、性暗示及广告法中不允许使用的词汇。 -1.1.4 游戏不得带有人身攻击或者侮辱、诽谤他人,侵害他人合法权益(包括但不限于著作权、商标权、肖像权、名誉权等)。 -1.1.5 游戏不得对武器进行过于逼真的表述(如不能涉及武器的制造工艺和参数等),并宣扬违法或滥用武器。 -1.1.6 游戏不得存在反政府、反社会或者不符合主流政治、国家法律禁止的行为。 -1.1.7 游戏内容存在安全隐患、召集、推销、鼓动犯罪、有明显侵犯社会善良风俗行为的、扰乱社会秩序或违规违法嫌疑信息,平台将有权拒绝上架。 - -### **1.2 游戏内付费** -1.2.1 游戏内所有付费必须明码标价,必须明确告知可享受的服务,如付费后使用的服务与说明不符,游戏可能被下架,严重者会被封禁账号。 -1.2.2 游戏不得存在诱导付费行为,包括但不限于诱导站外直接充值付费、诱导进行线下交易、二维码赞助众筹等。 -1.2.3 游戏不得存在自动扣费等行为。 - - -### **1.3 游戏内广告及捆绑下载** -1.3.1 游戏不得包含空白广告位或者招商广告位,且主要目的不得是展示广告或市场营销。 -1.3.2 游戏广告不得含有违法内容或其他敏感信息,包括但不限于涉政、涉赌、暴力、血腥、虐待动物或儿童、淫秽、色情、性暗示及广告法中不允许使用的词汇。 -1.3.3 游戏广告不得存在模仿系统通知或提示,从而诱导用户点击的行为。 -1.3.4 游戏广告不得在应用已关闭或者退出至后台时依然存在。 -1.3.5 游戏内不得存在无法关闭的悬浮窗广告或弹窗广告。 -1.3.6 游戏广告不得存在捆绑下载行为,例如,游戏登录注册页面默认勾选下载其他 APP 或者必须下载其他 APP 才可使用。 -1.3.7 游戏不得存在需强制用户下载其他应用或游戏才可使用。 -1.3.8 广告与其他非广告信息同时出现,可能造成用户误解为内容时,广告应当显著标明「广告」。 - -### **1.4 捆绑下载** -1.4.1 游戏启动页未经用户许可不得默认勾选下载其他 APP ,如存在默认勾选项需明确提示用户。 -1.4.2 游戏未经玩家许可不得自动下载其他 APP 。 - -### **1.5 游戏重复** -1.5.1 请勿上传多个内容相似、相同的游戏,雷同游戏可能会被下架处理。 -1.5.2 游戏的新版本主体功能不得与旧版本的主体功能相差过大,如旧版本为文字游戏,新版本更新为动作游戏。 -1.5.3 游戏不得为简单网站页面打包或套用模板,玩家体验质量过低的游戏可能会被下架处理。 -1.5.4 游戏主要功能不得依赖于第三方应用或者需跳转至网页来获取内容及功能。 -1.5.5 游戏内容不得与已经收录的游戏相同,如发现被侵权请进行申诉,点击查看[侵权投诉流程](https://www.taptap.cn/doc/27)。 - -### **1.6 欺诈行为** -1.6.1 游戏存在欺诈、误导玩家的行为。将被下架,严重者会被封禁账号。 -1.6.2 游戏在审核前后通过服务端控制应用内容,在上架后开启违规服务。将被下架,严重者会被封禁账号。 - -### **1.7 暂不收录游戏** -1.7.1 非官方包,如第三方联运游戏。 -1.7.2 破解、盗版、未获得版权所有者授权或重新打包第三方游戏的行为(包括但不限于图片、音乐和文字素材等)。 -1.7.3 赌博类、博彩类地下彩类(包括但不限于棋牌、捕鱼和娃娃机等)或是带有宣扬赌博色彩的游戏,如使用包含炸金花(扎金花)、梭哈、百家乐、赌场、比大小、赢三张、三张牌、六合彩、轮盘、港式五张、21 点、黑杰克等词汇作为应用的名称;或包含以真实筹码为原型的道具购买内容。 -1.7.4 实物奖励类、现金奖励类或网赚类的游戏。 -1.7.5 虚拟货币类、比特币类或区块链类的游戏。 -1.7.6 色情类、血腥暴力类的游戏。 -1.7.7 内容涉及宗教、民族文化,宣扬邪教和封建迷信,煽动民族仇恨、破坏民族团结的游戏。 -1.7.8 二次创作的同人游戏。 -1.7.9 违反国家法律法规的游戏。 - ---- - -## **2. 基础信息** -### **2.1 厂商** -2.1.1 厂商名称仅限使用汉字、数字、字母,及其组合,不得使用特殊符号。 -2.1.2 厂商名称不得使用占位文本、空格、乱码等无关字符。 -2.1.3 厂商名不得使用游戏名命名。 -2.1.4 厂商名不得使用个人姓名命名。 -2.1.5 厂商名不得使用多个合并或并列厂商名。 - -### **2.2 游戏类型、兼容性及付费** -2.2.1 需根据游戏实际情况填写,不得选择与产品实际功能不符的分类。 -2.2.2 需根据实际情况选择,不得随意填写,否则会误导玩家。 -2.2.3 游戏付费情况请根据实际情况填写及后续更新。 - ---- - -## **3. 物料审核** -### **3.1 基本规范** -3.1.1 图片及视频素材必须保证清晰,不得出现明显模糊、拉伸、压缩、黑边、白边等情况,不得使用纯色、渐变等过于简单图案。 -3.1.2 图片及视频不得违反基本法律法规,需符合基本要求。 -3.1.3 图片及视频中不得含有违反平台收录标准的内容,包括但不限于提现、实质性奖励信息内容及相关素材。 -3.1.4 图片及视频中不得含有与游戏内容无关的素材,包括但不限于其他游戏或应用广告、厂商联系方式。 -3.1.5 图片及视频中不得含有侵权内容,已获授权素材或购买素材需上传相应证明材料或授权书。 -3.1.6 图片及视频中不得含有游戏商店评分等数据信息。 -3.1.7 图片及视频中不得含有与该游戏无关信息。 -3.1.8 图片及视频中不得添加强制引导信息。 -3.1.9 图片及视频中不得添加引导至其他网站的信息; - -### **3.2 ICON 图标** -3.2.1 ICON 不得添加存在误导玩家或违反相关规定的素材或角标,不得添加与游戏内容无关的热门搜索词或信息,如极速版、提现等。 -3.2.2 ICON 必须符合游戏内容设定,不得添加与游戏内容无关的热门搜索词或信息。 - -### **3.3 截图** -3.3.1 截图不得含有手机 UI ,或不属于提供安装版本的 UI 。 -3.3.2 截图需保持尺寸基本一致。 -3.3.3 不得使用重复单一截图作为游戏详情页截图栏宣传素材。 -3.3.4 截图不得添加存在误导用户或违反相关规定的信息或素材。 -3.3.5 截图需和游戏实际内容一致,不得使用非该游戏内容的素材进行宣传推广。 - -### **3.4 宣传图​** - -3.4.1 不得含有除游戏名称之外的宣传文案,不得含有人物名片、游戏 UI 元素等内容。 -3.4.2 不得以游戏截图充当推广图,不得多图拼接、平铺作为推广图。 -3.4.3 需含有游戏 Logo(方形推广图除外) ,背景不得大面积留白。 -3.4.4 不得出现实物手机。 -3.4.5 不得出现游戏 ICON 。 -3.4.6 不得使用人物三视图、草稿图、建模图。 - - -### **3.5 文案、简介及开发者的话** -3.5.1 文案不得违反基本法律法规,需符合基本要求。 -3.5.2 文案中不得含有违反平台收录标准的内容。 -3.5.3 文案中不得含有强制引导等信息内容。 -3.5.4 文案中不得含有引战、蹭热度等信息。 -3.5.5 文案中不得含有引流信息。 - -### **3.6 游戏标题** -3.6.1 标题需与游戏资质对应,不得添加热门搜索词类副标题。 -3.6.2 标题中不得添加违反平台规定的信息。 -3.6.3 标题中不得添加误导玩家、与游戏内容无关的信息。 -3.6.4 标题中不得添加地区信息。 -3.6.5 游戏安装到手机上显示的 App 名称需与游戏详情页展示的标题一致。 - -### **3.7 简介及开发者的话** -3.7.1 简介用以介绍该游戏玩法、特色,请勿添加无关广告信息。 -3.7.2 简介内容不可与开发者的话内容高度相似。 - -### **3.8 玩家交流群** -3.8.1 玩家交流群名称需与游戏或官方匹配。 -3.8.2 玩家交流群名称不得带有引流信息。 -3.8.3 玩家交流群名称不得带有其他渠道或平台标识。 -3.8.4 玩家交流群号码不得填写多个号码。 -3.8.5 玩家交流群链接格式为纯链接,请勿添加文字或符号。 - ---- - -## **4. 商店配置** -### **4.1 游戏状态** -4.1.1 游戏状态请根据实际情况填写。 -4.1.2 测试服/先行服不支持选择游戏状态,前台默认为「关注」。 -4.1.3 未配置状态地区,游戏状态与「默认」保持一致。 - -### **4.2 多语言资料** -4.2.1 游戏多语言资料页内容需与上架地区匹配。 -4.2.2 游戏上架海外区服,需配置相应语言资料页。 -4.2.3 相应语言资料页物料素材内语言需匹配。 - -### **4.3 预约里程碑** -4.3.1 预约里程碑内容仅针对游戏正式公测开服。 -4.3.2 预约里程碑支持最大 3 档预约。 -4.3.3 预约里程碑奖励素材尺寸为 200x200 px 透明背景,且大小不超过 100 KB 的 PNG 素材。 -4.3.4 预约里程碑奖励素材不得使用黑白图案。 -4.3.5 预约里程碑里程数仅为 TapTap 平台预约数值。 -4.3.6 预约里程碑里程数需使用阿拉伯数字,不得出现任何符号。 -4.3.7 预约里程碑奖励内容需是游戏内具体奖励,奖励内容不得模糊。 -4.3.8 已添加的预约里程碑内容不得自行修改。 - -### **4.4 官网链接** -需填写游戏官方网站链接,不可填写第三方网站链接。 - ---- - -## **5. 安装包文件** -### **5.1 包名** -5.1.1 包名不得含有任何渠道相关标识。 -5.1.2 包名须与官方包名保持一致。 -5.1.3 游戏包名变更,请发布变更公告后,再通过更新游戏上传。 -5.1.4 请勿使用打包工具默认包名。 - -### **5.2 版本号( Version Code )** -5.2.1 游戏版本号不得为 0。 -5.2.2 新提交安装包版本号不得低于当前版本号。 - -### **5.3 版本名( Version Name )** -版本名需与官网保持一致。 - -### **5.4 更新日志** -更新日志内容需与游戏版本变动有关,不得出现宣传、广告或其他无关内容。 - -### **5.5 签名** -APK 不得使用公用证书签名。 - ---- - -## **6. 资质文件及授权** -### **6.1 游戏资质** -6.1.1 上传游戏的公司如与软著或版号的公司主体为子母公司、分公司,请提交真实有效的证明。 -6.1.2 上传游戏的公司如与软著或版号的公司存在授权关系,请提交完整、真实、可追溯的授权链证明。 -6.1.3 上传游戏的公司如有软著或版号中的著作人、运营单位发生变更,请提交真实有效的证明。 -6.1.4 如果存在授权关系,必须提供独家授权书文件。 - -### **6.2 运营和 IP 授权** -6.2.1 游戏内容为运营代理,请提交完整、真实、可追溯的独家授权链证明。 -6.2.2 游戏素材、剧情、音乐等内容涉及相关 IP ,需提交完整、真实、可追溯的授权链证明。 -6.2.3 IP 权利人若为上传游戏的公司所有,需提交真实有效的证明。 -6.2.4 IP 授权范围必须与上传游戏的内容吻合,且真实有效。 -6.2.5 授权书格式必须为 JPG 或 PNG,每张图片大小不超过 2M,最多可上传 10 张。 - -### **6.3 素材授权** -6.3.1 游戏素材不得使用版权敏感内容,包括但不限于网络素材、同人素材、其他游戏内素材。 -6.3.2 游戏素材若有真人形象,需提交肖像使用授权书。 -6.3.3 游戏素材若为原创素材,需提交真实有效的证明。 -6.3.4 游戏使用素材若为购买的商业素材,需提交素材购买页面和订单证明。 - -### **6.4 版号** -开发者需提供网络游戏出版物号(ISBN)以及核发单扫描件( 必须为 JPG 或 PNG 格式 ),如果提交游戏的厂商非该版号运营单位,需提供相应版号授权,请务必保证授权链独家且完整。 - -**特别注意** -- ISBN 号格式必须为 999-9-9999-9999-1 -- 如版号有更名、转移主体等历史,请务必提交完整的扫描件材料( 拼接为同一张,必须为 JPG 或 PNG 格式 ) -- 根据国家有关规定,未经国家新闻出版广电总局批准的移动游戏,不得上线运营,即没有版号的游戏不得有任何形式的内购计费内容 -- 如果游戏在大陆地区开放预约及信息展示,则暂不需要版号信息 -- 如果游戏需要在大陆地区开放下载,请确保资质齐全,如果涉及授权,请提供独代授权 - - -### **6.5 软著** -6.5.1 开发者需提供软件著作权登记号以及登记证书扫描件( 必须为 JPG 或 PNG 格式 ),如果提交游戏的厂商非该软著著作权人,需提供相应软著授权,请务必保证授权链独家且完整。 -6.5.2 如版号有更名、转移主体等历史,请务必提交完整的扫描件材料( 拼接为同一张,必须为 JPG 或 PNG 格式 )。 - -### **6.6 APP的 ICP 备案** -开发者需要提供 APP 的ICP备案信息,包括备案号和备案主办单位等信息,以确保应用的合法性和合规性,符合国家互联网信息管理的要求。 - ---- - -## **7. 政策相关** -### **7.1 实名认证与防沉迷** -7.1.1 实名认证和防沉迷所提交的信息必须真实有效。 -7.1.2 实名认证和防沉迷的视频需符合[TapTap 实名认证和防沉迷说明](/store/release/store-policy#32-防沉迷政策填写要求/)中的填写要求。 - ---- - -### **8. 其他** -8.1 TapTap 在法律规定的范围内有权对本指南做出解释。 -8.2 审核规范细则一经公布即刻生效,TapTap 有权随时对指南内容进行修改,修改后的结果公布于 TapTap 开发者平台网站。 \ No newline at end of file diff --git a/docs/store/release/store-creategame.mdx b/docs/store/release/store-creategame.mdx deleted file mode 100644 index a53a9027b..000000000 --- a/docs/store/release/store-creategame.mdx +++ /dev/null @@ -1,258 +0,0 @@ ---- -title: 新建和管理游戏 -sidebar_position: 1 ---- - -import { Red, Blue, Black, Gray } from "/src/docComponents/doc"; - -## 1. 游戏入库 - -1. 进入 [开发者中心](https://developer.taptap.cn/) >> 创建游戏 >> 按照要求填写基础信息和基础游戏资料 >> 保存并提交。 -2. 创建游戏后,游戏的基础信息已经入库,如果需要调整基础信息需要在「全部游戏」面板点击对应游戏右下角的“┆”按钮进行删除并重新创建游戏。 - -**特别注意** - -- 创建游戏后,游戏当前状态为「未发布」状态(未上架)。游戏发布上架需要点击游戏图标或游戏标题进入管理面板,选择 商店 >> 进入 商店资料 >> 完善详情信息后提交审核 -- 创建游戏后,可在「全部游戏」面板查看创建过的游戏,点击游戏图标或游戏标题即可进入对应游戏的管理面板。厂商也可以点击页面顶部的「厂商名称」或「游戏名称 >> 「全部游戏」查看全部游戏已创建的游戏 -- 游戏在「未发布」状态下可以使用 商店游戏服务数据分析 的部分功能,游戏运营 功能可能无法生效 -- 游戏在「未发布」状态下可以先上传 APK,但 游戏服务 的部分功能需要上传 APK 并通过审核后才能使用 -- 「未发布」状态游戏的上限为 10 个,超过将无法创建新游戏 - -![gameadmin](/img/创建游戏.png) -![gameadmin](/img/查看全部1.png) -![gameadmin](/img/查看全部2.png) - ---- - -## 2. 游戏预约 - -1. 游戏创建后,厂商可根据自己的宣发节点选择是否需要开启「预约」或「敬请期待」。 -2. TapTap 官方建议游戏入库的同时开启预约,玩家会主动选择预约感兴趣的游戏,浏览过详情页的玩家可通过“预约”按钮转化为预约量。 - -**特别注意** - -- - 创建游戏 - 时只能选择「预约」或「敬请期待」,如需以「测试」或「下载」的状态上架游戏,除了需要在 - 商店资料 - 处上传 APK 外,还需要厂商进入 商店 >> - 游戏资质 - 处完善相关资质并通过审核 - ---- - -## 3. 游戏资质 - -1. 上传游戏资质需要点击「编辑」按钮进行填写并保存。 -2. 已保存的资质不会自动提交审核,上传完成资质并保存后,需要厂商点击页面右上角的「提交审核」按钮。如果已提交的信息需要再次修改,可点击「撤销审核」撤回审核。 - -**特别注意** - -- 测试服/先行服仅支持上传官方游戏出版物号(ISBN)、未成年人防沉迷、ICP 备案 - -![gameadmin](/img/游戏资质1.png) -![gameadmin](/img/游戏资质2.png) - ---- - -## 4. 游戏更新 - -1. 在「全部游戏」面板点击进入对应的游戏图标或游戏标题即可进入 商店 完善或更新资料。 -2. 首次创建的游戏在「未发布」状态下且未提交审核时,可以直接完善或修改资料,如果更新的版本已经通过审核,则需要点击「构建新版本」更新资料。 - -**特别注意** - -- 构建新版本 时默认复制上一个版本提交的所有信息,包括 APK 和图片 -- 在无需更新 APK 的情况下,可以直接修改文字或图片信息并提交审核。如果需要更新 APK,请先选择「构建新版本」,然后删除默认复制的 APK 并上传更新的 APK,最后提交审核 -- 游戏出现 APK 包名变更、版本号倒退等情况,可能导致无法上传 APK 。包名变更会使之前安装过的玩家出现存档丢失,重新安装等情况。如确定需要变更包名,建议厂商发布相关公告、正常提交审核即可。如果需要版本号倒退,请走工单申请。 - -![gameadmin](/img/游戏更新1.png) -![gameadmin](/img/游戏更新2.png) - ---- - -## 5. 游戏测试或上线 - -1. 游戏如需开启「测试」或「开放下载」,厂商可通过 商店 >> 商店资料 >> 发布地区和状态 中修改对应状态提交审核,审核通过后游戏状态将变更为选择的状态。 -2. 当游戏进入人数到达饱和需要关闭本次测试,或上线后因不可抗力需要关闭下载时,厂商可通过 商店 >> 商店资料 >> 发布地区和状态 中自行修改状态为「预约」或「敬请期待」提交审核。 - -**特别注意** - -- 游戏状态调整为「测试」或「下载」时,必须同步提交 APK 并确保相关游戏资质已经通过审核,否则将无法正常提交审核 -- 游戏状态变更需在符合“TapTap 游戏审核规范细则”和“国家相关规范”才能通过审核(如:无版号游戏提交「下载」状态审核时将无法正常提交) -- 如果变更游戏状态为「测试」或「下载」时,提交按钮置灰无法提交时,请确认 游戏资质 是否通过审核 -- 请确保游戏在变更状态前的 24 小时内已正确提交相关资质和 APK 并已通过审核 -- 测试服/先行服不支持选择游戏状态,前台默认为「关注」 - -![gameadmin](/img/游戏测试或上线.png) - ---- - -## 6. 定时功能 - -1. 通过 创建游戏构建新版本 时填写的信息,都可在 商店 >> 商店资料 发布设置 中选择「定时上线」或「立即上线」。具体可满足以下几种场景:首发页面上线曝光、发布状态变更、新游戏资料(APK 和素材)的定时需求。 - -**特别注意** - -- 通过 创建游戏 新建的游戏版本在首次设置定时上线功能时,需要将时间设置在当前时间的 6 小时后,在首次审核通过后,定时上线可定时到当前时间之后的任意时间 -- - 游戏资质 - 不支持定时功能,为确保游戏能够正常在设置定时上线的时间上线,请务必预留时间提前提交资质审核,并确保资质审核通过 -- 创建游戏 新建的游戏版本在首次提交审核后,在其相关资料符合规范的情况下,官方预计在 - 1-3 个工作日通过审核,还请留意定时功能设置的时效范围,审核通过且在设置定时发布的时间后,玩家将在 - TapTap 平台搜索到游戏详情页面 -- 构建新版本 更新的游戏资料在提交审核后,在其相关资料符合审核规范的情况下 - ,官方预计在 1 个工作日内通过审核,还请留意定时功能设置的时效范围,审核通过且在设置的发布时间后,玩家将在 - TapTap 平台搜索到游戏更新后的详情页面 - -![gameadmin](/img/定时功能1.png) -![gameadmin](/img/定时功能2.png) - ---- - -## 7. 游戏审核状态及对应的操作 - -1. 游戏审核状态可在「全部游戏」面板中游戏图标右侧和对应游戏的 商店资料 查看,不同审核状态对应的操作有所不同。 - -**不同游戏资料状态下对应的操作** - -| 游戏资料状态 | 操作 | -| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 未发布 |
    • 编辑资料,提交审核
    • 删除项目
    | -| 审核中 |
    • 查看提审的资料,无法编辑
    • 撤销审核,撤销后,变更为「未发布」状态
    | -| 审核失败 |
    • 查看审核失败的原因
    • 编辑资料,再次提交审核
    | -| 定时上线 |
    • 仅支持查看定时上线的资料,无法编辑
    • 修改定时上线的时间,仅支持修改为当前时间之后的时间
    • 撤销定时上线,撤销后,当前资料版本变更为「审核失败」状态
    | -| 已上线 |
    • 查看已上线版本的资料,无法编辑
    • 构建新版本
    | -| 已下架 |
    • 查看已下架版本的资料,无法编辑
    • 构建新版本
    | - -![gameadmin](/img/游戏状态.png) - - -## 8. 预约里程碑 - -1. 预约里程碑用来帮助游戏吸引玩家进行预约。可在 游戏运营 >> 预约里程碑 中设置预约里程碑并提交审核,审核通过后将在游戏详情页展示预约里程碑。 -2. 预约里程碑最多三档,需填写内容及要求: - -- 达成里程预约数 -- 奖励内容:限 25 字符 -- 奖励图片:透明背景 200 x 200px,不超过 100KB,格式必须为 PNG - -3. 预约里程碑发布后不可自行修改,请确认后再提交审核。如因提交错误或不可抗力需修改预约里程碑,请在论坛发布预约里程碑修改公告,公告内容需要包括:新达成奖励数、新奖励内容、预约里程碑需修改原因等。公告发布后,可在 [开发者中心](https://developer.taptap.cn/) >> 工单 中对应的分类下提交修改申请。 - -![gameadmin](/img/预约里程碑.jpeg) - ---- - - -## Q&A - -### 1. TapTap 是所有的游戏都收录吗? - -详情可参考 [TapTap 游戏审核规范细则](/store/release/store-agree#暂不收录游戏/) - -### 2. 创建游戏审核通过后,可暂不发布游戏吗? - -可以。 -可通过 商店 >> 商店资料 >> 发布设置 中设置定时上线。 - -### 3. 我的游戏已经被 TapTap 收录,可以进行游戏认领吗? - -可以。 -请于 工单 联系我们,并提供企业营业执照扫描件或其他能证明您为该厂商官方人员的材料。 - -**【申请格式 】** - -> **申请标题** -> -> TapTap 游戏认领申请 - XXX 游戏名称 - -> 需认领游戏名称及页面链接: -> -> 申请认领的厂商名称或厂商页链接: -> -> 营业执照扫描件或相关证明材料(可以附件形式上传): -> -> 申请人联系方式: - -TapTap 官方人员确认所提交资料合规后,预计在 2 个工作日内操作。 - -### 4. 游戏主体可以进行转移吗? - -可以。 -请于 工单 联系我们,并提供游戏原所有方厂商和游戏主体新所有方厂商的授权证明材料。 - -**【 申请格式 】** - -> **申请标题** -> -> TapTap 游戏主体转移申请 - XXX 游戏名称 - -> 需进行主体转移的游戏链接: -> -> 游戏主体原所有方厂商名称或厂商页链接: -> -> 游戏主体新所有方厂商名称或厂商页链接: -> -> 游戏原所有方厂商和游戏主体新所有方厂商的授权证明材料(可以附件形式上传): -> -> 申请人联系方式: - -**特别注意** - -- 如主体双方均为同一方所持有的,请在提交时加以说明并在附件中添加对应主体的开发者后台截图以供核实。如遇到迁出方合约终止等不愿配合的情况,可由迁入方提供有效的代理授权或其他证明申请转移,TapTap 官方将会在核实其合法性有效的情况后进行操作。 - -TapTap 官方人员确认所提交资料合规后,预计在收到申请的 2 个工作日内完成操作。 - -### 5. 如何进行游戏下架? - -请于 工单 联系我们,并提供企业营业执照扫描件或其他能证明您是该厂商官方人员的材料。 - -**【 申请格式 】** - -> **申请标题** -> -> TapTap 游戏下架申请 - XXX 游戏名称 - -> 需下架游戏链接: -> -> 下架原因: -> -> 期望下架时间: -> -> 营业执照扫描件或相关证明材料(可以附件形式提供): -> -> 申请人联系方式: - -TapTap 官方人员确认所提交资料合规后,预计在收到申请的 3 个工作日内完成操作。 - -**特别注意** - -- TapTap 希望保留玩家产出内容,下架游戏一般不会进行页面删除处理。如需下架页面请在单独声明删除页面需求。 - -### 6. 新提交的 APK 可以回退版本吗? - -可以。 -请于工单申请并声明需要回退的版本。 - -### 7.游戏标题可以修改吗? - -无版号/软著/ICP 备案等资质的游戏:可以直接修改。如需修改游戏标题,可在对应游戏下的 商店 >> 商店资料 中点击新版本修改标题然后提交审核。 - -有版号/软著/ICP 备案等资质的游戏:不可直接修改。需先工单联系我们申请更名,通过后在 商店 >> 游戏资质 中将更名后的资质一并提交审核,可同时在商店 >> 商店资料 中点击新版本修改标题然后提交审核。 - -注意,修改游戏标题时,请统一把游戏图标、宣传图、游戏截图等资料上的名称一并修改,游戏标题必须与相关资质全部保持一致才能通过审核。我们建议您修改标题后在「开发者的话」或论坛中公告玩家游戏原标题以及修改游戏标题的原因,以免给玩家带来不良游戏体验,影响游戏评分。 - - -### 8. TapTap 收录没有版权但资质齐全的游戏吗? - -TapTap 非常重视版权问题,没有版权的游戏一律不做收录。 - -### 9. 预约里程碑内容有限制吗? - -预约里程碑需对奖励内容进行清晰说明,不可用例如 “ 预约大礼包 ” 等字样表示。预约里程碑不可涉及任何实物或金钱性质内容(包括且不限于现金、礼品)。 - -### 10. 如何向玩家发放预约里程碑奖励? - -预约里程碑奖励一般在游戏首发后向预约玩家发放,建议在游戏内通过站内信的形式直接向全服玩家发放。 - -如需通过礼包码的形式发放,请于 游戏运营 >> 礼包码 >> 新增游戏码 中上传礼包码,提交审核即可。建议多档预约里程碑奖励合在一组礼包码中一并发放。如需只把礼包码发放给预约玩家,请 工单 联系官方配置。 diff --git a/docs/store/release/store-material.mdx b/docs/store/release/store-material.mdx deleted file mode 100644 index 95d3982df..000000000 --- a/docs/store/release/store-material.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: 游戏物料要求 -sidebar_position: 2 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -其中 \* 标注为必须提交的资料 - -## 1. **图标** \* 必填 - -- 尺寸:512x512px -- 格式:png/jpg -- 要求:请直接上传直角方图,系统会根据 TapTap 的圆角规范自动裁剪,建议保证主要内容在图示的安全区域内 - -![图标](https://img.tapimg.com/market/images/7d9b54999437f091912d925058a3c1d6.png) - ---- - -## 2. **文案** -### 2.1 简介 \* 必填 - -- 内容:描述游戏的类型、玩法以及特色 -- 运营建议:建议将主要内容控制在前 50 个字,有利于加深用户对游戏的第一印象 - -### 2.2 开发者的话 - -- 内容:描述游戏设计理念、游戏开发心路历程 -- 运营建议:建议开发者的话具备个人特色且真情实感,并建议将主要内容控制在前 50 个字 - -:::tip - -- 为避免影响用户端展示效果,简介与开发者的话请勿雷同 - -::: - -### 2.3 更新日志 - -- 内容:描述本次游戏更新内容 -- 运营建议:建议文案简洁明了,使用有序列表展示更新点,比如:1. 2. 3. ...。 - -![文案](https://img.tapimg.com/market/images/c7e388a27365c349158c5ee8fa9ad0bc.png) - ---- - -## 3. **实机录屏及游戏截图** - -### 3.1 游戏实机录屏\* 可下载或可试玩必填 - -- 格式:MP4 -- 大小:500 MB 以内 -- 长度:视频长度建议在 2 分钟内,最长不得超过 3 分钟 -- 内容:视频需演示游戏的核心玩法 - -:::tip - -- 为保证用户能够快速了解游戏玩法,游戏正式上线开放下载或开放试玩版必须提供实机视频 -- 视频需演示游戏的核心玩法,只展示角色、立绘、Loading 界面、主界面等画面,以掩盖游戏本身的真实美术水平、核心玩法,或仅用较短时间演示核心玩法等对用户造成误导的,都可能对游戏的正常分发造成影响 -- 您可适当剪辑视频以提升观看体验,但请勿使用额外的人声配音,以免对用户了解游戏玩法造成影响 - -::: - -![实机录屏](https://img.tapimg.com/market/images/ad637b869bd34cab1002d20c8774bbf5.png) - - -### 3.2 截图\* 必填 - -- 图片方向:支持选择横图、竖图方式展示游戏画面,但请保证每张截图的图片方向一致,如:如果您选择上传横图,请保证所有的图片均为横图 -- 尺寸:横版 1280x720px 及以上;竖版 720x1280px 及以上 -- 格式:png/jpg -- 数量:至少 3 张,请勿上传相同截图 -- 大小:建议每张截图 1 MB以内,截图文件大小过大会导致上传速度及用户打开游戏详情页时加载速度过慢,影响游戏转化 - -:::tip - -- 为保证用户能够快速了解游戏玩法,在 TapTap 开放包体下载、测试的游戏截图中必须有实机核心玩法内容展示,实机画面至少占图片的 50%,概念类、广告宣传类图片至多 2 张 -- 游戏截图使用游戏中不存在的画面、过度美化游戏中的画面、仅展示卡片、立绘、Loading 界面、主界面等行为,以掩盖游戏本身的真实美术水平、核心玩法,对用户造成误导的,都可能对游戏的正常分发造成影响 - -::: - -![截图](https://img.tapimg.com/market/images/8ead7a90c2146d34ac52881453e6494b.png) - ---- - -## 4.**推广物料** - -### 4.1 **宣传图 16:9 **\* 必填 - -- 用途:首页推荐、今日游戏等使用卡片展示游戏的页面,用于宣传游戏;在游戏预约等阶段,未上传游戏实机录屏的情况下,作为游戏详情页的首张图片,用以突出游戏的品牌形象;同时用于横版游戏实机录屏、宣传视频(如有)的视频封面。 -- 尺寸:1920x1080 px(16:9) -- 格式:png/jpg -- 内容:建议使用游戏主要角色、主要画面场景作为图片的核心视觉,且图片上必须展示清晰可见的游戏名或游戏 Logo - -:::tip - -- 除了游戏名或游戏 Logo 外,请勿引用或使用其他文本 -- 在某些页面会使用 4:3 比例对图片做裁剪进行展示,请误将游戏 logo、主要角色放置在裁剪线上 - -::: - -![宣传图 16:9](https://img.tapimg.com/market/images/2a80d98ee07e006244dce783f5c0b04a.png) - -### 4.2 **宣传图 1:1 ** - -- 用途:竖版实机录屏的视频封面 -- 尺寸:1440x1440 px(1:1) -- 格式:png/jpg -- 内容:除游戏 Logo 的位置和大小、游戏主要角色的位置和大小等为了适配当前尺寸而调整的细节外,请保证图片主体内容与宣传图 (16:9) 一致 - -:::tip - -- 如果您上传了横版实机录屏而非竖版,则无需上传此图片 - -::: - -![宣传图 1:1](https://img.tapimg.com/market/images/c4b7b5a41c343edca2538029c0fbac3c.png) - - -### 4.3 宣传片视频 - -- 用途:游戏在首页推荐展示素材 -- 尺寸:1280 x 720px 及以上,推荐 1920 x 1080px -- 格式:MP4 -- 大小:500 MB 以内 -- 内容:展示游戏的核心玩法为主 -- 运营建议:游戏更新后可呈现游戏新版本 / 新角色 / 新玩法素材、节日相关素材等;视频前 3s 代表了游戏的第一印象,请避免开屏的 logo、黑屏等无游戏实机画面的情况 - -:::tip - -- 您可按需设置游戏在首页推荐展示效果,可选择的推荐素材有:宣传图 16:9、游戏实机录屏、宣传片视频 - -::: - -![宣传片视频](https://img.tapimg.com/market/images/e5b2d85a6261f03fc622ba74a5fd3c85.png) - - -## **5.首页编辑推荐栏目推荐图** - -- 用途:在 TapTap 首页编辑推荐栏目展示的图片,仅限 [编辑推荐游戏](/store/store-editorschoice/) -- 尺寸:1920x1280 px(3:2) -- 格式:建议提供 .psd 原文件,必须提供 .png 或 .jpg 图片 - -:::tip - -- 相较于其他物料,编辑推荐栏目推荐图有更高标准,请详细阅读素材要求。提交新素材,或更新素材,请通过「工单系统 - 商店运营服务支持 - TapTap 新功能申请/咨询 - 首页编辑推荐位材料补充」入口提交 -- 当图片不满足编辑推荐位的素材要求时,TapTap 将优先尝试使用 .psd 原文件重新制作图片。如无 .psd 原文件可使用,您提交的素材将直接被拒绝 -- 如果您暂未提供素材,TapTap 将按游戏现有素材重新制作推荐图。如果重新制作推荐图的游戏数量较多,TapTap 将按照一定次序为每款游戏排期制作 -- 没有首页编辑推荐栏目推荐图的游戏将不展示在编辑推荐游戏的专属栏目 -- 当前推荐图仅限编辑推荐的游戏,如果您为非编辑推荐的游戏提交素材,可能会被判定影响平台运营工作,并受到相应的处罚 - -::: - -![编辑推荐栏目推荐图](https://img.tapimg.com/market/images/f2348fa2871a7381d175ad9455e74526.png) \ No newline at end of file diff --git a/docs/store/release/store-policy/_category_.json b/docs/store/release/store-policy/_category_.json deleted file mode 100644 index 05b8b98bc..000000000 --- a/docs/store/release/store-policy/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "监管政策及要求", - "collapsed": true, - "position": 4 -} diff --git a/docs/store/release/store-policy/filing-certificate.mdx b/docs/store/release/store-policy/filing-certificate.mdx deleted file mode 100644 index 306dc4db5..000000000 --- a/docs/store/release/store-policy/filing-certificate.mdx +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: TapTap 测试游戏备案辅助证明申请介绍 -sidebar_position: 3 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; -import useBaseUrl from "@docusaurus/useBaseUrl"; - -## 一、如何加入及相关要求 - -1. 游戏仅支持接入 [TapTap 登录](/sdk/taptap-login/features/)服务,或仅支持通过 [TapPlay](/sdk/tap-play/features/) 启动游戏(两种方式任选其一) -2. 游戏必须体现 TapTap 平台特征 - 1. 包名必须添加特征信息 game.taptap.XXX,提交备案的包名也必须添加特征信息 game.taptap. XXX - 2. 游戏名称必须是 XXX(TapTap测试版),提交备案的服务名称也必须是 XXX(TapTap测试版) - -:::tip -**举个例子** - -包名:game.taptap.abc.defg - -游戏名称:心动小镇(TapTap 测试版) -::: - -3. 合作期内,游戏仅在 TapTap 平台测试,不可主动提交上架到其他平台,否则 TapTap 将保留追责的权利 - -:::tip -1. 推荐游戏接入 TapTap 登录服务时,选择基于[内建账户](/sdk/authentication/features/)系统接入 TapTap 登录。当您取消合作后,系统将保留老玩家登录的数据,以免游戏更换包名后导致老玩家无法登录的情况。 -2. 如果您希望使用自研登录系统或其他登录方式,请通过 TapPlay 的形式上架,推荐先使用 [TapCanary](/sdk/tap-canary/features/) 自测稳定性。 -::: - -4. 如果您有意愿申请《测试游戏备案辅助证明》,请在**「开发者后台 - 商店 - 游戏资质 - 联网游戏测试申请」**位置签署独家游戏测试协议和相关文件。 - -![](https://capacity-files.lcfile.com/eXvnbEl3EjAg7b8haNWfTPU9Cv4Tgx6K/image%20%281%29.png) - -:::tip -如果您进入后台后,未发现申请入口,请通过工单**「商店运营服务支持 / 监管政策要求及咨询 / 移动互联网应用程序备案工作」**与我们联系。 -::: - -## 二、准备材料(电子签) - -1. 测试游戏承诺书 - -2. TapTap 平台独家游戏测试协议 - -3. 维权授权书 - -4. 计算机软件著作权登记证书 - -:::tip -1. 计算机软件著作权登记证书请在「开发者后台 - 商店 - 游戏资质 - 软件著作登记」处上传扫描件 - 1. 如果软著还在办理中,请在证书下来后尽快上传至后台 -2. 《TapTap 平台独家游戏测试协议》、《维权授权书》只需要开发者签署,完成后**不支持**调整信息,请慎重签署 -3. 根据部分云服务商要求,申请《TapTap 测试游戏备案辅助证明》的运营主体需要和域名备案主体一致。**如果游戏当前管理厂商主体非域名备案主体,请在开发者后台确认申请文件签署主体时谨慎选择,否则会影响审核结果!** -::: - -## 三、常见问题 Q&A - -### Q1:TapTap 测试游戏备案辅助证明申请的合作期限? - -A1:联网游戏办理并在后台上传新的 APP 的 ICP 备案资质后,可以取消合作。同时,请开发者前往云服务商处注销已经备案的 TapTap 独家游戏测试版备案信息。 - -### Q2:《TapTap 平台独家游戏测试协议》、《维权授权书》、《测试游戏承诺书》中合作主体是否需要和厂商主体保持一致? - -A2:建议保持一致,TapTap 平台会根据游戏实际管理厂商的主体信息申报游戏名单。 - -:::tip -**厂商主体信息说明** - -- 个人:身份证上的姓名 -- 企业:营业执照上的单位名称 -::: - -### Q3:《计算机软件著作权登记证书》中的软件名称是否需要是 XXX(TapTap测试版) - -A3:不需要。 - -### Q4:TapTap 测试游戏备案辅助证明申请通过后,如何办理 APP 的 ICP 备案? - -A4:TapTap 确认独家游戏测试协议及相关文件签署成功后,平台将对游戏进行审核。审核通过后,平台将通过站内信形式发放回函(即《测试游戏备案辅助证明》)。请您在 APP 分类为游戏的前置审批文件处上传回函证明文件,如有其他疑问可联系云服务商的工作人员进一步咨询。 - -
    - -
    - -:::tip -当游戏被拒审后,请第一时间联系平台确认驳回原因并进行整改。请勿多次重复提交审核,以免影响游戏运营。 -::: - -### Q5:如何获取《测试游戏备案辅助证明》? - -A5:平台将通过站内信的形式通知大家获取《测试游戏备案辅助证明》,您可在站内信中直接点击下载《测试游戏备案辅助证明》,或在「开发者后台 - 商店 - 游戏资质 - 联网游戏测试申请」位置处下载《测试游戏备案辅助证明》。 - -### Q6:是否支持未带有 TapTap 特征信息的游戏备案? - -A5:不支持。 - -### Q7:如何获取 APP 的 ICP 备案结果? - -A7:各省级通信管理局一般在 20 个工作日内予以审批,审核结果将通过短信、邮件形式告知,主办者也可以通过[备案系统网站](https://beian.miit.gov.cn/#/Integrated/index)自行查询。材料不齐全或不准确的,省级通信管理局不予备案,并反馈驳回理由。 - -### Q8:游戏变更信息后,还需要重新备案吗? - -A8:需要。详情页的 APP 信息必须与备案信息一致,如果 APP 有相关信息变更,则需同步更新备案信息。 - -### Q9:拿到《测试游戏备案辅助证明》后,是否就可以开启测试? - -A9:不可以,游戏需要办理完成 APP 的 ICP 备案后才可以有分发行为。 - -如果您关于备案还有其他疑问,可先参考[《移动应用程序备案通知及要求》](/store/release/store-policy/filing-notice/)。如果上述文档未能解决您的疑问,可通过工单**「商店运营服务支持 / 监管政策要求及咨询 / 移动互联网应用程序备案工作」**进一步咨询。 - - - - - - - - diff --git a/docs/store/release/store-policy/filing-notice.mdx b/docs/store/release/store-policy/filing-notice.mdx deleted file mode 100644 index 1d4254229..000000000 --- a/docs/store/release/store-policy/filing-notice.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: 移动应用程序备案通知及要求 -sidebar_position: 2 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; -import useBaseUrl from "@docusaurus/useBaseUrl"; - -## 背景介绍 - -2023 年 8 月 4 日,工信部发布[《工业和信息化部关于开展移动互联网应用程序备案工作的通知》](https://www.miit.gov.cn/zwgk/zcwj/wjfb/tz/art/2023/art_920db564162e4312916a01bed6540ad8.html),要求互联网行业从业者进一步做好移动互联网信息服务管理,积极开展移动互联网应用程序(以下简称 APP)备案工作。备案细分为网站、APP、小程序、快应用四种服务类型,各位移动应用程序开发者需要通过其网络接入服务提供者、分发平台向其住所所在地省级通信管理局履行备案手续。 - -## 需要做什么 - -1. 积极履行办理 APP 的备案手续。 - -2. 在 APP 内的显著位置标明其备案编号,并在备案编号下方按要求链接备案系统网址。备案管理系统网址:https://beian.miit.gov.cn/。 - -:::tip -1. 推荐在 APP 内的「设置」、「介绍」位置标明 APP 备案许可证号。 -2. 完成备案手续后,请在「开发者后台 - 商店 - 游戏资质 - APP 的 ICP 备案」位置上传管局备案的截图,并填写备案许可证号码。 -3. 如果 APP 需要提供备案运营授权书,请在「开发者后台 - 商店 - 游戏资质 - 独代授权书及 IP 类授权书」位置上传。 -::: - -**APP 的 ICP 备案截图示例** - -![](https://capacity-files.lcfile.com/7yWd1kicAMiJm6tbAjwjhU7q4KGktvFn/image-1.png) - -![](https://capacity-files.lcfile.com/GSBSqwR8JP2deJHMIQ7IrAo7dnOwHX0F/image-2.png) - - -## 常见问题 Q&A - -### Q1:哪些 APP 需要办理备案?单机游戏是否需要办理备案? - -A1:所有联网 APP 都需要办理 APP 的 ICP 备案,请先确认 APP 是否使用了自有服务器或自有域名。仅调用第三方 SDK 或 API,无自有服务器或自有域名的单机 APP 无需备案。 - -### Q2:已经备案了网站 ICP 信息,还需要进行APP备案吗? - -A2:需要的,网站备案号与 APP 备案号不同。 - -:::tip -网站备案号:京ICP备2023111111-1 - -APP 备案号:京ICP备2023111111-1A - -小程序备案号:京ICP备2023111111-3X - -快应用备案号:京ICP备2023111111-4K -::: - -### Q3:APP 备案的时间预计需要多久? - -A3:各省级通信管理局一般在 20 个工作日内予以审批,但各家云服务商初审的时间略有不同,详情可咨询使用的云服务商工作人员。 - -### Q4:APP 主办单位是否需要和厂商主体保持一致? - -A4:建议保持一致。如果存在不一致的情况,需要提供授权书。当 APP 信息发生变更、注销等情况,APP 主办单位应当向原备案机关履行变更、注销等手续,同时尽快在后台更新资质,避免审核驳回的情况。 - -:::tip -APP 的 ICP 备案授权书中需要提供: -1. 授权双方的信息 - 1. 个人:身份证上的姓名+身份证号码+电子签名和手印 - 2. 企业:营业执照上的单位名称+统一社会信用代码+公章 -2. 授权形式:独家或者在 TapTap 平台独家 -3. APP 的 ICP 备案许可证号 -::: - -### Q5:什么时候需要完成 APP 备案,未完成备案会有哪些影响? - -A5:首次上传 APK 的 APP需要上传 APP 的 ICP 备案资质,如果备案还在办理中,可通过工单「商店运营服务支持 - 监管政策要求及咨询 - 移动互联网应用程序备案工作」与我们联系。 - -存量 APP 要求在 2024 年 3 月底前完成备案手续,2024 年 1 月 4 日起,平台原则上将对在架未备案的移动应用程序逐步开始进行相应处理(包括但不限于关闭下载、页面下架、冻结开发者账号等)。 - -### Q6:服务器在境外,需要备案吗? - -A6:如果 APP 域名解析到的服务器 IP 地址在中国香港、中国台湾、中国澳门、或其它非中国大陆的国家和地区,则不需要备案。 但如果 APP 或网站在中国大陆运营则需要备案,建议尽快切割数据,使用中国大陆地区的服器。 - -### Q7:服务器和域名分别是不同厂商的,域名是其他云厂商的,应该如何备案? - -A7:备案遵循 “ 谁接入谁备案 ” 的原则,服务器接入了哪家,那就在对应的服务商办理备案。 - -### Q8:APP 备案的包名需要和上架包名一致吗? - -A8:需要的,如果 APP 有多个包名,可在办理备案手续时提交所有已有的包名。 - -### Q9:APP 接入了 TDS 功能,应该如何备案呢? - -A9:详情可通过工单「TDS 游戏服务 - ICP 备案 - App 备案 」进一步咨询。 - -### Q10:游戏是单机玩法,也未使用自有服务器或自有域名,为什么会判定为联网? - -A10:请您确认游戏是否开通以下 TDS 服务:【内建账户】、【游戏好友】、【成就系统】、【公告系统】、【排行榜】、【云存档】、【数据存储】,如果游戏开通了以上任一 TDS 服务,默认为联网。如果您确认游戏未使用上述服务,可以通过工单「TDS 游戏服务 - ICP 备案 - App 备案 」进一步申诉。 - -:::tip -使用共享域名可能会影响游戏的稳定性,也存在数据安全风险。如果您的游戏是通过共享域名实现了功能,请尽快切换绑定为自有域名。 -::: - -### Q11:游戏提交备案资质后,审核驳回原因是 “ 您好,您所提交的 ICP 备案信息与后台上传的 APK 包名不相符,请检查上传正确的 ICP 备案或更新为相同的包名。如有问题,您可工单联系。” 应该如何解决? - -A11:请您提供备案时在云服务商那边提供的包名信息截图,审核人员会比对与后台上传的包名是否一致。 - -1. 如果备案的包名与后台使用的包名不一致,则需要变更后台包名为已经备案的包名。 - -2. 或者请联系云服务商的工作人员变更备案的包名信息。 - -:::tip -1. 平台已经接入了管局备案信息的校验系统,我们会同时校验游戏备案的包名、名称、备案主办单位,任一信息不一致都会被驳回处理。 -2. 同一个 APP 仅支持备案一个母包,但可以备案多个渠道包。 -3. 一般系统返回结果会延迟 1-2 天,推荐备案审核通过后的 1-2 天再上传资质。 -::: - -### Q12:游戏提交备案资质后,审核驳回原因是 “ 您好,您上传的ICP备案图像中,主办单位名称与入库的厂商主体不符,请确认后重新提交、或请提交授权书。如有疑问,您可工单咨询。谢谢!” 应该如何解决? - -A12:备案主办单位信息需要和实际管理游戏的厂商主体保持一致,如果不一致可以提供备案运营授权书,或者可以将[游戏主体转移](https://developer.taptap.cn/docs/store/release/store-creategame/#4-%E6%B8%B8%E6%88%8F%E4%B8%BB%E4%BD%93%E5%8F%AF%E4%BB%A5%E8%BF%9B%E8%A1%8C%E8%BD%AC%E7%A7%BB%E5%90%97)到已备案主体名下。 - -### Q13:游戏提交备案资质后,审核驳回原因是 “未上传 APK 的情况下,无法校验 ICP 资质是否合规 。” 应该如何解决? - -A13:平台已经接入了管局备案信息的校验系统,我们会同时校验游戏备案的包名、名称、备案主办单位,任一信息不一致都会被驳回处理。因此,请同时上传 APK 和 APP 备案资质的审核。 - -### Q14:游戏提交备案资质后,审核驳回原因是 “ 您好,您所填写的ICP备案登记信息有误,请提交APP备案登记信息,谢谢!” 应该如何解决? - -A14:APP 备案号都是以 A 结尾的,以这个示例为例,备案号应当为:京ICP备2023111111-1A,请注意后缀词。 - -### Q15:无版号游戏如何办理备案? - -A15:考虑到游戏类 APP 备案需提供出版前置审批文件,若您游戏暂未获批版号,可通过 TapTap 申请游戏测试,TapTap 将提供《测试游戏备案辅助证明》用于开展 APP备案。具体合作方案请见:[《TapTap 测试游戏备案辅助证明申请介绍》](/store/release/store-policy/filing-certificate/)。 - -## 快速通道 - 常见云服务商备案(入口)流程指引 - -排名不分先后 - -1. [阿里云备案服务](https://beian.aliyun.com/?spm=a2c4g.11186623.J_3207526240.8.2de8224d7sxE8J) -2. [华为云备案中心](https://beian.huaweicloud.com/) -3. [腾讯云备案服务](https://cloud.tencent.com/product/ba) -4. [火山引擎(字节云)备案中心](https://www.volcengine.com/beian) -5. [百度智能云备案中心](https://cloud.baidu.com/beian/index.html) -6. [Azure](https://www.azure.cn/support/icp/) -7. [亚马逊云科技(AWS)](https://www.amazonaws.cn/support/icp/?searchQuery=ICP&tag=search&targetPage=MARKETING_PRODUCT) -8. [天翼云备案](https://www.ctyun.cn/beian/) - -:::tip -如果大家还有其他疑问,可通过工单**「商店运营服务支持 - 监管政策要求及咨询 - 移动互联网应用程序备案工作」**与我们联系,感谢您一如既往的支持与理解! -::: - - diff --git a/docs/store/release/store-policy/store-policy.mdx b/docs/store/release/store-policy/store-policy.mdx deleted file mode 100644 index 5f5c477d1..000000000 --- a/docs/store/release/store-policy/store-policy.mdx +++ /dev/null @@ -1,304 +0,0 @@ ---- -title: 监管政策及要求 -sidebar_position: 1 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; -import useBaseUrl from "@docusaurus/useBaseUrl"; - -## 1. 新广告法及审核规范 - -### 1.1 政策说明 -根据《中华人民共和国广告法》(以下简称“新广告法”),TapTap 平台将根据新广告法对游戏页的推荐语、广告语进行审核,禁止平台内开发者使用“违规”宣传语,以保护消费者的合法权益,促进行业的健康发展。 - -[中华人民共和国广告法(全文)](http://www.gov.cn/xinwen/2015-04/25/content_2852914.htm) - -### 1.2. 审核规范细则 -1.2.1 描述不得与游戏内容无关。 - -1.2.2 描述不得添加时效性内容,如X月X日。 - -1.2.3 描述不得添加商业化用语,如无广告无内购无商城、不充值不氪金、免费送vip、连抽、不肝不氪、送福利红包元宝等。 - -1.2.4 描述不得添加该游戏所有平台和媒体获得奖项的评价。 - -1.2.5 描述不得添加大量占位符文本、空格、乱码等非法字符,如:.、#、*、& 等。 - -1.2.6 描述不得添加未获得授权的ip内容,如电影改编、真实人名、其他游戏等。 - -1.2.7 描述不得添加恶俗,玩梗等内容。 - -1.2.8 描述不得只重复填写游戏标题而不含其他内容。 - -1.2.9 描述不得添加极限词等绝对化用语,包括但不限于“最”、“首/家/国”、“一”及相关词语。 - -1.2.10 描述不得添加迷信用语,如提升运气、增加事业运、招财进宝等。 - -1.2.11 描述不得添加虚构的统计数据,如1亿玩家都在玩的手游、耗资X亿开发等。 - -1.2.12 描述不得添加打色情擦边球的用语,如零距离接触、余温、余香、身体器官描述等违背社会良好风尚的色情暗示词语。 - -1.2.13 描述不得添加虚假内容相关词语,如史无前例、前无古人、永久等无法提供证明的虚假宣传词语。 - -1.2.14 描述不得添加涉嫌欺诈玩家的表述,如点击领奖、恭喜获奖、点击有惊喜、领取奖品等涉嫌诱导的表述。 - -1.2.15 描述不得添加激发玩家下载或者购买心理的表述,如秒杀、抢爆、史上最低价、免费领等。 - -1.2.16 描述不得添加不利于未成年人身心健康的内容。 - -## 2. 隐私政策声明及审核规范 - -### 2.1 政策说明 -近年来,国家相关部门为贯彻落实加强用户个人信息保护、营造健康安全的网络生态环境,陆续推行了多份法律法规与执行依据。 - -为切实保证广大玩家个人信息安全,加强 App 隐私合规把控,TapTap 平台响应国家政策,依规加强推进游戏用户隐私政策协议链接填写工作。 - -依据相关部门监管要求和相关规范,未及时上传隐私政策协议链接的 APP,游戏的上架、更新等发布动作将受到影响,严重的或将面临国家相关监管部门的行政处罚。 - -2.1.1 [工业和信息化部关于开展纵深推进APP侵害用户权益专项整治行动的通知(工信部信管函〔2020〕164号)](http://www.gov.cn/zhengce/zhengceku/2020-08/02/content_5531975.htm) - -2.1.2 [关于印发《App违法违规收集使用个人信息行为认定方法》的通知](http://www.cac.gov.cn/2019-12/27/c_1578986455686625.htm) - -2.1.3 [常见类型移动互联网应用程序必要个人信息范围规定(国信办秘字〔2021〕14号)](http://www.cac.gov.cn/2021-03/22/c_1617990997054277.htm) - -2.1.4 [上海市网络游戏行业协会提供的隐私政策模版](http://www.oga.org.cn/newsinfo/1688274.html)(仅供参考) - -### 2.2 隐私政策链接填写要求 - -2.2.1 填写的“隐私政策协议”内容必须与APP内的“隐私政策”协议内容一致且真实有效。 - -2.2.2 如“隐私政策协议”内包含链接,请保证链接可正常跳转。 - -2.2.3 字体格式务必统一,请勿出现字体时大时小、多种字体随意穿插使用的情况。 - -2.2.4 整体排版简洁明了,段落清晰,请勿出现整体内容无段落分层,从头连到尾的情况。 - -2.2.5 不得出现随意乱用标点符号、空格、英文字母等情况。 - -2.2.6 “隐私政策协议”中的主体公司名称须与 APP 开发、发行或运营主体保持一致。 - -2.2.7 暂不接受以 TapTap 论坛帖形式承载隐私政策协议。 - -**特别注意** - -出现上述问题后,可能导致上架/更新申请被驳回,进而导致游戏上架/更新延期,还请开发者伙伴提交时严格自检。 - -### 2.3 隐私政策链接的添加入口 - -可在 [开发者中心](https://developer.taptap.cn/) >> 商店 >> 申请上架/更新游戏 >> 游戏资料 >> 隐私政策链接 中添加后提交审核,审核通过后会显示在游戏详情页中的详细信息下方。 - -![gameadmin](/img/隐私政策1.png) - -![gameadmin](/img/隐私政策2.png) - -## 3.防沉迷政策及审核规范 - -### 3.1 政策说明 -根据[《国家新闻出版署关于进一步严格管理切实防止未成年人沉迷网络游戏的通知》](https://www.nppa.gov.cn/xxfb/tzgs/202108/t20210830_666285.html)(下简称《通知》)要求,各游戏出版及运营单位、游戏厂商需在2021年9月1日落实该项规定。具体要求如下: - -3.1.1 严格限制向未成年人提供网络游戏服务的时间。所有网络游戏企业仅可在周五、周六、周日和法定节假日每日20时至21时向未成年人提供1小时网络游戏服务,其他时间均不得以任何形式向未成年人提供网络游戏服务。 - -3.1.2 严格落实网络游戏用户账号实名注册和登录要求。所有网络游戏必须接入国家新闻出版署网络游戏防沉迷实名验证系统,所有网络游戏用户必须使用真实有效身份信息进行游戏账号注册并登录网络游戏,网络游戏企业不得以任何形式(含游客体验模式)向未实名注册和登录的用户提供游戏服务。 - -### 3.2 防沉迷政策填写要求 - -请开发者参考《通知》规定进行自检和调整,提交符合下述要求的真实录屏,建议添加字幕表明展示的内容: -3.2.1 证明是该游戏 -3.2.2 游戏无游客模式(强制实名认证) -3.3.3 未成年真实姓名+成年真实身份证号无法通过认证(需体现因假名不匹配或因假名字无法通过认证) -3.2.4 同一未成年真实姓名+未成年真实身份证号通过认证,在规定时间外不可游玩,在周五六日和法定节假日20时至21时的时段,未成年人(即未满18周岁)可进行游戏。 - -**特别注意** - -实名过程中请勿遮挡出生日期。 - -### 3.3 防沉迷视频提交入口 - -可在 [开发者中心](https://developer.taptap.cn/) >> 商店 >> 游戏资质 >> 未成年防沉迷 中按要求提交相关证明后提交审核。 - -![gameadmin](/img/防沉迷截图.png) - - -## 4. 未成年人网络游戏适龄分级及审核规范 - -### 4.1 政策说明 -根据《未成年人保护法》、《关于进一步严格管理切实防止未成年人沉迷网络游戏的通知》、《未成年人网络保护条例》、《未成年人网络游戏适龄分级》等法规和标准要求,TapTap平台综合考虑不同年龄阶段未成年人身心发展特点和认知能力,通过评估游戏产品的类型、内容与功能等要素,对游戏产品进行分类,明确游戏产品适合的未成年人用户年龄阶段。 - -### 4.2 适龄划分标准 -参考《未成年人网络游戏适龄分级》团体标准,依据我国现行法律法规,根据不同年龄段未成年人的特点、综合考虑适龄的游戏类型与游戏功能,对游戏进行评估,确定游戏适用的年龄段,具体将游戏适龄范围划分为“8周岁(含)以上(8+)”、“12周岁(含)以上(12+)”、“16周岁(含)以上(16+)”三级。 - -#### 4.2.1 适龄分级心理准则 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    范围情感认知社会化
    8 周岁+自我评价水平较弱;情绪管控能力弱;意志力相对薄弱,注意力稳定性较弱。独立性和自制力弱,易受外界干扰。具备基本认知能力,开始从具体形象思维逐步过渡到抽象逻辑思维,出现辩证逻辑思维的萌芽。抽象、比较和分类能力有所发展,特别是思维的敏捷性、灵活性、深刻性和独创性开始发展,但思维发展具有不平衡性。具备一定的是非判断能力,道德观开始构建,但受到外部、具体的情境的制约,社会认知处于起步阶段。社会自我的意识逐渐萌发,对社会关系产生初步认识。建立同伴关系和友谊,社会交往能力得到初步锻炼,合作意识、尝试团体协作、同伴交往能力开始形成。
    12 周岁+开始进行现实的规划和探索思考,对事物的反应更灵敏,有追求冒险和刺激的心理,但自我控制力较弱。处于身心发展不平衡时期。以基于经验的抽象逻辑思维为主,但是水平相对较低。辩证思维开始发展,但是具有一定的表面性和偏颇性,容易产生自我中心的思维观念。自我意识发展的飞跃期,出现逆反的心理和意识,表现出复杂矛盾的情绪变化。内心渴望倾吐烦恼、交流思想并保守秘密的渠道。社会交往更加聚焦,朋友关系日益重要。
    16 周岁+身心发展日益成熟,情感需求向成人趋同,对生活充满美好憧憬。同时思想敏感,自我评价具有起伏性,情绪易波动。生理和心理发展均趋于成熟稳定,感知觉、观察能力、记忆能力和想象能力不断完善,抽象逻辑思维能力逐渐增强,特别是认识、分析和解决问题的能力。自我意识高度发展,高度关心自我个性成长,自我评价偏高,自尊心和道德意识强烈。对自己、他人和社会事件具有独立思考和判断标准。团队合作意识强,渴望在集体中展现自我价值,期待社会赞许。
    - -#### 4.2.2 适龄分级产品准则(内容要求) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    标识特征及类型内容表现促进功能
    - - 适合 8 周岁以上的未成年人使用的游戏,玩法简单、单局耗时短。如: -
  • 简单的休闲益智类
  • -
  • 解谜类
  • -
  • 模拟养成/经营类
  • -
  • 音乐舞蹈类
  • -
  • 塔防类等
  • -
    适用于 8 周岁以上未成年人的游戏,对游戏内容有以下要求: -
  • 内容要素:游戏背景、情节、角色等设计简单友好,文字和语法简单易懂,无复杂的故事背景和人物关系。可存在积极美好的想象和虚构,可存在需要自我选择、责任感及成功冒险的故事情节,有正义及快乐的结局。无基于历史和现实事件的改编,不会与现实生活相混淆。游戏可以促进同伴关系,存在少量简单的团队合作或好友助力内容。
  • -
  • 画面表现:游戏画面和美术设计为简单轻松的卡通、手绘风格。
  • -
  • 音效表现:游戏音效欢快活泼或舒缓悠扬。
  • -
  • 竞技和对抗:竞技和对抗玩法基于简单的思维判断或肢体操作。竞技或对抗内容以卡通或幻想的形式出现。游戏存在剧情需要的少量对抗设置,没有体现具体对抗行为和结果的视听内容,不会与现实生活相混淆。 竞技内容以培养竞争概念和勇气为主要目的,内容可含体育竞技、棋类竞技。
  • -
  • 社交系统:游戏中不存在基于文字、语音、图片等自由交流的陌生人社交系统,可存在简单的好友系统。
  • -
    适用于 8 周岁以上未成年人的游戏宜具有下列功能: -
  • 知识学习:可包含亲子互动、入门知识、学科启蒙知识等内容。
  • -
  • 技能与行为训练:可培养玩家简单的日常生活技能。
  • -
  • 认知促进:能够辅助培养玩家手眼协调能力、观察力、想象力等。
  • -
  • 情感与审美:可辅助玩家形成正向积极的情感表达、形成基础的审美能力, 或帮助玩家初步认识中华传统文化等。
  • -
  • 社会化发展:有助于玩家形成简单的社交能力,初步认识社会主义核心价值观等。
  • -
    - - 适合 12 周岁以上的未成年人使用的游戏,玩法相对复杂,需投入一定的时间和精力。如: -
  • 跑酷竞速类
  • -
  • 角色扮演类
  • -
  • 沙盒类
  • -
  • 音乐舞蹈类
  • -
  • 体育类
  • -
  • 策略类
  • -
  • 动作冒险类等
  • -
    适用于 12 周岁以上未成年人的游戏,对游戏内容有以下要求: -
  • 内容要素:游戏背景、情节、角色等设计丰富多彩。可存在清晰完整的故事背景和适量的人物关系;可存在积极美好的想象和虚构;可存在生存奋斗的故事情节;可存在部分基于历史和现实事件的改编,但不会与现实生活相混淆。 游戏中可有少量团队任务,需要组队完成。
  • -
  • 画面表现:游戏画面和美术设计应丰富美好、色彩鲜明。可为动漫或 3D 风格,不宜有写实的对抗画面。
  • -
  • 音效表现:可根据游戏情节增加不同风格的音效,但整体以较欢快活泼或舒缓悠扬为主。
  • -
  • 竞技和对抗:竞技和对抗玩法基于一定难度的思维判断或肢体操作鼓励玩家通过思考和训练达成目标。 游戏存在少量与情节相关的对抗内容,可存在以对抗为主的竞技比赛,但对抗内容以非写实的形式出现,不会与现实生活相混淆。不含宣扬、美化不当对抗行为的内容。
  • -
  • 社交系统:游戏中可存在基于文字、语音、图片等自由交流的陌生人社交系统,但社交系统的管理应遵循相关法律 法规。
  • -
    适用于 12 周岁以上未成年人的游戏宜具有下列功能: -
  • 知识学习:包含初、中级基础知识、学科入门知识等内容。
  • -
  • 技能与行为训练:可培养玩家自理能力和日常学习能力。
  • -
  • 认知促进:有助于玩家锻炼逻辑思维能力、辨别能力、推理判断能力、注意广度与分配能力,提高手眼协调能力和反应能力等。
  • -
  • 情感与审美:可引导玩家建立自信,有助于玩家开阔眼界、提升基础审美能力及了解中华传统文化等。
  • -
  • 社会化发展:有助于玩家提升沟通能力、建立团队合作意识,学习恰当处理自己与环境、他人的关系。可培养玩家品德,帮助玩家深入了解社会主义核心价值观。
  • -
    - - 适合 16 周岁以上的未成年人使用的游戏,玩法复杂,投入精力较多,游戏流程耗时较长。如: -
  • 内容操作较复杂的角色扮演类
  • -
  • 动作冒险类
  • -
  • 射击类(战术竞技类)
  • -
  • MOBA
  • -
  • 策略类
  • -
  • 卡牌类
  • -
  • 棋牌类等
  • -
    适用于 16 周岁以上未成年人的游戏,对游戏内容有以下要求: -
  • 内容要素:游戏背景、情节、角色等设计丰富多彩且富有个性,可含有宏大的故事背景和丰富的人物关系。可存在合理的想象和虚构,有适量基于历史和现实事件的改编,但不会与现实生活相混淆。游戏内容对玩家逻辑思维、团队协 作能力有一定要求。游戏中可存在较多大型团队任务或团队比赛。
  • -
  • 画面表现:游戏画面和美术设计多样化,可为 3D 写实风格,但不会与现实生活相混淆。
  • -
  • 音效表现:游戏中有丰富的音效,以烘托游戏氛围。
  • -
  • 竞技和对抗:竞技和对抗玩法可基于较高难度的思维判断或肢体操作,鼓励玩家提升和挑战自我。 游戏以对抗为主要玩法,可存在大型对抗竞技比赛,竞争内容可以较写实的形式出现(对抗双方均为写实的人类形象或人形生物且对抗展 示了接近真实的击打效果),但不会与现实生活相混淆。游戏允许用户之间的对抗行为,不含宣扬、美化不当对抗行为的内容。
  • -
  • 社交系统:游戏中可存在基于文字、语音、图片等自由交流的陌生人社交系统,但社交系统的管理应遵循相关法律 法规。
  • -
    适用于 16 周岁以上未成年人的游戏宜具有下列功能: -
  • 知识学习:包含中、高级知识体系、较复杂的学科知识等。可能包含大量课本外的文学、科学、历史、地理等知识,帮助玩家扩充知识面。
  • -
  • 技能与行为训练:包含全面的日常生活与学习技能的指导和训练,或可扩充训练玩家专业技能等。
  • -
  • 认知促进:有助于玩家锻炼独立思考和分析解决问题能力、进一步提高逻辑思维能力、推理判断能力、手眼协调和快速反应能力等。
  • -
  • 情感与审美:可引导玩家建立自信,鼓励玩家提升和挑战自我。有助于玩家开阔眼界、进一步提升审美能力,或深入传播中华传统文化等。
  • -
  • 社会化发展:提升玩家责任感,维持良好的社群关系。可传播和巩固社会主义核心价值观。
  • -
    - -### 4.3 适龄分级提交入口 - -可在 [开发者中心](https://developer.taptap.cn/) >> 商店 >> 申请上架/更新游戏 >> 游戏资料 >> 适龄分级 中选择后提交审核,审核通过后会显示在游戏详情页中的游戏名下方。 - - - -
    - -
    - - -## 5. 移动互联网应用程序备案及审核规范 - -### 5.1 政策说明 - -依据《中华人民共和国反电信网络诈骗法》、《互联网信息服务管理办法》(国务院令第292条)等多项政策法规要求,工业和信息化部于 2023 年 8 月 4 日正式发布[《关于开展移动互联网应用程序备案工作的通知》(工信部信管〔2023〕105号)](https://www.miit.gov.cn/zwgk/zcwj/wjfb/tz/art/2023/art_920db564162e4312916a01bed6540ad8.html),要求在中华人民共和国境内从事互联网信息服务的 APP 主办者依法履行备案手续,未履行备案手续的,不得从事 APP 互联网信息服务。 - -**备案流程** - -![](https://capacity-files.lcfile.com/crCRjSt9k9JRXlkkWE1rDETAwXJndSUE/beian.jpg) - -1. 初审时间:初审时间预计 1 个工作日。 - -2. 核验信息:APP 主办者收到短信通知后,需要登录[「ICP/IP地址/域名信息备案管理系统」](https://beian.miit.gov.cn/#/Integrated/index)确认信息。 - -3. 管局审核时间:管局审核时间为 20 个工作日左右,审核结果管局将以短信和邮件的形式通知 APP 主办者,或请通过[「ICP/IP地址/域名信息备案管理系统」](https://beian.miit.gov.cn/#/Integrated/index)查看备案结果。 - -### 5.2 移动互联网应用程序备案填写要求 - -1. ICP 备案号请正确填写 APP 的备案号信息,例如:京ICP备2023111111-1A。 - -2. 移动互联网应用程序备案的主办单位信息如果与厂商主体信息不一致,请提供完整、真实、可追溯的独家授权证明。请注意,授权证明中必须提供授权方的身份证件信息,企业是统一社会信用代码和公章,个人是身份证号码和手印。 - -3. ICP 备案截图请截取[「ICP/IP地址/域名信息备案管理系统」](https://beian.miit.gov.cn/#/Integrated/index)的图片信息。 - -![](https://capacity-files.lcfile.com/7yWd1kicAMiJm6tbAjwjhU7q4KGktvFn/image-1.png) - -![](https://capacity-files.lcfile.com/GSBSqwR8JP2deJHMIQ7IrAo7dnOwHX0F/image-2.png) - -### 5.3 移动互联网应用程序备案提交入口 - -可在[开发者中心](https://developer.taptap.cn/) >> 商店 >> 游戏资质 >> ICP 备案中按要求提交相关证明后保存信息,然后点击右上角提交审核。 - -![](https://capacity-files.lcfile.com/WQpKV2FwgtJfEDsRCA4C0WqlDSguRM1v/image-3.png) - -关于移动互联网应用程序备案还有其他疑问,可点击[《移动应用程序备案通知及要求》](/store/release/store-policy/filing-notice/)查看细节。 - - diff --git a/docs/store/store-admin.mdx b/docs/store/store-admin.mdx deleted file mode 100644 index f6a29d5d6..000000000 --- a/docs/store/store-admin.mdx +++ /dev/null @@ -1,145 +0,0 @@ ---- -title: 新建和管理开发者账号 -slug: /store -sidebar_position: 2 ---- - -import { Red, Blue, Black, Gray } from "/src/docComponents/doc"; - -## 1. 厂商创建通道及流程 - -进入 [开发者中心](https://developer.taptap.cn/) >> 点击 创建厂商 >> 选择需要申请入驻的开发者类型(注册开发者 / 认证开发者) >> 填写资料 >> 保存并提交审核 >> 通过审核后即可进入开发者后台。 - -| 入驻类型 | 前提条件 | 必要的资料信息 | 非必要的资料信息 | 是否需要审核 | 厂商详情页 | -| ---------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------- | -| 注册开发者 | 具备可用的 TapTap 账号 | 厂商名称、邮箱、手机号(需要验证) | 厂商英文名称 | 无需审核 | 不对玩家展示 | -| 认证开发者 | 具备可用的 TapTap 账号 | **个人主体**
    • 厂商名称、开发者姓名、常用地址、身份证正反面扫描件
    • 邮箱、手机号(需要验证)

    **企业主体**
    • 厂商名称、单位名称、注册地址、统一社会信用代码、营业执照扫描件、认证公函扫描件
    • 联系邮箱、手机号(需要验证)
    | **个人主体**
    • 厂商英文名称
    • QQ、微信

    **企业主体**
    • 厂商英文名称
    • QQ、微信
    • 法人信息
    • 紧急联系人信息
    | 需要审核 | 审核通过后对玩家展示 | - -![gameadmin](/img/创建厂商.png) - -**特别注意** - -- 注册开发者 通道最多支持创建 2 个「未发布」状态的游戏,且游戏无法提交审核。如需在 - TapTap 平台运营上线游戏,需要选择入驻厂商类型为 认证开发者 -- 已成为 注册开发者 账号的厂商主体,可以直接在 [开发者中心](https://developer.taptap.cn/) 申请成为认证开发者,无需重复创建厂商申请账号 - -![gameadmin](/img/变更认证开发者.png) - ---- - -## 2. 权限管理 - -**仅超级管理员可进行此项操作(超级管理员是首次申请成为开发者的账号)。** - -### 2.1 添加成员或超级管理员 - -进入 权限管理 >> 点击添加用户 >> 输入用户昵称或用户 TapTap ID >> 角色类型选择「成员」或「超级管理员」。 - -![gameadmin](/img/添加成员或管理员.png) - -### 2.2 成员的权限设置和删除 - -用户权限可以根据项目需要划分「厂商权限」和「游戏权限」。 - -进入 编辑权限 >> 厂商权限 >> 可设置「厂商管理员」对游戏进行创建、更新的操作权限,或设置「财务」对账单、游戏服务账户的操作权限。 - -进入 编辑权限 >> 游戏权限 >> 可以根据实际需要,分游戏单独进行勾选操作权限。 - -在对应用户后面点击 “┆” 按钮,可将该用户的权限「设为超级管理员」或「移除用户」。 - -![gameadmin](/img/权限管理1.png) -![gameadmin](/img/权限管理2.png) -![gameadmin](/img/成员身份调整.png) - -### 2.3 角色配置 - -权限管理 >> 角色配置 中默认配置了一些常用的角色。如:开发、发行、投放等,对应角色所拥有的权限可在“查看角色”中了解,默认角色的权限是无法修改的。 - -如需自定义角色权限,可选择「添加角色」。角色类型分两种,一种为针对游戏页管理权限的「游戏角色」,另一种为针对厂商页管理权限的「厂商角色」,主管理员添加角色后根据需要进行命名和权限勾选即可。 - -![gameadmin](/img/角色配置.png) - ---- - -## 3.工单咨询 - -TapTap 为您提供一站式自助功能,如遇到更多问题可在 [开发者中心](https://developer.taptap.cn/) >> 工单 中创建工单咨询。 - -![gameadmin](/img/工单咨询1.png) -![gameadmin](/img/工单咨询2.png) -![gameadmin](/img/工单咨询3.png) - ---- - -## 4.开发者认证 - -**仅主管理员可进行此操作** -TapTap 开发者认证是企业或个人开发者独有的认证,已认证玩家头像下方将会有蓝色“√”标志,该认证头衔会显示在个人主页。此认证仅为展示作用,不包含开发者中心的权限。 - -申请和删除开发者认证,可在 [开发者中心](https://developer.taptap.cn/) >> 工单 中对应的分类下提交申请。头衔固定形式为 【 厂商名称 / 游戏名称 】 + 【 官方 / 职位 】(申请时请勿使用花名,如吉祥物、打杂等) - -![gameadmin](/img/开发者认证.png) - ---- - -## Q&A - -### **1.个人开发者与企业开发者有什么区别?可以互相转换吗?** - -个人开发者与企业开发者区别仅在于申请开发者时提供的申请资料类型不同,[开发者中心](https://developer.taptap.cn/) 的功能体验也无区别。 - -个人主体需提供开发者个人信息,企业主体则需要提供企业信息。(该信息可在开发者注册完成后在 厂商设置 >> 账号信息中查看) - -TapTap 暂不支持个人主体和企业主体之间身份的相互转换,**请根据实际情况选择开发者类型进行申请**。如需修改注册时提交的资料,请按照 [解绑厂商超级管理员的流程](/store/#6-主管理员可以解绑吗可以设置新的主管理员吗) 申请,当超级管理员身份解绑后,可使用相同的厂商名称重新创建原厂商名称,然后修改资料。 - -### **2.个人开发者没有公司,如何填写厂商名称?** - -建议以团队或工作室名称而非个人名称作为厂商名称,例如:XXX 工作室。 - -### **3.厂商名称已存在,如何进行厂商账号认领?** - -请于 [开发者中心](https://developer.taptap.cn/) >> 工单 中对应的分类下提交申请,提供企业营业执照扫描件或其他能证明您是该厂商官方人员的材料,并以相同厂商名称提交开发者申请。 - -**【申请格式】** - -> **申请标题** -> -> TapTap 厂商账号认领申请 - XXX 厂商名称 - -> 需认领的厂商名称或厂商页链接: -> -> 营业执照扫描件或相关证明材料(可以附件形式上传) : -> -> 申请人联系方式: - -TapTap 官方人员确认所提交资料合规后,预计在 2 个工作日内操作。厂商申请审核通过后,即完成开发者账号认领,申请人员自动成为该厂商主管理员。 - -### **4.如何修改厂商在详情页的展示名称** - -请于 [开发者中心](https://developer.taptap.cn/) >> 工单 对应的分类下提交申请。 - -### **5.开发者审核通过后需要立即进行游戏入库吗?** - -不需要。 -TapTap 官方对创建游戏时间无要求,请厂商根据自己的时间安排进行游戏创建。TapTap 建议尽早进行游戏入库,积累预约玩家。 - -### **6. 主管理员可以解绑吗?可以设置新的主管理员吗?** - -可以。 -解绑超级管理员后将无法再次进入[开发者中心](https://developer.taptap.cn/),确认解绑操作后,请于 [开发者中心](https://developer.taptap.cn/) >> 工单 对应的分类下提交申请。 - -原主管理员解绑后,请尽快以相同厂商名称创建厂商账号并提交审核,在相关资料核实无误的情况下,TapTap 官方预计在 2 个工作日内通过审核。此申请审核通过后,申请人员将自动成为该厂商账号的超级管理员。 - -若厂商申请被驳回或无法登录厂商账号,可通过运营部邮箱 [operation@taptap.com](mailto:operation@taptap.com) 联系我们。 - -### **7. 用户账号绑定的邮箱可以更换吗?** - -可以。 -请确认新邮箱未绑定其他账号,否则将无法成功换绑。确认后请于 [开发者中心](https://developer.taptap.cn/) >> 工单 对应分类下提交申请,在相关资料核实无误的情况下,TapTap 官方预计在 3 个工作日内通过审核,更换完成后即可使用新邮箱接收验证码登录厂商账号。 - -若邮箱更换后仍无法登录厂商账号,可通过运营部邮箱 [operation@taptap.com](mailto:operation@taptap.com) 联系我们。 - -### **8. 厂商主体可以注销不对玩家展示吗?** - -可以。 -厂商主体注销是不可逆的操作,请确认需要注销主体后,在 [开发者中心](https://developer.taptap.cn/) >> 工单 对应分类下提交申请。注销后您将无法再次进入厂商账号,如需重新发布游戏请再次创建厂商账号。 diff --git a/docs/store/store-contact.mdx b/docs/store/store-contact.mdx deleted file mode 100644 index 53f774e2b..000000000 --- a/docs/store/store-contact.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: 联系我们 -sidebar_position: 20 ---- - -推荐在 **开发者中心导航栏右上角提交工单** 联系我们。 - -除此以外,也可以通过邮件联系我们: - -- 运营部邮箱: -- 商务合作邮箱: diff --git a/docs/store/store-devagreement.mdx b/docs/store/store-devagreement.mdx deleted file mode 100644 index c7aa12e96..000000000 --- a/docs/store/store-devagreement.mdx +++ /dev/null @@ -1,260 +0,0 @@ ---- -title: TapTap 平台开发者协议 -sidebar_position: 1 ---- - -# **TapTap 平台开发者协议** - -更新日期:2024 年 5 月 27 日 - -生效日期:2024 年 5 月 27 日 - -欢迎您使用 TapTap 平台及相关服务! - -为使用易玩(上海)网络科技有限公司及其关联公司(以下简称“TapTap”)向开发者提供的 TapTap 平台及相关服务(以下简称“本服务”),您应当阅读并遵守《TapTap 平台开发者协议》(以下简称“本协议”)。**请您务必审慎阅读、充分理解各条款内容,特别是以加粗形式提示您重点注意的免除或限制责任的条款,以及开通或使用某项服务的单独协议、规则**。除非您已阅读并接受本协议及相关协议、规则等所有条款,否则,您无权使用本服务。一旦您选择“同意”(具体措辞以相关页面最终展示为准),或您以任何方式使用本服务,即视为您已阅读并同意上述协议、规则等的约束。 - -**当您有违反本协议的任何行为时,TapTap 有权依照违反情况依据自身的判断,随时不经通知即单方采取限制、中止或终止向您提供服务等措施,并有权追究您的相关责任,您对此表示理解并接受。** - -**如果您代表您的雇主或其他实体同意接受本协议的约束,则表示您声明并担保,您已取得充分的合法授权,可使您的雇主或此类实体受本协议的约束。如果您未获得必要的授权,则不得代表雇主或其他实体接受本协议或使用 TapTap 平台。** - - - -## **1. 定义** - -1.1 **TapTap 平台**:是指由 TapTap 拥有和/或运营的以网站(域名包括但不限于 taptap.com、taptap.cn、taptap.io)及客户端等各种形态(包括未来技术发展出现的新服务形态)向您提供产品管理等服务的平台(包括但不限于 TapTap 开发者中心、TapTap 推广平台、TapTap REP 平台、TapADN 平台等)。 - -1.2 **开发者**或**您**:是指已在 TapTap 开发者中心注册成功并且按照本协议条款取得开发者账号的任何个人、法人或其他组织。 - -1.3 **开发者账号**:是指开发者在 TapTap 开发者中心注册取得,并由 TapTap 平台分配给开发者的账号,可让开发者通过 TapTap 平台提供和/或推广产品并使用 TapTap 平台提供的其他服务。 - -1.4 **产品**:是指通过 TapTap 平台提供和/或推广的游戏、内容、数字资料及其它产品和服务。 - -1.5 **技术支持**:TapTap 向您提供的软件、代码、API 和 SDK 及相关文档的总称。 - -1.6 **API**:Application Program Interface,即 TapTap 提供给您的应用程序开放接口。 - -1.7 **SDK**:Software Development Kit,即 TapTap 提供给您的软件开发工具包。 - -1.8 **用户**:指所有为自身用途而非为转分发、转售之目的直接或间接使用您发布或更新在 TapTap 平台中的产品的用户。 - -1.9 **用户数据**:是指用户在 TapTap 平台、产品等中产生的与用户相关的数据,包括但不限于用户使用 TapTap 平台、产品等时主动提供的信息、经用户同意由 TapTap 平台获取的信息、用户操作行为形成的数据及各类交易数据等。除 TapTap 隐私政策另有约定外,“用户数据”的所有权及其他相关权利属于 TapTap 平台,且是 TapTap 平台的商业秘密。 - -1.10 **平台运营数据**:是指用户、开发者在 TapTap 平台、应用等中产生的相关数据,包括但不限于用户或开发者提交的数据、操作行为形成的数据及各类交易数据等。“平台运营数据”的所有权及其他相关权利属于 TapTap 平台,且是 TapTap 平台的商业秘密,依法属于用户或开发者享有的相关权利除外。 - -1.11 **品牌标识**:是指各方独立拥有(或取得合法授权)的游戏名、商标、Logo、域名以及其他显著品牌特征的标识。 - -1.12 **TapTap 平台用户侧功能**:指 TapTap 向用户提供的各项服务与功能,包括但不限于内容发布、消息中心、游戏预约与下载等。 - -1.13 **法律/法律法规**:是指中华人民共和国大陆地区(不含港澳台地区)及相关适用的法律、行政法规、规范性文件、监管政策等。 - - - -## **2. 账号** - -2.1 为使用本服务,您需要创建开发者账号并完成在线实名认证流程以开通开发者账号并成为开发者。 - -2.2 您需要满足(a)至(d)或(e)至(f)的要求方可开通开发者账号: -(a)您已达到与 TapTap 订立有约束力的合同的法定年龄; -(b)根据适用的法律,您未被禁止接受本服务; -(c)您具备与 TapTap 订立本协议的能力并能承担相应违约法律责任; -(d)您具备与 TapTap 订立本协议所需要的资质或许可; -或 -(e)您是合法成立的独立法人; -(f)您具备与 TapTap 订立本协议所需要的资质或许可。 - -2.3 TapTap 有权依据法律法规的要求,在您申请开通开发者账号时审查您是否满足上述第 2.2 条中所述的要求,并定期或不定期进行巡查,您应当积极配合。 - -2.4 您应确保您提供的主体资质材料、相关资质或证明以及其他任何文件等信息真实、准确、完整,并在信息发生变更后的三个工作日内进行更新。您应承担由于您的信息不准确或未及时更新导致您和/或 TapTap 遭受的任何损失、责任或惩罚,且 TapTap 有权随时中止或终止向您提供本服务。 - -2.5 开发者账号一经开通,不得变更,不可转让、不可赠与、不可继承。您不得以任何形式与任何第三方分享您的开发者账号和密码。**您应妥善保管账号和密码,并对您的开发者账号的全部行为负责。** - -2.6 您可以在开发者账号下创建虚拟账号、游戏官方号等(以下统称为“子账号”,具体类型由 TapTap 平台确定),以便更高效使用 TapTap 提供的各项服务。您应妥善管理子账号的安全,且**同意对子账号的全部行为承担责任。** - -2.7 您可以在开发者账号和/或子账号下建立团队,并将您的团队成员的账号加入您的团队,作为您的开发者账号和/或子账号下的成员账号。您可以授权这些成员账号以您的名义使用 TapTap 平台提供的各项服务。您应严格管理成员账号权限,且**同意对团队成员的成员账号的全部行为承担责任。** - -2.8 您不得违反本协议的宗旨将您的开发者账号号用于其它目的。如因您违反本协议导致开发者账号被 TapTap 停止本服务或删除的,未经 TapTap 事先书面同意,您不得再次注册。 - - - -## **3. 您对 TapTap 平台的使用** - -3.1 您在此陈述、保证和承诺: -(a)您具有必要的权利和授权签订本协议,且订立或履行本协议不会违反您与第三方的任何协议或侵犯任何第三方权利,也不会违反任何适用的法律法规; -(b)您在 TapTap 平台进行的所有活动、您对本服务的使用、您通过 TapTap 平台提供或推广给用户的所有产品及您发布的信息和内容:(i)不违反任何适用的法律、法规、政策、行业惯例或相关管辖区域的规范或指南;(ii)不会侵犯 TapTap 或任何第三方的合法权益(包括但不限于隐私权、知识产权、名誉权、肖像权、商业秘密等);(iii)不违反 TapTap 平台公告、提示、要求及其他您同意的单独的协议或规则等;(iv)若您的开发者账号或子账号使用 TapTap 平台用户侧服务的,您应同时遵守[《TapTap 服务协议》](https://www.taptap.cn/doc/terms/)等面向用户的相关条款、规则的要求; -(c)您承诺遵守任何可能适用的网络安全相关的法律法规,不会从事或参与任何干扰、中断、破坏或未经授权访问 TapTap 平台或任何第三方(包括但不限于用户、移动网络运营商)的设备、服务器、网络、软件或其它财产或服务的活动; -(d)您不会干扰或试图干扰 TapTap 平台的正常运营; -(e)您不会越过、试图越过或声称能够越过任何内容保护机制或 TapTap 提供的数据分析工具,或蓄意引导用户使其认为他们在与 TapTap 直接互动; -(f)无论何时您都将遵守本协议及 TapTap 不定期在 TapTap 平台发布的与本服务有关的管理政策及其它相关指南、规则和政策; -(g)您不会参与或以其它任何方式涉及以下任何活动:(i)任何违反适用的法律法规的活动;(ii)任何导致 TapTap 违反任何适用的法律法规的活动;或(iii)任何导致 TapTap 在适用的法律法规下被处以任何罚款、制裁、限制或承担任何法律责任的活动。 - -3.2 TapTap 授予您有限、非独家的、不可转让的、不可转授的、可撤销的许可,供您依照本协议及其他相关指南、规则和政策访问和使用本服务。您理解并同意,您应合理使用本服务,TapTap 有权自行判断您的使用是否合理,一旦认定为不合理使用,您应按照 TapTap 要求进行代码整改等操作。如果由于您的安全措施不足导致相关损失,您自行承担责任,造成 TapTap 或任何第三方损失的,您应予赔偿。 - -**3.3 如 TapTap 发现您违反本协议中的任何陈述、保证、约定和承诺,TapTap 有权根据自己的合理判断采取以下一种或多种行动(且不影响 TapTap 根据本协议和适用法律享有的权利和救济):(i)随时删除相关信息或内容,和/或终止相关产品在 TapTap 平台的分发、推广和运营;(ii)要求您替换或修改违法或侵权内容;(iii)停止本服务、(iv)暂时中止履行合同义务;(v)删除开发者账号;和/或(vi)暂停或终止本协议。若因此造成您或用户损失的,由您自行承担。** - -3.4 TapTap 仅向您提供面向所有开发者开放的技术支持及服务保障。您理解并同意,对您使用 TapTap 技术支持发布的任何产品产生的任何纠纷、责任等,以及您违反相关法律法规或本协议约定引发的任何后果,均由您独立承担责任、赔偿损失,与 TapTap 平台无关。如侵害到 TapTap 平台或他人权益的,您须自行承担全部责任并赔偿一切损失。 - -3.5 TapTap 平台提供的部分服务(包括但不限于付费服务、TapTap SDK 服务、TapTap API 服务等)会有其单独的协议或规则,您以任何形式登录、使用上述服务即表示您已理解并接受该服务的相关协议、规则等的约束。 - -3.6 游戏评分。用户可通过 TapTap 平台对游戏进行评分和评价。TapTap 平台有权自行确定或变更游戏在 TapTap 平台中的展示位置,并有权以其自行决定的方式向用户展示游戏及评分。 - -3.7 上传及更新。您要负责将您的游戏上传到 TapTap 平台,同时向用户提供所需的游戏信息和支持,并准确地公开必需的隐私政策、安全权限等,以确保游戏在用户设备上的正常运行。如果游戏未正常上传或存在违法违规情形,TapTap 平台将不予发布。您的游戏应符合 TapTap 平台在技术、安全等方面的统一要求,以确保您可以在 TapTap 平台安全、稳定地运营游戏。同时为了向用户提供优质的产品和服务,您应在游戏上线运营(包括但不限于下载、测试、试玩等模式)后提供游戏的持续更新,并保证通过 TapTap 平台提供的游戏版本为用户通过各种公开渠道所能获得的最新版本,即您向 TapTap 平台提供的游戏版本不应低于该游戏在其他任何安卓游戏平台或渠道提供的游戏版本,无论其他平台或渠道提供的游戏版本是否系您自行或授权他人提供。 - -3.8 您需为您的游戏提供支持。如果从 TapTap 平台下载及安装的游戏出现任何缺陷或性能问题,用户会依照说明与开发者联系。您必须对相关问题全权负责,TapTap 平台无须负责承担或处理您游戏的支持和维护工作,以及处理所有与您的游戏相关的投诉。如果您的游戏没有提供适当的信息或支持,则可能会导致出现游戏评分较低、游戏曝光率降低、销售量下降、结算纠纷等情况,且 TapTap 平台有权将游戏从 TapTap 平台下架。 - -3.9 重新安装。您授权用户可以多次重新安装通过 TapTap 平台下载的每个游戏,除非根据相关规定由您或 TapTap 平台对游戏进行了彻底移除。 - -3.10 您可选择向用户提供游戏的免费下载或付费下载,TapTap 仅以您的名义显示游戏。若您选择付费下载,您有权自行或与 TapTap 平台协商确定游戏在 TapTap 平台的销售价格,同时应当与 TapTap 另行签署相关游戏合作协议,对游戏发布事宜作出具体约定。 - - -## **4. 您对 TapTap SDK(或 TapTap API)的使用** - -4.1 若您选择使用 TapTap SDK(或 TapTap API)服务,您需保证(并应促使您的用户保证)您(及您的用户)的使用不违反国家各项法律法规的规定,且不侵害任何第三方权益,如因此造成任何后果及损失,由您(及您的用户)自行承担全部责任。 - -4.2 您充分理解并同意(并应促使您的用户充分理解并同意),您(及您的用户)在使用 TapTap SDK(或 TapTap API)时,除非法律法规允许且 TapTap 事先书面许可,您(及您的用户)不得从事以下活动,也不得同意、授权或指示任何第三人从事包括但不限于以下活动: -(a)任何有损 TapTap 一切相关知识产权的行为,包括但不限于删除相关服务中包含的任何版权声明、商标声明或其他所有权声明; -(b)向任何第三方提交错误地明示或暗示向您提供的服务为 TapTap 所属、赞助或认可的内容等; -(c)宣扬或提供关于非法行为的说明信息、宣扬针对任何团体或个人的人身伤害或传播任何病毒、蠕虫、缺陷、特洛伊木马或其他具有破坏性的内容等; -(d)对 TapTap SDK(或 TapTap API)进行逆向工程、反编辑或试图从 TapTap SDK(或 TapTap API)或相关内容的任何部分提取源代码,或获取原始数据和其他数据等; -(e)对 TapTap SDK(或 TapTap API)或者其运行过程中释放出的任何数据或相关交互数据进行复制、更改、修改等操作,包括但不限于使用插件、外挂或非经授权的第三方工具或服务接入相关服务或相关系统; -(f)创造任何网站或应用程序以重现或复制相关服务或 TapTap 平台; -(g)其他 TapTap 认为不应该、不适当的行为、内容。 - -4.3 您不得滥用 TapTap SDK(或 TapTap API)服务。您理解并同意,TapTap 对您每天发起的服务请求次数及并发请求量具有一定的限制,且 TapTap 有权自行判断您的使用是否为滥用。一旦您的使用达到上限或被 TapTap 认定为滥用,TapTap 将可能暂时中断您(或您的用户)对服务的正常使用,且您应按照 TapTap 要求进行代码整改等操作。在双方协商一致的前提下,您(及您的用户)应在应用中正确、完整、醒目地标注「TapTap」或「技术由 TapTap 提供」的字样。 - -4.4 您(及您的用户)应对 TapTap SDK(或 TapTap API)的内容自行判断并决定是否使用,并承担因使用相关内容而引起的所有风险,包括因对 TapTap SDK(或 TapTap API)及其内容的真实性、完整性、准确性、及时性及实用性的依赖而产生的风险。TapTap 不对此提供任何担保和保证,不对因前述风险而导致的任何后果或损失对您(或您的用户)承担责任。 - -4.5 您同意(并应取得您的用户事先同意)在此授予 TapTap 免费的、非独家的和不可转让的权利和许可,在本协议期限内使用您(及您的用户)的标志或行为来宣传您(及您的用户)对 TapTap SDK(或 TapTap API)的使用。 - -4.6 若您使用 TapTap SDK(或 TapTap API)需要收集您的用户任何数据的,您应当遵守相关法律法规要求,事先获得您的用户的明确同意,同时应当告知您的用户相关数据收集的目的、范围以及您如何使用、如何与 TapTap 及其他第三方共享等,以保障您的用户的知情权。您承诺并保证,您仅出于有限目的收集并根据适用法律法规合法处理用户的个人数据,并仅在最低限度的必要期限内留存个人数据。您应保证您所持有的个人数据的安全,并保护此类数据免遭任何实际或潜在的滥用。 - - - -## **5. 授权和知识产权** - -5.1 您授予 TapTap 在全球范围内非独占、免费的许可,允许 TapTap 平台向用户提供您上传至本平台的产品及产品信息。 - -**5.2 为避免疑义,除非您与 TapTap 平台另有书面协议约定,一旦您通过 TapTap 平台向用户提供任何版本的游戏,即表示您同意授予 TapTap 平台相关非独占的许可,包括但不限于允许 TapTap 平台发布该游戏包括其任何后续版本,向用户提供该游戏预约、测试、下载服务以及本协议第 5 条所述的非独占许可,并自愿在上述许可期间内遵守本协议项下有关权利义务的规定,无论该游戏于 TapTap 平台提供预约或测试当时或之后您是否已授予或即将授予除 TapTap 平台以外的第三方在全球范围或指定区域发布游戏的独占许可。** - -5.3 您授予 TapTap 平台在全球范围内非独占的许可,允许 TapTap 平台在与以下事项相关的活动中复制、运行、展示、分析、使用您的产品和产品信息以及您提交给 TapTap 的品牌标识和公司信息:(a)TapTap 发起的运营和营销活动;(b)以介绍、推广、宣传您的产品为目的的公开展示;(c)TapTap 平台内部的数据共享;(d)改善 TapTap 平台服务;以及(e)检查是否符合本协议及其他相关的平台政策。 - -5.4 您承诺并保证,您拥有产品包含的以及与之相关的所有知识产权,包括所有必需的专利、商标、商业机密、版权或其他专有权利。如果您使用了第三方提供的内容,则需要承诺并保证您有权发布产品中的第三方内容,并取得第三方的授权文件。您同意,您不会向 TapTap 平台提交受版权、商业机密或第三方专有权利(包括专利、隐私权和公开权)保护的任何内容,除非您是此类权利的拥有者,或者此类权利的合法拥有者允许您提交这些内容。 - -**5.5 您同意并保证,在 TapTap 平台提供游戏预约、测试、试玩或下载后,为保证玩家利益及用户体验,您应持续不断地更新并提供下载。如您未履行持续更新及提供下载的义务,视为您授权 TapTap 平台有权自行更新,包括有权根据您在其他渠道发布的版本于 TapTap 平台进行自动更新等。本约定优先于您与其他渠道签订的任何协议,由此造成的所有损失由您独自承担,并且您应赔偿给 TapTap 平台造成的全部损失。** - -5.6 为促进产品的宣传推广,您授权用户有权在 TapTap 平台内发布含有您的产品内容的图片或视频。用户通过授权发布的内容,不得违反法律法规及 TapTap 平台规范。用户的违规行为,您有权自行处理或通过 TapTap 平台规定的方式进行投诉。 - -5.7 TapTap 拥有 TapTap 品牌标识、TapTap 平台及 TapTap 技术支持(以下简称为“TapTap 内容”)的所有知识产权(包括但不限于商标权、版权和专利权)。如您在产品的开发或推广过程中需使用 TapTap 内容和/或 TapTap 知识产权,您只能在经 TapTap 事先明确书面许可后方能使用。未经 TapTap 事先书面许可,您不得(且不应协助、鼓励任何其他第三方):(a)使用、复制、出版、发行、复制、修改、转载、翻译、传播或分发任何 TapTap 内容的部分或全部;(b)出租、出借、出售、再许可、转让或以其它方式处置任何 TapTap 内容的部分或全部,或与 TapTap 内容相关的任何权利;或(c)复制、反向工程、反编译、反汇编 TapTap 技术支持或创作 TapTap 技术支持的衍生品。 - - - -## **6. 下架产品** - -6.1 开发者下架游戏。除非您与 TapTap 平台另有书面协议约定,下述情形下您可以主动申请终止通过 TapTap 平台向用户发布游戏(“下架”):(a)您决定终止游戏运营的;且(b)您已在所有其他安卓游戏平台或渠道下架该游戏,或保证其他平台或渠道下架该游戏的时间不晚于 TapTap 平台。您应当提前 90 日以书面通知方式告知 TapTap 平台相关决定,书面通知应当包括拟终止运营的原因、游戏在其他平台或渠道的下架计划或情况、游戏拟于 TapTap 平台下架的具体日期等,经 TapTap 平台确认并履行相关程序后方可下架。无论因为何种原因,如需要终止游戏运营的,您均应按照相关法规或办法的规定至少提前 60 日在游戏相关页面中向用户发布终止运营的公告,并关闭支付入口,公告应当持续发布至游戏正式终止运营之日止。 - -您理解并同意,游戏下架(a)不会影响之前已购买或下载该游戏的用户的许可权;(b)不会从已获得游戏的用户设备中移除该游戏;(c)不会更改您对用户之前所购买或下载的游戏或服务所承担的配送或支持义务。 - -6.2 TapTap 平台下架产品。尽管 TapTap 平台没有义务监控产品内容,但是如果 TapTap 平台通过您的通知、第三方投诉、用户举报、机构监管或其他方式知晓您的产品包括其任何部分或者您的品牌标识违反法律法规、本协议约定、TapTap 平台政策等,则 TapTap 平台可以自行决定从 TapTap 平台中下架该产品或重新对该产品进行分类。 - -6.3 您在此确认并同意:产品数据,包括但不限于游戏评论、游戏评分、游戏帖子、产品信息、产品点击量、产品下载量等,均属于用户数据和/或平台运营数据,相关权利属于用户和/或 TapTap 平台所有,若任何第三方擅自以转载、复制等方式使用用户数据和/或平台运营数据时,TapTap 平台有权以自身名义单独进行维权,包括但不限于发送权利通知、提起民事诉讼、进行行政举报等。为保护相关用户权益,产品下架后,除不再向用户提供与产品相关的服务外,TapTap 平台有权保留与下架产品有关的页面及数据。 - - - -## **7. 隐私及数据信息保护** - -7.1 用户隐私保护:您的产品应尊重用户的隐私,遵守国家关于数据保护的法律法规,包括但不限于,根据适用的法律法规的要求以自身名义发布隐私政策、就数据处理行为(包括但不限于数据收集、存储行为)获取用户的授权、在用户界面(如有)可见的显著位置展示上述隐私政策。您不得实施非法跟踪用户活动或泄露、破坏用户个人数据等侵害个人数据和隐私的行为。您承诺采取充分的数据安全保护措施。若您的产品会存储用户提供的个人信息,则您应保障存储方式安全,且仅在所需的期限内进行存储。 - -7.2 **若您使用 TapTap 平台部分功能可能从 TapTap 获取信息与数据的(包括但不限于用户数据、平台运营数据等,本条统称“数据”),您应当:** -(a)遵守相关法律法规,遵循合法、正当、必要和诚信原则,不会通过误导、欺诈、胁迫等方式来处理相关数据; -(b)在数据传输过程中采取的相关安全措施,并确保以符合 TapTap 要求的形式接收数据; -(c)采取加密等满足相关国家标准或行业标准的要求的安全措施进行数据的存储,并落实必要且不低于 TapTap 的管理和技术措施确保数据安全。存储期限应为实现合作目的所必需的最短时间。超出前述存储期限后,您应及时对数据进行删除或匿名化处理; -(d)严格按照与 TapTap 的合作协议或平台规则等使用数据,不得将数据用于对应合作目的之外的其他目的; -(e)在实现合作目的或收到 TapTap 书面要求后,立刻删除相关数据并确保其已被完全删除且无法恢复。如相关数据涉及个人信息,您应及时响应/协助 TapTap 响应个人信息主体的权利请求,包括但不限于知情权、决定权、拒绝权、查阅权、复制权、更正权、删除权和撤回同意权。 - -7.3 为了不断地对 TapTap 平台进行创新和改进,TapTap 平台可能会收集某些使用统计数据,包括但不限于有关如何使用 TapTap 平台的信息。TapTap 平台将对收集的数据进行汇总研究,以便根据用户和开发者的需求对 TapTap 平台进行完善,并会依照 TapTap 隐私政策保留这些数据。为了确保产品得到改善,TapTap 平台将在开发者后台提供产品在 TapTap 平台内的部分数据,若您需要额外数据,可以书面形式向 TapTap 平台申请,TapTap 平台视情况决定是否提供。 - -7.4 除 TapTap 隐私政策另有规定外,平台运营数据、用户数据等数据的全部权利,均归属 TapTap 平台,且是 TapTap 平台的商业秘密。未经 TapTap 平台事先书面同意,您不得为本协议约定之外的目的使用前述数据,亦不得以任何形式将前述数据提供给任何第三方。 - - - -## **8. 赔偿** - -8.1 在适用法律允许的最大范围内,您将就以下原因导致或与其相关的索赔、诉讼或主张为 TapTap 及其关联公司、子公司、管理人员、董事、员工、代理、合作伙伴、分包商、承包商及许可方(统称“TapTap 方”)辩护、使其免受伤害并予以赔偿: -(a)您违反了本协议的任何条款; -(b)您违反了任何您的陈述、保证或承诺; -(c)您或您的产品侵犯了 TapTap 或任何第三方的知识产权或任何其它权利; -(d)您或您的产品违反了任何法律法规; -(e)您与用户之间的争议。 - -8.2 您在本第 8 条下的赔偿包括由前述索赔、诉讼或主张(无论是基于合同、侵权、过失或其它理论)引起的任何责任、罚款、处罚、损害、费用、诉讼费及律师费。您承诺并同意在任何 TapTap 方合理要求时立即全力协助并配合其就前述索赔或要求进行辩护。TapTap 有权自费独立进行辩护,且全权处理与本第 8 条相关的事宜。 - -8.3 在适用法律允许的最大限度内,您访问和使用本服务的风险由您自行承担。TapTap 方对您的最大及全部责任以及您在本协议下使用或无法使用本服务或第三方服务而造成的任何及所有赔偿、索赔、法律程序、责任、义务、损失、损害、成本和/或财产损失的唯一救济措施是基于您遭受的实际损失,且无论是基于合同、侵权(包括过失)或其它任何理论,对您的赔偿不超过导致该等实际损失的事件发生前 1 个月内您为使用对应服务所支付的金额。**您理解并同意,TapTap 仅对由 TapTap 过错造成的损害负责,且 TapTap 不为任何数据丢失或损坏、利润损失、业务或商誉损失、业务中断和/或任何间接、附带、偶发、特殊、衍生性或惩罚性的损害(即使 TapTap 已被告知此类损害的可能性)承担任何责任。** - - - -## **9. 免责声明** - -**9.1 您理解并同意,TapTap 平台是在「现状」和「可提供」的基础上提供本服务,TapTap 平台不向您提供任何类型的担保,因此您同意自行承担使用 TapTap 平台的风险。TapTap 平台会尽最大努力向您提供服务,确保服务的连贯性和安全性;但 TapTap 平台不能保证其所提供的服务在任何程度和范围内都毫无瑕疵,也无法随时预见和防范全部法律、技术以及其他风险,包括但不限于不可抗力、病毒、木马、黑客攻击、系统不稳定、第三方服务瑕疵、政府行为等,以及因该等原因可能导致的服务中断、数据丢失以及其他的损失和风险。所以您也同意:即使 TapTap 平台提供的服务存在瑕疵,但上述瑕疵是当时行业技术水平所无法避免的,其将不被视为 TapTap 平台违约,同时,如由此给您造成数据或信息丢失等损失的,您同意放弃追究 TapTap 平台的责任。** - -**9.2 TapTap 平台进一步明确声明,TapTap 平台不提供任何形式的明示或默示的担保和条件,包括但不限于适销性、特定用途适用性以及不侵犯他人权利的默示担保和条件。** - -**9.3 鉴于网络服务的特殊性,TapTap 平台有权在无需通知您的情况下根据 TapTap 平台的整体运营情况或相关运营规范、规则等,随时变更、中止或终止部分或全部的服务,若由此给您造成损失的,您同意放弃追究 TapTap 平台的责任。** - -**9.4 为了向您提供更完善的服务,TapTap 平台有权定期或不定期地对提供本服务的平台或相关设备进行检修、维护、升级等,此类情况可能会造成相关服务在合理时间内中断或暂停,若由此给您造成损失的,您同意放弃追究 TapTap 平台的责任。** - -**9.5 您理解并同意,为了遵守法律法规、维护公序良俗、保护他人合法权益,TapTap 将在能力范围内根据相关投诉或通知尽最大的努力对平台内容进行判断和处理,但并不保证 TapTap 判断完全与司法机关、行政机关的判断相一致,由此产生的不利后果您已经理解并同意自行承担。** - -**9.6 在本协议中,“不可抗力”是指不能预见、不能克服并不能避免且对一方或双方造成重大影响的客观事件,包括但不限于自然灾害如洪水、地震、瘟疫流行和风暴等以及社会事件如战争、动乱、政府行为等。出现上述情况时,TapTap 平台将努力在第一时间与相关方配合,及时进行修复,若由此给您造成损失的,您同意放弃追究 TapTap 平台的责任。** - - - -## **10.协议终止** - -10.1 如果出现以下任意一种情况,TapTap 平台有权经书面通知后单方终止本协议: -(a)您违反了本协议或与 TapTap 平台另行签署的合作协议的规定; -(b)依照法律法规,相关判决、仲裁或政府机构的要求停止提供本协议项下相关服务; -(c)因不可抗力因素导致您无法继续使用本服务或 TapTap 平台无法提供本服务; -(d)您不再作为合法或有效的权利人有权使用 TapTap 平台服务; -(e)TapTap 平台决定不再提供本协议项下相关服务; -(f)本协议约定的其他服务终止条件发生或实现。 - -10.2 任一方有权在没有说明原因的情况下提前至少 90 天向另一方发出书面通知后自行终止本协议。如果您想要终止本协议,您将需要停止使用本协议项下的所有服务。 - -10.3 如本协议或本服务因为任何原因终止的,您应自行处理好相关数据等信息的备份以及与您的用户之间的相关事项等。对于您的账号中的全部数据或您因使用本服务而存储在 TapTap 平台服务器中的数据等任何信息,TapTap 平台可根据情况自主选择将该等信息保留或删除,包括服务终止前您尚未完成的任何数据。 - -10.4 任何本协议中明示或依其性质应当在协议终止后继续有效的条款,将在本协议终止后继续保持其完整效力,直至这些条款中的条件被满足或者依其性质应当到期为止。 - - - -## **11. 通知与协议变更** - -11.1 TapTap 可通过 TapTap 平台的网页公告、站内信及您在开发者账号中提供的联系方式向您发出与本协议及本服务相关的通知及重大变更。此类通知在发送时即视为已送达收件人。若出现争议,此类通知可作参考,且视为有法律效力的确证。 - -11.2 对于因本服务及其相关交易活动引起的任何纠纷,您同意 TapTap 或司法机关可以通过现代电子通讯方式或邮寄方式向您送达法律文书。您指定接收法律文书的电子邮箱为您在开发者账号中提供的最新电子邮箱,TapTap 或司法机关向上述联系方式发出法律文书即视为送达。您指定的邮寄地址为您的法定联系地址或您在开发者账号中提供的最新有效联系地址。 - -11.3 您同意 TapTap 或司法机关可采取以上一种或多种送达方式向您送达法律文书。如 TapTap 或司法机关采取多种方式向您送达法律文书,送达时间以上述送达方式中最先送达的为准。您同意上述送达方式适用于各个司法程序阶段。 - -11.4 您保证您在开发者账号中提供的联系方式是准确、有效的,并进行实时更新。如果因提供的联系方式不确切,或不及时告知变更后的联系方式,使法律文书无法送达或未及时送达,由您自行承担由此可能产生的法律后果。 - -11.5 TapTap 有权酌情对本协议进行变更,并会通知您。如果您继续使用本服务,即视为您已接受变更后的协议。如果您不接受变更后的协议,应当立即停止使用本服务。 - -11.6 TapTap 有权自行决定调整、增加或停止 TapTap 平台上的服务。除非另有特别声明,否则本协议将适用于新的 TapTap 服务。 - - - -## **12. 一般条款** - -12.1 本协议构成您和 TapTap 平台之间的完整法律协议,且您对 TapTap 平台服务的使用将受本协议的约束,同时,本协议将完全取代您和 TapTap 平台之前就使用 TapTap 平台服务达成的开发者协议。TapTap 平台保留随时修改本协议条款、平台政策等的权利。本协议未尽事宜,双方另有约定的从其约定。 - -12.2 您同意,即使 TapTap 平台未行使或强制执行本协议中所述的(或TapTap 平台根据任何适用的法律所享有的)任何法定权利或补救措施,也不应视为 TapTap 平台正式自动放弃这些权利,TapTap 平台仍然可以行使这些权利或采取相应补救措施。 - -12.3 您同意,TapTap 平台因您的开发者身份而向您披露的任何软件、服务、硬件、材料、文件等均属于 TapTap 的保密信息,除非获得 TapTap 平台书面同意,您不得将 TapTap 的保密信息透露给任何第三方。您应仅为履行本协议之目的使用保密信息,并采取不弱于您保护自有保密信息的安全措施程度的必要合理的措施保护 TapTap 的保密信息。该等保密义务在本协议解除或终止后仍然有效。 - -12.4 您使用本服务可能会受到服务使用地相关法律法规或政策的限制。您必须遵守所有适用于您的产品发布或使用所在国家或地区的相关法律法规或政策。这些法律法规或政策包括对产品目的地、用户以及最终用途的限制。 - -12.5 如果对此类事项有司法决定权的任何法院判定本协议的任何规定无效,则 TapTap 平台将在不影响本协议其余部分的情况下,将该规定从本协议中删除。本协议的其余规定仍继续有效并可予以执行。 - -12.6 未经另一方的事先书面许可,您或 TapTap 平台不得转让或转移本协议中授予的权利。未经另一方的事先书面许可,您或 TapTap 平台不得将本协议中规定的责任或义务委派给他人。其他任何试图转让的行为均无效。 - -12.7 因本协议或您与 TapTap 平台依据本协议建立的关系而产生的或与之相关的所有申诉均受中华人民共和国大陆地区法律的约束。此外,有关本协议的任何争议应由双方秉承善意友好协商解决,若协商不成,双方同意将争议提交上海国际仲裁中心按照其仲裁规则进行仲裁,仲裁语言为中文。尽管如此,您同意 TapTap 平台仍然可以向任何司法辖区内的法院请求禁令救济。 - -本页面内容具有多种语言版本,若其他语言版本与简体中文版本发生冲突,应以简体中文版本为准。 diff --git a/docs/store/store-editorschoice.mdx b/docs/store/store-editorschoice.mdx deleted file mode 100644 index aa54f2674..000000000 --- a/docs/store/store-editorschoice.mdx +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: 入选 TapTap 编辑推荐 -sidebar_position: 1.3 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -TapTap 致力于构建热爱游戏的玩家和带来好作品的开发者之间的桥梁。希望借助编辑推荐的机会,为那些勇于尝试、大胆创新,为游戏开发者带来创意与启迪,为游戏玩家带来快乐的作品提供更多的关注。 - -我们关注在游戏制作中更着重于自我表达与尝试的中小型开发者,偏好研发独立游戏与单机游戏玩的独立游戏制作者,也不吝于将目光投向那些在成熟玩法的基础上进行创新,或是全方位提升玩家游玩体验的商业化作品。我们将在编辑推荐中展示各种新的游戏、以及广受欢迎的游戏的重大更新。 - -

    - -## 概览 - -TapTap 拥有一支伴随 TapTap 成长多年,了解 TapTap 用户喜好,对各类游戏均有深度了解的团队,我们的编辑团队会在 TapTap 上挑选各类游戏中的佼佼者,并帮助它们在其重要的时间节点,展示给适合的游戏玩家。 - -我们会持续关注新发布的游戏以及长期运营型游戏的重大更新,尤其是在玩法、内容上能带给玩家全新体验的新版本,为 TapTap 提供的独家内容等。 - -## 我们考虑的因素 - -TapTap 的团队会从所有在 TapTap 上架的游戏中寻找值得推荐的作品。在考量的过程中,我们绝不进行竞价排名,也不受其他商业合作模式影响。 - -在整个挑选过程中,我们会综合游戏本身的品质、TapTap 希望保持的推荐调性等多方面考量,最终挑选出深受所有用户喜爱的出色产品。 - -友好度:令玩家感受到舒适的内容及玩法引导,UI 设计、按钮排布兼具美观与易用性。 - -主题立意:游戏题材等世界观的整体构建或开发者的自我表达,与游戏玩法的融合度。 - -独特性:独特游戏玩法、画风或内容,在其他同类游戏中具备显著的差异化或辨识度。 - -技术力:利用对现有技术的精进与挖掘,或创新技术带来更好的游玩体验。 - -游戏在 TapTap 的综合表现:具有吸引力但不会误导玩家的游戏页资料、帮助玩家了解游戏核心玩法的实机视频展示,以及评分、评价等在玩家中的美誉度。 - -适用于游戏的其他考虑因素: -- 游戏的可玩性与趣味性 -- 游戏的完整度 -- 操作手感 -- 整体工业化水平 -- 剧情 (如是游戏的重点) -- 音效与音乐 -- 复玩性 (如果适用) -- 与 TapTap 调性的契合度 diff --git a/docs/store/store-faq.mdx b/docs/store/store-faq.mdx deleted file mode 100644 index ff8bafa18..000000000 --- a/docs/store/store-faq.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: 常见问题 -sidebar_position: 21 ---- -import {FaqLink} from '/src/docComponents/doc'; - -## **一、 开发者注册** -### [1. 开发者注册流程开发者中心申请流程](https://developer.taptap.cn/docs/store/#1-%E5%8E%82%E5%95%86%E5%88%9B%E5%BB%BA%E9%80%9A%E9%81%93%E5%8F%8A%E6%B5%81%E7%A8%8B) -### [2. 如何进入开发者中心](https://developer.taptap.cn/docs/store/#1-%E5%8E%82%E5%95%86%E5%88%9B%E5%BB%BA%E9%80%9A%E9%81%93%E5%8F%8A%E6%B5%81%E7%A8%8B) -### [3. 个人开发者和企业开发者可以相互转换吗?](https://developer.taptap.cn/docs/store/#1%E4%B8%AA%E4%BA%BA%E5%BC%80%E5%8F%91%E8%80%85%E4%B8%8E%E4%BC%81%E4%B8%9A%E5%BC%80%E5%8F%91%E8%80%85%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%E5%8F%AF%E4%BB%A5%E4%BA%92%E7%9B%B8%E8%BD%AC%E6%8D%A2%E5%90%97) -### [4. 个人开发者如何填写厂商名称?](https://developer.taptap.cn/docs/store/#2%E4%B8%AA%E4%BA%BA%E5%BC%80%E5%8F%91%E8%80%85%E6%B2%A1%E6%9C%89%E5%85%AC%E5%8F%B8%E5%A6%82%E4%BD%95%E5%A1%AB%E5%86%99%E5%8E%82%E5%95%86%E5%90%8D%E7%A7%B0) -### [5. 如何认领厂商账号?](https://developer.taptap.cn/docs/store/#3%E5%8E%82%E5%95%86%E5%90%8D%E7%A7%B0%E5%B7%B2%E5%AD%98%E5%9C%A8%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E5%8E%82%E5%95%86%E8%B4%A6%E5%8F%B7%E8%AE%A4%E9%A2%86) -### [6. 如何修改厂商名称?](https://developer.taptap.cn/docs/store/#4%E5%A6%82%E4%BD%95%E4%BF%AE%E6%94%B9%E5%8E%82%E5%95%86%E5%9C%A8%E8%AF%A6%E6%83%85%E9%A1%B5%E7%9A%84%E5%B1%95%E7%A4%BA%E5%90%8D%E7%A7%B0) - - -## **二、 开发者中心权限管理** -### [1. 如何添加成员或超级管理员?](https://developer.taptap.cn/docs/store/#21-%E6%B7%BB%E5%8A%A0%E6%88%90%E5%91%98%E6%88%96%E8%B6%85%E7%BA%A7%E7%AE%A1%E7%90%86%E5%91%98) -### [2. 如何设置管理员权限?](https://developer.taptap.cn/docs/store/#22-%E6%88%90%E5%91%98%E7%9A%84%E6%9D%83%E9%99%90%E8%AE%BE%E7%BD%AE%E5%92%8C%E5%88%A0%E9%99%A4) -### [3. 如何删除管理员?](https://developer.taptap.cn/docs/store/#22-%E6%88%90%E5%91%98%E7%9A%84%E6%9D%83%E9%99%90%E8%AE%BE%E7%BD%AE%E5%92%8C%E5%88%A0%E9%99%A4) -### [4. 如何解绑或更换主管理员?](https://developer.taptap.cn/docs/store/#6-%E4%B8%BB%E7%AE%A1%E7%90%86%E5%91%98%E5%8F%AF%E4%BB%A5%E8%A7%A3%E7%BB%91%E5%90%97%E5%8F%AF%E4%BB%A5%E8%AE%BE%E7%BD%AE%E6%96%B0%E7%9A%84%E4%B8%BB%E7%AE%A1%E7%90%86%E5%91%98%E5%90%97) - - -## **三、 开发者认证** -### [1. 什么是开发者认证?](https://developer.taptap.cn/docs/store/#4%E5%BC%80%E5%8F%91%E8%80%85%E8%AE%A4%E8%AF%81) -### [2. 如何获得或移除开发者认证?](https://developer.taptap.cn/docs/store/#4%E5%BC%80%E5%8F%91%E8%80%85%E8%AE%A4%E8%AF%81) - - -## **四、 物料要求** -### [1. 图标要求](https://developer.taptap.cn/docs/store/release/store-material/#1-%E5%9B%BE%E6%A0%87-) -### [2. 简介要求及展示位置](https://developer.taptap.cn/docs/store/release/store-material/#21-%E7%AE%80%E4%BB%8B) -### [3. 截图要求及展示位置](https://developer.taptap.cn/docs/store/release/store-material/#32-%E6%88%AA%E5%9B%BE) -### [4. 推广图要求及展示位置](https://developer.taptap.cn/docs/store/release/store-material/#5%E6%8E%A8%E5%B9%BF%E5%9B%BE) -### [5. 视频要求及展示位置](https://developer.taptap.cn/docs/store/release/store-material/#31-%E8%A7%86%E9%A2%91) -### [6. 资质上传要求](https://developer.taptap.cn/docs/store/release/store-agree/#6-%E8%B5%84%E8%B4%A8%E6%96%87%E4%BB%B6%E5%8F%8A%E6%8E%88%E6%9D%83) -### [7. 游戏必须有版号吗?](https://developer.taptap.cn/docs/store/release/store-agree/#64-%E7%89%88%E5%8F%B7) - - -## **五、 创建游戏** -### [1. 游戏入库流程](https://developer.taptap.cn/docs/store/release/store-creategame/#1-%E6%B8%B8%E6%88%8F%E5%85%A5%E5%BA%93) -### [2. 游戏入库后需要立即发布吗?](https://developer.taptap.cn/docs/store/release/store-creategame/#2-%E5%88%9B%E5%BB%BA%E6%B8%B8%E6%88%8F%E5%AE%A1%E6%A0%B8%E9%80%9A%E8%BF%87%E5%90%8E%E5%8F%AF%E6%9A%82%E4%B8%8D%E5%8F%91%E5%B8%83%E6%B8%B8%E6%88%8F%E5%90%97) -### [3. TapTap 游戏收录标准](https://developer.taptap.cn/docs/store/release/store-agree/#17-%E6%9A%82%E4%B8%8D%E6%94%B6%E5%BD%95%E6%B8%B8%E6%88%8F) - - -## **六、 游戏认领、转移及下架** -### [1. 如何进行游戏认领?](https://developer.taptap.cn/docs/store/release/store-creategame/#3-%E6%88%91%E7%9A%84%E6%B8%B8%E6%88%8F%E5%B7%B2%E7%BB%8F%E8%A2%AB-taptap-%E6%94%B6%E5%BD%95%E5%8F%AF%E4%BB%A5%E8%BF%9B%E8%A1%8C%E6%B8%B8%E6%88%8F%E8%AE%A4%E9%A2%86%E5%90%97) -### [2. 如何进行游戏转移?](https://developer.taptap.cn/docs/store/release/store-creategame/#4-%E6%B8%B8%E6%88%8F%E4%B8%BB%E4%BD%93%E5%8F%AF%E4%BB%A5%E8%BF%9B%E8%A1%8C%E8%BD%AC%E7%A7%BB%E5%90%97) -### [3. 如何进行游戏下架?](https://developer.taptap.cn/docs/store/release/store-creategame/#5-%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E6%B8%B8%E6%88%8F%E4%B8%8B%E6%9E%B6) - - -## **七、 游戏预约** -### [1. 如何开启预约功能?](https://developer.taptap.cn/docs/store/release/store-creategame/#2-%E6%B8%B8%E6%88%8F%E9%A2%84%E7%BA%A6) -### [2. 为什么要开启预约?](https://developer.taptap.cn/docs/store/release/store-creategame/#2-%E6%B8%B8%E6%88%8F%E9%A2%84%E7%BA%A6) -### [3. 什么是预约里程碑?](https://developer.taptap.cn/docs/store/release/store-creategame/#9-%E9%A2%84%E7%BA%A6%E9%87%8C%E7%A8%8B%E7%A2%91) -### [4. 如何修改预约里程碑?](https://developer.taptap.cn/docs/store/release/store-creategame/#9-%E9%A2%84%E7%BA%A6%E9%87%8C%E7%A8%8B%E7%A2%91) -### [5. 如何发放预约里程碑奖励?](https://developer.taptap.cn/docs/store/release/store-creategame/#9-%E9%A2%84%E7%BA%A6%E9%87%8C%E7%A8%8B%E7%A2%91) - - -## **八、 游戏更新** -### [1. 如何进行游戏更新?](https://developer.taptap.cn/docs/store/release/store-creategame/#4-%E6%B8%B8%E6%88%8F%E6%9B%B4%E6%96%B0) -### [2. 如何进行游戏更名?](https://developer.taptap.cn/docs/store/release/store-creategame/#7-%E6%B8%B8%E6%88%8F%E6%A0%87%E9%A2%98%E5%8F%AF%E4%BB%A5%E4%BF%AE%E6%94%B9%E5%90%97) - - -## **九、 游戏测试或上线** -### [1. 关于内部测试(Internal Testing)](https://developer.taptap.cn/docs/store/test/test-support/#%E5%86%85%E9%83%A8%E6%B5%8B%E8%AF%95%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D) -### [2. 关于封闭式测试(Closed Beta Testing)](https://developer.taptap.cn/docs/store/test/test-support/#%E5%B0%81%E9%97%AD%E5%BC%8F%E6%B5%8B%E8%AF%95%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D) -### [3. 关于开放式测试(Open Beta Testing)](https://developer.taptap.cn/docs/store/test/test-support/#%E5%BC%80%E6%94%BE%E5%BC%8F%E6%B5%8B%E8%AF%95%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D) -### [4. iOS端测试流程](https://developer.taptap.cn/docs/store/test/test-ios/#ios-%E6%B5%8B%E8%AF%95) - diff --git a/docs/store/store-practice.mdx b/docs/store/store-practice.mdx deleted file mode 100644 index 8123d5464..000000000 --- a/docs/store/store-practice.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: 最佳实践 -sidebar_position: 19 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - - -无论您是准备在 TapTap 平台入库开启预约或是正式发行您的产品。我们提供的以下最佳实践,都将有助于您得到更多玩家的青睐,或玩家更好的口碑。 - -## **实用建议** - -以下建议总结来自曾经上架过 TapTap 平台的游戏经验,并不代表照做后一定能显著改善游戏口碑或显著提升新玩家,游戏最佳的运营方式应该是您结合以下经验和游戏的特色做出相应调整。 - - -### **一、 提高玩家对游戏的青睐,从优化游戏详情页面开始** - -详情页面的内容展示决定玩家对您游戏的第一印象,请尽量让游戏的详情页勾起玩家对游戏的兴趣和好感。 - -**游戏简介** -- 围绕游戏背景、玩法、特色的介绍有助于提高您游戏的辨识度。简明扼要、突出亮点的介绍有助于玩家迅速了解游戏的特点,游戏设计理念、彩蛋和有趣的文案有助于获取玩家的好感。 -- 请注意排版的美观性,冗长且缺乏分段的文案会令玩家厌烦。 - -**游戏截图** -- 请务必在截图中加入真实的游戏画面,以向玩家展现出游戏核心的玩法、亮点。 -- 商业气息浓厚的宣传图及氪金元素的内容易招致玩家的反感,缺乏真实画面的纯 CG 式的概念图容易让玩家无法判断游戏的品质而降低对游戏的认可度。 - -**游戏视频** -- 同游戏截图一样,建议使用能真实反馈游戏品质的试玩视频(可带音乐音效),并在视频中体现游本身的亮点及核心玩法。 -- 只有 CG 、过场剧情或广告宣传的视频,效果可能不如实际试玩视频,也可能带来游戏的非目标玩家,进而影响到游戏的口碑。 - - ---- - - -### **二、 提升游戏口碑,维护玩家评价是重点** - -客观、中肯、真诚、敢于接受不足,才能赢得玩家的认可,让游戏获得更好的推广。 - -**不要人为影响评分** - - TapTap 对刷评分和刷榜行为零容忍,严禁任何形式的刷评价行为。一经确认,游戏将全站限制露出,甚至要求下架! - - 请不要安排「公司内部员工」对游戏「先刷一波好评」,若玩家误认为水军,会引发玩家反感,适得其反。 - -**真诚是维护玩家的核心** - - TapTap 的玩家乐于和游戏官方讨论游戏,这是赢取玩家信任的绝佳机会。 - - 安排了解游戏的制作人或策划人员参与讨论交流更容易赢取玩家的认可,尽可能回复长篇评测以及容易激发玩家共鸣的高质量评价,「客服式」的复制黏贴会显得毫无诚意和敷衍。 - - 玩家提出游戏相关的建议时,正面的回复和进一步的交流,会让玩家体会到游戏官方的真诚、开明和乐于听取意见。若感到玩家的理解有所偏差,可以进行诠释(部分玩家会因游戏官方的态度改分,其他玩家也能通过回复看到官方的态度)。 - - 塑造一个游戏内热门角色作为人设的账号,与玩家进行一些偏轻松氛围的交流,可以提高游戏官方的亲和力。 - - ---- - -### **三、 游戏「动态」运营,让游戏的生命力更长久** - -「动态」是玩家获取游戏官方信息的重要途径。借助动态运营可以增加玩家对游戏的粘性,减缓玩家的流失。 - -**合理规划动态内容** -- 游戏可发布的「动态」内容包括但不仅限于:游戏公告、开发者日志、图文资讯、视频、玩家活动等。 -- 「动态」运营的重点是培养玩家使用动态的习惯,以维持玩家在非游戏时间内对游戏的兴趣和关注。可以结合数据分析筛选玩家最关心的内容,发布紧密围绕游戏的特色和亮点的「动态」内容,如:福利活动、画师介绍、Coser、版本介绍、攻略等。一味的将「动态」作为游戏公告使用会降低玩家对动态的关注。 - -**助力打造社区生态** -- 通过游戏官方「动态」互动、正向鼓励等方式,主动挖掘 TapTap 游戏社区中的高质量、高产出的玩家,可以提高玩家的积极性,促进活跃玩家向 KOL 转化。进而为游戏产出更多高质量内容,共同提升游戏在 TapTap 中的社区环境,形成社区生态的良好循环。 - - ---- - -### **四、 做好服务器准备,避免炸服风险** - -**玩家预估** -- 在 TapTap 正式开放下载或者开启游戏 beta 测试前,建议您根据已预约人数及测试玩家筛选方式提前预估玩家数量,做好服务器压力测试,避免过多玩家进入造成服务器崩溃。 -- 根据以往经验,开服瞬时登录及注册玩家过多,是导致「炸服」的重要原因之一。因此建议您做好服务器分流和瞬时承载能力的压力测试,或谨慎使用「预下载」功能(预下载容易造成玩家同时登录)。 - -**事故停服保护** -- 如果您在做好上述准备后,服务器依旧出现问题,请提交「事故停服保护申请」并尽快同步我们。 --「事故停服保护」期间的所有评分都不会计入总分。 - - ---- - -### **五、 独家合作,获取更多合作支持** - -若您的游戏和 TapTap 的玩家契合度较高,您也非常重视与 TapTap 玩家间的沟通,我们建议您考虑在 TapTap 平台独家上架。 - -**合作支持** -- 我们将会有更多的站内外资源来支持您的游戏在 TapTap 上发行运营。对于重点的独家游戏,我们会提供评测内容、社区运营建议、动态内容规划、资源位整合、市场发行节奏的配合及制定等,为好游戏的发行助力。 - - ---- - -### **六、 广告投放,获取额外玩家** - -您可免费在 TapTap 上架有合格资质(软著版号等)的游戏,若您需要在这基础上再获取一些玩家,广告投放会是一个不错的选择。 - - - TapTap 内有且仅有一个基于 CPA 的信息流广告位,您在广告后台充值后,可以直接进行投放。 - - 您在 TapTap 站内投放后会有概率获取其他自然玩家的下载新增,我们会在广告后台核算出广告直接投放的获取玩家成本和综合获取玩家成本,以便您能更加精准的评估广告投放效果。 diff --git a/docs/store/teaser/_category_.json b/docs/store/teaser/_category_.json deleted file mode 100644 index b17c3862e..000000000 --- a/docs/store/teaser/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "预约首曝期", - "collapsed": true, - "position": 4 -} diff --git a/docs/store/teaser/reserve-handbook.mdx b/docs/store/teaser/reserve-handbook.mdx deleted file mode 100644 index f6f8e6f76..000000000 --- a/docs/store/teaser/reserve-handbook.mdx +++ /dev/null @@ -1,474 +0,0 @@ ---- -title: 预约首曝期运营手册 -sidebar_position: 3 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -预约首爆期开发者可以通过申请和配置 TapTap 站内资源位来帮助游戏获得更多曝光。此外,开放预约的新游戏还将自动获得平台首页推荐流量扶持,具体流量倾斜情况依据游戏的预约/下载量变化(预约/下载量越高,给予的曝光扶持越多) - -## 一、 预约首曝光期如何获得免费资源位 -### 1. 首页推荐扶持(算法分发) -首次开放预约以及首发(预约➡️下载)的游戏,算法会自动帮助进行首页推荐曝光流量倾斜 -- 申请方式:无需申请,机器自动配置 -- 扶持逻辑:曝光倾斜程度依据游戏的曝光转化率动态调整,相同曝光下,预约/下载的人数越多,算法给予的流量倾斜越多(整个过程算法自动动态调整) -- 扶持时长:整个扶持流程持续3天,3天后进入自然推荐流程,不再做额外倾斜 -- 注意:冷启动阶段的流量倾斜仅在首次开放预约和首发各进行一次,版本更新或重复发布无法再次参与 - - - -:::info Tips - -详情页的配置丰富度会影响游戏最终的曝光转化率,如何配置优质详情页请见:[详情页优化建议](https://developer.taptap.cn/docs/store/operations-skills/suggestions) - - -::: - - - -### 2. 论坛配置(自行配置) -搭建并完善 TapTap 站内论坛是首曝阶段的重要运营动作。TapTap 为厂商提供了完整的的论坛框架和功能。需要厂商填充并规划更多运营内容,建立玩家对游戏产品的好感度,并吸引第一批核心玩家关注。 - -详细配置指南请关注[#学习社区模块,掌握基本操作](https://developer.taptap.cn/docs/community/features/) - -### 3. 预约里程碑(需申请) -#### 3.1 什么是「预约里程碑」 - -预约里程碑适用于帮助游戏吸引玩家进行预约。可在 游戏运营 >> 内容管理 >> 预约里程碑 中设置预约里程碑并提交审核,审核通过后将在游戏详情页展示预约里程碑。 - -![gameadmin](/img/如何吸引-预约里程碑.png) - -#### 3.2 创建「预约里程碑」有要求吗 - -3.2.1 预约里程碑内容仅针对游戏正式公测开服。 -3.2.2 预约里程碑支持最大 3 档预约。 -3.2.3 预约里程碑奖励素材尺寸为 200x200 px 透明背景,且大小不超过 100 KB 的 PNG 素材。 -3.2.4 预约里程碑奖励素材不得使用黑白图案。 -3.2.5 预约里程碑里程数仅为 TapTap 平台预约数值。 -3.2.6 预约里程碑里程数需使用阿拉伯数字,不得出现任何符号。 -3.2.7 预约里程碑需对奖励内容进行清晰说明,不得模糊,不可用例如 “ 预约大礼包 ” 等字样表示。 -3.2.8 预约里程碑不可涉及任何实物或金钱性质内容(包括且不限于现金、礼品)。 -3.2.9 预约里程碑发布后不可自行修改,请确认后再提交审核。

    -3.2.10 特殊奖品:如果要使用包含TapTap素材的奖品(如站内头像框,道具涂装等),需要额外交由对接商务/运营审核,需提供的信息如下: - -> a. 游戏名称/链接: -> -> b. 植入内容(皮肤、头像、道具等)及监修的素材文件 -> -> c. 获取条件: -> -> d. 面向群体:(全服、安卓预约、TapTap 玩家) -> -> e. 发放方式: -> -> f. 预计上线时间: - -#### 3.3 如何修改「预约里程碑」 - -预约里程碑发布后不可自行修改。如因提交错误或不可抗力需修改预约里程碑,请在论坛发布预约里程碑修改公告,公告内容需要包括:新达成奖励数、新奖励内容、预约里程碑需修改原因等。公告发布后,可在 [开发者中心](https://developer.taptap.cn/) >> 工单 中对应的分类下提交修改申请。 - -#### 3.4 如何发放「预约里程碑」奖励 - -预约里程碑奖励一般在游戏首发后向预约玩家发放,建议在游戏内通过站内信的形式直接向全服玩家发放。 - -如需通过礼包码的形式发放,请于 游戏运营 >> 礼包码 >> 新增游戏码 中上传礼包码,提交审核即可。建议多档预约里程碑奖励合在一组礼包码中一并发放。如需只把礼包码发放给预约玩家,请 工单 联系官方配置。 - -### 4. 详情页标签配置(需申请) -针对游戏的类型,玩法,内容主题以及其他特征进行标签配置,添加和修改以帮助进行算法精准推荐 -- 申请方式:向对接商务/运营或发起工单申请,编辑审核通过后帮助添加,删除或修改 -- 审核逻辑:与游戏相关程度,标签内容是否合规 -- 审核周期:1-3 个工作日 - -![](https://capacity-files.lcfile.com/ftTAeKj3jv5VEQJJTB5JEIm3F9fRmBAX/%E6%A0%87%E7%AD%BE%E9%A1%B5.png) - - -## 二、 如何更好地运营游戏并与 TapTap 运营合作——首曝预约期 -### 1. 基于TapREP与TapTap的运营合作 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    资源置换形式置换说明重点置换形式示例创建指南价值计算方式预期可获资源价值
    分享SDK接入 -

    分享控件加入TapTap分享

    -

    -

    前提:

    -
      -
    • 结合论坛活动,做分享活动
    • -
    • 接入Tap分享SDK,玩家在分享时可以绑定指定游戏论坛或指定hashtag
    • -
    -
    接入文档:TapREP分享能力接入
    -
    -

    分享用户流程

    -

    玩家在游戏内截图

    -

    →生成截图和可分享路径

    -

    →点击TapTap分享

    -

    →老用户拉起TapTap编辑器分享,新用户提示下载TapTap

    -

    →内容生成时绑定游戏论坛/hashtag

    -

    →内容发布成功;根据论坛内容/hashtag内容发布奖励

    -
    -

    分享类的论坛活动

    -

    示例活动

    -

    示例活动

    -

    - - -

    -
    -

    过往案例

    -

    -

    推荐场景

    -

    -

    -
    -
      -
    • 一次性接入奖励:10000元
    • -
    • 通过分享带进来的用户:走拉新、拉活的价值计算
    • -
    • - 论坛活动价值:单次活动价值在2k~7.5k之间,依据活动实际数据结算 -
        -
      • 以100万用户量覆盖预估,预计获得流量金20万,即首页曝光80万
      • -
      -
    • -
    -
    -

    形式一预估

    -

    按覆盖用户量350万,长期覆盖预估

    -

    首月预估价值20万

    -

    大盘单个拉新用户价值100-300元

    -

    大盘单个拉活用户价值10-30元

    -


    -

    形式二预估

    -

    按覆盖用户量350万,长期覆盖预估

    -

    首月预估价值10万

    -
    品牌标记内容媒体(微博/微信/b站/小红书/抖音/快手)宣发时文案/内容带TapTap---官号/KOL合作均可 -

    品牌标记示例

    -

    bilbili:文案带TapTap示例

    -

    抖音:视频内容带TapTap示例

    -

    快手:文案带TapTap示例

    -

    微博:文案带TapTap示例

    -

    小红书:文案带TapTap示例

    -

    直播:

    -

    -
    -

    TapREP:创建品牌资源

    -
    -

    以实际互动量结算,互动量定义为转评赞投币收藏等各个平台的互动行为

    -
      -
    • 抖音/快手/小红书/微博 统计自上传且审核通过之日起7天的增量互动数据
    • -
    • bilibili 统计自上传且审核通过之日起30天的增量互动数据
    • -
    -
    -

    根据实际转化结算:

    -

    目前数据平均一万赞视频价值在3500元左右(仅预估数据),结算实际还是以所有互动行为为结算依据

    -

    不同热度的视频价值示例:

    - -
    官网置换+品牌专区 -

    官网+品牌专区主入口挂TapTap游戏详情页+媒体渠道挂TapTap论坛首页

    -
    -

    渠道增加TapTap专区,引导至论坛

    -
    -

    -
    -

    step1: TapREP:创建效果资源

    -

    step2: 在更新完效果链接后,在TapREP后台上传品牌挂架

    -
    -

    一次性资源奖励+持续拉新拉活价值收益

    -

    一次性资源奖励阶梯奖励,单次最高收益1万元,根据放的位置不同,收益不同

    -

    -

    实际拉新拉活效果次日结算

    -

    大盘单个拉新用户价值100-300元

    -

    大盘单个拉活用户价值10-30元

    -
    -

    一次性资源奖励1000~10000元

    -


    -

    效果根据实际转化结算:

    -

    预约期游戏,站内预约超50万的游戏,首月预估资源价值20万,结算实际依赖官网带转化效果

    -
    -

    主入口加上TapTap下载

    -
    -

    -
    自媒体引流 -

    媒体通稿+微博、微信等自媒体带TapTap详情页效果链接

    -
    -

    媒体通稿带tap预约链接

    -
    -

    -
    -

    TapREP:创建效果资源

    -
    -

    按实际拉新拉活效果次日结算

    -

    大盘单个拉新用户价值100-300元

    -

    大盘单个拉活用户价值10-30元

    -
    -

    效果根据实际转化结算:

    -

    预约期游戏,站内预约超50万的游戏,首月预估资源价值5万,结算实际依赖各个资源位带转化效果

    -
    -

    微博/微信等内容带tap预约链接

    -
    -

    -
    -

    微信公众号阅读原文跳转tap预约链接

    -
    -

    -
    -

    微信公众号导航栏跳转tap预约链接

    -
    -

    -
    运营资源 -

    在TapTap站内开启论坛活动和签到活动

    -
    -
      -
    1. 签到活动示例
    2. -
    3. 论坛活动示例
    4. -
    -

    - 抽奖活动 - {" "} - 回复活动 - {" "} - 话题活动 -

    -
    -

    TapREP:创建运营资源

    -
    -

    平台将根据活动曝光、参与情况折算价值,并根据论坛dau这一核心指标,作为加权和降权,形成最终价值。

    -

    对厂商侧来说:

    -
      -
    • 帖子/活动的曝光越高,拿到的价值越多
    • -
    • 帖子/活动的互动越多,拿到的价值越多
    • -
    • 论坛dau越高,拿到的价值越多
    • -
    -
    -
      -
    • 单次签到活动价值在1000~1.8W不等,由论坛DAU和具体活动数据决定
    • -
    • 单次论坛活动价值在1250~7500不等,由论坛DAU和具体活动数据决定
    • -
    • 每月可做多次签到和论坛活动
    • -
    -
    线下合作 -

    线下和TapTap联办漫展/市集/快闪等

    -
    -

    - - -

    -
    -

    需特殊申请开白,REP资源置换后台提工单,联系对接商务或发送邮件到tapop@xd.com发起申请

    -
    -
      -
    • 根据人流量和展台露出程度给到固定金额的品牌价值结算
    • -
    • 根据实际转化次日结算
    • -
    -
    -
      -
    • 按CP29人流量,高露出核心展台预估,预估价值5万
    • -
    -
    素材授权 -

    提供游戏素材授权给taptap,允许在站外推广时使用游戏相关素材,一次性给到1000元流量金

    -
    -

    计算机软著

    -
    -

    -
    -

    TapREP:上传素材授权

    -
    -

    单次素材授权,审核通过后,发放1000元流量包

    -
    对易玩的授权书
    -
    - - - - -:::info 注意 - -表格中所提到的“元”为等价流量包 - -::: - - - -### **2. 推荐:TapTap 登录接入** -TapTap 账号登录为开发者提供了简单、安全、快速的账号登录授权功能,为用户免去输入账号密码的繁琐步骤,让用户只需一键通过 TapTap 账号授权,即可使用你的应用。 - -更多信息与指南请见:[TapTap登录功能介绍](https://developer.taptap.cn/docs/sdk/taptap-login/features/) - -### **3. 推荐:TapTap 唤起更新接入** -当游戏发布了新版本,且需要玩家进行更新才能体验新版本时,在游戏内绘制一个界面告知玩家并提供「更新」按钮,玩家点击后自动跳转至 TapTap 游戏详情页,玩家可以在 TapTap 完成更新 - -更多信息与指南请见:[唤起更新开发指南](https://developer.taptap.cn/docs/sdk/update/guide/) - -### **4. 推荐:TapTap 成就系统接入** -成就系统接入后,厂商可以在开发者中心后台配置并发布游戏成就,玩家在游戏内触发并获得成就,从而提升玩家在游戏中的参与度,鼓励玩家以不同的玩法来玩游戏。 - -更多信息与指南请见:[成就系统功能介绍](https://developer.taptap.cn/docs/sdk/achievement/features/) - -### 5. 论坛配置及优秀实践分享 -TapTap 除了作为商店为厂商吸引玩家外,还是一个拥有巨大用户群的玩家社区。它允许玩家在其中讨论,分享游戏相关内容,并帮助开发者与玩家直接进行沟通。因此定期的论坛活动有助于提高玩家对于游戏的忠诚度,并帮助开发者及时收获真实反馈。 - -- 社区发帖操作:[学习社区模块,掌握基本操作](https://developer.taptap.cn/docs/community/features/) - -- 社区活动设计指南:[挑战进阶操作,玩转社区运营](https://developer.taptap.cn/docs/community/advanced/) - -- 优秀案例分享: - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    活动类型案例展示操作方式注意事项
    -

    意见收集

    -
    -

    【深度冻结】bug反馈&建议征集专用帖!

    - -
    -

    论坛后台发帖

    -
    -

    -
    -

    社区互动

    -
    -

    【有奖活动】星穹会客厅 | 景元:运筹帷幄的云骑将军

    - -
    -

    论坛后台发帖

    -
    -

    -
    -

    评论抽奖

    -
    -

    【内含周边抽奖】《深空之眼》太一·庚辰角色PV「夙夜长愿」

    - -
    -

    论坛后台发帖

    -
      -
    • TapTap社区内的评论抽奖活动支持自动化组件,只需在发帖时,在发布设置中选择【添加评论抽奖活动】并按照指引填写相关信息即可。目前支持多种奖品设置(实物/兑换码),定时自动开奖,自动去除bot等功能
    • -
    -
    -
      -
    1. 活动发起方务必在帖子内清楚、准确描述活动规则、活动奖品、开奖时间。
    2. -
    3. 中奖用户需要在 7 个自然日内完成收件信息填写,活动发起方请务必在开奖后 30 个自然日内完成奖品发放。
    4. -
    5. 开奖时将自动过滤机器人用户。
    6. -
    7. 使用抽奖功能组建需要开通版主运营权限
    8. -
    -
    -
    diff --git a/docs/store/teaser/tag-management.mdx b/docs/store/teaser/tag-management.mdx deleted file mode 100644 index ad98c5aa9..000000000 --- a/docs/store/teaser/tag-management.mdx +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: 对游戏进行标签管理 -sidebar_position: 2 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -标签管理是指开发者在 TapTap 平台上使用标签对游戏进行管理和维护的过程。通过合理选择和维护标签,可以提高游戏在 TapTap 的可见性、搜索排名和目标受众吸引力。 - -## 1. 什么是标签管理 - -标签管理通常包括以下几个方面: - -### 1.1 标签选择 -选择与游戏内容、特点和风格相关的标签。 -这些标签应准确地描述游戏的类型、主题、游戏机制等关键特征,以便玩家能够更容易地找到和识别游戏。 - -### 1.2 标签更新 -随着游戏内容、版本更新或市场需求的变化,需要及时更新和调整标签。 -保持标签的准确性和相关性,以确保游戏在搜索结果中持续可见,并与目标受众保持一致。 - -### 1.3 标签优化 -通过分析和了解 TapTap 的搜索趋势、玩家偏好和竞争情况,对标签进行优化。 -优化可以包括添加新标签、删除不相关或不太有效的标签,以及调整标签的权重和顺序,以提高游戏的曝光度和搜索排名。 - -### 1.4 标签关联性 -将游戏的标签与其他相关游戏进行关联,以增加游戏在 TapTap 的相关推荐和曝光机会。 -这可以通过合作推广、联合活动或 TapTap 提供的相关游戏推荐功能来实现。 - -### 1.5 标签反馈和分析 -关注玩家对标签的反馈和互动,了解他们如何使用标签来搜索和筛选游戏。 -通过分析标签的使用情况和效果,可以获取有关玩家需求、市场趋势和改进机会的有价值信息。 - -## 2. 为什么要进行标签管理 - -进行标签维护对于游戏在 TapTap 上的可见性和搜索排名非常重要,以下是一些进行标签维护的必要性: - -### 2.1 提高搜索可见性 -TapTap 允许玩家使用标签进行游戏搜索。通过选择相关和准确的标签,可以增加游戏在搜索结果中的曝光度,使更多的玩家能够找到和发现您的游戏。 - -### 2.2 目标受众吸引 -通过维护适当的标签,您可以吸引与您游戏内容和风格相匹配的目标受众。标签可以帮助玩家了解游戏的类型、特点和主题,从而提高游戏的吸引力,并吸引对该类型游戏感兴趣的玩家。 - -### 2.3 竞争力提升 -TapTap 上的游戏众多,竞争激烈。通过进行标签维护,您可以将您的游戏与竞争对手进行区分。选择独特的、与游戏相关的标签,有助于突出游戏的特点和优势,并吸引更多的玩家选择您的游戏。 - -### 2.4 曝光度和下载量增加 -准确和相关的标签有助于增加游戏的曝光度,吸引更多的玩家点击游戏页面并进行下载。通过标签维护,您可以提高游戏在 TapTap 的搜索排名,使其在相关标签搜索结果中更容易被发现。 - -### 2.5 用户反馈和改进 -通过观察玩家对标签的反馈和互动,您可以了解玩家对您的游戏的感受和期望。这有助于您进行游戏的改进和优化,以更好地满足玩家的需求和期待。 - -## 3. 标签管理须知 - -### 3.1 现状 -选择适合自己游戏的标签需要综合考虑游戏内容、目标受众、市场趋势和竞争对手等因素。确保标签与游戏相关、准确,并根据实际表现进行调整和优化。 -为确保标签与游戏的匹配程度,减少利用标签进行「挂羊头卖狗肉」等不正当竞争,目前 TapTap 对标签修改采取统一管理。 -如有需求,您可以联系对接运营或提交工单,提交标签词的添加、删除、排序等申请。 - -### 3.2 须知 - -在提交标签管理申请前,请您知悉以下规则: - -3.2.1 如原本存在不合理标签,会被删除; -3.2.2 挂机等意思与其他标签重复的标签不添加; -3.2.3 复古等硬蹭传奇 IP 的标签不添加; -3.2.4 单机、休闲等明显与游戏事实相悖的标签不添加; -3.2.5 标签顺序只支持修改前三顺序,后续标签综合考虑露出与权重,判定为无实际意义; -3.2.6 标签不支持随意删减,除非宣传资料去除相关要素; -3.2.7 如合理标签少于三个,会根据资料添加其他标签; -3.2.8 符合游戏事实但描述不合规的标签会手动修正; -3.2.9 请勿在短时间内重复提交不合理的标签,否则将被暂停评估。 - -TapTap 工作人员将会在收到如您的需求后进行评估,符合规则的部分将被通过,其余内容会予以驳回。 \ No newline at end of file diff --git a/docs/store/teaser/teaser-page.mdx b/docs/store/teaser/teaser-page.mdx deleted file mode 100644 index f66c72767..000000000 --- a/docs/store/teaser/teaser-page.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: 建立不带评分评价的游戏详情页 -sidebar_position: 1 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -同为游戏详情页,也可以有功能上的差异。作为开发者,您可以利用上线前的爆料期,建立一个不带评分评价的游戏页面。 - -## 1. 与一般游戏详情页的异同 - -![admin](https://img.tapimg.com/market/images/ba081085430e942f8397ac87f08ae333.png) - -### 1.1 不同点 - -1.1.1 功能限制:不开放评分与评价区。 -1.1.2 流量限制:无法在首页推荐、排行榜等模块曝光。 -1.1.3 用户行为限制:用户仅可关注,无法进行预约、下载等操作。 - -### 1.2 相同点 - -1.2.1 信息对外:具备基础游戏详情页功能,支持添加游戏基础信息、推广图文视频资料、玩家交流群、官网链接等内容。 -1.2.2 流量分发:可以在热搜曝光。 -1.2.3 用户管理:开放论坛。用户可在此聚集讨论,您亦可通过版主权限进行管理。 - -## 2. 为什么要建立「残缺的」游戏详情页 - -### 2.1 建立可量化的市场关注度 - -尽管没有可试玩版本,但借助页面上关注的功能,玩家仍能表达对游戏的兴趣和期待。这有助您了解游戏的受欢迎程度,并为后续的市场推广和用户留存策略提供指导。 - -### 2.2 避免过早开放评分评价的影响 - -在游戏尚在开发的早期,可能存在未完善的功能、bug和平衡问题。建立不带评分评价的页面可以避免玩家根据早期版本的体验给出不准确或过早的评价,从而保护游戏的声誉和开发进程。 - -### 2.3 建立开发者与玩家的沟通渠道 - -通过论坛和玩家群链接,您可以与玩家建立起直接的沟通渠道。这种沟通可以让开发者分享游戏的最新进展、回答玩家的问题,以及收集玩家的反馈和建议,从而为游戏的改进和发布做好准备。 - -### 2.4 着重宣传游戏特色 - -在不带评分评价的页面上,您可以更加专注地展示游戏的特色、创新点和核心玩法。这样能够吸引更多的关注,让玩家更好地理解游戏的独特之处,而不被评分评价所干扰。 - -## 3. 如何建立不带评分评价的游戏页面 - -### 3.1 入口 - -您可在开发者中心首页点击创建游戏,并在「上架类型」处选择仅宣发,完成所有必填内容后单击保存并提交。 - -### 3.2 信息填写 - -详细的填写须知,可在 [上架游戏须知](https://developer.taptap.cn/docs/store/release/store-material/) 进行查阅。 - -### 3.3 相较正式页面,无需准备的素材 - -3.3.1 游戏包体 -3.3.2 游戏资质信息,如未成年人防沉迷信息、官方游戏出版物号(ISBN)、软件著作登记信息、网络游戏备案通知单等 \ No newline at end of file diff --git a/docs/store/test/_category_.json b/docs/store/test/_category_.json deleted file mode 100644 index d3cf3906b..000000000 --- a/docs/store/test/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "测试期", - "collapsed": true, - "position": 5 -} diff --git a/docs/store/test/test-closedbetatest.mdx b/docs/store/test/test-closedbetatest.mdx deleted file mode 100644 index f49888b3e..000000000 --- a/docs/store/test/test-closedbetatest.mdx +++ /dev/null @@ -1,448 +0,0 @@ ---- -title: 常规封闭式测试(限量测试) -sidebar_position: 5 ---- - -常规封闭式测试适用于绝大多数的限量测试需求,已覆盖 Android、iOS 平台,最多支持 10 万人参与测试。 -阅读本文档将有利于您详细了解封闭式测试的玩法。 - -## 创建一个封闭式测试计划 - -在您使用以下所有功能前,均需要创建一个封闭式测试计划。 - -您可以将一个封闭式测试计划对应一轮测试,当一轮测试结束后,您可结束或关闭当前封闭式测试计划。当您需开启新一轮测试时,创建一个新的封闭式测试计划即可。 - -创建一个封闭式测试计划,请前往商店 >> 版本发布 >> 测试 >> 创建封闭式测试计划。保存后无需审核,直接生效。但请放心,您刚刚创建完封闭式测试计划时,没有用户拥有测试资格,所以也不会有用户看到您所创建的测试计划。 -每个游戏可创建最多 10 个封闭式测试计划。 - - - -:::tip - -创建测试计划时请注意: -- 测试计划名称请不要含任何标点符号,如「」、《》,也不要含游戏名。 -- 测试开始时间请填写用户能够登录游戏的时间,此信息仅已获得测试资格的用户可见。测试开始时间过去后无法修改。所以,如果您未确定测试开始时间可不填写,已获得测试的用户将会看到“暂未公布”测试时间。 -- 测试结束时间非必填,如果您还未确定测试结束时间可不填写,已获得测试的用户将会看到“暂未公布”测试结束时间。 -- 测试开始后,用户可下载游戏;测试结束后,用户无法下载游戏。 -- 假如您的游戏同时开放 iOS 测试,请开启“本次测试发放 TestFlight 测试资格”并填写 TestFlight 公开链接。别担心,此时不填写 TestFlight 公开链接后续依旧可补充。 -- 限量人数用户不可见,但会校验您后续发放测试资格时的表单,所以请如实填写。 - -::: - -## 如何招募用户并发放测试资格 - -您可发放测试报名问卷,或提前招募测试用户。 - -1. 展开已创建的封闭式测试计划 >> 选择开启测试招募 >> 设置招募时间和招募方式,招募时间需要早于测试开始时间。 -2. 当前支持报名招募和问卷招募两种方式。问卷招募暂时仅支持第三方。 - - - -3. 未到达招募开始时间时,招募信息对用户不可见;到达招募开始时间后,用户可在您的游戏页见到招募信息并参与招募;招募结束后,招募信息对用户不可见。 - -![](https://img.tapimg.com/market/images/e916cf897fadc4e9f6413c518c7b6e9e.png) - -4. 参与招募的用户,您可以查看他的设备、游戏时长、游戏喜好、省份信息等,请在对应的封闭式测试计划 >> 报名用户 >> 查看明细。 -5. 在您挑选测试用户后,可对其发送测试资格,请在添加测试用户 >> 粘贴用户 ID 或导入用户表格 >> 定时设置发放资格时间 >> 保存。到达发放资格时间时,用户将收到 TapTap 的站内信通知。为了保证您的测试用户激活率,建议您将发放时间定时在开放下载前半小时。 - - - -:::tip - -发放测试资格请注意: -- 发放测试资格前,建议填写封闭式测试计划开始时间,否则用户将无法在站内信中获取该信息。 -- 本轮测试如需开启预下载,也请在发放测试资格前填写预下载时间。预下载功能说明请看文档《[如何开启测试预下载](#如何开启测试预下载)》。 -- 本轮测试如需发放激活码,请在发放资格前勾选“发放激活码”。激活码功能说明请查看文档《[如何发放游戏激活码](#如何发放游戏激活码)》。 -- 如需发放 iOS 测试资格,请提前填写 [TestFlight 公开链接](https://developer.apple.com/cn/testflight/)。 -- 您可多批次发放测试资格。 -- 开始发放测试资格后,需要一定发放时间,如果看到状态变为“已发放”但实际发放人数为 0 ,请等待 1 分钟左右后刷新页面。 - -::: - -## 如何分享测试资格领取链接 / 海报 - -您可以将测试资格链接发送至玩家群,或者合作的游戏主播,以便玩家领取测试资格。 - -1. 展开已创建的封闭式测试计划 >> 添加测试用户 >> 生成短链 >> 设置本批次发放时间和人数上限 >> 保存 >> 复制链接。发放时间需早于测试结束时间,即您在测试开始前或测试期间均可通过此方式发放资格。 - - - -2. 访问链接的用户可点击领取测试资格。 - -![](https://img.tapimg.com/market/images/bac5852159a089c21c412047996ea0a0.png) - -:::tip - -生成短链请注意: -- 生成后的短链是唯一且不会变化,用户点击链接即可领取测试资格。 -- 您可生成多个批次的不同链接,发放至不同的社交媒体或社群。 -- 如果您需要生成海报,可使用短链转为二维码并放置到海报中。 - -::: - -## 如何开放测试资格至 TapTap 用户自由领取 - -您可限时、限量开放测试资格至 TapTap 用户主动领取。 - -1. 展开已创建的封闭式测试计划 >> 添加测试用户 >> 先到先得 >> 设置本批次发放时间和人数上限 >> 保存。您只能在测试开始或开启预下载后开放测试资格。 - - - -2. 未到达先到先得时间时,测试信息对用户不可见;到达先到先得时间时,用户可“参与测试”领取资格并下载;先到先得时间已结束,新用户无法“参与测试”,而已获得资格的用户可持续“参与测试”直至本次测试结束。 -3. 建议您分批次发放测试资格,即创建多个先到先得的发放计划,举例:10日上午10点-11点开放500个名额,10日下午14点-15点开放500个名额...以此类推。 - -![](https://img.tapimg.com/market/images/ab8c24fbd34de59bf9036baafcefb5d0.png) - -4. 如果您的测试需要用户进入游戏抢注测试账号或完成创角后才可获得测试资格,您可在设置先到先得时勾选“用户抢注游戏账号后获取测试资格”。此时,用户参与测试时将收到通知,提示其尽快抢注游戏账号。 - -![](https://img.tapimg.com/market/images/74f860d69a90112d6213ff85c4e73b75.png) - - -## 如何快速实现限量测试期间内的玩家裂变测试 - -当您需要在封闭式测试阶段实现裂变能力时,可尝试此玩法。玩法举例:在游戏内举办老带新、人拉人活动,让参与测试的老玩家分享 1 个限量测试资格至 TA 的朋友领取。 - -![](https://img.tapimg.com/market/images/bac5852159a089c21c412047996ea0a0.png) - -此功能需要游戏内做一定的开发改动,如需使用此功能,请按照下方使用说明接入。 - -### 接入流程概述 - -![](https://img.tapimg.com/market/images/eb7c56716bba2f1fcaef5d90ae47c5a6.png) - -### 接入前准备 - -请提前创建封闭式测试计划,并创建玩家分享资格的发放方式。 - -:::tip - -创建玩家分享的发放方式请注意: -- 一个测试计划只能够创建 1 个玩家分享资格的发放计划。 -- 如果您创建了 1 个玩家分享资格的发放计划用于 Debug ,也请直接将此发放方式用户正式上线使用。 - -::: - -### 接口使用说明 - -该接口用于生成邀请链接,通过该链接可以邀请新用户加入。 - -接口需要 client id 对应的游戏存在开启的测试计划,且测试计划中存在玩家分享资格类型的发放计划,需要注意一个测试计划中仅能有一个玩家分享资格类型的发放计划生效。 - -生成链接的数量是没有限制的,但单个链接的邀请上限和所有链接的邀请上线受到对应发放计划的限制,当超过限制时,访问生成的链接会提示名额已满。 - -请务必在服务端进行接口调用,避免 Server Secret 泄漏。 - -### 接口信息 - -接口地址: `https://partner.taptapdada.com/test/v1/invite-link` - -请求方式: `POST`,使用 query params 传递参数 - -#### 请求参数: -| 参数名 | 类型 | 必选 | 说明 | -|--------|--------|----|----------------------------------------| -| client_id | String | 是 | 游戏对应的 client_id | -| uid | String | 是 | 用户唯一标识,由调用方决定,最多 100 位 | -| time | Integer | 是 | 时间戳 | -| nonce | String | 是 | 随机字符串,建议 5 位 | -| sign | String | 是 | 签名 | - - - - - -#### 请求示例: -``` -curl --location --request POST 'https://partner.taptapdada.com/test/v1/invite-link?client_id=abcdefg&uid=12345&time=1690000000&nonce=abcde&sign=sign' -``` - -#### 签名算法: - -1. 把除 `sign` 外所有参数按 `key` 的字母顺序排序 - -2. 用 `&` 符号把 `key=value` 参数对链接 - - * `key` 和 `value` 不用 `urlencode` - -3. 在字符串最后拼接上应用密钥 `secret`,请使用 游戏服务 - 应用配置 - 基本信息 下的 Server Secret - -4. 步骤 3 中得到的字符串做 md5 hash - -#### 返回参数: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    参数名类型 说明
    link -

    String

    -
    -

    邀请链接

    -
    invited -

    Integer

    -
    -

    link 链接已使用的邀请次数

    -
    limit -

    link 链接邀请次数上限

    -
    - - -

    link 链接的邀请次数(对应「开发者中心」 -> 「商店」-> 「版本发布」-> 「测试」 -> 「封闭式测试」 -> 「添加测试用户」 -> 「玩家分享资格」 -> 「单次分享资格可使用人」配置的值)


    -
    - - - - - -当生成了一个链接(称为 A 链接)并邀请了用户 A 时,系统返回的信息为:{"link":"A 链接","limit":50,"invited":1}。这表示用户 A 已被成功邀请,并且该链接的邀请次数上限是 50 次。 - -假设在此之后,生成了另一个链接(称为 B 链接)并再次邀请用户 A 。由于用户 A 已经被邀请过,因此 B 链接的返回仍然是:{"link":"B 链接","limit":50,"invited":0}。这表明虽然邀请了用户 A ,但实际上用户 A 已经在之前的邀请中被纳入统计,因此 B 链接的邀请数为 0 。 - - -返回示例: -``` -{ - "data": { - "invited": 0, - "limit": 10, - "link": "https://example.com/invite?code=1234" - }, - "now": 1692101733, - "success": true -} -``` - -
    -Python3 请求示例 - -```python - -import requests -import random -import string -import hashlib -import urllib -from datetime import datetime - - -if __name__ == '__main__': - # request the secret from your partner - secret = "your Server Secret" - params = { - 'client_id': "your client ID", - "uid": "uid", - } - # sign param - randomStr = ''.join(random.choices(string.ascii_uppercase + string.digits, k=5)) - params['nonce'] = randomStr - params['time'] = int(datetime.now().timestamp()) - payload = "" - for i in sorted(params): - payload += '%s=%s&' % (i, params[i]) - payload = payload.strip("&") + secret - sign = hashlib.md5(payload.encode('utf-8')).hexdigest() - params['sign'] = sign - url = "https://partner.taptapdada.com/test/v1/invite-link?" + urllib.parse.urlencode(params) - print(url) - response = requests.post(url) - print(response.text) - -``` - -
    - - - -## 如何接入 TapTap 登录增强测试保密性 - -如果您担心下载的 APK 被转发或 TestFlight 公开链接被复制,最终导致游戏包体泄露、测试内容不保密,那么推荐您使用 TapTap 登录 SDK 验证测试资格。接入后,仅已获得测试资格的用户在测试开始期间可登录游戏,不在测试时间或未获得测试资格的用户无法登录游戏。 - -使用方法如下: -1. 在游戏内接入 [TapTap 登录 SDK](/sdk/taptap-login/features/)。 -2. 将封闭式测试 >> 游戏测试设置 >> 仅有测试资格可登录游戏开启。 -3. 使用上述发放方式,如招募用户并发放测试资格、分享测试资格领取链接、开放测试资格至 TapTap 用户自由领取等方式发放测试资格,拥有测试资格的用户可下载并登录游戏。 - -:::tip - -接入 TapTap 登录请注意: -- 您只需要接入 TapTap 登录 SDK 即可,如果曾接入旧版的测试资格校验 API ,现可移除相关代码。 -- 如果您的内部团队无法登录游戏,可使用内部测试功能登录游戏。内部测试功能说明请看文档《[内部测试](/store/test/test-internaltest)》。 -- 关闭仅有测试资格可登录游戏后,所有人均可登录游戏,请谨慎操作。 -- 因测试资格验证是在用户登录游戏的环节,所以已经登录游戏的用户并不会触发拦截。为保证您的测试内容保密,请尽早开启仅有测试资格可登录游戏开关。 - -::: - -## 如何发放游戏激活码 - -TapTap 不提供生成激活码功能,但如果您的游戏已经可以通过激活码验证测试资格,TapTap 可以帮助您在发放测试资格的同时发放激活码。 - -1. 在通过任一方式发放测试资格时,您都可勾选“发放激活码”。 -2. 如果您在发放资格时,未勾选“发放激活码”,后续也可以操作补发。 -3. 激活码需要选择可用的平台,此平台和测试资格的平台一一对应,即:Android 可用的激活码仅可以和 Android 测试资格一同发放。 -4. 已领取激活码的用户,在 TapTap 站内信和您的游戏页均可复制激活码。 - -## 如何获得更加精准的人群 - -当您的游戏在站内招募或者开放测试资格至 TapTap 用户自由领取时,您可使用“定向曝光”功能来圈选更精确的人群。 - -请在封闭式测试计划 >> 定向人群曝光游戏 >> 创建并选择定向人群。您可根据游戏类型,选择偏好某一类游戏的人群,最多可选择 5 个分类。 - -选择定向人群后,TapTap 的推荐算法会尽可能将游戏曝光至该人群。请注意,如果您的人群选择并不是很贴合游戏,那么转化效率可能将不达预期。如需“破圈”,可尝试通过 TapTap 站内投放扩大人群。 - -## 如何开启测试预下载 - -如果您的游戏开服时间晚于开放测试包下载时间,您可使用测试预下载功能。 - -请在封闭式测试计划 >> 编辑 >> 提前开放预下载 >> 填写测试开始时间和预下载时间即可。预下载开启后,获得测试资格的用户可下载游戏。 - -:::tip - -开放预下载请注意: -- iOS 也能够开放预下载,但是请提前维护 TestFlight 公开链接。 - -::: - -## 如何移除测试用户 - -和内部测试相同,您也可以直接移除某个用户的测试资格。 - -在商店 >> 版本发布 >> 测试 >> 封闭式测试中找到您想操作的封闭式测试计划,点击测试资格明细,输入想要移除的测试用户的 TapTap ID 搜索后移除。 - -以下为内部测试的操作示例,您也可以在封闭式测试采用相同的操作。 - - - -## 如何使用测试服进行封闭式测试 - -当您希望您的测试用户完全和其他用户隔离开来时,您可使用测试服功能进行测试。 - -1. 在您的游戏的预约页(即正式服页面)进入开发者中心 >> 版本发布 >> 测试 >> 封闭式测试 >> 使用测试服功能。 -2. 根据页面引导,创建测试服并提交审核即可。第一次创建的测试服将直接复制正式服的素材,但不会复制 APK。 - - - -3. 测试服创建完成后,测试服的封闭式测试的服务均可用,您只需按照上方的帮助文档进行操作即可。 - -:::tip - -使用测试服测试请注意: -- 测试服审核中时,也可提前创建测试计划。 -- 为保证 TapTap 的用户体验,测试服审核中/定时上线/已上线时,正式服的测试服务(内部测试除外)将停用,如果您的正式服有正在进行中测试计划将直接失效。 -- 新建的测试服审核通过后将正常上架 TapTap,如果您不希望提前上架,可通过定时上线功能或后续再提交审核。 -- 如果您需要在创建测试服时一并提交 APK 审核,请在测试服页面选择商店资料的对应版本后更新即可。 -- 测试服仅用于游戏测试,部分功能将受限制不可用,含:开放测试服预约/云玩/TapPlay、游戏故障评价处理、签到活动、新版本活动。 - -::: - -## 常见问题 - -### 封闭式测试可以申请上资源位吗? -封闭式测试功能并不影响分发,能够在常见的资源位上展示,您可查看对应文档申请。有一些资源位为系统自动创建的资源位,无需申请。为了能够达到曝光最大化,您可提前一天创建招募、先到先得等发放资格的方式。 - -### 我可以即使用招募,又使用先到先得吗? -可以。封闭式测试的玩法,均可搭配随意使用,举例:您可先招募测试用户并发送资格,同时在核心玩家群分享测试资格链接,在您测试开启后发觉参与测试的人数不足,还可公开限量测试资格以供 TapTap 玩家自由领取。 - -### 如果我的测试出状况,需要紧急关闭应该怎么操作? -如果当前状况还在可控范围内,您可关闭新用户继续领取测试资格通道,即终止发放中的先到先得、生成短链等。如果当前状况不可控,您可操作关闭测试让所有用户都无法继续参与测试,直到您的问题已解决时,可操作重新开启。 - -### 封闭式测试情况下,我的游戏状态应该怎么修改? -仅需要保持原游戏状态即可,即:如果您在预约页上测试,请保持游戏状态为预约;如果在测试服上测试,请保持游戏状态为敬请期待。 - -### 如果测试开始后,我的游戏还没有包体可下载,会如何? -您的游戏测试将不会生效,用户无法参与测试下载包体。 - -### 测试服无法修改为“试玩”了怎么办? -TapTap 的游戏测试功能进行了升级,如果您的测试服需开放测试,请使用版本发布 >> 测试功能进行操作。 - -### 测试账号无法登录游戏怎么办? -![](https://img.tapimg.com/market/images/f4685a7a1f782bf2d24941c67c25454c.png) - -### 封闭式测试的评分如何计算? -如果您在预约页上开放测试,封闭式测试的评价将不计入总分,且封闭式测试结束后评价不显示。如果您在测试服上开放测试,所有评价将计入测试服评分。 diff --git a/docs/store/test/test-gameplaytest.mdx b/docs/store/test/test-gameplaytest.mdx deleted file mode 100644 index d0c98abf0..000000000 --- a/docs/store/test/test-gameplaytest.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: 早期玩法测试 -sidebar_position: 4 ---- - -您需进行早期玩法测试验证核心玩法时,篝火测试模式是不错的选择。当前篝火测试模式已覆盖 Android、iOS 平台。 - -## 什么是篝火测试模式 - -篝火测试模式主要用于早期的玩法测试场景,是一种保密性和随机性都兼具的封闭式测试能力。 - -开启此模式后,您的游戏将无法被搜索、无法通过链接直接访问、也不显示 TapTap 评分、不显示厂商信息。仅来自首页推荐、站内投放的用户可参与您的封闭式测试的招募或资格领取,其余来源用户将无法浏览测试相关信息,也无法参与测试。已领取测试资格的用户,直至本次测试结束都可参与测试。测试结束后,您的游戏将无法被访问(即页面显示为 404)。 - -上述状态将持续至您关闭篝火测试模式 或 游戏开放预约、开放试玩、开放下载。 - -## 如何开启篝火测试模式 - -首先,您需要完成以下准备工作: -1. 篝火测试模式旨在增强您的游戏测试保密性,所以在开启前请先[接入 TapTap 登录增强测试保密性](/store/test/test-closedbetatest/#如何接入-taptap-登录增强测试保密性),并开启“仅有测试资格可登录游戏”。 -2. 篝火测试模式本质上也是封闭式测试,故需要先 [创建一个封闭式测试计划](/store/test/test-closedbetatest/#创建一个封闭式测试计划)。 - -在上述准备完成后,您可以开始开放测试了。封闭式测试的大部分功能,如招募、先到先得、生成短链,在篝火测试模式下均可使用。但由于您的游戏处于保密阶段,用户量基数较低,推荐您使用以下 2 种玩法搭配使用: -1. [开放测试资格至 TapTap 用户自由领取](/store/test/test-closedbetatest/#如何开放测试资格至-taptap-用户自由领取),别担心,使用此方式时也是仅限首页推荐、站内推广的用户可浏览测试信息和参与测试。 -2. [分享测试资格领取链接至种子用户群](/store/test/test-closedbetatest/#如何分享测试资格领取链接--海报)。 - -## 篝火测试专属流量包 - -由于开启篝火测试模式后,游戏将仅显示在首页推荐,曝光量可能不足。为方便您正常测试,TapTap 为您提供篝火测试模式的专属流量包 2000 元。 - -您可在开发者中心 >> 商店 >> 版本发布 >> 测试 >> 游戏测试设置 >> 开启篝火测试模式点击领取后,跳转 TapREP >> 首页 >> 游戏任务 >> 篝火模式专属流量包。篝火专属流量包领取后的有效期是 3 个月,一次性发放 2000 元奖励金,余额不支持重复使用。 - -领取流量包后,如需使用,请查看文档《[流量包使用](https://rep.taptap.cn/docs/taprep-ref)》。 - - - -## 常见问题 - -### 使用篝火测试模式可以接入其他渠道 / 平台的登录方式吗? -不能。篝火测试模式是 TapTap 为独家开测游戏提供的专属服务,且提供了专属流量包,不支持游戏内接入其他渠道的登录方式。但是在您完成了保密测试后,可以自由接入其他登录方式。 - -### 篝火测试模式开启后,可以申请资源位吗? -篝火测试模式是开放少量名额的随机限量测试,注重保密性,不上架常规资源位。但在 TapREP 后台通过新手任务获得的流量奖励同样可以在篝火测试模式,您可酌情使用,获得更多曝光。 - -### 篝火测试模式是否支持广告投放? -支持,篝火测试模式的游戏是一个普通的游戏,能够在站内常规投放位投放。 - -### 篝火测试模式期间,是否可开放预约? -不可开放预约,开放预约后篝火测试模式将自动失效。 - -### 我已经有一个预约服,想开放测试服开启篝火测试模式,能支持吗? -能。一个游戏的预约服有且只能关联一个测试服(含常规测试服),您可以在测试服上开启篝火测试模式。但因为正式服和测试服的唯一 ID 不同,在接入 TapTap 登录 SDK 时请多多注意。 - -### 开启篝火测试模式后,我的页面显示 404 了怎么办? -这是正常现象。为避免用户分享链接后,非测试用户通过链接访问,从而泄露游戏测试内容,TapTap 限制通过链接直接访问游戏。 -如需查看游戏数据,可前往开发者中心 >> 概览查看数据;如需查看游戏评价,可前往开发者中心 >> 商店 >> 评分评价 >> 评价处理和投诉。 - -### 登录游戏显示“您没有测试资格,无法登录游戏”怎么办? -请确认报错页面的 UI。 -1. 如果 UI 为您游戏的 UI 界面,表示您接入了旧版的测试资格校验 API。在新篝火测试模式下,您只需要接入 TapTap 登录 SDK 即可,如果曾接入旧版的测试资格校验 API ,现可移除相关代码。 -2. 如果 UI 为 TapTap 的蓝白 UI,表示您当前的账号未获取测试资格,或您未正确配置封闭式测试计划的开始和结束时间,请确认您接入的 TapTap 登录 SDK 和您配置的封闭式测试计划同属于于一个游戏唯一 ID 。 - -![](https://img.tapimg.com/market/images/f4685a7a1f782bf2d24941c67c25454c.png) - -### 如何关闭篝火测试模式? -在未领取篝火流量包时,您可随时关闭此模式。在已领取篝火流量包后,关闭篝火测试模式请联系对接运营,或通过工单咨询申请。 - -### 为什么开启篝火测试模式后,依旧显示了供应商信息? -根据国家有关部门规定,游戏在 TapTap 开放下载或测试必须展示供应商信息。如果担心这会对您的游戏造成影响,可申请个人开发者账号进行测试。 - -### 新版篝火测试模式开启后,旧版篝火测试的玩家还可以正常登录游戏吗? -旧版篝火测试玩家需要您重新发放资格才可正常下载并登录游戏。您可以使用以下 2 种方式为玩家重新发放资格: -1. 如果玩家已经在您的核心玩家交流群中,您可以 [将测试资格链接发送至玩家群](/store/test/test-closedbetatest/#如何分享测试资格领取链接--海报)。 -2. 如果您无法联系到旧版篝火测试的玩家,可以通过工单联系 TapTap 工作人员,TapTap 会为您提供曾经在旧版篝火测试阶段下载过您的游戏的玩家的 TapTap 用户 ID。您可以添加测试用户 >> 粘贴用户 ID 或导入用户表格 >> 定时设置发放资格时间 >> 保存,指定他们发放测试资格。 - - \ No newline at end of file diff --git a/docs/store/test/test-handbook.mdx b/docs/store/test/test-handbook.mdx deleted file mode 100644 index 50d491e62..000000000 --- a/docs/store/test/test-handbook.mdx +++ /dev/null @@ -1,307 +0,0 @@ ---- -title: 测试期运营手册 -sidebar_position: 7 ---- -import {Red, Blue, Black, Gray, Popup} from '/src/docComponents/doc'; - -封闭测,开放式测试和公测各有 2 种资源位可申请,每种资源位每次测试仅可申请一次(封闭测中如需进行招募,则还可额外申请 1 次测试招募资源位),日均曝光量预期最高可达约 **11 万次**(实际曝光情况与游戏热度以及资源位排序相关)。 - -## 一、 测试期如何获得免费资源位 -### 1. 测试招募(需申请) - -- **测试招募页曝光量:** 每日最高约 **1.5 万次**曝光(实际曝光量与游戏位次与热度强相关) - -- **申请频次:** 每次测试招募开启只可申请一次 - -- **审核周期:** 所有活动申请,自提交审核成功后 2 个工作日内处理,请及时查看审核状态 - -- **审核标准:** - - -> a. 对于评分低于 5 分(不含 5 分)的游戏,不做通过 -> -> b. 对于预约数< 15 万,不做通过 -> -> c. 对于存在争议、用户口碑极差的游戏,不做通过 -> -> d. 对于有强烈舆论问题的游戏,舆论期内不做通过 -> -> e. 纯游戏内活动、引导评价式活动、活动形式不在 TapTap 内落地的活动不做通过 -> -> f. 纯实物奖励活动,总获奖人数低于 10 人不做通过 -> -> g. 一等奖总价值低于 200 元,总价值低于 500 元的活动,不做通过 - -- **测试招募页展示逻辑:** 根据游戏热度(预约量)和招募结束时间进行排序,热度越高排序越靠前 - -- **展示周期:** 在招募期内进行展示,招募期结束后会进行沉底,直到被其他新的测试招募活动替换 - -- **隐藏扶持:** 高质量活动将获得 banner 头图,push,首页推荐等资源扶持 - -- **其他相关信息&申请方式:** [上“活动中心”栏目 | TapTap 开发者文档](https://developer.taptap.cn/docs/store/operations-skills/event-center/) - - -![](https://capacity-files.lcfile.com/g6BmVTc1CJcHsIcQDbrcMbHOlIlwu1YU/%E6%B5%8B%E8%AF%95%E6%8B%9B%E5%8B%9F.jpg) - - -### 2. 今日游戏(自行配置) - -为了尽可能让所有好游戏都能够获得足够的曝光,TapTap 在首页新增了「今日游戏」栏目以替代之前的「即将上线」。今日游戏能够承载游戏生命周期里的首曝、测试、首发、更新、折扣等各个关键节点的游戏曝光需求,信息更加全面,与用户的关联性更强。开发者仅需要按规范操作即可自动获得「今日游戏」栏目的基础曝光。 - -更多详细信息与申请指南:[如何获得「今日游戏」栏目的曝光](https://developer.taptap.cn/docs/store/operations-skills/today/) - - - - -## 二、 如何更好地运营游戏并与 TapTap 运营合作——测试期 -### 1. 基于 TapREP 与 TapTap 的运营合作 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    资源置换形式置换说明重点置换形式示例创建指南价值计算方式预期可获资源价值
    品牌标记内容媒体(微博/微信/b站/小红书/抖音/快手)宣发时文案/内容带TapTap---官号/KOL合作均可 -

    品牌标记示例

    -

    bilbili:文案带TapTap示例

    -

    抖音:视频内容带TapTap示例

    -

    快手:文案带TapTap示例

    -

    微博:文案带TapTap示例

    -

    小红书:文案带TapTap示例

    -

    直播:

    -

    -
    -

    TapREP:创建品牌资源

    -
    -

    以实际互动量结算,互动量定义为转评赞投币收藏等各个平台的互动行为

    -
      -
    • 抖音/快手/小红书/微博 统计自上传且审核通过之日起7天的增量互动数据
    • -
    • bilibili 统计自上传且审核通过之日起30天的增量互动数据
    • -
    -
    -

    根据实际转化结算:

    -

    目前数据平均一万赞视频价值在3500元左右(仅预估数据),结算实际还是以所有互动行为为结算依据

    -

    不同热度的视频价值示例:

    - -
    官网置换+品牌专区 -

    官网+品牌专区主入口挂TapTap游戏详情页+媒体渠道挂TapTap论坛首页

    -
    -

    渠道增加TapTap专区,引导至论坛

    -
    -

    -
    -

    step1: TapREP:创建效果资源

    -

    step2: 在更新完效果链接后,在TapREP后台上传品牌挂架

    -
    -

    一次性资源奖励+持续拉新拉活价值收益

    -

    一次性资源奖励阶梯奖励,单次最高收益1万元,根据放的位置不同,收益不同

    -

    -

    实际拉新拉活效果次日结算

    -

    大盘单个拉新用户价值100-300元

    -

    大盘单个拉活用户价值10-30元

    -
    -

    一次性资源奖励1000~10000元

    -


    -

    效果根据实际转化结算:

    -

    预约期游戏,站内预约超50万的游戏,首月预估资源价值20万,结算实际依赖官网带转化效果

    -
    -

    主入口加上TapTap下载

    -
    -

    -
    自媒体引流 -

    媒体通稿+微博、微信等自媒体带TapTap详情页效果链接

    -
    -

    媒体通稿带tap预约链接

    -
    -

    -
    -

    TapREP:创建效果资源

    -
    -

    按实际拉新拉活效果次日结算

    -

    大盘单个拉新用户价值100-300元

    -

    大盘单个拉活用户价值10-30元

    -
    -

    效果根据实际转化结算:

    -

    预约期游戏,站内预约超50万的游戏,首月预估资源价值5万,结算实际依赖各个资源位带转化效果

    -
    -

    微博/微信等内容带tap预约链接

    -
    -

    -
    -

    微信公众号阅读原文跳转tap预约链接

    -
    -

    -
    -

    微信公众号导航栏跳转tap预约链接

    -
    -

    -
    素材授权 -

    提供游戏素材授权给taptap,允许在站外推广时使用游戏相关素材,一次性给到1000元流量金

    -
    -

    计算机软著

    -
    -

    -
    -

    TapREP:上传素材授权

    -
    -

    单次素材授权,审核通过后,发放1000元流量包

    -
    对易玩的授权书
    -
    - - - - -:::info 注意 - -表格中所提到的“元”为等价流量包 - -::: - - - -### 2. 论坛配置及优秀实践分享 -TapTap 除了作为商店为厂商吸引玩家外,还是一个拥有巨大用户群的玩家社区。它允许玩家在其中讨论,分享游戏相关内容,并帮助开发者与玩家直接进行沟通。因此定期的论坛活动有助于提高玩家对于游戏的忠诚度,并帮助开发者及时收获真实反馈。 - -- 社区发帖操作:[学习社区模块,掌握基本操作](https://developer.taptap.cn/docs/community/features/) - -- 社区活动设计指南:[挑战进阶操作,玩转社区运营](https://developer.taptap.cn/docs/community/advanced/) - -- 优秀案例分享: - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    活动类型案例展示操作方式注意事项
    -

    意见收集

    -
    -

    【深度冻结】bug反馈&建议征集专用帖!

    - -
    -

    论坛后台发帖

    -
    -

    -
    -

    社区互动

    -
    -

    【有奖活动】星穹会客厅 | 景元:运筹帷幄的云骑将军

    - -
    -

    论坛后台发帖

    -
    -

    -
    -

    评论抽奖

    -
    -

    【内含周边抽奖】《深空之眼》太一·庚辰角色PV「夙夜长愿」

    - -
    -

    论坛后台发帖

    -
      -
    • TapTap社区内的评论抽奖活动支持自动化组件,只需在发帖时,在发布设置中选择【添加评论抽奖活动】并按照指引填写相关信息即可。目前支持多种奖品设置(实物/兑换码),定时自动开奖,自动去除bot等功能
    • -
    -
    -
      -
    1. 活动发起方务必在帖子内清楚、准确描述活动规则、活动奖品、开奖时间。
    2. -
    3. 中奖用户需要在 7 个自然日内完成收件信息填写,活动发起方请务必在开奖后 30 个自然日内完成奖品发放。
    4. -
    5. 开奖时将自动过滤机器人用户。
    6. -
    7. 使用抽奖功能组建需要开通版主运营权限
    8. -
    -
    -
    diff --git a/docs/store/test/test-internaltest.mdx b/docs/store/test/test-internaltest.mdx deleted file mode 100644 index 94daeeeaf..000000000 --- a/docs/store/test/test-internaltest.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: 内部测试 -sidebar_position: 3 ---- - -:::note - -内部测试仅限于已接入 [TapTap 登录 SDK](/sdk/taptap-login/features/) 的游戏可用,已覆盖 Android、iOS 平台。 - -::: - -## 如何开启内部测试 - -1. 在商店 >> 版本发布 >> 测试 >> 内部测试 >> 游戏测试设置 >> 开启仅有测试资格可登录游戏,并创建一个内部测试计划。 -2. 选择添加测试用户 >> 导入测试用户 >> 保存。如果需要分批次发放测试资格,则可以重复添加测试用户。 - - - -3. 获得资格的用户将收到 TapTap 的站内信通知,并且在本次测试开启期间可登录游戏。 - -## 如何关闭内部测试 - -1. 在商店 >> 版本发布 >> 测试 >> 内部测试中找到您想操作的内部测试计划,点击关闭测试。 -2. 关闭测试后,本次测试的所有测试用户无法登录游戏。但请注意,已经登录游戏的测试用户并不会被拦截。 -3. 如果您仅希望移除单个测试用户的资格,请阅读文档《[如何移除测试用户](#如何移除测试用户)》。 - -## 如何移除测试用户 - -在商店 >> 版本发布 >> 测试 >> 内部测试中找到您想操作的内部测试计划,点击测试资格明细,输入想要移除的测试用户的 TapTap ID 搜索后移除。 - - - -## 常见问题 - -### 我同时可以创建多少个内部测试计划? -您最多只能开启 1 个内部测试计划。 - -### 我已经在 TapTap 配置了封闭式测试、开放式测试后,还可以使用内部测试吗? -可以使用,内部测试能够和其他测试并存,您可以在招募期间或测试前的最后准备期间使用此功能登录游戏。 - -### 内部测试阶段能提供游戏包体下载吗? -不提供,因此如果您有外部人员需要参与内部测试,请通过其他渠道发送游戏包体。 - -### 内部测试最多支持多少人? -内部测试是针对司内其他同事、第三方安全检测机构等对象提供的,仅限 100 人。 - -### 开启仅有测试资格可登录游戏的作用是什么? -开启仅有测试资格可登录游戏后,TapTap 的服务将会在用户使用 TapTap 登录游戏时进行测试资格校验,没有测试资格的用户将无法登录游戏。如果关闭此开关,则所有用户均可登录游戏。 - -![](https://img.tapimg.com/market/images/f4685a7a1f782bf2d24941c67c25454c.png) - -### 如果我想对测试用户发放仅限 Android / 仅限 iOS 的测试资格怎么办? -TapTap 支持该功能,您只需要在发放测试资格时勾选不同的平台即可。 - -### 已经发放的测试资格可以修改平台吗? -不能修改,但是您可以再为这部分用户发放其他平台的测试资格,即假设用户 A 仅收到了 Android 测试资格时,您还可以为 A 发放 iOS 测试资格。 - -### 内部测试可以申请资源位吗? -内部测试并非面向用户的测试,不能申请资源位。 diff --git a/docs/store/test/test-onepic.mdx b/docs/store/test/test-onepic.mdx deleted file mode 100644 index 383b6dfad..000000000 --- a/docs/store/test/test-onepic.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: 一张图看懂在 TapTap 游戏测试 -sidebar_position: 2 ---- - -![](https://img.tapimg.com/market/images/f7ae87c38010523436d3126cb1393262.png) \ No newline at end of file diff --git a/docs/store/test/test-openbetatest.mdx b/docs/store/test/test-openbetatest.mdx deleted file mode 100644 index 62cd591ab..000000000 --- a/docs/store/test/test-openbetatest.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: 常规开放式测试(不限量测试) -sidebar_position: 6 ---- - -常规开放式测试适用于绝大多数的不限量测试需求,已覆盖 Android 平台,暂不支持 iOS 平台。 -阅读本文档将有利于您详细了解开放式测试的玩法。 - -## 如何进行开放式测试 - -您可以在游戏的预约页同时开放不限量测试,这样有利于您的游戏获得更多的转化。 - -1. 在商店 >> 版本发布 >> 测试 >> 开放式测试 >> 创建开放式测试计划,填写测试计划名称和测试计划时间。 - - - -2. 创建成功后,用户访问您的游戏页将会看到预约和参与测试双按钮,同时也可以了解本次测试时间周期。 - -![](https://img.tapimg.com/market/images/3f3fa934e5859139a0eaf473a3e8f9fc.png) - -:::tip - -创建测试计划时请注意: -- 测试计划名称请不要含任何标点符号,如「」、《》,也不要含游戏名。 -- 测试开始时间已填写后,用户可以在您的游戏页看到测试信息。但测试开始后才可参与测试。 -- 测试开始时间非必填,如未确定,可不填写。测试开始后,无法修改测试开始时间。 -- 测试结束时间非必填,如果您还未确定测试结束时间可不填写,用户将会看到“暂未公布”测试结束时间。 - -::: - -## 如何使用测试服进行开放式测试 - -当您希望您的测试用户完全和其他用户隔离开来时,您可使用测试服功能进行测试。 - -1. 在您的游戏的预约页(即正式服页面)进入开发者中心 >> 版本发布 >> 测试 >> 开放式测试 >> 使用测试服功能。 -2. 根据页面引导,创建测试服并提交审核即可。第一次创建的测试服将直接复制正式服的素材,但不会复制 APK。 - - - -3. 测试服创建完成后,测试服的开放式测试的服务可用,您只需按照上方的帮助文档进行操作即可。 - -:::tip - -使用测试服测试请注意: -- 测试服审核中时,也可提前创建测试计划。 -- 为保证 TapTap 的用户体验,测试服审核中/定时上线/已上线时,正式服的测试服务(内部测试除外)将停用,如果您的正式服有正在进行中测试计划将直接失效。 -- 新建的测试服审核通过后将正常上架 TapTap,如果您不希望提前上架,可通过定时上线功能或后续再提交审核。 -- 如果您需要在创建测试服时一并提交 APK 审核,请在测试服页面选择商店资料的对应版本后更新即可。 -- 测试服仅用于游戏测试,部分功能将受限制不可用,含:开放测试服预约/云玩/TapPlay、游戏故障评价处理、签到活动、新版本活动。 - -::: - -## 常见问题 - -### 开放式测试和上线试玩版有什么区别? -上线试玩版将视作正式上线,使用开放式测试则是正式上线前的最后几轮不限量删档测试。核心的区别是游戏是否为删档测试。 - -### 如果测试开始后,我的游戏还没有包体可下载,会如何? -您的游戏测试将不会生效,用户无法参与测试下载包体。 - -### 测试服无法修改为“试玩”了怎么办? -TapTap 的游戏测试功能进行了升级,如果您的测试服需开放测试,请使用版本发布 >> 测试功能进行操作。 - -### 为什么 iOS 无法进行开放式测试? -因为 TestFlight 功能限制,公开链接仅支持最多 1 万人。 - -### 开放式测试的评分如何计算? -如果您在预约页上开放测试,开放式测试的评价将计入总分。如果您在测试服上开放测试,所有评价将计入测试服评分。 \ No newline at end of file diff --git a/docs/store/test/test-support.mdx b/docs/store/test/test-support.mdx deleted file mode 100644 index 7abf4a0e0..000000000 --- a/docs/store/test/test-support.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: 如何在 TapTap 开启测试 -sidebar_position: 1 ---- - -如果您的游戏需要向测试用户开放测试,阅读本文档有便于您简单了解 TapTap 的测试能力。目前 TapTap 支持以下测试能力: - -## 内部测试 - -如果需将游戏 APK 发送至团队外部(如司内其他同事、第三方安全检测机构),或游戏已配置完成常规封闭式测试时,您可以使用为团队成员添加内部测试白名单,确保他们可以正常进入游戏。 - -详细内部测试功能,请查看文档《[内部测试](/store/test/test-internaltest)》。 - -## 早期玩法测试 - -如果您的游戏未上架 TapTap 开放预约,且此时仅仅希望开放少量用户测试核心玩法时,您可以使用篝火测试模式。 - -详细篝火测试模式,请查看文档《[早期玩法测试](/store/test/test-gameplaytest)》。 - -## 常规封闭式测试 - -当您的游戏已经开放预约,且进入常规限量测试期时,您可以使用封闭式测试功能。封闭式测试功能支持招募用户、开放测试资格以供玩家领取、发放激活码、玩家邀请测试等能力。 - -详细封闭式测试功能,请查看文档《[常规封闭式测试(限量测试)](/store/test/test-closedbetatest)》。 - -## 常规开放式测试 - -如果您的游戏想要开放给更多的玩家参与测试时,您可以使用开放式测试功能。建议您在开放式测试期间同时开放预约,这样有利于您的游戏在首发时获得更多关注。 - -详细开放式测试能力,请查看文档《[常规开放式测试(不限量测试)](/store/test/test-openbetatest)》。 - -## 素材测试 - -当您想要测试图标、宣传片、Banner、游戏截图等物料效果时,可以使用素材测试功能。TapTap 目前仅站内投放流量支持此服务。 - -详细素材测试能力,请咨询 TapTap 工作人员。 - diff --git a/docs/tap-android-faq.mdx b/docs/tap-android-faq.mdx deleted file mode 100644 index 97202bc81..000000000 --- a/docs/tap-android-faq.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: TapSDK Android FAQ ---- - - - - - -## 登录失败 -可能有以下几个原因: - -1. 没有在 TapTap 开发者中心开启登录功能; - -2. 由于 TapTap 客户端授权登录,会对 `Client ID`、应用包名、签名文件的 MD5 进行校验 , 其中任何一项配置错误都会导致登录失败。 -例如:`{"status":403,"data":{"code":-1,"msg":"暂未开通","error":"forbidden","error_description":"The action forbidden."}} -原因:`Client ID` 没有配置正确,检查 `Client ID`。 由 `Client ID`、应用包名、签名文件的 MD5 错误导致的登录失败是最常见的情况,建议参考 [接入准备](/sdk/start/get-ready/) 进行检查,该问题开发者自己就可以解决; - -3. 设备系统时间不准确,此类情况大多数是因为设备的系统时间没有开启自动联网同步导致; - -4. 登录功能所在的 Activity 设置横竖屏时添加 android:configChanges 属性配置,需要在 AndroidMainfest.xml 文件中对登录功能所在的 Activity 添加如下配置(以横屏为例): -```xml -android:screenOrientation="landscape" -android:configChanges="orientation|keyboardHidden|screenSize|locale|uiMode|screenLayout" -``` -否则会导致无法正常登录,登录回调方法不会被执行。 - -5. 登录时报 404 或者 405 -请检查 `TapConfig.Builder()` 配置,中国大陆请配置为 `TapRegionType.CN` -```java -// TapSDK 初始化 -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(getApplicationContext()) - .withRegionType(TapRegionType.CN) // TapRegionType.CN: 中国大陆 TapRegionType.IO: 国际 - .withClientId("Client ID") // TapTap 开发者中心创建应用后获取对 Client ID - .withClientSecret("Client Token") // TapTap 开发者中心创建应用后获取对 Client Token - .build(); -TapBootstrap.init(MainActivity.this, tapConfig); -``` - - -## Android resource linking failed -error: attribute android:requestLegacyExternalStorage not found. -error: failed processing manifest. - -原因: -SDK 内部默认配置了 android:requestLegacyExternalStorage = true -当 targetSdkVersion < 29 时会报这个错误 -解决: -方法一、设置 targetSdkVersion = 29 -方法二、targetSdkVersion < 29 时 -manifest 节点配置 `xmlns:tools="http://schemas.android.com/tools"` -application 节点配置 `tools:remove="android:requestLegacyExternalStorage"` - diff --git a/docs/tap-download.mdx b/docs/tap-download.mdx deleted file mode 100644 index 5bab881e7..000000000 --- a/docs/tap-download.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: 资源下载 ---- - -import sdkVersions from '/src/docComponents/sdkVersions'; -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; - -## SDK - - - -若无法顺利通过上述链接获取 SDK,可尝试换用以下镜像: - - - -:::tip -TapSDK 为我们提供的众多服务的总称,为了方便开发者按需接入,我们将每一个子服务使用一个独立模块来对外提供服务,由易玩(上海)网络科技有限公司开发提供。[SDK 隐私政策](https://developer.taptap.cn/docs/sdk/start/agreement/)、[SDK 合规使用说明](https://developer.taptap.cn/docs/sdk/start/compliance/); -::: - - -## TapSDK Demos - -- [Unity](https://github.com/taptap/TapSDK-Unity-Demo/tree/master) 【[Unity 安装包下载](https://capacity-files.lcfile.com/0iB7AQs4NlVpKirz8t94g7HePh03SAl6/demo.apk)】 -- [Android](https://github.com/taptap/TapSDK-Android-Demo) 【[Android 安装包下载](https://capacity-files.lcfile.com/MgMrSbWK8LBoYnjB3cBsYEsk63C3E3LV/tapsdk_android_v3.29.0.apk)】 -- [iOS](https://github.com/taptap/TapSDK-iOS) - -## 商业变现 -TapADN SDK 是由「易玩(上海)网络科技有限公司」开发,向媒体提供丰富的广告资源,依托高效的算法引擎,帮助开发者实现流量变现。

    - -[接入文档](/sdk/tap-adn/tds-tapad/)    [合规指南](/sdk/tap-adn/adn-compliance/) -1. [TapADN Android SDK](https://tapad-platform.tapimg.com/sdk/TapAD_3.16.3.31.aar) 版本: 3.16.3.31 更新日期: 2024-07-03 -2. [TapADN Unity SDK](https://tapad-platform.tapimg.com/sdk/TapAD_3.16.3.31.unitypackage) 版本: 3.16.3.31 更新日期: 2024-07-03 -3. [TapADN Android Demo](https://tapad-platform.tapimg.com/sdk/TapADDemo_3.16.3.31.zip) 【[Android 安装包下载](https://tapad-platform.tapimg.com/sdk/tapaddemo_external_3.16.3.31-release.apk)】 - -:::tip -TapADN SDK 为 APP 提供广告投放及广告监测归因、反作弊和广告投放统计分析等服务,由易玩(上海)网络科技有限公司开发提供。[SDK 隐私政策](https://developer.taptap.cn/docs/sdk/tap-adn/agreement/)、[SDK 合规使用说明](https://developer.taptap.cn/docs/sdk/tap-adn/adn-compliance/); -::: - -## 登录按钮素材 - -点击下载 [icon.zip](https://assets.tapimg.com/img/TapTap_Login_Source.zip) diff --git a/docs/tap-ios-faq.mdx b/docs/tap-ios-faq.mdx deleted file mode 100644 index 05c249c3c..000000000 --- a/docs/tap-ios-faq.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: TapSDK iOS FAQ ---- - - - - - -## 登录失败 -可能有几个以下原因: - -1. 没有在 TapTap 开发者中心开启登录功能; - -2. 由于 TapTap 客户端授权登录,会对 `Client ID`、`BundleID` 进行校验,其中任何一项配置错误都会导致登录失败。 - -3. 登录时报 404 或者 405 -请检查 `TapConfig` 的区域配置,中国大陆请配置为 `TapSDKRegionTypeCN`。 -```c# -// TapSDK 初始化 -TapConfig *config = TapConfig.new; -config.clientId = @"clientId"; -config.clientSecret=@"clientSecret";// 开发者中心对应 Client Token -config.region = TapSDKRegionTypeCN; -[TapBootstrap initWithConfig:config]; -``` - -## registerLoginCallback 代理无法回调 -检查一下 client ID 设置,info.plist 和初始化代码里面保持一致。 - -## [UIWindow tds_topWindow]: unrecognized selector sent to class 0xxxxxxxx -到 Build Setting --> Other Linker Flags 添加 - ObjC 。 -![](/img/tap_ios_003.png) - -## ld: symbol(s) not found for architecture arm64 clang: error: linker command failed with exit code 1 (use -v to see invocation) -到下图所示位置,再检查一遍包的导入情况。 - -![](/img/tap_ios_faq_libc.png) - -## Xcode 版本过低导致的异常 - -下图异常主要是 Xcode 版本过低导致,TapSDK2.X 版本,iOS 建议使用 Xcode12.3 及其以上版本打包。 - -![](/img/tap_fqa_ios_xcode.png) diff --git a/docs/tap-unity-faq.mdx b/docs/tap-unity-faq.mdx deleted file mode 100644 index 7d6b561e3..000000000 --- a/docs/tap-unity-faq.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: TapSDK Unity FAQ ---- - - - - - -## 登录失败 -可能有以下几个原因: - -1. 没有在 TapTap 开发者中心开启登录功能; - -2. 由于 TapTap 客户端授权登录,会对 Android 应用的 `Client ID`、应用包名、签名文件的 MD5 进行校验 ; iOS 应用的 `Client ID` 和 `BundleID` 进行校验 , 其中任何一项配置错误都会导致登录失败。 - -3. 登录时报 404 或者 405 -请检查 `TapConfig` 的区域配置,中国大陆请配置为 `true`。 -```c# -TapConfig tapConfig = new TapConfig.Builder() - .ClientID("clientId")// 必须 - .ClientSecret("client_secret")// 必须,开发者中心对应 Client Token - .RegionType(RegionType.CN)// 非必须,默认 CN - .ConfigBuilder(); - -TapBootstrap.Init(tapConfig); -``` - -## The type or namespace name 'TapSDK' could not be found -不需要手动引入命名空间 using TapSDK; 直接使用就可以,详情参考 [快速开始](/sdk#初始化) 的用法 - -## 打开动态页面出现视频声音跟游戏声音重合 -请在 `openTapMoment` 调用时,主动屏蔽游戏声音 - -## 点击动态后小红点未消失 -OpenMoment 后会刷新动态,小红点逻辑需要游戏手动根据 FetchNotification 来改变 diff --git a/docs/taptap-ad.mdx b/docs/taptap-ad.mdx deleted file mode 100644 index 908075959..000000000 --- a/docs/taptap-ad.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: 营销推广 -slug: /store/taptap-ad ---- - - - - - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - - -## **一、信息流广告** -### **1. 营销推广位置** -目前 TapTap 营销推广的展现位在首页信息流内。 -![Appearance](https://img.tapimg.com/market/images/968f05c22b1a4ba041c1ce8a4e0b5e68.png) - ---- - -### **2. 营销推广投放平台** -支持** TapTap 大陆地区**的网页端及客户端营销推广投放。 - ---- - -### **3. 广告后台** -1. 请确保厂商管理员已为相关营销推广人员添加 TapAD 权限,厂商管理员可于[TapAD 推广平台 - 企业设置 - 权限管理](https://biz.taptap.com/sense/m/org/0/authority)界面进行添加 - - ![Authority](https://img.tapimg.com/market/images/96eca038ff054e02b568770d0c87f2a2.png) - -2. 厂商管理员或营销推广人员可前往 [TapAD 推广平台](https://biz.taptap.com/),通过 TapTapID 登录推广平台后台查看游戏推广情况  - - ---- - -## **二、搜索广告** -### **1.营销推广位置** -1)搜索引导页发现词(1个关键词位置) -2)搜索引导页热搜词(2个关键词位置) -3)搜索结果页综合tab(1-2个关键词结果广告位) -4)搜索结果页游戏tab(N个关键词结果广告位,无限广告位) -*注:以上搜索广告仅在2.19及以上版本展现 - ---- - -### **2.营销推广展示示意** -**广告位名称:搜索发现词/搜索热门词** -![Performance1](https://img.tapimg.com/market/images/19dbd7500fddc86745441ff00177e8d0.png) -**广告位名称:搜索关键词-游戏词** -![Performance2](https://img.tapimg.com/market/images/80c11236ac8410d880b8c449c79b2ffa.png) -**广告位名称:搜索关键词-通用词(游戏标签)** -![Performance3]](https://img.tapimg.com/market/images/dcbf16a9c87f1df2e9721e22b2f05d17.png) - ---- - -### **3. 广告后台** -1)请确保厂商管理员已为相关营销推广人员添加 TapAD 权限,厂商管理员可于[TapAD 推广平台 - 企业设置 - 权限管理](https://biz.taptap.com/sense/m/org/0/authority)界面进行添加 -![Authority](https://img.tapimg.com/market/images/96eca038ff054e02b568770d0c87f2a2.png) -2)厂商管理员或营销推广人员可前往 [TapAD 推广平台](https://biz.taptap.com/),通过 TapTapID 登录推广平台后台查看游戏推广情况  diff --git a/docs/tds-payment.mdx b/docs/tds-payment.mdx deleted file mode 100644 index 24f075e05..000000000 --- a/docs/tds-payment.mdx +++ /dev/null @@ -1,593 +0,0 @@ ---- -title: 支付系统 TapPayment ---- - - - - - -import MultiLang from "../src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "../src/docComponents/sdkVersions"; -import { Conditional } from "../src/docComponents/conditional"; - - -TapPayment 是一项可让您在游戏中销售虚拟商品的服务。 - -您可以在 TapPayment 中销售以下类型的商品: - -* 消耗型商品 -* 非消耗型商品 - -商品类型介绍: - -| 类型 | 描述 | -| --- | --- | -| 消耗型商品 | **消耗型商品**是指当用户消耗该商品时,您的游戏会分配相关联的游戏内容,而用户随后可以再次重复购的商品。例如游戏货币、游戏道具等。| -| 非消耗型商品 | **非消耗型商品**是指购买一次就能永久使用的商品。例如付费升级和关卡包等。| - -借助 TapTap 开发者中心,可以方便地创建游戏内商品,并且在游戏中便捷地接入 TapPayment 的服务,来进行游戏商品的销售。**TapPayment 支付渠道暂时只支持微信支付 & 支付宝支付** - -## SDK 安装 - -### 开发环境 - - -<> - -- 支持 Unity 2019.4 或更高版本。 - - -<> - -- **最低 Android 版本为 5.0**,SDK 编译环境为 Android Studio。 - -1. [下载 TapSDK Android](/tap-download),解压后选择 TapBootstrap、TapCommon、TapPayment 包导入到项目 `project/app/libs` 目录下。 - -2. 打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: - - {` -dependencies { - ... - // 导入 libs 目录下所有 aar 的包: - implementation fileTree(dir: 'libs', include: ['*.aar']) - // 如果需要单独导入 libs 目录下的指定包,请按照如下方式: - implementation files('libs/TapBootstrap_${sdkVersions.taptap.android}.aar') // TapTap 启动器 - implementation files('libs/TapCommon_${sdkVersions.taptap.android}.aar') // TapTap 基础库 - implementation files('libs/TapPayment_${sdkVersions.taptap.android}.aar') // TapTap 登录 - // 如果要支持 app 支付,添加下面的微信和支付宝的 SDK - implementation 'com.alipay.sdk:alipaysdk-android:15.8.11@aar' - implementation 'com.tencent.mm.opensdk:wechat-sdk-android:6.8.0' - ... - // 数据存储 - implementation 'com.taptap:lc-storage-android:${sdkVersions.leancloud.java}' - // 即时通讯 - implementation 'com.taptap:lc-realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' -} -`} - - -<> - -- iOS 示例代码待补充 - - - - -### 集成库 - -使用 TapTap.Payment 前提是必须依赖以下库: - -* TapTap.Bootstrap -* TapTap.Common - -接入方式参考 [入门指南 - 项目配置](/sdk/start/quickstart.mdx)。 - -## 初始化 - -:::info -以下两种初始化方式任选其一。 -::: - -### TapSDK 初始化 - -如果你已经参考快速开始完成了初始化,这里只需要引入支付模块即可。 - - -<> - -```cs -using TapTap.Payment; - -var config = new TapConfig.Builder() - .ClientID(clientId) // 必须,开发者中心对应 Client ID - .ClientToken(clientToken) // 必须,开发者中心对应 Client Token - .ServerURL(serverUrl) // 必须,开发者中心 > 你的游戏 > 游戏服务 > 基本信息 > 域名配置 > API - .RegionType(RegionType.CN) // 必须,CN 表示中国大陆,IO 表示其他国家或地区 - .TapPaymentConfig( - "CN", // 地区选项如果国内填写「CN」,如果是海外填写「US」 - "zh_CN", // 语言请参考 「语言代码列表」 - "https://${domain}" // 国内需要填写微信商户申请 H5 时提交的授权域名,详见 https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_4 里 Referer 设置相关部分,海外这里填空字符串。 - ) - .ConfigBuilder(); -TapBootstrap.Init(config); -``` - - -<> - -```java -TapPaymentConfig tapPaymentConfig = new TapPaymentConfig.Builder() - // 地区暂时只支持「中国地区」 - .withRegionId(Constants.Region.REGION_CN) - // 语言暂时只支持中文 - .withLanguage(Constants.Language.LANGUAGE_CN) - // 微信商户申请H5时提交的授权域名,详见 https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_4 里 Referer 设置相关部分 - .withWXAuthorizedDomainName("https://${domain}") - .build(); - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext("${context}") // 初始化的 Activity - .withClientId("${ClientId}" ) // DC 后台配置的 Client ID - .withClientToken("${ClientToken}") // DC 后台配置的 Client Token - .withServerUrl("${ServerUrl}") // DC 后台绑定的域名 - .withRegionType(TapRegionType.CN) // TapPayment 只支持国内部分 - .withTapPaymentConfig(tapPaymentConfig) - .build(); -TapBootstrap.init(context, tapConfig); -``` - - -<> - -- iOS 示例代码待补充 - - - - -### 支付系统单独初始化 -如果游戏不通过上面提供的 `TapBootstrap` 方法初始化 TapSDK,仅希望初始化支付系统。可以这么做: - -- **复制上面提供的第一种初始化方式代码**。 -- **修改最后一行**为: - - -<> - -```cs -TapPayment.Init(config); -``` - - -<> - -- Android 示例代码待补充 - - -<> - -- iOS 示例代码待补充 - - - - -还可以用下面的方法判断支付模块是否已经初始化: - - -<> - -```cs -var result = TapPayment.IsReady(); -``` - - -<> - -- Android 示例代码待补充 - - -<> - -- iOS 示例代码待补充 - - - - -## SDK 开发指南 - -### 查询单个商品 - -查询单个商品的示例代码如下, `skuId` 是游戏在**开发者中心后台**定义的商品 ID,`action` 是获取商品回调结果。 - - -<> - -```cs -TapPayment.QueryProduct(skuId, (skuDetail, error) => -{ - if (error != null) - { - // get product fail & do something - } - else - { - if (skuDetail == null) { - // not found any product with given skuId - } else { - // do something - } - } -}); -``` - -<> - -```java -/** - * 查询单个商品 - * @param skuId 在DC后台定义的商品id - * @param callback 获取商品返回结果 - */ -TapPayment.queryProduct(String skuId, new Callback() { - @Override - public void onSuccess(SkuDetails result) { - if (result == null) { - // not found any product with given skuId - } else { - // do something - } - } - - @Override - public void onError(TapPaymentException tapPaymentException) { - // do something - } -}); -``` - -<> - -- iOS 示例代码待补充 - - - - -商品信息定义: - -| Parameters | type | Description | -|------------- |---------------| -------------| -| goodsOpenId | string | 开发者中心后台定义的商品 ID | -| goodsType | int | 1:消耗型;2:非消耗型 | -| goodsConfig | object | 详见「商品配置」| -| goodsPrice | object | 详见「价格配置」| - -商品配置定义: - -| Parameters | type | Description | -|------------- |---------------| -------------| -| languageId | string | 开发者中心后台创建商品时配置的「语言 ID」,例如 "zh_CN" | -| goodsName | string | 开发者中心后台创建商品时配置的「商品名称」| -| goodsDescription | string | 开发者中心后台创建商品时配置的「商品名称」| - -价格配置定义: - -| Parameters | type | Description | -|------------- |---------------| -------------| -| regionId | string | 国内开发者中心后台的地区 ID,默认是 "CN" | -| goodsPriceAmount | string | 开发者中心后台创建商品时配置的「商品价格」| -| goodsPriceCurrency | string | 国内开发者中心后台的默认货币「元」| - -错误码信息: - -| code | 场景 | -|------------- |---------------| -| 19999 | 服务端返回的数据格式非预期 | -| 50x 的服务器错误 | 服务端内部错误 | -| 401 | 未授权(可以检查初始化配置的参数是否正确配置)| -| 403 | 用户无权限访问该服务(检查开发者中心是否开通了 TapPayment 服务)| - - -### 查询多个商品 - -查询多个商品的示例代码如下, `skuIds` 是商品 ID 列表,`callback` 是获取商品的返回结果。 - - -<> - -```cs -TapPayment.QueryProducts(skuIds, (list, error) => -{ - if (error != null) - { - // get product fail & do something - } - else - { - if (list == null || list.Count == 0) { - // not found any product with given skuIds - } else { - // do something - } - } -}); -``` - -<> - -```java -/** - * 查询单个商品 - * @param skuIds 商品id列表 eg "test01,test02" - * @param callback 获取商品返回结果 - */ -TapPayment.queryProducts(Arrays.asList("${id1}", "${id2}"), new ListCallback() { - @Override - public void onSuccess(List resultList) { - if (resultList.size() == 0) { - // not found any product with given skuId - } else { - // do something - } - } - - @Override - public void onError(TapPaymentException tapPaymentException) { - // do something - } -}); -``` - - -<> - -- iOS 示例代码待补充 - - - - -### 启动应用内购买 - -应用内购买的接口定义: - - -<> - -```cs -/** - * 启动购买流程 - * @param skuDetails 购买的商品信息 - * @param roleId 游戏内角色 id - * @param serverId 游戏的服务器 id - * @param extra 游戏的额外信息 json 格式的字符串 eg."{\"test\":\"test\"}" - * @param action -*/ -void LaunchBillingFlow(SkuDetails skuDetails, string roleId, String serverId, String extra, Action action); -``` - -<> - -```java -/** - * 启动购买流程 - * @param activity 唤起购买流程的 Activity - * @param skuDetails 购买的商品信息 - * @param roleId 游戏内角色 id - * @param serverId 游戏的服务器 id - * @param extra 游戏的额外信息 json 格式的字符串 eg."{\"test\":\"test\"}" - * @param purchaseCallback -*/ -void launchBillingFlow(final Activity activity, SkuDetails skuDetails, String roleId, String serverId, String extra, PurchaseCallback purchaseCallback) -``` - - -<> - -- iOS 示例代码待补充 - - - - -**其中 roleId、serverId、extra 均是透传字段,如果游戏有自己的订单id的话建议通过extra字段来传递,当TapPayment Server收到成功支付消息后会将这些透传字段回传到游戏Server** - -示例代码如下: - -<> - -```cs -TapPayment.LaunchBillingFlow(skuDetailsList[buyPosition], "roleId001", "1e59c66f-51d8-4e9e-afd2-f91965628e9b", "{\"test\":\"test\"}", (responseCode, error) => -{ - if (error != null) - { - // native bridge exception - } - else - { - if (responseCode == 0) - { - // complete - } - else if (responseCode == 1) - { - // error - } - else if (responseCode == 2) - { - // user cancel - } - } -}); -``` - -<> - -```java -TapPayment.launchBillingFlow(activity - , skuDetails - , roleId - , serverId - , extra - , new PurchaseCallback() { - @Override - public void onPurchaseUpdated(int responseCode, String message) { - // do some thing - } - } -); -``` - - -<> - -- iOS 示例代码待补充 - - - - -responseCode 的定义: - -| code | 场景 | -|------------- |---------------| -| COMPLETE(0) | 购买完成,或者购买被动取消(前端超时),或者用户点击关闭按钮均会返回该结果 | -| ERROR(1) | 购买异常,前端返回 hash 值是 fail 的情况 | -| USER_CANCEL(2) | 用户取消,用户还未开始支付流程就关闭了支付窗口的情况 | - -## Server 端 Webhook 设置 - -### Webhook 说明 - -Webhook 用于支付到账后 TapPayment 通知游戏方发放商品,游戏方需要在游戏服务器上设置Webhook,并在开发者中心后台配置好 Webhook 的 URL。游戏收到 Webhook 通知后,即可认为支付成功到账,可以发放商品,同时返回 200 状态码,如果返回其他的状态的话,则认为支付失败,TapPayment 将会重复调用 Webhook 最多 10 次。 - -### Webhook 内容 - -回调游戏 webhook 是 POST 方法。Webhook 的内容格式为 json,具体格式如下: - -```json -{ - "clientId": "", // 游戏方的 clientId - "orderId": "", // 购买时内部生成的 orderId - "serverId": "", // 启动购买时传入的 serverId - "roleId": "", // 启动购买时传入的 roleId - "userId": "", // 启动购买时的 userId - "goodOpenId": "", // 启动购买时传入的商品 openId - "extra": "", // 启动购买时传入的额外信息 - "timestamp":"", // 时间戳 - "paymentChannelName", "" // 支付渠道名称 - "signature": "signature" // 签名 -} -``` - -游戏方成功处理 Webhook 调用后,需要给 TapPayment 返回 200 状态码,并且返回结果为 `success` 的字符串,否则 TapPayment 将认为调用失败而重试。 - -### Webhook 验签 - -Webhook 使用 HmacSHA256 算法验签,使用的 secret 需在开发者中心后台 Payment Secret Key 处设置,如果没有设置 Webhook 不会被调用。Webhook 的签名验证过程中,需要先将 json 中所有 field 按照字母顺序排序,然后将排序后的 field 按照 `key=value` 的格式组合成字符串,类似 `a=v1&b=v2` 的形式,最后将组合后的字符串后使用开发者中心后台设置好的 secret 用 HmacSHA256 算法进行签名,最后将签名结果作为 Webhook 的 signature 字段返回,最后的 16 进制结果全部小写。 - -```java -Mac sha256HMAC = Mac.getInstance("HmacSHA256"); -SecretKeySpec secretKey = new SecretKeySpec(token.getBytes(), "HmacSHA256"); -sha256HMAC.init(secretKey); -byte[] bytes = sha256HMAC.doFinal(str.getBytes()); -hash = byteArrayToHexString(bytes); -``` - -切换语言的接口定义: - - -<> - -```cs -/** - * 切换语言 - * @param languageId // 详情请参考「语言代码列表」 -*/ -void SwitchLanguage(String languageId); -``` - -<> - -- Android 示例代码待补充 - - -<> - -- iOS 示例代码待补充 - - - - -示例代码如下: - -<> - -```cs -TapPayment.SwitchLanguage("en_US"); -``` - -<> - -- Android 示例代码待补充 - - -<> - -- iOS 示例代码待补充 - - - - -## 语言代码列表 - -使用 ISO 639-1 中定义的双小写字母语言代码(例如,`en` 表示英语,`jp` 表示日语),但: - -1. ISO 639-1 中未包括的语言,使用 ISO 632-2 中定义的三小写字母语言代码(例如,`fil` 表示菲律宾语) -2. 仅使用语言代码无法表示所需语言时,附加 ISO 3166-1 中定义的地区代码(例如,`zh_CN` 表示简体中文) - -当前支持的语言代码如下: - -| 代码 | 语言 | -| ------- | ----------- | -| zh_CN | 简体中文 | -| zh_TW | 繁体中文 | -| en_US | 英语(美国) | -| ja_JP | 日文 | -| ko_KR | 韩文 | -| pt_PT | 葡萄牙语 | -| vi_VN | 越南语 | -| hi_IN | 印度语 | -| id_ID | 印尼语 | -| ms_MY | 马来语 | -| th_TH | 泰语 | -| es_ES | 西班牙 | -| af | 南非荷兰语 | -| am | 阿姆哈拉语 | -| bg | 保加利亚语 | -| ca | 加泰罗尼亚语 | -| hr | 克罗地亚语 | -| cs | 捷克语 | -| da | 丹麦语 | -| nl | 荷兰语 | -| et | 爱沙尼亚语 | -| fil | 菲律宾语 | -| fi | 芬兰语 | -| fr | 法语 | -| de | 德语 | -| el | 希腊语 | -| he | 希伯来语 | -| hu | 匈牙利语 | -| is | 冰岛语 | -| it | 意大利语 | -| lv | 拉脱维亚语 | -| lt | 立陶宛语 | -| no | 挪威语 | -| pl | 波兰语 | -| ro | 罗马尼亚语 | -| ru | 俄语 | -| sr | 塞尔维亚语 | -| sk | 斯洛伐克语 | -| sl | 斯洛文尼亚语 | -| sw | 斯瓦希里语 | -| sv | 瑞典语 | -| tr | 土耳其语 | -| uk | 乌克兰语 | -| zu | 祖鲁语 | diff --git a/docs/user-licence-service-agreement.mdx b/docs/user-licence-service-agreement.mdx deleted file mode 100644 index 4b05f2134..000000000 --- a/docs/user-licence-service-agreement.mdx +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: TapTap 用户授权服务协议 ---- - -《TapTap 用户授权服务协议》(以下简称“本协议”)是易玩(上海)网络科技有限公司及其关联公司(以下简称 “TapTap” 或“我们”)与用户(以下简称“您”)之间关于使用数据授权服务所订立的协议。**请您先仔细阅读本协议内容,尤其是字体加粗部分。如您对本协议内容或页面提示信息有疑问,请勿进行下一步操作**。您可通过 TapTap 最新版客户端侧边栏中的“帮助与反馈”联系我们。**如您通过页面点击或我们认可的其他方式确认本协议即表示您已同意本协议。** - -1、**为了便于您使用第三方的服务,您同意 TapTap 将您的用户标识及页面提示的相关信息传递给第三方。页面提示上会展示具体授权对象以及授权信息类型,您的信息将通过加密通道传递给第三方**。TapTap 会要求第三方严格遵守相关法律法规与监管要求,依法使用您的信息,并应对您的信息保密。**点击授权之后,授权关系长期有效,直至您主动解除**。为了更好的保护您的信息安全,我们采用了相关接口技术。授权有效期内,当您使用被授权方服务时,将打开接口,被授权方仅在接口激活期内方可查询您的信息。激活期届满后,接口将暂时失效,直至您再次使用被授权方服务时被激活。 - -2、**TapTap 是中立的技术服务提供者,上述第三方服务由该第三方独立运营并独立承担全部责任。因第三方服务或其使用您的信息而产生的纠纷,或第三方服务违反相关法律法规或协议约定,或您在使用第三方服务过程中遭受损失的,请您和第三方协商解决。** - -3、如我们对本协议进行变更,我们将通过公告或客户端消息等方式予以通知,该等变更自通知载明的生效时间开始生效。若您无法同意变更修改后的协议内容,您有权停止使用相关服务;双方协商一致的,也可另行变更相关服务和对应协议内容。 - -4、本协议未约定事宜,均以 TapTap 网站及客户端公布的[《TapTap 服务协议》](https://www.taptap.cn/doc/terms/)及相关规则为补充;本协议与[《TapTap 服务协议》](https://www.taptap.cn/doc/terms/)及相关规则不一致的地方,以本协议为准。 - -5、本协议之效力、解释、变更、执行与争议解决均适用中华人民共和国法律。因本协议产生的争议,均应依照中华人民共和国法律予以处理,并由上海市静安区有管辖权的人民法院管辖。 - - diff --git a/docusaurus.config.js b/docusaurus.config.js index 6d63bd5b8..85d95cfef 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,153 +1,173 @@ // @ts-check -const path = require("path"); const PREVIEW = process.env.PREVIEW ?? "false"; /** @type {import('@docusaurus/types').Config} */ const config = { - title: "TapTap 开发者文档", - url: "https://developer.taptap.cn", - baseUrl: PREVIEW === "true" ? "/" : "/docs/", - onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", - favicon: "img/logoh.png", - trailingSlash: true, - customFields: { - searchUrl: "https://tds-doc-search-api.leanapp.cn/search", - upItemListIndexUrl: "https://tds-doc-search-check-log.leanapp.cn/api/check-log-up", - aiSearchUrl: "https://tds-doc-search-ai-api.ap-sg.tdsapps.com/api/ai-search?type=TDS", - aiSearchEnUrl: "https://tds-doc-search-ai-api.ap-sg.tdsapps.com/api/ai-search?type=TDSen", - searchProviderName: "LeanDB Elasticsearch", - searchProviderWebsite: - "https://developer.taptap.cn/docs/sdk/engine/database/es/", - mainDomainHost: "https://developer.taptap.cn/", - dcDomainHost: "https://developer.taptap.cn?from=tds-docs", + title: "LeanCloud 开发者文档", + url: "https://docs.leancloud.cn", + baseUrl: "/", + onBrokenLinks: "throw", + onBrokenMarkdownLinks: "warn", + favicon: "img/lc-favicon.ico", + trailingSlash: true, + customFields: { + searchUrl: "https://lc-doc-search-api.leanapp.cn/search", + upItemListIndexUrl: "https://lc-doc-search-check-log.leanapp.cn/api/check-log-up", + aiSearchUrl :"https://tds-doc-search-ai-api.ap-sg.tdsapps.com/api/ai-search?type=LC", + aiSearchEnUrl :"https://tds-doc-search-ai-api.ap-sg.tdsapps.com/api/ai-search?type=LCen", + searchProviderName: "LeanDB Elasticsearch", + searchProviderWebsite: "https://docs.leancloud.cn/sdk/engine/database/es/", + mainDomainHost: "https://www.leancloud.cn", + dcDomainHost: "https://www.leancloud.cn", + }, + + i18n: { + localeConfigs: { + en: { + label: "English", + }, + "zh-Hans": { + label: "简体中文", + }, }, + defaultLocale: "zh-Hans", + locales: ["zh-Hans", "en"], + }, - i18n: { - localeConfigs: { - en: { - label: "English", - }, - "zh-Hans": { - label: "简体中文", + presets: [ + [ + "classic", + /** @type {import('@docusaurus/preset-classic').Options} */ + ({ + docs: { + sidebarPath: require.resolve("./sidebars.js"), + routeBasePath: "/", + lastVersion: "current", + versions: { + current: { + label: "v3", }, + }, }, - defaultLocale: "zh-Hans", - locales: ["zh-Hans", "en"], - }, - - presets: [ - [ - "classic", - /** @type {import('@docusaurus/preset-classic').Options} */ - ({ - docs: { - sidebarPath: require.resolve("./sidebars.js"), - routeBasePath: "/", - lastVersion: "current", - versions: { - 'v4': { - label: 'v4', - path: 'v4', - banner: "unreleased", - }, - current: { - label: "v3", - }, - 'v2': { - label: 'v2', - path: 'v2', - banner: 'unmaintained', - }, - }, - }, - theme: { - customCss: require.resolve("./src/styles/index.scss"), - }, - googleAnalytics: { - trackingID: "UA-73963350-1", - }, - }), - ], + theme: { + customCss: require.resolve("./src/styles/index.scss"), + }, + googleAnalytics: { + trackingID: "UA-73963350-1", + }, + }), ], + ], - themeConfig: - /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ - ({ - navbar: { - items: [ - { - label: "文档首页", - to: "/", - position: "right", - activeBaseRegex: `^${PREVIEW === "true" ? "/" : "/docs/" - }(en/)?(?!.+)`, - }, - { - label: "游戏商店", - to: "store", - position: "right", - }, - { - label: "游戏服务", - to: "sdk", - position: "right", - }, - { - label: "SDK API", - to: "/sdk-api", - position: "right", - }, - { - label: "下载", - position: "right", - items: [ - { - label: "设计资源", - to: "/design", - }, - { - label: "SDK 工具包", - to: "/tap-download", - }, - { - label: "Demos", - to: "/demos", - }, - ], - }, - { - type: "docsVersionDropdown", - position: "right", - }, - { - type: "localeDropdown", - position: "right", - }, - ], - }, - prism: { - theme: require("./src/theme/prism-taptap"), - additionalLanguages: ["csharp", "java", "php", "groovy", "swift", "dart", "kotlin", "json"], - }, - image: "/img/logo.svg", - metadata: [ - { - name: "keywords", - content: "taptap tds 开发者 文档", - }, + themeConfig: + /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ + ({ + navbar: { + items: [ + { + label: "文档首页", + to: "/", + position: "right", + activeBaseRegex: "^/(?!.+)", + }, + { + label: 'API 文档', + position: 'right', + items: [ + { + label: 'Android/Java SDK API', + href: 'https://leancloud.github.io/java-unified-sdk/', + }, + { + label: 'Objective-C SDK API', + href: 'https://leancloud.github.io/objc-sdk/', + }, + { + label: 'Swfit SDK API', + href: 'https://leancloud.github.io/swift-sdk/', + }, + { + label: 'Flutter 数据存储 SDK API', + href: 'https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/leancloud_storage-library.html', + }, + { + label: 'Flutter 即时通讯 SDK API', + href: 'https://pub.dev/documentation/leancloud_official_plugin/latest/leancloud_plugin/leancloud_plugin-library.html', + }, + { + label: 'JavaScript 数据存储 SDK API', + href: 'https://leancloud.github.io/javascript-sdk/docs/', + }, + { + label: 'JavaScript 即时通讯 SDK API', + href: 'https://leancloud.github.io/js-realtime-sdk/docs/', + }, + { + label: 'JavaScript 多人在线对战 SDK API', + href: 'https://leancloud.github.io/Play-SDK-JS/doc/global.html', + }, + { + label: 'Python SDK API', + href: 'https://leancloud.github.io/python-sdk/', + }, + { + label: 'PHP SDK API', + href: 'https://leancloud.github.io/php-sdk/', + }, + { + label: 'Go SDK API', + href: 'https://pkg.go.dev/github.com/leancloud/go-sdk/leancloud', + }, + { + label: '.NET SDK API', + href: 'https://leancloud.github.io/csharp-sdk/html/', + } ], - colorMode: { - defaultMode: "light", - disableSwitch: true, - }, - }), + }, + { + label: '资源', + position: 'right', + items: [ + { + label: 'SDK', + href: '/sdk/sdk-page/', + }, + { + label: 'Demo', + to: '/demo', + } + ], + }, + { + label: "云课堂", + to: "/classroom", + position: "right", + }, + { + type: "localeDropdown", + position: "right", + } + ], + }, + prism: { + theme: require("./src/theme/prism-taptap"), + additionalLanguages: ["csharp", "java", "php", "groovy", "swift", "dart"], + }, + image: "/img/logo.svg", + metadata: [ + { + name: "keywords", + content: "leancloud 开发者 文档", + }, + ], + colorMode: { + defaultMode: "light", + disableSwitch: true, + }, + }), - plugins: [ - "docusaurus-plugin-sass", - path.resolve(__dirname, "./plugins/npsmeter"), - ], + plugins: ["docusaurus-plugin-sass"], }; module.exports = config; diff --git a/i18n/en/docusaurus-plugin-content-docs/current.json b/i18n/en/docusaurus-plugin-content-docs/current.json index 0519270d3..d05e6f4a6 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current.json +++ b/i18n/en/docusaurus-plugin-content-docs/current.json @@ -34,7 +34,7 @@ "description": "The label for category 内嵌动态 in sidebar sdk" }, "sidebar.sdk.category.内建账户": { - "message": "TDS Authentication", + "message": "Authentication", "description": "The label for category 内建账户 in sidebar sdk" }, "sidebar.sdk.category.游戏好友": { diff --git a/i18n/en/docusaurus-plugin-content-docs/current/aws-partner.mdx b/i18n/en/docusaurus-plugin-content-docs/current/aws-partner.mdx deleted file mode 100644 index e8e4d7fd2..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/aws-partner.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: TapTap is an AWS software partner for global game developers -slug: /aws-partner/ ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; - -TapTap—China's active mobile game community—has officially become Amazon's software partner. The two parties will aggregate each other's technology and publishing capabilities, starting from 0, to help developers going overseas to solve the problem of early going overseas, reduce developers costs, and provide overseas games more comprehensive support. - -TapTap is a zero-commission app store that lets gamers download gaming applications onto Android mobile phone while content providers are able to take 100% revenue. Meanwhile, TapTap has a robust community of developers and gamers that safely use the application. Since TapTap includes a community of developers that can add their apps to the platform, multiple versions of the same game can be found like TapTap PUBG. - -

    - TapTap -

    - -TapTap is trying to remodel the 30% revenue cut taken by Apple’s App Store and Google Play — announcing a 0% revenue share with game developers. After two years of exploration and development, TapTap International has users in more than 170 countries (regions). In April this year, the new version of TapTap International was launched, serving overseas players with a new look and exciting functions. TapTap Developer Service (TDS) is also gradually opening more functions in the international version to provide support for overseas developers and domestic overseas manufacturers. - -

    - AWS Partner Network -

    - -AWS helps TapTap International Edition to provide Amazon Web Services Cloudfront, Amazon EC2, Amazon RDS, and other services, and the global cloud infrastructure provides support for TapTap International Edition. As the pioneer and leader of cloud computing, AWS provides more than 200 full-featured cloud computing services around the world. AWS provide almost unlimited scalability, so we can scale our application automatically as we continue to grow and add new customers. - -Through cooperation with AWS, Taptap has upgraded the underlying architecture, improved the security of the architecture, expanded international delivery capabilities, and improved the experience of gamers and developers. - -

    - -

    - -Up to now, a number of games such as "Sausage Man" and "Flash Party" have completed the whole process of publishing, testing and launching through the TapTap international version. In addition, TapTap's unique community system also brings novel and effective operation methods for the game's overseas platform distribution. TapTap International's "Sausage Man" launched in July 2021. Its game forum has gathered more than 1.4 million players and published nearly 3,000 discussion posts. Developers hold various activities in the community to enhance user stickiness and retention. - -Benefiting from TapTap's zero-commission business model, well-known game developers such as Epic Games have also partnered with TapTap. The worldwide popular Fortnite is now officially exclusive to TapTap, offering download, installation and forum services. Currently, Fortnite has recorded over 10 million downloads on TapTap. - -

    - Sausage Man -

    - -According to XD’s financial report in 2021, the MAU of the TapTap international version has grown to 12.24 million, and it still maintains a high growth rate. Based on tens of millions of MAUs, in addition to the basic distribution capabilities, TapTap has the ability to provide more platform-based one-stop services for domestic developers going overseas. For example, "bonfire testing" or "beta testing" can help developers acquire seed users and provide a sufficient and diverse population for each level of testing. Users help developers tune the game through the mechanism of community feedback and grow in testing. For global testing, developers generally cannot easily open Google and iOS stores in key countries such as the United States, Germany and France during the testing period. Only some small-volume countries such as the Philippines, Canada, etc. can be opened for testing. With the help of TapTap, we can introduce more users from core countries, especially users in Tier-1 countries. The number of users in TapTap international is quite stable, which help developer solve the problem of user volume during the test period. In global testing process, advertising purchases are often limited by the insufficient cold start speed and the small purchase area. It is difficult to import sufficient number of users on the test day or the next day. - -
    - PUBG: NEW STATE -
    - PUBG: NEW STATE open second alpha test TapTap International -
    -
    - -TapTap Developer Service (TDS) is releasing its service capabilities for TapTap international developers after repeated verifications in China. Taking the first batch of “Flash Party” that received a version number this year as an example, the game launched on “TapTap International” in February 2022, and access the TapTap Developer Service (TDS) Tap Login, friend system, embedded community and other functions open up the social system for "Flash Party", which is convenient for players to exchange games and view strategies. At the same time, various operational activities can directly reach players in the game, improving player retention and duration. - -
    - Flash Party -
    Flash Party Moments
    -
    diff --git a/i18n/en/docusaurus-plugin-content-docs/current/design/design-login.mdx b/i18n/en/docusaurus-plugin-content-docs/current/design/design-login.mdx deleted file mode 100644 index a5631065f..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/design/design-login.mdx +++ /dev/null @@ -1,217 +0,0 @@ ---- -title: TapTap Login Button Design Guideline -sidebar_label: TapTap Login Button Design Guideline -sidebar_position: 1 -slug: /design ---- - -import { Background, Figure } from "/src/docComponents/doc"; -import useBaseUrl from "@docusaurus/useBaseUrl"; - -## Types of Login Buttons - - -
    -
    - - -## Icon - -### Composition of information - -The icon is consisted of the TapTap logo and a background. - -### Button Shape - -The default shape of the button is circle. It can be changed to a square with rounded corners to match the appearance of other in-game buttons. - - -
    -
    - - -### Button size - -The default size is 40*40 pt. Please make sure that the aspect ratio of the icon is always 1:1. - -### Button margin - -The materials in the downloadable resource packs already contain the correct amounts of padding. Please do not cut them out or add any extra padding. - -## Pill - -### Composition of information - -The icon is consisted of the TapTap logo, text, and a background. - - -
    -
    - - -### Rounded corners - -Defaults to the maximum radius (pill). The radius can be adjusted to match the appearance of other buttons in the game interface. - - -
    -
    - - - -### Button size - -The default height is 40pt and the button width varies depending on the length of text in different languages. - -### Button text - -The recommended text is "Log in with TapTap". There should be no line breaks or spaces within "TapTap". - - -
    - The TapTap logo and text - - } - imgSrc={useBaseUrl("https://capacity-files.lcfile.com/gA4wwKXJqAqkpAusEh9zU6BaxpXjBXM5/io-design-2.2.1.png")} - imgAlt="" - /> -
    - - - -## Colour rules for login portal - -Depending on the background color of the game interface, you can choose from one of the two default colors: black and the brand color (green). - -### Black - -Use this style on a light background that has enough contrast to allow the text to appear in white if you are using a black background for the buttons. - - -
    -
    - - -### Brand color (green) - -Use this style on a dark background with sufficient contrast, with black text if using the brand color as the button's background. It is recommended to add a black circular background image under the TapTap logo to enhance the overall look. - - -
    -
    - - -### Rules for customization - -As the TapTap Login SDK can be used in a variety of mobile games, game developers may make minor adjustments to the TapTap logo, but must maintain the brand identity of the TapTap logo. There are no restrictions on borders, backgrounds, or button text, but changes to the style and color of the TapTap logo are not allowed. - - -
    -
    -
    - - -## Rules for the arrangement of multiple login options - -The size and style of the login button's background can be adjusted to match the overall style of the buttons in the game by changing the background of the buttons to any shape, but it must be consistent with the style of the other login buttons. - - -
    -
    - \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/design/design-moment.mdx b/i18n/en/docusaurus-plugin-content-docs/current/design/design-moment.mdx deleted file mode 100644 index 044f7c71c..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/design/design-moment.mdx +++ /dev/null @@ -1,270 +0,0 @@ ---- -title: Embedded Moments Design Guideline -sidebar_position: 2 ---- - -import { Background, Figure } from "/src/docComponents/doc"; -import useBaseUrl from "@docusaurus/useBaseUrl"; - -## Navbar Labels - -### Color Schemes and Applications - -Navbar labels are the textual elements used for menu items. They can be in both light and dark colors. - - -
    -
    - - - -
    -
    - - - -
    -
    - - -### Contrast Is Important - -The more contrast you set between the text and the background, the more legible the text will be. If you pick a light background for your page, you should set your text in a dark color, and vice versa. - - -
    -
    - - - -
    -
    - - -## Backgrounds - -### Background Sizes - -Embedded Moments can be displayed in both landscape and portrait modes. This means that you need to provide two background images for your game, one for each orientation. - - -
    - - -### Cropping Backgrounds - -When Embedded Moments is opened on a device with a short screen, the background image will be cropped to fit the screen. - - -
    - - -### Designing Background Images - -#### Style - -The background image shall not be too prominent that it takes the user’s attention away from the main content. Therefore, we suggest that you add simple patterns on the background and keep the contrast within the background to a minimum. - - -
    - A background that doesn’t catch the user’s attention -
    - Fewer colors -
    - Low contrast between the foreground and the background - - } - imgSrc={useBaseUrl("/img/io/design-moment/2.3.1.1.png")} - imgAlt="" - /> -
    - A background that catches the user’s attention -
    - Too many colors -
    - Too much contrast between the foreground and the background - - } - imgSrc={useBaseUrl("/img/io/design-moment/2.3.1.2.png")} - imgAlt="" - /> - - -#### What to Place in the Background - -You may add patterns or illustrations to the background as long as they don’t get too much attention from the user. - - -
    -
    - - - -
    -
    - - The contrast is too strong -
    - The illustration is too complex - - } - imgSrc={useBaseUrl("/img/io/design-moment/2.3.2.2.2.png")} - imgAlt="" - /> - - - -
    -
    - - Cluttered decorations -
    - Decorations occupy too much space - - } - imgSrc={useBaseUrl("/img/io/design-moment/2.3.2.3.2.png")} - imgAlt="" - /> - - -#### Safe Zones - -To ensure that the entirety of the illustrations in the background can be seen by the user, please keep the illustrations within the safe zones defined below. - - -
    -
    - - - -
    -
    - - - -
    -
    - - -#### Background Color of the Sticky Tab List - -You can set a background color for the sticky tab list. The tab list will fit well with the rest of the UI if you pick the color from the top area of the background image. - - -
    -
    - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/_category_.json deleted file mode 100644 index 236d14f6a..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "成就系统", - "collapsed": true, - "position": 7 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/bestpractice.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/bestpractice.mdx deleted file mode 100644 index 92bc6d7e5..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/bestpractice.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: 《泰拉瑞亚》 使用 Tap 成就来吸引更多玩家 -sidebar_label: 最佳实践 -sidebar_position: 3 ---- - -《泰拉瑞亚》是一个跨越手机、PC、主机平台的常青树游戏,在全球都有海量的忠实玩家。在中国大陆,中国港澳台发行的手机版《泰拉瑞亚》,目前在 TapTap 上销量也超过了 200 万份,收获了 9.3 的 Tap 评分。 - - -## 支持跨平台的 TDS 成就 -由于泰拉瑞亚可以在多个平台和渠道发行,他们使用了不受平台和引擎的限制的 TDS 的成就系统,不论游戏发布在 iOS AppStore、Android 各大渠道、PC、甚至主机平台,都能帮助游戏实现跨平台的成就系统。 -截止到 2022 年 5 月底,游戏总计触发成就人数超过 200 万人,解锁白金成就(即获得了全成就)的玩家超过 3500 人,游戏社区中也有不少玩家晒自己的成就进度、讨论成就的具体达成方法。 - -![img](https://capacity-files.lcfile.com/zqc1dU4x4cV06hcCok4CwCxFWoowsf4v/achievement_show_on_taptap.png) - - -我们也即将在未来几个月在 TapTap 客户端内增加更多成就系统相关的功能和露出,包括在动态显示好友获得的成就、对比成就、成就专题页等,为喜欢成就的玩家提供更多的实用和分享功能,也能帮助接入了成就的游戏进行更好的宣发和曝光。 - - -## 白金成就 - -我们会为那些获得 TapTap 玩家和 TapTap 编辑认可的游戏,提供白金奖杯,以此来激励玩家冲击游戏全成就,并且白金成就也可作为一种社交资产被玩家所拥有。 - -如果您的游戏符合至少以下条件,就可以在开发者中心后台申请开通白金成就,我们的编辑会来评估您的游戏是否可以开通白金: -- 游戏时长在同类游戏中处于正常水平 -- 免费游戏中,用户不需要付费,仍然可以获得全成就 -- 付费游戏中,用户不需要强制购买主线流程外的额外 DLC 或内购,仍然可以获得白金成就 - -![img](https://capacity-files.lcfile.com/3kBnhO30aI8ukszjt61L4527zHbyAaW5/baijin_achievement.png) - - -## 成就稀有度 -成就的稀有度在一定的冷启动数据后,会开始自动计算。稀有度可以反映这个成就的获得难度,对于稀有度极高的成就,玩家挑战完成会有强烈的成就感。 -![img](https://capacity-files.lcfile.com/C15DpYOKoz9DBFRcUmhy85GDX9Mv8PRT/achievement_rare.png) - - -## 开发者中心的成就配置 -成就后台的配置也较为简单,并且可以看到每一个成就的解锁人数、完成率等数据。 -![img](https://capacity-files.lcfile.com/dTh8PAoMPzbySH1Vw1D6XvF9gswNY6KK/achievement_list.png) - - -## 接入成就的方法 - -目前成就系统提供 SDK 上报成就与服务端上报成就 2 种方式。 - -《泰拉瑞亚》采用了服务端的方式接入了 TDS 成就系统,这样能够比较好的做好防作弊的处理,也能比较及时和稳定的做到成就的同步。 - -如果您的成就已经存储在服务端,那么我们优先建议您使用服务端上报的方式。 - -如果您的游戏是个单机游戏,没有服务端,或者没有存储过用户成就,那么也可以选择接入 TapSDK 来上报玩家的成就。当您设置到成就的触发点后,玩家联网时 SDK 会上报成就数据;若玩家在断网状态,SDK 也会在本地存储成就数据,待网络恢复后上报。 -在后台配置完成就,并确保您的游戏已经接入完成并且测试无误后,您就可以点击发布,将成就发布到 TapTap 上了。 - - -## 将玩家的成就展示在 TapTap - -如果希望在 TapTap 客户端内的成就列表中看到您的游戏,那就需要在游戏中接入 TapTap 登录。您可以把 TapTap 登录作为游戏的一种登录方式,可以 将 TapTap 登录作为游戏账号可以绑定的一个第三方账号,专门用于做成就的同步。 - - -## 立即开始使用 TDS 成就服务 - -如果希望了解如何接入TDS 成就系统,可以访问我们的 [成就产品指南](/sdk/achievement/features/) 和 [成就开发指南](/sdk/achievement/guide/)。整个系统的接入非常简单,有任何问题也欢迎通过 TapTap 开发者中心的工单系统来与我们取得联系。 \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/features.mdx deleted file mode 100644 index eb2357482..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/features.mdx +++ /dev/null @@ -1,241 +0,0 @@ ---- -title: Achievements Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -The Achievements service allows you to configure and release game achievements on the Developer Center. Players can unlock the achievements you configure as they play your game. Those who unlock all of your game’s achievements can also earn a Platinum Achievement. - - -## Our Benefits - -**For game developers**: -- Reduce development costs: Developers don’t have to build the low-level logic of an achievement system themselves. They can release an achievement system in their games by following a simple setup process. -- Increase customer lifecycle value: Improve player engagement by creating a user growth system and incentivizing players as they progress through your game. -- Enable data analysis: Developers can evaluate the difficulty of the games they create by looking at the data collected by the achievement system. - -**For players**: -- Enhance the game experience: Players will enjoy your game more when you add fun and challenging achievements to your game. -- Evoke emotions: By instilling a sense of honor and a desire to collect, players will be more likely to stick with your game. - - -## Glossary - -### Overview - -| Term | Definition | Rules | -| --- | --- | --- | -| Achievement ID | A unique identifier used to report the achievement progress to the SDK. You can customize the achievement ID of an achievement. | Can only contain letters and numbers and cannot be longer than 100 characters. | -| Name | A short name for the achievement, such as “Experienced Driver”. | Can be up to 80 characters long. | -| Description | A short description of the achievement used to tell players how to unlock it, such as “Avoid 10 obstacles in a row”. | Can be up to 400 characters long. | -| Icon | A square icon to illustrate the achievement. | Upload a 512×512 color image in PNG or JPG format. TDS will automatically generate a grayscale variant that will be displayed if the achievement hasn’t been unlocked yet. | -| Initial Status | The initial status of the achievement. Can be either hidden or displayed. | Cannot be changed once the achievement is released. | -| Hidden Achievement | Players cannot see the achievement details. | If selected, TDS will provide a special description and icon for the achievement if it hasn’t been unlocked yet. | -| Displayed Achievement | Players can see the achievement details. | This is the default option. If selected, the achievement description will be visible to everyone. | -| Category | The category of a normal achievement. | A normal achievement can only have one category. You cannot change the category of an achievement unless it is unreleased. | -| Main Achievements | One of the two categories of achievements, containing all the achievements for the initial release of the game. | In most cases, there should be at least 10 achievements for a game. The number of main achievements is fixed for the lifetime of a game. | -| DLC Achievements | One of the two categories of achievements that includes the achievements that come with the game’s DLCs. | There is no limit to the number of DLC achievements that can be created for a game. | - -### Accumulation Achievement - -Accumulation achievements allow the player to incrementally approach the goals for unlocking achievements over time. As the player works toward the goals, your game can continuously report the progress to TDS so that TDS can keep track of the progress and send notifications to your game when certain achievements are unlocked. Your game can then notify the player that they have unlocked the achievements. - -**Configuring steps**: When you create an accumulation achievement, you must specify the number of steps required to unlock the achievement (between 2 and 100,000,000). The achievement is unlocked for a player when they complete the steps required for the achievement, even if the achievement is hidden. TDS keeps track of the steps a player completes for each achievement, so you don’t have to. - -**Steps cannot be reset**: Steps for an accumulation achievement accumulate over time and cannot be deleted or reset in-game. An example of a proper accumulation achievement is “Win 20 games”, while “Win 5 games in a row” is not an accumulation achievement because the progress is reset when the player loses a game. Similarly, “Own 2,000 coins” cannot be an accumulation achievement because players can spend coins as well as earn them. You could, however, set up the latter two achievements as basic achievements and have them unlocked for the player as they complete them, but you’d have to keep track of the progress yourself. - -### Rarity - -This is the percentage of players who have unlocked the achievement. The lower the number, the rarer the achievement. - -Rarity = Number of players who have unlocked the achievement / Number of players who have initialized the achievement -- 50% ≤ Common ≤ 100% -- 10% ≤ Uncommon < 50% -- 1% ≤ Rare < 10% -- Ultra Rare < 1% - -The image below shows the different rarity levels: - -![img](https://capacity-files.lcfile.com/ekmGiWHqhUBcbWxlslbqmptgvUM0YUCh/achievement02.png) - -### Unlock Status - -The unlock status of an achievement is updated when the player reaches the goal you configured for the achievement. -- Locked: The default status of an achievement, meaning that the player hasn’t unlocked it yet. -- Unlocked: The player has already unlocked the achievement. There are two cases: - - Basic achievement: The status of the achievement becomes *Unlocked* when the player reaches the goal. - - Accumulation achievement: A percentage is displayed to indicate how many steps the player has completed. Once all the steps have been completed, the status of the achievement becomes *Unlocked*. - -The image below shows the different unlock statuses an achievement can have: - -![img](https://capacity-files.lcfile.com/Ln6jBCdAHYNMQUYuKgX3tM2HutRwshLk/achievement01.png) - -### Platinum Achievement - -For quality games, if a player unlocks all the **Main Achievements**, they will also unlock the **Platinum Achievement**. - -**Main Achievements** only include those that have been released. - -
    - -## Features - -### Create Basic Achievements - -To create an achievement for your game, go to **Developer Center > Game Services > Cloud Services > Achievements** and click on **Create**. - -After filling in all the information, click on **Save**. The status of the achievement will change to **Ready**. - -![img](https://capacity-files.lcfile.com/lGQMKMLDecXlBHk81mx1gLEnlpaW4Wcn/io-achievement04.png) - -### Edit Basic Achievements - -To edit an existing achievement before releasing all of your achievements, click on **Edit**. You will see an interface similar to the one used to create an achievement. Here you can edit the details of the achievement. - -Once achievements are released, each achievement’s **Achievement ID**, **Accumulation Achievement**, **Category**, and **Initial Status** settings cannot be changed anymore. -![img](https://capacity-files.lcfile.com/OnBx19wreaA077dklPHSToigELWqo8p7/io-achievement05.png) - -### Release Basic Achievements - -Your achievements will remain in **Ready** status while you are still editing them. Once you have at least **5** normal achievements and have tested them all, you can release them by clicking on **Release Normal Achievement**. Please be careful as this will release all achievements to the production environment. - -![img](https://capacity-files.lcfile.com/MbgyWimo7v1WShObhuSSMLKpgTffBVvE/io-achievement06.png) - -### Delete/Reset Achievements - -Before releasing achievements, you can **delete** or **reset the progress** of an achievement using the buttons at the end of the achievement entry. - -Once achievements are released, **they cannot be deleted or reset**. - -![img](https://capacity-files.lcfile.com/O1Uo3NM5sVsVbwJ6yN1lc7zoDkDQUE7q/io-achievement07.png) - -### Apply for a Platinum Achievement - -To ensure the quality of Platinum Achievements, our reviewers will evaluate the quality of your game when you apply for a Platinum Achievement for it. Your game will only qualify for a Platinum Achievement if our reviewers determine that your game is of above-average quality. - -![img](https://capacity-files.lcfile.com/28Lyi8vJSvNo9qYmuE4dlopxMV3wA0zF/io-achievement08.png) - -### Create a Platinum Achievement - -Once your game qualifies for a Platinum Achievement, you can click on **Create** to create a Platinum Achievement. A Platinum Achievement requires an **icon**, **name**, and **description**. - -![img](https://capacity-files.lcfile.com/IPjkqIE5KwHAMzL8i3aof2zG4oN39VKX/io-achievement09.png) - -### Release a Platinum Achievement -To ensure that your game’s Platinum Achievement is challenging enough, you will not be able to release the Platinum Achievement until you have at least **10 Main Achievements**. -Keep in mind that once the Platinum Achievement is released, **you will no longer be able to create Main Achievements for your game**. You will only be able to create DLC Achievements. - -
    - -### Languages - -To set up multiple languages, go to **Developer Center > Game Services > Configuration** and click on **Languages**. - -![](https://capacity-files.lcfile.com/hNyOSJxHfooDgI2iUQwWSFnJx5pNyFpJ/io-achievement10.png) - -In the pop-up window, select the languages you want to use for the achievements in your game, and then click on **Confirm**. You can remove any added languages from the list on the right. - -![](https://capacity-files.lcfile.com/ChpxOosYo5aNbT5Kg7EDectggvNOcdqb/io-achievement11.png) - -Once you have completed the above steps, you can click on the **+** icon in the achievement editing window and select the languages you have added. You will then be able to set up different languages for the **Name** and **Description**. The icon and the other configurations will be the same for all the languages and you won’t need to fill them in again. - -![](https://capacity-files.lcfile.com/Q1g0pXpR8OTGMlexzizV5IT8WEyOSVF8/io-achievement12.png) - -
    - -### Displaying Achievements in Game - -- TapSDK: Using the user interface provided by the TapSDK, players can view their unlocked and locked achievements by tapping a button provided by your game. -- API: You can also choose to implement your own user interface and access the achievement data through our API. - -![img](https://capacity-files.lcfile.com/P7pD6xiO2KlkAxXUFzvwEA5zry2tVa23/achievement13.png) - -### In-Game Toasts - -- When the player performs an action that unlocks one or more achievements, a toast will appear at the top area of the screen. Only one achievement can be displayed at a time. Additional achievements will be queued and displayed one at a time. -- You can choose whether to show or hide the achievements page and the toasts. - -![img](https://capacity-files.lcfile.com/DdEv3pd3kitPheTXD47vMhTjF1LpHv5r/achievement14.png) - -![img](https://capacity-files.lcfile.com/DSXcQaiQirx31spBfuQvsjvzQxE2Nyok/achievement15.png) - -### Displaying Achievements on TapTap -- If you use TapTap Login (with game released on TapTap): Players logged in with TapTap Login can view their unlocked and locked achievements under “TapTap app > Profile > About”. -- If you don’t use TapTap Login: If your game has its own account system, you’ll need to create a feature that links a player’s account to their TapTap account and syncs their achievements to their TapTap account. -- At this time, only games released in the Mainland China region can have achievements displayed on TapTap. - -:::tip -**Two scenarios for linking TapTap accounts** -- Scenario 1: The user has an account created without TapTap Login (Account A) and a TapTap account that hasn’t been used to log in to the game. Now the game would be able to link the two accounts. Both accounts have the same TDS user ID and the TapTap account would display Account A’s achievements. -- Scenario 2: The user has an account created without TapTap Login (Account A) and a TapTap account that has already been used to log in to the game (Account B). Currently, the TapTap account shows the achievements of Account B. In order for the TapTap account to show the achievements of Account A, your game must provide a feature that allows the user to unlink Account B from the TapTap account and then link Account A to the TapTap account. -::: - -![img](https://capacity-files.lcfile.com/RxtLqJKJPAbHRz5qNJ3bD0wUx0UF4MY3/achievement_show_on_taptap1.png) - - -## Integrating the Service - -### Getting Started - -1. Become a TapTap developer; -2. Create your game in the TapTap Developer Center and enable “TDS Authentication” for the game; -3. Request to enable the Achievements service by submitting a ticket. To allow sub-accounts to have access to the service, go to “Manage Permissions” and give the “Game Administrator” permission to the accounts; -4. Download the TapSDK (v3.2.0 or higher) and integrate it into the game. - -### Workflow - -![img](https://capacity-files.lcfile.com/exAftqivPqahCSJlTjBYNR3vnkjXmYWc/achievement16.png) - -### Developer Guide - -See **[Achievements > Guide](/sdk/achievement/guide/)**. - -### Testing - -1. After you have added all the achievements for your game, you can set some users as **testers** for the **production environment**; -2. The testers will be able to test the achievements with the **Ready** status; -3. You can [reset the achievement data](#deletereset-achievements) at any time during testing; -4. If the achievement data is not reset before you release your game, the data will be transferred to the production environment and mixed with the data generated by other users. It’s up to you if you want to keep the data after testing. - -:::note -To add testers and have them test the achievements with the **Ready** status, please contact the TDS technical support team by opening a ticket on the Developer Center. In the ticket, please provide the `Client ID` of the application and the `Object ID` returned after the test account implements TapTap Login. -::: - - -## FAQ - -#### Can I still apply for a Platinum Achievement after releasing my game? - -Yes. You can apply for a Platinum Achievement either before or after you release your game. - -#### Can I release new achievements while my Platinum Achievement application is being reviewed? - -Yes. Applying for and creating a Platinum Achievement doesn’t prevent you from releasing other achievements. - -#### If a player has already earned all the achievements before my Platinum Achievement application is approved, will they still receive the Platinum Achievement? - -Yes. The player will automatically receive the Platinum Achievement. - -#### If my game already uses its own account system or third-party login, how can I integrate TDS Authentication into my game? - -For existing users, you can let them log in with their existing accounts and then invoke the TapSDK’s account linking interface to link those accounts to TapTap. The players will then have their IDs created with TDS Authentication. The TapSDK will then be able to determine the identity of each player using the TDS User IDs. For new users, you can just let them log in with TapTap without going through these steps. - -#### If I integrate the Achievements service into my existing game, will players still get the achievements they’re supposed to get? - -There are two ways to award achievements to users of your existing game: -- Sync the expected achievement data with the Achievements service using our API. -- If your game already tracks the achievements your players have earned, you can convert those achievements to the appropriate Achievement IDs and submit them all at once with the SDK when it’s convenient (you may want to temporarily turn off toasts so the players don’t see them). - -#### If a player already has the Platinum Achievement and I add a new achievement, will the player lose the Platinum Achievement? - -No. The Platinum Achievement is only dependent on main achievements. The achievements you create after the Platinum Achievement will be DLC achievements, which won’t affect the Platinum Achievement. - -#### If a player can create multiple profiles in my game and earn achievements multiple times, what would happen when I submit the same achievements to the SDK? - -Each TapTap account can only receive each achievement once, so it won’t be possible for a player to receive the same achievement multiple times. - -#### I’m getting `Empty sign or session` when invoking the interface for initializing the achievement data. What could be causing this? - -As the Achievements service is based on TDS Authentication (`TDSUser`), please make sure that you have integrated TDS Authentication into your game and instantiated the `TDSUser` object before initializing the achievements data (`[TapAchievement initData];`). If `TDSUser` is empty, you will see `Empty sign or session`. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/guide.mdx deleted file mode 100644 index da6cf7760..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/achievement/guide.mdx +++ /dev/null @@ -1,713 +0,0 @@ ---- -title: Achievements Guide -sidebar_label: Guide -sidebar_position: 2 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import Languages from '../_partials/languages.mdx'; - -This page shows you how to integrate the Achievements service into your game. The Achievements service is based on TDS Authentication (TDSUser). You can read more about this in **[TDS Authentication > Guide](/sdk/authentication/guide/)**. - -## Install the SDK - -Please make sure that you have already [created the game](/sdk/start/quickstart/#creating-a-game), [configured the project](/sdk/start/quickstart/#project-configuration), and [initialized the project](/sdk/start/quickstart/#initialization) according to the [TapSDK Quickstart](/sdk/start/quickstart/). Once you have completed these steps, you can proceed to add the `TapAchievement` module from the TapSDK downloaded from the [Downloads](/tap-download) page: - - - - -{`"dependencies":{ - ... - // Achievements service - "com.taptap.tds.achievement": "https://github.com/TapTap/TapAchievement-Unity.git#${sdkVersions.taptap.unity}", -}`} - - - -{`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapAchievement_${sdkVersions.taptap.android}', ext:'aar') // TapTap Achievements service -}`} - - - -{`// Achievements service -TapAchievementResource.bundle -TapAchievementSDK.framework`} - - - - -## Register Callbacks -The Achievements SDK has several callbacks that are triggered when the data is initialized, when the data cannot be initialized, and when there is a progress update. The data initialization callback is particularly important as it is required for the SDK to work properly. If the data cannot be initialized, please either prompt the user or try to initialize the data again at another time. - - - -<> - -**Prerequisite** - -TapTap.Achievement depends on the `TapTap.Bootstrap` library. - -**Namespace** - -```cs -using TapTap.Achievement; -``` - -To register callbacks: - -```cs -TapAchievement.RegisterCallback(IAchievementCallback callback); - -private class AchievementCallback:IAchievementCallback -{ - public void OnAchievementSDKInitSuccess() - { - // The Achievement SDK is initialized - } - - public void OnAchievementSDKInitFail(TapError errorCode) - { - if (errorCode != null) - { - // Failed to initialize the SDK - } - } - - public void OnAchievementStatusUpdate(TapAchievementBean bean, TapError errorCode) - { - if (errorCode != null) - { - // Failed to update achievements - return; - } - - if (bean != null) - { - // Achievements updated - } - } -} -``` - - -<> - -```java -TapAchievement.registerCallback(new AchievementCallback() { - @Override - public void onAchievementSDKInitSuccess() { - // Data loaded - } - - @Override - public void onAchievementSDKInitFail(AchievementException exception) { - // Failed to load data; please try again - } - - @Override - public void onAchievementStatusUpdate(TapAchievementBean item, AchievementException exception) { - if (exception != null) { - // Failed to update achievements - return; - } - if (item != null) { - // item updated - } - } -}); -``` - - -<> - - -```objectivec -[TapAchievement registerCallBack:self]; - -- (void)onAchievementSDKInitSuccess { - // Data loaded -} - -// Failed to initialize the SDK -- (void)onAchievementSDKInitFail:(nullable NSError *)error { - // Failed to load data; please try again -} - -// Achievements updated -- (void)onAchievementStatusUpdate:(nullable TapAchievementModel *)achievement failure:(nullable NSError *)error { - if (error) { - // Failed to update achievements - } else { - // achievement updated - } -} -``` - - - - - -## Initialize the Data -Since the Achievements service keeps track of the user’s achievement data locally, please make sure to **initialize the data when the user is logged in**. If the user switches to a different account, be sure to call the interface for initializing the data again. Failure to do so may result in the achievement data for different accounts being mixed up. - -This step is asynchronous, which means that you should not proceed until you have received a successful callback. - - - -```cs -TapAchievement.InitData(); -``` - -```java -TapAchievement.initData(); -``` - -```objectivec -[TapAchievement initData]; -``` - - - -## Get All Achievements -You can get all achievements from either the local cache or the server. The local cache is refreshed when you successfully invoke the data initialization interface. It is also refreshed when you proactively invoke the data fetching interface. - -If you need to fetch the latest data from the server while the player is playing, you can use the data fetching interface. Most of the time you just need to load the data from the local cache. - - - -```cs -// Get local data -TapAchievement.GetLocalAllAchievementList((list, code) => -{ - if (code != null) - { - // Failed to get all achievements - } - else - { - // Got all achievements successfully - }); -} -// Fetch data from the server -TapAchievement.FetchAllAchievementList((list, code) => -{ - if (code != null) - { - // Failed to fetch all achievements - } - else - { - // Fetched all achievements successfully - }); -} -``` - -```java -// Local data -List allList = TapAchievement.getLocalAllAchievementList(); - -// Server data -TapAchievement.fetchAllAchievementList(new GetAchievementListCallBack() { - @Override - public void onGetAchievementList(List achievementList, AchievementException exception) { - if (exception != null) { - switch (exception.errorCode) { - case AchievementException.SDK_NOT_INIT: - // The SDK has not initialized the data yet - break; - default: - // Failed to fetch data - } - } else { - // Fetched data successfully - } - } -}); -``` - -```objectivec -// Local data - NSArray *allList = [TapAchievement getLocalAllAchievementList]; - - // Server data - [TapAchievement fetchAllAchievementList:^(NSArray *_Nullable result, NSError *_Nullable error) { - if (error) { - switch (error.code) { - case 9001: - // The SDK has not initialized the data yet - break; - default: - // Failed to fetch data - break; - } - } else { - // Fetched data successfully - } - }]; -``` - - - -## Get Current User’s Achievements -You can get the current user’s achievements from either the local cache or the server. The local cache is merged with the server data when you successfully invoke the data initialization interface (if different steps are tracked for the same achievement in the server and the local cache, the larger one is kept). They are also merged when you proactively invoke the data fetching interface. - -The local data is often more accurate than the server data because the server data may be out of date due to previous synchronization failures. - - -```cs -// Get local data -TapAchievement.GetLocalUserAchievementList((list, code) => -{ - if (code != null) - { - // Failed to get user’s achievements - } - else - { - // Got user’s achievements successfully - }); -} -// Fetch data from the server -TapAchievement.FetchUserAchievementList((list, code) => -{ - if (code != null) - { - // Failed to fetch user’s achievements - } - else - { - // Fetched user’s achievements successfully - }); -} -``` - -```java -// Local data -List userList = TapAchievement.getLocalUserAchievementList(); - -// Server data -TapAchievement.fetchUserAchievementList(new GetAchievementListCallBack() { - @Override - public void onGetAchievementList(List achievementList, AchievementException exception) { - if (exception != null) { - switch (exception.errorCode) { - case AchievementException.SDK_NOT_INIT: - // The SDK has not initialized the data yet - break; - default: - // Failed to fetch data - } - } else { - // Fetched data successfully - } - } -}); -``` - -```objectivec -// Local data - NSArray *userList = [TapAchievement getLocalUserAchievementList]; - -// Server data -[TapAchievement fetchUserAchievementList:^(NSArray *_Nullable result, NSError *_Nullable error) { - if (error) { - switch (error.code) { - case 9001: - // The SDK has not initialized the data yet - break; - default: - // Failed to fetch data - break; - } - } else { - // Fetched data successfully - } - }]; -``` - - - -## Get an Achievement (Directly) - - - -```cs -// displayID is the Achievement ID you configured on the Developer Center -TapAchievement.Reach("displayID"); -``` - -```java -// displayID is the Achievement ID you configured on the Developer Center -TapAchievement.reach("displayID"); -``` - -```objectivec -// displayID is the Achievement ID you configured on the Developer Center -[TapAchievement reach:@"displayId"]; -``` - - - -## Add Steps to an Accumulation Achievement -There are two ways to add steps to an accumulation achievement. The first is to call `growSteps` with the number of steps to add (for example, enter `5` to add `5` steps). The second is to use `makeSteps` with the total number of steps (for example, enter `100` to set the total number of steps to `100`). Internally, the SDK would calculate the total number of steps when you call `growSteps`. - - - -```cs -// displayID is the Achievement ID you configured on the Developer Center -TapAchievement.GrowSteps("displayID", step); -TapAchievement.MakeSteps("displayID", step); -``` - -```java -// displayID is the Achievement ID you configured on the Developer Center -TapAchievement.growSteps("displayID", 5); -TapAchievement.makeSteps("displayID", 100); -``` - -```objectivec -// displayID is the Achievement ID you configured on the Developer Center -[TapAchievement growSteps:@"displayID" numSteps:5]; -[TapAchievement makeSteps:@"displayID" numSteps:100]; -``` - - - -## Set Toasts -By default, the SDK automatically displays a toast when the player gets an achievement. You can turn off toasts using the following interface: - - - -```cs -TapAchievement.SetShowToast(bool isShow); -``` - -```java -TapAchievement.setShowToast(false); -``` - -```objectivec -[TapAchievement setShowToast:NO]; -``` - - - -## Show Achievements -The SDK comes with a page that shows all the achievements the player has gotten: - - - -```cs -TapAchievement.ShowAchievementPage(); -``` - -```java -TapAchievement.showAchievementPage(); -``` - -```objectivec -[TapAchievement showAchievementPage]; -``` - - - -## Interpreting Achievement Data - - - -```cs -public string displayId; // Achievement ID -public int visible = VisibleFalse; // Whether the achievement is hidden -public string title; // Name -public string subTitle; // Description -public string achieveIcon; // Icon -public int step; // Total steps -public bool fullReached; // Whether the player has gotten the achievement -public int reachedStep; // Current steps -public long reachedTime; // When the player has gotten the achievement -public AchievementStats stats; // Rarity -``` - -```java - /*base*/ - private String displayId; // Achievement ID - private int visible = VISIBLE_TRUE; // Whether the achievement is hidden - private String title; // Name - private String subTitle; // Description - private String achieveIcon; // Icon - private int step; // Total steps - private AchievementStats stats; // Rarity - private int type; // Type; 1 means basic achievement; 99 means Platinum Achievement - /*user*/ - private boolean fullReached; // Whether the player has gotten the achievement - private int reachedStep; // Current steps - private long reachedTime; // When the player has gotten the achievement -``` - -```objectivec -@property (nonatomic, copy, readonly) NSString *displayId; // Achievement ID -@property (nonatomic, copy, readonly) NSString *achieveIcon; // Icon -@property (nonatomic, copy) NSString *title; // Name -@property (nonatomic, copy, readonly) NSString *subTitle; // Description -@property (nonatomic, assign, readonly) NSNumber *step; // Total steps -@property (nonatomic, strong) TDSAchievementStatus *stats; // Rarity -@property (nonatomic, assign) NSInteger type; // Type; 1 means basic achievement; 99 means Platinum Achievement - -// User data -@property (nonatomic, assign) BOOL fullReached; // Whether the player has gotten the achievement -@property (nonatomic, assign) long reachedTime; // When the player has gotten the achievement -@property (nonatomic, assign) NSInteger reachedStep; // Current steps -``` - - - -## Internationalization - -The Achievements service supports multiple languages: - -:::tip -When the data is initialized, only the achievement data for the current language is synchronized from the server. To switch to another language after initialization, call `fetchAllAchievementList` and `fetchUserAchievementList`. -::: - - - -## REST API - -Now let’s look at the REST API provided by the Achievements service. -You can write your own programs or scripts to invoke these interfaces to perform server-side administrative operations. - -### Request Format - -The body of any request sent to the REST API must be a JSON object. The `Content-Type` in the HTTP header should be `application/json`. - -Requests sent to the server are authenticated using the key-value pairs in the HTTP Header: - -Key|Value|Description ----|----|--- -`X-TDS-Id`|`{{clientId}}`|The game’s `Client Id`, which can be found on the Developer Center. -`X-TDS-Server-Secret`|`{{serverSecret}}`|The game’s `Server Secret`, which can be found on the Developer Center. - -Find out more about application credentials [here](/sdk/storage/guide/setup-dotnet#credentials). - -In addition to providing the `Client Id` through the `X-TDS-Id` HTTP Header, you must also provide the same `Client Id` in the URL. - -When obtaining achievements, you must specify the language in the URL. See [Language Codes](#language-codes) for more information. - - -If there is an error, the HTTP status code 500 will be returned, like: - -```json -{ - "code": "500", - "msg": "成就服务忙,稍后请求", -} -``` - -### Get All Achievements - -Below is the interface for retrieving all the game’s achievements. Make sure you include the language in the URL. - -```sh -curl -X GET \ - -H "X-TDS-Id: {{clientId}}" \ - -H "X-TDS-Server-Secret: {{serverSecret}}" \ - https://tds-tapsdk.cn.tapapis.com/achievement/open/v1/clients/{{clientId}}/achievements/languages/ -``` - -The response body looks like this: - -```json -{ - "success": true, - "data": { - "list": [ - { - "achievement_id": "Achievement ID", - "client_id": "Client ID", - "achievement_open_id": "Achievement open ID (a custom achievement ID specified by the developer when creating the achievement; this is the unique identifier used when submitting achievement data with the SDK)", - "achievement_type": "Achievement type; 1 means basic achievement; 99 means Platinum Achievement", - "is_hide": "Whether the achievement is hidden; 0 means not hidden; 1 means hidden", - "count_step": "Total steps; if the achievement is not an accumulation achievement, this will be 1", - "show_order": "The order of the achievement; if the achievement is a Platinum Achievement, this will be 0", - "achievement_config_out_dto": { - "achievement_config_id": "The achievement’s configuration ID", - "achievement_id": "Achievement ID", - "language_id": "Language ID", - "achievement_icon": "Link to the achievement’s icon", - "achievement_title": "Achievement name", - "achievement_sub_title": "Achievement description" - }, - "achievement_rarity": { - "rarity": "Rarity rate", - "level": "Rarity; 1 means Common; 2 means Uncommon; 3 means Rare; 4 means Ultra Rare" - } - } - ] - } -} -``` - -### Get a User’s Achievements - -Below is the interface for retrieving a user’s achievements. Make sure you include the player’s account objectId and language code in the URL. - -```sh -curl -X GET \ - -H "X-TDS-Id: {{clientId}}" \ - -H "X-TDS-Server-Secret: {{serverSecret}}" \ - https://tds-tapsdk.cn.tapapis.com/achievement/open/v1/clients/{{clientId}}/users//achievements/languages/ -``` - -The response body looks like this: - -```json -{ - "success": true, - "data": { - "list": [ - { - "achievement_id": "Achievement ID", - "client_id": "Client ID", - "achievement_open_id": "Achievement open ID (an ID assigned to the achievement when it is added on the Developer Center)", - "achievement_type": "Achievement type; 1 means basic achievement; 99 means Platinum Achievement", - "is_hide": "Whether the achievement is hidden; 0 means not hidden; 1 means hidden", - "count_step": "Total steps; if the achievement is not an accumulation achievement, this will be 1", - "show_order": "The order of the achievement; if the achievement is a Platinum Achievement, this will be 0", - "achievement_config_out_dto": { - "achievement_config_id": "The achievement’s configuration ID", - "achievement_id": "Achievement ID", - "language_id": "Language ID", - "achievement_icon": "Link to the achievement’s icon", - "achievement_title": "Achievement name", - "achievement_sub_title": "Achievement description" - }, - "achievement_rarity": { - "rarity": "Rarity rate", - "level": "Rarity; 1 means Common; 2 means Uncommon; 3 means Rare; 4 means Ultra Rare" - }, - "user_achievement_id": "User achievement ID", - "complete_time": "Time of completion (in miliseconds)", - "completed_step": "Steps completed", - "full_completed": "Whether completed; true means yes; false means no" - } - ] - } -} -``` - -### Submit Achievements - -Below is the interface for submitting one or more achievements that a player has earned. The achievements submitted will be **added** to the list of achievements the player has already earned. - -```sh -curl -X POST \ - -H "X-TDS-Id: {{clientId}}" \ - -H "X-TDS-Server-Secret: {{serverSecret}}" \ - -H "Content-Type: application/json" \ - -d '{"data": [{"user_id": , "list": - [{ - "achievement_id": "Achievement ID", - "achievement_open_id": "Achievement open ID (a custom achievement ID specified by the developer when creating the achievement; this is the unique identifier used when submitting achievement data with the SDK)", - "complete_time": "Time of completion (in miliseconds)", - "completed_step": "Steps completed" - }] - }] - }' \ - https://tds-tapsdk.cn.tapapis.com/achievement/open/v1/clients/{{clientId}}/achievements -``` - -The response body looks like this: - -```json -{ - "success": true, - "data": { - "list": [ - { - "user_id": "ObjectId", - "result_list": [ - { - "result": "Whether this data has been successfully submitted; true means yes; false means no" - "code": "Is 0 if succeeded and the error code if not" - "msg": "Is not included if succeeded and the error message if not" - } - ] - } - ] - } -} -``` - -### Language Codes - -The REST API accepts language codes defined by ISO 639-1. For example, `en` means English and `jp` means Japanese. There are a couple of exceptions: - -1. For languages not included in ISO 639-1, the language codes defined in ISO 632-2 are used. For example, `fil` means Filipino. -2. When a language cannot be represented by a language code, the location code defined in ISO 3166-1 will be attached. For example, `zh_CN` means Simplified Chinese. - -The following language codes are supported by the REST API: - -| Code | Language | -| ------- | ----------- | -| zh_CN | Simplified Chinese | -| zh_TW | Traditional Chinese | -| en_US | English (US) | -| ja_JP | Japanese | -| ko_KR | Korean | -| pt_PT | Portuguese | -| vi_VN | Vietnamese | -| hi_IN | Hindi | -| id_ID | Indonesian | -| ms_MY | Malay | -| th_TH | Thai | -| es_ES | Spanish | -| af | Afrikaans | -| am | Amharic | -| bg | Bulgarian | -| ca | Catalan | -| hr | Croatian | -| cs | Czech | -| da | Danish | -| nl | Dutch | -| et | Estonian | -| fil | Filipino | -| fi | Finnish | -| fr | French | -| de | German | -| el | Greek | -| he | Hebrew | -| hu | Hungarian | -| is | Icelandic | -| it | Italian | -| lv | Latvian | -| lt | Lithuanian | -| no | Norwegian | -| pl | Polish | -| ro | Romanian | -| ru | Russian | -| sr | Serbian | -| sk | Slovak | -| sl | Slovenian | -| sw | Swahili | -| sv | Swedish | -| tr | Turkish | -| uk | Ukrainian | -| zu | Zulu | - -Keep in mind that some of the languages listed above are only supported by the REST API but [not the client SDK](#internationalization). - - -## Video Tutorials - -You can refer to the video tutorial:[How to Integrate Achievements in Games](https://www.bilibili.com/video/BV1yH4y1z7F7/) to learn how to access achievements in Untiy projects. - -For more video tutorials, see [Developer Academy](https://developer.taptap.cn/tds-tutorials/list). As the SDK features are constantly being improved, there may be inconsistencies between the video tutorials and the new SDK features, so the current documentation should prevail. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/_category_.json deleted file mode 100644 index f96b0e4ae..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "防沉迷", - "collapsed": true, - "position": 10 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/faq.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/faq.mdx deleted file mode 100644 index e192fbaa1..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/faq.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Frequently Asked Questions -sidebar_label: Frequently Asked Questions -sidebar_position: 3 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - -### Do individual/group developers need access to anti-addiction tools? - -According to the State Press and Publication Administration's [Notice on Further Strict Management to Effectively Prevent Minors from Becoming Addicted to Online Games](https://www.nppa.gov.cn/xxfb/tzgs/202108/t20210830_666285.html), all game publishers and operators are required to implement in-game real name authentication and new anti-addiction strategies. -For games that do not have a version number, they can access the [Real Name Authentication and Anti-Addiction](/sdk/anti-addiction/features/) launched by TDS. - -### What is the relationship between anti-addiction and login? - -As of version 3.7.1, Quick Authentication does not rely on the TapTap Login SDK, so anti-addiction and login are not **strongly related**. However, when logged in with TapTap, you can use the [Quick Authentication for anti-addiction](/sdk/anti-addiction/features/#taptap-quick-verification), which allows players to quickly complete the in-game authentication process using their real name information that has been authenticated by the country in TapTap after they have agreed to authorise it. -In addition, you can also use our [real-name authentication](/sdk/anti-addiction/features/#game-real-name authentication and anti-addiction) feature, which automatically connects to the Ministry of Propaganda's online game anti-addiction real-name authentication system for developers and reports as a developer entity, in line with the Ministry's compliance requirements for game companies to access the system. This is in line with the Ministry's compliance requirements for game companies. - -### If you log out of your account and restart the game after passing the real name authentication, the real name authentication process will be skipped. - -As of version 3.7.1, Quick Authentication does not rely on the TapTap Login SDK, so there is no **strong correlation** between anti-addiction and login. The first time a player enters the game, a real name authentication popup is triggered, asking the player to authorise the game to obtain TapTap's real name information or to enter their identity information. If the player needs to log in using a different account, they will need to actively log out of their account. - -### How do I enable the Real-Name Verification and Anti-addiction service? - -You can enable the service on **Developer Centre Backend > Games Services > Development & Build > Compliance Certification**. You can choose one of the following two options for your game: - -* **The game has an ISBN**. Once you have completed the prerequisites listed on the page, turn on the service by providing the [parameters obtained from Zhongxuanbu’s system](/sdk/anti-addiction/features/#sign-up-for-zhongxuanbus-real-name-verification-system): - * Fill in the **bizId, APPID, and Secret Key** obtained from Zhongxuanbu’s system. - * Then provide the **IP addresses** displayed on the Developer Center to Zhongxuanbu’s system. -* **The game doesn’t have an ISBN**. You can turn on the service without providing an ISBN. Once your game has obtained an ISBN, you can make a switch by providing the [parameters obtained from Zhongxuanbu’s system](/sdk/anti-addiction/features/#sign-up-for-zhongxuanbus-real-name-verification-system). You won’t need to update the client after you do this. - -### What user interfaces does the SDK include? - -Most of the interfaces provided by the SDK are used for identity verification. You can take a look at them [here](/sdk/anti-addiction/features/#integrating-tds-real-name-verification-and-anti-addiction-service). - -### Authorisation failure - -If you see the “Authorisation failure” error when using TapTap Quick Verification, please [configure the signature certificate](/sdk/start/quickstart/#configure-signature-certificate) on the Developer Center. - -### No real name authentication configuration was queried - -If you see the “No real name authentication configuration was queried” error, it is because the Real-Name Verification service is not enabled. Please enable it on Developer Centre Back Office > Games Services > Development & Build > Compliance. - -### userIdentifier is empty - -If you see this error, it means that the value you provided for `userIdentifier` is empty. We recommend that your program checks whether this value is empty before submitting it to the server. - -### The pop-up for real-name verification is not displayed, and the game is not receiving any callbacks from the SDK. - -This often indicates that you have only invoked the code for initializing the UI of the Anti-addiction module and setting up event listeners. - -To pull up the pop-up for real-name verification, you’ll **need to invoke** the [interface for identity verification](/sdk/anti-addiction/guide/#identity-verification). After this, you will be able to receive callbacks. - -### Verifying Identity Repetitively - -We expect that each player only gets verified once and no further pop-ups should show up once the player is already verified. This ensures a better user experience, as the Anti-addiction service can use the result from the previous verification. - -If you notice that a player that has already been verified is being verified for a second time, you can try to solve the problem by following the instructions below: - -* First of all, make sure that the [`userIdentifier`](#regarding-useridentifier) provided meets the requirements. If the `userIdentifier` of the same player may get changed, the player will be seen as different users and may be asked to verify their identity repetitively. Please make sure that you use the proper value for the `userIdentifier`. We recommend that you use [TDS’ built-in account system](/sdk/taptap-login/guide/start/#quick-log-in-with-taptap) and use each player’s `objectId` as the unique identifier of the player. You can also use the [basic TapTap user verification](/sdk/taptap-login/guide/tap-login/#check-login-status-and-user-info) and use each player’s `openid` or `unionid` as the unique identifier. -* If your game **already has an ISBN**, please make sure the parameters entered on 开发者中心后台游戏服务 > 开发与构建 > 合规认证 are correct. If the parameters are incorrect, the service will be unable to make requests to Zhongxuanbu’s interface and players may be asked to verify their identity repetitively. Please check the following parameters: - * All **IP addresses** should be provided to Zhongxuanbu’s system. - * The **bizId and APPID** should be the ones obtained from Zhongxuanbu’s system. - * The **Secret Key** should be valid (it is only valid for half a year, so make sure to update it before it expires). - -## Caveats - -### Regarding `userIdentifier` - -When a player opens your game for the first time, the pop-up for identity verification will be displayed, allowing the player to either authorize the game to obtain the identity information from TapTap or manually enter their identity information. Once the verification is done, whenever your game invokes the interface for identity verification with the same `userIdentifier`, the result from the previous verification will be returned and no further pop-ups will be triggered. - -Therefore, please make sure that the unique identifier of each user stays unique. - -### Environments for Testing Real-Name Verification - -No matter if you are building an Android or iOS project, you cannot test the service in Unity Editor’s environment. Please test the service by running packages on real devices or emulators. - - -### In a testing environment for real-name verification, the verification process is still required for test accounts - -Please make sure that the **player's unique identifier** `userIdentifier` passed into `startup` is the `unionid` of the TapTap user, which can be obtained by referring to [Quick Log-in With TapTap](/sdk/taptap-login/guide/start/#quick-log-in-with-taptap). - -### I see the error "Could not get real name information, please try again later" when using TapTap Quick Authentication - -- Verify that the account logged in to the TapTap client has completed real-name verification in the client. As of v3.22.0, TapTap Quick Authentication will not throw an exception if the account logged in to the TapTap client has not completed real-name verification in the TapTap client, but will take the user to the TapTap client for real-name verification. -- Check that the device time is synchronized with the network. An inaccurate device time will also cause this exception. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/features.mdx deleted file mode 100644 index f53f1ac4a..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/features.mdx +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: Real-Name Verification and Anti-addiction Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -## Getting Ready -:::tip -Real-name authentication and anti-addiction features are also available for unlicensed games. -::: - -To start using TDS’ Real-Name Verification and Anti-addiction service, please first enable it for your game under **游戏服务 > 开发与构建 > 合规认证**. As shown in the picture, you can choose the option of "已有版号" or "暂无版号", and then click "立即开通". - - -![](https://capacity-files.lcfile.com/8xGphnizlUYCzCxGCgsda1d07119IRqG/compliance-certification.png) - -The option "No ISBN" or "With ISBN" is chosen according to the actual situation of the game. - -### No ISBN - -If the game's ISBN has not yet been applied for, you can choose the "No ISBN" option. After the game has received the ISBN, you can switch to the "With ISBN" option. For the "No ISBN" option, you only need to click the "Open Now" button in the picture above, and the operation will be completed. - -### With ISBN - -#### Sign Up for Zhongxuanbu’s Real-Name Verification System - -Please register your game on [Zhongxuanbu’s anti-addiction and real-name verification system for online games](https://wlc.nppa.gov.cn/fcm_company/index.html#/login?redirect=%2F), complete the interface test, and make sure your game has passed Zhongxuanbu’s review. - -You will get the following credentials from the system: - -- `bizId` -- `APPID` -- `Secret Key` - -![](/img/anti-addiction/biz-id.png) - -![](/img/anti-addiction/secretkey.png) - -The game must have passed the review with Zhongxuanbu. - -![](https://capacity-files.lcfile.com/BC7gUR6wfpOh9PHj8XwKybxseeHhl6Fq/anti-examine-success.png) - -The game must also pass the interface test with Zhongxuanbu. There are currently 8 test cases. Please note that the IP address whitelist on Zhongxuanbu's **测试接口 > 预置参数 > IP 白名单** should be configured with your game's own IP addresses. The test interface is for the testing purposes only and won't be needed once your game has passed the test. - -![](/img/anti-addiction/testcase.png) - -Now finish configuring the parameters on the TapTap Developer Center and enter the IP address whitelist provided by the TapTap Developer Center on Zhongxuanbu's website: - -![](https://capacity-files.lcfile.com/3WYezwPnXmK3zOLdVGLPWuvuf0MX4m1M/anti-addiction-qualification.png) - -## Integrating TDS’ Real-Name Verification and Anti-addiction Service - -Once you have finished the steps above, you will be able to start integrating TDS’ Real-Name Verification and Anti-addiction service into your game. - -The best practice is: after the user has successfully logged in, the "TapTap Quick Authentication" authorization box pops up. If you click on the "Use" button, you will be taken to the TapTap app to complete the authentication. If you click on the "Don't Use" button, a prompt box will be displayed for you to manually fill in the identity information. - - -:::tip -The "TapTap Quick Authentication" pop-up box is displayed by default, and the SDK does not support directly popping up the input box for manually filling in real-name identity information. Users have to manually click the "Do not use" button to open up the input box for manually filling in the information. -::: - - -### TapTap Quick Verification - -With TapTap Quick Verification, a player can quickly complete the verification process with the real-name information filed in their TapTap account if they wish to. - -Workflow: - -![](/img/anti-addiction/anti-addiction-flow.png) - -What the player will see: - -![](/img/anti-addiction/image2021-10-18_17-57-51.png) - -### Real-Name Verification and Anti-addiction - -According to the [announcement](https://www.nppa.gov.cn/xxfb/tzgs/202108/t20210830_666285.html) made by the National Press and Publication Administration, all businesses publishing and operating games have to have their games follow the latest policy regarding real-name verification and anti-addiction. - -The announcement requires that all users have to sign up and log in to online games with their real and valid identification information. When you use TDS in your game, your game will be connected to Zhongxuanbu’s anti-addiction and real-name verification system and TDS will represent your business to report the information required by the system. This ensures that your business complies with Zhongxuanbu’s legal requirements. - -If you don't use TapTap Quick Authentication, that is, after the "TapTap Quick Authentication" authorization box pops up, the "Don't use" button is clicked, then a prompt box will be displayed, and you need to manually fill in the identity information to complete the real-name authentication. - -Once the player submits the information, TDS will submit the information to Zhongxuanbu’s system to validate the information. Please make sure you have finished configuring your game on the Developer Center. Otherwise, the validation process won’t work. - -![](/img/anti-addiction/anti-addiction-flow-2.png) - -What the player will see: - -![](/img/anti-addiction/image2021-10-19_17-4-12.png) - -## Anti-addiction Policies - -The announcement requires that your business strictly limits when a minor can play your game as well as the amount of money they can spend on your game. -With the interfaces provided by our SDK, your game would be able to check if a minor is currently allowed to play your game, as well as whether they can make a payment of a given amount of money. - -![](/img/anti-addiction/not-allow-play.png) - - -### Time Restrictions - -Minors under the age of 18 are only allowed to game between **8:00 pm and 9:00 pm on Fridays, Saturdays, Sundays, and public holidays**. - -### Payment Restrictions - -- Minors under the age of 8 are not allowed to make payments in your game. - -Among all the games provided by **the same business**, the following payment restrictions are imposed: - -- A minor over the age of 8 but under the age of 16 can make a payment of no more than 50 CNY each time. The total amount they can spend each month is 200 CNY. -- A minor over the age of 16 but under the age of 18 can make a payment of no more than 100 CNY each time. The total amount they can spend each month is 400 CNY. - -Keep in mind that when keeping track of payment restrictions, the amount spent by a minor on all the games provided by the same business will be accumulated. -For example, if a business has two games, and a minor over the age of 8 but under the age of 16 has already spent 100 CNY in one of the games, they can only spend no more than 100 CNY in the other game. - -## Test Accounts -There are two scenarios for using test accounts: - -- The first is when the game is in development and testing, test accounts are needed to test the real-name authentication anti-addiction features. -- The second is when you are applying for the ISBN. - -The test accounts provided by the anti-addiction service are strongly dependent on TapTap Login. 30 test account are provided in accordance with the requirements of the State Press and Publication Administration. The accounts include: - -- One group of empty accounts without real-name verification, with a total of 9 accounts; -- Two groups of minor accounts, 18 in total, each containing 3 each of high, medium, and low ages (above 16 and under 18, above 8 and under 16, and under 8); -- One group of adult accounts (over 18 years old), with a total of 9 accounts. - -According to the usage scenario of test accounts, the usage mode of test accounts is divided into two types: **Regular Mode** and **Test Mode**. - -### Regular Mode - -The status and attributes of the test accounts cannot be changed in the regular mode, which is applicable to the scenario of submitting the game for review and applying for an ISBN. - -### Test Mode - -To facilitate developers to test the real-name verification and anti-addiction functions for underage players, the test mode is introduced, under which the status and attributes of the test accounts can be changed. For example, for the underage test accounts provided, you can change whether the user is playable or not at the moment, and the amount of money that has been spent, so that the developer does not need to wait for the time when underage players are allowed to play to test the anti-addiction function. Test accounts can be viewed by going to **TapTap Developer Centre > Your Games > Game Services > Compliance Certification > Test Accounts**. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/guide.mdx deleted file mode 100644 index 4837e2de6..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/anti-addiction/guide.mdx +++ /dev/null @@ -1,908 +0,0 @@ ---- -title: Real-Name Verification and Anti-addiction Guide -sidebar_label: Guide -sidebar_position: 2 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; - -:::tip -Before using the TDS’ Real-Name Verification and Anti-addiction service, please first enable the service on **开发者中心后台 > 游戏服务 > 开发与构建 > 合规认证**. When enabling the service, you can select whether your game has gotten an ISBN. -::: - -## Configure the SDK - -You can download the TapSDK from the [Downloads page](/tap-download) and import the anti-addiction module into your game. - - - -<> - -If you are building your game with Unity, you can import a `.unitypackage` file to your game, which includes the iOS module and the Android module. If you are building with other engines or for other platforms, you can import the iOS or Android module natively. Please refer to the documentation for iOS or Android for more information. - -**Unity requirement**: You will need Unity 2019.4 or higher to use the SDK. - -Supported OS versions: - -- Android: 5.0 and higher -- iOS: 10.0 and higher, Xcode 14.1 or later - -The SDK can be imported **either using the Unity Package Manager or manually**. - -#### Method 1: Use Unity Package Manager - -Add the following dependencies into `Packages/manifest.json`: - - - {`"dependencies":{ - "com.tapsdk.antiaddiction":"https://github.com/taptap/TapAntiAddiction-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#${sdkVersions.taptap.unity}", -}`} - - -In Unity’s menu bar, select **Window > Package Manager** to view the packages already installed in the project. - -#### Method 2: Import Manually - -Open the download pages of **TapSDK Unity** and **LeanCloud C# SDK** from the [Downloads](/tap-download) page and download `TapSDK-UnityPackage.zip` and `LeanCloud-SDK-Realtime-Unity.zip` from these pages. - -
      -
    • Import the TapTap_Common_{sdkVersions.taptap.unity}.unitypackage and TapTap_AntiAddiction_{sdkVersions.taptap.unity}.unitypackage and TapTap_Login_{sdkVersions.taptap.unity}.unitypackageunzipped from TapSDK-UnityPackage.zip.
    • -
    - -- If the current project has integrated the Newtonsoft.Json dependency, ignore this step, otherwise download the library file by clicking `Download package` on the right side of the NuGet.org [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json) page and change the downloaded file extension from `.nupkg` to `.zip`, extract the file and copy the internal `Newtonsoft.Json.dll` file into the Plugins directory of the project `Assets - Plugins`. In addition, to avoid deleting the necessary data when exporting the `IL2CPP` platform, create a `link.xml` file in the `Assets` directory (if you already have this file, add the following content) with the following content - -```xml - - - - - -``` - -Configuring for iOS: - -Using Xcode 13.0 beta 5 for compiling the project, please follow the steps below to make sure the Xcode project produced by Unity is correctly configured: - -1. Go to `Xcode` - `General` - `Frameworks, Libraries, and Embedded Content` and ensure that `AntiAddictionService.framework` and `AntiAddictionUI.framework` are set to `Do Not Embed`. -2. If the compilation fails due to missing header files or modules, please make sure the path set in `Xcode` - `Build Settings` - `Framework Search Paths` is correct. -3. Make sure the `Swift Compile Language` / `Swift Language Version` under `Build Settings` of the Xcode project is `Swift5`. -4. Add `libz.tbd` and `libc++.tbd` as dependencies. -5. Write your code to integrate the service. -6. Copy `AntiAddiction-Unity/Assets/Plugins/iOS/Resource/AntiAdictionResources.bundle` to your game project (if `AntiAddictionResources.bundle` is not correctly imported to the Unity project). - - -<> - -The SDK supports Android 5.0 (API level 21) and higher. The SDK is compiled with Android Studio. - -
      -
    • Copy the Anti-addiction SDK AntiAddiction_{sdkVersions.taptap.android}.aar to the src/main/libs directory under your project
    • -
    • Copy the Anti-addiction SDK AntiAddictionUI_{sdkVersions.taptap.android}.aar to the src/main/libs directory under your project
    • -
    • Copy TapCommon_{sdkVersions.taptap.android}.aar to the src/main/libs directory under your project
    • -
    • Copy TapLogin_{sdkVersions.taptap.android}.aar to the src/main/libs directory under your project
    • -
    - -Add the following code to `build.gradle` under your project - - -{`repositories{ - flatDir{ - dirs 'src/main/libs' - } -} -dependencies { - // ... - implementation(name: "AntiAddiction_${sdkVersions.taptap.ios}", ext: "aar") // Anti-addiction SDK - implementation(name: "AntiAddictionUI_${sdkVersions.taptap.ios}", ext: "aar") // Anti-addiction SDK - implementation(name: "TapCommon_${sdkVersions.taptap.ios}", ext: "aar") - implementation(name: "TapLogin_${sdkVersions.taptap.ios}", ext: "aar") - // ... -}`} - - - -<> - -The SDK supports iOS 11 and higher, Xcode 14.1 or later. - -Below is the structure of the **Anti-addiction SDK** for iOS: - -- `AntiAddictionService`: The base library for the Anti-addiction service written with Swift. -- `AntiAddictionUI`: The Anti-addiction library with UI, which depends on `AntiAddictionService`. It is written with Objective-C. -- `AntiAddictionResources.bundle`: Resource files. - -Other dependencies: - -- `TapCommonSDK.framework`: The base library. -- `TapLoginSDK.framework`: The base library. -- `TapCommonResource.bundle`: The Resource files. -- `TapLoginResource.bundle`: The Resource files. - -To add the libraries for the Anti-addiction service: - -- Add `AntiAddictionService.framework`, `AntiAddictionUI.framework`, `TapLoginSDK.framework` , and `TapCommonSDK.framework`. Make sure to choose **Do Not Embed** as the embed method when adding these libraries. - -- Import the code: - - ``` objc - // AntiAddictionUI - #import - ``` - -Add system dependencies: - -Check if the following dependencies have been automatically added to your project: - -- `libc++.tdb` -- `libz.tdb` - -If the dependencies cannot be correctly loaded when you try to run your project, you can set the dependencies to be optional and try again. - -Configure options for compiling: - -- Under *Build Setting*, add `-ObjC` and `-Wl -ld_classic`to *Other Link Flag*. - -- Under *Build Setting*, set *Always Embed Swift Standard Libraries* to *YES* so that the Swift standard library will always be imported. This prevents exceptions from happening when you run your app due to missing the Swift standard library. If the library cannot be found in your project, you can create an empty Swift file and Xcode will automatically set up the bridging relationship. - -- Under *Build Setting*, select *Swift 5* for *Swift Compiler - Language / Swift Language Version*. - - - -<> - -#### Environment Requirements - -* UE 4.26 or higher -* iOS 12 or higher, Xcode 14.1 or later -* Android 5.0 (API level 21) or higher -* macOS 10.14.0 or higher -* Windows 7 or higher - -**Platforms supported**: iOS / Android / Windows / macOS - -#### Install Plugins - -* Download [TapSDK UE4](/tap-download), unzip `TapSDK-UE4-xxx.zip`, and copy `AntiAddiction`, TapLogin and `TapCommon` to the `Plugins` directory of your project -* Restart Unreal Editor -* Go to Edit > Plugins > Project > TapTap and enable the `AntiAddiction` module - -#### Add Dependencies - -Add the required dependencies to `Project.Build.cs`: - -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapLogin", - "AntiAddiction" -}); -``` - -#### Import the Header File - -```cpp -#include "TapUEBootstrap.h" -``` - -
    - -Compiling a project with a mixture of Objective-C and Swift code - -You can take either of the following two methods. - -Method one: Replace the Anti-addiction library with the dynamic library - -Pros can cons: - -* Pros: You don’t have to edit the code of the engine -* Cons: - * The size of the package will increase a little bit - * Only iOS 13 and higher is supported; the project will crash on iOS 12 and below - -Steps: - -1. Download the [library file](https://github.com/taptap/TapSDK-iOS/releases) that has the same version as the `TapSDK-iOS` you’re using -2. Replace the `AntiAddictionService.framework` in `Plugins/AntiAddiction/Source/AntiAddiction/ios/framework/AntiAddictionService.zip` with the `Dylib/AntiAddictionService.framework` you just downloaded (unzip -> replace -> zip) -3. Replace the following part in `AntiAddiction.Build.cs` - - ```cs - new Framework( - "AntiAddictionService", - "../AntiAddiction/ios/framework/AntiAddictionService.zip" - ) - ``` - - with: - - ```cs - new Framework( - "AntiAddictionService", - "../AntiAddiction/ios/framework/AntiAddictionService.zip", - null, - true - ) - ``` - -4. Compile the project again - -Method two: Edit `UnrealBuildTool` - -1. Edit `XcodeProject.cs` - - Path: `Engine/Source/Programs/UnrealBuildTool/ProjectFiles/Xcode/XcodeProject.cs` - - Find the function: - - ```cpp - private void AppendProjectBuildConfiguration(StringBuilder Content, string ConfigName, string ConfigGuid) - ``` - - And add the following code to the function: - - ```cpp - // Enable Swift - Content.Append("\t\t\t\tCLANG_ENABLE_MODULES = YES;" + ProjectFileGenerator.NewLine); - Content.Append("\t\t\t\tSWIFT_VERSION = 5.0;" + ProjectFileGenerator.NewLine); - Content.Append("\t\t\t\tLIBRARY_SEARCH_PATHS = \"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\";" + ProjectFileGenerator.NewLine); - if (ConfigName == "Debug") - { - Content.Append("\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";" + ProjectFileGenerator.NewLine); - } - Content.Append("\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;" + ProjectFileGenerator.NewLine); - Content.Append("\t\t\t\tEMBEDDED_CONTENT_CONTAINS_SWIFT = YES;" + ProjectFileGenerator.NewLine); - ``` - - Your code should look like this: - ![](https://img.tapimg.com/market/images/e62c405ba77c57475dfaed7b5e550c5b.jpg) - -2. Edit `IOSToolChain.cs` - - Path: `Engine/Source/Programs/UnrealBuildTool/Platform/IOS/IOSToolChain.cs` - - Find the function: - - ```cpp - string GetLinkArguments_Global(LinkEnvironment LinkEnvironment) - ``` - - And add the following code to the function: - - ```cpp - // This line shall be placed at the beginning (see the screenshot below) - // Added by uwellpeng: enable swift support, make sure '/usr/lib/swift' goes before '@executable_path/Frameworks' - Result += " -rpath \"/usr/lib/swift\""; - - // enable swift support - Result += " -rpath \"@executable_path/Frameworks\""; - // /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/swift/ - String swiftLibPath = String.Format(" -L {0}Platforms/{1}.platform/Developer/SDKs/{1}{2}.sdk/usr/lib/swift", - Settings.Value.XcodeDeveloperDir, bIsDevice? Settings.Value.DevicePlatformName : Settings.Value.SimulatorPlatformName, Settings.Value.IOSSDKVersion); - Result += swiftLibPath; - Log.TraceInformation("Add swift lib path : {0}", swiftLibPath); - ///Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos - swiftLibPath = String.Format(" -L {0}Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/{1}", - Settings.Value.XcodeDeveloperDir, bIsDevice? Settings.Value.DevicePlatformName.ToLower() : Settings.Value.SimulatorPlatformName.ToLower()); - Result += swiftLibPath; - Log.TraceInformation("Add swift lib path : {0}", swiftLibPath); - ///Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/iphoneos - swiftLibPath = String.Format(" -L {0}Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/{1}", - Settings.Value.XcodeDeveloperDir, bIsDevice? Settings.Value.DevicePlatformName.ToLower() : Settings.Value.SimulatorPlatformName.ToLower()); - Result += swiftLibPath; - // Xcode 12 has an additional `swiftcompatabiliy51` library and the following code needs to be added - if (Settings.Value.IOSSDKVersionFloat >= 14.0f) - { - Result += String.Format(" -lswiftCompatibility51"); - } - ``` - - Keep in mind that `Result += " -rpath \"/usr/lib/swift\"";` should be placed above `@executable_path/Frameworks` - - Your code should look like this: - - ![](https://img.tapimg.com/market/images/84ca51379a9f6c7a69262450fbf89b7b.jpg) - -3. Recompile UBT - - Recompile `UnrealBuildTool` with `msbuild`. You can run the `Terminal` command `msbuild` under the `Engine/Source/Programs/UnrealBuildTool` directory. If the engine directory is under a directory that you cannot edit, you can prepend the command with `sudo`, like `sudo msbuild`. - - Once you finish the three steps above, you will be able to compile a project with a mixture of Objective-C and Swift code. - -
    - - - -
    - -The Anti-addiction SDK needs the permissions for connecting to the internet and sending request data. Please make sure to declare those permissions in your project. - -## Initialize the SDK - -During this step, you will be initializing the Anti-addiction UI module, providing configurations for starting the anti-addiction function, and setting up listeners for events related to anti-addiction. Keep in mind that **the [Identity Verification](#identity-verification) interface needs to be invoked when the callback is triggered**. - - -<> - -```cs -using TapTap.AntiAddiction; -using TapTap.AntiAddiction.Model; - -AntiAddictionConfig config = new AntiAddictionConfig() -{ - gameId = "your_client_id", // The Client ID obtained from the TapTap Developer Center - useAgeRange = false , // Whether to use age group information - showSwitchAccount = false, // Whether to display the button for switching account -}; - -Action callback = (code, errorMsg) => { - // code == 500; // Logged in - // code == 1000; // Logged out - // code == 1001; // The user tapped the button for switching account - // code == 1030; // The user is not allowed to play games at this time - // code == 1050; // The time limit is reached - // code == 1100; // The user is unable to access the game due to triggering the age limit set by the app - // code == 1200; // If the data request fails, the game needs to check whether the currently set application information is correct and judge whether the current network connection is normal. - // code == 9002; // The user closed the window for real-name verification - UnityEngine.Debug.LogFormat($"code: {code} error Message: {errorMsg}"); -}; - -AntiAddictionUIKit.Init(config); -AntiAddictionUIKit.SetAntiAddictionCallback(callback); -``` - - -<> - - -```java -// The first argument passed into the interfaces of the Android SDK is always the current `Activity` - -Config config = new Config.Builder() - .withClientId("your_client_id") // The Client ID obtained from the TapTap Developer Center - .useAgeRange(false) // Whether to use age group information - .showSwitchAccount(false) // Whether to display the button for switching account - .build(); - -AntiAddictionUIKit.init(activity, config, new AntiAddictionUICallback() { - @Override - public void onCallback(int code, Map extras) { - if (code == Constants.ANTI_ADDICTION_CALLBACK_CODE.LOGIN_SUCCESS){ - Log.d("TapTap-AntiAddiction", "The player is qualified to play the game"); - } - } -}); -``` - - -<> - -```objc -AntiAddictionConfig *config = [[AntiAddictionConfig alloc] init]; -config.clientID = @"your_client_id"; // The Client ID obtained from the TapTap Developer Center -config.useAgeRange = NO; // Whether to use age group information -config.showSwitchAccount = NO; // Whether to display the button for switching account -// `self` needs to implement the `AntiAddictionDelegate` protocol; `self` can be replaced by another object that follows the `AntiAddictionDelegate` protocol -[AntiAddiction initWithConfig:config]; -[AntiAddiction setDelegate:delegate]; -``` - -The `delegate` object of `+[AntiAddiction initWithConfig:delegate:]` needs to implement the following protocol to receive callbacks: - -```objc -- (void)antiAddictionCallbackWithCode:(AntiAddictionResultHandlerCode)code extra:(NSString * _Nullable)extra { - // Anti-addiction callback -} -``` - - - -<> - -```cpp -FAAUConfig Config; -Config.ClientID = TEXT("your_client_id"); // The Client ID obtained from the TapTap Developer Center -Config.ShowSwitchAccount = false; // Whether to display the button for switching account -Config.UseAgeRange = false; // Whether to use age group information -AntiAddictionUE::Init(Config); -``` - -Once the Anti-addiction service is started, there will be callbacks for different events being triggered, so the `AntiAddictionUE::OnCallBack` callback needs to be listened on. See the values of `AntiAddictionUE::ResultHandlerCode` for a list of possible events. - -```cpp -// Bind the `AntiAddictionUE::OnCallBack` callback -AntiAddictionUE::OnCallBack.BindUObject(this, &UAntiAddictionWidget::OnCallBack); - -void UAntiAddictionWidget::OnCallBack(AntiAddictionUE::ResultHandlerCode Code, const FString& Message) { - TUDebuger::DisplayShow(FString::Printf(TEXT("Code: %d, Message: %s"), Code, *Message)); - switch (Code) { - case AntiAddictionUE::LoginSuccess: - TUDebuger::DisplayShow(TEXT("Logged in")); - break; - case AntiAddictionUE::Exited: - TUDebuger::DisplayShow(TEXT("Logged out")); - break; - case AntiAddictionUE::SwitchAccount: - TUDebuger::DisplayShow(TEXT("The user tapped the button for switching account")); - break; - case AntiAddictionUE::DurationLimit: - TUDebuger::DisplayShow(TEXT("The time limit is reached")); - break; - case AntiAddictionUE::PeriodRestrict: - TUDebuger::DisplayShow(TEXT("The user is not allowed to play games at this time")); - break; - case AntiAddictionUE::RealNameStop: - TUDebuger::DisplayShow(TEXT("The user closed the window for real-name verification")); - break; - default: - TUDebuger::WarningLog(TEXT("Unknown Code: ") + FString::FromInt(Code)); - break; - } -} -``` - - - - - -### Parameters - - - -<> - -- `config` is the configuration for the Anti-addiction service. It contains the following parameters: - - `gameId`: The `Client ID` of the game, which can be found on the Developer Center (**开发者中心 > 你的游戏 > 游戏服务 > 应用配置**). - - - `useTapLogin`: Whether to use [TapTap Quick Verification](#taptap-quick-verification). We recommend that you enable this feature so that players who have already confirmed their identities on TapTap can quickly complete the verification process for anti-addiction. - - - `showSwitchAccount`: Whether to display the button for switching accounts. If your game doesn’t provide the function for players to switch accounts, you can make this button hidden from the user interface. If you choose to have the button displayed (as shown in the image below), the `1001` callback will be triggered when the player hits the button. Your game can handle the player’s request for switching accounts upon receiving this code. - -- `callback` is the callback interface provided by the SDK to notify users of events. It contains the following parameters: - - `code`: The callback type for the event. See [Callback Types](#callback-types) for more information. - - - `errorMsg`: If an error occurs, this will be the error message. - - - -<> - -The following parameters need to be provided when you initialize the Anti-addiction service: - -- The `Client ID` of the game, which can be found on the Developer Center (**开发者中心 > 你的游戏 > 游戏服务 > 应用配置**). -- Whether to use [TapTap Quick Verification](#taptap-quick-verification). We recommend that you enable this feature so that players who have already confirmed their identities on TapTap can quickly complete the verification process for anti-addiction. -- Whether to display the button for switching accounts. If your game doesn’t provide the function for players to switch accounts, you can make this button hidden from the user interface. If you choose to have the button displayed (as shown in the image below), the `1001` callback will be triggered when the player hits the button. Your game can handle the player’s request for switching accounts upon receiving this code. - - - -<> - -- `config` is the configuration for the Anti-addiction service. It contains the following parameters: - - `clientID`: The `Client ID` of the game, which can be found on the Developer Center (**开发者中心 > 你的游戏 > 游戏服务 > 应用配置**). - - - `useTapLogin`: Whether to use [TapTap Quick Verification](#taptap-quick-verification). We recommend that you enable this feature so that players who have already confirmed their identities on TapTap can quickly complete the verification process for anti-addiction. - - - `showSwitchAccount`: Whether to display the button for switching accounts. If your game doesn’t provide the function for players to switch accounts, you can make this button hidden from the user interface. If you choose to have the button displayed (as shown in the image below), the `1001` callback will be triggered when the player hits the button. Your game can handle the player’s request for switching accounts upon receiving this code. - -- The `delegate` object needs to implement the `-[AntiAddictionDelegate antiAddictionCallbackWithCode:extra:]` protocol to receive callbacks - - `code`: The callback type for the event. See [Callback Types](#callback-types) for more information. - - - `extra`: Additional information for the current event. - - - -<> - -- `config` is the configuration for the Anti-addiction service. It contains the following parameters: - - `ClientID`: The `Client ID` of the game, which can be found on the Developer Center (**开发者中心 > 你的游戏 > 游戏服务 > 应用配置**). - - - `UseTapLogin`: Whether to use [TapTap Quick Verification](#taptap-quick-verification). We recommend that you enable this feature so that players who have already confirmed their identities on TapTap can quickly complete the verification process for anti-addiction. - - - `ShowSwitchAccount`: Whether to display the button for switching accounts. If your game doesn’t provide the function for players to switch accounts, you can make this button hidden from the user interface. If you choose to have the button displayed (as shown in the image below), the `1001` callback will be triggered when the player hits the button. Your game can handle the player’s request for switching accounts upon receiving this code. - - - - - - -![The user interface for switching account](/img/anti-addiction/switch-account.png) - -### Callback Types - -| Callback code | Callback type | When does it get triggered | -|---|---|---| -| 500 | `LOGIN_SUCCESS` | Logged in | -| 1000 | `EXITED` | Logged out | -| 1001 | `SWITCH_ACCOUNT` | The user tapped the button for switching account | -| 1030 | `PERIOD_RESTRICT` | The user is not allowed to play games at this time | -| 1050 | `DURATION_LIMIT` | The time limit is reached | -| 1100 | `AGE_LIMIT` | The user is unable to access the game due to triggering the age limit set by the app | -| 1200 | `INVALID_CLIENT_OR_NETWORK_ERROR` | If the data request fails, the game needs to check whether the currently set application information is correct and judge whether the current network connection is normal. | -| 9002 | `REAL_NAME_STOP` | The user closed the window for real-name verification | - -## Identity Verification - -The SDK provides two methods for you to verify the identities of players: - -1. TapTap Quick Verification: This allows players to authorize your game to use their age range information from TapTap to quickly complete the real-name verification process. -2. Manual entry: This lets players enter their identity information with the user interface provided by the SDK. In this case, the "TapTap Quick Authentication" box pops up and the "Don't use" button is clicked. - -TDS’ server will tell if a player can play your game based on their identity information. The player’s information will be automatically sent to Zhongxuanbu’s system. - -We recommend that you adopt the first method so that players won’t have to manually enter their identity information and can start playing your game more quickly. - -### TapTap Quick Verification - -:::info -**Please make sure you have completed the following steps**: - -- **Enable TapTap Login**. If this step is not completed, the player will see “应用未开通 TapTap 登录服务” when using TapTap Quick Verification. -- **[Configure the signature certificate](/sdk/start/quickstart/#configure-signature-certificate) on the Developer Center**. If this step is not completed, the player will see “授权失败” when using TapTap Quick Verification. -::: - -To enable TapTap Quick Verification, set the parameter for TapTap Quick Verification to `true` when initializing the SDK. - -To start the verification process, you need to provide `userIdentifier`, the unique identifier of the player. The SDK will then open the TapTap app. If the TapTap app is not installed on the device, a WebView will be opened. The player can then authorize the game to use their age range information from TapTap to complete the real-name verification process in the game. - -For `userIdentifier`, the **unique identifier of the player**, if your game is using [TDS’ built-in account system](/sdk/taptap-login/guide/start/#quick-log-in-with-taptap), you can use the player’s `objectId` as it; if your game is using the [basic TapTap user verification](/sdk/taptap-login/guide/tap-login/#check-login-status-and-user-info), you can use the player’s `openid` or `unionid`. - - - -<> - -```cs -// The unique identifier cannot be longer than 64 characters -string userIdentifier = "THE_UNIQUE_IDENTIFIER_OF_THE_PLAYER"; -AntiAddictionUIKit.StartupWithTapTap(userIdentifier); -``` - -**Attention**: If your Unity project hasn’t integrated the TapTap Login module, you’ll need to configure the `URLSchema` when creating a package for iOS from the exported Xcode project. - -
    -How to configure the `URLSchema` for an Xcode project - -Open `info.plist` and add the following configurations (replace the `clientID` with the Client ID you obtained from the Developer Center): - -![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` -
    - -<> - -```java -// The unique identifier cannot be longer than 64 characters -String userIdentifier = "THE_UNIQUE_IDENTIFIER_OF_THE_PLAYER"; -AntiAddictionUIKit.startupWithTapTap(activity, userIdentifier); -``` - - -<> - -```objc -// The unique identifier cannot be longer than 64 characters -NSString *userIdentifier = @"THE_UNIQUE_IDENTIFIER_OF_THE_PLAYER"; -[AntiAddiction startupWithTapTap:userIdentifier]; -``` - -To use **TapTap Quick Verification**, the callback needs to be embedded into `AppDelegate`. - -Attention: If you have already added this for the TapTap Login module, you can skip this step. - -```objc -#import - -// The new callback -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSHandleUrl handleOpenURL:url]; -} -``` - -If you haven’t integrated the TapTap Login module, you will need to configure the `URLSchema` as well. - -Open `info.plist` and add the following configurations (replace the `clientID` with the Client ID you obtained from the Developer Center): - -![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` - - - -<> - -```cpp -AntiAddictionUE::StartupWithTapTap(TEXT("your_userIdentifier")); -``` - - - -
    - -### Manual Entry - -If you prefer not to enable TapTap Quick Verification, set the parameter for TapTap Quick Verification to `false` when initializing the SDK. - -The format of the `userIdentifier`, the **unique identifier of the player**, will be defined by your game. It should be no longer than 64 characters. - -## Log Out - -When a user logs out of your game, you can invoke this method to reset the states of the Anti-addiction service. - - - -```cs -AntiAddictionUIKit.Exit(); -``` - -```java -AntiAddictionUIKit.exit(); -``` - -```objc -[AntiAddiction exit]; -``` - -```cpp -AntiAddictionUE::Exit(); -``` - - - -## Obtain Player’s Age Range - -Invoke the following interface to get the player’s age range: - - - -```cs -int ageRange = AntiAddictionUIKit.AgeRange; -``` - -```java -int ageRange = AntiAddictionUIKit.getAgeRange(); -``` - -```objc -NSInteger ageRange = [AntiAddiction getAgeRange]; -``` - -```cpp -EAAUAgeLimit AgeLimit = AntiAddictionUE::GetAgeRange(); -``` - - - -The `ageRange` in the example above is an integer indicating the minimum age among the age range of the player. -If its value is `-1`, it means that the age range of the user is unknown. This indicates that the user hasn’t verified their identity yet. - -| Value | Meaning | -| - | - | -| -1 | Unknown | -| 0 | 0 to 7 years old | -| 8 | 8 to 15 years old | -| 16 | 16 to 17 years old | -| 18 | Adult | - -## Get Remaining Time (In Seconds) - -To get the time remaining for the player: - - - -```cs -int remainingTimeInSeconds = AntiAddictionUIKit.RemainingTime; -``` - -```java -int remainingTimeInSeconds = AntiAddictionUIKit.getRemainingTime(); -``` - -```objc -NSInteger remainingTimeInSeconds = [AntiAddiction getRemainingTime]; -``` - -```cpp -int RemainingTime = AntiAddictionUE::GetRemainingTime(); -``` - - - -## Get Remaining Time (In Minutes) - -To get the time remaining for the player: - - - -```cs -int remainingTimeInMinutes = AntiAddictionUIKit.RemainingTimeInMinutes; -``` - -```java -int remainingTimeInMinutes = AntiAddictionUIKit.getRemainingTimeInMinutes(); -``` - -```objc -NSInteger remainingTimeInMinutes = [AntiAddiction getRemainingTimeInMinutes]; -``` - -```cpp -int RemainingTimeInMinutes = AntiAddictionUE::GetRemainingTimeInMinutes(); -``` - - - -## Check Payment Restrictions - -Different payment restrictions are imposed on minors that fall under different age ranges. -To enable payment restrictions, you will be checking if restrictions exist before a minor attempts to make a payment. When a payment goes through, you will also be reporting the amount paid to our service. - -To check if the current player is allowed to make a payment when your game receives a request from the player to make a payment: - - - -```cs -long amount = 100; -AntiAddictionUIKit.CheckPayLimit(amount, - (result) => { - // If `status` is `1`, the player is allowed to make a payment - int status = result.status; - if (status == 1) { - // The player is allowed to make a payment - } - }, - (exception) => { - // Handle exception - } -); -``` - -```java -long amount = 100; -AntiAddictionUIKit.checkPayLimit(activity, amount, - new Callback() { - @Override - public void onSuccess(CheckPayResult result) { - // If `status` is `true`, the player is allowed to make a payment; otherwise, the player cannot make a payment - if (result.status) { - } - } - - @Override - public void onError(Throwable throwable) { - // Handle exception - } - } -); -``` - -```objc -NSInteger amount = 100; -[AntiAddiction checkPayLimit:amount resultBlock:^(BOOL status) { - if (status) { - // Not restricted - } -} failureHandler:^(NSString * _Nonnull error) { - // Handle exception -}]; -``` - -```cpp -AntiAddictionUE::CheckPayLimit(FCString::Atoi(*AmountTF->Text.ToString()), [](bool Status) { - TUDebuger::DisplayShow(FString::Printf(TEXT("Status: %d"), Status)); -}, [](const FString& Msg) { - TUDebuger::ErrorShow(Msg); -}); -``` - - - -The unit of the amount is *cent*. - -To make the function for checking payment restrictions work, your game should be reporting all the payments made by minor players. -We recommend that you report payments on the server side. Refer to [the description of the relative REST API](#report-payment-amounts) for more information on how you can report payments on the server side. -You can also report payments using the interface provided by the SDK when a minor player successfully makes a payment. However, this is a less reliable method compared to the former one and is often used by games that don’t require connecting to servers. - - - -```cs -long amount = 100; -AntiAddictionUIKit.SubmitPayResult(amount, - () => { - // Succeeded - }, (exception) => { - // Handle exception - } -); -``` - -```java -long amount = 100; -AntiAddictionUIKit.submitPayResult(amount, - new Callback() { - @Override - public void onSuccess(SubmitPayResult result) { - // Succeeded - } - - @Override - public void onError(Throwable throwable) { - // Handle exception - } - } -); -``` - -```objc -NSInteger amount = 100; -[AntiAddiction submitPayResult:amount callBack:^(BOOL success) { - if (success) { - // Succeeded - } -} failureHandler:^(NSString * _Nonnull error) { - // Handle exception -}]; -``` - -```cpp -AntiAddictionUE::SubmitPayResult(FCString::Atoi(*AmountTF->Text.ToString()), [](bool Success) { - TUDebuger::DisplayShow(FString::Printf(TEXT("Success: %d"), Success)); -}, [](const FString& Msg) { - TUDebuger::ErrorShow(Msg); -}); -``` - - - -When reporting payment amount, the unit of the amount is also *cent*. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/faq.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/faq.mdx index 85ab44116..8110d8eea 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/faq.mdx +++ b/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/faq.mdx @@ -1,13 +1,23 @@ --- -title: FAQ -sidebar_label: FAQ -sidebar_position: 5 +title: 内建账户常见问题 +sidebar_label: 常见问题 +sidebar_position: 4 --- -### The built-in account username is inconsistent with the TapTap account username +### 应用内用户的密码需要加密吗 -Currently, TapTap nicknames and game nicknames are separate. Our design logic is that after using TapTap login for the first time, the login information will be stored in the built-in account. However, if you modify your TapTap nickname later, the original nickname stored in the built-in account will not be updated. If you want to use the updated nickname, you can try adding a [change nickname](/sdk/authentication/guide/#setting-other-user-properties) feature in the game. After a user provides their new nickname, the client calls the SDK method for setting other user attributes to update the nickname. +不需要加密密码,我们的服务端已使用随机生成的 salt,自动对密码做了加密。 如果用户忘记了密码,可以调用 `requestResetPassword` 方法(具体查看 SDK 的 AVUser 用法),向用户注册的邮箱发送邮件,用户以此可自行重设密码。 在整个过程中,密码都不会有明文保存的问题,密码也不会在客户端保存,只是会保存 sessionToken 来标示用户的登录状态。 -### 403 (Forbidden) error after changing password for built-in accounts +### sessionToken 在什么情况下会失效? -If you have checked "Force client to re-login after password change" in Developer Center > Your Game > Game Services > Cloud Services > Built-in Accounts > Settings, then when a user changes their password, their session token will be reset. At this point, you need to prompt the user to log in again; otherwise, they will encounter a 403 (Forbidden) error. \ No newline at end of file +如果在控制台的存储的设置中勾选了「密码修改后,强制客户端重新登录」,则用户修改密码后, sessionToken 会变更,需要重新登录。如果没有勾选这个选项,Token 就不会改变。当新建应用时,这个选项默认是被勾上的。 + +### 在 PC 端用手机号登录,在小程序上用微信登录,如何绑定到同一个账号上? + +从逻辑上,在 PC 端登录的账号,与在小程序中用微信登录的账号,他们没有任何可以联系在一起的地方。如果都是独立创建了两个账号,只能在业务层面进行绑定(也就是将一个账号的所有关联对象全都迁移到另一个账号,然后删除原账号)。 + +如果可以在业务上加一些限制,则可以避免上面这种「创建了两个独立的账号」的情况。比如,如果手机号是账号必须设置的信息,那么我们可以在以手机号作为关联项。具体的步骤如下,首先是 `loginWithWeapp` 并带上 `failOnNotExist` 参数,这样如果该微信关联的用户已经存在则照常登录,如果没有则会失败,此时跳转到使用手机号登录/注册的页面,让用户通过手机号登录或注册,成功之后再通过 `associateWithWeapp` 接口关联当前微信账号。 + +### 不通过短信验证能否强制修改 _User 表 mobilePhoneVerified 字段,使其设置为 true? + +可以通过云引擎使用 [master key](/sdk/engine/functions/sdk/#使用超级权限) 来修改 `mobilePhoneVerified` 的值。因为云引擎运行在可信的服务器端环境中,所以可以全局开启超级权限(Master Key),这样云端会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据,当然这种方式也只允许调用一些仅供 Master Key 使用的 API。 diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/features.mdx deleted file mode 100644 index 318949be8..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/features.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: TDS Authentication Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -## Introduction - -It’s a common practice for games to have an account system that can store players’ data on the cloud. With TDS Authentication, you can easily add a robust and secure account system in your game that allows players to sign in with services like TapTap, Apple, WeChat, QQFacebook, or even as a guest. Using such a system doesn’t require the knowledge of implementing the backend of an account system. You can simply call the APIs provided by the service and utilize all the account-related features by accessing the TDS User object. - -## Use Cases - -- If you don’t have an account system in your game yet, you can quickly build one with TDS Authentication. -- If you already have an account system built on your own, you can still use TDS Authentication for accessing other TDS services (including Data Storage, Friends, and Achievements). - -### Sign-in Methods - -- Guest account -- OAuth with TapTap, Apple, WeChat, QQFacebook, etc. - -## Using the Service - -1. Add TapSDK into your game. -2. Call the APIs provided by the SDK to access the TDS User object. -3. Manage user accounts on the TapTap Developer Center. - -### TDS User - -To access the account system on TDS, the client needs to provide a unique identifier of the player that gets linked to a TDS User on the cloud so that the player can access other TDS services. It’s up to you to decide the form of the identifier. - -### Guest Accounts - -You may allow players to sign in as a guest so that they can access TDS services without having to link an account. - -### Third-Party Sign-on - -To allow players to log in with services like TapTap, Apple, WeChat, and QQand Facebook, you have to obtain an OAuth token from the service provider. If you want to use TapTap Login, please first complete the application process to enable TapTap Login. You can then access the account system on TDS with the OAuth token obtained from TapTap Login. - -## Validating Access Tokens - -When using third-party sign-on, TDS Authentication can help you validate access tokens with service providers to improve the security of the account system of your game. You can enable this feature on **TapTap Developer Center > Game Services > TDS Authentication**. - -![Third-party websites](https://capacity-files.lcfile.com/hcF5qI41k0MVh7xxVQTjQiJtHiYUopuS/io-tdsuser-oauth-providers.png) - - -## Admin Console - -You can manage user accounts by going to the TapTap Developer Center. - -![Managing users](https://capacity-files.lcfile.com/JlrSbqS0DcEHQAWTgMRjoOGR9sj2qBsV/io-lc-users-console.png) - -## TDS Services That Depend on TDS Authentication - -All the cloud services provided by TDS require you to use TDS Authentication. You can browse those services on the Developer Center. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/guide.mdx index 7b321a45a..865d680d3 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/guide.mdx +++ b/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/guide.mdx @@ -1,342 +1,1878 @@ --- -title: TDS Authentication Guide -sidebar_label: Guide +title: 内建账户指南 +sidebar_label: 开发指南 sidebar_position: 2 --- import MultiLang from "/src/docComponents/MultiLang"; import { Conditional } from "/src/docComponents/conditional"; -Starting from TapSDK 3.0, there will be a built-in account system for you to use in your game. You can generate user accounts (`TDSUser`) in your game with the results of TapTap OAuth. You can also link the authentication results of third-party platforms to this account. -The Friends and Achievements services provided by the TapSDK also depend on this account system. +用户系统几乎是每款应用都要加入的功能,我们为此专门提供了一个 `LCUser` 类来方便应用使用各项用户管理的功能。 -## Initialization +`LCUser` 是 `LCObject` 的子类,这意味着任何 `LCObject` 提供的方法也适用于 `LCUser`,唯一的区别就是 `LCUser` 提供一些额外的用户管理相关的功能。 -See [TapSDK Quickstart](/sdk/start/quickstart/) for how to initialize the SDK. +## 用户的属性 -## `TDSUser` and `LCUser` +`LCUser` 相比一个普通的 `LCObject` 多出了以下属性: -`TDSUser` is inherited from the `LCUser` class. -`LCUser` is the account system provided by LeanCloud, and `TDSUser` basically inherited all the interfaces provided by `LCUser`. `TDSUser` includes some minor adjustments we made on functionalities and interfaces to fulfill the needs of TDS, so we recommend that you implement the account system in your game with `TDSUser`. +- `username`:用户的用户名。 +- `password`:用户的密码。 +- `email`:用户的电子邮箱。 +- `emailVerified`:用户的电子邮箱是否已验证。 +- `mobilePhoneNumber`:用户的手机号。 +- `mobilePhoneVerified`:用户的手机号是否已验证。 -## TapTap Login +在接下来对用户功能的介绍中我们会逐一了解到这些属性。 -See [Integrate TapTap Login](/sdk/taptap-login/guide/start/) for more details. +## 注册 -## Guest Login +用户第一次打开应用的时候,可以让用户注册一个账户。下面的代码展示了一个典型的使用用户名和密码注册的流程: -To create a guest account in the account system: + + +<> + +```cs +// 创建实例 +LCUser user = new LCUser(); + +// 等同于 user["username"] = "Tom"; +user.Username = "Tom"; +user.Password = "cat!@#123"; + +// 可选 +user.Email = "tom@xd.com"; +user.Mobile = "+8619201680101"; + +// 设置其他属性的方法跟 LCObject 一样 +user["gender"] = "secret"; +await user.SignUp(); +``` + +新建 `LCUser` 的操作应使用 `SignUp` 而不是 `Save`,但以后的更新操作就可以用 `Save` 了。 + + + +<> + +```java +// 创建实例 +LCUser user = new LCUser(); + +// 等同于 user.put("username", "Tom") +user.setUsername("Tom"); +user.setPassword("cat!@#123"); + +// 可选 +user.setEmail("tom@xd.com"); +user.setMobilePhoneNumber("+8619201680101"); + +// 设置其他属性的方法跟 LCObject 一样 +user.put("gender", "secret"); + +user.signUpInBackground().subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 注册成功 + System.out.println("注册成功。objectId:" + user.getObjectId()); + } + public void onError(Throwable throwable) { + // 注册失败(通常是因为用户名已被使用) + } + public void onComplete() {} +}); +``` + +新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 + + + +<> + +```objc +// 创建实例 +LCUser *user = [LCUser user]; + +// 等同于 [user setObject:@"Tom" forKey:@"username"] +user.username = @"Tom"; +user.password = @"cat!@#123"; + +// 可选 +user.email = @"tom@xd.com"; +user.mobilePhoneNumber = @"+8619201680101"; + +// 设置其他属性的方法跟 LCObject 一样 +[user setObject:@"secret" forKey:@"gender"]; + +[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + if (succeeded) { + // 注册成功 + NSLog(@"注册成功。objectId:%@", user.objectId); + } else { + // 注册失败(通常是因为用户名已被使用) + } +}]; +``` + +新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 + + + +<> + +```swift +do { + // 创建实例 + let user = LCUser() + + // 等同于 user.set("username", value: "Tom") + user.username = LCString("Tom") + user.password = LCString("cat!@#123") + + // 可选 + user.email = LCString("tom@xd.com") + user.mobilePhoneNumber = LCString("+8619201680101") + + // 设置其他属性的方法跟 LCObject 一样 + try user.set("gender", value: "secret") + + _ = user.signUp { (result) in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } + } +} catch { + print(error) +} +``` + +新建 `LCUser` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 + + + +<> + +```dart +// 创建实例 +LCUser user = LCUser(); + +// 等同于 user['username'] = 'Tom'; +user.username = 'Tom'; +user.password = 'cat!@#123'; + +// 可选 +user.email = 'tom@xd.com'; +user.mobile = '+8619201680101'; + +// 设置其他属性的方法跟 LCObject 一样 +user['gender'] = 'secret'; +await user.signUp(); +``` + +新建 `LCUser` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 + + + +<> + +```js +// 创建实例 +const user = new AV.User(); + +// 等同于 user.set('username', 'Tom') +user.setUsername("Tom"); +user.setPassword("cat!@#123"); + +// 可选 +user.setEmail("tom@xd.com"); +user.setMobilePhoneNumber("+8619201680101"); + +// 设置其他属性的方法跟 AV.Object 一样 +user.set("gender", "secret"); + +user.signUp().then( + (user) => { + // 注册成功 + console.log(`注册成功。objectId:${user.id}`); + }, + (error) => { + // 注册失败(通常是因为用户名已被使用) + } +); +``` + +新建 `AV.User` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 + + + +<> + +```python +# 创建实例 +user = leancloud.User() + +# 等同于 user.set('username', 'Tom') +user.set_username('Tom') +user.set_password('cat!@#123') + +# 可选 +user.set_email('tom@xd.com') +user.set_mobile_phone_number('+8619201680101') + +# 设置其他属性的方法跟 leancloud.Object 一样 +user.set('gender', 'secret') + +user.sign_up() +``` + +新建 `leancloud.User` 的操作应使用 `sign_up` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 + + + +<> + +```php +// 创建实例 +$user = new User(); + +// 等同于 $user->set("username", "Tom") +$user->setUsername("Tom"); +$user->setPassword("cat!@#123"); + +// 可选 +$user->setEmail("tom@xd.com"); +$user->setMobilePhoneNumber("+8619201680101"); + +// 设置其他属性的方法跟 LeanObject 一样 +$user->set("gender", "secret"); + +$user->signUp(); +``` + +新建 `User` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 + + + +```go +// 注册用户 +user, err := client.Users.SignUp("Tom", "cat!@#123") +if err != nil { + panic(err) +} + +// 设置其他属性 +if err := client.Users.ID(user.ID).Set("email", "tom@xd.com", leancloud.UseUser(user)); err != nil { + panic(err) +} +``` + + + +如果收到 `202` 错误码,意味着已经存在使用同一 `username` 的账号,此时应提示用户换一个用户名。除此之外,每个用户的 `email` 和 `mobilePhoneNumber` 也需要保持唯一性,否则会收到 `203` 或 `214` 错误。可以考虑在注册时把用户的 `username` 设为与 `email` 相同,这样用户可以直接 [用邮箱重置密码](#重置密码)。 + +采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码(云端对密码的长度、复杂度不作限制),并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调 **在客户端,应用切勿再次对密码加密,这会导致 [重置密码](#重置密码) 等功能失效**。 + +### 手机号注册 + +对于移动应用来说,允许用户以手机号注册是个很常见的需求。实现该功能大致分两步,第一步是让用户提供手机号,点击「获取验证码」按钮后,该号码会收到一个六位数的验证码: + + + +```cs +await LCSMSClient.RequestSMSCode("+8619201680101"); +``` + +```java +LCSMSOption option = new LCSMSOption(); +option.setSignatureName("sign_name"); // 设置短信签名名称 +LCSMS.requestSMSCodeInBackground("+8619201680101", option).subscribe(new Observer() { + @Override + public void onSubscribe(Disposable disposable) { + } + @Override + public void onNext(LCNull avNull) { + Log.d("TAG","Result: succeed to request SMSCode."); + } + @Override + public void onError(Throwable throwable) { + Log.d("TAG","Result: failed to request SMSCode. cause:" + throwable.getMessage()); + } + @Override + public void onComplete() { + } +}); +``` + +```objc +LCShortMessageRequestOptions *options = [[LCShortMessageRequestOptions alloc] init]; +options.templateName = @"template_name"; // 控制台配置好的模板名称 +options.signatureName = @"sign_name"; // 控制台配置好的短信签名名称 +[LCSMS requestShortMessageForPhoneNumber:@"+8619201680101" options:options callback:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + /* 请求成功 */ + } else { + /* 请求失败 */ + } +}]; +``` + +```swift +// templateName 是短信模版名称,signatureName 是短信签名名称。可以在控制台 > 短信 > 设置中查看。 +_ = LCSMSClient.requestShortMessage(mobilePhoneNumber: "+8619201680101", templateName: "template_name", signatureName: "sign_name") { (result) in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } +} +``` + +```dart +await LCSMSClient.requestSMSCode('+8619201680101'); +``` + +```js +AV.Cloud.requestSmsCode("+8619201680101"); +``` + +```python +leancloud.cloud.request_sms_code('+8619201680101') +``` + +```php +SMS::requestSmsCode("+8619201680101"); +``` + +```go +// 暂不支持 +``` + + + +用户填入验证码后,用下面的方法完成注册: + + + +```cs +await LCUser.SignUpOrLoginByMobilePhone("+8619201680101", "123456"); +``` + +```java +LCUser.signUpOrLoginByMobilePhoneInBackground("+8619201680101", "123456").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 注册成功 + System.out.println("注册成功。objectId:" + user.getObjectId()); + } + public void onError(Throwable throwable) { + // 验证码不正确 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser signUpOrLoginWithMobilePhoneNumberInBackground:@"+8619201680101" smsCode:@"123456" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 注册成功 + NSLog(@"注册成功。objectId:%@", user.objectId); + } else { + // 验证码不正确 + } +}]; +``` + +```swift +_ = LCUser.signUpOrLogIn(mobilePhoneNumber: "+8619201680101", verificationCode: "123456", completion: { (result) in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) + } +}) +``` + +```dart + await LCUser.signUpOrLoginByMobilePhone('+8619201680101', '123456'); +``` + +```js +AV.User.signUpOrlogInWithMobilePhone("+8619201680101", "123456").then( + (user) => { + // 注册成功 + console.log(`注册成功。objectId:${user.id}`); + }, + (error) => { + // 验证码不正确 + } +); +``` + +```python +user = leancloud.User.signup_or_login_with_mobile_phone('+8619201680101', '123456') +``` + +```php +User::signUpOrLoginByMobilePhone("+8619201680101", "123456"); +``` + +```go +user, err := client.Users.SignUpByMobilePhone("+8619201680101", "123456") +if err != nil { + panic(err) +} +``` + + + +`username` 将与 `mobilePhoneNumber` 相同,`password` 会由云端随机生成。如果希望让用户指定密码,可以在客户端让用户填写手机号和密码,然后按照上一小节使用用户名和密码注册的流程,将用户填写的手机号作为 `username` 和 `mobilePhoneNumber` 的值同时提交。同时根据业务需求,在**云服务控制台 > 内建账户 > 设置**勾选**未验证手机号码的用户,禁止登录**、**已验证手机号码的用户,允许以短信验证码登录**。 + +### 手机号格式 + +云端接受的手机号以 `+` 和国家代码开头,后面紧跟着剩余的部分。手机号中不应含有任何划线、空格等非数字字符。例如,`+15559463664` 是一个合法的美国或加拿大手机号(`1` 是国家代码),`+8619201680101` 是一个合法的中国手机号(`86` 是国家代码)。 + +请参阅官网的[价格](https://www.leancloud.cn/pricing/)页面以了解支持的国家和地区。 + +## 登录 + +下面的代码用用户名和密码登录一个账户: + + + +```cs +try { + // 登录成功 + LCUser user = await LCUser.Login("Tom", "cat!@#123"); +} catch (LCException e) { + // 登录失败(可能是密码错误) + print($"{e.code} : {e.message}"); +} +``` + +```java +LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 登录成功 + } + public void onError(Throwable throwable) { + // 登录失败(可能是密码错误) + } + public void onComplete() {} +}); +``` + +```objc +[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 登录成功 + } else { + // 登录失败(可能是密码错误) + } +}]; +``` + +```swift +_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +try { + // 登录成功 + LCUser user = await LCUser.login('Tom', 'cat!@#123'); +} on LCException catch (e) { + // 登录失败(可能是密码错误) + print('${e.code} : ${e.message}'); +} +``` + +```js +AV.User.logIn("Tom", "cat!@#123").then( + (user) => { + // 登录成功 + }, + (error) => { + // 登录失败(可能是密码错误) + } +); +``` + +```python +user = leancloud.User() +user.login(username='Tom', password='cat!@#123') +``` + +```php +User::logIn("Tom", "cat!@#123"); +``` + +```go +user, err := client.Users.LogIn("Tom", "cat!@#123") +if err != nil { + panic(err) +} +``` + + + +### 邮箱登录 + +下面的代码用邮箱和密码登录一个账户: + + + +```cs +try { + // 登录成功 + LCUser user = await LCUser.LoginByEmail("tom@xd.com", "cat!@#123"); +} catch (LCException e) { + // 登录失败(可能是密码错误) + print($"{e.code} : {e.message}"); +} +``` + +```java +LCUser.loginByEmail("tom@xd.com", "cat!@#123").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 登录成功 + } + public void onError(Throwable throwable) { + // 登录失败(可能是密码错误) + } + public void onComplete() {} +}); +``` + +```objc +[LCUser loginWithEmail:@"tom@xd.com" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 登录成功 + } else { + // 登录失败(可能是密码错误) + } +}]; +``` + +```swift +_ = LCUser.logIn(email: "tom@xd.com", password: "cat!@#123") { result in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +try { + // 登录成功 + LCUser user = await LCUser.loginByEmail('tom@xd.com', 'cat!@#123'); +} on LCException catch (e) { + // 登录失败(可能是密码错误) + print('${e.code} : ${e.message}'); +} +``` + +```js +AV.User.loginWithEmail("tom@xd.com", "cat!@#123").then( + (user) => { + // 登录成功 + }, + (error) => { + // 登录失败(可能是密码错误) + } +); +``` + +```python +user = leancloud.User() +user.login(email='tom@xd.com', password='cat!@#123') +``` + +```php +User::logInWithEmail("tom@xd.com", "cat!@#123"); +``` + +```go +user, err := client.LoginByEmail("tom@xd.com", "cat!@#123") +if err != nil { + panic(err) +} + +fmt.Println(user) +``` + + + +### 手机号登录 + +如果应用允许用户以手机号注册,那么也可以让用户以手机号配合密码或短信验证码登录。下面的代码用手机号和密码登录一个账户: + + + +```cs +try { + // 登录成功 + LCUser user = await LCUser.LoginByMobilePhoneNumber("+8619201680101", "cat!@#123"); +} catch (LCException e) { + // 登录失败(可能是密码错误) + print($"{e.code} : {e.message}"); +} +``` + +```java +LCUser.loginByMobilePhoneNumber("+8619201680101", "cat!@#123").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 登录成功 + } + public void onError(Throwable throwable) { + // 登录失败(可能是密码错误) + } + public void onComplete() {} +}); +``` + +```objc +[LCUser logInWithMobilePhoneNumberInBackground:@"+8619201680101" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 登录成功 + } else { + // 登录失败(可能是密码错误) + } +}]; +``` + +```swift +_ = LCUser.logIn(mobilePhoneNumber: "+8619201680101", password: "cat!@#123") { result in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +try { + // 登录成功 + LCUser user = await LCUser.loginByMobilePhoneNumber('+8619201680101', 'cat!@#123'); +} on LCException catch (e) { + // 登录失败(可能是密码错误) + print('${e.code} : ${e.message}'); +} +``` + +```js +AV.User.logInWithMobilePhone("+8619201680101", "cat!@#123").then( + (user) => { + // 登录成功 + }, + (error) => { + // 登录失败(可能是密码错误) + } +); +``` + +```python +user = leancloud.User.login_with_mobile_phone('+8619201680101', 'cat!@#123') +``` + +```php +User::logInWithMobilePhoneNumber("+8619201680101", "cat!@#123"); +``` + +```go +user, err := client.LogInByMobilePhoneNumber("+8619201680101", "cat!@#123") +if err != nil { + panic(err) +} + +fmt.Println(user) +``` + + + +默认情况下,云服务允许所有关联了手机号的用户直接以手机号登录,无论手机号是否 [通过验证](#验证手机号)。为了让应用更加安全,你可以选择只允许验证过手机号的用户通过手机号登录。可以在 **控制台 > 内建账户 > 设置** 里面开启该功能。 + +除此之外,还可以让用户通过短信验证码登录,适用于用户忘记密码且不愿重置密码的情况。和 [通过手机号注册](#手机号注册) 的步骤类似,首先让用户填写与账户关联的手机号码,然后在用户点击「获取验证码」后调用下面的方法: + + + +```cs +await LCUser.RequestLoginSMSCode("+8619201680101"); +``` + +```java +LCUser.requestLoginSmsCodeInBackground("+8619201680101").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser requestLoginSmsCode:@"+8619201680101"]; +``` + +```swift +_ = LCUser.requestLoginVerificationCode(mobilePhoneNumber: "+8619201680101") { result in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } +} +``` + +```dart +await LCUser.requestLoginSMSCode('+8619201680101'); +``` + +```js +AV.User.requestLoginSmsCode("+8619201680101"); +``` + +```python +leancloud.User.request_login_sms_code('+8619201680101') +``` + +```php +SMS::requestSmsCode("+8619201680101"); +``` + +```go +if err := client.Users.RequestLoginSMSCode("+8619201680101"); err != nil { + panic(err) +} +``` + + + +用户填写收到的验证码后,用下面的方法完成登录: + + + +```cs +try { + // 登录成功 + await LCUser.SignUpOrLoginByMobilePhone("+8619201680101", "123456"); +} catch (LCException e) { + // 验证码不正确 + print($"{e.code} : {e.message}"); +} +``` + +```java +LCUser.signUpOrLoginByMobilePhoneInBackground("+8619201680101", "123456").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 登录成功 + } + public void onError(Throwable throwable) { + // 验证码不正确 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser logInWithMobilePhoneNumberInBackground:@"+8619201680101" smsCode:@"123456" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 登录成功 + } else { + // 验证码不正确 + } +}]; +``` + +```swift +_ = LCUser.logIn(mobilePhoneNumber: "+8619201680101", verificationCode: "123456") { result in + switch result { + case .success(object: let user): + print(user) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +try { + // 登录成功 + await LCUser.signUpOrLoginByMobilePhone('+8619201680101', '123456'); +} on LCException catch (e) { + // 验证码不正确 + print('${e.code} : ${e.message}'); +} +``` + +```js +AV.User.logInWithMobilePhoneSmsCode("+8619201680101", "123456").then( + (user) => { + // 登录成功 + }, + (error) => { + // 验证码不正确 + } +); +``` + +```python +user = leancloud.User.signup_or_login_with_mobile_phone('+8619201680101', '123456') +``` + +```php +User::signUpOrLoginByMobilePhone("+8619201680101", "123456"); +``` + +```go +user, err := client.Users.LogInByMobilePhoneNumber("+8619201680101", "123456") +if err != nil { + panic(err) +} +``` + + + +### 测试手机号和固定验证码 + +在开发过程中,可能会因测试目的而需要频繁地用手机号注册登录,然而运营商的发送频率限制往往会导致测试过程耗费较多的时间。 + +为了解决这个问题,可以在 **控制台 > 短信 > 设置** 里面设置一个测试手机号,而云端会为该号码生成一个固定验证码。以后进行登录操作时,只要使用的是这个号码,云端就会直接放行,无需经过运营商网络。 + +测试手机号还可用于将 iOS 应用提交到 App Store 进行审核的场景,因为审核人员可能因没有有效的手机号码而无法登录应用来进行评估审核。如果不提供一个测试手机号,应用有可能被拒绝。 + +可参阅 [短信 SMS 服务使用指南](/sdk/sms/guide/) 来了解更多有关短信发送和接收的限制。 + +### 单设备登录 + +某些场景下需要确保用户的账户在同一时间只在一台设备上登录,也就是说当用户在一台设备上登录后,其他设备上的会话全部失效。可以按照以下方案来实现: + +1. 新建一个专门用于记录用户登录信息和当前设备信息的 class。 +2. 每当用户在新设备上登录时,将该 class 中该用户对应的设备更新为该设备。 +3. 在另一台设备上打开客户端时,检查该设备是否与云端保存的一致。若不一致,则将用户 [登出](#当前用户)。 + +### 账户锁定 + +输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 `{ "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." }`,开发者可在客户端进行必要提示。 + +锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 + +## 验证邮箱 + +可以通过要求用户在登录或使用特定功能之前验证邮箱的方式防止恶意注册。默认情况下,当用户注册或变更邮箱后,`emailVerified` 会被设为 `false`。在应用的 **云服务控制台 > 内建账户 > 设置** 中,可以开启 **启用邮箱验证功能** 选项,这样当用户注册或变更邮箱时,会收到一封含有验证链接的邮件。在同一设置页面还可找到阻止未验证邮箱的用户登录的选项。 + +如果用户忘记点击链接并且在未来某一时刻需要进行验证,可以用下面的代码发送一封新的邮件: + + + +```cs +await LCUser.RequestEmailVerify("tom@xd.com"); +``` + +```java +LCUser.requestEmailVerifyInBackground("tom@xd.com").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser requestEmailVerify:@"tom@xd.com"]; +``` + +```swift +_ = LCUser.requestVerificationMail(email: "tom@xd.com") { result in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } +} +``` + +```dart +await LCUser.requestEmailVerify('tom@xd.com'); +``` + +```js +AV.User.requestEmailVerify("tom@xd.com"); +``` + +```python +leancloud.User.request_email_verify('tom@xd.com') +``` + +```php +User::requestEmailVerify("tom@xd.com"); +``` + +```go +if err := client.Users.RequestEmailVerify("tom@xd.com"); err != nil { + panic(err) +} +``` + + + +用户点击邮件内的链接后,`emailVerified` 会变为 `true`。如果用户的 `email` 属性为空,则该属性永远不会为 `true`。 + +## 验证手机号 + +和 [验证邮箱](#验证邮箱) 类似,应用还可以要求用户在登录或使用特定功能之前验证手机号。默认情况下,当用户注册或变更手机号后,`mobilePhoneVerified` 会被设为 `false`。在应用的 **控制台 > 内建账户 > 设置** 中,可以开启阻止未验证手机号的用户登录的选项。 + +可以用下面的代码发送一条新的验证码(如果相应用户的 `mobilePhoneVerified` 已经为 `true`,那么验证短信不会发送): + + + +```cs +await LCUser.RequestMobilePhoneVerify("+8619201680101"); +``` + +```java +LCUser.requestMobilePhoneVerifyInBackground("+8619201680101").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser requestMobilePhoneVerify:@"+8619201680101" withBlock:^(BOOL succeeded, NSError * _Nullable error) { + if(succeeded){ + // 请求成功 + }else{ + // 请求失败 + } +}]; +``` + +```swift +_ = LCUser.requestVerificationCode(mobilePhoneNumber: "+8619201680101") { result in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } +} +``` + +```dart +await LCUser.requestMobilePhoneVerify('+8619201680101'); +``` + +```js +AV.User.requestMobilePhoneVerify("+8619201680101"); +``` + +```python +leancloud.User.request_mobile_phone_verify('+8619201680101') +``` + +```php +User::requestMobilePhoneVerify("+8619201680101"); +``` + +```go +if err := client.Users.RequestMobilePhoneVerify("+8619201680101"); err != nil { + panic(err) +} +``` + + + +用户填写验证码后,调用下面的方法来完成验证。`mobilePhoneVerified` 将变为 `true`: -```cs -try{ - // tdsUSer will hold a unique identifier of the user, if it exists - var tdsUser = await TDSUser.LoginAnonymously(); -}catch(Exception e){ - // Failed to log in - Debug.Log($"{e.code} : {e.message}"); +```cs +await LCUser.VerifyMobilePhone("+8619201680101", "123456"); +``` + +```java +LCUser.verifyMobilePhoneInBackground("123456").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // mobilePhoneVerified 将变为 true + } + public void onError(Throwable throwable) { + // 验证码不正确 + } + public void onComplete() {} +}); +``` + +```objc +[LCUser verifyMobilePhone:@"123456" withBlock:^(BOOL succeeded, NSError * _Nullable error) { + if(succeeded){ + // mobilePhoneVerified 将变为 true + }else{ + // 验证码不正确 + } +}]; +``` + +```swift +_ = LCUser.verifyMobilePhoneNumber(mobilePhoneNumber: "+8619201680101", verificationCode: "123456") { result in + switch result { + case .success: + // mobilePhoneVerified 将变为 true + break + case .failure(error: let error): + // 验证码不正确 + print(error) + } } ``` -```java -TDSUser.logInAnonymously().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { +```dart +await LCUser.verifyMobilePhone('+8619201680101','123456'); +``` +```js +AV.User.verifyMobilePhone("123456").then( + () => { + // mobilePhoneVerified 将变为 true + }, + (error) => { + // 验证码不正确 } +); +``` - @Override - public void onNext(TDSUser resultUser) { - // Successfully logged in and obtained an account instance - String userId = resultUser.getObjectId(); - } +```python +leancloud.User.verify_mobile_phone_number('123456') +``` - @Override - public void onError(Throwable throwable) { +```php +User::verifyMobilePhone("123456"); +``` - } +```go +// 暂不支持 +``` - @Override - public void onComplete() { - } + + +### 绑定、修改手机号之前先验证 + +除了在用户绑定、修改手机号**之后**进行验证,云服务也支持在用户绑定或修改手机号**之前**先通过短信验证。也就是说,绑定手机号或修改手机号时先请求发送验证码(用户需处于登录状态),再凭短信验证码完成绑定或修改操作。 + + + +```cs +await LCUser.RequestSMSCodeForUpdatingPhoneNumber("+8619201680101"); + +await LCUser.VerifyCodeForUpdatingPhoneNumber("+8619201680101", "123456"); +// 更新本地数据 +LCUser currentUser = await LCUser.GetCurrent(); +user.Mobile = "+8619201680101"; +``` + +```java +LCUser.requestSMSCodeForUpdatingPhoneNumberInBackground("+8619201680101",null).subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + } + @Override + public void onNext(@NonNull LCNull lcNull) { + // 成功调用 + } + @Override + public void onError(@NonNull Throwable e) { + // 调用出错 + } + @Override + public void onComplete() { + } +}); + +LCUser.verifySMSCodeForUpdatingPhoneNumberInBackground("123456", "+8619201680101").subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + } + @Override + public void onNext(@NonNull LCNull lcNull) { + // 更新本地数据 + LCUser currentUser = LCUser.getCurrentUser(); + currentUser.setMobilePhoneNumber("+8619201680101"); + } + @Override + public void onError(@NonNull Throwable e) { + // 验证码不正确 + } + @Override + public void onComplete() { + } }); ``` -```objectivec -[TDSUser loginAnonymously:^(TDSUser * _Nullable user, NSError * _Nullable error) { - if (user) { - NSString *userId = user.objectId; +```objc +[LCUser requestVerificationCodeForUpdatingPhoneNumber:@"+8619201680101" withBlock:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + // 请求成功 + } else { + // 请求失败 + } +}]; + +[LCUser verifyCodeToUpdatePhoneNumber:@"+8619201680101" code:@"123456" withBlock:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + // mobilePhoneNumber 变为 +8619201680101 + // mobilePhoneVerified 变为 true } else { - NSLog(@"%@", error); + // 验证码不正确 } }]; ``` - +```swift +_ = LCUser.requestVerificationCode(forUpdatingMobilePhoneNumber: "+8619201680101") { result in + switch result { + case .success: + break + case .failure(error: let error): + print(error) + } +} + +_ = LCUser.verifyVerificationCode("123456", toUpdateMobilePhoneNumber:"+8619201680101") { result in + switch result { + case .success: + // mobilePhoneNumber 变为 +8619201680101 + // mobilePhoneVerified 变为 true + break + case .failure(error: let error): + // 验证码不正确 + print(error) + } +} +``` + +```dart +await LCUser.requestSMSCodeForUpdatingPhoneNumber('+8619201680101'); + +await LCUser.verifyCodeForUpdatingPhoneNumber('+8619201680101', '123456'); +// 更新本地数据 +LCUser currentUser = await LCUser.getCurrent(); +user.mobile = '+8619201680101'; +``` + +```js +AV.User.requestChangePhoneNumber("+8619201680101"); + +AV.User.changePhoneNumber("+8619201680101", "123456").then( + () => { + // 更新本地数据 + const currentUser = AV.User.current(); + currentUser.setMobilePhoneNumber("+8619201680101"); + }, + (error) => { + // 验证码不正确 + } +); +``` + +```python +User.request_change_phone_number("+8619201680101") + +User.change_phone_number("123456", "+8619201680101") +# 更新本地数据 +current_user = leancloud.User.get_current() +current_user.set_mobile_phone_number("+8619201680101") +``` + +```php +User::requestChangePhoneNumber("+8619201680101"); + +User::changePhoneNumber("123456", "+8619201680101"); +// 更新本地数据 +$currentUser = User::getCurrentUser(); +$user->setMobilePhoneNumber("+8619201680101"); +``` -:::info +```go +if err := client.Users.requestChangePhoneNumber("+8619201680101"); err != nil { + panic(err) +} -The guest account ensures that the player will have access to the same account on different logins. However, if the player deletes the game and then reinstalls the game, it is not guaranteed that the player will still have access to the same account. +if err := client.Users.ChangePhoneNumber("123456", "+8619201680101"); err != nil { + panic(err) +} +``` -::: + -## Current User +## 当前用户 -Once the user has logged in, the SDK will automatically save the session to the client so that the user won’t need to log in again the next time they open the client. The code below checks if there is a logged-in user: +用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户: ```cs -TDSUser currentUser = await TDSUser.GetCurrent(); +LCUser currentUser = await LCUser.GetCurrent(); if (currentUser != null) { - // Go to homepage + // 跳到首页 } else { - // Show the sign-up or the log-in page + // 显示注册或登录页面 } ``` ```java -TDSUser currentUser = TDSUser.getCurrentUser(); +LCUser currentUser = LCUser.getCurrentUser(); if (currentUser != null) { - // Go to homepage + // 跳到首页 } else { - // Show the sign-up or the log-in page + // 显示注册或登录页面 } ``` ```objc -TDSUser *currentUser = [TDSUser currentUser]; +LCUser *currentUser = [LCUser currentUser]; if (currentUser != nil) { - // Go to homepage + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```swift +if let user = LCApplication.default.currentUser { + // 跳到首页 } else { - // Show the sign-up or the log-in page + // 显示注册或登录页面 } ``` +```dart +LCUser currentUser = await LCUser.getCurrent(); +if (currentUser != null) { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```js +const currentUser = AV.User.current(); +if (currentUser) { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```python +current_user = leancloud.User.get_current() +if current_user is not None: + # 跳到首页 + pass +else: + # 显示注册或登录页面 + pass +``` + +```php +$currentUser = User::getCurrentUser(); +if ($currentUser != null) { + // 跳到首页 +} else { + // 显示注册或登录页面 +} +``` + +```go +// 暂不支持 +``` + -The session will remain valid until the user logs out: +会话信息会长期有效,直到用户主动登出: ```cs -await TDSUser.Logout(); +await LCUser.Logout(); -// currentUser becomes null -TDSUser currentUser = await TDSUser.GetCurrent(); +// currentUser 变为 null +LCUser currentUser = await LCUser.GetCurrent(); ``` ```java -TDSUser.logOut(); +LCUser.logOut(); -// currentUser becomes null -TDSUser currentUser = TDSUser.getCurrentUser(); +// currentUser 变为 null +LCUser currentUser = LCUser.getCurrentUser(); ``` ```objc -[TDSUser logOut]; +[LCUser logOut]; + +// currentUser 变为 nil +LCUser *currentUser = [LCUser currentUser]; +``` + +```swift +LCUser.logOut() + +// currentUser 变为 nil +let currentUser = LCApplication.default.currentUser +``` + +```dart +await LCUser.logout(); + +// currentUser 变为 null +LCUser currentUser = await LCUser.getCurrent(); +``` + +```js +AV.User.logOut(); + +// currentUser 变为 null +const currentUser = AV.User.current(); +``` + +```python +user.logout() + +current_user = leancloud.User.get_current() # None +``` + +```php +User::logOut(); -// currentUser becomes nil -TDSUser *currentUser = [TDSUser currentUser]; +// currentUser 变为 null +$currentUser = User::getCurrentUser(); +``` + +```go +// 暂不支持 ``` -## Setting the Current User +## 设置当前用户 -A **session token** will be returned to the client after a user is logged in. It will be cached by our SDK and will be used for authenticating requests made by the same `TDSUser` in the future. The session token will be included in the header of each HTTP request made by the client, which helps the cloud identify the `TDSUser` sending the request. +用户登录后,云端会返回一个 **session token** 给客户端,它会由 SDK 缓存起来并用于日后同一用户的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个用户发起的请求了。 -Below are the situations when you may need to log a user in with their session token: +以下是一些应用可能需要用到 session token 的场景: -- A session token is already cached on the client which can be used to automatically log the user in (you can get the session token of the current user by accessing the `sessionToken` property; you can also get the `sessionToken` of any user on the server side with your Master Key (also called Server Secret)). -- A WebView within the app needs to know the current user. -- The user is logged in on the server side using your own authentication routines and the server is able to provide the session token to the client. +- 应用根据以前缓存的 session token 登录。 +- 应用内的某个 WebView 需要知道当前登录的用户。 +- 在服务端登录后,返回 session token 给客户端,客户端根据返回的 session token 登录。 -The code below logs a user in with a session token (the session token will be validated before proceeding): +下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效): ```cs -await TDSUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf"); +await LCUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf"); ``` ```java -TDSUser.becomeWithSessionTokenInBackground("anmlwi96s381m6ca7o7266pzf").subscribe(new Observer() { +LCUser.becomeWithSessionTokenInBackground("anmlwi96s381m6ca7o7266pzf").subscribe(new Observer() { public void onSubscribe(Disposable disposable) {} - public void onNext(TDSUser user) { - // Update currentUser - TDSUser.changeCurrentUser(user, true); + public void onNext(LCUser user) { + // 修改 currentUser + LCUser.changeCurrentUser(user, true); } public void onError(Throwable throwable) { - // session token is invalid + // session token 无效 } public void onComplete() {} }); ``` ```objc -[TDSUser becomeWithSessionTokenInBackground:@"anmlwi96s381m6ca7o7266pzf" block:^(TDSUser * _Nullable user, NSError * _Nullable error) { +[LCUser becomeWithSessionTokenInBackground:@"anmlwi96s381m6ca7o7266pzf" block:^(LCUser * _Nullable user, NSError * _Nullable error) { if (user != nil) { - // Successfully logged in + // 登录成功 } else { - // session token is invalid + // session token 无效 } }]; ``` +```swift +_ = LCUser.logIn(sessionToken: "anmlwi96s381m6ca7o7266pzf") { (result) in + switch result { + case .success(object: let user): + // 登录成功 + print(user) + case .failure(error: let error): + // session token 无效 + print(error) + } +} +``` + +```dart +await LCUser.becomeWithSessionToken('anmlwi96s381m6ca7o7266pzf'); +``` + +```js +AV.User.become("anmlwi96s381m6ca7o7266pzf").then( + (user) => { + // 登录成功 + }, + (error) => { + // session token 无效 + } +); +``` + +```python +user = leancloud.User.become('anmlwi96s381m6ca7o7266pzf') +``` + +```php +User::become("anmlwi96s381m6ca7o7266pzf"); +``` + +```go +user, err := client.Users.Become("anmlwi96s381m6ca7o7266pzf") +if err != nil { + panic(err) +} +``` + -For security reasons, please avoid passing URLs containing session tokens in non-private environments. This increases the risk of your session tokens being captured by attackers. +请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。 -If **Log out the user when password is updated** is enabled on **Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings**, the session token of a user will be reset in the cloud after this user changes the password and the client needs to prompt the user to log in again. Otherwise, `403 (Forbidden)` will be returned as an error. +如果在 **控制台 > 内建账户 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 `403 (Forbidden)` 错误。 -The code below checks if a session token is valid: +下面的代码检查 session token 是否有效: ```cs -TDSUser currentUser = await TDSUser.GetCurrent(); +LCUser currentUser = await LCUser.GetCurrent(); bool isAuthenticated = await currentUser.IsAuthenticated(); if (isAuthenticated) { - // session token is valid + // session token 有效 } else { - // session token is invalid + // session token 无效 } ``` ```java -boolean authenticated = TDSUser.getCurrentUser().isAuthenticated(); +boolean authenticated = LCUser.getCurrentUser().isAuthenticated(); if (authenticated) { - // session token is valid + // session token 有效 } else { - // session token is invalid + // session token 无效 } ``` ```objc -TDSUser *currentUser = [TDSUser currentUser]; +LCUser *currentUser = [LCUser currentUser]; NSString *token = currentUser.sessionToken; [currentUser isAuthenticatedWithSessionToken:token callback:^(BOOL succeeded, NSError * _Nullable error) { if (succeeded) { - // session token is valid + // session token 有效 } else { - // session token is invalid + // session token 无效 } }]; ``` +```swift +if let sessionToken = LCApplication.default.currentUser?.sessionToken?.value { + _ = LCUser.logIn(sessionToken: sessionToken) { (result) in + if result.isSuccess { + // session token 有效 + } else { + // session token 无效 + } + } +} +``` + +```dart +LCUser currentUser = await LCUser.getCurrent(); +bool isAuthenticated = await currentUser.isAuthenticated(); +if (isAuthenticated) { + // session token 有效 +} else { + // session token 无效 +} +``` + +```js +const currentUser = AV.User.current(); +currentUser.isAuthenticated().then((authenticated) => { + if (authenticated) { + // session token 有效 + } else { + // session token 无效 + } +}); +``` + +```python +authenticated = leancloud.User.get_current().is_authenticated() +if authenticated: + # session token 有效 + pass +else: + # session token 无效 + pass +``` + +```php +$authenticated = User::isAuthenticated(); +if ($authenticated) { + // session token 有效 +} else { + // session token 无效 +} +``` + +```go +// 暂不支持 +``` + -## Setting Other User Properties +## 重置密码 + +我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了多种重置密码的方法。 + +邮箱重置密码的流程如下: + +1. 用户输入注册的电子邮箱,请求重置密码; +2. 云端向该邮箱发送一封包含重置密码的特殊链接的电子邮件; +3. 用户点击重置密码链接后,一个特殊的页面会打开,让他们输入新密码; +4. 用户的密码已被重置为新输入的密码。 -The account system allows you to store `nickname` and `avatar` data associated with users. For example, you can store users’ nicknames by using `nickname` field. +首先让用户填写注册账户时使用的邮箱,然后调用下面的方法: ```cs -var currentUser = await TDSUser.GetCurrent(); // Get the instance of the current user -currentUser["nickname"] = "Tarara"; -await currentUser.Save(); +await LCUser.RequestPasswordReset("tom@xd.com"); ``` ```java -TDSUser currentUser = TDSUser.currentUser(); // Get the instance of the current user -currentUser.put("nickname", "Tarara"); -currentUser.saveInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable d) { +LCUser.requestPasswordResetInBackground("tom@xd.com").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 + } + public void onComplete() {} +}); +``` +```objc +[LCUser requestPasswordResetForEmailInBackground:@"tom@xd.com"]; +``` + +```swift +_ = LCUser.requestPasswordReset(email: "tom@xd.com") { (result) in + switch result { + case .success: + break + case .failure(error: let error): + print(error) } +} +``` - @Override - public void onNext(@NotNull LCObject lcObject) { - // Saved; the properties of currentUser are updated - TDSUser tdsUser = (TDSUser) lcObject; +```dart +await LCUser.requestPasswordReset('tom@xd.com'); +``` + +```js +AV.User.requestPasswordReset("tom@xd.com"); +``` + +```python +leancloud.User.request_password_reset('tom@xd.com') +``` + +```php +User::requestPasswordReset("tom@xd.com"); +``` + +```go +if err := client.Users.RequestPasswordReset("tom@xd.com"); err != nil { + panic(err) +} +``` + + + +上面的代码会查询是否有用户的 `email` 属性与前面提供的邮箱匹配。如果有的话,则向该邮箱发送一封密码重置邮件。之前提到过,应用可以让 `username` 与 `email` 保持一致,也可以单独收集用户的邮箱并将其存为 `email`。 + +密码重置邮件的内容可在应用的 **云服务控制台 > 内建账户 > 邮件模版** 中自定义。更多关于自定义邮件模板和验证链接的内容,请参考《自定义邮件验证和重设密码页面》。 + +除此之外,还可以用手机号重置密码: + +1. 用户输入注册的手机号,请求重置密码; +2. 云端向该号码发送一条包含验证码的短信; +3. 用户输入验证码和新密码。 + +下面的代码向用户发送含有验证码的短信: + + + +```cs +await LCUser.RequestPasswordRestBySmsCode("+8619201680101"); +``` + +```java +LCUser.requestPasswordResetBySmsCodeInBackground("+8619201680101").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 成功调用 + } + public void onError(Throwable throwable) { + // 调用出错 } + public void onComplete() {} +}); +``` - @Override - public void onError(@NotNull Throwable e) { +```objc +[LCUser requestPasswordResetWithPhoneNumber:@"+8619201680101"]; +``` +```swift +_ = LCUser.requestPasswordReset(mobilePhoneNumber: "+8619201680101") { (result) in + switch result { + case .success: + break + case .failure(error: let error): + print(error) } +} +``` - @Override - public void onComplete() { +```dart +await LCUser.requestPasswordRestBySmsCode('+8619201680101'); +``` + +```js +AV.User.requestPasswordResetBySmsCode("+8619201680101"); +``` + +```python +leancloud.User.request_password_reset_by_sms_code('+8619201680101') +``` + +```php +User::requestPasswordResetBySmsCode("+8619201680101"); +``` + +```go +if err := client.Users.RequestPasswordResetBySmsCode("+8619201680101"); err != nil { + panic(err) +} +``` + + + +上面的代码会查询是否有用户的 `mobilePhoneNumber` 属性与前面提供的手机号匹配。如果有的话,则向该号码发送验证码短信。 + +可以在 **云服务控制台 > 内建账户 > 设置** 中设置只有在 `mobilePhoneVerified` 为 `true` 的情况下才能用手机号重置密码。 + +用户输入验证码和新密码后,用下面的代码完成密码重置: + + +```cs +await LCUser.ResetPasswordBySmsCode("+8619201680101", "123456", "cat!@#123"); +``` + +```java +LCUser.resetPasswordBySmsCodeInBackground("123456", "cat!@#123").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCNull null) { + // 密码重置成功 + } + public void onError(Throwable throwable) { + // 验证码不正确 } + public void onComplete() {} }); ``` -```objectivec -TDSUser *currentUser = [TDSUser currentUser]; -currentUser[@"nickname"] = @"Tarara"; -[currentUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) { +```objc +[LCUser resetPasswordWithSmsCode:@"123456" newPassword:@"cat!@#123" block:^(BOOL succeeded, NSError *error) { if (succeeded) { - // Saved + // 密码重置成功 } else { - NSLog(@"%@", error); + // 验证码不正确 } }]; ``` - +```swift +_ = LCUser.resetPassword(mobilePhoneNumber: "+8619201680101", verificationCode: "123456", newPassword: "cat!@#123") { result in + switch result { + case .success: + // 密码重置成功 + break + case .failure(error: let error): + // 验证码不正确 + print(error) + } +} +``` + +```dart +await LCUser.resetPasswordBySmsCode('+8619201680101', '123456', 'cat!@#123'); +``` + +```js +AV.User.resetPasswordBySmsCode("123456", "cat!@#123").then( + () => { + // 密码重置成功 + }, + (error) => { + // 验证码不正确 + } +); +``` + +```python +leancloud.User.reset_password_by_sms_code('123456', 'cat!@#123') +``` + +```php +User::resetPasswordBySmsCode("123456", "cat!@#123"); +``` + +```go +if err := client.Users.ResetPasswordBySmsCode("+8619201680101", "123456", "cat!@#123"); err != nil { + panic(err) +} +``` + + + +## 用户的查询 + +使用下面的代码来查询用户: + + + +```cs +LCQuery userQuery = LCUser.GetQuery(); +``` + +```java +LCQuery userQuery = LCUser.getQuery(); +``` -The account system supports only two extra fields besides the built-in ones: `nickname` and `avatar`. Adding other new fields will cause an error. +```objc +LCQuery *userQuery = [LCUser query]; +``` -The account system contains users’ authentication data as well as emails and phone numbers, so there will be strict permission settings imposed on it to prevent the leak of the data. +```swift +let userQuery = LCQuery(className: "_User") +``` -Besides the security concerns, having too much data in the account system can also lead to performance issues like the occurrence of slow queries. +```dart +LCQuery userQuery = LCUser.getQuery(); +``` -Therefore, we restrict the use of fields. If you want to store other user information, we suggest that you create a dedicated class (like `UserProfile`) to store it. +```js +const userQuery = new AV.Query("_User"); +``` -:::tip -We recommend that you store users’ nicknames with the `nickname` field because [TDS’s Friends module](/sdk/friends/guide/) **uses the data in this field when looking up friends with nicknames or generating invitation links**. +```python +user_query = leancloud.Query('_leancloud.User') +``` -If you log a user in [with the result of TapTap OAuth](/sdk/taptap-login/guide/start/#quick-log-in-with-taptap), the SDK will automatically set the `nickname` of the user to be the username of their TapTap account. -::: +```php +$userQuery = new Query("_User"); +``` -## Queries on Users +```go +userQuery := client.Users.NewUserQuery() +``` -`TDSUser` is a subclass of `LCObject`. This means that you can create, read, update, and delete user objects in the same way as you do with `LCObject`s. See [Data Storage Overview](/sdk/storage/features/) for more details. + -For security reasons, **the account system (the `_User` table) has its `find` permission disabled by default**. Each user can only access their own data in the `_User` table and cannot access that of others. If you need to allow each user to view other users’ data, we recommend that you create a new table to store such data and enable the `find` permission of this table. You may also encapsulate queries on users within Cloud Engine and avoid opening up `find` permissions of `_User` tables. +为了安全起见,**新创建的应用的 `_User` 表默认关闭了 `find` 权限**,这样每位用户登录后只能查询到自己在 `_User` 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 `find` 查询权限。除此之外,还可以在 [云引擎](/sdk/engine/overview) 里封装用户查询相关的方法。 -See [Security of User Objects](#security-of-user-objects) for other restrictions applied to the `_User` table and [Data Security](/sdk/storage/guide/security/) for more information regarding class-level permission settings. +可以参见 [用户对象的安全](#用户对象的安全) 来了解 `_User` 表的一些限制,还可以阅读[《数据和安全》](/sdk/storage/guide/security)来了解更多 class 级权限设置的方法。 -## Associations +## 关联用户对象 -Associations involving `TDSUser`s work in the same way as that of `LCObject`s. The code below saves a new book for an author and retrieves all the books written by that author: +关联用户的方法和对象是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书: ```cs LCObject book = new LCObject("Book"); -TDSUser author = await LCUser.GetCurrent(); -book["title"] = "My Fifth Book"; +LCUser author = await LCUser.GetCurrent(); +book["title"] = "我的第五本书"; book["author"] = author; await book.Save(); LCQuery query = new LCQuery("Book"); query.WhereEqualTo("author", author); -// books is an array of Book objects by the same author +// books 是包含同一作者所有 Book 对象的数组 ReadOnlyCollection books = await query.Find(); ``` ```java LCObject book = new LCObject("Book"); -TDSUser author = TDSUser.getCurrentUser(); -book.put("title", "My Fifth Book"); +LCUser author = LCUser.getCurrentUser(); +book.put("title", "我的第五本书"); book.put("author", author); book.saveInBackground().subscribe(new Observer() { public void onSubscribe(Disposable disposable) {} public void onNext(LCObject book) { - // Find all the books by the same author + // 获取所有该作者写的书 LCQuery query = new LCQuery<>("Book"); query.whereEqualTo("author", author); query.findInBackground().subscribe(new Observer>() { public void onSubscribe(Disposable disposable) {} public void onNext(List books) { - // books is an array of Book objects by the same author + // books 是包含同一作者所有 Book 对象的数组 } public void onError(Throwable throwable) {} public void onComplete() {} @@ -349,45 +1885,139 @@ book.saveInBackground().subscribe(new Observer() { ```objc LCObject *book = [LCObject objectWithClassName:@"Book"]; -TDSUser *author = [TDSUser currentUser]; -[book setObject:@"My Fifth Book" forKey:@"title"]; +LCUser *author = [LCUser currentUser]; +[book setObject:@"我的第五本书" forKey:@"title"]; [book setObject:author forKey:@"author"]; [book saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - // Find all the books by the same author + // 获取所有该作者写的书 LCQuery *query = [LCQuery queryWithClassName:@"Book"]; [query whereKey:@"author" equalTo:author]; [query findObjectsInBackgroundWithBlock:^(NSArray *books, NSError *error) { - // books is an array of Book objects by the same author + // books 是包含同一作者所有 Book 对象的数组 }]; }]; ``` +```swift +do { + guard let author = LCApplication.default.currentUser else { + return + } + let book = LCObject(className: "Book") + try book.set("title", value: "我的第五本书") + try book.set("author", value: author) + _ = book.save { result in + switch result { + case .success: + // 获取所有该作者写的书 + let query = LCQuery(className: "Book") + query.whereKey("author", .equalTo(author)) + _ = query.find { result in + switch result { + case .success(objects: let books): + // books 是包含同一作者所有 Book 对象的数组 + break + case .failure(error: let error): + print(error) + } + } + case .failure(error: let error): + print(error) + } + } +} catch { + print(error) +} +``` + +```dart +LCObject book = LCObject('Book'); +LCUser author = await LCUser.getCurrent(); +book['title'] = '我的第五本书'; +book['author'] = author; +await book.save(); + +LCQuery query = LCQuery('Book'); +query.whereEqualTo('author', author); +// books 是包含同一作者所有 Book 对象的数组 +List books = await query.find(); +``` + +```js +const Book = AV.Object.extend("Book"); +const book = new Book(); +const author = AV.User.current(); +book.set("title", "我的第五本书"); +book.set("author", author); +book.save().then((book) => { + // 获取所有该作者写的书 + const query = new AV.Query("Book"); + query.equalTo("author", author); + query.find().then((books) => { + // books 是包含同一作者所有 Book 对象的数组 + }); +}); +``` + +```python +Book = leancloud.Object.extend('Book') +book = Book() +author = leancloud.User.get_current() +book.set('title', '我的第五本书') +book.set('author', author) +book.save() + +# 获取所有该作者写的书 +query = Book.query +query.equal_to('author', author) +book_list = query.find() +``` + +```php +$book = new LeanObject("Book"); +$author = User::getCurrentUser(); +$book->set("title", "我的第五本书"); +$book->set("author", $author); +$book->save(); + +// 获取所有该作者写的书 +$query = new Query("Book"); +$query->equalTo("author", $author); +$books = $query->find(); +``` + +```go +// 暂不支持 +``` + -## Security of User Objects +## 用户对象的安全 -The `TDSUser` class is secured by default. You are not able to invoke any save- or delete-related methods unless the `TDSUser` was obtained using an authenticated method like logging in. This ensures that each user can only update their own data. +用户对象自带安全保障,只有通过经过鉴权的方法获取到的用户对象才能进行更新或删除操作,保证每个用户只能修改自己的数据。 -The reason behind this is that most data stored in `TDSUser` can be very personal and sensitive, such as mobile phone numbers, social network account IDs, etc. Even the app’s owner should avoid tampering with these data for the sake of users’ privacy. +这样设计是因为用户对象中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。 -The code below illustrates this security policy: +下面的代码展现了这种安全措施: ```cs try { - TDSUser tdsUser = await TDSUser.LoginWithTapTap(); - // Attempt to change username + LCUser user = await LCUser.Login("Tom", "cat!@#123"); + // 试图修改用户名 user["username"] = "Jerry"; - // This will work since the user is authenticated + // 密码已被加密,这样做会获取到空字符串 + string password = user["password"]; + // 可以执行,因为用户已鉴权 await user.Save(); - // Get the user with a non-authenticated method - LCQuery userQuery = TDSUser.GetQuery(); - TDSUser unauthenticatedUser = await userQuery.Get(user.ObjectId); + // 绕过鉴权直接获取用户 + LCQuery userQuery = LCUser.GetQuery(); + LCUser unauthenticatedUser = await userQuery.Get(user.ObjectId); unauthenticatedUser["username"] = "Toodle"; - // This will cause an error since the user is unauthenticated + // 会出错,因为用户未鉴权 unauthenticatedUser.Save(); } catch (LCException e) { print($"{e.code} : {e.message}"); @@ -395,119 +2025,310 @@ try { ``` ```java -TDSUser.loginWithTapTap(MainActivity.this, new Callback() { - @Override - public void onSuccess(TDSUser resultUser) { - Toast.makeText(MainActivity.this, "Logged in with TapTap.", Toast.LENGTH_SHORT).show(); - // This will work since the user is authenticated - resultUser.put("username", "Toodle"); - // For demonstration only; please use the asynchronous method in your project to avoid blocking the thread - resultUser.save(); - - // Get the user with a non-authenticated method - LCQuery query = new LCQuery<>("_User"); - query.getInBackground(user.getObjectId()).subscribe(new Observer() { +LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // 试图修改用户名 + user.put("username", "Jerry"); + // 密码已被加密,这样做会获取到空字符串 + String password = user.getString("password"); + // 可以执行,因为用户已鉴权 + user.save(); + + // 绕过鉴权直接获取用户 + LCQuery query = new LCQuery<>("_User"); + query.getInBackground(user.getObjectId()).subscribe(new Observer() { public void onSubscribe(Disposable disposable) {} - public void onNext(TDSUser unauthenticatedUser) { + public void onNext(LCUser unauthenticatedUser) { unauthenticatedUser.put("username", "Toodle"); - // This will cause an error since the user is unauthenticated + // 会出错,因为用户未鉴权 unauthenticatedUser.save(); } public void onError(Throwable throwable) {} public void onComplete() {} }); } - - @Override - public void onFail(TapError error) { - Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show(); - } -}, "public_profile"); + public void onError(Throwable throwable) {} + public void onComplete() {} +}); ``` ```objc -[TDSUser loginByTapTapWithPermissions:@[@"public_profile"] callback:^(TDSUser * _Nullable user, NSError * _Nullable error) { - if (user) { - // Attempt to change username +[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { + if (user != nil) { + // 试图修改用户名 [user setObject:@"Jerry" forKey:@"username")]; - // Save changes + // 密码已被加密,这样做会获取到空字符串 + NSString *password = user[@"password"]; + // 保存更改 [user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { if (succeeded) { - // This will work since the user is authenticated + // 可以执行,因为用户已鉴权 - // Get the user with a non-authenticated method - LCQuery *query = [TDSUser query]; + // 绕过鉴权直接获取用户 + LCQuery *query = [LCQuery queryWithClassName:@"_User"]; [query getObjectInBackgroundWithId:user.objectId block:^(LCObject *unauthenticatedUser, NSError *error) { [unauthenticatedUser setObject:@"Toodle" forKey:@"username"]; [unauthenticatedUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { if (succeeded) { - // This will not succeed since the user is unauthenticated + // 无法执行,因为用户未鉴权 } else { - // Failure is expected + // 操作失败 } }]; }]; } else { - // Error handling + // 错误处理 } }]; } else { - // Error handling + // 错误处理 } }]; ``` +```swift +_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in + switch result { + case .success(object: let user): + // 试图修改用户名 + try! user.set("username", "Jerry") + // 密码已被加密,这样做会获取到空字符串 + let password = user.get("password") + // 可以执行,因为用户已鉴权 + user.save() + + // 绕过鉴权直接获取用户 + let query = LCQuery(className: "_User") + _ = query.get(user.objectId) { result in + switch result { + case .success(object: let unauthenticatedUser): + try! unauthenticatedUser.set("username", "Toodle") + _ = unauthenticatedUser.save { result in + switch result { + .success: + // 无法执行,因为用户未鉴权 + .failure: + // 操作失败 + } + } + case .failure(error: let error): + print(error) + } + } + case .failure(error: let error): + print(error) + } +} +``` + +```dart +try { + LCUser user = await LCUser.login('Tom', 'cat!@#123'); + // 试图修改用户名 + user['username'] = 'Jerry'; + // 密码已被加密,这样做会获取到空字符串 + String password = user['password']; + // 可以执行,因为用户已鉴权 + await user.save(); + + // 绕过鉴权直接获取用户 + LCQuery userQuery = LCQuery('_User'); + LCUser unauthenticatedUser = await userQuery.get(user.objectId); + unauthenticatedUser['username'] = 'Toodle'; + + // 会出错,因为用户未鉴权 + unauthenticatedUser.save(); +} on LCException catch (e) { + print('${e.code} : ${e.message}'); +} +``` + +```js +const user = AV.User.logIn("Tom", "cat!@#123").then((user) => { + // 试图修改用户名 + user.set("username", "Jerry"); + // 密码已被加密,这样做会获取到空字符串 + const password = user.get("password"); + // 保存更改 + user.save().then((user) => { + // 可以执行,因为用户已鉴权 + + // 绕过鉴权直接获取用户 + const query = new AV.Query("_User"); + query.get(user.objectId).then((unauthenticatedUser) => { + unauthenticatedUser.set("username", "Toodle"); + unauthenticatedUser.save().then( + (unauthenticatedUser) => {}, + (error) => { + // 会出错,因为用户未鉴权 + } + ); + }); + }); +}); +``` + +```python +leancloud.User.login('Tom', 'cat!@#123') +current_user = leancloud.User.get_current() + +# 试图修改用户名 +current_user.set('username', 'Jerry') +# 密码已被加密,这样做会获取到空字符串 +password = current_user.get('password') +# 可以执行,因为用户已鉴权 +current_user.save() + +# 绕过鉴权直接获取用户 +query = leancloud.Query('_User') +unauthenticated_user = query.get(current_user.id) +unauthenticated_user.set('username', 'Toodle') +# 会出错,因为用户未鉴权 +unauthenticated_user.save() +``` + +```php +User::logIn("Tom", "cat!@#123"); +$currentUser = User::getCurrentUser(); + +// 试图修改用户名 +$currentUser->set("username", "Jerry"); +// 密码已被加密,这样做会获取到空字符串 +$password = $currentUser->get("password"); +// 可以执行,因为用户已鉴权 +$currentUser->save(); + +// 绕过鉴权直接获取用户 +$query = new Query("_User"); +$unauthenticatedUser = $query->get($currentUser->getObjectId()) +$unauthenticatedUser->set("username", "Toodle"); +// 会出错,因为用户未鉴权 +$unauthenticatedUser->save() +``` + +```go +user, err := client.Users.LogIn("Tom", "cat!@#123") +if err != nil { + panic(err) +} + +// 试图修改用户名,未鉴权将失败 +if err := client.User(user).Set("username", "Jerry"); err != nil { + panic(err) +} + +// 密码已被加密,这样做会获取到空字符串 +password := user.String("password") + +// 可以执行,因为用户已鉴权 +if err := client.User(user).Set("username", "Jerry", leancloud.UseUser(user)); err != nil { + panic(err) +} + +// 绕过鉴权直接获取用户 +unauthenticatedUser := User{} +if err := client.Users.NewUserQuery().EqualTo("objectId", user.ID).First(&unauthenticatedUser); err != nil { + panic(err) +} + +// 会出错,因为用户未鉴权 +if err := client.User(unauthenticatedUser).Set("username", "Toodle"); err != nil { + panic(err) +} +``` + -The `LCUser` obtained from `TDSUser.GetCurrent()` will always be authenticated. +通过调用 [当前用户](#当前用户) 相关方法获取的用户总是经过鉴权的。 + +要查看一个用户对象是否经过鉴权,可以调用如下方法。通过经过鉴权的方法获取到的用户对象无需进行该检查。 + + + +```cs +IsAuthenticated +``` + +```java +isAuthenticated +``` + +```objc +isAuthenticatedWithSessionToken +``` + +```swift +// 暂不支持 +``` + +```dart +isAuthenticated +``` + +```js +isAuthenticated; +``` + +```python +is_authenticated +``` -To check if a `TDSUser` is authenticated, you can invoke the `isAuthenticated` method. You do not need to check if `TDSUser` is authenticated if it is obtained via an authenticated method. +```php +isAuthenticated +``` -## Security of Other Objects +```go +// 暂不支持 +``` -For each given object, you can specify which users are allowed to read it and which are allowed to modify it. To support this type of security, each object has an access control list implemented by an `ACL` object. More details can be found in [ACL Guide](/sdk/storage/guide/acl/). + +注意,用户的密码只能在注册的时候进行设置,日后如需修改,只能通过 [重置密码](#重置密码) 的方式进行。密码不会被缓存在本地。如果尝试直接获取已登录用户的密码,会得到 `null`。 -## Third-Party Sign-on +## 其他对象的安全 -We have already introduced how to [implement quick log-in with TapTap](/sdk/taptap-login/guide/start/#quick-log-in-with-taptap). +对于给定的一个对象,可以指定哪些用户有权限读取或修改它。为实现该功能,每个对象都有一个由 `ACL` 对象组成的访问控制表。请参阅[ACL 权限管理开发指南](/sdk/storage/guide/acl/)。 -Besides TapTap, you can also use other services (like Apple, WeChat, and QQ and Facebook) to implement your account system. You can also associate existing accounts with these services so that the users can quickly log in with their existing accounts on these services. +## 第三方账户登录 -Technically, we have implemented our interfaces in an open-ended manner. You can specify your own platform identifiers and authorization data, which means that our account system supports whatever third-party services you wish to connect to. For example, once you get the authorization data from Facebook, you can use `TDSUser.loginWithAuthData` to log the user in (you may set the platform name to be `facebook`). +云服务支持应用层直接使用第三方社交平台(例如微信、微博、QQ 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。 -The code below shows how you can log a user in with WeChat: +例如以下的代码展示了终端用户使用微信登录的处理流程: ```cs Dictionary thirdPartyData = new Dictionary { - // Optional + // 必须 { "openid", "OPENID" }, { "access_token", "ACCESS_TOKEN" }, { "expires_in", 7200 }, + + // 可选 { "refresh_token", "REFRESH_TOKEN" }, { "scope", "SCOPE" } }; -TDSUser currentUser = await TDSUser.LoginWithAuthData(thirdPartyData, "weixin"); +LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin"); ``` ```java Map thirdPartyData = new HashMap(); -// Optional +// 必须 thirdPartyData.put("expires_in", 7200); thirdPartyData.put("openid", "OPENID"); thirdPartyData.put("access_token", "ACCESS_TOKEN"); +// 可选 thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); thirdPartyData.put("scope", "SCOPE"); -TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "weixin").subscribe(new Observer() { +LCUser.loginWithAuthData(thirdPartyData, "weixin").subscribe(new Observer() { public void onSubscribe(Disposable disposable) { } - public void onNext(TDSUser user) { - System.out.println("Logged in."); + public void onNext(LCUser user) { + System.out.println("成功登录"); } public void onError(Throwable throwable) { - System.out.println("An error occurred."); + System.out.println("尝试使用第三方账号登录,发生错误。"); } public void onComplete() { } @@ -516,33 +2337,104 @@ TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "weixin").subscribe(new ```objc NSDictionary *thirdPartyData = @{ - // Optional + // 必须 @"openid":@"OPENID", @"access_token":@"ACCESS_TOKEN", @"expires_in":@7200, + + // 可选 @"refresh_token":@"REFRESH_TOKEN", @"scope":@"SCOPE", }; -TDSUser *user = [TDSUser user]; +LCUser *user = [LCUser user]; LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; option.platform = LeanCloudSocialPlatformWeiXin; [user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { if (succeeded) { - NSLog(@"Logged in."); - } else { - NSLog(@"An error occurred: %@",error.localizedFailureReason); + NSLog(@"登录成功"); + }else{ + NSLog(@"登录失败:%@",error.localizedFailureReason); } }]; ``` +```swift +let thirdPartyData: [String: Any] = [ + // 必须 + "openid": "OPENID", + "access_token": "ACCESS_TOKEN", + "expires_in": 7200, + + // 可选 + "refresh_token": "REFRESH_TOKEN", + "scope": "SCOPE" +] +let user = LCUser() +user.logIn(authData: thirdPartyData, platform: .weixin) { (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +var thirdPartyData = { + // 必须 + 'openid': 'OPENID', + 'access_token': 'ACCESS_TOKEN', + 'expires_in': 7200, + + // 可选 + 'refresh_token': 'REFRESH_TOKEN', + 'scope': 'SCOPE' +}; +LCUser currentUser = await LCUser.loginWithAuthData(thirdPartyData, 'weixin'); +``` + +```js +const thirdPartyData = { + // 必须 + openid: "OPENID", + access_token: "ACCESS_TOKEN", + expires_in: 7200, + + // 可选 + refresh_token: "REFRESH_TOKEN", + scope: "SCOPE", +}; +AV.User.loginWithAuthData(thirdPartyData, "weixin").then( + (user) => { + // 登录成功 + }, + (error) => { + // 登录失败 + } +); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + -`loginWithAuthData` requires two arguments to locate a unique account: +`loginWithAuthData` 系列方法需要两个参数来唯一确定一个账户: -- The name of the third-party platform, which is `weixin` in the example above. You can decide this name on your own. -- The authorization data from the third-party platform, which is the `thirdPartyData` in the example above (depending on the platform, it usually includes `uid`, `access_token`, and `expires_in`). +- 第三方平台的名字,就是前例中的 `weixin`,该名字由应用层自己决定。 +- 第三方平台的授权信息,就是前例中的 `thirdPartyData`(一般包括 `uid`、`access_token`、`expires_in` 等信息,与具体的第三方平台有关)。 -The cloud will then verifies that the provided `authData` is valid and checks if a user is already associated with it. If so, it returns the status code `200 OK` along with the details (including a [`sessionToken`](#setting-the-current-user) of the user). If the `authData` is not linked to any accounts, you will instead receive the status code `201 Created`, indicating that a new user has been created. The body of the response contains `objectId`, `createdAt`, `sessionToken`, and an automatically-generated unique `username`. For example: +云端会使用第三方平台的鉴权信息来查询是否已经存在与之关联的账户。如果存在的话,则返回 `200 OK` 状态码,同时附上用户的信息(包括 [`sessionToken`](#设置当前用户))。如果第三方平台的信息没有和任何账户关联,客户端会收到 `201 Created` 状态码,意味着新账户被创建,同时附上用户的 `objectId`、`createdAt`、`sessionToken` 和一个自动生成的 `username`,例如: ```json { @@ -551,7 +2443,7 @@ The cloud will then verifies that the provided `authData` is valid and checks if "createdAt": "2018-05-21T09:33:26.406Z", "updatedAt": "2018-05-21T09:33:26.575Z", "sessionToken": "…", - // authData won't be returned in most cases; see explanations below + // authData 通常不会返回,继续阅读以了解其中原因 "authData": { "weixin": { "openid": "OPENID", @@ -565,94 +2457,139 @@ The cloud will then verifies that the provided `authData` is valid and checks if } ``` -Now we will see a new record showing up in the `_User` table that has an `authData` field. Within this field is the authorization data from the third-party platform. For security reasons, the `authData` field won’t be returned to the client unless the current user owns it. +这时候我们会看到 `_User` 表中出现了一条新的账户记录,账户中有一个名为 `authData` 的列,保存了第三方平台的授权信息。出于安全考虑,`authData` 不会被返回给客户端,除非它属于当前用户。 -You will need to implement the authentication process involving the third-party platform yourself (usually with OAuth 1.0 or 2.0) to obtain the authentication data, which will be used to log a user in. +开发者需要自己完成第三方平台的鉴权流程(一般通过 OAuth 1.0 或 2.0),以获取鉴权信息,继而到云端来登录。 ### Sign in with Apple -If you plan to implement [Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api), the cloud can help you verify `identityToken`s and obtain `access_token`s from Apple. Below is the structure of `authData` for Sign in with Apple: +如果你需要开发 [Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api),云服务可以帮你校验 `identityToken`,并获取 Apple 的 `access_token`。Apple Sign In 的 `authData` 结构如下: ```json { "lc_apple": { - "uid": "The User Identifier obtained from Apple", - "identity_token": "The identityToken obtained from Apple", - "code": "The Authorization Code obtained from Apple" + "uid": "从 Apple 获取到的 User Identifier", + "identity_token": "从 Apple 获取到的 identityToken", + "code": "从 Apple 获取到的 Authorization Code" } } ``` -Each `authData` has the following fields: +`authData` 中的 key 的作用: -- **`lc_apple`**: The cloud will run the logic related to `identity_token` and `code` only when the platform name is `lc_apple`. -- **`uid`**: Required. The cloud tells if the user exists with `uid`. -- **`identity_token`**: Optional. The cloud will automatically validate `identity_token` if this field exists. Please make sure you have provided relevant information on **Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings > Third-party accounts**. -- **`code`**: Optional. The cloud will automatically obtain `access_token` and `refresh_token` from Apple if this field exists. Please make sure you have provided relevant information on **Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings > Third-party accounts**. +- **`lc_apple`**:只有 platform 为 `lc_apple` 时,云服务才会执行 `identity_token` 和 `code` 的逻辑。 +- **`uid`**:必填。云服务通过 `uid` 判断是否存在用户。 +- **`identity_token`**:可选。`authData` 中有 `identity_token` 时云端会自动校验 `identity_token` 的有效性。开发者需要在 **云服务控制台 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 +- **`code`**:可选。`authData` 中有 `code` 时云端会自动用该 `code` 向 Apple 换取 `access_token` 和 `refresh_token`。开发者需要在 **云服务控制台 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 -#### Getting Client ID +#### 获取 Client ID -Client ID is used to verify `identity_token` and to obtain `access_token`. It is the identifier of an Apple app (`AppID` or `serviceID`). For native apps, it is the Bundle Identifier in Xcode, which looks like `com.mytest.app`. See [Apple’s docs](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens) for more details. +Client ID 用于校验 `identity_token` 及获取 `access_token`,指的是 Apple 应用的 identifier,也就是 `AppID` 或 `serviceID`。对于原生应用来说,指的是 Xcode 中的 Bundle Identifier,例如 `com.mytest.app`。详情请参考 [Apple 的文档](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)。 -#### Getting Private Key and Private Key ID +#### 获取 Private Key 及 Private Key ID -Private Key is used to obtain `access_token`. You can go to Apple Developer, select “Keys” from “Certificates, Identifiers & Profiles”, add a Private Key for Sign in with Apple, and then download the `.p8` file. You will also obtain the Private Key ID from the page you download the key. See [Apple’s docs](https://developer.apple.com/cn/help/account/) for more details. +Private Key 用于获取 `access_token`。登录 Apple 开发者平台,在左侧的「Certificates, Identifiers & Profiles」中选择「Keys」,添加一个用于 Apple Sign In 的 Private Key,下载 `.p8` 文件,同时在下载 Key 的页面获得 Private Key ID。详情请参考 [Apple 的文档](https://help.apple.com/developer-account/#/dev77c875b7e)。 -The last step is to fill in the Key ID on the Developer Center and upload the downloaded Private Key. You can only upload Private Keys, but cannot view or download them. +将 Key ID 填写到控制台,将下载下来的 Private Key 文件上传到控制台。控制台只能上传 Private Key 文件,无法查看及下载其内容。 -#### Getting Team ID +#### 获取 Team ID -Team ID is used to obtain `access_token`. You can view your team’s Team ID by going to Apple Developer and looking at the top-right corner or the Membership page. Make sure to select the team matching the selected Bundle ID. +Team ID 用于获取 `access_token`。登录 Apple 开发者平台,在右上角或 Membership 页面即可看到自己所属开发团队的 Team ID。注意选择 Bundle ID 对应的 Team。 -#### Logging in to Cloud Services With Sign in with Apple +#### 使用 Apple Sign In 登录云服务 -After you have filled in all the information on the Developer Center, you can log a user in with the following code: +在控制台填写完成所有信息后,使用以下代码登录。 ```cs Dictionary appleAuthData = new Dictionary { - // Required + // 必须 { "uid", "USER IDENTIFIER" }, - // Optional + // 可选 { "identity_token", "IDENTITY TOKEN" }, { "code", "AUTHORIZATION CODE" } }; -TDSUser currentUser = await TDSUser.LoginWithAuthData(appleAuthData, "lc_apple"); +LCUser currentUser = await LCUser.LoginWithAuthData(appleAuthData, "lc_apple"); ``` ```java -// Not supported yet +// 不支持 ``` ```objc NSDictionary *appleAuthData = @{ - // Required + // 必须 @"uid":@"USER IDENTIFIER", - // Optional + // 可选 @"identity_token":@"IDENTITY TOKEN", @"code":@"AUTHORIZATION CODE", }; -TDSUser *user = [TDSUser user]; +LCUser *user = [LCUser user]; [user loginWithAuthData:appleAuthData platformId:"lc_apple" options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { if (succeeded) { - NSLog(@"Logged in."); + NSLog(@"登录成功"); }else{ - NSLog(@"Failed to log in: %@",error.localizedFailureReason); + NSLog(@"登录失败:%@",error.localizedFailureReason); } }]; ``` - +```swift +let appleData: [String: Any] = [ + // 必须 + "uid": "USER IDENTIFIER", + // 可选 + "identity_token": "IDENTITY TOKEN", + "code": "AUTHORIZATION CODE" +] +let user = LCUser() +user.logIn(authData: appleData, platform: .apple) { (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) + } +} + +``` + +```dart +var appleData = { + // 必须 + "uid": "USER IDENTIFIER", + // 可选 + "identity_token": "IDENTITY TOKEN", + "code": "AUTHORIZATION CODE" +}; +LCUser currentUser = await LCUser.loginWithAuthData(appleData, 'lc_apple'); +``` + +```js +// 不支持 +``` + +```python +# 不支持 +``` + +```php +// 不支持 +``` + +```go +// 不支持 +``` -### Storing Authentication Data + -The `authData` of each user is a JSON object with platform names as keys and authentication data as values. +### 鉴权数据的保存 - +每个用户的 `authData` 是一个以平台名为键名,鉴权信息为键值的 JSON 对象。 -A user associated with a WeChat account will have the following object as its `authData`: +一个关联了微信账户的用户应该会有下列对象作为 `authData`: ```json { @@ -666,7 +2603,7 @@ A user associated with a WeChat account will have the following object as its `a } ``` -A user associated with a Weibo account will have the following object as its `authData`: +而一个关联了微博账户的用户,则会有如下的 `authData`: ```json { @@ -679,7 +2616,7 @@ A user associated with a Weibo account will have the following object as its `au } ``` -A user can be associated with multiple third-party platforms. If a user is associated with both WeChat and Weibo, their `authData` may look like this: +我们允许一个账户绑定多个第三方平台的鉴权数据,这样如果某个用户同时关联了微信和微博账户,则其 `authData` 可能会是这样的: ```json { @@ -699,9 +2636,7 @@ A user can be associated with multiple third-party platforms. If a user is assoc } ``` - - -It’s important to understand the data structure of `authData`. When a user logs in with the following authentication data: +理解 `authData` 的数据结构至关重要。一个终端用户通过如下的鉴权信息来登录的时候, ```json "platform": { @@ -713,29 +2648,29 @@ It’s important to understand the data structure of `authData`. When a user log } ``` -The cloud will first look at the account system to see if there is an account that has its `authData.platform.openid` to be the `OPENID`. If there is, return the existing account. If not, create a new account and write the authentication data into the `authData` field of this new account, and then return the new account’s data as the result. +云端首先会查找账户系统,看看是否存在 `authData.platform.openid` 等于 `OPENID` 的账户,如果存在,则返回现有账户,如果不存在那么就创建一个新账户,同时将上面的鉴权信息写入新账户的 `authData` 属性中,并将新账户的数据当成结果返回。 -The cloud will automatically create a unique index for the `authData..` of each user, which prevents the formation of duplicate data. -For some of the platforms specially supported by us, `` refers to the `openid` field. For others (the other platforms specially supported by us, and those not specially supported by us), it refers to the `uid` field. +云端会自动为每个用户的 `authData..` 创建唯一索引,从而避免重复数据。 +`` 在微信等部分云服务内建支持的第三方平台上为 `openid` 字段,在其他第三方平台(包括部分云服务专门支持的第三方平台和所有云服务没有专门支持的第三方平台)上为 `uid` 字段。 -### Automatically Validating Third-Party Authorization Data +### 自动验证第三方平台授权信息 -The cloud can automatically validate access tokens for certain platforms, which prevents counterfeit account data from entering your app’s account system. If the validation fails, the cloud will return the `invalid authData` error, and the association will not be created. For those services that are not recognized by the cloud, you need to validate the access tokens yourself. -You can validate access tokens when a user signs up or logs in by using LeanEngine’s `beforeSave` hook and `beforeUpdate` hook. +为了确保账户数据的有效性,云端还支持对部分平台的 Access Token 的有效性进行自动验证,以防止伪造账户数据。如果有效性验证不通过,云端会返回 `invalid authData` 错误,关联不会被建立。对于云端无法识别的服务,开发者需要自己去验证 Access Token 的有效性。 +比如,注册、登录时分别通过云引擎的 `beforeSave` hook、`beforeUpdate` hook 来验证 Access Token 有效性。 -To enable the feature, please configure the platforms’ **App IDs** and **Secret Keys** on **Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings**. +如果希望使用这一功能,则在开始使用前,需要在 **云服务控制台 > 内建账户 > 设置** 配置相应平台的 **应用 ID** 和 **应用 Secret Key**。 -To disable the feature, please uncheck **Validate access tokens when logging in with third-party accounts** on **Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings**. +如果不希望云端自动验证 Access Token,可以在 **云服务控制台 > 内建账户 > 设置** 里面取消勾选 **第三方登录时,验证用户 AccessToken 合法性**。 -The reason for configuring the platforms is that when a `TDSUser` is created, the cloud will use the relevant data to validate the `thirdPartyData` to ensure that the `TDSUser` matches a real user, which ensures the security of your app. +配置平台账号的目的在于创建用户对象时,云端会使用相关信息去校验请求参数 `thirdPartyData` 的合法性,确保用户对象实际对应着一个合法真实的用户,确保平台安全性。 -### Linking Third-Party Accounts +### 绑定第三方账户 -If a user is already logged in, you can link third-party accounts to this user. For example, if a user first logs in as a guest and then links their TapTap or other third-party accounts, the user will be able to access the same account when they log in with the same TapTap or third-party accounts in the future. +如果用户已经登录,也可以在当前账户上绑定或解绑更多第三方平台信息。 -After a user links their third-party account, the account information will be added to the `authData` field of the corresponding `TDSUser`. +绑定成功后,新的第三方账户信息会被添加到用户对象的 `authData` 字段里。 -The following code links a WeChat account to a user: +例如,下面的代码可以关联微信账户: @@ -744,64 +2679,257 @@ await currentUser.AssociateAuthData(weixinData, "weixin"); ``` ```java -user.associateWithAuthData(weixinData, "weixin").subscribe(new Observer() { +user.associateWithAuthData(weixinData, "weixin").subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + } + @Override + public void onNext(LCUser user) { + System.out.println("绑定成功"); + } + @Override + public void onError(Throwable e) { + System.out.println("绑定失败:" + e.getMessage()); + } + @Override + public void onComplete() { + } +}); +``` + +```objc +[user associateWithAuthData:weixinData platformId:LeanCloudSocialPlatformWeiXin options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + NSLog(@"成功"); + } else{ + NSLog(@"失败:%@",error.localizedFailureReason); + } +}]; +``` + +```swift +currentUser.associate(authData: weixinData, platform: .weixin) { (result) in + switch result { + case .success: + // 关联成功 + case .failure(error: let error): + // 关联失败 + } +} +``` + +```dart +await currentUser.associateAuthData(weixinData, 'weixin'); +``` + +```js +user + .associateWithAuthData(weixinData, "weixin") + .then(function (user) { + // 成功绑定 + }) + .catch(function (error) { + console.error("error: ", error); + }); +``` + +```python +user.link_with("weixin", weixin_data) +``` + +```php +$user->linkWith("weixin", $weixinData); +``` + +```go +// 暂不支持 +``` + + + +为节省篇幅,上面的代码示例中没有给出具体的平台授权信息,相关内容请参考上面的[「第三方账户登录」](#第三方账户登录)一节。 + +### 解除与第三方账户的关联 + +类似地,可以解绑第三方账户。 + +例如,下面的代码可以解除用户和微信账户的关联: + + + +```cs +LCUser currentUser = await LCUser.GetCurrent(); +await currentUser.DisassociateWithAuthData("weixin"); +``` + +```java +LCUser user = LCUser.currentUser(); +user.dissociateWithAuthData("weixin").subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + } + @Override + public void onNext(LCUser user) { + System.out.println("解绑成功"); + } + @Override + public void onError(Throwable e) { + System.out.println("解绑失败:" + e.getMessage()); + } + @Override + public void onComplete() { + } +}); +``` + +```objc +[user disassociateWithPlatform:LeanCloudSocialPlatformWeiXin callback:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + NSLog(@"成功"); + } else{ + NSLog(@"失败:%@",error.localizedFailureReason); + } +}]; +``` + +```swift +currentUser.disassociate(authData: .weixin) { (result) in + switch result { + case .success: + // 解除关联成功 + case .failure(error: let error): + // 解除关联失败 + } +} +``` + +```dart +LCUser currentUser = await LCUser.getCurrent(); +await currentUser.disassociateWithAuthData('weixin'); +``` + +```js +user.dissociateAuthData("weixin").then( + (s) => { + // 解除关联成功 + }, + (error) => { + // 解除关联失败 + } +); +``` + +```python +user.unlink_from("weixin") +``` + +```php +$user->unlinkWith("weixin"); +``` + +```go +// 暂不支持 +``` + + + +
    + +扩展:第三方登录时补充完整的用户信息 + +有些产品,新用户在使用第三方账号授权拿到相关信息后,仍然需要补充设置用户名、手机号、密码等重要信息后,才被允许登录成功。 + +这时要使用 `loginWithauthData` 登录接口的 `failOnNotExist` 参数并将其设置为 `true`。服务端会判断是否已存在能匹配上的 `authData`,如果不存在则会返回 `211` 错误码和 `Could not find user` 报错信息。开发者根据这个 `211` 错误码,跳转到要求输入用户名、密码、手机号等信息的页面,实例化一个用户对象,保存上述补充数据,再次调用 `loginWithauthData` 接口进行登录,并 **不再传入 `failOnNotExist` 参数**。示例代码如下: + + + +```cs +try { + Dictionary thirdPartyData = new Dictionary { + // 必须 + { "openid", "OPENID" }, + { "access_token", "ACCESS_TOKEN" }, + { "expires_in", 7200 }, + + // 可选 + { "refresh_token", "REFRESH_TOKEN" }, + { "scope", "SCOPE" } + }; + LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); + option.FailOnNotExist = true; + LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); +} catch (LCException e) { + if (e.code == 211) { + // 不存在 authData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 + } +} + +// 跳转到输入用户名、密码、手机号等业务页面之后 +Dictionary thirdPartyData = new Dictionary { + { "expires_in", 7200 }, + { "openid", "OPENID" }, + { "access_token", "ACCESS_TOKEN" } +}; +try { + LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); + option.FailOnNotExist = true; + LCUser user = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); + user.Username = "Tom"; + user.Mobile = "+8618200008888"; + await user.Save(); +} catch (LCException e) { + //其他报错信息 +} +``` + +```java +Map thirdPartyData = new HashMap(); +thirdPartyData.put("expires_in", 7200); +thirdPartyData.put("openid", "OPENID"); +thirdPartyData.put("access_token", "ACCESS_TOKEN"); +thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); +thirdPartyData.put("scope", "SCOPE"); +Boolean failOnNotExist = true; +LCUser user = new LCUser(); +user.loginWithAuthData(thirdPartyData, "weixin", failOnNotExist).subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { } @Override - public void onNext(TDSUser user) { - System.out.println("Account linked."); + public void onNext(LCUser user) { + System.out.println("存在匹配的用户,登录成功"); } @Override public void onError(Throwable e) { - System.out.println("Failed to link the account: " + e.getMessage()); + LCException avException = new LCException(e); + int code = avException.getCode(); + if (code == 211){ + // 跳转到输入用户名、密码、手机号等业务页面 + } else { + System.out.println("发生错误:" + e.getMessage()); + } } @Override public void onComplete() { } }); -``` - -```objc -[user associateWithAuthData:weixinData platformId:LeanCloudSocialPlatformWeiXin options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"Account linked."); - } else{ - NSLog(@"Failed to link the account: %@",error.localizedFailureReason); - } -}]; -``` - - - -The code above omitted the authorization data of the platform. See [Third-Party Sign-on](#third-party-sign-on) for more details. - -### Unlinking - -Similarly, a third-party account can be unlinked. -For example, the code below unlinks a user’s WeChat account: - - - -```cs -TDSUser currentUser = await TDSUser.GetCurrent(); -await currentUser.DisassociateWithAuthData("weixin"); -``` - -```java -TDSUser user = TDSUser.currentUser(); -user.dissociateWithAuthData("weixin").subscribe(new Observer() { +// 跳转到输入用户名、密码、手机号等业务页面之后 +LCUser user = new LCUser(); +user.setUsername("Tom"); +user.setMobilePhoneNumber("+8618200008888"); +Boolean failOnNotExist = false; +user.loginWithAuthData(thirdPartyData, "weixin", failOnNotExist).subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { } @Override - public void onNext(TDSUser user) { - System.out.println("Unlinked."); + public void onNext(LCUser user) { + System.out.println("登录成功"); } @Override public void onError(Throwable e) { - System.out.println("Failed to unlink: " + e.getMessage()); + System.out.println("登录失败:" + e.getMessage()); } @Override public void onComplete() { @@ -810,18 +2938,164 @@ user.dissociateWithAuthData("weixin").subscribe(new Observer() { ``` ```objc -[user disassociateWithPlatform:LeanCloudSocialPlatformWeiXin callback:^(BOOL succeeded, NSError * _Nullable error) { +NSDictionary *thirdPartyData = @{ + @"access_token":@"ACCESS_TOKEN", + @"expires_in":@7200, + @"refresh_token":@"REFRESH_TOKEN", + @"openid":@"OPENID", + @"scope":@"SCOPE", + }; +LCUser *user = [LCUser user]; +LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; +option.platform = LeanCloudSocialPlatformWeiXin; +option.failOnNotExist = true; +[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { if (succeeded) { - NSLog(@"Unlinked."); - } else{ - NSLog(@"Failed to unlink: %@",error.localizedFailureReason); + // 你的逻辑 + } else if ([error.domain isEqualToString:kLeanCloudErrorDomain] && error.code == 211) { + // 不存在 thirdPartyData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 + } +}]; + +// 跳转到输入用户名、密码、手机号等业务页面之后 +LCUser *user = [LCUser user]; +user.username = @"Tom"; +user.mobilePhoneNumber = @"+8618200008888"; +LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; +option.platform = LeanCloudSocialPlatformWeiXin; +[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { + if (succeeded) { + NSLog(@"登录成功"); + }else{ + NSLog(@"登录失败:%@",error.localizedFailureReason); } }]; ``` +```swift +let thirdPartyData: [String: Any] = [ + "access_token": "ACCESS_TOKEN", + "expires_in": 7200, + "refresh_token": "REFRESH_TOKEN", + "openid": "OPENID", + "scope": "SCOPE" +] +let user = LCUser() +user.logIn(authData: thirdPartyData, platform: .weixin, options: [.failOnNotExist]) { (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + if error.code == 211 { + // 不存在绑定了当前 authData 的 User 的实例 + // 跳转到输入用户名、密码、手机号等业务页面 + let user = LCUser() + user.username = "Tom" + user.password = "cat!@#123" + user.mobilePhoneNumber = "+8618200008888" + user.logIn(authData: thirdPartyData, platform: .weixin, completion: { (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) + } + }) + } + } +} +``` + +```dart +try { + Map thirdPartyData = { + // 必须 + 'openid': 'OPENID', + 'access_token': 'ACCESS_TOKEN', + 'expires_in': 7200, + + // 可选 + 'refresh_token': 'REFRESH_TOKEN', + 'scope': 'SCOPE' + }; + LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); + option.failOnNotExist = true; + LCUser currentUser = await LCUser.loginWithAuthData(thirdPartyData, 'weixin', option: option); +} on LCException catch (e) { + if (e.code == 211) { + // 不存在 authData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 + } +} + +// 跳转到输入用户名、密码、手机号等业务页面之后 +Map thirdPartyData = { + 'expires_in': 7200, + 'openid': 'OPENID', + 'access_token': 'ACCESS_TOKEN' +}; +try { + LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); + option.failOnNotExist = true; + LCUser user = await LCUser.loginWithAuthData(thirdPartyData, 'weixin', option: option); + user.username = 'Tome'; + user.mobile = '+8618200008888'; + await user.save(); +} on LCException catch (e) { + //其他报错信息 +} +``` + +```js +const thirdPartyData = { + access_token: "ACCESS_TOKEN", + expires_in: 7200, + refresh_token: "REFRESH_TOKEN", + openid: "OPENID", + scope: "SCOPE", +}; +AV.User.loginWithAuthData(thirdPartyData, "weixin", { + failOnNotExist: true, +}).then( + (s) => { + // 登录成功 + }, + (error) => { + // 登录失败 + // 检查 error.code == 211,跳转到用户名、手机号等资料的输入页面 + } +); + +const user = new AV.User(); +// 设置用户名 +user.setUsername("Tom"); +// 设置密码 +user.setMobilePhoneNumber("+8618200008888"); +user.setPassword("cat!@#123"); +// 设置邮箱 +user.setEmail("tom@leancloud.rocks"); +user.loginWithAuthData(thirdPartyData, "weixin").then( + (loggedInUser) => { + console.log(loggedInUser); + }, + (error) => {} +); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + - +
    @@ -881,7 +3155,7 @@ Dictionary thirdPartyData = new Dictionary { LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); option.AsMainAccount = true; option.UnionIdPlatform = "weixin"; -TDSUser currentUser = await TDSUser.LoginWithAuthDataAndUnionId( +LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( thirdPartyData, "wxleanoffice", "unionid4a", option: option); ``` @@ -892,15 +3166,15 @@ thirdPartyData.put("expires_in", 1384686496); thirdPartyData.put("uid", "officeopenid"); thirdPartyData.put("access_token", "officetoken"); thirdPartyData.put("scope", "SCOPE"); -TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "wxleanoffice", +LCUser.loginWithAuthData(LCUser.class, thirdPartyData, "wxleanoffice", "unionid4a", "weixin", true) // 新增参数,分别表示 uniondId,unionIdPlatform,asMainAccount // 对于 unionIdPlatform,这里使用「weixin」来指代微信平台。 - .subscribe(new Observer() { + .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { } @Override - public void onNext(TDSUser user) { + public void onNext(LCUser user) { System.out.println("登录成功"); } @Override @@ -921,7 +3195,7 @@ NSDictionary *thirdPartyData = @{ @"scope":@"SCOPE", @"unionid":@"unionid4a" // 新增属性 }; -TDSUser *currentuser = [TDSUser user]; +LCUser *currentuser = [LCUser user]; LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; option.platform = @"weixin"; // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 option.unionId = thirdPartyData[@"unionid"]; @@ -935,9 +3209,92 @@ option.isMainAccount = true; }]; ``` +```swift +let thirdPartyData: [String: Any] = [ + "access_token": "officetoken", + "expires_in": 1384686496, + "uid": "officeopenid", + "scope": "SCOPE", + "unionid": "unionid4a" // 新增属性 +] +let user = LCUser() +user.logIn( + authData: thirdPartyData, + platform: .custom("wxleanoffice"), + unionID: thirdPartyData["unionid"] as? String, + unionIDPlatform: .weixin, // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 + options: [.mainAccount]) +{ (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +Map thirdPartyData = { + // 必须 + 'uid': 'officeopenid', + 'access_token': 'officetoken', + 'expires_in': 1384686496, + 'unionId': 'unionid4a', // 新增属性 + + // 可选 + 'refresh_token': '...', + 'scope': 'SCOPE' +}; +LCUserAuthDataLoginOption option = LCUserAuthDataLoginOption(); +option.asMainAccount = true; +option.unionIdPlatform = 'weixin'; +LCUser currentUser = await LCUser.loginWithAuthDataAndUnionId( + thirdPartyData, 'wxleanoffice', 'unionid4a', + option: option); +``` + +```js +const thirdPartyData = { + access_token: "officetoken", + expires_in: 1384686496, + uid: "officeopenid", + scope: "SCOPE", +}; + +AV.User.loginWithAuthDataAndUnionId( + thirdPartyData, + "wxleanoffice", + "unionid4a", // 新增参数 + { + unionIdPlatform: "weixin", // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 + asMainAccount: true, + } +).then( + (user) => { + // 绑定成功 + }, + (error) => { + // 绑定失败 + } +); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + -注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 `authData..uid` 的唯一索引,具体可以参考[数据存储 REST API 使用详解](/sdk/storage/guide/rest/)的《连接用户账户和第三方平台》一节。 +注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 `authData..uid` 的唯一索引,具体可以参考《数据存储 REST API 使用详解》的[《连接用户账户和第三方平台》](/sdk/authentication/rest/#连接用户账户和第三方平台)一节。 如果用户 A 是第一次在「云服务通讯」中通过微信登录,那么 `_User` 表中会增加一个新用户(假设其 `objectId` 为 `ThisIsUserA`),其 `authData` 的结果如下: @@ -983,7 +3340,7 @@ Dictionary thirdPartyData = new Dictionary { LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); option.AsMainAccount = false; option.UnionIdPlatform = "weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -TDSUser currentUser = await TDSUser.LoginWithAuthDataAndUnionId( +LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( thirdPartyData, "wxleansupport", "unionid4a", option: option); ``` @@ -994,14 +3351,14 @@ thirdPartyData.put("expires_in", 1384686496); thirdPartyData.put("uid", "supportopenid"); thirdPartyData.put("access_token", "supporttoken"); thirdPartyData.put("scope", "SCOPE"); -TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "wxleansupport", "unionid4a", +LCUser.loginWithAuthData(LCUser.class, thirdPartyData, "wxleansupport", "unionid4a", "weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 - false).subscribe(new Observer() { + false).subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { } @Override - public void onNext(TDSUser user) { + public void onNext(LCUser user) { System.out.println("登录成功"); } @Override @@ -1022,7 +3379,7 @@ NSDictionary *thirdPartyData = @{ @"scope":@"SCOPE", @"unionid":@"unionid4a" }; -TDSUser *currentuser = [TDSUser user]; +LCUser *currentuser = [LCUser user]; LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; option.platform = @"weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 option.unionId = thirdPartyData[@"unionid"]; @@ -1036,6 +3393,89 @@ option.isMainAccount = false; }]; ``` +```swift +let thirdPartyData: [String: Any] = [ + "access_token": "supporttoken", + "expires_in": 1384686496, + "uid": "supportopenid", + "scope": "SCOPE", + "unionid": "unionid4a" +] +let user = LCUser() +user.logIn( + authData: thirdPartyData, + platform: .custom("wxleansupport"), + unionID: thirdPartyData["unionid"] as? String, + unionIDPlatform: .weixin, // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 + options: [.mainAccount]) +{ (result) in + switch result { + case .success: + assert(user.objectId != nil) + case .failure(error: let error): + print(error) + } +} +``` + +```dart +Map thirdPartyData = { + // 必须 + 'uid': 'supportopenid', + 'access_token': 'supporttoken', + 'expires_in': 1384686496, + 'unionId': 'unionid4a', + + // 可选 + 'refresh_token': '...', + 'scope': 'SCOPE' +}; +LCUserAuthDataLoginOption option = LCUserAuthDataLoginOption(); +option.asMainAccount = false; +option.unionIdPlatform = 'weixin'; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 +LCUser currentUser = await LCUser.loginWithAuthDataAndUnionId( + thirdPartyData, 'wxleansupport', 'unionid4a', + option: option); +``` + +```js +const thirdPartyData = { + access_token: "supporttoken", + expires_in: 1384686496, + uid: "supportopenid", + scope: "SCOPE", +}; + +AV.User.loginWithAuthDataAndUnionId( + thirdPartyData, + "wxleansupport", + "unionid4a", + { + unionIdPlatform: "weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 + asMainAccount: false, + } +).then( + (user) => { + // 绑定成功 + }, + (error) => { + // 绑定失败 + } +); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + 与「云服务通讯」中的登录过程相比,在「云服务技术支持」这个应用中,我们在登录时只是将 `asMainAccount` 设为了 `false`。 这时我们看到,本次登录得到的还是 `objectId` 为 `ThisIsUserA` 的 `_User` 表记录(同一个账户),同时该账户的 `authData` 属性中发生了变化,多了 `wxleansupport` 的数据,如下: @@ -1064,11 +3504,11 @@ option.isMainAccount = false; } ``` -在新的登录方式中,当一个用户以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录得到新的 `TDSUser` 后,接下来这个用户以「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 `wxleansupport` 的新用户数据加入到已有账户的 `authData` 里了,不会再创建新的账户。 +在新的登录方式中,当一个用户以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录得到新的用户对象后,接下来这个用户以「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 `wxleansupport` 的新用户数据加入到已有账户的 `authData` 里了,不会再创建新的账户。 -这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个 `TDSUser` 上,实现互通。 +这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个用户对象上,实现互通。 -#### 为 UnionID 建立索引 +### 为 UnionID 建立索引 云端会为 UnionID 自动建立索引,不过因为自动创建基于请求的抽样统计,可能会滞后。 因此,我们推荐自行创建相关索引,特别是用户量(`_User` 表记录数)很大的应用,更需要预先创建索引,否则用户使用 UnionID 账号登录时可能超时失败。 @@ -1078,7 +3518,7 @@ option.isMainAccount = false; - `authData.wxleansupport.uid` - `authData._weixin_unionid.uid` -#### 该如何指定 unionIdPlatform +### 该如何指定 unionIdPlatform 从上面的例子可以看出,使用 UnionID 登录的时候,需要指定 `unionIdPlatform` 的主要目的,就是为了便于查找已经存在的唯一主账号。云端会在主账号对应账户的 `authData` 属性中增加一个 `_{unionIdPlatform}_unionid` 键值对来标记唯一性,终端用户在其他应用中登录的时候,云端会根据参数中提供的 `uniondId` + `unionIdPlatform` 的组合,在 `_User` 表中进行查找,这样来确定唯一的既存主账号。 @@ -1094,11 +3534,11 @@ option.isMainAccount = false; - 微博平台的多个应用,统一使用 `weibo` 作为 `unionIdPlatform`; - 除此之外的其他平台,开发者可以自行定义 `unionIdPlatform` 的名字,只要自己保证多个应用间统一即可。 -#### 主副应用不同登录顺序出现的不同结果 +### 主副应用不同登录顺序出现的不同结果 上面的流程是用户先登录了「云服务通讯」这个主应用,然后再登录「云服务技术支持」这个副应用,所以账号都被通过 UnionID 有效关联起来了。可能有人会想到另外一个问题,如果用户 B 先登录副应用,后登录主应用,这时候会发生什么事情呢? -用户 B 首先登录副应用的时候,提供了「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,并且指定「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `false`」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个 `TDSUser` 对象,该账户 `authData` 结果为: +用户 B 首先登录副应用的时候,提供了「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,并且指定「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `false`」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个用户对象,该账户 `authData` 结果为: ```json { @@ -1113,7 +3553,7 @@ option.isMainAccount = false; } ``` -用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,以及「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `true`」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个 `TDSUser` 对象,该账户 `authData` 结果为: +用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,以及「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `true`」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个用户对象,该账户 `authData` 结果为: ```json { @@ -1133,7 +3573,7 @@ option.isMainAccount = false; 还有更复杂的情况。如果某公司的产品之前就接入了微信登录,产生了很多存量用户,并且分散在不同的子产品中,这时候怎么办?我们接下来专门讨论此时的解决方案。 -#### 存量账户如何通过 UnionID 实现关联 +### 存量账户如何通过 UnionID 实现关联 还是以我们的两个子产品「云服务通讯」(后续以「产品 1」指代)和「云服务技术支持为例」(后续以「产品 2」指代)为例,在接入 UnionID 之前,我们就接入了之前版本的微信平台登录,这时候账户系统内可能存在多种账户: @@ -1192,4 +3632,221 @@ option.isMainAccount = false;
    - +## 匿名用户 + +将数据与用户关联需要首先创建一个用户,但有时你不希望强制用户在一开始就进行注册。使用匿名用户,可以让应用不提供注册步骤也能创建用户。下面的代码创建一个新的匿名用户: + + + +```cs +await LCUser.LoginAnonymously(); +``` + +```java +LCUser.logInAnonymously().subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // user 是新的匿名用户 + } + public void onError(Throwable throwable) {} + public void onComplete() {} +}); +``` + +```objc +[LCUser loginAnonymouslyWithCallback:^(LCUser *user, NSError *error) { + // user 是新的匿名用户 +}]; +``` + +```swift +// 暂不支持 +``` + +```dart +await LCUser.loginAnonymously(); +``` + +```js +AV.User.loginAnonymously().then((user) => { + // user 是新的匿名用户 +}); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + + + +可以像给普通用户设置属性那样给匿名用户设置 `username`、`password`、`email` 等属性,还可以通过走正常的注册流程来将匿名用户转化为普通用户。匿名用户能够: + +- [使用用户名和密码注册](#注册) +- [关联第三方平台](#第三方账户登录),比如微信 + +下面的代码为一名匿名用户设置用户名和密码: + + + +```cs +LCUser currentUser = await LCUser.LoginAnonymously(); +currentUser.Username = "Tom"; +currentUser.Password = "cat!@#123"; + +await currentUser.SignUp(); +``` + +```java +// currentUser 是个匿名用户 +LCUser currentUser = LCUser.getCurrentUser(); + +currentUser.setUsername("Tom"); +currentUser.setPassword("cat!@#123"); + +currentUser.signUpInBackground().subscribe(new Observer() { + public void onSubscribe(Disposable disposable) {} + public void onNext(LCUser user) { + // currentUser 已经转化为普通用户 + } + public void onError(Throwable throwable) { + // 注册失败(通常是因为用户名已被使用) + } + public void onComplete() {} +}); +``` + +```objc +// currentUser 是个匿名用户 +LCUser *currentUser = [LCUser currentUser]; + +user.username = @"Tom"; +user.password = @"cat!@#123"; + +[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + if (succeeded) { + // currentUser 已经转化为普通用户 + } else { + // 注册失败(通常是因为用户名已被使用) + } +}]; +``` + +```swift +// 暂不支持 +``` + +```dart +LCUser currentUser = await LCUser.loginAnonymously(); +currentUser.username = 'Tom'; +currentUser.password = 'cat!@#123'; + +await currentUser.signUp(); +``` + +```js +// currentUser 是个匿名用户 +const currentUser = AV.User.current(); + +user.setUsername("Tom"); +user.setPassword("cat!@#123"); + +user.signUp().then( + (user) => { + // currentUser 已经转化为普通用户 + }, + (error) => { + // 注册失败(通常是因为用户名已被使用) + } +); +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + + + +下面的代码检查当前用户是否为匿名用户: + + + +```cs +LCUser currentUser = await LCUser.GetCurrent(); +if (currentUser.IsAnonymous) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```java +LCUser currentUser = LCUser.getCurrentUser(); +if (currentUser.isAnonymous()) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```objc +LCUser *currentUser = [LCUser currentUser]; +if (currentUser.isAnonymous) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```swift +// 暂不支持 +``` + +```dart +LCUser currentUser = await LCUser.getCurrent(); +if (currentUser.isAnonymous) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```js +const currentUser = AV.User.current(); +if (currentUser.isAnonymous()) { + // currentUser 是匿名用户 +} else { + // currentUser 不是匿名用户 +} +``` + +```python +# 暂不支持 +``` + +```php +// 暂不支持 +``` + +```go +// 暂不支持 +``` + + + +如果匿名用户未能在登出前转化为普通用户,那么该用户将无法再次登录同一账户,且之前产生的数据也无法被取回。 diff --git a/leancloud/docs/sdk/authentication/rest.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/rest.mdx similarity index 100% rename from leancloud/docs/sdk/authentication/rest.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/rest.mdx diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/copyright-verification/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/copyright-verification/_category_.json deleted file mode 100644 index 57b7c1615..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/copyright-verification/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "付费下载和正版验证", - "collapsed": true, - "position": 11 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/copyright-verification/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/copyright-verification/features.mdx deleted file mode 100644 index ee1ae632f..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/copyright-verification/features.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Paid Games & Copyright Verification -sidebar_label: Features -sidebar_position: 1 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { Conditional } from "/src/docComponents/conditional"; - -## Overview - -TapTap offers copyright verification services for pay-to-download games. When a player launches a paid game, the service can verify if the player has purchased the game. - -## How to Enable - -You will need to submit an application to enable this service. Please follow the instructions on the following screenshot. - - - - - - - - -## Price Settings - -TapTap’s copyright verification service must be configured together with the paid game’s price settings. TapTap helps you manage your paid games and offers multiple payment methods for the players. You can make your game a paid game in the Price Settings at [TapTap Developer Center](https://developer.taptap.cn/)[TapTap Developer Center](https://developer.taptap.io/) and set up the price. If you want to start a promotion for your game, you can turn on the promotion feature and specify the promotion period and discounted price for your game. - - - - - - - - -## Integrating SDK - -After you integrate the copyright verification SDK into your game, the SDK will make a verification query on purchase results when a user launches the game. The user will be able to access the game if it has been purchased normally. Otherwise, a message will remind the player to purchase the game first. Refer to [Developer Guide](/sdk/copyright-verification/guide/) for details about the integration. - -## FAQ - -### Do I have to integrate the Copyright Verification SDK if I’m to release a paid game on TapTap? - -It’s not a requirement for the games released on TapTap to integrate the Copyright Verification SDK. -But without the SDK, you can only expect TapTap to stop the unpaid players from downloading your game from TapTap. - -- If a player downloads the APK of the game from somewhere else, they can still install and enter the game. -- If a player purchased the game from TapTap and later on requested a refund, the player won’t be able to download the game anymore, but they can still enter the game they already installed on their device. - -That’s why we highly recommend you to integrate the Copyright Verification SDK in your game, as it is the easiest way for you to prevent players from playing unauthorized copies of your game. -If you choose not to use the SDK, you will have to implement the anti-piracy mechanism yourself. - -At this moment, if you haven’t enabled Copyright Verification on the Developer Center, you won’t be able to access the Price Settings. -This means that even you only need to launch a paid game without using the Copyright Verification SDK in your game, you still need to enable the Copyright Verification feature on the Developer Center. -Once your application to enable the feature has been approved, you will be able to access the Price Settings. - -## How much share does TapTap take? - -TapTap does not take a share of the revenue. - -However, there will be a 5% payment processing fee in the Mainland China region. For other countries and regions, the fee will be determined upon communication (the fee includes third-party payment channel processing fees and tax costs). - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/copyright-verification/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/copyright-verification/guide.mdx deleted file mode 100644 index b70e6277b..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/copyright-verification/guide.mdx +++ /dev/null @@ -1,208 +0,0 @@ ---- -title: Copyright Verification -sidebar_label: Guide -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import AndroidFaq from "../_partials/android-package-visibility.mdx"; - -If you ever thought about selling your game online, you probably have worried if the players could bypass the legitimate process of paying for your game and download copies of your game from unauthorized sources like piracy websites. Luckily, TapTap offers an easy-to-use Copyright Verification SDK that lets you perform a quick license check when a player opens your game for the first time. If the player opens your game without purchasing it beforehand, the SDK will guide the player to make a purchase. This ensures that a player who didn’t purchase your game won’t be able to enter the game even if they managed to obtain a copy of the game. - -## Installing SDK - -You can download the TapSDK from the [Downloads](/tap-download) page. Once you have the SDK on your computer, add them to your project: - - - -<> - -You can add the SDK **either manually or with the Unity Package Manager**. - -If you choose to use the Unity Package Manager, you should add the following dependencies into `Packages/manifest.json`: - - - {`"dependencies":{ - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.dlc": "https://github.com/TapTap/TapLicense-Unity.git#${sdkVersions.taptap.unity}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -}`} - - -If you choose to manually import the SDK, you should: - -* In the [download page](/tap-download), click `TapSDK Unity` to download `TapSDK-UnityPackage.zip`. -* Go to your Unity project, navigate to **Assets > Import Package > Custom Package**, select the `TapTap_Common` and `TapTap_License` modules from unzipped SDK. -* Download [LeanCloud-SDK-Storage-Unity.zip](https://github.com/leancloud/csharp-sdk/releases), unzip it as a `Plugins` folder, and drag and drop the folder into Unity. - - - -<> - -Put the SDK into `project/app/libs` and then add the following lines into `project/app/build.gradle`: - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - implementation name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar' - implementation name:'TapLicense_${sdkVersions.taptap.android}', ext:'aar' -}`} - - - - -<> - -```objc -// Not supported yet -``` - - - -<> - -Copy the ** TapLicense ** , ** TapCommon ** plugins into the project's plugins directory and add the dependencies to the ** build.cs ** file of the project module: - -```csharp -PublicDependencyModuleNames.AddRange(new string[] { - "Json", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapLicense", -}); -``` - - - - - -## Set Up Authorization Callback - - - -```cs -// The `License` library (required) -using TapTap.License; - -// By default, the SDK will display a window -// that can’t be closed manually by the player to avoid unauthorized players from entering the game. -// If you want to use a callback to trigger a customized procedure, -// please add the following code. -TapLicense.SetLicencesCallback(ITapLicenseCallback callback); - -public interface ITapLicenseCallback -{ - // Authorization success callback - void OnLicenseSuccess(); -} -``` - -```java -// By default, the SDK will display a window -// that can’t be closed manually by the player to avoid unauthorized players from entering the game. -// If you want to use a callback to trigger a customized procedure, -// please add the following code. -TapLicenseHelper.setLicenseCallback(new TapLicenseCallback() { - @Override - public void onLicenseSuccess() { - // Authorization success callback - } -}); -``` - -```objc -// Not supported yet -``` - -```cpp -// The `License` library is required. -#include "TapLicense.h" - -// By default, the SDK displays a popup that cannot be manually cancelled by the player to prevent unauthorised players from entering the game; if you need a callback to trigger the process, add the following code -FTapLicense::SetLicenseCallback(FSimpleDelegate::CreateLambda([]() { - // Authorisation successful -})); -``` - - - -## Payment and Authorization Verification - -The default value of the parameter in the Check method is false, which means that the SDK will confirm whether or not the currently logged in user has purchased the game via the TapTap client the first time and again after the 5th day from the first trigger. If you use the value of true, then every time the interface is called it will confirm whether or not the currently logged in user has purchased the game via the TapTap client. - - - -```cs -TapLicense.Check(); -TapLicense.Check(true) -``` - -```java -TapLicenseHelper.check(Activity activity); -TapLicenseHelper.check(Activity activity, boolean forceCheck); -``` - -```objc -// Not supported yet -``` - -```cpp -FTapLicense::Check(); -FTapLicense::Check(true); -``` - - - -## Compatibility with Android 11 and later versions - - - -## Testing - -To ensure that the game can determine whether a player has a valid purchase after it has been released, **please follow the instructions below to complete a self-testing.** - -### 1. Upload the APK - -Open your game in the Developer Center and go to **Game Services > Gaming Ecosystem > Copyright Verification > Package Name**. - -Here you can upload the APK to be tested. Once you upload the APK, please wait for the review process to be completed. - -### 2. Add Test Accounts - -Go to **Developer Center > Game Services > Gaming Ecosystem > Copyright Verification > Game Configuration > Manage testers** and enter the test accounts’ TapTap IDs. - -### 3. Price Settings - -You can set a price for your game by going to **Developer Center > Store > Price Settings**. For testing purposes, you can set the price to ¥0.01 CNY $1.00 USD. Once you set the price, please wait for the review process to be completed. - -### 4. Begin the Test - -Now you can open the TapTap app on your device and log in with a test account. Start the testing process from the store page of the app. - -You can make a purchase of the game with the test account and then download and enter the game. - -If you install the game with the APK without having the test account purchase the game first, there will be a pop-up when you open the game. The pop-up will say that the game is not activated and you should purchase the game from TapTap. - -## Start Selling Your Game - -Once you finish the testing process, you’ll be ready to sell your game. - -### 1. Complete Information - -Go to the Developer Center, fill in the information, and submit your game for review. - -### 2. Set Up Pricing - -Go to **Developer Center > Store > Price Settings**, make your game a paid game, set a price for your game, and submit the information for review. Don’t forget to update the TapTap operation staff on your progress. - -### 3. Official Release - -After completing the steps above, your game will be ready for the official release. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/dlc/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/dlc/_category_.json deleted file mode 100644 index 0e4f3783b..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/dlc/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "DLC", - "collapsed": true, - "position": 12 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/dlc/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/dlc/features.mdx deleted file mode 100644 index 1a624181d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/dlc/features.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: DLC Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { Conditional } from "/src/docComponents/conditional"; - -Many games offer downloadable content (DLC) that players can purchase to unlock. With TapTap Developer Services, you can easily implement the same feature in your game. The players can browse and purchase the products you set up in your game without leaving the game. You can even set up bundles that contain a collection of products. Once a player triggers the purchase operation in your game, the DLC SDK will handle the rest of the work to guide the player to make the purchase. - -Usually, a player will immediately receive the product once they make a purchase. If the player fails to make a purchase or chooses to cancel the purchase, your game will be notified by the SDK. Please follow the development guide to integrate the SDK into your game. - -## Enabling the Feature - - - -Please reach out to us if you wish to enable DLC in your game. - - - - - -Please send an email to [international_operation@taptap.com](mailto:international_operation@taptap.com) if you wish to enable DLC in the game. - - - -## Updating DLC - -To add more products available as DLC in your game, please add them to your game’s bundle and upload an updated version of the APK to TapTap. Once the player updates the game, they will see the added products in the game. - -## Selling and Refunding - -Your DLC will be available for players to purchase once you release your game. To provide the best experience to the players, TapTap allows players to request refunds on what they have purchased. You can view detailed sales data on [TapTap Developer Center](https://developer.taptap.cn/)[TapTap Developer Center](https://developer.taptap.io/). diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/dlc/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/dlc/guide.mdx deleted file mode 100644 index 7c5b00080..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/dlc/guide.mdx +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: DLC Integration Guide -sidebar_label: Guide -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Gray, Blue, Red, Black } from "/src/docComponents/doc"; -import { Conditional } from "/src/docComponents/conditional"; - -## Querying and Purchasing DLC - -Please [download](/tap-download) the TapSDK and add the following dependencies to your game: - - - -<> - -You can add the SDK **either manually or with the Unity Package Manager**. - -If you choose to use the Unity Package Manager, you should add the following dependencies into `Packages/manifest.json`: - - - {`"dependencies":{ - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.dlc": "https://github.com/TapTap/TapLicense-Unity.git#${sdkVersions.taptap.unity}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -}`} - - -If you choose to manually import the SDK, you should: - -* In the [download page](/tap-download), click `TapSDK Unity` to download `TapSDK-UnityPackage.zip`. -* Go to your Unity project, navigate to **Assets > Import Package > Custom Package**, select the `TapTap_Common` and `TapTap_License` modules from unzipped SDK. -* Download [LeanCloud-SDK-Storage-Unity.zip](https://github.com/leancloud/csharp-sdk/releases), unzip it as a `Plugins` folder, and drag and drop the folder into Unity. - - - -<> - -Import the SDK to the `project/app/libs` of your game project. Then open `project/app/build.gradle` and add the following lines: - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - implementation name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar' - implementation name:'TapLicense_${sdkVersions.taptap.android}', ext:'aar' -}`} - - - - -<> - -```objc -// Not supported yet -``` - - - - - -### DLC Callback Settings - - - -```cs -public class MyTapDLCCallback:ITapDlcCallback -{ - public void OnQueryCallBack(TapLicenseQueryCode code, Dictionary queryList) - { - - } - - public void OnOrderCallBack(string sku, TapLicensePurchasedCode status) - { - - } -} - -TapLicense.SetDLCCallback(new MyTapDLCCallback()); -``` - -```java -TapLicenseHelper.setDLCCallback(new DLCManager.InventoryCallback() { - @Override - public boolean onQueryCallBack(int i, HashMap hashMap) { - return false; - } - - @Override - public void onOrderCallBack(String s, int i) { - - } -}); -``` - -```objc -// Not supported yet -``` - - - -### Querying DLC - - - -```cs -TapLicense.QueryDLC(string[] skuIds); -``` - -```java -TapLicenseHelper.queryDLC(Activity activity, String[] skuIds); -``` - -```objc -// Not supported yet -``` - - - -### Purchasing DLC - - - -```cs -TapLicense.PurchaseDLC(string skuId); -``` - -```java -TapLicenseHelper.purchaseDLC(Activity activity, String skuIds); -``` - -```objc -// Not supported yet -``` - - - -### Parameters - -#### TapLicenseQueryCode - -| Callback | Callback value | Description | -| ------------------------------- | -------------- | ------------------------------------- | -| QUERY_RESULT_OK | 0 | Query succeeded | -| QUERY_RESULT_NOT_INSTALL_TAPTAP | 1 | TapTap is not installed on the device | -| QUERY_RESULT_ERR | 2 | Query failed | -| ERROR_CODE_UNDEFINED | 80000 | Unknown error | - -#### skuId - -This is a unique ID assigned to each product. Please reach out to TapTap to have them set up. - -## Testing - -To ensure that players are able to make purchases once you have released your game, **please follow the instructions below to test the payment procedure** - -### Upload APK - -Upload the APK of your game to the TapTap Developer Center and wait for it to go through the review process. - -### Add Test Users - -Go to TapTap Developer Center >> Click on Game Services >> Click on Game Ecosystem >> Click onCopyright Verification >> Enter the TapTap ID of the test user - -### Start Testing - -Log in to the TapTap app on your device with the test account. - -## Start Selling - -### Complete Application Information - -Go to TapTap Developer Center and fill in the application information, then wait for the review. - -### Set Prices - -Go to TapTap Developer Center >> Price Settings, enable the feature, set up the prices, and submit everything for review. - -### Release Your Game - -If everything worked out well, you may release your game. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/_category_.json deleted file mode 100644 index 62bdd9b00..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "内嵌动态", - "collapsed": true, - "position": 4 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/features.mdx deleted file mode 100644 index 0b5171c2d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/features.mdx +++ /dev/null @@ -1,199 +0,0 @@ ---- -title: Embedded Moments Features -sidebar_label: Features -sidebar_position: 1 ---- - -## Introduction - -By adding Embedded Moments to your game, you can have players access TapTap’s forum without leaving the game so they can easily browse walkthroughs, share their game highlights, and interact with other players and officials. - -## Core Advantages - -**For game developers:** - -- Allow players to share their gameplay with just a single click. -- Display officially published content on players’ feeds. -- View what players have posted and provide timely feedback to them. - -**For game players:** - -- Communicate and interact with other players on a delayed basis while gaming. -- Look up walkthroughs and top players’ solutions when stuck on certain scenes in the game. - -## Account System - -For a player to make a post or interact with other posts using Embedded Moments, they have to log in to a TapTap account. Therefore, you need to enable **[TapTap Login](/sdk/taptap-login/features/)** for your game before you can use Embedded Moments. - -![](https://capacity-files.lcfile.com/ILQvpPvMr6YfmdpSyMORt8ylRoNFjl75/taplogin-moment.png) - -## Moments - -### Game Forum - -A player can access the TapTap forum directly from the “Games” module: - -![](https://capacity-files.lcfile.com/9Kbu67cyRsrEXOk6H4flyBTVDV37lWtw/game.png) - -The following modules are included in the “Games” module: - -![](https://capacity-files.lcfile.com/KIBDwA9wha829rk2uH9ewWrnK4tWIdw8/game-detail.png) - -1. **Sub-groups**: Sub-groups can help users filter feeds by categories. The settings of sub-groups are shared between the TapTap forum and Embedded Moments. You can edit sub-groups in “Group Management Center > Sub-group” and the edits will be displayed to the players once they are approved. - -2. **Banners**: Banners can help you convey important notifications and events to players. This is a module exclusive to Embedded Moments. You can edit banners in “Gaming Ecosystem > Embedded Moments > Banner Configuration” and the edits will be displayed to the players once they are approved. - -3. **Recommendations**: The recommendations section can be used to hold shortcuts to posts, sub-groups, and index pages. The settings of recommendations are also shared between the TapTap forum and Embedded Moments. You can edit recommendations in “Group Management Center > Recommendation” and the edits will be displayed to the players once they are approved. - -4. **Feeds**: Trending posts are displayed to the player when they first enter Embedded Moments. The player can choose to sort posts by the time of the last reply or the time the post is published. - -The following functions are also available to you: - -- **Posting moments**: Players can post moments containing pictures and videos to the forum. - -![](https://capacity-files.lcfile.com/0xlsu9vXy0Hkwel36Snj1MRsf7sAvtDf/post.png) - -- **Interactions**: Players can **like, comment, and repost** other players’ moments. - -![](https://capacity-files.lcfile.com/OPvQNI7N8RFErcWlXJksJLTzRiCwBmju/repost.png) - -### Walkthroughs - -#### Walkthrough Labels - -The player can look for “labels” in the quick navigation bar on the right with each containing three pieces of walkthroughs: - -![](https://capacity-files.lcfile.com/Nzt5T3XotWMLSFBuJaBB9kXrPR4hsGqw/tips.png) - -The player can click on the “View all” button on the top to view all the walkthroughs under the current “label”: - -![](https://capacity-files.lcfile.com/J6wisBtATvR3H36kUGQyq5zc0Wp1RTJD/all-tips.png) - -#### Walkthrough Indexes - -You can set up indexes that are displayed under “All walkthroughs” according to the number and types of walkthroughs. - -![](https://capacity-files.lcfile.com/6dYy2HATlXhgT0O1IEuuTV7Pq1ObOVmI/all.png) - -#### Notes on Walkthroughs - -##### Why can’t I see the “Walkthroughs” module in my game’s Embedded Moments? How do I enable it? - -If you want to add a “Walkthroughs” module, please reach out to us by submitting a **ticket** in the TapTap Developer Center. - -##### Can I set up walkthroughs in the TapTap Developer Center? How should I do it? - -Unfortunately, you can’t set up walkthroughs yourself at this time. Please submit a **ticket** to us and we will have our professional editor team help you figure things out. - -##### Submitting Tickets - -To submit a ticket, go to the TapTap Developer Center and click on “Tickets” at the top-right corner. - -### Feeds - -Players who logged in to TapTap can view the moments posted by their friends and the official accounts. When there are new moments available, there will be a **badge** on the “Follow” section on the navigation bar. This ensures that players will never miss out on important updates. - -![](https://capacity-files.lcfile.com/6dYy2HATlXhgT0O1IEuuTV7Pq1ObOVmI/all.png) - -### Profile Page - -Players can find their past moments on the “Me” page. Here players can share their moments on other apps or delete their moments. - -![](https://capacity-files.lcfile.com/VXrB4UDQrhEILMd9CPyPNtWvVyreuLOf/me.png) - -Players can view notifications by tapping the “Alarm” icon on the top right corner. Interactions between players will trigger notifications, which encourages players to interact more with each other. - -![](https://capacity-files.lcfile.com/6HHynVGKc7nD0HmPAYM9RIP9SLgAUuPD/msg.png) - -## SDK Features - -### Scenario-Based Portals - -You can make any of the objects in your game as a portal that opens up Embedded Moments. You can even specify landing pages for certain scenes in the [Developer Center](#scenario-based-portal-configuration). This could be helpful if you want to allow players to quickly get help from the community when they’re stuck on certain scenes in the game. - -:::tip - -1. TDS doesn’t provide any guidelines for the design of portals. We encourage you to design your portals so they look harmonious with the scenes they’re placed at. -2. The landing page of a portal can be set up to be an article or a specific module according to your own needs. - -::: - -![](https://capacity-files.lcfile.com/8DLQ5ISYjGdB4Y8rXNPkvV2jnitUAzfe/scenario-portal.png) - -### Badges - -You can place buttons that can display badges in your game so that the players can be attracted to open the Embedded Moments when they see the badges. - -![](https://capacity-files.lcfile.com/D8CIJaldFjoS6fHkFmv92XqVClt7XYu0/red-dot.png) - -:::tip - -1. Using badges can help you increase the chance for players to open the Embedded Moments. We encourage you to place buttons with badges on prominent places in your game. -2. The badges here share the same logic as the badges for the “Follow” module within the Embedded Moments. The new content posted by the users followed by the players will trigger a notification, and the interval of retrieving new notifications is once per minute (here 1 minute is the minimum interval; you can change it to 3 minutes, 5 minutes, etc.). -3. Once the player opens the Embedded Moments, the game needs to clear the badge and continue inquiring for the next display of the badge. - -::: - -### Quick Sharing - -Players can take screenshots within the game and quickly share them on Embedded Moments. Only text and images can be shared through this method. - -![](https://capacity-files.lcfile.com/QUWwDR2u9lJxXiR2xvPNNhKpYf1iffRR/share.png) - -### Pop-up for Dynamically Closing Embedded Moments - -While the player is browsing Embedded Moments, if there are events that demand the player to immediately return to the game, a pop-up can be displayed to serve as a reminder and offer a shortcut for the player to close the Embedded Moments. - -![](https://capacity-files.lcfile.com/jDRpSrhLkTavEveFtbqgu183AVoDjHXp/popup.png) - -## Administration - -### Theme Configuration - -To have Embedded Moments fit better with the game scenes and not make players feel cut off, TDS allows you to customize the theme of the Embedded Moments. You can upload a background image and specify the colors of texts in “Game Services” > “Embedded Moments” > “Theme”. - -:::tip - -1. You can use [Embedded Moments Design Specifications](/design/design-moment/) as a reference. -2. If the game only supports landscape _or_ portrait mode, you only need to provide one background image. Otherwise, you need to provide background images for both. -3. Images are subject to review, which usually takes 2 business days. - -::: - -![](https://capacity-files.lcfile.com/tAQ4vo4oypHCWQgVxRrSdjdR79wc88zn/io-tapmoment-theme-config.png) - -### Banner Configuration - -You can set up banners in Embedded Moments to help you broadcast your events to the players. To set up banners, go to “Game Services” > “Embedded Moments” > “Banner Configuration”. **A title, background image, and link** are required for each banner. - -:::tip - -1. You can add up to 5 banners that link to any website. -2. Banners are subject to review, which usually takes less than one day. - -::: - -![](https://capacity-files.lcfile.com/NcVau0qn8pM93pDxXXfFa651SKPyoDgS/io-banner-config.png) - - -### Scenario-Based Portal Configuration - -You can set up scenario-based portals in “Game Services” > “Embedded Moments” > “Scenario-based Portal”. Once you submit a **portal name, landing page type, and landing page**, you can use the generated portal ID in your game. This module doesn’t require any reviews, and you are free to change the landing page of each portal. - -![](https://capacity-files.lcfile.com/tgh3I8pB3R8bVlxTYQHA9lj7TOYeETvV/io-scenario-based-portal-configuration.png) - -## Group Management Center - -![](https://capacity-files.lcfile.com/NblfCHQ5MVVAO54TqJKRAHqnh6Isk9m9/forum-management.png) - -### Group Data - -To help you better assess the value of Embedded Moments and get feedback about the quality of content, you can view data associated with Embedded Moments through “Game Services” > “Embedded Moments” > “Group Management Center”. Make sure you have the permission required to view such data. - -### Recommendations - -You can set up recommendations in “Group Management Center” > “Recommendation”. **A title, image, and link** are required for each recommendation. Adding and editing recommendations are subject to review. - -### Sub-Groups - -You can set up sub-groups in “Group Management Center” > “Sub-group”. **A name (in multiple languages)** is required for each sub-group. Adding and editing sub-groups are subject to review. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/guide.mdx deleted file mode 100644 index 9c35a423d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/embedded-moments/guide.mdx +++ /dev/null @@ -1,438 +0,0 @@ ---- -title: Embedded Moments Guide -sidebar_label: Guide -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -In this article, we will introduce how you can add [TapTap Embedded Moments](/sdk/embedded-moments/features/) to your game. Embedded Moments depends on TapTap Login, which requires the `TapMoment` module. - -## Installing SDK - -:::info - -- If you are [using TapTap Login without other TDS cloud services](/sdk/taptap-login/guide/tap-login/), please see [Using Embedded Moments Without TDS Cloud Services](#using-embedded-moments-without-tds-cloud-services) for how to initialize your app and install the SDK. - -::: - -If you are coming from [TapSDK Quickstart](/sdk/start/quickstart/#initialization) and have already initialized the SDK, you can add the `TapMoment` module of the TapSDK obtained from the [Downloads](/tap-download) page: - - - - - {`"dependencies":{ - ... - // Embedded Moments - "com.taptap.tds.moment":"https://github.com/TapTap/TapMoment-Unity.git#${sdkVersions.taptap.unity}", -}`} - - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapMoment_${sdkVersions.taptap.android}', ext:'aar') // TapTap Embedded Moments -}`} - - - - {`// Embedded Moments -TapMomentResource.bundle -TapMomentSDK.framework -`} - - - - -## Setting up Callbacks - -You can set up a callback to capture status updates: - - - -```cs -TapMoment.SetCallback((code, msg) => { - Debug.Log(code + "---" + msg); -}); -``` - -```java -TapMoment.setCallback(new TapMoment.TapMomentCallback() { - @Override - public void onCallback(int code, String msg) { - - } -}); -``` - -```objectivec -@interface ViewController () -@end - -[TapMoment setDelegate:self]; -- (void)onMomentCallbackWithCode:(NSInteger)code msg:(NSString *)msg -{ - NSLog (@"msg:%@, code:%i", msg, code); -} -``` - - - -The `code` in the callback function refers to the type of the event. The following types of events are supported: - -| Callback | Value | Description | -| -------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------- | -| CALLBACK_CODE_PUBLISH_SUCCESS | 10000 | Moment posted successfully. | -| CALLBACK_CODE_PUBLISH_FAIL | 10100 | Failed to post the moment. | -| CALLBACK_CODE_PUBLISH_CANCEL | 10200 | The page for posting moments is closed. | -| CALLBACK_CODE_GET_NOTICE_SUCCESS | 20000 | Notifications retrieved successfully. | -| CALLBACK_CODE_GET_NOTICE_FAIL | 20100 | Failed to retrieve notifications. | -| CALLBACK_CODE_MOMENT_APPEAR | 30000 | Embedded Moments is opened. | -| CALLBACK_CODE_MOMENT_DISAPPEAR | 30100 | Embedded Moments is closed. | -| CALLBACK_CODE_CLOSE_CANCEL | 50000 | The user refused to close all the Embedded Moments pages (the “Cancel” button on the pop-up is tapped). | -| CALLBACK_CODE_CLOSE_CONFIRM | 50100 | The user confirmed to close all the Embedded Moments pages (the “Confirm” button on the pop-up is tapped). | -| CALLBACK_CODE_LOGIN_SUCCESS | 60000 | Logged in successfully. | -| CALLBACK_CODE_SCENE_EVENT | 70000 | Callback for scenario-based portals. | - -## Retrieving Notifications - -You can periodically call the API to retrieve unread notifications. When there are unread notifications available, you can display a badge on the button for Embedded Moments to remind the player to check the notifications. - - - -```cs -TapMoment.FetchNotification(); -``` - -```java -TapMoment.fetchNotification(); -``` - -```objectivec -[TapMoment fetchNotification]; -``` - - - -The result of retrieving unread notifications will be available in the callback function mentioned earlier. If you get `CALLBACK_CODE_GET_NOTICE_SUCCESS` (`20000`) for the `code`, that means the notifications are retrieved successfully. `CALLBACK_CODE_GET_NOTICE_FAIL` (`20100`) means the SDK failed to retrieve the notifications. If the notifications are retrieved successfully, `msg` will be the number of unread notifications. When `msg` is `0`, it means there are no unread notifications. - -:::tip - -To make it easier for the player to view what you and their friends have posted, we suggest that you put the button for Embedded Moments on a prominent position in your game and retrieve unread notifications **once a minute**. - -When retrieving notifications, if there are no unread notifications (`msg` equals `0`), you can remove the badge displayed on the button. You should also remove the badge once the player opens Embedded Moments. - -::: - -## Displaying Embedded Moments - -You can call the following API to display Embedded Moments in your game. Here the player can view the moments posted by other players and post their own as well. - - - -```cs -TapMoment.Open(Orientation.ORIENTATION_LANDSCAPE); -``` - -```java -TapMoment.open(TapMoment.ORIENTATION_PORTRAIT); -``` - -```objectivec -TapMomentConfig *mConfig = TapMomentConfig.new; -mConfig.orientation = TapMomentOrientationDefault; -[TapMoment open:mConfig]; -``` - - - -:::note - -Embedded Moments might contain videos that have sounds. Therefore, please mute the sounds from the game itself when the Embedded Moments is opened. - -To make Embedded Moments rotate along with the device, your game has to support rotation as well. - -Don’t forget to remove the badge after the player opens the Embedded Moments. - -::: - -The following picture shows how you can customize the background image of the Embedded Moments page in your game. The background image will be reviewed before the players can see them. - -![](https://capacity-files.lcfile.com/tAQ4vo4oypHCWQgVxRrSdjdR79wc88zn/io-tapmoment-theme-config.png) - -## Scenario-Based Portals - -With [Scenario-Based Portals](/sdk/embedded-moments/features/#scenario-based-portals), the player can be taken to specific pages when they open Embedded Moments through them. Make sure to [set up Scenario-Based Portals](/sdk/embedded-moments/features/#scenario-based-portal-configuration) in TapTap Developer Center before you use this feature. - - -<> - -```cs -var sceneDic = new Dictionary() { { TapMomentConstants.TapMomentPageShortCutKey, sceneId } }; -// sceneId is the “portal ID” generated when you create a scenario-based portal in TapTap Developer Center -TapMoment.DirectlyOpen(Orientation.ORIENTATION_DEFAULT, TapMomentConstants.TapMomentPageShortCut, sceneDic); -``` - -#### Arguments - -| Argument | Description | -| ----------- | ------------------------------------------------------------------------------------------------------------------------ | -| orientation | The screen orientation for displaying the Embedded Moments. | -| page | Should be `TapMomentConstants.TapMomentPageShortCut`. | -| Dictionary | Leave `TapMomentConstants.TapMomentPageShortCutKey` as it is and change the third argument to the ID of the target page. | - - -<> - -```java -Map extras = new HashMap<>(); -// Note: The key is always "scene_id" and the second argument is the “portal ID” generated when you create a scenario-based portal in TapTap Developer Center -extras.put("scene_id", "xxxx"); -// Note: The second argument is always "tap://moment/scene/" -TapMoment.directlyOpen(TapMoment.ORIENTATION_DEFAULT,"tap://moment/scene/", extras); -``` - -#### Arguments - -| Argument | Description | -| ----------- | ----------------------------------------------------------------------- | -| orientation | The screen orientation for displaying the Embedded Moments. | -| page | Should be `tap://moment/scene/`. | -| HashMap | The key is always `scene_id` which indicates the ID of the target page. | - - -<> - -```objectivec -TapMomentConfig *mConfig = TapMomentConfig.new; -mConfig.orientation = TapMomentOrientationDefault; -[TapMoment directlyOpen:mConfig page:TapMomentPageShortcut extras:@{ TapMomentPageShortcutKey: @"sceneid" }]; -``` - -#### Arguments - -| Argument | Description | -| ----------- | --------------------------------------------------------------- | -| orientation | The screen orientation for displaying the Embedded Moments. | -| page | Should be `TapMomentPageShortcut`. | -| Dictionary | `TapMomentPageShortcutKey` indicates the ID of the target page. | - - - - - -### Callbacks for Scenario-Based Portals - -**Properties** - -| Field name | Type | Required | Description | -| ------------ | ------- | -------- | ---------------------------------------------------------- | -| sceneId | String | Yes | The ID of the scenario-based portal. | -| eventType | String | Yes | The type of the event, like `VIEW`, `FORWARD`, and `VOTE`. | -| eventPayload | String | Yes | Custom JSON string depending on the event type. | -| timestamp | Integer | Yes | UNIX timestamp in ms. | - -**Event types** - -| eventType | eventPayload (unserialized) | Description | -| --------- | --------------------------- | ------------------------------------------------- | -| READY | {} | The DOM has been mounted and the data is pending. | -| REPOST | {} | The user reposts a post. | -| VOTE | { isCancel: boolean } | The user likes (or removes their like on) a post. | -| FOLLOW | { isCancel: boolean } | The user follows (or unfollows) a post. | -| COMMENT | {} | The user comments on a post. | - -## Closing Embedded Moments - -The player can close the Embedded Moments at any time to return to the game. In certain scenarios, the game itself can also request to close the Embedded Moments. - -A case for the game to initiate a request to close the Embedded Moments is when the player is matched with another player to start a battle. Here you can confirm with the player whether they want to stay in the Embedded Moments or return to the game immediately. - - - -```cs -TapMoment.Close("Are you ready?", "The game is ready to start."); -``` - -```java -TapMoment.closeWithConfirmWindow("Are you ready?", "The game is ready to start."); -``` - -```objectivec -[TapMoment closeWithTitle:@"Are you ready?" content:@"The game is ready to start." showConfirm:YES]; -``` - - - -The player’s selection will be returned with a callback: - -- `CALLBACK_CODE_CLOSE_CANCEL` (50000) means the player selected “Cancel” and wants to stay in Embedded Moments. -- `CALLBACK_CODE_CLOSE_CONFIRM` (50100) means the player selected “Confirm” and wants to return to the game. - -To close the Embedded Moments without confirming: - - - -```cs -TapMoment.Close(); -``` - -```java -TapMoment.close(); -``` - -```objectivec -[TapMoment close]; -``` - - - -## Quick Sharing - -:::info - -This feature is optional. You can choose whether or not to enable this feature in your game. - -::: - -In general, the play can post their moments on the Embedded Moments page. But if you’d like, you can also allow the player to post moments containing images and texts without leaving the game. - - - -```cs -string content = "This is the content"; -string[] images = {"imgpath01","imgpath02","imgpath03"}; -TapMoment.Publish(Orientation.ORIENTATION_LANDSCAPE, images, content); -``` - -```java -int orientation = TapMoment.ORIENTATION_PORTRAIT; -String content = "This is the content"; -String[] imagePaths = new String[]{"content://hello.jpg", "/sdcard/world.jpg"}; -TapMoment.publish(orientation, imagePaths, content); -``` - -```objectivec -TapMomentConfig * tconfig = TapMomentConfig.new; -tconfig.orientation = TapMomentOrientationDefault; - -TapMomentImageData *postData = TapMomentImageData.new; -postData.images = @[@"file://..."]; -postData.content = @"This is the content"; -[TapMoment publish:tconfig content:(postData)]; -``` - - - -:::info - -The player can post texts, images, and videos on the Embedded Moments page. However, they can only post texts and images with quick sharing. - -::: - -## Using Embedded Moments Without TDS Cloud Services - -See the following instructions if you are [using TapTap Login without TDS cloud services](/sdk/taptap-login/guide/tap-login/). - -1. Please first [download](/tap-download) the TapSDK and add the required dependencies. Embedded Moments depends on `TapLogin`, `TapCommon`, and `TapMoment`. - -If you use TapSDK Unity v3.7.1 or a higher version, make sure that you add the `com.leancloud.storage` module. - - - - - {`"dependencies":{ - "com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.moment":"https://github.com/TapTap/TapMoment-Unity.git#${sdkVersions.taptap.unity}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -}`} - - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') // Required: TapSDK essentials - implementation (name:'TapLogin_${sdkVersions.taptap.android}', ext:'aar') // Required: TapTap Login - implementation (name:'TapMoment_${sdkVersions.taptap.android}', ext:'aar') // TapTap Embedded Moments -}`} - - - - {`// Essentials -TapCommonSDK.framework -TapLoginSDK.framework -TapCommonResource.bundle -TapLoginResource.bundle -// Embedded Moments -TapMomentResource.bundle -TapMomentSDK.framework -`} - - - - -2. Make sure you have finished [initializing TapTap Login](/sdk/taptap-login/guide/tap-login/#initialization). - -3. To **initialize** Embedded Moments: - - -<> - -```cs -TapMoment.Init(string clientID, boolean isCN); -``` - -**Arguments** - -| Argument | Description | -| -------- | --------------------------- | -| clientID | The Client ID of your game. | -| isCN | true for Mainland China; false for other countries/regions. | - - -<> - -```java -TapMoment.init(Context context, String clientID, boolean isCN); -``` - -**Arguments** - -| Argument | Description | -| -------- | -------------------------------- | -| context | Usually the current application. | -| clientID | The Client ID of your game. | -| isCN | true for Mainland China; false for other countries/regions. | - - -<> - -```objectivec -[TapMoment initWithClientID:@"your clientId" isCN:isCN]; -``` - -**Arguments** - -| Argument | Description | -| -------- | --------------------------- | -| clientId | The Client ID of your game. | -| isCN | true for Mainland China; false for other countries/regions. | - - - - - -4. Now you’re good to use the other interfaces mentioned on this page. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/gamesaves/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/gamesaves/_category_.json deleted file mode 100644 index 7226c3acc..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/gamesaves/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "云存档", - "collapsed": true, - "position": 9 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/gamesaves/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/gamesaves/features.mdx deleted file mode 100644 index abb86de4a..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/gamesaves/features.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Cloud Save Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -With Cloud Save, you can let your game save the players’ progress on the cloud as checkpoints. Your game can then retrieve those checkpoints from the cloud and let players continue from any checkpoint saved from any device. - -Cloud Save allows you to synchronize players’ progress across multiple devices. For example, with Cloud Save, if you have an Android game, your player can start the game on a phone and continue the game on a tablet without losing their progress. Even when the player lost their device, damaged their device, or got a new device, they can still continue the game from where they left off. - -To learn how to add Cloud Save into your game, see [Cloud Save Guide](/sdk/gamesaves/guide/). - -## The Basics - -Each checkpoint is made up of two parts: - -- A binary file containing the progress, which your game can read and update. -- [Metadata](/sdk/gamesaves/features#metadata) holding some useful information related to this checkpoint. - -## Cover Image - -Each checkpoint has a cover image as part of its metadata. We strongly recommend that you pick an image that best describes the checkpoint. - -## Checkpoint Summary - -You can assign a brief summary to each checkpoint. The summary is intended to be seen by the player, so it should reflect the status represented by the checkpoint. An example would be “Fighting with the black-clad man on the rooftop”. - -## Resolving Conflicts - -If a player runs multiple instances of your game on different devices at the same time, there could be conflicts when these instances try to save the progress data to the cloud. Your app should resolve such conflicts in a manner that provides the best user experience to the players. - -Conflicts usually happen when your app tries to load or save data but is unable to make a connection to the cloud. The best way to avoid conflicts is to always have the app load the latest data from the cloud when it is opened or resumed, as well as regularly save the local data to the cloud. Your app should make the best attempt to resolve such conflicts in order to preserve players’ data and bring the best experience to the players. - -## Metadata - -The metadata of each checkpoint contains the following fields: - -| Field meaning | Field name | Required | Description | -| ------------------ | --------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Checkpoint ID | `objectId` | Automatically generated | A unique ID generated by the cloud for the checkpoint. Use this ID to reference the checkpoint in your game. | -| Associated user | `user` | Automatically obtained | The SDK automatically associates the checkpoint with the current user. | -| Original file | `gameFile` | Required | The original file containing the progress. | -| Checkpoint name | `name` | Required | A brief name assigned to the checkpoint. The name is not visible to the player. You can group checkpoints by their names if you assign a custom rule to the names. | -| Checkpoint summary | `summary` | Required | A string \*_no longer than 1000 characters_. You can assign each checkpoint a summary that is visible to the player. | -| Modified at | `modifiedAt` | Required | The time the original file is modified or added. | -| Time played | `playedTime` | Optional | The time the player has spent on the game in milliseconds. | -| Progress | `progressValue` | Optional | An integer indicating the game progress. An example is the current level of the game. | -| Cover image | `cover` | Optional | An [image](/sdk/gamesaves/features#cover-image) provided by you. At this time, you can only upload PNG and JPG files with the SDK. | diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/gamesaves/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/gamesaves/guide.mdx deleted file mode 100644 index b6507f229..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/gamesaves/guide.mdx +++ /dev/null @@ -1,465 +0,0 @@ ---- -title: Cloud Save Guide -sidebar_label: Guide -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; - -Before continuing, make sure you have read [Cloud Save Introduction](/sdk/gamesaves/features/) to learn about the basic concepts and features of Cloud Save. - -## Prerequisites - -Please complete the following steps before using Cloud Save: - -1. Add your [domains](/sdk/start/get-ready/#domains), including API domains and file domains, on the Developer Center. - -2. Enable **TDS Authentication** since the checkpoints will be associated with `TDSUser`s. Make sure you have completed [project configuration](/sdk/start/quickstart/#project-configuration), [SDK initialization](/sdk/start/quickstart/#initialization), and the implementation of [TDS Authentication](/sdk/authentication/guide/). - -## Creating Checkpoints - -The SDK will automatically get the information of the currently logged-in player (`TDSUser`) and associate it with the checkpoint. -Therefore, a user can only create a checkpoint if they are logged in. - - - -```cs -var gameSave = new TapGameSave -{ - Name = "internal name", - Summary = "description", - ModifiedAt = DateTime.Now.ToLocalTime(), - PlayedTime = 60000L, // ms - ProgressValue = 100, - CoverFilePath = image_local_path, // jpg/png - GameFilePath = dll_local_path -}; -await gameSave.Save(); -``` - -```java -TapGameSave snapshot = new TapGameSave(); -snapshot.setName("internal name"); -snapshot.setSummary("description"); -snapshot.setPlayedTime(60000); // ms -snapshot.setProgressValue(100); -snapshot.setCover(image_local_path); // jpg/png -snapshot.setGameFile(dll_local_path); -snapshot.setModifiedAt(new Date()); -snapshot.saveInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable d) {} - - @Override - public void onNext(@NotNull TapGameSave gameSave) { - System.out.println("Checkpoint saved: " + gameSave.toJSONString()); - } - - @Override - public void onError(@NotNull Throwable e) { - e.printStackTrace(); - } - - @Override - public void onComplete() {} -}); -``` - -```objc -TapGameSave *gameSave = [TapGameSave new]; -gameSave.name = @"internal name"; -gameSave.summary = @"description"; -gameSave.modifiedAt = [NSDate date]; -gameSave.playedTime = 60000; // ms -gameSave.progressValue = 100; -[gameSave setCoverWithLocalPath:@"image_local_path" error:&error]; // jpg/png -[gameSave setGameFileWithLocalPath:@"dll_local_path" error:&error]; -[gameSave saveInBackgroundWithBlock:^(BOOL succeeded, NSError *_Nullable error) { - if (succeeded) { - NSLog(@"Checkpoint saved. objectId: %@", gameSave.objectId); - } else { - // Error handling - } -}]; -``` - - - -See [Metadata](/sdk/gamesaves/features#metadata) to learn more about the fields of metadata appearing in the example above. -When you save a checkpoint, the SDK will ensure that only the current user can read and write into the checkpoint as well as the associated file and the cover image. - -## Retrieving Checkpoints - -The most common scenario is to retrieve all the checkpoints belonging to the current player: - - - -<> - -```cs -var collection = await TapGameSave.GetCurrentUserGameSaves(); - -foreach(var gameSave in collection){ - var summary = gameSave.Summary; - var modifiedAt = gameSave.ModifiedAt; - var playedTime = gameSave.PlayedTime; - var progressValue = gameSave.ProgressValue; - var coverFile = gameSave.Cover; - var gameFile = gameSave.GameFile; - var gameFileUrl = gameFile.Url; -} -``` - -`gameFile.Url` is the URL of the file stored on the cloud. The extension of the downloaded file will be the same as that of the uploaded file. - - -<> - -```java -TapGameSave.getCurrentUserGameSaves() - .subscribe(new Observer>() { - @Override - public void onSubscribe(@NotNull Disposable d) {} - - @Override - public void onNext(@NotNull List tapGameSaves) { - for (TapGameSave gameSave : tapGameSaves) { - String summary = gameSave.getSummary(); - Date modifiedAt = gameSave.getModifiedAt(); - double playedTime = gameSave.getPlayedTime(); - int progressValue = gameSave.getProgressValue(); - LCFile cover = gameSave.getCover(); - LCFile gameFile = gameSave.getGameFile(); - } - } - - @Override - public void onError(@NotNull Throwable e) { - e.printStackTrace(); - } - - @Override - public void onComplete() {} -}); -``` - - -<> - -```objc -LCQuery *query = [TapGameSave queryWithCurrentUser]; -[query findObjectsInBackgroundWithBlock:^(NSArray *_Nullable gameSaves, NSError *_Nullable error) { - if (error) { - NSLog(@"test fail because %@", error); - } else { - for (TapGameSave *gameSave in gameSaves) { - NSString *summary = gameSave.summary; - NSDate *modifiedAt = gameSave.modifiedAt; - double playedTime = gameSave.playedTime; - int progressValue = gameSave.progressValue; - LCFile* cover = gameSave.cover; - LCFile* gameFile = gameSave.gameFile; - } - } -}]; -``` - - - - - -You can also retrieve checkpoints that meet certain criteria. For example, to retrieve all the current player’s checkpoints with progress larger than 3: - - - -<> - -```cs -TDSUser user = await TDSUser.GetCurrent(); -LCQuery gameSaveQuery = TapGameSave.GetQueryWithUser(user); -gameSaveQuery.WhereGreaterThan("progressValue", 3); -var collections = await gameSaveQuery.Find(); -``` - -See [Data Storage Guide](/sdk/storage/guide/dotnet/) for how to add constraints to queries. - - -<> - -```java -LCQuery gameSaveQuery = TapGameSave.getQueryWithUser(); -gameSaveQuery.whereGreaterThan("progressValue", 3); -gameSaveQuery.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List gamesaves) { - // gamesaves is an array containing checkpoint objects meeting the criteria - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -See [Data Storage Guide](/sdk/storage/guide/java/) for how to add constraints to queries. - - -<> - -```objc -LCQuery *query = [TapGameSave queryWithCurrentUser]; -[query whereKey:@"progressValue" greaterThan:@3]; -[query findObjectsInBackgroundWithBlock:^(NSArray *_Nullable gameSaves, NSError *_Nullable error) { - // Things to do with the retrieved checkpoint objects -}]; -``` - - - - - -Notice that **if none of the players of your game have ever created a checkpoint, you will get an error when retrieving checkpoints that says** `Class or object doesn't exists.`. -This is because the table (class) for storing checkpoint objects in our database only gets created when the first checkpoint object gets created. - -## Deleting Checkpoints - -A player can only delete their own checkpoints. - -To delete a checkpoint: - - - -```cs -await gameSave.Delete(); -``` - -```java -gameSave.deleteInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) {} - - @Override - public void onNext(LCNull response) { - // Deleted. - } - - @Override - public void onError(@NonNull Throwable e) { - System.out.println("Failed to delete:" + e.getMessage()); - } - - @Override - public void onComplete() {} -}); -``` - -```objc -[gameSave deleteInBackground]; -``` - - - -When a checkpoint gets deleted, the associated cover image and the original file will also be deleted. - -## REST API - -Below are the REST API interfaces available for Cloud Save. -You can write your own programs to access these interfaces and perform administrative operations on the server side. - -### Request Format - -For POST and PUT requests, the request body must be in JSON and the Content-Type of the HTTP Header must be `application/json`. - -Requests are authenticated by the key-value pairs in the HTTP Header shown in the following table: - -| Key | Value | Description | Source | -| -------------- | ---------------- | ------------------------------------------------- | ------------------------------------ | -| `X-LC-Id` | `{{appid}}` | The `App Id` (`Client Id`) of the current app | Can be found on the Developer Center | -| `X-LC-Key` | `{{appkey}}` | The `App Key` (`Client Token`) of the current app | Can be found on the Developer Center | -| `X-LC-Session` | `` | The player’s credential for logging in | | - -The `Master Key` is required for you to access the administration interface. Use `X-LC-Key: {{masterkey}},master` to have the server treat the given key as a master key. -`Master Key` is also called `Server Secret`, which can be found on the Developer Center. When accessing the administration interface, `sessionToken` can be omitted. - -See [Credentials](/sdk/storage/guide/setup-dotnet#credentials) for more details. - -The cloud imposes restrictions on each checkpoint so that it can only be accessed by the player who created it. Therefore, when accessing the REST API, you have to either include a player’s `sessionToken` in the `X-LC-Session` HTTP header or use the `Master Key`, otherwise the request will fail due to a lack of permission. - -### Base URL - -The Base URL for REST API requests (the `{{host}}` in the curl examples) is the custom API domain of your app. You can update or find it on the Developer Center. -See [Domain](/sdk/storage/guide/setup-dotnet#domain) for more details. - -### Interfaces - -| Name | Method | Address | Description | -| --------------------- | ------ | ---------------- | ------------------------------------------------ | -| Retrieve a checkpoint | GET | `/gamesaves/:id` | Retrieve a checkpoint by its ID. | -| Retrieve checkpoints | GET | `/gamesaves` | Retrieve checkpoints that meet certain criteria. | -| Add a checkpoint | POST | `/gamesaves` | Add a new checkpoint. | -| Update a checkpoint | PUT | `/gamesaves/:id` | Update a checkpoint by its ID. | -| Delete a checkpoint | DELETE | `/gamesaves/:id` | Delete a checkpoint by its ID. | - -### Retrieving a Checkpoint - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - https://API_BASE_URL/1.1/gamesaves/ -``` - -Response: - -```json -{ - "updatedAt": "2021-08-16T09:18:30.093Z", - "progressValue": 123, - "name": "dennis", - "objectId": "611a2d65bcf94a3222b6d5f3", - "createdAt": "2021-08-16T09:18:29.761Z", - "gameFile": { - "__type": "Pointer", - "className": "_File", - "objectId": "60d1af149be3180684000002" - }, - "summary": "hello", - "modifiedAt": { - "__type": "Date", - "iso": "2015-06-21T18:02:52.249Z" - }, - "user": { - "__type": "Pointer", - "className": "_User", - "objectId": "5b62c15a9f54540062427acc" - } -} -``` - -See [Metadata](/sdk/gamesaves/features#metadata) to learn more about the fields of metadata. - -### Retrieving Checkpoints - -Use `where` to specify the criteria: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"progressValue":123}' \ - https://API_BASE_URL/1.1/gamesaves -``` - -Response: - -```json -{ - "results": [ - { - "updatedAt": "2021-08-16T09:30:20.643Z", - "name": "dennis", - "createdAt": "2021-08-16T09:30:20.643Z", - "gameFile": { - "__type": "Pointer", - "className": "_File", - "objectId": "60d1af149be3180684000002" - }, - "summary": "hello", - "modifiedAt": { - "__type": "Date", - "iso": "2015-06-21T18:02:52.249Z" - }, - "objectId": "611a302cbcf94a3222b6d687" - } - ] -} -``` - -See [Data Storage REST API](/sdk/storage/guide/rest#query-constraints) for more details about the usage of `where`. - -### Adding a Checkpoint - -See [Metadata](/sdk/gamesaves/features#metadata) to learn more about the required and optional fields of metadata. - -Before accessing this interface, please first create the [files](/sdk/storage/guide/rest/#creating-files) referenced by the `gameFile` and `cover` fields of the checkpoint. Make sure **the ACLs of the files are set to be accessible by the current user only**. - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - -d '{ - "progressValue":123, - "playedTime":1283490343, - "name":"dennis", - "gameFile":{"id":"55a39634e4b0ed48f0c1845c", "__type":"File"}, - "cover":{"id": "543cbaede4b07db196f50f3c", "__type": "File"}, - "summary":"hello", - "modifiedAt":{"__type":"Date", "iso":"2015-06-21T18:02:52.249Z"} - }' \ -https://API_BASE_URL/1.1/gamesaves -``` - -If the operation succeeded, the `objectId` and creation time of the checkpoint will be returned: - -``` -{"objectId":"611a3407bcf94a3222b6d789", "createdAt":"2021-08-16T09:46:47.290Z"} -``` - -If the operation failed, there will be an error, like: - -- `gameFile is required.`: The required field `gameFile` is not provided. -- `Forbidden to add new fields by class '_GameSave' permissions.`: Custom fields are included in the request, which is not allowed yet. - -### Deleting a Checkpoint - -``` -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - https://API_BASE_URL/1.1/gamesaves/ -``` - -Response: - -```json -{} -``` - -You can add a `where` condition when deleting a checkpoint so you won’t accidentally delete the wrong checkpoints. -See [Conditional Deletions](/sdk/storage/guide/rest#conditional-deletions). - -When a checkpoint gets deleted, the associated cover image and the original file will also be deleted. - -### Updating a Checkpoint - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - -d '{"progressValue": 114514}' \ - https://API_BASE_URL/1.1/gamesaves/ -``` - -If the operation succeeded, the `objectId` and creation time of the checkpoint will be returned: - -```json -{ - "updatedAt": "2021-08-16T09:49:49.579Z", - "objectId": "611a34bdbcf94a3222b6d7af" -} -``` - -If the operation failed, there will be an error: - -- `Forbidden to add new fields by class '_GameSave' permissions.`: Custom fields are included in the request, which is not allowed yet. - -Notice that if you update the cover image or the original file of a checkpoint, the old files used as them will not be automatically deleted. -You will need to manually [delete](/sdk/storage/guide/rest/#deleting-files) them. -Therefore, we recommend that you update a checkpoint by deleting and recreating it instead of directly updating it. The interface for updating checkpoints is mainly used to serve administrative scenarios. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/_category_.json deleted file mode 100644 index 7e7e6520d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "即时通讯", - "collapsed": true, - "position": 18 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/_category_.json deleted file mode 100644 index d61b21d85..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "最佳实践", - "collapsed": true, - "position": 3 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/hook-text-moderation.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/hook-text-moderation.mdx deleted file mode 100644 index d607b35aa..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/hook-text-moderation.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Text Moderation With Instant Messaging -sidebar_label: Text Moderation ---- - -This article introduces how you can integrate third-party text moderation services into your application using a hook offered by the Instant Messaging service. - -## Before You Start - -This article assumes that you already know about the hooks offered by the Instant Messaging service as well as Cloud Engine’s web interface for editing Cloud Functions. If you haven’t learned about them yet, please first take a look at the following pages: - -1. [Hooks and System Conversations](/sdk/im/guide/systemconv/) -2. [Cloud Functions and Hooks Guide § Writing Cloud Functions Online](/sdk/engine/functions/guides#writing-cloud-functions-online) - -## Enabling the Feature - -1. With the Cloud Engine service enabled, go to **Cloud Engine > web > Deploy > Create function** and select `Hook` in the pop-up window. Select `_messageReceived` as the name of the hook and provide your code below. Once you finish creating the hook, click on **Deploy** and wait for the deployment to complete. - -![](https://capacity-files.lcfile.com/dmLYApCx0fTIx0FCsrJbs4PnXwPGkiA2/Frame%202%20%281%29.png) - -2. Now when a message is sent through the Instant Messaging service, you will see that the message gets changed according to the mechanism you set up. - -## Code Example - -Below is a code example for the hook written in Node.js. You can use it as a boilerplate for your code: - -```javascript -const https = require("https"); - -// Assuming that the third-party text moderation service requires authentication using the HTTP Header -const authToken = "THE_TOKEN_FOR_THE_THIRD_PARTY_TEXT_MODERATION_SERVICE"; - -const params = request.params; - -// Submit the message and the user ID to the third-party text moderation service -// Different services may require different parameters -const postData = JSON.stringify({ - data: { - text: params.content, - user_id: params.fromPeer, - }, -}); - -const options = { - hostname: "third-party-text-moderation.example.com", - port: 443, - path: "/path/to/text/moderation/interface", - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Third-Party-Auth": authToken, - }, -}; - -return new Promise((resolve, reject) => { - const req = https.request(options, (res) => { - if (res.statusCode != 200) { - // or resolve(null) - reject(new Error(`BAD STATUS: ${res.statusCode}`)); - return; - } - let body = ""; - res.setEncoding("utf8"); - res.on("data", (chunk) => { - body += chunk; - }); - res.on("end", () => { - json = JSON.parse(body); - - if (json.result == 0) { - resolve(null); - } else { - // Assuming that the third-party text moderation service will return the filtered text as the value of the `filtered_text` field - if (json.filtered_text) { - resolve({ content: json.filtered_text }); - } else { - resolve({ drop: true }); - } - } - }); - }); - - req.on("error", (e) => { - // or resolve(null) - reject(e); - }); - - req.write(postData); - req.end(); -}); -``` - -The code above shows how you can use a third-party text moderation service with the Instant Messaging service. You can customize the request and response according to your own requirements. -To learn more about the parameters of the hook and the request and response parameters of the text moderation service, please refer to [the \_messageReceived hook of the Instant Messaging service](/sdk/im/guide/systemconv/#_messagereceived) and the API documentation of the text moderation service you are using. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/realtime-guide-onoff-status.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/realtime-guide-onoff-status.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/realtime-guide-onoff-status.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/im/best-practice/realtime-guide-onoff-status.mdx diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/features.mdx deleted file mode 100644 index de049c87e..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/features.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Instant Messaging Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -With Instant Messaging, you can easily implement the commonly-used messaging features for socializing, live streaming, customer support, and online games. - -## Features - -Below are the main features provided by the Instant Messaging service: - -### Basic Features - -Add one-on-one chats, group chats, chat rooms, and system conversations into your app and allow users to send text messages, voice messages, videos, and locations. You can even create your own message types. - -### Authorization and Signing - -You can enable the third-party signing mechanism to verify the requests from the clients and ensure the data in your app is always secure. - -### Single-Device Sign-on and Push Notifications - -Decide whether a user can log in on multiple devices or just a single device at a time. Set up push notifications to get offline users notified of new messages. - -### Message Hooks - -Set up hooks on different stages of a message or conversation to extend our features. - -### Mentioning People, Recalling Messages, and Editing Messages - -Users can mention other people in a message. They can also recall or edit the messages they sent out. Messages can be cached on the local device for faster retrieval. - -## Why Choose Us - -- We provide comprehensive and flexible interfaces for you to quickly implement a fully-functional instant messaging function in your app. We offer common features like text moderation, as well as system conversations and hooks, which help you easily meet all types of messaging-related requirements. - -- We have served tens of thousands of products with some of them bearing tens of millions of DAU. We promise a 99.9% availability for our service. - -- Our service can handle more than 120 million messages per minute. If you ever plan to hold events that demand high concurrency, we’ve got you covered. - -- Our SDKs and demos are all open-source. If you ever get any questions, you can directly contact our engineers by submitting a ticket or asking on our forum. We also provide 24/7 phone support for emergencies. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/_category_.json deleted file mode 100644 index 3446da847..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 2 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/beginner.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/beginner.mdx deleted file mode 100644 index 0204b8097..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/beginner.mdx +++ /dev/null @@ -1,5080 +0,0 @@ ---- -title: 1. Basic Conversations and Messages -sidebar_label: The Basics -sidebar_position: 1 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import Mermaid from "/src/docComponents/Mermaid"; - -## Introduction - -A lot of products today have the need to offer instant messaging functions to their users. For example: - -- To have the staff behind the product talk to the users. -- To have the employees of a company communicate with each other. -- To have the audience of a live stream interact with each other. -- To have the users of an app or players of a game chat with each other. - -Based on the hierarchy of needs and the difficulty of implementation, we wrote four chapters of documentation for you to learn how you can embed Instant Messaging into your app: - -- In this chapter, we will introduce how you can implement one-on-one chats and group chats, how you can create and join conversations, and how you can send and receive rich media messages. We will also cover how history messages are kept on the cloud and how you can retrieve them. By the end of this chapter, you should be able to build a simple chatting page in your app. -- [In the second chapter](/sdk/im/guide/intermediate/), we will introduce some advanced features built around messaging, including mentioning people with "@", recalling messages, editing messages, getting receipts when messages are delivered and read, sending push notifications, and synchronizing messages. The implementation of multi-device sign-on and custom message types will also be covered. By the end of this chapter, you should be able to integrate a chatting component into your app with these features. -- [In the third chapter](/sdk/im/guide/senior/), we will introduce the security features offered by our services, including third-party signing mechanism. We will also go over the usage of chat rooms and temporary conversations. By the end of this chapter, you will get a set of skills to improve the security and usability of your app, as well as to build conversations that serve different purposes. -- [In the last chapter](/sdk/im/guide/systemconv/), we will introduce the usage of hooks and system conversations, plus how you can build your own chatbots based on them. By the end of this chapter, you will learn how you can make your app extensible and adapted to a wide variety of requirements. - -We aim our documentation to not only help you complete the functions you are currently building but also give you a better understanding of all the things Instant Messaging can do, which you will find helpful when you plan to add more features to your app. - -Before you continue: - -Take a look at [Instant Messaging Overview](/sdk/im/guide/overview/) if you haven't done it yet. -Also, make sure you have already installed and initialized the SDK for the platform (language) you are using: - -- [Installing C# SDK](/sdk/storage/guide/setup-dotnet/) -- [Installing Java SDK](/sdk/storage/guide/setup-java/) -- [Installing Objective-C SDK](/sdk/storage/guide/setup-objc/) - -## One-on-One Chats - -Before diving into the main topic, let's see what an `IMClient` object is in the Instant Messaging SDK: - -> An `IMClient` refers to an actual user, meaning that the user logged in to the system as a client. - -See [Instant Messaging Overview](/sdk/im/guide/overview/) for more details. - -### Creating `IMClient` - -Assuming that there is a user named "Tom". Now let's create an `IMClient` instance for him (make sure you have already initialized the SDK): - - - -```cs -LCIMClient tom = new LCIMClient("Tom"); -``` - -```java -// clientId is Tom -LCIMClient tom = LCIMClient.getInstance("Tom"); -``` - -```objc -// Define a property variable that persists in the memory -@property (nonatomic) LCIMClient *tom; -// Initialization -NSError *error; -tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error]; -if (error) { - NSLog(@"init failed with error: %@", error); -} else { - NSLog(@"init succeeded"); -} -``` - -```js -// Tom logs in with his name as clientId -realtime - .createIMClient("Tom") - .then(function (tom) { - // Successfully logged in - }) - .catch(console.error); -``` - -```swift -// Define a global variable that persists in the memory -var tom: IMClient -// Initialization -do { - tom = try IMClient(ID: "Tom") -} catch { - print(error) -} -``` - -```dart -// clientId is Tom -Client tom = Client(id: 'Tom'); -``` - - - -Keep in mind that an `IMClient` refers to an actual user. It should be stored globally since all the further actions done by this user will have to access it. - -### Logging in to the Instant Messaging Server - -After creating the `IMClient` instance for Tom, we will need to have this instance log in to the Instant Messaging server. -Only clients that are logged in can chat with other users and receive notifications from the cloud. - -For some SDKs (like the C# SDK), the client will be automatically logged in when the `IMClient` instance is created; for others (like iOS and Android SDKs), the client needs to be logged in manually with the `open` method: - - - -```cs -await tom.Open(); -``` - -```java -// Tom creates a client and logs in with his name as clientId -LCIMClient tom = LCIMClient.getInstance("Tom"); -// Tom logs in -tom.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient client, LCIMException e) { - if (e == null) { - // Successfully connected - } - } -}); -``` - -```objc -// Define a property variable that persists in the memory -@property (nonatomic) LCIMClient *tom; -// Initialize and log in -NSError *error; -tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error]; -if (error) { - NSLog(@"init failed with error: %@", error); -} else { - [tom openWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // open succeeded - } - }]; -} -``` - -```js -// Tom logs in with his name as clientId and gets the IMClient instance -realtime - .createIMClient("Tom") - .then(function (tom) { - // Successfully logged in - }) - .catch(console.error); -``` - -```swift -// Define a global variable that persists in the memory -var tom: IMClient -// Initialize and log in -do { - tom = try IMClient(ID: "Tom") - tom.open { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// Tom creates a client and logs in with his name as clientId -Client tom = Client(id: 'Tom'); -// Tom logs in -await tom.open(); -``` - - - -### Logging in with `_User` - -Besides specifying a `clientId` on the application layer, you can also log in directly by creating an `IMClient` with a `_User` object. By doing so, the signing process for logging in can be skipped which helps you easily integrate Data Storage with Instant Messaging: - - - -```cs -var user = await LCUser.Login("USER_NAME", "PASSWORD"); -var client = new LCIMClient(user); -``` - -```java -//Log in to the Data Storage service with the username and password of an LCUser -LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // Logged in successfully and connected to the server - LCIMClient client = LCIMClient.getInstance(user); - client.open(new LCIMClientCallback() { - @Override - public void done(final LCIMClient avimClient, LCIMException e) { - // Do other stuff - } - }); - } - public void onError(Throwable throwable) { - // Failed to log in (possibly because the password is incorrect) - } - public void onComplete() {} -}); -``` - -```objc -// Define a property variable that persists in the memory -@property (nonatomic) LCIMClient *client; -// Log the User in and use the logged in User to initialize the Client and log in to the Instant Messaging service -[LCUser logInWithUsernameInBackground:USER_NAME password:PASSWORD block:^(LCUser * _Nullable user, NSError * _Nullable error) { - if (user) { - NSError *err; - client = [[LCIMClient alloc] initWithUser:user error:&err]; - if (err) { - NSLog(@"init failed with error: %@", err); - } else { - [client openWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // open succeeded - } - }]; - } - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -// Log in to Instant Messaging with the username and password of an AVUser -AV.User.logIn("username", "password") - .then(function (user) { - return realtime.createIMClient(user); - }) - .catch(console.error.bind(console)); -``` - -```swift -// Define a global variable that persists in the memory -var client: IMClient -// Log the User in and use the logged in User to initialize the Client and log in to the Instant Messaging service -LCUser.logIn(username: USER_NAME, password: PASSWORD) { (result) in - switch result { - case .success(object: let user): - do { - client = try IMClient(user: user) - client.open { (result) in - // handle result - } - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -// Not supported yet -``` - - - -### Creating Conversations - -A `Conversation` needs to be created before a user can chat with others. - -`Conversation`s are the carriers of messages. All the messages are sent to conversations to be delivered to the members in them. - -Since Tom is already logged in, he can start chatting with other users now. If he wants to chat with Jerry, he can create a `Conversation` containing Jerry and himself: - - - -```cs -var conversation = await tom.CreateConversation(new string[] { "Jerry" }, name: "Tom & Jerry", unique: true); -``` - -```java -tom.createConversation(Arrays.asList("Jerry"), "Tom & Jerry", null, false, true, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if(e == null) { - // Successfully created - } - } -}); -``` - -```objc -// Create a conversation with Jerry -[self createConversationWithClientIds:@[@"Jerry"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) { - // handle callback -}]; -``` - -```js -// Create a conversation with Jerry -tom - .createConversation({ - // tom is an IMClient instance - // Members of the conversation include Jerry (the SDK will automatically add the current user into the conversation) besides Tom - members: ["Jerry"], - // The name of the conversation - name: "Tom & Jerry", - unique: true, - }) - .then(/* Do other stuff */); -``` - -```swift -do { - try tom.createConversation(clientIDs: ["Jerry"], name: "Tom & Jerry", isUnique: true, completion: { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - // Create a conversation with Jerry - Conversation conversation = await tom.createConversation( - isUnique: true, members: {'Jerry'}, name: 'Tom & Jerry'); -} catch (e) { - print('Failed to create the conversation: $e'); -} -``` - - - -`createConversation` creates a new conversation and stores it into the `_Conversation` table which can be found on **Developer Center > Your game > Game Services > Cloud Services > Data Storage > Data**. Below are the interfaces offered by different SDKs for creating conversations: - - - -```cs -/// -/// Creates a conversation -/// -/// The list of clientIds of participants in this conversation (except the creator) -/// The name of this conversation -/// Whether this conversation is unique; -/// if it is true and an existing conversation contains the same composition of members, -/// the existing conversation will be reused, otherwise a new conversation will be created. -/// Custom attributes of this conversation -/// -public async Task CreateConversation( - IEnumerable members, - string name = null, - bool unique = true, - Dictionary properties = null) { - return await ConversationController.CreateConv(members: members, - name: name, - unique: unique, - properties: properties); -} -``` - -```java -/** - * Create or find an existing conversation - * - * @param members The members of the conversation - * @param name The name of the conversation - * @param attributes Custom attributes - * @param isTransient Whether the conversation is a chat room - * @param isUnique Whether to return the existing conversation satisfying conditions - * If false, create a new conversation - * If true, find if there is an existing conversation satisfying conditions; if so, return the conversation, otherwise create a new conversation - * If true, only members is the valid query condition - * @param callback The callback after the conversation is created - */ -public void createConversation(final List members, final String name, - final Map attributes, final boolean isTransient, final boolean isUnique, - final LCIMConversationCreatedCallback callback); -/** - * Create a conversation - * - * @param members The members of the conversation - * @param attributes Custom attributes - * @param isTransient Whether the conversation is a chat room - * @param callback The callback after the conversation is created - */ -public void createConversation(final List members, final String name, - final Map attributes, final boolean isTransient, - final LCIMConversationCreatedCallback callback); -/** - * Create a conversation - * - * @param conversationMembers The members of the conversation - * @param name The name of the conversation - * @param attributes Custom attributes - * @param callback The callback after the conversation is created - * @since 3.0 - */ -public void createConversation(final List conversationMembers, String name, - final Map attributes, final LCIMConversationCreatedCallback callback); -/** - * Create a conversation - * - * @param conversationMembers The members of the conversation - * @param attributes Custom attributes - * @param callback The callback after the conversation is created - * @since 3.0 - */ -public void createConversation(final List conversationMembers, - final Map attributes, final LCIMConversationCreatedCallback callback); -``` - -```objc -/// The option of conversation creation. -@interface LCIMConversationCreationOption : NSObject -/// The name of the conversation. -@property (nonatomic, nullable) NSString *name; -/// The attributes of the conversation. -@property (nonatomic, nullable) NSDictionary *attributes; -/// Create or get an unique conversation, default is `true`. -@property (nonatomic) BOOL isUnique; -/// The time interval for the life of the temporary conversation. -@property (nonatomic) NSUInteger timeToLive; -@end - -/// Create a Normal Conversation. Default is a Normal Unique Conversation. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned. -/// @param callback Result callback. -- (void)createConversationWithClientIds:(NSArray *)clientIds - callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback; - -/// Create a Normal Conversation. Default is a Normal Unique Conversation. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned. -/// @param option See `LCIMConversationCreationOption`. -/// @param callback Result callback. -- (void)createConversationWithClientIds:(NSArray *)clientIds - option:(LCIMConversationCreationOption * _Nullable)option - callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback; - -/// Create a Chat Room. -/// @param callback Result callback. -- (void)createChatRoomWithCallback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback; - -/// Create a Chat Room. -/// @param option See `LCIMConversationCreationOption`. -/// @param callback Result callback. -- (void)createChatRoomWithOption:(LCIMConversationCreationOption * _Nullable)option - callback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback; - -/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID. -/// @param callback Result callback. -- (void)createTemporaryConversationWithClientIds:(NSArray *)clientIds - callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback; - -/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID. -/// @param option See `LCIMConversationCreationOption`. -/// @param callback Result callback. -- (void)createTemporaryConversationWithClientIds:(NSArray *)clientIds - option:(LCIMConversationCreationOption * _Nullable)option - callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback; -``` - -```js -/** - * Create a conversation - * @param {Object} options The fields beside the following ones will be treated as custom attributes - * @param {String[]} options.members The members of the conversation; required; include the current client by default - * @param {String} [options.name] The name of the conversation; optional; defaults to null - * @param {Boolean} [options.transient=false] Whether the conversation is a chat room; optional - * @param {Boolean} [options.unique=false] Whether the conversation is unique; if it is true and an existing conversation contains the same composition of members, the existing conversation will be reused, otherwise a new conversation will be created - * @param {Boolean} [options.tempConv=false] Whether the conversation is temporary; optional - * @param {Integer} [options.tempConvTTL=0] Optional; if tempConv is true, the TTL of the conversation can be specified here - * @return {Promise.} - */ -async createConversation({ - members: m, - name, - transient, - unique, - tempConv, - tempConvTTL, - // You may add more properties -}); -``` - -```swift -/// Create a Normal Conversation. Default is a Unique Conversation. -/// -/// - Parameters: -/// - clientIDs: The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned. -/// - name: The name of the conversation. -/// - attributes: The attributes of the conversation. -/// - isUnique: True means create or get a unique conversation, default is true. -/// - completion: callback. -public func createConversation(clientIDs: Set, name: String? = nil, attributes: [String : Any]? = nil, isUnique: Bool = true, completion: @escaping (LCGenericResult) -> Void) throws - -/// Create a Chat Room. -/// -/// - Parameters: -/// - name: The name of the chat room. -/// - attributes: The attributes of the chat room. -/// - completion: callback. -public func createChatRoom(name: String? = nil, attributes: [String : Any]? = nil, completion: @escaping (LCGenericResult) -> Void) throws - -/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle. -/// -/// - Parameters: -/// - clientIDs: The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID. -/// - timeToLive: The time interval for the life of the temporary conversation. -/// - completion: callback. -public func createTemporaryConversation(clientIDs: Set, timeToLive: Int32, completion: @escaping (LCGenericResult) -> Void) throws -``` - -```dart -/// To create a normal [Conversation]. -/// -/// [isUnique] is a special parameter, default is `true`, it affects the creation behavior and property [Conversation.isUnique]. -/// * When it is `true` and the relevant unique [Conversation] not exists in the server, this method will create a new unique [Conversation]. -/// * When it is `true` and the relevant unique [Conversation] exists in the server, this method will return that existing unique [Conversation]. -/// * When it is `false`, this method always create a new non-unique [Conversation]. -/// -/// [members] is the [Conversation.members]. -/// [name] is the [Conversation.name]. -/// [attributes] is the [Conversation.attributes]. -/// -/// Returns an instance of [Conversation]. -Future createConversation({ - bool isUnique = true, - Set members, - String name, - Map attributes, -}) async {} - -/// To create a new [ChatRoom]. -/// -/// [name] is the [Conversation.name]. -/// [attributes] is the [Conversation.attributes]. -/// -/// Returns an instance of [ChatRoom]. -Future createChatRoom({ - String name, - Map attributes, -}) async {} - -/// To create a new [TemporaryConversation]. -/// -/// [members] is the [Conversation.members]. -/// [timeToLive] is the [TemporaryConversation.timeToLive]. -/// -/// Returns an instance of [TemporaryConversation]. -Future createTemporaryConversation({ - Set members, - int timeToLive, -}) async {} -``` - - - -Although SDKs for different languages/platforms share different interfaces, they take in a similar set of parameters when creating a conversation: - -1. `members`: Required; includes the initial list of members in the conversation. The creator of the conversation is included by default, so `members` does not have to include the `clientId` of the current user. - -2. `name`: The name of the conversation; optional. The code above sets "Tom & Jerry" for it. - -3. `attributes`: The custom attributes of the conversation; optional. The code above does not specify any custom attributes. If you ever specify them for your conversations, you can retrieve them later with `LCIMConversation`. Such attributes will be stored in the `attr` field of the `_Conversation` table. - -4. `unique`/`isUnique` or `LCIMConversationOptionUnique`: Marks if the conversation is unique; optional. - - - If true, the cloud will perform a query on conversations with the list of members specified. If an existing conversation contains the same members, the conversation will be returned, otherwise a new conversation will be created. - - If false, a new conversation will be created each time `createConversation` is called. - - If not specified, it defaults to true. - - In general, it is more reasonable that there is only one conversation existing for the same composition of members, otherwise it could be messy since multiple sets of message histories are available for the same group of people. - -5. Other parameters specifying the type of the conversation; optional. For example, `transient`/`isTransient` specifies if it is a chat room, and `tempConv`/`tempConvTTL` or `LCIMConversationOptionTemporary` specifies if it is a temporary conversation. If nothing is specified, it will be a basic conversation. We will talk more about them later. - -The built-in properties of a conversation can be retrieved once the conversation is created. For example, a globally unique ID will be created for each conversation which can be retrieved with `Conversation.id`. This is the field often used for querying conversations. - -### Sending Messages - -Now that the conversation is created, Tom can start sending messages to it: - - - -```cs -var textMessage = new LCIMTextMessage("Get up, Jerry!"); -await conversation.Send(textMessage); -``` - -```java -LCIMTextMessage msg = new LCIMTextMessage(); -msg.setText("Get up, Jerry!"); -// Send the message -conversation.sendMessage(msg, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - Log.d("Tom & Jerry", "Message sent!"); - } - } -}); -``` - -```objc -LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"Get up, Jerry!" attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Message sent!"); - } -}]; -``` - -```js -var { TextMessage } = require("leancloud-realtime"); -conversation - .send(new TextMessage("Get up, Jerry!")) - .then(function (message) { - console.log("Tom & Jerry", "Message sent!"); - }) - .catch(console.error); -``` - -```swift -do { - let textMessage = IMTextMessage(text: "Get up, Jerry!") - try conversation.send(message: textMessage) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - TextMessage textMessage = TextMessage(); - textMessage.text = 'Get up, Jerry!'; - await conversation.send(message: textMessage); -} catch (e) { - print(e); -} -``` - - - -The code above sends a message to the conversation specified. All the other members who are online will immediately receive the message. - -So how would Jerry see the message on his device? - -### Receiving Messages - -On another device, we create an `IMClient` with `Jerry` as `clientId` and log in to the server (just as how we did for Tom): - - - -```cs -var jerry = new LCIMClient("Jerry"); -``` - -```java -// Jerry logs in -LCIMClient jerry = LCIMClient.getInstance("Jerry"); -jerry.open(new LCIMClientCallback(){ - @Override - public void done(LCIMClient client,LCIMException e){ - if(e==null){ - // Things to do after logging in - } - } -}); -``` - -```objc -NSError *error; -jerry = [[LCIMClient alloc] initWithClientId:@"Jerry" error:&error]; -if (!error) { - [jerry openWithCallback:^(BOOL succeeded, NSError *error) { - // handle callback - }]; -} -``` - -```js -var { Event } = require("leancloud-realtime"); -// Jerry logs in -realtime - .createIMClient("Jerry") - .then(function (jerry) {}) - .catch(console.error); -``` - -```swift -do { - let jerry = try IMClient(ID: "Jerry") - jerry.open { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -Client jerry = Client(id: 'Jerry'); -await jerry.open(); -``` - - - -As the receiver of the message, Jerry doesn't have to create a conversation with Tom and may as well not know that Tom created a conversation with him. Jerry needs to set up a callback function to get notified of the things Tom did. - -By setting up callbacks, clients will be able to handle notifications sent from the cloud. Here we focus on the following two events: - -- The user is invited to a conversation. At the moment Tom creates a new conversation with Jerry, Jerry will receive a notification saying something like "Tom invited you to a conversation". -- A new message is delivered to a conversation the user is already in. At the moment Tom sends out the message "Get up, Jerry!", Jerry will receive a notification including the message itself as well as the context information like the conversation the message is sent to and the sender of the message. - -Now let's see how clients should handle such notifications. The code below handles both "joining conversation" and "getting new message" events for Jerry: - - - -```cs -jerry.OnInvited = (conv, initBy) => { - WriteLine($"{initBy} invited Jerry to join the conversation {conv.Id}"); -}; -jerry.OnMessage = (conv, msg) => { - if (msg is LCIMTextMessage textMessage) { - // textMessage.ConversationId is the ID of the conversation the message belongs to - // textMessage.Text is the content of the text message - // textMessage.FromClientId is the clientId of the sender - } -}; -``` - -```java -// Java/Android SDK responds to notifications with custom event handlers -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * This method is implemented to handle the events when the current user is invited to a conversation - * - * @param client - * @param conversation The conversation - * @param operator The inviter - * @since 3.0 - */ - @Override - public void onInvited(LCIMClient client, LCIMConversation conversation, String invitedBy) { - // Things to do after the current clientId (Jerry) is invited to the conversation - } -} -// Set up global conversation event handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); - -// Java/Android SDK responds to notifications with custom event handlers -public static class CustomMessageHandler extends LCIMMessageHandler{ - /** - * Overloading this method to handle message receiving - * - * @param message - * @param conversation - * @param client - */ - @Override - public void onMessage(LCIMMessage message,LCIMConversation conversation,LCIMClient client){ - if(message instanceof LCIMTextMessage){ - Log.d(((LCIMTextMessage)message).getText()); // Get up, Jerry! - } - } - } -// Set up global message handling handler -LCIMMessageManager.registerDefaultMessageHandler(new CustomMessageHandler()); -``` - -```objc -// Implement the LCIMClientDelegate delegate to respond to the notifications from the server -// For those unfamiliar with the concept of delegation, please refer to: -// https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/DelegatesandDataSources/DelegatesandDataSources.html -jerry.delegate = delegator; - -/*! - The current user is added to a conversation - @param conversation - The conversation - @param clientId - The ID of the inviter - */ -- (void)conversation:(LCIMConversation *)conversation invitedByClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"The current clientId (Jerry) is invited by %@ to join the conversation.",clientId]); -} - -/*! - The current user receives a message - @param conversation - The conversation - @param message - The content of the message - */ -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - NSLog(@"%@", message.text); // Get up, Jerry! -} -``` - -```js -// JS SDK responds to notifications by listening to event callbacks on the IMClient instance - -// The current user is added to a conversation -jerry.on(Event.INVITED, function invitedEventHandler(payload, conversation) { - console.log(payload.invitedBy, conversation.id); -}); - -// The current user receives a message; can be handled by responding to Event.MESSAGE -jerry.on(Event.MESSAGE, function (message, conversation) { - console.log("Message received: " + message.text); -}); -``` - -```swift -let delegator: Delegator = Delegator() -jerry.delegate = delegator - -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - print(message) - default: - break - } - default: - break - } -} -``` - -```dart -jerry.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - if (message.stringContent != null) { - print('Received message: ${message.stringContent}'); - } -}; -``` - - - -With the two event handling functions above, Jerry will be able to receive messages from Tom. Jerry can send messages to Tom as well, as long as Tom has the same functions on his side. - -Now let's take a look at this sequence diagram showing how the first message sent from Tom to Jerry is processed: - ->Cloud: 1. Tom adds Jerry into the conversation -Cloud-->>Jerry: 2. Sends notification: you are invited to the conversation -Jerry-->>UI: 3. Loads UI -Tom->>Cloud: 4. Sends message -Cloud-->>Jerry: 5. Sends notification: you have a new message -Jerry-->>UI: 6. Shows the message -`} -/> - -Besides responding to notifications about new messages, clients also need to respond to those indicating the change of members in a conversation, like "XX invited XX into the conversation", "XX left the conversation", and "XX is removed by the admin". -Such notifications will be delivered to clients in real time. See [Summary of Event Notifications Regarding Changes of Members](#summary-of-event-notifications-regarding-changes-of-members) for more details. - -## Group Chats - -We just discussed how we can create a conversation between two users. Now let's see how we can create a group chat with more people. - -There aren't many differences between the two types of conversations and a major one would be the number of members in them. You can either specify all the members of a group chat when creating it, or add them later after the conversation is created. - -### Creating Group Chats - -In the previous conversation between Tom and Jerry (assuming conversation ID to be `CONVERSATION_ID`), if Tom wants to add Mary into the conversation, the following code can be used: - - - -```cs -// Get the conversation with ID -var conversation = await tom.GetConversation("CONVERSATION_ID"); -// Invite Mary -await conversation.AddMembers(new string[] { "Mary" }); -``` - -```java -// Get the conversation with ID -final LCIMConversation conv = client.getConversation("CONVERSATION_ID"); -// Invite Mary -conv.addMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() { - @Override - public void done(LCIMException e, List successfulClientIds, List failures) { - // Member added - } -}); -``` - -```objc -// Get the conversation with ID -LCIMConversationQuery *query = [self.client conversationQuery]; -[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) { - // Invite Mary - [conversation addMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Member added!"); - } - }]; -}]; -``` - -```js -// Get the conversation with ID -tom - .getConversation("CONVERSATION_ID") - .then(function (conversation) { - // Invite Mary - return conversation.add(["Mary"]); - }) - .then(function (conversation) { - console.log("Member added!", conversation.members); - // The conversation now contains ['Mary', 'Tom', 'Jerry'] - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let conversationQuery = client.conversationQuery - try conversationQuery.getConversation(by: "CONVERSATION_ID") { (result) in - switch result { - case .success(value: let conversation): - do { - try conversation.add(members: ["Mary"], completion: { (result) in - switch result { - case .allSucceeded: - break - case .failure(error: let error): - print(error) - case let .slicing(success: succeededIDs, failure: failures): - if let succeededIDs = succeededIDs { - print(succeededIDs) - } - for (failedIDs, error) in failures { - print(failedIDs) - print(error) - } - } - }) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -List conversations; -try { -// Get the conversation with ID - ConversationQuery query = tom.conversationQuery(); - query.whereEqualTo('objectId', 'CONVERSATION_ID'); - conversations = await query.find(); -} catch (e) { - print(e); -} -try { - Conversation conversation = conversations.first; -// Invite Mary - MemberResult addResult = await conversation.addMembers( - members: {'Mary'}, - ); -} catch (e) { - print(e); -} -``` - - - -On Jerry's side, he can add a listener for handling events regarding "new members being added". With the code below, he will be notified once Tom invites Mary to the conversation: - - -<> - -```cs -jerry.OnMembersJoined = (conv, memberList, initBy) => { - WriteLine($"{initBy} invited {memberList} to join the conversation {conv.Id}"); -} -``` - -`AVIMOnInvitedEventArgs` contains the following fields: - -1. `InvitedBy`: The inviter -2. `JoinedMembers`: The list of members being added -3. `ConversationId`: The conversation - - -<> - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * This method is implemented to handle the events when a new member joins a conversation - * - * @param client - * @param conversation - * @param members The list of new members - * @param invitedBy The ID of the inviter; could be the new member itself - * @since 3.0 - */ - @Override - public void onMemberJoined(LCIMClient client, LCIMConversation conversation, - List members, String invitedBy) { - // Shows that Mary is added to 551260efe4b01608686c3e0f by Tom - Toast.makeText(LeanCloud.applicationContext, - members + " is added to " + conversation.getConversationId() + " by " - + invitedBy, Toast.LENGTH_SHORT).show(); - } -} -// Set up global event handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); -``` - - -<> - -```objc -jerry.delegate = delegator; - -#pragma mark - LCIMClientDelegate -/*! - All members will receive a notification when a new member joins the conversation - @param conversation - The conversation - @param clientIds - The list of new members - @param clientId - The ID of the inviter - */ -- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"%@ is added to the conversation by %@",[clientIds objectAtIndex:0],clientId]); -} -``` - - -<> - -```js -// A user is added to the conversation -jerry.on( - Event.MEMBERS_JOINED, - function membersjoinedEventHandler(payload, conversation) { - console.log(payload.members, payload.invitedBy, conversation.id); - } -); -``` - -`payload` contains the following fields: - -1. `members`: Array of strings; the list of `clientId`s of the members being added -2. `invitedBy`: String; the `clientId` of the inviter - - -<> - -```swift -jerry.delegate = delegator - -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .joined(byClientID: byClientID, at: atDate): - print(byClientID) - print(atDate) - case let .membersJoined(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` - - -<> - -```dart -// Get notified when someone joins a conversation -jerry.onMembersJoined = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('${members.toString()} joined the conversation.'); -}; -``` - - - - -Here is the sequence diagram of the operation: - ->Cloud: 1. Adds Mary -Cloud->>Tom: 2. Sends notification: you invited Mary to the conversation -Cloud-->>Mary: 2. Sends notification: you are added to the conversation by Tom -Cloud-->>Jerry: 2. Sends notification: Mary is added to the conversation by Tom -`} -/> - -On Mary's side, to know that she is added to the conversation between Tom and Jerry, she can follow the way Jerry listens to the `INVITED` event, which can be found in [One-on-One Chats](#one-on-one-chats). - -If Tom wants to **create a new conversation with all the members included**, the following code can be used: - - - -```cs -var conversation = await tom.CreateConversation(new string[] { "Jerry","Mary" }, name: "Tom & Jerry & friends", unique: true); -``` - -```java -tom.createConversation(Arrays.asList("Jerry","Mary"), "Tom & Jerry & friends", null, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if (e == null) { - // Conversation created - } - } - }); -``` - -```objc -// Tom creates a conversation with his friends -[tom createConversationWithClientIds:@[@"Jerry", @"Mary"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) { - if (!error) { - NSLog(@"Conversation created!"); - } -}]; -``` - -```js -tom - .createConversation({ - // Add Jerry and Mary to the conversation when creating it; more members can be added later as well - members: ["Jerry", "Mary"], - // The name of the conversation - name: "Tom & Jerry & friends", - unique: true, - }) - .catch(console.error); -``` - -```swift -do { - try tom.createConversation(clientIDs: ["Jerry", "Mary"], name: "Tom & Jerry & friends", isUnique: true, completion: { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - Conversation conversation = await jerry.createConversation( - isUnique: true, - members: {'Jerry', 'Mary'}, - name: 'Tom & Jerry & friends'); -} catch (e) { - print(e); -} -``` - - - -### Sending Group Messages - -In a group chat, if a member sends a message, the message will be delivered to all the online members in the group. The process is the same as how Jerry receives the message from Tom. - -For example, if Tom sends a welcome message to the group: - - - -```cs -var textMessage = new LCIMTextMessage("Welcome everyone!"); -await conversation.Send(textMessage); -``` - -```java -LCIMTextMessage msg = new LCIMTextMessage(); -msg.setText("Welcome everyone!"); -// Send the message -conversation.sendMessage(msg, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - Log.d("Group chat", "Message sent!"); - } - } -}); -``` - -```objc -[conversation sendMessage:[LCIMTextMessage messageWithText:@"Welcome everyone!" attributes:nil] callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Message sent!"); - } -}]; -``` - -```js -conversation.send(new TextMessage("Welcome everyone!")); -``` - -```swift -do { - let textMessage = IMTextMessage(text: "Welcome everyone!") - try conversation.send(message: textMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage textMessage = TextMessage(); - textMessage.text = 'Welcome everyone!'; - await conversation.send(message: textMessage); -} catch (e) { - print(e); -} -``` - - - -Both Jerry and Mary will have the `Event.MESSAGE` event triggered which can be used to retrieve the message and have it displayed on the UI. - -### Removing Members - -One day Mary said something that made Tom angry and Tom wants to kick her out of the group chat. How should Tom do that? - - - -```cs -await conversation.RemoveMembers(new string[] { "Mary" }); -``` - -```java -conv.kickMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() { - @Override - public void done(LCIMException e, List successfulClientIds, List failures) { - } -}); -``` - -```objc -[conversation removeMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Member removed!"); - } -}]; -``` - -```js -conversation - .remove(["Mary"]) - .then(function (conversation) { - console.log("Member removed!", conversation.members); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - try conversation.remove(members: ["Mary"], completion: { (result) in - switch result { - case .allSucceeded: - break - case .failure(error: let error): - print(error) - case let .slicing(success: succeededIDs, failure: failures): - if let succeededIDs = succeededIDs { - print(succeededIDs) - } - for (failedIDs, error) in failures { - print(failedIDs) - print(error) - } - } - }) -} catch { - print(error) -} -``` - -```dart -try { - MemberResult removeMemberResult = await conversation.removeMembers(members: {'Mary'}); -} catch (e) { - print(e); -} -``` - - - -The following process will be triggered: - ->Cloud: 1. Removes Mary -Cloud-->>Mary: 2. Send notification: You are removed by Tom -Cloud-->>Jerry: 2. Send notification: Mary is removed by Tom -Cloud-->>Tom: 2. Send notification: Mary is removed -`} -/> - -Here we see that Mary receives `KICKED` which indicates that she (the current user) is removed. Other members (Jerry and Tom) will receive `MEMBERS_LEFT` which indicates that someone else in the conversation is removed. Such events can be handled with the following code: - - - -```cs -jerry.OnMembersLeft = (conv, leftIds, kickedBy) => { - WriteLine($"{leftIds} removed from {conv.Id} by {kickedBy}"); -} -jerry.OnKicked = (conv, initBy) => { - WriteLine($"You are removed from {conv.Id} by {initBy}"); -}; -``` - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * This method is implemented to handle the events when someone is removed from a conversation - * - * @param client - * @param conversation - * @param members The members being removed - * @param kickedBy The ID of the operator; could be the current user itself - * @since 3.0 - */ - @Override - public abstract void onMemberLeft(LCIMClient client, - LCIMConversation conversation, List members, String kickedBy) { - Toast.makeText(LeanCloud.applicationContext, - members + " are removed from " + conversation.getConversationId() + " by " - + kickedBy, Toast.LENGTH_SHORT).show(); - } - /** - * This method is implemented to handle the events when the current user is removed from a conversation - * - * @param client - * @param conversation - * @param kickedBy The person who removed you - * @since 3.0 - */ - @Override - public abstract void onKicked(LCIMClient client, LCIMConversation conversation, - String kickedBy) { - Toast.makeText(LeanCloud.applicationContext, - "You are removed from " + conversation.getConversationId() + " by " - + kickedBy, Toast.LENGTH_SHORT).show(); - } -} -// Set up global event handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); -``` - -```objc -jerry.delegate = delegator; - -#pragma mark - LCIMClientDelegate -/*! - The remaining members in a conversation will receive this notification when someone is removed from the conversation. - @param conversation - The conversation - @param clientIds - The list of members being removed - @param clientId - The ID of the operator - */ -- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray * _Nullable)clientIds byClientId:(NSString * _Nullable)clientId { - ; -} -/*! - The notification for the events when the current user is removed from a conversation. - @param conversation - The conversation - @param clientId - The ID of the operator - */ -- (void)conversation:(LCIMConversation *)conversation kickedByClientId:(NSString * _Nullable)clientId { - ; -} -``` - -```js -// Someone else is removed -jerry.on( - Event.MEMBERS_LEFT, - function membersjoinedEventHandler(payload, conversation) { - console.log(payload.members, payload.kickedBy, conversation.id); - } -); -// The current user is removed -jerry.on( - Event.KICKED, - function membersjoinedEventHandler(payload, conversation) { - console.log(payload.kickedBy, conversation.id); - } -); -``` - -```swift -jerry.delegate = delegator - -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .left(byClientID: byClientID, at: atDate): - print(byClientID) - print(atDate) - case let .membersLeft(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` - -```dart -// Someone else is removed -jerry.onMembersLeft = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('${members.toString()} is removed by $byClientID'); -}; -// The current user is removed -jerry.onKicked = ({ - Client client, - Conversation conversation, - String byClientID, - DateTime atDate, -}) { - print('You are removed by $byClientID'); -}; -``` - - - -### Joining Conversations - -Tom is feeling bored after removing Mary. He goes to William and tells him that there is a group chat that Jerry and himself are in. He gives the ID (or name) of the group chat to William which makes him curious about what's going on in it. William then adds himself to the group: - - - -```cs -var conv = await william.GetConversation("CONVERSATION_ID"); -await conv.Join(); -``` - -```java -LCIMConversation conv = william.getConversation("CONVERSATION_ID"); -conv.join(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // Successfully joined - } - } -}); -``` - -```objc -LCIMConversationQuery *query = [william conversationQuery]; -[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) { - [conversation joinWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Successfully joined!"); - } - }]; -}]; -``` - -```js -william - .getConversation("CONVERSATION_ID") - .then(function (conversation) { - return conversation.join(); - }) - .then(function (conversation) { - console.log("Successfully joined!", conversation.members); - // The conversation now contains ['William', 'Tom', 'Jerry'] - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let conversationQuery = client.conversationQuery - try conversationQuery.getConversation(by: "CONVERSATION_ID") { (result) in - switch result { - case .success(value: let conversation): - do { - try conversation.join(completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -List conversations; -try { - ConversationQuery query = william.conversationQuery(); - query.whereEqualTo('objectId', 'CONVERSATION_ID'); - conversations = await query.find(); -} catch (e) { - print(e); -} - -try { - Conversation conversation = conversations.first; - MemberResult joinResult = await conversation.join(); -} catch (e) { - print(e); -} -``` - - - -The following process will be triggered: - ->Cloud: 1. Joins the conversations -Cloud-->>William: 2. Sends notification: you joined the conversation -Cloud-->>Tom: 2. Sends notification: William joined the conversation -Cloud-->>Jerry: 2. Sends notification: William joined the conversation -`} -/> - -Other members can listen to `MEMBERS_JOINED` to know that William joined the conversation: - - - -```cs -jerry.OnMembersJoined = (conv, memberList, initBy) => { - WriteLine($"{memberList} joined {conv.Id}; operated by {initBy}"); -} -``` - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - @Override - public void onMemberJoined(LCIMClient client, LCIMConversation conversation, - List members, String invitedBy) { - // Shows that William joined 551260efe4b01608686c3e0f; operated by William - Toast.makeText(LeanCloud.applicationContext, - members + " joined " + conversation.getConversationId() + "; operated by " - + invitedBy, Toast.LENGTH_SHORT).show(); - } -} -``` - -```objc -- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"%@ joined the conversation; operated by %@",[clientIds objectAtIndex:0],clientId]); -} -``` - -```js -jerry.on( - Event.MEMBERS_JOINED, - function membersJoinedEventHandler(payload, conversation) { - console.log(payload.members, payload.invitedBy, conversation.id); - } -); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .membersJoined(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` - -```dart -jerry.onMembersJoined = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('${members.toString()} joined'); -}; -``` - - - -### Leaving Conversations - -With more and more people being invited by Tom, Jerry feels that he doesn't like most of them and wants to leave the conversation. He can do that with the following code: - - - -```cs -await conversation.Quit(); -``` - -```java -conversation.quit(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // You left the conversation - } - } -}); -``` - -```objc -[conversation quitWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"You left the conversation!"); - } -}]; -``` - -```js -conversation - .quit() - .then(function (conversation) { - console.log("You left the conversation!", conversation.members); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - try conversation.leave(completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - MemberResult quitResult = await conversation.quit(); -} catch (e) { - print(e); -} -``` - - - -After leaving the conversation, Jerry will no longer receive messages from it. Here is the sequence diagram of the operation: - ->Cloud: 1. Leaves the conversation -Cloud-->>Jerry: 2. Sends notification: You left the conversation -Cloud-->>Mary: 2. Sends notification: Jerry left the conversation -Cloud-->>Tom: 2. Sends notification: Jerry left the conversation -`} -/> - -Other members can listen to `MEMBERS_LEFT` to know that Jerry left the conversation: - - - -```cs -mary.OnMembersLeft = (conv, members, initBy) => { - WriteLine($"{members} left {conv.Id}; operated by {initBy}"); -} -``` - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - @Override - public void onMemberLeft(LCIMClient client, LCIMConversation conversation, List members, - String kickedBy) { - // Things to do after someone left - } -} -``` - -```objc -// If Mary is logged in, the following callback will be triggered when Jerry leaves the conversation -- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray *)clientIds byClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"%@ 离开了对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]); -} -``` - -```js -mary.on( - Event.MEMBERS_LEFT, - function membersLeftEventHandler(payload, conversation) { - console.log(payload.members, payload.kickedBy, conversation.id); - } -); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .membersLeft(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` - -```dart -mary.onMembersLeft = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('${members.toString()} left'); -}; -``` - - - -### Summary of Event Notifications Regarding Changes of Members - -The sequence diagrams displayed earlier already described what would happen when certain events are triggered. The table below serves as a summary of them. - -Assuming that Tom and Jerry are already in the conversation: - - -<> - -| Operation | Tom | Jerry | Mary | William | -| ---------------- | ----------------- | ----------------- | ----------- | ----------------- | -| Tom invites Mary | `OnMembersJoined` | `OnMembersJoined` | `OnInvited` | / | -| Tom removes Mary | `OnMembersLeft` | `OnMembersLeft` | `OnKicked` | / | -| William joins | `OnMembersJoined` | `OnMembersJoined` | / | `OnMembersJoined` | -| Jerry leaves | `OnMembersLeft` | `OnMembersLeft` | / | `OnMembersLeft` | - - -<> - -| Operation | Tom | Jerry | Mary | William | -| ---------------- | ---------------- | ---------------- | ----------- | ---------------- | -| Tom invites Mary | `onMemberJoined` | `onMemberJoined` | `onInvited` | / | -| Tom removes Mary | `onMemberLeft` | `onMemberLeft` | `onKicked` | / | -| William joins | `onMemberJoined` | `onMemberJoined` | / | `onMemberJoined` | -| Jerry leaves | `onMemberLeft` | `onMemberLeft` | / | `onMemberLeft` | - - - -<> - -| Operation | Tom | Jerry | Mary | William | -| ---------------- | ---------------- | ------------------ | ------------------- | ---------------- | -| Tom invites Mary | `membersAdded` | `membersAdded` | `invitedByClientId` | / | -| Tom removes Mary | `membersRemoved` | `membersRemoved` | `kickedByClientId` | / | -| William joins | `membersAdded` | `membersAdded` | / | `membersAdded` | -| Jerry leaves | `membersRemoved` | `kickedByClientId` | / | `membersRemoved` | - - - - -## Rich Media Messages - -We've seen how we can send messages containing plain text. Now let's see how we can send rich media messages like images, videos, and locations. - -The Instant Messaging service provides out-of-the-box support for messages containing text, files, images, audios, videos, locations, and binary data. All of them, except binary data, are sent as strings, though there are some slight differences between text messages and rich media messages (files, images, audios, and videos): - -- When sending text messages, the messages themselves are sent directly as strings. -- When sending rich media messages (like images), the SDK will first upload the binary files to the cloud with the Data Storage service's `AVFile` interface, then embed the URLs of them into the messages being sent. We can say that **the essence of an image message is a text message holding the URL of the image**. - -Files stored on the Data Storage service have CDN enabled by default. Therefore, binary data (like images) are not directly encoded as part of text messages. This helps users access them faster and the cost on you can be lowered as well. - -### Default Message Types - -The following message types are offered by default: - -- `TextMessage` Text message -- `ImageMessage` Image message -- `AudioMessage` Audio message -- `VideoMessage` Video message -- `FileMessage` File message (.txt, .doc, .md, etc.) -- `LocationMessage` Location message - -All of them are derived from `LCIMMessage`, with the following properties available for each: - - -<> - -| Name | Type | Description | -| ------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `content` | `String` | The content of the message. | -| `clientId` | `String` | The `clientId` of the sender. | -| `conversationId` | `String` | The ID of the conversation. | -| `messageId` | `String` | A unique ID for each message. Assigned by the cloud automatically. | -| `timestamp` | `long` | The time the message is sent. Assigned by the cloud automatically. | -| `receiptTimestamp` | `long` | The time the message is delivered. Assigned by the cloud automatically. | -| `status` | A member of `AVIMMessageStatus` | The status of the message. Could be one of:

    `AVIMMessageStatusNone` (unknown)
    `AVIMMessageStatusSending` (sending)
    `AVIMMessageStatusSent` (sent)
    `AVIMMessageStatusReceipt` (delivered)
    `AVIMMessageStatusFailed` (failed) | -| `ioType` | A member of `AVIMMessageIOType` | The direction of the message. Could be one of:

    `AVIMMessageIOTypeIn` (sent to the current user)
    `AVIMMessageIOTypeOut` (sent by the current user) | - - -<> - -| Name | Type | Description | -| ------------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `content` | `String` | The content of the message. | -| `clientId` | `String` | The `clientId` of the sender. | -| `conversationId` | `String` | The ID of the conversation. | -| `messageId` | `String` | A unique ID for each message. Assigned by the cloud automatically. | -| `timestamp` | `long` | The time the message is sent. Assigned by the cloud automatically. | -| `receiptTimestamp` | `long` | The time the message is delivered. Assigned by the cloud automatically. | -| `status` | A member of `MessageStatus` | The status of the message. Could be one of:

    `StatusNone` (unknown)
    `StatusSending` (sending)
    `StatusSent` (sent)
    `StatusReceipt` (delivered)
    `StatusFailed` (failed) | -| `ioType` | A member of `MessageIOType` | The direction of the message. Could be one of:

    `TypeIn` (sent to the current user)
    `TypeOut` (sent by the current user) | - - -<> - -| Name | Type | Description | -| -------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `content` | `NSString` | The content of the message. | -| `clientId` | `NSString` | The `clientId` of the sender. | -| `conversationId` | `NSString` | The ID of the conversation. | -| `messageId` | `NSString` | A unique ID for each message. Assigned by the cloud automatically. | -| `sendTimestamp` | `int64_t` | The time the message is sent. Assigned by the cloud automatically. | -| `deliveredTimestamp` | `int64_t` | The time the message is delivered. Assigned by the cloud automatically. | -| `status` | A member of `AVIMMessageStatus` | The status of the message. Could be one of:

    `LCIMMessageStatusNone` (unknown)
    `LCIMMessageStatusSending` (sending)
    `LCIMMessageStatusSent` (sent)
    `LCIMMessageStatusDelivered` (delivered)
    `LCIMMessageStatusFailed` (failed) | -| `ioType` | A member of `LCIMMessageIOType` | The direction of the message. Could be one of:

    `LCIMMessageIOTypeIn` (sent to the current user)
    `LCIMMessageIOTypeOut` (sent by the current user) | - - -<> - -| Name | Type | Description | -| ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `from` | `String` | The `clientId` of the sender. | -| `cid` | `String` | The ID of the conversation. | -| `id` | `String` | A unique ID for each message. Assigned by the cloud automatically. | -| `timestamp` | `Date` | The time the message is sent. Assigned by the cloud automatically. | -| `deliveredAt` | `Date` | The time the message is delivered. | -| `status` | `Symbol` | The status of the message. Could be one of the members of [`MessageStatus`](https://leancloud.github.io/js-realtime-sdk/docs/module-leancloud-realtime.html#.MessageStatus):

    `MessageStatus.NONE` (unknown)
    `MessageStatus.SENDING` (sending)
    `MessageStatus.SENT` (sent)
    `MessageStatus.DELIVERED` (delivered)
    `MessageStatus.FAILED` (failed) | - - -<> - -| Name | Type | Description | -| -------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `content` | `IMMessage.Content` | The content of the message. Could be `String` or `Data`. | -| `fromClientID` | `String` | The `clientId` of the sender. | -| `currentClientID` | `String` | The `clientId` of the receiver. | -| `conversationID` | `String` | The ID of the conversation. | -| `ID` | `String` | A unique ID for each message. Assigned by the cloud automatically. | -| `sentTimestamp` | `int64_t` | The time the message is sent. Assigned by the cloud automatically. | -| `deliveredTimestamp` | `int64_t` | The time the message is received. | -| `readTimestamp` | `int64_t` | The time the message is read. | -| `patchedTimestamp` | `int64_t` | The time the message is edited. | -| `isAllMembersMentioned` | `Bool` | Whether all members are mentioned. | -| `mentionedMembers` | `[String]` | A list of members being mentioned. | -| `isCurrentClientMentioned` | `Bool` | Whether the current `Client` is mentioned. | -| `status` | `IMMessage.Status` | The status of the message. Could be one of:

    `none` (unknown)
    `sending` (sending)
    `sent` (sent)
    `delivered` (delivered)
    `read` (read)
    `failed` (failed) | -| `ioType` | `IMMessage.IOType` | The direction of the message. Could be one of:

    `in` (sent to the current user)
    `out` (sent by the current user) | - - -
    - -A number is assigned to each message type which can be used by your app to identify it. Negative numbers are for those defined by the SDK (see the table below) and positive ones are for your own types. `0` is reserved for untyped messages. - -| Message Type | Number | -| ----------------- | ------ | -| Text messages | `-1` | -| Image messages | `-2` | -| Audio messages | `-3` | -| Video messages | `-4` | -| Location messages | `-5` | -| File messages | `-6` | - -### Image Messages - -#### Sending Image Files - -An image message can be constructed from either binary data or a local path. The diagram below shows the sequence of it: - ->Local: 1. Get the content of the image -Tom-->>Storage: 2. The SDK uploads the file (LCFile) to the cloud -Storage-->>Tom: 3. Return the URL of the image -Tom-->>Cloud: 4. The SDK sends the image message to the cloud -Cloud->>Jerry: 5. Receive the image message and display it in UI -`} -/> - -Notes: - -1. The "Local" in the diagram could be `localStorage` or `camera`, meaning that the image could be either from the local storage of the phone (like iPhone's Photo Library) or taken in real time with the camera API. -2. `LCFile` is the file object used by the Data Storage service. - -The diagram above may look complicated, but the code itself is quite simple since the image gets automatically uploaded when being sent with the `send` method: - - - -```cs -var image = new LCFile("screenshot.png", new Uri("http://example.com/screenshot.png")); -var imageMessage = new LCIMImageMessage(image); -imageMessage.Text = "Sent from Windows"; -await conversation.Send(imageMessage); -``` - -```java -LCFile file = LCFile.withAbsoluteLocalPath("San_Francisco.png", Environment.getExternalStorageDirectory() + "/San_Francisco.png"); -// Create an image message -LCIMImageMessage m = new LCIMImageMessage(file); -m.setText("Sent from Android"); -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // Sent - } - } -}); -``` - -```objc -NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); -NSString *documentsDirectory = [paths objectAtIndex:0]; -NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:@"Tarara.png"]; -NSError *error; -LCFile *file = [LCFile fileWithLocalPath:imagePath error:&error]; -LCIMImageMessage *message = [LCIMImageMessage messageWithText:@"Tarara looks sweet." file:file attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Sent!"); - } -}]; -``` - -```js -// ImageMessage and other rich media messages depend on the Data Storage service and the rich media message plugin. -// Refer to the SDK setup guide for details on how to import and initialize the SDKs. - -var fileUploadControl = $("#photoFileUpload")[0]; -var file = new AV.File("avatar.jpg", fileUploadControl.files[0]); -file - .save() - .then(function () { - var message = new ImageMessage(file); - message.setText("Sent from Ins"); - message.setAttributes({ location: "San Francisco" }); - return conversation.send(message); - }) - .then(function () { - console.log("Sent!"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - if let imageFilePath = Bundle.main.url(forResource: "image", withExtension: "jpg")?.path { - let imageMessage = IMImageMessage(filePath: imageFilePath, format: "jpg") - try conversation.send(message: imageMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` - -```dart -import 'package:flutter/services.dart' show rootBundle; - -// Assuming there is an `assets` directory under the root directory of the project, and this directory is included in pubspec.yaml. -ByteData imageData = await rootBundle.load('assets/test.png'); -// image message -ImageMessage imageMessage = ImageMessage.from( - binaryData: imageData.buffer.asUint8List(), - format: 'png', - name: 'image.png', -); -try { - conversation.send(message: imageMessage); -} catch (e) { - print(e); -} -``` - - - -#### Sending Image URLs - -Besides sending an image directly, a user may also copy the URL of an image from somewhere else and send it to a conversation: - - - -```cs -var image = new LCFile("girl.gif", new Uri("http://example.com/girl.gif")); -var imageMessage = new LCIMImageMessage(image); -imageMessage.Text = "Sent from Windows"; -await conversation.Send(imageMessage); -``` - -```java -LCFile file = new LCFile("girl","http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif", null); -LCIMImageMessage m = new LCIMImageMessage(file); -m.setText("She looks sweet."); -// Create an image message -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // Sent - } - } -}); -``` - -```objc -// Tom sends an image to Jerry -LCFile *file = [LCFile fileWithURL:[self @"http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif"]]; -LCIMImageMessage *message = [LCIMImageMessage messageWithText:@"girl" file:file attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Sent!"); - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -var { ImageMessage } = initPlugin(AV, IM); -// Create an image message from URL -var file = new AV.File.withURL( - "girl", - "http://pic2.zhimg.com/6c10e6053c739ed0ce676a0aff15cf1c.gif" -); -file - .save() - .then(function () { - var message = new ImageMessage(file); - message.setText("She looks sweet."); - return conversation.send(message); - }) - .then(function () { - console.log("Sent!"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - if let url = URL(string: "http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif") { - let imageMessage = IMImageMessage(url: url, format: "gif") - try conversation.send(message: imageMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` - -```dart -ImageMessage imageMessage = ImageMessage.from( - url: 'http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif', - format: 'png', - name: 'image.png', -); -try { - conversation.send(message: imageMessage); -} catch (e) { - print(e); -} -``` - - - -#### Receiving Image Messages - -The way to receive image messages is similar to that of basic messages. The only thing that needs to be added is to have the callback function retrieve the image and render it on the UI. For example: - - - -```cs -client.OnMessage = (conv, msg) => { - if (e.Message is LCIMImageMessage imageMessage) { - WriteLine(imageMessage.Url); - } -} -``` - -```java -LCIMMessageManager.registerMessageHandler(LCIMImageMessage.class, - new LCIMTypedMessageHandler() { - @Override - public void onMessage(LCIMImageMessage msg, LCIMConversation conv, LCIMClient client) { - // Only handle messages from Jerry - // sent to the conversation with conversationId 55117292e4b065f7ee9edd29 - if ("Jerry".equals(client.getClientId()) && "55117292e4b065f7ee9edd29".equals(conv.getConversationId())) { - String fromClientId = msg.getFrom(); - String messageId = msg.getMessageId(); - String url = msg.getFileUrl(); - Map metaData = msg.getFileMetaData(); - if (metaData.containsKey("size")) { - int size = (Integer) metaData.get("size"); - } - if (metaData.containsKey("width")) { - int width = (Integer) metaData.get("width"); - } - if (metaData.containsKey("height")) { - int height = (Integer) metaData.get("height"); - } - if (metaData.containsKey("format")) { - String format = (String) metaData.get("format"); - } - } - } -}); -``` - -```objc -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - LCIMImageMessage *imageMessage = (LCIMImageMessage *)message; - - // The ID of the message - NSString *messageId = imageMessage.messageId; - // The URL of the image file - NSString *imageUrl = imageMessage.file.url; - // The clientId of the sender - NSString *fromClientId = message.clientId; -} -``` - -```js -var { Event, TextMessage } = require('leancloud-realtime'); -var { ImageMessage } = initPlugin(AV, IM); - -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - var file; - switch (message.type) { - case ImageMessage.TYPE: - file = message.getFile(); - console.log('Image received. URL: ' + file.url()); - break; - } -} -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - switch message { - case let imageMessage as IMImageMessage: - print(imageMessage) - default: - break - } - default: - break - } - default: - break - } -} -``` - -```dart -lient.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - if (message is ImageMessage) { - print('Image received. URL: ${message.url}'); - } -}; -``` - - - -### Sending Audios, Videos, and Files - -#### The Workflow - -The SDK follows the steps below to send images, audios, videos, and files: - -When **constructing a file from a data stream using the client API**: - -1. Construct an `LCFile` locally -2. Upload the `LCFile` to the cloud and retrieve its `metaData` -3. Embed the `objectId`, URL, and metadata of the `LCFile` into the message -4. Send the message - -When **constructing a file with an external URL**: - -1. Embed the URL into the message without the metadata (like the length of audio) or `objectId` -2. Send the message - -For example, when sending an audio message, the basic workflow would be: read the audio file (or record a new one) > construct an audio message > send the message. - - - -```cs -var audio = new LCFile("never-gonna-give-you-up.mp3", Path.Combine(Application.persistentDataPath, "never-gonna-give-you-up.mp3")); -var audioMessage = new LCIMAudioMessage(audio); -audioMessage.Text = "Check this out!"; -await conversation.Send(audioMessage); -``` - -```java -LCFile file = LCFile.withAbsoluteLocalPath("never-gonna-give-you-up.mp3",localFilePath); -LCIMAudioMessage m = new LCIMAudioMessage(file); -m.setText("Check this out!"); -// Create an audio message -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // Sent - } - } -}); -``` - -```objc -NSError *error = nil; -LCFile *file = [LCFile fileWithLocalPath:localPath error:&error]; -if (!error) { - LCIMAudioMessage *message = [LCIMAudioMessage messageWithText:@"Check this out!" file:file attributes:nil]; - [conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Sent!"); - } - }]; -} -``` - -```js -var AV = require("leancloud-storage"); -var { AudioMessage } = initPlugin(AV, IM); - -var fileUploadControl = $("#musicFileUpload")[0]; -var file = new AV.File( - "never-gonna-give-you-up.mp3", - fileUploadControl.files[0] -); -file - .save() - .then(function () { - var message = new AudioMessage(file); - message.setText("Check this out!"); - return conversation.send(message); - }) - .then(function () { - console.log("Sent!"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - if let filePath = Bundle.main.url(forResource: "audio", withExtension: "mp3")?.path { - let audioMessage = IMAudioMessage(filePath: filePath, format: "mp3") - audioMessage.text = "Check this out!" - try conversation.send(message: audioMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` - -```dart -import 'package:flutter/services.dart' show rootBundle; - -// Assuming there is an `assets` directory under the root directory of the project containing an mp3 file, and this directory is included in pubspec.yaml. -ByteData audioData = await rootBundle.load('assets/test.mp3'); -AudioMessage audioMessage = AudioMessage.from( - binaryData: audioData.buffer.asUint8List(), - format: 'mp3', -); -audioMessage.text = 'Check this out!'; -try { - await conversation.send(message: audioMessage); -} catch (e) { - print(e); -} -``` - - - -Similar to image messages, you can construct audio messages from URLs as well: - - - -```cs -var audio = new LCFile("apple.aac", new Uri("https://some.website.com/apple.aac")); -var audioMessage = new LCIMAudioMessage(audio); -audioMessage.Text = "Here is the recording from Apple Special Event."; -await conversation.Send(audioMessage); -``` - -```java -LCFile file = new LCFile("apple.aac", "https://some.website.com/apple.aac", null); -LCIMAudioMessage m = new LCIMAudioMessage(file); -m.setText("Here is the recording from Apple Special Event."); -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // Sent - } - } -}); -``` - -```objc -LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"https://some.website.com/apple.aac"]]; -LCIMAudioMessage *message = [LCIMAudioMessage messageWithText:@"Here is the recording from Apple Special Event." file:file attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Sent!"); - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -var { AudioMessage } = initPlugin(AV, IM); - -var file = new AV.File.withURL( - "apple.aac", - "https://some.website.com/apple.aac" -); -file - .save() - .then(function () { - var message = new AudioMessage(file); - message.setText("Here is the recording from Apple Special Event."); - return conversation.send(message); - }) - .then(function () { - console.log("Sent!"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - if let url = URL(string: "https://some.website.com/apple.aac") { - let audioMessage = IMAudioMessage(url: url, format: "aac") - audioMessage.text = "Here is the recording from Apple Special Event." - try conversation.send(message: audioMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` - -```dart -AudioMessage audioMessage = AudioMessage.from( - url: 'https://some.website.com/apple.aac', - name: 'apple.aac', -); -try { - await conversation.send(message: audioMessage); -} catch (e) { - print(e); -} -``` - - - -### Sending Location Messages - -The code below sends a message containing a location: - - - -```cs -var location = new LCGeoPoint(31.3753285, 120.9664658); -var locationMessage = new LCIMLocationMessage(location); -await conversation.Send(locationMessage); -``` - -```java -final LCIMLocationMessage locationMessage = new LCIMLocationMessage(); -// The location here is hardcoded for demonstration; you can get actual locations with the API offered by the device -locationMessage.setLocation(new LCGeoPoint(31.3753285,120.9664658)); -locationMessage.setText("Here is the location of the bakery."); -conversation.sendMessage(locationMessage, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (null != e) { - e.printStackTrace(); - } else { - // Sent - } - } -}); -``` - -```objc -LCIMLocationMessage *message = [LCIMLocationMessage messageWithText:@"Here is the location of the bakery." latitude:31.3753285 longitude:120.9664658 attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Sent!"); - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -var { LocationMessage } = initPlugin(AV, IM); - -var location = new AV.GeoPoint(31.3753285, 120.9664658); -var message = new LocationMessage(location); -message.setText("Here is the location of the bakery."); -conversation - .send(message) - .then(function () { - console.log("Sent!"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let locationMessage = IMLocationMessage(latitude: 31.3753285, longitude: 120.9664658) - try conversation.send(message: locationMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -LocationMessage locationMessage = LocationMessage.from( - latitude: 22, - longitude: 33, -); -try { - await conversation.send(message: locationMessage); -} catch (e) { - print(e); -} -``` - - - -### Back to Receiving Messages - - -<> - -The C# SDK handles new messages with `OnMessage` event callbacks: - -```cs -jerry.OnMessage = (conv, msg) => { - if (msg is LCIMImageMessage imageMessage) { - - } else if (msg is LCIMAudioMessage audioMessage) { - - } else if (msg is LCIMVideoMessage videoMessage) { - - } else if (msg is LCIMFileMessage fileMessage) { - - } else if (msg is AVIMLocationMessage locationMessage) { - - } else if (msg is InputtingMessage) { - WriteLine($"Custom message received: {inputtingMessage.TextContent} {inputtingMessage.Ecode}"); - } -} -``` - - -<> - -The Java/Android SDK handles new messages with `LCIMMessageHandler`. You can register your own message handlers by calling `LCIMMessageManager.registerDefaultMessageHandler`. `LCIMMessageManager` offers two different methods for you to register default message handlers and handlers for specific message types: - -```java -/** - * Register default message handler. - * - * @param handler - */ -public static void registerDefaultMessageHandler(LCIMMessageHandler handler); -/** - * Register handler for specific message type. - * - * @param clazz The message type - * @param handler - */ -public static void registerMessageHandler(Class clazz, MessageHandler handler); -/** - * Deregister handler for specific message type. - * - * @param clazz - * @param handler - */ -public static void unregisterMessageHandler(Class clazz, MessageHandler handler); -``` - -Different handlers can be registered or deregistered for different message types (including those defined by yourself). These handlers should be set up when initializing the app. - -If you call `registerDefaultMessageHandler` on `LCIMMessageManager` multiple times, only the last one would take effect. However, if you register `LCIMMessageHandler` through `registerMessageHandler`, different handlers could coexist with each other. - -When a message is received by the client, the SDK would: - -- Detect the type of the message, look for all the handlers registered for this type, and call the `onMessage` functions within all these handlers. -- If no handler is found for this type, `defaultHandler` will be triggered. - -So when handlers are specified for `AVIMTypedMessage` (and its subtypes) and a global `defaultHandler` is also specified, if the sender sends a general `LCIMMessage` message, the receiver will have its handler in `LCIMMessageManager.registerDefaultMessageHandler()` triggered; if the sender sends a message of `LCIMTypedMessage` (or its subtype), the receiver will have its handler in `LCIMMessageManager#registerMessageHandler()` triggered. - -```java -// 1. Register the default handler, which will only be invoked when none of the other handlers are invoked -LCIMMessageManager.registerDefaultMessageHandler(new LCIMMessageHandler(){ - public void onMessage(LCIMMessage message, LCIMConversation conversation, LCIMClient client) { - // Receive the message - } - - public void onMessageReceipt(LCIMMessage message, LCIMConversation conversation, LCIMClient client) { - // Your application may add new custom message types in the future. The SDK may add new built-in message types as well. - // Therefore, do not forget to handle them here. For example, you can notify the user to upgrade to a new version. - } -}); -// 2. Register a handler for each type of message -LCIMMessageManager.registerMessageHandler(LCIMTypedMessage.class, new LCIMTypedMessageHandler(){ - public void onMessage(LCIMTypedMessage message, LCIMConversation conversation, LCIMClient client) { - switch (message.getMessageType()) { - case LCIMMessageType.TEXT_MESSAGE_TYPE: - // Do something - LCIMTextMessage textMessage = (LCIMTextMessage)message; - break; - case LCIMMessageType.IMAGE_MESSAGE_TYPE: - // Do something - LCIMImageMessage imageMessage = (LCIMImageMessage)message; - break; - case LCIMMessageType.AUDIO_MESSAGE_TYPE: - // Do something - LCIMAudioMessage audioMessage = (LCIMAudioMessage)message; - break; - case LCIMMessageType.VIDEO_MESSAGE_TYPE: - // Do something - LCIMVideoMessage videoMessage = (LCIMVideoMessage)message; - break; - case LCIMMessageType.LOCATION_MESSAGE_TYPE: - // Do something - LCIMLocationMessage locationMessage = (LCIMLocationMessage)message; - break; - case LCIMMessageType.FILE_MESSAGE_TYPE: - // Do something - LCIMFileMessage fileMessage = (LCIMFileMessage)message; - break; - case LCIMMessageType.RECALLED_MESSAGE_TYPE: - // Do something - LCIMRecalledMessage recalledMessage = (LCIMRecalledMessage)message; - break; - case 123: - // This is a custom message type - // Do something - CustomMessage customMessage = (CustomMessage)message; - break; - } - } - - public void onMessageReceipt(LCIMTypedMessage message, LCIMConversation conversation, LCIMClient client) { - // Do something after receiving the message - } -}); -``` - - -<> - -The Objective-C SDK handles new messages with `LCIMClientDelegate` and uses two separate methods to handle basic messages (`LCIMMessage`) and rich media messages (`LCIMTypedMessage`; including messages with custom types): - -```objc -/*! - New basic message received. - @param conversation - The conversation. - @param message - The content of the message. - */ -- (void)conversation:(LCIMConversation *)conversation didReceiveCommonMessage:(LCIMMessage *)message; - -/*! - New rich media message received. - @param conversation - The conversation. - @param message - The content of the message. - */ -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message; -``` - -```objc -// Handle messages with built-in types -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - if (message.mediaType == LCIMMessageMediaTypeImage) { - LCIMImageMessage *imageMessage = (LCIMImageMessage *)message; // Handle image message - } else if(message.mediaType == LCIMMessageMediaTypeAudio){ - // Handle audio message - } else if(message.mediaType == LCIMMessageMediaTypeVideo){ - // Handle video message - } else if(message.mediaType == LCIMMessageMediaTypeLocation){ - // Handle location message - } else if(message.mediaType == LCIMMessageMediaTypeFile){ - // Handle file message - } else if(message.mediaType == LCIMMessageMediaTypeText){ - // Handle text message - } else if(message.mediaType == 123){ - // Handle custom message type - } -} - -// Handle unknown messages types -- (void)conversation:(LCIMConversation *)conversation didReceiveCommonMessage:(LCIMMessage *)message { - // Your application may add new custom message types in the future. The SDK may add new built-in message types as well. - // Therefore, do not forget to handle them here. For example, you can notify the user to upgrade to a new version. -} -``` - - -<> - -When a new message comes in, the JavaScript SDK would always trigger the callback set for the event `Event.MESSAGE` on `IMClient` regardless of the type of the message. You can address different types of messages in different ways within the callback function. - -```js -// Load TypedMessagesPlugin when initializing Realtime -var { Event, TextMessage } = require("leancloud-realtime"); -var { FileMessage, ImageMessage, AudioMessage, VideoMessage, LocationMessage } = - initPlugin(AV, IM); -// Register handler for the MESSAGE event -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - // Your logic here - var file; - switch (message.type) { - case TextMessage.TYPE: - console.log( - "Text message received. Text: " + - message.getText() + - ", ID: " + - message.id - ); - break; - case FileMessage.TYPE: - file = message.getFile(); // file is an AV.File instance - console.log( - "File message received. URL: " + - file.url() + - ", Size: " + - file.metaData("size") - ); - break; - case ImageMessage.TYPE: - file = message.getFile(); - console.log( - "Image message received. URL: " + - file.url() + - ", Width: " + - file.metaData("width") - ); - break; - case AudioMessage.TYPE: - file = message.getFile(); - console.log( - "Audio message received. URL: " + - file.url() + - ", Duration: " + - file.metaData("duration") - ); - break; - case VideoMessage.TYPE: - file = message.getFile(); - console.log( - "Video message received. URL: " + - file.url() + - ", Duration: " + - file.metaData("duration") - ); - break; - case LocationMessage.TYPE: - var location = message.getLocation(); - console.log( - "Location message received. Latitude: " + - location.latitude + - ", Longitude: " + - location.longitude - ); - break; - case 1: - console.log("OperationMessage is the custom message type"); - default: - // Your application may add new custom message types in the future. The SDK may add new built-in message types as well. - // Therefore, do not forget to handle them here. For example, you can notify the user to upgrade to a new version. - console.warn("收到未知类型消息"); - } -}); - -// `MESSAGE` event will be triggered on conversation as well -conversation.on(Event.MESSAGE, function messageEventHandler(message) { - // Your logic here -}); -``` - - -<> - -The Swift SDK handles new messages with `IMClientDelegate`: - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - print(message) - default: - break - } - default: - break - } -} -``` - -```swift -// Handle messages with built-in types -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - if let categorizedMessage = message as? IMCategorizedMessage { - switch categorizedMessage { - case let textMessage as IMTextMessage: - print(textMessage) - case let imageMessage as IMImageMessage: - print(imageMessage) - case let audioMessage as IMAudioMessage: - print(audioMessage) - case let videoMessage as IMVideoMessage: - print(videoMessage) - case let fileMessage as IMFileMessage: - print(fileMessage) - case let locationMessage as IMLocationMessage: - print(locationMessage) - case let recalledMessage as IMRecalledMessage: - print(recalledMessage) - case let customMessage as CustomMessage: - print("customMessage is a custom message type") - default: - break - } else { - // Your application may add new custom message types in the future. The SDK may add new built-in message types as well. - // Therefore, do not forget to handle them here. For example, you can notify the user to upgrade to a new version. - print("Message with unknown type received.") - } - default: - break - } - default: - break - } -} -``` - - -<> - -```dart -jerry.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - if (message.binaryContent != null) { - print('Received a binary message: ${message.binaryContent.toString()}'); - } else if (message is TextMessage) { - print('Received a text message: ${message.text}'); - } else if (message is LocationMessage) { - print('Received a location message: ${message.latitude},${message.longitude}'); - } else if (message is FileMessage) { - if (message is ImageMessage) { - print('Received an image message: ${message.url}'); - } else if (message is AudioMessage) { - print('Received an audio message: ${message.duration}'); - } else if (message is VideoMessage) { - print('Received a video message: ${message.duration}'); - } else { - print('Received a file message: ${message.url}'); - } - } else if (message is CustomMessage) { - // CustomMessage is a custom message type - print('Received a custom message'); - } else { - // Add more conditions for custom types - print('Received an unknown message'); - if (message.stringContent != null) { - print('Received a basic message: ${message.stringContent}'); - } - } -}; -``` - - - - -The code above involves the reception of messages with custom types. -We will cover more about it in [the second chapter](/sdk/im/guide/intermediate/). - -## Custom Attributes - -A `Conversation` object holds some built-in properties which match the fields in the `_Conversation` table. The table below shows these **built-in** properties: - - - -| Property of `AVIMConversation` | Field in `_Conversation` | Description | -| ------------------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `CurrentClient` | N/A | The `AVIMClient` the conversation belongs to. | -| `ConversationId` | `objectId` | A globally unique ID. | -| `Name` | `name` | The name of the conversation. Shared by all members. | -| `MemberIds` | `m` | The list of members. | -| `MuteMemberIds` | `mu` | The list of members that muted the conversation. | -| `Creator` | `c` | The creator of the conversation. | -| `IsTransient` | `tr` | Whether it is a chat room. | -| `IsSystem` | `sys` | Whether it is a system conversation. | -| `IsUnique` | `unique` | If this is `true`, the same conversation will be reused when a new conversation is created with the same composition of members and `unique` to be `true`. | -| `IsTemporary` | N/A | Whether it is a temporary conversation that will not be saved to the `_Conversation` class. | -| `CreatedAt` | `createdAt` | The time the conversation is created. | -| `UpdatedAt` | `updatedAt` | The time the conversation is updated. | -| `LastMessageAt` | `lm` | The time the last message is sent. | - -| Getter of `LCIMConversation` | Field in `_Conversation` | Description | -| ---------------------------- | ------------------------ | ------------------------------------------------------------------------------------------- | -| `getAttributes` | `attr` | Custom attributes. | -| `getConversationId` | `objectId` | A globally unique ID. | -| `getCreatedAt` | `createdAt` | The time the conversation is created. | -| `getCreator` | `c` | The creator of the conversation. | -| `getLastDeliveredAt` | N/A | The time the last message being delivered is sent (for one-on-one chats only). | -| `getLastMessage` | N/A | The last message. Could be empty. | -| `getLastMessageAt` | `lm` | The time the last message is sent. | -| `getLastReadAt` | N/A | The time the last message being read is sent (for one-on-one chats only). | -| `getMembers` | `m` | The list of members. | -| `getName` | `name` | The name of the conversation. Shared by all members. | -| `getTemporaryExpiredat` | N/A | Time to live (applicable for temporary conversations only). | -| `getUniqueId` | `uniqueId` | A globally unique `ID` for `Unique Conversation`. | -| `getUnreadMessagesCount` | N/A | The number of unread messages. | -| `getUpdatedAt` | `updatedAt` | The time the conversation is updated. | -| `isSystem` | `sys` | Whether it is a system conversation. | -| `isTemporary` | N/A | Whether it is a temporary conversation that will not be saved to the `_Conversation` class. | -| `isTransient` | `tr` | Whether it is a chat room. | -| `isUnique` | `unique` | Whether it is a `Unique Conversation`. | -| `unreadMessagesMentioned` | N/A | Whether unread messages contain a mention of the current `Client`. | - -| Property of `LCIMConversation` | Field in `_Conversation` | Description | -| ------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- | -| `clientID` | N/A | The `ID` of the `Client` the conversation belongs to. | -| `conversationId` | `objectId` | A globally unique ID. | -| `creator` | `c` | The creator of the conversation. | -| `createdAt` | `createdAt` | The time the conversation is created. | -| `updatedAt` | `updatedAt` | The time the conversation is updated. | -| `lastMessage` | N/A | The last message. Could be empty. | -| `lastMessageAt` | `lm` | The time the last message is sent. | -| `lastReadAt` | N/A | The time the last message being read is sent (for one-on-one chats only). | -| `lastDeliveredAt` | N/A | The time the last message being delivered is sent (for one-on-one chats only). | -| `unreadMessagesCount` | N/A | The number of unread messages. | -| `unreadMessageContainMention` | N/A | Whether unread messages contain a mention of the current `Client`. | -| `name` | `name` | The name of the conversation. Shared by all members. | -| `members` | `m` | The list of members. | -| `attributes` | `attr` | Custom attributes. | -| `uniqueId` | `uniqueId` | A globally unique `ID` for `Unique Conversation`. | -| `unique` | `unique` | Whether it is a `Unique Conversation`. | -| `transient` | `tr` | Whether it is a chat room. | -| `system` | `sys` | Whether it is a system conversation. | -| `temporary` | N/A | Whether it is a temporary conversation that will not be saved to the `_Conversation` class. | -| `temporaryTTL` | N/A | Time to live (applicable for temporary conversations only). | -| `muted` | N/A | Whether the current user muted the conversation. | -| `imClient` | N/A | The `LCIMClient` the conversation belongs to. | - -| Property of `Conversation` | Field in `_Conversation` | Description | -| -------------------------- | ------------------------ | ------------------------------------------------------------------------------ | -| `createdAt` | `createdAt` | The time the conversation is created. | -| `creator` | `c` | The creator of the conversation. | -| `id` | `objectId` | A globally unique ID. | -| `lastDeliveredAt` | N/A | The time the last message being delivered is sent (for one-on-one chats only). | -| `lastMessage` | N/A | The last message. Could be empty. | -| `lastMessageAt` | `lm` | The time the last message is sent. | -| `lastReadAt` | N/A | The time the last message being read is sent (for one-on-one chats only). | -| `members` | `m` | The list of members. | -| `muted` | N/A | Whether the current user muted the conversation. | -| `mutedMembers` | `mu` | The list of members that muted the conversation. | -| `name` | `name` | The name of the conversation. Shared by all members. | -| `system` | `sys` | Whether it is a system conversation. | -| `transient` | `tr` | Whether it is a chat room. | -| `unreadMessagesCount` | N/A | The number of unread messages. | -| `updatedAt` | `updatedAt` | The time the conversation is updated. | - -| Property of `IMConversation` | Field in `_Conversation` | Description | -| ------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | -| `client` | N/A | The `Client` the conversation belongs to. | -| `ID` | `objectId` | A globally unique `ID`. | -| `clientID` | N/A | The `ID` of the `Client` the conversation belongs to. | -| `isUnique` | `unique` | Whether it is a `Unique Conversation`. | -| `uniqueID` | `uniqueId` | A globally unique `ID` for `Unique Conversation`. | -| `name` | `name` | The name of the conversation. | -| `creator` | `c` | The creator of the conversation. | -| `createdAt` | `createdAt` | The time the conversation is created. | -| `updatedAt` | `updatedAt` | The time the conversation is updated. | -| `attributes` | `attr` | Custom attributes. | -| `members` | `m` | The list of members. | -| `isMuted` | N/A | Whether the current user muted the conversation. | -| `isOutdated` | N/A | Whether the properties of the conversation are outdated. Can be used to determine if the data of the conversation needs to be updated. | -| `lastMessage` | N/A | The last message. Could be empty. | -| `unreadMessageCount` | N/A | The number of unread messages. | -| `isUnreadMessageContainMention` | N/A | Whether unread messages contain a mention of the current `Client`. | -| `memberInfoTable` | N/A | A table of member information. | - -| `Conversation` 属性名 | `_Conversation` 字段 | 含义 | -| ------------------------- | -------------------- | ------------------------------------------------------------------------------ | -| `attributes` | `attr` | Custom attributes. | -| `client` | N/A | The `Client` the conversation belongs to. | -| `createdAt` | `createdAt` | The time the conversation is created. | -| `creator` | `c` | The creator of the conversation. | -| `id` | `objectId` | A globally unique `ID`. | -| `isMuted` | N/A | Whether the current user muted the conversation. | -| `isUnique` | `unique` | Whether it is a `Unique Conversation`. | -| `lastDeliveredAt` | N/A | The time the last message being delivered is sent (for one-on-one chats only). | -| `lastMessage` | N/A | The last message. Could be empty. | -| `lastReadAt` | N/A | The time the last message being read is sent (for one-on-one chats only). | -| `members` | `m` | The list of members. | -| `name` | `name` | The name of the conversation. Shared by all members. | -| `uniqueID` | `uniqueId` | A globally unique `ID` for `Unique Conversation`. | -| `unreadMessagesCount` | N/A | The number of unread messages. | -| `unreadMessagesMentioned` | N/A | Whether unread messages contain a mention of the current `Client`. | -| `updatedAt` | `updatedAt` | The time the conversation is updated. | - - - -However, direct write operations on the `_Conversation` table are frowned upon: - -- The conversation queries sent by client-side SDKs in websocket connections will first reach the Instant Messaging server's in-memory cache. Direct write operations on the `_Conversation` table will not update the cache, which may cause cache inconsistency. -- With direct write operations on the `_Conversation` table, the Instant Messaging server has no chance to notify the client-side. Thus the client-side will not receive any corresponding events. -- If Instant Messaging hooks are defined, direct write operations on the `_Conversation` table will not trigger them. - -For administrative tasks, the dedicated Instant Messaging REST API interface is recommended. - -Besides these built-in properties, you can also define your custom attributes to store more data with each conversation. - -### Creating Custom Attributes - -When introducing [one-on-one conversations](#creating-conversations), we mentioned that `IMClient#createConversation` allows you to attach custom attributes to a conversation. Now let's see how we can do that. - -Assume that we need to add two properties `{ "type": "private", "pinned": true }` to a conversation we are creating. We can do so by passing in the properties when calling `IMClient#createConversation`: - - - -```cs -var properties = new Dictionary { - { "type", "private" }, - { "pinned", true } -}; -var conversation = await tom.CreateConversation("Jerry", name: "Tom & Jerry", unique: true, properties: properties); -``` - -```java -HashMap attr = new HashMap(); -attr.put("type","private"); -attr.put("pinned",true); -client.createConversation(Arrays.asList("Jerry"),"Tom & Jerry", attr, false, true, - new LCIMConversationCreatedCallback(){ - @Override - public void done(LCIMConversation conv,LCIMException e){ - if(e==null){ - // Conversation created - } - } - }); -``` - -```objc -// Tom creates a conversation named "Tom & Jerry" and attaches custom attributes to it -LCIMConversationCreationOption *option = [LCIMConversationCreationOption new]; -option.name = @"Tom & Jerry"; -option.attributes = @{ - @"type": @"private", - @"pinned": @(YES) -}; -[self createConversationWithClientIds:@[@"Jerry"] option:option callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"Conversation created!"); - } -}]; -``` - -```js -tom - .createConversation({ - members: ["Jerry"], - name: "Tom & Jerry", - unique: true, - type: "private", - pinned: true, - }) - .then(function (conversation) { - console.log("Conversation created! ID: " + conversation.id); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - try tom.createConversation(clientIDs: ["Jerry"], name: "Tom & Jerry", attributes: ["type": "private", "pinned": true], isUnique: true, completion: { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - Conversation conversation = await jerry.createConversation( - members: {'client1.id', 'client2.id'}, - attributes: { - 'members': ['Jerry'], - 'name': 'Tom & Jerry', - 'unique': true, - 'type': 'private', - 'pinned': true, - }, - ); -} catch (e) { - print(e); -} -``` - - - -**The SDK allows everyone in a conversation to access its custom attributes.** You can even query conversations that satisfy certain custom attributes. See [Querying Conversations with Custom Conditions](#querying-conversations-with-custom-conditions). - -### Updating and Retrieving Properties - -The built-in properties (like `name`) of a `Conversation` object can be updated by all the members unless you set restrictions in your app: - - - -```cs -await conversation.UpdateInfo(new Dictionary { - { "name", "Tom is Smart" } -}); -``` - -```java -LCIMConversation conversation = client.getConversation("55117292e4b065f7ee9edd29"); -conversation.setName("Tom is Smart"); -conversation.updateInfoInBackground(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // Updated - } - } -}); -``` - -```objc -conversation[@"name"] = @"Tom is Smart"; -[conversation updateWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"Updated!"); - } -}]; -``` - -```js -conversation.name = "Tom is Smart"; -conversation.save(); -``` - -```swift -do { - try conversation.update(attribution: ["name": "Tom is Smart"], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - await conversation.updateInfo(attributes: { - 'name': 'Tom is Smart', - }); -} catch (e) { - print(e); -} -``` - - - -Custom attributes can also be retrieved or updated by all the members: - - - -```cs -// Retrieve custom attribute -var type = conversation["type"]; -// Set new value for pinned -await conversation.UpdateInfo(new Dictionary { - { "pinned", false } -}); -``` - -```java -// Retrieve custom attribute -String type = conversation.get("attr.type"); -// Set new value for pinned -conversation.set("attr.pinned",false); -// Save -conversation.updateInfoInBackground(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // Saved - } - } -}); -``` - -```objc -// Retrieve custom attribute -NSString *type = conversation.attributes[@"type"]; -// Set new value for pinned -[conversation setObject:@(NO) forKey:@"attr.pinned"]; -// Save -[conversation updateWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Saved!"); - } -}]; -``` - -```js -// Retrieve custom attribute -var type = conversation.get("attr.type"); -// Set new value for pinned -conversation.set("attr.pinned", false); -// Save -conversation.save(); -``` - -```swift -do { - let type = conversation.attributes?["type"] as? String - try conversation.update(attribution: ["attr.pinned": false]) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { -// Retrieve custom attribute - String type = conversation.attributes['type']; -// Set new value for pinned - await conversation.updateInfo(attributes: { - 'pinned': false, - }); -} catch (e) { - print(e); -} -``` - - - -Notes on custom attributes: - -The custom attributes specified with `IMClient#createConversation` will be stored in the field `attr` of the `_Conversation` table. If you need to retrieve or update them later, the full path needs to be specified, like `attr.type`. - -### Synchronization of Properties - -The properties of a conversation (like name) are shared by everyone in it. If someone ever changes a property, other members need to get updated on it. In the example we used earlier, a user changed the name of a conversation to "Tom is Smart". How would other members get to know about it? - -Instant Messaging offers the mechanism that automatically delivers the change made by a user to a conversation to all the members in it (for those who are offline, they will receive updates once they get online): - - - -```cs -jerry.OnConversationInfoUpdated = (conv, attrs, initBy) => { - WriteLine($"Conversation ${conv.Id} updated."); -}; -``` - -```java -// The following definition exists in LCIMConversationEventHandler -/** - * The properties of a conversation are updated - * - * @param client - * @param conversation - * @param attr The properties being updated - * @param operator The ID of the operator - */ -public void onInfoChanged(LCIMClient client, LCIMConversation conversation, JSONObject attr, - String operator) -``` - -```objc -/// Notification for conversation's attribution updated. -/// @param conversation Updated conversation. -/// @param date Updated date. -/// @param clientId Client ID which do this update. -/// @param updatedData Updated data. -/// @param updatingData Updating data. -- (void)conversation:(LCIMConversation *)conversation didUpdateAt:(NSDate * _Nullable)date byClientId:(NSString * _Nullable)clientId updatedData:(NSDictionary * _Nullable)updatedData updatingData:(NSDictionary * _Nullable)updatingData; -``` - -```js -/** - * The properties of a conversation are updated - * @event IMClient#CONVERSATION_INFO_UPDATED - * @param {Object} payload - * @param {Object} payload.attributes The properties being updated - * @param {String} payload.updatedBy The ID of the operator - */ -var { Event } = require("leancloud-realtime"); -client.on(Event.CONVERSATION_INFO_UPDATED, function (payload) {}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .dataUpdated(updatingData: updatingData, updatedData: updatedData, byClientID: byClientID, at: atDate): - print(updatingData) - print(updatedData) - print(byClientID) - print(atDate) - default: - break - } -} -``` - -```dart -jerry.onInfoUpdated = ({ - Client client, - Conversation conversation, - Map updatingAttributes, - Map updatedAttributes, - String byClientID, - DateTime atDate, -}) { - print('Conversation ${conversation.id} updated.'); -}; -``` - - - -Notes: - -You can either retrieve the properties being updated from the callback function or directly read the latest values from the `Conversation` object. - -### Retrieving Member Lists - -To get the list of members in a conversation, we can call the method for fetching on a `Conversation` object and then get the result from it: - - - -```cs -await conversation.Fetch(); -``` - -```java -// fetchInfoInBackground will trigger an operation to retrieve the latest data from the cloud -conversation.fetchInfoInBackground(new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - conversation.getMembers(); - } - } -}); -``` - -```objc -// fetchWithCallback will trigger an operation to retrieve the latest data from the cloud -[conversation fetchWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"", conversation.members); - } -}]; -``` - -```js -// fetch will trigger an operation to retrieve the latest data from the cloud -conversation.fetch().then(function(conversation) { - console.log('members: ', conversation.members); -).catch(console.error.bind(console)); -``` - -```swift -do { - try conversation.refresh { (result) in - switch result { - case .success: - if let members = conversation.members { - print(members) - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// Not supported yet -``` - - - -Notes: - -You can only get member lists of **basic conversations**. Chat rooms and system conversations don't have member lists. - -## Querying Conversations with Custom Conditions - -There are more ways to get a `Conversation` besides listening to incoming events. You might want your users to search chat rooms by the names or locations of them, or to look for conversations that have certain members in them. All these requirements can be satisfied with the help of queries. - -### Queries on ID - -Here ID refers to the `objectId` in the `_Conversation` table. Since IDs are indexed, querying by ID is the easiest and most efficient way to look for a conversation: - - - -```cs -var query = tom.GetQuery(); -var conversation = await query.Get("551260efe4b01608686c3e0f"); -``` - -```java -LCIMConversationsQuery query = tom.getConversationsQuery(); -query.whereEqualTo("objectId","551260efe4b01608686c3e0f"); -query.findInBackground(new LCIMConversationQueryCallback(){ - @Override - public void done(List convs,LCIMException e){ - if(e==null){ - if(convs!=null && !convs.isEmpty()){ - // convs.get(0) is the conversation being found - } - } - } -}); -``` - -```objc -LCIMConversationQuery *query = [tom conversationQuery]; -[query getConversationById:@"551260efe4b01608686c3e0f" callback:^(LCIMConversation *conversation, NSError *error) { - if (succeeded) { - NSLog(@"Query completed!"); - } -}]; -``` - -```js -tom - .getConversation("551260efe4b01608686c3e0f") - .then(function (conversation) { - console.log(conversation.id); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let conversationQuery = tom.conversationQuery - try conversationQuery.getConversation(by: "551260efe4b01608686c3e0f") { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// We suggest you to get a conversation from the device's memory first to avoid making unnecessary network requests - -String convID = '551260efe4b01608686c3e0f'; -Conversation conversation = tom.conversationMap[convID]; -if (conversation == null) { - try { - ConversationQuery query = tom.conversationQuery(); - query.whereEqualTo('objectId', convID); - conversation = await query.find(); - } catch (e) { - print(e); - } -} -``` - - - -### Querying by Conditions - -There are a variety of ways for you to look for conversations that satisfy certain conditions. - -Let's start with `equalTo` which is the simplest method for querying conversations. The code below looks for all the conversations that have `type` (a string field) to be `private`: - - - -```cs -var query = tom.GetQuery() - .WhereEqualTo("type", "private"); -await query.Find(); -``` - -```java -LCIMConversationsQuery query = tom.getConversationsQuery(); -query.whereEqualTo("attr.type","private"); -// Perform query -query.findInBackground(new LCIMConversationQueryCallback(){ - @Override - public void done(List convs,LCIMException e){ - if(e == null){ - // convs contains all the results - } - } -}); -``` - -```objc -LCIMConversationQuery *query = [tom conversationQuery]; -[query whereKey:@"attr.type" equalTo:@"private"]; -// Perform query -[query findConversationsWithCallback:^(NSArray *objects, NSError *error) { - NSLog(@"找到 %ld 个对话!", [objects count]); -}]; -``` - -```js -var query = client.getQuery(); -query.equalTo("attr.type", "private"); -query - .find() - .then(function (conversations) { - // conversations contains all the results - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let conversationQuery = tom.conversationQuery - try conversationQuery.where("attr.type", .equalTo("private")) - try conversationQuery.findConversations { (result) in - switch result { - case .success(value: let conversations): - print(conversations) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - ConversationQuery query = jerry.conversationQuery(); - query.whereEqualTo('attr.type', 'private'); -// conversations contains all the results - List conversations = await query.find(); -} catch (e) { - print(e); -} -``` - - - -The interface for querying conversations is very similar to that for querying objects in Data Storage. If you're already familiar with Data Storage, it shouldn't be hard for you to learn how to query conversations: - -- You can get query results with `find` -- You can get number of results with `count` -- You can get the first conversation satisfying conditions with `first` -- You can implement pagination with `skip` and `limit` - -You can also apply conditions like "greater than", "greater than or equal to", "less than", and "less than or equal to" to `Number` and `Date` fields: - - - -| Logic | `AVIMConversationQuery` Method | -| ------------------------ | ------------------------------ | -| Equal to | `WhereEqualTo` | -| Not equal to | `WhereNotEqualsTo` | -| Greater than | `WhereGreaterThan` | -| Greater than or equal to | `WhereGreaterThanOrEqualsTo` | -| Less than | `WhereLessThan` | -| Less than or equal to | `WhereLessThanOrEqualsTo` | - -| Logic | `LCIMConversationsQuery` Method | -| ------------------------ | ------------------------------- | -| Equal to | `whereEqualTo` | -| Not equal to | `whereNotEqualsTo` | -| Greater than | `whereGreaterThan` | -| Greater than or equal to | `whereGreaterThanOrEqualsTo` | -| Less than | `whereLessThan` | -| Less than or equal to | `whereLessThanOrEqualsTo` | - -| Logic | `LCIMConversationQuery` Method | -| ------------------------ | ------------------------------ | -| Equal to | `equalTo` | -| Not equal to | `notEqualTo` | -| Greater than | `greaterThan` | -| Greater than or equal to | `greaterThanOrEqualTo` | -| Less than | `lessThan` | -| Less than or equal to | `lessThanOrEqualTo` | - -| Logic | `Constraint` of `IMConversationQuery` | -| ------------------------ | ------------------------------------- | -| Equal to | `equalTo` | -| Not equal to | `notEqualTo` | -| Greater than | `greaterThan` | -| Greater than or equal to | `greaterThanOrEqualTo` | -| Less than | `lessThan` | -| Less than or equal to | `lessThanOrEqualTo` | - -| Logic | `ConversationQuery` Method | -| ------------------------ | -------------------------- | -| Equal to | `equalTo` | -| Not equal to | `notEqualTo` | -| Greater than | `greaterThan` | -| Greater than or equal to | `greaterThanOrEqualTo` | -| Less than | `lessThan` | -| Less than or equal to | `lessThanOrEqualTo` | - - - -Notes on default query conditions: - -When querying conversations, if there isn't any `where` condition specified, `ConversationQuery` will look for conversations containing the current user by default. Such a condition will be dismissed if any `where` condition is applied to the query. If you want to look for conversations containing certain `clientId`, you can follow the way introduced in [Queries on Array Values](#queries-on-array-values) to perform queries on `m` with the value of `clientId`. This won't cause any conflict with the default condition. - -### Using Regular Expressions - -You can use regular expressions as conditions when querying with `ConversationsQuery`. For example, to look for all the conversations that have `language` to be a Chinese character: - - - -```cs -query.WhereMatches("language", "[\\u4e00-\\u9fa5]"); // language is a Chinese character -``` - -```java -query.whereMatches("language","[\\u4e00-\\u9fa5]"); // language is a Chinese character -``` - -```objc -[query whereKey:@"language" matchesRegex:@"[\\u4e00-\\u9fa5]"]; // language is a Chinese character -``` - -```js -query.matches("language", /[\\u4e00-\\u9fa5]/); // language is a Chinese character -``` - -```swift -try conversationQuery.where("language", .matchedRegularExpression("[\\u4e00-\\u9fa5]", option: nil)) -``` - -```dart -// Not supported yet -``` - - - -### Queries on String Values - -You can look for conversations with string values that **start with** a particular string, which is similar to `LIKE 'keyword%'` in SQL. For example, to look for all the conversations with names starting with `education`: - - - -```cs -query.WhereStartsWith("name", "education"); -``` - -```java -query.whereStartsWith("name","education"); -``` - -```objc -[query whereKey:@"name" hasPrefix:@"education"]; -``` - -```js -query.startsWith("name", "education"); -``` - -```swift -try conversationQuery.where("name", .prefixedBy("education")) -``` - -```dart -// Not supported yet -``` - - - -You can also look for conversations with string values that **include** a particular string, which is similar to `LIKE '%keyword%'` in SQL. For example, to look for all the conversations with names including `education`: - - - -```cs -query.WhereContains("name", "education"); -``` - -```java -query.whereContains("name","education"); -``` - -```objc -[query whereKey:@"name" containsString:@"education"]; -``` - -```js -query.contains("name", "education"); -``` - -```swift -try conversationQuery.where("name", .matchedSubstring("education")) -``` - -```dart -// Not supported yet -``` - - - -If you want to look for conversations with string values that **exclude** a particular string, you can use [regular expressions](#using-regular-expressions). For example, to look for all the conversations with names excluding `education`: - - - -```cs -query.WhereMatches("name", "^((?!education).)* $ "); -``` - -```java -query.whereMatches("name","^((?!education).)* $ "); -``` - -```objc -[query whereKey:@"name" matchesRegex:@"^((?!education).)* $ "]; -``` - -```js -var regExp = new RegExp("^((?!education).)*$", "i"); -query.matches("name", regExp); -``` - -```swift -try conversationQuery.where("name", .matchedRegularExpression("^((?!education).)* $ ", option: nil)) -``` - -```dart -// Not supported yet -``` - - - -### Queries on Array Values - -You can use `containsAll`, `containedIn`, and `notContainedIn` to perform queries on array values. For example, to look for all the conversations containing `Tom`: - - - -```cs -var members = new List { "Tom" }; -query.WhereContainedIn("m", members); -``` - -```java -query.whereContainedIn("m", Arrays.asList("Tom")); -``` - -```objc -[query whereKey:@"m" containedIn:@[@"Tom"]]; -``` - -```js -query.containedIn("m", ["Tom"]); -``` - -```swift -try conversationQuery.where("m", .containedIn(["Tom"])) -``` - -```dart -// Not supported yet -``` - - - -### Queries on Existence - -You can look for conversations with or without certain fields to be empty. For example, to look for all the conversations with `lm` to be empty: - - - -```cs -query.WhereDoesNotExist("lm"); -``` - -```java -query.whereDoesNotExist("lm"); -``` - -```objc -[query whereKeyDoesNotExist:@"lm"]; -``` - -```js -query.doesNotExist("lm"); -``` - -```swift -try conversationQuery.where("lm", .notExisted) -``` - -```dart -// Not supported yet -``` - - - -Or, to look for all the conversations with `lm` not to be empty: - - - -```cs -query.WhereExists("lm"); -``` - -```java -query.whereExists("lm"); -``` - -```objc -[query whereKeyExists:@"lm"]; -``` - -```js -query.exists("lm"); -``` - -```swift -try conversationQuery.where("lm", .existed) -``` - -```dart -// Not supported yet -``` - - - -### Compound Queries - -To look for all the conversations with `age` to be less than `18` and `keywords` containing `education`: - - - -```cs -query.WhereContains("keywords", "education") - .WhereLessThan("age", 18); -``` - -```java -query.whereContains("keywords", "education"); -query.whereLessThan("age", 18); -``` - -```objc -[query whereKey:@"keywords" containsString:@"education"]; -[query whereKey:@"age" lessThan:@(18)]; -``` - -```js -query.contains("keywords", "education").lessThan("age", 18); -``` - -```swift -try conversationQuery.where("keywords", .matchedSubstring("education")) -try conversationQuery.where("age", .lessThan(18)) -``` - -```dart -// Not supported yet -``` - - - -You can also combine two queries with `and` or `or` to form a new query. - -For example, to look for all the conversations that either have `age` to be less than `18` or have `keywords` containing `education`: - - - -```cs -// Not supported yet -``` - -```java -LCIMConversationsQuery ageQuery = tom.getConversationsQuery(); -ageQuery.whereLessThan('age', 18); - -LCIMConversationsQuery keywordsQuery = tom.getConversationsQuery(); -keywordsQuery.whereContains('keywords', 'education'); - -LCIMConversationsQuery query = LCIMConversationsQuery.or(Arrays.asList(priorityQuery, statusQuery)); -``` - -```objc -LCIMConversationQuery *ageQuery = [tom conversationQuery]; -[ageQuery whereKey:@"age" greaterThan:@(18)]; - -LCIMConversationQuery *keywordsQuery = [tom conversationQuery]; -[keywordsQuery whereKey:@"keywords" containsString:@"education"]; - -LCIMConversationQuery *query = [LCIMConversationQuery orQueryWithSubqueries:[NSArray arrayWithObjects:ageQuery,keywordsQuery,nil]]; -``` - -```js -// Not supported yet -``` - -```swift -do { - let ageQuery = tom.conversationQuery - try ageQuery.where("age", .greaterThan(18)) - - let keywordsQuery = tom.conversationQuery - try keywordsQuery.where("keywords", .matchedSubstring("education")) - - let conversationQuery = try ageQuery.or(keywordsQuery) -} catch { - print(error) -} -``` - -```dart -// Not supported yet -``` - - - -### Sorting - -You can sort the results of a query by ascending or descending order on certain fields. For example: - - - -```cs -query.OrderByDescending("createdAt"); -``` - -```java -query.orderByDescending("createdAt"); -``` - -```objc -[query orderByDescending:@"createdAt"]; -``` - -```js -// Ascend by name and descend by creation time -query.addAscending("name").addDescending("createdAt"); -``` - -```swift -try conversationQuery.where("createdAt", .descending) -``` - -```dart -// Not supported yet -``` - - - -### Excluding Member Lists from Results - -When searching conversations, you can exclude the lists of members from query results if you don't need them. By doing so, their `members` fields will become empty arrays. This helps you improve the speed of your app and reduces the network traffic needed. - - - -```cs -query.Compact = true; -``` - -```java -query.setCompact(true); -``` - -```objc -query.option = LCIMConversationQueryOptionCompact; -``` - -```js -query.compact(true); -``` - -```swift -conversationQuery.options = [.notContainMembers] -``` - -```dart -query.excludeMembers = true; -``` - - - -### Including Latest Messages in Results - -Many chatting apps show the latest message of each conversation in the conversation list. If you want a similar function in your app, you can turn on the option when querying conversations: - - - -```cs -query.WithLastMessageRefreshed = true; -``` - -```java -query.setWithLastMessagesRefreshed(true); -``` - -```objc -query.option = LCIMConversationQueryOptionWithMessage; -``` - -```js -// enable withLastMessagesRefreshed to include the latest message of each conversation in the results -query.withLastMessagesRefreshed(true); -``` - -```swift -conversationQuery.options = [.containLastMessage] -``` - -```dart -query.includeLastMessage = true; -``` - - - -Keep in mind that what this option really does is to refresh the latest messages of conversations. Due to the existence of the cache, it is still possible for you to retrieve the outdated "latest messages" even though you set the option to be `false`. - -### Caching Results - - -<> - -Not supported yet. - - -<> - -By caching query results locally, if the device is offline, or if the app is just opened and the request for synchronizing with the cloud is not completed yet, there could still be some data available. You can also reduce the data usage of the user by performing queries with the cloud only when the app is first opened and having subsequent queries completed with local cache first. - -Keep in mind that query results will be fed with local cache first and will be synchronized with the cloud right after that. The expiration time for cache is 1 hour. You can configure cache with the following method provided by `LCIMConversationsQuery`: - -```java -// Set caching policy for LCIMConversationsQuery -public void setQueryPolicy(LCQuery.CachePolicy policy); -``` - -If you want cache to be accessed only when there's an error querying with the cloud, you can do this: - -```java -LCIMConversationsQuery query = client.getConversationsQuery(); -query.setQueryPolicy(LCQuery.CachePolicy.NETWORK_ELSE_CACHE); -query.findInBackground(new LCIMConversationQueryCallback() { - @Override - public void done(List conversations, LCIMException e) { - - } -}); -``` - - -<> - -By caching query results locally, if the device is offline, or if the app is just opened and the request for synchronizing with the cloud is not completed yet, there could still be some data available. You can also reduce the data usage of the user by performing queries with the cloud only when the app is first opened and having subsequent queries completed with local cache first. - -Keep in mind that query results will be fed with local cache first and will be synchronized with the cloud right after that. The expiration time for cache is 1 hour. You can configure cache with the following method provided by `LCIMConversationQuery`: - -```objc -// Set caching policy; defaults to kLCCachePolicyCacheElseNetwork -@property (nonatomic) LCCachePolicy cachePolicy; - -// Set expiration time; defaults to 1 hour (1 * 60 * 60) -@property (nonatomic) NSTimeInterval cacheMaxAge; -``` - -If you want cache to be accessed only when there's an error querying with the cloud, you can do this: - -```objc -LCIMConversationQuery *query = [client conversationQuery]; -query.cachePolicy = kLCCachePolicyNetworkElseCache; -[query findConversationsWithCallback:^(NSArray *objects, NSError *error) { - -}]; -``` - -See [Data Storage Guide](/sdk/storage/guide/dotnet/) to learn more about the difference between all the caching policies. - - -<> - -Conversations will be cached in memory using dictionaries according to their IDs. Such cache will not be persisted. - - -<> - -The Swift SDK allows you to cache conversation to either memory or local storage. - -The code below caches conversations to memory: - -```swift -client.getCachedConversation(ID: "CONVERSATION_ID") { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } -} - -client.removeCachedConversation(IDs: ["CONVERSATION_ID"]) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -The code below caches conversations to local storage. **Note that when querying or deleting conversations stored in local storage, you need to call `prepareLocalStorage` and make sure the result is success; `prepareLocalStorage` only needs to be called once (for a result with success) and is often called between `IMClient.init()` and `IMClient.open()`**: - -```swift -// Switch for Local Storage of IM Client -do { - // Client init with Local Storage feature - let clientWithLocalStorage = try IMClient(ID: "CLIENT_ID") - - // Client init without Local Storage feature - var options = IMClient.Options.default - options.remove(.usingLocalStorage) - let clientWithoutLocalStorage = try IMClient(ID: "CLIENT_ID", options: options) -} catch { - print(error) -} - -// Preparation for Local Storage of IM Client -do { - try client.prepareLocalStorage { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} - -// Get and Load Stored Conversations to Memory -do { - try client.getAndLoadStoredConversations(completion: { (result) in - switch result { - case .success(value: let conversations): - print(conversations) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} - -// Delete Stored Conversations and Messages belong to them -do { - try client.deleteStoredConversationAndMessages(IDs: ["CONVERSATION_ID"], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -Be aware that: - -- Chat rooms and temporary conversations are not cached. -- Conversations have both in-memory cache and persistent (disk) cache. Messages only have in-memory cache, and only message query results are cached (if a message query has less than 3 results, it will not be cached). - - -<> - -Not supported yet. - - - - - -### Optimizing Performance - -Since `Conversation` objects are stored on Data Storage, you can make use of indexes to improve the efficiency of querying, just like how you would do to other classes. Here are some suggestions for optimizing performance: - -- By default, indexes are created for `objectId`, `updatedAt`, and `createdAt` of `Conversation`, so querying by these fields would be naturally fast. -- Although it's possible to implement pagination with `skip` and `limit`, the speed would slow down when the dataset grows larger. It would be more efficient to make use of `updatedAt` or `lastMessageAt` instead. -- When searching for conversations containing a certain user by using `contains` on `m`, it's recommended that you stick to the default `limit` (which is 10) and make use of `updatedAt` or `lastMessageAt` for pagination. -- If your app has too many conversations, consider creating a cloud function that periodically cleans up inactive conversations. - -## Retrieving Messages - -By default, message histories are stored on the cloud for **180** days. You may either pay to extend the period (contact us by submitting a ticket) or synchronize them to your own server with REST API. - -Our SDKs offer various ways for you to retrieve message histories. iOS and Android SDKs also provide a caching mechanism to help you reduce the number of queries you have to perform and display message histories to users even when their devices are offline. - -### Retrieving Messages Chronologically (New to Old) - -The most common way to retrieve messages is to fetch them from new to old with the help of pagination: - - - -```cs -// limit could be any number from 1 to 100 (defaults to 20) -var messages = await conversation.QueryMessages(limit: 10); -foreach (var message in messages) { - if (message is LCIMTextMessage textMessage) { - - } -} -``` - -```java -// limit could be any number from 1 to 100; invoking queryMessages without the limit parameter will retrieve 20 messages -int limit = 10; -conv.queryMessages(limit, new LCIMMessagesQueryCallback() { - @Override - public void done(List messages, LCIMException e) { - if (e == null) { - // The last 10 messages retrieved - } - } -}); -``` - -```objc -// Retrieve the last 10 messages; limit could be any number from 1 to 100; use 0 for the default value (20) -[conversation queryMessagesWithLimit:10 callback:^(NSArray *objects, NSError *error) { - NSLog(@"Messages retrieved!"); -}]; -``` - -```js -conversation - .queryMessages({ - limit: 10, // limit could be any number from 1 to 100 (defaults to 20) - }) - .then(function (messages) { - // The last 10 messages ordered from old to new - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - try conversation.queryMessage(limit: 10) { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// limit could be any number from 1 to 100 (defaults to 20) -try { - List messages = await conversation.queryMessage( - limit: 10, - ); -} catch (e) { - print(e); -} -``` - - - -Here `queryMessage` supports pagination. Given the fact that you can locate a single message with its `messageId` and timestamp, this means that you can retrieve the next few messages after a given message by providing the `messageId` and timestamp of that message: - - - -```cs -// limit could be any number from 1 to 1000 (defaults to 100) -var messages = await conversation.QueryMessages(limit: 10); -var oldestMessage = messages[0]; -var start = new LCIMMessageQueryEndpoint { - MessageId = oldestMessage.Id, - SentTimestamp = oldestMessage.SentTimestamp -}; -var messagesInPage = await conversation.QueryMessages(start: start); -``` - -```java -// limit could be any number from 1 to 1000 (defaults to 100) -conv.queryMessages(10, new LCIMMessagesQueryCallback() { - @Override - public void done(List messages, LCIMException e) { - if (e == null) { - // The last 10 messages retrieved - // The earliest message will be the first one - LCIMMessage oldestMessage = messages.get(0); - - conv.queryMessages(oldestMessage.getMessageId(), oldestMessage.getTimestamp(),20, - new LCIMMessagesQueryCallback(){ - @Override - public void done(List messagesInPage,LCIMException e){ - if(e== null){ - // Query completed - Log.d("Tom & Jerry", "got " + messagesInPage.size()+" messages"); - } - } - }); - } - } -}); -``` - -```objc -// Retrieve the last 10 messages -[conversation queryMessagesWithLimit:10 callback:^(NSArray *messages, NSError *error) { - NSLog(@"First retrieval completed!"); - // Get the messages right before the first message in the first page - LCIMMessage *oldestMessage = [messages firstObject]; - [conversation queryMessagesBeforeId:oldestMessage.messageId timestamp:oldestMessage.sendTimestamp limit:10 callback:^(NSArray *messagesInPage, NSError *error) { - NSLog(@"Second retrieval completed!"); - }]; -}]; -``` - -```js -// JS SDK encloses the feature into an iterator so you can keep retrieving new data by calling next -// Create an iterator and retrieve 10 messages each time -var messageIterator = conversation.createMessagesIterator({ limit: 10 }); -// Call next for the first time and get the first 10 messages; done equals to false means that there are more messages -messageIterator - .next() - .then(function (result) { - // result: { - // value: [message1, ..., message10], - // done: false, - // } - }) - .catch(console.error.bind(console)); -// Call next for the second time and get the 11th to 20th messages; done equals to false means that there are more messages -// The iterator will keep track of the breaking point so you don't have to specify it -messageIterator - .next() - .then(function (result) { - // result: { - // value: [message11, ..., message20], - // done: false, - // } - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let start = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID", - sentTimestamp: 31415926, - isClosed: false - ) - try conversation.queryMessage(start: start, limit: 10, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -List messages; -try { -// First query completed - messages = await conversation.queryMessage( - limit: 10, - ); -} catch (e) { - print(e); -} - -try { - // The earliest message will be the first one - Message oldMessage = messages.first; - // Get the messages right before the first message in the first page - List messages2 = await conversation.queryMessage( - startTimestamp: oldMessage.sentTimestamp, - startMessageID: oldMessage.id, - startClosed: true, - limit: 10, - ); -} catch (e) { - print(e); -} -``` - - - -### Retrieving Messages by Types - -Besides retrieving messages in time orders, you can also do that based on the types of messages. This could be helpful in scenarios like displaying all the images in a conversation. - -`queryMessage` can take in the type of messages: - - - -```cs -// Pass in a generic type parameter and the SDK will automatically read the type and send it to the server for searching messages -var imageMessages = await conversation.QueryMessages(messageType: -2); -``` - -```java -int msgType = LCIMMessageType.IMAGE_MESSAGE_TYPE; -conversation.queryMessagesByType(msgType, limit, new LCIMMessagesQueryCallback() { - @Override - public void done(List messages, LCIMException e){ - } -}); -``` - -```objc -[conversation queryMediaMessagesFromServerWithType:LCIMMessageMediaTypeImage limit:10 fromMessageId:nil fromTimestamp:0 callback:^(NSArray *messages, NSError *error) { - if (!error) { - NSLog(@"Query completed!"); - } -}]; -``` - -```js -conversation - .queryMessages({ type: ImageMessage.TYPE }) - .then((messages) => { - console.log(messages); - }) - .catch(console.error); -``` - -```swift -do { - try conversation.queryMessage(limit: 10, type: IMTextMessage.messageType, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - List messages = await conversation.queryMessage(type: -2); -} catch (e) { - print(e); -} -``` - - - -To retrieve more images, follow the way introduced in the previous section to go through different pages. - -### Retrieving Messages Chronologically (Old to New) - -Besides the two ways mentioned above, you can also retrieve messages from old to new. The code below shows how you can retrieve messages starting from the time the conversation is created: - - - -```cs -var earliestMessages = await conversation.QueryMessages(direction: LCIMMessageQueryDirection.OldToNew); -``` - -```java -LCIMMessageInterval interval = new LCIMMessageInterval(null, null); -conversation.queryMessages(interval, DirectionFromOldToNew, limit, - new LCIMMessagesQueryCallback(){ - public void done(List messages, LCIMException exception) { - // Handle result - } -}); -``` - -```objc -[conversation queryMessagesInInterval:nil direction:LCIMMessageQueryDirectionFromOldToNew limit:20 callback:^(NSArray * _Nullable messages, NSError * _Nullable error) { - if (messages.count) { - // Handle result - } -}]; -``` - -```js -var { MessageQueryDirection } = require('leancloud-realtime'); -conversation.queryMessages({ - direction: MessageQueryDirection.OLD_TO_NEW, -}).then(function(messages) { - // Handle result -}.catch(function(error) { - // Handle error -}); -``` - -```swift -do { - try conversation.queryMessage(direction: .oldToNew, limit: 10, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - List messages = await conversation.queryMessage( - direction: MessageQueryDirection.oldToNew, - ); -} catch (e) { - print(e); -} -``` - - - -It is a bit more complicated to implement pagination with this method. See the next section for details. - -### Retrieving Messages Chronologically (From a Timestamp to a Direction) - -You can retrieve messages starting from a given message (determined by ID and timestamp) toward a certain direction: - -- New to old: Retrieve messages sent **before** a given message -- Old to new: Retrieve messages sent **after** a given message - -Now we can implement pagination in different directions. - - - -```cs -var earliestMessages = await conversation.QueryMessages(direction: LCIMMessageQueryDirection.OldToNew, limit: 1); -// Get messages sent after earliestMessages.Last() -var start = new LCIMMessageQueryEndpoint { - MessageId = earliestMessages.Last().Id -}; -var nextPageMessages = await conversation.QueryMessages(start: start); -``` - -```java -LCIMMessageIntervalBound start = LCIMMessageInterval.createBound(messageId, timestamp, false); -LCIMMessageInterval interval = new LCIMMessageInterval(start, null); -LCIMMessageQueryDirection direction; -conversation.queryMessages(interval, direction, limit, - new LCIMMessagesQueryCallback(){ - public void done(List messages, LCIMException exception) { - // Handle result - } -}); -``` - -```objc -LCIMMessageIntervalBound *start = [[LCIMMessageIntervalBound alloc] initWithMessageId:nil timestamp:timestamp closed:false]; -LCIMMessageInterval *interval = [[LCIMMessageInterval alloc] initWithStartIntervalBound:start endIntervalBound:nil]; -[conversation queryMessagesInInterval:interval direction:direction limit:20 callback:^(NSArray * _Nullable messages, NSError * _Nullable error) { - if (messages.count) { - // Handle result - } -}]; -``` - -```js -var { MessageQueryDirection } = require('leancloud-realtime'); -conversation.queryMessages({ - startTime: timestamp, - startMessageId: messageId, -startClosed: false, - direction: MessageQueryDirection.OLD_TO_NEW, -}).then(function(messages) { - // Handle result -}.catch(function(error) { - // Handle error -}); -``` - -```swift -do { - let start = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID", - sentTimestamp: 31415926, - isClosed: true - ) - try conversation.queryMessage(start: start, direction: .oldToNew, limit: 10, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - List messages = await conversation.queryMessage( - startTimestamp: textMessage.sentTimestamp, - startMessageID: textMessage.id, - startClosed: true, - direction: MessageQueryDirection.oldToNew, - limit: 10, - ); -} catch (e) { - print(e); -} -``` - - - -### Retrieving Messages Within a Period of Time - -Besides retrieving messages chronologically, you can also retrieve messages within a period of time. For example, if you already have two messages, you can have one of them to be the starting point and another one to be the ending point to retrieve all the messages between them: - -Note: **The limit of 100 messages per query still applies here. To fetch more messages, keep changing the starting point or the ending point until all the messages are retrieved.** - - - -```cs -var earliestMessage = await conversation.QueryMessages(direction: LCIMMessageQueryDirection.OldToNew, limit: 1); -var latestMessage = await conversation.QueryMessages(limit: 1); -var start = new LCIMMessageQueryEndpoint { - MessageId = earliestMessage[0].Id -}; -var end = new LCIMMessageQueryEndpoint { - MessageId = latestMessage[0].Id -}; -// messagesInInterval contains at most 100 messages -var messagesInInterval = await conversation.QueryMessages(start: start, end: end); -``` - -```java -LCIMMessageIntervalBound start = LCIMMessageInterval.createBound(messageId, timestamp, false); -LCIMMessageIntervalBound end = LCIMMessageInterval.createBound(endMessageId, endTimestamp, false); -LCIMMessageInterval interval = new LCIMMessageInterval(start, end); -LCIMMessageQueryDirection direction; -conversation.queryMessages(interval, direction, limit, - new LCIMMessagesQueryCallback(){ - public void done(List messages, LCIMException exception) { - // Handle result - } -}); -``` - -```objc -LCIMMessageIntervalBound *start = [[LCIMMessageIntervalBound alloc] initWithMessageId:nil timestamp:startTimestamp closed:false]; -LCIMMessageIntervalBound *end = [[LCIMMessageIntervalBound alloc] initWithMessageId:nil timestamp:endTimestamp closed:false]; -LCIMMessageInterval *interval = [[LCIMMessageInterval alloc] initWithStartIntervalBound:start endIntervalBound:end]; -[conversation queryMessagesInInterval:interval direction:direction limit:100 callback:^(NSArray * _Nullable messages, NSError * _Nullable error) { - if (messages.count) { - // Handle result - } -}]; -``` - -```js -conversation.queryMessages({ - startTime: timestamp, - startMessageId: messageId, - endTime: endTimestamp, - endMessageId: endMessageId, -}).then(function(messages) { - // Handle result -}.catch(function(error) { - // Handle error -}); -``` - -```swift -do { - let start = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID_1", - sentTimestamp: 31415926, - isClosed: true - ) - let end = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID_2", - sentTimestamp: 31415900, - isClosed: true - ) - try conversation.queryMessage(start: start, end: end, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - List messages = await conversation.queryMessage( - startTimestamp: textMessage.sentTimestamp, - startMessageID: textMessage.id, - startClosed: true, - endTimestamp: fileMessage.sentTimestamp, - endMessageID: fileMessage.id, - endClosed: true, - ); -} catch (e) { - print(e); -} -``` - - - -### Caching Messages - -iOS and Android SDKs come with a mechanism that automatically caches all the messages received and retrieved on the local device. It provides the following benefits: - -1. Message histories can be viewed even devices are offline -2. The frequency of querying and the consumption of data can be minimized -3. The speed for viewing messages can be increased - -Caching is enabled by default. You can turn it off with the following interface: - - - -```cs -// Not supported yet -``` - -```java -// Need to be set before calling LCIMClient.open(callback) -LCIMOptions.getGlobalOptions().setMessageQueryCacheEnabled(false); -``` - -```objc -// Need to be set before calling [avimClient openWithCallback:callback] -avimClient.messageQueryCacheEnabled = false; -``` - -```js -// Not supported yet -``` - -```swift -// Switch for Local Storage of IM Client -do { - // Client init with Local Storage feature - let clientWithLocalStorage = try IMClient(ID: "CLIENT_ID") - - // Client init without Local Storage feature - var options = IMClient.Options.default - options.remove(.usingLocalStorage) - let clientWithoutLocalStorage = try IMClient(ID: "CLIENT_ID", options: options) -} catch { - print(error) -} - -// Message Query Policy -enum MessageQueryPolicy { - case `default` - case onlyNetwork - case onlyCache - case cacheThenNetwork -} - -do { - try conversation.queryMessage(policy: .default, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -// Not supported yet -``` - - - -## Logging out and Network Changes - -### Logging out - -If your app allows users to log out, you can use the `close` method provided by `LCIMClient` to properly close the connection to the cloud: - - - -```cs -await tom.Close(); -``` - -```java -tom.close(new LCIMClientCallback(){ - @Override - public void done(LCIMClient client,LCIMException e){ - if(e==null){ - // Logged out - } - } -}); -``` - -```objc -[tom closeWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"Logged out."); - } -}]; -``` - -```js -tom - .close() - .then(function () { - console.log("Tom logged out."); - }) - .catch(console.error.bind(console)); -``` - -```swift -tom.close { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await tom.close(); -``` - - - -After the function is called, the connection between the client and the server will be terminated. If you check the status of the corresponding `clientId` on the cloud, it would show as "offline". - -### Network Changes - -The availability of the messaging service is highly dependent on the Internet connection. If the connection is lost, all the operations regarding messages and conversations will fail. At this time, there need to be some indicators on the UI to tell the user about the network status. - -Our SDKs maintain a heartbeat mechanism with the cloud which detects the change of network status and have your app notified if certain events occur. To be specific, if the connection status changes (becomes lost or recovered), the following events will be populated: - - -<> - -The following events will be populated on `LCIMClient`: - -- `OnPaused` occurs when the connection is lost. The messaging service is unavailable at this time. -- `OnResume` occurs when the connection is recovered. The messaging service is available at this time. -- `OnClose` occurs when the connection is closed and there will be no auto reconnection. - - -<> - -The following events will be populated on `LCIMClientEventHandler`: - -- `onConnectionPaused()` occurs when the connection is lost. The messaging service is unavailable at this time. -- `onConnectionResume()` occurs when the connection is recovered. The messaging service is available at this time. -- `onClientOffline()` occurs when single-device sign-on is enabled and the current device is forced to go offline. - - -<> - -The following events will be populated on `LCIMClientDelegate`: - -- `imClientResumed` occurs when the connection is recovered. -- `imClientPaused` occurs when the connection is lost. Possible causes include a network problem occurred and the application goes into the background. -- `imClientResuming` occurs when trying to reconnect. -- `imClientClosed` occurs when the connection is closed and there will be no auto reconnection. Possible causes include there is a single-device login conflict or the client has been kicked off by the server. - -```objc -- (void)imClientResumed:(LCIMClient *)imClient -{ - -} - -- (void)imClientResuming:(LCIMClient *)imClient -{ - -} - -- (void)imClientPaused:(LCIMClient *)imClient error:(NSError * _Nullable)error -{ - -} - -- (void)imClientClosed:(LCIMClient *)imClient error:(NSError * _Nullable)error -{ - -} -``` - - -<> - -- `DISCONNECT`: Connection to the server is lost. The messaging service is unavailable at this time. -- `OFFLINE`: Network is unavailable. -- `ONLINE`: Network is recovered. -- `SCHEDULE`: Scheduled to reconnect after a period of time. The messaging service is still unavailable at this time. -- `RETRY`: Reconnecting. -- `RECONNECT`: Connection to the server is recovered. The messaging service is available at this time. - -```js -var { Event } = require("leancloud-realtime"); - -realtime.on(Event.DISCONNECT, function () { - console.log("Connection to the server is lost."); -}); -realtime.on(Event.OFFLINE, function () { - console.log("Network is unavailable."); -}); -realtime.on(Event.ONLINE, function () { - console.log("Network is recovered."); -}); -realtime.on(Event.SCHEDULE, function (attempt, delay) { - console.log( - "Reconnecting in " + delay + " ms as attempt " + (attempt + 1) + "." - ); -}); -realtime.on(Event.RETRY, function (attempt) { - console.log("Reconnecting as attempt " + (attempt + 1) + "."); -}); -realtime.on(Event.RECONNECT, function () { - console.log("Connection to the server is recovered."); -}); -``` - - -<> - -The following events will be populated on the `IMClientDelegate.client(_:event:)` method of `IMClientDelegate`: - -- `sessionDidOpen` occurs when the connection is recovered. -- `sessionDidPause` occurs when the connection is lost. Possible causes include a network problem occurred and the application goes into the background. -- `sessionDidResume` occurs when trying to reconnect. -- `sessionDidClose` occurs when the connection is closed and there will be no auto reconnection. Possible causes include there is a single-device login conflict or the client has been kicked off by the server. - -```swift -func client(_ client: IMClient, event: IMClientEvent) { - switch event { - case .sessionDidOpen: - break - case .sessionDidPause(error: let error): - print(error) - case .sessionDidResume: - break - case .sessionDidClose(error: let error): - print(error) - } -} -``` - - -<> - -The following events will be populated on `Client`: - -- `onOpened` occurs when the connection is established. -- `onClosed` occurs when the connection is closed. -- `onResuming` occurs when trying to reconnect. The messaging service is still unavailable at this time. -- `onDisconnected` occurs when the connection is lost. - - - - - -## More Suggestions - -### Sorting Conversations by Last Activities - -In many scenarios you may need to sort conversations based on the time the last message in each of them is sent. - -There is a `lastMessageAt` property for each `LCIMConversation` (`lm` in the `_Conversation` table) which dynamically changes to reflect the time of the last message. The time is server-based (accurate to a second) so you don't have to worry about the time on the clients. `LCIMConversation` also offers a method for you to retrieve the last message of each conversation, which gives you more flexibility to design the UI of your app. - -### Auto Reconnecting - -If the connection between a client and the cloud is not properly closed, our iOS and Android SDKs will automatically reconnect when the network is recovered. You can listen to `IMClient` to get updated about the network status. - -### More Conversation Types - -Besides the [one-on-one chats](#one-on-one-chats) and [group chats](#group-chats) mentioned earlier, the following types of conversations are also supported: - -- Chat room: This can be used to build conversations that serve scenarios like live streaming. It's different from a basic group chat in the number of members supported and the deliverability promised. See [Chapter 3](/sdk/im/guide/senior/) for more details. - -- Temporary conversation: This can be used to build conversations between users and customer service representatives. It's different from a basic one-on-one chat in the fact that it has a shorter TTL which brings higher flexibility and lower cost (on data storage). See [Chapter 3](/sdk/im/guide/senior/) for more details. - -- System conversation: This can be used to build accounts that could broadcast messages to all their subscribers. It's different from a basic group chat in the fact that users can subscribe to it and there isn't a number limit of members. Subscribers can also send one-on-one messages to these accounts and these messages won't be seen by other users. See [Chapter 4](/sdk/im/guide/systemconv/) for more details. - -## Continue Reading - -- [2. Advanced Messaging Features, Push Notifications, Synchronization, and Multi-Device Sign-on](/sdk/im/guide/intermediate/) -- [3. Security, Chat Rooms, and Temporary Conversations](/sdk/im/guide/senior/) -- [4. Hooks and System Conversations](/sdk/im/guide/systemconv/) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/intermediate.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/intermediate.mdx deleted file mode 100644 index 7a33bdf19..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/intermediate.mdx +++ /dev/null @@ -1,2265 +0,0 @@ ---- -title: 2. Advanced Messaging Features, Push Notifications, Synchronization, and Multi-Device Sign-on -sidebar_label: Advanced Features -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; - -## Introduction - -In the previous chapter, [Basic Conversations and Messages](/sdk/im/guide/beginner/), we introduced how you can create components in your app that support one-on-one chats and group chats, as well as how you can handle events triggered by the cloud. In this chapter, we will show you how to implement advanced features like: - -- Getting receipts when messages are delivered and read -- Mentioning people with "@" -- Recalling and editing messages -- Push notifications and message synchronization -- Single-device or multi-device sign-on -- Sending messages of your custom types - -## Advanced Messaging Features - -If you are building an app for team collaboration or social networking, you may want more features to be included besides basic messaging. For example: - -- To mention someone with "@" so that they can easily find out what messages are important to them. -- To edit or recall a message that has been sent out. -- To send status messages like "Someone is typing". -- To allow the sender of a message to know if it's delivered or read. -- To synchronize messages sent to a receiver that has been offline. - -With Instant Messaging, you can easily implement the functions mentioned above. - -### Mentioning People - -Some group chats have a lot of messages going on and people may easily overlook the information that's important to them. That's why we need a way for senders to get people's attention. - -The most commonly used way to mention someone is to type "@ + name" when composing a message. But if we break it down, we'll notice that the "name" here is something determined by the app (it could be the real name or the nickname of the user) and could be totally different from the `clientId` identifying the user (since one is for people to see and one is for computers to read). A problem could be caused if someone changes their name at the moment another user sends a message with the old name mentioned. Besides this, we also need to consider the way to mention all the members in a conversation. Is it "@all"? "@group"? Or "@everyone"? Maybe all of them will be used, which totally depends on how the UI of the app is designed. - -So we cannot mention people by simply adding "@ + name" into a message. To walk through that, two additional properties are given to each message (`LCIMMessage`): - -- `mentionList`, an array of strings containing the list of `clientId`s being mentioned; -- `mentionAll`, a `Bool` indicating whether all the members are mentioned. - -Depending on the logic of your app, it's possible for you to have both `mentionAll` to be set and `mentionList` to contain a list of members. Your app shall provide the UI that allows users to type in and select the members they want to mention. The only thing you need to do with the SDK is to call the setters of `mentionList` and `mentionAll` to set the members being mentioned. Here is a code example: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("@Tom Come back early.") { - MentionIdList = new string[] { "Tom" } -}; -await conversation.Send(textMessage); -``` - -```java -String content = "@Tom Come back early."; -LCIMTextMessage message = new LCIMTextMessage(); -message.setText(content); -List list = new ArrayList<>(); // A list for holding the members being mentioned; you can add members into the list with the code below -list.add("Tom"); -message.setMentionList(list); -imConversation.sendMessage(message, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` - -```objc -LCIMMessage *message = [LCIMTextMessage messageWithText:@"@Tom Come back early." attributes:nil]; -message.mentionList = @[@"Tom"]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError * _Nullable error) { - /* A message mentioning Tom has been sent */ -}]; -``` - -```js -const message = new TextMessage(`@Tom Come back early.`).setMentionList([ - "Tom", -]); -conversation - .send(message) - .then(function (message) { - console.log("Sent!"); - }) - .catch(console.error); -``` - -```swift -do { - let message = IMTextMessage(text: "@Tom Come back early.") - message.mentionedMembers = ["Tom"] - try conversation.send(message: message, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = '@Tom Come back early.'; - message.mentionMembers = ['Tom']; - await conversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - -You can also mention everyone by setting `mentionAll`: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("@all") { - MentionAll = true -}; -await conv.Send(textMessage); -``` - -```java -String content = "@all"; -LCIMTextMessage message = new LCIMTextMessage(); -message.setText(content); - -boolean mentionAll = true; // Indicates if everyone is mentioned -message.mentionAll(mentionAll); - -imConversation.sendMessage(message, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` - -```objc -LCIMMessage *message = [LCIMTextMessage messageWithText:@"@all" attributes:nil]; -message.mentionAll = YES; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError * _Nullable error) { - /* A message mentioning everyone has been sent */ -}]; -``` - -```js -const message = new TextMessage(`@all`).mentionAll(); -conversation - .send(message) - .then(function (message) { - console.log("Sent!"); - }) - .catch(console.error); -``` - -```swift -do { - let message = IMTextMessage(text: "@all") - message.isAllMembersMentioned = true - try conversation.send(message: message, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = 'content'; - message.mentionAll = true; - await conversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - -The receiver of the message can call the getters of `mentionList` and `mentionAll` to see the members being mentioned: - - - -```cs -jerry.onMessage = (conv, msg) => { - List mentionIds = msg.MentionIdList; -}; -``` - -```java -@Override -public void onMessage(LCIMAudioMessage msg, LCIMConversation conv, LCIMClient client) { - // Get a list of clientIds being mentioned - List currentMsgMentionUserList = message.getMentionList(); -} -``` - -```objc -// The code below shows how you can get a list of clientIds being mentioned in an LCIMTypedMessage; the code can be modified to serve other types inherited from LCIMMessage -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - // Get a list of clientIds being mentioned - NSArray *mentionList = message.mentionList; -} -``` - -```js -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - var mentionList = receivedMessage.getMentionList(); -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - if let mentionedMembers = message.mentionedMembers { - print(mentionedMembers) - } - if let isAllMembersMentioned = message.isAllMembersMentioned { - print(isAllMembersMentioned) - } - default: - break - } - default: - break - } -} -``` - -```dart -jerry.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - List mentionList = message.mentionMembers; -}; -``` - - - -To make it easier to display things on the UI, the following two flags are offered by `LCIMMessage` to indicate the status of mentioning: - -- `mentionedAll`: Whether all the members in the conversation are mentioned. Becomes `true` only when `mentionAll` is true, otherwise it remains `false`. -- `mentioned`: Whether the current user is mentioned. Becomes `true` when `mentionList` contains the `clientId` of the current user or when `mentionAll` is `true`, otherwise it remains `false`. - -Here is a code example: - - - -```cs -client.OnMessage = (conv, msg) => { - bool mentioned = msg.MentionAll || msg.MentionList.Contains("Tom"); -}; -``` - -```java -@Override -public void onMessage(LCIMAudioMessage msg, LCIMConversation conv, LCIMClient client) { - // Check if all the members in the conversation are mentioned - boolean currentMsgMentionAllUsers = message.isMentionAll(); - // Check if the current user is mentioned - boolean currentMsgMentionedMe = message.mentioned(); -} -``` - -```objc -// The code below shows how you can check if all the members in the conversation or the current user is mentioned in an LCIMTypedMessage; the code can be modified to serve other types inherited from LCIMMessage -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - // Check if all the members in the conversation are mentioned - BOOL mentionAll = message.mentionAll; - // Check if the current user is mentioned - BOOL mentionedMe = message.mentioned; -} -``` - -```js -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - var mentionedAll = receivedMessage.mentionedAll; - var mentionedMe = receivedMessage.mentioned; -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - print(message.isCurrentClientMentioned) - default: - break - } - default: - break - } -} -``` - -```dart -// Not supported yet -``` - - - -### Modify a Message - -To allow users to edit the messages they sent, you need to enable **Allow editing messages with SDK** on **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Instant Messaging settings**. There are no limits on the time within which users can perform this operation. However, users are only allowed to edit the messages they sent, not the ones others sent. - -To modify a message, what you would do is not update the original message instance, but create a new one and call `Conversation#updateMessage(oldMessage, newMessage)` to submit the request to the cloud. Here is a code example: - - - -```cs -LCIMTextMessage newMessage = new LCIMTextMessage("The new message."); -await conversation.UpdateMessage(oldMessage, newMessage); -``` - -```java -LCIMTextMessage textMessage = new LCIMTextMessage(); -textMessage.setContent("The new message."); -imConversation.updateMessage(oldMessage, textMessage, new LCIMMessageUpdatedCallback() { - @Override - public void done(LCIMMessage avimMessage, LCException e) { - if (null == e) { - // The message is updated; avimMessage is the updated message - } - } -}); -``` - -```objc -LCIMMessage *oldMessage = <#MessageYouWantToUpdate#>; -LCIMMessage *newMessage = [LCIMTextMessage messageWithText:@"Just a new message" attributes:nil]; - -[conversation updateMessage:oldMessage - toNewMessage:newMessage - callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"The message is updated."); - } -}]; -``` - -```js -var newMessage = new TextMessage("new message"); -conversation - .update(oldMessage, newMessage) - .then(function () { - // The message is updated - }) - .catch(function (error) { - // Handle error - }); -``` - -```swift -do { - let newMessage = IMTextMessage(text: "Just a new message") - try conversation.update(oldMessage: oldMessage, to: newMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - Message updatedMessage = await conversation.updateMessage( - oldMessage: oldMessage, - newMessage: newMessage, - ); -} catch (e) { - print(e); -} -``` - - - -If the modification succeeded, other members in the conversation will receive a `MESSAGE_UPDATE` event: - - - -```cs -tom.OnMessageUpdated = (conv, msg) => { - if (msg is LCIMTextMessage textMessage) { - WriteLine($"Content: {textMessage.Text}; Message ID: {textMessage.Id}"); - } -}; -``` - -```java -void onMessageUpdated(LCIMClient client, LCIMConversation conversation, LCIMMessage message) { - // message is the updated message -} -``` - -```objc -/* A delegate method for handling events of editing messages */ -- (void)conversation:(LCIMConversation *)conversation messageHasBeenUpdated:(LCIMMessage *)message reason:(LCIMMessagePatchedReason * _Nullable)reason { - /* A message is updated */ -} -``` - -```js -var { Event } = require("leancloud-realtime"); -conversation.on(Event.MESSAGE_UPDATE, function (newMessage, reason) { - // newMessage is the updated message - // Look for the original message with its ID and replace it with newMessage - // reason (optional) is the reason the message is edited - // If reason is not specified, it means the sender edited the message - // If the code of reason is positive, it means a hook on Cloud Engine caused the edit - // (the code value can be specified when defining hooks) - // If the code of reason is negative, it means a built-in mechanism of the system caused the edit - // For example, -4408 means the message is edited due to text moderation - // The detail of reason is a string containing the explanation -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .updated(updatedMessage: updatedMessage, reason: _): - print(updatedMessage) - default: - break - } - default: - break - } -} -``` - -```dart -tom.onMessageUpdated = ({ - Client client, - Conversation conversation, - Message updatedMessage, - int patchCode, - String patchReason, -}) { - // updatedMessage is the updated message -}; -``` - - - -For Android and iOS SDKs, if caching is enabled (it is enabled by default), the SDKs will first update the modified message in the cache and then trigger an event to the app. When you receive such an event, simply refresh the chatting page to reflect the latest collection of messages. - -If a message is modified by the system (for example, due to text moderation or by a hook on Cloud Engine), the sender will receive a `MESSAGE_UPDATE` event, and other members in the conversation will receive the modified message. - -### Recall a Message - -Besides modifying a sent message, a user can also recall a message they sent. -Similarly, you need to enable **Allow recalling messages with SDK** on **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Instant Messaging settings**. -Also, there are no limits on the time within which users can perform this operation, and users are only allowed to recall the messages they sent, not the ones others sent. - -To recall a message, invoke the `Conversation#recallMessage` method: - - - -```cs -await conversation.RecallMessage(message); -``` - -```java -conversation.recallMessage(message, new LCIMMessageRecalledCallback() { - @Override - public void done(LCIMRecalledMessage recalledMessage, LCException e) { - if (null == e) { - // The message is recalled; the UI may be updated now - } - } -}); -``` - -```objc -LCIMMessage *oldMessage = <#MessageYouWantToRecall#>; - -[conversation recallMessage:oldMessage callback:^(BOOL succeeded, NSError * _Nullable error, LCIMRecalledMessage * _Nullable recalledMessage) { - if (succeeded) { - NSLog(@"The message is recalled."); - } -}]; -``` - -```js -conversation - .recall(oldMessage) - .then(function (recalledMessage) { - // The message is recalled - // recalledMessage is a RecalledMessage - }) - .catch(function (error) { - // Handle error - }); -``` - -```swift -do { - try conversation.recall(message: oldMessage, completion: { (result) in - switch result { - case .success(value: let recalledMessage): - print(recalledMessage) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - RecalledMessage recalledMessage = await conversation.recallMessage( - message: oldMessage, - ); -} catch (e) { - print(e); -} -``` - - - -Once a message is recalled, other members in the conversation will receive the `MESSAGE_RECALL` event: - - - -```cs -tom.OnMessageRecalled = (conv, recalledMsg) => { - // recalledMsg is the message being recalled -}; -``` - -```java -void onMessageRecalled(LCIMClient client, LCIMConversation conversation, LCIMMessage message) { - // message is the message being recalled -} -``` - -```objc -/* A delegate method for handling events of recalling messages */ -- (void)conversation:(LCIMConversation *)conversation messageHasBeenRecalled:(LCIMRecalledMessage *)message reason:(LCIMMessagePatchedReason * _Nullable)reason { - /* A message is recalled */ -} -``` - -```js -var { Event } = require("leancloud-realtime"); -conversation.on(Event.MESSAGE_RECALL, function (recalledMessage, reason) { - // recalledMessage is the message being recalled - // Look for the original message with its ID and replace it with recalledMessage - // reason (optional) is the reason the message is recalled; see the part for editing messages for more details -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .updated(updatedMessage: updatedMessage, reason: _): - if let recalledMessage = updatedMessage as? IMRecalledMessage { - print(recalledMessage) - } - default: - break - } - default: - break - } -} -``` - -```dart -tom.onMessageRecalled = ({ - Client client, - Conversation conversation, - RecalledMessage recalledMessage, -}) { - // recalledMessage is the message being recalled -}; -``` - - - -For Android and iOS SDKs, if caching is enabled (it is enabled by default), the SDKs will first delete the recalled message from the cache and then trigger an event to the app. This ensures the consistency of data internally. When you receive such an event, simply refresh the chatting page to reflect the latest collection of messages. Depending on your implementation, either the recalled message will simply disappear, or an indicator saying the message has been recalled will take the original message's place. - -### Transient Messages - -Sometimes we need to send status updates like "Someone is typing…" or "Someone changed the group name to XX". Different from messages sent by users, these messages don't need to be stored in the history, nor do they need to be guaranteed to be delivered (if members are offline or there is a network error, it would be okay if these messages are not delivered). Such messages are best sent as transient messages. - -Transient message is a special type of message. It has the following differences compared to a basic message: - -- It won't be stored in the cloud so it couldn't be retrieved from history messages. -- It's only delivered to those who are online. Offline members cannot receive it later or get push notifications about it. -- It's not guaranteed to be delivered. If there's a network error preventing the message from being delivered, the server won't make a second attempt. - -Therefore, transient messages are best for communicating real-time updates of statuses that are changing frequently or implementing simple control protocols. - -The way to construct a transient message is the same as that for a basic message. The only difference is the way it is being sent. So far we have shown the following way of sending messages with `LCIMConversation`: - - - -```cs -public async Task Send(LCIMMessage message, LCIMMessageSendOptions options = null); -``` - -```java -/** - * Send a message - */ -public void sendMessage(LCIMMessage message, final LCIMConversationCallback callback) -``` - -```objc -/*! - Send a message - */ -- (void)sendMessage:(LCIMMessage *)message - callback:(void (^)(BOOL succeeded, NSError * _Nullable error))callback; -``` - -```js -/** - * Send a message - * @param {Message} message The message itself; an instance of Message or its subtype - * @return {Promise.} The message being sent - */ -async send(message) -``` - -```swift -/// Send Message. -/// -/// - Parameters: -/// - message: The message to be sent. -/// - options: @see `MessageSendOptions`. -/// - priority: @see `IMChatRoom.MessagePriority`. -/// - pushData: The push data of APNs. -/// - progress: The file uploading progress. -/// - completion: callback. -public func send(message: IMMessage, options: MessageSendOptions = .default, priority: IMChatRoom.MessagePriority? = nil, pushData: [String : Any]? = nil, progress: ((Double) -> Void)? = nil, completion: @escaping (LCBooleanResult) -> Void) throws -``` - -```dart -Future send({ - @required Message message, -}) async {} -``` - - - -In fact, an additional parameter `LCIMMessageOption` can be provided when sending a message. Here is a complete list of interfaces offered by `LCIMConversation`: - - - -```cs -/// -/// Sends a message in this conversation. -/// -/// The message to send. -/// -public async Task Send(LCIMMessage message, LCIMMessageSendOptions options = null); -``` - -```java -/** - * Send a message - * @param message - * @param messageOption - * @param callback - */ -public void sendMessage(final LCIMMessage message, final LCIMMessageOption messageOption, final LCIMConversationCallback callback); -``` - -```objc -/*! - Send a message - @param message - The message itself - @param option - Options - @param callback - Callback - */ -- (void)sendMessage:(LCIMMessage *)message - option:(nullable LCIMMessageOption *)option - callback:(void (^)(BOOL succeeded, NSError * _Nullable error))callback; -``` - -```js -/** - * Send a message - * @param {Message} message The message itself; an instance of Message or its subtype - * @param {Object} [options] since v3.3.0; Options - * @param {Boolean} [options.transient] since v3.3.1; Whether it is a transient message - * @param {Boolean} [options.receipt] Whether receipts are needed - * @param {Boolean} [options.will] since v3.4.0; Whether it is a will message - * A will message will be sent when the user loses connection - * @param {MessagePriority} [options.priority] The priority of the message; for chat rooms only - * see: {@link module:leancloud-realtime.MessagePriority MessagePriority} - * @param {Object} [options.pushData] The content for push notification; if the receiver is offline, a push notification with this content will be triggered - * @return {Promise.} The message being sent - */ -async send(message, options) -``` - -```swift -/// Message Sending Option -public struct MessageSendOptions: OptionSet { - /// Get Receipt when other client received message or read message. - public static let needReceipt = MessageSendOptions(rawValue: 1 << 0) - - /// Indicates whether this message is transient. - public static let isTransient = MessageSendOptions(rawValue: 1 << 1) - - /// Indicates whether this message will be auto delivering to other client when this client disconnected. - public static let isAutoDeliveringWhenOffline = MessageSendOptions(rawValue: 1 << 2) -} - -/// Send Message. -/// -/// - Parameters: -/// - message: The message to be sent. -/// - options: @see `MessageSendOptions`. -/// - priority: @see `IMChatRoom.MessagePriority`. -/// - pushData: The push data of APNs. -/// - progress: The file uploading progress. -/// - completion: callback. -public func send(message: IMMessage, options: MessageSendOptions = .default, priority: IMChatRoom.MessagePriority? = nil, pushData: [String : Any]? = nil, progress: ((Double) -> Void)? = nil, completion: @escaping (LCBooleanResult) -> Void) throws -``` - -```dart -Future send({ - @required Message message, - bool transient, - bool receipt, - bool will, - MessagePriority priority, - Map pushData, -}) async {} -``` - - - -With `LCIMMessageOption`, we can specify: - -- Whether it is a transient message (field `transient`). -- Whether receipts are needed (field `receipt`; more details will be covered later). -- The priority of the message (field `priority`; more details will be covered later). -- Whether it is a will message (field `will`; more details will be covered later). -- The content for push notification (field `pushData`; more details will be covered later); if the receiver is offline, a push notification with this content will be triggered. - -The code below sends a transient message saying "Tom is typing…" to the conversation when Tom's input box gets focused: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("Tom is typing…"); -LCIMMessageSendOptions option = new LCIMMessageSendOptions() { - Transient = true -}; -await conversation.Send(textMessage, option); -``` - -```java -String content = "Tom is typing…"; -LCIMTextMessage message = new LCIMTextMessage(); -message.setText(content); - -LCIMMessageOption option = new LCIMMessageOption(); -option.setTransient(true); - -imConversation.sendMessage(message, option, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` - -```objc -LCIMMessage *message = [LCIMTextMessage messageWithText:@"Tom is typing…" attributes:nil]; -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.transient = true; -[conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - /* A transient message is sent */ -}]; -``` - -```js -const message = new TextMessage("Tom is typing…"); -conversation.send(message, { transient: true }); -``` - -```swift -do { - let message = IMTextMessage(text: "Tom is typing…") - try conversation.send(message: message, options: [.isTransient], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = 'Tom is typing…'; -// Send a transient message - await conversation.send(message: message, transient: true); -} catch (e) { - print(e); -} -``` - - - -The procedure for receiving transient messages is the same as that for basic messages. You can run different logic based on the types of messages. The example above sets the type of the message to be text message, but it would be better if you assign a distinct type to it. Our SDK doesn't offer a type for transient messages, so you may build your own depending on what you need. See [Custom Message Types](#custom-message-types) for more details. - -### Receipts - -When the cloud is delivering messages, it follows the sequence the messages are pushed to the cloud and delivers the former messages before the latter ones (FIFO). Our internal protocol also requires that the SDK sends an acknowledgment (ack) back to the cloud for every single message received by it. If a message is received by the SDK but the ack is not received by the cloud due to a packet loss, the cloud would assume that the message is not successfully delivered and will keep redelivering it until an ack is received. Correspondingly, the SDK also does its work to make duplicate messages insensible by the app. The entire mechanism ensures that no messages will be lost in the entire delivery process. - -However, in certain scenarios, functionality beyond the one mentioned above is demanded. For example, a sender may want to know when the receiver got the message and when they opened the message. In a product for team collaboration or private communication, a sender may even want to monitor the real-time status of every message sent out by them. Such requirements can be satisfied with the help of receipts. - -Similar to the way of sending transient messages, if you want receipts to be given back, you need to specify an option in `LCIMMessageOption`: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("A very important message."); -LCIMMessageSendOptions option = new LCIMMessageSendOptions { - Receipt = true -}; -await conversation.Send(textMessage, option); -``` - -```java -LCIMMessageOption messageOption = new LCIMMessageOption(); -messageOption.setReceipt(true); -imConversation.sendMessage(message, messageOption, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` - -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.receipt = true; -[conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Message sent with receipts requested."); - } -}]; -``` - -```js -var message = new TextMessage("A very important message."); -conversation.send(message, { - receipt: true, -}); -``` - -```swift -do { - let message = IMTextMessage(text: "A very important message.") - try conversation.send(message: message, options: [.needReceipt], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = 'A very important message.'; - await conversation.send(message: message, receipt: true); -} catch (e) { - print(e); -} -``` - - - -> Note: -> -> Receipts are not enabled by default. You need to manually turn that on when sending each message. Receipts are only available for conversations with no more than 2 members. - -So how do senders handle the receipts they get? - -#### Delivery Receipts - -When a message is delivered to the receiver, the cloud will give a delivery receipt to the sender. **Keep in mind that this is not the same as a read receipt.** - - - -```cs -// Tom creates an LCIMClient with his name as clientId -LCIMClient client = new LCIMClient("Tom"); -// Tom logs in -await client.Open(); - -// Enable delivery receipt -client.OnMessageDelivered = (conv, msgId) => { - // Things to do after messages are delivered -}; -// Send message -LCIMTextMessage textMessage = new LCIMTextMessage("Wanna go to the bakery tonight?"); -await conversation.Send(textMessage); -``` - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * Handle notifications for messages being delivered - */ - public void onLastDeliveredAtUpdated(LCIMClient client, LCIMConversation conversation) { - ; - } -} - -// Set up global event handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); -``` - -```objc -// Implement `conversation:messageDelivered` to listen if there are messages delivered -- (void)conversation:(LCIMConversation *)conversation messageDelivered:(LCIMMessage *)message { - NSLog(@"%@", @"Message delivered."); // Print log -} -``` - -```js -var { Event } = require("leancloud-realtime"); -conversation.on(Event.LAST_DELIVERED_AT_UPDATE, function () { - console.log(conversation.lastDeliveredAt); - // Update the UI to mark all the messages before lastDeliveredAt to be "delivered" -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .delivered(toClientID: toClientID, messageID: messageID, deliveredTimestamp: deliveredTimestamp): - if messageID == message.ID { - message.deliveredTimestamp = deliveredTimestamp - } - default: - break - } - default: - break - } -} -``` - -```dart -tom.onMessageDelivered = ({ - Client client, - Conversation conversation, - String messageID, - String toClientID, - DateTime atDate, -}) { - // Things to do after the message is delivered -}; -``` - - - -The content included in the receipt will not be a specific message. Instead, it will be the time the messages in the current conversation are last delivered (`lastDeliveredAt`). We have mentioned earlier that messages are delivered according to the sequence they are pushed to the cloud. Therefore, given the time of the last delivery, we can infer that all the messages sent before it are delivered. On the UI of the app, you can mark all the messages sent before `lastDeliveredAt` to be "delivered". - -#### Read Receipts - -When we say a message is delivered, what we mean is that the message is received by the client from the cloud. At this time, the actual user might not have the conversation page or even the app open (Android apps can receive messages in the background). So we cannot assume that a message is read just because it is delivered. - -Therefore, we offer another kind of receipt showing if a receiver has actually seen a message. - -Again, since messages are delivered in time order, we don't have to check if every single message is being read. Think about a scenario like this: - -![A pop-up with the title "Welcome Back" says "You have 5002 unread messages. Would you like to skip them all? (Select 'Yes' to mark them as read)". There are two buttons on the bottom: "Yes" and "No".](/img/io/realtime_read_confirm.png) - -When a user opens a conversation, we can say that the user has read all the messages in it. You can use the following interface of `Conversation` to mark all the messages in it as read: - - - -```cs -/// -/// Mark the last message of this conversation as read. -/// -/// -public Task Read(); -``` - -```java -/** - * Mark as read - */ -public void read(); -``` - -```objc -/*! - Mark as read - This method marks the latest message sent into a conversation by the other member as read. The sender of the message will get a read receipt. - */ -- (void)readInBackground; -``` - -```js -/** - * Mark as read - * @return {Promise.} self - */ -async read(); -``` - -```swift -/// Clear unread messages that its sent timestamp less than the sent timestamp of the parameter message. -/// -/// - Parameter message: The default is the last message. -public func read(message: IMMessage? = nil) -``` - -```dart -await conversation.read(); -``` - - - -After the receiver has read the latest messages, the sender will get a receipt indicating that the messages they have sent out are read. - -So if Tom is chatting with Jerry and wants to know if Jerry has read the messages, the following procedure would apply: - -1. Tom sends a message to Jerry and requests receipts on it: - - - - ```cs - LCIMTextMessage textMessage = new LCIMTextMessage("A very important message."); - LCIMMessageSendOptions options = new LCIMMessageSendOptions { - Receipt = true - }; - await conversation.Send(textMessage); - ``` - - ```java - LCIMClient tom = LCIMClient.getInstance("Tom"); - LCIMConversation conv = client.getConversation("551260efe4b01608686c3e0f"); - - LCIMTextMessage textMessage = new LCIMTextMessage(); - textMessage.setText("Hello, Jerry!"); - - LCIMMessageOption option = new LCIMMessageOption(); - option.setReceipt(true); /* Request receipts */ - - conv.sendMessage(textMessage, option, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - /* Sent */ - } - } - }); - ``` - - ```objc - LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; - option.receipt = YES; /* Request receipts */ - - LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"Hello, Jerry!" attributes:nil]; - - [conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (!error) { - /* Sent */ - } - }]; - ``` - - ```js - var message = new TextMessage("A very important message."); - conversation.send(message, { - receipt: true, - }); - ``` - - ```swift - do { - let message = IMTextMessage(text: "Hello, Jerry!") - try conversation.send(message: message, options: [.needReceipt], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } catch { - print(error) - } - ``` - - ```dart - try { - TextMessage message = TextMessage(); - message.text = 'A very important message.'; - await conversation.send(message: message, receipt: true); - } catch (e) { - print(e); - } - ``` - - - -2. Jerry reads Tom's message and calls `read` on the conversation to mark the latest messages as read: - - - - ```cs - await conversation.Read(); - ``` - - ```java - conversation.read(); - ``` - - ```objc - [conversation readInBackground]; - ``` - - ```js - conversation - .read() - .then(function (conversation) {}) - .catch(console.error.bind(console)); - ``` - - ```swift - conversation.read() - ``` - - ```dart - await conversation.read(); - ``` - - - -3. Tom gets a read receipt with the conversation's `lastReadAt` updated. The UI can be updated to mark all messages sent before `lastReadAt` to be read: - - - - ```cs - tom.OnLastReadAtUpdated = (conv) => { - // The message is read by Jerry; the time Jerry read messages for the last time can be retrieved by calling conversation.LastReadAt - }; - ``` - - ```java - public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * Handle notifications for messages being read - */ - public void onLastReadAtUpdated(LCIMClient client, LCIMConversation conversation) { - /* The message is read by Jerry; the time Jerry read messages for the last time can be retrieved by calling conversation.getLastReadAt() */ - } - } - - // Set up the global event handler - LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); - ``` - - ```objc - // Tom can get the update of lastReadAt within the delegate method of client - - (void)conversation:(LCIMConversation *)conversation didUpdateForKey:(LCIMConversationUpdatedKey)key { - if ([key isEqualToString:LCIMConversationUpdatedKeyLastReadAt]) { - NSDate *lastReadAt = conversation.lastReadAt; - /* The message is read by Jerry; lastReadAt can be used to update UI; for example, to mark all the messages before lastReadAt to be "read" */ - } - } - ``` - - ```js - var { Event } = require("leancloud-realtime"); - conversation.on(Event.LAST_READ_AT_UPDATE, function () { - console.log(conversation.lastReadAt); - // Update the UI to mark all the messages before lastReadAt to be "read" - }); - ``` - - ```swift - func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .read(byClientID: byClientID, messageID: messageID, readTimestamp: readTimestamp): - if messageID == message.ID { - message.readTimestamp = readTimestamp - } - default: - break - } - default: - break - } - } - ``` - - ```dart - jerry.onLastReadAtUpdated = ({ - Client client, - Conversation conversation, - }) { - // Update the UI to mark all the messages before lastReadAt to be "read" - }; - ``` - - - -Note: - -To use read receipts, turn on [notifications on updates of unread message count](#notifications-on-updates-of-unread-message-count) when initializing your app. - -### Muting Conversations - -If a user doesn't want to receive notifications from a conversation but still wants to stay in it, they can mute the conversation. See [Muting Conversations](/sdk/im/guide/senior/) in the next chapter for more details. - -### Will Messages - -Will message can be used to automatically notify other members in a conversation when a user goes offline unexpectedly. It gets its name from the wills filed by testators, giving people a feeling that the last messages of a person should always be heard. It looks like the message saying "Tom is offline and cannot receive messages" in this image: - -![In a conversation named "Tom & Jerry", Jerry receives a will message saying "Tom is offline and cannot receive messages". The will message looks like a system notification and shares a different style with other messages.](/img/io/lastwill-message.png) - -A will message needs to be composed ahead of time and cached on the cloud. The cloud doesn't send it out immediately after receiving it. Instead, it waits until the sender of it goes offline unexpectedly. You can implement your own logic to handle such an event. - - - -```cs -LCIMTextMessage message = new LCIMTextMessage("I am a will message. I will be sent out to other members in the conversation when the sender goes offline unexpectedly."); -LCIMMessageSendOptions options = new LCIMMessageSendOptions { - Will = true -}; -await conversation.Send(message, options); -``` - -```java -LCIMTextMessage message = new LCIMTextMessage(); -message.setText("I am a will message. I will be sent out to other members in the conversation when the sender goes offline unexpectedly."); - -LCIMMessageOption option = new LCIMMessageOption(); -option.setWill(true); - -conversation.sendMessage(message, option, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // Sent - } - } -}); -``` - -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.will = YES; - -LCIMMessage *willMessage = [LCIMTextMessage messageWithText:@"I am a will message. I will be sent out to other members in the conversation when the sender goes offline unexpectedly." attributes:nil]; - -[conversation sendMessage:willMessage option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"Sent!"); - } -}]; -``` - -```js -var message = new TextMessage( - "I am a will message. I will be sent out to other members in the conversation when the sender goes offline unexpectedly." -); -conversation - .send(message, { will: true }) - .then(function () { - // Sent; other members will see the message once the current client goes offline unexpectedly - }) - .catch(function (error) { - // Handle error - }); -``` - -```swift -do { - let message = IMTextMessage(text: "I am a will message. I will be sent out to other members in the conversation when the sender goes offline unexpectedly.") - try conversation.send(message: message, options: [.isAutoDeliveringWhenOffline], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = 'I am a will message. I will be sent out to other members in the conversation when the sender goes offline unexpectedly.'; - await conversation.send(message: message, will: true); -} catch (e) { - print(e); -} -``` - - - -Once the sender goes offline unexpectedly, other members will immediately receive the will message. You can design your own way to display it on the UI. - -Will message has the **following restrictions**: - -- Each user can only have one will message set up at a time. This means that if a user sets will messages for multiple conversations or multiple will messages for the same conversation, only the last one will take effect. -- Will messages don't get stored in the history. -- If a user logs out proactively, the will message set by this user will not be sent out (if there is one). - -### Text Moderation - - - -If your app allows users to create group chats, you might consider filtering cuss words from the messages sent by users. Instant Messaging offers a built-in component that helps you easily implement this function. See [Text Moderation](/sdk/im/guide/senior/) in the next chapter for more details. - - - - - -See [Text Moderation](/sdk/im/guide/senior/) in the next chapter for more details. - - - -### Handling Undelivered Messages - -Sometimes you may need to store the messages that are not successfully sent out into a local cache and handle them later. For example, if a client's connection to the server is lost and a message cannot be sent out due to this, you may still keep the message locally. Perhaps you can add an error icon and a button for retrying next to the message displayed on the UI. The user may tap on the button when the connection is recovered to make another attempt to send the message. - -By default, both Android and iOS SDKs enable a local cache for storing messages. The cache stores all the messages that are already sent to the cloud and keeps itself updated with the data in the cloud. To make things easier, undelivered messages can also be stored in the same cache. - -The code below adds a message to the cache: - - - -```cs -// Not supported yet -``` - -```java -conversation.addToLocalCache(message); -``` - -```objc -[conversation addMessageToCache:message]; -``` - -```js -// Not supported yet -``` - -```swift -do { - try conversation.insertFailedMessageToCache(failedMessage) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// Not supported yet -``` - - - -The code below removes a message from the cache: - - - -```cs -// Not supported yet -``` - -```java -conversation.removeFromLocalCache(message); -``` - -```objc -[conversation removeMessageFromCache:message]; -``` - -```js -// Not supported yet -``` - -```swift -do { - try conversation.removeFailedMessageFromCache(failedMessage) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// Not supported yet -``` - - - -When reading messages from the cache, you can make messages look different on the UI based on the property `message.status`. If the `status` of a message is `LCIMMessageStatusFailed`, it means the message cannot be sent out, so you can add a button for retrying on the UI. An additional benefit of using the local cache is that the SDK will make sure the same message only gets sent out once. This ensures that there won't be any duplicate messages on the cloud. - -## Push Notifications - -If your users are using your app on mobile devices, they might close the app at any time, which prevents you from delivering new messages to them in the ordinary way. At this time, using push notifications becomes a good alternative to get users notified when new messages are coming in. - -If you are building an iOS or Android app, you can utilize the built-in push notification services offered by these operating systems, as long as you have your certificates configured for iOS or have the function enabled for Android. Check the following pages for more details: - -1. [Instant Messaging Overview](/sdk/im/guide/overview/) -2. [Android Push Notification Guide](/sdk/push/guide/android-mixpush/) / [iOS Push Notification Guide](/sdk/push/guide/ios/) - -The cloud will associate the `clientId`s of users with the data in the `_Installation` table that keeps track of the devices. When a user sends a message to a conversation, the cloud will automatically convert the message to a push notification and send it to those who are offline but are using iOS devices or using Android devices with push notification services enabled. We also allow you to connect third-party push notification services to your app. - -The highlight of this feature is that you can **customize the contents of push notifications**. You have the following three ways to specify the contents: - -1. Setting up a static message - - You can fill in a global static JSON string on **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Push notifications** for delivering push notifications with a static message. For example, if you put: - - ```json - { "alert": "New message received", "badge": "Increment" } - ``` - - Then whenever there is a new message going to an offline user, the user will receive a push notification saying "New message received". - - Keep in mind that `badge` is for iOS devices only which means to increase the number displayed on the badge of the app. Its value, `Increment`, is case-sensitive. - Typically, when an end user opens or closes the application, you need to set the value of the badge field of the `_Installation` class to zero, which clears the badge number. - - Besides, you can also customize the sounds of push notifications for iOS devices. - -2. Specifying contents when sending messages from a client - - When using the first way introduced above, the content included in each push notification is the same regardless of the message being sent out. Is it possible to dynamically generate these contents to make them relevant to the actual messages? - - Remember how we specified `LCIMMessageOption` when sending transient messages? The same parameter takes in a `pushData` property which allows you to specify the contents of push notifications. Here is a code example: - - - -```cs -LCIMTextMessage message = new LCIMTextMessage("Hey Jerry, me and Kate are gonna watch a game at a bar tonight. Wanna come with us?"); -LCIMMessageSendOptions sendOptions = new LCIMMessageSendOptions { - PushData = new Dictionary { - { "alert", "New message received"}, - { "category", "Message"}, - { "badge", 1}, - { "sound", "message.mp3"}, // The name of the file for the sound; has to be present in the app - { "custom-key", "This is a custom attribute with custom-key being the name of the key. You can use your own names for keys."} - } -}; -``` - -```java -LCIMTextMessage msg = new LCIMTextMessage(); -msg.setText("Hey Jerry, me and Kate are gonna watch a game at a bar tonight. Wanna come with us?"); - -LCIMMessageOption messageOption = new LCIMMessageOption(); -String pushMessage = "{\"alert\":\"New message received\", \"category\":\"Message\"," - + "\"badge\":1,\"sound\":\"message.mp3\"," - + "\"custom-key\":\"This is a custom attribute with custom-key being the name of the key. You can use your own names for keys.\"}"; -messageOption.setPushData(pushMessage); -conv.sendMessage(msg, messageOption, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // Sent - } - } -}); -``` - -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.pushData = @{@"alert" : @"New message received", @"sound" : @"message.mp3", @"badge" : @1, @"custom-key" : @"This is a custom attribute with custom-key being the name of the key. You can use your own names for keys."}; -[conversation sendMessage:[LCIMTextMessage messageWithText:@"Hey Jerry, me and Kate are gonna watch a game at a bar tonight. Wanna come with us?" attributes:nil] option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - // Handle result and error -}]; -``` - -```js -const message = new TextMessage('Hey Jerry, me and Kate are gonna watch a game at a bar tonight. Wanna come with us?'); -conversation.send(message), { - pushData: { - "alert": "New message received", - "category": "Message", - "badge": 1, - "sound": "message.mp3", // The name of the file for the sound; has to be present in the app - "custom-key": "This is a custom attribute with custom-key being the name of the key. You can use your own names for keys." - } -}); -``` - -```swift -do { - let message = IMTextMessage(text: "Hey Jerry, me and Kate are gonna watch a game at a bar tonight. Wanna come with us?") - let pushData: [String: Any] = [ - "alert": "New message received", - "category": "Message", - "badge": 1, - "sound": "message.mp3", - "custom-key": "This is a custom attribute with custom-key being the name of the key. You can use your own names for keys." - ] - try conversation.send(message: message, pushData: pushData, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = 'Hey Jerry, me and Kate are gonna watch a game at a bar tonight. Wanna come with us?'; - await conversation.send(message: message, pushData: { - "alert": "New message received", - "category": "Message", - "badge": 1, - "sound": "message.mp3", // The name of the file for the sound; has to be present in the app - "custom-key": "This is a custom attribute with custom-key being the name of the key. You can use your own names for keys." - }); -} catch (e) { - print(e); -} -``` - - - -3. Generating contents dynamically on the server side - - The second way introduced above allows you to compose the contents of push notifications based on the messages being sent, but the logic needs to be predefined on the client side, which makes things less flexible. - - So we offer a third way that allows you to define the logic of push notifications on the server side with the help of hooks. Check [this page](/sdk/im/guide/systemconv/) for more details. - -Here is a comparison of the priorities of the three methods mentioned above: **Generating contents dynamically on the server side > Specifying contents when sending messages from a client > Setting up a static message**. - -If more than one of these methods are implemented at the same time, the push notifications generated on the server side will always get the highest priority. Those sent from clients will get a lower priority, and the static message set up on the dashboard will get the lowest priority. - -### Implementations and Restrictions - -If your app is using push notification services together with Instant Messaging, whenever a client logs in, the SDK will automatically associate the `clientId` with the device information (stored in the `Installation` table) by having the device **subscribe** to the channel with `clientId` as its name. The association can be found in the `channels` field of the `_Installation` table. By doing so, when the cloud wants to send a push notification to a client, the client's device can be targeted by the `clientId` associated with it. - -Since Instant Messaging generates way more push notifications than other sources, the cloud will not keep any records of them, nor can you find them on **Developer Center** > **Your game** > **Game Services** > **Cloud Services** > **Push Notification** > **Push records**. - -Each push notification is only valid for 7 days. This means that if a device doesn't connect to the cloud for more than 7 days, it will not receive this push notification anymore. - -### Other Settings for Push Notifications - -By default, push notifications are sent to the production environment of APNs for iOS devices. -Use `"_profile": "dev"` if you want to switch to the development environment of APNs (the development certificate will be used if certificate-based authentication is selected): - -```json -{ - "alert": "New message received", - "_profile": "dev" -} -``` - -When push notifications are sent via Token Authentication, if your app has private keys for multiple Team IDs, please confirm the one that should be used for your target devices and fill it into the `_apns_team_id` parameter, since Apple doesn't allow a single request to include push notifications sent to devices belonging to different Team IDs. - -```json -{ - "alert": "New message received", - "_apns_team_id": "my_fancy_team_id" -} -``` - -The `_profile` and `_apns_team_id` attributes are used internally by the push service and neither will actually be used. -When specifying additional push messages, different push messages are supported for different kinds of devices (e.g. `ios`, `android`). Keep in mind that the internal attributes, `_profile` and `_apns_team_id`, should not be specified inside the `ios` object, otherwise they will not take effect. -As an example, a push message like this will cause the message to be pushed to APNs’ production environment: - -```json -{ - "ios": { - "badge": "Increment", - "category": "NEW_CHAT_MESSAGE", - "sound": "default", - "thread-id": "chat", - "alert": { - "title": "New message received", - "body": "This message will still be pushed to the APNs production environment because of the incorrect location of the internal property _profile." - }, - "_profile": "dev" - }, - "android": { - "title": "New message received", - "alert": "" - } -} -``` - -To push to the development environment: - -```json -{ - "_profile": "dev", - "ios": { - /* … */ - }, - "android": { - /* … */ - } -} -``` - -You can insert certain built-in variables into the content you enter on **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Push notifications**. By doing this, you can embed certain context into the content of push notifications: - -- `${convId}` The ID of the conversation -- `${timestamp}` The Unix timestamp when the push notification is triggered -- `${fromClientId}` The `clientId` of the sender - -## Message Synchronization - -Push notification seems to be a good way to remind users of new messages, but the actual messages won't get delivered until the user goes online. If a user hasn't been online for an extremely long time, there will be tons of messages piled up on the cloud. How can we make sure that all these messages will be properly delivered once the user goes online? - -Instant Messaging provides a way for clients to pull messages from the cloud. The cloud keeps track of the last message each user receives from each conversation. When a user goes online, the conversations containing new messages as well as the number of unread messages in each of them will be computed and the client will receive a notification indicating that there is an update on the total number of unread messages. The client can then proactively fetch these messages. - -### Notifications on Updates of Unread Message Count - -When the client goes online, the cloud will compute the numbers of unread messages of all the conversations the client belongs to. - -To receive such notifications, the client needs to indicate that it is using the method of pulling messages from the cloud. As mentioned earlier, the JavaScript, Android, and iOS SDKs use this method by default, so there isn't any configuration that needs to be done. - -The SDK will maintain an `unreadMessagesCount` field on each `IMConversation` to track the number of unread messages in the conversation. - -When the client goes online, the cloud will drop in a series of `` in the format of events indicating updates on the numbers of unread messages. Each of them matches a conversation containing new messages and serves as the initial value of a `` maintained on the client side. After this, whenever a message is received by the SDK, the corresponding `unreadMessageCount` will be automatically increased. When the number of unread messages of a conversation is cleared, both `` on the cloud and maintained by the SDK will be reset. - -> If the count of unread messages is enabled, it will keep increasing until you explicitly reset it. -> It will not be reset automatically if the client goes offline again. -> Even if a message is received when the client is online, the count will still increase. -> Make sure to reset the count by marking conversations as read whenever needed. - -When the number of `` changes, the SDK will send an `UNREAD_MESSAGES_COUNT_UPDATE` event to the app through `IMClient`. You can listen to this event and make corresponding changes to the number of unread messages on the UI. We recommend you cache unread counts at the application level, and whenever there are two different counts available for the same conversation, replace the older data with the newer one. - - - -```cs -tom.OnUnreadMessagesCountUpdated = (convs) => { - foreach (LCIMConversation conv in convs) { - // conv.Unread is the number of unread messages in conversation - } -}; -``` - -```java -// Implement the delegate method onUnreadMessagesCountUpdated of LCIMConversationEventHandler to receive notifications on updates of unread message count -onUnreadMessagesCountUpdated(LCIMClient client, LCIMConversation conversation) { - // conversation.getUnreadMessagesCount() is the number of unread messages in conversation -} -``` - -```objc -// Use delegate method conversation:didUpdateForKey: to observe the unreadMessagesCount property of the conversation -- (void)conversation:(LCIMConversation *)conversation didUpdateForKey:(LCIMConversationUpdatedKey)key { - if ([key isEqualToString:LCIMConversationUpdatedKeyUnreadMessagesCount]) { - NSUInteger unreadMessagesCount = conversation.unreadMessagesCount; - /* New messages exist; update UI or fetch messages */ - } -} -``` - -```js -var { Event } = require("leancloud-realtime"); -client.on(Event.UNREAD_MESSAGES_COUNT_UPDATE, function (conversations) { - for (let conv of conversations) { - console.log(conv.id, conv.name, conv.unreadMessagesCount); - } -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .unreadMessageCountUpdated: - print(conversation.unreadMessageCount) - default: - break - } -} -``` - -```dart -tom.onUnreadMessageCountUpdated = ({ - Client client, - Conversation conversation, -}) { - // conversation.unreadMessageCount is the number of unread messages in conversation -}; -``` - - - -When responding to an `UNREAD_MESSAGES_COUNT_UPDATE` event, you get a `Conversation` object containing the `lastMessage` property which is the last message received by the current user from the conversation. To display the actual unread messages, [fetch the messages](/sdk/im/guide/beginner/) that come after it. - -The only way to clear the number of unread messages is to mark the messages as read with `Conversation#read`. You may do so when: - -- The user opens a conversation -- The user is already in a conversation and a new message comes in - -Implementation details on unread message counts for iOS and Android SDKs: - -iOS SDKs (Objective-C and Swift) will fetch all `UNREAD_MESSAGES_COUNT_UPDATE` events provided by the cloud on login, while Android SDK only fetches the latest events generated after the previous fetch (Android SDK remembers the timestamp of the last fetch). - -Therefore, Android developers need to cache the events for unread messages at the application level, because the events of some conversations have been fetched on previous logins, but not the current one. For iOS developers, they need to do the same thing because the cloud tracks at most 50 conversations containing unread messages, and the events for unread messages are only available for those conversations. If the events for unread messages are not cached, some conversations may have inaccurate counts of unread messages. - -## Multi-Device Sign-on and Single-Device Sign-on - -In some scenarios, a user can stay logged in on multiple devices at the same time. In other ones, a user can be logged in on only one device at a time. With Instant Messaging, you can easily implement both **multi-device sign-on** and **single-device sign-on** depending on your needs. - -When creating an `IMClient` instance, you can provide a `tag` parameter besides `clientId` to have the cloud check the uniqueness of `` when logging the user in. If the user is already logged in on another device with the same `tag`, the cloud will log the user out from that device, otherwise the user will stay logged in on all their devices. When a user is logged in on multiple devices, the messages coming to this user will be delivered to all these devices and the numbers of unread messages will be synchronized. If the user sends a message from one of these devices, the message will appear on other devices as well. - -Based on the above mechanism, a variety of requirements can be met with Instant Messaging: - -1. Multi-Device Sign-on: If `tag` is not specified when logging in, a user will be able to have any number of devices logged in at the same time. -2. Single-Device Sign-on: If all the clients share the same `tag`, a user will be able to stay logged in on only one device at a time. -3. Multi-Device Sign-on With Restrictions: You can assign a unique `tag` for each type of device. For example, if you have `Mobile` for phones, `Pad` for tablets, and `Web` for desktop computers, a user will be able to stay logged in on three devices with different types, but not two desktop computers. - -### Setting Tags - -The code below sets a `tag` called `Mobile` when creating `IMClient`, which can be used for the mobile client of your app: - - - -```cs -LCIMClient client = new LCIMClient(clientId, "Mobile", "your-device-id"); -``` - -```java -// Provide tag as the second parameter -LCIMClient currentClient = LCIMClient.getInstance(clientId, "Mobile"); -currentClient.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient avimClient, LCIMException e) { - if(e == null){ - // Successfully logged in - } - } -}); -``` - -```objc -NSError *error; -LCIMClient *currentClient = [[LCIMClient alloc] initWithClientId:@"Tom" tag:@"Mobile" error:&error]; -if (!error) { - [currentClient openWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // Successfully logged in - } - }]; -} -``` - -```js -realtime.createIMClient("Tom", { tag: "Mobile" }).then(function (tom) { - console.log("Tom logged in."); -}); -``` - -```swift -do { - let client = try IMClient(ID: "CLIENT_ID", tag: "Mobile") - client.open { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - Client tom = Client(id: 'Tom', tag: 'Mobile'); - await tom.open(); -} catch (e) { - print(e); -} -``` - - - -With the code above, if a user logs in on one mobile device and then logs in on another one (with the same `tag`), the user will be logged out from the former one. - -### Handling Conflicts - -When the cloud encounters the same `` for a second time, the device used for the earlier one will be logged out and receive a `CONFLICT` event: - - - -```cs -tom.OnClose = (code, detail) => { - -}; -``` - -```java -public class AVImClientManager extends LCIMClientEventHandler { - /** - * Implementing this method to handle the event of being logged out - * - * - * @param client - * @param code The status code indicating the reason of being logged out - */ - @Override - public void onClientOffline(LCIMClient avimClient, int i) { - if(i == 4111){ - // Tell the user that the same clientId is logged in on another device - } - } -} - -// Need to register the custom LCIMClientEventHandler to receive onClientOffline notifications -LCIMClient.setClientEventHandler(new AVImClientManager()); -``` - -```objc -- (void)imClientClosed:(LCIMClient *)imClient error:(NSError * _Nullable)error -{ - if ([error.domain isEqualToString:kLeanCloudErrorDomain] && - error.code == 4111) { - // Tell the user that the same clientId is logged in on another device - } -} -``` - -```js -var { Event } = require("leancloud-realtime"); -tom.on(Event.CONFLICT, function () { - // Tell the user that the same clientId is logged in on another device -}); -``` - -```swift -func client(_ client: IMClient, event: IMClientEvent) { - switch event { - case .sessionDidClose(error: let error): - if error.code == 4111 { - // Tell the user that the same clientId is logged in on another device - } - default: - break - } -} -``` - -```dart -tom.onClosed = ({ - Client client, - RTMException exception, -}) { - if (exception.code == '4111') { - // Tell the user that the same clientId is logged in on another device - } -}; -``` - - - -The reason a device gets logged out will be included in the event so that you can display a message to the user with that. - -All the "logins" mentioned above refer to users' explicit logins. If the user has already logged in, when the application restarts or reconnects, the SDK will relogin automatically. Under these scenarios, if a login conflict is encountered, the cloud will not log out earlier devices. The device trying to relogin will receive an error instead. - -Similarly, if you want an explicit login to receive an error when encountering a conflict, you can pass a special parameter on login: - - - -```cs -await tom.Open(false); -``` - -```java -LCIMClientOpenOption openOption = new LCIMClientOpenOption(); -openOption.setReconnect(true); -LCIMClient currentClient = LCIMClient.getInstance(clientId, "Mobile"); -currentClient.open(openOption, new LCIMClientCallback() { - @Override - public void done(LCIMClient avimClient, LCIMException e) { - if(e == null){ - // Connected - } - } -}); -``` - -```objc -NSError *err; -LCIMClient *currentClient = [[LCIMClient alloc] initWithClientId:@"Tom" tag:@"Mobile" error:&err]; -if (err) { - NSLog(@"init failed with error: %@", err); -} else { - [currentClient openWithOption:LCIMClientOpenOptionReopen callback:^(BOOL succeeded, NSError * _Nullable error) { - if ([error.domain isEqualToString:kLeanCloudErrorDomain] && - error.code == 4111) { - // Failed to log in; the previously logged in device will not be logged out - } - }]; -} -``` - -```js -realtime - .createIMClient("Tom", { tag: "Mobile", isReconnect: true }) - .then(function (tom) { - console.log( - "Failed to log in; the previously logged in device will not be logged out" - ); - }); -``` - -```swift -do { - let client = try IMClient(ID: "Tom", tag: "Mobile") - client.open(options: [.reconnect]) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - if error.code == 4111 { - // Failed to log in; the previously logged in device will not be logged out - } - } - } -} catch { - print(error) -} -``` - -```dart -try { - Client tom = Client(id: 'Tom', tag: 'Mobile'); - // Failed to log in; the previously logged in device will not be logged out - await tom.open(reconnect: true); -} catch (e) { - print(e); -} -``` - - - -## Custom Message Types - -Although Instant Messaging already supports a number of common message types by default, you can still define your own types as you need. For example, if you want your users to send messages containing payments and contacts, you can implement that with custom message types. - -### Custom Message Attributes - -The following message types are offered by default: - -- `TextMessage` Text message -- `ImageMessage` Image message -- `AudioMessage` Audio message -- `VideoMessage` Video message -- `FileMessage` File message (.txt, .doc, .md, etc.) -- `LocationMessage` Location message - -When composing messages with these types, you can include additional information by attaching custom attributes in the format of key-value pairs. For example, if you are sending a message and need to include city information, you can put it into `attributes` of the message rather than create your own message type. - - - -```cs -LCIMTextMessage messageWithCity = new LCIMTextMessage("It is getting cold."); -messageWithCity["city"] = "Montreal"; -``` - -```java -LCIMTextMessage messageWithCity = new LCIMTextMessage(); -messageWithCity.setText("It is getting cold."); -HashMap attr = new HashMap(); -attr.put("city", "Montreal"); -messageWithCity.setAttrs(attr); -``` - -```objc -NSDictionary *attributes = @{ @"city": @"Montreal" }; -LCIMTextMessage *messageWithCity = [LCIMTextMessage messageWithText:@"It is getting cold." attributes:attributes]; -``` - -```js -var messageWithCity = new TextMessage("It is getting cold."); -messageWithCity.setAttributes({ city: "Montreal" }); -``` - -```swift -let messageWithCity = IMTextMessage(text: "It is getting cold.") -messageWithCity.attributes = ["city": "Montreal"]; -``` - -```dart -TextMessage message = TextMessage(); -message.text = 'It is getting cold.'; -message.attributes = {'city': 'Montreal'}; -``` - - - -### Creating Your Own Message Types - -If the built-in types cannot fulfill your requirements at all, you can implement your custom message types. - - -<> - -By inheriting from `LCIMTypedMessage`, you can define your own types of messages. The basic steps include: - -- Define a subclass inherited from `LCIMTypedMessage`. -- Register the subclass when initializing. - -```cs -class EmojiMessage : LCIMTypedMessage { - public const int EmojiMessageType = 1; - - public override int MessageType => EmojiMessageType; - - public string Ecode { - get { - return data["ecode"] as string; - } set { - data["ecode"] = value; - } - } -} - -// Register subclass -LCIMTypedMessage.Register(EmojiMessage.EmojiMessageType, () => new EmojiMessage()); -``` - - -<> - -By inheriting from `LCIMTypedMessage`, you can define your own types of messages. The basic steps include: - -- Implement the new message type inherited from `LCIMTypedMessage`. Make sure to: - - Add the `@LCIMMessageType(type=123)` annotation to the class
    The value assigned to the type (`123` here) can be defined yourself. Negative numbers are for types offered by default and positive numbers are for those defined by you. - - Add the `@LCIMMessageField(name="")` annotation when declaring custom fields
    `name` is optional. Custom fields need to have their getters and setters. - - **Include an empty constructor** (see the sample below), otherwise there will be an error with type conversion. -- Call `LCIMMessageManager.registerLCIMMessageType()` to register the class. -- Call `LCIMMessageManager.registerMessageHandler()` to register the handler for messages. - -```java -@LCIMMessageType(type = 123) -public class CustomMessage extends LCIMTypedMessage { - // An empty constructor has to be present - public CustomMessage() { - - } - - @LCIMMessageField(name = "_lctext") - String text; - @LCIMMessageField(name = "_lcattrs") - Map attrs; - - public String getText() { - return this.text; - } - - public void setText(String text) { - this.text = text; - } - - public Map getAttrs() { - return this.attrs; - } - - public void setAttrs(Map attr) { - this.attrs = attr; - } -} - -// Register -LCIMMessageManager.registerLCIMMessageType(CustomMessage.class); -``` - - -<> - -By inheriting from `LCIMTypedMessage`, you can define your own types of messages. The basic steps include: - -- Implement the `LCIMTypedMessageSubclassing` protocol; -- Register the subclass. This is often done by calling `[YourClass registerSubclass]` in the `+load` method of the subclass or `-application:didFinishLaunchingWithOptions:` in `UIApplication`. - -```objc -// Definition - -@interface CustomMessage : LCIMTypedMessage - -+ (LCIMMessageMediaType)classMediaType; - -@end - -@implementation CustomMessage - -+ (LCIMMessageMediaType)classMediaType { - return 123; -} - -@end - -// Register subclass -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [CustomMessage registerSubclass]; -} -``` - - -<> - -By inheriting from `TypedMessage`, you can define your own types of messages. The basic steps include: - -- Declare the new message type by inheriting from `TypedMessage` or its subclass and then: - - Apply the `messageType(123)` decorator to the class. The value assigned to the type (`123` here) can be defined yourself (negative numbers are for types offered by default and positive numbers are for those defined by you). - - Apply the `messageField(['fieldName'])` decorator to the class to declare the fields that should be sent. -- Call `Realtime#register()` to register the type. - -For example, to implement the `OperationMessage` introduced in [Transient Messages](#transient-messages): - -```js -// TypedMessage, messageType, and messageField are provided by leancloud-realtime -// Use `var { TypedMessage, messageType, messageField } = AV;` in browser -var { TypedMessage, messageType, messageField } = require("leancloud-realtime"); -// Define OperationMessage for sending and receiving messages regarding user operations -export class OperationMessage extends TypedMessage {} -// Specify the type; can be other positive integers -messageType(1)(OperationMessage); -// The `op` field needs to be sent with the message -messageField("op")(OperationMessage); -// Register the class, otherwise an incoming OperationMessage cannot be automatically resolved -realtime.register(OperationMessage); -``` - - -<> - -By inheriting from `IMCategorizedMessage`, you can define your own types of messages. The basic steps include: - -- Implement the `IMMessageCategorizing` protocol; -- Register the subclass. This is often done by calling `try CustomMessage.register()` in the `application(_:didFinishLaunchingWithOptions:)` method of `AppDelegate`. - -```swift -// Specify the CustomMessage class -class CustomMessage: IMCategorizedMessage { - - // Specify the type; can be other positive integers - class override var messageType: MessageType { - return 1 - } -} - -// Register the message type -func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - do { - try CustomMessage.register() - } catch { - print(error) - return false - } - - return true -} -``` - - -<> - -You can define your own message type with a subclass inherited from `TypedMessage`: - -```dart -// Custom message type CustomMessage -class CustomMessage extends TypedMessage { - @override - - int get type => 123; - CustomMessage() : super(); - CustomMessage.from({ - @required String text, - //... - }) { - this.text = text; - } -} -TypedMessage.register(() => CustomMessage()); -``` - - -
    - -See [Back to Receiving Messages](/sdk/im/guide/beginner/) in the previous chapter for more details on how to receive messages with custom types. - -## Continue Reading - -- [3. Security, Chat Rooms, and Temporary Conversations](/sdk/im/guide/senior/) -- [4. Hooks and System Conversations](/sdk/im/guide/systemconv/) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/overview.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/overview.mdx deleted file mode 100644 index d2850dde0..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/overview.mdx +++ /dev/null @@ -1,306 +0,0 @@ ---- -title: Instant Messaging Overview -sidebar_label: Overview -sidebar_position: 0 ---- - -Instant Messaging allows you to quickly implement functions like instant messaging and data synchronization in your app. It is designed with the following goals: - -- **Easily add messaging functions to existing apps** - - Many of our customers already have their products running smoothly and the only thing they need is to add instant messaging as a bonus feature in their apps. With this in mind, Instant Messaging is designed to work independently without interfering with your existing account systems. - - We offer a full collection of UI frameworks and SDKs for you to design and develop messaging functions for a variety of platforms and scenarios. - -- **Provide extensibility for custom features** - - Instant Messaging supports common types of messages including text messages, images, audios, videos, locations, and binary data. You can define your own types of messages and build UI for them if you need. You can also implement functions like recalling messages, editing messages, mentioning people, transient messages, delivery receipts, push notifications, and text moderation. If you want to build chat rooms that could hold infinite numbers of users, or system conversations that could be used for chatbots, Instant Messaging also got you covered. - -- **Enhance security with permission management** - - When you use Instant Messaging, clients and servers use full-duplex communication channels through WebSocket with TLS encrypted transmissions. This ensures that conversations happening within your app cannot be fetched by any unauthorized parties. To further control user activities, you can use our third-party signing mechanism to verify all the sensitive operations before they can be processed. - -- **Lower costs for maintenance** - - With our 24/7 technical support backed by experienced engineers, you can be confident that you are able to integrate our services into your projects with a minimal amount of hassle. Besides this, you are also freed from handling all the errors that might happen to software and infrastructure, as well as upgrading servers when the usage of your app grows quickly. - -Instant Messaging is already widely used in scenarios like in-app socializing, remote collaboration, customer service, live streaming, and game state synchronization. - -## Features - -Here are the main features offered by Instant Messaging: - -- **Basic chatting** - - - Besides one-on-one chats and group chats, we also offer **open chat rooms** that could hold infinite numbers of people, which can be used for scenarios like live streaming and open courses that demand massive discussions in a group. There are also **system conversations** that could interact with users spontaneously, as well as **temporary conversations** which work best if you need to build customer service systems. All these functions share the same programming interface. - - Users can send all kinds of messages, including text messages, images, audios, videos, locations, and **binary data**. You can develop your own types of messages if you need. - - History messages are automatically saved on the cloud, which can be retrieved in multiple ways. - -- **Advanced messaging features** - - - Mentioning people with **"@"** - - **Recalling and editing** messages - - **Getting receipts** when messages are delivered and read - - **Muting** conversations - - **Sending status updates** like "Someone is typing…" - - Converting messages to **push notifications** when receivers are offline - - Setting **priorities** for messages in chat rooms so that important messages can get quicker delivery - -- **Multi-device sign-on and message synchronization** - - It is becoming a common requirement that users can have their accounts logged in on multiple devices at the same time. Here we allow you to make your own decision whether users can do so and receive messages on all their devices, or they can only **log in on a single device** at a time. - What if a user accidentally loses their connection to the Internet? No worries. Our **synchronization mechanism** guarantees that unreceived messages can always be synchronized to users' devices once the connection is re-established. - -- **Group management** - - The messages sent through all the conversations can be filtered upon a list of keywords you define or through a plugin you build. This helps your apps comply with the laws and regulations in the areas they are operated. - -- **Access control** - - For any user that wants to send and receive messages, the only thing they need is a `clientId` identifying them. By decoupling with the account system of the app, it is made easy for you to add Instant Messaging into your app and it also helps us better focus on our role as a "messenger". - We provide a **third-party signing mechanism** that allows you to verify all the operations performed within the app with your own server. This ensures that all the requests sent to the server are legitimate. - Our SDKs and the cloud use full-duplex communication channels through WebSocket with TLS encrypted transmissions, which further ensures the security of users' conversations. - -- **Customizable components** - - We offer a number of common features that you can directly use, while you also have the freedom to build your own logic for your special needs: - - - You can verify operations like creating/joining/leaving conversations and retrieving history messages by connecting to your existing account system via our third-party signing mechanism. - - You can attach **hooks** to different stages of a message delivery process to implement your custom logic like filtering out certain keywords or customizing push notifications. - - You can use **webhooks** to implement message synchronization between the cloud and your app's backend. - - Besides client-side SDKs, we also offer REST APIs for you to implement functions under trusted environments. - - Our flexibility and extensibility give you the power to add more fun to your app besides the basic chatting functions. - -## Cross-Platform Support - -We offer client-side SDKs for a variety of mainstream platforms. Feel free to take a look at their source code on our [GitHub](https://github.com/leancloud). You are welcome to talk to us if you have any questions or needs. - -You may also try out our demos to quickly familiarize yourself with our services. You can find them on **Download > Demos**. - -## Terminologies - -The concepts mentioned below will be used frequently in our documentation. It would be helpful if you could familiarize yourself with them. - -### `clientId`, User, and Log in - -In Instant Messaging, each terminal is called a "client". Each client has a unique ID (`clientId`) that marks itself within an app. Such an ID can be generated by your app, but it has to be **a string of alphanumeric characters, underscores, and hyphens that is not longer than 64 characters and does not begin with a numeric character**. In most cases, each client matches an actual user of your app, but it does not mean that only users can act as clients. A probe can also be a client that keeps broadcasting the data collected by it to other people. - -To start using Instant Messaging, each client needs to build a WebSocket connection to the cloud and register itself with a unique `clientId`. Such a process is called "logging in". Keep in mind that this is not the same as the log-in process of the app itself. - -By default, a `clientId` can log in on multiple devices, and multiple `clientId`s can log in on the same device as well. If you want each user to be logged in on only one device at a time, you can specify a "tag" when logging in so that when duplicated tags are detected on the cloud, the devices that are already logged in will be logged out. You can decide whether to use tags or not based on your actual needs. - -After logging in, our SDK will ensure that the connection is always alive and will automatically reconnect if the network status ever changes. The connection will be ended once the app goes into the background and push notifications will be used for message delivery. - -### Conversation - -When a user starts chatting with someone after logging in, a `Conversation` would be formed. In Instant Messaging, a conversation contains all the members belonging to it and holds all the messages sent by them, so whenever a client sends a message, it is always sent into a conversation. Before a client could send messages to others, it has to create or join a conversation first and invite other people to it (optional). Only those who are inside a conversation can have access to the messages in it. - -When a conversation is created, a record would be added to the `_Conversation` table which can be found on **Developer Center > Your game > Game Services > Cloud Services > Data Storage > Data**. Each conversation holds properties like group name, members, as well as custom attributes. The table below shows the relationship between the properties of a conversation and the fields of the `_Conversation` table: - -| Property | Table Field | Type | Constraint | Description | -| ---------------- | ----------- | --------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | `name` | `String` | Optional | The name of the group chat. | -| `attributes` | `attr` | `Object` | Optional | Custom attributes. | -| `conversationId` | `objectId` | `String` | | A unique ID generated by the cloud (read-only). | -| `creator` | `c` | `String` | | The `clientId` of the creator (read-only). | -| `members` | `m` | `Array` | | All the members of a basic conversation (not applicable for chat rooms or system conversations). | -| `mute` | `mu` | `Array` | | Members who muted the conversation. These members will not receive push notifications. | -| `lastMessageAt` | `lm` | `Date` | | The time the last message is sent. | -| `transient` | `tr` | `Boolean` | Optional | Whether it is a chat room. | -| `system` | `sys` | `Boolean` | Optional | Whether it is a system conversation. | -| `unique` | `unique` | `Boolean` | Optional | If this is `true`, the same conversation will be reused when a new conversation is created with the same composition of members and `unique` to be `true`. | - -We do have different types of conversations designed for different scenarios. All of them will be stored in `_Conversation` regardless of their types. - -#### Common Scenarios - -Let's first go through some common scenarios where Instant Messaging could be used. - -- **One-on-one chats** - - This is the conversation between two clients and you can decide whether other users can find it or not (usually it is private). It will be converted to a group chat if new clients are added to it and, again, you can decide if all the members will stay in the same conversation or form a new one. - -- **Group chats** - - A group chat contains two or more clients and oftentimes members can be added or removed at any time. A name is usually assigned to a group chat, such as "Family", "Friends", or "Co-workers". It is possible for a group chat to have only two or even one member, but it does not make any difference on whether it is considered a group chat or not. You can decide if group chats are public (such as searchable by name) or not. - -- **Chat rooms** - - A chat room is similar to a group chat since both of them involve a lot of people. What makes a chat room different is that the number of people in it is often way higher than that of a group chat. In such a situation, the number of people in a chat room becomes more meaningful than the specific list of people in it. There is also a difference in how members of a conversation are managed. By opening a chat room, a client joins it, and by closing it, the client leaves it. Therefore, message synchronization and push notifications are disabled for chat rooms. - -- **Official accounts and bots** - - You can build official accounts in your app and have them broadcast messages to the users who subscribe to them or chat with specific users. You can also implement bots that can automatically reply to messages sent from users. - -- **Customer service** - - Customers can start temporary conversations with representatives to discuss solutions to their problems. These conversations can be closed once the problems are solved. - -Instant Messaging offers the following four types of conversations to cover all these scenarios. - -#### Basic Conversation - -This is the most commonly used type of conversation that could serve one-on-one chats and group chats. It offers the following functions: - -- Send and receive messages among members of it -- Add and remove members (500 maximum) with all the existing members notified -- Find out who are online -- Mention members, recall messages, edit messages, send transient messages, get receipts when messages are delivered and read, send will messages, and receive push notifications -- Receive push notifications when offline and synchronize unreceived messages when online -- Store message history on the cloud and perform various types of queries on it - -Tips: You may assign properties to a conversation to mark if it is a one-on-one chat or a group chat. You may also mark if a conversation is private or public. Such properties can be stored in `Conversation.attributes`. - -#### Chat Room - -This type of conversation is dedicated to chat rooms (it has `tr` to be `true` in `_Conversation`). Similar to a basic conversation, users can create, join, or leave it at any time and all the messages will be stored on the cloud. It is different from a basic conversation on the following basis: - -- **No limit on the number of people**; does not have a fixed list of members; members are removed once they are offline (field `m` is ignored) -- **The number of members** can be retrieved instead of the specific list of members -- No offline messages, push notifications, or delivery receipts -- No notifications for members joining or leaving -- Cannot invite or remove people -- A user can only be in one chat room at each time and will automatically leave the previous one when joining a new one -- A user has to rejoin a chat room if it becomes offline for more than 30 minutes - -Tips: Although a chat room does not limit the number of people in it, the user experience can be impaired when there are too many people sending a lot of messages. We suggest that you limit the number of people in each chat room to less than **5,000** and split big chat rooms into smaller ones if possible. - -#### System Conversation - -This type of conversation can be used to build bots, official accounts, service accounts, and in-app notifications (it has `sys` to be `true` in `_Conversation`). It has the following traits: - -- A user subscribes to it by joining and unsubscribes by leaving (field `m` is ignored) -- Conversations can only be created on the server side and clients can only subscribe or unsubscribe to the existing conversations -- Messages can be sent to either all subscribers or specific users -- The messages sent from users will be stored in the `_SysMessage` table and cannot be seen by other users -- You can set up hooks to receive messages from users and reply to them with REST API - -#### Temporary Conversation - -This type of conversation does not get stored in the `_Conversation` table. It is meant for special scenarios with: - -- Short TTL -- Fewer members (10 clients maximum) -- No message history needed - -We recommend temporary conversation to be used for customer service. It has the following traits: - -- Cannot mute or unmute -- Cannot update attributes -- Allow message operations and querying members just like basic conversations - -Tips: Temporary conversations last shorter and do not get stored in the `_Conversation` table. This **reduces the size of data stored in your app** and **lowers the cost needed** as well. - -#### Summary - -| Type of Conversation | Scenarios | Member Management | Message Operations | Message History | -| -------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | --------------- | -| **Basic Conversation** | One-on-one chats and group chats | Members are persisted (500 members maximum) | Only members can send or receive messages | Supported | -| **Chat Room** | Chat rooms, barrage, and real-time comments | Members are not persisted; Users cannot be invited; No limit on the number of people | Everyone can send messages; Online users can receive messages | Supported | -| **System Conversation** | Official accounts, bots, system notifications, and custom messages | Does not have the concept of members; A list of subscribers can be maintained; No limit on the number of subscribers | Messages can be sent to subscribers through API; Webhooks can be set up to handle messages | Supported | -| **Temporary Conversation** | Customer service | Fixed members | Only members can send or receive messages | Not supported | - -### Message - -A message is a basic unit for transmitting data within a conversation. Each message can hold up to **5 KB** of data in the format of text. There is no requirement on how the text should be formatted and you are free to define your own types of messages based on it. - -When sending a message, a parameter can be specified to make it either a normal message or a transient message. A normal message comes with features like delivery receipts, persistent storage, and push notifications, while a transient message cannot be saved, received later, or pushed to offline users. Therefore, status updates like "Someone is typing…" are best sent as transient messages and messages composed by users shall be sent as normal messages. - -Our service ensures that a normal message can be delivered at least once. At the same time, our SDKs are able to filter out duplicate messages. We offer both SDKs and REST API for you to send messages: SDKs are often for clients and REST API is for sending messages from the server side. When using REST API, you can specify the sender of a message and the ID of the target conversation, as well as the receivers of the message if it is sent to a system conversation. - -#### Default Types for Rich Media Messages - -We offer a number of predefined JSON-based types (`TypedMessage`) for rich media messaging, including: - -- Text (`TextMessage`) -- Image (`ImageMessage`) -- Audio (`AudioMessage`) -- Video (`VideoMessage`) -- Location (`LocationMessage`) - -The image below shows the inheritance relationships among them: - -![TypedMessage is inherited from Message. TextMessage, ImageMessage, AudioMessage, VideoMessage, LocationMessage, and other types of messages are inherited from TypedMessage.](/img/realtime_v2_message_types.svg) - -As mentioned above, rich media messages are based on the JSON format. Therefore, they need to be serialized to a JSON string containing the following properties when sent via the REST API. -Client-side SDKs will do the conversion automatically when sending messages. - -| Property | Constraint | Description | -| ---------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `_lctype` | | Type of the rich media message
    MessageType
    Text message-1
    Image message-2
    Audio message-3
    Video message-4
    Location message-5
    File message-6
    All the types above use negative numbers. Positive numbers are reserved for custom types, and zero is reserved for "no type". | -| `_lctext` | | Text description of the rich media message | -| `_lcattrs` | | A JSON string containing custom attributes | -| `_lcfile` | | If the message contains a file (image, audio, video, or generic file), `_lcfile` would contain the information about that file. | -| `url` | | The URL of the file after it is uploaded. Note that the URL in historical messages will not be updated when you bind or re-bind a custom domain name. | -| `objId` | Optional | The objectId of the file in the `_File` class. | -| `metaData` | Optional | File metadata. | - -The above properties are common to all types of rich media messages. - -You can easily extend your own message types based on our framework. - -#### Advanced Features - -As mentioned earlier, we offer the following functions besides basic chatting: - -- Mentioning people with "@" -- Recalling and editing messages -- Text moderation -- Getting receipts when messages are delivered and read -- Muting conversations -- Sending status updates like "Someone is typing…" -- Converting messages to push notifications when receivers are offline -- Setting priorities for messages in chat rooms so that important messages can get prioritized delivery - -You can read [Advanced Messaging Features](/sdk/im/guide/intermediate/) and [Text Moderation](/sdk/im/guide/senior/) to learn more about them. - -## Restrictions - -- In **every minute**, a client can send **at most 60 messages**, query history messages **at most 120 times**, and perform operations like joining conversations, leaving conversations, logging in, and logging out **at most 30 times**. Incoming requests will be rejected if the limits are exceeded (the callbacks implemented with SDK will not be triggered). If you are performing these operations with REST API, the limits will not apply. -- For each app, at most 160,000 messages can be delivered to all the clients each second. The messages exceeding the limit will be discarded. Please contact us if your app needs a higher quota. -- The size of each message (including metadata such as `pushData`) shall be less than or equal to 5 KB. -- Each conversation can hold at most 500 people. If you add more than 500 IDs into the `m` field with the Data Storage API, only the first 500 IDs will be used. -- The same ID is not supposed to be logged in on too many devices. If we detect that an ID is logged in on more than 5 IP addresses at the same time, this ID will be billed with each IP as an independent user on that day. -- If a user has more than 50 conversations containing unread messages, the cloud will **randomly** pick 50 of them and deliver their unread messages (or the amounts of them) to the user when the user logs in. The undelivered messages will not be lost but need to be manually retrieved. -- If a user has a conversation containing more than 100 offline messages, the former messages will not be automatically delivered when the user logs in and the user will not see the amount of them. The undelivered messages can be manually retrieved. -- Updating the file URL (**Developer Center > Your game > Game Services > Cloud Services > Data Storage > File > Settings**) will not automatically refresh the URLs in the existing messages (including rich media messages). -- There are request frequency and quantity limitations on invoking Instant Messaging-related REST APIs. See [Instant Messaging REST API Guide](/sdk/im/guide/rest/) for details. - -### Lifespan of Conversations - -If no message is sent to a conversation (basic conversation, chat room, or system conversation) in the **past 6 months** through either SDK or REST API, or none of its fields in the `_Conversation` table are updated, the conversation will be considered **inactive** and will be deleted automatically. Keep in mind that querying messages does not update the `_Conversation` table, so a conversation that only gets queried will also be seen as inactive. - -If you attempt to send a message to a deleted conversation through either SDK or REST API, the error `4401 INVALID_MESSAGING_TARGET` will be returned which means that the conversation does not exist anymore. The messages associated with deleted conversations will also be gone. - -Active conversations will never get deleted. - -### Lifespan of Messages - -A message will be stored on the cloud for **6 months**. This means that you can only query the message history of a conversation in the past 6 months. If you would like to pay to extend the period, please reach out to us. -You may also synchronize the message history to your own server with REST API. - -## Hooks - -See [Hooks](/sdk/im/guide/systemconv/). - -## Price - -Charged by active users, up to 500 users for free each day, for overage: 1 USD / 10,000 users per day. - -* REST API charges by number of requests. Same as Data Storage: first 30,000 times for free each day, for overage: 0.04 USD / 1000 times. -* A file fee is charged for multimedia messages using the file service. Please refer to the price page of the [official website](https://developer.taptap.io/product-intro/price). -* All requests to the '_Conversation' table incur a data storage charge. The price is the same as Data Storage. - -## Guides - -Basic features: - -- [1. Basic Conversations and Messages](/sdk/im/guide/beginner/) -- [2. Advanced Messaging Features, Push Notifications, Synchronization, and Multi-Device Sign-on](/sdk/im/guide/intermediate/) -- [3. Security, Chat Rooms, and Temporary Conversations](/sdk/im/guide/senior/) -- [4. Hooks and System Conversations](/sdk/im/guide/systemconv/) - -REST API: - -- [Instant Messaging REST API](/sdk/im/guide/rest/) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/rest.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/rest.mdx deleted file mode 100644 index def1989f7..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/rest.mdx +++ /dev/null @@ -1,1746 +0,0 @@ ---- -title: Instant Messaging REST API -sidebar_position: 5 ---- - -## Overview - -The base URL for sending requests can be found on **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings**. -For POST and PUT requests, the request body must be a JSON object and the Content-Type in the HTTP Header should be `application/json`. -Requests are authenticated according to the key-value pairs included in the HTTP Header. See [Data Storage REST API](/sdk/storage/guide/rest/) for more information. - -The `_Conversation` table includes some built-in fields that denote a conversation’s attributes and members. One-on-one chats, group chats, chat rooms, and system conversations are all stored in this table. See [Instant Messaging Overview](/sdk/im/guide/overview/) for more information. -To avoid inconsistencies, we strongly discourage you to manipulate the data in the `_Conversation` table with the Data Storage API. - -The current API version is `1.2`: - -- APIs related to one-on-one chats and group chats start with `rtm/conversations`. -- APIs related to chat rooms start with `rtm/chatrooms`. In the `_Conversation` table, chat rooms have their `tr` field being `true`. -- APIs related to system conversations start with `rtm/service-conversations`. In the `_Conversation` table, system conversations have their `sys` field being `true`. - -APIs related to clients start with `rtm/clients`. -APIs used for global operations start with `rtm/{function}`. For example, `rtm/all-conversations` can be used to look up all conversations regardless of their types. - -## One-On-One Chats and Group Chats - -### Creating Conversations - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"My First Conversation", "m": ["BillGates", "SteveJobs"], "unique": true}' \ - https://{{host}}/1.2/rtm/conversations -``` - -Above is a minimal example of creating a conversation. The conversation contains two members with their client IDs being BillGates and SteveJobs. The objectId of the conversation will be returned once the conversation is created, after which the client will be able to send messages with the ID. The newly created conversation can be found in the `_Conversation` table. -See [Instant Messaging Overview](/sdk/im/guide/overview/) for more information about the attributes of a conversation. -To ensure that the conversation is unique, you can provide the `"unique": true` parameter. - -The response looks like - -```json -{ - "unique": true, - "updatedAt": "2020-05-26T06:42:31.492Z", - "name": "My First Conversation", - "objectId": "5eccba570d3a42c5fd4e25c3", - "m": ["BillGates", "SteveJobs"], - "createdAt": "2020-05-26T06:42:31.482Z", - "uniqueId": "6c7b0e5afcae9aa1139a0afa25833dec" -} -``` - -Keep in mind that the only difference between group chats and one-on-one chats is that they have different numbers of clients. The same API is used for both types of conversations. - -### Retrieving Conversations - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'where={"name": "first conversation"}' \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/conversations -``` - -| Parameter | Constraint | Description | -| ----- | ---- | ------------------------------------------------------------------------ | -| skip | Optional | -| limit | Optional | Can be used for pagination when used together with `skip` | -| where | Optional | See [Data Storage REST API](/sdk/storage/guide/rest/) for more information | - -The response looks like - -```json -{ - "results": [ - { - "name": "test conv1", - "m": ["tom", "jerry"], - "createdAt": "2018-01-17T04:15:33.386Z", - "updatedAt": "2018-01-17T04:15:33.386Z", - "objectId": "5a5ecde6c3422b738c8779d7" - } - ] -} -``` - -### Updating Conversations - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"Updated Conversation"}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id} -``` - -All attributes of the `_Conversation` table except `m` can be updated with this interface. - -The response looks like - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### Deleting Conversations - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id} -``` - -The response looks like - -```json -{} -``` - -### Adding Members - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/members -``` - -The response looks like - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### Removing Members - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/members -``` - -The response looks like - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### Retrieving Members - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/members -``` - -The response looks like - -```json -{ "result": ["client1", "client2"] } -``` - -### Adding Members That Mute a Conversation - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/mutes -``` - -| Parameter | Description | -| ---------- | -------------------------- | -| client_ids | An array of `Client ID`s that mute the conversation | - -The response looks like - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### Removing Members That Mute a Conversation - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/mutes -``` - -The response looks like - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### Retrieving Members That Muted a Conversation - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/mutes -``` - -The response looks like - -```json -{ "result": ["client1", "client2"] } -``` - -### One-On-One Chats and Group Chats: Sending Messages - -This interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages -``` - -**Attention**: Due to the administrative essence of this interface, when you send messages with this interface, our system won’t check if the **from_client** has the permission to send messages to the conversation. -If you are using rich media messages in your application, please make sure to have the **message** field follow the required format. - -| Parameter | Constraint | Description | -| ------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| from_client | Required | The client ID of the sender. | -| message | Required | The content of the message. Although the value of this field is a string, there is no limit on the format of the string. Therefore, you can define messages with different formats using this field, as long as the size of the value of this field doesn’t exceed the limit of 5 KB. | -| transient | Optional | Whether the message is transient. Defaults to `false`. | -| no_sync | Optional | By default, the message will be synced to the client of the `from_client` that is online. You can disable this feature by setting this property to `true`. | -| push_data | Optional | The content used for push notifications. If the receiver uses an iOS device and is not online, the value of this property will be used for sending push notifications. See [2. Advanced Messaging Features, Push Notifications, Synchronization, and Multi-Device Sign-on](/sdk/im/guide/intermediate/) for more information. | -| priority | Optional | The priority of the message. Could be `high`, `normal`, or `low`. The value is case-insensitive. Defaults to `normal`. This parameter is only valid if the message is transient or is sent to a chat room. When there is high traffic on the server or users’ devices, messages with high priorities will still be queued. | -| mention_all | Optional | Boolean. Used to remind all the members in the conversation to pay attention to this message. | -| mention_client_ids | Optional | Array. Includes the list of `client_id` that will be reminded to pay attention to this message. Can contain at most 20 client IDs. | - -Response: - -By default, messages are sent asynchronously with the API. You will receive the ID of the message along with the server timestamp, like `{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151}`. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Retrieving History Messages - -This interface can only be accessed with the master key. -To ensure the security of history messages, you can enable the signing mechanism. See [3. Security, Permission Management, Chat Rooms, and Temporary Conversations](/sdk/im/guide/senior/) for more information. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages -``` - -| Parameter | Constraint | Description | -| -------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------ | -| msgid | Optional | The ID of the starting message. **When this parameter is provided, the `timestamp` of the corresponding message must be provided as well.** | -| timestamp | Optional | The start timestamp (in milliseconds). Defaults to the current time. | -| till_msgid | Optional | The ID of the ending message. **When this parameter is provided, the `till_timestamp` of the corresponding message must be provided as well.** | -| till_timestamp | Optional | The end timestamp (in milliseconds). Defaults to 0. | -| include_start | Optional | Whether to include the message determined by `timestamp` and `msgid`. Boolean. Defaults to `false`. | -| include_stop | Optional | Whether to include the message determined by `till_timestamp` and `till_msgid`. Boolean. Defaults to `false`. | -| reversed | Optional | Sort the results in reverse of the default order (time order). With this option enabled, `till_timestamp` defaults to the current time and `timestamp` defaults to 0. Boolean. Defaults to `false`. | -| limit | Optional | Used to limit the number of records returned. Defaults to 100. Can be no more than 1000. | -| client_id | Optional | Viewer ID (used for signature). | -| nonce | Optional | Nonce for the signature (used for signature). | -| signature_ts | Optional | Timestamp for the signature (used for signature) in milliseconds. | -| signature | Optional | The signature (used for signature). | - -This interface contains a lot of parameters related to time. To make things clear, here is an example. Assuming that a conversation has 3 messages with their IDs being `id1`, `id2`, and `id3` and timestamps being `t1`, `t2`, and `t3` (`t1` < `t2` < `t3`). The table below shows the results for queries with different parameters (blank cells indicate that the default values are used): - -| timestamp | msgid | till_timestamp | till_msgid | include_start | include_stop | reversed | Result | -| --------- | ----- | -------------- | ---------- | ------------- | ------------ | -------- | ------- | -| t3 | id3 | t1 | id1 | | | | id2 | -| t3 | id3 | t1 | id1 | true | | | id3 id2 | -| t3 | id3 | t1 | id1 | | true | | id2 id1 | -| t1 | id1 | t3 | id3 | | | true | id2 | -| t1 | id1 | t3 | id3 | true | | true | id1 id2 | -| t1 | id1 | t3 | id3 | | true | true | id2 id3 | - -The response would be a JSON array containing messages sorted from the newest to the oldest. If `reversed` is enabled, the messages will be sorted in reverse order. - -Response: - -```json -[ - { - "timestamp": 1408008498571, - "conv-id": "219946ef32e40c515d33ae6975a5c593", - "data": "Nice weather!", - "from": "u111872755_9d0461adf9c267ae263b3742c60fa", - "msg-id": "vdkGm4dtRNmhQ5gqUTFBiA", - "is-conv": true, - "is-room": false, - "to": "5541c02ce4b0f83f4d44414e", - "bin": false, - "from-ip": "202.117.15.217" - } - // ... -] -``` - -To look up all the messages sent by a user, use `GET /rtm/clients/{client_id}/messages`. -To look up all the messages sent through your application, use `GET /rtm/messages`. - -### One-On-One Chats and Group Chats: Updating Messages - -This interface can only be accessed with the master key. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages/{message_id} -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender | -| message | Required | The message | -| timestamp | Required | The timestamp of the message | - -If successful, the `200 OK` status code will be returned. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### One-On-One Chats and Group Chats: Recalling Messages - -This interface can only be accessed with the master key. SDK support is needed for requests sent to this interface to take effect. Refer to the interface for updating messages for more information. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages/{message_id}/recall -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender | -| timestamp | Required | The timestamp of the message | - -If successful, the `200 OK` status code will be returned. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Deleting Messages - -This interface can only be accessed with the master key. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'from_client=some-client-id' \ - --data-urlencode 'timestamp=123' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages/{message_id} -``` - -Keep in mind that this interface will only delete messages on the server and won’t affect clients. - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender | -| timestamp | Required | The timestamp of the message | - -Response: - -```json -{} -``` - -## Chat Rooms - -### Creating Chat Rooms - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"My First Chatroom"}' \ - https://{{host}}/1.2/rtm/chatrooms -``` - -See [Instant Messaging Overview](/sdk/im/guide/overview/) for more information about the attributes of a conversation. - -The response looks like - -```json -{ - "objectId": "5a5d7432c3422b31ed845e75", - "createdAt": "2018-01-16T03:40:32.814Z" -} -``` - -### Retrieving Chat Rooms - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'where={"name": "chatroom"}' \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/chatrooms -``` - -| Parameter | Constraint | Description | -| ----- | ---- | ------------------------------------------------------------------------ | -| skip | Optional | -| limit | Optional | Can be used for pagination when used together with `skip` | -| where | Optional | See [Data Storage REST API](/sdk/storage/guide/rest/) for more information | - -The response looks like - -```json -{ - "results": [ - { - "name": "My First Chatroom", - "createdAt": "2018-01-17T04:15:33.386Z", - "updatedAt": "2018-01-17T04:15:33.386Z", - "objectId": "5a5ecde6c3422b738c8779d7" - } - ] -} -``` - -### Updating Chat Rooms - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"Updated Chatroom"}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id} -``` - -The response looks like - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### Deleting Chat Rooms - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id} -``` - -The response looks like - -```json -{} -``` - -### Randomly Retrieving Some Online Members - -This interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/members -``` - -The response looks like - -```json -{ "result": ["clientid1", "clientid2", "clientid3"] } -``` - -### Retrieving Numbers of Online Members - -This interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/members/online-count -``` - -The response looks like - -```json -{ "result": 3 } -``` - -### Chat Rooms: Sending Messages - -This interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages -``` - -**Attention**: Due to the administrative essence of this interface, when you send messages with this interface, our system won’t check if the **from_client** has the permission to send messages to the conversation. -If you are using rich media messages in your application, please make sure to have the **message** field follow the required format. -Besides, for chat rooms, messages **cannot** be synced to the online **from_client**. - -| Parameter | Constraint | Description | -| ------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| from_client | Required | The client ID of the sender. | -| message | Required | The content of the message. Although the value of this field is a string, there is no limit on the format of the string. Therefore, you can define messages with different formats using this field, as long as the size of the value of this field doesn’t exceed the limit of 5 KB. | -| transient | Optional | Whether the message is transient. Defaults to `false`. | -| priority | Optional | The priority of the message. Could be `high`, `normal`, or `low`. The value is case-insensitive. Defaults to `normal`. This parameter is only valid if the message is transient or is sent to a chat room. When there is high traffic on the server or users’ devices, messages with high priorities will still be queued. | -| mention_all | Optional | Boolean. Used to remind all the members in the conversation to pay attention to this message. | -| mention_client_ids | Optional | Array. Includes the list of `client_id` that will be reminded to pay attention to this message. Can contain at most 20 client IDs. | - -Response: - -By default, messages are sent asynchronously with the API. You will receive the ID of the message along with the server timestamp, like `{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151}`. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Retrieving History Messages - -This interface can only be accessed with the master key. -To ensure the security of history messages, you can enable the signing mechanism. See [3. Security, Permission Management, Chat Rooms, and Temporary Conversations](/sdk/im/guide/senior/) for more information. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/messages -``` - -| Parameter | Constraint | Description | -| -------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------ | -| msgid | Optional | The ID of the starting message. **When this parameter is provided, the `timestamp` of the corresponding message must be provided as well.** | -| timestamp | Optional | The start timestamp (in milliseconds). Defaults to the current time. | -| till_msgid | Optional | The ID of the ending message. **When this parameter is provided, the `till_timestamp` of the corresponding message must be provided as well.** | -| till_timestamp | Optional | The end timestamp (in milliseconds). Defaults to 0. | -| include_start | Optional | Whether to include the message determined by `timestamp` and `msgid`. Boolean. Defaults to `false`. | -| include_stop | Optional | Whether to include the message determined by `till_timestamp` and `till_msgid`. Boolean. Defaults to `false`. | -| reversed | Optional | Sort the results in reverse of the default order (time order). With this option enabled, `till_timestamp` defaults to the current time and `timestamp` defaults to 0. Boolean. Defaults to `false`. | -| limit | Optional | Used to limit the number of records returned. Defaults to 100. Can be no more than 1000. | -| client_id | Optional | Viewer ID (used for signature). | -| nonce | Optional | Nonce for the signature (used for signature). | -| signature_ts | Optional | Timestamp for the signature (used for signature) in milliseconds. | -| signature | Optional | The signature (used for signature). | - -This interface contains a lot of parameters related to time. To make things clear, here is an example. Assuming that a conversation has 3 messages with their IDs being `id1`, `id2`, and `id3` and timestamps being `t1`, `t2`, and `t3` (`t1` < `t2` < `t3`). The table below shows the results for queries with different parameters (blank cells indicate that the default values are used): - -| timestamp | msgid | till_timestamp | till_msgid | include_start | include_stop | reversed | Result | -| --------- | ----- | -------------- | ---------- | ------------- | ------------ | -------- | ------- | -| t3 | id3 | t1 | id1 | | | | id2 | -| t3 | id3 | t1 | id1 | true | | | id3 id2 | -| t3 | id3 | t1 | id1 | | true | | id2 id1 | -| t1 | id1 | t3 | id3 | | | true | id2 | -| t1 | id1 | t3 | id3 | true | | true | id1 id2 | -| t1 | id1 | t3 | id3 | | true | true | id2 id3 | - -The response would be a JSON array containing messages sorted from the newest to the oldest. If `reversed` is enabled, the messages will be sorted in reverse order. - -Response: - -```json -[ - { - "timestamp": 1408008498571, - "conv-id": "219946ef32e40c515d33ae6975a5c593", - "data": "Nice weather!", - "from": "u111872755_9d0461adf9c267ae263b3742c60fa", - "msg-id": "vdkGm4dtRNmhQ5gqUTFBiA", - "is-conv": true, - "is-room": false, - "to": "5541c02ce4b0f83f4d44414e", - "bin": false, - "from-ip": "202.117.15.217" - } - // ... -] -``` - -To look up all the messages sent by a user, use `GET /rtm/clients/{client_id}/messages`. -To look up all the messages sent through your application, use `GET /rtm/messages`. - -### Chat Rooms: Updating Messages - -This interface can only be accessed with the master key. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages/{message_id} -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender | -| message | Required | The message | -| timestamp | Required | The timestamp of the message | - -If successful, the `200 OK` status code will be returned. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Chat Rooms: Recalling Messages - -This interface can only be accessed with the master key. SDK support is needed for requests sent to this interface to take effect. Refer to the interface for updating messages for more information. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages/{message_id}/recall -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender | -| timestamp | Required | The timestamp of the message | - -If successful, the `200 OK` status code will be returned. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Deleting Messages - -This interface can only be accessed with the master key. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'from_client=some-client-id' \ - --data-urlencode 'timestamp=123' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages/{message_id} -``` - -Keep in mind that this interface will only delete messages on the server and won’t affect clients. - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender | -| timestamp | Required | The timestamp of the message | - -Response: - -```json -{} -``` - -## System Conversations - -### Creating System Conversations - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"My First Service-conversation"}' \ - https://{{host}}/1.2/rtm/service-conversations -``` - -See [Instant Messaging Overview](/sdk/im/guide/overview/) for more information about the attributes of a conversation. - -The response looks like - -```json -{ - "objectId": "5a5d7432c3422b31ed845e75", - "createdAt": "2018-01-16T03:40:32.814Z" -} -``` - -### Retrieving System Conversations - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'where={"name": "service"}' \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/service-conversations -``` - -| Parameter | Constraint | Description | -| ----- | ---- | ------------------------------------------------------------------------ | -| skip | Optional | -| limit | Optional | Can be used for pagination when used together with `skip` | -| where | Optional | See [Data Storage REST API](/sdk/storage/guide/rest/) for more information | - -The response looks like - -```json -{ - "results": [ - { - "name": "My First Service-conversation", - "createdAt": "2018-01-17T04:15:33.386Z", - "updatedAt": "2018-01-17T04:15:33.386Z", - "objectId": "5a5ecde6c3422b738c8779d7" - } - ] -} -``` - -### Updating System Conversations - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"Updated Service-conversation"}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id} -``` - -The response looks like - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### Deleting System Conversations - -With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id} -``` - -The response looks like - -```json -{} -``` - -### Subscribing System Conversations - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_id":"client_id"}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers -``` - -The response looks like - -```json -{} -``` - -### Unsubscribing System Conversations - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/{client_id} -``` - -The response looks like - -```json -{} -``` - -### Retrieving Subscribers - -This interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers -``` - -| Parameter | Constraint | Description | -| --------- | ---- | ------------------------------------------------------------------------------------------------------------ | -| limit | Optional | Used to limit the number of records returned. Defaults to 50. Can be no more than 50. | -| client_id | Optional | The client ID to start retrieving from. If left blank, the system will start from the first subscriber in the list. The result will not contain the client ID specified here. | - -The response looks like - -```json -[ - { - "timestamp": 1491467841116, - "subscriber": "client id 1", - "conv_id": "55b871" - }, - { - "timestamp": 1491467852768, - "subscriber": "client id 2", - "conv_id": "55b872" - } - // ... -] -``` - -Here `timestamp` is the time the user subscribed to the system conversation. `subscriber` is the client ID of the subscriber. If the result doesn’t contain all the subscribers and you need to retrieve more records, you can send another request with the last client ID from the result as the `client_id` of the request. - -### Retrieving Numbers of Subscribers - -This interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/count -``` - -The response looks like - -```json -{ "count": 100 } -``` - -### Messaging All Subscribers - -This interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/broadcasts -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------------------------------------------------------------------------- | -| from_client | Required | The client ID of the sender. | -| message | Required | The message content. | -| push | Optional | The content for push notifications. If specified, all iOS and Android users will receive a push notification with this content. String or JSON object. | - -Response: - -```json -{ "msg-id": "qNkRkFWOeSqP65S9fDyHJw", "timestamp": 1495431811151 } -``` - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Updating Messages Sent to All Subscribers - -This interface can only be accessed with the master key. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id} -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender. | -| message | Required | The message content. | -| timestamp | Required | The timestamp of the message. | - -If successful, the `200 OK` status code will be returned. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Recalling Messages Sent to All Subscribers - -This interface can only be accessed with the master key. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id}/recall -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender. | -| timestamp | Required | The timestamp of the message. | - -If successful, the `200 OK` status code will be returned. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Messaging Specific Users - -This interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages -``` - -**Attention**: Due to the administrative essence of this interface, when you send messages with this interface, our system won’t check if the **from_client** has the permission to send messages to the conversation. -If you are using rich media messages in your application, please make sure to have the **message** field follow the required format. - -| Parameter | Constraint | Description | -| ----------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| from_client | Required | The client ID of the sender. | -| to_clients | Required | An array of client IDs that will receive the message. Can contain at most 20 client IDs. | -| message | Required | The content of the message. Although the value of this field is a string, there is no limit on the format of the string. Therefore, you can define messages with different formats using this field, as long as the size of the value of this field doesn’t exceed the limit of 5 KB. | -| transient | Optional | Whether the message is transient. Defaults to `false`. | -| no_sync | Optional | By default, the message will be synced to the client of the `from_client` that is online. You can disable this feature by setting this property to `true`. | -| push_data | Optional | The content used for push notifications. If the receiver uses an iOS device and is not online, the value of this property will be used for sending push notifications. See [2. Advanced Messaging Features, Push Notifications, Synchronization, and Multi-Device Sign-on](/sdk/im/guide/intermediate/) for more information. | -| priority | Optional | The priority of the message. Could be `high`, `normal`, or `low`. The value is case-insensitive. Defaults to `normal`. This parameter is only valid if the message is transient or is sent to a chat room. When there is high traffic on the server or users’ devices, messages with high priorities will still be queued. | - -Response: - -By default, messages are sent asynchronously with the API. You will receive the ID of the message along with the server timestamp, like `{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151}`. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Updating Messages Sent to Specific Users - -This interface can only be accessed with the master key. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123, "to_clients":["a","b","c"]}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id} -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ----------------------------------------------------------------------- | -| from_client | Required | The client ID of the sender. | -| message | Required | The message content. | -| timestamp | Required | The timestamp of the message. | -| to_clients | Required | An array of client IDs that will receive the message. Can contain at most 20 client IDs. | - -If successful, the `200 OK` status code will be returned. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Recalling Messages Sent to Specific Users - -This interface can only be accessed with the master key. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123, "to_clients":["a","b","c"]}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id}/recall -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ----------------------------------------------------------------------- | -| from_client | Required | The client ID of the sender. | -| timestamp | Required | The timestamp of the message. | -| to_clients | Required | An array of client IDs that will receive the message. Can contain at most 20 client IDs. | - -If successful, the `200 OK` status code will be returned. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Deleting Messages Sent to Specific Users - -Invoking this interface requires the master key. You can only delete messages sent through system conversations or sent to specific users. You cannot delete broadcast messages with this interface. -To delete broadcast messages, use `DELETE /1.2/rtm/broadcasts/{message_id}` instead. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'from_client=some-client-id' \ - --data-urlencode 'timestamp=123' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/{client_id}/messages/{message_id} -``` - -Keep in mind that this interface will only delete messages on the server and won’t affect clients. - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender. | -| timestamp | Required | The timestamp of the message. | - -Response: - -```json -{} -``` - -### Retrieving Messages Sent to Specific Users - -This interface can only be accessed with the master key. The result contains messages sent to all users as well as those sent to specific users. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/{client_id}/messages -``` - -The parameters and response formats of this interface are the same as those for retrieving history messages. - -## Users - -### Retrieving Online Members - -This interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/clients/check-online -``` - -| Parameter | Constraint | Description | -| ---------- | ---- | ----------------------------------- | -| client_ids | Required | A list of client IDs (no more than 20) to look up. | - -The response contains the IDs of the online members - -```json -{ "results": ["client1"] } -``` - -Keep in mind that this interface doesn’t check if a user exists. If a user doesn’t exist, the user will be considered offline. - -### Retrieving Numbers of Unread Messages - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'conv_id=...' \ - https://{{host}}/1.2/rtm/clients/{client_id}/unread-count -``` - -| Parameter | Constraint | Description | -| ------- | ---- | ----------------------------------------------------------- | -| conv_id | Optional | Conversation ID. If omitted, the numbers of unread messages of all conversations will be returned. | - -The response looks like - -```json -{ "count": 1 } -``` - -### Forcing Users to Log Out - -This interface can only be accessed with the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"reason": "why"}' \ - https://{{host}}/1.2/rtm/clients/{client_id}/kick -``` - -| Parameter | Constraint | Description | -| ------ | ---- | ---------------------------------- | -| reason | Optional | A string indicating the reason. Can contain more than 20 characters. | - -The response looks like - -```json -{} -``` - -### Retrieving Subscribed System Conversations - -This interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'conv_id=...' \ - --data-urlencode 'timestamp=...' \ - --data-urlencode 'limit=...' \ - --data-urlencode 'direction=...' \ - https://{{host}}/1.2/rtm/clients/{client_id}/service-conversations -``` - -| Parameter | Constraint | Type | Description | -| --------- | ---- | ------ | -------------------------------------------------------------------------------------------------------------------------------- | -| conv_id | Optional | String | The conversation ID to start retrieving from. If left blank, the system will start from the first conversation in the list. The result will not contain the conversation specified here. | -| timestamp | Optional | Number | The subscription time to start from (in milliseconds). Although this parameter is optional, it becomes required when `conv_id` is provided and the value shall be the time that matches the `conv_id` specified. | -| limit | Optional | Number | Limit the number of records returned. Defaults to 50. | -| direction | Optional | String | The order to sort the result. `old` means descending order and `new` means ascending order. Defaults to `new`. If using `old`, the latest subscribed conversation will be returned first; if using `new`, the earliest subscribed conversation will be returned first. | - -The response contains the system conversations subscribed by the target user: - -```json -[ - { "timestamp": 1482994126561, "subscriber": "XXX", "conv_id": "convId1" }, - { "timestamp": 1491467945277, "subscriber": "XXX", "conv_id": "convId2" } - // ... -] -``` - -Here `timestamp` is the time the user subscribed to the system conversation. `subscriber` is the client ID of the subscriber. If the result doesn’t contain all the conversations and you need to retrieve more records, you can send another request with the last conversation ID from the result as the `conv_id` of the request and its timestamp as the `timestamp` of the request. - -### Retrieving Messages Sent by Users - -This interface can only be accessed with the master key. -Use this interface to retrieve all the messages sent from a `client_id` to one-on-one chats, group chats, and chat rooms. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/clients/{client_id}/messages -``` - -The parameters and response formats of this interface are the same as those for `GET /1.2/rtm/conversations/{conv_id}/messages`. - -### Obtaining Signatures for Logging In - -If your app uses `LCUser`, you can have your app quickly authenticate users with this interface. -This feature is disabled by default. You can enable it by going to **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Instant Messaging settings** and enabling **Verify signatures for logging in**. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'session_token=some-token' \ - https://{{host}}/1.2/rtm/clients/sign -``` - -| Parameter | Constraint | Description | -| ------------- | ---- | ---------------------- | -| session_token | Required | The `sessionToken` of the `LCUser` | - -The response looks like - -```json -{ - "signature": "bc884dbb617aab1efc228229210e487330abfc7d", - "nonce": "akywke3f28", - "client_id": "5fb4ff18d0deed36ea501c8a", - "timestamp": 1614237989966 -} -``` - -Keep in mind that although this interface takes `GET` requests, requests are not idempotent. Each invocation will get you a different signature. - -This interface comes with the `_rtmClientSign` hook, which gets invoked once `sessionToken` is validated. The argument passed into the hook is a JSON object that represents the `LCUser`: - -```json -{ - "email": "", - "sessionToken": "", - "updatedAt": "", // Format: 2017-07-11T07:58:10.149Z - "phone": "", - "objectId": "", - "username": "", - "createdAt": "", // Format: 2017-07-11T07:58:10.149Z - "emailVerified": true, // true/false - "mobilePhoneVerified": true // true/false -} -``` - -There are two possible results: - -```json -{"result": true} // Allow to sign -{"result": false, "error": "error message"} // Reject to sign -``` - -## Global APIs - -### Retrieving User Count - -This interface will return the number of online users as well as the number of users who logged in today. Invoking this interface requires the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/stats -``` - -The response looks like - -```json -{ "result": { "online_user_count": 10212, "user_count_today": 1002324 } } -``` - -Here `online_user_count` is the number of online users and `user_count_today` is the number of users who logged in today. - -### Retrieving All Conversations - -This interface will return all the conversations, including one-on-one chats, group chats, chat rooms, and system conversations. With the default ACL settings of the `_Conversation` table, this interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/all-conversations -``` - -| Parameter | Constraint | Description | -| ----- | ---- | ------------------------------------------------------------------------ | -| skip | Optional | -| limit | Optional | Can be used for pagination when used together with `skip` | -| where | Optional | See [Data Storage REST API](/sdk/storage/guide/rest/) for more information | - -The response looks like - -```json -{ - "results": [ - { - "name": "conversation", - "createdAt": "2018-01-17T04:15:33.386Z", - "updatedAt": "2018-01-17T04:15:33.386Z", - "objectId": "5a5ecde6c3422b738c8779d7" - } - ] -} -``` - -### Sending Broadcast Messages - -This interface can be used to send broadcast messages to all the clients in your application. You can send at most 30 broadcast messages everyday. Invoking this interface requires the master key. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "1a", "message": "{\"_lctype\":-1,\"_lctext\":\"This is a text message\",\"_lcattrs\":{\"a\":\"_lcattrs can be used to store custom key-value pairs\"}}", "conv_id": "..."}' \ - https://{{host}}/1.2/rtm/broadcasts -``` - -| Parameter | Constraint | Type | Description | -| ----------- | ---- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| from_client | Required | String | The client ID of the sender. | -| conv_id | Required | String | The ID of the conversation. For system conversations only. | -| message | Required | String | The content of the message. Although the value of this field is a string, there is no limit on the format of the string. Therefore, you can define messages with different formats using this field, as long as the size of the value of this field doesn’t exceed the limit of 5 KB. | -| valid_till | Optional | Number | Expiration time in UTC timestamp (milliseconds). Can be no later than 1 month. Defaults to 1 month. | -| push | Optional | String or JSON object | The content for push notifications. If specified, **all** iOS and Android users will receive a push notification with this content. | -| transient | Optional | Boolean | Whether the message is transient. Defaults to `false`. Transient messages can only be received by online users. Offline users won’t see those messages when they get online. | - -Push 的格式与《推送 REST API 指南》的《消息内容参数》一节中 `data` 下面的部分一致。如果你需要指定开发证书推送,需要在 push 的 json 中设置 `"_profile": "dev"`,例如: - -```json -{ - "alert": "消息内容", - "category": "通知分类名称", - "badge": "Increment", - "_profile": "dev" -} -``` - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Updating Broadcast Messages - -This interface can only be accessed with the master key. - -Updating broadcast messages only works for devices that haven’t received the messages yet. Devices that have already received the messages won’t see the updated messages, so please be careful when sending broadcast messages. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id} -``` - -| Parameter | Constraint | Description | -| ----------- | ---- | ---------------------- | -| from_client | Required | The client ID of the sender. | -| message | Required | The message content. | -| timestamp | Required | The timestamp of the message. | - -If successful, the `200 OK` status code will be returned. - -Rate limits: - -This interface has rate limits enforced. See [Rate Limits](#rate-limits) for more information. - -### Deleting Broadcast Messages - -This API can be used to delete published broadcast messages. Deleting broadcast messages only works for devices that haven’t received the messages yet. Devices that have already received the messages won’t see the messages deleted. Invoking this interface requires the master key. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/broadcasts/{message_id} -``` - -| Parameter | Constraint | Description | -| ---------- | ---- | ----------------------- | -| message_id | Required | The ID of the message to be deleted. String. | - -If successful, the `200 OK` status code will be returned. - -### Retrieving Broadcast Messages - -This API can be used to retrieve the valid broadcast messages. Invoking this interface requires the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/broadcasts?conv_id={conv_id} -``` - -| Parameter | Constraint | Description | -| ------- | ---- | ---------------------- | -| conv_id | Required | The ID of the system conversation. | -| limit | Optional | The number of messages returned. | -| skip | Optional | The number of messages skipped. Used for pagination. | - -### Retrieving All History Messages in an Application - -This interface can only be accessed with the master key. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/messages -``` - -The parameters and response formats of this interface are the same as those for `GET /1.2/rtm/conversations/{conv_id}/messages`. - -## Rich Media Messages - -The only difference between the parameter format of [rich media messages](/sdk/im/guide/overview/#default-types-for-rich-media-messages) and that for basic messages is that the value of the `message` parameter for a rich media message is a string containing a JSON. -See [Instant Messaging Overview · Default Types for Rich Media Messages](/sdk/im/guide/overview/#default-types-for-rich-media-messages) for the meanings of the fields in the JSON. -Below are some examples of rich media messages derived from predefined types that are serialized to JSON objects. - -### Text - -```json -{ - "_lctype": -1, - "_lctext": "This is a text message", - "_lcattrs": { - "a": "_lcattrs can be used to store custom key-value pairs" - } -} -``` - -### Image - -```json -{ - "_lctype": -2, // Required parameter - "_lctext": "Image description", - "_lcattrs": { - "a": "_lcattrs can be used to store custom key-value pairs", - "b": true, - "c": 12 - }, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", // Required parameter - "objId": "54699d87e4b0a56c64f470a4", // The LCFile.objectId of the file - "metaData": { - "name": "IMG_20141223.jpeg", // Image name - "format": "png", // Image format - "height": 768, // Pixels - "width": 1024, // Pixels - "size": 18 // b - } - } -} -``` - -Above is a full example. To only send the URL of an image: - -```json -{ - "_lctype": -2, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25" - } -} -``` - -### Audio - -```json -{ - "_lctype": -3, - "_lctext": "This is an audio message", - "_lcattrs": { - "a": "_lcattrs can be used to store custom key-value pairs" - }, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", - "objId": "54699d87e4b0a56c64f470a4", // The LCFile.objectId of the file - "metaData": { - "name": "Never Gonna Give You Up.wav", - "format": "wav", - "duration": 26, // Seconds - "size": 2738 // b - } - } -} -``` - -Simplified version: - -```json -{ - "_lctype": -3, - "_lcfile": { - "url": "http://www.somemusic.com/x.mp3" - } -} -``` - -### Video - -```json -{ - "_lctype": -4, - "_lctext": "This is a video message", - "_lcattrs": { - "a": "_lcattrs can be used to store custom key-value pairs" - }, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/99de0f45-171c-4fdd-82b8-1877b29bdd12", - "objId": "54699d87e4b0a56c64f470a4", // The LCFile.objectId of the file - "metaData": { - "name": "video.mov", - "format": "avi", - "duration": 168, // Seconds - "size": 18689 // b - } - } -} -``` - -Simplified version: - -```json -{ - "_lctype": -4, - "_lcfile": { - "url": "http://www.somevideo.com/Y.flv" - } -} -``` - -### File - -```json -{ - "_lctype": -6, - "_lctext": "This is a file", - "_lcattrs": { - "a": "_lcattrs can be used to store custom key-value pairs" - }, - "_lcfile": { - "url": "http://www.somefile.com/resume.doc", - "name": "resume.doc", - "size": 18689 // b - } -} -``` - -Simplified version: - -```json -{ - "_lctype": -6, - "_lcfile": { - "url": "http://www.somefile.com/resume.doc", - "name": "resume.doc" - } -} -``` - -### Location - -```json -{ - "_lctype": -5, - "_lctext": "This is a location message", - "_lcattrs": { - "a": "_lcattrs can be used to store custom key-value pairs" - }, - "_lcloc": { - "longitude": 23.2, - "latitude": 45.2 - } -} -``` - -Simplified version: - -```json -{ - "_lctype": -5, - "_lcloc": { - "longitude": 23.2, - "latitude": 45.2 - } -} -``` - -## Rate Limits - -Rate limits are enforced on all the REST APIs on this page that perform operations on messages (**client SDKs are not affected by these limits**). Those limits are listed below: - -### Basic Messages - -Version 1.1: - -- Sending messages and having system conversations send messages to users (`/1.1/rtm/messages`) -- Updating and recalling messages (`/1.1/rtm/patch/message`) - -Version 1.2: - -- [One-On-One Chats and Group Chats: Sending Messages](#one-on-one-chats-and-group-chats-sending-messages) -- [One-On-One Chats and Group Chats: Updating Messages](#one-on-one-chats-and-group-chats-updating-messages) -- [One-On-One Chats and Group Chats: Recalling Messages](#one-on-one-chats-and-group-chats-recalling-messages) -- [Chat Rooms: Sending Messages](#chat-rooms-sending-messages) -- [Chat Rooms: Updating Messages](#chat-rooms-updating-messages) -- [Chat Rooms: Recalling Messages](#chat-rooms-recalling-messages) -- [System Conversations: Messaging Specific Users](#messaging-specific-users) -- [System Conversations: Updating Messages Sent to Specific Users](#updating-messages-sent-to-specific-users) -- [System Conversations: Recalling Messages Sent to Specific Users](#recalling-messages-sent-to-specific-users) - -#### Limits - -| Business Plan (per application) | Developer Plan (per application) | -| -------------------------------------- | ---------------- | -| No more than 9000 requests per minute; defaults to 1800 requests per minute | 120 requests per minute | - -The limit is shared by all interfaces. If the rate limit is exceeded, the server will reject further requests with the 429 error code until 1 minute later. - -If your application has a Business Plan, you can edit its limit by going to **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Thresholds > Frequency limit for calling API for basic messages**. -You will be billed each day according to the peak request rate that occurred on the day: - -| Requests per minute | Price | -| -------------- | ----------- | -| 0 to 1800 | Free | -| 1801 to 3600 | ¥20 CNY per day | -| 3601 to 5400 | ¥30 CNY per day | -| 5401 to 7200 | ¥40 CNY per day | -| 7201 to 9000 | ¥50 CNY per day | - -International version - -| Requests per minute | Price | -| -------------- | ------------ | -| 0 to 1800 | Free | -| 1801 to 3600 | $6 USD per day | -| 3601 to 5400 | $9 USD per day | -| 5401 to 7200 | $12 USD per day | -| 7201 to 9000 | $15 USD per day | - -The peak request rate that occurred on each day can be viewed on **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Statistics > REST API Peak QPM**. - -### Messages Sent From System Conversations - -Version 1.1: - -- Sending messages from system conversations (`/1.1/rtm/broadcast/subscriber`) - -Version 1.2: - -- [Messaging All Subscribers](#messaging-all-subscribers) -- [Updating Messages Sent to All Subscribers](#updating-messages-sent-to-all-subscribers) -- [Recalling Messages Sent to All Subscribers](#recalling-messages-sent-to-all-subscribers) - -#### Limits - -| Limit | Business Plan | Developer Plan | -| -------- | ------------------ | ------------------ | -| Rate Limit | 30 requests per minute per application | 10 requests per minute per application | -| Quota | 1000 requests per day | 100 requests per day | - -The limit is shared by all interfaces. If the rate limit is exceeded, the server will reject further requests with the 429 error code until 1 minute later. If the quota is exceeded, the server will reject further requests with the 429 error code until the next day. - -### Broadcast Messages - -Version 1.1: - -- Sending broadcast messages (`/1.1/rtm/broadcast`) - -Version 1.2: - -- [Sending Broadcast Messages](#sending-broadcast-messages) -- [Updating Broadcast Messages](#updating-broadcast-messages) - -#### Limits - -| Limit | Business Plan | Developer Plan | -| -------- | ------------------ | ----------------- | -| Rate Limit | 10 requests per minute per application | 1 request per minute per application | -| Quota | 30 requests per day | 10 requests per day | - -The limit is shared by all interfaces. If the rate limit is exceeded, the server will reject further requests with the 429 error code until 1 minute later. If the quota is exceeded, the server will reject further requests with the 429 error code until the next day. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/senior.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/senior.mdx deleted file mode 100644 index 37bc09957..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/senior.mdx +++ /dev/null @@ -1,1131 +0,0 @@ ---- -title: 3. Security, Chat Rooms, and Temporary Conversations -sidebar_label: Permission and Chat Rooms -sidebar_position: 3 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import Mermaid from "/src/docComponents/Mermaid"; -import { Conditional } from "/src/docComponents/conditional"; - -## Introduction - -In the previous chapter, [Advanced Messaging Features, Push Notifications, Synchronization, and Multi-Device Sign-on](/sdk/im/guide/intermediate/), we introduced a number of bonus features that you can implement beyond basic messaging. In this chapter, we will introduce more features from the perspectives of system security and permission management, including: - -- How to verify the requests made by clients with a third-party signing mechanism -- How to control the permissions each user has -- How to build a chat room with an unlimited number of people -- How to enforce text moderation on the messages sent by users -- How to implement temporary conversations - -## Signing Mechanism - -Instant Messaging is decoupled from the account system offered by Data Storage. This makes it possible for you to use Instant Messaging even though the account system of your app is not built with Data Storage. To ensure the security of your app, we offer a third-party signing mechanism that helps your app verify all the requests sent from clients. - -The mechanism comes with an authentication server (the so-called "third party") deployed between clients and the cloud. Each time a client wants to make a request involving sensitive operations (like logging in, creating conversations, joining conversations, or inviting users), it has to obtain a signature from the authentication server. The signature gets attached to the request and will be verified by the cloud according to a predefined protocol. Only those requests with valid signatures will be accepted by the cloud. - -The signing mechanism is turned off by default. You can turn it on by going to **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Instant Messaging settings**: - -- **Verify signatures for logging in**: Verify all the activities of logging in -- **Verify signatures for conversation operations**: Verify all the activities of creating conversations, joining conversations, inviting users, and removing users -- **Verify signatures for retrieving history messages**: Verify all the activities of retrieving history messages - -You are free to change the settings here based on your app's actual needs, though we highly recommend that you at least keep **verifying signatures for logging in** on, which guarantees the basic security of your app. - ->Authentication Server: 1. Apply for signature with request -Authentication Server-->>Client: 2. Return timestamp, nonce, and signature to the client -Client->>Authentication Server: 3. Send the request to the cloud with the signature -Authentication Server-->>Client: 4. Verify the signature along with the request -`} -/> - -1. When the client performs operations like logging in or creating conversations, the SDK applies for a signature by calling `SignatureFactory` with the user's information and a request containing the operations to be done. -2. The authentication server checks if the operations are performed with enough permissions. If that's true, the server will follow the [signing algorithm](#signatures-for-logging-in) that will be mentioned later to generate the timestamp, nonce, and signature, and send them back to the client. -3. The client attaches the signature to the request and sends them to the cloud. -4. The cloud verifies the signature along with the request to ensure that the operations in the request are allowed. The request will be accepted only if the signature is valid. - -The algorithm used for the signing process is **HMAC-SHA1** and the output would be the hex dump of a byte stream. For different requests, different strings with different UTC timestamps and nonces need to be constructed. - -If you are using `LCUser` in your app, you can get signatures for logging in through our REST API. - -### Formats of Signatures - -Below we will introduce the formats of strings used to obtain signatures for different types of operations. - -#### Signatures for Logging in - -Below is the format of strings for logging in. Notice that there are _two colons_ between `clientid` and `timestamp`: - -``` -appid:clientid::timestamp:nonce -``` - -| Parameter | Description | -| ----------- | ------------------------------------------------------------------------ | -| `appid` | Your App ID. | -| `clientid` | The `clientId` used for logging in. | -| `timestamp` | The number of **milliseconds** that have elapsed since Unix epoch (UTC). | -| `nonce` | A random string. | - -> Note: The key for signing has to be the **Master Key** of your app. You can find it from **Developer Center > Your game > Game Services > Configuration**. **Make sure your Master Key is well-protected and doesn't get leaked.** - -You may implement your own `SignatureFactory` to retrieve signatures from remote servers. If you don't have your own server, you may use the **web hosting** service provided by Cloud Engine. Generating signatures within your mobile app is **extremely dangerous** since your **Master Key** can get exposed. - -This signature expires in 6 hours, but it becomes invalid immediately once the client has been forced to log out. -The signature invalidness does not affect the currently connected clients. - -#### Signatures for Creating Conversations - -Below is the format of strings for creating conversations: - -``` -appid:clientid:sorted_member_ids:timestamp:nonce -``` - -- `appid`, `clientid`, `timestamp`, and `nonce` are [the same as above](#signatures-for-logging-in). -- `sorted_member_ids` is a list of `clientId`s (users being invited to the conversation) arranged in **ascending order** and divided by colon (`:`). - -#### Signatures for Group Operations - -Below is the format of strings for **joining conversations**, **inviting users**, and **removing users**: - -``` -appid:clientid:convid:sorted_member_ids:timestamp:nonce:action -``` - -- `appid`, `clientid`, `sorted_member_ids`, `timestamp`, and `nonce` are the same as above. `sorted_member_ids` would be an empty string if you are creating a new conversation. -- `convid` is the conversation ID. -- `action` is the operation being performed: `invite` means joining a conversation or inviting users and `kick` means removing users. - -#### Signatures for Retrieving Message Histories - -``` -appid:client_id:convid:nonce:timestamp -``` - -The meanings of these parameters are the same as above. - -This signature is only used for REST API. It is not applicable to client-side SDKs. - -### Demo for Generating Signatures on Cloud Engine - -To help you better understand the signing algorithm, we made a server-side signing program based on Node.js and Cloud Engine. It's available [here](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/rtm-signature.js) for you to study and use. - -### Supporting Signatures on the Client Side - -So far we have been talking about the protocol used by the authentication server to generate signatures. Now let's see what we need to do with the client side to make the entire signing mechanism work. - -The SDK reserves a factory interface `Signature` for each `AVIMClient` instance. To enable signing, implement the interface with a class that calls the signing method on the authentication server to get signatures, and then bind the class to the `AVIMClient` instance: - - - -```cs -public class LocalSignatureFactory : ILCIMSignatureFactory { - const string MasterKey = "pyvbNSh5jXsuFQ3C8EgnIdhw"; - - public Task CreateConnectSignature(string clientId) { - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, string.Empty, timestamp.ToString(), nonce); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - public Task CreateStartConversationSignature(string clientId, IEnumerable memberIds) { - string sortedMemberIds = string.Empty; - if (memberIds != null) { - List sortedMemberList = memberIds.ToList(); - sortedMemberList.Sort(); - sortedMemberIds = string.Join(":", sortedMemberList); - } - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, sortedMemberIds, timestamp.ToString(), nonce); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - public Task CreateConversationSignature(string conversationId, string clientId, IEnumerable memberIds, string action) { - string sortedMemberIds = string.Empty; - if (memberIds != null) { - List sortedMemberList = memberIds.ToList(); - sortedMemberList.Sort(); - sortedMemberIds = string.Join(":", sortedMemberList); - } - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, conversationId, sortedMemberIds, timestamp.ToString(), nonce, action); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - private static string SignSHA1(string key, string text) { - HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key)); - byte[] bytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(text)); - string signature = BitConverter.ToString(bytes).Replace("-", string.Empty); - return signature; - } - - private static string NewNonce() { - byte[] bytes = new byte[10]; - using (RandomNumberGenerator generator = RandomNumberGenerator.Create()) { - generator.GetBytes(bytes); - } - return Convert.ToBase64String(bytes); - } - - private static string GenerateSignature(params string[] args) { - string text = string.Join(":", args); - string signature = SignSHA1(MasterKey, text); - return signature; - } -} - -// Specify the signature factory -LCIMClient tom = new LCIMClient("tom", signatureFactory: new LocalSignatureFactory()); -``` - -```java -// An example of performing signing with Cloud Engine -public class KeepAliveSignatureFactory implements SignatureFactory { - @Override - public Signature createSignature(String peerId, List watchIds) throws SignatureException { - Map params = new HashMap(); - params.put("self_id",peerId); - params.put("watch_ids",watchIds); - - try{ - Object result = LCCloud.callFunction("sign",params); - if(result instanceof Map){ - Map serverSignature = (Map) result; - Signature signature = new Signature(); - signature.setSignature((String)serverSignature.get("signature")); - signature.setTimestamp((Long)serverSignature.get("timestamp")); - signature.setNonce((String)serverSignature.get("nonce")); - return signature; - } - }catch(LCException e){ - throw (SignatureFactory.SignatureException) e; - } - return null; - } - - @Override - public Signature createConversationSignature(String convId, String peerId, - List targetPeerIds,String action) throws SignatureException{ - Map params = new HashMap(); - params.put("client_id",peerId); - params.put("conv_id",convId); - params.put("members",targetPeerIds); - params.put("action",action); - - try{ - Object result = LCCloud.callFunction("sign2",params); - if(result instanceof Map){ - Map serverSignature = (Map) result; - Signature signature = new Signature(); - signature.setSignature((String)serverSignature.get("signature")); - signature.setTimestamp((Long)serverSignature.get("timestamp")); - signature.setNonce((String)serverSignature.get("nonce")); - return signature; - } - }catch(LCException e){ - throw (SignatureFactory.SignatureException) e; - } - return null; - } -} - -// Bind an instance of the signature factory class to LCIMClient -LCIMOptions.getGlobalOptions().setSignatureFactory(new KeepAliveSignatureFactory()); -``` - -```objc -// Implement the LCIMSignatureDataSource protocol -- (void)client:(LCIMClient *)client - action:(LCIMSignatureAction)action - conversation:(LCIMConversation * _Nullable)conversation - clientIds:(NSArray * _Nullable)clientIds -signatureHandler:(void (^)(LCIMSignature * _Nullable))handler -{ - if ([action isEqualToString:LCIMSignatureActionOpen]) { - // For modules with signing enabled, return the corresponding signature - LCIMSignature *signature; - /* - ... - ... - See "Demo for Generating Signatures on Cloud Engine" - */ - handler(signature); - } else { - // For modules with signing disabled, return nil - handler(nil); - } -} - -// Set the protocol delegator -NSError *error; -LCIMClient *imClient = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error]; -if (!error) { - imClient.signatureDataSource = signatureDelegator; -} -``` - -```js -// A Cloud Engine-based signature factory for signing requests for logging in -var signatureFactory = function (clientId) { - return AV.Cloud.rpc("sign", { clientId: clientId }); // AV.Cloud.rpc returns a Promise -}; -// A Cloud Engine-based signature factory for signing requests for creating conversations, joining conversations, inviting users, and removing users -var conversationSignatureFactory = function ( - conversationId, - clientId, - targetIds, - action -) { - return AV.Cloud.rpc("sign-conversation", { - conversationId: conversationId, - clientId: clientId, - targetIds: targetIds, - action: action, - }); -}; - -realtime - .createIMClient("Tom", { - signatureFactory: signatureFactory, - conversationSignatureFactory: conversationSignatureFactory, - }) - .then(function (tom) { - console.log("Tom logged in."); - }) - .catch(function (error) { - // Errors thrown by signatureFactory or for invalid signatures will be caught here - }); -``` - -```swift -class SignatureDelegator: IMSignatureDelegate { - - // A Cloud Engine-based function for getting signatures for logging in - func getClientOpenSignature(completion: (IMSignature) -> Void) { - // See "Demo for Generating Signatures on Cloud Engine" - } - - func client(_ client: IMClient, action: IMSignature.Action, signatureHandler: @escaping (IMClient, IMSignature?) -> Void) { - switch action { - case .open: - // For modules with signing enabled, return the corresponding signature - self.getClientOpenSignature { (signature) in - signatureHandler(client, signature) - } - default: - // For modules with signing disabled, return nil - signatureHandler(client, nil) - } - } -} - -do { - let signatureDelegator = SignatureDelegator() - let client = try IMClient(ID: "Tom", signatureDelegate: signatureDelegator) -} catch { - print(error) -} -``` - -```dart - -``` - - - -You should never perform signing using your Master Key on the client side. If your Master Key gets leaked, the data in your app would be accessible by anyone who has the key. Therefore, we highly recommend that you host the signing program on a server that is well-secured (like Cloud Engine). - -### Signing Mechanism for `User` - -`User` is the built-in account system coming with Data Storage. If your users have their accounts signed up or logged in with `User`, they can skip the signing process when logging in to Instant Messaging. The code below shows how a user can log in to Instant Messaging with `User`: - - - -```cs -LCUser user = await LCUser.Login("username", "password"); -CIMClient client = new LCIMClient(user); -await client.Open(); -``` - -```java -// Log in to the account system with the username and password of an LCUser -LCUser.logInInBackground("username", "password", new LogInCallback() { - @Override - public void done(LCUser user, LCException e) { - if (null != e) { - return; - } - // Create a client with the LCUser instance - LCIMClient client = LCIMClient.getInstance(user); - // Log in to Instant Messaging - client.open(new LCIMClientCallback() { - @Override - public void done(final LCIMClient avimClient, LCIMException e) { - // Do something as you need - } - }); - } -}); -``` - -```objc -// Log in to the account system with the username and password of an LCUser -[LCUser logInWithUsernameInBackground:username password:password block:^(LCUser * _Nullable user, NSError * _Nullable error) { - // Create a client with the LCUser instance - NSError *err; - LCIMClient *client = [[LCIMClient alloc] initWithUser:user error:&err]; - if (!err) { - // Log in to Instant Messaging - [client openWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - // Do something as you need - }]; - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -// Log in to the account system with the username and password of an LCUser -AV.User.logIn("username", "password") - .then(function (user) { - // Log in to Instant Messaging with the LCUser instance - return realtime.createIMClient(user); - }) - .catch(console.error.bind(console)); -``` - -```swift -_ = LCUser.logIn(username: "username", password: "password") { (result) in - switch result { - case .success(object: let user): - do { - let client = try IMClient(user: user) - client.open(completion: { (result) in - // Do something as you need - }) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -// Not supported yet -``` - - - -When creating `IMClient` with an `LCUser` instance that has completed the `logIn` process, the user's signature information can be directly accessed by Instant Messaging from the account system. This allows Instant Messaging to automatically verify the client being logged in and the process of applying for signatures from the third-party server can be skipped. - -Once `IMClient` is logged in, all the other features work in the same way as discussed earlier. - -## Chat Rooms - -We have compared different types of scenarios and conversations in our service overview. Now let's learn how to build a chat room. - -### Creating Chat Rooms - -`IMClient` has the `createChatRoom` method for creating chat rooms: - - - -```cs -// Pass in the name of the chat room -tom.CreateChatRoom("Chat Room"); -``` - -```java -tom.createChatRoom("Chat Room", null, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conv, LCIMException e) { - if (e == null) { - // Chat room created - } - } -}); -``` - -```objc -[client createChatRoomWithCallback:^(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error) { - if (chatRoom && !error) { - LCIMTextMessage *textMessage = [LCIMTextMessage messageWithText:@"This is a message." attributes:nil]; - [chatRoom sendMessage:textMessage callback:^(BOOL success, NSError *error) { - if (success && !error) { - - } - }]; - } -}]; -``` - -```js -tom.createChatRoom({ name: "Chat Room" }).catch(console.error); -``` - -```swift -do { - try client.createChatRoom(name: "Chat Room", attributes: nil) { (result) in - switch result { - case .success(value: let chatRoom): - print(chatRoom) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -ChatRoom chatRoom = await jerry.createChatRoom(name: 'Chat Room'); -``` - - - -When creating a chat room, you can specify its name and optional attributes. The interface for creating chat rooms has the following differences compared to that for creating basic conversations: - -- A chat room doesn't have a member list, so there is no need to specify `members`. -- For the same reason, there is no need to specify `unique` (the cloud doesn't need to merge conversations by member lists). - -> Although it's possible to create a chat room by passing `{ transient: true }` into `createConversation`, we still recommend that you use `createChatRoom` directly. - -### Finding Chat Rooms - -In the first chapter, we have discussed how you can use `ConversationsQuery` to look for conversations with your custom conditions. This works for chat rooms as well, as long as you add `transient = true` as a constraint. - - - -```cs -LCIMConversationQuery query = new LCIMConversationQuery(tom); -query.WhereEqualTo("tr", true); -``` - -```java -LCIMConversationsQuery query = tom.getChatRoomQuery(); -query.findInBackground(new LCIMConversationQueryCallback() { - @Override - public void done(List conversations, LCIMException e) { - if (null == e) { - // Success - } else { - // Error handling - } - } -}); -``` - -```objc -LCIMConversationQuery *query = [tom conversationQuery]; -[query whereKey:@"tr" equalTo:@(YES)]; -``` - -```js -var query = tom.getQuery().equalTo("tr", true); // Restrict to chat rooms -query - .find() - .then(function (conversations) { - // conversations contains all the results - }) - .catch(console.error); -``` - -```swift -do { - let query = client.conversationQuery - try query.where("tr", .equalTo(true)) - try query.findConversations { (result) in - switch result { - case .success(value: let conversations): - guard conversations is [IMChatRoom] else { - return - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - ConversationQuery query = tom.conversationQuery(); - query.whereEqualTo('tr', true); - // conversations contains all the results - List conversations = await query.find(); -} catch (e) { - print(e); -} -``` - - - -> The Java (Android) SDK offers the `LCIMClient#getChatRoomQuery` method that is dedicated to querying chat rooms. By using this method, you won't need to deal with the `transient` attribute of conversations. - -### Joining and Leaving Chat Rooms - -When coming to the interfaces for joining or leaving conversations, group chats are the same as basic conversations. See [Group Chats](/sdk/im/guide/beginner/) in the first chapter for more details. - -However, there are several differences in the ways members are managed and notifications are delivered: - -- A user cannot be invited to or removed from a chat room. They are only able to join or leave on their own. -- If a user logs out, this user will be automatically removed from the chat room they are already in. An exception is that if the user gets offline unexpectedly, they will be added back to the chat room they are previously in as long as they get back within 30 minutes. -- The cloud will not deliver notifications for users joining or leaving chat rooms. -- The list of members in a chat room cannot be retrieved. Only the count of members is available. - -As a side note, functions like **push notifications, message synchronization, and receipts are also not supported by chat rooms**. - -### Getting Member Counts - -The `LCIMConversation#memberCount` method lets you get the count of members in a conversation. When used on a chat room, you get the number of people in it at that moment: - - - -```cs -int membersCount = await conversation.GetMembersCount(); -``` - -```java -private void TomQueryWithLimit() { - LCIMClient tom = LCIMClient.getInstance("Tom"); - tom.open(new LCIMClientCallback() { - - @Override - public void done(LCIMClient client, LCIMException e) { - if (e == null) { - // Successfully logged in - LCIMConversationsQuery query = tom.getConversationsQuery(); - query.setLimit(1); - // Get the first conversation - query.findInBackground(new LCIMConversationQueryCallback() { - @Override - public void done(List convs, LCIMException e) { - if (e == null) { - if (convs != null && !convs.isEmpty()) { - LCIMConversation conv = convs.get(0); - // Get the count of people in the first conversation - conv.getMemberCount(new LCIMConversationMemberCountCallback() { - - @Override - public void done(Integer count, LCIMException e) { - if (e == null) { - Log.d("Tom & Jerry has " + count + " people online"); - } - } - }); - } - } - } - }); - } - } - }); -} -``` - -```objc -// Get the count of people -[conversation countMembersWithCallback:^(NSInteger number, NSError *error) { - NSLog(@"%ld",number); -}]; -``` - -```js -chatRoom - .count() - .then(function (count) { - console.log("Count: " + count); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - chatRoom.getOnlineMembersCount { (result) in - switch result { - case .success(count: let count): - print(count) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -int count = await chatRoom.countMembers(); -``` - - - -### Message Priorities - -To ensure that important messages get delivered promptly, the server would selectively discard a certain amount of messages with lower priorities when the network connection is bad. Below are the priorities supported: - -| Priority | Description | -| ------------------------ | -------------------------------------------------------------------- | -| `MessagePriority.HIGH` | High priority. Used for messages that need to be delivered promptly. | -| `MessagePriority.NORMAL` | Normal priority. Used for ordinary text messages. | -| `MessagePriority.LOW` | Low priority. Used for messages that are less important. | - -The default priority is `NORMAL`. - -The priority of a message can be set when sending the message. The code below shows how you can send a message with high priority: - - - -```cs -LCIMTextMessage message = new LCIMTextMessage("The score is still 0:0. China definitely needs a substitution for the second half."); -LCIMMessageSendOptions options = new LCIMMessageSendOptions { - Priority = LCIMMessagePriority.High -}; -await chatRoom.Send(message, options); -``` - -```java -LCIMClient tom = LCIMClient.getInstance("Tom"); - tom.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient client, LCIMException e) { - if (e == null) { - // Create a conversation named "Tom & Jerry" - client.createConversation(Arrays.asList("Jerry"), "Tom & Jerry", null, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conv, LCIMException e) { - if (e == null) { - LCIMTextMessage msg = new LCIMTextMessage(); - msg.setText("Get up, Jerry!"); - - LCIMMessageOption messageOption = new LCIMMessageOption(); - messageOption.setPriority(LCIMMessageOption.MessagePriority.High); - conv.sendMessage(msg, messageOption, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // Sent - } - } - }); - } - } - }); - } - } - }); -``` - -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.priority = LCIMMessagePriorityHigh; -[chatRoom sendMessage:[LCIMTextMessage messageWithText:@"Get up, Jerry!" attributes:nil] option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - // Things to do after the message is sent -}]; -``` - -```js -var { Realtime, TextMessage, MessagePriority } = require("leancloud-realtime"); -var realtime = new Realtime({ - appId: "GDBz24d615WLO5e3OM3QFOaV-gzGzoHsz", - appKey: "dlCDCOvzMnkXdh2czvlbu3Pk", -}); -realtime - .createIMClient("host") - .then(function (host) { - return host.createConversation({ - members: ["broadcast"], - name: "2094 FIFA World Cup - Vatican City vs China", - transient: true, - }); - }) - .then(function (conversation) { - console.log(conversation.id); - return conversation.send( - new TextMessage( - "The score is still 0:0. China definitely needs a substitution for the second half." - ), - { priority: MessagePriority.HIGH } - ); - }) - .then(function (message) { - console.log(message); - }) - .catch(console.error); -``` - -```swift -do { - let message = IMTextMessage(text: "The score is still 0:0. China definitely needs a substitution for the second half.") - try chatRoom.send(message: message, priority: .high) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = 'The score is still 0:0. China definitely needs a substitution for the second half.'; - await chatRoom.send(message: message, priority: MessagePriority.high); -} catch (e) { - print(e); -} -``` - - - -> Note: -> -> This feature is only available for _chat rooms_. There won't be an effect if you set priorities for messages in basic conversations, since these messages will never get discarded. - -### Muting Conversations - -If a user doesn't want to get notifications for new messages in a conversation but still wants to stay in the conversation, they can mute the conversation. - -For example, Tom is getting busy and wants to mute a conversation: - - - -```cs -await chatRoom.Mute(); -``` - -```java -LCIMClient tom = LCIMClient.getInstance("Tom"); -tom.open(new LCIMClientCallback(){ - - @Override - public void done(LCIMClient client,LCIMException e){ - if(e==null){ - // Logged in - LCIMConversation conv = client.getConversation("551260efe4b01608686c3e0f"); - conv.mute(new LCIMConversationCallback(){ - - @Override - public void done(LCIMException e){ - if(e==null){ - // Muted - } - } - }); - } - } -}); -``` - -```objc -// Tom mutes the conversation -[conversation muteWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"Muted!"); - } -}]; -``` - -```js -tom - .getConversation("CONVERSATION_ID") - .then(function (conversation) { - return conversation.mute(); - }) - .then(function (conversation) { - console.log("Muted!"); - }) - .catch(console.error.bind(console)); -``` - -```swift -conversation.mute { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await chatRoom.mute(); -``` - - - -After a conversation is muted, the current user will not get push notifications from it anymore. To unmute a conversation, use `Conversation#unmute`. - -> Tips: -> -> - Both chat rooms and basic conversations can be muted. -> - `mute` and `unmute` operations will change the `mu` field in the `_Conversation` class. **Do not change the `mu` field directly in your app's dashboard**, otherwise push notifications may not work properly. - -### Text Moderation - - - -You might consider filtering cuss words out from the messages sent into group chats by users. Instant Messaging offers a built-in text moderation function that supports this need. It works for group chats, chat rooms, and system conversations by default. You can also enable it for one-on-one chats by going to **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings**. - - - - - -Instant Messaging offers a built-in text moderation function that allows you to filter cuss words out from the messages sent by users. You can enable it for one-on-one chats by going to **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings**. - - - -Matched keywords will be replaced with `***`. - -Text moderation will lead to message modifications at the system level, so the message sender will receive a `MESSAGE_UPDATE` event, which the clients can listen to. Please refer to the "Modify a Message" section of the previous chapter for code samples. - - - -Instant Messaging offers a set of keywords by default, but if you have upgraded to the Business Plan, you can customize the keywords. To do so, go to **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings** and upload your keywords file to replace the default list. The uploaded file must be UTF-8 encoded with one keyword in each line. If you upload your custom word list, it will override the default one. - - - - - -If you have upgraded to the Business Plan, you can customize the keywords. To do so, go to **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings** and upload your keywords file. The uploaded file must be UTF-8 encoded with one keyword in each line. For now, the **Enable sensitive keyword filtering against group chats** option on **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings** is always enabled, but if you don't upload a file of keywords, the text moderation function will not function in the end. - - - -Filtering rules for sensitive words: If the message is a rich media message type (the `_lctype` attribute has a value), only the contents of the `_lctext` field are filtered. If the message is not a rich media message type (the message does not have the `_lctype` attribute), then the entire message body is filtered. - -If you have more complicated requirements regarding text moderation, we recommend that you make use of the `_messageReceived` hook of Cloud Engine. You can define your own logic for controlling messages. - -## Temporary Conversations - -Temporary conversations can be used for special scenarios with: - -- Short TTL -- Fewer members (10 `clientId`s maximum) -- No message history needed - -What makes temporary conversations different from other conversations is that they **expire very quickly**. This helps you reduce the space needed for storing conversations and lower the cost of maintaining your app. Temporary conversations are best used for customer service systems. - -### Creating Temporary Conversations - -`IMConversation` has its `createTemporaryConversation` method for creating temporary conversations: - - - -```cs -LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" }); -``` - -```java -tom.createTemporaryConversation(Arrays.asList(members), 3600, new LCIMConversationCreatedCallback(){ - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if (null == e) { - LCIMTextMessage msg = new LCIMTextMessage(); - msg.setText("This is a temporary conversation."); - conversation.sendMessage(msg, new LCIMConversationCallback(){ - @Override - public void done(LCIMException e) { - } - }); - } - } -}); -``` - -```objc -[self createTemporaryConversationWithClientIds:@[@"Jerry", @"William"] callback:^(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error) { - if (temporaryConversation) { - // success - } -}]; -``` - -```js -realtime - .createIMClient("Tom") - .then(function (tom) { - return tom.createTemporaryConversation({ - members: ["Jerry", "William"], - }); - }) - .then(function (conversation) { - return conversation.send( - new AV.TextMessage("This is a temporary conversation.") - ); - }) - .catch(console.error); -``` - -```swift -do { - try client.createTemporaryConversation(clientIDs: ["Jerry", "William"], timeToLive: 3600) { (result) in - switch result { - case .success(value: let tempConversation): - print(tempConversation) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -TemporaryConversation temporaryConversation; -try { - temporaryConversation = await jerry.createTemporaryConversation( - members: {'Jerry', 'William'}, - ); -} catch (e) { - print(e); -} - -try { - TextMessage message = TextMessage(); - message.text = 'This is a temporary conversation.'; - await temporaryConversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - -Temporary conversations have an **important** attribute that differentiates them from others: TTL. It is set to 1 day by default, but you can change it to any time no longer than 30 days. If you want a conversation to survive for more than 30 days, make it a basic conversation instead. The code below creates a temporary conversation with a custom TTL: - - - -```cs -LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" }, - ttl: 3600); -``` - -```java -LCIMClient client = LCIMClient.getInstance("Tom"); -client.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient avimClient, LCIMException e) { - if (null == e) { - String[] members = {"Jerry", "William"}; - avimClient.createTemporaryConversation(Arrays.asList(members), 3600, new LCIMConversationCreatedCallback(){ - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if (null == e) { - LCIMTextMessage msg = new LCIMTextMessage(); - msg.setText("This is a temporary conversation. It will expire in 1 hour."); - conversation.sendMessage(msg, new LCIMConversationCallback(){ - @Override - public void done(LCIMException e) { - } - }); - } - } - }); - } - } -}); -``` - -```objc -LCIMConversationCreationOption *option = [LCIMConversationCreationOption new]; -option.timeToLive = 3600; -[self createTemporaryConversationWithClientIds:@[@"Jerry", @"William"] option:option callback:^(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error) { - if (temporaryConversation) { - // success - } -}]; -``` - -```js -realtime - .createIMClient("Tom") - .then(function (tom) { - return tom.createTemporaryConversation({ - members: ["Jerry", "William"], - ttl: 3600, - }); - }) - .then(function (conversation) { - return conversation.send( - new AV.TextMessage( - "This is a temporary conversation. It will expire in 1 hour." - ) - ); - }) - .catch(console.error); -``` - -```swift -do { - try client.createTemporaryConversation(clientIDs: ["Jerry", "William"], timeToLive: 3600) { (result) in - switch result { - case .success(value: let tempConversation): - print(tempConversation) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -TemporaryConversation temporaryConversation; -try { - temporaryConversation = await jerry.createTemporaryConversation( - members: {'Jerry', 'William'}, - timeToLive: 3600, - ); -} catch (e) { - print(e); -} - -try { - TextMessage message = TextMessage(); - message.text = 'This is a temporary conversation. It will expire in 1 hour.'; - await temporaryConversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - -Besides this, a temporary conversation shares the same functionality as a basic conversation. - -## Continue Reading - -- [4. Hooks and System Conversations](/sdk/im/guide/systemconv/) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/systemconv.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/systemconv.mdx deleted file mode 100644 index 24817c77d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/guide/systemconv.mdx +++ /dev/null @@ -1,1428 +0,0 @@ ---- -title: 4. Hooks and System Conversations -sidebar_label: Hooks and System Conversations -sidebar_position: 4 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import Mermaid from "/src/docComponents/Mermaid"; - -## Introduction - -In the previous chapter, [Security, Chat Rooms, and Temporary Conversations](/sdk/im/guide/senior/), we introduced our third-party signing mechanism. In this chapter, we will cover the following functionalities offered by the Instant Messaging service: - -- Hooks -- System conversations - -## Hooks - -Instant Messaging is built with an open architecture that has strong extensibility, allowing you to do more than implement basic chatting features. Instant Messaging provides a collection of hooks that make it handy for you to utilize such extensibility. We’ll delve into them later in this section. - -### The Connection Between Hooks and Instant Messaging - -Hooks are a special message-handling mechanism for your app to intercept and process the various types of events and messages sent through it. They allow you to trigger custom logics when these events and messages are sent, enabling you to extend the existing features provided by the Instant Messaging service. - -Take **\_messageRecieved** as an example. This hook gets triggered when a message arrives at the server. Within the hook, you can obtain the properties of the message including its content, sender, and receivers. All these properties can be modified within the hook before they get taken over by the server. The server will then complete the delivery of the message with the modified properties and the message seen by the receivers will be the one with the modified properties instead of the original message. The hook can also reject the message so that the message won’t be seen by the receivers anymore. - -**Keep in mind that by default, if a hook fails due to timing out or returning a non-200 status code, the server will disregard the failure and continue processing the original request.** You can change this behavior by enabling **Return error and stop processing request when hook failed** under **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Instant Messaging settings**. With this option enabled, if a hook fails, the server will return the error message to the client and abort the request. - -### Hooks for Messages - -After being sent out from the sender and before being received by the receivers, a message would go through a series of stages determined by the online statuses of the receivers. You can set up a hook that gets triggered for each of the stages: - -- **\_messageReceived**
    - This hook gets triggered after the server receives a message and parses the members in the group, but before the message gets delivered to the receivers. Here you can perform operations like modifying the message’s content and receivers. -- **\_messageSent**
    - This hook gets triggered after a message gets delivered. Here you can perform operations like logging and making a copy of the message on your backup server. -- **\_receiversOffline**
    - This hook gets triggered after a message gets delivered with some of the receivers offline, but before push notifications get sent to the offline receivers. Here you can perform operations like dynamically updating the content and device list of the push notifications. -- **\_messageUpdate**
    - This hook gets triggered after the server receives a request for updating a message, but before the updated message gets delivered to the receivers. Similar to the situation when a new message is sent, here you can perform operations like modifying the message’s content and receivers. - -### Hooks for Conversations - -A hook can be triggered before or after a conversation-related operation takes place, like when a conversation gets created or when the member list of a conversation gets updated: - -- **\_conversationStart**
    - When a conversation is being created, this hook gets triggered after the signature validation (if enabled) has been completed but before the conversation actually gets created. Here you can perform operations like adding additional internal attributes to the conversation and performing authentication. -- **\_conversationStarted**
    - This hook gets triggered after a conversation gets created. Here you can perform operations like logging and making a copy of the conversation on your backup server. -- **\_conversationAdd**
    - When a member is joining or being added to a conversation, this hook gets triggered after the signature validation (if enabled) has been completed but before the member actually joins or gets added to the conversation. Here you can perform operations like determining whether the request shall be accepted or declined. -- **\_conversationRemove**
    - When a member is being removed from a conversation, this hook gets triggered after the signature validation (if enabled) has been completed but before the member actually gets removed from the conversation. This hook doesn’t get triggered when a member is leaving a conversation. Here you can perform operations like determining whether the request shall be accepted or declined. -- **\_conversationAdded**
    - This hook gets triggered after a user successfully joins a conversation. -- **\_conversationRemoved**
    - This hook gets triggered after a user successfully leaves a conversation. -- **\_conversationUpdate**
    - When a conversation’s name, custom attributes, or notification settings are being updated, this hook gets triggered before the update actually takes place. Here you can perform operations like adding additional internal attributes to the conversation and performing authentication. - -### Hooks for Client Status Changes - -A hook can be triggered when a client logs in or logs out: - -- **\_clientOnline**
    - This hook gets triggered when a client logs in successfully. -- **\_clientOffline**
    - This hook gets triggered when a client logs out successfully or loses connection unexpectedly. - -You can use these hooks together with LeanCache to implement an endpoint for looking up the online statuses of clients. - -### Hooks and Cloud Engine - -To maintain the necessary performance for handling an abundance of messages, the Instant Messaging service itself doesn’t provide the computing resources for running hooks. In order to use hooks, you will have to set up Cloud Engine instances for your application and deploy hooks onto these instances. - -The hooks for Instant Messaging will only take effect when deployed to the **production environment** of Cloud Engine. The staging environment shall be used for testing hooks but the hooks deployed there can only be triggered manually. Due to the existence of the cache, it may take up to 3 minutes for hooks to take effect if you’re deploying hooks to Cloud Engine for the first time. After that, the hooks deployed will take effect immediately. - -### Hooks API - -Conversation-related hooks can be used to perform additional permission checks besides those taken care of by the signing mechanism, controlling whether a conversation can be created or whether a user can be allowed into a conversation. One thing you can do with this hook is to implement a blocklist for your application. - -#### `_messageReceived` - -This hook gets triggered after a message arrives at the server. If the message is sent to a group, the server will parse all the receivers of the message. - -You can have the hook return a value to control whether the message should be discarded, which receivers should be removed, and what the updated message should be if the message is to be updated. If the hook returns an empty object (`response.success({})`), the message will go through the default workflow. - -If the hook contains conditionals, please be careful to **make sure the hook will always invoke `response.success` in the end to return a result** so that the message can be delivered without delay. The hook will **block the process of message delivery**, which means that you should keep the hook efficient by eliminating unnecessary invocations within the hook. - -For a rich media message, the `content` parameter will be a string containing a JSON object. See [Instant Messaging REST API Guide](/sdk/im/guide/rest/) for more information about the structure of this object. - -Parameters: - -| Parameter | Description | -| ----------- | ---------------------------------------------------------------------------------- | -| `fromPeer` | The ID of the sender. | -| `convId` | The ID of the conversation the message belongs to. | -| `toPeers` | The `clientId`s of the members in the conversation. | -| `transient` | Whether this is a transient message. | -| `bin` | Whether the content of the original message is binary. | -| `content` | The string representing the content of the message. If `bin` is `true`, this string will be the original message encoded in Base64 format. | -| `receipt` | Whether a receipt is requested. | -| `timestamp` | The timestamp the server received the message (in milliseconds). | -| `system` | Whether the message belongs to a system conversation. | -| `sourceIP` | The IP address of the sender. | - -Example arguments: - -```json -{ - "fromPeer": "Tom", - "receipt": false, - "groupId": null, - "system": null, - "content": "{\"_lctext\":\"Holy crap!\",\"_lctype\":-1}", - "convId": "5789a33a1b8694ad267d8040", - "toPeers": ["Jerry"], - "bin": false, - "transient": false, - "sourceIP": "121.239.62.103", - "timestamp": 1472200796764 -} -``` - -Return values: - -| Parameter | Constraint | Description | -| --------- | ---- | --------------------------------------------------------------------------------------------------------------------------- | -| `drop` | Optional | The message will be discarded if this value is `true`. | -| `code` | Optional | A custom error code (integer) to be returned when `drop` is `true`. | -| `detail` | Optional | A custom error message (string) to be returned when `drop` is `true`. | -| `bin` | Optional | Whether the returned `content` is binary. If omitted, this will be the same as the value of `bin` in the request. | -| `content` | Optional | The updated `content`. If omitted, the original message content will be used. If `bin` is `true`, this should be the message encoded in Base64 format. | -| `toPeers` | Optional | An array containing the updated receivers. If omitted, the original receivers will be used. | - -Code example: - - - -```js -AV.Cloud.onIMMessageReceived((request) => { - let content = request.params.content; - let processedContent = content.replace("crap", "**"); - // Must provide a return value, or an error will occur - return { - content: processedContent, - }; -}); -``` - -```python -import json - -@engine.define -def _messageReceived(**params): - content = json.loads(params['content']) - text = content['_lctext'] - content['_lctext'] = text.replace('crap', '**') - # Must provide a return value, or an error will occur - return { - 'content': json.dumps(content) - } -``` - -```php -Cloud::define("_messageReceived", function($params, $user) { - $content = json_decode($params["content"], true); - $text = $content["_lctext"]; - $content["_lctext"] = preg_replace("crap", "**", $text); - // Must provide a return value, or an error will occur - return array("content" => json_encode($content)); -}); -``` - -```java -@IMHook(type = IMHookType.messageReceived) - public static Map onMessageReceived(Map params) { - Map result = new HashMap(); - String content = (String)params.get("content"); - String processedContent = content.replace("crap", "**"); - result.put("content", processedContent); - // Must provide a return value, or an error will occur - return result; - } -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.MessageReceived)] -public static object OnMessageReceived(Dictionary parameters) { - string content = parameters["content"] as string; - string processedContent = content.Replace("crap", "**"); - return new Dictionary { - { "content", processedContent } - }; -} -``` - -```go -// Not supported yet -``` - - - -With the code above enabled, the sequence diagram of a message will be: - ->RTM: 1. Send a message -RTM-->>Engine: 2. Trigger the _messageReceived hook -Engine-->>RTM: 3. Return the result of the hook -RTM-->>SDK: 4. Send the result of the hook to the receiver -`} -/> - -- The diagram above assumes that all the members in the conversation are online. The sequence will be slightly different if some of the members are offline. We will talk about this in the next section. -- _RTM_ refers to the cluster for the Instant Messaging service and _Engine_ refers to that for the Cloud Engine service. These two clusters communicate through our internal network. - -#### `_receiversOffline` - -This hook gets triggered when some of the receivers are offline. A common use case of this hook is to customize the content and receivers of push notifications. You can even trigger custom push notifications with this hook. Keep in mind that messages sent to chat rooms won’t trigger this hook. - -Parameters: - -| Parameter | Description | -| --------------------- | --------------------------------------------------------------------------------------------------------------------- | -| `fromPeer` | The ID of the sender. | -| `convId` | The ID of the conversation the message belongs to. | -| `offlinePeers` | An array containing the receivers that are offline. | -| `content` | The content of the message. | -| `timestamp` | The timestamp the server received the message (in milliseconds). | -| `mentionAll` | A boolean indicating whether all the members are mentioned. | -| `mentionOfflinePeers` | Members who are offline but got mentioned by this message. If `mentionAll` is `true`, this parameter will be empty, indicating that all the members in `offlinePeers` are mentioned. | - -Return values: - -| Parameter | Constraint | Description | -| -------------- | ---- | -------------------------------------------------------------------- | -| `skip` | Optional | If set to `true`, push notifications will be skipped. This could be useful if you have already triggered push notifications in a different manner. | -| `offlinePeers` | Optional | An array containing the updated receivers. | -| `pushMessage` | Optional | The content of the push notifications. You can provide a JSON object with a custom structure. | -| `force` | Optional | If set to `true`, push notifications will be sent to the users in `offlinePeers` who `mute`d the conversation. Defaults to `false`. | - -Code example: - - - -```js -AV.Cloud.onIMReceiversOffline((request) => { - let params = request.params; - let content = params.content; - - // params.content is the content of the message - let shortContent = content; - - if (shortContent.length > 6) { - shortContent = content.slice(0, 6); - } - - console.log("shortContent", shortContent); - - return { - pushMessage: JSON.stringify({ - // Increment the number of unread messages; you can provide a number as well - badge: "Increment", - sound: "default", - // Use the dev certificate - _profile: "dev", - alert: shortContent, - }), - }; -}); -``` - -```python -@engine.define -def _receiversOffline(**params): - print('_receiversOffline start') - # params['content'] is the content of the message - content = params['content'] - short_content = content[:6] - print('short_content:', short_content) - payloads = { - # Increment the number of unread messages; you can provide a number as well - 'badge': 'Increment', - 'sound': 'default', - # Use the dev certificate - '_profile': 'dev', - 'alert': short_content, - } - print('_receiversOffline end') - return { - 'pushMessage': json.dumps(payloads), - } -``` - -```php -Cloud::define('_receiversOffline', function($params, $user) { - error_log('_receiversOffline start'); - // content is the content of the message - $shortContent = $params["content"]; - if (strlen($shortContent) > 6) { - $shortContent = substr($shortContent, 0, 6); - } - - $json = array( - // Increment the number of unread messages; you can provide a number as well - "badge" => "Increment", - "sound" => "default", - // Use the dev certificate - "_profile" => "dev", - "alert" => shortContent - ); - - $pushMessage = json_encode($json); - return array( - "pushMessage" => $pushMessage, - ); -}); -``` - -```java -@IMHook(type = IMHookType.receiversOffline) - public static Map onReceiversOffline(Map params) { - // content is the content of the message - String alert = (String)params.get("content"); - if(alert.length() > 6){ - alert = alert.substring(0, 6); - } - System.out.println(alert); - Map result = new HashMap(); - JSONObject object = new JSONObject(); - // Increment the number of unread messages - // You can provide a number as well - object.put("badge", "Increment"); - object.put("sound", "default"); - // Use the dev certificate - object.put("_profile", "dev"); - object.put("alert", alert); - result.put("pushMessage", object.toString()); - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ReceiversOffline)] -public static Dictionary OnReceiversOffline(Dictionary parameters) { - string alert = parameters["content"] as string; - if (alert.Length > 6) { - alert = alert.Substring(0, 6); - } - Dictionary pushMessage = new Dictionary { - { "badge", "Increment" }, - { "sound", "default" }, - { "_profile", "dev" }, - { "alert", alert }, - }; - return new Dictionary { - { "pushMessage", JsonSerializer.Serialize(pushMessage) } - }; -} -``` - -```go -// Not supported yet -``` - - - -#### `_messageSent` - -This hook gets triggered after a message gets delivered. It won’t impact the performance of the message-delivery process, so you can leave time-consuming operations here. - -Parameters: - -| Parameter | Description | -| -------------- | -------------------------------- | -| `fromPeer` | The ID of the sender. | -| `convId` | The ID of the conversation the message belongs to. | -| `msgId` | The ID of the message. | -| `onlinePeers` | The list of the online users’ IDs. | -| `offlinePeers` | The list of the offline users’ IDs. | -| `transient` | Whether this is a transient message. | -| `system` | Whether the message belongs to a system conversation. | -| `bin` | Whether this is a binary message. | -| `content` | The string representing the content of the message. | -| `receipt` | Whether a receipt is requested. | -| `timestamp` | The timestamp the server received the message (in milliseconds). | -| `sourceIP` | The IP address of the sender. | - -Example arguments: - -```json -{ - "fromPeer": "Tom", - "receipt": false, - "onlinePeers": [], - "content": "12345678", - "convId": "5789a33a1b8694ad267d8040", - "msgId": "fptKnuYYQMGdiSt_Zs7zDA", - "bin": false, - "transient": false, - "sourceIP": "114.219.127.186", - "offlinePeers": ["Jerry"], - "timestamp": 1472703266522 -} -``` - -Return values: - -The return value of this hook won’t be checked. You can just have the hook return `{}`. - -Code example: - -The code below shows how you can have a log printed to Cloud Engine when a message gets delivered: - - - -```js -AV.Cloud.onIMMessageSent((request) => { - console.log("params", request.params); -}); -``` - -```python -@engine.define -def _messageSent(**params): - print('_messageSent start') - print('params:', params) - print('_messageSent end') - return {} -``` - -```php -Cloud::define('_messageSent', function($params, $user) { - error_log('_messageSent start'); - error_log('params' . json_encode($params)); - return array(); -}); -``` - -```java -@IMHook(type = IMHookType.messageSent) - public static Map onMessageSent(Map params) { - System.out.println(params); - Map result = new HashMap(); - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.MessageSent)] -public static Dictionary OnMessageSent(Dictionary parameters) { - Console.WriteLine(JsonSerializer.Serialize(parameters)); - return default; -} -``` - -```go -// Not supported yet -``` - - - -#### `_messageUpdate` - -This hook gets triggered after the server receives a request for updating a message, but before the updated message gets delivered to the receivers. - -You can have the hook return a value to control whether the request for updating the message should be discarded, which receivers should be removed, and what the updated message should be if the message is to be updated again. - -If the hook contains conditionals, please be careful to **make sure the hook will always invoke `response.success` in the end to return a result** so that the updated message can be delivered without delay. The hook will **block the process of message delivery**, which means that you should keep the hook efficient by eliminating unnecessary invocations within the hook. - -For a rich media message, the `content` parameter will be a string containing a JSON object. See [Instant Messaging REST API Guide](/sdk/im/guide/rest/) for more information about the structure of this object. - -Parameters: - -| Parameter | Description | -| ----------- | ---------------------------------------------------------------------------------- | -| `fromPeer` | The ID of the sender. | -| `convId` | The ID of the conversation the message belongs to. | -| `toPeers` |The `clientId`s of the members in the conversation. | -| `bin` | Whether the content of the original message is binary. | -| `content` | The string representing the content of the message. If `bin` is `true`, this string will be the original message encoded in Base64 format. | -| `timestamp` | The timestamp the server received the message (in milliseconds). | -| `msgId` | The ID of the message being updated. | -| `sourceIP` | The IP address of the sender. | -| `recall` | Whether the message is recalled. | -| `system` | Whether the message belongs to a system conversation. | - -Return values: - -| Parameter | Constraint | Description | -| --------- | ---- | --------------------------------------------------------------------------------------------------------------------------- | -| `drop` | Optional | The request for updating the message will be discarded if this value is `true`. | -| `code` | Optional | A custom error code (integer) to be returned when `drop` is `true`. | -| `detail` | Optional | A custom error message (string) to be returned when `drop` is `true`. | -| `bin` | Optional | Whether the returned `content` is binary. If omitted, this will be the same as the value of `bin` in the request. | -| `content` | Optional | The updated `content`. If omitted, the original message content will be used. If `bin` is `true`, this should be the message encoded in Base64 format. | -| `toPeers` | Optional | An array containing the updated receivers. If omitted, the original receivers will be used. | - -#### `_conversationStart` - -When a conversation is being created, this hook gets triggered after the signature validation (if enabled) has been completed but before the conversation actually gets created. - -Parameters: - -| Parameter | Description | -| --------- | ---------------------------- | -| `initBy` | The `clientId` of the initiator of the conversation. | -| `members` | An array containing the initial members of the conversation. | -| `attr` | Additional attributes assigned to the conversation. | - -Example arguments: - -``` -{ - "initBy": "Tom", - "members": ["Tom", "Jerry"], - "attr": { - "name": "Tom & Jerry" - } -} -``` - -Return values: - -| Parameter | Constraint | Description | -| -------- | ---- | ---------------------------------------------------------------- | -| `reject` | Optional | Whether to reject the request. Defaults to `false`. | -| `code` | Optional | A custom error code (integer) to be returned when `reject` is `true`. | -| `detail` | Optional | A custom error message (string) to be returned when `reject` is `true`. | - -For example, to refuse a conversation to be created if it contains less than 4 initial members: - - - -```js -AV.Cloud.onIMConversationStart((request) => { - if (request.params.members.length < 4) { - return { - reject: true, - code: 1234, - detail: "Please invite at least 3 people to the conversation", - }; - } else { - return {}; - } -}); -``` - -```python -@engine.define -def _conversationStart(**params): - if len(params["members"]) < 4: - return { - "reject": True, - "code": 1234, - "detail": "Please invite at least 3 people to the conversation", - } - else: - return {} -``` - -```php -Cloud::define('_conversationStart', function($params, $user) { - if (count($params["members"]) < 4) { - return [ - "reject" => true, - "code" => 1234, - "detail" => "Please invite at least 3 people to the conversation", - ]; - } else { - return array(); - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationStart) -public static Map onConversationStart(Map params) { - String[] members = (String[])params.get("members"); - Map result = new HashMap(); - if (members.length < 4) { - result.put("reject", true); - result.put("code", 1234); - result.put("detail", "Please invite at least 3 people to the conversation"); - } - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationStart)] -public static object OnConversationStart(Dictionary parameters) { - List members = parameters["members"] as List; - if (members.Count < 4) { - return new Dictionary { - { "reject", true }, - { "code", 1234 }, - { "detail", "Please invite at least 3 people to the conversation" } - }; - } - return default; -} -``` - -```go -// Not supported yet -``` - - - -#### `_conversationStarted` - -This hook gets triggered after a conversation gets created. - -Parameters: - -| Parameter | Description | -| -------- | ----------------- | -| `convId` | The ID of the conversation being created. | - -Return values: - -The return value of this hook won’t be checked. You can just have the hook return `{}`. - -For example, to save the ID of the conversation to a list of recently created conversations on LeanCache after a conversation gets created: - - - -```js -AV.Cloud.onIMConversationStarted((request) => { - redisClient.lpush("recent_conversations", request.params.convId); - return {}; -}); -``` - -```python -@engine.define -def _conversationStarted(**params): - redis_client.lpush("recent_conversations", params["convId"]) - return {} -``` - -```php -Cloud::define('_conversationStarted', function($params, $user) { - $redis->lpush("recent_conversations", $params["convId"]); - return array(); -}); -``` - -```java -@IMHook(type = IMHookType.conversationStarted) -public static Map onConversationStarted(Map params) throws Exception { - String convId = (String)params.get("convId"); - jedis.lpush("recent_conversations", params.get("convId")); - Map result = new HashMap(); - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationStarted)] -public static object OnConversationStarted(Dictionary parameters) { - string convId = parameters["convId"] as string; - Console.WriteLine($"{convId} started"); - return default; -} -``` - -```go -// Not supported yet -``` - - - -#### `_conversationAdd` - -When a member is joining or being added to a conversation, this hook gets triggered after the signature validation (if enabled) has been completed but before the member actually joins or gets added to the conversation. **Keep in mind that this hook won’t be triggered in the situation when a conversation is being created with other users’ `clientId`s as members.** If a member is joining a conversation, initBy` will be the same as the only element of `members`. - -Parameters: - -| Parameter | Description | -| --------- | ----------------------- | -| `initBy` | The `clientId` of the initiator. | -| `members` | An array containing the members joining the conversation. | -| `convId` | The ID of the conversation. | - -Return values: - -| Parameter | Constraint | Description | -| -------- | ---- | ---------------------------------------------------------------- | -| `reject` | Optional | Whether to reject the request. Defaults to `false`. | -| `code` | Optional | A custom error code (integer) to be returned when `reject` is `true`. | -| `detail` | Optional | A custom error message (string) to be returned when `reject` is `true`. | - -For example, to refuse new members to be added to the conversation created by a specific member: - - - -```js -AV.Cloud.onIMConversationAdd((request) => { - if (request.params.initBy === "Tom") { - return { - reject: true, - code: 9890, - detail: "This is a private conversation. You cannot add anyone else to it.", - }; - } else { - return {}; - } -}); -``` - -```python -@engine.define -def _conversationAdd(**params): - if params["initBy"] == "Tom": - return { - "reject": True, - "code": 9890, - "detail": "This is a private conversation. You cannot add anyone else to it." - } - else: - return {} -``` - -```php -Cloud::define('_conversationAdd', function($params, $user) { - if ($params["initBy"] === "Tom") { - return [ - "reject" => true, - "code" => 9890, - "detail" => "This is a private conversation. You cannot add anyone else to it.", - ]; - } else { - return array(); - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationAdd) -public static Map onConversationAdd(Map params) { - Map result = new HashMap(); - if ("Tom".equals(params.get("initBy"))) { - result.put("reject", true); - result.put("code", 9890); - result.put("detail", "This is a private conversation. You cannot add anyone else to it.") - } - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationAdd)] -public static object OnConversationAdd(Dictionary parameters) { - if ("Tom".Equals(parameters["initBy"])) { - return new Dictionary { - { "reject", true }, - { "code", 9890 }, - { "detail", "This is a private conversation. You cannot add anyone else to it." } - }; - } - return default; -} -``` - -```go -// Not supported yet -``` - - - -#### `_conversationRemove` - -When a member is being removed from a conversation, this hook gets triggered after the signature validation (if enabled) has been completed but before the member actually gets removed from the conversation. This hook doesn’t get triggered when a member is leaving a conversation. - -Parameters: - -| Parameter | Description | -| --------- | -------------------- | -| `initBy` | The initiator of the operation. | -| `members` | An array containing the members to be removed. | -| `convId` | The ID of the conversation. | - -Return values: - -| Parameter | Constraint | Description | -| -------- | ---- | ---------------------------------------------------------------- | -| `reject` | Optional | Whether to reject the request. Defaults to `false`. | -| `code` | Optional | A custom error code (integer) to be returned when `reject` is `true`. | -| `detail` | Optional | A custom error message (string) to be returned when `reject` is `true`. | - -For example, to have some staff members in each of the conversations of an application that cannot be removed even by the owner of the conversation: - - - -```js -AV.Cloud.onIMConversationRemove(async (request) => { - const supporters = ["Bast", "Hypnos", "Kthanid"]; - const members = request.params.members; - for (const member of members) { - if (supporters.includes(member)) { - return { - "reject": true, - "code": 1928, - "detail": `You cannot remove the staff member ${member}`, - }; - } - } - return {}; -} -``` - -```python -@engine.define -def _conversationRemove(**params): - supporters = ["Bast", "Hypnos", "Kthanid"] - members = params["members"] - for member in members: - if member in supporters: - return { - "reject": True, - "code": 1928, - "detail": f"You cannot remove the staff member {member}" - } - return {} -``` - -```php -Cloud::define('_conversationRemove', function($params, $user) { - $supporters = array("Bast", "Hypnos", "Kthanid"); - $members = $params["members"]; - foreach ($members as $member) { - if (in_array($member, $supporters)) { - return [ - "reject" => true, - "code" => 1928, - "detail" => "You cannot remove the staff member $member", - ]; - } - } - return array(); -}); -``` - -```java -@IMHook(type = IMHookType.conversationRemove) -public static Map onConversationRemove(Map params) { - String[] supporters = {"Bast", "Hypnos", "Kthanid"}; - String[] members = (String[])params.get("members"); - Map result = new HashMap(); - for (String member : members) { - if (Arrays.asList(supporters).contains(member)) { - result.put("reject", true); - result.put("code", 1928); - result.put("detail", "You cannot remove the staff member " + member); - } - } - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationRemove)] -public static object OnConversationRemove(Dictionary parameters) { - List supporters = new List { "Bast", "Hypnos", "Kthanid" }; - List members = parameters["members"] as List; - foreach (object member in members) { - if (supporters.Contains(member as string)) { - return new Dictionary { - { "reject", true }, - { "code", 1928 }, - { "detail", $"You cannot remove the staff member {member}" } - }; - } - } - return default; -} -``` - -```go -// Not supported yet -``` - - - -#### `_conversationAdded` - -This hook gets triggered after a user successfully joins a conversation. - -Parameters: - -| Parameter | Description | -| --------- | -------------------- | -| `initBy` | The initiator of the operation. | -| `convId` | The ID of the conversation. | -| `members` | An array containing the IDs of the new members. | - -Return values: - -The return value of this hook won’t be checked. - -For example, to send a text message to a staff member if more than 10 members are added to a conversation at once: - - - -```js -AV.Cloud.onIMConversationAdded((request) => { - if (request.params.members.length > 10) { - AV.Cloud.requestSmsCode({ - mobilePhoneNumber: "+15559463664", - template: "Group_Notice", - sign: "sign_example", - conv_id: request.params.convId, - }).then( - function () { - /* Succeeded */ - }, - function (err) { - /* Failed */ - } - ); - } -}); -``` - -```python -@engine.define -def _conversationAdded(**params): - if len(params["members"]) > 10: - cloud.request_sms_code( - "+15559463664", - template="Group_Notice", sign: "sign_example", - params={"conv_id": params["convId"]} - ) -``` - -```php -Cloud::define('_conversationAdded', function($params, $user) { - if (count($params["members"]) > 10) { - $options = [ - "template" => "Group_Notice", - "name" => "sign_example", - "conv_id" => $params["convId"], - ]; - SMS::requestSmsCode("+15559463664", $options); - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationAdded) -public static void onConversationAdded(Map params) { - String[] members = (String[])params.get("members"); - if (members.length > 10) { - LCSMSOption option = new LCSMSOption(); - option.setTemplateName("Group_Notice"); - option.setSignatureName("sign_example"); - Map parameters = new HashMap(); - parameters.put("conv_id", params.get("convId")); - option.setEnvMap(parameters); - LCSMS.requestSMSCodeInBackground("+15559463664", option).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) {} - @Override - public void onNext(LCNull avNull) { - Log.d("TAG","Result: Successfully sent text message."); - } - @Override - public void onError(Throwable throwable) { - Log.d("TAG","Result: Failed to send text message. Reason: " + throwable.getMessage()); - } - @Override - public void onComplete() {} - }); - } -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationAdded)] -public static async Task OnConversationAdded(Dictionary parameters) { - List members = (parameters["members"] as List) - .Cast() - .ToList(); - if (members.Count > 10) { - Dictionary variables = new Dictionary { - { "conv_id", request.Params["convId"] } - }; - try { - await LCSMSClient.RequestSMSCode("+15559463664", "Group_Notice", "sign_example", variables: variables); - Console.WriteLine("Successfully sent text message."); - } catch (Exception e) { - Console.WriteLine($"Failed to send text message. Reason: {e.Message}"); - } - } -} -``` - -```go -// Not supported yet -``` - - - -#### `_conversationRemoved` - -This hook gets triggered after a user successfully leaves a conversation. - -Parameters: - -| Parameter | Description | -| --------- | -------------------- | -| `initBy` | The initiator of the operation. | -| `convId` | The ID of the conversation. | -| `members` | An array of the IDs of the users removed from the conversation. | - -Return values: - -The return value of this hook won’t be checked. - -For example, to save the ID of the conversation to a list of recently left conversations on LeanCache after a user leaves a conversation: - - - -```js -AV.Cloud.onIMConversationRemoved((request) => { - const initBy = request.params.initBy; - const members = request.params.members; - if (members.length === 1) { - if (members[0] === initBy) { - redisClient.lpush(initBy, request.params.convId); - } - } -}); -``` - -```python -@engine.define -def _conversationRemoved(**params): - init_by = params["initBy"] - members = params["members"] - if len(members) == 1: - if members[0] == init_by: - redis_client.lpush(init_by, params["convId"]) -``` - -```php -Cloud::define('_conversationRemoved', function($params, $user) { - $initBy = $params['initBy']; - $members = $params['members']; - if (count($members) === 1) { - if (members[0] === $initBy) { - $redis->lpush($initBy, $params["convId"]); - } - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationRemoved) -public static void onConversationRemoved(Map params) { - String[] members = (String[])params.get("members"); - String initBy = (String)params.get("initBy"); - if (members.length == 1) { - if (initBy.equals(members[0])) { - jedis.lpush(initBy, params.get("convId")); - } - } -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationRemoved)] -public static void OnConversationRemoved(Dictionary parameters) { - List members = (parameters["members"] as List) - .Cast() - .ToList(); - string initBy = parameters["initBy"] as string; - if (members.Count == 1 && members[0].Equals(initBy)) { - Console.WriteLine($"{parameters["convId"]} removed."); - } -} -``` - -```go -// Not supported yet -``` - - - -#### `_conversationUpdate` - -When a conversation’s name, custom attributes, or notification settings are being updated, this hook gets triggered before the update actually takes place. - -Parameters: - -| Parameter | Description | -| -------- | ---------------------- | -| `initBy` | The initiator of the operation. | -| `convId` | The ID of the conversation. | -| `mute` | Whether to disable notifications for the current conversation. | -| `attr` | Attributes to be set to the conversation. | - -`mute` and `attr` are mutually exclusive and won’t show up together. - -Return values: - -| Parameter | Constraint | Description | -| -------- | ---- | ------------------------------------------------------------------ | -| `reject` | Optional | Whether to reject the request. Defaults to `false`. | -| `code` | Optional | A custom error code (integer) to be returned when `reject` is `true`. | -| `detail` | Optional | A custom error message (string) to be returned when `reject` is `true`. | -| `attr` | Optional | The updated attributes to be set to the conversation. If omitted, the `attr` in the request will be used. | -| `mute` | Optional | The updated setting for disabling notifications. If omitted, the `mute` in the request will be used. | - -`mute` and `attr` are mutually exclusive and can’t be returned together. The return value should also match what’s in the request. If the request contains `attr`, only the `attr` in the return value will take effect. If the request contains `mute`, the `attr` in the return value will be discarded if it exists. - -For example, to prevent the names of conversations from being updated: - - - -```js -AV.Cloud.onIMConversationUpdate((request) => { - if ("attr" in request.params && "name" in request.params.attr) { - return { - reject: true, - code: 1949, - detail: "The name of the conversation cannot be updated.", - }; - } -}); -``` - -```python -@engine.define -def _conversationUpdate(**params): - if ('attr' in params) and ('name' in params['attr']): - return { - "reject": True, - "code": 1949, - "detail": "The name of the conversation cannot be updated." - } -``` - -```php -Cloud::define('_conversationUpdate', function($params, $user) { - if (array_key_exists('attr', $params) && array_key_exists('name', $params["attr"])) { - return [ - "reject" => true, - "code" => 1949, - "detail" => "The name of the conversation cannot be updated.", - ]; - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationUpdate) -public static Map onConversationUpdate(Map params) { - Map result = new HashMap(); - Map attr = (Map)params.get("attr"); - if (attr != null && attr.containsKey("name")) { - result.put("reject", true); - result.put("code", 1949); - result.put("detail", "The name of the conversation cannot be updated."); - } - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationUpdate)] -public static object OnConversationUpdate(Dictionary parameters) { - Dictionary attr = parameters["attr"] as Dictionary; - if (attr != null && attr.ContainsKey("name")) { - return new Dictionary { - { "reject", true }, - { "code", 1949 }, - { "detail", "The name of the conversation cannot be updated." } - }; - } - return default; -} -``` - -```go -// Not supported yet -``` - - - -#### `_clientOnline` - -This hook gets triggered when a client logs in successfully. - -Keep in mind that this hook only serves as a notification indicating that a user has gone online. If a user quickly goes online and offline (maybe with multiple devices), the sequence the `_clientOnline` and `_clientOffline` hooks get triggered can’t be guaranteed. This means that the `_clientOffline` hook may be triggered for a user before the `_clientOnline` hook gets triggered. - -Parameters: - -| Parameter | Description | -| --------- | ----------------------------------------------------------------------------------------------------------- | -| peerId | The ID of the client logging in. | -| sourceIP | The IP address of the client logging in. | -| tag | If left empty or set to "default", other devices with the same `tag` won’t be logged out. Otherwise, other devices with the same `tag` will be logged out. | -| reconnect | Whether to automatically reconnect for this log-in attempt. If left empty or set to 0, auto-reconnect will be disabled. If set to 1, auto-reconnect will be enabled. | - -Return values: - -The return value of this hook won’t be checked. - -For example, to update the data stored in LeanCache for looking up the online statuses of users: - - - -```js -AV.Cloud.onIMClientOnline((request) => { - // 1 means online - redisClient.set(request.params.peerId, 1); -}); -``` - -```python -@engine.define -def _clientOnline(**params): - # 1 means online - redis_client.set(params["peerId"], 1) -``` - -```php -Cloud::define('_clientOnline', function($params, $user) { - // 1 means online - $redis->set($params["peerId"], 1); -} -``` - -```java -@IMHook(type = IMHookType.clientOnline) -public static void onClientOnline(Map params) { - // 1 means online - jedis.set(params.get("peerId"), 1); -} -``` - -```cs -// The code below doesn’t update the data stored in LeanCache but only outputs the online status of the user -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ClientOnline)] -public static void OnClientOnline(Dictionary parameters) { - Console.WriteLine($"{parameters["peerId"]} online."); -} -``` - -```go -// Not supported yet -``` - - - -#### `_clientOffline` - -This hook gets triggered when a client logs out successfully or loses connection unexpectedly. - -Keep in mind that this hook only serves as a notification indicating that a user has gone online. If a user quickly goes online and offline (maybe with multiple devices), the sequence the `_clientOnline` and `_clientOffline` hooks get triggered can’t be guaranteed. This means that the `_clientOffline` hook may be triggered for a user before the `_clientOnline` hook gets triggered. - -Parameters: - -| Parameter | Description | -| --------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| peerId | The ID of the client getting offline. | -| closeCode | How the client got offline. 1 means the client logged out proactively. 2 means the connection was lost. 3 means the client was logged out due to a duplicate `tag`. 4 means the client was logged out by a request sent to the API. | -| closeMsg | A message describing how the client got offline. | -| sourceIP | The IP address of the client closing the session. Will be omitted if the hook is triggered by a loss of connection. | -| tag | Provided when the session got created. If left empty or set to "default", other devices with the same `tag` won’t be logged out. Otherwise, other devices with the same `tag` will be logged out. | -| errorCode | The code of the error causing the loss of the connection; optional. | -| errorMsg | The message of the error causing the loss of the connection; optional. | - -Possible errors: - -| Error code | Error message | Description | -| ------ | ------------------- | ------------------------------------ | -| 4107 | READ_TIMEOUT | The connection timed out due to a lack of new messages or heartbeats for a while. | -| 4108 | LOGIN_TIMEOUT | The connection timed out due to not logging in for a while. | -| 4109 | FRAME_TOO_LONG | The WebSocket frame is too long. | -| 4114 | UNPARSEABLE_RAW_MSG | The message is malformatted and cannot be parsed. | -| 4200 | INTERNAL_ERROR | There is an internal error in our server. | - -Return values: - -The return value of this hook won’t be checked. - -For example, to update the data stored in LeanCache for looking up the online statuses of users: - - - -```js -AV.Cloud.onIMClientOffline((request) => { - // 0 means offline - redisClient.set(request.params.peerId, 0); -}); -``` - -```python -@engine.define -def _clientOffline(**params): - # 0 means offline - redis_client.set(params["peerId"], 0) -``` - -```php -Cloud::define('_clientOffline', function($params, $user) { - // 0 means offline - $redis->set($params["peerId"], 0); -} -``` - -```java -@IMHook(type = IMHookType.clientOffline) -public static void onClientOffline(Map params) { - // 0 means offline - jedis.set(params.get("peerId"), 0); -} -``` - -```cs -// The code below doesn’t update the data stored in LeanCache but only outputs the online status of the user -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ClientOffline)] -public static void OnClientOffline(Dictionary parameters) { - Console.WriteLine($"{parameters["peerId"]} offline"); -} -``` - -```go -// Not supported yet -``` - - - -## System Conversations - -With system conversations, you can easily add functions like auto-reply, official accounts, and service accounts to your application. We have a [demo](https://leancloud.github.io/leanmessage-demo/) that contains a MathBot that can calculate the mathematical expressions sent from users and respond with the results, which is implemented using hooks for system conversations. You can find the server-side program of this bot on [GitHub](https://github.com/leancloud/leanmessage-demo/tree/master/server). - -### Creating System Conversations - -A system conversation is also a kind of conversation. When a system conversation is created, an entry will be added to the `_Conversation` table with `sys` being `true`. See [Instant Messaging REST API Guide](/sdk/im/guide/rest/) for more information on how to create a system conversation. - -### Sending Messages to System Conversations - -[Instant Messaging REST API Guide](/sdk/im/guide/rest/) contains detailed instructions on how to send messages to users through system conversations. Besides this, users can send messages to system conversations in the same way they send messages to basic conversations. - -System conversations can also be used to send broadcast messages to all the users of your application so you don’t have to manually obtain the IDs of these users before sending messages. All you need to do is to invoke the REST API for sending broadcast messages. A broadcast message has the following traits: - -- A broadcast message has to be associated with a conversation. The broadcast message will show up together with other messages in the history of the system conversation. -- When a user gets online, they will be notified of any broadcast messages sent to them when they are offline. -- You can set a TTL for a broadcast message so that users won’t be notified of it when getting online if the message has expired. Users will still be able to find the message in the history. -- When a new user logs in for the first time, they will receive the last broadcast message that’s not expired. - -Besides what’s mentioned above, a broadcast message will be treated in the same way as a basic message. See [Instant Messaging REST API Guide](/sdk/im/guide/rest/) for more information on how to send broadcast messages. - -### Getting History Messages of System Conversations - -To obtain the messages sent to users through system conversations, see [Instant Messaging REST API Guide](/sdk/im/guide/rest/). - -To obtain the messages sent from users to system conversations, you can use one of the following ways: - -- Look up the `_SysMessage` table. This table gets created the first time a user of your application sends a message to a system conversation. All the messages sent from users to system conversations will be stored in this table. -- Set up a [Web Hook](#web-hook). You will have to define your [Web Hook](#web-hook) for receiving messages sent from users to system conversations. - -### Messages in System Conversations - -#### `_SysMessage` - -This table contains the messages sent from users to system conversations. It contains the following attributes: - -| Attribute | Description | -| ----------- | ------------------------------ | -| `ackAt` | The time the message got delivered. | -| `bin` | Whether this is a binary message. | -| `conv` | A `Pointer` to the associated system conversation. | -| `data` | The content of the message. | -| `from` | The `clientId` of the sender. | -| `fromIp` | The IP address of the sender. | -| `msgId` | An internal ID for the message. | -| `timestamp` | The time the message got created. | - -#### Web Hook - -To set up a Web Hook for receiving the messages sent from users to system conversations, go to **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Service conversation callback**. The data structure of the messages conforms to the schema of the `_SysMessage` table mentioned earlier. - -When users send messages to system conversations, our system will send an HTTP POST request containing data in JSON to the Web Hook you provided. Keep in mind that our system won’t generate a request for each message, but will combine multiple messages into a single request. You’ll notice that the outermost layer of the JSON in a request is an `Array` from the example below. - -The timeout for each request will be 5 seconds. If your hook doesn’t respond to a request within the time limit, our system will retry up to 3 times. - -The format of a request will look like this: - -```json -[ - { - "fromIp": "121.238.214.92", - "conv": { - "__type": "Pointer", - "className": "_Conversation", - "objectId": "55b99ad700b0387b8a3d7bf0" - }, - "msgId": "nYH9iBSBS_uogCEgvZwE7Q", - "from": "A", - "bin": false, - "data": "Hello sys", - "createdAt": { - "__type": "Date", - "iso": "2015-07-30T14:37:42.584Z" - }, - "updatedAt": { - "__type": "Date", - "iso": "2015-07-30T14:37:42.584Z" - } - } -] -``` - -## Previous Chapters - -- [Overview](/sdk/im/guide/overview/) -- Chapter 1: [Basic Conversations and Messages](/sdk/im/guide/beginner/) -- Chapter 2: [Advanced Messaging Features, Push Notifications, Synchronization, and Multi-Device Sign-on](/sdk/im/guide/intermediate/) -- Chapter 3: [Security, Chat Rooms, and Temporary Conversations](/sdk/im/guide/senior/) - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/im-faq.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/im-faq.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/im-faq.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/im/im-faq.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/server/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/server/_category_.json similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/server/_category_.json rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/im/server/_category_.json diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/server/python.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/server/python.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/server/python.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/im/server/python.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/ui-library/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/ui-library/_category_.json similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/ui-library/_category_.json rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/im/ui-library/_category_.json diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/ui-library/android.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/ui-library/android.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/im/ui-library/android.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/im/ui-library/android.mdx diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/leaderboard/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/leaderboard/_category_.json deleted file mode 100644 index b53b82043..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/leaderboard/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "排行榜", - "collapsed": true, - "position": 8 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/leaderboard/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/leaderboard/features.mdx deleted file mode 100644 index 0939feee7..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/leaderboard/features.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Leaderboard Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -:::tip - -Setting up leaderboards in your game can promote fun competitions among players and encourage players to interact more with your game. - -::: - -TapSDK Leaderboard provides the following functions: - -- **Automatically calculate rankings:** When the players’ scores are updated, the latest rankings of the players will be automatically re-calculated. -- **Get the top players:** Get a given number of top players from the current leaderboard. -- **Get the ranking of the current player:** Get the ranking of the current player no matter if they are on the top of the leaderboard. -- **Get the players with similar rankings as the current player:** This can be used to find opponents or friends whose levels are close to the current player. -- **Reset data:** You can determine whether the leaderboard should be reset periodically automatically or manually only. For example, the leaderboard can be automatically reset after a season is over, in which each season lasts for a day, a week, or a month. You may also manually reset the leaderboard while you are testing your game or when there is an error with the data. -- **Easily update the data:** You can choose among three modes for updating a player’s score on the leaderboard: `better`, `last`, and `sum`. `better` will keep the best score of the player, `last` will keep the latest score, and `sum` will add up all the scores generated by the player. - -## Creating a Leaderboard - -:::note - -We recommend that you create the leaderboards in advance on TapTap Developer Center. The client can then update a leaderboard by providing its name (`statisticName`). - -::: - -Each leaderboard consists of a name (`statisticName`) and the scores of its members (`statisticValue`). You can specify its order, update strategy, and reset interval as well. - -### Name - -`statisticName` is the name of the leaderboard, which should be unique within your game. The name cannot be modified after a leaderboard has been created. It can only contain letters, numbers, and underscores, and has to start with a letter. - -### Member Type - -`memberType` is the type of the members on the leaderboard. You can choose one of the following types: - -- User: The value will be `_User`. Each member will correspond to the `objectId` of a user in the built-in account system (the `_User` class). If you plan to update the scores on the leaderboard from the client (for the current players only), this would be your only choice. -- Object: The value will be the name of a class other than `_User` in the Data Storage service. Each member will correspond to the `objectId` of an object in the class. For example, if you have a class named `Weapon`, you can enter `Weapon` for `memberType` to form a leaderboard for weapons. -- Entity: The value will be `_Entity`. Each member will be a string provided by you. The string can only contain letters, numbers, and underscores. - -Note: - -- When retrieving data from the leaderboard, you can specify certain parameters to get more data associated with the users or objects. -- If you selected “Object” or “Entity” as the type, you will only be able to update the leaderboard **with your Master Key on the server side**. -- If “Only Master Key is allowed to update the score” is checked on the dashboard (unchecked by default), you will only be able to update the leaderboard with your Master Key on the server side even if you selected “User” as the type. From the perspective of anti-cheating, we recommend that you enable this option. - -## Uploading Scores - -`statisticValue` contains the scores (in numbers) generated by the players on the client. It could be points, kills, times, etc. - -### Order - -- `descending`: Rand scores in descending order. In most of the games, the higher score a player achieves, the higher ranking this player gets. -- `ascending`: Rand scores in ascending order. For example, in some racing games, the less time a player spends to complete a task, the higher ranking this player gets. - -### Update Strategy - -`updateStrategy` is the strategy for updating players’ scores. Each leaderboard can have one of the following strategies: - -- `better`: Keep the **best** score of each player. When the descending order is selected, the largest score will be kept; when the ascending order is selected, the smallest score will be kept. -- `last`: Keep the **latest** score of each player. This means that a player’s latest score will overwrite their past scores. -- `sum`: The final score of each player is the sum of all the scores they got. Each time a player gets a new score, the score will be added to the existing score. - -## Resetting Data - -The leaderboard can be reset at any time. Once reset, the leaderboard will become empty. A use case for resetting is to clear the leaderboard once a season is over. Resetting a leaderboard will remove all the scores from it and the version (`Version`) of the leaderboard will be updated by adding 1. All the new requests from the clients for updating scores will be written to the newer version of the leaderboard. - -`versionChangeInterval` is the interval for resetting data. It can be one of the following: - -- `day`: Reset at 00:00 every day. -- `week`: Reset at 00:00 every Monday. -- `month`: Reset at 00:00 on the 1st of every month. -- `never`: Do not reset automatically, which means that only manual resets will happen. - -## Retrieving History Data - -Once the leaderboard is reset, clients can only retrieve the latest and the previous version of the leaderboard with the SDK. -If you did not check “Keep the previous version” on the dashboard (unchecked by default) before you reset the leaderboard, the client SDK will only be able to retrieve the current version of the leaderboard. -If the interval for resetting data is set to `never`, there will be an additional restriction: you can only retrieve the previous version within 7 days after you reset the leaderboard. After 7 days, you cannot retrieve the previous version anymore. -To illustrate: - -- With `month` selected as the interval, assuming it’s currently March, the version becomes 3 after the leaderboard has been reset on March 1st. You will be able to retrieve version 2 for February but not version 1 for January. -- With `never` selected as the interval, assuming the version becomes 3 after you manually reset the leaderboard. You will be able to retrieve version 2 within 7 days. After that, you won’t be able to retrieve version 2 anymore. - -Although the previous versions cannot be retrieved by the client SDK, you can still retrieve the archived CSV files containing them with the REST API. -Each leaderboard can have at most 60 archived versions. After exceeding the limit, **the older versions will be deleted**. -If you need to keep the previous versions of the leaderboard for a longer time, please download them promptly so you can back them up to your own places. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/leaderboard/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/leaderboard/guide.mdx deleted file mode 100644 index 725f821fb..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/leaderboard/guide.mdx +++ /dev/null @@ -1,2128 +0,0 @@ ---- -title: Leaderboard Guide -sidebar_label: Guide -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; - -:::info - -Before continuing, make sure you have read: - -- [Leaderboard Introduction](/sdk/leaderboard/features/). It introduces the core concepts and functions of Leaderboard. -- [TDS Authentication Guide](/sdk/authentication/guide/). There are three types of members: user, object, and entity. Here “user” refers to the users in the built-in account system. Besides, the `currentUser` mentioned later in this page refers to the currently logged-in user. - -::: - -## Installing and Setting up SDK - -Leaderboard comes as a part of the Data Storage SDK. Check the following pages if you haven’t set up the Data Storage SDK. - -- [Installing .NET SDK](/sdk/storage/guide/setup-dotnet) -- [Installing Java SDK](/sdk/storage/guide/setup-java) -- [Installing Objective-C SDK](/sdk/storage/guide/setup-objc) - -## Quick Start - -### Creating Leaderboards - -There are 3 ways to create a leaderboard: - -- Go to the Developer Center’s [dashboard](#dashboard). -- [Access the REST API](#creating-leaderboards-2) under **trusted environments like the server side**. -- [Access the management interface of the SDK](#creating-leaderboards-1) under **trusted environments like the server side**. - -For example, you can create a leaderboard named `world` with “built-in account system” as the type of members, `descending` as the order, `better` as the update strategy, and “every month” as the reset interval. - -![create leaderboard](https://capacity-files.lcfile.com/0UqoMYAOerNgQLfyHiBFxhrULm8kjCPO/io-create_leaderboard.png) - -### Submitting Scores - -If the player is logged in (accessible with `currentUser`), you can update their score with the following code: - - - -```cs -var statistic = new Dictionary(); -statistic["world"] = 20.0; -await LCLeaderboard.UpdateStatistics(currentUser, statistic); -``` - -```java -Map statistic = new HashMap<>(); -statistic.put("world", 20.0); -LCLeaderboard.updateStatistic(currentUser, statistic).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCStatisticResult jsonObject) { - // scores saved - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - } - - @Override - public void onComplete() {} -}); -``` - -```objc -NSDictionary *statistic = @{ - @"world" : 20.0, -}; -[LCLeaderboard updateCurrentUserStatistics:statistic, callback:^(NSArray *statistics, NSError *error) { - if (statistics) { - // statistics is the best or latest score after the update - } else if (error) { - // Handle error - } -}]; -``` - - - -### Getting Results - -The code below retrieves the top 10 players from the leaderboard. Since we’ve only submitted one player’s score so far, the leaderboard will only have this one score. - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("world"); -var rankings = await leaderboard.GetResults(limit: 10); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world"); -leaderboard.getResults(0, 10, null, null).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCLeaderboardResult leaderboardResult) { - List rankings = leaderboardResult.getResults(); - // process rankings - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - } - - @Override - public void onComplete() {} -}); -``` - -```objc -LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"world"]; -leaderboard.limit = 10; -[leaderboard getUserResultsWithOption:nil, callback:^(NSArray *rankings, NSInteger count, NSError *error) { - // rankings contains the top 10 players’ data -}]; -``` - - - -We’ve just introduced the most basic usage of Leaderboard. Now let’s look at all the interfaces provided by Leaderboard. - -## Managing Scores - -### Updating the Current Player’s Scores - -Once a player finishes a game, you can use the `updateStatistic` method provided by the client SDK to update this player’s score. -However, **to effectively prevent cheating from happening, we suggest that you enable “Only Master Key is allowed to update the score” on the dashboard and update scores on the server side only.** - - - -```cs -var statistic = new Dictionary { - { "score", 3458.0 }, - { "kills", 28.0 } -}; -await LCLeaderboard.UpdateStatistics(currentUser, statistic); -``` - -```java -Map statistic = new HashMap<>(); -statistic.put("score", 3458.0); -statistic.put("kills", 28.0); -LCLeaderboard.updateStatistic(currentUser, statistic).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCStatisticResult jsonObject) { - // saved - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - } - - @Override - public void onComplete() {} -}); -``` - -```objc -NSDictionary *statistic = @{ - @"score" : 3458.0, - @"kills" : 28.0, -}; -[LCLeaderboard updateCurrentUserStatistics:statistic, callback:^(NSArray *statistics, NSError *error) { - if (!error) { - // saved - } -}]; -``` - - - -Updating a player’s score requires this player to be logged in. A player can only update their own scores. -You can update multiple leaderboards at once. The example above updates the scores in both `score` and `kills`. - -You cannot update an object or entity’s scores with the client SDK. -To update other players’, an object’s, or an entity’s scores, you have to use the [REST API](#updating-scores) or the [management interface provided by the SDK](#updating-leaderboard-members-scores). - -### Deleting the Current Player’s Scores - -A player can delete their own scores: - - - -```cs -await LCLeaderboard.DeleteStatistics(currentUser, new List { "world" }); -``` - -```java -// Not supported yet -``` - -```objc -[LCLeaderboard deleteCurrentUserStatistics:@[@"world"], callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Score deleted - } else if (error) { - // Handle error - } -}]; -``` - - - -Same as updating scores, a player can only delete their own scores. -To delete other players’, an object’s, or an entity’s scores, you have to use the [REST API](#deleting-scores) or the [management interface provided by the SDK](#deleting-leaderboard-members-scores). - -### Getting Leaderboard Members’ Scores - -A **logged-in player** can retrieve the scores of other players in all leaderboards with `GetStatistics`: - - - -```cs -var otherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "5c76107144d90400536fc88b"); -var statistics = await LCLeaderboard.GetStatistics(otherUser); -foreach(var statistic in statistics) { - Debug.Log(statistic.Name); - Debug.Log(statistic.Value); -} -``` - -```java -// Retrieve leaderboard members’ scores -LCUser otherUser = null; -try { - otherUser = LCUser.createWithoutData(LCUser.class, "5c76107144d90400536fc88b"); -} catch (LCException e) { - e.printStackTrace(); -} -LCLeaderboard.getUserStatistics(otherUser).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCStatisticResult lcStatisticResult) { - List statistics = lcStatisticResult.getResults(); - for (LCStatistic statistic : statistics) { - Log.d(TAG, statistic.getName()); - Log.d(TAG, String.valueOf(statistic.getValue())); - } - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - Toast.makeText(MainActivity.this, "Failed: " + throwable.getMessage(), Toast.LENGTH_SHORT).show(); - } - - @Override - public void onComplete() {} -}); -``` - -```objc -NSString *otherUserObjectId = @"5c76107144d90400536fc88b"; -[LCLeaderboard getStatisticsWithUserId:otherUserObjectId, statisticNames:nil, callback:^(NSArray * _Nullable *statistics, NSError _Nullable *error) { - if (statistics) { - for (LCLeaderboardStatistic *statistic in statistics) { - NSLog(@"Leaderboard name: %@", statistic.name); - NSLog(@"Scores: %f", statistic.value); - } - } else if (error) { - // Handle error - } -}]; -``` - - - -In most cases, you may only need to retrieve the scores of a user from specific leaderboards. -To do so, provide the names of the leaderboards when querying: - - - -```cs -var statistics = await LCLeaderboard.GetStatistics(otherUser, new List { "world" }); -``` - -```java -LCLeaderboard.getUserStatistics(otherUser, Arrays.asList("world")).subscribe(/** Other logic **/); -``` - -```objc -[LCLeaderboard getStatisticsWithUserId:otherUserObjectId, statisticNames:@[@"world"], callback:^(NSArray * _Nullable statistics, NSError * _Nullable error) { - // Other logic -}]; -``` - - - -Similarly, you can retrieve scores from leaderboards with member types being object or entity. - - -<> - -For example, if the weapon leaderboard has its member type to be object: - -```cs -var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b"); -var statistics = await LCLeaderboard.GetStatistics(excalibur, new List { "weapons" }); -``` - -If the weapon leaderboard has its member type to be entity: - -```cs -var statistics = await LCLeaderboard.GetStatistics("excalibur", new List { "weapons" }); -``` - - -<> - -For example, if the weapon leaderboard has its member type to be object: - -```java -String excaliburObjectId = "582570f38ac247004f39c24b"; -LCLeaderboard.getMemberStatistics("Weapon", excaliburObjectId, - Arrays.asList("weapons")).subscribe(/** Other logic **/); -``` - -If the weapon leaderboard has its member type to be entity: - -```java -LCLeaderboard.getMemberStatistics(LCLeaderboard.MEMBER_TYPE_ENTITY, "excalibur", - Arrays.asList("weapons")).subscribe(/** Other logic **/); -``` - -The `getUserStatistics` method mentioned earlier: - -```java -LCLeaderboard.getUserStatistics(otherUser, Arrays.asList("weapons")).subscribe(/** Other logic **/); -``` - -Is equivalent to: - -```java -LCLeaderboard.getMemberStatistics(LCLeaderboard.LCLeaderboard.MEMBER_TYPE_USER, - otherUser.getObjectId(), - Arrays.asList("weapons")).subscribe(/** Other logic **/); -``` - - -<> - -For example, if the weapon leaderboard has its member type to be object: - -```objc -NSString *excaliburObjectId = @"582570f38ac247004f39c24b"; -[LCLeaderboard getStatisticsWithObjectId:excaliburObjectId, statisticNames:@[@"weapons"], - option:nil - callback:^(NSArray *statistics, NSError *error) { - // Other logic -}]; -``` - -If the weapon leaderboard has its member type to be entity: - -```objc -[LCLeaderboard getStatisticsWithEntity:@"excalibur", statisticNames:@[@"weapons"], - callback:^(NSArray * _Nullable *statistics, NSError * _Nullable error) { - // Other logic -}]; -``` - - - - -You can also retrieve the scores of a group of members: - - - -```cs -var otherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "5c76107144d90400536fc88b"); -var anotherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "672a127144a90d00536f3456"); -var statistics = await LCLeaderboard.GetStatistics({otherUser, anotherUser}, new List { "world" }); - -var oneObject = LCObject.CreateWithoutData("abccb27133a90ddd536ffffa"); -var anotherUser = LCObject.CreateWithoutData("672a1279345777005a2b2444"); -var statistics = await LCLeaderboard.GetStatistics({oneObject, anotherObject}, new List { "weapons" }); - -var statistics = await LCLeaderboard.GetStatistics({"Sylgr", "Leiptr"}, new List { "rivers" }); -``` - -```java -// Not supported yet -``` - -```objc -NSString *otherUserObjectId = @"5c76107144d90400536fc88b"; -NSString *anotherUserObjectId = @"672a127144a90d00536f3456"; -[leaderboard getStatisticsWithUserIds:@[otherUserObjectId, anotherUserObjectId] - callback:^(NSArray * _Nullable statistics, NSError * _Nullable error) { -// Other logic -}]; - -NSString *oneObjectId = @"abccb27133a90ddd536ffffa"; -NSString *anotherObjectId = @"672a1279345777005a2b2444"; -[leaderboard getStatisticsWithObjectIds:@[oneObjectId, anotherObjectId] - option:nil - callback:^(NSArray * _Nullable statistics, NSError * _Nullable error) { -// Other logic -}]; - -[leaderboard getStatisticsWithEntities:@[@"Sylgr", "Leiptr"] - callback:^(NSArray * _Nullable statistics, NSError * _Nullable error) { -// Other logic -}]; -``` - - - -## Getting Leaderboard Results - -You can get the result of a leaderboard with the `Leaderboard#getResults` method. -The most common use case of it is to get the scores of the top players or to get the players with similar rankings as the current player. - -Let’s first construct a leaderboard instance: - - - -<> - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("world"); -``` - -`LCLeaderboard.CreateWithoutData` accepts two arguments: - -```cs -public static LCLeaderboard CreateWithoutData(string statisticName, string memberType = LCLeaderboard.USER_MEMBER_TYPE) -``` - -- `statisticName` is the name of an existing leaderboard. It’s set to be `world` in the example above. -- `memberType` is the type of members: `LCLeaderboard.USER_MEMBER_TYPE` for user and `LCLeaderboard.ENTITY_MEMBER_TYPE` for entity. For object, provide the corresponding class name. The example above omitted this argument, which means to default to user. - - -<> - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world"); -``` - -`LCLeaderboard.createWithoutData` accepts two arguments: - -```java -public static LCLeaderboard createWithoutData(String name, String memberType) -``` - -- `name` is the name of an existing leaderboard. It’s set to be `world` in the example above. -- `memberType` is the type of members: `LCLeaderboard.MEMBER_TYPE_USER` for user and `LCLeaderboard.MEMBER_TYPE_ENTITY` for entity. For object, provide the corresponding class name. Since user is the most common type, `createWithoutData` provides an overload method with a single argument. The example above only passes the name of the leaderboard to the function, which indicates that the type is user. - - -<> - -```objc -LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"world"]; -``` - - - - - -After the leaderboard instance is constructed, you can call the corresponding methods on the instance to get the rankings. - -### Getting Rankings Within a Scope - -To get the top 10 on the leaderboard: - - - -<> - -```cs -var rankings = await leaderboard.GetResults(limit: 10); -``` - -`GetResults` accepts the following arguments for specifying constraints: - -| Name | Type | Description | -| :-----------------: | :--------: | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| `aroundUser` | `LCUser` | Get the players with similar rankings as a given player. See the next section for more information. | -| `aroundObject` | `LCObject` | Get the objects with similar rankings as a given object. See the next section for more information. | -| `aroundEntity` | `string` | Get the entities with similar rankings as a given entity. See the next section for more information. | -| `limit` | `number` | Limit the number of results. Defaults to 10. | -| `skip` | `number` | Set the offset. Can be used with `limit` to implement pagination. Defaults to 10. | -| `selectKeys` | `string[]` | Specify the properties that need to be included with the `user`s from the returned `Ranking`s. Continue reading for more information. | -| `includeKeys` | `string[]` | Specify the `Pointer` properties that need to be included with the `user`s from the returned `Ranking`s. Continue reading for more information. | -| `includeStatistics` | `string[]` | Specify the scores in other leaderboards that need to be included in the `Ranking`s. Continue reading for more information. | -| `version` | `number` | Specify the version of the leaderboard. | - -The returned result is an array (`Ranking[]`) with each `Ranking` holding the following properties: - -| Name | Type | Description | -| :------------------: | :---------------: | ------------------------------------------------------- | -| `Rank` | `int` | The ranking. Starts with 0. | -| `User` | `LCUser` | The user who got the score (for user leaderboards). | -| `Object` | `LCObject` | The object who got the score (for object leaderboards). | -| `Entity` | `string` | The entity who got the score (for entity leaderboards). | -| `Value` | `double` | The score. | -| `IncludedStatistics` | `List` | The member’s scores in other leaderboards. | - - -<> - -```java -leaderboard.getResults(0, 10, null, null).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCLeaderboardResult leaderboardResult) { - List rankings = leaderboardResult.getResults(); - } - - @Override - public void onError(@NotNull Throwable throwable) { - // handle error - } - - @Override - public void onComplete() {} -}); -``` - -`Leaderboard#getResults` accepts the following arguments for specifying constraints: - -```java -Observable getResults( - int skip, int limit, - List selectMemberKeys, List includeStatistics) -``` - -- `skip` Set the offset. Can be used with `limit` to implement pagination. Defaults to 10. -- `limit` Limit the number of results. Defaults to 20. -- `selectMemberKeys` Specify the properties that need to be included with the `user`s from the returned `LCRanking`s. Continue reading for more information. -- `includeStatistics` Specify the scores in other leaderboards that need to be included in the `LCRanking`s. Continue reading for more information. - -By providing a version number, you can retrieve data from a previous version of the leaderboard: - -```java -int previousVersion = currentVersion - 1; -leaderboard.setVersion(previousVersion); -``` - -`LCRanking` provides the following methods for getting members’ information: - -```java -// The ranking; starts with 0 -int getRank() -// The user who got the score (for user leaderboards) -LCUser getUser() -// The object who got the score (for object leaderboards) -LCObject getObject() -// The entity who got the score (for entity leaderboards) -String getEntityId() -// The score -double getStatisticValue() -// The member’s scores in other leaderboards -List getIncludedStatistics() -``` - - -<> - -```objc -leaderboard.limit = 10; -[leaderboard getUserResultsWithOption:nil, - callback:^(NSArray * _Nullable *rankings, NSInteger count, NSError * _Nullable error) { - // rankings contains the data of the top 10 members in the leaderboard -}]; -``` - -Leaderboard accepts the following properties for specifying constraints: - -```objc -/// Set the offset; can be used with `limit` to implement pagination; defaults to 10. -@property (nonatomic) NSInteger skip; -/// Limit the number of results; defaults to 20. -@property (nonatomic) NSInteger limit; -/// Specify the scores in other leaderboards that need to be included in the `LCLeaderboardRanking`s -@property (nonatomic, nullable) NSArray *includeStatistics; -/// Specify the version of the leaderboard; defaults to 0 -@property (nonatomic) NSInteger version; -``` - -The first option of `getUserResultsWithOption` is `LCLeaderboardQueryOption`, in which you can specify the properties you want the members of the returned `LCLeaderboardRanking` to include. Continue reading for more information. - -The `rankings` in the callback is an `LCLeaderboardRanking` array. -`LCLeaderboardRanking` contains the following properties: - -```objc -// The name of the leaderboard -@property (nonatomic, readonly, nullable) NSString *statisticName; -/// The ranking; starts with 0 -@property (nonatomic, readonly) NSInteger rank; -/// The score -@property (nonatomic, readonly) double value; -/// The member’s scores in other leaderboards -@property (nonatomic, readonly, nullable) NSArray *includedStatistics; -/// The user who got the score (for user leaderboards) -@property (nonatomic, readonly, nullable) LCUser *user; -/// The object who got the score (for object leaderboards) -@property (nonatomic, readonly, nullable) LCObject *object; -/// The entity who got the score (for entity leaderboards) -@property (nonatomic, readonly, nullable) NSString *entity; -``` - -Since the `getUserResultsWithOption` method is called in the previous example, the `user` property is not empty and the `object` and `entity` properties are empty. - -For an object or entity leaderboard, the `getObjectResultsWithOption` and `getEntityResultsWithCallback` methods need to be called correspondingly. -The option for `getObjectResultsWithOption` is the same as that for `getUserResultsWithOption`. -Since the members of entity leaderboards are strings, `getEntityResultsWithCallback` doesn’t support `LCLeaderboardQueryOption` and its first option is the callback. The callback has the same options as `getUserResultsWithOption` and `getObjectResultsWithOption`: - -```objc -- (void)getEntityResultsWithCallback:(void (^)(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error))callback; -``` - - - - - -By default, the `user`s in the results are `LCUser` `Pointer`s. -To include the usernames or other properties (in the `_User` class) of the users so that they can be displayed like the table below, specify them with `selectKeys`. - -| Ranking | Username | Score↓ | -| :-----: | -------- | :----: | -| 0 | Genji | 3458 | -| 1 | Lúcio | 3252 | -| 2 | D.Va | 3140 | - - - -```cs -var rankings = await leaderboard.GetResults(limit: 10, - selectKeys: new List { "username" }); -``` - -```java -List selectKeys = new ArrayList<>(); -selectKeys.add("username"); -leaderboard.getResults(0, 10, selectKeys, null).subscribe(/* Other logic */); -``` - -```objc -leaderboard.limit = 10; -LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init]; -option.selectKeys = @[@"username"]; -[leaderboard getUserResultsWithOption:option, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // Other logic -}]; -``` - - - -To include the players’ scores in other leaderboards, use `includeStatistics`. -For example, to include the kills when retrieving the leaderboard for scores: - -| Ranking | Username | Score↓ | Kills | -| :-----: | -------- | :----: | :---: | -| 0 | Genji | 3458 | 28 | -| 1 | Lúcio | 3252 | 2 | -| 2 | D.Va | 3140 | 31 | - - - -```cs -var rankings = await leaderboard.GetResults(limit: 10, selectKeys: new List { "username" } - includeStatistics: new List { "kills" }); -``` - -```java -List selectKeys = new ArrayList<>(); -selectKeys.add("username"); -List includeStatistics = new ArrayList<>(); -includeStatistics.add("kills"); -leaderboard.getResults(0, 10, selectKeys, includeStatistics).subscribe(/* Other logic */); -``` - -```objc -leaderboard.limit = 10; -leaderboard.includeStatistics = @[@"kills"]; -LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init]; -option.selectKeys = @[@"username"]; -[leaderboard getUserResultsWithOption:option, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // Other logic -}]; -``` - - - -If a `Pointer` or file property is included using `selectKeys`, you will only get the `Pointer`s themselves. -To include the objects referenced by the `Pointer`s, you need to use `includeKeys` as well. -For example, assuming `club` is a `Club` `Pointer`: - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("weapons", "Weapon"); -var rankings = await leaderboard.GetResults(limit: 10, - selectKeys: new List { "name", "club" }, - includeKeys: new List { "club" }); -``` - -```java -// Not supported yet; you can invoke an additional query within the onNext method instead -``` - -```objc -LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"weapons"]; -leaderboard.limit = 10; -LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init]; -option.selectKeys = @[@"name", @"club"]; -option.includeKeys = @[@"club"]; -[leaderboard getUserResultsWithOption:option, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // Other logic -}]; -``` - - - -Keep in mind that the order of the members with the same score in the returned result is not guaranteed. -For example, if A, B, and C got 42, 32, and 32, and the leaderboard has a descending order, the result might list the three members in the order of either “A, B, C” or “A, C, B”. - -When the score is the same, if you need other factors to set the ranking, you can refer to the following example ** (example is to judge the ranking by the time stamp of the score upload, when the score is the same, the earlier/later the submission, the higher/lower the ranking) ** : - -
    -Android request example: - -``` -public class RankingActivity extends AppCompatActivity{ - - // ... - - /** - * Upload/update grades - * - * */ - private void submitScore() { - - double score = 324.45; // Actual score - long ts = System.currentTimeMillis() / 1000; // time stamp - double last_score = toEncode( score, ts); // Combine the actual score with the timestamp to generate a new data upload to the server - - Map statistic = new HashMap<>(); - statistic.put("word", last_score); - - LCLeaderboard.updateStatistic(LCUser.currentUser(), statistic).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull LCStatisticResult jsonObject) { - Log.e(TAG, "onNext: "+jsonObject.getResults().get(0).toString()); - - } - - @Override - public void onError(@NotNull Throwable throwable) { - ToastUtil.showCus(throwable.getMessage(), ToastUtil.Type.ERROR); - } - - @Override - public void onComplete() {} - }); - - } - - /** - * 查询排行榜列表 - * */ - private void searchRankList() { - // Obtain an instance of a leaderboard - LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("word"); - - leaderboard.getResults(0, 10, null, null).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public void onNext(@NotNull LCLeaderboardResult leaderboardResult) { - List rankings = leaderboardResult.getResults(); - for(int i=0; i> 32); - - /** - * When the leaderboard is arranged in descending order and the scores are the same, the closer the time and the lower the ranking, use this line of code - */ - int score = (int) (encryptedNewScore >> 32) + 1; - long ts = encryptedNewScore & 0xFFFFFFFFL; - return new int[]{score, (int) ts}; - } - -} - -``` -
    - - -### Getting the Players With Similar Rankings as the Current Player - -| Ranking | Username | Score↓ | -| :------: | ---------- | :----: | -| … | | | -| 24 | Bastion | 716 | -| 25 (You) | Widowmaker | 698 | -| 26 | Hanzo | 23 | -| … | | | - - - -<> - -To implement something like the table above in your game, provide the current user when calling `GetResults`: - -```cs -var rankings = await leaderboard.GetResults(aroundUser: currentUser, limit: 3, selectKeys: new List { "username" }); -``` - - -<> - -To implement something like the table above in your game, call the `getAroundResults` method: - -```java -List selectKeys = new ArrayList<>(); -selectKeys.add("username"); -leaderboard.getAroundResults(currentUser.getObjectId(), 0, 3, selectKeys, null).subscribe(/* Other logic */); -``` - -The first argument of `getAroundResults` is the ID of the member (`objectId` for user and object; string for entity). Other arguments are the same as those for `getResults`. - - -<> - -To implement something like the table above in your game, call the `getUserResultsAroundUser` method: - -```objc -leaderboard.limit = 3; -[leaderboard getUserResultsAroundUser:currentUser.objectId, - option:nil, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // Other logic -}]; -``` - - - - - -In the example above, the `limit` is set to 3, which means to get two players with similar rankings as the current player together with the current player placed between them. You can set `limit` to 1 to get the current player’s ranking only. - -Similarly, you can retrieve objects or entities with similar rankings as a given object or entity. -For example, to get the weapons with similar rankings as a given weapon from the weapon leaderboard, including their names, attacks, and levels: - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("weapons", "Weapon"); -var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b"); -var rankings = await leaderboard.GetResults(aroundObject: excalibur, limit: 3, selectKeys: new List { "name", "attack", "level" }); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world", "Weapon"); -String excaliburObjectId = "582570f38ac247004f39c24b"; -List selectKeys = new ArrayList<>(); -selectKeys.add("name"); -selectKeys.add("attack"); -selectKeys.add("level"); -leaderboard.getAroundResults(excaliburObjectId, 0, 3, selectKeys, null).subscribe(/* Other logic */); -``` - -```objc -LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"weapons"]; -leaderboard.limit = 3; -LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init]; -option.selectKeys = @[@"name", @"attack", @"level"]; -NSString *excaliburObjectId = @"582570f38ac247004f39c24b"; -[leaderboard getObjectResultsAroundObject:excaliburObjectId, - option:option, - callback:^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // Other logic -}]; -``` - - - -The example above assumes that the weapon leaderboard is an object leaderboard and the class for storing weapons is named `Weapon`. -If the weapon leaderboard has entity as its type, and you only need to retrieve the names of the weapons, the following code would apply: - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("weapons", LCLeaderboard.ENTITY_MEMBER_TYPE); -var rankings = await leaderboard.GetResults(aroundEntity: "excalibur", limit: 3); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world", LCLeaderboard.ENTITY_MEMBER_TYPE); -leaderboard.getAroundResults("excalibur", 0, 3, null, null).subscribe(/* Other logic */); -``` - -```objc -LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"weapons"]; -leaderboard.limit = 3; -[leaderboard getEntityResultsAroundEntity:@"excalibur", - callback:^(NSArray^(NSArray * _Nullable rankings, NSInteger count, NSError * _Nullable error) { - // Other logic -}]; -``` - - - -## Dashboard - -On **Game Services > Cloud Services > Leaderboard**, you can: - -- Create, reset, edit, and delete leaderboards. -- View the current versions of the leaderboards, delete scores, and download the archives of the earlier versions of the leaderboards. -- Configure whether the client can retrieve the previous version of each leaderboard, and whether scores can be updated with the Master Key only. - -## SDK Management Interface - -Besides using the dashboard, you can also manage leaderboards with the management interfaces provided by the C# SDK and the Java SDK. Those interfaces can be used **in trusted environments including the server side**. -Another way to access the management interface is to use the REST API. - -Let’s first take a look at the management interfaces provided by the SDKs. - -:::caution - -To use the management interface, the SDK needs to be initialized with the `masterKey`. Therefore, **you should only use the management interface in trusted environments like the server side, and not the client side.** - -::: - - - -```cs -LCApplication.Initialize({{appid}}, {{appkey}}, "https://xxx.example.com", {{masterkey}}); -LCApplication.UseMasterKey = true; -``` - -```java -LeanCloud.setMasterKey({{masterkey}}); -``` - -```objc -// Not supported yet -``` - - - -### Creating Leaderboards - - - -<> - -```cs -var leaderboard = await LCLeaderboard.CreateLeaderboard("time", order: LCLeaderboardOrder.ASCENDING); -``` - -Below are the available options and their default values: - -```cs -public static async Task CreateLeaderboard(string statisticName, - LCLeaderboardOrder order = LCLeaderboardOrder.Descending, - LCLeaderboardUpdateStrategy updateStrategy = LCLeaderboardUpdateStrategy.Better, - LCLeaderboardVersionChangeInterval versionChangeInterval = LCLeaderboardVersionChangeInterval.Week, - string memberType = LCLeaderboard.USER_MEMBER_TYPE) -``` - -- `statisticName` The name of the leaderboard. -- `order` The order. Can be `LCLeaderboardOrder.Descending` or `LCLeaderboardOrder.Ascending`. -- `updateStrategy` The strategy for updating scores. Can be `LCLeaderboardUpdateStrategy.Better`, `LCLeaderboardUpdateStrategy.Last`, or `LCLeaderboardUpdateStrategy.Sum`. -- `versionChangeInterval` The interval for resetting the leaderboard. Can be `LCLeaderboardVersionChangeInterval.Never`, `LCLeaderboardVersionChangeInterval.Day`, `LCLeaderboardVersionChangeInterval.Week`, or `LCLeaderboardVersionChangeInterval.Month`. -- `memberType` The type of the members. Use `LCLeaderboard.USER_MEMBER_TYPE` for user and `LCLeaderboard.ENTITY_MEMBER_TYPE` for entity. For object, use the name of the class. - - -<> - -```java -LCLeaderboard.createWithMemberType(LCLeaderboard.MEMBER_TYPE_USER, "time", - LCLeaderboard.LCLeaderboardOrder.Ascending, - LCLeaderboard.LCLeaderboardUpdateStrategy.Last, - LCLeaderboard.LCLeaderboardVersionChangeInterval.Day).subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) {} - - @Override - public void onNext(@NotNull final LCLeaderboard lcLeaderboard) { - System.out.println("leaderboard created"); - } - - @Override - public void onError(@NotNull Throwable throwable) { - System.out.println("failed to create leaderboard. Cause " + throwable); - } - - @Override - public void onComplete() {} -}); -``` - -Below are the available options: - -```java -public static Observable createWithMemberType(String memberType, String name, - LCLeaderboardOrder order, - LCLeaderboardUpdateStrategy updateStrategy, - LCLeaderboardVersionChangeInterval versionChangeInterval) -``` - -- `memberType` The type of the members. Use `LCLeaderboard.MEMBER_TYPE_USER` for user and `LCLeaderboard.MEMBER_TYPE_ENTITY` for entity. For object, use the name of the class. -- `name` The name of the leaderboard. -- `order` The order. Can be `LCLeaderboard.LCLeaderboardOrder.Descending` (default) or `LCLeaderboard.LCLeaderboardOrder.Ascending`. -- `updateStrategy` The strategy for updating scores. Can be `LCLeaderboard.LCLeaderboardUpdateStrategy.Better` (default), `LCLeaderboard.LCLeaderboardUpdateStrategy.Last`, or `LCLeaderboard.LCLeaderboardUpdateStrategy.Sum`. -- `versionChangeInterval` The interval for resetting the leaderboard. Can be `LCLeaderboard.LCLeaderboardVersionChangeInterval.Never`, `LCLeaderboard.LCLeaderboardVersionChangeInterval.Day`, `LCLeaderboard.LCLeaderboardVersionChangeInterval.Week` (default), or `LCLeaderboard.LCLeaderboardVersionChangeInterval.Month`. - -“Default” indicates the value used when `null` is provided. - - - -<> - -Not supported yet. - - - - - -### Manually Resetting the Leaderboard - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("score"); -await leaderboard.Reset(); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("score"); -leaderboard.reset().subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) { - } - - @Override - public void onNext(@NotNull Boolean aBoolean) { - if (aBoolean) { // aBoolean should always be true - System.out.println("leaderboard reset"); - } - } - - @override - public void onerror(@notnull throwable throwable) { - system.out.println("Failed to reset leaderboard. Cause " + throwable); - } - - @override - public void oncomplete() {} -}); -``` - -```objc -// Not supported yet -``` - - - -### Retrieving Leaderboard Properties - -Use the following interface to get the properties of a leaderboard, including its reset interval, version, and update strategy. - - - -```cs -var leaderboardData = await LCLeaderboard.GetLeaderboard("world"); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.fetchByName("world").blockingFirst(); -``` - -```objc -// Not supported yet -``` - - - -### Updating Leaderboard Properties - -Once a leaderboard is created, only its reset interval and update strategy can be updated. Other properties cannot be modified. - - - -```cs -var leaderboard = LCLeaderboard.CreateWithoutData("equip"); -await leaderboard.UpdateVersionChangeInterval(LCLeaderboardVersionChangeInterval.Week); -await leaderboard.UpdateUpdateStrategy(LCLeaderboardUpdateStrategy.Last); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("equip"); -leaderboard.updateVersionChangeInterval(LCLeaderboard.LCLeaderboardVersionChangeInterval.Week) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) { - } - - @Override - public void onNext(@NotNull Boolean aBoolean) { - if (aBoolean) { // aBoolean should always be true - System.out.println("version update interval updated"); - } - } - - @override - public void onerror(@notnull throwable throwable) { - system.out.println("Failed to change version update interval. Cause: " + throwable); - } - - @override - public void oncomplete() {} -}); -leaderboard.updateUpdateStrategy(LCLeaderboard.LCLeaderboardUpdateStrategy.Last) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) { - } - - @Override - public void onNext(@NotNull Boolean aBoolean) { - if (aBoolean) { // aBoolean should always be true - System.out.println("update strategy updated"); - } - } - - @override - public void onerror(@notnull throwable throwable) { - system.out.println("Failed to change update strategy. Cause: " + throwable); - } - - @override - public void oncomplete() {} -}); -``` - -```objc -// Not supported yet -``` - - - -### Deleting Leaderboards - - - -```cs -var leaderboard = lcleaderboard.createwithoutdata("equip"); -await leaderboard.destroy(); -``` - -```java -LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("equip"); -leaderboard.destroy().subscribe(new Observer() { - @Override - public void onSubscribe(@NotNull Disposable disposable) { - } - - @Override - public void onNext(@NotNull Boolean aBoolean) { - if (aBoolean) { // aBoolean should always be true - System.out.println("leaderboard deleted"); - } - } - - @override - public void onerror(@notnull throwable throwable) { - system.out.println("Failed to delete leaderboard. Cause " + throwable); - } - - @override - public void oncomplete() {} -}); -``` - -```objc -// Not supported yet -``` - - - -**Deleting a leaderboard will delete all the data within it, including the current version and the archives of the past versions.** - -### Updating Leaderboard Members’ Scores - -You can use the `overwrite` option to bypass the update strategy and force update a member’s score: - - - -```cs -var statistic = new Dictionary { - { "score", 0.0 } -}; -await LCLeaderboard.UpdateStatistics(user, statistic, overwrite: true); -``` - -```java -Map statistic = new HashMap<>(); -statistic.put("world", 0.0); -LCLeaderboard.updateStatistic(currentUser, statistic, true).subscribe(/** Other logic **/); -``` - -```objc -// Not supported yet -``` - - - -Object and entity leaderboards can only be updated on the server side with the Master Key. The update strategy of the leaderboard will still be followed, though: - - - -```cs -var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b"); -await LCLeaderboard.UpdateStatistics(excalibur, statistic); -``` - -```java -// Not supported yet; consider using the REST API -``` - -```objc -Not supported yet. -``` - - - -To force update the data, set `overwrite` to `true`: - - - -```cs -await LCLeaderboard.UpdateStatistics("Vimur", statistic, overwrite: true); -``` - -```java -// Not supported yet; consider using the REST API -``` - -```objc -// Not supported yet -``` - - - -### Deleting Leaderboard Members’ Scores - -Use the Master Key on the server side to delete the score of any user, object, or entity: - - - -```cs -var otherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "5c76107144d90400536fc88b"); -await LCLeaderboard.DeleteStatistics(otherUser, new List { "world" }); - -var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b"); -await LCLeaderboard.DeleteStatistics(excalibur, new List { "weapons" }); - -await LCLeaderboard.DeleteStatistics("Vimur", new List { "rivers" }); -``` - -```java -// Not supported yet; consider using the REST API -``` - -```objc -// Not supported yet -``` - - - -## REST API - -Now we will introduce the Leaderboard-related REST API interfaces. -You can write your own programs or scripts to access these interfaces to perform administrative operations on the server side. - -### Request Format - -For POST and PUT requests, the body of the request must be in JSON, and the `Content-Type` of the HTTP Header should be `application/json`. - -Requests are authenticated by the following key-value pairs in the HTTP Header: - -| Key | Value | Meaning | Source | -| ---------- | ------------ | ------------------------------------------------- | ------------------------------------ | -| `X-LC-Id` | `{{appid}}` | The `App Id` (`Client Id`) of the current app | Can be found on the Developer Center | -| `X-LC-Key` | `{{appkey}}` | The `App Key` (`Client Token`) of the current app | Can be found on the Developer Center | - -To access the management interface, the `Master Key` is required: `X-LC-Key: {{masterkey}},master`. -`Master Key` is also named `Server Secret`, which can be found on the Developer Center as well. - -See [Credentials](/sdk/storage/guide/setup-dotnet#credentials) for more information. - -### Base URL - -The Base URL for the REST API (`{{host}}` in curl examples) is the app’s custom API domain, which can be added and viewed on the Developer Center. See [Domain](/sdk/storage/guide/setup-dotnet#domain) for more information. - -### Managing Leaderboards - -#### Creating Leaderboards - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"statisticName": "world", "memberType": "_User", "order": "descending", "updateStrategy": "better", "versionChangeInterval": "month"}' \ - https://{{host}}/1.1/leaderboard/leaderboards -``` - -| Parameter | Required | Description | -| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| `statisticName` | Required | The name of the leaderboard. Cannot be edited once the leaderboard is created. | -| `memberType` | Required | The type of the members. Cannot be edited once the leaderboard is created. Can be `_Entity`, `_User`, or the name of an existing class. | -| `order` | Optional | The strategy for ordering. Cannot be edited once the leaderboard is created. Can be `ascending` or `descending`. Defaults to `descending`. | -| `updateStrategy` | Optional | Can be `better`, `last`, or `sum`. Defaults to `better`. | -| `versionChangeInterval` | Optional | Can be `day`, `week`, `month`, or `never`. Defaults to `week`. | - -The response body will be a JSON object containing all the parameters provided when creating the leaderboard, as well as the following fields: - -- `version` The version of the leaderboard. -- `expiredAt` The time the leaderboard will be reset for the next time. -- `activatedAt` The time the current version started. - -```json -{ - "objectId": "5b62c15a9f54540062427acc", - "statisticName": "world", - "memberType": "_User", - "versionChangeInterval": "month", - "order": "descending", - "updateStrategy": "better", - "version": 0, - "createdAt": "2018-08-02T08:31:22.294Z", - "updatedAt": "2018-08-02T08:31:22.294Z", - "expiredAt": { - "__type": "Date", - "iso": "2018-08-31T16:00:00.000Z" - }, - "activatedAt": { - "__type": "Date", - "iso": "2018-08-02T08:31:22.290Z" - } -} -``` - -#### Retrieving Leaderboard Properties - -The following interface allows you to retrieve the properties of a leaderboard, including its update strategy and version number. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/leaderboard/leaderboards/ -``` - -The returned JSON object contains all the information related to the leaderboard: - -```json -{ - "objectId": "5b0b97cf06f4fd0abc0abe35", - "statisticName": "world", - "memberType": "_User", - "order": "descending", - "updateStrategy": "better", - "version": 5, - "versionChangeInterval": "day", - "expiredAt": { "__type": "Date", "iso": "2018-05-02T16:00:00.000Z" }, - "activatedAt": { "__type": "Date", "iso": "2018-05-01T16:00:00.000Z" }, - "createdAt": "2018-04-28T05:46:58.579Z", - "updatedAt": "2018-05-01T01:00:00.000Z" -} -``` - -#### Updating Leaderboard Properties - -The following interface allows you to update the `updateStrategy` and `versionChangeInterval` of a leaderboard. Properties other than these cannot be updated. You can update only one of the two properties. For example, to update `versionChangeInterval` only: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"versionChangeInterval": "day"}' \ - https://{{host}}/1.1/leaderboard/leaderboards/ -``` - -The returned JSON object contains all the updated fields as well as an `updatedAt` field. - -```json -{ - "objectId": "5b0b97cf06f4fd0abc0abe35", - "versionChangeInterval": "day", - "updatedAt": "2018-05-01T08:01:00.000Z" -} -``` - -#### Resetting Leaderboards - -The following interface allows you to reset a leaderboard regardless of its reset strategy. Once you reset a leaderboard, the current version of it will be cleared and the cleared data will be archived as a CSV file for you to download. The `version` of the leaderboard will automatically increment by 1. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/leaderboard/leaderboards//incrementVersion -``` - -The returned JSON object will contain the new version number, the time the leaderboard will be reset for the next time (`expiredAt`), and the time the current version started (`activatedAt`): - -```json -{ - "objectId": "5b0b97cf06f4fd0abc0abe35", - "version": 7, - "expiredAt": { "__type": "Date", "iso": "2018-06-03T16:00:00.000Z" }, - "activatedAt": { "__type": "Date", "iso": "2018-05-28T06:02:56.169Z" }, - "updatedAt": "2018-05-28T06:02:56.185Z" -} -``` - -#### Retrieving Archives - -Since each leaderboard can hold at most 60 archive files, we recommend that you retrieve the archived files regularly and back them up in your own places with the following interface. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - https://{{host}}/1.1/leaderboard/leaderboards//archives -``` - -The returned objects will be in decreasing order by `createdAt`. For each object, it has the file name (`file_key`), the URL for downloading (`url`), and a `status` property being one of the following statuses: - -- `scheduled`: The archiving process is queued. This usually won’t last very long. -- `inProgress`: Archiving in progress. -- `failed`: Failed to archive. Please reach out to our technical support. -- `completed`: Successfully archived. - -```json -{ - "results": [ - { - "objectId": "5b0b9da506f4fd0abc0abe6e", - "statisticName": "wins", - "version": 9, - "status": "completed", - "url": "https://lc-paas-files.cn-n1.lcfile.com/yK5s6YJztAwEYiWs.csv", - "file_key": "yK5s6YJztAwEYiWs.csv", - "activatedAt": { "__type": "Date", "iso": "2018-05-28T06:11:49.572Z" }, - "deactivatedAt": { "__type": "Date", "iso": "2018-05-30T06:11:49.951Z" }, - "createdAt": "2018-05-01T16:00.00.000Z", - "updatedAt": "2018-05-28T06:11:50.129Z" - } - ] -} -``` - -#### Deleting Leaderboards - -**This will delete everything within the leaderboard**, including the current version and all the archives. You won’t be able to undo this operation. - -Provide the `statisticName` of the leaderboard to delete it. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/leaderboard/leaderboards/ -``` - -Once done, an empty JSON object will be returned: - -```json -{} -``` - -### Managing Scores - -#### Updating Scores - -Use the Master Key to update any score while still following the `updateStrategy`. - -Provide the corresponding user’s `objectId` when you update their score: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 5}, {"statisticName": "world", "statisticValue": 91}]' \ - https://{{host}}/1.1/leaderboard/users//statistics -``` - -The returned data will be the current score: - -```sh -{ - "results": [ - { - "statisticName": "wins", - "version": 0, - "statisticValue": 5 - }, - { - "statisticName": "world", - "version": 2, - "statisticValue": 91 - } - ] -} -``` - -Similarly, you can provide the `objectId` of an object when updating its score: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 5}, {"statisticName": "weapons","statisticValue": 91}]' \ - https://{{host}}/1.1/leaderboard/objects//statistics -``` - -For entity, provide the string for it: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 5}, {"statisticName": "cities","statisticValue": 91}]' \ - https://{{host}}/1.1/leaderboard/entities//statistics -``` - -The current user can update their own score, though this won’t require the `Master Key` for the management interface. However, the `sessionToken` of the current user needs to be provided (the SDK has already encapsulated this interface): - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 5}, {"statisticName": "world", "statisticValue": 91}]' \ - https://{{host}}/1.1/leaderboard/users/self/statistics -``` - -#### Force Updating Scores - -Add `overwrite=1` to ignore the `better` and `sum` update strategies and use `last` instead. -For example, if cheating is detected on a user, you can force update the user’s score with this interface. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '[{"statisticName": "wins", "statisticValue": 10}]' \ - https://{{host}}/1.1/leaderboard/users//statistics?overwrite=1 -``` - -The returned data is the current score: - -```json -{ "results": [{ "statisticName": "wins", "version": 0, "statisticValue": 10 }] } -``` - -`overwrite=1` can be used for object scores and entity scores as well. - -#### Deleting Scores - -Use this interface to remove the score and ranking of a user from the leaderboard. Note that only the score on the current version of the leaderboard can be removed. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/leaderboard/users//statistics?statistics=wins,world -``` - -Once done, an empty object will be returned: - -``` -{} -``` - -Similarly, you can delete the score of an object: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - --data-urlencode 'statistics=weapons,equipments' \ - https://{{host}}/1.1/leaderboard/objects//statistics -``` - -And the score of an entity: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - --data-urlencode 'statistics=cities' \ - https://{{host}}/1.1/leaderboard/entities//statistics -``` - -The current user can delete their own score, though this won’t require the `Master Key` for the management interface. However, the `sessionToken` of the current user needs to be provided (the SDK has already encapsulated this interface): - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: " \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/leaderboard/users/self/statistics?statistics=wins,world -``` - -### Retrieving Scores - -The REST API interfaces for retrieving scores are not management interfaces and the `Master Key` is not required: - -#### Retrieving a Single Score - -Retrieve a score by providing the `objectId` of a user. -You can specify multiple leaderboards within the `statistics` property (separated by `,`) to get the scores of the user in all the given leaderboards. If this option is not provided, the user’s scores in all leaderboards will be returned. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - --data-urlencode 'statistics=wins,world' \ - https://{{host}}/1.1/leaderboard/users//statistics -``` - -Response: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 5, - "version": 0, - "user": { - "__type": "Pointer", - "className": "_User", - "objectId": "60d950629be318a249000001" - } - }, - { - "statisticName": "world", - "statisticValue": 91, - "version": 0, - "user": {...} - } - ] -} -``` - -Similarly, you can get an object’s scores by providing its `objectId`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - --data-urlencode 'statistics=wins,world' \ - https://{{host}}/1.1/leaderboard/objects//statistics -``` - -Response: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 5, - "version": 0, - "object": { - "__type": "Pointer", - "className": "Weapon", - "objectId": "60d1af149be3180684000002" - } - }, - { - "statisticName": "world", - "statisticValue": 91, - "version": 0, - "object": { - "__type": "Pointer", - "className": "Weapon", - "objectId": "60d1af149be3180684000002" - } - } - ] -} -``` - -To get an entity’s scores, provide the string for the entity: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - --data-urlencode 'statistics=wins,world' \ - https://{{host}}/1.1/leaderboard/entities//statistics -``` - -Response: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 5, - "version": 0, - "entity": "1a2b3c4d" - }, - { - "statisticName": "world", - "statisticValue": 91, - "version": 0, - "entity": "1a2b3c4d" - } - ] -} -``` - -#### Retrieving a Group of Scores - -With this interface, you can get the scores of no more than 200 users at once. To use this interface, provide an array of `objectId`s of the users in the body. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '["60d950629be318a249000001", "60d950629be318a249000000"]' - https://{{host}}/1.1/leaderboard/users/statistics/ -``` - -The response is similar to that of [retrieving a single score](#retrieving-a-single-score): - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 1, - "version": 0, - "user": { - "__type": "Pointer", - "className": "_User", - "objectId": "60d950629be318a249000001" - } - }, - { - "statisticName": "wins", - "statisticValue": 2, - "version": 0, - "user": { - "__type": "Pointer", - "className": "_User", - "objectId": "60d950629be318a249000000" - } - } - ] -} -``` - -Similarly, provide a list of `objectId`s of objects (no more than 200) to get these objects’ scores: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '["60d950629be318a249000001", "60d950629be318a249000000"]' - https://{{host}}/1.1/leaderboard/objects/statistics/ -``` - -Provide a list of strings of entities (no more than 200) to get these entities’ scores: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '["Vimur", "Fimbulthul"]' - https://{{host}}/1.1/leaderboard/entities/statistics/ -``` - -### Queries on Leaderboards - -#### Retrieving Scores Within a Scope - -Use this interface to retrieve the top players. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=20' \ - --data-urlencode 'selectKeys=username,club' \ - --data-urlencode 'includeKeys=club' \ - --data-urlencode 'includeStatistics=wins' \ - https://{{host}}/1.1/leaderboard/leaderboards/user//ranks -``` - -| Parameter | Required | Description | -| ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| startPosition | Optional | The offset of the query. Defaults to 0. | -| maxResultsCount | Optional | The maximum number of results. Defaults to 20. | -| selectKeys | Optional | Return the other fields of the user in the `_User` class. You can provide multiple fields separated by `,`. For security reasons, the `email` and `mobilePhoneNumber` fields will not be returned if you are not using the `masterKey`. | -| includeKeys | Optional | Return the referenced objects of the other fields of the user in the `_User` class. You can provide multiple fields separated by `,`. For security reasons, the `email` and `mobilePhoneNumber` fields will not be returned if you are not using the `masterKey`. | -| includeStatistics | Optional | Return the user’s scores in other leaderboards. If a non-existant leaderboard is provided, an error will be returned. | -| version | Optional | Return the results from a specific version. By default, the results from the current version will be returned. | -| count | Optional | If set to 1, the number of members in the leaderboard will be returned. Defaults to 0. | - -The response will be a JSON object: - -```json -{ - "results": [ - { - "statisticName": "world", - "statisticValue": 91, - "rank": 0, - "user": { - "__type": "Pointer", - "className": "_User", - "updatedAt": "2021-07-21T03:08:10.487Z", - "username": "zw1stza3fy701rvgxqwiikex7", - "createdAt": "2020-09-04T04:23:04.795Z", - "club": { - "objectId": "60f78f98d9f1465d3b1da12d", - "name": "board games", - "updatedAt": "2021-07-21T03:08:08.692Z", - "createdAt": "2021-07-21T03:08:08.692Z", - }, - "objectId": "5f51c1287628f2468aa696e6" - } - }, - {...} - ], - "count": 500 -} -``` - -Querying on object leaderboards shares a similar interface. The only difference is that `user` will be replaced by `object`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=2' \ - --data-urlencode 'selectKeys=name,image' \ - --data-urlencode 'includeKeys=image' \ - --data-urlencode 'count=1' \ - https://{{host}}/1.1/leaderboard/leaderboards/object//ranks -``` - -Response: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 4, - "rank": 0, - "object": { - "__type": "Pointer", - "className": "Weapon", - "name": "sword", - "image": { - "bucket": "test_files", - "provider": "leancloud", - "name": "sword.jpg", - "url": "https://example.com/sword.jpg", - "objectId": "60d2f3a39be3183377000002", - "__type": "File" - }, - "objectId": "60d2f22f9be318328b000007" - } - }, - { - "statisticName": "wins", - "statisticValue": 3, - "rank": 1, - "object": {...} - } - ], - "count": 500 -} -``` - -Change `user` to `entity` to query on entity leaderboards: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=2' \ - --data-urlencode 'count=1' \ - https://{{host}}/1.1/leaderboard/leaderboards/entity//ranks -``` - -Response: - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 4, - "rank": 0, - "entity": "1234567890" - }, - { - "statisticName": "wins", - "statisticValue": 3, - "rank": 1, - "entity": "2345678901" - } - ], - "count": 500 -} -``` - -#### Retrieving Members With Similar Rankings as the Given Member - -Add the corresponding `objectId` to the end of the URL to retrieve the users and objects with similar rankings as the given one. - -To get the users with similar rankings as the given user: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=20' \ - --data-urlencode 'selectKeys=username,club' \ - --data-urlencode 'includeKeys=club' \ - https://{{host}}/1.1/leaderboard/leaderboards/user//ranks/ -``` - -See [Retrieving Scores Within a Scope](#retrieving-scores-within-a-scope) for the meanings of the parameters. -The response is similar to that for [retrieving-scores-within-a-scope](#retrieving-scores-within-a-scope). - -```json -{ - "results": [ - { - "statisticName": "wins", - "statisticValue": 3, - "rank": 2, - "user": {...} - }, - { - "statisticName": "wins", - "statisticValue": 2.5, - "rank": 3, - "user": { - "__type": "Pointer", - "className": "_User", - "username": "kate", - "club": { - "objectId": "60f78f98d9f1465d3b1da12d", - "name": "board games", - "updatedAt": "2021-07-21T03:08:08.692Z", - "createdAt": "2021-07-21T03:08:08.692Z", - }, - "objectId": "60d2faa99be3183623000001" - } - }, - { - "statisticName": "wins", - "statisticValue": 2, - "rank": 4, - "user": {...} - } - ], - "count": 500 -} -``` - -To get the objects with similar rankings as the given object: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=2' \ - --data-urlencode 'selectKeys=name,image' \ - --data-urlencode 'includeKeys=image' \ - --data-urlencode 'count=1' \ - https://{{host}}/1.1/leaderboard/leaderboards/object//ranks/ -``` - -To get the entities with similar rankings as the given entity: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'startPosition=0' \ - --data-urlencode 'maxResultsCount=2' \ - --data-urlencode 'count=1' \ - https://{{host}}/1.1/leaderboard/leaderboards/entity//ranks/ -``` - -## Video Tutorials - -You can refer to the video tutorial:[How to Integrate Leaderboard in Games](https://www.bilibili.com/video/BV1ZN411s7Jj/) to learn how to access leaderboard in Untiy projects. - -For more video tutorials, see [Developer Academy](https://developer.taptap.cn/tds-tutorials/list). As the SDK features are constantly being improved, there may be inconsistencies between the video tutorials and the new SDK features, so the current documentation should prevail. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/_category_.json deleted file mode 100644 index 234726994..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "多人在线对战", - "collapsed": true, - "position": 20 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/_category_.json deleted file mode 100644 index 7bf7181d3..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Client Engine", - "collapsed": true, - "position": 4 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/client-engine.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/client-engine.mdx deleted file mode 100644 index 1e371ea09..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/client-engine.mdx +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Client Engine Overview -sidebar_label: Overview -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -:::info -**Before reading this document, please read the [Multiplayer Service Features](/sdk/multiplayer/features/) and [MasterClient](/sdk/multiplayer/guide/js/#masterclient) to understand the architecture you can use for your game when developing features with Multiplayer.** -::: - -## The problem solved by Client Engine - -The multiplayer service (i.e. the multiplayer service in the figure below) is a good solution to the problem of abstracting rooms and exchanging messages between players in a room. Let's take the game Rock, Paper, Scissors as an example, the game flow looks like this: - -![image](/img/client-engine/client-engine1.png) - -where the multiplayer service only plays a role of message relay, and for the sake of discussion we can simplify this diagram a bit (the dotted line represents the message being relayed through the multiplayer service): - -![image](/img/client-engine/client-engine2.png) - -This process is simple, but there are a few problems: - -1. All players have the same god view: they can see all states, and the player who throws a punch later (like B in the picture) can throw a punch according to A's choice to get a sure win. -2. The final result is reported by the client, and the client can fake the result. -3. A, as the master client, can manipulate some operations involving randomness, such as shuffling cards, rolling dice, etc. (There is no similar mechanism in Rock, Paper, Scissors). - -Different types of games have different tolerances for these problems. Without changing the flow of the diagram above, each of these problems can be solved in its own way. However, the Client Engine solution attempts to solve these problems by taking a radical step away from them: running the MasterClient on the server side. In the Client Engine scenario, the flow of the game is as follows: - -![image](/img/client-engine/client-engine3.png) - -In this process, the MasterClient running on the server is the only referee with a God's eye view. All players exchange information with the MasterClient, and the MasterClient will only synchronize some information with the client (e.g. it will only tell B that A threw a punch, but what was thrown is unknown). The game logic (including randomness, win/loss decisions) and the reporting of final results are performed on the server. - -**The Multiplayer service is the foundation. The player client does not communicate directly with the MasterClient. The dotted lines in the diagram indicate that messages are still routed through the Multiplayer service.** - -## Client Engine Introduction - -Client Engine is a client-hosting solution for multiplayer online games. The [Multiplayer Service](/sdk/multiplayer/features/) provides a MasterClient mechanism to control the game logic: MasterClient is a special client that receives and processes all events and messages in the game, processes them in real time, and then sends the results to the other game clients, controlling the execution of the game. Developers can develop a complete set of MasterClient logic based on the Multiplayer SDK, and then host such a client in the Client Engine, saving the burden of deploying and maintaining the program. See the picture below: - -![image](/img/client-engine/structure.png) - -In addition to hosting MasterClient, developers can also do the following things in Client Engine: - -* Host regular virtual players to increase the fun and activity of the game. -* Customise the REST API to develop additional logic. - -The benefits of hosting game logic in the Client Engine include: - -* Reduced network latency. The game running process involves frequent message interactions among game players, the Multiplayer cloud, and MasterClient. Client Engine and the Multiplayer cloud are in the same physical network, which greatly reduces the transmission delay caused by the public network. -* Client Engine provides perfect log collection, status monitoring, load balancing and automatic fault recovery mechanism, which can provide higher stability guarantee. -* Client Engine provides a huge resource pool, which can quickly respond to the temporary and sudden expansion demand of a single game product without having developers manually adjust the instances. Expansions are done automatically. - -## Documentation and Demos - -For more information on using the Client Engine, please refer to the documentation: - -* [Client Engine Quick Start - Node.js](/sdk/multiplayer/client-engine/quick-start-node/) describes how to get started with your first project, how to develop and debug locally, and how to deploy to the cloud. -* [Your First Client Engine Game - Node.js](/sdk/multiplayer/client-engine/first-game-node/) is a tutorial that will help you get started with a rock-paper-scissors guessing game using the Client Engine. After completing this tutorial you will have a basic understanding of the Client Engine process. -* [Client Engine Development Guide - Node.js](/sdk/multiplayer/client-engine/guide-node/) provides an in-depth explanation of the Client Engine SDK based on the first project. - -Example demo: - -* [Turn-based Demo](/sdk/multiplayer/client-engine/demo/#turn-based-demo). - -## Price - -### Developer Edition - -Developer Edition provides a trial edition for developers to use free of charge: - -* Free 100 CCU and 50% CPU. -* No staging environment provided. -* No auto-scaling and load balancing. -* Forced hibernation. - -Hibernation policy: - -Standard instances do not hibernate. - -Trial instances enforce the following hibernation policies: - -* If the application has had no external requests in the last half hour, it hibernates. -* If a new external request is made after the hibernation, the instance is started immediately. The response time for the first request is 5 to 30 seconds (depending on the instance startup time), and the response time for subsequent requests returns to normal. -* Forced hibernation: The instance will be forced into hibernation if the total number of hours of operation in the last 24 hours exceeds 18 hours. At this point, new requests will receive a 503 error response code, which can be viewed in **Cloud Services Console > Play > Client Engine > Statistics**. - -If you don't want the service to be interrupted due to trial instances in the staging environment being forced to hibernate, or if you need multiple instances to fully simulate the production environment, you can purchase standard instances in the trial environment as needed. - -### Business Edition - -With Business Edition, you can upgrade from the trial version to the standard version via the console. Standard version provides a staging environment, supports automatic scaling and load balancing, and does not hibernate. - -Standard version is scaled and billed by Compute Unit. A Compute Unit contains 100 CCU and 50% CPU and we will automatically add more Compute Units if any of them gets exhausted. For example, if the Client Engine is using 80 CCU and 90% CPU at a particular time, the system will allocate 2 units. - - - -The system will charge according to the daily peak consumption of computing units. The price of a single computing unit can be found on the [official website](https://developer.taptap.io/product-intro/price). For example, if an application on a China node uses up to 2 computing units on a given day, the charge for that day will be 2 * computing unit price of China. - - - - - -The system will charge according to the daily peak consumption of computing units. The price of a single computing unit can be found on the [official website](https://developer.taptap.io/product-intro/price). For example, if an application on a China node uses up to 2 computing units on a given day, the charge for that day will be 2 * computing unit price of China. - - - - - -:::info -Note: After upgrading to the Standard version, the service will be provided and billed at a minimum of 1 compute unit, regardless of whether it is used or not. -::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/demo.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/demo.mdx deleted file mode 100644 index 4f67c9ab8..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/demo.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Game Demo Example -sidebar_label: Demo -sidebar_position: 5 ---- - -This is a collection of game-related demos that you can use when developing your own projects. - -## Multiplayer - -### Round Robin Battle Demo - -This demo is implemented with [Multiplayer](/sdk/multiplayer/features/) and [Client Engine](/sdk/multiplayer/client-engine/). The language used is JavaScript. It took about 7 days to implement all server-side and client-side code. The main features are: quick start, character attribute setting, in-room battle, etc. For details, please click [project link](https://github.com/leancloud/multiplayer-turn-based-game-demo). - -### Real-time battle demo - -This demo is a streamlined version of the game "Ball Battle" made with [Multiplayer](/sdk/multiplayer/features/), Cocos Creator (JavaScript), and Unity (C#). It took about 8 days to build the project. The demo mainly demonstrates the logic related to mobile synchronization. - -For more information: - -- [Cocos Creator project](https://github.com/onerain88/BallBattle) - -- [Unity project](https://github.com/onerain88/BallBattle-Unity) - -## Weakly connected single player game - -LeanCloud Anniversary Game is a WeChat game where players can score points by clicking on falling cakes, and the highest scorers can win prizes. The server side mainly uses [Cloud Engine](/sdk/engine/overview/) and [Leaderboard](/sdk/leaderboard/features/). For details, please click [project link](https://github.com/leancloud/LeanCloudBirthday). - -Scan the code with WeChat to try the game: - -![image](/img/client-engine/leancloud-birthday-game.jpg) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/first-game-node.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/first-game-node.mdx deleted file mode 100644 index 7659882e1..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/first-game-node.mdx +++ /dev/null @@ -1,474 +0,0 @@ ---- -title: Your first game with Client Engine - Node.js -sidebar_label: Your first game -sidebar_position: 3 ---- - -This document will help you get up to speed on implementing a rock-paper-scissors guessing game with the Client Engine. After completing this tutorial you will have a basic understanding of the Client Engine process. - -## Preparing your first project - -The game is divided into two parts: the server side, which is implemented by the Client Engine, and the client side, which is a simple web page. In this tutorial we will focus on teaching you how to write the Client Engine code step by step. For the client-side code please refer to the sample project. - -### Client Engine Project - -Please read [Client Engine Quickstart: Running and Deploying a Project](/sdk/multiplayer/client-engine/quick-start-node/) to get the initial project and learn how to run and deploy a project locally. - -`./src` contains the following source files: - -``` -├── configs.ts // Configuration files -├── index.ts // The project entry point -├── reception.ts // Reception class implementation file; subclass of GameManager; responsible for managing Game; contains the custom methods to create Game -└── rps-game.ts // RPSGame class implementation file; a subclass of Game, in which the specific logic of the guessing game is written -``` - -The `Game` and `GameManager` in this project use the functionality of the Client Engine SDK. Please refer to the [Client Engine Developer's Guide](/sdk/multiplayer/client-engine/guide-node/) for more details on how to use the SDK. - -You can get a feel for the project by starting with the `index.ts` file, which is the entry point to the project, and defines a web API called `/reservation` using the express framework to issue new room names to clients when quick-starting new games. - -`reception.ts` and `rps-game.ts` contain all the code for this tutorial. You can choose to make a copy of these two files, empty them, and write your own code based on this document, or view the code already written for comparison. - -### Client Project - -[Click here to download the client project](https://github.com/leancloud/client-engine-demo-webapp). **Open `config.ts` in `./src`, change the appId and appKey to your own information**, and follow the README to start the project and observe the interface changes. The game-related logic is in `./src/components`. You can open this file to view the code if needed. - -## Core Process - -In the Multiplayer service, the room is created by a MasterClient, so in this game, each room is created by a MasterClient managed by the Client Engine that calls the interfaces related to the Multiplayer service. There are multiple MasterClients in the Client Engine, and each MasterClient manages the game logic in its own room. - -The core logic of this game is: **MasterClient in Client Engine and player client join in the same room. In the communication process, the MasterClient controls the logic of the game.** The specific steps are as follows: - -1. The player client connects to the [Multiplayer service](/sdk/multiplayer/features/) and requests the `/reservation` interface provided by the Client Engine to start the game quickly. -2. Each time the Client Engine receives a request, it will check if there is an available room, if there is, it will return the existing roomName to the client, if there is not, it will create a new MasterClient and create a new room, and return the roomName to the client. -3. The Client joins the room using the roomName returned by the Client Engine. -4. MasterClient and Client are in the same room. Each time the Client throws a punch it will send a message to the MasterClient, the MasterClient will forward the message to other Clients, and finally judge the game result. -5. The MasterClient decides that the game is over. The Client leaves the room and the Client Engine destroys the game. - -## Code development - -### Customize Game - -Our goal is to get the MasterClient and Client into the same room. The first step is to prepare the room in the Client Engine. In the Client Engine SDK, each room corresponds to a `Game` object, and each `Game` object corresponds to its own MasterClient. Next, we create a subclass `RPSGame` that inherits from `Game`, and write the in-room logic for the guessing game in `RPSGame`. - -To initialize the custom `RPSGame` in `rpg-game.ts`: - -```js -import { Game } from "@leancloud/client-engine"; -import { Event, Play, Room } from "@leancloud/play"; -export default class RPSGame extends Game { - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - } -} -``` - -### Managing the Game - -In the Client Engine SDK, the `GameManager` is responsible for creating and destroying the `Game`. For more information, please refer to the [Client Engine Developer's Guide](/sdk/multiplayer/client-engine/guide-node/). In this document, we can utilize the `GameManager` management functions through simple configuration. - -#### Customizing the GameManager - -First, create a subclass `Reception` by inheriting from `GameManager`. We can use the methods provided by `GameManager` to assist us in writing our own logic. - -Initialize the custom `Reception` in the `reception.ts` file: - -```js -import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine"; -export default class Reception extends GameManager { - -} -``` - -This custom class `Reception` is used to manage `Game` objects of type T, which in the actual game will be instances of your custom `Game` type. Next, we use the `GameManager` method in `reception` to implement our own custom logic: quick start. - -#### Implementing Logic: Quick Start - -The implemented quick start logic finds and returns a random room with available seats to the client. If there is no available room in the current Client Engine instance, the logic creates a new room and returns it to the client. To enable the [Entry API](#entry-api-quick-start) to call the implemented logic, a custom method called `makeReservation()` is written in the `Reception` class. - -```js -import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine"; -export default class Reception extends GameManager { - - /** - * Reserve a game for the given player. If no game is available, a new one will be created. - * @param playerId The ID of the player who made the reservation. - * @return The room name of the successfully reserved game. - */ - public async makeReservation(playerId: string) { - let game: T; - const availableGames = this.getAvailableGames(); - if (availableGames.length > 0) { - game = availableGames[0]; - this.reserveSeats(game, playerId); - } else { - game = await this.createGame(playerId); - } - return game.room.name; - } - -} -``` - -In this code, we call `GameManager`'s `getAvailableGames()` to get the `Game` objects managed by the current Client Engine instance: - - -* If there are `Game` instances with empty seats in the room, use the `reserveSeats()` method of the `GameManager` to take the seats for the player and return the roomName. -* If all `Game` rooms are full, use `createGame()` method of `GameManager` to create a new room and return roomName. - -#### Implementing Logic: Create New Game - - -If you want to create a room yourself and then invite your friends to join the room, you can write a method for creating a new game in `reception` for [Entry API](#entry-entry-api-creating-a-new-game) to call. Similarly, the `createGame()` method in our custom `createGameAndGetName()` method is provided by the `GameManager` in the SDK. - -```js -export default class Reception extends GameManager { - - public async makeReservation(playerId: string) { - ...... - } - - /** - * Creates a new game. - * @param playerId The player ID of the reservation. - * @param options Some configuration items that can be specified when creating a new game. - * @return The room name of the game being created. - */ - public async createGameAndGetName(playerId: string, options?: ICreateGameOptions) { - const game = await this.createGame(playerId, options); - return game.room.name; - } - -} -``` - -#### Binding GameManager and Game - -Once both `Reception`, a subclass of `GameManager`, and `RPSGame`, a subclass of `Game`, are initialized, we need to provide `RPSGame` to `Reception` within the entry point of the entire project and have `Reception` manage `RPSGame`. - -The `index.ts` file contains a method which creates the `Reception` object. In this method, the first parameter is provided with `RPSGame`. Should your custom `Game` class have a different name, you can substitute `RPSGame` with your custom `Game` class. - -```js -import PRSGame from "./rps-game"; -const reception = new Reception( - PRSGame, - APP_ID, - APP_KEY, - { - concurrency: 2, - }, -); -``` - -Once configured in this section, `reception` will create and manage `PRSGame` along with the corresponding MasterClient when it is appropriate. - -#### Load Balancing Configuration - -As the logic in `GameManager` is invoked directly by external requests, load balancing must be configured for `GameManager` at the entry point. For more details on load balancing, please consult the [Client Engine Developer's Guide](/sdk/multiplayer/client-engine/guide-node/#load-balancing). In this section, we can refer to the `index.ts` file code to learn how to configure it. - -```js -import { ICreateGameOptions,LoadBalancerFactory } from "@leancloud/client-engine"; - -// Create the object responsible for load balancing. No need to change the code here; just copy and paste it. -const loadBalancerFactory = new LoadBalancerFactory({ - poolId: `${APP_ID.slice(0, 5)}-${process.env.LEANCLOUD_APP_ENV || "development"}`, - redisUrl: process.env.REDIS_URL__CLIENT_ENGINE, -}); - -// Configure load balancing with reception and our custom method makeReservation. -loadBalancerFactory.bind(reception, ["makeReservation", "createGameAndGetName"]) -``` - -Now that the `reception` for managing the `RPSGame` has been prepared, we'll start writing the specific in-room game logic. - -### Setting the number of players in a room - -In this guessing game, we've set it so that only two players are allowed to play, and no new players are allowed to enter the room when there are already two players. You can set the `Game`'s static property `defaultSeatCount` like this: - -```js -export default class RPSGame extends Game { - public static defaultSeatCount = 2; -} -``` - -After configuring, the Client Engine initial project will limit the number of players in a room every time it requests the Multiplayer service to create a room, based on the value set here. - -For more information on setting the number of players in a room, please refer to the [Client Engine Developer's Guide](/sdk/multiplayer/client-engine/guide-node/#setting-the-number-of-players-in-a-room). - -### The MasterClient and the client enter the same room - -Once the basic configuration of the `Game` is complete, both the MasterClient and Client are now able to enter the same room. - -#### Entry API Quick Start - -When a client makes a request to the `/reservation` API endpoint specified in the `index.ts` file, the endpoint will invoke the `makeReservation()` method in `Reception`, which helps the client start the game quickly. - -```js -app.post("/reservation", async (req, res, next) => { - try { - const { - playerId, - } = req.body as { - playerId: any - }; - if (typeof playerId !== "string") { - throw new Error("Missing playerId"); - } - debug(`Making reservation for player[${playerId}]`); - // Call the makeReservation() method we prepared in the Reception class. - const roomName = await reception.makeReservation(playerId); - debug(`Seat reserved, room: ${roomName}`); - return res.json({ - roomName, - }); - } catch (error) { - next(error); - } -}); -``` - -Clients can call this API to get a quick start. Sample code using this interface is as follows **(not Client Engine code)**: - -```js -// Here the client calls the `/reservation` interface implemented in the Client Engine over HTTP. -const { roomName } = await (await fetch( - `${CLIENT_ENGINE_SERVER}/reservation`, - { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - playerId: play.userId, - }) - } -)).json(); -// Join the room -return play.joinRoom(roomName); -``` - -When the client calls `/reservation` and joins the room successfully, it means the Client and MasterClient are in the same room, and you can start the game when there are enough people in the room. - -The code to call `/reservation` is already written in the client project, so you don't need to write the code by yourself. You can check the related code in `/src/components/Lobby.vue`. - -#### Entry API Creating a New Game - -This entry API is written in the same way as Quick Start, so I won't repeat the instructions, but you can refer to the `/game` method in the index.ts file. - -### Announcing the start of a game - -In this project, we can start the game when a room is full. We can announce the start of the game in the Game's room-full event: - -```js -import { AutomaticGameEvent, Game, watchRoomFull } from "@leancloud/client-engine"; -import { Play, Room } from "@leancloud/play"; - -enum Event { - Start = 10, -}; - -@watchRoomFull() -export default class RPSGame extends Game { - public static defaultSeatCount = 2; - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - // Listen for the ROOM_FULL event and call the `start() method` when it receives it. - this.once(AutomaticGameEvent.ROOM_FULL, this.start); - } - - protected start = async () => { - // Mark the room as closed - this.masterClient.setRoomOpened(false); - // Broadcast the game start event to the client - this.broadcast(Event.Start); - } -} -``` - -In this code, the `watchRoomFull` decorator causes `Game` to throw the `AutomaticGameEvent.ROOM_FULL` event when the room is full, where we choose to call our custom `start` method. In the `start` method we open the room and broadcast the start of the game to all clients. - -At this point, you can start the current Client Engine project, launch the client and open two client web pages, click "Quick Start" on the interface, and observe that the first interface in which you clicked "Quick Start" shows the log: `xxxx joined the room`. - -### Guessing Logic - -Now let's start developing the in-game logic. The steps are as follows: - -1. Player A selects a gesture and sends a punch event to the MasterClient. -2. MasterClient receives the event and forwards it to Player B. -3. Player B receives the event forwarded by MasterClient, and the interface displays: the opponent has chosen. -4. Player B selects gesture and sends punch event to MasterClient. -5. MasterClient receives the event and forwards it to Player A. -6. Player A receives the event forwarded by MasterClient, and the interface displays: opponent has chosen. -7. MasterClient finds that both players have thrown punches, so it judges the result, announces the answer, and declares the end of the game. - -The interaction between these three players can be illustrated by this diagram: - -![image](/img/client-engine/rps-game-flow.png) - -Next, we break down each step and write the code: - -#### Player A selects a gesture and sends a punch event to the MasterClient - -This part of the code is client-side only and **you don't need to write it in the Client Engine**. You can find the code in the client-side project's `./src/components/Game.vue`. - -```js -enum Event { - Start = 10, - Play = 11, -}; -choices = ["✊", "✌️", "✋"]; - -// When the user makes a selection, we send the index of the corresponding option to the server. -play.sendEvent(Event.Play, {index}, {receiverGroup: ReceiverGroup.MasterClient}); -``` - -#### MasterClient receives the event and forwards it to Player B - -This part of the code is written in Client Engine. You can write it in your own `RPSGame` based on the sample code below. We register the custom event in the `start` method, and when we receive the `play` event, we clear the contents of player A's action and forward it to player B. - -```js -protected start = async () => { - ...... - // Receiving custom events - this.masterClient.on(PlayEvent.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { - if (eventId === Event.Play) { - // Receive events from other players and forward the events - this.forwardToTheRests({ eventId, eventData, senderId }, (eventData) => { - return {} - }) - } - }); -} -``` - -In this code, the MasterClient object in Game registers a custom event for multiplayer games, which is triggered when Player A sends the `play` event to the MasterClient. We use Game's forward event method `forwardToTheRests()` in this event. The first parameter of this method is the original event, and the second parameter is the eventData data handler of the original event. We change the original eventData data, that is the `{index}` sent by player A, to empty data, so that when player B receives the event, he can't know the details of player A's action. - -#### The player B receives the event forwarded by the MasterClient and the interface shows that the opponent has selected. - -This part of the code is client-side and **you don't need to write it in the Client Engine**. You can find it in the client-side project's `./src/components/Game.vue`. - -```js -play.on(PlayEvent.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { - ...... - switch (eventId) { - ...... - case Event.Play: - this.log(`The opponent has chosen`); - break; - ..... - } -}); -``` - -#### Player B selects a gesture and sends a punch event to MasterClient - -This part of the logic is the same as "Player A selects a gesture and sends a punch event to MasterClient" above, and uses the same part of the code. You can find the code in the client project's `./src/components/Game.vue`. - -#### MasterClient receives an event and passes it on to Player A - -This part of the logic is the same as "MasterClient receives event and forwards event to player B" above, and uses the same part of the code, so you don't need to write any additional code in the Client Engine. - -#### Player A receives the event forwarded by the MasterClient and the interface shows: the opponent has selected - -This part of the logic is the same as the "Player A receives event forwarded by MasterClient and the interface displays: opponent selected" above, using the same part of the code. You can find the code in the client project's `./src/components/Game.vue` in the client project. - -At this point you can run the project, open both interfaces for guessing, and observe that both players' actions are synchronised to their respective interfaces, but each does not know what the other player has chosen. - -#### MasterClient detects that both players have thrown a punch, determines the result, announces the answer, and declares the game over - -Each time the MasterClient receives a player's choice event, we need to store the player's choice and determine if both players have made their choices: - -```js -protected start = async () => { - ...... - // [this.player[0]'s choice, this.player[1]'s choice]. When neither player has a choice, it is assumed that both players have a choice of -1 - const choices = [-1, -1]; - - this.masterClient.on(PlayEvent.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { - if (eventId === Event.Play) { - // Receive events from other players and forward events - ...... - // Stores the current player's selection - if (this.players[0].actorId === senderId) { - // If it's player[0], store it in choices[0]. - choices[0] = eventData.index; - } else { - // If it's player[1], store it in choices[1]. - choices[1] = eventData.index; - } - } - }); -} -``` - -In the above code, we construct a choice of type Array to store the player's choices, and store the user's choices when the punch event is received, then we determine whether both players have made their choices, and if they have, we broadcast the result of the game, and broadcast the end of the game: - -```js -enum Event { - Start = 10, - Play = 11, - Over = 20, -}; -...... -protected start = async () => { - ...... - // [this.player[0]'s choice, this.player[1]'s choice]. When neither player has a choice, it is assumed that both players have a choice of -1 - const choices = [-1, -1]; - - this.masterClient.on(PlayEvent.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { - if (eventId === Event.Play) { - // Receive events from other players and forward events - ...... - // Store the current player's selection - ...... - // Check if both players have already made their choices - if (choices.every((choice) => choice > 0)) { - // Both players have made their choices. The game is over, and the result is broadcast to the client. - const winner = this.getWinner(choices); - this.broadcast(Event.Over, { - choices, - winnerId: winner ? winner.userId : null, - }); - } - } - }); -} - -``` - -In the above code, the `getWinner()` method is used to get the result of the game. This is our customized method to determine the winner. You can copy and paste the code below directly into your own `RPSGame` file: - -```js -// The client's array of punches is :[✊, ✌️, ✋]. -// ✊ (index is 0) beats✌️ (index is 1), so wins[0] = 1, and so on -const wins = [1, 2, 0]; - -@watchRoomFull() -export default class RPSGame extends Game { - ...... - - /** - * Calculate the winner based on the player's choices - * @return returns the winning Player, or null for a tie - */ - private getWinner([player1Choice, player2Choice]: number[]) { - if (player1Choice === player2Choice) { return null; } - if (wins[player1Choice] === player2Choice) { return this.players[0]; } - return this.players[1]; - } -} -``` - -The client receives the MasterClient broadcast end event and then displays the corresponding result on the interface. Here the basic logic has been developed. You can run the project, open the two pages, and happily start your own battle with yourself. - -### Leaving the room - -When all the clients leave the room, `GameManager` will help us to destroy the empty room, so we don't need to write this part of code in our game. - -### RxJS - -When you look at the sample demo, you will see that the code is a bit more compact compared to the code in this document, because the sample demo uses RxJS. If you are interested, you can study the [RxJS](https://rxjs-dev.firebaseapp.com/) and the [API documentation](https://rxjs-dev.firebaseapp.com/api) of the related interfaces by yourself. - -## Development Guide - -After you have developed the guessing game step by step according to the instructions in this document, you must have a preliminary feeling about the Client Engine SDK and the initial project. Now you can refer to the [Client Engine Developer's Guide](/sdk/multiplayer/client-engine/guide-node/) to learn more about the structure and usage of Client Engine. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/guide-node.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/guide-node.mdx deleted file mode 100644 index 0f915e8f9..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/guide-node.mdx +++ /dev/null @@ -1,386 +0,0 @@ ---- -title: Client Engine Developer Guide - Node.js -sidebar_label: Developer Guide -sidebar_position: 4 ---- - -Please read [Client Engine Quick Start - Node.js](/sdk/multiplayer/client-engine/quick-start-node/) and [Your First Client Engine Game](/sdk/multiplayer/client-engine/first-game-node/) to get an initial idea of how to develop a game using a starter project. This document will build on the initial project to explain the Client Engine SDK in depth. - -The Client Engine starter project relies on the Client Engine SDK, which is a wrapper around the Multiplayer SDK to help you write server-side game logic. You can install the dependencies by following the [quickstart guide](/sdk/multiplayer/client-engine/quick-start-node/). - -## Components - -The SDK provides the following components: - -* **`Game` :** Responsible for the specific logic of the game in the room. Client Engine maintains a number of game rooms, each of which is a Game instance, i.e., each Game instance corresponds to a unique Play Room and MasterClient. Logic in the game rooms is controlled by the code in the Game, so **the logic of the game in the room must be inherited from this class**. -* **`GameManager` :** Responsible for creating, managing and distributing specific Game objects. The management and destruction of Game are handled by the SDK, you don't need to write additional code. - -### GameManager - -#### GameManager instantiation - -`GameManager` will help you to create, manage and destroy Game automatically, so you need to instantiate `GameManager` when your project starts. The sample code is shown below: - -##### Customising GameManager - -First of all, you need to customise a Class inherited from `GameManager`, such as `SampleGameManager` in the sample code: - -```js -import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine"; -export default class SampleGameManager extends GameManager { - -} -``` - -##### Custom Methods in GameManager - -One of the core uses of the client engine is to create a Game and return the roomName to the client, so in the `SampleGameManager` class we need to write a method to create a `Game` for the web API to use. Like [quick start](/sdk/multiplayer/client-engine/quick-start-node/#implements-the-quick-start-logic) and [create new game](/sdk/multiplayer/client-engine/first-game-node/#implements-the-create-new-game-logic) in the example project. Here's the sample code we'll use to create a new game: - -```js -import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine"; -export default class SampleGameManager extends GameManager { - /** - * Creates a new game. - * @param playerId The player ID of the reservation. - * @param options Some configuration items that can be specified when creating a new game. - * @return The room name of the game being created. - */ - public async createGameAndGetName(playerId: string, options?: ICreateGameOptions) { - const game = await this.createGame(playerId, options); - return game.room.name; - } -} -``` - -After writing the custom method, later we also need to configure [load balancing](#load-balancing) for the method here. It should be noted that by the requirement of [load balancing system](#load-balancing), `public` methods in `GameManager` subclass must have their parameters and return values be `string`, `number`, `boolean`, `null`, `Object`, or `Array`. In the above code, we can see that the `createGame()` method of `GameManager` returns a `Game`, which does not meet the requirement of load balancing, so we encapsulate it into our own method `createGameAndGetName()` here. - -##### Creating GameManager Subclass Objects - -Next, create a subclass of `GameManager`. When creating `SampleGameManager`, you need to pass [Custom Game](#implementing-your-own-game) in the first parameter. Here we use [Sample Demo](/sdk/multiplayer/client-engine/first-game-node/) of the guessing game `RPSGame`. - -```js -import PRSGame from "./rps-game"; -const gameManager = new SampleGameManager( - gameConstructor: PRSGame, - appId: {{appid}}, - appKey: {{appkey}}, - playServer: "https://please-replace-with-your-customized.domain.com", - concurrency: 2, -); -``` - -##### Setting up load balancing - -The `GameManager` needs to be configured for load balancing to ensure that the `Game` objects created by the `GameManager` are distributed as evenly as possible to each Client Engine instance. See below for detailed documentation on [load balancing](#load-balancing). Here we will start by explaining how to configure it. - -Here we will create a [load balancing](#load-balancing) object and bind the above `gameManager` to it: - -```js -import { ICreateGameOptions,LoadBalancerFactory } from "@leancloud/client-engine"; - -// Create the object responsible for load balancing. Don't change it; just copy and paste it when you use it. -const loadBalancerFactory = new LoadBalancerFactory({ - poolId: `${APP_ID.slice(0, 5)}-${process.env.LEANCLOUD_APP_ENV || "development"}`, - redisUrl: process.env.REDIS_URL__CLIENT_ENGINE, -}); - -// Configure load balancing with reception and our custom method makeReservation. -const loadBalancer = loadBalancerFactory.bind(gameManager, ["createGameAndGetName"]); -``` - -In the `bind()` method of `loadBalancerFactory`, the first parameter is the `gameManager` and the second parameter is an array containing the names of the methods that need load balancing, like `["createGameAndGetName"]`. - -At this point, the configuration of the `gameManager` is complete, and you can call the relevant method at your own defined Web API like this: `gameManager.createGameAndGetName()`. - -#### Creating a Room - -In the section [GameManager Instantiation](#gamemanager-instantiation), we used `createGame()` of `GameManager` in a subclass to create a room. - -`createGame()` accepts the following parameters: - -* playerId: [userId](/sdk/multiplayer/guide/js/#initialisation) of the client initiating the request in the Multiplayer service. -* createGameOptions (optional): create the room with the specified conditions. - * roomName (optional): create the room with the specified roomName. For example, if you need to play with your friends, you can use this interface to create a room and then share the roomName with your friends. If you don't care about roomName, you can leave it out. - * roomOptions (optional): with this parameter, the client can set `customRoomProperties`, `customRoomPropertyKeysForLobby`, and `visible` when requesting the Client Engine to create a room. Please refer to [Create Room](/sdk/multiplayer/guide/js/#creating-a-room) for the description of these three parameters. - * seatCount (optional): when creating a room, specify how many players are needed for this game. This value needs to be between `minSeatCount` and `maxSeatCount` of [setting the number of players in a room](#setting-the-number-of-players-in-a-room), otherwise the Client Engine will refuse to create the room. If not specified, `defaultSeatCount` is used. - -For example, to create a new room with matching conditions, call `createGame()` like this: - -```js -// You can get the playerId and createGameOptions from the request sent by the client. -const props = { - level: 2, -}; - -const roomOptions = { - customRoomPropertyKeysForLobby: ['level'], - customRoomProperties: props, -}; - -const createGameOptions = { - roomOptions -}; - -gameManager.createGame(playerId, createGameOptions); -``` - -In [your first Client Engine game](/sdk/multiplayer/client-engine/first-game-node/), `reception.ts` writes two custom methods for "QuickStart" and "Create a new game" using `createGame() and invokes relevant logic with the web APIs `/reservation` and `/game` in `index.ts`. If you don't need any customization, you can just use the interfaces in the sample demo with the above parameters. - -#### Getting currently available rooms - -GameManager provides a `getAvailableGames()` method to retrieve the list of available games in the Client Engine instance where the GameManager object resides. Here, available means that the room still has empty seats. The sample code is as follows. - -```js -var games = gameManager.getAvailableGames(); -``` - -Note that this method does not fetch all available rooms in the Multiplayer service but only **the available rooms in the current Client Engine instance**. For Client Engine multi-instance load balancing, please refer to [Load Balancing](#load-balancing). - -#### Matching - -The `GameManager` does not provide a matching mechanism for the time being. If a client only needs to join a room randomly, please refer to the `Quick Start` implementation in [sample project](/sdk/multiplayer/client-engine/first-game-node/). This implementation looks for available rooms or creates rooms in the least loaded instance, and eventually returns to the client the name of a room that can be joined. - -If you wish to implement conditional matching, you can implement it like this: - -1. The client requests [conditional match](/sdk/multiplayer/guide/js/#randomly-joining-rooms) from the multiplayer service, and if there is a room available, the join-success event is triggered. -2. If there is no spare room in the Multiplayer service, the client will receive the "join room failed" event. In this event, if the error code is [4301](/sdk/multiplayer/error-code/#4301), then request the Client Engine to create a room. -3. The Client Engine receives the request, creates the room and returns the roomName to the client. The logic of this part can use the `/game` entry in [sample project](/sdk/multiplayer/client-engine/first-game-node/). -4. The client gets the roomName returned by the Client Engine, joins the room, and waits for others to join. - -The sample code for this process in the client is as follows (**not Client Engine**): - -The client first makes a conditional request to the Multiplayer service to join the room: - -```js -const matchProps = {level: 2}; -play.joinRandomRoom({matchProperties: matchProps}); -``` - -If the multiplayer service has a new room available at this time, you will automatically be added to the new room and the join-room-success event will be triggered. - -```js -play.on(Event.ROOM_JOINED, () => { - // TODO can do things like jumping to other scenes -}); -``` - -If no room is available, the join-room-failed event will be triggered. In this event, the error code [4301](/sdk/multiplayer/error-code/#4301) means there's no room available. Now we can request the Client Engine to create a new room, get the roomName of the new room, and join the new room: - -```js -// Requesting Client Engine to create a room after failing to join a room -play.on(Event.ROOM_JOIN_FAILED, (error) => { - if (error.code === 4301) { - // Setting up to create rooms with matching properties - const props = {level: 2}; - const options = {customRoomPropertyKeysForLobby: ['level']}; - // The `/game` interface implemented in the Client Engine is called over HTTP. - const { roomName } = await (await fetch( - `${CLIENT_ENGINE_SERVER}/game`, - { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - playerId: play.userId, - options - }) - } - )).json(); - // Join the room - return play.joinRoom(roomName); - } else { - console.log(error); - } -}); -``` - -### Game - -#### Game lifecycle - -1. **Create:** `Game` is managed by `GameManager` in the SDK, which creates a `Game` as appropriate when it receives a request to create a room. -2. **Run:** After creation, control of the `Game` is transferred from the `GameManager` in the SDK to the `Game` itself. From this moment on, players will join the game room one after another. -3. **Destruction:** Once all players have left the room, the game is over, and the `Game` hands control back to the `GameManager`, which does the final cleanup, including disconnecting and destroying the masterClient for the room, deleting the `Game` from the list of games it manages, and so on. - -#### General Properties of Game - -The `Game` class provides the following properties to simplify the implementation of common requirements in implementing game logic. You can easily get the following properties in your own class inheriting `Game`: - -* `room` property: the room that the game corresponds to, which is an instance of Room in the Play SDK. -* `masterClient` property: the masterClient for the game, which is an instance of Play in the Play SDK. -* `players` property: a list of players that do not contain a masterClient. Note that if you get the list of room members via the `playerList` property of a Play SDK Room instance, it includes the masterClient. - -#### General Methods of Game - -The Game class encapsulates the following methods on top of the Multiplayer SDK, which allows MasterClient to send custom events more conveniently: - -* `broadcast()` method: broadcast custom event to all players. Please refer to [Broadcast Custom Events](#broadcasting-custom-events) for example code. -* `forwardToTheRests()` method: forward the custom events sent by one player to other players. Please refer to [Forwarding Custom Events](#forwarding-custom-events) for example code. - -#### Implementing Your Own Game - -To implement your own in-room game logic, you need to create a class that inherits from `Game` to write your own game logic. The sample method is as follows: - -```js -import { Game } from "@leancloud/client-engine"; -export default class SampleGame extends Game { - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - } -} -``` - -#### Setting the number of players in a room - -The number of players here refers to the number of players excluding the MasterClient, and according to the limitations of the Multiplayer service, the maximum number of players cannot exceed 9. - -In `Game`, you need to specify `defaultSeatCount` static attribute as the default number of players, and the Client Engine will request the multiplayer service to create a room according to this value. For example, if you need 3 players to play Landlord, you can set it like this: - -```js -export default class SampleGame extends Game { - public static defaultSeatCount = 3; // Maximum 9 -} -``` - -If your game requires a certain number of players, in addition to setting `defaultSeatCount`, you need to use the `minSeatCount` static attribute to limit the minimum number of players and the `maxSeatCount` static attribute to set the maximum number of players. For example, Triple Triad requires a minimum of 2 players and a maximum of 8 players to play, but the default is 5 players, so you can set it like this: - -```js -export default class SampleGame extends Game { - public static minSeatCount = 2; - public static maxSeatCount = 8; // Maximum 9 - public static defaultSeatCount = 5; -} -``` - -In the [Create Room](#creating-a-room) interface, you can dynamically override `defaultSeatCount` with the `seatCount` parameter in the client request. - -You can optionally configure the [room full event](#room-full-event) to fire when the room reaches `seatCount`; if your client does not specify `seatCount`, the `defaultSeatCount` value will be used for the room full event. - -#### Join Room Event - -When a client successfully joins a room, the MasterClient in the Client Engine will receive the [new player join event](/sdk/multiplayer/guide/js/#new-player-join-event). If you need to listen to this event, you can write the code to listen to it in the `constructor()` method in your custom `Game`. - -```js -import { Game } from "@leancloud/client-engine"; -export default class SampleGame extends Game { - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - this.masterClient.on(Event.PLAYER_ROOM_JOINED, () => { - console.log('Someone\'s coming'); - }); - } -} -``` - -#### Room Full Event - -When the number of people in a room satisfies the room full logic of [set the number of players in the room](#setting-the-number-of-players-in-a-room), the `watchRoomFull` decorator lets you receive the `AutomaticGameEvent.ROOM_FULL` event thrown by the Game, where you can write the appropriate game logic, such as closing the room, broadcasting the start of the game to the Client: - -```js -import { AutomaticGameEvent, Game, watchRoomFull } from "@leancloud/client-engine"; - -enum Event { - GameStart = 15, -}; - -@watchRoomFull() -export default class SampleGame extends Game { - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - // Listen for the ROOM_FULL event and call the `start() method` when it receives it. - this.once(AutomaticGameEvent.ROOM_FULL, this.start); - } - - protected start = async () => { - // Write the logic for when your room is full here. - // Mark the room as no longer available - this.masterClient.setRoomOpened(false); - // Broadcast the game start event to the client - this.broadcast(Event.GameStart); - } -} -``` - -#### Broadcasting custom events - -In [Room Full Event](#room-full-event), `Game` broadcasts the start of the game to all members of the room: - -```js -enum Event { - GameStart = 15, -}; -this.broadcast(Event.GameStart); -``` - -You can also broadcast events with some data: - -```js -enum Event { - GameStart = 15, -}; -const gameData = {someGameData}; -this.broadcast(Event.GameStart, gameData); -``` - -At this point the client's [receive custom event](/sdk/multiplayer/guide/js/#receiving-custom-events) method will be triggered, and if it finds out it's a `game-start` event, the client can show the start of the matchmaking on the UI. - -#### Forwarding custom events - -MasterClient can forward events from one client to other clients, and process data while doing so: - -```js -enum Event { - SomeEvent = 15, -}; -this.forwardToTheRests(event, (eventData) => { - // Preparing data to be forwarded - const actUserId = event.senderId; - const result = {actUserId}; - return result; - // Event.SomeEvent is the ID of the custom event, or the ID of the original event if omitted. -}, Event.SomeEvent) -``` - -In this code, the `event` parameter is the original event sent by some client, and `eventData` is the data of the original event, which you can manipulate when forwarding the event to other clients, e.g., to erase or add some information. After MasterClient sends the event, the client's [Receive Custom Event](/sdk/ multiplayer/guide/js/#ReceiveCustomEvent) is triggered. - -#### Communications between the MasterClient and clients - -In addition to the [Broadcast Custom Events](#broadcasting-custom-events) and [Forward Custom Events](#forwarding-custom-events) provided in the initial project above, you can still use [Custom Attributes](/sdk/multiplayer/guide/js/#custom-attributes-and-synchronisation) and [Custom Events](/sdk/multiplayer/guide/js/#custom-events) in the Multiplayer service for communication. - -In addition, `Game` provides the following [RxJS](http://reactivex.io/rxjs) methods to stream events and streamline your code and logic. - -* `getStream()` method: get the stream of custom events sent by the player, which is an Observable object in RxJS. Please refer to [API Documentation](https://leancloud.github.io/client-engine-nodejs-sdk/classes/game.html#getstream) for interface description. -* `takeFirst()` method: get the stream of the first custom event sent by the player with the specified condition counting from now. Returns an Observable object in RxJS. Please refer to the [API documentation](https://leancloud.github.io/client-engine-nodejs-sdk/classes/game.html#takefirst) for the interface description. - -Note that the above two methods require you to know [RxJS](http://reactivex.io/rxjs) in order to use them. If you don't know [RxJS](http://reactivex.io/rxjs), you can still use the [event methods](/sdk/multiplayer/guide/js/#custom-events) for communication. - -#### Game Over - -When all players have left, `GameManager` will automatically destroy the current room and the associated MasterClient for you; at this point, if you have no other logic to do, you don't need to concern yourself with this section of the documentation. If you want to do some cleanup yourself, such as saving user data, you can use the `autoDestroy` decorator, which will automatically trigger the `destroy()` method in the `Game` subclass when all the players have left, and you can write the relevant logic in this method. - -```js -import { autoDestroy, Game } from "@leancloud/client-engine"; - -@autoDestroy() -export default class SampleGame extends Game { - protected destroy() { - super.destroy(); - console.log('Extra cleaning can be done here'); - } -} -``` - -## Load Balancing - -Client Engine automatically adjusts the number of instances based on the overall instance load. - -In Client Engine, there are two types of load balancing: the first is for requests initiated by clients through the REST API, and the second is for the load of the number of `Games` running on each instance. For requests initiated by clients via the REST API, the Client Engine automatically distributes the requests evenly among all current instances, without requiring any configuration work. For the second scenario, each `Game` object (per game) usually exists for a certain period of time, and in order to make the `Game` objects carried by each instance as balanced as possible, we need to additionally configure the `GameManager` into the load balancing system. - -This feature is implemented by the `LoadBalancerFactory` class provided by the SDK. As we can see in [GameManager Instantiation](#gamemanager-instantiation), `LoadBalancerFactory` generates a `LoadBalancer` object by binding `gameManager`, which is present in every Client Engine instance. - -When an instance of the Client Engine receives a REST API request from a client and calls a method in the `gameManager`, the load-balanced node `LoadBalancer` in the instance that receives the request finds the instance with the smallest number of `Games` in the cluster, forwards the specified `gameManager` API call to the `gameManager` of that instance to run, and returns the result. In this case, the `LoadBalancer` is only responsible for forwarding the request and does not care about how the request is handled. - -## API Documentation - -You can find more descriptions of the SDK's classes, methods, and properties in the API documentation. [Click to view Client Engine SDK API documentation](https://leancloud.github.io/client-engine-nodejs-sdk/). \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/quick-start-node.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/quick-start-node.mdx deleted file mode 100644 index 906251edb..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/client-engine/quick-start-node.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: Client Engine Quick Start - Node.js -sidebar_label: Quick Start -sidebar_position: 2 ---- - -This document helps you get up to speed on how to create a Client Engine project, a simple two-player rock-paper-scissors game that relies on the Multiplayer JavaScript SDK for its logic. - -In this document, we'll first learn how to start the project locally, briefly try it out, and then deploy the project to the cloud. After that, we will introduce the detailed game logic and how to develop our own game in [Developer Guide](/sdk/multiplayer/client-engine/guide-node/). - -### Start the project - -### Install command line tools - -Please check the **[Installation section](/sdk/engine/cli/#installation)** of the documentation of the command line tool to install the command line tool and execute the **[Login](/sdk/engine/cli/#login-account)** command to log in. - -### Create a project - -Get the sample project from Github. Please use this project as your project base: - -```js -git clone https://github.com/leancloud/client-engine-nodejs-getting-started -cd client-engine-nodejs-getting-started -``` - -Add the application's App ID and other information to the project: - -```sh -lean switch -``` - -In the first step for selecting an App, select the LeanCloud app that corresponds to your game. In the second step, when selecting the Cloud Engine group, you must select the `_client-engine` group, for which LeanCloud provides optimized maintenance and support specifically for the Client Engine, as shown here: - -![image](/img/client-engine/lean-switch.png) - -### Running locally - -First install the necessary dependencies in the current project's directory by executing the following command: - -```sh -npm install -``` - -If the Data Storage service will also be used, execute the following command: - -```sh -npm install leancloud-storage -``` - -Open the debug log when you start the application: - -```sh -DEBUG=ClientEngine*,RPS*,Play lean up -``` - -If you do not need the debug log, you can start it directly with the following command: - -```sh -lean up -``` - -After startup, open `http://localhost:3000/` in your browser to check if the project started properly. - -### Visit the site - -#### Experience the game - -After the server-side project is started, if you want to experience the Demo game, you need to open two additional [Client Sample Demo](https://client-engine-app.leanapp.cn/) pages at the same time, and do the following configurations in these two pages: - -Click Configs and enter the App ID, App Key, and server URL of the previous selected application into APP_ID, APP_KEY, and PLAY_SERVER: - -```sh -# If your browser is already logged into LeanCloud, select the relevant app below and copy and paste the relevant information into Configs: - APP_ID: "your-app-id" - APP_KEY: "your-app-key" -``` - -Next, enter `http://localhost:3000` in Client Engine Server. As shown in the figure: - -![image](/img/client-engine/browser-demo.png) - -Once the information is filled in, click Login to Play to start the game. - -#### Client Code - -If you want to see the detailed client code, you can visit [Client Sample Code](https://github.com/leancloud/client-engine-demo-webapp) on github. - -## Deploying to the Cloud - -To deploy to the staging environment, run the following command in the root directory: - -```sh -lean deploy --staging -``` - -Log in to the LeanCloud console in a browser, bind a [Cloud Engine domain](/sdk/domain/guide/#cloud-engine-domain) (custom domains starting with `stg-` will be automatically bound to the staging environment), and then visit the corresponding URL to see the text indicating that the Client Engine server is running. - -For other detailed deployment methods, please refer to [Deployment](/sdk/engine/cli/#deployment) in the command line tools documentation. - -## Your first Client Engine game - -Next, please see the documentation [Your First Client Engine Game](/sdk/multiplayer/client-engine/first-game-node/) for a step-by-step guide on how to develop a rock-paper-scissors game based on this initial project. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/error-code.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/error-code.mdx deleted file mode 100644 index fb2673e18..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/error-code.mdx +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: Multiplayer Error Codes -sidebar_label: Error Codes -sidebar_position: 5 ---- - -## Application-level error code - -### 4001 - -- Message - APP_NOT_FOUND -- Meaning - App not found. Please make sure the App ID and App Key are entered correctly and the correct node is set. - -### 4002 - -- Message - INSUFFICIENT_BALANCE -- Meaning - The account to which the application belongs has negative balance. Please recharge the account before using the application. - - - -### 4004 - -- Message - CCU_QUOTA_EXCEEDED -- Meaning - CCU has exceeded the current quota limit. Please upgrade to Business or Enterprise Edition. - - - -### 4006 - -- Message - JOIN_OR_CREATE_ROOM_NOT_ALLOWED_DUE_TO_APP_MSG_QUOTA_EXCEEDED -- Meaning - The maximum message sending rate in an application room exceeds 500 messages per second, prohibiting the creation of new rooms and joining other rooms. - -## Service Fault Error Code - -### 4200 - -- Message - INTERNAL_ERROR -- Meaning - Internal server error. Contact technical support. - -### 4202 - -- Message - SERVICE_TEMPORARILY_UNAVAILABLE -- Meaning - Game service is temporarily unavailable. Please contact technical support. - -## Room error code - -### 4301 - -- Message - ROOM_NOT_FOUND -- Meaning - Room could not be found. The following conditions will cause this error: - - No room matches the criteria during random matching. You can create a new room at this point. - - Room name was specified but the Room has been destroyed or the name is incorrect. - -### 4302 - -- Message - ROOM_FULL -- Meaning - Room is full. Please join another room. - - - -### 4308 - -- Message - ROOM_MEMBERSHIP_REQUIRED -- Meaning - Requires the current player to be in the room in order to perform the action. - -### 4309 - -- Message - ROOM_GAME_VERSION_OR_SDK_VERSION_NOT_MATCH -- Meaning - The Game Version or SDK Version of the user who created the Room does not match the Game Version or SDK Version of the user who is currently in the Room. This error occurs when you force join a room with a Room name that does not match your Game Version or SDK Version. - - - -### 4311 - -- Message - ROOM_ALREADY_EXISTS -- Meaning - The Room already exists. Create a room with another Room name. - -### 4312 - -- Message - ROOM_ATTRS_FULL -- Meaning - The attributes of the Room have reached the maximum length limit. - -### 4313 - -- Message - INVALID_ROOM_ATTR -- Meaning - The attribute of Room does not match the requirement. - -### 4314 - -- Message - ROOM_CLOSED -- Meaning - Room is closed. This error occurs when actively joining a room that is already closed. - -### 4315 - -- Message - TARGET_MASTER_CLIENT_OFFLINE -- Meaning - The target Master was not online when the Master was transferred. - -### 4316 - -- Message - INVALID_ROOM_ID -- Meaning - Room id is not in the required format. - -### 4317 - -- Message - SHOULD_LEAVE -- Meaning - The user joins another room without leaving the current room. Please call the leave method first before joining another room. - - - - - - - - - - - -### 4324 - -- Message - SHOULD_JOIN -- Meaning - The Client is not currently in any Room, but is trying to do some operation in a Room. Please join a Room first before doing the operation. - - - -### 4326 - -- Message - ROOM_ATTR_NOT_MATCHED -- Meaning - When you join a Room by matching Room attributes, there are no rooms matching the relevant attributes, so you can create a new room matching the relevant attributes. - - - -### 4328 - -- Message - OPERATION_NOT_ALLOWED -- Meaning - Not authorised to do this operation. - -### 4329 - -- Message - PLAYER_PROPERTIES_FULL -- Meaning - User properties are full. Please reduce the size of the properties. - - - -## RPC message related errors - - - -### 4406 - -- Message - NO_VALID_MESSAGE_RECEIVER -- Meaning - The message has no legitimate recipient. - - - -## Other errors - -### 4101 - -- Message - DUPLICATE_LOGIN -- Meaning - On a connection that already has a user logged in, another login request is received from another user. - -### 4102 - -- Message - DUPLICATE_CONNECTIONS -- Meaning - The same user is logged in on different connections. - -### 4013 - -- Message - SIGNATURE_VERIFICATION_FAILED -- Meaning - Signature error. - -### 4104 - -- Message - INVALID_APP_ID_OR_CLIENT_ID -- Meaning - App id or Client id format is not legal. - -### 4105 - -- Message - SESSION_REQUIRED -- Meaning - The user sent a request without logging in. - - - - - - - -### 4110 - -- Message - FRAME_TOO_LONG -- Meaning - The received packet was too large and exceeded the limit. - - - - - -### 4121 - -- Message - INVALID_PARAMS -- Meaning - Wrong parameter. Please check the detail for more information. - - - - - - - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/features.mdx deleted file mode 100644 index 30113110c..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/features.mdx +++ /dev/null @@ -1,815 +0,0 @@ ---- -title: Multiplayer Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; - -Multiplayer is a backend service specifically designed for multiplayer online games by TDSLeanCloud. Developers don't need to build their own backend systems; they can easily implement features such as player matching and real-time battle message synchronization using cloud services. - -## Core Features - -- **Player Matching**: Match players together to play games either randomly or based on specified criteria. In online battles, the matching functionality brings players about to play together into the same room (Room). For example, in games like "Identity V," "Honor of Kings," and "PUBG," players can quickly match with others by clicking "Quick Match," and they all enter the same room to prepare for the game. Players can also create rooms and invite friends to play together. -- **Fast Battle Message Synchronization**: Real-time bidirectional communication between the client and server is achieved through WebSocket channels, ensuring that all in-game messages are quickly synchronized. -- **Game Logic Processing**: Multiplayer provides [MasterClient](/sdk/multiplayer/guide/js/#masterclient) as the client host for controlling game logic. All in-game logic is managed by the MasterClient, and in case the MasterClient unexpectedly disconnects, the client with the best network status will be automatically switched to MasterClient to ensure smooth gameplay. Developers can also choose to implement game logic on the server side (server-side game logic support is under development). -- **Multi-Platform Support**: Perfectly compatible with Unity and Cocos Creator, supporting multiple platforms. - -## Features - -- Supports dynamic scalability to handle massive concurrency with ease. -- Built on a proven underlying architecture, it has undergone deep optimization and improvements, ensuring stability for handling message delivery rates of up to billions per second. - -## Core Concepts - -### Client and UserId - -In the Multiplayer service, each terminal is referred to as a "Client." Each Client has a unique identifier called `UserId` throughout the entire Multiplayer service. This `UserId` can only consist of English letters, numbers, and underscores, with a length of up to 32 characters and shall be unique within the application. Each game player is definitely a Client, but not all Clients are actual players. For instance, the MasterClient that manages rooms in the Client Engine or self-made AI players are also Clients. - -The Multiplayer service only allows one Client to establish a connection with one server at a time. If a Client with an already logged-in `UserId` attempts to log in again, the second login will kick out the previous one. - -### Rooms and ActorId - -Once players are successfully matched, they will enter the same room to play the game, and battle messages will be swiftly synchronized within this room. Each player in this room has a unique ActorId, which is used for all communication within the room. When a player leaves the room, the ActorId becomes invalid. Upon entering a new room, the player will be assigned a new ActorId specific to that room. - -**A room can support a maximum of 10 players online simultaneously.** - -## Game Core Process - -Here's a simple example code to help you quickly understand the overall process. For detailed development guidance, please refer to: - -- [Multiplayer Development Guide · JavaScript](/sdk/multiplayer/guide/js/) -- [Multiplayer Development Guide · C#](/sdk/multiplayer/guide/cs/) - -### Connecting to the Server - - -<> - - - -```js -const client = new Client({ - // Set your APP ID - appId: {{appid}}, - // Set your APP Key - appKey: {{appkey}}, - // Set the Server (replace xxx.example.com with your custom API domain bound to your app) - playServer: 'https://xxx.example.com', - // Set the user id - userId: 'tarara', - // Set the game version; optional; default is 0.0.1; players with different versions won't match in the same room - gameVersion: '0.0.1' -}); - -client.connect().then(()=> { - // Connection successful -}).catch(console.error); -``` - - - - - -```js -const client = new Client({ - appId: 'your-client-id', // Your game's Client ID - appKey: 'your-client-token', // Your game's Client Token - playServer: 'https://your_server_url', // Your game's API domain - userId: 'tarara', // Set user id - gameVersion: '0.0.1' // Set game version; optional; default is 0.0.1; players with different versions won't match in the same room -}); - -client.connect().then(()=> { - // Connection successful -}).catch(console.error); -``` - -- You can find your game's `Client ID` and `Client Token` in **Developer Center > Your Game > Game Services > App Configuration**. -- The API domain can be found in **App Configuration > Domain Configuration > API**. Refer to the documentation on [Domains](/sdk/domain/guide/) for more information. - - - - -<> - -```cs -Play.UserID = "tarara"; -// You can declare the game version when connecting to the server; players with different versions won't match in the same room -Play.Connect("0.0.1"); -``` - - - - -### Player Matching - -#### Random Matching - -The most common scenario for single players is to quickly match with other players. The implementation steps are as follows: - -1、Call `JoinRandomRoom` to start matching. - - -<> - -```js -client - .joinRandomRoom() - .then(() => { - // Successfully joined the room - }) - .catch(console.error); -``` - - -<> - -```cs -Play.JoinRandomRoom(); -``` - - - - -2、In ideal cases, players will enter a room with available slots and start the game. - - -<> - -```js -// Use the Promise returned by joinRandomRoom to determine if joining the room was successful in the JavaScript SDK -``` - - -<> - -```cs -play.On(Event.ROOM_JOINED, (evtData) => { - // Successfully joined the room - -}); -``` - - - - -3、If there is no empty room, it will fail to join. At this point we can create a room in the callback triggered by the failure and wait for others to join. When creating the room: - -- You don't need to worry about the name of the room. -- The default maximum number of people in a room is 10. You can set MaxPlayerCount to limit the maximum number of people. -- Set [Retention time after player dropped](/sdk/multiplayer/guide/cs/#retain-disconnected-user-in-room). If the player is added back to the room within the valid time, the room will still retain the player's custom attributes. - - -<> - -```js -client - .joinRandomRoom() - .then() - .catch((error) => { - if (error.code === 4301) { - const options = { - // Set the maximum number of people so that when the room is full, the server will not match new players in. - maxPlayerCount: 4, - // Set the hold time after a player is dropped to 120 seconds - playerTtl: 120, - }; - // Creating a Room - client - .createRoom({ - roomOptions: options, - }) - .then(() => { - // Room created successfully - }); - } - }); -``` - - -<> - -```cs -// This callback is triggered when joining fails -play.On(Event.ROOM_JOIN_FAILED, (evtData) => -{ - var options = new RoomOptions() - { - // Set the maximum number of players so that when the room is full, the server won't match new players in. - MaxPlayerCount = 4, - // Set the hold time after a player is dropped to 120 seconds. - PlayerTtl = 120, - }; - play.CreateRoom(roomOptions: options); -}); -``` - - - - -#### Customised room matching rules - -There are times when we want to match players with similar levels. For example, if the current player is level 5, he can only be matched with players of level 0-10, and players above level 10 cannot be matched. This scenario can be realised by setting attributes for the room. The logic is as follows: - -1. Determine the matching attributes, e.g. level 0-10 is level-1, and levels above 10 is level-2. - - -<> - -```js -var matchLevel = 0; -if (level < 10) { - matchLevel = 1; -} else { - matchLevel = 2; -} -``` - - -<> - -```cs -int matchLevel = 0; -if (level < 10) { - matchLevel = 1; -} else { - matchLevel = 2; -} -``` - - - - -2、Join the room according to the matching attributes - - -<> - -```js -const matchProps = { - level: matchLevel, -}; - -client - .joinRandomRoom({ matchProperties: matchProps }) - .then(() => { - // Successfully joined the room - }) - .catch(console.error); -``` - - -<> - -```cs -Hashtable matchProp = new Hashtable(); -matchProp.Add("matchLevel", matchLevel); -Play.JoinRandomRoom(matchProp); -``` - - - - -3. If randomly joining a room fails, a room with matching attributes is created to wait for other people of the same level to join. - - -<> - -```js -const matchProps = { - level: matchLevel, -}; - -client - .joinRandomRoom({ matchProperties: matchProps }) - .then() - .catch((error) => { - if (error.code === 4301) { - const options = { - // Set the maximum number of people so that when the room is full, the server won't match new players in. - maxPlayerCount: 4, - // Set the hold time after a player is dropped to 120 seconds - playerTtl: 120, - // Custom properties of the room - customRoomProperties: matchProps, - // Select the key for matching from the room's custom properties - customRoomPropertyKeysForLobby: ["level"], - }; - - client - .createRoom({ - roomOptions: options, - }) - .then() - .catch(console.error); - } - }); -``` - - -<> - -```cs -play.On(Event.ROOM_JOIN_FAILED, (error) => { - if (error["code"] == 4301) - { - var props = new Dictionary(); - props.Add("level", 2); - var options = new RoomOptions() - { - // Set the maximum number of people so that when the room is full, the server won't match new players in. - MaxPlayerCount = 3, - // Set the hold time after a player is dropped to 120 seconds - PlayerTtl = 120, - // Custom properties of the room - CustomRoomProperties = props, - // Select the key for matching from the room's custom properties - CustoRoomPropertyKeysForLobby = new List() { "level" }, - }; - play.CreateRoom(roomOptions: options); - } -}); -``` - - - - -#### Playing with friends - -Suppose PlayerA wants to play the game with his best friend PlayerB, then there are two scenarios: - -- Just two people playing together, no strangers allowed. -- Friends and strangers playing together - -##### No strangers allowed - -1. PlayerA creates a room and makes it invisible, so that other people don't randomly join the room that PlayerA has created. - - -<> - -```js -const options = { -// Room invisible - visible: false, -}; -client - .createRoom({ - roomOptions: options, - }) - .then() - .catch(console.error); -``` - - -<> - -```cs -var options = new RoomOptions() -{ - Visible = false, -}; -play.CreateRoom(roomOptions: options); -``` - - - - -2. PlayerA tells PlayerB the name of the room through some kind of communication (e.g. [instant messaging](/sdk/im/features/)). - -3. PlayerB joins the room based on the room name. - - - -<> - -```js -client.joinRoom("LiLeiRoom").then().catch(console.error); -``` - - -<> - -```cs -Play.JoinRoom(roomName); -``` - - - - -##### Friends and strangers playing together - -PlayerA invites PlayerB via some communication method (e.g. [Instant Messaging](/sdk/im/features/)), and PlayerB accepts the invitation. - -1、PlayerA sets up a match with PlayerB to enter a room. - - -<> - -```js -client - .joinRandomRoom({ expectedUserIds: ["playerB"] }) - .then(() => { - // Join Success - }) - .catch(console.error); -``` - - -<> - -```cs -Play.JoinRandomRoom(expectedUserIds: new string[] {"playerB"}); -``` - - - - -2、If there is enough space in the room, PlayerA joins successfully. - - -<> - -```js -// JavaScript SDK determines whether a room has been successfully joined by using the joinRandomRoom Promise. -``` - - -<> - -```cs -play.On(Event.ROOM_JOINED, (evtData) => { - // TODO can do things like jumping to other scenes - -}); -``` - - - - -PlayerA tells PlayerB the roomName of the room it has joined via some form of communication (e.g. [instant messaging](/sdk/im/features/)), and PlayerB joins the room based on the roomName. - - -<> - -```js -client.joinRoom("LiLeiRoom").then().catch(console.error); -``` - - -<> - -```cs -Play.JoinRoom(roomName); -``` - - - - -3. If there is no suitable room then create and join the room: - - -<> - -```js -const expectedUserIds = ["playerB"]; -client - .joinRandomRoom({ expectedUserIds }) - .then() - .catch((error) => { - // No room available or insufficient room space - if (error.code === 4301 || error.code === 4302) { - client - .createRoom({ - expectedUserIds: expectedUserIds, - }) - .then() - .catch(console.error); - } - }); -``` - - -<> - -```cs -play.On(Event.ROOM_JOIN_FAILED, (error) => { - var expectedUserIds = new List() { "cr3_2" }; - Play.CreateRoom(expectedUserIds: expectedUserIds); -}); -``` - - - - -After PlayerA creates a room, it tells PlayerB the roomName of the room it has joined via some form of communication (e.g., [instant messaging](/sdk/im/features/)), and PlayerB joins the room based on the roomName. - - -<> - -```js -client.joinRoom("LiLeiRoom").then().catch(console.error); -``` - - -<> - -```cs -Play.JoinRoom(roomName); -``` - - - - -For other matching interfaces, see the room matching documentation: [JavaScript](/sdk/multiplayer/guide/js/#room-matching), [C#](/sdk/multiplayer/guide/cs/#room-matching). - -### In game - -#### Relevant concepts - -- **MasterClient**: Multiplayer uses the [MasterClient](/sdk/multiplayer/guide/js/#masterclient) to act as a computational host on the client side, where the MasterClient controls the game logic, such as deciding whether to start or end the game, who should play the next round, how many coins should be deducted from the player, etc. -- **Custom Attributes**: Custom attributes are divided into [Room Custom Attributes](/sdk/multiplayer/guide/js/#room-custom-attributes) and [Player Custom Attributes](/sdk/multiplayer/guide/js/#player-custom-attributes). We recommend adding game data to the custom attributes, such as the current room map, total coin bet, everyone's cards, etc. This way, when the MasterClient is transferred, the new MasterClient can get the latest data from the current game and continue to calculate. - -#### Starting the game - -Before the game starts, it is recommended that each player has a ready status. When all players are ready, the MasterClient will start the game. The room must be made invisible before the game starts to prevent other players from being matched during the game. - -Player A sets the ready state by setting a custom property: - - -<> - -```js -// Player setup readiness -const props = { - ready: true, -}; -// Request to set player attributes -play.player - .setCustomProperties(props) - .then(() => { - // Setting properties succeeded - }) - .catch(console.error); -``` - - -<> - -```cs -// Player setup readiness -Hashtable prop = new Hashtable(); -prop.Add("ready", true); -play.Player.SetCustomProperties(props); -``` - - - - -All players (including PlayerA) are notified of the event callback: - - -<> - -```js -play.on(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, (data) => { - // Only the MasterClient performs this operation. - if (play.player.isMaster) { - // Check the number of players who are ready in a method you defined; you can get the list of players via play.room.playerList. - const readyPlayerCount = getReadyPlayerCount(); - // Start the game if players are all set - if ( - readyPlayersCount > 1 && - readyPlayersCount == play.room.playerList.length() - ) { - // Set the room to be invisible to avoid other players being matched in - play.setRoomVisible(false); - // Start the game - start(); - } - } -}); -``` - - -<> - -```cs - -play.On(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, (evtData) => { - // Only the MasterClient performs this operation. - if (play.Player.IsMaster) - { - // Check the number of players who are ready in your own method; you can get the player list via play.Room.playerList. - var readyPlayerCount = getReadyPlayerCount(); - // Start the game if players are all set - if (readyPlayersCount > 1 && readyPlayersCount == Play.Players.Count()) - { - // Set the room to be invisible to avoid other players being matched in - play.SetRoomVisible(false); - // Start the game - start(); - } - } -}); -``` - - - - -#### Sending messages in game - -Most messages in the game are sent to the [MasterClient](/sdk/multiplayer/guide/js/#masterclient), where the MasterClient crunches the numbers and decides what to do next. Suppose we have this scenario: Player A tells the MasterClient that he is done following cards, then the MasterClient receives the message and informs everyone that the next player to act is Player B. - -The process of sending the message is as follows: - -1、Player A sends a custom event `follow` to notify MasterClient that his follow is complete. - - -<> - -```js -// Set the receiving group of the event to Master -const options = { - receiverGroup: ReceiverGroup.MasterClient, -}; - -// Set the message to be sent -const eventData = { - actorId: play.player.actorId, -}; - -// Set the Event Id -const FOLLOW_EVENT_ID = 1; - -// Send event -play.sendEvent(FOLLOW_EVENT_ID, eventData, options); -``` - - -<> - -```cs -// Set the receiving group of the event to Master -var options = new SendEventOptions() { - ReceiverGroup = ReceiverGroup.MasterClient -}; -// Set the message to be sent -var eventData = new Dictionary(); -eventData.Add("actorId", play.player.actorId); - -// Set the Event Id -byte followEventId = 1; - -// Send event -play.SendEvent(followEventId, eventData, options); -``` - - - - -2、The relevant method in the MasterClient is triggered. The MasterClient calculates that the next player to act is PlayerB, and then calls the `next` method to notify all players that PlayerB currently needs to act. - - -<> - -```js -// Event.CUSTOM_EVENT method will be triggered -play.on(Event.CUSTOM_EVENT, event => { - const { eventId } = event; - - if (eventId === FOLLOW_EVENT_ID) { - // follow custom events - // Determine that PlayerB will act next - int PlayerBId = getNextPlayerId(); - - // Notify all players that PlayerB will act next - const options = { - receiverGroup: ReceiverGroup.All, - }; - const eventData = { - actorId: PlayerBId, - }; - const NEXT_EVENT_ID = 2; - play.sendEvent(NEXT_EVENT_ID, eventData, options); - } -}); - -``` - - -<> - -```cs -// Event.CUSTOM_EVENT method will be triggered -play.On(Event.CUSTOM_EVENT, (evtData) => { - // Getting event parameters - var eventId = evtData["eventId"]; - if (eventId == followEventId) { - byte nextEventId = 2; - - // Content of the event - var eventData = new Dictionary(); - eventData.Add("actorId", PlayerBId); - - // Send to all - var options = new SendEventOptions() - { - ReceiverGroup = ReceiverGroup.All - }; - - play.SendEvent(nextEventId, eventData, options); - } -}); -``` - - - - -3、Relevant methods are triggered for all players. - - -<> - -```js -// Event.CUSTOM_EVENT method will be triggered -play.on(Event.CUSTOM_EVENT, event => { - const { eventId, eventData } = event; - if (eventId === FOLLOW_EVENT_ID) { - ...... - }; - if (eventId === NEXT_EVENT_ID) { - // next event logic - console.log('Next Player:' + eventData.actorId); - } -}); - -``` - - -<> - -```cs -play.On(Event.CUSTOM_EVENT, (evtData) => { - if (eventId == followEventId) - { - ...... - } - - if (eventId == nextEventId) - { - // next event logic - var actorId = evtData["actorId"]; - } -}); -``` - - - - -For more detailed usage and introduction, please refer to : - -- [JavaScript - Custom Events](/sdk/multiplayer/guide/js/#customevents) -- [C# - Custom Events](/sdk/multiplayer/guide/cs/#customevents) - -#### Disconnect and reconnect in game - -If the MasterClient is located on the client side, when the MasterClient is disconnected, the Multiplayer service will pick another member to be the new MasterClient, and the original MasterClient will become a normal member after returning to the room. Please refer to [Disconnect Reconnect](/sdk/multiplayer/guide/js/#disconnect-reconnect) for details. - -#### Exiting the room - - -<> - -```js -play - .leaveRoom() - .then(() => { - // Successfully exited the room - }) - .catch(console.error); -``` - - -<> - -```cs -Play.LeaveRoom(); -``` - - - - -## Documentation - -### JavaScript - -- [Quickstart](/sdk/multiplayer/start/js/): Getting up to speed with Multiplayer and running a small demo. -- Multiplayer Development Guide - JavaScript](/sdk/multiplayer/guide/js/): A detailed introduction to all the features and interfaces of Multiplayer. - -### C# - -- [Quickstart](/sdk/multiplayer/start/cs/): Getting started and running a small demo. -- [Multiplayer Development Guide - C#](/sdk/multiplayer/guide/cs/): A detailed introduction to all the features and interfaces of Multiplayer. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/guide/cs.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/guide/cs.mdx deleted file mode 100644 index ef31900cd..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/guide/cs.mdx +++ /dev/null @@ -1,893 +0,0 @@ ---- -title: Multiplayer Development Guide - C# -sidebar_label: C# -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -## Introduction - -Multiplayer is a C#-based game SDK that provides a complete client-side SDK solution for online games with strong networking requirements, thus eliminating the need for development teams to build their own servers and saving most of the development and maintenance costs. The main features provided by Multiplayer are as follows: - -- Getting room list -- Creating a room -- Joining a room -- Randomly joining a (eligible) room -- Getting players in a room -- Getting, setting, and synchronizing room properties -- Getting, setting, and synchronizing player properties -- Sending and receiving custom events -- Leaving a room - -## SDK Import - -Please read [Installation Guide](/sdk/multiplayer/start/cs/#installation) to get the dll library files. - -## Initialisation - -Import the required namespace. - -```cs -using LeanCloud.Play; -``` - -Next we need to instantiate a client object for the Multiplayer SDK. - - - -```cs -var client = new Client("your-app-id", "your-app-key", userId, playServer: "https://xxx.example.com", gameVersion: "0.0.1"); -// Please replace xxx.example.com with the domain name of the custom API your app is bound to. -``` - - - - - -```cs -var client = new Client( - "your-client-id", // Client ID of the game - "your-client-token", // Client Token for the game - "tarara", // Set the user id - playServer: "https://your_server_url", // The game's API domain name - gameVersion: "0.0.1" // Set the game version; optional; default is 0.0.1; players with different versions will not be matched in the same room -); -``` - -- The `Client ID` and `Client Token` of the game can be viewed at **Developer Centre > Your Games > Game Services > Application Configuration**. -- The API domain name is viewable at **Application Configuration > Domain Configuration > API**. Refer to the documentation for [domain name](/sdk/domain/guide/) for more information. - - - -Here -`userId` serves as a unique identifier for the client connecting to the server. -Note that this `userId` has the following restrictions: - -- Only letters, numbers, and underscores are allowed -- Cannot exceed 32 characters in length -- Globally unique within an application - -`gameVersion` indicates the version number of the client, which can be used to route to different game servers if multiple versions of the game are allowed to co-exist. - -## Connections - -### Establishing a connection - -Connect the current player to the Multiplayer service with the following code: - -```cs -try { - await client.Connect(); - // connection successful -} catch (PlayException e) { - // connection failure - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -## Lobby - -We recommend that you **don't add players to the lobby**, because in the lobby, the server will constantly send out the latest full list of rooms, and players will choose one of the rooms to play by themselves, which is not only unfriendly to the player experience, but also brings a lot of bandwidth pressure. -We recommend you to **use [Room Matching](#room-matching) like most of the mobile games nowadays to quickly match players and let them start the game**. - -If you have a special game scenario where you need to get the room list, you can call the following method: - -```cs -try { - await client.JoinLobby(); - // Joined the lobby successfully -} catch (PlayException e) { - // Failed to join the lobby - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -When a player joins the lobby, the server sends the list of rooms in the current lobby to the client, and the developer can view the list of rooms or join a room to join the game as needed. - -```cs -client.OnLobbyRoomListUpdated += () => { - var roomList = client.LobbyRoomList; - // TODO can do the logic for displaying the list of rooms. -}; -``` - -For more on `LobbyRoom`, see [API documentation](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1LobbyRoom.htm). - -### Related events - -| Event | Parameter | Description | -| ---------------------- | ---- | ---------------- | -| OnLobbyRoomListUpdated | None | Lobby Room List Updated | - -## Room Matching - -A room is a unit that generates "combat interactions" between players. For example, the card table of Landlord, the copy of MMO, the battle of WeChat game, etc., all belong to the category of room in a broad sense. -The combat interactions between players are all done in the room. Therefore, how players enter a room is the key to room matching. Below we will analyse the commonly used "Room Matching" function from the aspects of "Create Room" and "Join Room". - -### Creating a Room - -We can create a room simply like this. The player who creates the room is the room owner ([MasterClient](#masterclient)), and when the owner creates a room successfully, he/she also joins the room. - -```cs -try { - await client.CreateRoom(); - // Successful room creation also means that you have successfully joined the room. -} catch (PlayException e) { - // Failed to create room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -We can also create a room that sets the relevant information. - -```cs -// Custom properties of the room -var props = new PlayObject { - { "title", "room title" }, - { "level", 2 } -}; -var roomOptions = new RoomOptions { - Visible = false, - EmptyRoomTtl = 10000, - PlayerTtl = 600, - MaxPlayerCount = 2, - CustomRoomProperties = props, - CustoRoomPropertyKeysForLobby = new List { "level" }, - Flag = CreateRoomFlag.MasterSetMaster | CreateRoomFlag.MasterUpdateRoomProperties, -}; -var expectedUserIds = new List { "cr3_2" }; -try { - await client.CreateRoom(roomName, roomOptions, expectedUserIds); - // Successful room creation also means that you have successfully joined the room. -} catch (PlayException e) { - // Failed to create room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -The parameters `roomName`, `roomOptions` and `expectedUserIds` are optional. - -#### roomName - -The room name must be unique; if it is not set, the server generates a unique room id and returns it. - -#### roomOptions - -Specified parameters when creating a room, including: - -- `Opened`: whether the room is open or not. If set to false, no other players are allowed to join. -- `Visible`: whether the room is visible. If set to false, it will not appear in the lobby's room list, but other players can join the room by specifying the room name. -- `EmptyRoomTtl`: how long (in seconds) the room will be kept when there are no players in the room. Default is 0, i.e. the room data will be destroyed immediately when there are no players in the room. Maximum value is 300, i.e. 5 minutes. -- `PlayerTtl`: the time (in seconds) to keep the player's data in the room when the player drops out. Default is 0, i.e. player data is destroyed immediately after the player drops out. Maximum value is 300, i.e. 5 minutes. -- `MaxPlayerCount`: the maximum number of players allowed in the room. -- `CustomRoomProperties`: custom properties for the room. -- `CustomRoomPropertyKeysForLobby`: an array of keys in `CustomRoomProperties`. The properties contained in `CustomRoomPropertyKeysForLobby` will appear in the lobby's room Properties (`client.LobbyRoomList`), and the full set of properties can be viewed in `room.CustomProperties` after joining the room. These properties will be used when matching rooms. -- `Flag`: flag bit for room creation. See [MasterClient drop without transfer](#masterclient-drop-transfer), [Specify other member as MasterClient](#specify-other-members-as-masterclient), and [Allow only MasterClient to modify room properties](#allow-only-masterclient-to-modify-room-properties) below for more details. - -#### expectedUserIds - -An array of specified player IDs. This parameter is mainly used to "reserve space" for certain players who can join the room. - -Note: These "specific players" will not actually join the room. There will only be space in the room reserved for those "specific players" to join. If you want to invite players to join a room, you need to send the room name to your friends through other channels, such as IM, WeChat, etc., and then your friends will join the room through the `JoinRoom(roomName)` interface. - -For more information about `CreateRoom`, please refer to [API Documentation](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1Client.htm#a0478f278b300dd4ae4c4e1fe311d3c7c). - -### Joining a room - -When a room is created, other players can participate in the game by "joining the room". - -#### Join the designated room - -You can join a room by specifying the room name. - -```cs -try { - // The player is joining the 'LiLeiRoom' room. - await client.JoinRoom("LiLeiRoom"); - // Join the room successfully -} catch (PlayException e) { - // Failed to join room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -When joining a room, you can also take up space for other players. If the remaining empty space in the room is less than the number of occupied spaces, you will fail to join the room. - -```cs -var expectedUserIds = new List() { "LiLei", "Jim" }; -try { - // Players are joining the "game" room and taking up space for LiLei and Jim - await client.JoinRoom("game", expectedUserIds); -} catch (PlayException e) { - // Failed to join room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} - -``` - -For more on `JoinRoom`, see the [API documentation](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1Client.htm#ab19049bfb2cf5e62c746f5e4a4ce79b2). - -#### Randomly joining rooms - -Sometimes, instead of joining a specific room, we can randomly join "rooms that meet certain conditions" (or even no conditions), such as quick start, quick match, etc. In this case, we can randomly join rooms by calling the `JoinRandomRoom` method. - -```cs -try { - await client.JoinRandomRoom(); - // Join the room successfully -} catch (PlayException e) { - // Failed to join room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -It is also possible to set "conditions" for random joining, such as randomly joining a room with level = 2. - -```cs -// Setting Matching Properties -var matchProps = new PlayObject { - { "level", 2 } -}; -try { - await client.JoinRandomRoom(matchProps); -} catch (PlayException e) { - // Failed to join room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -### Join or create a specific room - -Many games don't have scenarios that force a designated room owner. As long as a number of players can play in the same room, it is fine for anyone to be the owner. We can use `JoinOrCreateRoom()` to achieve this. This method will join the current player directly to the room if there is a relevant room, or create a new room if there isn't such a room. - -```cs -try { - // For example, if 4 players join a room with the name "room1" at the same time, if it doesn't exist, then create and join the room with the name "room1". - await client.JoinOrCreateRoom("room1"); -} catch (PlayException e) { - // Failed to join a room and did not successfully create a room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -For more on `JoinOrCreateRoom`, see the [API documentation](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1Client.htm#ad4a489ac7663ee9eee812cba2659a187). - -### New player joining event - -For players who are already in the room, when a new player joins the room, the server will send the `OnPlayerRoomJoined` event to notify the client, and the client can use the properties of the new player to do some display logic. - -```cs -// Register the event for new players joining -client.OnPlayerRoomJoined += newPlayer => { - // TODO New Player Joining Logic -}; -``` - -You can get all the players in the room by `client.Room.PlayerList`. - -### Determining local players - -Once you have all the players in the room, you can determine if a `Player` is the current local player. - -```cs -var players = client.Room.PlayerList; -var player = players[0]; -var isLocal = player.isLocal; -``` - -### Leaving a room - -The following interface can be called when the player wants to proactively leave the room. - -```cs -try { - await client.LeaveRoom(); - // Leaving the room successfully; you can do things like going to another scene -} catch (PlayException e) { - // Failed to leave the room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -Other players in the room will receive the `OnPlayerRoomLeft` event. - -```cs -// Register the event of a player leaving the room -client.OnPlayerRoomLeft += leftPlayer => { - // TODO can perform the destruction operation for the player's departure -} -``` - -### Related events - -| Event | Parameter | Description | -| ------------------ | ---------- | -------------- | -| OnPlayerRoomJoined | newPlayer | A new player joined the room | -| OnPlayerRoomLeft | leftPlayer | A player left the room | - -## MasterClient - -The room creator will become the MasterClient by default in the Multiplayer service. They can close the room, set the room to be invisible, kick people, and so on. Their device is also responsible for "logical operations". For example, it can be responsible for the following scenarios in the game: - -- Dealing cards for users in card games; -- Controlling the timing and logic of killing monsters; -- Judging the winner at the end of the game; -- ...and so on. - -### MasterClient located on the player's client - -You can write the MasterClient logic in the client, and the player terminal ofthe room creator will take care of the game operations. In this case, Multiplayer provide the following convenient features for MasterClient: - -#### MasterClient Drop Transfer - -When a MasterClient is dropped, the Multiplayer service assigns a new player client as the MasterClient. Even if the original MasterClient comes back online, it does not become the new MasterClient. When the MasterClient is changed, the SDK dispatches the `OnMasterSwitched` event to notify the client when the MasterClient changes. - -```cs -// Register the host switch event -client.OnMasterSwitched += newMaster => { - // TODO can display that the host is changed - - // You can determine whether to perform logical processing according to whether the current client is the Master. - if (client.Player.IsMaster) { - - } -} -``` - -#### Related Events - -| Event | Parameter | Description | -| ---------------- | --------- | ----------- | -| OnMasterSwitched | newMaster | Master changed | - -#### Specify other members as MasterClient - -During the game, if the MasterClient does not want to take the computing function anymore, it can call the following method to actively transfer its role to other player clients: - -```cs -try { - // Specify MasterClient by Player Id - await client.SetMaster(newMasterId); -} catch (PlayException e) { - // Failed to set Master - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -After transferring the MasterClient identity, all players in the room will receive the `OnMasterSwitched` event. - -### MasterClient on the server - -In order to ensure game security and prevent users from cheating, we recommend that you host your MasterClient in the Client Engine provided by TDS and configure the following permissions: - -#### MasterClient will not be transferred when it drops - -After placing the MasterClient on the server, the MasterClient role should not be transferred even if it unexpectedly drops out of the game, in which case you need to specify the `FixedMaster` flag when creating a room: - -```cs -var options = new RoomOptions { - Flag = CreateRoomFlag.FixedMaster -}; -await client.CreateRoom(roomOptions: options); -``` - -### MasterClient operation - -#### Set whether a room is open or not - -MasterClient can set whether a room is open or not, so that other players are not allowed to join when the room is closed. - -```cs -try { - // Setting the room to close - await client.SetRoomOpen(false); - Debug.Log(client.Room.Open); -} catch (PlayException e) { - // Failed to set - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -#### Setting whether a room is visible or not - -MasterClient can set whether a room is visible or not. When the room is not visible, this room will not appear in the player's lobby room list, but **other players can join by specifying roomName**. - -```cs -try { - // Setting the room invisible - await client.SetRoomVisible(false); - Debug.Log(client.Room.Visible); -} catch (PlayException e) { - // Failed to set - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -#### Kicking people - -MasterClient can kick other players in a room out of the room: - -```cs -try { - // You can pass in the code and msg of the kicker. - await client.KickPlayer(otherPlayer.ActorId, 1, "You've been kicked out of your room."); -} catch (PlayException e) { - // Failed to kick - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -After being kicked out of a room, the kicked player receives the `OnRoomKicked` event. - -```cs -client.OnRoomKicked += (code, msg) => { - // code and msg are what the MasterClient passes when it kicks someone. -}; -``` - -At the same time, players other than the MasterClient that are still in the room will receive the `OnPlayerRoomLeft` event: - -```cs -client.OnPlayerRoomLeft += leftPlayer => { - -}; -``` - -## Custom Attributes and Synchronisation - -In order to meet the different gameplay needs of developers, the Multiplayer SDK allows developers to set custom properties. - -The main functions of custom property synchronization include: - -- Keeping the data consistent among multiple clients. -- Custom attributes are managed by the server. When a player enters a room, all custom attributes will be available. - -Custom attributes are divided into "Room Custom Attributes" and "Player Custom Attributes". - -### Room Custom Attributes - -You can set a `PlayObject` type custom attribute for a room, such as the number of rounds in a battle, all the pieces in a room, and so on. - -```cs -// Set the custom properties you want to modify -var props = new PlayObject { - { "gold", 1000 } -}; -try { - // Set the gold property to 1000 - await client.Room.SetCustomProperties(props); - var newProperties = client.Room.CustomProperties; -} catch (PlayException e) { - // Failed to set the property - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -Note: This interface does not directly set the memory values of custom properties in the client, but sends a message to modify the custom properties, and the server makes the final decision whether to change them or not. - -When a room property is changed, the SDK will dispatch the `OnRoomCustomPropertiesChanged` event to notify all player clients (including itself). - -```cs -// Registering room property change events -client.OnRoomCustomPropertiesChanged += changedProps => { - var props = client.Room.CustomProperties; - var gold = props.GetInt("gold"); -}; -``` - -Note: The `changedProps` parameter only indicates the currently changed parameters, not all properties. To get all properties, please get them via `client.Room.CustomProperties`. - -#### Allow only MasterClient to modify room properties - -By default, all clients in a room can modify the room's custom properties. If you want to allow only the MasterClient to do so, you can specify the `MasterUpdateRoomProperties` flag when creating the room: - -```cs -var options = new RoomOptions { - Flag = MasterUpdateRoomProperties -}; -await client.CreateRoom(roomOptions: options); -``` - -### Player custom attributes - -Player custom attributes are essentially the same as [room custom attributes](#room-custom-attributes). - -```cs -// Poker Objects -var poker = new PlayObject { - { "flower", 1 }, - { "num", 13 } -}; -var props = new PlayObject { - { "nickname", "Li Lei" }, - { "gold", 1000 }, - { "poker", poker } -}; -try { - // Request to set player attributes - await client.Player.SetCustomProperties(props); -} catch (PlayException e) { - // Failed to set the property - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -Any player in the room (including yourself) who modifies their custom attributes triggers the Player Custom Attribute Change event: - -```cs -// Register player custom attribute change events -client.OnPlayerCustomPropertiesChanged += (player, changedProps) => { - // Get all the player's custom attributes - var props = player.CustomProperties; - var title = props.GetString("title"); - var gold = props.GetInt("gold"); -}; -``` - -### CAS - -CAS stands for Compare And Swap, which means check and update, and is used to avoid some concurrency problems. - -When `SetCustomProperties` is called, the server receives all the values submitted by the client, which is not enough for some scenarios. - -For example, a property holds the holder of an item in this room, i.e. the key of the property is the item and the value is the holder. -Any client can set this property at any time, and if multiple clients call it at the same time, **the server will take the last call received as the final value**, which is usually illogical. Usually the item should belong to the first person who got it. - -`SetCustomProperties` has an optional parameter `expectedValues` that can be used as a judgement condition. The server will only update properties that currently match `expectedValues`. Updates with expired `expectedValues` will be ignored. - -Suppose there are 10 players in a room, but there is only 1 Tulong Knife, and the Tulong Knife can only be won by the first person who "grabs it". - -We can set the `tulong` value in `expectedValues`: - -```cs -var props = new PlayObject { - // X indicates the current client - { "tulong", X } -}; -var expectedValues = new PlayObject { - // Current owner of the Tulong Knife - { "tulong", null } -}; -await client.Room.SetCustomProperties(props, expectedValues); -``` - -This way, after the first player gets the Tulong Knife, `tulong` corresponds to the value of first player, and subsequent requests with `expectedValues = { tulong: null }` will fail. - -### Related events - -| Events | Parameters | Description -| ------------------------------- | ---------------------- | ------------------ | -| OnRoomCustomPropertiesChanged | changedProps | Room custom property is changed | -| OnPlayerCustomPropertiesChanged | (player, changedProps) | Player custom property is changed | - -## Custom events - -In [Custom Properties](#custom-attributes-and-synchronisation), we introduced how to customize the game's data structures and data types based on the game's requirements. -However, data by itself is not enough. To allow different parties to communicate, custom events are also needed. - -### Sending custom events - -We can send various events through custom events, such as game start, card capture, release X skill, game end, etc. - -```cs -var options = new SendEventOptions { - // Set the receiving group of the event to Master - ReceiverGroup = ReceiverGroup.MasterClient - // You can also specify the receiver actorId - // TargetActorIds = new List() { 1 }, -}; -// Setting Skill Id -var eventData = new PlayObject { - { "skillId", 123 } -}; -// Setting the skill event id -byte SKILL_EVENT_ID = 1; -try { - // Send custom events - await client.SendEvent(SKILL_EVENT_ID, eventData, options); -} catch (PlayException e) { - // Failed to send the event - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -The `options` are the event sending parameters, including the receiving group and the receiver ID array. - -- ReceiverGroup is an enumeration of the targets of the received event, including Others (everyone in the room except yourself), All (everyone in the room), and MasterClient (the host). -- The Receiver ID array is the enumerated value of the target of the received event, i.e., the player's `ActorId` array. The `ActorId` can be obtained via `player.ActorId`. - -Note: If both receiver group and receiver ID array are set, receiver ID array will override receiver group. - -### Receiving custom events - -When the event is sent successfully, the custom event `OnCustomEvent` in the event receiver will be triggered, and different events can be handled according to the `eventId` (event ID). - -```cs -// Registering custom events -client.OnCustomEvent += (eventId, eventData, senderId) => { - if (eventId == SKILL_EVENT_ID) { - // If it is a skill event - var skillId = eventData.GetString("skillId"); - // TODO Handling the display of released skills - - } -}; -``` - -### `event` Parameters - -| parameter | type | description | -| --------- | ---------- | ------------------------------- | -| eventId | byte | Event Id for the event | -| eventData | PlayObject | Event parameter | -| senderId | int | Event sender ID (player's actorId) | - -## Disconnect - -In case of unstable network, you may be disconnected passively. When you are disconnected passively, the SDK will send `OnDisconnected` event to the client, where the developer can alert the player on the UI: - -```cs -// Registering for disconnection events -client.OnDisconnected += () => { - // TODO Reconnect if needed -}; -``` - -## Disconnect and reconnect - -### Keep disconnected users in the room. - -Players may get disconnected when there's unstable network or when they put the game in the background. When the player drops out, the `OnPlayerActivityChanged` event will be triggered for other online players: - -```cs -// Register Player Dropping/Connecting Events -client.OnPlayerActivityChanged += player => { - // Get the status of whether a user is "active" or not - Debug.Log(player.IsActive); - // TODO You can update display and run other logic according to the player's online status. -}; -``` - -If a player doesn't come back to the game for a long time, the server will remove the player from the room and destroy the player's data, and the other players in the room will receive the `OnPlayerRoomLeft` event. -When creating a room, we can set the timeout via `PlayerTtl`, so that when a player drops out of the room, the server will keep the data of the dropped player in the room during the `PlayerTtl` time and wait for the player to come back online. - -```cs -var options = new RoomOptions { - // Set PlayerTtl to 300 seconds - PlayerTtl = 300 -} -await client.CreateRoom(roomOptions: options); -``` - -When a dropped player returns to the room within the `PlayerTtl` time, the other player's `OnPlayerActivityChanged` event will be triggered again, and the developer can determine the player's current state in this event. - -### Reconnect - -Users can reconnect to the server after being disconnected through the following interface. - -```cs -client.OnDisconnected += async () => { - // Reconnect - await client.Reconnect(); -}; -``` - -Note: This interface will only reconnect you to the server, not return you directly to the room if you were previously playing in the room. - -**Recommended** [Reconnect and get room](#rejoin-a-room), then the player can choose whether or not to return to the room. - -You can also just [Reconnect and return to room](#reconnect-and-return-to-room). - -### Reconnect and get room - -After reconnecting, the player can use the `FetchMyRoom` interface to fetch the room they were in before leaving: - -- If the player is still in the room, the room ID will be returned and the game will inform the player of this state. If the player chooses to return to the room they were in before leaving, they can call the `RejoinRoom` interface. -- If the player is not in the room, or the room has been destroyed, a [4301](/sdk/multiplayer/error-code/#4301) exception will be returned, and the game can handle it accordingly. - -```cs -string roomName = await client.FetchMyRoom(); -``` - -### Rejoin a room - -When a player connects to the server, they can rejoin a room via the `Rejoin` interface. If the player calls this interface within the `PlayerTtl` time limit, the player will be able to rejoin the room successfully; if the `PlayerTtl` time limit is exceeded, rejoining the room will fail. - -```cs -try { - // reconnect - await client.Reconnect(); - // The reconnection was successful. We're back in the same room as before. - if (roomName) { - try { - await client.RejoinRoom(roomName); - // If you return to the room successfully, the other players in the room will receive the `OnPlayerActivityChanged` event. - } catch (PlayException e) { - // Failed to return to room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); - } - } -} catch (PlayException e) { - // reconnect failure - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -### Reconnect and return to the room - -This interface is the equivalent of `Reconnect()` and `RejoinRoom()` combined. With this interface, you can directly reconnect and go back to the previous room. - -```cs -try { - await client.ReconnectAndRejoin(); - // Successfully return to the room; update data and interface -} catch (PlayException e) { - // Failure to reconnect or return - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -## Close - -Developers can also proactively close the SDK via the following interface, after which the Client needs to be re-instantiated if it needs to be used again. - -```cs -client.Close(); -``` - -## Error handling - -Specific exception messages can be caught with try-catch when we initiate a request. For example, when creating a room: - -```cs -try { - await client.createRoom(); - // Room Creation Successful -} catch (PlayException e) { - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -## Critical error event - -If a major error is currently occurring, this event will be triggered. It won't be triggered easily, and if it is triggered, please contact technical support to deal with it. - -```cs -client.OnError += (code, detail) => { - // Contact Technical Support - -}; -``` - -## Serialisation - -In the new version of Play, we provide richer ways to synchronise data. The main ones include container types (PlayObject/PlayArray) and custom types. - -### PlayObject - -`PlayObject` is a replacement for the `Dictionary` type in older versions. - -`PlayObject` implements the `IDictionary` interface, which provides a more convenient fetch interface in addition to the `IDictionary` interface. - -Common interfaces are listed below: - -```csharp -// basic type -public bool GetBool(object key); -public int GetInt(object key); -public float GetFloat(object key); -... -// Container type -public PlayObject GetPlayObject(object key); // PlayObject supports nesting -public PlayArray GetPlayArray(object key); -public T Get(object key); -``` - -[For more interfaces, please refer to](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1PlayObject.htm) - -### PlayArray - -`PlayArray` implements the `IList` interface and is mainly used for synchronisation of array objects, similar to `PlayObject`. - -Commonly used interfaces are as follows: - -```csharp -// basic type -public bool GetBool(int index); -public int GetInt(int index); -public float GetFloat(int index); -... -// Container type -public PlayObject GetPlayObject(int index); -public PlayArray GetPlayArray(int index); -public T Get(int index); -// conversion interface -public List ToList(); -``` - -[For more interfaces, please refer to](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1PlayArray.htm) - -### Custom types - -In addition to supporting the two container types mentioned above, `Play` also supports synchronising data of `Custom Types`. - -Suppose we have a Hero type with id, name, and hp defined as follows: - -```csharp -class Hero { - int id; - string name; - int hp; -} -``` - -To synchronise data of type Hero, the following two steps are required: - -#### Implement serialisation / deserialisation methods - -The implementation of serialisation methods is up to the developer. You can use protobuf, thrift, etc. It is sufficient if the serialisation and deserialisation interfaces supported by `Play` are met. - -```csharp -public delegate byte[] SerializeMethod(object obj); -public delegate object DeserializeMethod(byte[] bytes); -``` - -The following is an example of the way to serialise a `PlayObject` provided by `Play`. - -```csharp -// Serialisation methods -public static byte[] Serialize(object obj) { - Hero hero = obj as Hero; - var playObject = new PlayObject { - { "id", hero.id }, - { "name", hero.name }, - { "hp", hero.hp }, - }; - return CodecUtils.SerializePlayObject(playObject); -} -// deserialisation method -public static object Deserialize(byte[] bytes) { - var playObject = CodecUtils.DeserializePlayObject(bytes); - Hero hero = new Hero { - id = playObject.GetInt("id"), - name = playObject.GetString("name"), - hp = playObject.GetInt("hp"), - }; - return hero; -} -``` - -#### Registering Custom Types - -When implementing a serialisation method, remember to register the custom type before using it. - -```csharp -CodecUtils.RegisterType(typeof(Hero), typeCode, Hero.Serialize, Hero.Deserialize); -``` - -where `typeCode` is a numeric code representing the custom type, which is used to determine the custom type during deserialisation. - -## API Documentation - -For more interfaces and details, please refer to [API Interfaces](https://leancloud.github.io/Play-SDK-CSharp/html/). \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/guide/js.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/guide/js.mdx deleted file mode 100644 index e9644f06b..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/guide/js.mdx +++ /dev/null @@ -1,909 +0,0 @@ ---- -title: Multiplayer Development Guide - JavaScript -sidebar_label: JavaScript -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -## Preface - -Multiplayer is a JavaScript game SDK that provides a complete client-side SDK solution for online games with strong networking requirements, thus eliminating the need for development teams to build their own servers and saving most of the development and maintenance costs. The main features provided by Multiplayer are as follows: - -- Getting room list -- Creating a room -- Joining a room -- Randomly joining a (eligible) room -- Getting players in a room -- Getting, setting, and synchronizing room properties -- Getting, setting, and synchronizing player properties -- Sending and receiving custom events -- Leaving a room - -## SDK Import - -Please read [Installation Guide](/sdk/multiplayer/start/js/#installation) to get the JS library files. - -## Initialisation - -First of all, you need to introduce common types and constants in the SDK. - -```javascript -const { - // SDK - Client, - // Play SDK Event Constants - Event, - // event receiver group - ReceiverGroup, - // Creating Room Signs - CreateRoomFlag, -} = Play; -``` - -Note: Cocos Creator can't load `Play` into global variables properly when building `WeChatGame` project, so you need to import `Play` module first. - -```javascript -const Play = require("./play"); -``` - -Next we need to instantiate a client object for the Multiplayer SDK. - - - -```javascript -const client = new Client({ - // Setting the APP ID - appId: "your-app-id", - // Setting the APP Key - appKey: "your-app-key", - // Setting up the Server (replace xxx.example.com with the custom API domain name your app is bound to) - playServer: 'https://xxx.example.com', - // Setting the user id - userId: 'tarara', - // Set game version number (optional, default 0.0.1) - gameVersion: '0.0.1' -}); -``` - - - - - -```js -const client = new Client({ - appId: 'your-client-id', // the game's Client ID - appKey: 'your-client-token', // the game's Client Token - playServer: 'https://your_server_url', // the game's API domain name - userId: 'tarara', // set user id - gameVersion: '0.0.1' // Set the game version, optional, default 0.0.1, players with different versions will not be matched in the same room. -}); -``` - -- The `Client ID` and `Client Token` of the game can be viewed at **Developer Centre > Your Games > Game Services > Application Configuration**. -- The API domain name is viewable at **Application Configuration > Domain Configuration > API**. Refer to the documentation for [domain name](/sdk/domain/guide/) for more information. - - - -Here -`userId` serves as a unique identifier for the client connecting to the server. -Note that this `userId` has the following restrictions: - -- Only letters, numbers, and underscores are allowed -- Cannot exceed 32 characters in length -- Globally unique within an application - -`gameVersion` indicates the version number of the client, which can be used to route to different game servers if multiple versions of the game are allowed to co-exist. - -## Connection - -### Establishing a connection - -Connect the current player to the Multiplayer service with the following code: - -```javascript -client - .connect() - .then(() => { - // Connection successful - }) - .catch((error) => { - // connection failure - console.error(error.code, error.detail); - }); -``` - -## Lobby - -We recommend that you **don't add players to the lobby**, because in the lobby, the server will constantly send out the latest full list of rooms, and players will choose one of the rooms to play by themselves, which is not only unfriendly to the player experience, but also brings a lot of bandwidth pressure. -We recommend you to **use [Room Matching](#room-matching) like most of the mobile games nowadays to quickly match players and let them start the game**. - -If you have a special game scenario where you need to get the room list, you can call the following method: - -```javascript -client - .joinLobby() - .then(() => { - // Join Lobby Successfully - }) - .catch((error) => { - // Failed to join the lobby - console.error(error.code, error.detail); - }); -``` - -When a player joins the lobby, the server sends the list of rooms in the current lobby to the client, and the developer can view the list of rooms or join a room to join the game as needed. - -```javascript -client.on(Event.LOBBY_ROOM_LIST_UPDATED, () => { - const roomList = client.lobbyRoomList; - // TODO can do the logic for displaying the list of rooms. -}); -``` - -For more on `LobbyRoom`, see [API documentation](https://leancloud.github.io/Play-SDK-JS/doc/LobbyRoom.html). - -### Related events - -| Event | Parameter | Description | -| ----------------------- | ---- | ---------------- | -| LOBBY_ROOM_LIST_UPDATED | None | Lobby Room List Update | - -## Room Matching - -A room is a unit that generates "combat interactions" between players. For example, the card table of Landlord, the copy of MMO, the battle of WeChat game, etc., all belong to the category of room in a broad sense. -The combat interactions between players are all done in the room. Therefore, how players enter a room is the key to room matching. Below we will analyse the commonly used "Room Matching" function from the aspects of "Create Room" and "Join Room". - -### Creating a Room - -We can create a room simply like this. The player who creates the room is the room owner ([MasterClient](#masterclient)), and when the owner creates a room successfully, he/she also joins the room. - -```javascript -client - .createRoom() - .then(() => { - // Successful room creation also means that you have successfully joined the room. - }) - .catch((error) => { - // Failed to create room - console.error(error.code, error.detail); - }); -``` - -We can also create a room that sets the relevant information. - -```javascript -// Room customisation properties -const props = { - title: "room title", - level: 2, -}; -const options = { - // The room is not visible - visible: false, - // Retention time after room emptying, in seconds - emptyRoomTtl: 300, - // Maximum number of players allowed - maxPlayerCount: 2, - // The amount of time, in seconds, that the player's data is retained after the player goes offline. - playerTtl: 300, - customRoomProperties: props, - // Custom attribute key for room matching, i.e. room matching condition is level = 2 - customRoomPropertyKeysForLobby: ["level"], - // Setting permissions on the MasterClient - flag: - CreateRoomFlag.MasterSetMaster | CreateRoomFlag.MasterUpdateRoomProperties, -}; -const expectedUserIds = ["world"]; -client - .createRoom({ - roomName, - roomOptions: options, - expectedUserIds: expectedUserIds, - }) - .then(() => { - // Successful room creation also means that you have successfully joined the room. - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -The parameters `roomName`, `roomOptions` and `expectedUserIds` are optional. - -#### roomName - -The room name must be unique; if it is not set, the server generates a unique room id and returns it. - -#### roomOptions - -Specified parameters when creating a room, including: - -- `Opened`: whether the room is open or not. If set to false, no other players are allowed to join. -- `Visible`: whether the room is visible. If set to false, it will not appear in the lobby's room list, but other players can join the room by specifying the room name. -- `EmptyRoomTtl`: how long (in seconds) the room will be kept when there are no players in the room. Default is 0, i.e. the room data will be destroyed immediately when there are no players in the room. Maximum value is 300, i.e. 5 minutes. -- `PlayerTtl`: the time (in seconds) to keep the player's data in the room when the player drops out. Default is 0, i.e. player data is destroyed immediately after the player drops out. Maximum value is 300, i.e. 5 minutes. -- `MaxPlayerCount`: the maximum number of players allowed in the room. -- `CustomRoomProperties`: custom properties for the room. -- `CustomRoomPropertyKeysForLobby`: an array of keys in `CustomRoomProperties`. The properties contained in `CustomRoomPropertyKeysForLobby` will appear in the lobby's room Properties (`client.LobbyRoomList`), and the full set of properties can be viewed in `room.CustomProperties` after joining the room. These properties will be used when matching rooms. -- `Flag`: flag bit for room creation. See [MasterClient drop without transfer](#masterclient-drop-transfer), [Specify other member as MasterClient](#specify-other-members-as-masterclient), and [Allow only MasterClient to modify room properties](#allow-only-masterclient-to-modify-room-properties) below for more details. - -#### expectedUserIds - -An array of specified player IDs. This parameter is mainly used to "reserve space" for certain players who can join the room. - -Note: These "specific players" will not actually join the room. There will only be space in the room reserved for those "specific players" to join. If you want to invite players to join a room, you need to send the room name to your friends through other channels, such as IM, WeChat, etc., and then your friends will join the room through the `JoinRoom(roomName)` interface. - -For more information about `CreateRoom`, please refer to [API Documentation](https://leancloud.github.io/Play-SDK-JS/doc/Client.html#createRoom). - -### Joining a room - -When a room is created, other players can participate in the game by "joining the room". - -#### Join the designated room - -You can join a room by specifying the room name. - -```javascript -// Players are joining the 'LiLeiRoom' room. -client - .joinRoom("LiLeiRoom") - .then(() => { - // Join the room successfully - }) - .catch((error) => { - // Failed to join room - console.error(error.code, error.detail); - }); -``` - -When joining a room, you can also take up space for other players. If the remaining empty space in the room is less than the number of occupied spaces, you will fail to join the room. - -```javascript -const expectedUserIds = ["LiLei", "Jim"]; -// Players are joining the 'game' room and taking up space for the hello and world players. -client - .joinRoom("game", { - expectedUserIds, - }) - .then(() => { - // Join the room successfully - }) - .catch((error) => { - // Failed to join room - console.error(error.code, error.detail); - }); -``` - -For more on `joinRoom`, see [API documentation](https://leancloud.github.io/Play-SDK-JS/doc/Client.html#joinRoom). - -#### Randomly joining rooms - -Sometimes, instead of joining a specific room, we can randomly join "rooms that meet certain conditions" (or even no conditions), such as quick start, quick match, etc. In this case, we can randomly join rooms by calling the `joinRandomRoom` method. - -```javascript -client - .joinRandomRoom() - .then(() => { - // Join the room successfully - }) - .catch((error) => { - // Failed to join room - console.error(error.code, error.detail); - }); -``` - -It is also possible to set "conditions" for random joining, such as randomly joining a room with level = 2. - -```javascript -// Set the match attribute -const matchProps = { - level: 2, -}; -client - .joinRandomRoom({ - matchProperties: matchProps, - }) - .then(() => { - // Join the room successfully - }) - .catch((error) => { - // Failed to join room - console.error(error.code, error.detail); - }); -``` - -### Join or create a specific room - -Many games don't have scenarios that force a designated room owner. As long as a number of players can play in the same room, it is fine for anyone to be the owner. We can use `joinOrCreateRoom()` to achieve this. This method will join the current player directly to the room if there is a relevant room, or create a new room if there isn't such a room. - -```javascript -// For example, if 4 players join a room with the name "room1" at the same time, if it doesn't exist, then create and join the room with the name "room1". -client - .joinOrCreateRoom("room1") - .then(() => { - // Join or create a room successfully - }) - .catch((error) => { - // Failed to join a room and did not successfully create a room - console.error(error.code, error.detail); - }); -``` - -For more on `joinOrCreateRoom`, see the [API documentation](https://leancloud.github.io/Play-SDK-JS/doc/Client.html#joinOrCreateRoom). - -### New player join event - -For players who are already in the room, when a new player joins the room, the server will send the `PLAYER_ROOM_JOINED` event to notify the client, and the client can use the properties of the new player to do some display logic. - -```javascript -// Register new players to join the event -client.on(Event.PLAYER_ROOM_JOINED, ({ newPlayer }) => { - // TODO New Player Joining Logic -}); -``` - -You can get all the players in the room by `client.room.playerList`. - -### Determining local players - -Once you have all the players in the room, you can determine if a `Player` is the current local player. - -```javascript -const players = client.room.playerList; -const player = players[0]; -const isLocal = player.isLocal; -``` - -### Leaving a room - -The following interface can be called when the player wants to proactively leave the room. - -```javascript -client - .leaveRoom() - .then(() => { - // Leaving the room successfully allows you to execute logic such as jumping scenes - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -Other players in the room will receive the `PLAYER_ROOM_LEFT` event. - -```javascript -// Registering a player leaving the room event -client.on(Event.PLAYER_ROOM_LEFT, ({ leftPlayer }) => { - // TODO Can perform the destruction of the player's departure -}); -``` - -### Related events - -| Event | Parameter | Description | -| ------------------ | -------------- | -------------- | -| PLAYER_ROOM_JOINED | { newPlayer } | A new player joined the room | -| PLAYER_ROOM_LEFT | { leftPlayer } | A player left the room | - -## MasterClient - -The room creator will become the MasterClient by default in the Multiplayer service. They can close the room, set the room to be invisible, kick people, and so on. Their device is also responsible for "logical operations". For example, it can be responsible for the following scenarios in the game: - -- Dealing cards for users in card games; -- Controlling the timing and logic of killing monsters; -- Judging the winner at the end of the game; -- ...and so on. - -### MasterClient located on the player's client - -You can write the MasterClient logic in the client, and the player terminal ofthe room creator will take care of the game operations. In this case, Multiplayer provide the following convenient features for MasterClient: - -#### MasterClient Drop Transfer - -When a MasterClient is dropped, the Multiplayer service assigns a new player client as the MasterClient. Even if the original MasterClient comes back online, it does not become the new MasterClient. When the MasterClient is changed, the SDK dispatches the `MASTER_SWITCHED` event to notify the client when the MasterClient changes. - -```javascript -// Registering for host switching events -client.on(Event.MASTER_SWITCHED, ({ newMaster }) => { - // TODO can do host switching display - - // You can determine whether to perform logical processing based on determining whether the current client is a Master. - if (client.player.isMaster) { - } -}); -``` - -#### Related Events - -| Event | Parameter | Description | -| --------------- | ------------- | ----------- | -| MASTER_SWITCHED | { newMaster } | Master changed | - -#### Specify other members as MasterClient - -During the game, if the MasterClient does not want to take the computing function anymore, it can call the following method to actively transfer its role to other player clients: - -```javascript -// Assign MasterClient by Player Id -client - .setMaster(otherActorId) - .then(() => { - // The value here is false - console.log(client.player.isMaster); - }) - .catch(console.error); -``` - -After transferring the MasterClient identity, all players in the room receive the `MASTER_SWITCHED` event. - -### MasterClient on the server - -In order to ensure game security and prevent users from cheating, we recommend that you host your MasterClient in the Client Engine provided by TDS and configure the following permissions: - -#### MasterClient will not be transferred when it drops - -After placing the MasterClient on the server, the MasterClient role should not be transferred even if it unexpectedly drops out of the game, in which case you need to specify the `FixedMaster` flag when creating a room: - -```js -client.createRoom({ - roomOptions: { - flag: CreateRoomFlag.FixedMaster, - }, -}); -``` - -### MasterClient operation - -#### Set whether a room is open or not - -MasterClient can set whether a room is open or not, so that other players are not allowed to join when the room is closed. - -```javascript -// Setting the room to close -client - .setRoomOpen(false) - .then(() => { - console.log(client.room.open); - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -#### Setting whether a room is visible or not - -MasterClient can set whether a room is visible or not. When the room is not visible, this room will not appear in the player's lobby room list, but **other players can join by specifying roomName**. - -```javascript -// Setting the room invisible -client - .setRoomVisible(false) - .then(() => { - console.log(client.room.visible); - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -#### Kicking people - -MasterClient can kick other players in a room out of the room: - -```javascript -// You can pass in an Object object, which only supports the code and msg keys. -var info = { code: 1, msg: "You've been kicked out of your room." }; -client.kickPlayer(otherPlayer.actorId, info).then(() => { - console.log("Successfully kicked out of the room"); -}); -``` - -After being kicked out of a room, the kicked player receives the `ROOM_KICKED` event. - -```javascript -client.on(Event.ROOM_KICKED, ({ code, msg }) => { - // code and msg are what the MasterClient passes when it kicks someone. -}); -``` - -At the same time, players other than the MasterClient who still have a room will receive the `PLAYER_ROOM_LEFT` event: - -```javascript -client.on(Event.PLAYER_ROOM_LEFT, ({ leftPlayer }) => {}); -``` - -## Custom Attributes and Synchronisation - -In order to meet the different game requirements of developers, the Online Battle SDK allows developers to set "custom properties". The custom property interface parameters are defined as JavaScript `Object` types, and the supported data types include: - -- Boolean -- Number -- String -- Object -- Array - -The main functions of custom property synchronization include: - -- Keeping the data consistent among multiple clients. -- Custom attributes are managed by the server. When a player enters a room, all custom attributes will be available. - -Custom attributes are divided into "Room Custom Attributes" and "Player Custom Attributes". - -### Room Custom Attributes - -You can set a `Object` type custom attribute for a room, such as the number of rounds in a battle, all the pieces in a room, and so on. - -```javascript -// Set the custom properties you want to modify -const props = { - gold: 1000, -}; -// Set the gold property to 1000 -client.room - .setCustomProperties(props) - .then(() => { - var newProperties = client.room.customProperties; - }) - .catch(console.error); -``` - -Note: This interface does not directly set the `Memory values of custom properties in the client`, but rather sends a message to modify the custom properties, with the server making the final determination of whether to do so or not. - -When a room property is changed, the SDK will dispatch the `ROOM_CUSTOM_PROPERTIES_CHANGED` event to notify all player clients (including itself). - -```javascript -// Registering room property change events -client.on(Event.ROOM_CUSTOM_PROPERTIES_CHANGED, ({ changedProps }) => { - // The full properties of the room can be obtained from this method - const properties = client.room.customProperties; - const gold = properties.gold; - // TODO can do the interface display of attribute changes. -}); -``` - -Note: The `changedProps` parameter only indicates incrementally changed parameters, not `all properties`. To get all properties, please get them via `client.room.customProperties`. - -#### Allow only MasterClient to modify room properties - -By default, all clients in a room can modify the room's custom properties. If you want to allow only the MasterClient to do so, you can specify the `MasterUpdateRoomProperties` flag when creating the room: - -```js -client - .createRoom({ - roomOptions: { - flag: CreateRoomFlag.MasterUpdateRoomProperties, - }, - }) - .then(() => { - // Room Creation Successful - }) - .catch(console.error); -``` - -### Player custom attributes - -Player custom attributes are essentially the same as [room custom attributes](#room-custom-attributes). - -```javascript -// Poker Objects -const poker = { - // design and colour - flower: 1, - // numerical value - num: 13, -}; -const props = { - nickname: "Li Lei", - gold: 1000, - poker: poker, -}; -// Request to set player attributes -client.player - .setCustomProperties(props) - .then(() => { - // Setting properties succeeded - }) - .catch(console.error); -``` - -Any player in the room (including yourself) who modifies their custom attributes triggers the Player Custom Attribute Change event: - -```javascript -// Registering for player-defined attribute change events -client.on(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, ({ player }) => { - // Get all custom attributes of the player - const props = player.customProperties; - const { title, gold } = props; - // TODO You can do an interface display of property changes -}); -``` - -### CAS - -CAS stands for Compare And Swap, which means check and update, and is used to avoid some concurrency problems. - -When `SetCustomProperties` is called, the server receives all the values submitted by the client, which is not enough for some scenarios. - -For example, a property holds the holder of an item in this room, i.e. the key of the property is the item and the value is the holder. -Any client can set this property at any time, and if multiple clients call it at the same time, **the server will take the last call received as the final value**, which is usually illogical. Usually the item should belong to the first person who got it. - -`SetCustomProperties` has an optional parameter `expectedValues` that can be used as a judgement condition. The server will only update properties that currently match `expectedValues`. Updates with expired `expectedValues` will be ignored. - -Suppose there are 10 players in a room, but there is only 1 Tulong Knife, and the Tulong Knife can only be won by the first person who "grabs it". - -We can set the `tulong` value in `expectedValues`: - -```javascript -const props = { - tulong: X, // X denotes the current client, respectively -}; -const expectedValues = { - tulong: null, // Current owner of the Dragon Slayer -}; -client.room - .setCustomProperties(props, { expectedValues }) - .then(() => {}) - .catch(console.error); -``` - -This way, after the first player gets the Tulong Knife, `tulong` corresponds to the value of first player, and subsequent requests with (`const expectedValues = { tulong: null }`) will fail. - -### Related events - -| Events | Parameters | Description | -| -------------------------------- | ------------------------ | ------------------ | -| ROOM_CUSTOM_PROPERTIES_CHANGED | { changedProps } | Room custom property is changed | -| PLAYER_CUSTOM_PROPERTIES_CHANGED | { player, changedProps } | Player custom property is changed | - -## Custom events - -In [Custom Properties](#custom-attributes-and-synchronisation), we introduced how to customize the game's data structures and data types based on the game's requirements. -However, data by itself is not enough. To allow different parties to communicate, custom events are also needed. - -### Sending custom events - -We can send various events through custom events, such as game start, card capture, release X skill, game end, etc. - -```javascript -const options = { - // Set the receiving group of the event to MasterClient - receiverGroup: ReceiverGroup.MasterClient, - // You can also specify the recipient actorId - // options.targetActorIds: [1], -}; -// Setting Skill Id -const eventData = { - skillId: 123, -}; - -// Set Event Id -const SKILL_EVENT_ID = 1; -// Send custom events -client - .sendEvent(SKILL_EVENT_ID, eventData, options) - .then(() => {}) - .catch(console.error); -``` - -The `options` are the event sending parameters, including the receiving group and the receiver ID array. - -- ReceiverGroup is an enumeration of the targets of the received event, including Others (everyone in the room except yourself), All (everyone in the room), and MasterClient (the host). -- The Receiver ID array is the enumerated value of the target of the received event, i.e., the player's `actorId` array. The `actorId` can be obtained via `player.actorId`. - -Note: If both receiver group and receiver ID array are set, receiver ID array will override receiver group. - -### Receiving custom events - -When the event is sent successfully, the custom event `CUSTOM_EVENT` in the event receiver will be triggered, and different events can be handled according to `eventId` (event ID). - -```javascript -// Registering custom events -client.on(Event.CUSTOM_EVENT, ({ eventId, eventData }) => { - if (eventId === SKILL_EVENT_ID) { - // Deconstruct event data if it is a skill event - const { skillId, targetId } = eventData; - // TODO Handling the performance of released skills - } -}); -``` - -### `event` Parameters - -| parameter | type | description | -| --------- | ------ | ------------------------------- | -| eventId | Number | Event Id for the event | -| eventData | Object | Event parameter | -| senderId | Number | Event sender ID (player's actorId)| - -## Disconnect - -In case of unstable network, you may be disconnected passively. When you are disconnected passively, the SDK will send `DISCONNECTED` event to the client, where the developer can alert the player on the UI: - -```javascript -// Registering for disconnection events -client.on(Event.DISCONNECTED, () => { - // TODO Option to detect the network and reconnect if required -}); -``` - -## Disconnect and reconnect - -### Keep disconnected users in the room. - -Players may get disconnected when there's unstable network or when they put the game in the background. When the player drops out, the `PLAYER_ACTIVITY_CHANGED` event will be triggered for other online players: - -```javascript -// Registered Player Dropping/Uploading Events -client.on(Event.PLAYER_ACTIVITY_CHANGED, ({ player }) => { - // Get the status of whether a user is "active" or not - cc.log(player.isActive()); - // TODO can do display and logic processing according to the player's online status. -}); -``` - -If a player doesn't come back to the game for a long time, the server will remove the player from the room and destroy the player's data, and the other players in the room will receive the `PLAYER_ROOM_LEFT` event. -When creating a room, we can set the timeout via `PlayerTtl`, so that when a player drops out of the room, the server will keep the data of the dropped player in the room during the `PlayerTtl` time and wait for the player to come back online. - -```javascript -const options = { - // Set playerTtl to 300 seconds - playerTtl: 300, -}; -client - .createRoom({ - roomOptions: options, - }) - .then(() => {}) - .catch(console.error); -``` - -When a dropped player returns to the room within the `PlayerTtl` time, the other player's `PLAYER_ACTIVITY_CHANGED` event will be triggered again, and the developer can determine the player's current state in this event. - -### Reconnect - -Users can reconnect to the server after being disconnected through the following interface. - -```javascript -client - .reconnect() - .then(() => { - // Reconnect successfully, you can choose whether to return to the room or not at this time - }) - .catch(console.error); -``` - -Note: This interface will only reconnect you to the server, not return you directly to the room if you were previously playing in the room.**Recommended** [Reconnect and get room](#rejoin-a-room), then the player can choose whether or not to return to the room.You can also just [Reconnect and return to room](#reconnect-and-return-to-room). - -### Returning to a room - -Once a player is connected to the server, they can "rejoin" a room via the `rejoin` interface. If this interface is called within `playerTtl` time, the player will be able to return to the room successfully, if the `playerTtl` time is exceeded, then rejoining the room will fail. - -```javascript -client - .reconnect() - .then(() => { - // The reconnection was successful. We're back in the same room as before. - return client.rejoinRoom(roomName); // The roomName of the room needs to be cached in the client itself. - }) - .then(() => { - // If you return to the room successfully, the other players in the room will receive the `PLAYER_ACTIVITY_CHANGED` event. - }) - .catch(console.error); -``` - -### Reconnect and return to room - -This interface is the equivalent of `reconnect()` and `rejoinRoom()` combined. This interface allows you to directly reconnect and go back to the previous room. - -```javascript -client - .reconnectAndRejoin() - .then(() => { - // Return to room success, update data and interface - }) - .catch(console.error); -``` - -## Close - -Developers can also proactively close the SDK via the following interface, after which the Client needs to be re-instantiated if it needs to be used again. - -```javascript -client.close().then(() => { - // Disconnect successful. -}); -``` - -## Error handling - -Specific exception messages can be caught with Promise catch when we initiate a request. For example, when creating a room: - -```javascript -client - .createRoom() - .then(() => { - // Room Creation Successful - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -## Critical error event - -If a major error is currently occurring, this event will be triggered. It won't be triggered easily, and if it is triggered, please contact technical support to deal with it. - -```javascript -client.on(Event.Error, ({ code, detail }) => { - // Contact Technical Support -}); -``` - -## Serialisation - -JavaScript is a weakly typed language, so data can be synchronised as long as it is of type `Object/Array`. - -### Custom types - -Suppose we have a Hero type with id, name, hp defined as follows: - -```javascript -class Hero { - constructor(id, name, hp) { - this._id = id; - this._name = name; - this._hp = hp; - } -} -``` - -To synchronise data of type Hero, the following two steps are required: - -#### Implement serialisation / deserialisation methods - -The implementation of serialisation methods is up to the developer, you can use protobuf, thrift, etc. It is sufficient if the serialisation and deserialisation interfaces supported by `Play` are met. As long as the serialisation and deserialisation interfaces supported by `Play` are satisfied. - -The following is an example of how to serialise an `Object` with the serialisation methods provided by `Play`: - -```javascript -const { - serializeObject, - deserializeObject, -} = Play; - -// serialisation -static serialize(hero) { - // You can filter the fields to be serialised - const obj = { - id: hero._id, - name: hero._name, - hp: hero._hp, - }; - return serializeObject(obj); -} - -// deserialisation -static deserialize(bytes) { - const obj = deserializeObject(bytes); - const { id, name, hp } = obj; - const hero = new Hero(id, name, hp); - return hero; -} -``` - -#### Registering Custom Types - -When implementing a serialisation method, remember to register the custom type before using it. - -```javascript -const { registerType } = Play; - -registerType(Hero, typeCode, Hero.serialize, Hero.deserialize); -``` - -where `typeCode` is a numeric code representing the custom type, which is used to determine the custom type during deserialisation. - -## API Documentation - -For more interfaces and details, please refer to [API Interfaces](https://leancloud.github.io/Play-SDK-JS/doc/). diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/start/cs.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/start/cs.mdx deleted file mode 100644 index 5460b9e6b..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/start/cs.mdx +++ /dev/null @@ -1,201 +0,0 @@ ---- -title: Getting Started with Multiplayer - C# -sidebar_label: C# -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import CodeBlock from "@theme/CodeBlock"; - -Welcome to Multiplayer. This tutorial will explain the core usage of the SDK by simulating a scenario that compares players' scores. - -## Installation - -The Play client SDK is open source. The source code can be found at [csharp-sdk](https://github.com/leancloud/csharp-sdk). - -You can also download [Release version](https://github.com/leancloud/csharp-sdk/releases). - -### Unity Projects - -The SDK can be imported via Unity Package Manager or manually: - -* **UPM**: Please add dependencies in Packages/manifest.json of your project: - - - {`"dependencies": { - "com.leancloud.play": "https://github.com/leancloud/csharp-sdk-upm.git#play-${sdkVersions.leancloud.csharp}", - }`} - - -* **Direct Import**: Unzip the downloaded LeanCloud-SDK-Play-Unity.zip and drag and drop the `Plugins` directory into the Unity project. If there is already a `Plugins` directory in the project, merge the SDK into the `Plugins` directory in the project. - -### Turn on debug logging - -For debugging purposes, you can register callbacks for logging. In Unity, you can refer to the following settings: - -```cs -// Setting the SDK Log Delegation -LeanCloud.Common.Logger.LogDelegate = (level, log) => -{ - if (level == LogLevel.Debug) { - Debug.LogFormat("[DEBUG] {0}", log); - } else if (level == LogLevel.Warn) { - Debug.LogWarningFormat("[WARN] {0}", log); - } else if (level == LogLevel.Error) { - Debug.LogErrorFormat("[ERROR] {0}", log); - } -}; -``` - -## Initialisation - -Import the required namespaces - -```cs -using LeanCloud.Play; -``` - - - -```cs -var client = new Client("your-app-id", "your-app-key", "tarara", playServer: "https://xxx.example.com"); -// Please replace xxx.example.com with the domain name of the custom API your app is bound to. -``` - - - - - -```cs -var client = new Client( - "your-client-id", // Client ID of the game - "your-client-token", // Client Token for the game - "tarara", // Set the user id - playServer: "https://your_server_url" // The game's API domain name -); -``` - -- The `Client ID` and `Client Token` of the game can be viewed at **Developer Centre > Your Games > Game Services > Application Configuration**. -- The API domain name is viewable at **Application Configuration > Domain Configuration > API**. Refer to the documentation for [domain name](/sdk/domain/guide/) for more information. - - - -## Connecting to a Multiplayer server - -```cs -try { - await client.Connect(); -} catch (PlayException e) { - // connection failure - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -## Creating or Joining a Room - -By default the Play SDK does not require a "lobby" to create/join a room. - -```cs -try { - await client.JoinOrCreateRoom(roomName); -} catch (PlayException e) { - // Failed to create or join a room - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -`JoinOrCreateRoom` ensures that two client players can enter the same room by having the same roomName. Please refer to the [Developer Guide](/sdk/multiplayer/guide/cs/#joinorcreateroom) for more information on the usage of `JoinOrCreateRoom`. - -## Synchronising Player Properties via CustomPlayerProperties - -When a new player joins the room, the Master assigns a score to each player, which is synchronised to the players via "Player Custom Properties". -(No more complex algorithms are done here, just 10 points are assigned to the Master and 5 points to the other players). - -```cs -// Register the event for new players joining the room -client.OnPlayerRoomJoined += (newPlayer) => { - Debug.LogFormat("new player: {0}", newPlayer.UserId); - if (client.Player.IsMaster) { - // Get a list of players in the room - var playerList = client.Room.PlayerList; - for (int i = 0; i < playerList.Count; i++) { - var player = playerList[i]; - var props = new PlayObject(); - // Set 10 points for the master and 5 points for other players - if (player.IsMaster) { - props.Add("point", 10); - } else { - props.Add("point", 5); - } - player.SetCustomProperties(props); - } - var data = new PlayObject { - { "winnerId", client.Room.Master.ActorId } - }; - var opts = new SendEventOptions { - ReceiverGroup = ReceiverGroup.All - }; - client.SendEvent(GAME_OVER_EVENT, data, opts); - } -}; -``` - -Display the score after the player gets their score. - -```cs -// Register for "Player Attribute Change" event -client.OnPlayerCustomPropertiesChanged += (player, changedProps) => { - // Determine if the player is themselves and do the UI display - if (player.IsLocal) { - // Get the player's score - long point = player.CustomProperties.GetInt("point"); - Debug.LogFormat("{0} : {1}", player.UserId, point); - scoreText.text = string.Format("Score: {0}", point); - } -}; -``` - -## Communication via Custom Events - -When the score is distributed, the ID of the winner (Master) is sent as an argument to all players via a custom event. - -```cs -if (client.Player.IsMaster) { - // ... - var data = new PlayObject { - { "winnerId", client.Room.Master.ActorId } - }; - var opts = new SendEventOptions { - ReceiverGroup = ReceiverGroup.All - }; - client.SendEvent(GAME_OVER_EVENT, data, opts); -} -``` - -Make different UI displays depending on whether you judge the winner to be yourself or not. - -```cs -// Registering custom events -client.OnCustomEvent += (eventId, eventData, senderId) => { - if (eventId == GAME_OVER_EVENT) { - // Get the winner's Id - int winnerId = eventData.GetInt("winnerId"); - // If the winner is yourself, display the victory UI; otherwise display the defeat UI - if (client.Player.ActorId == winnerId) { - Debug.Log("win"); - resultText.text = "Win"; - } else { - Debug.Log("lose"); - resultText.text = "Lose"; - } - client.Close(); - } -}; -``` - -## Demo - -We have completed this Demo through Unity for you to run for reference. - -[QuickStart Project](https://github.com/leancloud/Play-CSharp-Quick-Start). \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/start/js.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/start/js.mdx deleted file mode 100644 index 7dbe43410..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/multiplayer/start/js.mdx +++ /dev/null @@ -1,332 +0,0 @@ ---- -title: Getting Started with Multiplayer - JavaScript -sidebar_label: JavaScript -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -Welcome to Multiplayer. This tutorial will explain the core usage of the SDK by simulating a scenario that compares players' scores. - -## Installation - -The Multiplayer Client SDK is open source. You can download [Release version](https://github.com/leancloud/Play-SDK-JS/releases) directly. For source code, please visit [Play-SDK-JS](https://github.com/leancloud/Play-SDK-JS). - -## Supported Development Platforms - -WeChat Developer Tools: WeChat Mini Programs / WeChat Mini Games - -CocosCreator: Mac, Web, WeChat Mini Games, Facebook Instant Games, iOS, Android. - -LayaAir: WeChat Small Game - -Egret:Web - -### Cocos Creator - -[Download play.js](https://github.com/leancloud/Play-SDK-JS/releases) and drag and drop it into your Cocos Creator project, select "Plugin" mode to import. See [Cocos Creator plugin script](https://docs.cocos.com/creator/manual/zh/scripting/external-scripts.html). - -Select the play.js file you just imported in Cocos Creator, and check all the following options in the Property Inspector: - -- Import as plugin -- Allow web platform loading -- Allow editor loading -- Allow Native platform loading - -As shown in the figure: - -![image](/img/multiplayer/cocos-creator-multiplayer-install.png) - -### LayaAir - -[Download play-laya.js](https://github.com/leancloud/Play-SDK-JS/releases) to the bin/libs directory of your Laya project. - -In bin/index.html, introduce the SDK file you just downloaded before the item "UI file generated by IDE": - -```diff - - - - -+ - - - -``` - -### Egret - -[Download play-egret.zip](https://github.com/leancloud/Play-SDK-JS/releases) and extract it to the libs directory of your Egret project. - -Add the SDK configuration to the egretProperties.json file in the Egret project: - -```diff -{ - "engineVersion": "5.2.13", - "compilerVersion": "5.2.13", - "template": {}, - "target": { - "current": "web" - }, - "modules": [ - { - "name": "egret" - }, - ... -+ { -+ "name": "Play", -+ "path": "./libs/play" -+ } - ] -} -``` - -Under the Egret project, execute the `Egret build -e` command, and if a reference to the SDK is generated in manifest.json, the SDK has been installed successfully. - -```diff -{ - "initial": [ - "libs/modules/egret/egret.js", - ... -+ "libs/play/Play.js" - ], - "game": [ - ... - ] -} -``` - -You can refer to [Egret third-party library usage](https://docs.egret.com/engine/docs/extension/threes/instructions). - -### WeChat Mini Program - -[Download play-weapp.js](https://github.com/leancloud/Play-SDK-JS/releases) and drag and drop it to the project directory of WeChat Mini Program. - -### Node.js installation - -Install and reference the SDK: - -```sh -npm install @leancloud/play --save -``` - -## Logging - -Logging can be used to track down problems. The SDK supports debugging with logging turned on in both browser and Node.js environments. When debugging logging is enabled, the SDK will output network requests, error messages, and other information to the console. - -### Browser - -In browser environment, please open the console of your browser and run the following command: - -```shell -localStorage.debug = 'Play' -``` - -### Node.js - -In a Node.js environment, you need to set the environment variable DEBUG to Play, which you can set before starting a command. -Here's an example of a command to start Cloud Engine debugging locally, `lean up`: - -```sh -# Unix -DEBUG='Play' lean up -# Windows cmd -set DEBUG=Play lean up -``` - -## Initialisation - -Importing the SDK - -```javascript -const { - Client, - Region, - Event, - ReceiverGroup, - setAdapters, - LogLevel, - setLogger, -} = Play; -``` - - - -```javascript -const client = new Client({ - // Setting the APP ID - appId: "your-app-id", - // Setting the APP Key - appKey: "your-app-key", - // Setting up the Server (please replace xxx.example.com with the custom API domain name your app is bound to) - playServer: 'https://xxx.example.com', - // Setting the user id - userId: 'tarara', - // Set game version number (optional, default 0.0.1) - gameVersion: '0.0.1' -}); -``` - - - - - -```js -const client = new Client({ - appId: 'your-client-id', // Client ID of the game - appKey: 'your-client-token', // Client Token for the game - playServer: 'https://your_server_url', // The game's API domain name - userId: 'tarara', // Set the user id - gameVersion: '0.0.1' // Set the game version number, optional, default 0.0.1, players of different versions will not be matched in the same room. -}); -``` - -- The `Client ID` and `Client Token` of the game can be viewed at **Developer Centre > Your Games > Game Services > Application Configuration**. -- The API domain name is viewable at **Application Configuration > Domain Configuration > API**. Refer to the documentation for [domain name](/sdk/domain/guide/) for more information. - - - -## Connecting to a Multiplayer server - -```javascript -client - .connect() - .then(() => { - // Connection successful - }) - .catch((error) => { - // connection failure - console.error(error.code, error.detail); - }); -``` - -## Creating or Joining a Room - -By default the Play SDK does not require a "lobby" to create/join a room. - -```javascript -// For example, if 4 players join a room with the name "room1" at the same time, and if it doesn't exist, then create and join the -client - .joinOrCreateRoom("room1") - .then(() => { - // Join or create a room successfully - }) - .catch((error) => { - // Failed to join a room and did not successfully create a room - console.error(error.code, error.detail); - }); -``` - -`joinOrCreateRoom` ensures that two client players can enter the same room by having the same roomName. Please refer to the [Developer Guide](/sdk/multiplayer/guide/js/#join-or-create-a-specific-room) for more information on the usage of `joinOrCreateRoom`. - -## Synchronising Player Properties via CustomPlayerProperties - -When a new player joins the room, the Master assigns a score to each player, which is synchronised to the players via "Player Custom Properties". -(No more complex algorithms are done here, just 10 points are assigned to the Master and 5 points to the other players). - -```javascript -// Register new players to join the room event -client.on(Event.PLAYER_ROOM_JOINED, (data) => { - const { newPlayer } = data; - console.log(`new player: ${newPlayer.userId}`); - if (client.player.isMaster) { - // Get a list of players in the room - const playerList = client.room.playerList; - for (let i = 0; i < playerList.length; i++) { - const player = playerList[i]; - // Set 10 points if the judgement is a homeowner, otherwise set 5 points - if (player.isMaster) { - player.setCustomProperties({ - point: 10, - }); - } else { - player.setCustomProperties({ - point: 5, - }); - } - } - // ... - } -}); -``` - -Players get scores and display their scores. - -```javascript -// Register for "Player Attribute Change" event -client.on(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, (data) => { - const { player } = data; - // Deconstruct to get the player's score - const { point } = player.customProperties; - console.log(`${player.userId}: ${point}`); - if (player.isLocal) { - // Determine if the player is themselves and do the UI display - this.scoreLabel.string = `score:${point}`; - } -}); -``` - -## Communication via Custom Events - -When the score is distributed, the ID of the winner (Master) is sent as an argument to all players via a custom event. - -```javascript -if (client.player.isMaster) { - const WIN_EVENT_ID = 2; - client.sendEvent( - WIN_EVENT_ID, - { winnerId: client.room.masterId }, - { receiverGroup: ReceiverGroup.All } - ); -} -``` - -Make different UI displays depending on whether you judge the winner to be yourself or not. - -```javascript -// Registering custom events -client.on(Event.CUSTOM_EVENT, (event) => { - // Deconstructing event parameters - const { eventId, eventData } = event; - if (eventId === "win") { - // Deconstruction gets the winners Id - const { winnerId } = eventData; - console.log(`winnerId: ${winnerId}`); - // If the winner is yourself, display the victory UI; otherwise display the defeat UI - if (client.player.actorId === winnerId) { - this.resultLabel.string = "win"; - } else { - this.resultLabel.string = "lose"; - } - client.close().then(() => { - // Disconnect successful. - }); - } -}); -``` - -## Demo - -We have completed this Demo in Cocos Creator, LayaAir, and Egret Wing for you to run and reference. - -[QuickStart Project](https://github.com/leancloud/Play-Quick-Start-JS)。 - -## Build Notes - -You can build the projects supported by Cocos Creator. - -Only the Android project requires a little extra configuration when building it, which requires the following code to be added before initialising `play`: - -```js -onLoad() { - const { setAdapters } = Play; - if (cc.sys.platform === cc.sys.ANDROID) { - const caPath = cc.url.raw('resources/cacert.pem'); - setAdapters({ - WebSocket: (url) => new WebSocket(url, 'protobuf.1', caPath) - }); - } -} -``` - -The reason for this is that the SDK uses WebSocket-based wss for secure communication, and you need to adapt the CA certificate mechanism of Android platform through the above code. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/push/guide/flutter.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/push/guide/flutter.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/push/guide/flutter.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/push/guide/flutter.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/push/guide/setup-flutter.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/push/guide/setup-flutter.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/push/guide/setup-flutter.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/push/guide/setup-flutter.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/_category_.json similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/_category_.json rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/_category_.json diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/faq.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/faq.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/faq.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/faq.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/guide.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/guide.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/guide.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/rest.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/rest.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/rest.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/sms/rest.mdx diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/_category_.json deleted file mode 100644 index 7d914738b..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "入门指南", - "collapsed": true, - "position": 0 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/agreement.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/agreement.mdx deleted file mode 100644 index ba9fd5064..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/agreement.mdx +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: TapSDK Privacy Policy -sidebar_position: 8 ---- - -This Privacy Policy is intended to explain to developers and their end users the types of personal information we collect and how we handle and protect personal information. Before registering, accessing, and using TapSDK products and/or services, developers must read the Privacy Policy carefully and access and use it after confirming their full understanding and consent. If developers do not agree with the Privacy policy, they should immediately stop accessing and using TapSDK products and/or services. - -The Privacy Policy does not apply to the behavior of developers who access and use TapSDK products and/or services to process personal information of end users controlled by them in their apps, nor does it apply to services displaying, linking to or repackaging our products and/or services that apply third-party privacy policies and are provided by third parties. We recommend that end users carefully read, fully understand and agree to the privacy policy of the App/relevant third parties before using corresponding products/services. - -In order to facilitate reading and understanding by developers and end users, we have defined key terms, please refer to the Privacy Policy “Appendix: Definition of Key Terms”. - -**Special Instructions:** - -If the developer accesses and uses TapSDK products and/or services in its App, the developer is requested to know and promise: - -(1) The developer has complied with and will continue to comply with applicable laws, regulations, policies and regulatory requirements to collect, use and process personal information of end users and protect the security of personal information. - -(2) The developer has informed the end users about access and use of TapSDK products and/or services in his App, as well as the collection, use and protection rules of TapSDK on necessary personal information of the end users (i.e., the Privacy Policy), and has obtained sufficient, necessary and explicit authorization and consent from the end users on collection, use, and processing of their personal information by TapSDK (including obtaining authorization and consent from the child’s guardian to provide personal information of the end user who is a child). - -(3) The developer has provided the end users with an easy-to-operate user right realization mechanism, including, but not limited to, accessing, correcting, and deleting their personal information, revoking or changing the scope of their authorization and consent, and canceling their personal accounts. - -## I. How we collect and use personal information of the developers and/or end users - -### (I) Personal information we collect - -1. We will collect different information according to the different services the developer chooses: - -| Specific Functions/Service Scenarios | Collect Information/Obtain Permission | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [**TapTap Login**](https://developer.taptap.cn/docs/en/sdk/taptap-login/features/): Provide TapTap login method, players can quickly start the game through TapTap authorization. | Device version, mobile phone style, system version | -| [**Data Analysis**](https://developer.taptap.cn/docs/en/sdk/tapdb/features/): Provide a set of analysis tools that focus on solving data requirements in game projects, which can obtain rich and practical data dashboards and advertising tracking capabilities through simple access, making data analysis and advertising easy to operate, and can also be used for analyzing user persona to help developers better understand users. | Read and write storage permissions, read phone status, IMSI, network device manufacturer, android ID, bssid device application list, WiFi information, device version, mobile phone model, system version | -| [**Embedded Moments**](https://developer.taptap.cn/docs/en/sdk/embedded-moments/features/): Players can visit community forum of TapTap (official announcements, game guides, problem feedback, hot topics, etc.) within the game. They can also see the game dynamics of TapTap friends, and participate in interaction between other players, officials and great gods. | Read and write storage permissions, android ID, WiFi information, device version, system version | -| [**Friends**](https://developer.taptap.cn/docs/en/sdk/friends/features/): Provide game developers with a complete functional interface for adding, deleting, and finding friends, helping games quickly form a social network. | WiFi information, system version | -| [**Achievement**](https://developer.taptap.cn/docs/en/sdk/achievement/features/): You can set “Normal Achievements” and “Platinum Achievements” in the game to increase players’ participation in the game and encourage players to play the game in different ways. | System Version | -| [**Anti-addiction**](https://developer.taptap.cn/docs/en/sdk/anti-addiction/features/): The fast real-name authentication function based on the TapTap account. For players who log in to the game with the TapTap account, after the player agrees to the authorization, the player is allowed to use real-name information that has been certified by the State in the TapTap to quickly complete the in-game authentication process. | IMSI, network equipment manufacturer, system version, read and write storage permissions | -| [**Copyright Verification**](https://developer.taptap.cn/docs/en/sdk/lisence/features/): Help developers to verify whether paid games on players’ devices are paid for downloads through the TapTap store, effectively controlling scenarios where unpaid players get games from other sources. | Task List, System Version | -| [**Real-time Communication (RTC)**](https://developer.taptap.cn/docs/en/sdk/rtc/features/): One-stop voice solution provides real-time voice, voice compliance services, covering FPS, MOBA, MMORPG, casual battle, online board games and other gameplay types. | Recording permission, WiFi information | - -2. When developers choose some of the TapTap platform services provided by TapSDK, or on the scenarios that the end users directly interact with the TapTap platform, please also abide by relevant provisions of privacy policy in the TapTap platform. - -3. We will collect and use personal information of the developers and/or end users in accordance with the purpose of collecting personal information stated in the Privacy Policy. If we want to use the collected personal information for other services/purposes not specified in the Privacy Policy, we will inform the developers and/or end users in a reasonable manner. - -4. The personal information/permission that TapSDK may collect depends on the selection of specific functions/services of TapSDK by the developer. If some apps do not cover certain services or do not provide specific functions, the content in the Privacy Policy involving the above services/functions and related personal information will not apply. - -### (II) Rules for the use of personal information - -1. We will process the collected personal information in accordance with the Privacy Policy and/or the agreement with the developers, and only for realizing functions of TapSDK products and/or services. If the collected personal information needs to be used for other purposes, we will inform the developers in a reasonable manner, and use it after the developers obtain consent from the end users. - -2. After collecting personal information from the developers and/or end users, we will de-identify or anonymize the personal information through technical means. - -3. After the TapSDK products and/or services we provide to the developers cease to operate, or we are reasonably notified that the developers and/or end users withdraw authorization of personal information, or we are reasonably notified that the developers and/or end users close the account or voluntary delete personal information, we will destroy, anonymize or de-identify all personal information received from the developers and/or end users within a reasonable period of time, unless otherwise required by law. - -### (III) Exception of prior authorization and consent - -1. Necessary for the personal information processor to perform legal obligations or legal duties; - -2. Necessary for conclusion and performance of the contract to which you are a party; - -3. Necessary to respond to public health emergencies, or to protect the life, health and property safety of natural persons in emergencies; - -4. To handle personal information already disclosed by yourself or other personal information that has been legally disclosed, within a reasonable scope in accordance with the law; - -5. Other circumstances stipulated by laws and administrative regulations. - -## II. How we use cookies and other similar technologies - -1. In order to ensure normal operation of the website, we will store small data file called Cookie on the end user’s computer or mobile device. Cookie usually contains identifier, site name, and some numbers and characters. The main function of Cookie is to facilitate end user to use website products and services, and to help website count the number of unique visitors. Using Cookie technology, we can provide the end user with more thoughtful personalized services, allow the end user to set their specific service options, and can manage or delete cookies according to his/her own preferences. - -2. When an end user uses products and/or services of TapSDK, we will send Cookie to the end user’s device. We allow Cookie, or other anonymous identifiers, to be sent to our server when an end user interacts with the products and/or services we provide to our partner. We will not use Cookie for any purpose other than those described in the Privacy Policy. - -## III. How we share, transfer, and publicly disclose personal information of the developers and/or end users - -### (I) Sharing - -1. We will share necessary personal information with our affiliates, which are bound by purposes mentioned in the Privacy Policy. - -2. We may share necessary personal information of the developers and/or end users with partners and third parties to ensure smooth completion of products and/or services provided to the developers and/or end users. Our partners are not authorized to use the shared personal information for any other purpose. - -3. For companies, organizations and individuals with whom we share personal information, we will investigate their data security environment and sign strict confidentiality agreements with them, requiring them to handle personal information in accordance with the same standards as ours. - -### (II) Transfer - -We will not transfer personal information of the developers and/or end users to any companies, organizations and individuals, except in the following cases: - -1. Obtain explicit authorization or consent from the developers and/or end users in advance; - -2. Meet requirements of laws & regulations and legal procedures, mandatory government requirements, or judicial rulings; - -3. When it comes to mergers, acquisitions, asset transfers, or similar transactions, if it involves transfer of personal information, we will require new companies and organizations that hold personal information of the developers and/or end users to continue to be bound by the Privacy Policy statement, otherwise, we will require the Companies or Organizations to seek authorization and consent from the developers and/or end users again. - -### (III) Public disclosure - -We will not publicly disclose personal information of the developers and/or end users, except in the following cases: - -1. Obtained explicit consent from the developers and/or end users; - -2. Mandatory requirements from laws, legal procedures, lawsuits, or government authorities. - -### (IV) Exceptions to prior authorization and consent when sharing, transferring, and publicly disclosing personal information - -In the following cases, the sharing, transfer, and public disclosure of personal information does not require prior authorization and consent from the subjects of personal information: - -1. Those directly related to national security and national defense security; - -2. Those directly related to criminal investigation, prosecution, trial and execution of judgments, etc.; - -3. Those related to the personal information controller’s performance of obligations under laws and regulations; - -4. The personal information disclosed by the subjects of personal information to the public on their own; - -5. The personal information collected from legally publicly disclosed information, such as legal news reports, government information disclosure, and other channels. - -## IV. How we store personal information of the developers and/or end users - -### (I) Storage period - -During use of TapSDK products and services by the developers and/or end users, we will continue to store personal information of the developers and/or end users. If the developers and/or end users close the account or voluntarily delete the above information, we will store personal information of the developers and/or end users within the time limit specified by relevant laws and regulations or within the time limit otherwise authorized and agreed by the subject of personal information. - -### (II) Storage location - -We will store in Chinese territory about personal information of the developers and/or end users obtained from the territory of China in accordance with Chinese laws and regulations. On the premise of not violating local laws and regulations, we will also store personal information of the developers and/or end users obtained locally in Chinese territory. - -## V. How we keep personal information of the developers and/or end users secure - -### (I) Security measures - -1. We will collect, use, store, and transmit user information in accordance with mature security standards and specifications in the industry, and use technical protection measures that comply with industry standards to protect personal information and personal sensitive information provided by developers and/or end users, including, but not limited to, firewalls, encryption, de-identification or anonymization, data desensitization encryption, access control measures, to prevent unauthorized access, public disclosure, use, modification, damage, loss, or leakage of data. - -2. We will conduct security background checks on the person in charge of security management and personnel in key security positions, and will also conduct identity authentication and authority control on employees who handle personal information, and sign confidentiality agreements with employees who have access to personal information, clarifying job duties and code of conduct, to ensure that only authorized personnel can access to personal information. - -3. We have established a dedicated team to ensure personal information security, and established special information security management system and internal handling mechanism for security incidents to prevent from security incidents, such as personal information leakage, damage, and loss. - -4. At present, TapTap has passed the network security level protection of the Ministry of Public Security of the People’s Republic of China, and the rating, filing and evaluation for the communication network unit rating and filing by the Ministry of Industry and Information Technology. We will try our best to protect security of your personal information. - -### (II) Handling mechanism for security incidents - -1. We will formulate emergency plan for security incidents about personal information, and regularly organize work team members to conduct drills about security emergency plan to prevent such security incidents from happening. - -2. In the event of security incidents, such as leakage, damage, and loss of personal information, we will promptly activate an emergency plan, taking corresponding measures to prevent expansion of the security incident, and report to relevant competent authorities in accordance with regulations. - -3. After security incidents about personal information occur, we will promptly notify the developers and/or end users about basic information of the security incident, the handling measures we have taken or will take, and our advice on how to respond to the subjects of personal information. - -## VI. How to manage personal information - -You can access and manage personal information in the following ways: - -1. Inquiry, correction, and supplement of personal information - -If the developers need to review, correct or supplement information that we store, please visit our website and log in to your account to operate. If exercise of certain right cannot be performed on the account page, you can contact us through the customer service system or contact information in the Policy, and we will assist you for corresponding operation. - -2. Account cancellation and deletion of personal information - -If the developers do not want to continue using our products, they can submit an application for account cancellation to us through the work order system (the specific path is to log in to the developer’s backstage account, click my customer service, and create a problem for feedback). We will respond to your application needs usually within 15 working days, but in order to ensure account security, we will require the developers to submit sufficient and valid identity information so that we can identify, verify, and process the request. After the account is cancelled, we will no longer provide services. If developers want us to delete their personal information, they can also submit an application for deletion of personal information to us. - -## VII. How we handle personal information of minors - -We attach great importance to the protection of minors’ personal information. - -Please ensure that the developer is over 18 years old, and understand and know: - -1. If the App is aimed at minor users under the age of 18 years old, and/or designed and developed for minor users under the age of 18 years old, the developer must ensure that the guardian of the end user (minor) has read and agreed to the privacy policy of the App, and has authorized and agreed to provide personal information of the minor to us in order to realize relevant functions described in the privacy policy of the App. - -2. If the App is aimed at children users under the age of 14 years old, and/or designed and developed for children users under the age of 14 years old, the developer must ensure that the guardian of the end user (child) has read and agreed to the privacy policy of the App and “TapTap Protection Rules for Children’s Personal Information”, and has authorized and agreed to provide personal information of the child to us in order to realize relevant functions described in the privacy policy of the App. - -We will only collect, use, and publicly disclose personal information of minors when permitted by law, with consent from parents or guardians, or necessary to protect minors. If we unknowingly or mistakenly collect personal information from minors, we will delete it promptly, unless we are required by law to retain such information. If we become aware that personal information of a minor has been collected without prior verifiable parental consent, we will take steps to delete relevant information as soon as possible. - -## VIII. Revision of privacy policy - -In order to provide developers with better services and with continuous development and changes of TapSDK products and/or services, we may revise the Statement in due course. - -When there are major changes to the Privacy Policy, we will announce to the developers and end users on the developer center page for developers and end users to view at any time; or send email or station mail to the developers to explain specific changes to the Privacy Policy, and indicate the effective date. - -If the developer does not agree to accept the revised Privacy Policy, please stop accessing and using our products and/or services; if the end user does not agree to accept the Privacy Policy, he/she can stop using TapSDK products and/or services. The developer shall provide corresponding implementation mechanism to the end user. If the end user continues to use it, it means that he/she agrees to be bound by the revised Privacy Policy. - -## IX. How to contact us - -If developers and/or end users have any questions about the Privacy Policy or matters related to personal information, please contact us in the following ways: - -(i) Send email to: ‘privacy@taptap.com’; - -(ii) Send postal mails to: North Building, B1, No. 718 Lingshi Road, Jing’an District, Shanghai, China Attn.: TapTap Legal Department Postal code: 200072. - -We will promptly verify identity of the applicant, review the issue involved as soon as possible, and respond within 15 days. - -### Appendix: Definition of key terms - -1. **We**: Refers to the corresponding enterprise entity that has the right to operate TapSDK in relevant area. - -2. **TapSDK**: It is a software development kit that provides mobile game data analysis services for mobile application developers (hereinafter referred to as “Developers”). - -3. **TapSDK products and/or services**: Refers to the SDK products and/or services developed by us and our affiliates. - -4. **Developers**: Refers to developer customers who register, access, and use TapSDK products and/or services. - -5. **End Users**: Refers to App end users who use embedded TapSDK products and/or services. - -6. **Personal Information**: Refers to various information recorded by electronic or other means that can identify the identity of a specific natural person or reflect activities of a specific natural person alone or in combination with other information. Personal information includes name, date of birth, ID number, personal biometric information, address, communication contact information, communication records and content, account and password, property information, credit information, whereabouts trajectory, accommodation information, health and physiological information, transactions information, etc. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/faq.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/faq.mdx deleted file mode 100644 index 91bd96203..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/faq.mdx +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: Frequently Asked Questions -sidebar_position: 6 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -## Miscellaneous - -### Does TDS Cloud Services come with DDoS protection? - -TDS provides 2 Gbps of protection bandwidth by default, which protects against small attacks at no additional cost to the developer. -For exclusive games, TDS provides high protection free of charge during the exclusive period. Developers simply need to communicate with TDS in advance about configuration details. -For other games, TDS can assist in accessing the IaaS provider's DDoS protection service at the developer's expense (IaaS providers charge for DDoS protection bandwidth). Please contact us for further details. - - -### Is there a limit to the number of accounts with real-name authentication for the same ID? - -Currently, TapTap requires that a single ID can only be used for real-name authentication on up to 5 TapTap accounts. There is no limit on real-name authentication for game accounts. - - -### How do I get the MD5 of an Android application? - -#### 1. Using an app - -If you only have the APK file of your application, you can use the following tool to get the MD5: [**GenSignatureMD5**](https://capacity-files.lcfile.com/vW65JxH2b2KwDS8JcbVUfiwLHSeHTlD5/tds_getsign.apk). To use the tool, sign and package the game application with an official signature certificate, and then install the APK package on the phone. Install the GenSignatureMD5 tool on the same phone, then open the tool and enter the name of the game package to get the signature MD5. - -#### 2. Using Android Studio - -**For JDK 10 or below**, after signing the application correctly, open a terminal in the jks signature file directory and enter the command: - -```sh -keytool -list -v -keystore xxx.jks -``` -You will see the signature file information in the command window, including the SHA1 and MD5 values. **Note that this approach does not work for versions below JDK 10**. - -For versions above JDK 10, you can't get the MD5 using the above method, but you can use Android Studio's Gradle Tasks to check it, and the debug window will output the MD5 after double-clicking the signingReport in the figure below. - -![](https://capacity-files.lcfile.com/y7fcVDW6cUFKfG4ATDXj8KKE9L2jWprB/%E8%8E%B7%E5%8F%96MD5%E7%A4%BA%E4%BE%8B1.jpeg) - -:::caution - -Note that the MD5 value output from the signingReport debug window is separated by colons, so you will need to manually remove the colons when entering it into the Developer Center. -Example of a value to enter in the TapTap Developer Center: -> -> Correct: 6EB4347CF9C098BE1C8D965D539C42E2 -> -> Incorrect: 6E:B4:34:7C:F9:C0:98:BE:1C:8D:96:5D:53:9C:42:E2 - -::: - -If there is no Gradle Tasks tab in the right Gradle panel, uncheck the options shown below in the settings and sync Gradle again to see the Gradle Tasks tab. - -![](https://capacity-files.lcfile.com/8dEVF81X34JFtUE50tnqj6OIoxxDdXsU/%E8%8E%B7%E5%8F%96MD5%E7%A4%BA%E4%BE%8B2.jpeg) - - -### What should I do if the versions of okhttp used by TapSDK and Bilibili SDK on Android have conflicts? - -TapSDK now automatically includes the LeanCloud core SDK, and the LeanCloud SDK relies on the following base libraries: -- com.squareup.okhttp3:okhttp:4.7.2 -- com.squareup.retrofit2:retrofit:2.9.0 -- io.reactivex.rxjava2:rxjava:2.2.19 - -Some developers have given us feedback that the Bilibili game SDK is provided as an aar with okhttp 3.9.0 (at least until version 5.4.0), which conflicts with the TapSDK dependencies and causes the program to start with the following error: -`Caused by: java.lang.NoSuchMethodError: No static method get(Ljava/lang/String;)Lokhttp3/HttpUrl; in class Lokhttp3/HttpUrl; or its super classes (declaration of 'okhttp3.HttpUrl' appears in /data/app/` - -Since the Bilibili game SDK fixes the okhttp version, solving this problem requires downgrading the okhttp, retrofit, and rxjava base libraries in TapSDK. -You can copy the following configuration to the `dependencies` section of the app's build.gradle: - -
    - -Configuration of build.gradle - - {` - // Note that the following code uses the latest version of the LeanCloud core SDK; if you are not using ${sdkVersions.leancloud.java}, please replace the version codes accordingly - implementation('cn.leancloud:realtime-android:${sdkVersions.leancloud.java}'){ - exclude group: 'cn.leancloud', module: 'storage-android' - exclude group: 'cn.leancloud', module: 'realtime-core' - exclude group: 'cn.leancloud', module: 'storage-core' - } - implementation('cn.leancloud:storage-android:${sdkVersions.leancloud.java}'){ - exclude group: 'cn.leancloud', module: 'storage-core' - } - implementation('cn.leancloud:realtime-core:${sdkVersions.leancloud.java}') { - exclude group: 'cn.leancloud', module: 'storage-core' - } - implementation('cn.leancloud:storage-core:${sdkVersions.leancloud.java}') { - exclude group: 'com.squareup.okhttp3', module: 'okhttp' - exclude group: 'com.squareup.retrofit2', module: 'retrofit' - exclude group: 'com.squareup.retrofit2', module: 'adapter-rxjava2' - exclude group: 'com.squareup.retrofit2', module: 'converter-gson' - exclude group: 'io.reactivex.rxjava2', module: 'rxjava' - } - implementation("com.squareup.retrofit2:retrofit:2.3.0") - implementation("com.squareup.retrofit2:adapter-rxjava2:2.3.0") - implementation("com.squareup.retrofit2:converter-gson:2.3.0") - implementation("io.reactivex.rxjava2:rxjava:2.0.0") - implementation("com.google.code.gson:gson:2.8.6") - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'\n - configurations { - all*.exclude group: 'com.squareup.okhttp3' - } -`} - -
    - -### application id is empty - -Please ensure that the code for initializing the TapTap SDK is in the main thread to avoid this issue and subsequent functionality exceptions, such as not being able to access the network when logging in. - -### The TapTap Client will not notify in-game users of changes to their information. -The TapTap client does not support synchronous update, but when the user successfully logs in through the client, it will return `access_token`, `mac_key`, and the game can locally cache the credentials. -The game can cache the credentials locally, and then each time the user enters the game, it can call the [server-side authentication interface](/sdk/taptap-login/guide/taptap-oauth/) to make a query to synchronise the user's information. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/get-ready.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/get-ready.mdx deleted file mode 100644 index 72de1aa86..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/get-ready.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Before You Start -sidebar_position: 3 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import DomainBinding from "../_partials/setup-domain.mdx"; - -Please complete the following setup process before you start using TapTap Developer Services (TDS). - -## Create an Application - -Before integrating TDS into your game, please first create an application. See [Store Guide](/store/) for more details. - -## Turn On Game Services - -Go to **TapTap Developer Center > Your Game > Game Services > Configuration** and tap “Turn on Now” to obtain the basic info of your app. - -### Basic Info - -`Client ID` is the unique identifier for an app package in the TapTap Developer Center. TapTap uses the `Client ID` to identify applications. Since each app has only one `Client ID`, if you need to have a test server besides an official server for the same app, you must create two different apps and activate their configurations separately. - -### Applicable Region - - - -TapTap has two service areas: one for Mainland China and one for other countries and regions. Each service area has its own account system and each application you create can only work for one of the two service areas. - -![](https://capacity-files.lcfile.com/nnQKxgJJzgErOlIOcxnbIHt8Vc1RmGYe/tap_get_ready.png) - - - - - -The service is applicable for countries and regions outside of Mainland China. - -![](https://capacity-files.lcfile.com/SaYP7m4TEQQTpuvy5n2r0GjxAzgim624/io_tap_get_ready.png) - - - -## Domains - - - -## Privacy Policy - -To integrate Account Services into your game, you must first agree to the [TapTap Platform Developers Agreement](/store/store-devagreement/). By using TDS, you agree to the above agreement. You will hereby bear the corresponding legal liabilities and obligations as per this agreement. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/overview.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/overview.mdx deleted file mode 100644 index fd509e95d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/overview.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Overview -slug: /sdk -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -TapTap Developer Services (TDS) empowers game developers with a collection of tools that helps them develop, operate, and maintain quality games more efficiently than ever. Our vision is to promote a positive environment within the game industry for both developers and players. - -TDS provides the following services that you can use by integrating TapSDK in your games: - -- **[TapTap Login](/sdk/taptap-login/features/)**: Allows players to quickly log into the game by authorizing with a TapTap account. - -- **[TapTap Connect](/sdk/taptap-connect/features/)**: A tool that connects TapTap, the game, and the player. - -- **[Data Analysis](/sdk/tapdb/features/)**: A complete set of analytic tools focused on solving data needs of game projects. Simple integration allows you to obtain an in-depth and practical data dashboard and ad-tracking capabilities, making data analysis and advertising easy to conduct. Additionally, it can also be used to perform crowd analysis to help you better understand users. - -- **[Moments](/sdk/embedded-moments/features/)**: Allows players to access TapTap’s community forums (official announcements, game strategies, problem feedback, trending topics, etc.) without leaving the game. Additionally, they can view their friends’ game moments and interact with their friends as well as game developers and top players. - - - -- **[TapTap Friends](/sdk/tap-friend/features/)**: Player can access a list of TapTap friends who are playing the same game by logging into their TapTap account. - - - -- **[TDS Authentication](/sdk/authentication/features/)**: Helps you quickly build a safe and secure player login system at a low cost, providing the capability for players to log into your game with a variety of accounts including guest accounts and third-party accounts (TapTap, Apple, WeChat, QQ, Facebook, etc.). - - - -- **[Achievements](/sdk/achievement/features/)**: Allows you to set up “Achievements” and “Platinum Achievements” to increase player participation and encourage players to explore different ways to play the game. - - - -- **[Leaderboards](/sdk/leaderboard/features/)**: Based on TDS Authentication, you can set up leaderboards in your game to promote competition among players and thereby increase player activity. - -- **[Cloud Save](/sdk/gamesaves/features/)**: Saves the player’s game progress to the TDS server, where the game can retrieve saved game data and allow the player to continue playing from any save point on any device. - -- **[Gift Packages](/sdk/tds-gift/features/)**: Gift Packages lets you create gift codes for your game which players can redeem in your game later. - - - -- **[TapLink](/sdk/taplink/features/)**: Helps players jump from TapTap to the game to collect the gift packs. - -- **[Anti Addiction](/sdk/anti-addiction/features/)**: A fast real-name authentication feature based on TapTap accounts. With players’ permission, those who log in to the game with a TapTap account can quickly complete the real-name authentication process with their already-verified personal information filed with TapTap. - - - -- **[Copyright Verification](/sdk/copyright-verification/features/)**: Helps buyout games verify paid download eligibility and DLC unlock eligibility. - -- **[Updates](/sdk/update/guide/)**: Whenever the game has an update, players can jump directly from the game to TapTap and download the update. - - - -- **[TapPlay](/sdk/tap-play/features/)**: Run your games in sandboxes so you can develop your games more efficiently and let your games reach more players. - -- **[TapCanary](/sdk/tap-canary/features/)**: Release early versions of your games to internal testers or trusted users for closed testing with either cloud play or TapPlay (sandbox). - - - -- **[Data Storage](/sdk/storage/features/)**: Store and retrieve JSON objects, binary files, geolocations, and other types of data. Its built-in row-level ACL permission control and general user and role management system can help you quickly achieve safe and flexible data access. - -- **[Cloud Engine](/sdk/engine/overview/)**: Provides an exclusive cloud computing platform for hosting static websites. Additionally, it allows customized development using any programming language to dynamically process external requests and meet the needs of business customization. This eliminates the need to build your own server for back-end development. - -- **[Voice Chat](/sdk/rtc/features/)**: Provides a one-stop-shop for voice chat and voice compliance solutions, covering FPS, MOBA, MMORPG, matchmaking, online board games, and various other gaming genres. - - - - -- **[Multiplayer](/sdk/multiplayer/features/)**: Rely on cloud services for easy in-game player matching, online matchmaking message synchronisation and more. - - - -- **[Customer Support](/sdk/tap-support/features/)**: Helps the game's operation team better solve the problems encountered by players. - -To use the corresponding services, complete [Developer Registration](https://developer.taptap.cn/)[Developer Registration](https://developer.taptap.io/), log into the Developer Center, and “Turn on” Game Services. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/quickstart.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/quickstart.mdx deleted file mode 100644 index 53df3fd2b..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/quickstart.mdx +++ /dev/null @@ -1,435 +0,0 @@ ---- -title: TapSDK Quickstart -sidebar_label: Quickstart -sidebar_position: 4 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -This article will introduce how to quickly integrate TapSDK into your app and use the **[TapTap Login](/sdk/taptap-login/guide/start/)** function. - -:::note - -The [Downloads](/tap-download) page provides Unity, Android, and iOS demos for your reference. - -::: - -## Creating a Game - -Log into the [TapTap Developer Center](https://developer.taptap.cn/)[TapTap Developer Center](https://developer.taptap.io/) to register as a developer and create a game. - -## Downloading the TapTap App - -Download the [TapTap app](https://www.taptap.cn/mobile)[TapTap app](https://www.taptap.io/mobile) on your test device. When testing your game, the SDK will take you to the TapTap app for the authorization process. If the TapTap app is not installed on your device, a WebView will be opened for you to log in. - -## Environment Requirements - - -<> - -- Unity 2019.4 or later -- iOS 11 or later, Xcode 14.1 or later -- Android 5.0 (API level 21) or higher - - -<> - -- Android 5.0 (API level 21) or higher - - -<> - -- iOS 11 or later, Xcode 14.1 or later - - - - -:::caution - -- In the **Project Configurations** and **Initialization** sections below, we assume that you will use [TDS Authentication](/sdk/authentication/features/). -- If the game already has a complete account system and only needs TapTap Login and Moments without additional TDS cloud services, there is no need for you to complete the configurations and initializations below. You can jump to [Basic TapTap Login Developer Guide](/sdk/taptap-login/guide/tap-login/) and [Moments Developer Guide](/sdk/embedded-moments/guide/). -- Please make your decision carefully. If you need other TDS services at a later time, it could be hard for you to make a switch. - -::: - -## Project Configuration - - -<> - -The SDK can be imported **either using the Unity Package Manager or manually**. - -#### Method 1: Use Unity Package Manager - -Add the following dependencies into `Packages/manifest.json`: - - - {`"dependencies":{ - "com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.bootstrap":"https://github.com/TapTap/TapBootstrap-Unity.git#${sdkVersions.taptap.unity}", - "com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-${sdkVersions.leancloud.csharp}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -}`} - - -In Unity’s menu bar, select **Window > Package Manager** to view the packages already installed in the project. - -#### Method 2: Import Manually - -1. Open the download pages of **TapSDK Unity** and **LeanCloud C# SDK** from the [Downloads](/tap-download) page and download `TapSDK-UnityPackage.zip` and `LeanCloud-SDK-Realtime-Unity.zip` from these pages. - -2. Go to your Unity project, navigate to **Assets > Import Packages > Custom Packages**, and select the TapSDK packages you want to use from the unzipped `TapSDK-UnityPackage.zip` file. - - - `TapTap_Bootstrap.unitypackage` is the TapSDK Launcher (required). - - `TapTap_Common.unitypackage` is the TapSDK Basic Library (required). - - `TapTap_Login.unitypackage` is for TapTap Login (required). - -3. Drag the unzipped `LeanCloud-SDK-Realtime-Unity.zip` (the Plugins folder) into Unity. - -:::tip - -If you have manually downloaded the `unitypackage` to import the SDK, please edit the configuration file `Assets/TapTap/Common/Plugins/iOS/TapTap.Common.dll` to support iOS only. - -::: - -After importing the SDK, please continue with the following instructions to get your project ready for iOS. - -#### iOS Configuration - -Create a file named `TDS-Info.plist` under the `Assets/Plugins/iOS/Resource` directory and paste the following code into the file with `ClientId` replaced with your game’s Client ID. If you plan to use [Embedded Moments](/sdk/embedded-moments/features/) or [Data Analysis](/sdk/tapdb/features/), make sure to update the permission configurations accordingly, as well as the **text for the permission dialogue**: - -```xml - - - - - taptap - - client_id - ClientId - - - - NSPhotoLibraryUsageDescription - Explain why your app requires this permission. - NSCameraUsageDescription - Explain why your app requires this permission. - NSMicrophoneUsageDescription - Explain why your app requires this permission. - - NSUserTrackingUsageDescription - Explain why your app requires this permission. - - -``` - - -<> - -1. [Download TapSDK Android](/tap-download), unzip it, and import the packages you need to `project/app/libs`. - -2. Open the `project/app/build.gradle` file under your project and add the following gradle configurations: - - {` -dependencies { - ... - // Import all aar packages in the libs directory: - implementation fileTree(dir: 'libs', include: ['*.aar']) - // Or import the specified packages in the libs directory: - //implementation files('libs/TapBootstrap_${sdkVersions.taptap.android}.aar') - //implementation files('libs/TapCommon_${sdkVersions.taptap.android}.aar') - //implementation files('libs/TapLogin_${sdkVersions.taptap.android}.aar') - ... - // Data Storage - implementation 'cn.leancloud:storage-android:${sdkVersions.leancloud.java}' - // Instant Messaging - implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' -} -`} - -3. Add the network permission to `AndroidManifest.xml`: - - ```java - - ``` - -4. Add additional configurations for older Android versions. - - If `targetSdkVersion < 29`, make sure to: - - - Add `xmlns:tools="http://schemas.android.com/tools"` into `manifest` - - Add `tools:remove="android:requestLegacyExternalStorage"` into `application` - - -<> - -#### Import the SDK - -1. Select the project in Xcode, add `-ObjC` and `-Wl -ld_classic` into **Build Setting > Other Linker Flags**. - -2. Download [TapSDK iOS](/tap-download), unzip it, and drag and drop the resources you need into the project directory. - -3. Import the downloaded resource files as needed: - - - Required: TapTap Launcher, Basic Library, and TapTap Login - - ``` - TapBootstrapSDK.framework - TapCommonSDK.framework - TapLoginSDK.framework - LeanCloudObjc.framework - TapCommonResource.bundle - TapLoginResource.bundle - ``` - -4. Please carefully check whether the following dependency libraries are added successfully: - - ``` - // Required - WebKit.framework - Security.framework - SystemConfiguration.framework - CoreTelephony.framework - SystemConfiguration.framework - libc++.tbd - - // TapTap Moments - AVFoundation.framework - CoreTelephony.framework - MobileCoreServices.framework - Photos.framework - SystemConfiguration.framework - WebKit.framework - - // Data Analysis - // If you do not need access to the IDFA, please don’t add the system libraries `AppTrackingTransparency` and `AdSupport`. - AppTrackingTransparency.framework - AdSupport.framework - CoreMotion.framework - Security.framework - SystemConfiguration.framework - libresolv.tbd - libsqlite3.0.tbd - libz.tbd - ``` - -#### Configure Permissions - -If you plan to use TapTap Moments or Data Analysis in your game, please add the following configurations to `info.plist` and **update the text for the permission dialogue**: - -```xml - -NSPhotoLibraryUsageDescription -Explain why your app requires this permission. -NSCameraUsageDescription -Explain why your app requires this permission. -NSMicrophoneUsageDescription -Explain why your app requires this permission. - -NSUserTrackingUsageDescription -Explain why your app requires this permission. -``` - -#### Launching the TapTap App From Your Game - -If a user does not have the TapTap app on their device, a WebView will be displayed for them to log in. - -1. Open `info.plist` and add the following configurations (replace `clientID` with the Client ID you obtained from the Developer Center): - - ![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` - -2. Configure openUrl: - - a) If the project has `SceneDelegate.m`, please first delete it and then add the following code into the `AppDelegate.m` file: - - ```objectivec - - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [TDSHandleUrl handleOpenURL:url]; - } - - - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSHandleUrl handleOpenURL:url]; - } - ``` - - b) Delete the Application Scene Manifest from `info.plist`. - - ![](/img/tap_ios_appmanifest.png) - - c) Delete the two lifecycle delegate methods for managing `Scenedelegate` from `AppDelegate.m`. - - ```objectivec - #pragma mark - UISceneSession lifecycle - - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { - - return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; - } - - - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { - } - ``` - - d) Add `UIWindow` to `AppDelegate.h`. - - ```objectivec - @property (strong, nonatomic) UIWindow *window; - ``` - - - - -## Initialization - -When initializing the TapSDK, please provide the application information including the `Client ID` and the region. - - - -<> - -```cs -using TapTap.Bootstrap; // Namespace -using TapTap.Common; // Namespace - -var config = new TapConfig.Builder() - .ClientID("your_client_id") // (Required) Corresponding Client ID in the Developer Center - .ClientToken("your_client_token") // (Required) Corresponding Client Token in the Developer Center - .ServerURL("https://your_server_url") // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API - .RegionType(RegionType.CN) // (Optional) CN for Mainland China; IO for international - .ConfigBuilder(); -TapBootstrap.Init(config); -``` - - -<> - -**Make sure to execute the code on the UI thread**: - -```java -TapConfig tdsConfig = new TapConfig.Builder() - .withAppContext(MainActivity.this) // Context - .withClientId("your_client_id") // (Required) Corresponding Client ID in the Developer Center - .withClientToken("your_client_token") // (Required) Corresponding Client Token in the Developer Center - .withServerURL("https://your_server_url") // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API - .withRegionType(TapRegionType.CN) // TapRegionType.CN for Mainland China; TapRegionType.IO for International - .build(); -TapBootstrap.init(MainActivity.this, tdsConfig); -``` - - -<> - -```objectivec -// Import `TapBootstrap`, `TapLogin`, `TapCommon`, and `LeanCloudObjc`, and then initialize the SDK: -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // (Required) Corresponding Client ID in the Developer Center -config.clientToken = @"your_client_token"; // (Required) Corresponding Client Token in the Developer Center -config.serverURL = @"https://your_server_url"; // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API -config.region = TapSDKRegionTypeCN; // TapSDKRegionTypeCN for Mainland China; TapSDKRegionTypeIO for International -[TapBootstrap initWithConfig:config]; -``` - - - - - -`client_id`, `client_token`, and `server_url` are required when you initialize the SDK. - -- You can find `client_id` and `client_token` on **Developer Center > Your Game > Game Services > Configuration**. - -- Please **use the HTTPS protocol** for `server_url`. See **[Bind Domain Name](/sdk/start/get-ready/#domain-names)** to learn more about `server_url`. - -## Integrate Functions - -TapSDK provides a variety of functions. After initializing the SDK, feel free to browse the other docs on this site to learn how to enable those functions. -One of our popular functions is TapTap Login. We recommend this as the starting point. - -### Integrate TapTap Login - -Follow the Developer Guide [Getting Started, Integrate TapTap Quick Login](/sdk/taptap-login/guide/start) to finish setting up. - -### Configure Signature Certificate - -For Android and iOS applications, go to your game from the Developer Center, then go to **Game Services > Gaming Ecosystem > TapTap Login** to configure the corresponding app information (see below). Otherwise, the SDK will return a `signature not match` error, and the TapTap Login function will not work properly. - -Fill in the MD5 hash in the "Android Signature" field. See **[How to get MD5 hash](/sdk/start/faq/#如何获取-android-应用的-md5-值)** to learn more. - - - -![](https://capacity-files.lcfile.com/YmcgwIbUzC7dRKunBT5jQ1lYEO2hedWG/start_getready_info.png) - - - - - -![](https://capacity-files.lcfile.com/MM13UMrcN5n1WSJyClE7QQHb5f9ue4o6/io-login-config.png) - - - - -Next, you can package the application and test the TapTap Login function. - -### Android Code Obfuscation - -TapSDK has already undergone obfuscation. Therefore, initiating another obfuscation will lead to unexpected issues. Please add the following configurations in the obfuscation scripts to skip TapSDK obfuscation: - -```java --keep class com.tds.** { *;} --keep class com.taptap.** { *;} --keep class com.tapsdk.** { *;} --keep class tds.androidx.** { *;} -``` - -If you plan to use any cloud services based on the **Data Storage** service (e.g. logging in with **TDS Authentication**), please also add the obfuscation code for **[Data Storage](/sdk/storage/guide/setup-java/#android-code-obfuscation)**. - -## Package - -For Android and iOS apps, you can follow the ordinary packaging process. The following instruction introduces the packaging process for Unity: - -### Package the APK - -Step 1: Configure package name and signature file: - -![](https://capacity-files.lcfile.com/qooIRbr5qtLrnhsP0hWjOSnBYW12eNg6/tap_unity_android_build.png) - -Step 2: Inspect **File > Build Settings > Player Settings > Other Settings > Target API Level**. If the API Level is below 29, please configure the manifest by adding the following to the `application` node: - -``` -tools:remove="android:requestLegacyExternalStorage" -``` - -This is because the SDK is configured with `android:requestLegacyExternalStorage = true` by default. If `targetSdkVersion < 29`, the SDK will throw the error `Android resource linking failed`. - -### Export Xcode Program - -Please configure the icon and `BundleID`: - -![](https://capacity-files.lcfile.com/Nke4QO6zdEz5mRd2Kwd8R9ydyP8QYaJy/tap_ios_build.png) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/ver-lifetime.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/ver-lifetime.mdx deleted file mode 100644 index 3df6dd07e..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/ver-lifetime.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: TapSDK Versions and Maintenance Cycle -sidebar_position: 9 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -## Rules for versioning - -TapSDK is the generic name for many of the services we provide. To make it easier for developers to access them on demand, we use a separate module for each sub-service. For example: `TapTap Login` corresponds to the `TapLogin` module, `Billboard` corresponds to the `TapBillboard` module, and so on. - -We use [semantic versioning](https://semver.org/) to plan versions for TapSDK. The version is formatted mainly with three parts: `MAJOR.MINOR.PATCH`, where: - -- MAJOR: Indicates a collection of features and API interfaces. If there are incompatible changes introduced in an update, we will upgrade the MAJOR version number; -- MINOR: When there are functional additions that ensure backward compatibility, the MINOR version number will be increased. For instance, when a new customer service module is added to version 3.4.0, the updated version will be 3.5.0; -- PATCH: If we fix a bug in the SDK or optimize its internal implementation while keeping the external interface unchanged, we will upgrade the PATCH version number; -- When there are pre-released versions or versions released due to platform-specific limitations, we sometimes append the information to "MAJOR.MINOR.PATCH" as an extension. - -Currently the main line of our development is the **3.x version**. For the SDK versions and changelist for each platform, please refer to the following links: - -- Unity Engine SDK: https://github.com/taptap/TapSDK-Unity/releases -- Unreal Engine SDK:https://github.com/taptap/TapSDK-UE4/releases -- Android SDK:https://github.com/taptap/TapSDK-Android/releases -- iOS SDK:https://github.com/taptap/TapSDK-iOS/releases - -## Lifecycle of version maintenance - -Each major release is **continuously maintained** if it is a mainline release that we develop/maintain - this is the case for the current 3.x version of the SDK**. If it's not a major release, we **maintain it for two years** after we stop developing new features. If the latest version is 3.16.5, and in the next iteration we change the external interfaces and move to 4.x, then we will maintain 3.x for two years from the time 4.0.0 is released - there will only be bug fixes and no new features; and then after two years 3.x will be completely deprecated. - -For minor versions under the current mainline (e.g. 3.1.x/3.2.x), if a bug is found, we will fix it in the current latest version (not in the minor version where the bug was found). Since the interfaces are fully compatible between minor version numbers and the sub-functions are mostly released as independent modules (no impact between modules), developers **can upgrade to the latest minor version without any burden**. - -Let's look at an example. A developer uses real name authentication and anti-addiction by integrating the `TapLogin/BootStrap/AntiAddiction` modules from the version `3.5.3`, while the latest version is currently `3.10.4`. Later: - -- The developer found and reported a new bug in the SDK. We followed up to fix it and released version `3.10.5`. The developer can directly upgrade the corresponding modules from `3.5.3` to `3.10.5`; -- If the developer found and reported a bug that has already been fixed (e.g. no longer exists since `3.8.0`), then the developer can simply upgrade to our latest version, `3.10.4`. - -We recommend developers to update TapSDK regularly and keep a reasonable update frequency to ensure the stability of the client and better user experience. Meanwhile, we will publish the SDK update plan in advance of any interface incompatibility update; we will also remind developers to upgrade the SDK that will be out of maintenance cycle in time. The update plan and upgrade notification will be sent to all developers via email. After receiving the email notification, developers who need to upgrade their SDKs must do so as soon as possible to avoid any impact on functionality or revenue. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/version-releases.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/version-releases.mdx deleted file mode 100644 index 47408c427..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/start/version-releases.mdx +++ /dev/null @@ -1,659 +0,0 @@ ---- -title: TapSDK Release Notes -sidebar_position: 7 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -## TapSDK V-3.19.1 -Released 2023-07-19 - -### Bug fixes -- **TapDB:** Fixed a bug in PC platform decoding cached data - -### Improvements -- **Billboard:** Optimized the blurring of the PC announcement panel (increased WebView rendering density) - -### Internal changes -- **Login:** Replace the domain name for web page authorization. - -## TapSDK V-3.19.0 -Released 2023-07-07 - -### Features -- **TapPayment:** Supports WeChat and Alipay App Payment and WeChat Payment, depends on TapCommon version 3.19.0. - -### Bug fixes -- **AntiAddiction:** Fix the issue that the ID input box in the manual real name window cannot input X on some devices. - -### Internal changes -- **Common:** Added support for TapPayment interface. - -## TapSDK V-3.18.9 -Released 2023-06-30 - -### Improvements -- **AntiAddiction:** To optimise the issue of multiple authorisations due to multiple clicks using Tap Authentication, add a Loading prompt when authorisation is triggered. - -### Features -- **PaymentsGlobal:** Added an overseas payments module to provide developers with the best solution for selling games on the TapTap shop and selling digital goods and content within games. - -## TapSDK V-3.18.8 -Released 2023-06-15 - -### Bug fixes -- **Login:** Fixes a login failure message that appears briefly in the login window during TapTap login. - -### Improvements -- **DB:** Android adds a catch for an exception caused by OAID missing a so library file for the platform architecture. -- **AntiAddiction:** Android changed the existing Lambda part to a normal anonymous class for compatibility with Java 1.8 and Gradle plugins below 4.0 that don't support Lambda expressions. -- **Friend:** Android for compatibility with Java 1.8 and Gradle plugin below 4.0 does not support Lambda expression issue, change the existing Lambda part to normal anonymous class. -- **Login:** PC now supports Indonesian, Japanese, Korean, Thai, and Traditional Chinese. - -### Internal changes -- **Common:** Android now supports to get clientToken and serverUrl fields in TapConfig via ContentProvider after TapSDK initialisation. - - -## TapSDK V-3.18.7 -Released 2023-05-25 - -### Bug fixes -- **AntiAddiction:** Fixed the issue that anti-addiction window is not fully displayed on some Android models. - -### Improvements -- **DB:** Support new CAID interface. - - -## TapSDK V-3.18.6 -Released 2023-04-27 - -### Bug fixes -- **AntiAddiction:** Fix the issue that the page can't disappear after cancelling the authorisation in some cases. - -### Improvements -- **Support:** Multi-language support for open customer service page. -- **Achievement:** Extend network timeout to 10s. -- **Achievement:** Optimise the parsing of some error messages. -- **DB:** Optimise the number of reads and writes to the cache file. -- **Moment:** Optimise rotation logic -- **Common:** Remove unnecessary architectural support in LZ4. - - -## TapSDK V-3.18.5 -Released 2023-04-10 - -### Features -- **Common:** Add application configuration data encapsulation. -- **DB:** TapDB adds device properties. -- **DB:** Add device identification data encapsulation. -- **Login:** Adds user information data encapsulation. - - -## TapSDK V-3.18.5-1 -Released 2023-04-20 - -### Improvements -- **DB:** Optimise language settings - - -## TapSDK V-3.18.4 -Released 2023-03-30 - -### Features -- **DB:** PC interface rewrite -- **DB:** TapDB new interface to control whether to use Themis or not. -- **DB:** Themis update -- **License:** New interface supports skip cache to force checking purchase status. - -### Bug fixes -- **DB:** TapDB no longer obtains ipv6 address -- **DB:** TapDB version number changed to TapSDK version number. - - -## TapSDK V-3.18.3 -Released 2023-03-23 - -### Bug fixes -- **AntiAddiction:** Fix iOS no callback exception. - - -## TapSDK V-3.18.2 -Released 2023-03-20 - -### Features -- **AntiAddiction:** Update anti-addiction authentication interface. -- **Achievement:** UI adapted to large screen devices. -- **Achievement:** Support for PC Version - -### Bug fixes - -- **AntiAddiction:** Fix UI misalignment of alerts -- **AntiAddiction:** Fixed iOS curfew not popping up in some cases. -- **DB:** Disable getting device id from TapTap by default. - - -## TapSDK V-3.18.1 -Released 2023-03-06 - -### Features -- **DB:** Add network redirection support. - -### Bug fixes -- **Payment:** Fixed the exception that there is no callback when user payment is cancelled voluntarily. -- **Support:** Fix the problem of white screen when clicking retry when disconnecting and reconnecting. -- **Billboard:** Fix a configuration issue with Unity's packaging. -- **Billboard:** Fix an exception in the initialisation interface. - - -## TapSDK V-3.18.0 -Released 2023-02-20 - -### Features -- **Support:** Support own account related login. -- **Support:** Support TDS built-in account login. -- **Support:** Optimise customer service interface. - - -## TapSDK V-3.17.0 -Released 2023-02-17 - -### Features -- **Billboard:** Add Billboard and Open Screen Announcement. - -### Improvements -- **DB:** Optimise device compatibility. - - -## TapSDK V-3.16.6 -Released 2023-01-06 - -### Bug fixes -- **AntiAddiction:** Fix the bug that the game may restart when quitting the game under Unity Android multi-activity. - - -## TapSDK V-3.16.5 -Released 2022-12-05 - -### Features -- **DB:** Add device time reporting. - -### Improvements -- **AntiAddiction:** Add platform compatibility handling. -- **Payment:** Optimises the device ID fetching process. - - -## TapSDK V-3.16.4 -Released 2022-11-17 - -### Features -- **DB:** Update reported domain name and data format. -- **Login:** Modify web login UI display. -- **AntiAddiction:** Add optimisation of real name process in sandbox. - -### Bug fixes -- **Login:** Fix the exception of eligibility verification callback in some cases. -- **Bootstrap:** Fix push service triggered when device starts or network status is updated. - - -## TapSDK V-3.16.2 -Released 2022-11-01 - -### Features -- **Support:** Added ability to open customer service page in SDK. -- **DB:** CAID attribution support for iOS. - - -## TapSDK V-3.16.1 -Released 2022-10-17 - -Changelog - - -### Features -- **Billboard:** Add close button event callback. -- **Billboard:** Optimise UI display when network exception occurs. -- **Friend:** Add support for buried data. -- **DB:** Support interface setting OAID application package key. - -### Bug fixes -- **Billboard:** Fix iOS 16 compatibility issue. - - -## TapSDK V-3.16.0 -Released 2022-09-30 - -### Features -- **Payment:** Support overseas payment. - -### Bug fixes -- **AntiAddiction:** Fix upm import error. - -### Improvements -- **DB:** TapDB iOS supports Mac with Arm chip. - - -## TapSDK V-3.15.1 -Released 2022-09-15 - -### Features -- **Common:** Add UI module for anti-addiction dependency. - -### Bug fixes -- **AntiAddiction:** Fix the compilation error caused by missing UI module (UI module has been moved to Common library). - - -## TapSDK V-3.15.0 -Released 2022-09-13 - -Warning: This version is anti-sedimentation, please don't use this version yet. - -### Bug fixes -- **Achievement:** Fix Achievement Android SDK failed to initialise for the first time on Android 12. - -### Improvements -- **AntiAddiction:** Anti-Addiction interface optimisation. -- **DB:** Adaptation of new OAID for Android SDK. -- **Login:** Optimise unnecessary error logs in Android SDK. - - -## TapSDK V-3.14.0 -Released 2022-08-25 - -### Features -- **Themis:** New exception reporting module. - -### Bug fixes -- **DB:** TapDB adds Themis support. - - -## TapSDK V-3.13.0 -Released 2022-08-18 - -### Features -- **Login:** iOS and Android Tap login support for new scopes (basic_info & email) -- **Login:** iOS and Android Tap login support new scope (basic_info & email). - -### Bug fixes -- **Login:** Android Tap login fixes some special case crashes (added null check to avoid NPE). - - -## TapSDK V-3.12.1 -Released 2022-08-09 - -### Bug fixes -- **Common:** Fix NPE generated by not passing announcement parameter during TapSDK initialisation - - -## TapSDK V-3.12.0 -Released 2022-08-04 - -Danger: TapBootstrap generates a null pointer exception when initialising, please don't use this version yet. - - -### Features -- **Billboard:** New Announcement module. - -### Bug fixes -- **Payment:** Fix a crash problem when the payment window is rotated. - - -## TapSDK V-3.11.1 -Released 2022-07-25 - -### Bug fixes -- **Login:** Fix iOS login module's usage of system URL callbacks. -- **Common:** Fix iOS Authorisation module's usage of system URL callbacks. - -### Improvements -- **Friend:** Update the way to get Tap friends. - - -## TapSDK V-3.11.0 -Released 2022-07-12 - -### Bug fixes -- **Achievement:** Fixed a parsing problem in the bridge section. -- **DB:** Compatibility handling for bridge incoming parameter null. -- **DB:** Interface optimisation -- **AntiAddiction:** iOS compatibility when incoming userId contains special characters. - - -## TapSDK V-3.9.0 -Released 2022-05-25 - -### Features -- **Payment:** Android provides domestic payment service (in beta). - -### Bug fixes -- **Common:** iOS blocked logging background tasks to avoid crash. -- **LeanCloud:** Update to 0.10.11 [address](https://github.com/leancloud/csharp-sdk/releases/tag/0.10.11) - -### Improvements -- **Moment:** Dynamic SDK local multi-language resource adaptation. - - -## TapSDK V-3.8.0 -Released 2022-05-09 - -### Features -- **Friend:** Add friend mode blacklist. - -### Bug fixes -- **AntiAddiction:** Fixed iOS local cache misconfiguration and optimised the configuration cache logic. - -### Improvements -- **Moment:** Optimise authorisation request logic. -- **LeanCloud:** Updated to 0.10.10 [address](https://github.com/leancloud/csharp-sdk/releases/tag/0.10.10) - - -## TapSDK V-3.7.1 -Released 2022-04-11 - -### Features -- **DB:** Add getting and reporting of boot time. - -### Bug fixes -- **Moment:** Fixed dynamic rotation issue. - -### Improvements -- **AntiAddiction:** Optimise the logic of authorisation from the master. - - -## TapSDK V-3.7.0 -Released 2022-03-28 - -### Bug fixes -- **Friend:** Support custom user name and configure additional custom parameters for sharing links. -- **Friend:** Add interface to parse and process sharing links. -- **Friend:** Add interface for blacklisting in follow mode. -- **Friend:** Add rich information data in friend request data. -- **Friend:** Support finding user information by user ID. -- **Login:** Add interface to get mutual friend list. - - -## TapSDK V-3.6.3 -Released 2022-03-08 - -### Features -- **DB:** Use overseas domain name when initialising region as overseas. -- **DB:** iOS When the report of playing hours fails at the end of the process, it will try to report the hours again when the process is opened again (if you have already accessed the data analysis function, and the initialisation region is overseas, and you need to migrate the data before upgrading the SDK to 3.6.3, please contact us by submitting a work order). -- **DB:** iOS internal version is updated to 3.0.9. - -### Bug fixes -- **Bootstrap:** Android LCPush receiver exported parameter changed to true to adapt to Android 12. -- **Achievement:** Achievement panel does not show rarity text when rarity is 0. -- **Achievement:** Android resource usage has been changed to R file references. -- **Login:** Android tries to fix the problem of missing static variables. -- **Login:** Android resource usage changed to R file references. -- **Moment:** Android fixes getDialog related NPE on some models. -- **Moment:** iOS provide api to switch to Controller mode to show UI. -- **Common:** Android fixes Traditional Chinese recognition on certain models. -- **Common:** Android fix TapDBConfig initialisation failure when not configured during initialisation. -- **AntiAddiction:** Improve the user experience when the anti-addiction service is abnormal. - - -## TapSDK V-3.6.1 -Released 2022-02-07 - -### Bug fixes -- **RTC:** RTC interface optimisation -- **Login:** Login UI optimisation -- **Moment:** Inline dynamic support for multi-language reporting. -- **AntiAddiction:** Real Name Authentication Interface Removal of Parameter TapToken - - -## TapSDK V-3.6.0 -Released 2022-01-07 - -### Features -- **Friend:** Support to get the list of friends of Tap and in-game one-way follow models. - -### Bug fixes -- **License:** DLC fixes the issue that the purchase callback cannot be obtained after directly initiating a purchase request in the query callback. - - -## TapSDK V-3.5.0 -Released 2021-11-30 - -### Bug fixes -- **RTC:** Fix range voice failure issue. -- **Friend:** Find friend by friend code. -- **Friend:** Add friend by friend code. -- **Friend:** Query third party friend list -- **Friend:** Follow TapTap Friends -- **Achievement:** Fixed the problem of reading local data and recognising achieved achievements as unachieved. -- **Login:** Inline web login page supports normal display of shaped bangs screen. -- **DB:** Add soc related parameter to report. -- **Moment:** Dependency update -- **Common:** Support update -- **AntiAddiction:** Added compatibility handling for anti-addiction server exceptions. -- **AntiAddiction:** Hiding when switching accounts. -- **AntiAddiction:** Update callback code for switching accounts (from 1000 -> 1001) - - -## TapSDK V-3.4.0 -Released 2021-11-09 - -### Features -- **RTC:** Add instant voice call. -- **DB:** Modify some reporting parameters and fix the issue of duration statistics. - - -## TapSDK V-3.3.1 -Released 2021-10-25 - -### Features -- **Friend:** Add multi-language support for friends. -- **Friend:** Change from prompt to callback when jumping from sharing page to in-app and sending friend request. - -### Bug fixes -- **DB:** Fixed reported address error. -- **AntiAddiction:** Api add public parameter. - - -## TapSDK V-3.3.0 -Released 2021-10-15 - -### Features -- **Support:** New customer service module. -- **Friend:** Add Friend module. -- **AntiAddiction:** New anti-addiction module. - -### Bug fixes -- **Achievement:** UI adjustment -- **Moment:** Optimise statistics parameters - - -## TapSDK V-3.2.0 -Released 2021-09-01 - -### Breaking changes -- **Bootstrap:** Migrated `RegionType.cs`, `TapConfig.cs` to `TapCommon` module. - -*## Features -- **Bootstrap:** Supports cloud archiving. -- **Login:** New PC support. -- **DB:** Android TapDB supports sandbox reporting. -- **DB:** iOS support for ASA -- **DB:** iOS Remove redundant reporting parameter (appId). - -### Bug fixes -- **Moment:** Fix the issue that there is no callback after logging in dynamically. -- **Moment:** iOS Optimise memory usage after opening dynamics multiple times - - -## TapSDK V-3.1.0 -Released 2021-07-28 - -### Features -- **Bootstrap:** Support in-game friends. -- **Achievement:** Support in-game achievement. - -### Bug fixes -- **Moment:** Fixes a crash when opening Moment on Android 11 devices without certain permissions. -- **License:** Fix the issue that authentication will crash after successful authentication in some cases. - - -## TapSDK V-3.0.0 -Released 2021-07-16 - -Tips: Current version does not support v2.x upgrade. - -### Breaking changes -- **Bootstrap:** Account system upgraded to TDSUser -- **Bootstrap:** Login related interface modification -- **Bootstrap:** Get Bonfire Qualification interface moved to TapLogin module -- **Bootstrap:** Removed Set Language interface. -- **Login:** Open all interfaces, support to get openID and unionID of TapTap account. - -### Bug fixes -- **Moment:** Fix a bug that could cause the page to crash. - - -## TapSDK V-2.1.8 -Released 2021-07-21 - -### Bug fixes -- **Moment:** Fix the issue that opening Moment crashes on Android 11 devices without certain permissions. -- **License:** Fix the issue that authentication will crash after successful authentication in some cases. - - -## TapSDK V-2.1.7 -Released 2021-07-14 - -### Features -- **DB:** TapDB recharge interface now supports passing custom fields. - -### Improvements -- **Friend:** TapLogin and TapFriends modified iOS openUrl intercept method. - - -## TapSDK V-2.1.6 -Released 2021-07-01 - -### Bug fixes -- **Moment:** Fix the bug that calling [TapMoment close] doesn't work. - - -## TapSDK V-2.1.5 -Released 2021-06-22 - -### Features -- **Moment:** New scenario-based callback interface. - - ``` - // Scenario callbacks are returned in a unified interface for dynamic callbacks, Code = 70000, content is a string in JSON format. - { - sceneId: "taprl0071417002", - eventType: "READY", - eventPayload: "{}", - eventType: "READY", eventPayload: "{}", timestamp: 1622791814130, // ms - } - ``` -- **DB:** Android Add Game TapTap Shared ID Switch -- **Login:** Evoke TapTap client login within CloudPlay. - - -## TapSDK V-2.1.4 -Released 2021-06-10 - -### Features -- **Friend:** New interface to set rich message and query rich message. -- **Friend:** TapUserRelationShip adds online & time & TapRichPresence parameters. - -### Bug fixes -- **Bootstrap:** Internal optimisations -- **Common:** Optimise multi-language correlation -- **Common:** Fixed JSON parsing issue. - - -## TapSDK V-2.1.3 -Released 2021-06-03 - -### Features -- **Bootstrap:** New multi-language configurations for Traditional Chinese, Japanese, Korean, Thai and Indonesian. -- **Common:** iOS now supports TapTap and Tap. - -### Bug fixes -- **Friend:** Fixed the possibility of friend popup multiple times in special scenarios. -- **Moment:** Android UI may be obscured due to judgement failure on some bangs screen devices. - - -## TapSDK V-2.1.2 -Released 2021-05-18 - -### Breaking changes -- **Bootstrap:** Deprecates OpenUserCenter interface. - -### Features -- **Friend:** New message callback interface. - ```c#'' - TapFriends.RegisterMessageListener(ITapMessageListener listener); - ``` -- **Friend:** Search for friends - ```c# - TapFriends.SearchUser(string userId, Action action) - ``` -- **Friend:** Sharing a friend invitation - ```c# - TapFriends.SendFriendInvitation(Action action);; - ``` -- **Friend:** Get the friend invitation link - ```c# - TapFriends.GenerateFriendInvitation(Action action); - ``` -- **Friend:** Add TapFriends UI - -### Bug fixes -- **Common:** Fix iOS ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES setting issue which may lead to AppStore audit failure. - - -## TapSDK V-2.1.1 -Released 2021-05-11 - -### Breaking changes -- **Bootstrap:** LoginType removes Apple, Guest login type. -- **Bootstrap:** TDS-Info.plist removes Apple_SignIn_Enable configuration. -- **Bootstrap:** Deprecates the Bind interface. -- **Bootstrap:** TapConfig Constructor Parameter Changes - -### Features -- **Common:** New TapTap App related support for Android. -- **Common:** New link support for TapTap. -- **Common:** Switch between haynesville and overseas domains. -- **Bootstrap:** New bonfire test eligibility verification - `` - TapBootstrap.GetTestQualification((bool, error)=>{ }):. - ``` -- **Bootstrap:** initial configuration via TapConfig - * New TapDBConfig for TapDB initialisation configuration. - * New ClientSecret for TapSDK initialisation. - ```c# - // It is recommended to use the following TapConfig constructor for initialisation - var config = new TapConfig.Builder() - .ClientID("client_id") - .ClientSecret("client_secret") - .RegionType(RegionType.CN) - .TapDBConfig(true, "gameChannel", "gameVersion", true) - .ConfigBuilder(); - TapBootstrap.Init(config); - ``` -- **Friend:** Add Tap friend service. -- **License:** New DLC purchase service. -- **Moment:** New DirectlyOpen interface. - ** Scenario entry - ```c# - var sceneDic = new Dictionary(){{TapMomentConstants.TapMomentPageShortCutKey,sceneId}}; - - TapMoment.DirectlyOpen(orientation,TapMomentConstants.TapMomentPageShortCut,sceneDic); - ``` -- **DB:** New RegisterDynamicProperties interface. -- **DB:** New AdvertiserIDCollectionEnabled IDFA get switch. - - -## TapSDK V-2.0.0 -Released 2021-04-08 - -[TapSDK Developer Centre](https://developer.taptap.cn/docs/en/tap-download/) \ No newline at end of file diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/app-data-share.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/app-data-share.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/app-data-share.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/app-data-share.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/custom-reset-verify-page.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/custom-reset-verify-page.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/custom-reset-verify-page.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/custom-reset-verify-page.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/network-connectivity-diagnosis.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/network-connectivity-diagnosis.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/network-connectivity-diagnosis.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/network-connectivity-diagnosis.mdx diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/features.mdx deleted file mode 100644 index 759459259..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/features.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Data Storage Overview -sidebar_label: Overview -sidebar_position: 1 ---- - -With TDS’s Data Storage service, your game gains the ability to store JSON objects, binary files, and geolocations on the cloud and retrieve them at any time. The security of the data is guaranteed by the row-level permission control with ACL as well as the user- and role-based permission management system. - -## The Problem We’re Solving - -Most online applications are data-driven and share a very similar architecture: - -- The frontend presents the UI with data and handles interactions with the user while exchanging data with the app server on the backend. - -- The app server handles the main logic of the application by either generating data and storing it on the data server or gathering data from the data server and sending it to the frontend. - -- The data server stores the data and makes backups of it. - -The model mentioned above works for most of the applications we see today. Although these applications share a variety of data structures and logic, they all have data flowing behind the scenes, which drives their functions. They also share a very similar backend framework (like LAMP). However, developers have been building the same system again and again, which tremendously increased the time and cost needed before they could launch their applications. - -## Features - -You can see TDS’s Data Storage service as an object-oriented database without worrying about the size of the data you’re going to store or the number of requests users will make through your game. - -### Structured Data Storage - -You can store any kind of JSON object on the cloud and set up associations among objects. Objects can be created, read, updated, and deleted through the APIs we offer. - -### File Storage - -You can store binary files like images, documents, audios, and videos on the cloud without allocating disk spaces for them in advance. Replicas are automatically created for your files so you don’t have to worry about the security of the files. Plus, your files are served through our CDN network by default so that your users won’t have to experience a delay when accessing them. - -### Permission Control With ACL - -You can set permissions for classes, rows, and columns to maximize the security of your data. - -### Syncing Data Among Devices - -Data can be synced among multiple devices, enabling real-time collaborations for your users. - -### Data Analytics - -With the SQL interface we provide, you can easily process and analyze your data in a parallel manner. This is best for conducting data mining, OLAP, and business intelligence. - -## Our Benefits - -- The multi-region data replication strategy ensures a 99.999% reliability and enables extremely high concurrent queries for your data. - -- With a whole ecosystem of services, you can easily add community features like feeds to your game. - -- Our service has been proven to be able to handle over a billion requests every single day. You can be confident that your game will run stably with our service. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/_category_.json similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/_category_.json rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/_category_.json diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/flutter.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/flutter.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/flutter.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/flutter.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/setup-flutter.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/setup-flutter.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/setup-flutter.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/flutter-guide/setup-flutter.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/friendship.md b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/friendship.md similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/friendship.md rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/friendship.md diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/_category_.json similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/_category_.json rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/_category_.json diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/go.md b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/go.md similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/go.md rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/go.md diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/setup-go.md b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/setup-go.md similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/setup-go.md rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/go-guide/setup-go.md diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/sdk-introduction.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/sdk-introduction.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/sdk-introduction.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/sdk-introduction.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/setup-android-securely.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/setup-android-securely.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/setup-android-securely.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/setup-android-securely.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/overview.md b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/overview.md similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/overview.md rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/overview.md diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/_category_.json similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/_category_.json rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/_category_.json diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/php.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/php.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/php.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/php.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/setup-php.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/setup-php.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/setup-php.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/php-guide/setup-php.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/_category_.json similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/_category_.json rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/_category_.json diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/python.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/python.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/python.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/python.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/setup-python.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/setup-python.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/setup-python.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/py-guide/setup-python.mdx diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/rest.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/rest.mdx deleted file mode 100644 index 8644c2775..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/rest.mdx +++ /dev/null @@ -1,2293 +0,0 @@ ---- -title: Data Storage REST API -sidebar_label: REST API -slug: /sdk/storage/guide/rest/ -sidebar_position: 7 ---- - -You can access the Data Storage service from any device that can send HTTP requests. There are many things you can do with our REST API. For example: - -- You can manipulate data on the cloud with any programming language. -- If you want to migrate from TDS to other services, you can export all your data. -- Your mobile site can fetch data from the cloud with vanilla JavaScript if you regard importing the JavaScript SDK as an overkill. -- You can import new data in batches to be consumed by your app later. -- You can export recent data for offline analysis or additional incremental backup. - -## API Version - -The current API version is `1.1`. - -### Testing - -To help you easily test out our REST API, this page provides curl command examples. Those examples are targeted for users of unix-like platforms (including macOS and Linux), so you may need to modify the commands if you are using cmd.exe on Windows. -For example, `\` in curl examples means to be continued on the next line, but cmd.exe will consider it as a path separator. -To make things easier, we recommend you to use [Postman](https://www.postman.com/) for testing on Windows. -You can directly import the curl commands shown on this page into Postman. - -![Click on “Import” and paste the curl command under the “Raw Text” tab](/img/postman-import-curl.png) - -With Postman, you can also generate code for the languages and libraries of your choice for accessing our REST API. - -![Click on “Code” and select the language and library you use](/img/postman-generate-code.png) - -### Base URL - -The Base URL for the REST API (represented with `{{host}}` in curl examples) is the API domain of your app. You can manage and view it on the dashboard. - -### Objects - -| URL | HTTP Method | Functionality | -| ----------------------------------------------- | ----------- | -------------------- | -| /1.1/classes/<className> | POST | Create an object | -| /1.1/classes/<className>/<objectId> | GET | Retrieve an object | -| /1.1/classes/<className>/<objectId> | PUT | Update an object | -| /1.1/classes/<className> | GET | Query objects | -| /1.1/classes/<className>/<objectId> | DELETE | Delete an object | -| /1.1/scan/classes/<className> | GET | Iterate over objects | - -### Users - -| URL | HTTP Method | Functionality | -| ----------------------------------------------- | ----------- | -------------------------------------------------- | -| /1.1/users | POST | Register
    Connect user | -| /1.1/usersByMobilePhone | POST | Register or log in via mobile phone | -| /1.1/login | POST | Log in | -| /1.1/users/<objectId> | GET | Retrieve a user | -| /1.1/users/me | GET | Retrieve a user with session token | -| /1.1/users/<objectId>/refreshSessionToken | PUT | Reset session token | -| /1.1/users/<objectId>/updatePassword | PUT | Reset password with old password | -| /1.1/users/<objectId> | PUT | Update user info
    Connect user
    Verify email | -| /1.1/users | GET | Query users | -| /1.1/users/<objectId> | DELETE | Delete a user | -| /1.1/requestPasswordReset | POST | Request to reset password with email | -| /1.1/requestEmailVerify | POST | Request to verify email | - -### Roles - -| URL | HTTP Method | Functionality | -| --------------------------- | ----------- | --------------- | -| /1.1/roles | POST | Create a role | -| /1.1/roles/<objectId> | GET | Retrieve a role | -| /1.1/roles/<objectId> | PUT | Update a role | -| /1.1/roles | GET | Query roles | -| /1.1/roles/<objectId> | DELETE | Delete a role | - -### Data Schema - -| URL | HTTP Method | Functionality | -| ------------------------------ | ----------- | ------------------------------- | -| /1.1/schemas | GET | Retrieve schemas of all classes | -| /1.1/schemas/<className> | GET | Retrieve the schema of a class | - -### Others - -| URL | HTTP Method | Functionality | -| -------------------------- | ----------- | --------------------------------------------------- | -| /1.1/date | GET | Retrieve server date and time | -| /1.1/exportData | POST | Request to export all data from the app | -| /1.1/exportData/<id> | GET | Retrieve the status and result of a data export job | - -### Request Format - -For POST and PUT requests, the request body must be in JSON, and the `Content-Type` HTTP header should be `application/json` accordingly. - -The `X-LC-Id` and `X-LC-Key` headers are used for authentication: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "The content of this blog post"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -`X-LC-Id` is the `App ID` and `X-LC-Key` is the `App Key` or `Master Key`. -A `,master` postfix is used to indicate that the value of `X-LC-Key` is a `Master Key`. For example: - -``` -X-LC-Key: {{masterkey}},master -``` - -Cross-origin resource sharing is supported so that you can use these headers with `XMLHttpRequest` in JavaScript. - -You can also use the `Accept-Encoding` header to enable compression with `gzip` or `brotli`. - -#### X-LC-Sign - -You may also authenticate requests with `X-LC-Sign` instead of `X-LC-Key` to minimize the risk of leaking the `App Key`: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" \ - -H "Content-Type: application/json" \ - -d '{"content": "Updating a post with the X-LC-Sign header"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -The value of `X-LC-Sign` is a string in the form of `sign,timestamp[,master]`: - -| Name | Optionality | Description | -| --------- | ----------- | ------------------------------------------------------------------------------------ | -| sign | Required | Concat timestamp and `App Key` (or `Master Key`), then calculate its MD5 hash value. | -| timestamp | Required | The unix timestamp of the current request, accurate to **milliseconds**. | -| master | Optional | Use this postfix to indicate that the Master Key is used. | - -Please make sure the letters in the MD5 hash value in the `sign` portion are in lowercase. - -For example, given the following application: - - - - - - - - - - - - - - - - - - - - - - - - -
    App Id - FFnN2hso42Wego3pWq4X5qlu -
    App Key - UtOCzqb67d3sN12Kts4URwy8 -
    Master Key - DyJegPlemooo4X1tg94gQkw1 -
    Request time2016-01-17 15:15:43.466 GMT+08:00
    timestamp - 1453014943466 -
    - -**To calculate the sign with `App Key`**: - -``` -md5( timestamp + App Key ) -= md5(1453014943466UtOCzqb67d3sN12Kts4URwy8) -= d5bcbb897e19b2f6633c716dfdfaf9be -``` - -```sh - -H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" \ -``` - -**To calculate the sign with `Master Key`**: - -``` -md5( timestamp + Master Key ) -= md5(1453014943466DyJegPlemooo4X1tg94gQkw1) -= e074720658078c898aa0d4b1b82bdf4b -``` - -```sh - -H "X-LC-Sign: e074720658078c898aa0d4b1b82bdf4b,1453014943466,master" \ -``` - -Here `,master` is added to the end of the string to tell the server that the signature is generated with the Master Key. - -**Using master key will bypass all permission validations. Make sure you do not leak the master key and only use it in restrained environments.** - -#### Specifying Hook Invocation Environment - -For requests that may trigger hooks on Cloud Engine, use the `X-LC-Prod` HTTP header to specify the invocation environment: - -- `X-LC-Prod: 0` means to use the staging environment -- `X-LC-Prod: 1` means to use the production environment - -If you do not specify the `X-LC-Prod` HTTP header, the hook in the production environment will be invoked. - -### Response Format - -For all the requests made to the REST API, The response body is always a JSON object. - -An HTTP status code is used to indicate whether a request succeeded or failed. -A 2xx status code indicates success, and a 4xx/5xx status code indicates the occurrence of an error. -When an error occurs, the response body will be a JSON object with two fields, `code` and `error`, -where `code` is the error code (integer) and `error` is a brief error message (string). -The `code` may be identical to the HTTP status code, -but oftentimes it is a customized error code more specific than the HTTP status code. -For example, if you try to save an object with an invalid key name, you will see: - -```json -{ - "code": 105, - "error": "Invalid key name. Keys are case-sensitive and 'a-zA-Z0-9_' are the only valid characters. The column is: 'invalid?'." -} -``` - -## Objects - -### Object Format - -The Data Storage service is built around objects. -Each object consists of several key-value pairs, -where values are in JSON-compatible formats. -Objects are schemaless, so you do not need to allocate keys in advance. -You only need to set key-value pairs as you wish and when needed. - -For example, if you are implementing a Twitter-like social app, you may give the following attributes (key-value pairs) to a post: - -```json -{ - "content": "Discover Superb Games.", - "pubUser": "TapTap", - "pubTimestamp": 1435541999 -} -``` - -Keys can only contain letters, numbers, and underscores. -Values can be anything encoded in JSON. - -Each object belongs to a class (table in traditional database terms). -We recommend using `CapitalizedWords` to name your classes, and `mixedCases` to name your attributes. -This naming style helps to improve the readability of your code. - -Each time when an object is saved to the cloud, a unique `objectId` will be assigned to it. -`createdAt` and `updatedAt` will also be filled in by the cloud, which indicate the time the object is created and updated. -These attributes are reserved and you cannot modify them yourself. -For example, the object above could look like this when retrieved: - -```json -{ - "content": "Discover Superb Games.", - "pubUser": "TapTap", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -`createdAt` and `updatedAt` are strings whose content is a UTC timestamp in ISO 8601 format with millisecond precision: `YYYY-MM-DDTHH:MM:SS.MMMZ`. -`objectId` is a string unique in the class, like the primary key of a relational database. - -In our REST API, class-level operations use the class name as its endpoint. -For example, the URL for operations on a class named `Post` will be: - -``` -https://{{host}}/1.1/classes/Post -``` - -There is also a special URL for **users**: - -``` -https://{{host}}/1.1/users -``` - -Object-specific operations use nested URLs under the class. -For example, the URL for operations on an object in the `Post` class with `558e20cbe4b060308e3eb36c` as its `objectId` will be: - -``` -https://{{host}}/1.1/classes/Post/558e20cbe4b060308e3eb36c -``` - -### Creating Objects - -To create a new object, send a **POST** request containing the object itself to the URL for the class. -For example, to create the object we mentioned earlier: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "Discover Superb Games.","pubUser": "TapTap","pubTimestamp": 1435541999}' \ - https://{{host}}/1.1/classes/Post -``` - -If succeeded, you will receive `201 Created` with a `Location` header point to the URL of the object just created: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/classes/Post/558e20cbe4b060308e3eb36c -``` - -And the response body is a JSON object with `objectId` and `createdAt` key-value pairs: - -```json -{ - "createdAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -To tell the cloud to return the full data of the new object, set the `fetchWhenSave` parameter to `true`: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "Discover Superb Games.","pubUser": "TapTap","pubTimestamp": 1435541999}' \ - https://{{host}}/1.1/classes/Post?fetchWhenSave=true -``` - -Class names can only contain letters, numbers, and underscores. -**Every application can contain up to 500 classes, and each class can contain up to 300 fields.** There is no limit on the number of objects in each class. - -### Retrieving Objects - -After you create an object, you can send a GET request to the `Location` of the response to fetch the object. For example, to fetch the object we just created: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/classes/Post/558e20cbe4b060308e3eb36c -``` - -The response body is a JSON object containing all the attributes you gave to the object, as well as the three preserved attributes (`objectId`, `createdAt`, and `updatedAt`): - -```json -{ - "content": "Discover Superb Games.", - "pubUser": "TapTap", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -If the object contains pointers to other objects, you can add an `include` parameter to indicate that you wish to retrieve these objects as well. For example, if a post has an `author` field indicating the person who posted it, you can retrieve the post together with its author in this way: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'include=author' \ - https://{{host}}/1.1/classes/Post/ -``` - -You can use a dot (`.`) in the value of the `include` parameter to further include the object pointed by a pointed object. For example, assuming that there is a `department` field for each author, you can use `include=author.department` to retrieve the information of the department. - -If the class does not exist, you will receive a `404 Not Found` error: - -```json -{ - "code": 101, - "error": "Class or object doesn't exists." -} -``` - -If the server cannot find the object according to the `objectId` you specified, you will receive an empty object (with HTTP status code being `200 OK`): - -```json -{} -``` - -One exception is that for the built-in classes (those with a name starting with a leading underscore), you may get a different result when trying to retrieve an object with an invalid `objectId`. -For example, when retrieving a `_User` object with an `objectId` that does not exist, you will get a `400 Bad Request` error. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/classes/_User/ -``` - -The request above will lead to the following response: - -```json -{ - "code": 211, - "error": "Could not find user." -} -``` - -We recommend using `GET /users/` to fetch user information instead of directly querying the `_User` class. -See also [Retrieving Users](#retrieving-users). - -### Updating Objects - -To update an object, you can send a PUT request to the object URL. -The server will only update the attributes you explicitly specified in the request (except for `updatedAt`). -For example, to only update the `content` of a post: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "Discover Superb Games. https://www.taptap.io/"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -If the update succeeds, a JSON object containing an `updatedAt` property will be returned: - -```json -{ - "updatedAt": "2015-06-30T18:02:52.248Z" -} -``` - -The `fetchWhenSave` parameter can also be used when updating an object. -Keep in mind that you will only get the updated fields instead of all the fields of the object. - -#### Counter - -For an existing post in our app, we may want to keep track of how many people liked it. However, since a lot of likes could happen at the same time, if we have the client retrieve the value of the number of likes, update it, and store the new value back to the cloud, there will likely be conflicts that cause the number stored on the cloud to be inaccurate. -To solve the problem, you can use the `Increment` atomic operator to increase a counter-like attribute: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"upvotes":{"__op":"Increment","amount":1}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -The command above adds 1 to the `upvotes` property of the object. You can specify how much you want to increment with the `amount` parameter. The number can be negative to indicate a subtraction from the original value. - -There is also a `Decrement` operator. -`Decrement`ing a positive number is equivalent to `Increment`ing a negative number. - -#### Bitwise Operators - -There are three bitwise operators for integers: - -- `BitAnd`: Bitwise AND -- `BitOr`: Bitwise OR -- `BitXor`: Bitwise XOR - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"flags":{"__op":"BitOr","value": 0x0000000000000004}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -#### Arrays - -There are three atomic operators for arrays: - -- `Add` extends an array attribute by appending elements to the given array. -- `AddUnique` is similar to `Add`, but only appends elements not already contained in the array attribute. -- `Remove` removes all occurrences of elements specified in the given array. - -The given array mentioned above is passed in as the value of the `objects` key. - -For example, to add some tags to the post: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"tags":{"__op":"AddUnique","objects":["Frontend","JavaScript"]}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -#### Conditional Updates - -Suppose we are going to deduct some money from an `Account`, and we want to make sure this deduction will not result in a negative balance. -We can use conditional updates by adding a `where` parameter with the condition `balance >= amount`: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"balance":{"__op":"Decrement","amount": 30}}' \ - "https://{{host}}/1.1/classes/Account/558e20cbe4b060308e3eb36c?where=%7B%22balance%22%3A%7B%22%24gte%22%3A%2030%7D%7D" -``` - -Here `%7B%22balance%22%3A%7B%22%24gte%22%3A%2030%7D%7D` is the URL-encoded condition `{"balance":{"$gte": 30}}`. - -If the condition is not met, the update will not be performed, and you will receive a `305` error: - -```json -{ - "code": 305, - "error": "No effect on updating/deleting a document." -} -``` - -**Note: `where` must be passed in as a query parameter of the URL.** - -#### A List of \_\_op Operations - -You can perform atomic operations with the `__op("Method", {JSON parameters})` function. - -| Operation | Description | Example | -| -------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -| Delete | Delete a property of the object | `__op('Delete', {'delete': true})` | -| Add | Add objects to the end of an array | `__op('Add',{'objects':['Apple','Google']})` | -| AddUnique | Add each of the objects to the end of an array only if the object does not exist in the array | `__op('AddUnique', {'objects':['Apple','Google']})` | -| Remove | Delete objects from the array | `__op('Remove',{'objects':['Apple','Google']})` | -| AddRelation | Add a relation | `__op('AddRelation', {'objects':[pointer('_User','558e20cbe4b060308e3eb36c')]})` | -| RemoveRelation | Remove a relation | `__op('RemoveRelation', {'objects':[pointer('_User','558e20cbe4b060308e3eb36c')]})` | -| Increment | Increment | `__op('Increment', {'amount': 50})` | -| Decrement | Decrement | `__op('Decrement', {'amount': 50})` | -| BitAnd | Bitwise AND | `__op('BitAnd', {'value': 0x0000000000000004})` | -| BitOr | Bitwise OR | `__op('BitOr', {'value': 0x0000000000000004})` | -| BitXor | Bitwise XOR | `__op('BitXor', {'value': 0x0000000000000004})` | - -### Deleting Objects - -To delete an object, send a `DELETE` request to the URL for the object: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/classes/Post/ -``` - -To delete an attribute from an object, send a `PUT` request with the `Delete` operator: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"downvotes":{"__op":"Delete"}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -#### Conditional Deletions - -Similar to conditional updates, we pass a URL-encoded `where` parameter to the `DELETE` request to conditionally delete the object. For example, to delete a post if its `clicks` equals to `0`: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - "https://{{host}}/1.1/classes/Post/?where=%7B%22clicks%22%3A%200%7D" -``` - -Here `%7B%22clicks%22%3A%200%7D` is the URL-encoded value for `{"clicks": 0}`. - -Again, if the condition is not met, the update will not be performed, and you will receive a `305` error: - -```json -{ - "code": 305, - "error": "No effect on updating/deleting a document." -} -``` - -**Keep in mind that `where` must be a query parameter in the URL.** - -### Iterating Over Objects - -For classes with a moderate number of objects, we can iterate over all the objects in the class by constructing queries with `skip`, `limit`, and `order`. -However, for classes with a large number of objects, there will be a performance issue if we keep using `skip`. -To avoid this problem, we can do pagination by specifying the scope of `createdAt` or `updatedAt`. -There is a `scan` endpoint that is dedicated for this purpose: it can be used to iterate over the objects in a class ordered by a given field. Using `scan` makes things easier compared to constructing queries manually by specifying the scopes of `createdAt` or `updatedAt`. -By default, `scan` returns 100 results in ascending order by `objectId`. You can ask the cloud to return up to 1000 results by specifying the `limit` parameter: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - https://{{host}}/1.1/scan/classes/Article -``` - -Be sure to use the `MasterKey` when using `scan`. - -The cloud will return a `results` array and a `cursor`. - -```json -{ - "results": [ - { - "tags": ["clojure", "\u7b97\u6cd5"], - "createdAt": "2016-07-07T08:54:13.250Z", - "updatedAt": "2016-07-07T08:54:50.268Z", - "title": "clojure persistent vector", - "objectId": "577e18b50a2b580057469a5e" - } - //... - ], - "cursor": "pQRhIrac3AEpLzCA" -} -``` - -The `cursor` will be `null` if there are no more results. -If `cursor` is not `null`, you can use `scan` again with the value of `cursor` to continue the iteration: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'cursor=pQRhIrac3AEpLzCA' \ - https://{{host}}/1.1/scan/classes/Article -``` - -Each `cursor` must be consumed within 10 minutes. -It becomes invalid after 10 minutes. - -You can also specify `where` conditions for filtering: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'where={"score": 100}' \ - https://{{host}}/1.1/scan/classes/Article -``` - -As mentioned above, by default the results are in ascending order by `objectId`. -To return results ordered by another attribute, -pass that attribute as the `scan_key` parameter: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'scan_key=score' \ - https://{{host}}/1.1/scan/classes/Article -``` - -To return results in descending order, prefix a minus sign (`-`) to the value of the `scan_key`, e.g., `-score`. - -**The value of the `scan_key` passed must be strictly monotonous, and it cannot be used in `where` conditions.** - -**You cannot set the `include` parameter when using `scan`.** -If you wish to use `include` when iterating over the objects in a class, please use a basic query with setting the scopes of `createdAt` and `updatedAt` instead. - -### Batch Operations - -To reduce the number of requests you make, you can wrap create, update, and delete operations on multiple objects in one request. - -You can assign each operation its own method, path, and body, which replaces the HTTP requests you would ordinarily make. The operations will be executed according to the order they are sent to the server. For example, to make a series of posts at once: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "requests": [ - { - "method": "POST", - "path": "/1.1/classes/Post", - "body": { - "content": "Post 1", - "pubUser": "TapTap" - } - }, - { - "method": "POST", - "path": "/1.1/classes/Post", - "body": { - "content": "Post 2", - "pubUser": "TapTap" - } - } - ] - }' \ - https://{{host}}/1.1/batch -``` - -Currently, there is no limit on the number of operations in each request, but there is a 20 MB size limit on the request body for all API requests. -It is recommended that you load at most 100 operations into each request. - -The response body will be an array with the length and order of the members corresponding to those of the operations in the request. -Each member will be a JSON object with one and only one key, and that key will be either `success` or `error`. -The value of `success` or `error` will be the response to the corresponding single request on success or failure respectively. - -```json -[ - { - "error": { - "code": 1, - "error": "Could not find object by id '558e20cbe4b060308e3eb36c' for class 'Post'." - } - }, - { - "success": { - "updatedAt": "2017-02-22T06:35:29.419Z", - "objectId": "58ad2e850ce463006b217888" - } - } -] -``` - -Be aware that the HTTP status `200` returned by a batch request only means the cloud had received and performed the operations. -It does not mean that all the operations within the batch request succeeded. - -Besides the `POST` requests in the above example, -you can also wrap `PUT` and `DELETE` requests in a batch request: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "requests": [ - { - "method": "PUT", - "path": "/1.1/classes/Post/55a39634e4b0ed48f0c1845b", - "body": { - "upvotes": 2 - } - }, - { - "method": "DELETE", - "path": "/1.1/classes/Post/55a39634e4b0ed48f0c1845c" - } - ] - }' \ - https://{{host}}/1.1/batch -``` - -Batch requests can also be used to replace requests with very long URLs (usually constructed with very complex queries or conditions) to bypass the limit on URL length enforced by the server side or the client side. - -### Advanced Data Types - -Besides standard JSON values, we also support advanced data types like Date, Byte, and Pointer. -These advanced data types are encoded as a JSON object with a `__type` key. - -**Date** contains an `iso` key, whose value is a UTC timestamp string in ISO 8601 format with millisecond precision: `YYYY-MM-DDTHH:MM:SS.MMMZ`. - -```json -{ - "__type": "Date", - "iso": "2015-06-21T18:02:52.249Z" -} -``` - -The **Date** type can be useful when you perform queries on the built-in `createdAt` and `updatedAt` fields. For example, to query all the posts made on a specific time: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"createdAt":{"$gte":{"__type":"Date","iso":"2015-06-21T18:02:52.249Z"}}}' \ - https://{{host}}/1.1/classes/Post -``` - -Keep in mind that since `createdAt` and `updatedAt` are built-in fields, when their values are appearing in the response of a request, they will be in UTC timestamps rather than encoded Dates. - -**Byte** contains a `base64` key, whose value is a MIME base64 string (with no whitespace characters). - -```json -{ - "__type": "Bytes", - "base64": "5b6I5aSa55So5oi36KGo56S65b6I5Zac5qyi5oiR5Lus55qE5paH5qGj6aOO5qC877yM5oiR5Lus5bey5bCGIExlYW5DbG91ZCDmiYDmnInmlofmoaPnmoQgTWFya2Rvd24g5qC85byP55qE5rqQ56CB5byA5pS+5Ye65p2l44CC" -} -``` - -**Pointer** contains a `className` key and an `objectId` key, whose values are the corresponding class name and objectId of the pointed value. - -```json -{ - "__type": "Pointer", - "className": "Post", - "objectId": "55a39634e4b0ed48f0c1845c" -} -``` - -A pointer to a user contains a `className` of `_User`. -The leading underscore indicates that `_User` is a built-in class. -Similarly, pointers to roles and installations contain a `className` of `_Role` or `_Installation` respectively. -However, a pointer to a file is a special case: - -```json -{ - "id": "543cbaede4b07db196f50f3c", - "__type": "File" -} -``` - -**GeoPoint** contains `latitude` and `longitude` of the location: - -```json -{ - "__type": "GeoPoint", - "latitude": 39.9, - "longitude": 116.4 -} -``` - -We may add more advanced data types in the future, so you should not use `__type` as the key of your own JSON objects. - -## Queries - -### Basic Queries - -To list objects in a class, just send a GET request to the class URL. For example, to get all the posts: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/classes/Post -``` - -The response body is a JSON object containing a `results` key, -whose value is an array of objects: - -```json -{ - "results": [ - { - "content": "Post 1", - "pubUser": "TapTap", - "upvotes": 2, - "createdAt": "2015-06-29T03:43:35.931Z", - "objectId": "55a39634e4b0ed48f0c1845b" - }, - { - "content": "Post 2", - "pubUser": "TapTap", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" - } - ] -} -``` - -The values of `createdAt` and `updatedAt` you see on the dashboard are in the timezone of your local computer, and it is the same for the values obtained through our SDKs. However, when using the REST API, you will get those values in UTC. You can convert them to local times yourself if you need them. - -### Query Constraints - -The `where` parameter can be used to apply query constraints. -It should be encoded as JSON first, then URL encoded. - -The simplest form of `where` parameter is a key-value pair (exact match). -For example, to query posts published by `TapTap`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"pubUser":"TapTap"}' \ - https://{{host}}/1.1/classes/Post -``` - -Other operators available for the `where` parameter: - -| Operator | Description | -| ------------- | ------------------------------------- | -| `$ne` | Not equal to | -| `$lt` | Less than | -| `$lte` | Less than or equal to | -| `$gt` | Greater than | -| `$gte` | Greater than or equal to | -| `$regex` | Match a regular expression | -| `$in` | Contain | -| `$nin` | Not contain | -| `$all` | Contain all (for array type) | -| `$exists` | The given key exists | -| `$select` | Match the result of another query | -| `$dontSelect` | Not match the result of another query | - -For example, to query all the posts published on **2015-06-29**: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"createdAt":{"$gte":{"__type":"Date","iso":"2015-06-29T00:00:00.000Z"},"$lt":{"__type":"Date","iso":"2015-06-30T00:00:00.000Z"}}}' \ - https://{{host}}/1.1/classes/Post -``` - -To query all the posts whose number of votes is an odd number less than 10: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"upvotes":{"$in":[1,3,5,7,9]}}' \ - https://{{host}}/1.1/classes/Post -``` - -To query all the posts _not_ published by TapTap: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"pubUser":{"$nin":["TapTap"]}}' \ - https://{{host}}/1.1/classes/Post -``` - -To query all the posts that have been voted by someone: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"upvotes":{"$exists":true}}' \ - https://{{host}}/1.1/classes/Post -``` - -To query all the posts that have not been voted: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"upvotes":{"$exists":false}}' \ - https://{{host}}/1.1/classes/Post -``` - -Suppose we use `_Followee` and `_Follower` classes for the following relationship, then we can query posts published by someone followed by the current user like this: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={ - "author": { - "$select": { - "query": { - "className":"_Followee", - "where": { - "user":{ - "__type": "Pointer", - "className": "_User", - "objectId": "55a39634e4b0ed48f0c1845c" - } - } - }, - "key":"followee" - } - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -The `order` parameter can be used to specify how the returned objects shuold be sorted. For example, to query posts and sort them in ascending order by creation time: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=createdAt' \ - https://{{host}}/1.1/classes/Post -``` - -To sort them in descending order: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=-createdAt' \ - https://{{host}}/1.1/classes/Post -``` - -To sort results by multiple keys, use a comma to separate the keys. For example, to sort posts in ascending order by `createdAt` and descending order by `pubUser`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=createdAt,-pubUser' \ - https://{{host}}/1.1/classes/Post -``` - -You can implement paginiation with `limit` and `skip`. `limit` defaults to 100 but you can set it to any integer between 1 and 1000. If you give it a value out of this range, the default value (100) will be used. For example, to get the first 200 posts after the 400th: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'limit=200' \ - --data-urlencode 'skip=400' \ - https://{{host}}/1.1/classes/Post -``` - -You can limit the fields being returned from the server by providing a `keys` parameter in your request. For example, to only include `pubUser` and `content` in the response (together with the built-in fields `objectId`, `createdAt`, and `updatedAt`): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'keys=pubUser,content' \ - https://{{host}}/1.1/classes/Post -``` - -You can further limit the returned values to the properties of a given field. For example, to only get the family names of the authors, you can write `keys=pubUser.familyName`. - -You can also specify which fields you want to exclude from the response by adding a minus sign before the field names. For example, to exclude the `author` field: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'keys=-author' \ - https://{{host}}/1.1/classes/Post -``` - -The inverted selection applies to preserved attributes as well. For example, you can write `keys=-createdAt,-updatedAt,-objectId`. -You can also use it with dot notation, e.g., `keys=-pubUser.createdAt,-pubUser.updatedAt`. - -### Including ACL in the Response - -By default, the response will not contain the ACL field. -The ACL field will only be included if you enabled **Include ACL with objects being queried** under **Developer Center > Your Game > Game Services > Cloud Services > Data Storage > Settings > Queries** and you included `returnACL=true` in the request. - -All the parameters mentioned above can be combined. - -### Regex Queries - -To query posts whose title begins with `WTO`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"title":{"$regex":"^WTO.*","$options":"i"}}' \ - https://{{host}}/1.1/classes/Post -``` - -We will use the following dataset to demonstrate how you can use `$options` to match different values of **title**: - -``` -{ "_id" : 100, "title" : "Single line description." }, -{ "_id" : 101, "title" : "First line\nSecond line" }, -{ "_id" : 102, "title" : "Many spaces before line" }, -{ "_id" : 103, "title" : "Multiple\nline description" }, -{ "_id" : 104, "title" : "abc123" } -``` - -| Option | Description | Example | -| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `i` | **Case-insensitive search** | `{"$regex":"single", "$options":"i"}` will match

    `{ "_id" : 100, "title" : "Single line description." }`
    | -| `m` | **Multi-line search**
    Can be used for strings containing `\n` | `{"$regex":"^S", "$options":"m"}` (starts with capital “S”) will match

    `{ "_id" : 100, "title" : "Single line description." },`
    `{ "_id" : 101, "title" : "First line\nSecond line" }`
    | -| `x` | **Free-spacing and line comments**
    Includes spaces, tabs, `\n`, and comments starting with `#`,
    but does not include vertical tabs (ASCII: 11). | `{"$regex":"abc #category code\n123 #item number", "$options":"x"}` (comments after #) will match

    `{ "_id" : 104, "title" : "abc123" }`
    | -| `s` | **Allow `.` to match newline characters** | `{"$regex":"m.*line", "$options":"si"}` will match

    `{ "_id" : 102, "title" : "Many spaces before     line" },`
    `{ "_id" : 103, "title" : "Multiple\nline description" }`
    | - -The options above can be combined, for example `"$options":"sixm"`. - -### Array Queries - -With `key` being an array field, to query all the objects with `key` containing `2`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":2}' \ - https://{{host}}/1.1/classes/TestObject -``` - -To query all the objects with `key` containing `2`, `3`, or `4`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":{"$in":[2,3,4]}}' \ - https://{{host}}/1.1/classes/TestObject -``` - -Using `$all` to query all the objects with `key` containing `2`, `3`, **and** `4`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":{"$all":[2,3,4]}}' \ - https://{{host}}/1.1/classes/TestObject -``` - -Using `$size` to query all the objects with `key` containing exactly 3 objects: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":{"$size": 3}}' \ - https://{{host}}/1.1/classes/TestObject -``` - -### Pointer Queries - -There are a couple of ways you can use to query relations between objects. If you want to query objects that have a field pointing to a specific object, you can construct a Pointer and pass it to the `where` parameter. Assuming there is a `Comment` class with a `post` field pointing to the `Post` class, you can query all the comments under a post with this command: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"post":{"__type":"Pointer","className":"Post","objectId":"558e20cbe4b060308e3eb36c"}}' \ - https://{{host}}/1.1/classes/Comment -``` - -To query objects with pointers according to another query on the pointed object, you can use the `$inQuery` operator. Keep in mind that the default value of `limit` is 100 and the maximum value of it is 1000, and this restriction applies to inner queries as well. You may need to carefully construct queries to get your expected result. - -For example, assuming each post has an `image` field, to query all the comments on posts with attached images: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"post":{"$inQuery":{"where":{"image":{"$exists":true}},"className":"Post"}}}' \ - https://{{host}}/1.1/classes/Comment -``` - -To include pointed objects in one query, use the `include` parameter. -For example, to query the most recent 10 comments with the posts commented on: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=-createdAt' \ - --data-urlencode 'limit=10' \ - --data-urlencode 'include=post' \ - https://{{host}}/1.1/classes/Comment -``` - -Without the `include` parameter, the post attribute of the returned comments will look like this: - -```json -{ - "__type": "Pointer", - "className": "Post", - "objectId": "51e3a359e4b015ead4d95ddc" -} -``` - -With the `include=post` parameter, the post attribute will be dereferenced: - -```json -{ - "__type": "Object", - "className": "Post", - "objectId": "51e3a359e4b015ead4d95ddc", - "createdAt": "2015-06-29T09:31:20.371Z", - "updatedAt": "2015-06-29T09:31:20.371Z", - "desc": "this is a post" -} -``` - -You can use dots (`.`) for multi-level dereference. For example, to get the `author`s of the posts pointed by comments: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=-createdAt' \ - --data-urlencode 'limit=10' \ - --data-urlencode 'include=post.author' \ - https://{{host}}/1.1/classes/Comment -``` - -And you can use comma (`,`) to separate multiple pointers to `include`. - -### GeoPoint Queries - -Early on we have briefly described GeoPoint. - -Assuming we are including the location information of each post in the `location` field, you can use the `$nearSphere` operator to query nearby objects. For example, to retrieve 10 posts whose locations are the closest to the current location: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'where={ - "location": { - "$nearSphere": { - "__type": "GeoPoint", - "latitude": 39.9, - "longitude": 116.4 - } - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -The returned results will be ordered by distance, with the first result being the post published at the nearest location. -This order can be overridden by the `order` parameter. - -To limit the maximum distance, you can use `$maxDistanceInMiles`, `$maxDistanceInKilometers`, or `$maxDistanceInRadians`. For example, to limit the distance to 10 miles: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={ - "location": { - "$nearSphere": { - "__type": "GeoPoint", - "latitude": 39.9, - "longitude": 116.4 - }, - "$maxDistanceInMiles": 10.0 - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -You can also query for objects within a rectangular area with this format: `{"$within": {"$box": [southwestGeoPoint, northeastGeoPoint]}}`. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={ - "location": { - "$within": { - "$box": [ - { - "__type": "GeoPoint", - "latitude": 39.97, - "longitude": 116.33 - }, - { - "__type": "GeoPoint", - "latitude": 39.99, - "longitude": 116.37 - } - ] - } - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -Be aware that the range of `latitude` is `[-90.0, 90.0]`, and the range of `longitude` is `[-180.0, 180.0]`. -There is currently one limit on GeoPoints: every class can only contain one GeoPoint attribute. - -### File Queries - -Querying files is similar to querying normal objects. -For example, to query all files (just like querying normal objects, it returns at most 100 results by default): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/classes/files -``` - -Be aware that the `url`s of the internal files (those uploaded to the cloud) are automatically generated by the cloud and are applied with the logic related to updating custom domains. -This means that you should only query external files (those saved with URLs) with the `url` field. For internal files, please query with the `key` field (the path in the URL) instead. - -For the same reason, when iterating over files with `scan`, the internal files in the result will not have the `url` field but only the `key` field. - -### Counting Results - -You can pass `count=1` parameter to retrieve the count of matched results. -For example, if you just need to know how many posts a specific user has made: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"pubUser":"TapTap"}' \ - --data-urlencode 'count=1' \ - --data-urlencode 'limit=0' \ - https://{{host}}/1.1/classes/Post -``` - -Since `limit=0`, only the `count` will be returned, and the `results` array will be empty. - -```json -{ - "results": [], - "count": 7 -} -``` - -Given a nonzero `limit` parameter, results will be returned together with the count. - -### Compound Queries - -You can use the `$or` operator to query objects matching **any one of the several queries**. For example, to query posts made by official accounts and personal accounts: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"$or":[{"pubUserCertificate":{"$gt":2}},{"pubUserCertificate":{"$lt":3}}]}' \ - https://{{host}}/1.1/classes/Post -``` - -Similarly, you can use `$and` operator to query objects **matching all subqueries**. For example, to query all the objects that have the `price` field and with `price` not equaling to `199`: - -``` -where={"$and":[{"price": {"$ne":199}},{"price":{"$exists":true}}]} -``` - -The query condition expressions are implicitly combined with the `$and` operator, so the query expression above could also be rewritten as: - -``` -where=[{"price": {"$ne":199}},{"price":{"$exists":true}}] -``` - -In fact, since both conditions are targeted at the same field (`price`), the above query expression can be further simplified to: - -``` -where={"price": {"$ne":199, "$exists":true}} -``` - -However, to combine two or more OR-ed queries, you have to use the `$and` operator: - -``` -where={"$and":[{"$or":[{"pubUserCertificate":{"$gt":2}},{"pubUserCertificate":{"$lt":3}}]},{"$or":[{"pubUser":"TapTap"},{"pubUser":"TDS"}]}]} -``` - -Be aware that **non-filtering constraints such as `limit`, `skip`, `order`, and `include` are not allowed in subqueries of a compound query**. - -## Users - -With the users API, you can build an account system for your application quickly and conveniently. - -Users (the `_User` class) share many traits with other classes. For example, `_User` is schema-free as well. -However, all user objects must have `username` and `password` attributes. `password` will be encrypted automatically. -`username` and `email` (if available) attributes must be unique (case sensitive). - -### Signing Up - -To create a new user: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"username":"tom","password":"f32@ds*@&dsa","phone":"18612340000"}' \ - https://{{host}}/1.1/users -``` - -As mentioned above, `username` and `password` are required. `password` will be stored in encrypted form, and the cloud will never return its value to the client side. - -If the registration succeeds, the cloud will return `201 Created` and the `Location` will contain the URL for that user: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -The response body is a JSON object containing three attributes: - -```json -{ - "sessionToken": "qmdj8pdidnmyzp0c7yqil91oc", - "createdAt": "2015-07-14T02:31:50.100Z", - "objectId": "55a47496e4b05001a7732c5f" -} -``` - -### Logging In - -To log in with username and password: - -```sh -curl -X POST \ --H "Content-Type: application/json" \ --H "X-LC-Id: {{appid}}" \ --H "X-LC-Key: {{appkey}}" \ --d '{"username":"tom","password":"f32@ds*@&dsa"}' \ -https://{{host}}/1.1/login -``` - -You can also log in with email and password by replacing the body with: - -```json -{ "email": "tom@example.com", "password": "f32@ds*@&dsa" } -``` - -Or, log in with phone number and password: - -```json -{ "mobilePhoneNumber": "+86186xxxxxxxx", "password": "f32@ds*@&dsa" } -``` - -The response body is a JSON object containing all the attributes of that user, except `password`: - -```json -{ - "sessionToken": "qmdj8pdidnmyzp0c7yqil91oc", - "updatedAt": "2015-07-14T02:31:50.100Z", - "phone": "18612340000", - "objectId": "55a47496e4b05001a7732c5f", - "username": "tom", - "createdAt": "2015-07-14T02:31:50.100Z", - "emailVerified": false, - "mobilePhoneVerified": false -} -``` - -### Refresh sessionToken - -To refresh a user's `sessionToken`: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/users/57e3bcca67f35600577c3063/refreshSessionToken -``` - -`X-LC-Session` can be omitted when using Master Key. - -If succeeded, a new `sessionToken` will be returned, with user information: - -```json -{ - "sessionToken": "5frlikqlwzx1nh3wzsdtfr4q7", - "updatedAt": "2016-10-20T03:10:57.926Z", - "objectId": "57e3bcca67f35600577c3063", - "username": "tom", - "createdAt": "2016-09-22T11:13:14.842Z", - "emailVerified": false, - "mobilePhoneVerified": false -} -``` - -#### Locking Users - -Seven consecutive failed login attempts for a user within 15 minutes will trigger a lock. -Once this happens, the cloud will return the following error: - -```json -{ - "code": 219, - "error": "Tried too many times to signin." -} -``` - -The cloud will release this lock automatically in 15 minutes after the last login failure. -You cannot adjust this behavior via SDK or REST API. -During the locking period, the user is not allowed to log in, -even if they provide the correct password. -This restriction also applies to SDK and Cloud Engine. - -### Verifying Email Address - -Once a user clicked the verification link in the email, their `emailVerified` will be set to `true`. - -`emailVerified` is a Boolean with 3 statuses: - -1. `true`: the user has verified their email address via clicking the link in the verification mail. -2. `false`: when a user's `email` attribute is set or modified, the cloud will set their `emailVerified` to `false` and send a verification email to the user. After the user clicks the verification link in the email, the cloud will set `emailVerified` to `true`. -3. `null`: The user does not have an `email`, or the user object is created when the verifying new user's email address option is disabled. - -The verification link expires in one week. -To resend the verification email: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"email":"tom@example.com"}' \ - https://{{host}}/1.1/requestEmailVerify -``` - -### Resetting Password - -A user can reset their password via email: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"email":"tom@example.com"}' \ - https://{{host}}/1.1/requestPasswordReset -``` - -If succeed, the response body will be an empty JSON object: - -```json -{} -``` - -### Retrieving Users - -To retrieve a user, you can send a GET request to the user URL (as in the `Location` header returned on [successful signing up](#signing-up)). - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -Alternatively, you can retrieve a user via their `sessionToken`: - -``` -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/users/me -``` - -The returned JSON object is the same as in [`/login`](#logging-in). - -If the user does not exist, a `400 Bad Request` will be returned: - -```json -{ - "code": 211, - "error": "Could not find user." -} -``` - -### Updating Users - -Similar to [Updating Objects](#updating-objects), you can send a `PUT` request to the user URL to update a user's data. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{"phone":"18600001234"}' \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -The `X-LC-Session` HTTP header is to authenticate the modification, -whose value is the user's `sessionToken`. - -If succeed, `updatedAt` will be returned. -This is the same as [Updating Objects](#updating-objects). - -If you want to update `username`, then you have to ensure that the new value of `username` must not conflict with other existing users. - -If you want to update `password` after verifying the old password, -you can use `PUT /1.1/users/:objectId/updatePassword` instead. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{"old_password":"the_old_password", "new_password":"the_new_password"}' \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f/updatePassword -``` - -Note that this API still requires the `X-LC-Session` header. - -### Querying Users - -You can query users like [how you query regular objects](#queries) by sending `GET` requests to `/1.1/users`. - -However, for security concerns, all queries on users will be rejected by the cloud unless you use the master key or have properly configured the `_User` class' ACL settings. - -### Deleting Users - -Just like deleting an object, you can send a `DELETE` request to delete a user. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -The `X-LC-Session` HTTP header is used for authenticating this request. - -### Linking Users - -To allow users to use third-party accounts to log in to your application, you can use the `authData` attribute of users. - -`authData` is a JSON object whose schema may be different for different services. - -The simplest form of `authData` is as follows: - -```json -{ - "anonymous": { - "id": "random UUID with lowercase hexadecimal digits" - // other optional keys - } -} -``` - -This is used for anonymous users, -for example, to provide a "try it before signing up" or "guest login" feature for your application. - -The `authData` for an arbitrary platform: - -```json -{ - "platform_name": { - "uid": "unique user id on that platform (string)", - "access_token": "access token for the user" - // other optional keys - } -} -``` - -`authData` can have other additional keys, but it must contain both `uid` and `access_token`. -The cloud will automatically create a unique index for `authData.platform_name.uid`. -This avoids binding a third-party account to multiple users. -However, you need to verify `authData` yourself (except for certain platforms, see below). - -Example `authData` objects: - -[Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api): - -```json -{ - "authData": { - "lc_apple": { - "uid": "user identifier", - "identity_token": "identity token", - "code": "authorization code" - } - } -} -``` - -[TapTap](https://www.taptap.io/): - -```json -{ - "taptap": { - "kid": "mac_key id", - "access_token": "Same as kid", - "token_type": "mac", - "mac_key": "mac key", - "mac_algorithm": "hmac-sha-1", - "openid": "The unique identifier of the user; a user has different openid's for different apps", - "name": "username", - "avatar": "URL of the user's avatar", - "unionid": "The unique identifier of the user; a user has the same unionid for all the apps under the same developer" - } -} -``` - -Other platforms: - -```json -{ - "platform name, like facebook": { - "uid": "A unique identifier of the user from the platform", - "access_token": "Access Token" - // ……optional properties - } -} -``` - -#### Third-Party Signing Up and Login - -To sign up or log in via a third party account, you also send a POST request with the `authData`. -For example, to log in with Apple: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "authData": { - "lc_apple": { - "uid": "user identifier", - "identity_token": "identity token", - "code": "authorization code" - } - } - }' \ - https://{{host}}/1.1/users -``` - -The response body will be a JSON object whose content is similar to the one returned when creating or logging in as a regular user. -A new user will be automatically assigned with a random username, e.g., `ec9m07bo32cko6soqtvn6bko5`. - -#### Linking a Third-Party Account - -To link a third-party account to an existing user, -just update this user's `authData` attribute. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{ - "lc_apple": { - "uid": "user identifier", - "identity_token": "identity token", - "code": "authorization code" - } - }' \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -This user can be authenticated via matching `authData` afterward. - -#### Unlinking a Third-Party Account - -Similarly, to unlink a user from a third party account, -just delete the platform in their `authData` attribute. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: 6fehqhr2t2na5mv1aq2om7jgz" \ - -H "Content-Type: application/json" \ - -d '{"authData.lc_apple":{"__op":"Delete"}}' \ - https://{{host}}/1.1/users/5b7e53a767f356005fb374f6 -``` - -## Roles - -The Data Storage service has a preserved class `_Role` for roles. - -For security concerns, roles are typically created and managed manually or via a separate management interface, not directly in your app. - -### Creating Roles - -Creating a role is similar to creating an object, except that you must specify the `name` and `ACL` attributes. To prevent allowing wrong users to modify a role accidentally, you should set a restrictive and rigid `ACL`. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Manager", - "ACL": { - "*": { - "read": true - } - } - }' \ - https://{{host}}/1.1/roles -``` - -The response is the same as creating an object. - -To create a role with existing child roles and users: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "CLevel", - "ACL": { - "*": { - "read": true - } - }, - "roles": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_Role", - "objectId": "55a48351e4b05001a774a89f" - } - ] - }, - "users": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_User", - "objectId": "55a47496e4b05001a7732c5f" - } - ] - } - }' \ - https://{{host}}/1.1/roles -``` - -You may have noticed that there is a new operator `AddRelation` we have not seen before. -This operator adds a Relation to an object. -The actual implementation of Relation is quite complicated for performance issues, -but conceptually you can consider a Relation as an array of pointers, and they are only used in roles. - -### Retrieving Roles - -Retrieving a role is similar to retrieving an object: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/roles/55a483f0e4b05001a774b837 -``` - -The response body will be a JSON object: - -```json -{ - "name": "CLevel", - "createdAt": "2015-07-14T03:37:20.992Z", - "updatedAt": "2015-07-14T03:37:20.994Z", - "objectId": "55a483f0e4b05001a774b837", - "users": { - "__type": "Relation", - "className": "_User" - }, - "roles": { - "__type": "Relation", - "className": "_Role" - } -} -``` - -### Updating Roles - -Updating roles are similar to updating objects, except that `name` cannot be modified, as mentioned above. -To add or remove users and child roles, you can use `AddRelation` and `RemoveRelation` operators. - -Suppose we have a `Manager` role with objectId `55a48351e4b05001a774a89f`, we can add a user to it as below: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "users": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_User", - "objectId": "55a4800fe4b05001a7745c41" - } - ] - } - }' \ - https://{{host}}/1.1/roles/55a48351e4b05001a774a89f -``` - -Similarly, to remove a child role: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "roles": { - "__op": "RemoveRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_Role", - "objectId": "55a483f0e4b05001a774b837" - } - ] - } - }' \ - https://{{host}}/1.1/roles/55a48351e4b05001a774a89f -``` - -### Querying Roles - -To find the roles a user belongs to: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"users": {"__type": "Pointer", "className": "_User", "objectId": "5e03100ed4b56c008e4a91dc"}}' \ - https://{{host}}/1.1/roles -``` - -To find the users contained in a role (users contained in sub-roles not counted): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode '"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"5f3dea7b7a53400006b13886"},"key":"users"}' \ - https://{{host}}/1.1/users -``` - -You can also query roles based on other attributes, just like querying a normal object. - -### Deleting Roles - -Deleting roles is similar to delete objects. -It is authenticated with the `X-LC-Session` HTTP header. -The session token passed in must belong to a user who has the permission to delete the specified role. - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/roles/55a483f0e4b05001a774b837 -``` - -### Roles and ACL - -As demonstrated above, accessing data via REST API is also restricted by ACL, just as SDKs. - -Roles make maintaining ACL easier. -For example, to set an ACL of an object with the following permissions: - -- It can be read by `Staff`s. -- It can only be written by `Manager`s and its creator. - -```json -{ - "55a4800fe4b05001a7745c41": { - "write": true - }, - "role:Staff": { - "read": true - }, - "role:Manager": { - "write": true - } -} -``` - -The creator belongs to the `Staff` role, and the `Manager` role is a child role of the `Staff` role. -Therefore, since they will inherit read permissions, we did not grant them the read permission manually. - -Let's look at another example of permission inherence among roles. -In UGC applications such as forums, `Administrators` typically have all the permissions of `Moderators`. -Thus `Administrators` should be a sub-role of `Moderators`. - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "roles": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_Role", - "objectId": "" - } - ] - } - }' \ - https://{{host}}/1.1/roles/ -``` - -## Files - -### Creating Files - -The REST API does not support uploading files. Please use an SDK or the CLI to upload files. - -If you already have a URL for a file, you can create a file by adding an entry like this: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"url": "https://example.com/foo.jpg", "name": "foo.jpg", "mime_type": "image/jpeg"}' \ - https://{{host}}/1.1/files -``` - -### Associating With Objects - -As mentioned above, files can be considered as a special form of pointers. -To associate a file object with an object, we just pass the file object `{"id": "objectId of the file", "__type": "File"}` to an attribute of that file. -For example, to create a `Staff` object with a photo: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "tom", - "picture": { - "id": "543cbaede4b07db196f50f3c", - "__type": "File" - } - }' \ - https://{{host}}/1.1/classes/Staff -``` - -Here `id` is the objectId of the file. - -### Deleting Files - -Deleting files is similar to deleting objects: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/files/543cbaede4b07db196f50f3c -``` - -## Schema - -You can use REST API to fetch the data schema of your application. -For security concerns, the master key is required to fetch data schema. - -To fetch the schema of all classes: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/schemas -``` - -Result: - -```json -{ - "_User": { - "username": { "type": "String" }, - "password": { "type": "String" }, - "objectId": { "type": "String" }, - "emailVerified": { "type": "Boolean" }, - "email": { "type": "String" }, - "createdAt": { "type": "Date" }, - "updatedAt": { "type": "Date" }, - "authData": { "type": "Object" } - } - // other classes -} -``` - -You can also fetch a single class's schema: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/schemas/_User -``` - -Data schema can be used with tools such as code generators and internal management interfaces. - -## Exporting Your Data - -For security concerns, master key is required to export your data: - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{}' \ - https://{{host}}/1.1/exportData -``` - -To specify date range (`updatedAt`) of data to export: - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_date":"2015-09-20", "to_date":"2015-09-25"}' \ - https://{{host}}/1.1/exportData -``` - -To specify classes of data to export: - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"classes":"_User,GameScore,Post"}' \ - https://{{host}}/1.1/exportData -``` - -Just export the schema (no data will be exported): - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"only-schema":"true"}' \ - https://{{host}}/1.1/exportData -``` - -The exported schema file can be imported into other applications via the import data function on the dashboard. - -After the data is exported, we will send an email to the application creator, containing the URL to download the data. -You can also specify the address to receive this email: - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"email":"username@exmaple.com"}' \ - https://{{host}}/1.1/exportData -``` - -The export data job id will be returned: - -```json -{ - "status": "running", - "id": "1wugzx81LvS5R4RHsuaeMPKlJqFMFyLwYDNcx6LvCc6MEzQ2", - "app_id": "{{appid}}" -} -``` - -You can also query the export data job status via the id returned previously: - -``` -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/exportData/1wugzx81LvS5R4RHsuaeMPKlJqFMFyLwYDNcx6LvCc6MEzQ2 -``` - -If the job status is `done`, the download url will also be returned: - -```json -{ - "status": "done", - "download_url": "https://download.leancloud.cn/export/example.tar.gz", - "id": "1wugzx81LvS5R4RHsuaeMPKlJqFMFyLwYDNcx6LvCc6MEzQ2", - "app_id": "{{appid}}" -} -``` - -If the job status is still `running`, you can query it again later. - -## Other - -### Server Time - -To retrieve the server's current time: - -``` -curl -i -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/date -``` - -The returned date is in UTC: - -```json -{ - "iso": "2015-08-27T07:38:33.643Z", - "__type": "Date" -} -``` - -## CORS Workarounds - -You can wrap GET, PUT, and DELETE requests in a POST request: - -- Specify the intended HTTP method in the `_method` parameter. -- Specify `appid` and `appkey` in `_ApplicationId` and `_ApplicationKey` parameters. - -This is a workaround that only works for certain platforms. -It is recommended to follow the HTML CORS standard instead. - -### GET - -``` - curl -i -X POST \ - -H "Content-Type: text/plain" \ - -d \ - '{"_method":"GET", - "_ApplicationId":"{{appid}}", - "_ApplicationKey":"{{appkey}}"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -### PUT - -``` -curl -i -X POST \ - -H "Content-Type: text/plain" \ - -d \ - '{"_method":"PUT", - "_ApplicationId":"{{appid}}", - "_ApplicationKey":"{{appkey}}", - "upvotes":99}' \ - https://{{host}}/1.1/classes/Post/ -``` - -### DELETE - -``` -curl -i -X POST \ - -H "Content-Type: text/plain" \ - -d \ - '{"_method": "DELETE", - "_ApplicationId":"{{appid}}", - "_ApplicationKey":"{{appkey}}"}' \ - https://{{host}}/1.1/classes/Post/ -``` diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/_category_.json similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/_category_.json rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/_category_.json diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/setup-swift.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/setup-swift.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/setup-swift.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/setup-swift.mdx diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/swift.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/swift.mdx similarity index 100% rename from leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/swift.mdx rename to i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/swift-guide/swift.mdx diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/_category_.json deleted file mode 100644 index 2b9e010f5..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapSupport", - "collapsed": true, - "position": 21 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features.mdx deleted file mode 100644 index 1b5d0d4fe..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features.mdx +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: TapSupport Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -TapSupport assists the game operation teams in solving problems encountered by players faster and better. - -## Key Benefits - -### Rich Tools to Improve Customer Support Efficiency - -TapSupport consists of two core systems: Ticket and Knowledge Base, providing a set of freely combinable functional modules, including announcements, FAQs, forms, etc., to solve players' problems and requests. - -- The ticket module comes with the category-based auto-assignment function. You can also customize triggers to achieve a more complex assignment mechanism that can quickly assign tickets to the most appropriate staff members. - -- Tickets support custom fields that can be filled in by the player or the developer via a form, providing detailed context for the staff. - -- Multiple notification methods are supported to ensure you don't miss any player responses. - - -### Multi-dimensional Analytics to Support Operations - -TapSupport is designed to help solve player problems, but it is also designed to help the operation team identify issues, refine valuable content, and improve the game itself. - -- The ticket module has flexible search and filtering capabilities and support aggregation and analysis by category, field, and status. It also supports data export for further analysis with third-party tools. - -- Users can leave feedback on knowledge base articles so that you can see behind the scenes what content is effective and what is not solving the player's problem. - - -### Simple and Powerful Management Features - -TapSupport provides an intuitive role-based permission control system that supports custom roles in addition to pre-built roles for more flexible needs. - -The system provides managers with detailed data to evaluate the quantity and quality of customer support work, including but not limited to the number of tickets handled, the timeliness of responses to players, and player satisfaction. - -### User Experience Designed for Players - -As a customer support tool focused on gaming scenarios, TapSupport provides an in-game customer support experience. Players don't have to switch apps to use the TapSupport feature, and they can receive in-game prompts immediately after customer support replies. - -In addition, the SDK optimizes visuals and interactions for landscape scenarios and provides developers with a control API to reduce the impact of the SDK on the performance of the game's core processes. - -### Out-of-the-box Development Experience - -The TapSupport client SDK covers popular game development platforms and provides features such as user interface, login, data reporting, and badge maintenance. The highly integrated features provide an out-of-the-box development experience. - -TapSupport supports multiple login methods. If the game is already using the TDS built-in account, you can log in directly by using it. It also supports logging in with the game's own account system. - - -:::info Still under development -TapSupport is currently in public beta, and some features that are still in development may not be available to all users. -::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features/_category_.json deleted file mode 100644 index ffebf8e89..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Guide", - "position": 2 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features/roles.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features/roles.mdx deleted file mode 100644 index 8f341dc1f..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features/roles.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: TapSupport Authority Control -sidebar_label: Customer Service Authorities -sidebar_position: 2 ---- - -The Customer Service module has a separate authority system from the Developer Center section. Authorities consist of two parts: role and scope. - -- Role: determines which functions and actions a user can use; -- Scope: determines which work orders a user can access. - -### Roles - -User roles are categorized into the following categories according to their different responsibilities: - -| user role | remit | access path | billing | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ---------- | -| Super Administrator | Maintenance services and security-related configurations | Developer Center | not billed | -| Administrator | daily management
    Has all configuration permissions except for service and security related configurations | Customer Service Workbench | billing | -| Customer Service | Communicate directly with players and handle player requests | Customer Service Workbench | billing | - -Super Administrators are Tap users with TapSupport authorities in the Developer Center. Users in other roles are independent users of the customer service system who log in to use the TapSupport module through the TapSupport Workbench, which is separate from the Developer Center, and are collectively referred to as "Members". Members can be added by super administrators in the Developer Center or by administrators in the TapSupport Workbench. A member can have one or more user roles, and super administrators or administrators can modify the roles of members. - -The detailed correspondence between roles and permissions is as follows: - -| | | Super Administrator | Administrator | Customer Services | Collaborators | Developers | -| --------------------------- | ---------------------------------------------------------------------------- | :----------------------------: | :----------------------------: | :--: | :----: | :-------------------------: | -| Services and Security | Enabling, disabling services | ✔️ | | | | | -| | Configuring a custom domain name | ✔️ | | | | | -| User Management | Querying member list and information | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | -| | Managing members
    Adding, disabling, and modifying profiles | ✔️ | ✔️ | | | | -| | Checking Player Profile | | ✔️ | ✔️ | ✔️ | ✔️ | -| | Managing Players
    Adding and modifying information | | ✔️ | ✔️ | | | -| Tickets | Access to Tickets
    - Inquiring and viewing tickets
    - Submitting an internal message
    | | ✔️ | ✔️ | ✔️ | ✔️ | -| | Modifying Tickets Properties | | ✔️ | ✔️ | | | -| | Replying to the player | | | ✔️ | | | -| Knowledge Base | Checking up | | ✔️ | ✔️ | ✔️ | ✔️ | -| | Management
    Adding, modifying content and posting status | | ✔️ | ✔️ | | | -| Setting | Content Setting
    - Products and Classification
    - Fields and forms of Tickets
    - Dynamic content
    | | ✔️ | | | ✔️
    read only | -| | TapSupport Settings
    - Cluster
    - Trigger
    - Notification channel management
    | | ✔️ | | | | -| | Development Settings
    - Player Authentication Methods
    - Customized domain names (read only)
    | | ✔️ | | | ✔️ | -| Audits | Audit log | ✔️
    Manufacturers' section | ✔️
    Membership component | | | | - -### Scope - -If a user has the "Access Tickets" permission, there are three different scopes of work orders that he/she can access as follows: - -- All Tickets -- Only Tickets for their group -- Only Tickets assigned to this user only \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features/setup.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features/setup.mdx deleted file mode 100644 index c7b7c157d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/features/setup.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Open TapSupport Privilege -sidebar_label: Opening process -sidebar_position: 1 ---- - -To use TapSupport Service, you need to activate it in the TapTap Developer Center. - -### Enable Service - -TapSupport Service is a vendor dimension service, developers can self-enable the service at **Developer Center Home Page > Game Service > TapSupport Service**. - -:::info Permission Requirements -Access to the TapSupport Service module in the Developer Center requires the **Customer Service Administrator** permission in the vendor permissions. -::: - -### Price Plan - -The TapSupport system is charged according to the number of activated customers. - -:::info -There is no billing during the beta period, we will post a notice in advance before the official billing. -::: - -### Add Customer Service - -After enabling customer service, you first need to add customer service. - -You can switch to **Customer Service Management** tab to add customer service. To add customer service, you need to provide the mobile phone number of the customer service, and the customer service will receive an invitation SMS after you add the customer service. Customer service logs in to the customer service backend by adding the SMS verification code via mobile phone. - -The SMS received by the customer service will have a link to the customer service workbench, you can also find the customer service workbench in the upper right corner of the game customer service module in the developer center to jump to the entrance. - -When adding customer service, you need to specify the role and scope of the customer service, the responsibilities and specific authorities of different roles see ["Authority Control of Customer Service"](/sdk/tap-support/features/roles/). - -:::info Be the first customer service! -Super administrators can only perform account and security related configurations in the DC backend, specific customer service business-related configurations need to be performed in the Customer Service Workbench. To configure the service, you can add yourself as an administrator. -::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/guide.mdx deleted file mode 100644 index 986ab6df9..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tap-support/guide.mdx +++ /dev/null @@ -1,906 +0,0 @@ ---- -title: Tap Customer Service Development Guide -sidebar_label: Guide -sidebar_position: 3 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -This article describes how to access the customer service system provided by TDS in the game. - -## SDK initialization - -Get TapSDK at [download page](/tap-download) and introduce `TapSupport` module: - - -<> - - - - -<> - -The TapSupport SDK relies on the TapCommon module: - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar' - implementation (name:'TapSupport_${sdkVersions.taptap.android}', ext:'aar') -}`} - - -Verify that network permissions have been added to `AndroidManifest.xml`: - -```java - -``` - - -<> - -The TapSupport SDK relies on the TapCommon module: - -```objc -TapCommonSDK.framework -TapSupportSDK.framework -``` - - - -<> - -The TapSupport SDK relies on the TapCommon module: - -```csharp -PublicDependencyModuleNames.AddRange( - new string[] - { - "TapCommon", - "TapSupport" - // ... add other public dependencies that you statically link with here ... - } - ); -``` - - - - - -Before initializing the SDK, there are a few preparations to complete: - -1. Ask the vendor administrator or customer service administrator to invite you to become a customer service administrator. -2. Each vendor will be assigned a unique domain name, which you need to write down and use. You can find the available domain names in **Settings > Developer Information** in the Customer Service Workbench. If you want to use your own domain name, you can contact the vendor administrator to bind it in the Game Customer Service module of the Developer Center. -3. In the Customer Service Workbench, create a product for your game (**Settings > Administration > Products & Categories**) and write down the ID of this product. - -Then use the following code to initialize the TapSupport module: - - - -```cs -using TapTap.Support; - -TapSupport.Init("https://please-replace-with-your-customized.domain.com", "Product ID"); -``` - -```java -import com.tds.tapsupport.TapSupport; -import com.tds.tapsupport.TapSupportCallback; -import com.tds.tapsupport.TapSupportConfig; - -TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "Product ID", new TapSupportCallback() { - @Override - public void onUnreadStatusChanged(boolean hasUnread) { - // We'll discuss the use of callbacks in § Unread message notifications. - } -}) -TapSupport.setConfig(this, config); -``` - -```objc -#import - -TapSupportConfig *config = [TapSupportConfig new]; -config.server = @"https://please-replace-with-your-customized.domain.com"; -config.productID = @"Product ID"; -config.callback = self; -[TapSupport shareInstance].config = config; -``` - -```cpp -#include "TapUESupport.h" - -FTapSupportConfig Config; -Config.ServerUrl = TEXT("https://please-replace-with-your-customized.domain.com"); -Config.ProductID = TEXT("Product ID"); -TapUESupport::Init(Config); -``` - - - -In the above code example, `please-replace-with-your-customized.domain.com` is the domain name obtained or bound in Preparation 2. The `product ID` is the ID of the new product created in Preparation 3. - -## Login - -In order to ensure that the information submitted by a player, such as forms, is only accessible to that player, the customer service system requires a login in order to be used. We offer three different login options: - -- TDSUser (TDS Built-in Account) Login -- TDSUser login -- Anonymous Login - -:::note -Logging in here refers to the process of authentication when a gamer uses the customer service system's features within the game. This step is the interaction between the game side and the customer service system, and is usually not perceived by the player, as opposed to logging in to the game itself (e.g., the anonymous login here has nothing to do with the game's guest login). -::: - -### TDS Built-in Account Login - -If your game uses the TDS built-in account service, you can log in to the customer service system directly on the client side using the logged-in TDSUser authorization. - -:::info -The TDS Built-in Account Service is a user system provided by TDS that supports multiple login methods. If your game is already using TapTap Login, Friends, Achievements, Leaderboards and other services, you are probably already using TDS Built-in Accounts. For more information, please refer to ["Introduction to Built-in Account Features"](/sdk/authentication/features/). -::: - -To use TDSUser authorization to log in to the customer service system, you first request an authorization token using a logged-in TDS user account, and then use that token to call the TDS login interface of the customer service module: - - - -```cs -try { - string token = await TDSUser.RetrieveShortToken(); - await TapSupport.LoginWithTDSCredential(token); - Debug.Log("Log in to TDSUser JWT Completion"); -} catch (TapException e) { - Debug.LogError($"{e.Code} : {e.Message}"); -} catch (Exception e) { - Debug.Log(e); -} -``` - -```java -TDSUser.retrieveShortTokenInBackground(tdsUser.getSessionToken()).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(JSONObject jsonObject) { - String credential = jsonObject.getString("identityToken"); - TapSupport.loginWithTDSCredential(credential, new TapSupport.LoginCallback() { - @Override - public void onComplete(boolean success, Throwable error) { - if (success) { - // Login Successful - } else { - // Login Failure - Log.e("TapSupportActivity", "login:error:" + error.toString()); - } - } - }); - } - - @Override - public void onError(Throwable error) { - // Failed to request authorization token - } - - @Override - public void onComplete() { - - } -}); -``` - -```objc -[TDSUser retrieveShortTokenWithCallback:^(NSString *_Nullable token, NSError *_Nullable error) { - if (error) { - // Failed to request authorization token - } else { - [TapSupport loginWithTDSCredential:token - handler:^(BOOL succcess, NSError *_Nullable error) { - if (succcess) { - // Successful login - } else { - // Login Failure - } - }]; - } - }]; -``` - -```cpp - -// Todo 1 - -if (TSharedPtr User = FTDSUser::GetCurrentUser()) -{ - User->RetrieveShortToken( - FStringSignature::CreateLambda([](const FString& Credential) - { - /** Getting Credential Success */ - TapUESupport::LoginWithTDSCredential( - Credential, - FSimpleDelegate::CreateLambda([]() - { - /*** Login Successful */ - }), - FTUError::FDelegate::CreateLambda([](const FTUError& Error) - { - /*** Login Failure */ - })); - }), - FLCError::FDelegate::CreateLambda([](const FLCError& Error) - { - /** Failed to get Credential */ - })); -} - -``` - - - -#### Exception Handling - -If the SDK throws an exception during login and subsequent processes, developers can be notified via a callback. - -Among these exceptions, we need to handle the token expiration (`EXPIRED_CREDENTIAL`) exception in particular, and re-execute the above `request authorization token, call login interface` process to log in the customer service again. - -For other kinds of exceptions, it is recommended to show them to players directly. Since customer service exceptions usually don't affect the player's playing experience, we can consider only prompting the player that customer service is not available at the customer service portal, and provide the interaction of manually triggering retries. - - - -```cs - -if (e is TapException ex && ex.Code == 9006) { - // Login expired - } else { - // Other exceptions -} -``` - -```java -if (e instanceof ServerException) { - try { - org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody); - if(errorResponse.getInt("numCode")==9006){ - // Login expired - } - } catch (JSONException ex) { - // ignore - } -} -``` - -```objc -if (error) { - if (error.code == 9006) { - // log back in - } else { - // Other exceptions - -using TapTap.Support; - -TapSupport.Init("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback -{ - OnGetUnreadStatusError = (exception) => { - if (e is TapException ex && ex.Code == 9006) { - // Login expired, re-execute TDSUser.RetrieveShortToken and TapSupport.LoginWithTDSCredential - } else { - // Other exceptions - } - } -}); -``` - -```java -import com.tds.tapsupport.TapSupport; -import com.tds.tapsupport.TapSupportCallback; -import com.tds.tapsupport.TapSupportConfig; - -TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback() { - @Override - public void onGetUnreadStatusError(Throwable e) { - if (e instanceof ServerException) { - try { - org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody); - if(errorResponse.getInt("numCode")==9006){ - // Login expired, re-execute TDSUser.RetrieveShortToken and TapSupport.LoginWithTDSCredential - } else { - // Other exceptions - } - } catch (JSONException ex) { - // ignore - } - } - } -}); -TapSupport.setConfig(this, config); -``` - -```objc -- (void)onGetUnreadStatusError:(nonnull NSError *)error { - if (error) { - if (error.code == 9006) { - // Login expired, re-execute TDSUser.RetrieveShortToken and TapSupport.LoginWithTDSCredential - } else { - // Other exceptions - } - } -} -``` - -```cpp - -// Todo 2 -// Handle the EXPIRED_CREDENTIAL exception (9006) and retry the login process. -// Prompt the developer to show other exceptions to the user - -TapUESupport::OnErrorCallBack.BindLambda([](const FTUError& Error) -{ - if (Error.code == 9006) - { - /*** Login expired, re-execute User->RetrieveShortToken and TapUESupport::LoginWithTDSCredential */ - } - else - { - /*** Other errors */ - } -}); - -``` - - - -### Associated login for game's own account system - -If your game uses another user system, the customer service system also supports logging in with signed user information. - -:::caution requires a server -Signing user information requires the use of a secret, so a server is required to use this method. -::: - -The developer first needs to generate an auth secret in **Settings - Developer Settings - Player Authentication** in the Customer Service Workbench, and then **Use this secret on the server** to JWT-sign the player information using the HS256 algorithm. The player information (payload) should contain the user's unique identifier and a name for display, and should be structured as follows: - -```json -{ - "sub": "U1234567", // unique identification - "name": "Dash" // Displayed in the name of the work order, customer service back office -} -``` - -
    -JWT Example of a signature -input: - -- Algorithm: HS256 -- payload: `{"sub": "U1234567", "name": "Dash"}` -- secret: `44a23a3701955756301768bbb5dd1e1ea51500b556fb73201de76d5365150653` - -exports JWT: - -``` -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJVMTIzNDU2NyIsIm5hbWUiOiJEYXNoIn0.OV2LCP-7cLVTfjJlx21q9O1Tj_LlM0LFyW3OOu7CeNk -``` - -
    - -Finally, the obtained JWT is returned to the client, and the client calls the third-party account login interface of the SDK to complete the login: - - - -```cs -TapSupport.LoginWithCustomCredential(jwt); -``` - -```java -TapSupport.loginWithCustomCredential("User JWT"); -``` - -```objc -[TapSupport loginWithCustomeCredential:@"User JWT"]; -``` - -```cpp -// Todo 3 -TapUESupport::LoginWithCustomCredential(TEXT("User JWT")); -``` - - - -For security reasons, we usually add an `exp` field to the JWT payload to specify its expiration time, and the customer service system will honor the JWT's expiration time setting: - -```json -{ - "sub": "U1234567", - "name": "Dash", - "exp": 1676546560 // Unix epoch -} -``` - -Considering both security and performance, we recommend setting the JWT expiration time to 1-3 days from the current time. - -#### Exception Handling - -If an exception occurs in the SDK during login and subsequent processes, the developer can be notified via a callback. - -Among these exceptions, we need to handle the JWT expiration (`EXPIRED_CREDENTIAL`) exception in particular, and re-execute the above `request JWT, call login interface` process to log in the customer service again. - -For other kinds of exceptions, it is recommended to show them to the player directly. Since customer service exceptions usually do not affect the player's playing experience, we can consider only prompting the player that the customer service is unavailable at the customer service portal, and provide the interaction of manually triggering the retry. - - - -```cs - -if (e is TapException ex && ex.Code == 9006) { - // Login expired -} else { - // Other exceptions -} -``` - -```java -if (e instanceof ServerException) { - try { - org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody); - if(errorResponse.getInt("numCode")==9006){ - // Login expired - } - } catch (JSONException ex) { - // ignore - } -} -``` - -```objc -if (error) { - if (error.code == 9006) { - // log back in - } else { - // Other exceptions - -using TapTap.Support; - -TapSupport.Init("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback -{ - OnGetUnreadStatusError = (exception) => { - if (e is TapException ex && ex.Code == 9006) { - // Login expires, refetches the new JWT and executes TapSupport.LoginWithCustomCredential(jwt) - } else { - // Other exceptions - } - } -}); -``` - -```java -import com.tds.tapsupport.TapSupport; -import com.tds.tapsupport.TapSupportCallback; -import com.tds.tapsupport.TapSupportConfig; - -TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback() { - @Override - public void onGetUnreadStatusError(Throwable e) { - if (e instanceof ServerException) { - try { - org.json.JSONObject errorResponse = new org.json.JSONObject(((ServerException) e).responseBody); - if(errorResponse.getInt("numCode")==9006){ - // Login expires, re-fetches the new JWT and executes TapSupport.loginWithCustomCredential("new JWT"); - } else { - // Other exceptions - } - } catch (JSONException ex) { - // ignore - } - } - } -}); -TapSupport.setConfig(this, config); -``` - -```objc -- (void)onGetUnreadStatusError:(nonnull NSError *)error { - if (error) { - if (error.code == 9006) { - // The login expires, refetches the new JWT and executes [TapSupport loginWithCustomeCredential:@"new JWT"];. - } else { - // Other exceptions - } - } -} -``` - -```cpp -// Todo 2 -// Handle the EXPIRED_CREDENTIAL exception (9006) and retry the login process. -// Prompt the developer to show other exceptions to the user -TapUESupport::OnErrorCallBack.BindLambda([](const FTUError& Error) -{ - if (Error.code == 9006) - { - /*** Login expires, re-fetches the new JWT and executes TapUESupport::LoginWithCustomCredential(TEXT("new JWT"));. */ - } - else - { - /*** Other errors */ - } -}); -``` - - - -### Anonymous Login - -Anonymous login means that the game uses a string that only the current player can get as an anonymous identification (ID) to log into the customer service system. - - - -```cs -TapSupport.LoginAnonymously("uuid"); -``` - -```java -TapSupport.loginAnonymously("uuid"); -``` - -```objc -[TapSupport loginAnonymously:@"uuid"]; -``` - -```cpp -TapUESupport::LoginAnonymously("uuid"); -``` - - - -Anonymous login can be used to differentiate players by device in scenarios where the game does not have an account system or the player is not logged in, or to differentiate players by account after they have logged into their game account. - -#### Associating with Devices - -The game client generates and persists a UUID for the device, and then uses this ID to call the anonymous login interface to associate the player with the device. - -At this point, the anonymous ID on the device is the only identity credentials. If the ID is lost due to deletion of the application or clearing of local data, the player will not be able to access data such as historical work orders. - -#### associated with a game account - -Similarly, to be associated with a game account, the anonymous login interface needs to be called using an ID that has a one-to-one relationship with the account. - -The anonymous login mechanism is very flexible. For example, if you want a player to have a separate customer service account for each game, you can assign a separate anonymous ID to each game; if you want to track the player across games, you can use an anonymous ID at the pass level, in which case the customer service system doesn't know what the ID means, which is where the name "anonymous" comes from. This is where the name "anonymous" comes from. - -The price of flexibility is security. If not used correctly, anonymous login can lead to data leakage. The customer service system doesn't care where the anonymous ID came from, and can't do any verification, which means that the historical work order data of the player who leaked the ID will also be leaked. Therefore, **"only the current player can get it" needs to be guaranteed by the game side**. More specifically, the anonymous ID needs to meet the following conditions: - -- Unique and unchanging to the player -- Cannot be guessed or deduced, cannot be enumerated. -- It is not public, and will not be disclosed by the player through sharing or screenshots. - -
    -Example of a correct anonymous ID - -- ✅ `b3a59993-c659-49f9-9f51-f2c808a472a0`:The game user system generates a UUID when a new player is created that serves as the player's anonymous ID for the customer service system, and the player logs in to log in to the customer service system with that ID. -- ✅ `sha1('2436234209' + secret)`:Append a fixed secret to the player's UID on the server side and then hash it. Only after the player logs in can he get the ID to log into the customer service system. - -
    - -
    -Example of an insecure anonymous ID - -Simply put, no client-only solution is secure: - -- ❌ `'2436234209'` Player UID: This is usually public information, visible and potentially shared by the player in the game, and a purely numeric ID that is easy to enumerate. -- ❌ `sha1('2436234209')` :Hashing alone is still easy to enumerate when the algorithm is guessed. - -
    - -To reduce the risk of misuse, the anonymous login interface restricts the minimum length of the anonymous ID to 32. - -### Clear Login Status - - - -```cs -TapSupport.Logout(); -``` - -```java -TapSupport.logout(); -``` - -```objc -[TapSupport logout]; -``` - -```cpp -TapUESupport::Logout(); -``` - - - -## Open the customer service page - - - -```cs -TapSupport.OpenSupportView(); -``` - -```java -TapSupport.openSupportView(); -``` - -```objc -[TapSupport openSupportView]; -``` - -```cpp -// TODO 4: Check if you can omit the parameter -TapUESupport::OpenSupportView("/", nullptr, nullptr); -TapUESupport::OpenSupportView(); -``` - - - -:::info -If the opened page has no content, you can first configure some subcategories for that game category in the Customer Service Workbench. -::: - -### Scenario-based portals - -In addition to landing pages, the SDK also supports opening specific pages directly in specific scenarios. - - - -```cs -TapSupport.OpenSupportView(); -``` - -```java -TapSupport.openSupportView("/path"); -``` - -```objc -[TapSupport openSupportViewWithPath:@"/path"]; -``` - -```cpp -// TODO 5: Check if you can omit the parameter -TapUESupport::OpenSupportView("/path", nullptr, nullptr); -TapUESupport::OpenSupportView(TEXT("/path")); -``` - - - -Different pages are distinguished by different path parameters. The currently supported pages are: - -| path | clarification | -| ------------------------------- | ------------------------------------------ | -| `/tickets/new?category_id={id}` | To submit a new work order, specify the id of the category. | -| `/tickets` | Player work order list page to see all historical work orders of the player. | -| `/articles/{id}` | Knowledge base article page. | - -### Reporting Information - -The work order module supports developers to collect additional information such as device, player, etc. through custom fields. Developers can bring in additional information when opening a support page (openSupportView), and the SDK will record this information in the work order fields when submitting a work order. This information can be viewed and analyzed in the customer service workbench. - -Before reporting data, you first need to define fields in the Customer Service Workbench (**Settings > Administration > Work Order Fields**). These fields need to be set to "Customer Service Only" access. Once created, you need to write down the ID of the field, which we will use in the SDK. - - - - - -```cs -Dictionary fields = new Dictionary(); -fields.Add("243", "iOS 15.1"); // 243 is the ID of the "OS" field created in the background. -fields.Add("244", "Dash"); // 244 is the ID of the Role Name field created in the backend - -TapSupport.OpenSupportView("/", fields); -``` - -```java -Map fields = new HashMap<>(); -fields.put("243", "iOS 15.1"); // 243 is the ID of the "OS" field created in the background. -fields.put("244", "Dash"); // 244 is the ID of the Role Name field created in the backend - -TapSupport.openSupportView("/", fields); -``` - -```objc -NSDictionary *fields = @{@"243":@"iOS 15.1", @"244":@"WiFi"}; // 243 is the ID in the "OS" field created in the backend, 244 is the ID in the "Role Name" field -[TapSupport openSupportViewWithPath:@"/" fieldsData:fields]; -``` - -```cpp -// TODO 6: Check if you can omit the parameter -TSharedPtr Fields = MakeShareable(new FJsonObject); -Fields->SetStringField("243", "iOS 15.1"); -Fields->SetStringField("244", "Dash"); -TapUESupport::OpenSupportView("/", Fields); -``` - - - -In addition to setting it when opening the customer service page, developers can also set the global default field information through the following interface: - - - -```cs -TapSupport.SetDefaultFieldsData(fields: fields); -``` - -```java -TapSupport.setDefaultFieldsData(fields); -``` - -```objc -[TapSupport shareInstance].defaultFieldsData = fields; -``` - -```cpp -TapUESupport::SetDefaultFieldsData(Fields); -``` - - - -Global fields can be updated after setup: - - - -```cs -TapSupport.DefaultFields = new Dictionary { - { "key", "value" } -}; -``` - -```java -TapSupport.updateDefaultField("key", "value"); -``` - -```objc -[TapSupport updateDefaultFieldWithValue:@"value" forKey:@"key"]; -``` - -```cpp -// TODO 7 -TSharedPtr Value = MakeShared(TEXT("Value")); -TapUESupport::UpdateDefaultField(TEXT("key"), Value); -``` - - - -Globally set fields are merged with the incoming `fields` parameter at OpenSupportView time. Fields passed in during the `OpenSupportView` method have a higher priority than global fields, meaning that global fields will not take effect if they are the same. - -### Closing the support page - -Players can click the close button within the customer service page to exit. However, in certain scenarios, the game may need to actively close the customer service page: - - - -```cs -TapSupport.CloseSupportView(); -``` - -```java -TapSupport.closeSupportView(); -``` - -```objc -[TapSupport closeSupportView]; -``` - -```cpp -TapUESupport::CloseSupportView(); -``` - - - -## Unread Message Notification - -Unread messages are generated when there are new developments in a submitted work order (e.g. a new customer service response). Usually in-game, players are notified of unread messages in the customer service portal using red dots, etc. The SDK automatically polls for dimensional messages, and when the status of an unread message changes - from none to yes or yes to no - the SDK notifies the developer via a callback to notify the developer: - - - -```cs -using TapTap.Support; - -TapSupport.Init("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback -{ - UnReadStatusChanged = (hasUnRead, exception) => - { - Debug.Log($"hasUnRead:{hasUnRead} exception:{exception}"); - } -}); -``` - -```java -import com.tds.tapsupport.TapSupport; -import com.tds.tapsupport.TapSupportCallback; -import com.tds.tapsupport.TapSupportConfig; - -TapSupportConfig config = new TapSupportConfig("https://please-replace-with-your-customized.domain.com", "categorization ID", new TapSupportCallback() { - @Override - public void onUnreadStatusChanged(boolean hasUnread) {} -}); -TapSupport.setConfig(this, config); -``` - -```objc -#import - -// callback need to be realized TapSupportDelegate -TapSupportConfig *config = [TapSupportConfig new]; -config.server = @"https://please-replace-with-your-customized.domain.com"; -config.productID = @"categorization ID"; -config.callback = self; -[TapSupport shareInstance].config = config; -``` - -```cpp -TapUESupport::OnUnreadStatusChanged.BindUObject(this, &YourUObject::OnUnreadStatusChanged); -``` - - - -:::info -Developers do not need to additionally clear the local unread notification status (small red dot) when tapping the customer service portal. This is because clicking on the customer service portal does not mean that the player has viewed all unread work orders. If the player has viewed all unread work orders, the SDK will get the latest status and notify the developer via the callback mentioned above. -::: - -### Pause Polling - -The SDK's built-in polling mechanism intelligently adjusts the frequency of getting the status of unread messages. However, there are some scenarios where these requests are still unnecessary overhead, such as when a player wants to pause all unnecessary background requests during a game match (when there is no red dot displayed on the interface), and the SDK provides a pair of APIs for controlling polling for this purpose: - -- `Pause`: pauses polling -- `Resume`: resumes polling. - - - -```cs -TapSupport.Resume(); -TapSupport.Pause(); -``` - -```java -TapSupport.resume(); -TapSupport.pause(); -``` - -```objc -[TapSupport resume]; -[TapSupport pause]; -``` - -```cpp -TapUESupport::Resume(); -TapUESupport::Pause(); -``` - - - -
    -SDK Polling Policy - -- Initially, a request is initiated immediately, and the next request interval is set to be 10s. - - If there is no change in the status of an unread message compared to the current status, increase the interval by 10s until the maximum interval is 300s, and reset the interval to 10s if there is a change. - - If the player never opens the WebView, the interval is increased to a constant 3600s. -- Calling the `Resume` method resets the polling to its initial state. - -
    - - \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/_category_.json deleted file mode 100644 index 6cff07e1a..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "TapDB", - "collapsed": true, - "position": 3 - -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/_category_.json deleted file mode 100644 index d1f31d6b5..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Changelog", - "position": 8 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/android.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/android.mdx deleted file mode 100644 index cae8da922..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/android.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Android -sidebar_position: 3 ---- - -## 2.2.0 | 2022-07-14 - -### What's new - -* Adjust the interface display of frozen items. - -## 2.1.0 | 2022-04-30 - -### What's new - -* New multi-account switching function, developers can add multiple domestic or overseas accounts and switch between them. - -## 2.0.0 | 2022-01-13 - -### What's new - -* New kanban function: you can view the kanban posted on the web side through the client. -* Fixed the data display problem of the advertising module. - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/ios.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/ios.mdx deleted file mode 100644 index b3cd85abd..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/ios.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: iOS -sidebar_position: 2 ---- - -## 2.2.0 | 2022-07-18 - -### What's new - -1. Optimize iPad landscape experience. -2. Adjust the interface display of frozen items. -3. Fix the problem of inaccurate display of weekly active and monthly active dates. - -## 2.1.0 | 2022-04-30 - -### What's new - -* New multi-account switching function, developers can add multiple domestic or overseas accounts and switch between them. - -## 2.0.0 | 2022-01-13 - -### What's new - -* New kanban function: you can view the kanban posted on the web side through the client. -* Fixed the data display problem of the advertising module. - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/web.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/web.mdx deleted file mode 100644 index dcdff026b..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/changelog/web.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Web -sidebar_position: 1 ---- - -## 2.17.0 | 2024-07-01 - -### What's new - -1. Dimension Renaming in Analysis Module: Dimensions can now be renamed within the analysis module. -2. New Data Alert Feature: Introduced a data alert feature that can send notifications to Enterprise WeChat, Slack, and email upon triggering an alert. -3. Display Game Status on TapTap: Shows specific statuses of games on TapTap, such as Test Server or Early Access, to help distinguish between projects with the same name. -4. Custom Callback Configuration for TapTap Ad Channels: TapTap ad channels now support custom callback configuration. -5. Ad Callback Log Query: Supports querying ad callback logs across all channels. -6. 31-60 Day Retention: Added retention metrics for 31-60 days. -7. Filter for Active Projects on Game List Page: The game list page now supports filtering for active projects. -8. Project Hiding: Supports hiding projects, which will no longer appear in the game list once hidden. -9. Shared Filter Conditions: Filter conditions can now be shared with project team members. -10. Refund Data in Payments: Added refund data to the payment section. -11. User Value (LTV): Supports displaying the daily LTV multipliers compared to the first day's LTV. -12. Global Observer Role: Introduced a global observer role with permissions for all projects under the enterprise (including new projects), but without management rights. -13. Weekly and Monthly Promotion Costs in Overview Revenue Module: Added weekly and monthly promotion costs to the revenue module in the overview. - -## 2.15.2 | 2022-09-08 - -### What's new - -* Diagnosis: Crash analysis and error analysis problem detail page, added open new window button. - - -## 2.15.1 | 2022-09-01 - -### What's new - -* Crash analysis and error analysis can share filter criteria by link - - -## 2.15.0 | 2022-08-26 - -### What's new - -1. New diagnostic module: including crash analysis, error analysis, symbol table, label management 4 functions. -2. Access to TapSDK 3.14.0 or later is required to use the Diagnostic Module. -3. In order to enhance the analysis capability, we provide some fields related to cell phone hardware for all projects, which can be used in the "Analysis" module. - -| Field Name | Display Name | -| --- | --- | -| product_name | product name | -| release_year | release year | -| chipset_brand | chipset brand | -| chipset_model | chipset model | -| single_core_score | single core score | -| multi_core_score | multi core score| -| price_1_median 1 | Current market price | -| price_2_median 2 | Current Used Price | - - -## 2.14.3 | 2022-08-11 - -### What's new - -* Optimized the operation experience of "Configuration Module - Early Warning Management". - - -## 2.14.2 | 2022-07-28 - -### What's new - -* Optimized the operating experience of most functions of the configuration module - -## 2.14.1 | 2022-07-14 - -### What's new - -* Optimized the operation experience of "Configuration - User Tab". - -## 2.14.0 | 2022-06-29 - -### What's new - -1. Event analysis no longer requires pre-selection of analysis subjects, and now supports selecting different analysis subjects for multiple metrics at the same time. -2. Simplified table names in SQL analysis. -3. The descriptions of table fields are added in SQL analysis, so that you can check them synchronously when using. - -## 2.13.0 | 2022-06-09 - -### What's new - -* When switching the language to English, the menu now supports English display. - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/_category_.json deleted file mode 100644 index 60548d80c..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "FAQ", - "position": 6 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/ads.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/ads.mdx deleted file mode 100644 index e7ccef063..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/ads.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Advertising Questions -sidebar_position: 3 ---- - -### Q: How to create an advertising plan? - -A:Enter TapDB's ad page, click Ad Management, select New Ad Campaign in the upper right corner, select the ad platform, pay attention to the matching method, fill in the information, and generate the placement link. - -### Q: What is IP Matching? - -A:"IP Matching" is used to check if the IP of the user when clicking on the ad is the same as the IP when opening the game, and is applicable to all ads. Custom ads can be created if no corresponding platform is found when creating a campaign. - -### Q: What is Device Matching? - -A:Device Matching is that TapDB and the ad platform have finished interfacing, and the ad platform will return the data of clicked ads to TapDB to match the new users by device information, which is very accurate, but it is only used in the ad platform that has finished interfacing with TapDB. - -### Q: How to place IP matches and device matches? - -IP Matching: When you add an ad, select the ad platform, the IP matching ad platform is marked, and the IP matching ad needs to fill in the download link, after generating the link, directly fill in the IP matching generated placement link into the landing page URL of the ad platform. - -Device Matching: The callback link can be generated directly after filling in the corresponding parameters of Device Matching. The callback link can be filled in the third-party monitoring link (the name may be different) of the ad platform, and the URL of the landing page can be filled in the download address of the game itself. - -### Q: What if I can't use TapDB links to place ads? - -A:For example, if the domain name of the game is a.b.c, you need to resolve a.b.c to [l.tapdb.net](http://l.tapdb.net) in the form of CNAME, then if the connection given by TapDB is `https://l.tapdb.net/xxxx`, directly replace [l.tapdb.net](http://l.tapdb.net) with a.b.c, replace "https" with "http", and finally become `http://a.b.c/xxxx`, and use this link to deliver the advertisement. - -### Q: Why does the ad match players from other systems? - -A:There are possible reasons why ads are matched to players on other systems. - -(1) The placement is on the web, and both iOS and Android users will be matched. - -(2) If the link is placed on iOS, Android users (who can see the ad) may click on it and actively search for the app to download, and then they will be matched. - -### Q: How do I enable data callbacks for the TapTap ad channel? -A: Go to Ads -> Ad Management -> Channel Settings. You can enable data callbacks for the TapTap ad channel there. Currently, it supports callbacks for activation, registration, payment, and next-day retention data. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/tran.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/tran.mdx deleted file mode 100644 index 4bc96881f..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/tran.mdx +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: SDK Integration Questions -sidebar_position: 1 ---- - -### Q: SDK initialization was successful, why no new data? - -A: Before you fix the problem, you should have a rough understanding of the data reporting process: - -1. The game App calls the relevant interfaces of the SDK to report data. -2. DB platform receives the reported data and displays it in the "Reporting Details". - -The troubleshooting method is also based on this process step by step. Open Configuration - Report Details and check: - -1. Check if initialization is the earliest call? If not, adjust and try again; -2. Observe whether the reported data is displayed in the "Reporting details" function; If the data is displayed, the data is reported normally. If no data is displayed, enter 3; -3. Open the mobile bag capture tool to test whether the equipment reports data normally. - -### Q: The SDK initialization was successful, why is the added data incorrect? - -A: You can use the "Report Details" function to verify the accuracy of buried point reporting. - -### Q: How can I check the reported data? - -A: You can check the reported data through "Report Details" and "Buried Site Management. - -"Reporting Details": you can use "Reporting Details" to view the real-time reporting of buried data during the debugging stage of SDK access and buried testing. - -"Buried point management" : you can view the data reception of the project in the last 7 days, quickly understand the overall situation of buried point reporting, as well as the details of error reporting and sampling examples. - -### Q: Why did the SDK initialization fail? - -You can try to debug the gameversion field, if there is no value, you need to fill in the value. - -### Q: How to store userId of single player game? - -A: The userId of the standalone game needs to pay attention to the following points. - -(1) iOS has to generate an ID by itself and store it in the certificate space. iOS has a storage space, one for application and one for certificate (enterprise). - -(2) Android try to save to SD card, use a user operation or call `setUser` after 1 minute. - -(3) Randomly generate a unique user ID and save it locally. - -### Q: Why no new data is added after SDK integration? - -A: Please check whether the initialization of the SDK is called successfully, in addition the initialization should be called at the earliest. If the initialization fails, then do the processing according to the failure log. - -### Q: Do both the server and client need to pass the recharge data? - -A: You can choose one way to pass the recharge data in the server side and the client side. If you pass both, the recharge data will be doubled. - -### Q: Why the TapDB page does not show the revenue or the revenue is more than the actual revenue when the server side delivers the recharge data? - -A: First of all, only one of the server-side and client-side interfaces can be used (if both are used, it will show double the recharge amount and make sure the recharge is successful before sending the recharge data), and secondly, the user_id in "identify": "user_id" on the server side and the "userId" in setUser in the document "Record a Player" need to be consistent. - -### Q: Why does TapDB show no data after sending real-time online data? - -A: There may be several reasons for not displaying data. - -(1) Check the file format, parameter type (returning 400 error means the format is not correct). - -(2) Pay attention to the timestamp unit (seconds), and only send the last 7 days of data, too early data will not be saved. - -(3) Note: **whether there is a required header information: `Content-Type: application/json`.** - -### Q: Can I not fill in the parameters that can be empty? - -A: No, the parameters that can be empty are filled with `null`. - -### Q: Why can't I find the subcontracted channel in TapDB after filling in the channel? - -A: There must be an added data for TapDB to receive the channel and display the subcontracted channel in this page. - -### Q: When TapDB reports events, are the letters of the events case-sensitive? - -A: All keys are case-insensitive, and all values are case-sensitive. The event name is value, the property name is key, and the property value is value. - -### Q: When reporting a custom event, must the property type be exactly the same as the registered one? Is there any compatibility policy? - -A: It must be identical, there is no compatibility policy. The wrong type will be discarded by the agent as dirty data. - -### Q: Is it possible to deploy privately? - -A: Private deployment is not supported for now. But we will ensure the security of your data. - -### Q: How to set the permission? - -A: You can modify account permissions in "Enterprise Settings" - "Permission Management" - "Edit Members". - -### Q:After reporting the Custom Event, no data can be found in the backend. - -A: Please first check if the [user_id](/sdk/tapdb/sdk/client-side-integration/#设置账号-id) is set in the code. If you're unsure, you can go to the backend and use SQL queries to see the user_id that has been reported previously. - - -![ SQL Link](/img/数据分析-Sql查询.png) - -```SQL -SELECT * FROM hive_saas1.tapdb."users" -WHERE user_id LIKE'%dTJTp6sA+OWsZ7Jf0JmGg==%' -LIMIT 100 -``` -Check if the user_id that needs to be reported is present in the reported user_id. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/user.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/user.mdx deleted file mode 100644 index c8ba306ae..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/faq/user.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: SDK Usage Questions -sidebar_position: 2 ---- - -### Q: How can I view the data under various dimensions? - -A:The table in the four pages of source, online, retention and payment has the option of switching the main dimension. The data displayed in each dimension is different, and the data under each dimension can be viewed according to different situations. - -### Q: What is subcontracting channel? - -A: The subcontracting channel is the information in the channel field when the data is reported, which is convenient for you to split the data. See [TapSDK initialization](/sdk/tapdb/sdk/client-side-integration#tapsdk-init) for details on how to connect. - -### Q: How to use filter conditions? - -A:Filter conditions can be selected according to different options, different ranges of TapDB table presentation data, easy to distinguish between various types of users. You can add multiple filter criteria to filter the data that meets the criteria precisely, or you can save the filter criteria for future use. - -### Q: Why is the source income less than the actual income? - -A:The source only shows the payment income of new users, and the payment information of old users can be found in the payment page. - -### Q: Why is the conversion rate so low? - -A:There are several possible reasons for low conversion rates: - -(1)The technician makes sure that the SDK calls' setUser 'when the user logs in. - -(2) Operators should pay attention to whether there is a large number of new equipment, there may be cheating machines in the refresh increase. - -### Q: How can I check what version of TapDB SDK is connected to my project? - -A:In event analysis, use "SDK version at the time of event" as the dimension and "any event" as the indicator to query. - -### Q: How to verify the accuracy of buried data? - -A:Use buried point management to verify the accuracy of buried points. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features.mdx deleted file mode 100644 index f6d8295d3..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: TapDB Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -TapDB is a set of analysis tools focused on solving the data needs of game projects. It is committed to helping developers achieve low-cost and efficient access and query experience. - -## Original Intention - -As game developers, we know that making games is not easy. Thinking about how to polish game products can be a headache, and choosing the right set of data tools and using them correctly can be a lot of work and ineffective. We believe there must be high value in providing a low-barrier, easy-to-use, and accurate tool. - -Over the years of making games, we have accumulated some experiences and methods, and we hope to share them with you through TapDB. We believe that helping developers create quality games is helping TapTap grow. - -## Function - -The main functions provided by TapDB service are: - -### Basic BI - -The four data reports are very practical and have a low learning threshold. That's what we've learned from decades of gaming. - -### Derived Event - -For the developer's experience, we have reduced the query time of the base BI to 1/10th of the time through a series of derived events, ensuring that you can quickly access the data you need. - -### Tracking Advertising Delivery - -Embed into the world's leading AD delivery systems (such as Ocean Engine, AMS, AppsFlyer, etc.) for easy AD tracking, callbacks, and data analysis. - -### Permission Role - -Each user is given a separate account and individually controlled permissions to ensure data security. - -### Custom Event Analysis - -- Freedom to customize events and focus on the behaviors that matter most. - -- Multi-dimensional perspective, quickly disassemble data, more efficiently find the root cause of the problem. - -- Log-level query capability to know exactly what the user has done. - -## Advantages And Features - -- Low-threshold access: Access to basic events is very simple, which is enough to give you perfect analytics and ad placement capabilities. - -- No delay: data can be checked immediately after reporting, time is life. - -- Keep updated game analysis framework: We will share the latest experience and methods with you continuously. - -- Combined with the data of TapTap ecology, we provide developers with full-chain game data analysis capability. - -- Free. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/_category_.json deleted file mode 100644 index 875bf03cc..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "功能指南", - "position": 4 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/_category_.json deleted file mode 100644 index 70251bade..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Custom Event", - "position": 1 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/ad.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/ad.mdx deleted file mode 100644 index 5c4b2d40a..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/ad.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: Ad Management -sidebar_position: 16 ---- - -## 1. Overview - -The number of new users per day is a very popular metric in Internet products. There are many more in-depth analyses that can be done on this metric of new users, such as - -- Analyzing the number of new users brought by different channels in order to optimize the placement strategy. -- Analyzing the retention and conversion of new users brought by different channels, so as to compare the quality of users brought by different channels. -- Analyzing the percentage of daily revenue contributed by different channels. - -On the Ads Overview page, you can see the overview data under all ads. You can also see the placement data for each ad link under each ad platform. - -![](/img/customEvent/ad/广告概览new.png) - -## 2. Ad Management - -In this page you can: set up ads, cost management, attribution settings, and view deleted ads. - -### 2.1 Ad Settings - -You can add, edit, query and delete on advertising campaigns. As shown in the figure. - -![](/img/customEvent/ad/广告管理.png) - -A: Add an ad
    -After clicking it you can create an ad, enter the ad name, select the ad platform to be placed, select the ad tag, and select the sub-dimension. -Click "Generate Placement Link" and the system will automatically generate the placement link. - -![](/img/customEvent/ad/新增广告.png) - -B: Tag Managemen t
    -Ad tags are notes and markers for each campaign, allowing you to quickly filter out campaigns that have certain characteristics. In tag management, you can view existing tags and tagged campaigns, create new tags, and delete unwanted tags. - -![](/img/customEvent/ad/标签管理.png) - -C: Search Ads
    -Enter a search term and you can search with the name of the ad campaign. - -D: Create similar ads -When you need to create multiple similar ads for monitoring at one time, you can click Create Similar Ads to mark each ad campaign. - -E: Edit Ads
    -Make edits to the ad campaign. - -F: Delete Ads
    -Deleted campaigns will be displayed on this page and you can restore the campaign. Ads deleted for more than 7 days will be automatically cleared. - -### 2.2 Cost Management - -On the Cost Management page you can see the daily cost of each campaign, and the cost-related metrics on the Ads Overview page will be calculated based on the data here. The prerequisite is that you have to enter the costs into the system first. You can enter the costs directly in the cost management screen, or you can click "Import Costs" to import costs in bulk as a template. - -![](/img/customEvent/ad/成本管理.png) - -![](/img/customEvent/ad/上传成本.png) - -### 2.3 Attribution Setting - -You can set the attribution "default window", and you can also set a separate attribution window for each platform. When no separate attribution window is set for the ad platform, the attribution window will follow the system window. - -![](/img/customEvent/ad/归因设置.png) - -### 2.4 Deleted Ads - -Check the deleted ad campaign and click "Restore" to restore Ads. - -![](/img/customEvent/ad/恢复广告.png) - -### 2.5 Tools - Create Link - -In "Tools" - "Create Link", you can share ad data to others. You can choose to share to ordinary external members or advertising agencies: - -1. share to ordinary external members: external members can only query the ad data. -2. share to ad agencies: agencies can query ad data and also support in creating and modifying ads and managing ad costs. - -![](/img/customEvent/ad/创建链接.png) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/alert.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/alert.mdx deleted file mode 100644 index 5aa227f9d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/alert.mdx +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: Alert Management -sidebar_position: 18 ---- - -## 1. Overview - -Construct a set of time series data by setting indicators and filtering grouping items. Timely query and compare with historical data according to time granularity, and notify project team members after triggering alert rules. - -Currently, we support setting indicators in the form of "event analysis" and alert notification by email. - -![概述](/img/customEvent/alert_1.png) - -## 2. Applicable Roles and Uses - -| Roles | Uses | -| ---------- | ------------------------------------- | -| Analyst / Business Person | To monitor key or abnormal data for early warning, grasp product dynamics in real time, and can find abnormal problems at the first time | - -## 3. New Data Alert - -Click New Alert in the upper right corner of the alert list. - -![新建数据预警](/img/customEvent/alert_2.png) - -### 3.1 Basic Information - -In the "Basic Information" section, enter or select the warning name, query object and warning indicator in turn. - -![填写基础信息](/img/customEvent/alert_3.png) - -The alert name is displayed in the alert list, which is the basis for identifying an alert. - -The query subject can choose "Account" or "Device", similar to the query subject in "Event Analysis". - -### 3.2 Early warning rules - -#### 3.2.1 Adding alert rules - -In the warning rule, you can select the grouping dimension and construct multiple groups of time series data at the same time by multi-selecting the grouping value. When there is no grouping dimension, the default grouping item is "Total". - -![预警规则](/img/customEvent/alert_4.png) - -The relationship between the optional alert time granularity, comparison base and parameter types are listed in the following table. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Time granularityComparison BasisParameter Type
    DayFixed valueValue
    Previous day, same day as last weekValue, percentage
    Past 7-day average, past 30-day averageValue, percentage, standard deviation
    HourFixed valuesValue
    Previous hour, same hour yesterdayValue, percentage
    Past 24 hour meanValue, percentage, standard deviation
    - -每个预警下可同时设置多条预警规则,每个预警规则下可添加多个分组。 - -#### 3.2.2 Alert Rule Details - -**Value** - -When fixed value is used as the comparison base: - -* High: actual value of indicator > Numerical value - -* Low: actual value of indicator < value - -When non-fixed values are used as the comparison benchmark: - -* High: actual value of the indicator > the benchmark + the value - -* Low: actual value of the indicator < the benchmark - the value - - -**Percentage** - -* High: actual value of indicator > benchmark * (1 + percentage) - -* Low: actual value of indicator < benchmark * (1 - percentage) - -**standard deviation** - -* High: actual value of indicator > comparative benchmark + parameter * standard deviation - -* Low: actual value of indicator < comparative benchmark - parameter * standard deviation - - -For example, the standard deviation of the "average value for the past 7 days" is the standard deviation of the actual value of the indicator for each day of the past 7 days. - -### 3.3 Notification settings - -Currently, we support email notification, and we will open various notification methods such as webhook, SMS and in-site notification. - -![通知设置](/img/customEvent/alert_5.png) - -Enter user email and press enter to confirm the entry, multiple emails can be entered. - -## 4. Warning Notification - -According to the time granularity set for each warning rule, the system will query the warning indicator at the end of each day or hour according to the set grouping. - -An email will be sent to the entered mailbox after the alert rule is triggered, as follows. - -![预警通知](/img/customEvent/alert_6.png) - -## 5. Alert Management -### 5.1 Alert List Management - -The created alerts are displayed as a list in the data alert page, and you can view, start/pause, edit, copy, delete, etc. - -![预警列表管理](/img/customEvent/alert_7.png) - -The progress of using data alert is based on "alert instance" as the most basic unit. One grouping under the alert rule is one "alert instance", the maximum number is 30, and the usage progress can be released by deleting the alert or unchecking the grouping. - -### 5.2 Alert details - -Click the alert name and alert rule to jump to the detail page, and filter the alert rule. - -![预警详情_1](/img/customEvent/alert_8.png) - -On the left side of the page, you can see the "Basic Information", "Alert Rules" and "Notification Settings" of the alert. - -On the right side of the page, you can see the historical trend graph and data table of the current filtered alert rules and grouping data, and the time when an alert occurred is marked in the trend graph as a red dot. - -![预警详情_2](/img/customEvent/alert_9.png) - -## 6. Best Practice - -### 6.1 Monitoring performance targets by "fixed value" alert - -For numerical performance targets, such as the amount of recharge and the number of active users, you can monitor whether the performance targets are achieved through "fixed value" alerts. - -### 6.2 Through the "percentage" monitoring indicators month-on-month and year-on-year changes - -For daily operation indicators, such as number of active users and number of new users, you can closely monitor their trends through "percentage" alerts, so that you can pay attention to them in time if there are abnormal trends. - - -### 6.3 Monitor short-term abnormal changes with long-term trend through "Standard deviation" alert - -For indicators with long-term growth or decay, "standard deviation" alert can eliminate the influence of long-term trends on short-term changes, so as to monitor the short-term abnormal changes of the indicator. - -### 6.4 Monitoring alert intervals - -For some important indicators, such as the number of active users, multiple alert rules can be created to monitor whether the performance target is achieved, the trend of change, etc. Two rules can be created for the same kind of monitoring rules, "high" and "low", to monitor the change interval of indicators. - -### 6.5 Multi-group alerting to detect abnormal data dimensions - -For some indicators that have changed abnormally, you can find the main dimensions that caused the change by splitting them into groups for further drill-down analysis. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/cluster.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/cluster.mdx deleted file mode 100644 index ef576ee72..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/cluster.mdx +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: User Clustering -sidebar_position: 9 ---- - -## 1. Overview - -User Clustering is a way to divide users who meet certain properties and behavioral characteristics into a group, and to study and analyze the group. - -In the analysis model of TapDB, "account" and "device" are used as query subjects, and "account" and "device" are also supported as subjects in user clustering. - -Currently, we support 3 types of user clustering: "Conditional Clustering", "ID Clustering" and "Result Clustering". - -![概述](/img/customEvent/cluster_summary.png) - -## 2. Applicable Scene - - -| Roles | Usage | -| ---------- | ---------------------------- | -| Analyst/business person | Segmenting specific user groups for focused analysis, troubleshooting, or exporting user lists, etc. | - -## 3. Creating User Clustering - -### 3.1 Conditional Clustering - -Select "New sub-group" and "New Condition group" in the upper right corner of the subgroup page. Please pay attention to the progress here. - -This page is divided into two parts: "basic information" and "Group rules". - -![新建条件分群](/img/customEvent/cluster_create_condition_cluster1.png) - -On the "basic information" page, enter or select "Group display name, Group, Group name, Update method, and Remark" in order. - -Group display name: displayed in the group list and analysis page, which is the basis for business personnel to identify subgroups. - -Group: support "account" or "device", choose according to the business scenario. - -Group name: It is the unique identification of the subgroup stored in the system background, and can be named as the parameter name with business meaning for the convenience of data analysts to query the database table directly. - -The update method is divided into "Manually" and "cycle". "Manually" means that the system will not automatically update the user group after the first calculation is completed, and users need to update manually; "cycle" means that the system will update the user group after 0:00 every day, taking the previous day as the base, and you can set the update delay to ensure that all the data of the previous day are received to ensure data integrity. - -![新建条件分群](/img/customEvent/cluster_create_condition_cluster2.png) - -In the "Group Rules" page, the rules are divided into two parts: "User attribute satisfaction" and "User behavior satisfaction", and you can switch the "and or" relationship between the two parts. - -In the "User behavior satisfaction" section, the attribute conditions are set based on the selected subgroup subject, and each attribute condition can switch the "and or" relationship. - -In the "User behavior satisfaction" section, it can be divided into two categories: "Never done event" and "done event", and both conditions can be added multiple times. - - -"Never done event" means that the user has not done the behavior in the selected time period. - -"Done event" means that the user has done the behavior in the selected event period, and the results of the behavior can be filtered. - -### 3.2 ID Clustering - -Select "New sub-group" and "Upload ID group" in the upper right corner of the subgroup page. - -![新建 ID 分群](/img/customEvent/cluster_create_id_cluster.png) - -Upload the ID file on the current page, and the system will correlate the IDs in the file with the existing user data in the system according to the selected "group subject" to find the eligible users. - -File format requirements: one ID per line, CSV file encoded in UTF-8 format. If there is an unmatched item in the upload (i.e. a user with this ID does not exist in the project), the item will be skipped and will not be included in the group. The template can be downloaded as a reference. - -### 3.3 Result Clustering - -In each analysis model, if the indicator is the number of users (e.g. "number of triggered users" in event analysis, retained or churned users in a segment of retention or funnel analysis), you can create groups by clicking "New result groups" in the result report. - -![新建结果分群](/img/customEvent/cluster_create_result_cluster1.png) - -You can set the name and remark of the resulting group. - -![新建结果分群](/img/customEvent/cluster_create_result_cluster2.png) - -You can't modify and update the rules of resulting group, you can only modify the name and remark. - -## 4. Various Operations - -The created clusters are displayed as a list on the user clustering page. - -![对用户分群的各类操作](/img/customEvent/cluster_operation.png) - -Users can view, edit, delete, update, download, and copy operations on the cluster, as follows. - -| Operation | Location | Effect | -| --- | --- | ---------------- | -| View | Name | View cluster Details | -| Edit | Action bar | Enter the Edit cluster popup | -| Delete | Action Bar | Delete cluster | -| Update | Action bar | Manually start the cluster calculation and update the cluster results | -| Download | Action bar | Download the list of users with the current cluster results| -| Copy | Action bar | Create a new cluster with the same parameters as the current one | - -## 5. Using User Clustering - -### 5.1 Focusing or excluding some users - -Focusing on high-value users who meet certain criteria, such as: those who pay more than $100, so as to target strong paying users in the analysis model and understand their behavioral characteristics. - -Excluding suspicious users that meet specific conditions, such as: devices that have been active on the same device for more than 3 accounts, you can exclude such studio devices without affecting the analysis results of normal users. - -### 5.2 User data import and export - -Import external user data into the system to create cluster, such as: the company already has a group of users and devices with high paying ability in other game projects, so it can be imported to explore their active and paying situation in this project and better guide potential high paying users to pay. - -Download the cluster results as the basis of operation in other systems, such as: exporting suspected studio devices, accounts, and then punishing and banning them in the game operation system. - -### 5.3 The analysis results of the drill-down analysis - -The result cluster is based on the report of the analytic model and is therefore well suited as a basis for drill-down analysis. - -For example, the funnel analysis calculates the funnel conversion of users browsing products, initiating orders, and paying for orders, and finds that a large number of users are lost after initiating orders. In this case, the result cluster of the lost users can be analyzed to explore the reasons why they initiated orders but did not pay successfully by analyzing the attributes and behaviors. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/data-model.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/data-model.mdx deleted file mode 100644 index 162b04165..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/data-model.mdx +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: Burial Point Design Guide -sidebar_position: 2 ---- - -## 1. Case Introduction - -Have you ever had the problem that a user has opened the game page, but before the user logs in to the account, some technical issue causes the user to churn. - -Here's a real-world example of how we used custom event analysis to complete the "user churn" process from opening the App to creating a role. -We designed our analysis as follows: - -**1. Be clear about the goal of analytics** : understand user churn before personas are created; - -**2. Clarify the process** : Break down the process from the user opening the App to creating a role: - -- Click on the game icon -- Unity initialization -- The Android Storage Permissions Allow screen appears -- SDK initialization (record point of "Add Device") -- The privacy agreement confirmation box pops up [select "No" to exit, SDK initialization fails]. -- Check version -- Confirm download button (in 4G environment) / Automatic download in WIFI environment -- Start downloading resources -- Resource downloading in progress -- When the resource download is complete, enter the login screen -- Click TapTap login button -- A pop-up window will appear, you can choose Tap to open or Tap Accelerator to open -- When the selection is complete, a pop-up will appear to recall the login authorization -- Click Agree and return to the game -- TapTap login completed (the record point of "Convert Device") -- Game server side to authenticate users -- Login to the game server -- Enter a nickname to create a character -- Create successfully and enter the game - -**3. Define the analysis indicators**: According to the above process, with funnel thinking, we designed several key indicators as: - -- Resource confirmation download rate (users who confirmed the download / users who updated the pop-up window). -- Resource download success rate (users who downloaded successfully / users who started downloading the resource). -- TapTap login rate (users who logged in successfully / users who clicked to log in). -- Character creation rate (users who successfully created a character / users who logged in successfully). - -**4. Explicit Event ** : In general, there are three types of event: - -| Event Type | Description | -| ---- | ----------------- | -| Exposure events | Exposure of XX pages, exposure of XX pop-ups | -| Click events | XX button clicks | -| System events | Initialization, version checking, resource download, etc. | - -In the previous step, we identified the analysis metrics. Next we determine the event name and event type: - -- Exposure: [Update Alert Pop-up Window] -- Click: [Update prompt pop-up window - Confirm download button] -- System Event: [Update Download Success] -- Click: [Home - TapTap Login Button] -- System Event: [Login Success] -- System Event: [Successful Character Creation] - -**5. Define event properties**. -Based on the event names and event types, we have compiled a more complete buried document as follows. - -| Event Name | Event Display Name | Property Name | Property Display Name | Property Value | Event Trigger Timing | -| ---------------- | ---------------- | ----------- | ----- | ---------------- | --------- | -| pv_download | Update alert pop-ups | #ts | Timestamp | | Triggered when pop-up window is displayed | -| click_download | Update prompt pop-up window, confirm download button | #networtype | Network Type | WiFi、4g、5g、3g、2g | Triggered when button is clicked | -| download_success | Update downloaded successfully | #ts | Timestamp | | System background trigger | -| login_start | Home - TapTap Login Button | #ts | Timestamp | | Triggered when button is clicked | -| login_success | Login successful | #ts | Timestamp | | System background trigger | -| create_role | Create Character | #ts | Timestamp | | Triggered when character creation is successful | - -With the above steps, we have completed the design of the buried point. - -Then we fill the buried point information (event name, event display name, attribute name, attribute display name) into TapDB's "Configuration" - "Event Management". - -Then develop according to the buried point document, and then verify the data. - -After the buried point is put on line, we use "Event" and "Dashboard" functions to analyze the process of "users losing users from opening the app to actually creating roles", as shown in the following figure. - -![](/img/customEvent/49e3e7c0d12cd20cdd4f4aed2c8d0044.png) - -The conversion rate of "Update Download Successfully (Step 3)" to "Home - TapTap Login Button (Step 4)" is very low, so we need to perform detailed data analysis. Click [TapDB User's Guide](/sdk/tapdb/features/custom-event/event-analyse) to see the detailed data analysis method. - -## 2. buried design ideas - -It is recommended to refer to the following steps for buried point design. -**1. Determine the analysis scenario**. -**2. Clarify the process**. -**3、Define analytics metrics**. -**4. Define the event: design the Event, determine its type, name and other details**. -**5. Define Event properties: specify the required information and use event properties for reporting**. - -## 3. Other design tips - -### 3.1 Similar abstraction of Event - -When designing events, you may encounter the following problems. - -1. To count the passing of three levels A, B and C, should we design a passing event for each level? -2. When designing the buried point of "apply for verification code" event, there is an event of "apply for verification code" in user registration, user login, password change and other scenarios. Do I need to design an event for each scenario? - -We can determine whether the same Event is set for different scenarios based on the same kind of abstraction principle: - -- Important events or events of special interest can be treated as separate Events. -- Events of general importance, such as those that only require analysis of the number of participants and counts, can be set up as a single event with multiple similar behaviors, and specific behaviors can be identified through properties. - -1. Events for different levels: if we count the passages of three levels A, B and C, instead of counting them three times, we only need to design a "level pass" event, and then use the attributes to distinguish the three levels A, B and C. 2. - -2. Buried function of "apply for verification code": you can define a "apply for verification code" event, and use the attribute value to distinguish the "scenes". - -### 3.2 Event naming convention - -When designing event display names, ensure that there is no ambiguity about the event. Naming the events in the form of "page name - module name - specific event name" can help analysts understand the events accurately. It is important to avoid incorrect analysis conclusions triggered by inaccurate descriptions. Maintaining uniformity in App page naming makes sense and helps us ensure that everyone's understanding is consistent. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/dimension-table.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/dimension-table.mdx deleted file mode 100644 index 3d321b94f..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/dimension-table.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Dimensional Table Properties -sidebar_position: 13 ---- - -## 1. Overview - -For event properties and user properties that have already been reported, the originally uploaded data can be mapped to another presentation or calculated value by uploading a dimension table so that the initial buried property value is not the same as the presentation value. - -Compared to virtual properties, dimension table properties can bring in information from outside the system that is not contained in the originally uploaded data, instead of just logical transformation based on data within the system. - -## 2. Applicable Roles and Uses - -| Role | Usage | -| :------------------------ | :------------------------------- | -| Burial Designer (Data Product Manager / Analyst) | Avoid excessive redundant fields in burial design, improve data model paradigm and burial design flexibility | -| Data Analysts (Data Product Managers / Analysts / Operators) | Self-supporting the introduction of external system data as analysis dimensions to meet more personalized analysis needs | - - -## 3. Creating Dimension Table Properties - -You can create a dimension table property in the "Dimension Table" column of Event Property Management and User Property Management. - -Dimension table properties can be created based on preconfigured properties, custom properties and virtual properties, but not the following types of virtual properties. -1. virtual attributes created based on dimension table attributes. -2. virtual event attributes created based on user attributes. - -![Create dimension table attributes](/img/customEvent/dimension_table_1.png) - -### 3.1 Uploading - -Select the "Upload" dimension table property on the base property field where the dimension table needs to be added. - -![Upload Inlet](/img/customEvent/dimension_table_2.png) - -### 3.2 File encoding requirements {#encoding} - -The system supports `CSV` files in `UTF-8` or `UTF-8 with Bom` encoding format, with a file size of no more than 2G. - -When using Excel, select the "**CSV UTF-8 (comma separated) (.csv)**" format when saving the document. - -![Excel](/img/customEvent/dimension_table_7.png) - -When using WPS, select the "**CSV (comma separated) (*.csv)**" format when saving the document. - -![WPS](/img/customEvent/dimension_table_8.png) - -The file can also be saved in `UTF-8` or `UTF-8 with Bom` encoding format using text editing tools such as Sublime, NotePad ++, etc. - -![Sublime、NotePad ++ 等](/img/customEvent/dimension_table_9.png) - -### 3.3 File Format - -The first row content will be used as the field name of the dimension table property, which can only contain English, numbers or `_`, and needs to start with English. - -The first column content will be associated with the original attribute, and the first column value needs to correspond to the original attribute value. If duplicate values are encountered, the first column will prevail and subsequent duplicate information will be discarded. - -The dimension table properties do not exceed 10 columns, and the content will be adapted to the data type of the property entered, with the following rules. - -| selected data type | file content | -| :----- | :----------------------------------------------------------- | -| Text | Any content is allowed | -| Numeric | Any number, starting with 0 will remove the 0. -| time | timestamp, or yyyy-MM-dd HH:mm:ss.SSS, yyyy-MM-dd HH:mm:ss, yyyy-MM-dd | -| boolean | true or false | - -For rows where the selected data type does not match the file content type, the row will be discarded completely, so please take care to keep the data type consistent. - -### 3.4 Fill in the display name and field type - -Fill in the display name and data type of the mapped dimension table fields as required. - -![Fill in display name and field type](/img/customEvent/dimension_table_3.png) - -### 3.5 Parsing Results - -Display the total number of rows parsed, the number of successful rows, the number of rows discarded in error and the reason for the error discard. - -![parsing results](/img/customEvent/dimension_table_4.png) - -### 3.6 Replacement - -If the parsing result is not as expected, the file can be updated and replaced. - -![Replace](/img/customEvent/dimension_table_5.png) - -## 4. Use of dimension table properties - -### 4.1 Managing dimension table properties - -Dimension table properties can be managed in the Event Property Management or User Property Management pages. - -Dimension table properties are collapsed in the base property column and can be expanded for further manipulation. - -! [Manage dimension table properties](/img/customEvent/dimension_table_6.png) - -### 4.2 Considerations for use in models - -Dimension table properties are used in the same way as usual properties, with their calculation logic and filtering conditions determined by their type. - -Dimension table properties of event properties can be used in the events associated with their base properties. - -Dimension table properties of user properties are used in the same scenarios as general user properties. - -## 5. Best Practice - -### 5.1 Improve buried design flexibility - -When collecting users' browsing and ordering information from the game store, only product IDs need to be collected; information such as product names and selling prices can be used to create dimension table properties through "product IDs" to meet analysis requirements. - -| Product ID (Base property) | (Dimension table property) | Price (Dimension table property) -| :---------- | :------ | :-------- | -| 1 | Refill Ticket | 50 | -| 2 | World Channel Speakers | 10 | - -### 5.2 Introduction of external information as analysis dimension - -The system collects the model information of user devices, combined with the information of the selling price of each model in e-commerce platforms collected through crawlers, so as to get the grade of each model, which is used to identify the value of users. - -| Model (basic property) | Price (dimension table property) | User value (dimension table property) -| :------- | :-------- | :---------- | -| model_1 | 999 | low | -| model_2 | 1299 | Low | -| model_3 | 1999 | medium | -| model_4 | 4999 | high | diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/distribution-analyse.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/distribution-analyse.mdx deleted file mode 100644 index 09304aec0..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/distribution-analyse.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Distribution Analysis -sidebar_position: 6 ---- - -## 1. Case Introduction - -Player payment is a very important in-game behavior, and we want to know the distribution of the total amount paid by players in the game in the recent week. For example, we want to know the number of users in the range of $0-$100, $101-$200, $201-$300, etc. At this time, we can use the function of "Distribution Analysis" to show it. - -**1. Setting the event**: we select the event "user pays" and set a custom interval, using 100 as a segment. - -![](/img/customEvent/distribution/fenbu-1-1.png) - -**2. Setting the dimension**: we focus on the daily change of the amount distribution, so we choose "time of event" in the dimension. - -![](/img/customEvent/distribution/fenbu-1-2-2.png) - -**3. Set display results**: set the time period and other parameters, and click query to view the analysis results. - -![](/img/customEvent/distribution/fenbu-1-2-3.png) - -**4、Save report**: save the query results as a report, and then create a dashboard based on the report. A dashboard makes it easy to view analysis results. - -![](/img/customEvent/distribution/fenbu-1-4.png) - -## 2. What Is Distribution Analysis - -Distribution analysis function is mainly used to understand the frequency of events in different zones, the cumulative sum of different event calculation variables, and the distribution of the number of users in different zones such as the length of page views. - -## 3. Use Cases For Distribution Analysis - -Typical user cases of funnel analysis: - - -- Did the number of users playing the game per day increase after the new version was launched? -- What are the differences in the number of users from different advertising channel sources in different amount ranges, e.g. range of $0-100, $101-200, $201-300. -- What are the differences in the distribution of hours played by different levels of players (new players, average players, heavy players). - - -## 4. How To Do Distribution Analysis - -There are 4 steps in distribution analysis: setting events, setting dimensions, setting display results, and saving reports. - -The last three steps are described in detail in Event Analysis, so here we focus on setting events. - -On the distribution analysis page, click "Select Indicator" to see the indicator selection screen. Click Event Analysis to see how to set indicators. Click the "Settings" button to set the display type of calculation results as "Discrete", "Default interval" and "Custom interval". - -* **Discrete**: the system will display the distribution values under each number. -* **Default interval**: the system will display the default interval distribution based on the calculation results. -* **Custom interval**: supports setting the starting and ending values of each interval segment. - -![](/img/customEvent/distribution/fenbu-1-1.png) - -## 5. Distribution Analysis Calculation Principle - -There are 2 types of statistics for distribution analysis, statistics by count and statistics by event attributes. - -**Statistics by number**: statistics on the number of times a user performs an action in a day/week/month. - -**Statistical Indicators by Event Properties**: Statistics on the value of a statistical indicator for properties of an event that occurred in a day/week/month. The statistical indicators for the properties are the same as for the event analysis, with sum, mean, maximum, minimum, and de-duplicated numbers. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/event-analyse.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/event-analyse.mdx deleted file mode 100644 index eab61c033..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/event-analyse.mdx +++ /dev/null @@ -1,272 +0,0 @@ ---- -title: Event Analysis -sidebar_position: 3 ---- - -## 1. Case Introduction - -We often need to analyze "New Users" and "DAU", and we can use the Event Analysis function to analyze these two indicators. - -**1. Determine the indicators and screening criteria**: To analyze DAU and new users, display them on both iOS and Android. - -Click on "Select Indicator", in the pop-up window we select the event and filter criteria, and rename the indicator (we can also set the filter criteria in the main screen of event analysis). - -![](/img/customEvent/event/event-2.png) - -**2. Determine the dimension**:The iOS and Android platforms are the dimensions we analyze, so we select "Device System Type" in " -Select dimension". - -![](/img/customEvent/event/event-3.png) - -**3. Select time period**:We need to focus on the last 30 days of data changes, so select "Past 30 day" in the date selector. - -![](/img/customEvent/event/event-4.png) - -**4. Select a comparison time period**:We use the last 30 days of data changes, compared to the previous month. -![](/img/customEvent/event/event-5.png) - -After setting the conditions, click "Inquire" to output the results. - -![](/img/customEvent/event/event-6.png) - -**5. Save Report**:We save the query results as a report. The reports are then transformed into a dashboard, which makes it easy to see the final report data every day. - -![](/img/customEvent/event/event-7.png) - -**These are the steps to view daily users through event analysis**。 - -## 2. What Is Event Analysis - -Event Analysis is to analyze the what, how, when, and where of an event. - -## 3. Use Cases For Event Analysis - -1. Number of people: how many people triggered the event. - -- Trends in UV and DAU; -- For example, the description of a button is adjusted, before the adjustment is A, after the adjustment is B. Conduct a control test to analyze the click situation of A and B. - -2. Times: how many times a certain event was triggered, such as the number of passes on a level per day. - - -3. Per capita times: how many times an event (behavior) is triggered on average. - -4. Active ratio: the ratio of the number of people who trigger an event to all active people in a time interval. - -5. Data under the event segmentation dimension: for example, by device type, we can see how many button clicks happened on different devices. - -6. Four calculations for complex indicators: What happens when the data we want isn't in the reported data? We can add, subtract, multiply and divide indicators to get a new indicator, this way is called custom indicator, such as ROI, ARPU. - -## 4. How To Do Event Analysis - -Just like the 5 steps mentioned in the case above: - -**1. Determine the indicators and screening criteria** - -**2. Determine the dimension** - -**3. Select time period** - -**4. Select a comparison time period** - -**5. Save Report** - -### 4.1 Select Indicator - -When determining the Indicator, it is also necessary to determine whether to enable "Approximate calculation". - -#### 4.1.1 Approximate Calculation - -When the Approximate calculation is turned on, only some of the samples are queried, with 99.9% accuracy and faster query speed. - -![](/img/customEvent/event/event-2-1.png) - -You need to click the "Inquire" button to make it effective. - -#### 4.1.2 Select Indicator - -In the Select Indicator screen, it includes "Select Event", "Select Property", "Rename", "Copy indicator -", "Switch into indicator formula" and "Indicator screening" functions, as shown below: - -![](/img/customEvent/event/event-2-2.png) - -**Select Event**:Drop-down option to select all pre-set events and custom events. - -**Select Properties or Indicator Name"**: The drop-down option displays the analysis dimensions and Properties of the event, as follows. - -![](/img/customEvent/event/event-2-3.png) - -A. Analysis indicators: any event has at least these three analysis indicators: total number of times, number of triggering users, number of times per capita. - -B. Properties: event properties. - -C. Properties analysis indicators: According to the Property value type, the following analysis indicators can exist: - - -| Value Type | Analysis Perspective | -| ------------------------------------------------------------- | ------------------------- | -| Number | Total, median, mean, maximum, minimum, per capita, deduplication | -| List | Deduplicating lists, deduplicating list elements | -| Boolean | Deduplicated, true, false, null, not null | -| Non-numeric, Boolean | Deduplicated | -| Time (supported formats are `yyyyy-MM-dd HH:mm:ss` or `yyyyy-MM-dd HH:mm:ss.SSS`) | Deduplicated | - -**Rename **: Take care to use an easy-to-understand description. - -**Switch into indicator formula**: Click to switch indicator formula. This feature is useful for special scale analysis scenarios, such as: - -- The ratio of active users that day to active users that month. In this scenario, the number of active users in the month is used as the denominator to participate in the calculation. -- The ratio of the number of paying users for the day to the number of active users for the selected time frame. In this scenario, the total number of active users in the selected time range was used as the denominator to participate in the calculation. - -For example, we build a pay rate indicator by using the indicator formula - -- A custom indicator named "Paid Rate". -- Set the event and indicator formula "user payment - number of triggered users" / "account login - number of triggered users". -- After setting the metric formula, we can choose "percentage", "two decimal" and "integer" presentation styles, we choose two decimal. - -![](/img/customEvent/event/event-2-4.png) - -#### 4.1.3 Indicator Screening - -It is used to set the restrictions of indicators. You can set the filter for individual indicators in the "Indicator Selection", or set the global filter in the main event analysis screen. - -![](/img/customEvent/event/event-2-5.png) - -##### 4.1.4.1 Screening Data Types - -There are 5 types, each supporting a different mathematical logic. - -1. Number. e.g.: recharge amount. Support mathematical logic: equal to, not equal to, less than, greater than, with value, without value, interval. - -2. String. For example: the city where the event occurred. Support mathematical logic: equal to, not equal to, contains, does not contain, has value, no value, regular match. - -3. List ID. e.g.: list. Support mathematical logic: presence element, non-existent element, element position, with value, without value. - -4. Time. e.g.: registration time (yyyy-MM-dd HH:mm:ss.SSS or yyyy-MM-dd HH:mm:ss). Support mathematical logic: absolute time, relative to the current time, relative to the moment of the event, with value, without value。 - -5. Boolean. e.g.:: Wifi use. Support mathematical logic: true, false, with value, without value. - -##### 4.1.4.2 The difference between equal to / not equal to and including / not including - -Equal/Not equal: filter items that strictly match the selected value. For example, if the sub-continent is equal to America, then only the data for the Americas will be queried. - -Includding/Not including: the filter item contains the selected value can be filtered, such as the subcontinent contains the Americas, then the data of the Americas, North America, South America can be filtered out. - -Absolute time: refers to the objective real time. For example, 2021-03-02 19:24:52 to 2021-03-08 19:24:52, the former time must be before the latter time. Click "Select time" to select seconds. - -![](/img/customEvent/event_analyse_time_filter.png) - -Relative current time: The last n days relative to the present. - -![](/img/customEvent/event_analyse_relative_time_1.png) - -Relative event time: The n days before and after the selected event. A negative number can be selected here, such as -1 day, which means the day before the selected event, and a positive number, such as 1 day, which means the day after the selected event. - -![](/img/customEvent/event_analyse_relative_time_2.png) - -##### 4.1.4.3 "And" & "Or" - -Multiple filters can be added. When there are at least 2 or more filters, the And / Or toggle button will appear, and the default is "And". "And" is the intersection of multiple filters, and "or" is the concatenation of multiple filters. - -After selecting the filter, click the query button to take effect. - -### 4.2 Select Dimension - -#### 4.2.1 Dimension - -A dimension is a decomposition of facts that splits metrics to observe patterns: - -"Country where the event occurred" is a dimension that groups the facts by the country where they occurred, thus observing the pattern of the indicator; - -The "age of the user base" is a dimension that looks at payment data for users in different age groups; - -"Channel package" is a dimension that looks at the retention data of users under different channel packages; - -Time is the most fundamental dimension. - -#### 4.2.2 Classification Of Dimensions - -The dimension drop-down allows you to select event properties, user properties, and user clusters: - -- Event properties: describe the state at the time of the event, e.g. : How much did the user charge in the United States? Here we query for payment events that occurred in the United States (the user who charged the money may not be in the United States right now). -- User properties: describe the state of the user that triggered the event. For example: How much money are users in the US currently charging? Here we query for paid events triggered by users who are now in the United States (including those who were not in the United States). -- User clusters: users belonging to the selected subgroup. - -![](/img/customEvent/event/event-2-6.png) - -Differences in data logic: - -- Event properties: Parameters for each event. For example, the "Mid-Autumn Festival" event is transmitted through the SDK, which has three properties: "the number of Mid-Autumn Festival props", "the type of zongzi" and "the number of dragon boats purchased by the user". When selecting the dimensions of an event property, the available dimensions are constrained by the selected event, for example, if the event is selected as "Mid-Autumn Festival", the event property can only select these three. -- User attributes: Fields in the user table. User attributes are not constrained by the selected event. - -When the selected dimension is a numeric field, such as payment amount, there will be the selection of interval. The discrete interval means that the aggregation query is carried out according to all the values of the field. The default interval is the aggregation interval set by the system according to a certain rule, and the custom interval is that the customer can define the interval segment to aggregate data. - -![](/img/customEvent/event_analyse_custom_range.png) - -When the selected dimension has "event occurrence time", the time can be selected to support by day, hour, minute, week, month. - -![](/img/customEvent/event_analyse_time_granularity.png) - -### 4.3 Select Date - -Select the date we care about. - -![](/img/customEvent/event/event-2-7.png) - -### 4.4 Select a comparison date - -For example, in the example below, if the selected date is 2021-3-2 to 2021-3-8 with an interval of 6 days, then the comparison date interval is also 6 days. When selecting a comparison date, you only need to select one date (e.g. 2021-02-23) and the system will automatically project back 6 days (to 2021-03-01). - -![](/img/customEvent/event_analyse_time_compare.png) - -### 4.5 Save Report - -After selecting the main dimensions and indicators, click Save Report to save the report, enter the name and confirm, then the report is saved to Saved Reports. - -![](/img/customEvent/event_analyse_save_report_1.png) - -After saving a report for the first time, when you open the report again, the "Save As" button will appear and you can save it as a new report, if you click "Save Report", you are updating the original report. - -![](/img/customEvent/event_analyse_save_report_2.png) - -The saved reports are displayed in My Reports on the right side of the screen. - -![](/img/customEvent/event_analyse_save_report_3.png) - -## 5. Pivot Table - -A pivot table is an interactive form of data reporting that features calculations related to the arrangement of data and pivot tables. It is called a pivot table because the order of the aggregated dimensions of the data can be dynamically changed, thus allowing customers to view the data from multiple perspectives. Below we compare the following two pivot tables. - -![](/img/customEvent/event_analyse_pivot_table_1.png) - -![](/img/customEvent/event_analyse_pivot_table_2.png) - - -In the first table, we want to query the sum of the recharge amount of users in each province, each device system and each network operator in China. From the perspective of "province", the data of "equipment system" is split, and then the data of "network operator" is split. - - In the second table, we want to query the total recharge amount of users in each network operator, each device system, and each province. From the perspective of "network operator", the data of "equipment system" is split, and then the data of "province" is split. - -### 5.1 Draggable - -Each column of a pivot table can be dragged and dropped with the mouse to change its order. After the dimensions are dragged out of order, the data re-enters the query. Flexible use of drag and drop can improve the efficiency of data query. Currently, pivottables can be ticked up to 5 dimensions and 20 indicators. - -### 5.2 Search - -In the dimension header of the pivot table, you can click the search input text, and the search function can help quickly filter out the eligible items. The query is not re-entered after filtering. - - -![](/img/customEvent/event_analyse_table_filter.png) - -### 5.3 Sort - -Default sorting logic for pivottables: - -Firstly, the total data of the first column of the main dimension index was sorted from large to small. - -Then, the total data of the second column of the main dimension index is sorted from large to small. - -And so on, sort the totals for the NTH main dimension indicator from large to small. - -### 5.4 Download Data - -Click the download button on the right side of the pivot table to download the tiled data, and the downloaded result is the current query result. What you see is what you get. Available in CSV/PDF/picture format. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/funnel-analyse.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/funnel-analyse.mdx deleted file mode 100644 index be4031bd9..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/funnel-analyse.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: Funnel Analysis -sidebar_position: 5 ---- - -## 1. Case Introduction - -In [buried design guide](/sdk/tapdb/features/custom-event/data-model), we designed the buried process based on the transformation process from "user opening the app to actually creating a persona", and here we analyze this transformation process with the "funnel analysis" function. - -**1. Set up steps**: create events for the entire process from the user opening the App to the actual creation of the role, and set up funnel steps in the order of the user's actions. - -![](/img/customEvent/funnel/案例-1.png) - -**2. Setting the dimension**: different system types may have an impact on the conversion situation, so select "device system type" in the dimension. - -![](/img/customEvent/funnel/案例-2.png) - -**3. Setting the funnel period**: we set the funnel period to 7 days. - -![](/img/customEvent/funnel/案例-3.png) - -**4. Set display results**: set up time periods and other parameters and click on the query to view the analysis results. - -![](/img/customEvent/funnel/案例-4.6.png) - -**5. Save report**: Save the query results as a report, and then create a dashboard based on the report. A dashboard makes it easy to view analysis results. - -![](/img/customEvent/funnel/案例-5.2.png) - -## 2. What Is Funnel Analysis - -Funnel analysis is to analyze the conversion rate of users from the starting point to the end point, which is used to measure the conversion effect of each node. In an ideal situation, the user will follow the path of the product design to reach the final target event, but the reality is that the user's behavior path is diverse. - - -By configuring the critical business path through buried events, it can analyze the user conversion and churn. This function can not only identify potential problems with the product, but also locate the loss of users in each link, so as to facilitate the subsequent conversion through product means or marketing means. - -## 3. Use Cases For Funnel Analysis - -Typical user cases of funnel analysis: - -- There is a lot of traffic, but few registered users. What part of the process is the problem? -- What is the overall conversion rate from sign-up - character creation - play - pay? -- What are the differences in conversion rates between regions? -- Two promotion channels bring different users. Which channel has the highest conversion rate? - -## 4. How To Do Funnel Analysis - -There are 5 steps in the funnel analysis. - -**1. Set up steps** - -**2. Setting the dimension** - -**3. Setting the funnel period** - -**4. Set up display results** - -**5. Save report** - -### 4.1 Setting the funnel - -On the Funnel Analysis page, click "Set Steps" to see the "Add Funnel Step" screen. - -![](/img/customEvent/funnel/exp/1-漏斗分析-设置漏斗.png) - -In the "Add Funnel Step" screen, you can select a funnel step and add a funnel step by. - -1. Step: consists of an event (one or more filter conditions can be added) that represents a critical step in a conversion process. - -A funnel contains at least 2 steps, each containing one event. More steps can be added, and the order of the steps can be changed by dragging the serial number in front of the step. - -It is also possible to set filter conditions for the steps. You can also set global filtering in the main interface of funnel analysis. - -2、Add steps: Add more steps to the funnel to be analyzed. - -![](/img/customEvent/funnel/exp/2-漏斗分析-设置漏斗.png) - -### 4.2 Set Dimension - -In the dimension drop-down box, the display includes event attributes (Step 1), user attributes, and user subgroups. - -![](/img/customEvent/funnel/exp/3-漏斗分析-选择维度.png) - -### 4.3 Set the funnel cycle - -**Funnel Cycle**: from the time the user triggers Step 1, the completion of the subsequent steps within the window period counts as the conversion of the subsequent steps. - -![](/img/customEvent/funnel/exp/4-漏斗分析-漏斗窗口期.png) - -**Limiting the window period within the time interval**: only within this time frame, the user travels from the first step, to the last step, is considered a complete funnel conversion. - -### 4.4 Set Display Results - -You can select the range of analysis steps, and according to the "Comparison/Trend" and "Conversion/Churn" settings, you can get 4 types of reports: Conversion Comparison Table, Churn Comparison Table, Conversion Trend Table, and Churn Trend Table. - -![展示结果](/img/customEvent/funnel_analyse_result_type.png) - -Conversion comparison table: to analyze the cumulative conversion rate from step 1 to the subsequent steps. - -![转化对比表](/img/customEvent/funnel_analyse_table_1.png) - -Churn comparison table: to analyze the churn rate between each step. - -![流失对比表](/img/customEvent/funnel_analyse_table_2.png) - -Conversion trend table: to analyze the trend of conversion rate by date. - -![转化趋势表](/img/customEvent/funnel_analyse_table_3.png) - -Churn trend table: to analyze the trend of attrition rate by date - -![流失趋势表](/img/customEvent/funnel_analyse_table_4.png) - -### 4.5 Save to Dashboard - -Save the query results as a report, and then create a dashboard based on the report. A dashboard makes it easy to view funnel analysis results. - -![](/img/customEvent/funnel/exp/5-漏斗分析-保存报表.png) - -## 5. Funnel Analysis Principle - -The funnel analysis will be explained in detail next, especially when there are grouping and filtering cases, the calculation principle will be more complicated. - -### 5.1. Basic calculation principle - -Suppose a funnel consisting of steps 1, 2, 3, 4, and 5 is selected for the time range March 1, 2021 to March 7, 2021, with a window of 3 days. If a user triggers step 1 from March 1, 2021 to March 7, 2021, and triggers steps 2, 3, 4, and 5 in sequence within the 3 days of step 1, the user is considered to have completed a complete funnel transformation, and if steps 1 > 2 > 4 > 5 are triggered in sequence, the user has completed only step 1 > 2. - -If the steps are interspersed with some other steps, such as 1 > X > 2 > X > 3 > 4 > X > 5 (where X represents other events), the user is still considered to have completed a complete funnel transformation. - -When a user has multiple events in the selected time period that all meet the definition of a certain conversion step, the event closer to the final conversion goal is preferred as the conversion event, and the conversion calculation stops when the final conversion goal is reached for the first time. - -Assuming that the steps of a funnel are defined as: Browse Mall, Select Prop, Generate Order, Pay Success, then the sequence of different user behaviors and the actual conversion steps (bolded parts) are as follows. - -Example 1: **Browse mall** > Select prop (prop B) > **Select prop (prop A)** > **Generate order** > Pay successfully - -Example 2:Browse the mall > Select prop (prop B) > **Browse the mall** > **Select prop (prop A)** > **Generate order** > **Payment success** - -Example 3:Browse mall > Select prop (prop B) > **Browse mall** > **Select prop (prop A)** > **Generate order** > **Payment successful** > Select prop (prop A) > Generate order > Payment successful - -The numbers presented in the funnel analysis represent the number of unique users converted/lost, not the number of events triggered. Within this time frame, even if a user completes the funnel multiple times, it is only counted once. - -### 5.2 Grouping and Screening - -The grouping and filtering of funnel analysis are based on the users who have completed conversions/lost. -Grouping and filtering based on user attributes and user subgroups: Grouping and filtering based on user attributes and user subgroups based on users who have completed conversion/lost. -Grouping and filtering based on event attributes: Grouping and filtering based on the event attributes of the user in step 1 based on the users who completed conversion/lost. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/kanban.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/kanban.mdx deleted file mode 100644 index 421fd46b4..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/kanban.mdx +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Dashboard -sidebar_position: 8 ---- - -## 1. Overview - -Dashboard is a collection of multiple reports. After saving the constructed indicators, retention, funnels, and so on as reports, you can add them to the dashboard to help you monitor daily data. - -![概述](/img/customEvent/kanban_summary.png) - -## 2. Using Dashboard For Data Analysis and Collaboration - -In this section, we will use the demo project as an example to demonstrate the process from creating a new dashboard to collaborating through dashboard. - -### 2.1 Dashboard Page - -![进入看板](/img/customEvent/kanban_layout.png) - -The dashboard page is divided into three parts: "Directory, Settings, and Report Display". - -1. Directory: you can create folders to view self-built dashboard or shared dashboard of team members. -2. Settings: setting items include adding reports, setting sharing, adjusting settings, refreshing dashboard, setting global filtering, etc. -3. Report Display: used to display each report information. Support drag and drop reports to sort, support custom size, support chart display. - -### 2.2 New Dashboard - -Now, we need to form some core indicators into daily reports, so aggregate the daily core indicators reports in dashboard. - -![新建看板](/img/customEvent/kanban_create_1.png) - -![新建看板](/img/customEvent/kanban_create_2.png) - -We click "+" in the upper right corner of the left column of the dashboard page, select "Add a board" and name it "Daily Board". - -### 2.3 Edit, Rename, Delete Dashboard - -![编辑、重命名、删除看板](/img/customEvent/kanban_operation.png) - -Dashboard that already exist can be edited or deleted. - -### 2.4 Manage Dashboard With Folders - -Folders are used to organize the dashboard. We can create, rename, and delete folders, and the system has built-in "ungrouped" folders and "shared to me" folders. - -![文件夹](/img/customEvent/kanban_create_folder.png) - -Click "+" at the top right corner of the dashboard page, select "Add a folder" and name it "Game Operation" to store the game operation related dashboard. - -![移动](/img/customEvent/kanban_move.png) - -We can move the previously created dashboard "Daily Board" to the folder "Game Operation". - -### 2.5 Adding Reports to Dashboard - -Reports are the basic elements of dashboard, and we can create new reports in event analysis, retention analysis, funnel analysis and other analysis model functions. - - -In order to meet the needs of daily reporting, we build reports such as the number of logged-in accounts and app launch devices in event analysis, and reports such as user app launch 7-day retention in retention analysis, and now we add these reports to the dashboard. - -![添加](/img/customEvent/kanban_add_report_1.png) - -Click the "Reports" button at the top right of the dashboard and click "+" to add a report. - -![添加](/img/customEvent/kanban_add_report_2.png) - -Click "+" to add a report to the current dashboard. - -### 2.6 Setting Reports - -![设置](/img/customEvent/kanban_setting_1.png) - -![设置](/img/customEvent/kanban_setting_2.png) - -For the reports added to dashboard you can adjust the report size, visualization, time filtering, display indicators, display groups, and other information. - -You can sort by group indicator value or group name, and set it to check "Top N items". The dashboard will then sort and dynamically change the selected groups based on the data at the time of query. - -![看板](/img/customEvent/kanban_function.png) - - -Set the "Number of active accounts and active devices" window size to small, so that we can quickly see the traffic situation of the day. - -Set the "Number of active devices by country" window size to medium and select the chart type as "Trend chart", so that you can observe the recent trend of the number of active devices by country. - -Set the "Active User Distribution by System" window size to medium, and select the chart type as Distribution, so that you can observe the distribution of users by system. - -Set the "App Launch 30-Day Retention" window size to large, so that you can observe the retention data at the same time. - -### 2.7 Setting Dashboard - -After adding the report to the board, we can update the board and set the "Approximate calculation or not". - -![设置看板](/img/customEvent/kanban_refresh.png) - -The dashboard is updated regularly by default. The system regularly refreshes the calculation results for the dashboard every day and caches the results for the next view. It is recommended to turn on regular update for scenarios such as "more reports loaded" or "more calculations" in the dashboard to improve the display efficiency. - -Dashboard can be set to update in real time, suitable for indicators that need to be refreshed in real time, such as the current day's ad placement data. - -Dashboard defaults to exact calculation, meaning that each calculation will calculate the exact value according to the conditions. If you have a need for query efficiency, you can turn on the "Approximate calculation" option, which will adopt the approximate algorithm for data such as the number of triggered users, the number of times per capita, the average value of people, the number of de-weighting, etc., which can greatly reduce the performance overhead and the calculation time. - -For the "Daily Dashboard" created by the current demo, we focus more on the recent trends of the main indicators, so we choose to update it regularly at 1am every day and turn on the approximation calculation. - -### 2.8 Shared Dashboard - -If you want other team members to be able to view the newly created dashboard and even maintain and update it together, you can share the permission to other team members. - -The shared permissions are divided into two categories: shared and visible. The two categories are independent of each other and can be granted separately. You can grant shared nd visible permissions to "all users", so that any member who joins the project will have the permission and does not need to update it frequently. - -![共享看板](/img/customEvent/kanban_share.png) - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/meta-data.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/meta-data.mdx deleted file mode 100644 index e5e4ac0c0..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/meta-data.mdx +++ /dev/null @@ -1,173 +0,0 @@ ---- -title: Metadata Management -sidebar_position: 10 ---- - -## 1. Overview - -"Metadata Management" is the data management module in the system, which is the place to unify the management of metadata. - -TapDB adopts metadata pre-registration mode. Before receiving the data reported by SDK, the events and attributes to be collected must be registered into the corresponding function module, and when receiving the data, the system needs to check the metadata according to the pre-registered metadata, and the data that meet the conditions will be stored, and the data that do not meet the conditions will be rejected. - -This model can effectively improve the accuracy of data, and this model can solve the problem of inconsistent data reporting and data storage. - -Metadata management consists of 3 parts: event management, event attribute management and user attribute management. - -## 2. Applicable Roles and Uses - -| Roles | Usage | -| ----------- | -------------- | -| Administrator | Entering buried scenarios and managing system metadata | -| Business person | View metadata and understand business implications of data | -| Client / Front End Engineer | View buried requirements | - - -## 3. Event Management - -"Event" is the basic analysis object of various data analysis models in the system, and the system has built-in "preset events" and provides the function of reporting "custom events". - -In the "Event Management" function, you can pre-register "custom events" before reporting, and manage "events" from various aspects. - -Developers can check the pre-registered custom events that have been reported as "No" and perform buried development. - -![事件管理](/img/customEvent/metadata_event_overview.png) - -### 3.1 Concepts Explanation - -The "concepts" and "explanations" related to "event" are as follows. - -| Concepts | Explanation | -| ---- | ---- | -| Event name | Unique identifier of the event. | -| Display name | Display name in the analysis model. | -| Description | Describe the information of buried points, such as: "trigger timing" to help technical staff more accurate buried points; "business connotation" to help business people more in-depth understanding. | -| Event types| Event types are divided into "preset" and "custom" categories:
    **Preset**: system built-in events, widely applicable to all kinds of game projects, only need to turn on the buried switch in the SDK to report
    **Custom**: custom-created events to meet the personalized needs of game projects, need to be entered in the event management before reporting.| -| Whether to report | Whether the event has been reported. | -| Receive Switch | The switch to receive event reports or not. | -| Status | There are four states: "Normal", "Hidden", "Deleting", and "Deleted".
    **Normal**: Events that are in a normal state.
    Hidden: Not displayed in the analysis model.
    **Deleting**: After the reported data is deleted, the event will enter the "Deleting" status, and the deletion can be withdrawn within 72 hours. During this period, the data is still received but not displayed in the analysis model.
    **Deleted**: Deleted: After the reported data is finally deleted, the events will be recorded in the metadata management with the status of "Deleted", no more data will be received, no more will be displayed in the analysis model, and the progress of using custom event data will not be occupied. | -| Event properties | You can choose to bind event properties in New and Edit. Event properties are divided into two categories: "preset" and "custom":
    **Preset**: The newly created events are in the "debug" state by default and are not displayed in the analysis model.
    **Custom**: Customized event properties to meet the personalized needs of game projects .| -| Data limit | To ensure system performance, the number of custom events in "Normal", "Hidden", and "Deleting" states must not exceed 100. "Deleted" custom events do not occupy the usage progress, and you can delete custom events to release the usage progress. | - -### 3.2 Create Custom Events - -![创建自定义事件](/img/customEvent/metadata_event_create2.png) - -Fill in the basic information. By default, all preconfigured properties are associated, and by default, all custom properties are not associated. - -Select the custom properties that need to be associated, create new custom event properties if they do not meet the requirements, and untie the preconfigured properties that do not need to be associated. The creation of custom events is completed. - -### 3.3 View, Edit, Delete Events - -![查看、编辑、删除事件](/img/customEvent/metadata_event_edit.png) - -Click on the event name to see the basic information and all associated properties. - -In the action bar, you can edit the basic information and change the association with the event properties, but the event name cannot be edited for reported events. - -In the action bar, you can delete existing custom events. If a reported event has occurred, it will enter the state of "deleting" and can be revoked within 72 hours. Events that have not been reported will enter the "Deleted" status and will no longer receive data and be displayed in the analysis model, while releasing the progress limit of custom event data usage. - -### 3.4 Q&A - -Q1:Why some events are deleted without keeping any records, while some events are deleted and left in the metadata management with "deleted" status? - -A1: Events that have not been reported do not keep records, while events that have been reported are kept. During the design process of buried points, business people may change the design of buried points many times due to requirement changes, requirement merging, etc. Therefore, before the act of reporting, it can be considered that all the entry acts are in "draft", while once the data is reported, it is equivalent to the final draft, and we hope that the system can faithfully record all the data collection solutions used in the project. - -## 4. Event Property Management {#event-props} - -"Event Attributes" is the attribute information used to describe "events", TapDB has built-in "Pre-set Event Attributes" and provides the function to report "Custom Event Attributes". - -In "Event Attribute Management", you can pre-register "Custom Event Attribute" and manage "Event Attribute" from various aspects. - -Developers can check the pre-registered custom event attributes that have been reported as "No" and perform buried development. - -![事件属性管理](/img/customEvent/metadata_event_prop_overview.png) - -### 4.1 Concepts Explanation - -The "concepts" and "explanations" related to "event property" are as follows. - -| Concepts | Explanations| -| ---- | -------------------------------------------------------------------------- | -| Property name | Unique identifier of the event property. | -| Display name | Display name in the analysis model. | -| Description | Describe the attribute information, such as: describe the business connotation, to help business personnel more in-depth understanding. | -| Data types | The data type of the property and the data matching the type can be entered into the library. | -| Units | Units of statistical values, which are displayed in analytical models and reports. | -| Property types | There are two types: "Preset" and "Custom":
    **Preset**: built-in event properties, applicable to each event in the game project, only need to turn on the buried switch in the SDK to report.
    **Custom**: custom event properties to meet the individual needs of the game project, need to be entered in the event property management before reporting . | -| Whether to report| Whether the event property has been reported. | -| Receive Switch| Switch for the system to receive event properties to report or not.| -| Status | There are two types: "Normal" and "Hidden":
    **Normal**: Events that are in a normal state
    **Hidden**: not shown in the individual analysis models. | -| Data limit | To ensure system performance, the number of custom event properties should not exceed 300. | - -### 4.2 Create Event Property - -![事件属性创建](/img/customEvent/metadata_event_prop_create.png) - -Fill in the basic information about the event property, which completes the creation of a custom property. You can associate it with an event in Event Management. - -### 4.3 View and Edit - -![事件属性查看与编辑](/img/customEvent/metadata_event_prop_edit.png) - -Click on the property name to see basic information and all associated events. - -In the action bar, you can edit other basic information except "Property name" and "Data type". - -### 4.4 Q&A - -Q1: Why is it not possible to delete a custom event property without reporting any data, but custom events can be deleted? - -A1: Adding a new event is just a single record in the table, while adding a new property requires adding a new column in the table, which changes the data structure. Both are very difficult to implement. Therefore, the ability to delete custom attributes is not supported in the current version, and this feature will be added in the future version. - -## 5. User Property Management {#user-props} - - -"User attributes" is used to describe the attribute information of "user", TapDB has built-in "preset user attributes" and provides the function of reporting "custom user attributes". - -TapDB uses "Account" and "Device" as user identifiers to analyze different subjects respectively, and "Account" and "Device" will be used as part of user identifiers in user attribute management respectively. - -In the "User Attribute Management" function, you can pre-register "custom user attributes" and manage "user attributes" from various aspects. - -Developers can check the pre-registered custom user attributes that have been reported as "No" and perform buried development. - -![用户属性管理](/img/customEvent/metadata_user_prop_overview.png) - -### 5.1 Concepts Explanation - -The "concepts" and "explanations" related to "user property" are as follows. - -| Concepts | Explanations | -| ---- | ---------------------------------------------------------------------| -| User ID | One of the identifiers of user properties, divided into two categories: "Account" and "Device". | -| Property name| One of the identifiers, which together with the "User ID" form a unique identifier. | -| Display name | Display name in the analysis model. | -| Description | Describe the attribute information, such as: Describe the trigger timing to help technical staff more accurate buried points; describe the business connotation to help business people more in-depth understanding. | -| Data types | The data type of the property and the data matching the type can be entered into the library. | -| Units | Units of statistical values, which are displayed in analytical models and reports. | -| Property types | There are two types: "Preset" and "Custom":
    **Preset**: built-in user properties, applicable to each event in the game project, only need to turn on the buried switch in the SDK to report.
    **Custom**: custom user properties to meet the individual needs of the game project, need to be entered in the event property management before reporting .| -| Whether to report| Whether the user property has been reported. | -| Receive Switch| Switch for the system to receive user properties to report or not.| -| Status | There are two types: "Normal" and "Hidden":
    **Normal**: Events that are in a normal state
    **Hidden**: not shown in the individual analysis models. | -| Data limit | To ensure system performance, the number of custom user properties should not exceed 100. | - -### 5.2 Create User Property - -![创建](/img/customEvent/metadata_user_prop_create.png) - -Fill in the basic information to complete the creation of the custom property. Currently, custom user properties cannot be deleted once they are created. - -### 5.3 View, Edit, And Copy - -![查看、编辑与复制](/img/customEvent/metadata_user_prop_edit.png) - -Click the property name to view the basic information. - -In the action bar, you can edit the basic information except "property name" and "data type". - -In the action bar, you can copy a custom user property with the same information but a different user identity. The copy function is convenient and quick to synchronize the device and account. - -### 5.4 Q&A - -Q1: Why is it necessary to distinguish between "device" and "account"? - -A1: Identifying users with different subjects is more in line with business needs. For example, in the business of advertising placement, device is a better user identification, while when analyzing users' specific playing behavior, account may be a better user identification. In addition, you can make good use of the copy operation to create user properties for both types of identifiers. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/overview.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/overview.mdx deleted file mode 100644 index f8b26a7c2..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/overview.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Overview -sidebar_position: 1 ---- - -The TapDB SDK has a number of preset reporting events that allow us to build a very robust and complete operational module. But as you dig deeper into data analysis, there are obvious limitations to a fully templated, preconfigured analysis model. We designed custom event analytics to give you the freedom to report and query the data you need. There are some barriers to access and understanding, but the ceiling for digging into player behavior with custom event analysis is significantly higher than the pre-built report template, so we highly recommend learning and using it. - -## Event Analysis - -Event analysis is the most basic analysis model, and its analysis object is events. - -- See all the information about the event, such as: - - See the total number of times the "Play PVP" event was triggered, the number of users triggered, and the number of times per capita. - - The total amount of purchase and the number of times per person for the "Buy Gift Package" event. - - The number of "card draw" events per capita in different provinces in the last 7 days. - - The proportion of TapTap logins and the proportion of wechat logins in daily "account logins" events. - -## Retention Analysis - -Retention analysis is a model that analyzes the generalized retention behavior of users, and its analysis object is the device/account. - -We can observe the user who triggered event A and then triggered event B. The initial event and the return event can be the same, for example: -- Check the "Login" status of "Paid" accounts within the next 7 days. -- Check if a "Level 10" account "pays" within the next 7 days. -- Check the status of "rewards" within 30 days for accounts that have "purchased a monthly card". -- Check the "login" status of accounts that triggered "login" for the first time and "login" within 90 days afterwards: this is what we usually call "account retention". - -## Funnel Analysis - -Funnel analysis is a model for analyzing users who trigger specific events in a sequence. It is analyzed for devices / accounts. - -- To view information about those users who converted strictly according to the funnel steps, you can set the funnel window period, for example: - - The number of accounts that "completed check-in" and the number of accounts that "engaged in PVP" within 30 minutes after "completed check-in". - - The number of accounts that "purchased a monthly card" and the number of accounts that "purchased a monthly card" and then "purchased a newbie pack" within 7 days after "purchased a monthly card". - - -## User Segmentation - -User segmentation is an extremely powerful feature in custom event analysis. Here's how it works: - -- Find a group of users to be analyzed by looking at the data, e.g. users with very low retention rate, high ARPPU, or consistently signing in but with very low rank, and save them as a user segment. -- Using this user segment as a condition, we observe or filter the behavioral data to try to find some behavioral characteristics, e.g., users with very low retention rates have Android versions below 7, presumably because of the large number of emulator devices included in the statistics. - -User segmentation opens the way from finding features to digging deeper, and is a necessary skill for custom event analysis problems. - -## Dashboard - -Dashboard is suitable for observing data. It is often used in scenarios such as "forming a daily report of core metrics" and "continuously observing a specific issue". Once you have saved a report in your analytic model, you can add it to Dashboard. Dashboard core ability is sharing: you can share your Dashboard to other users to reduce communication cost. Note that a good Dashboard should have a clear topic to avoid the distraction of combining unrelated information into one Dashboard. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/property-analyse.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/property-analyse.mdx deleted file mode 100644 index 41801c7d8..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/property-analyse.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Property Analysis -sidebar_position: 7 ---- - -## 1. What Is Property Analysis - - -We often encounter the need to analyze the distribution of players by province, the age distribution of players, and so on. With Property analysis, a user profile can be quickly drawn. - -Property analysis is a model used to analyze the statistics and distribution of user properties. The model categorizes users according to their properties and allows you to view the statistics of users under different groupings at the same time. Property analysis is used to analyze the model of the statistics and distribution of user properties. The model categorizes users according to their properties and supports viewing user statistics under different groupings. - -Property analysis can help developers understand the characteristics of player groups, help developers understand the composition and preferences of players, and provide a basis for refined operations. - -## 2. Use Cases For Property Analysis - -Typical user cases of property analysis: - -- Analyze the average amount spent by users with different membership levels. -- Analyze the distribution of players in different provinces. - -## 3. How To Do Property Analysis - -There are 4 steps in property analysis: setting indicators, setting dimensions, setting display results, and saving reports. - -The last three steps are described in detail in Event Analysis, so here we focus on setting indicators. - -![](/img/customEvent/character/character1.png) - -All types of properties can have "deduplicated number" as the analysis indicator. For numeric types, "Sum", "Mean", "Maximum" and "Minimum" can be used as the analysis indicator. - -1. **Number of users:** Number of all users. -2. **Deduplication:** The number of deduplicates for this property among all users. -3. **Sum:** The sum of the attribute among all users. -4. **Mean:** The arithmetic mean of this attribute over all users. -5. **Maximum:** The maximum value of this attribute among all users. -6. **Minimum:** The minimum value of this property among all users. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/retention-analyse.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/retention-analyse.mdx deleted file mode 100644 index ac7ed6acc..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/retention-analyse.mdx +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: Retention Analysis -sidebar_position: 4 ---- - -## 1. Case Introduction - -Games are usually updated frequently, so what is the change of retention rate after the update, we can use the function of "Retention Analysis" to analyze it. - -**1. Setting events**: Since we focus on the overall retention of users to the game, we set the initial event and return event of users as "account login". - -![](/img/customEvent/retention/LC-1-1.png) - -**2. Set the dimension**: We are concerned about whether version updates have an impact on retention, so select "App version number" in the dimension. - -![](/img/customEvent/retention/LC-1-2.png) - -**3. Set display results**: setting up time intervals and other parameters. - -![](/img/customEvent/retention/LC-1-3.png) - -**4. Save Report**: View the analysis results and save the analysis results as a report. - -![](/img/customEvent/retention/LC-1-4.png) - -## 2. What Is Retention Analysis - -Retention is when players stay in your game and keep using it. - -Only with good retention can you guarantee that new players will not be lost after registration. Sometimes we only look at the daily activity (DAU), we will think the data is good, but it may be because of the recent intensive operation to attract new users. but the remaining users are not necessarily growing, it may be decreasing, it is just hidden by the number of new users. This is like a basket that keeps leaking, and it is difficult to achieve sustained growth if you do not repair the cracks under it and only pour water into it. - -When we talk about retention, we mean the percentage of "target players" who "come back to complete an action" over a period of time. Common indicators are next-day retention, seven-day retention, next-week retention, and so on. For example, the "next-day retention rate" of a "new player" acquired at a given time is often used to measure the effect of attracting new players. - -## 3. Use Cases For Retention Analysis - -1. The number of players who have completed the login (initial event) and have made a top-up operation in the next month (return event). -2. How many players who upgraded to VIP level 9 (initial event) purchased gift packs in the coming month (return event). -3. To determine whether a game update is working or not. For example, observe whether someone uses the game for a few more months because of the new hero character. - -## 4. How To Do Retention Analysis - -There are 4 steps in the retention analysis. - -**1. Setting events** - -**2. Set the dimension** - -**3. Set display results** - -**4. Save Report** - -### 4.1 Setting events - -#### 4.1.1 Setting initial and return events - -On the funnel page, click "Select Events" to screen initial events and return events. Note that only event properties are selected. If you want to select user properties, you can do so in a global screening. - -![](/img/customEvent/retention/LC-2-1.png) - -#### 4.1.2 Set "Display at the same time" - -The purpose of this feature is to perform an in-depth analysis of the user who triggered the return visit event. For example, you want to count the number of users who completed the login, how many of them completed the payment, and the total amount of the users who completed the payment. - -In the following screenshot, the "Display at the same time" function is used to calculate the cumulative sum of the payments made by the paying users. - -![](/img/customEvent/retention/LC-2-2-1.png) - -The difference from the event analysis template is that the "Display at the same time" function changes the analysis perspective from "sum, median, mean, maximum, minimum, per capita, and de-weighted" to "sum, per capita, phase statistics sum, and phase statistics per capita" for attributes of the numeric type. - -And only numeric and boolean attributes can be selected, not time, string, or list attributes, because these types of attributes cannot be "Phase statistics". Using the "Display at the same time" function, we can analyze the attribute values of users who complete the return visit event in the next period of time, such as LTV, N-day payment, cumulative replica damage value, cumulative number of packages purchased per capita, and so on. - - -| Data Type | Analysis Perspective | -| ----------- | -------------------- | -| Number | Sum, per capita value, phase cumulative sum, phase cumulative per capita | -| Boolean | Is true, is false, is null, is not null | - -**In addition to the Analysis Perspective of each value type listed in the above table, the default "Analysis Perspective" for any event and any value type are: total number of times, number of triggered users, and number of times per capita. -"Phase Accumulation" is the core value of the "Simultaneous Display" function. The "Simultaneous Display" function can only do one analysis at most.** - -### 4.2 Set Dimension - -Time to event is a required dimension for retention analysis because retention is associated with time. In addition to the time dimension, you can choose up to four additional dimensions. - -![](/img/customEvent/retention/LC-2-3.png) - -### 4.3 Set Display Results - -The report of retention analysis is still in the form of a pivot table. Unlike the event analysis report, the dimension of "Event Time" cannot be dragged and dropped to change the order of aggregation, and "Event Time" can only be in the first column. - -![](/img/customEvent/retention/LC-2-4.png) - -#### 4.3.1 Select the analysis period for retention - -![](/img/customEvent/retention/LC-2-5.png) - -The default analysis period for retention is 7 days. Click on the drop-down box to select: current day, next day, 7 days, 14 days, 30 days, current week, next week, 4 weeks, 8 weeks, 16 weeks, current month, next month, 3 months, 6 months, 12 months. - -In addition to the above time, you can also fill in n days, n weeks, n months by yourself. - -If the selected analysis period is too long, for example, 180 days, the calculation will be slow due to the large amount of data, so you can use "Show key dates only". - -- The key dates for days are: 1, 7, 14, 21, 30, 60, 90, 120, 150, 180, 360. -- The key dates for weeks are: 1, 4, 8, 16, 24, 32, 40, 48, 52. -- The key dates for months are: 1, 3, 6, 12, 24. - -Checking "Show key dates only" will save time for searching. When the selected retention period is longer than 90 days, the system will automatically check the "Show key dates only" box. - -#### 4.3.2 Retention / Loss - -![](/img/customEvent/retention/LC-2-6.png) - -The logic of user retention on day n: If b players (assuming the number of a) who triggered the initial event trigger a return visit on day n, the "retention percentage" is b/a. -The logic of lost users on day n: If b players who triggered the initial event (assuming the number of a players) do not trigger a return event from day 1 to day n (duration), b is the number of "lost users" on day n. The "churn percentage" is b/a. - -#### 4.3.3 Display number / percentage - -On the right side of the report, you can choose to display all / percentage only / quantity only. - -![](/img/customEvent/retention/LC-2-7-2.png) - -### 4.4 Save Report - -![](/img/customEvent/retention/LC-2-7-3.png) - -Save the query results as a report, and then create a dashboard based on the report. A dashboard makes it easy to view retention analysis results. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/sqlide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/sqlide.mdx deleted file mode 100644 index 3a3148411..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/sqlide.mdx +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: SQL Query -sidebar_position: 14 ---- - -## 1. Overview - -You can use SQL Query to query all the data in the project freely and meet the needs of personalized data retrieval and analysis. - -Click "Analysis" and "SQL Query" to see the SQL page, which consists of "Write Box" and "Tabs", and the tabs consist of "Table Structure", "Query History", "Statement Bookmarks" and "Query Results". - -![](/img/customEvent/sql/sql_1.png) - -## 2. Applicable Roles and Uses - -| Role | Usage | -| :-------- | :------------------------------------------------- | -| Administrator / Analyst | Understand the project's current data assets. | -|Analyst | Freely write SQL queries for all project data to meet individualized fetching and analysis needs that cannot be met in TapDB's inherent analysis model. | -| Business | Replace dynamic parameters in analyst SQL code to meet ongoing fetching and analysis needs. | Business - -## 3. Table Scope and Notes - -### 3.1 Table Scope - -In the TapDB SQL query function, the range of library tables can be queried as follows. - -| Table Type | Library Name | Table Name | -| :---: | :-------: | :---------------------: | -| Events table | tapdb | view_{{Project ID}}_events | -| devices table | tapdb | view_{{Project ID}}_devices | -| user table | tapdb | view_{{project ID}}_users | -| user_cluster | tapdb | view_{{item_id}}_cluster | -| dimension properties table | tapdb_dim | view_{{item ID}}_{{dimension table name}} | - -It is recommended to paste the table name into the compose box by using the "Copy Table Name" function in the "Data Table List", as described in section 5.1.2 of this document. - -### 3.2 Notes on using user cluster tables - -All user cluster data is stored in the same table "view_{{project ID}}_cluster". - -![](/img/customEvent/sql/sql_2.png) - -The user IDs under this cluster can be obtained by filtering the cluster name and selecting the phase cluster body field. - -The subject of the cluster is "User": - -```sql -select - user_id -from hive.tapdb.view_{{项目 ID}}_cluster -where cluster_name = ‘{{cluster_name}}’ -``` - -The subject of the cluster is "Device": - -```sql -select - device_id -from hive.tapdb.view_{{项目 ID}}_cluster -where cluster_name = ‘{{cluster_name}}’ -``` - -## 4. Writing and Executing SQL - -The SQL statements are written and executed mainly in the statement writing box. - -![](/img/customEvent/sql/sql_3.png) - -### 4.1 Basic Syntax - -TapDB uses the Presto query engine and applies standard SQL syntax. However, only `select` statements and `with` clauses can be used. You can access the [presto documentation](https://trino.io/docs/332/functions.html) for the Presto syntax and how to use the functions. - -Field names in the data table are recommended to be enclosed in double quotes `""`, or by default, but if the query field name has special symbols (e.g. `$`, `#`, etc.), then double quotes must be used. -Strings must be enclosed in single quotes `' '`. - -### 4.2 Partitioning and Time Zones - -When querying the event table, you must use the partition key `$part_date` for conditional filtering to avoid a full table scan. - -! [](/img/customEvent/sql/sql_4.png) - -It is recommended to use the following types of partition constraints. - -```sql -"$part_date" = '2021-11-01' -"$part_date" in ('2021-11-01', '2021-11-02, '2021-11-03') -"$part_date" between '2021-11-01' and '2021-11-11' -``` - -By default, the SQL query function converts and displays the fields of time type according to the East 8 zone, such as `time` in the event table, `activation_time`, `last_login_time`, `first_charge_time`, `last_charge_time` in the equipment table and account table. - -If the project is not in Zone 8 East, it can be transformed using the time function. - -```sql -format_datetime("time" at time zone 'America/Chicago', 'yyyy-MM-dd') -``` - -`part_date`, `$part_date` are dates in string format after conversion according to project time zone, if there is no need to query hour, minute and second precisely, it is recommended to use "partition" filter. - -### 4.3 Dynamic Parameters - -The dynamic parameter function can replace the parameter in the query statement, and the subsequent query can meet the new query requirements by simply entering the parameter value below the parameter input box. - -The expression rule of dynamic parameter is `${parameter name}`, the parameter name can include English letters, numbers and "_", it should start with English letters. Parameters with the same name are regarded as one parameter, and multiple parameter variables can be created. The input box below corresponds to the dynamic parameters in the order of the first appearance of each parameter, and parameters with the same name only correspond to one parameter input box. - -![](/img/customEvent/sql/sql_5.png) - -### 4.4 Toolbar - -The toolbar is located below the input box and can perform the following operations. - -Formatting: formats the query statement. - -Copy statement: Copy the query statement from the input box to the clipboard. - -Add Bookmark: Save the query statement as a bookmark for subsequent queries or modifications. - -![](/img/customEvent/sql/sql_6.png) - -### 4.5 Shortcut keys - -When the cursor is in the input box, the following shortcut actions can be performed. - -Ctrl + Enter: Perform calculation - -Ctrl + Shift + F: Format the current query statement - -Ctrl + Z: Undo the previous operation - -Ctrl + Y: Resume the previous operation - -### 4.6 Executing queries - -After completing the SQL statement, you can click the "Calculate" button or the shortcut key Ctrl + Enter to launch a data query. - -By default, the maximum number of data to be queried is 10000. The system will automatically add "limit 10000" to the query statement to limit the number of rows of query, and the maximum number of rows to be displayed in the front end is 500. All data can be viewed through the download function in "Query History" and "Query Results". - -## 5. Tabs - -Tabs consist of "Table Structure", "Query History", "Statement Bookmarks" and "Query Results". - -![](/img/customEvent/sql/sql_8.png) - -### 5.1 Table Structure - -The table structure page can view the detailed information of database, data table and table field, which consists of 3 parts from left to right: database list, data table list and table field list. - -![](/img/customEvent/sql/sql_9.png) - -#### 5.1.1 Database list - -You can view the databases under the project: click on the library name and a list of data tables under the library will be displayed on the right side. - -![](/img/customEvent/sql/sql_10.png) - -#### 5.1.2 List of Data Tables - -You can view the data tables under the selected database: click on the table name and the fields under the table will be displayed on the right side. - -Click on "Copy Table Name" and the table name will be pasted to the clipboard. - -![](/img/customEvent/sql/sql_11.png) - -#### 5.1.3 List of Table Fields - -The table fields list allows you to view information about all fields of the selected table, including field names, data types, and explanations. - -![](/img/customEvent/sql/sql_12.png) - -### 5.2 Query History - -Query history page: You can view the executed query statements, including the statement completion time, calculation time, query statements and other information. It also supports searching query statements. - -![](/img/customEvent/sql/sql_13.png) - -Click "Query ID" to jump to the query result. - -Click the "Type" button in the upper right corner of the "Query Statement" to replace the statement in the input box. - -Click "Download" to download the query result as a csv format file to local, the maximum number of query results displayed on the page is 500, the data exceeding the maximum number can be downloaded to local for further view and analysis, the maximum number of data downloaded is 10000. - -![](/img/customEvent/sql/sql_14.png) - -### 5.3 Statement bookmarks - -You can view the saved statement bookmarks in the statement bookmarks page. - -Click "Set", the content in the bookmark will replace the content in the statement input box, and click "Delete" to delete the bookmark. - -![](/img/customEvent/sql/sql_15.png) - -### 5.4 Query Results - -You can view the history query results in the query result page. - -Click "Format" to format json, map and other objects in the query results. - -Click "Download" to download the query results as a csv file to the local area. - -! [](/img/customEvent/sql/sql_16.png) - -## 6. Best Practices - -### 6.1 Log Export - -Export the user table or event table, e.g. export all event logs for the last 7 days of users. - -```sql -select - * -from hive.tapdb.view_{{项目 ID}}_events -where "$part_date" between '2021-11-05' and '2021-11-11' -``` - -### 6.2 Data cleaning and extraction - -Extract the key information in complex fields, such as url, json, map, e.g. extract the product ID in the last 10 digits of url. - -```sql -select - substring("#url", -10) as product_id -from hive.tapdb.view_{{项目 ID}}_events -where "$part_date" between '2021-11-05' and '2021-11-11' -``` - -### 6.3 Personalized Fetching and Analysis - -For personalized analysis needs that cannot be met by TapDB's existing analysis models. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/tracking-management.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/tracking-management.mdx deleted file mode 100644 index c1b9cd094..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/tracking-management.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Burial Point Management -sidebar_position: 11 ---- - -## 1. Overview - -Burial Point Management is a data management function provided during data access testing and daily use, including 2 sub-modules "Buried point information" and "Reported details". - -The buried point information page can view the data received in the project for the last 7 days. It is convenient to quickly understand the overall situation of buried site reporting, as well as error reporting information and sampling examples. - -The report details page can view the report details of nearly 1000 buried points during the period when the "monitoring switch" is turned on, and view the buried point report log in real time to help users quickly test and accept the buried point development. - -## 2. Applicable Roles and Uses - -| Role | Usage | -| -------- | --------------- | -| Buried point designers (data product managers / analysts) | Testing and acceptance of new buried point requirements development report results, daily understanding of the overall operation of the buried point, timely detection of buried point errors. | -| Buried developers (front-end development / client-side development / test engineers) | Real-time view buried logs, test buried results, and quickly locate errors in the buried development process (recommended to be enabled in testing projects). | - -## 3. Buried information - -The buried information consists of two parts: "Buried information home page" and "Error details page". - -**Buried information home page** - -![埋点信息主页](/img/customEvent/tracking_manager_1.png) - -**Error details page:** - -![错误详情页](/img/customEvent/tracking_manager_2.png) - -### 3.1. Buried Information Home Page - -The buried information supports viewing the last 7 days of data received in the project. - -The home page of buried information consists of query settings, interval data, and data details area. - -![埋点信息主页](/img/customEvent/tracking_manager_3.png) - -#### 3.1.1 Query Setting - -Query time filter: The query time refers to the time of data reception. Support custom view of the last 7 days of any time period of data reception, the default statistics of today's data. - -Property name, display name search: filter the display of property names and display names according to keywords. - -![查询设置](/img/customEvent/tracking_manager_4.png) - -#### 3.1.2 Interval Data - -Displays the number of received, entered, incorrectly entered, and failed entries for all data in the filtered time period. - -![区间数据](/img/customEvent/tracking_manager_5.png) - -#### 3.1.3 Data Details - -Data name: the name of the data reported by the event burial point is "event name"; the name of the data reported by the burial point that sets user attributes is "user attributes"; the data that cannot be identified as the above two categories is "unknown data". - -display name: the display name corresponding to the name of the data. - -Received: the data received by the system. - -Entered: includes correctly entered and incorrectly entered data. - -Incorrectly entered: includes data with wrong attribute level such as illegal event attribute name, illegal attribute type, etc. This kind of data can be entered normally and the wrong attribute is set to empty. - -Failed entry: data that cannot be entered due to illegal data format or illegal data name. - -Error details: Click to enter the error details page to see the error details displayed according to the error reason. - - -![数据详情](/img/customEvent/tracking_manager_6.png) - -### 3.2 Error Details Page - -The data detail page displays the error entry and entry failure data under each "Event or User" property, and the error information is displayed according to the error cause. - -The error details page consists of query settings, interval data, and data details area. - -![错误详情页](/img/customEvent/tracking_manager_7.png) - -#### 3.2.1 Data details - -Number of error items: the number of data items containing the cause of the error. - -Error Type, Processing Result: The error type and the processing result of TapDB, so that users can filter the specific error cause. - -View Sample: View the details of the reported data that match the cause of the error, and each reported data can be formatted and copied with one click. - -![数据详情](/img/customEvent/tracking_manager_8.png) - -#### 3.2.2 Calculation Principle of Error Count - -Calculation principle of error count - -The "error count" is the result of counting the reported data that triggered the error cause, while the "error cause" in buried management occurs at the "attribute" level. - -When only one log of an event is reported, and the property name of field 1 is not legal and the property type of field 2 is not legal, the log will trigger two error reasons at the same time, and the number of error entries will be counted under the two types of error reasons, so the sum of the number of error entries in the data details summary may be greater than the number of error logs. - -## 4. Report Details {#realtime} - -The report details show the last 1000 logs reported during the period when the "monitoring switch" was turned on, and the processing results of their entry. - -![上报明细](/img/customEvent/tracking_manager_9.png) - -When "monitoring switch" is on, the buried logs received by the system will be displayed in the reporting details in real time, and when it is on for 1 hour, it will be automatically turned off and no more data will be displayed in real time, and any time the switch is turned on again will reset the 1-hour time progress. It is recommended to manually turn on the "monitoring switch" before testing and acceptance of buried development. - -Click the Refresh button to refresh the page for the latest data. - -Each line of reported log details can be formatted for viewing and copying. - -## 5. Using Buried Point Management - -### 5.1 Find the buried point error in time - - -Use the buried point information page to keep track of the overall operation of the buried point, and when there is unknown data, or some data is incorrectly entered or failed to be entered, you can locate the error in the error details page to prevent data assets from being lost. - -### 5.2. Use the report log to test and accept the buried point requirements - -After the buried developers finish the development, the designers can simulate the user's various clicking behaviors in the game, and then check the real-time reporting logs in the "Reporting Log" to check whether the reporting timing and log details are consistent with the buried design plan, so as to test and accept the buried requirements. - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/user-seq.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/user-seq.mdx deleted file mode 100644 index 213db8b2d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/user-seq.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: User Search -sidebar_position: 18 ---- - - -## 1. What Is User Search? - -User search is used to find the behavior of users that match a certain type of characteristic or an exact user. - -## 2. Usage Scenarios - -* Funnel analysis finds users who churn during login or during the newbie tutorial. "User Search" finds the behavior of users before they churn. -* Review the user's behavior sequence before Crash to identify problem scenarios. - -## 3. How To Use User Search? - -By analyzing the model if the result of analysis is population (number of triggering accounts, number of triggering devices), then click on the analysis result to display the list of users who match the conditions and the details of these users. You can also use the user properties to filter the matching users. Clicking on a user in the list can get the reported behavior distribution, behavior sequence, user properties, etc. for that user in a certain time period. - -### 3.1 User search - -Click "User Search" at the top right to open the user search interface. - -![](/img/customEvent/user/user_seq_1.png) - -Select the subject and set the query criteria to find the matching device or account, or you can search precisely by account or device ID. - -![](/img/customEvent/user/user_seq_2.png) - -### 3.2 User List - -From the analytics model or the user refinement page you can jump to the list of users. - -![](/img/customEvent/user/user_seq_3.png) - -### 3.3 User behavior sequence - -Click on the user ID in the user list to access the user behavior sequence query: - -![](/img/customEvent/user/user_seq_4.png) - -**Total number of behavioral events** - -The bar chart will show the trend of the number of reported incidents in the selected time range. Turn on "Show Incident Distribution" on the right to open the pie chart of behavior distribution. - -**Event details** - -* The user's behavior sequence is displayed in chronological order. -* Click the expand button to see all event properties of the event. -* When the event is expanded and the mouse is hovered over the event property, it can be set as an external property. Once set as an external property, you can view the event properties after the event name without expanding the event. - - -**User Properties** - -The right side of the page shows the current user's properties, which can be configured using the "Custom Properties" feature. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/user-tag.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/user-tag.mdx deleted file mode 100644 index a54a3be21..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/user-tag.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: User Label -sidebar_position: 17 ---- - -## 1. Overview - -User labels are like user properties, with certain types of user characteristics as "user labels" and the specific performance of users under the characteristics as "label values". It is used to divide users into different groups, and it is easy to use the labels for dimensional grouping or filtering in various analysis models. - -In TapDB, "User" and "Device" are used as query subjects for query, and "User" and "Device" are also used as subjects for creating tags in user labels. - -It supports creating user labels by "indicator value", and more label creation methods will be opened in the future. - -![概述](/img/customEvent/userTag_1.png) - -## 2. Applicable Roles and Uses - - -| Roles | Uses | -| ---------- | --------------------------------- | -| Analyst / Business Personnel | Calculate user behavior metrics over time, and group and filter users in the analytics model.| - -## 3. Create User Labels - -Click "New label" in the upper right corner of the tab list and select "index value label". - -![创建用户标签_1](/img/customEvent/userTag_2.png) - -The current page is divided into two parts: "basic information" and "Index value configuration". - -![创建用户标签_2](/img/customEvent/userTag_3.png) - -### 3.1 Basic Information - -Fill in "Label display name", "Tag", "Label", "Update method", and "Remark" in order. - -![基础信息](/img/customEvent/userTag_4.png) - -Tag display name is displayed in the subgroup list, analysis model, and is the basis for business personnel to identify tags. - -Tag support "User" or "device", choose which type according to the business scenario. - -The Label is the unique identification of the subgroup stored in the system background, and can be named as the parameter name with business meaning for the convenience of data analysts to query the database table directly. - -The update method is divided into "manual update" and "automatic update". "Manual update" means that the system will not automatically update user tags after the first calculation is completed, and users need to update them manually; "Automatic update" will update user tags after 0:00 every day, using the previous day as the base. - -### 3.2 Index Value Configuration - -The aggregated metrics of the user's completed events in the specified time period as the tag value. - - -![指标值配置](/img/customEvent/userTag_5.png) - -Users who completed the event will belong to the tag, and users who did not complete the event have no tag value. - -Determine the tag value of users through indicators. Based on the method of creating indicators in "Event Analysis", add "days" and "hours" of "events", and also support property filtering and editing formulas. - -## 4. Management And Use Of Tags - -### 4.1 Manage User Tags - -The tabs will be displayed in a list on the user tab, and users can view, edit, delete, update, download, and copy operations on the subgroups. - -![管理用户标签](/img/customEvent/userTag_6.png) - -The "Number" represents the number of users with values under the tag, and the "Data Update Time" is the time when the tag was last calculated. - -### 4.2 User Tag Details - -Since the values of the indicator value tags are discrete and numerous values, the display distribution of the indicator values can be set in the tag details for reading purposes, but without changing the indicator values themselves. - -![用户标签详情](/img/customEvent/userTag_7.png) - -### 4.3 Using User Tags In Analytics Model - -As with user properties, user tags can be used as filter conditions. - -![在分析模型中使用用户标签_1](/img/customEvent/userTag_8.png) - -The user tag can be used as a grouping dimension. - -![在分析模型中使用用户标签_2](/img/customEvent/userTag_9.png) - -## 5. Best Practices - -In " Indicator Value", you can calculate indicators of each user's behavior in a certain time period, such as the number of days logged in, amount paid, etc. These indicator value tags can be used as grouping dimensions in event analysis, attribute analysis to group users for analysis, or as filtering items for further drill-down analysis of users. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/virtual-event.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/virtual-event.mdx deleted file mode 100644 index 0b10d11dc..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/virtual-event.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Virtual Events -sidebar_position: 15 ---- - -## 1. Overview - -A virtual event can be composed of multiple events with similar business meaning, and any base event triggered is considered to be triggered as the virtual event. - -It is also possible to split an event into multiple events by different filtering conditions, and the base event that meets the filtering conditions is triggered as the virtual event. - -You can create and manage virtual events in the event management page. - -![overview](/img/customEvent/virtualEvent_1.png) - -## 2. Applicable Roles and Uses - -| Role | Usage | -| ------------------------- | ------------------------------------ | -| Buried Event Designer (Data Product Manager / Analyst) | To transform buried events, reduce the complexity of buried solution design, and make up for the shortcomings in buried design and development. | -| Data analysts (data product managers / analysts / operations) | Combine events with similar business meaning, or split an event to improve the efficiency of daily analysis use. | - -## 3. Create Virtual Event - -Click "New Event" in the upper right corner of the event management page and select "Virtual Event". - -![New Virtual Event](/img/customEvent/virtualEvent_2.png) - -### 3.1 Fill in the basic information - -Fill in the virtual event name, virtual event display name and description, the event name starts with "#ve@" by default. - -![Fill in the basic information](/img/customEvent/virtualEvent_4.png) - -### 3.2 Edit the rules for defining virtual events - -A virtual event is considered to be triggered when any of its base events is triggered (if the base event is filtered, the filtering condition must be satisfied here). - -When a virtual event consists of 2 or more base events, it can be filtered globally, and all base events must satisfy the filtering condition - -![编辑虚拟事件的定义规则](/img/customEvent/virtualEvent_3.png) - -## 4. Management and use of virtual events - -### 4.1 Managing virtual events - -You can manage virtual events in the event management page. The maximum number of virtual events that can be created is 300, and they can be edited, copied and deleted. - -![Manage virtual events](/img/customEvent/virtualEvent_5.png) - -### 4.2 Notes for use in the analysis model - -Virtual events are consistent in their use with preconfigured events and custom events. The properties of a virtual event are the intersection of all the properties of the base event. - -The number of users triggering virtual events has been "de-duplicated" based on the number of users of the base event. Therefore, in some cases the number of users triggered by virtual events is less than the sum of the number of users triggered by all base events. - -## 5. Best Practices - -### 5.1 Combine events with similar business meaning into a single event - -In the buried design, "big world battle", "copy battle" and "PVP battle" are reported as 3 different events. - -Now we need to count all the battle behaviors of users. The three virtual events mentioned above can be used as the base event of "Combat", and users triggering any one of the three combat events can be considered as triggering "Combat" events. This way, all the user's combat behaviors can be counted. - -### 5.2 Splitting a single event into multiple events with more specific business meaning - -In the buried design, all page views are uniformly reported as "page view" and different pages are distinguished by the event attribute "page type". - -In the daily analysis, the analysis demand of home page, store page and recharge page is more frequent, so we can use "page view" as the basic event in the virtual event and filter "page type" to construct 3 virtual events "home page view", "store page view" and "recharge page view", which can realize fast statistics and meet the daily analysis demand. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/virtual.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/virtual.mdx deleted file mode 100644 index 3971060b9..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/custom-event/virtual.mdx +++ /dev/null @@ -1,161 +0,0 @@ ---- -title: Virtual Properties -sidebar_position: 12 ---- - -## 1. Overview - -For event properties and user properties that have been reported, you can set virtual properties to map the originally uploaded data to another display or calculated value, so that the initial buried property value is not the same as the display value, and the data can be processed at a later time to improve flexibility. - -Virtual attributes are calculated by SQL expressions on the base property field to obtain a new property field. - -Virtual event properties can be created and managed in event property management, and virtual user properties can be created and managed in user property management. - -## 2. Applicable Roles and Uses - - -| Roles | Usage -| ------------------------- | ------------------------------------ | -| Data Product Manager / Analyst | Transformation of buried fields, reduce the complexity of buried solution design, and make up for the defects in buried design and development. | -| Data analysts (data product managers / analysts / operations) | Self-assisted extraction of some of the low-frequency or a small number of scenarios from the existing buried data, rapid response to analysis needs. | - -## 3. New Virtual Attribute - -Click "New Event Attribute" or "New User Attribute" in the upper right corner of Event Attribute Management or User Attribute Management, and select "New virtual attribute". - -![New Virtual Attribute](/img/customEvent/virtual_1.png) - -### 3.1 Fill in the basic information - -#### 3.1.1 General Basic Information - -Fill in or select the property name, display name, data type, unit, and description, with the property name starting with `#vp@` by default. - - -![General Basic Information](/img/customEvent/virtual_2.png) - -#### 3.1.2 虚拟事件属性的独有基础信息 - -Select "Associated Subjects", different choices correspond to different ranges of base properties (preset properties and custom properties are collectively called base properties), and the application range of this virtual property. - -| Associated Subjects | Range of Base Attributes | Range of Virtual Attribute Applications | -| ---- | -----------------------| ------------- | -| No Subject | Event Attributes | When using a device or account as the query subject. | -| Account number | Event properties & Account properties.
    Account properties | When using account number as the query subject. | -| Device | Event properties & Device properties.
    Device Properties | When the device is the subject of the query. - -Select "Associated Events" and the different association logic is as follows. - -- Auto-identify: associate the concatenation of the events associated with the base event property involved in the SQL code - -- All events: associate all events, or automatically associate if new events are added - -- Specified events: associate specified events - -! [unique to event property](/img/customEvent/virtual_3.png) - -#### 3.1.3 Unique base information for virtual user properties - -Select "User ID", different choices correspond to different base attribute ranges, and the application range of this virtual property. - -| User ID | Base attribute range | Virtual attribute application range | -| ---- | ------ | --------- | -| Account | Account Properties | When the account is the subject of the query. | -| Device | Device Properties | When the device is the subject of the query. | - -! [unique base information for account attributes](/img/customEvent/virtual_4.png) - -### 3.2 Edit the creation rules of virtual attributes - -You can quickly get information about the currently available base properties in the list on the left side by entering SQL expressions in the text box. - -![创建规则](/img/customEvent/virtual_5.png) - -The SQL expressions of virtual properties use presto syntax, and you can access [presto documentation](https://trino.io/docs/332/functions.html) to get the syntax of presto and how to use the functions. - -You can insert code snippets required for common application scenarios through the "Insert Template" function, and create virtual properties easily and quickly by replacing field names and modifying the logic. - -### 3.3 Logic Checking - -After inputting the content, you can click "Verify" to check the range and SQL syntax of the base property, and the value input box of the property will appear in the "Debug" panel if the verification is successful. - -You can check whether the result meets the requirement by checking the result after entering the validation value. If it meets the requirements, you can click "Next" to finish creating the property. If it does not meet the expectations, you can continue to modify the creation rules and repeat the verification. - -![logical_check](/img/customEvent/virtual_6.png) - -## 4. Management and use of virtual properties - -### 4.1 Managing virtual properties - -Virtual properties can be managed in the event property management or user property management pages. The maximum number of virtual event attributes and virtual user attributes that can be created is 300, and they can be edited and deleted. - -![Manage virtual properties](/img/customEvent/virtual_7.png) - -### 4.2 Considerations for use in models - -Virtual properties are used in the same way as usual properties, with their calculation logic and filtering conditions determined by their type. - -Virtual event properties can be used in the calculation of their associated events, and the association can be customized. - -Virtual user properties are used in the same scenarios as normal user properties. - -## 5. Best Practices - -### 5.1 Fix a field type reporting error in buried development - -The user age field "age" was incorrectly reported as text. Before re-release, the user age is now temporarily and urgently needed for analysis. - -```sql -cast("age" as int) -``` - -### 5.2 Calculating user lifecycle - -Based on the user's "activation_time" and the event "time", we need to calculate the user's lifecycle when the user's action occurs. - -```sql -date_diff('day', date("activation_time"), date("time")) -``` - -### 5.3 Unit conversion by arithmetic operations - -There is already a payment amount field "amount" in "cents", which needs to be converted to "dollars". - -```sql -"amount" / 100 -``` - -### 5.4 Identifying separate roles or other subjects that you want to analyze - -A user can create different roles on different servers. The fields identifying the roles are generated based on the user identity field "user_id" and the server identity field "server_id". - -```sql -concat("server_id", "user_id") -``` - -### 5.5 Intercepting fields to obtain key dimensional information in complex fields - -Intercept the month of posting based on the ID "post_id " of the forum post (example: 10086202012310001). - -```sql -substring("post_id", 6, 6) -``` - -### 5.6 Sorting pages by functional modules - -The buried point records the url address of the user's visit, and now it is necessary to know the situation of the functional module used by the user. - - -```sql -case - - when "url" like '%home%' then '首页' - - when "url" like '%store%' then '商城' - - when "url" like '%stage%' then '剧情' - - else '其他' - -end -``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/diagnosis.md b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/diagnosis.md deleted file mode 100644 index 8cb4ead65..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/diagnosis.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Diagnosis -sidebar_position: 3 ---- - - -"Diagnosis" is a TapDB module that generates reports based on data monitored by TapTap's self-developed crash monitoring SDK (Themis) and provides related log information to help developers efficiently locate and resolve problems. - -## How to use "Diagnosis"? - -The "Diagnosis" feature is available for games that are open to TapPlay, integrated with TapSDK and have Themis enabled. -Please refer to the [documentation](/sdk/tapdb/sdk/client-side-integration/#themis) for how to adjust the configuration. - - -## What scenarios can "Diagnosis" cover? - -- Monitor crashes, flashbacks and errors generated during the game, and obtain user behavior logs. - -## What modules are included in "Diagnosis"? - - - Crash analysis: Monitor the crashes and flashbacks generated during the game, provide positioning conditions and reports, and can be specific to a particular user's crash information. - - - Error analysis: Monitor the error reports and custom logs generated during the game, provide location conditions and reports, and can be specific to a particular user's reported information. - - - Label management: upload the label to parse and restore the stack of crashes of APP, and quickly and accurately locate the code location where the user's APP crashed. - -![「诊断」包含哪些模块?](/img/customEvent/diagnosis/diagnosis-1.png) - -## How does crash analysis and error analysis work? - -**The functions of "Crash Analysis" and "Error Analysis" are the same, only the monitoring conditions are different, so they are explained together.** - -### Observe the stability of the program through "Collapse overview” - -1. Observe the running stability trend of the program in the selected date, and determine whether there is a problem in the selected date by the error reporting rate and the reported trend graph. - -![观测程序的运行稳定性](/img/customEvent/diagnosis/diagnosis-2.png) - -2. The "High probability crash in" chart quickly locates the scenarios where the problem may exist. For example, if the game is connected to TapDB and TapPlay is open, it will additionally provide the distribution of the reported game in TapTap app version. - -![高占比统计](/img/customEvent/diagnosis/diagnosis-3.png) - -3. Go to the details of the most reported issues through the "TOP 10 list" to quickly anchor the issue. - -![Top 10 问题列表](/img/customEvent/diagnosis/diagnosis-4.png) - -### How do I identify the cause of the problem? - -1. Filter by "Platform" whether the platform you want to view is Andorid or IOS, Android is selected by default. - -2. Filter by "Data" which SDK you want to see the data reported, including TapDB and TapPlay. - -3. Set the conditions to locate the people who report errors with specific conditions through the filter, after setting the conditions, the data will be reloaded to filter out the information that matches the conditions. - -![通过筛选器设置条件定位特定条件的报错人群](/img/customEvent/diagnosis/diagnosis-5.png) - -4. Filtering error messages by filtering conditions, the same error reports from different users will be grouped according to characteristics and merged into the same issue ID. - - - a. Click on the issue ID to enter the details page, and you can view the trend and distribution of reported issues of this category. - - b. Click the report ID, the sidebar will pop up the error details of the specific user. Developers can locate the cause of the problem according to the "error stack", "trace data" and so on. - -![筛选出符合条件的报错详细信息](/img/customEvent/diagnosis/diagnosis-6.png) - -## How does the "Label Management" work? - -"Label Management" is used to manage the symbol table uploaded by developers, providing the ability to upload, filter, delete, etc. The detailed use process can be seen in the figure below, if you have any questions, you can submit a ticket for consultation. - -![符号表管理](/img/customEvent/diagnosis/diagnosis-7.png) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/exchange-rate.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/exchange-rate.mdx deleted file mode 100644 index a64de1d61..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/exchange-rate.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Exchange Rate -sidebar_position: 6 ---- - -import { ExchangeTable } from "/src/docComponents/ExchangeTable"; - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/subcontinent.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/subcontinent.mdx deleted file mode 100644 index cae9442d3..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/features/subcontinent.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: Subcontinent -sidebar_position: 4 ---- - -The subcontinents are divided according to the United Nations. The countries or regions included in each subcontinent are shown below: - -## Africa - -**East Africa** - -Burundi Comoros Djibouti Eritrea Ethiopia Kenya Madagascar Malawi Mauritius Mayotte Mozambique Réunion Rwanda Seychelles Islands Somalia South Sudan Tanzania Uganda Zambia Zimbabwe French Southern Territories British Indian Ocean Territory - -**Central Africa** - -Angola Cameroon Central African Republic Chad Congo Zaire Equatorial Guinea Gabon Sao Tome and Principe - -**North Africa** - -Algeria Egypt Libyan Arab Jamahiriya Morocco Sudan Tunisia Western Sahara - -**South Africa** - -Botswana Swaziland Lesotho Namibia South Africa - -**West Africa** - -Benin Burkina Faso Cape Verde Ivory Coast Gambia Ghana Guinea Guinea-Bissau Liberia Mali Mauritania Niger Nigeria Senegal Sierra Leone St. Helena Togo - -## Americas - -**Caribbean** - -Anguilla Antigua and Barbuda Aruba Barbados Bahamas British Virgin Islands Bonaire, St. Eustatius and Saba Cayman Islands Cuba Curaçao Dominica Dominican Republic Grenada Guadeloupe Haiti Martinique Montserrat Islands Puerto Rico St. Maarten St. Barthélemy St. Kitts and Nevis St. Lucia St. Martin St. Vincent and the Grenadines Trinidad and Tobago Turks and Caicos Islands U.S. Virgin Islands Jamaica - -**Central America** - -Belize Costa Rica Guatemala El Salvador Honduras Mexico Nicaragua Panama - -**North America** - -Bermuda Canada Greenland St. Pierre and Miquelon United States - -**South America** - -Argentina Bolivia Bouvet Island Brazil Chile Colombia Ecuador Falkland Islands French Guiana Guyana Paraguay Peru South Georgia and the South Sandwich Islands Suriname Uruguay Venezuela - -## Asia - -**Central Asia** - -Kazakhstan Kyrgyzstan Tajikistan Turkmenistan Uzbekistan - -**East Asia** - -China Hong Kong Japan Macau Mongolia Korea, Democratic Republic of Korea Taiwan - -**Southeast Asia** - -Brunei Cambodia Indonesia Lao People's Democratic Republic Malaysia Myanmar Philippines Singapore Thailand Vietnam Timor-Leste - -**South Asia** - -Afghanistan Bangladesh Bhutan India Iran (Islamic Republic of) Maldives Nepal Pakistan Sri Lanka - -**West Asia** - -Armenia Azerbaijan Bahrain Cyprus Georgia Iraq Israel Jordan Kuwait Lebanon Oman - -Palestinian Territories Qatar Saudi Arabia Syria Turkey United Arab Emirates Yemen - -## Europe - -**Eastern Europe** - -Belarus Bulgaria Czech Republic Hungary Republic of Moldova Poland Romania Russia Slovak Republic Ukraine - -**Northern Europe** - -Åland Islands Denmark Estonia Faroe Islands Finland Guernsey Iceland Ireland Isle of Man Jersey Latvia Lithuania Norway Svalbard and Jan Mayen Sweden United Kingdom - -**Southern Europe** - -Albania Andorra Bosnia and Herzegovina Croatia Gibraltar Greece Italy Kosovo Malta Montenegro The former Yugoslav Republic of Macedonia Portugal San Marino Serbia Slovenia Spain Holy See (Vatican) - -**Western Europe** - -Austria Belgium France Germany Liechtenstein Luxembourg Monaco Netherlands Switzerland - -## Oceania - -**Australasia** - -Australia Christmas Island Cocos Islands Heard and McDonald Islands New Zealand Norfolk Island - -**Melanesia** - -Fiji New Caledonia Papua New Guinea Solomon Islands Vanuatu - -**Micronesia** - -Guam Kiribati Marshall Islands Micronesia Nauru Northern Mariana Islands Palau U.S. Minor Outlying Islands - -## Oceania Overseas Territories - -Antarctica - -**Polynesia** - -American Samoa Cook Islands French Polynesia Niue Pitcairn Islands Tokelau Tonga Tuvalu Wallis and Futuna Samoa diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/_category_.json deleted file mode 100644 index c59a630d3..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Guides", - "position": 3 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/client-side-integration.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/client-side-integration.mdx deleted file mode 100644 index 8a9b8d078..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/client-side-integration.mdx +++ /dev/null @@ -1,1646 +0,0 @@ ---- -title: Client Integration Guides -sidebar_label: Client Integration Guides -sidebar_position: 3 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import {Conditional} from '/src/docComponents/conditional'; - -## Introduction - -TapSDK provides an API for game developers to collect account data. The system will collect and analyze the account data, and eventually form a data report to help game developers analyze account behavior and optimize the game. - -## - -Please [download TapSDK](/tap-download) and add the relevant dependencies. - - - -<> - -If you only need to use TapDB alone, you can import only the dependencies `common` and `tapdb`. - -For Unity v3.7.1 and higher, you also need to import `com.leancloud.storage`. - - - -{`"dependencies":{ - "com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.bootstrap":"https://github.com/TapTap/TapBootstrap-Unity.git#${sdkVersions.taptap.unity}", - "com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-${sdkVersions.leancloud.csharp}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", - // Data analysis - "com.taptap.tds.tapdb": "https://github.com/TapTap/TapDB-Unity.git#${sdkVersions.taptap.unity}", -}`} - - - -<> - -If you only need to use TapDB alone, you can import only the dependencies `common` and `tapdb`. - - -{`repositories { - flatDir { - dirs 'libs' - } -} -dependencies { - // ... - implementation (name:'TapBootstrap_${sdkVersions.taptap.android}', ext:'aar') // Required: TapSDK Launcher - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') // Required: TapSDK base library - implementation (name:'TapLogin_${sdkVersions.taptap.android}', ext:'aar') // Required: TapTap Login - implementation 'com.taptap:lc-realtime-android:${sdkVersions.leancloud.java}' - implementation 'com.taptap:lc-storage-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - implementation (name:'TapDB_${sdkVersions.taptap.android}', ext:'aar') //Statistics -}`} - - - -<> - -If you only need to use TapDB alone, you can import only the dependencies `common` and `tapdb`. - - -{`// Login -TapBootstrapSDK.framework -TapCommonSDK.framework -TapLoginSDK.framework -TapCommonResource.bundle -TapLoginResource.bundle -LeanCloudObjc.framework -//TapDB -TapDB.framework`} - - -Add: `-ObjC` and `-Wl -ld_classic` to 'Build Settings' >' link '>' Other Linker Flags'. -The following dependent frameworks or libraries need to be imported for your Xcode project. - -Package | Description | Notes ---- | --- | --- -AdSupport.framework | Used to obtain device advertising logos and track devices -iAd.framework | Advertising frame | Set to optional -AdServices.framework | Advertising frame | Set to optional -AppTrackingTransparency.framework | App tracking framework added in iOS 14 (not required if IDFA tracking is not required beyond iOS 14) | Set to optional -SystemConfiguration.framework | -CoreMotion.framework | -CoreTelephony.framework | -Security.framework | Used to persist the storage device ID -libc++.tdb | -libresolv.tbd | -libz.tbd | -libsqlite3.0.tbd | - - - - -<> - -#### Environmental Requirements - -* UE 4.26 or higher -* iOS 12 or higher -* Android 5.0 (API level 21) or higher -* macOS 10.14.0 or higher -* Windows 7 or higher - -**Support Platforms**:iOS / Android / Windows / macOS - -#### Install Plugins - -* Download [TapSDK UE4](/tap-download), unzip TapSDK-UE4-xxx.zip, and copy the `TapDB` and `TapCommon` folders to the `Plugins` directory. -* Restart Unreal Editor. -* Open Edit > Plugins > Projects > TapTap, open the `TapDB` module. - -#### Add dependencies - -Add the required modules to `Project.Build.cs`: - -```cs -PublicDependencyModuleNames.AddRange(new string[] { "Core", - "CoreUObject", - "Engine", - "Json", - "InputCore", - "JsonUtilities", - "SlateCore", - "TapCommon", - "TapDB" -}); -``` - - - - - -:::info -If you were using a version of TapDB lower than `3.6.3` and the region selected for initialization was International (`IO`), then upgrading the SDK to `3.6.3` and higher will require migrating the data first.Please submit a ticket to contact us to migrate the data. -::: - - -## Initialize SDK - - -Initialize the SDK and report a device login (`device_login`) event, access to this interface is a pre-requisite for using other interfaces and needs to be called early. - -:::info -Depending on your use case, either of the following initialization options is acceptable. -::: - -### Initialize TapSDK {#tapsdk-init} - -Synchronize the initialization of TapDB during TapSDK initialization. - - - -<> - - - -```cs -using TapTap.Bootstrap; - -var config = new TapConfig.Builder() - .ClientID("your_client_id") // (Required) Client ID in the Developer Center. - .ClientToken("your_client_token") // (Required) Client Token in the Developer Center. - .ServerURL("https://your_server_url") // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API - .RegionType(RegionType.CN) // (Optional) CN for Mainland China; IO for international. - .TapDBConfig(true, "gameChannel", "gameVersion", true) // TapDB is automatically initialized based on the configuration of TapConfig. - .ConfigBuilder(); - -TapBootstrap.Init(config); -``` - - - - - -```cs -using TapTap.Bootstrap; - -var config = new TapConfig.Builder() - .ClientID("your_client_id") // (Required) Client ID in the Developer Center. - .ClientToken("your_client_token") // (Required) Client Token in the Developer Center. - .ServerURL("https://your_server_url") // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API - .RegionType(RegionType.IO) // (Optional) CN for Mainland China; IO for international. - .TapDBConfig(true, "gameChannel", "gameVersion", true) // TapDB is automatically initialized based on the configuration of TapConfig. - - .ConfigBuilder(); - -TapBootstrap.Init(config); -``` - - - -TapDBConfig descriptions: - -```cs -public Builder TapDBConfig(bool enable, string channel, string gameVersion, bool advertiserIDCollectionEnabled) -``` - -Parameter | Can be null | Descriptions ---- | --- | --- -enable | No | Whether to open TapDB service. -channel | Yes | Subcontracting channels, length not greater than 256. -version | Yes | Game version, when empty, automatically obtains the version of the game installation package. The length is not greater than 256. -advertiserIDCollectionEnabled | No | IDFA switch, please refer to the [Collect Device Fingerprint](/sdk/tapdb/sdk/client-side-integration#idfaios) documentation. - - - -<> - - - -```java -TapDBConfig tapDBConfig = new TapDBConfig(); -tapDBConfig.setEnable(true); // Whether to open TapDB service. -tapDBConfig.setChannel("gameChannel"); // Subcontracting channels, length not greater than 256. -tapDBConfig.setGameVersion("1.0.0"); // Game version, when empty, automatically obtains the version of the game installation package. The length is not greater than 256. - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(getApplicationContext()) - .withClientId("your_client_id") // (Required) Client ID in the Developer Center. - .withClientToken("your_client_token") // (Required) Client Token in the Developer Center. - .withServerUrl("https://your_server_url") // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API - .withRegionType(TapRegionType.CN) // TapRegionType.CN: China; TapRegionType.IO: Other countries or regions。 - .withTapDBConfig(tapDBConfig) - .build(); -TapBootstrap.init(MainActivity.this, tapConfig); -``` - - - - - - -```java -TapDBConfig tapDBConfig = new TapDBConfig(); -tapDBConfig.setEnable(true); // Whether to open TapDB service. -tapDBConfig.setChannel("gameChannel"); // Subcontracting channels, length not greater than 256. -tapDBConfig.setGameVersion("1.0.0"); // Game version, when empty, automatically obtains the version of the game installation package. The length is not greater than 256. - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(getApplicationContext()) - .withClientId("your_client_id") // (Required) Client ID in the Developer Center. - .withClientToken("your_client_token") // (Required) Client Token in the Developer Center. - .withServerUrl("https://your_server_url") // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API - .withRegionType(TapRegionType.IO) // TapRegionType.CN: China; TapRegionType.IO: Other countries or regions. - .withTapDBConfig(tapDBConfig) - .build(); -TapBootstrap.init(MainActivity.this, tapConfig); -``` - - - - - -<> - - - -```objectivec - -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // (Required) Client ID in the Developer Center. -config.clientToken = @"your_client_token"; // (Required) Client Token in the Developer Center. -config.region = TapSDKRegionTypeCN; // TapRegionType.CN: China; TapRegionType.IO: Other countries or regions. -config.serverURL = @"https://your_server_url"; // // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API. - -TapDBConfig * dbConfig = [[TapDBConfig alloc]init]; -dbConfig.enable = YES; // Whether to open TapDB service. -dbConfig.channel=@"taptap"; // Subcontracting channels, length not greater than 256. -dbConfig.gameVersion=@"1.0.0"; // Game version, when empty, automatically obtains the version of the game installation package. The length is not greater than 256. -dbConfig.advertiserIDCollectionEnabled=YES; // IDFA switch, please refer to the [Collect Device Fingerprint](/sdk/tapdb/sdk/client-side-integration#idfaios) documentation. -config.dbConfig = dbConfig; - -config.region = TapSDKRegionTypeCN; -[TapBootstrap initWithConfig:config]; -``` - - - - - -```objectivec - -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // (Required) Client ID in the Developer Center. -config.clientToken = @"your_client_token"; // (Required) Client Token in the Developer Center. -config.region = TapSDKRegionTypeIO; // TapRegionType.CN: China; TapRegionType.IO: Other countries or regions. -config.serverURL = @"https://your_server_url"; // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API. - -TapDBConfig * dbConfig = [[TapDBConfig alloc]init]; -dbConfig.enable = YES; // Whether to open TapDB service. -dbConfig.channel=@"taptap"; // Subcontracting channels, length not greater than 256. -dbConfig.gameVersion=@"1.0.0"; // Automatically get the game version When the value is null. The length is not greater than 256. -dbConfig.advertiserIDCollectionEnabled=YES; // IDFA switch, please refer to the [Collect Device Fingerprint](/sdk/tapdb/sdk/client-side-integration#idfaios) documentation. -config.dbConfig = dbConfig; - -[TapBootstrap initWithConfig:config]; -``` - - - - - -<> - -Initializing in this way requires importing the `TapBootstrap`, opening the `TapBootstrap`, and adding `TapBootstrap` to the `Project.Build.cs` file. - -Import the header file. - -```cpp -#include "TapUEBootstrap.h" -#include "TapUECommon.h" -#include "TapUEDB.h" -``` - - - -```cpp -FTUConfig Config; -Config.ClientID = ClientID; // (Required) Client ID in the Developer Center. -Config.ClientToken = ClientToken; // (Required) Client Token in the Developer. -Center.Config.ServerURL = ServerURL; // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API. -Config.RegionType = ERegionType::CN; -Config.DBConfig.Enable = true; -Config.DBConfig.Channel = Channel; -Config.DBConfig.GameVersion = GameVersion; -Config.DBConfig.AdvertiserIDCollectionEnabled = AdvertiserIDCollectionEnabled; -TapUEBootstrap::Init(Config); -``` - - - - - -```cpp -FTUConfig Config; -Config.ClientID = ClientID; // (Required) Client ID in the Developer Center. -Config.ClientToken = ClientToken; // (Required) Client Token in the Developer. -Center.Config.ServerURL = ServerURL; // (Required) Developer Center > Your Game > Game Services > Configuration > Domain > API. -Config.RegionType = ERegionType::IO; -Config.DBConfig.Enable = true; -Config.DBConfig.Channel = Channel; -Config.DBConfig.GameVersion = GameVersion; -Config.DBConfig.AdvertiserIDCollectionEnabled = AdvertiserIDCollectionEnabled; -TapUEBootstrap::Init(Config); -``` - - - -#### DBConfig Descriptions - -Parameter | Can be null | Descriptions ---- | --- | --- -Enable | No | Whether to open TapDB service. -Channel | Yes | Subcontracting channels, length not greater than 256. -Version | Yes | Automatically get the game version When the value is null. The length is not greater than 256. -AdvertiserIDCollectionEnabled | No | IDFA switch, please refer to the [Collect Device Fingerprint](/sdk/tapdb/sdk/client-side-integration#idfaios) documentation. - - - - - -### TapDB Only - -When only using the TapDB without using the login functionality and without importing the `TapBootstrap` package, TapDB can be initialized as follows: - - - -<> - - - -```cs -public static void Init(string clientId, string channel, string gameVersion, bool isCN) - -TapDB.Init("clientId", "taptap", "gameVersion", true); -``` - - - - - -```cs -public static void Init(string clientId, string channel, string gameVersion, bool isCN) - -TapDB.Init("clientId", "taptap", "gameVersion", false); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -clientId | No | Client ID in the Developer Center. -channel | Yes | Subcontracting channels. -version | Yes | Game version, when empty, automatically obtains the version of the game installation package. -isCN | Yes | true: Chaina,false: Other countries or regions. - - - -<> - - - -```java -public synchronized static void init(final Context context, - final String clientId, - final String channel, - final String gameVersion, - final boolean isCN) - -public synchronized static void init(final Context context, - final String clientId, - final String channel, - final String gameVersion, - final boolean isCN, - final JSONObject properties) - -TapDB.init(getApplicationContext(), "clientId", "taptap", "gameVersion", true); -``` - - - - - -```java -public synchronized static void init(final Context context, - final String clientId, - final String channel, - final String gameVersion, - final boolean isCN) - -public synchronized static void init(final Context context, - final String clientId, - final String channel, - final String gameVersion, - final boolean isCN, - final JSONObject properties) - -TapDB.init(getApplicationContext(), "clientId", "taptap", "gameVersion", false); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -context | No | The Context object of the current Application or Activity. -clientId | No | Client ID in the Developer Center. -channel | Yes | Subcontracting channels, length not greater than 256. -version | Yes | Game version, when empty, automatically obtains the version of the game installation package. -isCN | Yes | true: Chaina,false: Other countries or regions. -properties | Yes | The event property of device login (device_login). It can be passed in with preset attributes to override the default values of the SDK, and it can also be passed in with custom attributes that have been configured in the background. - - - -<> - - - -```objectivec -+ (void)onStartWithClientId:(NSString *)clientId channel:(nullable NSString *)channel version:(nullable NSString *)gameVersion isCN:(BOOL)isCN; - -[TapDB onStartWithClientId:@"clientid" channel:@"taptap" version:@"gameVersion" isCN:YES]; -``` - - - - - -```objectivec -+ (void)onStartWithClientId:(NSString *)clientId channel:(nullable NSString *)channel version:(nullable NSString *)gameVersion isCN:(BOOL)isCN; - -[TapDB onStartWithClientId:@"clientid" channel:@"taptap" version:@"gameVersion" isCN:NO]; -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -clientId | No | Client ID in the Developer Center. -channel | Yes | Subcontracting channels. Length not greater than 256. -version | Yes | Game version. When empty, automatically obtains the version of the game installation package (Version in Xcode configuration). -isCN | Yes | true: Chaina,false: Other countries or regions. - - - -<> - -Import: - -```cpp -#include "TapUEDB.h" -``` - -Initialize TapDB: - - - -```cpp -FTUDBConfig Config; -Config.ClientId = TEXT("your client id"); -Config.RegionType = ERegionType::CN; -Config.Channel = TEXT("your Channel"); -Config.GameVersion = TEXT("GameVersion"); -TapUEDB::Init(Config); -``` - - - - - -```cpp -FTUDBConfig Config; -Config.ClientId = TEXT("your client id"); -Config.RegionType = ERegionType::IO; -Config.Channel = TEXT("your Channel"); -Config.GameVersion = TEXT("GameVersion"); -TapUEDB::Init(Config); -``` - - - -#### DBConfig - -Parameter | Can be null | Descriptions ---- | --- | --- -ClientId | No | Client ID in the Developer Center. -RegionType | No | Developer Center > Application Configuration > Applicable Region. -Channel | Yes | Subcontracting channels. Length not greater than 256. -GameVersion | Yes | Game version, when empty, automatically obtains the version of the game installation package. - - - - - -## User Setting - -### Set User ID - -This API records an account when it is called to log in an account. After calling, a user login (user_login) event will be reported, and the "has_user" attribute of the device will be set to true. Before restarting the app or calling clear User, the reported events will all have the account ID. - - - -<> - -```cs -public static void SetUser(string userId) - -TapDB.SetUser("userId"); -``` - -Parameter | Can be null | Descriptions -| --- | --- | --- | -| userId | No | The unique string of the account, the string length is not greater than 256, and can only contain numbers, capital and lower case letters, underscores (_), and dashes (-). Developers need to ensure that the userId of different accounts are not the same. | - - - -<> - -```java -public static void setUser(final String userId) - -public static void setUser(final String userId, final JSONObject properties) - -TapDB.setUser("userId"); - -// Passing event properties simultaneously -JSONObject properties = new JSONObject(); -properties.put("currentPoints", 10); -TapDB.setUser("userId", properties); -``` - -Parameter | Can be null | Descriptions -| --- | --- | --- | -| userId | No | The unique string of the account, the string length is not greater than 256, and can only contain numbers, capital and lower case letters, underscores (_), and dashes (-). Developers need to ensure that the userId of different accounts are not the same. | -| properties | Yes | Event properties for user login (`user_login`).| - - - -<> - -```objectivec -+ (void)setUser:(NSString *)userId; - -+ (void)setUser:(NSString *)userId properties:(nullable NSDictionary *)properties; - -[TapDB setUser:@"userId"]; - -// Passing event properties simultaneously -[TapDB setUser:@"userId" properties:@{@"#currentPoints":@10}]; -``` - -Parameter | Can be null | Descriptions -| --- | --- | --- | -| userId | No | The unique string of the account, the string length is not greater than 256, and can only contain numbers, capital and lower case letters, underscores (_), and dashes (-). Developers need to ensure that the userId of different accounts are not the same. | -| properties | Yes | Event properties for user login (`user_login`).| - - - -<> - -```cpp -FString UserId = TEXT("userId"); -FString LoginType = TUDBType::LoginType::TapTap; -TapUEDB::SetUserWithLoginType(UserId, LoginType); -``` - -Parameter | Can be null | Descriptions -| --- | --- | --- | -| userId | No | The unique string of the account, the string length is not greater than 256, and can only contain numbers, capital and lower case letters, underscores (_), and dashes (-). Developers need to ensure that the userId of different accounts are not the same. | -| LoginType | Yes | User login method, refer to `TUDBType::LoginType`. | - - - - - - -### Clear User ID - -Once the user logout, you can call `clearUser` to clear the account ID saved in the current SDK, subsequent events reported will not have the account ID, and no events will be reported by calling this interface. - - - -```cs -public static void ClearUser() - -TapDB.ClearUser(); -``` - -```java -public static void clearUser() - -TapDB.clearUser(); -``` - -```objectivec -+ (void)clearUser; - -[TapDB clearUser]; -``` - -```cpp -TapUEDB::ClearUser(); -``` - - - -### Set User Name - -Once the user logs in, this interface can be called to set the user name, which will update the user name property (`user_name`). - - - -```cs -public static void SetName(string name) - -TapDB.SetName("Tarara"); -``` - -```java -public static void setName(final String name) - -TapDB.setName("Tarara"); -``` - -```objectivec -+ (void)setName:(NSString *)name; - -[TapDB setName:@"Tarara"]; -``` - -```cpp -FString Name = TEXT("Tarara"); // 用户游戏昵称 -TapUEDB::SetName(Name); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -name | No | User name. Length greater than 0 and less than or equal to 256. - - -### Set User Level - -Once the user logs in, this interface can be called to set the user level, which will update the user level property ( `user_name` ). - - - -```cs -public static void SetLevel(int level) - -TapDB.SetLevel(5); -``` - -```java -public static void setLevel(final int level) - -TapDB.setLevel(5); -``` - -```objectivec -+ (void)setLevel:(NSInteger)level; - -[TapDB setLevel:5]; -``` - -```cpp -int32 Level = 5; -TapUEDB::SetLevel(Level); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -level | No | User level. - -### Set User Server - -Once the user logs in, this interface can be called to set the user server, which will update the user server properties (`first_server` and `current_server`). - - - -```cs -public static void SetServer(string server) - -TapDB.SetServer("1 Server"); -``` - -```java -public static void setServer(final String server) - -TapDB.setServer("Server 1"); -``` - -```objectivec -+ (void)setServer:(NSString *)server; - -[TapDB setServer:@"Server 1"]; -``` - -```cpp -FString Server = TEXT("Server 1"); -TapUEDB::SetServer(Server); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -server | No | User server. - - -## Report Charge - -This interface can be called to report charge information after a user has made a charge. -After the call, the `charge` event will be reported and the incoming parameters will be used as properties of the event. - - - -```cs -public static void OnCharge(string orderId, string product, long amount, string currencyType, string payment) - -public static void OnCharge(string orderId, string product, long amount, string currencyType, string payment, - string properties) - -TapDB.OnCharge("0xueiEns", "game name", "100", "CNY", "wechat", "{\"on_sell\":true}"); -``` - -```java -public static void onCharge(final String orderId, final String product, final long amount, - final String currencyType, final String payment) - -public static void onCharge(final String orderId, final String product, final long amount, - final String currencyType, final String payment, final JSONObject properties) - -JSONObject info = new JSONObject(); -info.put("on_sell": true); -TapDB.onCharge("0xueiEns", "game name", "100", "CNY", "wechat", info); -``` - -```objectivec -+ (void)onChargeSuccess:(nullable NSString *)orderId product:(nullable NSString *)product amount:(NSInteger)amount currencyType:(nullable NSString *)currencyType payment:(nullable NSString *)payment; - -+ (void)onChargeSuccess:(nullable NSString *)orderId product:(nullable NSString *)product amount:(NSInteger)amount currencyType:(nullable NSString *)currencyType payment:(nullable NSString *)payment properties:(nullable NSDictionary *)properties; - -[TapDB onChargeSuccess:@"0xueiEns" product:@"game name" amount:100 currencyType:@"CNY" payment:@"wechat", properties:@{@"on_sell":YES}]; -``` - -```cpp -TapUEDB::OnCharge(OrderId, Product, Amount, CurrencyType, Payment, Properties); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -orderId | No | Order ID -product | Yes | Product name -amount | No | Charge amount -currencyType | Yes | Currency type, following ISO 4217 standard. Reference: CNY; USD; EUR -payment | Yes | Payment method -properties | Yes | The `charge` event properties - - -**Tips:It is recommended to use the server-side recharge statistics interface under conditions that permit it. Please refer to the [server-side integration documentation](/sdk/tapdb/sdk/server-side-integration#充值记录).** - -## Custom Events - -### Report Events - -This interface can be used to report events after the SDK has been initialized: - - - -```cs -public static void TrackEvent(string eventName, string properties) - -TapDB.TrackEvent("eventName", "{\"weapon\":\"axe\"}"); -``` - -```java -public static void trackEvent(final String eventName, final JSONObject properties) - -JSONObject properties = new JSONObject(); -properties.put("#weapon", "axe"); -properties.put("#level", 10); -properties.put("#map", "atrium"); -TapDB.trackEvent("#battle", properties); -``` - -```objectivec -+ (void)trackEvent:(NSString *)eventName properties:(NSDictionary *)properties; - -NSDictionary* dic = @{@"aaa":@"xxx",@"bbb":@"yyy"}; -[TapDB trackEvent:@"testEvent2" properties:dic]; -``` - -```cpp -TapUEDB::TrackEvent(EventName, Properties); -``` - - - - -Parameter | Can be null | Descriptions ---- | --- | --- -eventName | No | Event name -properties | Yes | Event properties - - -**Tips:** - -* The eventName supports reporting of preset events and custom events, where custom events should start with `#`. -* The key of the event property supports NSString type. -* The value of the event property supports NSString (maximum length 256) and NSNumber (range of values [-9E15, 9E15]) types. -* The event property supports reporting of preset properties and custom properties, where custom properties should start with `#`. -* When passing in preset properties for the event properties, the preset properties collected by default by the SDK will be overwritten. - - -### Sets Generic Event Properties - -For properties that need to be carried for all events, it is recommended to use a generic event property implementation. - -#### Add Static Generic Event Properties - - - -```cs -public static void RegisterStaticProperties(string staticProperties) - -//When the static generic event property `#current_channel` is set to `TapDB`, using event reporting is equivalent to adding a `#current_channel` to the event property. -string properties = "{\"#current_channel\":\"TapDB\"}"; -TapDB.RegisterStaticProperties(properties); -``` - -```java -public static void registerStaticProperties(final JSONObject staticProperties) - -//When the static generic event property `#current_channel` is set to `TapDB`, using event reporting is equivalent to adding a `#current_channel` to the event property. -JSONObject commonProperties = new JSONObject(); -commonProperties.put("#current_channel", "TapDB"); -TapDB.registerStaticProperties(commonProperties); -``` - -```objectivec -+ (void)registerStaticProperties:(NSDictionary *)staticProperties; - -// When the static generic event property `#current_channel` is set to `TapDB`, using event reporting is equivalent to adding a `#current_channel` to the event property. -[TapDB registerStaticProperties:@{@"#current_channel":@"TapDB"}]; -``` - -```cpp -TapUEDB::RegisterStaticProperties(Properties); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -staticProperties | No | Static generic event property dictionary - - -#### Delete A Single Static Generic Event Property - - - -```cs -public static void UnregisterStaticProperty(string propertyName) - -TapDB.UnregisterStaticProperty("#current_channel"); -``` - -```java -public static void unregisterStaticProperty(string propertyName) - -TapDB.unregisterStaticProperty("#current_channel"); -``` - -```objectivec -+ (void)unregisterStaticProperty:(NSString *)propertyName; - -[TapDB unregisterStaticProperty:@"#current_channel"]; -``` - -```cpp -TapUEDB::UnregisterStaticProperty(Key); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -propertyName | No | Static generic event property name - - -#### Delete All Static Generic Event Properties - - - -```cs -public static void ClearStaticProperties() - -TapDB.ClearStaticProperties(); -``` - -```java -public static void clearStaticProperties() - -TapDB.clearStaticProperties(); -``` - -```objectivec -+ (void)clearStaticProperties; - -[TapDB clearStaticProperties]; -``` - -```cpp -TapUEDB::ClearStaticProperties(); -``` - - - -#### Register Dynamic Generic Event Properties - -For generic event properties that may change at any time. You can register a dynamic generic event property callback to add the calculated property to this reported event property. - - - -```cs -public static void RegisterDynamicProperties(IDynamicProperties properties) - -// All subsequent events will carry the #currentLevel property, which contains the value of level at the time the event was reported. -public class TapDBDynamicPropertiesImpl : IDynamicProperties -{ - public Dictionary GetDynamicProperties() - { - Dictionary dic = new Dictionary(); - dic["#currentLevel"] = level; - return dic; - } -} -TapDB.RegisterDynamicProperties(new TapDBDynamicPropertiesImpl()); -``` - -```java -public static void registerDynamicProperties( - final TapDBDataDynamicProperties dynamicProperties) - -// All subsequent events will carry the #currentLevel property, which contains the value of level at the time the event was reported. -TapDB.registerDynamicProperties( - () -> { - JSONObject properties = new JSONObject(); - // getCurrentLevel 在这里仅作为案例,表示用户任何的自有逻辑实现 - long level = getCurrentLevel(); - properties.put("#currentLevel", level); - return properties; - } -); -``` - -```objectivec -+ (void)registerDynamicProperties:(NSDictionary* (^)(void))dynamicPropertiesCaculator; - -// All subsequent events will carry the #currentLevel property, which contains the value of level at the time the event was reported. -[TapDB registerDynamicProperties:^NSDictionary *_Nonnull { - return @{ - @"#currentLevel": level - }; - }]; -``` - -```cpp -TapUEDB::RegisterDynamicProperties(PropertiesBlock); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -dynamicProperties | No | Dynamic generic event properties compute callbacks - -**Tips:** - -* Using the same property name in the reported event or generic property will result in property overwriting. The priority overwriting is from highest to lowest: event property, dynamic generic event property, static generic event property, and preset property. For example, an event property set in `trackEvent` will override a dynamic generic event property, a static generic event property, or a preconfigured property with the same name. - - -## Modify User Attributes - -TapDB supports two kinds of objects: devices and accounts, and you can manipulate the properties of these two users through the following interfaces. - -### Modify Device Properties - -#### Device Property Initialization - -This interface can be used to make only the first value valid. Only if the current value is null, the assignment will take effect, if the current value is not null, the assignment will be ignored. - - - -```cs -public static void DeviceInitialize(string properties) - -string properties = "{\"firstActiveServer\":\"server1\"}"; -TapDB.DeviceInitialize(properties); -// The value of "#firstActiveServer" in the device table is "server1". - -string properties = "{\"firstActiveServer\":\"server2\"}"; -TapDB.DeviceInitialize(properties); -// The value of "#firstActiveServer" in the device table is still "server1". -``` - -```java -public static void deviceInitialize(final JSONObject properties) - -JSONObject properties = new JSONObject(); -properties.put("firstActiveServer", "server1"); -TapDB.deviceInitialize(properties); -// The value of "#firstActiveServer" in the device table is "server1". - -properties.put("firstActiveServer", "server2"); -TapDB.deviceInitialize(properties); -// The value of "#firstActiveServer" in the device table is still "server1". -``` - -```objectivec -+ (void)deviceInitialize:(NSDictionary *)properties; - -[TapDB deviceInitialize:@{@"firstActiveServer":@"server1"}]; -// The value of "#firstActiveServer" in the device table is "server1". - -[TapDB deviceInitialize:@{@"firstActiveServer":@"server2"}]; -// The value of "#firstActiveServer" in the device table is still "server1". -``` - -```cpp -TapUEDB::DeviceInitialize(Properties); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -properties | No | Properties Dictionary - - -#### Device Property Updates - -For regular device properties, this interface can be used to assign values, and the new property values will directly overwrite the old ones. - - - -```cs -public static void DeviceUpdate(string properties) - -string properties = "{\"currentPoints\":10}"; -TapDB.DeviceUpdate(properties); -// "currentPoints" in the device table is 10. - -properties = "{\"currentPoints\":42}"; -TapDB.DeviceUpdate(properties); -// "currentPoints" in the device table is 42. -``` - -```java -public static void deviceUpdate(final JSONObject properties) - -JSONObject properties = new JSONObject(); -properties.put("currentPoints", 10); -TapDB.deviceUpdate(properties); -// "currentPoints" in the device table is 10. - -properties.put("currentPoints", 42); -TapDB.deviceUpdate(properties); -// "currentPoints" in the device table is 42. -``` - -```objectivec -+ (void)deviceUpdate:(NSDictionary *)properties; - -[TapDB deviceUpdate:@{@"currentPoints":@10}]; -// "currentPoints" in the device table is 10. - -[TapDB deviceUpdate:@{@"currentPoints":@42}]; -// "currentPoints" in the device table is 42. -``` - -```cpp -TapUEDB::DeviceUpdate(Properties); -``` - - - -Parameter | Can be null | Descriptions ---- | --- | --- -properties | No | Properties dictionary - - -#### Device Property Accumulation - -Properties of numeric type can use this interface for accumulation operation, after calling TapDB will accumulate the original property value and save the result value. - - - -<> - -```cs -public static void DeviceAdd(string properties) - -string properties = "{\"totalPoints\":10}"; -TapDB.DeviceAdd(properties); -// "totalPoints" in the device table is 10. - - -properties = "{\"totalPoints\":-2}"; -TapDB.DeviceAdd(properties); -// "totalPoints" in the device table is 8. -``` - - - -<> - -```java -public static void deviceAdd(final JSONObject properties) - -JSONObject properties = new JSONObject(); -properties.put("totalPoints", 10); -TapDB.deviceAdd(properties); -// "totalPoints" in the device table is 10. - -properties.put("totalPoints", -2); -TapDB.deviceAdd(properties); -// "totalPoints" in the device table is 8. -``` - - - -<> - -```objectivec -+ (void)deviceAdd:(NSDictionary *)properties; - -[TapDB deviceAdd:@{@"totalPoints":@10}]; -// "totalPoints" in the device table is 10. - -[TapDB deviceAdd:@{@"totalPoints":@(-2)}]; -// 此时设备表的 "totalPoints" 字段值为 8 -``` - - - -<> - -```cpp -TapUEDB::DeviceAdd(Properties); -``` - - - - - -Parameter | Can be null | Descriptions ---- | --- | --- -properties | No | Property dictionary, only supports numeric types - -In the above code example, the property value is an integer. -The accumulation operation also supports floating point numbers, but there is a precision problem with floating point summation, so developers need to pay attention to it. - -### Modify User Properties - -#### User Property Initialization - -Use the same method as "Device Property Initialization" operations. - - - -```cs -public static void UserInitialize(string properties) - -TapDB.UserInitialize(properties); -``` - -```java -public static void userInitialize(final JSONObject properties) - -TapDB.userInitialize(properties); -``` - -```objectivec -+ (void)userInitialize:(NSDictionary *)properties; - -[TapDB userInitialize:@{@"firstActiveServer":@"server1"}]; -``` - -```cpp -TapUEDB::UserInitialize(Properties); -``` - - - - -#### User Property Updates - -Use the same method as "Device Property Updates" operations. - - - -```cs -public static void UserUpdate(string properties) - -TapDB.UserUpdate(properties); -``` - -```java -public static void userUpdate(final JSONObject properties) - -TapDB.userUpdate(properties); -``` - -```objectivec -+ (void)userUpdate:(NSDictionary *)properties; - -[TapDB userUpdate:@{@"currentPoints":@10}]; -``` - -```cpp -TapUEDB::UserUpdate(Properties); -``` - - - -#### User Property Accumulation - -Use the same method as "Device Property Accumulation" operations. - - - -```cs -public static void UserAdd(string properties) - -TapDB.UserAdd(properties); -``` - -```java -public static void userAdd(final JSONObject properties) - -TapDB.userAdd(properties); -``` - -```objectivec -+ (void)userAdd:(NSDictionary *)properties; - -[TapDB userAdd:@{@"totalPoints":@10}]; -``` - -```cpp -TapUEDB::UserAdd(Properties); -``` - - - - -## Collecting Device Fingerprints - -Allows the SDK to capture device fingerprints to aid in data analysis, ad attribution and make statistical results more accurate. - -:::info -Please initialize the SDK after the operation such as permission application and setting IDFA switch to ensure the device fingerprint can be reported properly. -::: - -### OAID(Android) - -> Note: SDK version 3.15.0 and above support OAID version 1.0.5 ~ 1.2.1; 3.14.0 and below support OAID version 1.0.5 ~ 1.0.25. - - -TapDB SDK will carry this parameter (key is `device_id4`) in sending related events when the application accesses OAID third-party library. The supported versions of this third-party library are 1.0.5 ~ 1.2.1, because different versions change a lot, so the instructions for accessing different versions are as follows. - -For 1.0.5 ~ 1.0.25 no additional configuration is needed, just add the dependency of the corresponding third-party library to the application. - - -For 1.0.26 ~ 1.2.1, in addition to adding the corresponding third-party libraries, you need to add the following processing. - -#### 1. Set certificate information and profile - -The certificate is a "cert.pem" file applied through the Mobile Security Alliance mailbox , which corresponds to the package name. Two types of settings are supported. - -1. Copy the `cert.pem` file to the application `assets` directory, and note that the file name should be set to `packageName.cert.pem`, `packageName` is the current application package name. -2. Set the contents of the certificate file via the SDK interface `setOAIDCert`. - -One of the two options is sufficient, and when both are used, the certificate information set through the interface is preferred. - -The configuration file is' supplierconfig.json '. The application needs to change the contents corresponding to the internal appid to the application ID of the app market, and the other parts do not need to be modified. - -#### 2. Load the corresponding library file in the application project - -The library file names for different versions of OAID third-party libraries are as follows: - -| Versions | Libraries Name| -| ---- | ---- | -| 1.0.30 ~ 1.2.1 | msaoaidsec | -| 1.0.29 | nllvm1632808251147706677 | -| 1.0.27 | nllvm1630571663641560568 | -| 1.0.26 | nllvm1623827671 | - -In the 'onCreate' method of the custom 'Application' class in the Android project, add the code to load third-party libraries, for example, when the application integrates OAID version 1.2.1 as follows: The library file names of different versions of OAID third-party libraries are as follows. - -```java -System.loadLibrary("msaoaidsec"); -``` - -#### FAQ - -When the OAID library has been integrated in the project but the device OAID information is still not found when reporting, check the following items: - -1. Whether the device time is normal. -2. For version 1.0.26 and above, does the package name of the certificate correspond to the current package name. -3. For version 1.0.26 and above, whether the library file is loaded and whether the library file name is the same as the version. -4. The application in Android 12 reports an error as: `java.lang.UnsatisfiedLinkError`, and the application minSdkVersion is greater than or equal to 23, it is recommended to add in the application tag of AndroidManifest.xml: `android:extractNativeLibs="true"`. - -### IMEI(Android) - -After adding the following item in 'AndroidManifest.xml' and the user agrees to the permission application, the SDK will automatically collect the Android IMEI. - - - -<> - -```xml - -``` - - - -<> - -```xml - -``` - - -<> - -```xml -Not available for iOS platform -``` - -```cpp -//UE4 SDK does not provide this method -``` - - - - - -### IDFA(iOS) - - - For `iOS14.5` and above, getting IDFA requires a popup window with user confirmation. SDK does not get IDFA by default, you can call the interface to enable IDFA fetching. - - - -<> - -Please make sure the permission request description text is added to `info.plist`, the SDK will automatically pop up the permission request window during initialization. - -``` -NSUserTrackingUsageDescription -This item will be used to suggest personalized ads (or other descriptions) to you -``` - -If you are using TapSDK initialization, please pass `true` to `advertiserIDCollectionEnabled` in `TapDBConfig` to turn on the IDFA collection switch. - -If you are using TapDB SDK alone, please call the following interface to enable the IDFA acquisition switch: - -```cs -TapDB.AdvertiserIDCollectionEnabled(true); -``` - - - -<> - -```java -// Not available for Android platform -``` - - - -<> - -Please make sure the permission request description text is added to `info.plist`, the SDK will automatically pop up the permission request window during initialization. - -``` -NSUserTrackingUsageDescription -This item will be used to suggest personalized ads (or other descriptions) to you -``` - -If you are using TapSDK initialization, please pass `YES` in `advertiserIDCollectionEnabled` in `TapDBConfig` to turn on the IDFA collection switch. - -If you are using TapDB SDK alone, please call the following interface to enable the IDFA acquisition switch: - -```objective-c -[TapDB setAdvertiserIDCollectionEnabled:YES]; -``` - - - -<> - -iOS exclusive method - -```cpp -TapUEDB::AdvertiserIDCollectionEnabled(true); -``` - - - - - - -## Diagnosis {#themis} - -:::info -TapSDK 3.14.0 and above can access this feature. - -The UE4 SDK does not support diagnostic access at this time. -::: - -### Add Dependencies - - - -<> - -The SDK can be **imported via Unity Package Manager or imported manually**, either way. - -If you choose UPM import, you can add to the project's `Packages/manifest.json` file. - - -{`"dependencies":{ - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.tapdb": "https://github.com/TapTap/TapDB-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.themis": "https://github.com/taptap/TapThemis-Unity.git#3.2.3-3", -}`} - - -If you choose to import manually: - -* Find the TapSDK Unity download address on the [download page](/tap-download), download TapSDK-UnityPackage.zip and unzip it, then import the `TapTap_Common`, `TapTap_TapDB` and `TapTap_Themis` modules. - - - -<> - -```cs -repositories { - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'THEMIS-release2.6.7', ext:'aar') -} -``` - - - -<> - -```cs -// TapDB SDK is called automatically, no additional integration required. -``` - - - - - -### Namespace - - - -```cs -using TapTap.TapDB; -``` - -```cs -// TapDB SDK is called automatically, no additional integration required. -``` - -```cs -// TapDB SDK is called automatically, no additional integration required. -``` - - - -### Interface Description - -#### Set the log reporting level - - - -```cs -TapDB.ConfigAutoReportLogLevel(LogSeverity.LogError); -``` - -```cs -// not supported -``` - -```cs -// not supported -``` - - - -The default level is `LogError`, which will be reported automatically when the application log level is higher than the set level. - -#### Set whether to exit on exception - - - -```cs -TapDB.ConfigAutoQuitApplication(true); -``` - -```cs -// not supported -``` - -```cs -// not supported -``` - - - -Set whether to exit automatically when an uncaught exception occurs. - -#### Register Log Callback - - - -```cs -TapDB.RegisterLogCallback(logcallback); -public void logcallback(string condition, string statckTrace, LogType type ){ - -} -``` - -```cs -// not supported -``` - -```cs -// not supported -``` - - - -Register the application log Callback. Callback processing is invoked when the application outputs logs. - -#### Remove Log Listening - - - -```cs -TapDB.UnRegisterLogCallback(logcallback); -``` - -```cs -// not supported -``` - -```cs -// not supported -``` - - - -#### Reporting Exception - - - -```cs -TapDB.ReportException(new Exception("crash test from unity"),"crashMessage desc"); -``` - -```cs -// not supported -``` - -```cs -// not supported -``` - - - -Proactive reporting an exception. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/data-spec.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/data-spec.mdx deleted file mode 100644 index 7d71b9701..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/data-spec.mdx +++ /dev/null @@ -1,204 +0,0 @@ ---- -title: Data Specification -sidebar_position: 2 ---- - -## Device - -### Device ID - -When the SDK is initialized, it generates a unique ID for the terminal, which we call the device ID. - -#### ID Generation Rules - -| SDK | ID generation rules | -| --- | --- | -| Android | Try to get the locally saved device ID, Android ID in turn, if neither of them can get the correct result, then randomly generate UUID, the last obtained or generated device ID will be saved in local storage. | -| iOS | Try to get the local keychain saved device ID, if it fails, random UUID will be generated, and the last obtained or generated device ID will be saved in local storage.| - -#### Device Property - -Device properties are constant values and up-to-date states. The behavior of these users can be analyzed by correlating a characteristic of the device with an event. - -#### FAQ - -**Will the device ID change?** - -Android:The device ID is constant when the Android ID can be obtained. (Refer to [Google documentation](https://developer.android.com/training/articles/user-data-ids)). If the Android ID is not available, the device ID may change as you reinstall the app or reset the Google Ads ID. - - -iOS:The generated device ID is stored in the device's keychain, which ensures that the device ID is kept stable without resetting the system. - -## User - -### User ID - -User ID is a registered user ID. When a user registers or logs in, you can set the user's unique ID in your system directly or encrypted into the SDK, and this ID is the user's unique ID across platforms. - -#### User property - -User properties is constant values and up-to-date status. propertys or characteristics can be associated with events to analyze the behavior of these users. - -## Event - -[What is the event](/sdk/tapdb/sdk/user-event-model) - -### Preset Events - -| Event Name | Event | Description | -| --- | --- | --- | -| device_login | App Launching | This event is reported when the SDK initialization interface is called, and the first time a device ID is reported, a record is generated in the device table. | -| user_login | Login | This event is reported when the SDK SetUser interface is called. The first time an account ID is reported, a new record is added to the account table. | -| play_game | Game time | The SDK will start with the app entering the foreground as the timing point, and will end when the app enters the background. | -| charge | User charge | This event is reported when the SDK charge interface is called, and it is usually recommended to use the server-side REST API for reporting. | - -### Derivative Events - -In addition to reporting preset events, TapDB also records special events, which are called derivative events. Derivative events cannot be reported directly through API, and will only be triggered by preset events. - -| Event Name | Event | Description | -| --- | --- | --- | -| dau_device | App first launch of the day | This event is triggered when the App first reports `device_login` each day and can be used to quickly query the device DAU. | -| dvau_device | App first launch of the day (by version)| This event is triggered when the different versions of App first reports `device_login` each day and can be used to quickly query the device DAU. | -| wau_device | App First launch of the week | App triggered when `device_login` is first reported each week, can be used to quickly query device WAU. | -| mau_device | App First launch of the month | App triggered when `device_login` is first reported each month, can be used to quickly query device MAU.| -| dau_user | Account first login of the day | Triggered when the account reports `user_login` for the first time every day, can be used to quickly check the account DAU. | -| wau_user | Account first login of the week | Triggered when the account first reports `user_login` each week, can be used to quickly query the account WAU. | -| mau_user | Account first login of the month | Triggered when the account reports `user_login` each month, can be used to quickly query the account MAU. | - -### Custom Events - -In addition to preset events and derivative events, more custom events can also be created in the event management. - -## Data Format - -TapDB's REST API supports the JSON object format after URLEncode. If you use SDK to access, the data will also be converted into this format for reporting. - -### Event Data - -Record an event and its propertys: - -```js -{ - ["index" | "client_id"]: ["APPID" | "ClientID"], - "device_id": "DeviceID", - "user_id": "UserID", - "type": "track", - "name": "EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue" - } -} -``` - -#### System Fields - -Fields at the same level as properties are system fields. - -| Name | Type | Description | -| --- | --- | --- | -| index | string | The APPID of the project, you can view the ID in TapDB backend. | -| client_id | string | TapTap Client ID can be viewed in the TapTap Developer Center.| -| type | string | Data type, the value of "track" when reporting events. | -| device_id | string | The device ID at the time of the event. | -| user_id | string | User ID at the time of the event. | -| name | string | Event name,can be a preset event or a custom event. | - -Tips: - -- Preset events `device_login`,must also have `device_id`. -- Preset events `user_login`, `play_game` must have `device_id` and `user_id`. -- Preset events `charge`, must be a valid `user_id` (`user_id` was reported as `user_login`). - -#### Property Fields - -The fields inside properties are property fields, which will be the properties of the event, and you can extend them with custom properties. - -SDK preset event property: - -| Name | Type | Description | -| --- | --- | --- | -| os | string | Operating System(Support Android、iOS、Windows、Mac)| -| device_id1 | string | Reserved device ID (iOS SDK uses IDFA, Android SDK uses IMEI)| -| device_id2 | string | Reserved device ID(Android SDK uses Google ads ID)| -| device_id3 | string | Reserved device ID(Android SDK uses Android ID)| -| device_id4 | string | Reserved device ID(Android SDK uses OAID)| -| width | number | Screen width | -| height | number | Screen height | -| device_model | string | Equipment model (equipment manufacturer + equipment model) | -| os_version | string | OS Version | -| network | string | Network type (WiFi is 2, unknown is 3, 2G is 4, 3G is 5, 4G is 6)| -| channel | string | APP Packaging channels | -| app_version | string | App version | -| sdk_version | string | SDK version | - -### Property Action - -User property action: - -```js -{ - ["index" | "client_id"]: ["APPID" | "ClientID"], - "user_id": "UserID", - "type": ["initialise" | "update" | "add"], - "properties": { - "level": 15, - "#custom": "custom" - } -} -``` - -Device property action: - -```js -{ - ["index" | "client_id"]: ["APPID" | "ClientID"], - "device_id": "DeviceID", - "type": ["initialise" | "update" | "add"], - "properties": { - "level": 15, - "#custom": "custom" - } -} -``` - -#### System Fields - -At the same level as properties are the system fields. - -| Name | Type | Description | -| --- | --- | --- | -| client_id | string | TapTap Client ID can be viewed in the TapTap Developer Center. | -| type | string | Data type, property operations support `initialise`, `update`, `add`. | -| device_id | string | Device ID of the property operation | -| user_id | string | User ID of the property operation | - -Meaning of the type field. - -| Name | Description | -| --- | --- | -| initialise | Use initialise to make only the first value valid. Only if the current value is null, the assignment will take effect, if the current value is not null, the assignment will be ignored. | -| update | For regular device properties, you can use `update` assignment, the new property value will directly overwrite the old property value. | -| add | For numeric properties, you can use this interface to perform accumulation operation, TapDB will accumulate the original property value and save the result value. | - -Tips: - -- Only one of `device_id` and `user_id` can be selected for property operations. Select `device_id` for the device ID attribute and `user_id` for the account ID attribute. - -#### Property Fields - -The fields inside 'properties' are property fields. These fields will be treated as fields to be manipulated and treated as' type' values. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/server-side-integration.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/server-side-integration.mdx deleted file mode 100644 index f6617d094..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/server-side-integration.mdx +++ /dev/null @@ -1,230 +0,0 @@ ---- -title: Server Integration Guides -sidebar_label: Server Integration Guides -sidebar_position: 4 ---- - -import {Conditional} from '/src/docComponents/conditional'; - -Using the REST API, you can report data directly to TapDB without relying on the SDK. - -## Report Events and Properties - -Please refer to [Data Rules](/sdk/tapdb/sdk/data-spec#数据规则) for the format and meaning of data transfer. - -If the Response Code is 200, it means the data was reported successfully, please check the event writing status further in the burial site management. - -### Single Report - - - -`POST` `https://e.tapdb.net/v2/event` - - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/event` - - - -`Content-Type: application/json` - -Request content example: - -```json -{ - "client_id": "test_appid", - "device_id": "test_device_id", - "user_id": "test_user_id", - "type": "track", - "name": "#EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue" - } -} -``` - -### Batch Report - - - -`POST` `https://e.tapdb.net/v2/batch` - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/batch` - - - -`Content-Type: application/json` - -Request content example: - -```json -{ - "data": [ - { - "client_id": "test_appid", - "device_id": "test_device_id", - "user_id": "test_user_id", - "type": "track", - "name": "#EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue" - } - }, - { - "client_id": "test_appid", - "device_id": "test_device_id", - "user_id": "test_user_id", - "type": "track", - "name": "#EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue" - } - } - ] -} - -``` - -### FAQ - -- If the subject of the current event is not a device or a user, device_id and user_id can be passed any fixed value. - -- To ensure that events reported on the server side can also be analyzed using the device dimension, it is recommended to call the SDK's `GetDeviceID` interface on the client side to obtain a unique ID and report it to the App's server side. - -## Special Type Report - -### Online Number - -Since the SDK can't push accurate online data, we provide a server-side online data push interface here. The game server can count the number of people online every 5 minutes and push it to TapDB through the interface, TapDB will summarize the data. - -**Note: The online count is reported in json format, which is different from other common event reporting formats, please pay attention to the difference.** - - - -`POST` `https://se.tapdb.net/tapdb/online` - - - - - -`POST` `https://se.tapdb.ap-sg.tapapis.com/tapdb/online` - - - -`Content-Type: application/json` - -Request Content: - -| Parameter | Type | Descriptions | -| --- | --- | --- | -| client_id | string | The game ClientID. | -| onlines | array | Multiple online data (up to 100) | - -The structure of the onlines array is: - -| Parameter | Type | Descriptions | -| --- | --- | --- | -| server | string | TapDB accepts data only once per natural 5 minutes for the same server. | -| online | number | Online number | -| timestamp | number | Timestamp (in seconds) of the current statistics. TapDB will perform data alignment according to the natural 5 min. | - -Example: - -```json -{ - "client_id":"ClientID", - "onlines":[{ - "server":"s1", - "online":123, - "timestamp":1489739590 - },{ - "server":"s2", - "online":188, - "timestamp":1489739560 - }] -} -``` - -### Recharge Record - -Since the SDK push may be inaccurate, it is recommended to use the server-side recharge push interface directly. Note that you need to stop the reporting of recharge information in the client SDK to prevent duplicate statistics. - - - -`POST` `https://e.tapdb.net/v2/event` - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/event` - - - -`Content-Type: application/json` - -```json -{ - "name": "charge", // Event name, fixed to "charge" - "client_id": "ClientID", // Required. Note that the ClientID needs to be replaced with the game's ClientID. - "user_id": "userId", // Required. The userId must be the same as the userId passed by the SDK's setUser interface, and the user must have been pushed through the SDK interface. - "properties": { - "ip": "8.8.8.8", // Optional. IP of the rechargeable user. - "order_id": "100000", // Optional. The order ID is greater than 0 and less than or equal to 256. - "amount": 100, // Required. Greater than 0 and less than or equal to 100000000000. recharge amount. Unit cents, i.e. whatever the currency, needs to be multiplied by 100. - "virtual_currency_amount": 100, // Required, Number of virtual coins received, can be 0. - "currency_type": "CNY", // Optional. Currency type. Internationally accepted three-letter expression, default CNY when empty. - "product": "item1", // Optional. Length greater than 0 and less than or equal to 256. Product name. - "payment": "alipay" // Optional. Length greater than 0 and less than or equal to 256. Recharge channel. - } -} -``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/user-event-model.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/user-event-model.mdx deleted file mode 100644 index 642a94a99..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tapdb/sdk/user-event-model.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: User - Event Model -sidebar_position: 1 ---- - -We know that when playing a game, players will have different types of behaviors and events in the game. For example: - -- For RPGs, users will have behavioral events such as fighting monsters, upgrading, and buying equipment within the game. -- For PVP games, users will have behavioral events such as adding friends within the game. -- In card games, users will have behavioral events such as purchasing cards and using cards within the game. - - -Here are the users and events. We found that "Data Analysis" is analyzing **User** and **Event**. - -When users have various types of behavioral events, the events will have information related to Who, When, What, Where, How, etc. - -- **Who**:The user who triggered the Event. -- **What**:Event. -- **When**:Time when the Event is triggered. -- **Where**:Location information such as IP, country, province, city, and district when the Event is triggered. -- **How**:How the user triggered the Event, such as the type of device, OS type, OS version number, device brand, device model, device resolution, version of the game app, and other information. - -**Where What, When, Where, and How are all property states of the Event**. - -In the case of a competitive match game, "users teaming up for a match game" can be viewed as an "Event" named "match game" : - -- **Who**:Users who joined match game. -- **What**: Type of match game. -- **When**:Time of match game. -- **Where**:IP, country, province, city, district and other location information at the time of the match game. -- **How**:The user's phone model, system version, and phone resolution at the time of the match game. - -This is **Event** model and **Event** property information。**Every behavior to be analyzed in the game can be defined as Event and should be defined as Event, which is a prerequisite for data analysis.** - -You can configure Event information in TapDB's "Configuration" - "Event Management" page. - -User will also have a lot of information, such as registration time, registration address, registration channel, user level, device type, gender, age and other information, which are all property states of User. By using these properties, you can quickly filter users in behavioral analysis. or example, if we want to analyze the activity of paying users, you only need to analyze the users with the value of "accumulated payment amount" greater than 0. - -In the case of a competitive match game, "users teaming up for a match game" can be seen as the "who" in "event". - - -- Who are the users who participate in the game? -- How long have they been playing the game in total? -- What is the user's score? -- Which heroes have they played? -- What is the user's gender? -- What is the user's age? -- When did the user register? -- What is the user's registration channel? -- What is the user's cumulative payment amount? - -This is the User model. - -**The Event model and User model are called: User-event model**. - -Based on the User-Event model to design buried documents and collect information, you can: - -- PVP games: analyze the number of participations using different characters at different levels. -- Card games: analyze the participation of players with different VIP levels in Mid-Autumn Festival events. -- SLG: Analyze how being robbed of resources by other players affects retention in the first 7 days of entry; -- RPG: Focus on key churn causes by analyzing pass rates for key levels; -- SLG: Check the resource accumulation and consumption of different levels of players for the last 7 days in order to determine how to give out gifts. It is best to achieve visual display of net inflow and outflow; - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-appsafety/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-appsafety/_category_.json deleted file mode 100644 index 70785df10..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-appsafety/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Application Security", - "collapsed": true, - "position": 2.1 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-appsafety/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-appsafety/features.mdx deleted file mode 100644 index f16f9c015..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-appsafety/features.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Introduction to Application Security Features -sidebar_label: Function Introduction -sidebar_position: 1 ---- - -## Introduction to Application Security -TapTap Developer Service provides an app security module that mainly protects the security of game apps through APK reinforcement and anti-cheating detection capabilities. - -## Rules - -- Applicable games: games that have joined the Fire Plan and are running normally, or games exclusively released on the TapTap platform. -- Entrance: Developer Centre - Select Game - Game Service - Application Security (as below) - -![](https://capacity-files.lcfile.com/XFKCH21Nw56UDX7B2VGXTtevPl6Eh9DY/app-safety-introduce.png) \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-appsafety/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-appsafety/guide.mdx deleted file mode 100644 index b57466a8a..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-appsafety/guide.mdx +++ /dev/null @@ -1,231 +0,0 @@ ---- -title: Application Security Access Guide -sidebar_label: Guide -sidebar_position: 2 ---- - -This article describes how to add the app security feature introduced by TapTap to your game. - -## Description of APK hardening function - -### Functional features - -| Function Name | Descriptions | -| --- | --- | -| SO Encryption Protection | Encrypt and obfuscate the code, functions and export tables of the SO files to prevent HOOK attacks and effectively improve the difficulty of unpacking. | -| global-metadata Encrypted |For IL2CPP games, the global-metadata is hardened to raise the cracking threshold. | -| AAB Reinforcement | Support for Android app bundle hardening solutions. | -| Reinforcement programme self-protection | The source code of the reinforcement scheme is obfuscated, which greatly raises the threshold for reverse analysis of the reinforcement scheme by plug-in authors and further increases the difficulty of cracking the game. | -| ROOT Environmental Testing | Reduce the potential for fraud in high-risk environments. | -| Debugging Against | Detection of debugging tools such as IDA, FRIDA, etc., effectively raising the threshold for dynamic analysis. | -| Simulator Recognition | Based on a sample library, it accurately identifies simulator users and provides an accurate basis for game segregation matching. | -| Cloud Mobile Phone Recognition | Multi-dimensional determination of the cloud phone environment based on real phone data and multiple cloud phone samples from the extranet. | -| Anti Secondary Packaging | To keep the signature consistent before and after reinforcement, you can enable signature verification to prevent secondary packaging. | -| Malware Countermeasure | Anti-frame plug-ins, memory modifiers, multi-openers and other software. | - -### Operating Instructions - -#### Function Entrance - -- Games that have not joined the Fire Plan and have exclusive published on TapTap can be entered directly through the **Application Security** portal, or contact the relevant business or operation departments to apply for it if there is no such portal. - -- For games joined the Fire Plan, you can select the APK enhancement rights directly from the Fire Plan page and click Use Now (as shown below). - -![](https://capacity-files.lcfile.com/wKzxfJfVfKOxDKRlEgiQvAnQ69OdKiak/app-safety-jiagu.png) - -#### Rules - -- The number of successful reinforcement attempts for each game in a single natural month is not allowed to exceed 5 times. Developers should not abuse the service. -- For users with multi-channel requirements, please ensure that the project is in normal operation if reinforcement is required before secondary packaging. -- The size of the augmentation file should not exceed 2G; -- Reinforcement does not support automatic signing and you must re-sign after reinforcement; -- You can only reinforce a maximum of 2 files each time. - -## Description of the anti-cheating function - -The anti-cheating detection function mainly means that after the game is reinforced, the platform will monitor and capture data on illegal operations and environment when the app is running for display. By displaying cheating devices and related details, developers can gain a deep understanding of the current security situation and accurately grasp the security situation. - -![](https://capacity-files.lcfile.com/XJgCXueaiMHdcKDHjom7yBel6fqL8zD7/app-safety-resist.png) - -### Page Field Descriptions - -- UUID: running device tag -- UserID: reported on the game page -- Feature number: hexadecimal display value - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Feature Code ID Detection Type Detailed Information
    1000000000000000+id 0 root class root system, test signature pattern
    1 root root、magisk、TESTSIGNING
    2000000000000000+id 0 Debugging Classes Debugger
    1 android debug ida、frida、java Information
    3 android ADB adb enabled
    3000000000000000+id 0 hook class hook
    1 android hooked, iOS inline hook api flag
    2 android hook frame, iOS fh hook hook frame
    3 android java hooked java hook flag
    5 android hook framework (multi-opener))
    4000000000000000+id 0 Memory Classes Runtime memory modified externally
    1 Determining when a memory device is open
    5000000000000000+id 0 Resource Modification Class Resource, Signature Modified
    1 android signature, iOS exception signature team apk or so signature information
    6000000000000000+id 0 Emulator Classes Running with an emulator
    any Emulator Type
    7000000000000000+id 0 Cloud Phone Class Running with a Cloud Phone
    any Cloud Phone Type
    8000000000000000+id 0 Generic Plug-ins, Peripherals Classes
    any Generic Plug-ins, Peripherals type value Peripheral
    B000000000000000+id 0 Sandbox Classes
    any Sandbox Detection Type Type
    - - - - -- Detection type - -| index | type | description | -| --- | --- | --- | -| 1 | Root Class | android and ios: running environment is root environment, windows: test signature environment | -| 2 | Debugging Class | Running processes are debugged | -| 3 | Hook Class | hooked | -| 4 | Memory Modifier Class | Running content has been modified | -| 5 | Resource Modification Class | Resource signatures have been modified | -| 6 | Simulator Class | Simulator running | -| 7 | Cloud Provider Class | Cloud real machine operation | -| 8 | General Plug-ins | Known types of cheating software detection | -| 9 | Special Plug-ins | Peripheral Input Capture Calculation | -| 10 | Injection Class | Cheat frames or other injections | -| 11 | Sandbox Class | Sandbox environment operation | - -- Detection results - -1. Implement processing strategies according to detection types. - -2、Detect to stop or do not perform additional operations. - -- Detailed information - -1、Detection type details - - -### Function usage rules - -#### Diagram operation - -- Provide filter function based on report ID, user ID, detection type, detection result and time. -- Provide report download function to support downloading the detected data to the local area. - -![](https://capacity-files.lcfile.com/skGGCntpLlMUqjeQOMESC2mHQs7dOG9q/app-safety-diagrams.png) - -### Anti-cheat strategy configuration - -- Anti-cheating strategy configuration is provided, and developers can configure their own anti-cheating strategies. -- Configuration is enabled by default and supports configuration. Developers can disable it if they wish. After it is disabled, the corresponding configured strategy will not take effect. - -![](https://capacity-files.lcfile.com/ItAilmNzJsHD2usODnWMLHIqahLDt9Ow/app-safety-cheat.png) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-connect/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-connect/_category_.json deleted file mode 100644 index 0fac2fad0..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-connect/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapTap Connect (Floating Window) ", - "collapsed": true, - "position": 2.5 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-connect/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-connect/features.mdx deleted file mode 100644 index 17dab3b92..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-connect/features.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: TapTap Connect Features -sidebar_label: Features -sidebar_position: 1 ---- - -## Products -TapTap Connect (hereafter referred to as the Floating Window) is a tool that connects TapTap, games and players. Once a developer has enabled this feature, players who have successfully logged in their TapTap accounts can browse game updates, rate games and share game download links in the Floating Window to help improve retention and bring in new users. - - -![](https://capacity-files.lcfile.com/NiPOqorbHRaoXCJvgoAmJzXXvKwJnMCU/461703147053_.pic_hd.jpg) - - -## Player Interaction Flow - -- After logging into the game with TapTap, a Floating Window portal will appear, which is semi-hidden by default and supports drag and drop as well as hovering. -- Clicking on the portal will open the Floating Window panel, which displays the player's TapTap account information (avatar, nickname and ID), and also allows the player to open embedded updates, share games, and jump to the TapTap game page to rate the game from within the Floating Window. (The rating feature is only supported by the Android system.) -- Players who click on the blank space outside the Floating Window panel will close the Floating Window and the portal will automatically return to a semi-hidden state after 3 seconds. - -## Functional Advantages - -TapTap community content, review sharing and other capabilities are integrated so that players can perform related operations without leaving the game. - -- Firstly, it can help the game increase retention and bring in new users; - -- Secondly, it reduces the amount of access development for developers. After accessing the Floating Window, there is no need to separately access embedded dynamics, open the comment area, share and other features. - -## Instructions for use - -### Pre-Access Operation -- The game needs to access the TapTap Login feature and enable the embedded moments service. - -- Enable the Floating Window feature in the [Game Services - TapTap Login - Floating Window ] module in the Developer Centre (the Floating Window feature is disabled by default and must be enabled by operation before it is displayed to players). - -## Theme Style Configuration Instructions - -### 1、Supports customisation of the Floating Window portal - -- Using the icons directly provided by the platform by default. - -- TapTap avatar can be used directly as the entrance icon. - -- Developers should follow the design specification and design their own entrance icon to match the game. - -### Supports customizing the background of the user information area of the Floating Window panel - -- By default, the backgrounds provided by the platform are used directly - -- Developers follow the design specifications and design their own backgrounds to match the game. - -![](https://capacity-files.lcfile.com/FbnCykRXSvU2UW9roMmF1Npta6pguw1I/451703147034_.pic_hd.jpg) diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-connect/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-connect/guide.mdx deleted file mode 100644 index f74952b84..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-connect/guide.mdx +++ /dev/null @@ -1,209 +0,0 @@ ---- -title: Floating Window Developer Guide -sidebar_label: Developer Guide -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import Languages from "../_partials/languages.mdx"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -This article describes how to add [TapTap Floating Window](/sdk/taptap-connect/features/) to your game. Using the Floating Window feature relies on TapTap Login and TapTap Moment. - - -## Environmental requirements - - - -<> - -- TapSDK **3.21.0** and above -- Unity 2019.4 or higher -- iOS 11 or later, Xcode version [14.1 or later](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0 (API level 21) or later - - - -<> - -- TapSDK **3.21.0** and above -- Android 5.0 (API level 21) or later - - - -<> - -- TapSDK **3.21.0** and above -- iOS 11 or later, Xcode version [14.1 or later](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -- TapSDK **3.22.0** and above -- Install UE 4.26 and above -- iOS 12 or higher -- Android 5.0 (API level 21) or higher -- macOS 10.14.0 or later -- Windows 7 or higher - -**Supported platforms**: Android / iOS / Windows / macOS - - - - - -## Pre-integration preparation - -1. Refer to [Preparation](/sdk/start/get-ready/) to create the application and turn on the Floating Window settings accordingly; - -## SDK Acquisition -Please refer to [TapTap Login](/sdk/taptap-login/guide/tap-login/#getting-the-sdk) and [embedded-moments](/sdk/embedded-moments/guide/#installing-sdk) to complete the SDK obtaining, and then you can get the TapSDK through [download](/tap-download) to add the `TapConnect` module on top of it: - - - - - - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapConnect_${sdkVersions.taptap.android}', ext:'aar') // TapTap Floating Window - implementation (name:'TapMoment_${sdkVersions.taptap.android}', ext:'aar') // TapTap Floating windows rely on inline dynamics - Mandatory - implementation (name:'TapLogin_${sdkVersions.taptap.android}', ext:'aar') // TapTap Floating Window Dependent Login Module - Required - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') // Required: TapSDK Base Library - implementation (name:'TapBootstrap_${sdkVersions.taptap.android}', ext:'aar') // Optional: TapSDK Base Library -}`} - - - - {`// TapTap Hover -TapConnectResource.bundle -TapConnectSDK.framework -// Floating Window relies on inline dynamics and login module - Mandatory -TapMomentResource.bundle -TapMomentSDK.framework -TapLoginSDK.framework -TapLoginResource.bundle -// Basic Module - Mandatory -TapCommonResource.bundle -TapCommonSDK.framework -// Basic Module - Optional -TapBootstrapSDK.framework -`} - - -```cs -PublicDependencyModuleNames.AddRange(new string[] { - //... - "TapConnect" //Mandatory - "TapLogin" //Mandatory-Floating Window relies on the login module - "TapMoment" //Mandatory-Floating Window relies on inline dynamics - "TapCommon" //Mandatory-Basic Module - "TapBootstrap"//Optional-Basic Module -}); -``` - - - -## Initialisation - -:::info -Choose either of the following two initialisation methods. -::: - -### TapSDK Initialisation - -If you have already done the initialisation of [built-in account system Tap Login](/sdk/taptap-login/guide/tap-login/#initialization), you only need to introduce the Floating Window module here, no other additional processing is needed. - - -### Separate initialisation of the Floating Window - -If the game does not initialise the TapSDK via the `TapBootstrap` method provided above, the Floating Window can be initialised separately, as the Floating Window relies on pure Tap authentication and inline dynamics. - -:::tip - -Initialising the Floating Window alone requires that the `only TapTap authentication` and `inline dynamic` initialisation actions are prioritised. For TapTap authentication-only initialisation [reference](/sdk/taptap-login/guide/tap-login/#initialization), and for embedded dynamic initialisation [reference](/sdk/embedded-moments/guide/#setting-up-callbacks). -::: -Below is the sample code to initialise the Floating Window separately: - - - -```cs -using TapTap.Connect; // namespace - -TapConnect.Init("your_client_id", "your_client_token", (bool)isCN); -``` - -```java -//activity is the current Activity instance -TapConnect.init(activity, "clientId", "clientToken", isCN); -``` - -```objc -[TapConnect initWithClientId:@"clientId" clientToken:@"clientToken" isCN:YES]; -``` - -```cpp -FTapConnect::Init("clientId", "clientToken", true); -``` - - - -### Parameter description - -* The `client_id` and `client_token` information can be viewed at **Developer Centre > Your Games > Game Services > Application Configuration**, and isCN indicates whether the application is from Mainland China. - -## Setting the Floating Window portal to show hidden - -Sometimes developers want to directly control the display and hiding of the Floating Window entry in some scenarios, e.g. only display the Floating Window entry in some scenarios, then they can call the following interface: - - - -```cs -TapConnect.SetEntryVisible(bool visible) -``` - -```java -TapConnect.setEntryVisible(boolean visible); -``` - -```objc -[TapConnect setEntryVisible:YES]; -``` - -```cpp -FTapConnect::SetEntryVisible(true); -``` - - - -## Internalisation - -The Floating Window supports setting the language: - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/_category_.json deleted file mode 100644 index ee0a19161..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapTap 登录", - "collapsed": true, - "position": 2 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/best-practice.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/best-practice.mdx deleted file mode 100644 index 13b291ded..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/best-practice.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Best TapTap Login Practices -sidebar_label: Best Practices -sidebar_position: 3 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -## Login Process - -Conversion rate increases when players have a streamlined login process. Use the simplest method that retains all necessary steps, so players can quickly enter the game. As shown below: - - - -### Login Interface - -Provide players with a TapTap Login button designed according to [Login Design Guide](/design/). Refer to [Feature Summary](/sdk/taptap-login/features/) for information on displaying single/multiple login methods. - -### Using Players' Public Info - -When players create characters in-game, the system can utilize authorized public info upon logging into the game, such as the player's TapTap avatar, nickname, etc., to help players complete the registration process automatically. - -If you are using the TDS Authentication system, refer to [Setting Other Users' Properties](/sdk/authentication/guide/#Setting Other Users' Properties) section to set user info. - -If you are using the basic TapTap Login, refer to the following flow chart: - - - -*Reference the TapTap Login Guide. - -### Provide a Switch Account Function - -We recommend providing players with a Switch Account function in-game. - -- When switching accounts, the player will be logged out. This guarantees that other in-game services (i.e. Moments) stay consistent with the logged-in account. -- When players have logged out, automatically display the login screen with the option for players to log in with another account. - -### Provide a Bind Account Function - -We recommend providing players with a Bind Account function. This gives players additional login methods. - -If you are using TDS Authentication to create the account system, refer to [Binding Third-Party Accounts](/sdk/authentication/guide/#binding-third-party-accounts) section. - -If you have your own account system and only use the basic TapTap Login, the game must have a Bind Account function so players can bind their game account with their unique ID returned after logging into TapTap. - -## Checklist - -Prior to providing players with a login function, developers must test the login process to see if it can be completed as intended. Check the following: - -- Whether the game meets [SDK Environment Requirements](/sdk/start/quickstart/#environment-requirements). -- Whether the developer understands the two TapTap Login methods in TapSDK. See [Integrating TapTap Login](/sdk/taptap-login/guide/start/). -- Whether the TapTap developer added the corresponding Android or iOS platform configurations to the backend. See [Configure Signature Certification](/sdk/start/quickstart/#configure-signature-certificate) and [Format Requirements](/sdk/taptap-login/features/#format-requirements). -- Whether users without the TapTap app can log in using WebView. Then check if the developer is able to obtain players' authorized basic info. -- Whether users with the most recent TapTap app can open the game and log in. Then check if the developer is able to obtain players' authorized basic info. -- Whether Silent Login is available after logging out once login authorization is complete. See [Silent Login](/sdk/taptap-login/features/#performing-silent-login). -- Whether the login process will restart upon leaving and re-entering the game without completing login authorization. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/faq.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/faq.mdx deleted file mode 100644 index d457b2c46..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/faq.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Frequently Asked Questions -sidebar_label: Frequently Asked Questions -sidebar_position: 4 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -### TapTap login reports `The requested scope (compliance) does not match any of the allowed scopes` exception -Check if the Real-Name Authentication & Anti-Addiction SDK package has been imported but not initialized. If you don't need the Real-Name Authentication service, don't import the Real-Name Authentication package into your project. - -### TapTap For iOS login reports `accessToken: sdk_not_matched` exception -Check that the app has the bundle ID configured in the TapTap developer backend. - -### TapTap login throws `signature not match` exception -This exception is thrown if you have a TapTap login but have not configured a signature or have configured it incorrectly in the Developer Centre backend. Some developers may not be able to debug the exception and can verify it this way: If the TapTap client is uninstalled and the WebView authorisation pops up when testing the login function, but the test device has the TapTap client installed, it will not pull up the client authorisation, this is basically due to a signature configuration issue, please refer to [documentation](/sdk/start/quickstart/#configure-signature-certificate) to complete the configuration. - -### TapTap login prompt `Not in beta or not in beta, can't login to game` exception prompt. -**TapTap Developer Center > Store > Releases > Internal Testing** Check to see if Internal Testing is enabled, and if so, make sure your current TapTap ID account has not been added as a test user. - -### TapTap login reports `state not equal` exception -Check if the system time of the current device is already synchronised with the network and if the TapTap client version of the current device is too low. - -### TapTap Login reported a `java.lang.NoSuchFieldException: CACHE_ELSE_NETWORK' exception -The TapSDK is already obfuscated because the Android project has enabled obfuscation, so you need to skip obfuscating the TapSDK. Please see [Android Code Obfuscation](/sdk/start/quickstart/#android-code-obfuscation) for details. Please disable resource obfuscation if it is enabled in your project, `shrinkResources false`. - -### TapTap login reported `{"code":36869,"error_description": "Unauthorised."}` Exception -Check that the Client ID, Client Token and ServerURL parameters of the TapSDK initialisation are set correctly. Also **use the HTTPS protocol** for the ServerURL and refer to the documentation on **[domain](/sdk/start/get-ready/#domain)**. - -### TapTap Login reports `chain validation failed` exception -The following steps can be used to troubleshoot this issue: -1. Check that the device is connected to the proxy and that the certificate is installed correctly. -2. Check that the telephone system time has not been changed. - -### TapTap Login reports `application id is empty` exception -1. Check that the initialisation operations of the TapSDK are being performed in the Android UI thread, i.e. the main thread; -2. Ensure that TapSDK initialisation is complete, to avoid calling the TapTap login function immediately after TapSDK initialisation, it is recommended that TapSDK initialisation is done before. - -### Simple TapTap user authentication for Unity integration reports the following exception - -``` -Assembly 'Assets/TapTap/Common/Plugins/TapTap.Common.dll' will not be loaded due to errors: -Unable to resolve reference 'LC.Newtonsoft.Json'. Is the assembly missing or incompatible with the current platform? -``` -The reason for this error is that the `com.leancloud.storage` module has not been added to the project's `Packages/manifest.json` file when using TapSDK Unity v3.7.1 and above, see [documentation](/sdk/taptap-login/guide/tap-login/#getting-the-sdk) to add it. - -### Login prompt: this application is not allowed for this domain - -1. Check that the TapTap login service is enabled in the developer backend application configuration; -2. Check that the ClientId, ClientToken, ServerUrl (must start with `https://`) in the project initialisation code are consistent with the developer backend. - - -![](https://dc-file.leanticket.cn/VIFonJ9YOJ4SAXb3WdVhMYWlF84xGKkN/CF18E02C-C819-4940-9C9B-60050062EDD0.png) - -### Can a developer get a player's mobile phone number after they have logged in using TapTap? - -No. Mobile phone numbers are private player information and developers are not currently allowed to obtain mobile phone numbers from logged-in players. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/features.mdx deleted file mode 100644 index 915a8faf0..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/features.mdx +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: TapTap Login Features -sidebar_label: Features -sidebar_position: 1 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { Conditional } from "/src/docComponents/conditional"; - -You must have a TapTap user account to access TapTap Developer Services (TDS). Otherwise, your application may encounter errors when calling the TDS API. This document describes how to implement TapTap Login into your application. - -## Service Info - -TapTap Account Services is an authorized login system based on the standard OAuth 2.0 protocol. It provides developers with a simple, safe and fast account login authorization function that eliminates the need for users to enter their account password. Users will be able to instantly log into your application with a click of a button by authorizing their TapTap account. - -After obtaining user authorization, developers can use interface call to obtain TapTap users' relevant public information, including user nickname, avatar and other information, which can be used to improve the user experience within the application. - -## Preliminary Work - -Confirm that the startup operation has been completed in **TapTap Developer Center > Game Services > Configuration**. See [Before You Start](/sdk/start/get-ready/) in the Getting Started Guide. - -### Configure Signature Certificate - -In order to improve security, TapTap Login services must verify your game. You need to submit the Android package name, iOS bundle name, and Android signature for the game. - -:::tip - -1. For the Android package name, please use the naming method according to Android specifications. See document: [Android Developer - Set Application ID](https://developer.android.com/studio/build/application-id) - -2. The Android signature is the MD5 string (32 bits) in the Keystore file. Please remove special symbols when filling it in. - -3. iOS Bundle ID should be named according to Apple specifications. See document: [Property List Key - CFBundle Identifier](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier) - -::: - -## Interactive Login Implementation - -If there is no user login status, developers must provide users with an interactive login interface for users to click. The TapTap review team will review your login interface when the app is put on the TapTap Store. See [Login Button Design Specifications](/design/) for creation. - -### Single Login Method - -If the application's only login method is through TapTap Login, we recommend creating an interactive login button on the start menu of the game. The shape and size of the button should not mislead or hinder the normal login procedure. - -Login button design can include game elements consistent with [Login Button Design Specifications](/design/). In addition, TDS also has TapTap Login button designs for various application scenarios available to quickly implement login. Click [TapTap Login Button Design Icon](/tap-download) to download resources. - - - - - - - - - - - - - -### Multiple Login Method - -If the game has additional login methods, developers should provide users with a reasonably arranged login interface that clearly distinguishes the various login methods. This should allow users to quickly find their preferred login method. - - - -## Silent Login Implementation - -Silent Login can help users expedite the login process. This feature is typically used in scenarios where users still have login statuses upon restarting the game. - -When a user starts the game, you can check whether the user has logged in on the current device and whether the login information is still valid. - -- For using the TDS Authentication method, refer to [Check Login Status](/sdk/taptap-login/guide/start/#check-login-status) -- For Basic TapTap Login, refer to [Check Login Status and User Information](/sdk/taptap-login/guide/tap-login/#check-login-status-and-user-info). - -This will allow you to help users complete the login process without displaying a login button or interface. - -## Login Authorization - -TapTap Account Services for mobile applications need to be used in conjunction with the TapTap Mobile Client. TapSDK will automatically use the appropriate login process according to the TapTap Client and user's device. - - - [Click here](https://www.taptap.cn/mobile) - - - [Click here](https://www.taptap.io/mobile) - to download the TapTap Mobile Client. - -### Calling Authorized Login for the TapTap Client - -When the user clicks the TapTap login button and the TapSDK detects the TapTap Client installed on the user's device, it will automatically call the TapTap client, identify the login information, and authorize the login. - - - - - - - - - - - - - -### Open Authorized WebView Login - - - -If the user clicks the TapTap login button and TapSDK does not detect the TapTap Client on the user's device, it will open WebView for the login process. - - - - - - - - - -If the user clicks the TapTap login button and TapSDK does not detect the TapTap Client on the user's device, it will be prompted to download the TapTap client. - - - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/_category_.json deleted file mode 100644 index 3446da847..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 2 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/start.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/start.mdx deleted file mode 100644 index d072cccab..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/start.mdx +++ /dev/null @@ -1,272 +0,0 @@ ---- -title: Integrate TapTap Login -sidebar_label: Integrate Functions -sidebar_position: 0 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import Profiles from "../../_partials/tap-login-profile.mdx"; - -Integrate TapTap Login Methods: - -1. Integrate TapTap login via [TDS Authentication System](/sdk/authentication/features). -2. [Basic TapTap User Verification](/sdk/taptap-login/guide/tap-login/). - -We recommend the first method for the following scenarios: - -- Integrating the account system provided by TapSDK. -- Allowing players to bind additional third-party accounts to their account (Ex: QQ, WeChat, Apple). -- Integrating basic TapSDK system functions in TDS Authentication such as Friends or Achievements. - -Otherwise, if you already have an account system and do not plan on using TapSDK functions such as Friends or Achievements, you can integrate TapTap User Login via the second method. - -We will introduce the first method and then the [second method](/sdk/taptap-login/guide/tap-login/). - -Both methods require visiting **Developer Center > Game Services > Integrate Functions** to activate TapTap Login. - - - - -## Check Login Status - -The SDK will save the user's login data in the local cache. When your game launches, you can retrieve the login data using the following code. This will allow the player to enter the game without having to log in. -Cache will not automatically be cleared. - -If the player logs out of the game or clears the game's cache, then the login info will also be deleted. - - - -```cs -var currentUser = await TDSUser.GetCurrent(); -if (null == currentUser) -{ - Debug.Log("Not logged in"); - // Start logging in -} -else -{ - Debug.Log("Logged in"); - // Enter the game -} -``` - -```java -if (null == TDSUser.currentUser()) { - // Not logged in -} else { - // Logged in; enter the game -} -``` - -```objectivec -TDSUser *currentUser = [TDSUser currentUser] -if (currentUser == nil) { - // Not logged in -} else { - // Logged in; enter the game -} -``` - - - -## Quick Log-in With TapTap - -Regarding TapTap User Login, TapSDK provides special support to developers with the fastest and most convenient integration. - -You can call `TDSUser.loginWithTapTap` to log in with a press of a button. For example: - - - -```cs -try -{ - // On iOS and Android, the TapTap app will be called or WebView will be used to log in. - // On Windows and macOS, the SDK will display a QR code (default) and a jump link (to be configured). - var tdsUser = await TDSUser.LoginWithTapTap(); - Debug.Log($"login sucess:{tdsUser}"); - // Get properties of TDSUser - var objectId = tdsUser.ObjectId; // Unique ID - var nickname = tdsUser["nickname"]; // Nickname - var avatar = tdsUser["avatar"]; // Avatar -} -catch (Exception e) -{ - if (e is TapException tapError) // using TapTap.Common - { - Debug.Log($"encounter exception:{tapError.code} message:{tapError.message}"); - if (tapError.code == TapErrorCode.ERROR_CODE_BIND_CANCEL) // Cancel Login - { - Debug.Log("Login cancelled"); - } - } -} -``` - -```java -TDSUser.loginWithTapTap(MainActivity.this, new Callback() { - @Override - public void onSuccess(TDSUser resultUser) { - Toast.makeText(MainActivity.this, "succeed to login with Taptap.", Toast.LENGTH_SHORT).show(); - // Developers can call the resultUser method to obtain additional properties. - String userId = resultUser.getObjectId(); // Unique User ID - String avatar = (String) resultUser.get("avatar"); // Avatar - String nickName = (String) resultUser.get("nickname"); // Nickname - } - - @Override - public void onFail(TapError error) { - Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show(); - } -}, "public_profile"); -``` - -```objectivec -[TDSUser loginByTapTapWithPermissions:@[@"public_profile"] callback:^(TDSUser * _Nullable user, NSError * _Nullable error) { - if (user) { - // Developers can call the user method to obtain additional properties. - NSString *userId = user.objectId; - NSString *username = user[@"nickname"]; - NSString *avatar = user[@"avatar"]; - } else { - NSLog(@"%@", error); - } -}]; -``` - - - -After calling the interface above, the TapTap client or a WebView will be opened up to start the log-in process. Once the user finishes authorizing, your app can use the result of the OAuth to finish logging in to the TDS account system. - -`TDSUser` is the current user's account system. Upon logging in, developers can: - -- Visit `objectId` to obtain the user's system ID (the unique identifier), which can be used to bind or match the player on the game server with TDS Authentication. -- Visit `nickname` properties to obtain the user's TapTap account name. -- Visit `avatar` properties to obtain the TapTap account's avatar. - -You can [view and manage user accounts](/sdk/authentication/features/#admin-console) by going to the Developer Center. - -### Obtain User Details - -Once TapTap user login is complete, developers can use the following methods to obtain information on TapTap authorization results: - - - -```cs -var profile = await TapLogin.FetchProfile(); -Debug.Log($"profile: {profile.ToJson()}"); -``` - -```java -Profile profile = TapLoginHelper.getCurrentProfile(); -``` - -```objectivec -[TapLoginHelper currentProfile] -``` - - - - - - -## User Logout - -Players can easily log out by calling `logOut`. - - - -```cs -await TDSUser.Logout(); -``` - -```java -TDSUser.logOut(); -``` - -```objectivec -[TDSUser logOut]; -``` - - - -## PC Login Configurations - -:::tip - -Unity SDK 3.5.2 onwards on Windows and macOS supports using a QR code or jump link for players to access the TapTap login page. - -SDK **supports QR scanning to log in by default**. - -Jump links must be configured. See below for examples: - -::: - - - -![PC Login](https://capacity-files.lcfile.com/BGyFDAQUNUrw9EuMcx8SyP7pek4BKz5u/taptap-login-pc.png) - - - - - -![PC Login](https://capacity-files.lcfile.com/GI0d6OOdTq4Xaphumdiayqi16JBgbmMg/taptap-login-pc.png) - - - -### Windows - -Using a jump link on Windows requires filling out the following configurations in the Registry: - -``` -Windows Registry Editor Version 5.00 - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}] -@="{Game Name}" -"URL Protocol"="{Program.exe installation path}}" - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}] -@="{Game Name}" - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}] - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}\Shell\Open] - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}\Shell\Open\Command] -@="\"{Program.exe installation path}\" \"%1\"" -``` - -### macOS - -On macOS, SDK will automatically configure `CFBundleURLTypes`. - -Use the following steps to configure without errors: - -- On Unity, open `BuildSetting` and select `PC, Mac & Linux Standalone` Platform. In `Target Platform`, select `macOS`. -- Check `Create XCode Project`, select `XCode` to compile. -- Open output `XCode Project`, select `Target`, click `Info`, open `URL Types`, inspect whether you have input the `URL Scheme`: `TapWeb : open-taptap-{clientId}`. If not, then enter: - -```xml -CFBundleURLTypes - - - CFBundleURLName - TapWeb - CFBundleURLSchemes - - open-taptap-{client_id} - - - -``` - -## Additional Functions - -View the [TDS Authentication Guide](/sdk/authentication/guide/) for info on the other functions of TDS Authorization. - - -## Video Tutorials - -You can refer to the video tutorial:[How to Integrate TapTap Login Functionality in Games](https://www.bilibili.com/video/BV1YX4y1b7TB/) to learn how to access login functionality in Untiy projects. - -For more video tutorials, see [Developer Academy](https://developer.taptap.cn/tds-tutorials/list). As the SDK features are constantly being improved, there may be inconsistencies between the video tutorials and the new SDK features, so the current documentation should prevail. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/tap-login.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/tap-login.mdx deleted file mode 100644 index cad38c3a2..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/tap-login.mdx +++ /dev/null @@ -1,371 +0,0 @@ ---- -title: Basic TapTap User Verification -sidebar_label: Basic Verification -sidebar_position: 1 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import Profiles from "../../_partials/tap-login-profile.mdx"; - -If you only wish to use TapTap as a login method and do not wish to use any other TDS cloud services, reference this document. Note: If you only select TapTap Login, choosing other cloud services at a later time may result in an upgrade fee. - - - -Developers using TapSDK v1.x can reference this guide to upgrade TapSDK. - - - -## Getting the SDK - -Integrating the basic TapTap Login requires the `TapLogin` and `TapCommon` modules. Please first [download](/tap-download) the TapSDK and add the corresponding dependencies. - -## Initialization - - - -:::note -When opening the app's configuration settings, select the game's [**Applicable Region**](/sdk/start/get-ready/#applicable-region). -During initialization, you must indicate whether the app will be used in Mainland China or other countries and regions. -::: - - - - -<> - - - -```cs -// Used in Mainland China -TapLogin.Init(string clientID); - -// Used in other countries and regions -TapLogin.Init(string clientID, bool isCn, bool roundCorner); -``` - - - - - -```cs -TapLogin.Init(string clientID, bool isCn, bool roundCorner); -``` - - - -**Parameter Summary** - -| Parameter | Description | -| ----------- | --------------------------------------------------------------------------------------------------------------------------------- | -| clientID | Determines an app's corresponding Client ID in the TapTap Developer Center | -| isCn | Depends on the [applicable region](/sdk/start/get-ready/#applicable-region). `true` for mainland China, `false` for International | -| roundCorner | Determines if the web page border is rounded when logging in. Rounded=true, straight=false | - - -<> - - - -```java -// Used in Mainland China -TapLoginHelper.init(Context context, String clientID); - -// Used in other countries and regions -LoginSdkConfig config = new LoginSdkConfig(); -config.regionType = RegionType.IO; -TapLoginHelper.init(Context context, String clientID, config); -``` - - - - - -```java -LoginSdkConfig config = new LoginSdkConfig(); -config.regionType = RegionType.IO; -TapLoginHelper.init(Context context, String clientID, config); -``` - - - -**Parameter Summary** - -| Parameter | Description | -| --------- | -------------------------------------------------------------------------- | -| context | Context generally refers to the current application | -| clientID | Determines an app's corresponding Client ID in the TapTap Developer Center | - - -<> - - - -```objectivec -// Used in Mainland China -[TapLoginHelper initWithClientID:clientID]; - -// Used in other countries and regions -TTSDKConfig *config = [[TTSDKConfig alloc] init]; -config.regionType = RegionTypeIO; -config.roundCorner = YES; -[TapLoginHelper initWithClientID:clientID config:config]; -``` - - - - - -```objectivec -TTSDKConfig *config = [[TTSDKConfig alloc] init]; -config.regionType = RegionTypeIO; -config.roundCorner = YES; -[TapLoginHelper initWithClientID:clientID config:config]; -``` - - - -**Parameter Summary** - -| Parameter | Description | -| ----------- | ------------------------------------------------------------------------------------------------------- | -| clientID | Determines an app's corresponding Client ID in the TapTap Developer Center | -| regionType | Refers to the conditional region. Mainland China=`RegionTypeCN`,other countries/regions=`RegionTypeIO` | -| roundCorner | Determines if the corners are rounded | - -**Transferring Configurations to TapTap Applications** - -If the user doesn't have the TapTap application, WebView will open by default. - -Open info.plist to add the following configurations, then switch the clientID to the clientID obtained from your console. - -```xml -CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - tt[clientID] - - - - -LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - -``` - -If the project has SceneDelegate.m, delete it and add the following code to the AppDelegate.m file. - -```objectivec -#import -- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [TDSHandleUrl handleOpenURL:url];; -} - -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSHandleUrl handleOpenURL:url];; -} -``` - -Add UIWindow to AppDelegate.h, then delete the Application Scene Manifest in the info.plist. - -```objectivec -@property (strong, nonatomic) UIWindow *window; -``` - - - - - -## TapTap Login and Retrieving Login Results - -:::tip -After the user opens the game, the game can check the [login status of the user](#check-login-status-and-user-info). If there is already a user logged in, the game can let the user proceed to the game without having to manually log in again. -::: - - - -```cs -try -{ - // On iOS and Android, the SDK will call the TapTap app or WebView to initiate login. - // On Windows and macOS, the SDK will display a QR code (default) and a jump link (to be configured). - var accessToken = await TapLogin.Login(); - Debug.Log($"TapTap Login Complete accessToken: {accessToken.ToJson()}"); -} -catch (Exception e) -{ - if (e is TapException tapError) // using TapTap.Common - { - Debug.Log($"encounter exception:{tapError.code} message:{tapError.message}"); - if (tapError.code == TapErrorCode.ERROR_CODE_BIND_CANCEL) // Cancel Login - { - Debug.Log("Login cancelled"); - } - } -} - -// Fetch TapTap Profile to obtain basic user info, such as their nickname and avatar. -var profile = await TapLogin.FetchProfile(); -Debug.Log($"TapTap Login Complete profile: {profile.ToJson()}"); -``` - -```java -// Instantiating Listener -TapLoginHelper.TapLoginResultCallback loginCallback = new TapLoginHelper.TapLoginResultCallback() { - @Override - public void onLoginSuccess(AccessToken token) { - Log.d(TAG, "TapTap authorization succeed"); - // Call TapLoginHelper.getCurrentProfile() to obtain basic user info, such as nickname and avatar. - Profile profile = TapLoginHelper.getCurrentProfile(); - } - - @Override - public void onLoginCancel() { - Log.d(TAG, "TapTap authorization cancelled"); - } - - @Override - public void onLoginError(AccountGlobalError globalError) { - Log.d(TAG, "TapTap authorization failed. cause: " + globalError.getMessage()); - } -}; -// Listener Registration -TapLoginHelper.registerLoginCallback(loginCallback); -// Login -TapLoginHelper.startTapLogin(MainActivity.this, TapLoginHelper.SCOPE_PUBLIC_PROFILE); -``` - -```objectivec -[TapLoginHelper registerLoginResultDelegate:delegator]; -if ([TapLoginHelper currentProfile]) { - // Already logged in -} else { - [TapLoginHelper startTapLogin:@[@"public_profile"]]; -} - -// delegator -- (void)onLoginCancel { - // Login cancelled -} - -- (void)onLoginError:(nonnull NSError *)error { - // Login failed -} - -- (void)onLoginSuccess:(nonnull TTSDKAccessToken *)token { - // Login Complete -} -``` - - - - - -This information is under fair use for developers. - -See [TapTap OAuth Interface](/sdk/taptap-login/guide/taptap-oauth/). - -## Check Login Status and User Info - -Login status and user info are saved in the local cache, which resets after logging in again. Logging out will clear the cache. - - - -```cs -// Obtain login status -try -{ - var accesstoken = await TapLogin.GetAccessToken(); - Debug.Log("Logged in"); - // Enter game directly -} -catch (Exception e) -{ - Debug.Log("Not logged in"); - // Start login -} - -// Obtain user info -await TapLogin.GetProfile(); - -// Obtain real-time user info -await TapLogin.FetchProfile(); -``` - -```java -// Obtain login status -TapLoginHelper.getCurrentAccessToken(); - -// Obtain user info -TapLoginHelper.getCurrentProfile(); - -// Obtain real-time user info -TapLoginHelper.fetchProfileForCurrentAccessToken(new ApiCallback() { - @Override - public void onSuccess(Profile data) { - - } - - @Override - public void onError(Throwable error) { - - } -}); -``` - -```objectivec -// Obtain login status -[TapLoginHelper currentAccessToken]; - -// Obtain user info -[TapLoginHelper currentProfile]; - -// Obtain real-time user info -[TapLoginHelper fetchProfileForCurrentAccessToken:^(TTSDKProfile *_Nonnull profile, NSError *_Nonnull error) {}]; -``` - - - -## Log Out - - - -```cs -TapLogin.Logout(); -``` - -```java -TapLoginHelper.logout(); -``` - -```objectivec -[TapLoginHelper logout]; -``` - - - -## PC Login Configurations - -Unity SDK 3.5.2 onwards on Windows and macOS supports using a QR code or jump link for players to access the TapTap login page. - -SDK **supports QR scanning to log in by default**. Jump links require [additional configuration](/sdk/taptap-login/guide/start/#pc-login-configurations). - -## Upgrading to TDS Authentication - -As mentioned above, if preliminary development only requires TapTap Login as a third-party interface but requires TDS Authentication services later(or when upgrading older 1.x game versions to use 3.x services), this will incur a development fee. Details are as follows: - -1. Reference the above guide on [Initialization](#initialization) and [Quick Log-in With TapTap](/sdk/taptap-login/guide/start/#quick-log-in-with-taptap) to complete TapTap Login for TDS Authentication to obtain a TDSUser login. - -2. Obtain the authorized user's `Profile` info. Note: The `Profile` info here must be the same as the `Profile` previously obtained from the game. Game developers should be able to use this to find the persistent player data saved on the game server. Otherwise, they can bind the current TDSUser with the original player info. As for the game, developers can decide whether to bind TDSUser with player accounts: - - - TapSDK doesn't require binding because it saves the cached login status internally. [Get `currentUser`](/sdk/taptap-login/guide/start/#check-login-status) when necessary to retrieve the previous TDS login status. - - Binding simplifies the process and allows you to expand TDS account info to more third-party platforms. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/taptap-oauth.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/taptap-oauth.mdx deleted file mode 100644 index 3017a7bea..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/taptap-login/guide/taptap-oauth.mdx +++ /dev/null @@ -1,598 +0,0 @@ ---- -title: TapTap OAuth Interface -sidebar_position: 4 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; -import {Conditional} from '/src/docComponents/conditional'; - -## Summary - -An OAuth Mac Token is a string that the TapTap OpenAPI uses to validate the request. - -When using the [Basic TapTap User Verification Interface](/sdk/taptap-login/guide/tap-login/), an access token bundle is generated after the user grants permission to the current application. It can be transformed into a MAC Token string after encryption. This access token bundle remains valid until the user updates their account's security information or denies authorization. Developers are advised to manage MAC Tokens on their server as a means of subsequent communication with the TapTap server. - -For details on the MAC Token algorithm, see the [MAC Token Algorithm](#mac-token-algorithm) section of this document. - - - -The following interface is an example for domestic applications. However, when the mobile server is initialized to international regions, logins are registered as international. The following server document remains the same, except change `open.tapapis.cn` to `openapi.tap.io` as the international domain name. - - - -## Process - -1. When the client uses SDK's TapTap Login, it can [Obtain AccessTokens](/sdk/taptap-login/guide/tap-login/#check-login-status-and-user-info), including: - - ```java - public String kid; - public String access_token; - public String token_type; - public String mac_key; - public String mac_algorithm; - public String expire_in; - private String json = null; - ``` - -2. Send the parameters obtained from the mobile client to the game server, then the server will sign a MAC Token. -3. Request `https://open.tapapis.cn/account/profile/v1``https://openapi.tap.io/account/profile/v1` with `mac token` included in the header. - -Note: The returned `kid` and `access_token` values are equal. We recommend using `kid`. - -## API - -### Obtain Current Account Details - -> GET https://open.tapapis.cn/account/profile/v1?client_id=xxxhttps://openapi.tap.io/account/profile/v1?client_id=xxx
    Authorization mac token - - -#### Request Parameters - -| String      | Type   | Description   | -| --------- | ------ | ------ | -| client_id | string | The app's `Client ID` must be the same as the contract| - -#### Response Parameters - -String             | Type           | Description ---------------- | ------------- | ------------ -name            | string        | User name -avatar          | string        | User avatar address -gender          | string        | "female", "male" or empty string -openid          | string        | The unique ID of the authorized user. The openid of each player for each game is different. The openid of players obtained by the same game is always the same -unionid         | string        | Unique identifier of authorized users. Unionid is the same for a player in all games from a manufacturer. Therefore, manufactures have different unionids. - -#### Request Example - -Replace the `MAC ID` and `Client ID` with your own signed MAC Token and console's `Client ID`. - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://open.tapapis.cn/account/profile/v1?client_id=" -``` - - - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://openapi.tap.io/account/profile/v1?client_id=" -``` - - - -## Other - -### MAC Token Algorithm - -MAC Token contains the following strings: - -| String          | Type   | Description                            | -| ------------- | ------ | ------------------------------- | -| kid           | string | mac_key id, The key identifier. | -| access_token  | string | This string currently has no use                    | -| token_type    | string | Token type (i.e. MAC)               | -| mac_key       | string | MAC key                         | -| mac_algorithm | string | MAC algorithm computation is called hmac-sha-1      | - -Use the MAC Token to sign an interface: - -### Script Request Example - -Use this script to verify the direct replacement parameters to ensure the MAC Token signed by your server is correct. - -Replace the CLIENT_ID with the `Client ID` obtained by the console. Replace ACCESS_TOKEN and MAC_KEY with the  `access_token` and `mac_key` obtained after logging into the client: - - - -``` -#!/usr/bin/env bash - -# Client ID -CLIENT_ID="Change to the `Client ID` obtained from the console" -# Aaccess_token obtained by SDK -ACCESS_TOKEN="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" -# mac_key obtained by SDK -MAC_KEY="mSUQNYUGRBPXyRyW" - -# Random number. Please replace for official launch. -NONCE="8IBTHwOdqNKAWeKl7plt8g==" -# Current timestamp -TS=$(date +%s) - -# Request method -METHOD="GET" -# Request addres (contains query string) -REQUEST_URI="/account/profile/v1?client_id=${CLIENT_ID}" -# Request domain name -REQUEST_HOST="open.tapapis.cn" - -MAC=$(printf "%s\n%s\n%s\n%s\n%s\n443\n\n" "${TS}" "${NONCE}" "${METHOD}" "${REQUEST_URI}" "${REQUEST_HOST}" | openssl dgst -binary -sha1 -hmac ${MAC_KEY} | base64) - -AUTHORIZATION=$(printf 'MAC id="%s",ts="%s",nonce="%s",mac="%s"' "${ACCESS_TOKEN}" "${TS}" "${NONCE}" "${MAC}") - -curl -s -H"Authorization:${AUTHORIZATION}" "https://${REQUEST_HOST}${REQUEST_URI}" -``` - - - - - -``` -#!/usr/bin/env bash - -# Client ID -CLIENT_ID="Change to the `Client ID` obtained from the console" -# Aaccess_token obtained by SDK -ACCESS_TOKEN="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" -# mac_key obtained by SDK -MAC_KEY="mSUQNYUGRBPXyRyW" - -# Random number. Please replace for official launch. -NONCE="8IBTHwOdqNKAWeKl7plt8g==" -# Current timestamp -TS=$(date +%s) - -# Request method -METHOD="GET" -# Request addres (contains query string) -REQUEST_URI="/account/profile/v1?client_id=${CLIENT_ID}" -# Request domain name -REQUEST_HOST="openapi.tap.io" - -MAC=$(printf "%s\n%s\n%s\n%s\n%s\n443\n\n" "${TS}" "${NONCE}" "${METHOD}" "${REQUEST_URI}" "${REQUEST_HOST}" | openssl dgst -binary -sha1 -hmac ${MAC_KEY} | base64) - -AUTHORIZATION=$(printf 'MAC id="%s",ts="%s",nonce="%s",mac="%s"' "${ACCESS_TOKEN}" "${TS}" "${NONCE}" "${MAC}") - -curl -s -H"Authorization:${AUTHORIZATION}" "https://${REQUEST_HOST}${REQUEST_URI}" -``` - - - -### nodejs Request Example - - - -```javascript -const http = require('http'); -const https = require('https'); -const crypto = require('crypto'); - -function getAuthorization(requestUrl, method, keyId, macKey) { - const url = new URL(requestUrl); - const time = Math.floor(Date.now() / 1000).toString().padStart(10, '0'); - const randomStr = getRandomString(16); - const host = url.hostname; - const uri = url.pathname + url.search; - const port = url.port || (url.protocol === 'https:' ? '443' : '80'); - const other = ''; - const sign = signData(mergeData(time, randomStr, method, uri, host, port, other), macKey); - - return `MAC id="${keyId}", ts="${time}", nonce="${randomStr}", mac="${sign}"`; -} - -function getRandomString(length) { - return crypto.randomBytes(length).toString('base64'); -} - -function mergeData(time, randomCode, httpType, uri, domain, port, other) { - let prefix = - `${time}\n${randomCode}\n${httpType}\n${uri}\n${domain}\n${port}\n`; - - if (!other) { - prefix += '\n'; - } else { - prefix += `${other}\n`; - } - - return prefix; -} - -function signData(signatureBaseString, key) { - const hmac = crypto.createHmac('sha1', key); - hmac.update(signatureBaseString); - return hmac.digest('base64'); -} - -const client_id = "hskc**********kklm"; -const keyId = "1/VLDoiGUhNCIpUq827L**************zAJ-i8hT_w9vuPtPgdaPkWDv6K4eVe_yZnKz************EYep-T4ki5w3kyYACVnM61JJqDEKfpNnHoTZU********************iUArkgPsWEwOpZGxva7FnqbTwmpLT0a28UtiR5gyr4XXutbnE5tb4A-iSqRpqqtgABXBZd34U5Th3iJ1C666iYQFvuQL9uC-Zv7-xKCNjyPonBqU4ZWZnKLFf2mzprU5vJCA8q5by1SZxY63kZBQieHYxFjyOCQdJ-25gDlxiqDbNq08kmSdY6TB1qtQ68V37L6a8nIzyVHooX9uc2Yw"; -const macKey = 'VPDalRmxtBqi******************tH937GNKIvj3'; -const requestUrl = 'https://open.tapapis.cn/account/profile/v1?client_id='+ client_id ; -const method = 'GET'; - - -const authorization = getAuthorization(requestUrl, method, keyId, macKey); -console.log(authorization); - -const options = new URL(requestUrl); -const client = options.protocol === 'https:' ? https : http; - -const req = client.request({ - hostname: options.hostname, - port: options.port, - path: options.pathname + options.search, - method: 'GET', - headers: { - 'Authorization': authorization - } -}, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - console.log(data); - }); -}); - -req.end(); - - -``` - - - - - -```javascript -const http = require('http'); -const https = require('https'); -const crypto = require('crypto'); - -function getAuthorization(requestUrl, method, keyId, macKey) { - const url = new URL(requestUrl); - const time = Math.floor(Date.now() / 1000).toString().padStart(10, '0'); - const randomStr = getRandomString(16); - const host = url.hostname; - const uri = url.pathname + url.search; - const port = url.port || (url.protocol === 'https:' ? '443' : '80'); - const other = ''; - const sign = signData(mergeData(time, randomStr, method, uri, host, port, other), macKey); - - return `MAC id="${keyId}", ts="${time}", nonce="${randomStr}", mac="${sign}"`; -} - -function getRandomString(length) { - return crypto.randomBytes(length).toString('base64'); -} - -function mergeData(time, randomCode, httpType, uri, domain, port, other) { - let prefix = - `${time}\n${randomCode}\n${httpType}\n${uri}\n${domain}\n${port}\n`; - - if (!other) { - prefix += '\n'; - } else { - prefix += `${other}\n`; - } - - return prefix; -} - -function signData(signatureBaseString, key) { - const hmac = crypto.createHmac('sha1', key); - hmac.update(signatureBaseString); - return hmac.digest('base64'); -} - -const client_id = "5enu******wfy"; -const keyId = "1/JFZi8****IiumsGZI31iJH1q*****UKZ-eKA"; -const macKey = 'LMbNcKox*******kfmk7oWXbuRz'; -const requestUrl = 'https://openapi.tap.io/account/profile/v1?client_id='+ client_id ; -const method = 'GET'; - -const authorization = getAuthorization(requestUrl, method, keyId, macKey); -console.log(authorization); - -const options = new URL(requestUrl); -const client = options.protocol === 'https:' ? https : http; - -const req = client.request({ - hostname: options.hostname, - port: options.port, - path: options.pathname + options.search, - method: 'GET', - headers: { - 'Authorization': authorization - } -}, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - console.log(data); - }); -}); - -req.end(); - - -``` - - - -### java Request Example - - - -```java -package com.taptap; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.*; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -public class Authorization { -    public static void main(String[] args) throws IOException { -        String client_id = "0RiAlMny7jiz086FaU"; -        String kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"; // kid -        String mac_key = "mSUQNYUGRBPXyRyW"; // mac_key -        String method = "GET"; -        String request_url = "https://open.tapapis.cn/account/profile/v1?client_id=" + client_id; // -        String authorization = getAuthorization(request_url, method, kid, mac_key); -        System.out.println(authorization); -        URL url = new URL(request_url); -        HttpURLConnection conn = (HttpURLConnection) url.openConnection(); -        // Http -        conn.setRequestProperty("Authorization", authorization); -        conn.setRequestMethod("GET"); -        BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); -        String line; -        StringBuilder result = new StringBuilder(); -        while ((line = rd.readLine()) != null) { -            result.append(line); -        } -        rd.close(); -        System.out.println(result.toString()); -    } -    /** -     * @param request_url -     * @param method "GET" or "POST" -     * @param key_id key id by OAuth 2.0 -     * @param mac_key mac key by OAuth 2.0 -     * @return authorization string -     */ -    public static String getAuthorization(String request_url, String method, String key_id, String -            mac_key) { -        try { -            URL url = new URL(request_url); -            String time = String.format(Locale.US, "%010d", System.currentTimeMillis() / 1000); -            String randomStr = getRandomString(16); -            String host = url.getHost(); -            String uri = request_url.substring(request_url.lastIndexOf(host) + host.length()); -            String port = "80"; -            if (request_url.startsWith("https")) { -                port = "443"; -            } -            String other = ""; -            String sign = sign(mergeSign(time, randomStr, method, uri, host, port, other), mac_key); -            return "MAC " + getAuthorizationParam("id", key_id) + "," + getAuthorizationParam("ts", time) -                    + "," + getAuthorizationParam("nonce", randomStr) + "," + getAuthorizationParam("mac", -                    sign); -        } catch (MalformedURLException e) { -            e.printStackTrace(); -        } -        return null; -    } -    private static String getRandomString(int length) { -        byte[] bytes = new byte[length]; -        new SecureRandom().nextBytes(bytes); -        String base64String = Base64.getEncoder().encodeToString(bytes); -        return base64String; -    } -    private static String mergeSign(String time, String randomCode, String httpType, String uri, -                                    String domain, String port, String other) { -        if (time.isEmpty() || randomCode.isEmpty() || httpType.isEmpty() || domain.isEmpty() || port.isEmpty()) -        { -            return null; -        } -        String prefix = -                time + "\n" + randomCode + "\n" + httpType + "\n" + uri + "\n" + domain + "\n" + port -                        + "\n"; -        if (other.isEmpty()) { -            prefix += "\n"; -        } else { -            prefix += (other + "\n"); -        } -        return prefix; -    } -    private static String sign(String signatureBaseString, String key) { -        try { -            SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); -            Mac mac = Mac.getInstance("HmacSHA1"); -            mac.init(signingKey); -            byte[] text = signatureBaseString.getBytes(StandardCharsets.UTF_8); -            byte[] signatureBytes = mac.doFinal(text); -            signatureBytes = Base64.getEncoder().encode(signatureBytes); -            return new String(signatureBytes, StandardCharsets.UTF_8); -        } catch (NoSuchAlgorithmException | InvalidKeyException e) { -            throw new IllegalStateException(e); -        } -    } -    private static String getAuthorizationParam(String key, String value) { -        if (key.isEmpty() || value.isEmpty()) { -            return null; -        } -        return key + "=" + "\"" + value + "\""; -    } -} -``` - - - - - -```java -package com.taptap; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.*; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -public class Authorization { -    public static void main(String[] args) throws IOException { -        String client_id = "0RiAlMny7jiz086FaU"; -        String kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"; // kid -        String mac_key = "mSUQNYUGRBPXyRyW"; // mac_key -        String method = "GET"; -        String request_url = "https://openapi.tap.io/account/profile/v1?client_id=" + client_id; // -        String authorization = getAuthorization(request_url, method, kid, mac_key); -        System.out.println(authorization); -        URL url = new URL(request_url); -        HttpURLConnection conn = (HttpURLConnection) url.openConnection(); -        // Http -        conn.setRequestProperty("Authorization", authorization); -        conn.setRequestMethod("GET"); -        BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); -        String line; -        StringBuilder result = new StringBuilder(); -        while ((line = rd.readLine()) != null) { -            result.append(line); -        } -        rd.close(); -        System.out.println(result.toString()); -    } -    /** -     * @param request_url -     * @param method "GET" or "POST" -     * @param key_id key id by OAuth 2.0 -     * @param mac_key mac key by OAuth 2.0 -     * @return authorization string -     */ -    public static String getAuthorization(String request_url, String method, String key_id, String -            mac_key) { -        try { -            URL url = new URL(request_url); -            String time = String.format(Locale.US, "%010d", System.currentTimeMillis() / 1000); -            String randomStr = getRandomString(16); -            String host = url.getHost(); -            String uri = request_url.substring(request_url.lastIndexOf(host) + host.length()); -            String port = "80"; -            if (request_url.startsWith("https")) { -                port = "443"; -            } -            String other = ""; -            String sign = sign(mergeSign(time, randomStr, method, uri, host, port, other), mac_key); -            return "MAC " + getAuthorizationParam("id", key_id) + "," + getAuthorizationParam("ts", time) -                    + "," + getAuthorizationParam("nonce", randomStr) + "," + getAuthorizationParam("mac", -                    sign); -        } catch (MalformedURLException e) { -            e.printStackTrace(); -        } -        return null; -    } -    private static String getRandomString(int length) { -        byte[] bytes = new byte[length]; -        new SecureRandom().nextBytes(bytes); -        String base64String = Base64.getEncoder().encodeToString(bytes); -        return base64String; -    } -    private static String mergeSign(String time, String randomCode, String httpType, String uri, -                                    String domain, String port, String other) { -        if (time.isEmpty() || randomCode.isEmpty() || httpType.isEmpty() || domain.isEmpty() || port.isEmpty()) -        { -            return null; -        } -        String prefix = -                time + "\n" + randomCode + "\n" + httpType + "\n" + uri + "\n" + domain + "\n" + port -                        + "\n"; -        if (other.isEmpty()) { -            prefix += "\n"; -        } else { -            prefix += (other + "\n"); -        } -        return prefix; -    } -    private static String sign(String signatureBaseString, String key) { -        try { -            SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); -            Mac mac = Mac.getInstance("HmacSHA1"); -            mac.init(signingKey); -            byte[] text = signatureBaseString.getBytes(StandardCharsets.UTF_8); -            byte[] signatureBytes = mac.doFinal(text); -            signatureBytes = Base64.getEncoder().encode(signatureBytes); -            return new String(signatureBytes, StandardCharsets.UTF_8); -        } catch (NoSuchAlgorithmException | InvalidKeyException e) { -            throw new IllegalStateException(e); -        } -    } -    private static String getAuthorizationParam(String key, String value) { -        if (key.isEmpty() || value.isEmpty()) { -            return null; -        } -        return key + "=" + "\"" + value + "\""; -    } -} -``` - - - - -### General Interface Error Messages - -**Same format** - -| String              | Type   | Description                                                 | -| ----------------- | ------ | ---------------------------------------------------- | -| code              | int    | Reserved field for tracking problems in the future                           | -| error             | string | Error code used for assessing code logic                           | -| error_description | string | Error description info, which is used to help understand and solve errors during development| | - - -**Error Response** - -| Error code            | Description                                                    | -| ----------------------| ------------------------------------------------------------ | -| invalid_request       | The request is missing a required parameter, contains an unsupported parameter or parameter value, or the format is incorrect| | -| invalid_time          | Invalid ts time in the MAC Token algorithm. **Request to reconstruct the server time** | -| invalid_client        | Invalid client_id parameters                             | -| access_denied         | Authorized server rejects the request **this occurs when requesting user resources with a token. If this occurs, the client should log the user out and request the user to log in again.** | -| forbidden        | User does not have permission to perform this action. **Reauthenticating permission will not provide any help. This request should be not repeated.** | -| not_found        | Request failed. The requested resources were not found on the server. **Requests should not be repeated with the same parameters** | -| server_error    | The server error has occurred.  **You may retry the request later, but there must be an upper limit (recommended: 3). If the first attempt fails, interrupt and inform the user** |  diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/_category_.json deleted file mode 100644 index c1f75ec93..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "礼包系统", - "collapsed": true, - "position": 9 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/faq.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/faq.mdx deleted file mode 100644 index 584881d32..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/faq.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: FAQ -sidebar_label: FAQ -sidebar_position: 4 ---- - -### I’ve provided all the required parameters but I still got a 403 error. - -Please check if a special User Agent has been included in the request’s header. - -### With secondary checks enabled, does the game need to send two separate requests to the redemption system? - -No. With secondary checks enabled, the redemption system will send requests to the interface you configured on the Developer Center. - -### Unable to parse the content parameter of the serverless return value? - -The content in the return value is a nested JSON string. You can first read the string and then perform further JSON parsing. In addition, a new content_obj field has been added, which you can use selectively. - -### How to determine if the exchange is successful? - -You can judge by whether the error field in the return value is 0. - -### How to use the sign field in serverless exchange? - -Please refer to the [signature section](/sdk/tds-gift/guide/#signature) in the documentation. - -### Unity request for serverless exchange interface reports error HTTP/1.1 422 Unprocessable Entity - -Please check if the event switch is turned on and if the gift code has expired in Developer Console -> Gift Package Service. - -### Error when generating gift package, time cannot exceed 2040 - -The current maximum gift package time is 2038-01-19 11:14:07 . - -### What's the difference between the three methods for redeeming gift codes? - -| Method | Interfaces neded | Features | -| --------------- | ------------- |------------- | -| Verified by game | Redeeming, secondary checks, and delivering items | You must maintain interfaces for both secondary checks and item delivery. Your game will check if conditions are met to deliver gifts. The logic for checking conditions and delivering gifts can be maintained separately. | -| Requested by game | Redeeming and delivering items | You only need to maintain one interface for item delivery. The interface can be used to check conditions and deliver items. | -| Redeeming without a server | Redeeming | Our service will check if the code is valid and the number of items remaining. The item information is returned as long as the code is valid. This provides a simpler workflow and you don't have to maintain your own interfaces. | - -### The gift redemption interface returns the exception {"error":100016,"message":"该礼包码无效码","info":{"dev_message":"invalid code","hint":"gift code is invalid"}} - -Check if the gift code passed into the interface is wrong. The gift code can be exported in **Developer Center > Your Game > Operational Tools > Gifts > Gift Event > Data > Export**. After clicking the `Export` button, a `.csv` file will be generated, and you can check the gift code in that file. - -### The `sent by game` gift redemption interface returns the exception {"error":100015,"message":"发送道具失败"} - -This exception indicates that the parameters passed when calling the interface are correct, and the signing is also correct, but the interface for sending items on the server side of the game may not be working. The game developer should check if there are any issues with the interface for sending items. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/features.mdx deleted file mode 100644 index 7e5f6d673..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/features.mdx +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Gift Packages Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -## Overview - -Gift Packages lets you create gift codes for your game which players can redeem in your game later. - -### Capabilities - -- With Gift Packages, you can quickly add a gift system to your game without having to write the low-level logic for it. -- Gift Packages comes with interfaces for redeeming gift codes, which players can access through your game or a website you build. - -### Highlights - -- You can create two types of gift codes with Gift Packages: unlimited codes and one-time codes. It’s up to you to decide which one to use for each event held in your game. -- You can customize the characters and redeeming rules of your gift codes, as well as the items players would get by redeeming the codes. -- Compared to setting up your own system, Gift Packages allows you to implement the same feature in your game with a lower cost spent on data storage. Gift Packages itself will also keep evolving to cater to the change in users’ requirements so you don’t have to do the work of constantly updating your system. -- You can monitor the statistics data to observe how your gift codes have been consumed since they got launched and learn about the outcomes of the events you held for your game. - -## Basic Flow - -![](/img/tds-gift/gift01.png) - -## Using Gift Packages in Your Game - -### Accessing the Dashboard - -You can access the dashboard for managing gifts by going to **Developer Center**→**Game Services**→**Operational Tools**→**Gifts** - -### Creating/Editing an Event - -#### Setting up Servers - -- If you haven’t set up servers yet, please first complete this step by going to **Server Configuration**. -- If you have already set up servers, you can select the servers you wish to use when creating gifts. - -![](https://capacity-files.lcfile.com/YV4PepRyA4HKVSdgGk0WuwN953Sq8r6n/io-gift02.png) - -#### Configuring the Basic Information of the Event - -- Gift Event Name - - Give a name to the event (players will not see it). -- Valid Date - - Players can only redeem your gift codes during the dates you provided. -- Gift Code Type - - Unlimited Code: The same code can be redeemed by unlimited users. The code can be no longer than 13 digits and can only contain numbers and letters. - - One-time Code: The same code can only be redeemed by one user. -- Gift Code Generation Rules - - Generate Randomly: The system will generate random code(s) with 13 digits. - - Enter prefix (for one-time code only): You can specify a prefix for the codes being generated. To ensure that enough codes can be generated, please keep the length of the prefix within 6 digits. -- Custom redeeming rules - - Besides the basic redeeming rules, you can set up a maximum of 10 custom redeeming rules. - - Condition: The name of the condition (like season or registration time); Comparison operator: Could be equal to, less than, less than or equal to, greater than, or greater than or equal to; Value: The value to compare against. - - For example, to only allow users registered in 2022 to claim your gift, you can do: Registration time (Condition) is equal to (Comparison operator) 2022 (Value). -- Items and Amount - - Item: The name of the item given to players. - - Amount: The amount of the item given to players. - -:::tip - -**Attention**: Don’t fit multiple kinds of items into the same row. You can add no more than 10 kinds of items in total. - -::: - -#### Launching the Event - -To launch the event you created and make its codes effective, please turn on the toggle of the event under **Gift Event**. - -![](https://capacity-files.lcfile.com/zzozCSJXR5mfuOEXb2eVm865hMCLM6Kk/io-gift03.png) - -> **Attention**: Only the codes of launched events can be redeemed. Please make sure to turn on the toggle of your event when you are ready to start the event. - -#### Editing the Event - -Click on **Edit** to edit the event. You can only edit the Servers, Gift Event Name, and Valid Date of the event. - -### Adding Codes/Counts - -**Adding Codes** - -To add codes, click on **Add Codes**, enter the number of new codes, and click on **Generate And Export**. The new codes will be automatically downloaded to your computer. - -**Adding Counts** - -To add counts, click on **Add Counts**, enter the number of counts to be added, and click on **Confirm**. The same code used for the event will be downloaded to your computer. - -:::tip - -**Attention**: Adding counts for an event with unlimited codes will not produce new gift codes. To obtain new gift codes, create a new event instead. - -::: - -### Viewing History - -You can view the history of generating gift codes by going to **Data**. - -![](https://capacity-files.lcfile.com/Bc2dE6AOsxqbuH6BzWhnGLM8QQvc7sTe/io-gift04.png) - -### Server Configuration - -- Server Name - - The name of the server -- Server code - - URL for receiving notification about distributed items -- URL for receiving notification about distributed items - - check_url -- check_url - - Optional; used for secondary check of Gift Code Generation Rules - -### Data Lookup - -**Single CDKEY or single user** - -- Enter a **CDKEY** to get the status of the corresponding gift code -- Enter a **uid** to view the redemption history of the corresponding user - -![](https://capacity-files.lcfile.com/iwMdA4CclNiT1c5VKAHVlIJeEXJOzOAi/io-gift05.png) - -**Gift event** - -- Where to find: The homepage of Gifts -- Use any of the following filters to look up the statistics of gift events: - - Code Type - - Status - - Gift event ID or name - -![](https://capacity-files.lcfile.com/VwQ8oSydEB4R8oYaRPbTN21bBgpPGKYu/io-gift06.png) - -## FAQ - -Q: I see an error saying that the system cannot save the Client ID. - -A: Please go to **Developer Center**→**Game Services**→**Configuration** and enable the service to obtain a Client ID. - -Q: Why can’t I see the **Game Services** tab? - -A: When the administrator of a provider adds a new user to the provider, the administrator needs to grant the user the permission to view a game’s configuration before the user can see the **Game Services** tab and the content within it. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/guide.mdx deleted file mode 100644 index 2adef0517..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/guide.mdx +++ /dev/null @@ -1,617 +0,0 @@ ---- -title: Gift Packages Guide -sidebar_label: Guide -sidebar_position: 2 ---- - -:::tip -Before integrating Gift Packages into your game, please first complete setting up your game. You can obtain the **Client ID and Server Secret** of your game on **Game Services - Configuration**. This information should be kept private and shall not be shared with anyone else. -::: - -## Getting Started - -You can only use Gift Packages with your game if your game is capable of sending requests to our server. At this time, players cannot redeem gift codes without connecting to the internet. - -You can use one of the following methods to complete the process of redeeming a gift code. - -| Method | API | The game needs to verify that the player meets the conditions of claiming the gifts | The game needs to provide an interface for delivering items to the player | -| -------------- | -------------------------------- | :----------------------------- | :----------------------- | -| Verified by game | /api/v1.0/cdk/game/submit-check | ✓ | ✓ | -| Requested by game | /api/v1.0/cdk/game/submit-send | ✗ | ✓ | -| Redeeming without a server | /api/v1.0/cdk/game/submit-simple | ✗ | ✗ | -| Verified by game (used on a web page) | /api/v1.0/cdk/page/submit-check | ✓ | ✓ | -| Requested by game (used on a web page) | /api/v1.0/cdk/page/submit-send | ✗ | ✓ | - -### Steps to Integrate Gift Packages Into Your Game - -- Finish setting up the game and obtain the **Client ID and Server Secret**. -- Optional: Go to **Operational Tools - Gifts - Server Configuration** and add a server that will be used to receive notifications and deliver items to players. -- Create gift events and export gift codes. Once you finish this step, you will be ready to test the gift codes. -- Players redeem your gift codes and obtain the items. - -### Steps to Redeem a Gift Code - -Below are the basic steps for redeeming a gift code: - -1. Send the **gift code** and **configurations** to the interface for redeeming gift codes. -2. The interface will check if the information provided is valid. -3. The system will determine if **the game’s interface for secondary checks** needs to be invoked. (Optional; the interface is used if it is configured) -4. If all the checks have passed, the system will determine if **the game’s interface for delivering items to players** needs to be invoked. (Optional; the interface is used if it is configured) - -There are three different types of interfaces for redeeming a gift code. You can integrate one of them into your game: - -| Name | Description | -| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| Verified by game | Choose this flow if you plan to set up a server that connects to our system, are able to have your secrets securely stored, and wish to have your app verify if a request for redeeming a code should be allowed while having an interface provided by your game invoked to deliver the items to the player. | -| Requested by game | Choose this flow if you plan to set up a server that connects to our system, are able to have your secrets securely stored, and wish to have an interface provided by your game invoked to deliver the items to the player. | -| Redeeming without a server | Choose this flow if you don’t want to build and maintain your own interface. Your game will be invoking an interface provided by our system and a value will be returned indicating whether the request succeeded. | - -### Three Ways for Redeeming - -#### Verified by Game - -When your game sends a request to the `submit-check` interface for redeeming a gift code, the system will check the inventory as well as whether the submitted data is valid. If everything looks good, the system will invoke the URL you provided for secondary checks with the conditions you set up on the Developer Center as arguments. -If your interface approves the request, the system will invoke your interface for delivering items to the player with the items configured on the Developer Center. -![Verified by game](/img/tds-gift/submit_check.png) - -#### Requested by Game - -When your game sends a request to the `submit-send` interface for redeeming a gift code, the system will check the inventory as well as whether the submitted data is valid. If everything looks good, the system will invoke your interface for delivering items to the player with the items configured on the Developer Center. -![Requested by game](/img/tds-gift/submit_send.png) - -#### Redeeming Without a Server - -When your game sends a request to the `submit-simple` interface for redeeming a gift code, the system will check the inventory as well as whether the submitted data is valid. If everything looks good, the system will return the items configured on the Developer Center to you. -![Redeeming without a server](/img/tds-gift/submit_simple.png) - -## Connecting to the Interface for Redeeming Items - -### Implementing Secondary Checks With Your Game - -If you have set up custom redeeming rules on the Developer Center and you’re redeeming a gift code with an interface that has the capability of performing secondary checks, the system will send the redeeming rules to your game for a secondary check. The configuration shown in the image below will trigger the following request to the interface you set up on the Developer Center: - -![Custom redeeming rules](/img/tds-gift/custom_redemption.png) - -``` -curl --location -g --request POST \ ---header 'Content-Type: application/json' \ ---data-raw '{"activity_id":,"character_id":,"content":"[{\"condition\":\"level\",\"operate":\"gt\",\"value\":10}]","ext":,"gift_code":,"nonce_str":"abcdc","server_code": ,"sign":"42d2b58a8cd58b5e63d90524aaf63ef97e04f223","timestamp":1658825322}' -``` - -After verifying the request, please return a JSON containing the following fields to inform the system of the result. - -```json -{ - "status": true, - "errorCode": "An error code string" -} -``` - -You can use snake_case as well if you wish to maintain consistency. - -```json -{ - "status": true, - "error_code": "An error code string" -} -``` - -If you don’t need our system to know the specific error message, your interface can simply return a **non-200 HTTP code** to indicate that the redemption failed, or an **HTTP code 200** to indicate that the redemption succeeded. -This allows your interface to respond to requests without following the format specified above. - -| Request parameter | Type | Description | -| ----------------- | -------------- | -------------------- | -| activity_id | String | Event ID | -| character_id | String | User ID | -| content | String (JSON) | Custom redeeming rules | -| content.condition | String | Custom redeeming rules (condition) | -| content.operate | String | Custom redeeming rules (comparison operator) | -| content.value | String | Custom redeeming rules (value) | -| ext | String | A value provided when sending a request to the interface | -| gift_code | String | The gift code entered by the user | -| nonce_str | String | A random string used for this request | -| server_code | String | Server code | -| sign | String | Signature | -| timestamp | Number | Timestamp with a second precision | - -| Response parameter | Type | Description | -| --------- | -------------- | ----------------------------------------------- | -| errorCode | String | Error type; same as `error_code`; if both of them exist, `errorCode` will be used | -| ext | String (optional) | A string passed to the interface for delivering items to the player | -| status | Boolean | Whether the request passed the verification; `true` means yes and `false` means no | - -### Delivering Items to Players - -After validation is completed, our system will send a request to **the game’s interface for delivering items to players** with the items configured on the Developer Center as well as the configured parameters. The configuration shown in the image below will trigger the following request to the interface you set up on the Developer Center: -![Custom redeeming rules](/img/tds-gift/custom_prizes.png) - -``` -curl --location -g --request POST \ ---header 'Content-Type: application/json' \ ---data-raw '{"activity_id": ,"character_id":"uid","content":"[{\"name\": \"ITEM_NAME\", \"number\": 2}]","ext":,"gift_code":"gift_code","nonce_str":"abcdc","server_code":,"sign":"fbe23e6f6f5193355b29e9ea2913b1992af56346","check_ext":,"timestamp":1658827492}' -``` - -After completing the request, please return a JSON containing the following fields to inform the system of the result. You can use snake_case as well if you wish to maintain consistency. - -```json -{ - "status": true, - "errorCode": "An error code string" -} -``` - -If you don’t need our system to know the specific error message, your interface can simply return a **non-200 HTTP code** to indicate that the redemption failed, or an **HTTP code 200** to indicate that the redemption succeeded. -This allows your interface to respond to requests without following the format specified above. - -| Request parameter | Type | Description | -| -------------- | -------------- | ---------------------------- | -| activity_id | String | Event ID | -| character_id | String | User ID | -| server_code | String | Server code | -| content | String (JSON) | Custom items set up on the Developer Center | -| content.name | String | Item name | -| content.number | Number | Item count | -| ext | String | A value provided when sending a request to the interface | -| gift_code | String | The gift code entered by the user | -| nonce_str | String | A random string used for this request | -| sign | String | Signature | -| check_ext | String | The string provided by the interface that does secondary checks | -| timestamp | Number | Timestamp with a second precision | - -| Response parameter | Type | Description | -| --------- | ------ | ----------------------------------------------- | -| errorCode | String | Error type; same as `error_code`; if both of them exist, `errorCode` will be used | -| status | Boolean | Whether the request succeeded; `true` means yes and `false` means no | - -### Redeeming Without a Server - -With this method, you don’t have to implement an interface for validating requests and delivering items yourself. - -If you choose to use this method, make sure to **specify that the event doesn’t need a server when creating the event on the Developer Center**. - -Our system will provide the redemption result upon receiving a request. Your game will then determine whether to give the items to the player depending on the result. - -### Three Item Delivery Interfaces - -#### With Secondary Check - -POST /api/v1.0/cdk/game/submit-check - -Header Content-Type:application/json - -Request parameters: - -| Request parameter | Type | Required | Description | -| ------------ | ------ | -------- | ------------------------------------------------- | -| client_id | String | Yes | The Client ID of the game | -| gift_code | String | Yes | The gift code entered by the user | -| server_code | String | Yes | Server code | -| character_id | String | Yes | User ID | -| nonce_str | String | Yes | A random string used for this request; it’s recommended to use 5 digits | -| sign | String | Yes | Signature | -| timestamp | Number | Yes | Timestamp with a second precision; the request will only be accepted if the timestamp is within 1 minute | -| ext | String | No | This field will be passed to the game’s interface for secondary checks | - -Example request: - -``` -curl --location --request POST '/api/v1.0/cdk/game/submit-check' \ ---header 'Content-Type: application/json' \ ---data-raw '{"client_id":,"gift_code":,"server_code":,"character_id":,"nonce_str":,"sign":,"timestamp":,"ext": }' -``` - -#### Without Secondary Check - -POST /api/v1.0/cdk/game/submit-send - -Header Content-Type:application/json - -Request parameters: - -| Request parameter | Type | Required | Description | -| ------------ | ------ | -------- | -------------------------------- | -| client_id | String | Yes | The Client ID of the game | -| gift_code | String | Yes | The gift code entered by the user | -| server_code | String | Yes | Server code | -| character_id | String | Yes | User ID | -| nonce_str | String | Yes | A random string used for this request; it’s recommended to use 5 digits | -| sign | String | Yes | Signature | -| timestamp | Number | Yes | Timestamp with a second precision; the request will only be accepted if the timestamp is within 1 minute | - -Example request: - -``` -curl --location --request POST '/api/v1.0/cdk/game/submit-send' \ ---header 'Content-Type: application/json' \ ---data-raw '{"client_id":,"gift_code":,"server_code":,"character_id":,"nonce_str":,"sign":,"timestamp":,"ext": }' -``` - -#### Redeeming Without a Server - -POST /api/v1.0/cdk/game/submit-simple - -Header Content-Type:application/json - -Request parameters: - -| Request parameter | Type | Required | Description | -| ------------ | ------ | -------- | ----------------------------------- | -| client_id | String | Yes | The Client ID of the game | -| gift_code | String | Yes | The gift code entered by the user | -| character_id | String | Yes | User ID | -| nonce_str | String | Yes | A random string used for this request; it’s recommended to use 5 digits | -| sign | String | Yes | Signature; use the Client ID instead of the Secret for generating this signature | - -Response parameters: - -| Response parameter | Type | Description | -| -------------- | -------------- | -------------------------------- | -| activity_id | String | Event ID | -| nonce_str | String | A random string | -| timestamp | String | Timestamp | -| content | String (JSON) | Custom items configured on the Developer Center | -| content.name | String | Item name | -| content.number | Number | Item count | -| sign | String | Returned signature used to check if the returned data has been tampered | -| success | Boolean | Whether the request succeeded | - -Example request: - -``` -curl --location --request POST '/api/v1.0/cdk/game/submit-simple' \ ---header 'Content-Type: application/json' \ ---data-raw '{"client_id":,"gift_code":,"character_id":,"nonce_str":,"sign":,"timestamp":}' -``` -
    -Shell Request example: - -``` -#! /usr/bin/env bash - -# TapDC Background application Client ID -client_id="Replace with console's `Client ID`" -# The conversion code of this exchange can be found in the "Package Activity panel - Data - Export" and then the exported file in the format of S0LLC8ICB2MP2 -gift_code="Replace with the redemption code in the package panel" - -# A random character string. Five random characters are recommended -nonce_str="A2B3Z" -# Game user ID -character_id="6347de128b****3ee825e029" -# Signature, which replaces Secret with ClientId -signed=$(echo -n $(date +%s)$nonce_str$client_id |openssl sha1) -RESULT_REQUEST=`curl --location --request POST 'https://poster-api.xd.cn/api/v1.0/cdk/game/submit-simple' \ ---header 'Content-Type: application/json' \ ---data-raw "{\"client_id\":\"$client_id\",\"gift_code\":\"$gift_code\",\"character_id\":\"$character_id\",\"nonce_str\":\"$nonce_str\",\"sign\":\"$signed\",\"timestamp\":$(date +%s)}"` - -echo $RESULT_REQUEST -``` - -
    - -
    -Android Request example: - -available for reference[ Android Demo ](https://github.com/taptap/TapSDK-Android-Demo/blob/main/app/src/main/java/com/tds/demo/fragment/GiftFragment.java) Gift package system function - -
    - -
    -C# Request example: - -``` -using UnityEngine.Networking; -using System; -using System.Text; -using System.Security.Cryptography; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -public class APIClient : MonoBehaviour -{ - private const string apiUrl = "https://poster-api.xd.cn/api/v1.0/cdk/game/submit-simple"; - private const string contentType = "application/json"; - private const string clientId = "hskcocvse6x1cgkklm"; - private const string giftCode = "NZ4mp2cztRMXH"; - private const string characterId = "879a0cdd9nnb917055ee"; - private const string nonceStr = "RFG7U"; - - public GUISkin demoSkin; - - - void Start() - { - - } - - - private void OnGUI() - { - - GUI.skin = demoSkin; - float scale = 1.0f; - - - float btnWidth= Screen.width / 5 * 2; - float btnWidth2 = btnWidth + 80 * scale; - - float btnHeight = Screen.height / 25; - float btnTop = 30 * scale; - float btnGap = 20 * scale; - - GUI.skin.button.fontSize = Convert.ToInt32(13 * scale); - - var style = new GUIStyle(GUI.skin.button) { fontSize = 20 }; - var labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 30 }; - - if (GUI.Button(new Rect((Screen.width - btnGap) / 2 - btnWidth, btnTop, btnWidth /2, btnHeight), "返回", style)) - { - UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(0); - - } - - btnTop += btnHeight + 20 * scale; - - if (GUI.Button(new Rect((Screen.width - btnGap) / 2 - btnWidth, btnTop, btnWidth, btnHeight), "无服务器兑换", style)) - { - StartCoroutine(StartRequest()); - } - - - } - - private IEnumerator StartRequest() - { - // Get the current timestamp (seconds) - int timestamp = (int)(System.DateTime.UtcNow.Subtract(new System.DateTime(1970, 1, 1))).TotalSeconds; - - // Concatenate and encrypt the sign parameter - string sign = GetSign(timestamp, nonceStr, clientId); - - // Build request parameter - string requestBody = "{\"client_id\":\"" + clientId + "\",\"gift_code\":\"" + giftCode + "\",\"character_id\":\"" + characterId + "\",\"nonce_str\":\"" + nonceStr + "\",\"timestamp\":" + timestamp + ",\"sign\":\"" + sign + "\"}"; - - // Create the UnityWebRequest object - UnityWebRequest request = new UnityWebRequest(apiUrl, "POST"); - - // Set request header - request.SetRequestHeader("Content-Type", contentType); - - // Set request body - byte[] bodyRaw = Encoding.UTF8.GetBytes(requestBody); - request.uploadHandler = new UploadHandlerRaw(bodyRaw); - request.downloadHandler = new DownloadHandlerBuffer(); - - // send request - yield return request.SendWebRequest(); - - // Processing response - if (request.result == UnityWebRequest.Result.Success) - { - Debug.Log("API request succeeded"); - Debug.Log(request.downloadHandler.text); - } - else - { - - Debug.Log("API request failed: " + request.error); - Debug.Log(request.downloadHandler.text); - - } - } - - private string GetSign(int timestamp, string nonceStr, string clientId) - { - // Concatenate parameters and perform SHA1 encryption - string signString = timestamp.ToString() + nonceStr + clientId; - byte[] signBytes = Encoding.UTF8.GetBytes(signString); - byte[] signHash = new SHA1CryptoServiceProvider().ComputeHash(signBytes); - string sign = BitConverter.ToString(signHash).Replace("-", "").ToLowerInvariant(); - - return sign; - } -} - - -``` - -
    - - -Example response: - -```json -{ - "activity_id": "TDS20220928151122U9H", - "content": "[{\"name\": \"ITEM\", \"number\": 2}]", - "nonce_str": "0DD4B", - "sign": "5895f87d3dfb5e0918c1b195c015d9284609d122", - "timestamp": 1664354023, - "success": true -} -``` - -#### Response - -If succeeded: - -```json -{ - "success": true, - "messages": "Success" -} -``` - -If failed: - -| Response parameter | Description | -| ---------------- | ------------ | -| error | Error code | -| message | Summary of the error | -| info | Error detail | -| info.dev_message | Brief instructions for the developer | -| info.hint | Detailed instructions | - -```json -{ - "error": 100001, - "message": "Invalid input", - "info": { - "dev_message": "", - "hint": "test error" - } -} -``` - -### Request Domain - -| TapTap edition | Domain | -| --------------- | ------------------------- | -| China (taptap.cn) | https://poster-api.xd.cn | -| Other countries and regions (taptap.io) | https://poster-api.xd.com | - -### Signature - -For security reasons, when your game communicates with our system, it needs to provide a signature in each request. **The “Secret” mentioned below refers to the Server Secret found on Game Services > Configuration on the Developer Center**. - -:::tip -To ensure the highest security, please never use the Server Secret of your game on the client side. -::: - -Pseudo-code for obtaining a signature: - -``` -sign == sha1(timestamp + nonce_str + secret) -``` - -You can check if the process of obtaining signatures is implemented correctly with the help of unit testing. The code below is an example in golang: - -```go -func TestMakeSign(t *testing.T) { - timestamp := 1655724586 - nonceStr := "abcde" - secret := "abc" - sign := MakeSign(int64(timestamp), nonceStr, secret) - assert.Equal(t, sign, "3cb8c38833fa742e7873378faddcbe5b56088482") - //output: 3cb8c38833fa742e7873378faddcbe5b56088482 -} -``` - -To ensure that the Secret never appears on the client side, when redeeming without a server, the signature can be generated **with the Client ID instead of the Secret**. - -``` -sign == sha1(timestamp + nonce_str + client_id) -``` - -### Using the Redemption Interface on a Web Page - -Our system provides a simple interface for redeeming gift codes that supports CAPTCHA. If you have a web page for your event, you can embed this interface into the web page. This interface shares the same flow as those introduced earlier. The only difference is that some of the parameters have been changed. - -#### Getting a CAPTCHA - -:::tip -Each CAPTCHA is only valid for 5 minutes. You can request a new CAPTCHA if one expires. -::: - -GET /api/v1.0/cdk/page/captcha-img - -Header Content-Type:application/json - -| Response parameter | Description | -| -------- | -------------------------------- | -| img | The CAPTCHA image in base64 | -| key | The key of the CAPTCHA; needs to be submitted together with the user input | - -Example response: - -```json -{ - "img": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAA8CAYAAAAjW/WRAAANc0lEQVR4nOydCXAUV3qA/z7mHs2t0bWSkJCEECCO5Ta2w5GFmMCGrGHNemuXXZwYluxhQrEVkrWzIkUS29hlOxgfYFypJBATbONgG7ABGxwOg7GQQAJJCB0gCWkOzT3T09Od6haSR6PRXJpL0vuqKKZfv379uvt9+t97/UYihdWf/AEQCERQ8FRXAIFIZ5AgCEQIkCAIRAiQIAhECJAgCEQIkCAIRAiQIAhECJAgCEQIkCAIRAjIVFcgnTAUaqpSXQcOXavp2VTXAdHHuBMkXSQIRag6InmSy5gVZDSIEAvBrgtJkzjGjCBjVYhIQNIkjlEpSKJkSJdGFY/rCywjXa5ttIGNluXu8ZJitDcUdB+SS9oKMtKGMJ4aQDykGU/3KxrSSpCRPOgx+YBZwHLqjeqcelO2ssupETm80uofTrzcNUltDndoOt5LpdWare61lEvdrmLSS+twlpWwADQlFN4zqtUXujN1txJx3uPz1w17Lw7985Ihae8u3jRw/SkVJNaHOCZl8OPx7efWYwyLszjG2LVio6lA0WXOk5szDC75xPMd3z9atfAQJSXpaMpM5b1WWa05uZ1dj3HtzSaX19rksrsOqdRIkyQl8nhkOpO5XGs2L7VmyC/fKSg4NdLzhRICgkjhL0QgSR+kIynC4xWTLtzHkAIXLXVoxBbAAPJuGPKVnU49J46iyyE1FCut0ZQZeP8ifQ7++WJ5BlqTqSjLYFjcmaU/aVap2gP3u8Vi293cnMu9SsXtiS2tT2caja09Wm1TqDI3nHljoE5P/N3psHVYcfG9mNtOUgSJRYrxJEQg3SXKNhbDWMCA1Tf1FiruO3N6ipVNGMMQDo3YSEkFUUWPYMQiTCyy6A3GR4xq9flgcvhjl8lMZqXyrNZknr+y5vDPhssXToiRyBAM7Ml1k9nhdh7esTvmkyEpYkfXbFEUX+qa4FSLHPnVPSViGyW36aWGa39eXG0oUlhZHBv2mcWDaJ9dqOc2/UbdDqNaffJeTta3ABirNxhKVRbrzAynrQJnfVBgawC518Ln9RBiaFJOhymmS/x2oqNDJAwag6zd9bcR35hg8sTzxiL6WLzn2kMZ3c7Mrsma5q+fmFST7PPH+kz7u0Gdsgkg9LnAKtSCm5QChYsH8hbabkKXtADKzVf5ccCxRT+p+uOr22Hnr3YFLTvRMgQj4kF6NPLsffPAsPuQFNHBCSI3uLQdU7RN3zxeWpfKuvjLsu3d4I04EK6f2JZRBr2iTGABGJdEUmdUq7555y9/83O3WASv/OMm+MMzL/J5W0oL4H+fWAG/3vkWv50KIQIZdgxSfP9qVlnHhTKZx6JgMJzp/uGMD6+UrLrhEKk8EHCzNj/9i0HHBm77sxagaiRdt3BgHpK/Jlbg8wHOsuDDccxDEIRVJMJtQhFVajIm6twJgWUxWkhQMqNLkczT+g+E+9kWIv+LG3YMu+/05DVwdcF0mHnxGq4026YCAPcPGqaWwKwLfUGRk6HsdvOP3CJR54qL752Py0XEgSERRGu7K3/s6r+t1lnbygMzC4USWPHo38C0SUvDFuwfKUbadYsKBsN4MYJAGCUS8bdZhY5lLTdHdI4k8sjbtfNENq/UkiszxruLFUyC4Qg1FRrYDbuSsyxkWZwMOpOpKNNgfLSxuOi/aJKk5A6Hpqi1bUPdpLLXfAThjbReiWaQIApnj2T9uX/4Kyll03kEst6zFT858umcP91YXXcCTp/fz+fBcQKe2fjfIBbJhxQWS/cp7vI8EERYr8tUHaxYjzkEGm+hpcb0myvvc7vV+2Ysd1d217vmdrRFW9dUMPfgrUpVhz2TITDf57+b9UU0x0YjAISRYDjCvXPgmN35+aDtoy0dcPLufTiqz37eLRLZubTyxqYnzSrllfuZmQl5WRgrg7pYq668vJqTg8Vw36/X7lZVaSZs5NIXzFoLnd0NUN90DgSkCAh8cM9sJOOKUI0+UJ5QMg2U8yB6ZBwr+QHmJFWOZS1HOGnILrmczrbbvbm2TtENXeloEcSlFDp1LT4BsCAM3JcMAQIJJ4T/uCHYAP/V603QbnfCC/MrYQ9JbOfSDt1uh5cF5MV0kwP8I0hl66mipTX7+cHDjIrlsHLJM4MyUl4X1DeehbzsctBpClM+2B5OFm5QiAELF41nQCM8C2rhV3B22+p/0r4895c9z371hvTLglLx9czJpi3ffJT8WkcHJwBmEABRJwegMaD/xBTyS9LxEMCfSKJDpANpTpY365qhy+WGv581GUgMA5plYc/1JtCKRfDT0oKBvKluW/7wgnCV/+DELqhrPMsn/nTNv0Jh3vQhmdOp4sOBm8XiH+3dsuOcoRrK5M8CidvASU+AFudv4WHdDLB6Z8Bd18+gQrGVz5/ICYNICBcFMDsBxGUlL8a+59Y9x9ufIKKJDlGCldy5s1LgpTVfLJtbQmAYmD0UvHCtAdYU5cKcTE3Qg9KhvfXN+LAMNLZ8zSdgGAZ52ZP5zxZbN6y6dn6fXax2tmVOM6S2qpFBWERirtFnb4Wq6qmbPnHN7ryl2TtrC2AgOLxj93Mrq16volnVQP5wY6CRCjTSbpDARZMrL3292iMROOVGl8iuk7hHUp9+4hkdwlHYfneRyEPl3SqZeCCrzUxxaRW3Gp4SUVTB5W7ToLw4hsEUtQJeWjh9xMtc4gEviNHcDl5v332XSdVw8/ZXcPDLN7ulHot+OcBTXLpDpLx/ZtqGDxpz5nWkoqKRgnkJgvufxYBlZF6X/ETxQ5gPE/oyqG4u/fjmbf+i2TN1w+Edu18Pdny0455EjwO8EpKmZCQnh1bTblPEKkgCo0NYVFbrwi595oc0SVL9aXWTyvb55/GX4Y265iFl9O9Ptii8IHbndxbbHSY4evJ5EBLCDLM857aYsqsklE0r81iy/uzq67+g5wjfvqOf2Z3MSkYFjfO9dFbgc/q0LovsdOEaPjnf1sj9z8i8XozBhn3/Exgx/AVwXGgclJeTxxFhOdGib+pVVpxsnSozuVU4zRD3puluVh5rnjv/P+rVDo3Y+Onv5xxnSJwJdmwyo0MksBgwDI77QuXxb/hb3dSw9U92VOEbyqZbNf/5MMCT/YltmVMvHZv9uxMeUkpzl7fu/M7Vecab3ycYr2hJzYG/2L9s5luJrljMPJjF8kw1nJedzV+EsYDTWlcrK/DxLzgJk0QC7OChbqRRQLagdNgIENOMWwhmv9ewsHledj0lFdzRtVh1tBCn75eqWyU2qierwVySU2/S3Jum47u9qYwOkeASSxrlDkd+uFW6/Zww9La/1Gp6O9wyl2TIwgtCCaQDoc8jkFqOzN/x8XdZMPazyqc+//kX22dgLEMoXIbvlXVcyGvIXXAvERUaKayI5le6+jQui+RqVhn3eUrWkUKCdRUWnDm5xGwqh1q2AB4LIUUss0Hxnq4mKZ9w2vGWh2gBQblUol5um5KQ7lUnPn7Uiwvgx5v/fZKcDr7iPdVCBGJWKmv0BsNiADgTJiuGsQyOMQy/YMu/0adKFl4Qi1RnH0jwUZLATGZ5rsMq0d5TOnv4ubgJ3TWF6SSIfwTweJQgumOB6rqt0I1/D8QSA6jUN0Gp7vvhxQlixzWGdxdvejVZ9YtFHoKRgdCjh0eutgPB0ECwfT0UAUMBBgz0y5FuMgTDoNXcyenuXjWz9npEkdqSkfF/gWn9jT6aZfnxEIUXpF07xcjghBdnfAKCoYUqR5e0V5bt9M/oEUgHJBJ77bKRnjgaohkIswzZFwEWc9X/kk9rBu6H12K+e6X9bN4WVkx7ElnfaPCXZ2hXaeiKmP2r8x986nsE/UKlero6DGzt5PJX4lFQsqMKLwiDk4xFqm9T2zsnctslnZfzr5SsGvRWU0i7pf2fXcIMeywnC0W81gXhvWIx+CicMEgkPr3TCT4ME3TKM8TVWUXiK9mLcDehoLIcQ6dJUsBIxg7RdNsg/QWKiWTIMvAmfX7DkSkLbh35Mfe5U11afWjRH9/vzySmbIK//uxXvycYH7/c4YN5219r0c/oieZikroswofjGR+VzBQ1aCpwm0iHeXEJ0LgACMZL5ziaLOvrPqGz7XGXPBTJnllK6gLRNCOa77CEk2VAEJyh8Y2nfvu03G3OYQFjT1X+8kBt4dIWbt/yb/cuqrh77gfc5y7VxJqDD+/8n8CCUrEuKF1Jt2nWQMaLPPH4At+g1bw55gbV4xd2beYG6iyG+wyK/AacoUmt7V4pt58mMsAuqQQWiLAnG8sCBJLu06zRMFbliVWWId8HKeip1S2tfWe1ynF/Qn8ai5HgEeSCS1gw8CdFxpMA/qR7dEgkY0WeSGXhJBn2K7dZvc1KnbVN5RQp3K2ZlT3cQD7uNR0FjKXokEhGqzzh/tREWv1mxVQznqNDIolGHkihQMF+4fe4FgRFh/QgHaMPJ8u4EgRFh9FJquUZs4Kg6DD2ScbL0TEhCIoOiEQxKv/CFIoOiGSR9oKg6IBIJWknCIoOiHQipYKg6IBId5ImCJIBMRpJmCCoq4QYC8RFEBQdEGOVmARB0QExXggrCIoOiPHMEEFQdEAgvoMXJJQUSAjEeGZMrMVCIBLF/wcAAP//ettpiq6bhWEAAAAASUVORK5CYII=", - "key": "XfNTN1gQyvA2AcNOu1UN" -} -``` - -#### Using a Web Page With Secondary Checks - -POST /api/v1.0/cdk/page/submit-check - -Header Content-Type:application/json - -Request parameters: - - -| Request parameter | Type | Required | Description | -| ------------------- | ------ | -------- | ------------------------------------------------- | -| client_id | String | Yes | The Client ID of the game | -| gift_code | String | Yes | The gift code entered by the user | -| server_code | String | Yes | Server code | -| character_id | String | Yes | User ID | -| nonce_str | String | Yes | A random string used for this request; it’s recommended to use 5 digits | -| verify_captcha_key | String | Yes | The key of the CAPTCHA | -| verify_captcha_code | String | Yes | The CAPTCHA code entered by the user | -| ext | String | No | This field will be passed to the game’s interface for secondary checks | - -#### Using a Web Page Without Secondary Checks - -POST /api/v1.0/cdk/page/submit-send - -Header Content-Type:application/json - -Request parameters: - -| Request parameter | Type | Required | Description | -| ------------------- | ------ | -------- | ------------------------------ | -| client_id | String | Yes | The Client ID of the game | -| gift_code | String | Yes | The gift code entered by the user | -| server_code | String | Yes | Server code | -| character_id | String | Yes | User ID | -| nonce_str | String | Yes | A random string used for this request; it’s recommended to use 5 digits | -| verify_captcha_key | String | Yes | The key of the CAPTCHA | -| verify_captcha_code | String | Yes | The CAPTCHA code entered by the user | - -## Miscellaneous - -### Error Codes - -| Error code | Description | -| ------------ | ---------------------------------------------- | -| 100001 | Invalid input | -| 100002 | Invalid account | -| 100003 | Other error | -| 100004 | Wrong CAPTCHA | -| 100005 | Parameter error | -| 100006 | Quota exceeded | -| 100007 | Batch quota exceeded | -| 100008 | The event hasn’t started yet or has already ended | -| 100009 | Not open yet | -| 100010 | Out of scope | -| 100011 | Too many requests | -| 100012 | The user redeemed the same gift too many times | -| 100013 | Invalid CDKEY | -| 100014 | The server is responding slower than usual | -| 100015 | Failed to deliver the items | -| 100016 | Invalid gift code | -| 100017 | Invalid event | -| 100018 | Incorrect interface used for the current event | -| 500001 | Server error | -| 500002 | Failed to validate with an internal interface | - -### Comparison Operators - -| Comparison operator code | Description | -| --------- | -------- | -| lt | Less than | -| le | Less than or equal to | -| eq | Equal to | -| ne | Not equal to | -| ge | Greater than or equal to | -| gt | Greater than | - -### IP Addresses of the System - -If your interface can only accept a limited number of IP addresses, please add the IP address listed below. - -| TapTap edition | IP | -| --------------- | ------------- | -| China (taptap.cn) | 59.110.228.98 | -| Other countries and regions (taptap.io) | 8.214.95.148 | diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/no-server-guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/no-server-guide.mdx deleted file mode 100644 index 9833e790c..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/tds-gift/no-server-guide.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Serverless Exchange Development Guide -sidebar_label: Serverless Exchange -sidebar_position: 3 ---- - -:::tip -This article is to explain the access process of serverless redemption method. If you need complete development content, please refer to [**Guide**](/sdk/tds-gift/guide/). -::: - -## Present - -Docking gift redemption code requires the game party to maintain one or two web interfaces, which is relatively cumbersome. Therefore, we have simplified the redemption process as follows: - -** The game party requests the redemption interface for redemption, and judges whether the redemption is successful based on the information returned by the interface. ** - -In this way, the game party only needs to call one interface to complete the redemption process. - -:::tip -The exchange results received directly by the client can be intercepted, so the security of this method is not as high as other methods. -::: - -## Matchmaking - -### Redemption interface - -The domain name is divided into two addresses according to the developer centre, and the gift pack data of the two addresses are not interoperable: - -| TapTap Version | Domain Name | -| --------------- | ------------------------- | -| China taptap.cn | https://poster-api.xd.cn | -| Overseas taptap.io | https://poster-api.xd.com | - -The redemption interface parameters are described in detail in the [**Developer's Guide - Serverless Redemption**](/sdk/tds-gift/guide/#redeeming-without-a-server-1). - -A signature needs to be generated when initiating a redemption. The signature of the service-less redemption interface is calculated from the Client Id and other information, see [**Documentation**](/sdk/tds-gift/guide/#signature) for the signature calculation process. - -### Return value - -#### Result - -Judge whether the exchange behaviour is successful or not by whether the error field in the return value is 0. If the error field is judged not to be 0, you can see the specific reason according to [**error-codes**](/sdk/tds-gift/guide/#error-codes). - -#### Verification - -To prevent the return information from being tampered with, it is recommended that you take the following measures: -1. To prevent replay attacks, you need to verify whether the return timestamp is too different from the client's time; -2. Calculate a signature based on the return value and verify that it matches the signature in the return value. The signature calculation process is described in [**Documentation**](/sdk/tds-gift/guide/#signature); -2. It is recommended that the name of the gift content can be an encrypted name. - -#### Prize content - -The prize content corresponding to the redemption code can be obtained based on the content or content_obj field in the return value. The difference is that content is a nested json string and parsing it directly as an object may result in an error. - -The prize content setting is not limited to English and Chinese. In order to facilitate development and judgment, it is recommended to maintain a prop list. - -## Other -### Development Environment - -Using the serverless redemption interface to distinguish the formal testing environment requires creating different applications in the Developer Center. - -If it is not possible to deal with environmental issues by application, it is recommended to use the [without secondary check redemption interface](/sdk/tds-gift/guide/#without-secondary-check) to complete the redemption by configuring server information and configuring the server code to be forwarded to different environments . \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/update/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/sdk/update/_category_.json deleted file mode 100644 index 3ca55cc08..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/update/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "唤起更新", - "collapsed": true, - "position": 12 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/sdk/update/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/sdk/update/guide.mdx deleted file mode 100644 index c0016d47e..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/sdk/update/guide.mdx +++ /dev/null @@ -1,659 +0,0 @@ ---- -title: Updates Integration Guide -sidebar_label: Guide ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; -import AndroidFaq from "../_partials/android-package-visibility.mdx"; - -When you release a new version of your game and wish the existing players to upgrade to the new version, you may want to display a notification in the game. With TapTap’s Updates function, a pop-up can be triggered when the SDK detects a new version released on the TapTap store. The player can tap a button within the pop-up to quickly jump to the Taptap app to update the game. - -## Integrating the SDK - - - -<> - -You can add the SDK **either manually or with the Unity Package Manager**. - -If you choose to use the Unity Package Manager, you should add the following dependencies into `Packages/manifest.json`: - - - {`"dependencies":{ - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -}`} - - -If you choose to manually import the SDK, you should: - -* In the [download page](/tap-download), click `TapSDK Unity` to download `TapSDK-UnityPackage.zip`. -* Go to your Unity project, navigate to **Assets > Import Package > Custom Package**, select the `TapTap_Common` module from unzipped SDK. -* Download [LeanCloud-SDK-Storage-Unity.zip](https://github.com/leancloud/csharp-sdk/releases), unzip it as a `Plugins` folder, and drag and drop the folder into Unity. - - -<> - -[Download](/tap-download) the TapSDK and add the `TapCommon` module to your game: - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') -}`} - - - -<> - -[Download](/tap-download) the TapSDK and add the `TapCommon` module to your game: - -{`TapCommonSDK.framework`} - - - - - -## Open the TapTap App to Check for Updates - -:::tip -Starting from TapSDK 3.3.0, the logic for updating the game has been optimized. You can have the game open a custom webpage if it fails to open the TapTap app. TapSDK 3.3.0 is still compatible with the APIs introduced in the earlier versions. -With TapSDK 3.3.0, you won’t need to check if the TapTap app has been installed before you use the following APIs. We recommend you to use the new APIs if you are using TapSDK 3.3.0 and later. -::: - - -<> - -Open the TapTap app to check for updates. If failed, open the game’s page on the web app: - - - -```cs -// For Mainland China -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapTap(string appId); - -// For other countries/regions -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapGlobal(string appId); -``` - - - - - -```cs -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapGlobal(string appId); -``` - - - -Open a custom webpage if failed to open the TapTap app: - - - -```cs -// For Mainland China -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapTap(string appId, string webUrl); - -// For other countries/regions -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapGlobal(string appId, string webUrl); -``` - - - - - -```cs -bool isSuccess = await TapCommon.UpdateGameAndFailToWebInTapGlobal(string appId, string webUrl); -``` - - - - -<> - -Open the TapTap app to check for updates. If failed, open the game’s page on the web app: - - - -```java -// For Mainland China -TapGameUtil.updateGameAndFailToWebInTapTap(context, "your app id"); - -// For other countries/regions -TapGameUtil.updateGameAndFailToWebInTapGlobal(context, "your app id"); -``` - - - - - -```java -TapGameUtil.updateGameAndFailToWebInTapGlobal(context, "your app id"); -``` - - - -Open a custom webpage if failed to open the TapTap app: - - - -```java -// For Mainland China -TapGameUtil.updateGameAndFailToWebInTapTap(context, "your app id", "your website url"); - -// For other countries/regions -TapGameUtil.updateGameAndFailToWebInTapGlobal(context, "your app id", "your website url"); -``` - - - - - -```java -TapGameUtil.updateGameAndFailToWebInTapGlobal(context, "your app id", "your website url"); -``` - - - - -<> - -```objc -// Due to Apple’s restrictions, TapTap for iOS doesn’t offer the Updates function -``` - - - - - - -
    -If you are not using TapSDK 3.3.0 or later - -**This section is not applicable for TapSDK 3.3.0 and later**. If you are using TapSDK 3.3.0 and later, please use the APIs mentioned earlier on this page. - -**Check if TapTap is installed**: - - - -<> - -```cs -// For Mainland China -TapCommon.IsTapTapInstalled(installed => -{ - if (installed) { - Debug.Log("TapTap is installed"); - } -}); - -// For other countries/regions -TapCommon.IsTapTapGlobalInstalled(installed => -{ - if (installed) { - Debug.Log("TapTap is installed"); - } -}); -``` - - -<> - -Import `TapCommon.aar` and access the interfaces from `TapGameUtil`. - -```java -import com.tds.common.utils.TapGameUtil; - -// For Mainland China -if(TapGameUtil.isTapTapInstalled(this)){ - Log.d(TAG, "TapTap is installed"); -} -// For other countries/regions -if(TapGameUtil.isTapGlobalInstalled(this)){ - Log.d(TAG, "TapTap is installed"); -} -``` - - -<> - -Import `TapCommon.framework` and add `tapsdk`, `taptap` and `tapiosdk` under `LSApplicationQueriesSchemes` in `info.plist`. - -```objc -#import -// For Mainland China -BOOL isInstalled = [TapGameUtil isTapTapInstalled]; -// For other countries/regions -BOOL isInstalled = [TapGameUtil isTapGlobalInstalled]; -``` - - - - - -**Open TapTap to update the game**: - - - -```cs -TapCommon.UpdateGameInTapTap("appid", callSuccess => -{ - if (callSuccess) { - Debug.Log("TapTap opened"); - } -}); -``` - -```java -if(TapGameUtil.updateGameInTapTap(this,"appid")){ - Log.d(TAG, "TapTap opened"); -} -``` - -```objc -// Due to Apple’s restrictions, TapTap for iOS doesn’t offer the Updates function -``` - - - -
    - -
    - -## Open Game Reviews - - - -<> - - - -```cs -// For Mainland China -TapCommon.OpenReviewInTapTap(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("Game reviews opened"); - } -}); - -// For other countries/regions -TapCommon.OpenReviewInTapGlobal(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("Game reviews opened"); - } -}); -``` - - - - - -```cs -TapCommon.OpenReviewInTapGlobal(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("Game reviews opened"); - } -}); -``` - - - - - -<> - - - -```java -// For Mainland China -if(TapGameUtil.openReviewInTapTap(this,"appid")){ - Log.d(TAG, "Game reviews opened"); -} - -// For other countries/regions -if(TapGameUtil.openReviewInTapGlobal(this,"appid")){ - Log.d(TAG, "Game reviews opened"); -} -``` - - - - - -```java -if(TapGameUtil.openReviewInTapGlobal(this,"appid")){ - Log.d(TAG, "Game reviews opened"); -} -``` - - - - - -<> - -```objc -// Not supported yet -``` - - - - - -appid is a game’s unique identifier in the TapTap store. -For example, in `https://www.taptap.cn/app/187168``https://www.taptap.io/app/187168`, `187168` is the `appid`. - -## FAQ - -### The TapTap app cannot be launched from the game on Android 11. Why? - - - -### If I’m not using the TapSDK, how can I open the TapTap app to update the game? - -Due to Apple’s restrictions, TapTap for iOS doesn’t support the Updates function. The following instructions are for Android only. - -If you are not using the TapSDK or using older versions of the TapSDK, you can follow the instructions below to manually open the TapTap app to update the game. - -Open the corresponding URL according to whether TapTap is installed on the device: - -- If TapTap is installed, open the TapTap app and jump to the game’s page. -- If not, open the game’s page in a web browser. The player will follow the instructions on the page to install the TapTap app, open the app, and update the game within the app. - -URL for devices without TapTap: - - - -- For Mainland China: `https://l.taptap.cn/5d1NGyET?subc1=AppID` -- For other countries/regions: `https://l.taptap.io/GNYwFaZr?subc1=AppID` - - - - -- `https://l.taptap.io/GNYwFaZr?subc1=AppID` - - - -URL for devices with TapTap: - - - -- For Mainland China: `taptap://taptap.cn/app?app_id=AppID&source=outer|update` -- For other countries/regions: `tapglobal://taptap.tw/app?app_id=AppID&source=outer|update` - - - - -- `tapglobal://taptap.tw/app?app_id=AppID&source=outer|update` - - - -Make sure to replace the `AppID` in the URL. `AppID` is a game’s unique identifier in the TapTap store. For example, in `https://www.taptap.cn/app/187168``https://www.taptap.io/app/187168`, `187168` is the AppID. - -Keep in mind that you have to write the logic not only to open the URL but also to check whether TapTap is installed, as well as handle errors. - -Take a look at the code example below. - -
    -Code example - - - -For Mainland China: - -```java -package com.tds.common.utils; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; -import java.util.Locale; - -public class TapGameUtil { - - private static final String TAG = TapGameUtil.class.getName(); - - public static final String PACKAGE_NAME_TAPTAP = "com.taptap"; - - public static final String CLIENT_URI_TAPTAP = "taptap://taptap.cn"; - - public static final String DEFAULT_CLIENT_DOWNLOAD_URL_TAPTAP = "https://l.taptap.cn/5d1NGyET"; - - public static boolean updateGameAndFailToWebInTapTap(Activity activity, String appId) { - return updateGameInTapTap(activity, appId) || openWebDownloadUrlOfTapTap(activity, appId); - } - - public static boolean updateGameAndFailToWebInTapTap(Activity activity, String appId, String webUrl) { - if (TextUtils.isEmpty(webUrl)) { - return updateGameAndFailToWebInTapTap(activity, appId); - } - return updateGameInTapTap(activity, appId) || openWebDownloadUrl(activity, webUrl); - } - - public static boolean isTapTapInstalled(Context context) { - return isTapClientInstalled(context, PACKAGE_NAME_TAPTAP); - } - - public static boolean isTapClientInstalled(Context context, String clientPackageName) { - if (context != null && !TextUtils.isEmpty(clientPackageName)) { - boolean TapTapInstalled = false; - try { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo(clientPackageName, 0); - if (null != packageInfo) { - TapTapInstalled = true; - } - } catch (Exception e) { - Log.e(TAG, clientPackageName + " isInstalled=false"); - } - return TapTapInstalled; - } - return false; - } - - public static boolean updateGameInTapTap(Activity activity, String appId) { - return updateGameInTapClient(activity, appId, CLIENT_URI_TAPTAP); - } - - public static boolean updateGameInTapClient(Activity activity, String appId, String clientUri) { - if (activity != null && !TextUtils.isEmpty(appId) && !TextUtils.isEmpty(clientUri)) { - try { - Uri uri = Uri.parse(String.format(Locale.US, - "%s/app?app_id=%s&source=outer|update", clientUri, appId)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, clientUri + "updateGameInTapTap failed"); - return false; - } - return true; - } - Log.e(TAG, clientUri + "updateGameInTapTap failed"); - return false; - } - - public static boolean openReviewInTapTap(Activity activity, String appId) { - return openReviewInTapClient(activity, appId, CLIENT_URI_TAPTAP); - } - - public static boolean openReviewInTapClient(Activity activity, String appId, String clientUri) { - if (activity != null && !TextUtils.isEmpty(appId) && !TextUtils.isEmpty(clientUri)) { - try { - Uri uri = Uri.parse(String.format(Locale.US, - "%s/app?tab_name=review&app_id=%s", clientUri, appId)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, clientUri + "openTapTapReview failed"); - return false; - } - return true; - } - Log.e(TAG, clientUri + "openTapTapReview failed"); - return false; - } - - public static boolean openWebDownloadUrlOfTapTap(Activity activity, String appId) { - return openWebDownloadUrl(activity, String.format(Locale.US, DEFAULT_CLIENT_DOWNLOAD_URL_TAPTAP + "?subc1=%s", appId)); - } - - public static boolean openWebDownloadUrl(Activity activity, String url) { - if (activity != null && !TextUtils.isEmpty(url)) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setData(Uri.parse(url)); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, "openWebUrl fail"); - return false; - } - return true; - } - return false; - } - -} -``` - -For other countries/regions: - - - -```java -package com.tds.common.utils; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; -import java.util.Locale; - -public class TapGameUtil { - - private static final String TAG = TapGameUtil.class.getName(); - - public static final String PACKAGE_NAME_TAPTAP_GLOBAL = "com.taptap.global"; - - public static final String CLIENT_URI_TAPTAP_GLOBAL = "tapglobal://taptap.tw"; - - public static final String DEFAULT_CLIENT_DOWNLOAD_URL_TAPTAP_GLOBAL = "https://l.taptap.io/GNYwFaZr"; - - public static boolean updateGameAndFailToWebInTapGlobal(Activity activity, String appId) { - return updateGameInTapGlobal(activity, appId) || openWebDownloadUrlOfTapGlobal(activity, appId); - } - - public static boolean updateGameAndFailToWebInTapGlobal(Activity activity, String appId, String webUrl) { - if (TextUtils.isEmpty(webUrl)) { - return updateGameAndFailToWebInTapGlobal(activity, appId); - } - return updateGameInTapGlobal(activity, appId) || openWebDownloadUrl(activity, webUrl); - } - - public static boolean isTapGlobalInstalled(Context context) { - return isTapClientInstalled(context, PACKAGE_NAME_TAPTAP_GLOBAL); - } - - public static boolean isTapClientInstalled(Context context, String clientPackageName) { - if (context != null && !TextUtils.isEmpty(clientPackageName)) { - boolean TapTapInstalled = false; - try { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo(clientPackageName, 0); - if (null != packageInfo) { - TapTapInstalled = true; - } - } catch (Exception e) { - Log.e(TAG, clientPackageName + " isInstalled=false"); - } - return TapTapInstalled; - } - return false; - } - - public static boolean updateGameInTapGlobal(Activity activity, String appId) { - return updateGameInTapClient(activity, appId, CLIENT_URI_TAPTAP_GLOBAL); - } - - public static boolean updateGameInTapClient(Activity activity, String appId, String clientUri) { - if (activity != null && !TextUtils.isEmpty(appId) && !TextUtils.isEmpty(clientUri)) { - try { - Uri uri = Uri.parse(String.format(Locale.US, - "%s/app?app_id=%s&source=outer|update", clientUri, appId)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, clientUri + "updateGameInTapTap failed"); - return false; - } - return true; - } - Log.e(TAG, clientUri + "updateGameInTapTap failed"); - return false; - } - - public static boolean openReviewInTapGlobal(Activity activity, String appId) { - return openReviewInTapClient(activity, appId, CLIENT_URI_TAPTAP_GLOBAL); - } - - public static boolean openReviewInTapClient(Activity activity, String appId, String clientUri) { - if (activity != null && !TextUtils.isEmpty(appId) && !TextUtils.isEmpty(clientUri)) { - try { - Uri uri = Uri.parse(String.format(Locale.US, - "%s/app?tab_name=review&app_id=%s", clientUri, appId)); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, clientUri + "openTapTapReview failed"); - return false; - } - return true; - } - Log.e(TAG, clientUri + "openTapTapReview failed"); - return false; - } - - public static boolean openWebDownloadUrlOfTapGlobal(Activity activity, String appId) { - return openWebDownloadUrl(activity, String.format(Locale.US, DEFAULT_CLIENT_DOWNLOAD_URL_TAPTAP_GLOBAL + "?subc1=%s", appId)); - } - - public static boolean openWebDownloadUrl(Activity activity, String url) { - if (activity != null && !TextUtils.isEmpty(url)) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setData(Uri.parse(url)); - activity.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, "openWebUrl fail"); - return false; - } - return true; - } - return false; - } - -} -``` - -
    diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/_category_.json deleted file mode 100644 index 657b5a2e6..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "公告系统", - "collapsed": true, - "position": 8 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/design-billboard.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/design-billboard.mdx deleted file mode 100644 index e3653707b..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/design-billboard.mdx +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: Billboard Design Guideline -sidebar_position: 3 -slug: /sdk/billboard/design-billboard/ ---- - - - - - -import { Background, Figure } from "/src/docComponents/doc"; -import useBaseUrl from "@docusaurus/useBaseUrl"; - -## Types of Billboard - -
    -
    - - -## A.Navigation Template Type - -### Landscape screen cut size - -Custom landscape billboard need to upload top navigation bar background, left tab bar card background and pop-up window background image cutout, cutout are triple image @3X size, file type: JPG. PNG. - - -
    - - -### Landscape screen customization suggestions -The characters must be clearly visible. Therefore, it is recommended to use dark description characters when the background image is light and light description characters when the background image is dark. - - -
    - The text color contrasts clearly with the background color, making it easy to read, while the use of accent colors and secondary colors makes the overall structure clear. - - } - imgSrc={useBaseUrl("/img/design-billboard/2.2.png")} - imgAlt="" - /> -
    - Text color and background color contrast blurred, inconvenient to read, primary and secondary colors are relatively similar, the overall structure of the hierarchy is not clear. - - } - imgSrc={useBaseUrl("/img/design-billboard/2.3.png")} - imgAlt="" - /> - - -### Portrait screen cut size -Custom portrait billboard need to upload the top navigation bar background, tab bar card background, pop-up window background image and back to previous level button cutout, cutout are triple image @3X size, file type: JPG. PNG. - - -
    - - -### Portrait screen customization suggestions -The characters must be clearly visible. Therefore, it is recommended to use dark description characters when the background image is light and light description characters when the background image is dark. - - -
    - The text color contrasts clearly with the background color, making it easy to read, while the use of accent colors and secondary colors makes the overall structure clear. - - } - imgSrc={useBaseUrl("/img/design-billboard/2.5.png")} - imgAlt="" - /> -
    - Text color and background color contrast blurred, inconvenient to read, primary and secondary colors are relatively similar, the overall structure of the hierarchy is not clear. - - } - imgSrc={useBaseUrl("/img/design-billboard/2.6.png")} - imgAlt="" - /> - - -The text icons on the Back to previous level button are all part of the button cutout, and the cutout requires attention to. - - -
    - The button cutout must contain text or icon content. - - } - imgSrc={useBaseUrl("/img/design-billboard/2.7.png")} - imgAlt="" - /> -
    - Only the background cutout of the button is available, and the button meaning is not clear. - - } - imgSrc={useBaseUrl("/img/design-billboard/2.8.png")} - imgAlt="" - /> - - -### Global text type and font suggestions -Global text types can be divided into 4 types according to color configuration, which are default text, auxiliary text, announcement highlight text and announcement link text, fonts can be uploaded and configured by yourself, copyright issues are borne by the vendor. - - -
    - - - -
    - The text color of the main text is clear and easy to read, the color contrast between the auxiliary text and the highlighted text of the announcement is clear, and the text color of the announcement link is used correctly. - - } - imgSrc={useBaseUrl("/img/design-billboard/2.10.png")} - imgAlt="" - /> -
    - The text color of the main text is placed on the wrong background color, which is not easily recognizable, the color contrast between the auxiliary text and the highlighted text of the announcement is not strong enough, and the text of the announcement link has no clickable jumping sense. - - } - imgSrc={useBaseUrl("/img/design-billboard/2.11.png")} - imgAlt="" - /> - - -### More configurable areas globally with cutout sizes -In addition to fonts and text colors, there are more customizable details across the board such as close buttons, small red dots, empty status illustrations and announcement status labels, all cutouts are triple @3X size, file type: JPG. PNG. - - -
    - - -
    - - -
    - - -
    - - -## B.Image Template Type - -Please look forward to the relevant configuration documents~ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/features.mdx deleted file mode 100644 index 1fd969159..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/features.mdx +++ /dev/null @@ -1,202 +0,0 @@ ---- -title: Billboard Introduction -sidebar_label: Introduction -sidebar_position: 1 -slug: /sdk/billboard/features/ ---- - - - - - - -import { Conditional } from "/src/docComponents/conditional"; - -## Overview - -Billboard allows developers to publish and edit billboard contents on the Developer Center, which players can see when they open the game. - -## Highlights - -- Reduced costs: We’ll give you everything you need to create a billboard in your game so you don’t have to build it yourself. -- Easy-to-use tools: With the tools we provide, you can easily publish contents to the billboard in your game. -- Customizable configurations: You can upload custom themes and configure properties for content distribution without writing a single line of code. -- Outstanding compatibility: We offer user interfaces built with WebView, which supports layouts containing images and texts better compared to those built with game engines. -- Convenience to maintain: The Billboard service doesn’t depend on other services and you don’t have to update the SDK in order to update the contents of the billboard in your game. - -## Getting Started - -- For an account to have access to the Billboard service, please go to *Manage Permissions* and give this account the *Game Administrator* permission. -- To make the Billboard services accessible to the players, a custom domain has to be added to the game. -- Follow **[Billboard Design Guideline](../design-billboard/)** to design the assets needed for the billboard in the game. -- The minimum TapSDK version required to use Billboard is 3.12.0 for iOS and Android and 3.12.1 for Unity. - -## Feature Introduction - -### Basic Flow - -![](https://capacity-files.lcfile.com/qVCnc1ikCQTTYw8FC5AiLQWpAm6UnHUB/liuchengtu.png) - -### Enable the Service - -To start using Billboard in your game, go to **Developer Center > Game Services > Configuration** and *Turn On* the Billboard service. - -![](https://capacity-files.lcfile.com/uo1BNIYGnlMgcfB3IJGSN88MtEHxBTm5/io-open_billboard.png) - - - -### Configure Your Domain - -To ensure that users can have stable access to the Billboard service, we recommend that you add your own domain to your game. See **[Domain](/sdk/domain/guide/)** for more information on how to add your domain. - -![](https://capacity-files.lcfile.com/Az9ieoDfeykXfNUIjWcUyxoTFxYuRBmk/bond_url.png) - - - -### Configure Templates - -You can decide which template to show when players open the billboard in your game. - -- **Navigation Bar**: Designed for games with more announcements or publish announcements relatively more frequently. - -- **Poster**: Designed for games with fewer announcements or want a banner on the billboard. **(coming soon)** - -![](https://capacity-files.lcfile.com/E6MqYqVHaqh4jl4w2wnMT7cw82qWLASA/io-select_mod.png) - -If you choose to use the Navigation Bar for your game, you will be able to customize the UI of the billboard to make it better fit the visual style of your game. - -- We offer a default style for the billboard. You can reset the style of the billboard to this default style at any time. -- Configurations available to all games: Default Text Color, Secondary Text Color, Highlighted Text, Link Text, Global Font, Red dot, Close Button, Empty, and Icons for different types of announcement -- Configurations required for landscape games: Top Bar Background, Content Background Image, Default Background, and Background when selected -- Configurations required for portrait games: Top Bar Background, Content Background Image, Default Background, and Go-back Button - -:::note - -1. Please follow **[Billboard Design Guideline](../design-billboard/)** when designing the look of the billboard in your game. - -2. To ensure that the billboard can be opened without delay, each image uploaded should be within **100 KB**. - -3. To ensure that the billboard can be opened without delay, the font uploaded should be within **5 MB**. - -::: - -![](https://capacity-files.lcfile.com/IQJqjmWkKzg4ASs7Lneax4aF377J5l1L/io-edit_mod.png) - -**Regarding Icons for different types of announcement** - -You can use different icons for different types of announcements to get these announcements better distinguished. We offer 6 icons that you can use directly for different types, though it is not a requirement for you to pick an icon for each type. - -![](https://capacity-files.lcfile.com/v3gLbaELXfBbqrVpInrvib3OLy1RjKRG/billboard_label.png) - - -### Configure Properties - -Setting up properties helps you better **deliver your announcements**. You can customize the properties depending on how you are publishing your game. - -- There are two default properties provided: *Platform* and *Region*. These two properties cannot be deleted. - - *Platform* has 2 default fields: *iOS* and *Andriod-TapTap*. - - *Region* has 9 default fields: *China mainland*, *Macao China*, *Hong Kong China*, *Taiwan China*, *Japan*, *United States of America*, *South Korea*, *Indonesia*, and *Thailand*. -- You can add at most 10 custom properties. You will be able to edit and delete the custom properties after you create them. Different properties cannot have the same parameters or descriptions. - - You can add custom fields under each property. - - Different fields cannot have the same parameters or descriptions. -- Here are some recommended properties you can add: *Version Code*, *Server*, *Environment*, *Package*, and *Platform*. - -:::caution - -1. The parameter of a field can contain numbers, letters, and underscores. It is case-sensitive and can be no longer than 20 characters. - -2. The parameters you entered here will be used by clients and our server when they communicate. Different fields cannot have the same parameters. - -3. Once a property or field is created, you cannot update its parameter anymore. - -::: - -![](https://capacity-files.lcfile.com/OimaLKFzKOvQdIvvqeMd7Jk2ksoah12A/io-weidu_config.png) - -### Create Announcements - -When you create an announcement, you need to provide its content as well as settings. You will be able to **release the announcement immediately** or **save it as a draft**. Released announcements cannot be converted to drafts anymore, but you will be able to **hide** or **release (unhide)** a released announcement. - -For each announcement you create, you need to provide a Long Title, a Short Title, and its Content. You can set up multiple languages for the announcement. - -![](https://capacity-files.lcfile.com/CVsJKeJbayYO21JGUIUE8u76LwzwSyf4/io-release_billboard_content.png) - -You will be configuring *Display as*, *Type*, *Landing on*, *Distribution*, and *Date Settings* when you create an announcement. - -- Display as: You can associate the content of the announcement to a template. For now, you can only pick *Navigation Bar*. -- Type: You can choose from *Update*, *Maintenance*, *New Arrival*, *Event*, *Gameplay*, *Test*, and *Sever Closing*. -- Landing on: You can choose from *Content of Announcement*, *Redirection*, and *In-Game Scene*. -- Distribution: You can choose either to release the announcement to **everyone** or to **specify a number of distributions**. You can pick multiple options for each distribution. -- Date Settings: You can set a schedule for the announcement **to be released** and **to be hidden**. - -:::tip - -1. You can specify a callback URL for jumping from the announcement back to the game. - -2. You can still edit the distributions of an announcement after you release it. - -3. A hidden announcement will be released again when you edit and save it. - -::: - -![](https://capacity-files.lcfile.com/JjQMdA5TnRHY5uq6pHJvKmphwF8aszBq/io-release_billboard_config.png) - -### Sort Announcements - -You can give a number to each announcement to **control the order** of all the announcements of your game. Announcements with larger numbers will be displayed at the beginning of the list. - -You will be able to **filter announcements** according to **No.**, **Status**, **Display as**, **Region**, **Platform**, **Long Title**, and **Release Date**. - -![](https://capacity-files.lcfile.com/gpeJ1qWNlxh4ex3JBtcjE8riSRs65bCY/io-billboard_list.png) - -### Duplicate Announcements - -You will be able to **duplicate** an announcement to the current game or a different game and use it as a draft. The properties of an announcement cannot be duplicated. An announcement duplicated from another one will have unlimited properties by default. - -![](https://capacity-files.lcfile.com/3q9r9JbddBqk21I1cmKikaSs9Dgd4QHS/io-copy_billboard.png) - -### Display of Announcements - -#### Navigation Bar - -When there is at least one unread message under the *Announcements* tab or the *Events* tab, a badge will be displayed. The badge will disappear when all the messages are read. - -When using Navigation Bar, the most recent 20 announcements will be displayed. - -The content of an announcement can contain highlighted text, images, videos, and links. In landscape mode, the content of the first announcement will be automatically displayed. In portrait mode, only the announcement list will be displayed. The user will be able to go back to the list after they open an announcement. - -A link can either be opened in an external browser or take the user to a module within the game. It may also take the player to the content of an announcement. - -![](https://img.tapimg.com/market/images/a002825ac59f0c0456ff180afe9d6899.png) - -![](https://img.tapimg.com/market/images/ff9b021f99609e6d45445e92ab59e6bb.png) - -#### Poster - -Coming soon. - -## FAQ - -**Q: Can I use multiple templates in the same game?** - -A: Yes. You can use Navigation Bar and Poster altogether in your game. - -**Q: Can a link be opened within the game?** - -A: For now, a link can only be opened in an external browser. - -**Q: If I choose *iOS* as the *Platform*, *China mainland* as the *Region*, and *S1* and *S2* as the *Server*, then who will be able to see the announcement?** - -A: Only users who use iOS, are in China mainland, and are on S1 or S2 will see the announcement. Android users and those who are on other servers won’t see the announcement. - -**Q: Are there only two types of templates at this time?** - -A: At this time, there are only two types of templates available. If you need more customizations on the UI of the dashboard, we recommend that you integrate the Billboard service into your game using our API. - -**Q: How do I specify the value for players to jump to a module in the game from a link in an announcement?** - -A: You can provide anything like a string, ID, or URL. Just make sure the value is a unique identifier. - -**Q: Can I customize the type of an announcement?** - -A: Unfortunately, you cannot customize the type of an announcement at this time. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/guide.mdx deleted file mode 100644 index 37eb84993..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/billboard/guide.mdx +++ /dev/null @@ -1,778 +0,0 @@ ---- -title: Billboard Guide -sidebar_label: Guide -sidebar_position: 2 -slug: /sdk/billboard/guide/ ---- - - - - - - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import Languages from '../../sdk/_partials/languages.mdx'; -import { Conditional } from "/src/docComponents/conditional"; - -:::tip -Before integrating Billboard’s SDK into your game, please first **enable the service and configure your domains** according to [Feature Introduction](../features/#feature-introduction). -::: - -## SDK Setup - -Please download the TapSDK from the [Download page](/tap-download) and follow the instructions below to import the Billboard module to your project. - - - -<> - -If you are building your game with Unity, you can import a `.unitypackage` file to your game, which includes the iOS module and the Android module. If you are building with other engines or for other platforms, you can import the iOS or Android module natively. Please refer to the documentation for iOS or Android for more information. - -**Unity requirement**: You will need Unity 2019.4 or higher to use the SDK. - -Supported OS versions: - -- Android: 5.0 and higher -- iOS: 10.0 and higher, Xcode 14.1 or later - - -{`"dependencies":{ - "com.taptap.tds.billboard": "https://github.com/TapTap/TapBillboard-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.common": "https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.bootstrap": "https://github.com/TapTap/TapBootstrap-Unity.git#${sdkVersions.taptap.unity}", - "com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-${sdkVersions.leancloud.csharp}", - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -}`} - - - - -<> - - -{`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - - implementation (name:'TapBillboard_${sdkVersions.taptap.android}', ext:'aar') // The Billboard service - implementation (name:'TapCommon_${sdkVersions.taptap.android}', ext:'aar') - implementation (name:'TapBootstrap_${sdkVersions.taptap.android}', ext:'aar') -}`} - - - - -<> - - -{`// The Billboard service -TapBillboardResource.bundle -TapCommonResource.bundle -TapBillboardSDK.framework -TapCommonSDK.framework -TapBootstrapSDK.framework -`} - - - - - - - -## Initializing the SDK - -:::info -You can initialize the SDK using one of the following two methods. -::: - -### Initializing With the TapSDK - -If you have already initialized the TapSDK according to [Quickstart](/sdk/start/quickstart/#initialization), you will only need to import the Billboard module here and add the highlighted portions of the code below to your project. - - - -<> - -```cs -using TapTap.Common; -using TapTap.Bootstrap; -// highlight-next-line -using TapTap.Billboard; - -// highlight-start -var dimensionSet = new HashSet>(); -KeyValuePair platformPair = new KeyValuePair("platform", "TapTap"); -KeyValuePair locationPair = new KeyValuePair("location", "CN"); -dimensionSet.Add(platformPair); -dimensionSet.Add(locationPair); -var templateType = "navigate"; // Optional -var billboardServerUrl = "https://your-billboard-server-url"; // Developer Center > Your game > Game Services > Configuration > Domain > Billboard -// highlight-end - -var config = new TapConfig.Builder() - .ClientID("your_client_id") // Required; the Client ID obtained from the Developer Center - .ClientToken("your_client_token") // Required; the Client Token obtained from the Developer Center - .ServerURL("https://your_server_url") // Required; can be found on Developer Center > Your game > Game Services > Basic Information > Domain > Cloud Services API - .RegionType(RegionType.CN) // Optional; CN means China mainland and IO means other countries and regions - // highlight-next-line - .TapBillboardConfig(dimensionSet, templateType, billboardServerUrl) - .ConfigBuilder(); -TapBootstrap.Init(config); -``` - - - -<> - -**Attention: The Android SDK has to be initialized in the main thread.** - - - -To initialize for China mainland: - -```java -// highlight-start -Set> dimensionSet = new HashSet<>(); -dimensionSet.addAll(Arrays.asList(Pair.create("location", "CN"), Pair.create("platform", "TapTap"))); -String billboardServerUrl = "https://your-billboard-server-url"; // Developer Center > Your game > Game Services > Configuration > Domain > Billboard - -TapBillboardConfig billboardCnConfig = new TapBillboardConfig.Builder() - .withDimensionSet(dimensionSet) // Optional - .withServerUrl(billboardServerUrl) // Required; the custom domain for Billboard - .withTemplate("navigate") // Optional; defaults to `navigate` - .build(); -// highlight-end - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(gameMainActivity) // Required; provide the main `Activity` of the game - .withClientId("your_client_id") // Required; the Client ID obtained from the Developer Center - .withClientToken("your_client_token") // Required; the Client Token obtained from the Developer Center - .withServerUrl("https://your_server_url") // Required; can be found on Developer Center > Your game > Game Services > Basic Information > Domain > Cloud Services API - // highlight-next-line - .withBillboardConfig(billboardCnConfig) // Required - .withRegionType(TapRegionType.CN) - .build(); -TapBootstrap.init(gameMainActivity, tapConfig); -``` - -To initialize for other countries and regions: - - - -```java -// highlight-start -Set> dimensionSet = new HashSet<>() -dimensionSet.addAll(Arrays.asList(Pair.create("location", "XX"), Pair.create("platform", "TapTap"))); - -TapBillboardConfig billboardIntlConfig = new TapBillboardConfig.Builder() - .withDimensionSet(dimensionSet) // Optional - .withTemplate("navigate") // Optional; defaults to `navigate` - .build(); -// highlight-end - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(gameMainActivity) // Required; provide the main `Activity` of the game - .withClientId("your_client_id") // Required; the Client ID obtained from the Developer Center - .withClientToken("your_client_token") // Required; the Client Token obtained from the Developer Center - .withServerUrl("https://your_server_url") // Required; can be found on Developer Center > Your game > Game Services > Basic Information > Domain > Cloud Services API - // highlight-next-line - .withBillboardConfig(billboardIntlConfig) // Required - .withRegionType(TapRegionType.IO) - .build(); -TapBootstrap.init(gameMainActivity, tapConfig); -``` - - - -<> - - - -To initialize for China mainland: - -```objectivec -// highlight-start -NSMutableSet *dimensionSet = [[NSMutableSet alloc] init]; -[dimensionSet addObject:[NSArray arrayWithObjects:@"platform", @"ios", nil]]; -[dimensionSet addObject:[NSArray arrayWithObjects:@"location", @"CN", nil]]; - -TapBillboardConfig *billboardCnConfig = [TapBillboardConfig new]; -billboardCnConfig.templateType = @"navigate"; // Optional -billboardCnConfig.diemensionSet = dimensionSet; // Optional -billboardCnConfig.serverUrl = @"https://your-billboard-server-url"; // Developer Center > Your game > Game Services > Configuration > Domain > Billboard -// highlight-end - -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // Required; the Client ID obtained from the Developer Center -config.clientToken = @"your_client_token"; // Required; the Client Token obtained from the Developer Center -config.region = TapSDKRegionTypeCN; // `TapSDKRegionTypeCN` means China mainland; `TapSDKRegionTypeIO` means other countries and regions -config.serverURL = "https://your_server_url"; // Required; can be found on Developer Center > Your game > Game Services > Basic Information > Domain > Cloud Services API -// highlight-next-line -config.tapBillboardConfig = billboardCnConfig; -[TapBootstrap initWithConfig:config]; -``` - -To initialize for other countries and regions: - - - -```objectivec -// highlight-start -NSMutableSet *dimensionSet = [[NSMutableSet alloc] init]; -[dimensionSet addObject:[NSArray arrayWithObjects:@"platform", @"ios", nil]]; -[dimensionSet addObject:[NSArray arrayWithObjects:@"location", @"XX", nil]]; - -TapBillboardConfig *billboardIntlConfig = [TapBillboardConfig new]; -billboardIntlConfig.templateType = @"navigate"; // Optional -billboardIntlConfig.diemensionSet = dimensionSet; // Optional -// highlight-end - -TapConfig *config = [TapConfig new]; -config.clientId = @"your_client_id"; // Required; the Client ID obtained from the Developer Center -config.clientToken = @"your_client_token"; // Required; the Client Token obtained from the Developer Center -config.region = TapSDKRegionTypeIO; // `TapSDKRegionTypeCN` means China mainland; `TapSDKRegionTypeIO` means other countries and regions -config.serverURL = "https://your_server_url"; // Required; can be found on Developer Center > Your game > Game Services > Basic Information > Domain > Cloud Services API -// highlight-next-line -config.tapBillboardConfig = billboardIntlConfig; -[TapBootstrap initWithConfig:config]; -``` - - - - - -### Initializing the Billboard Service Independently - -If you prefer not to initialize the TapSDK with the `TapBootstrap` method mentioned above and would like to only initialize the Billboard service, you can: - -- **Copy the above code for initializing the TapSDK**. -- **Change the last line** to: - - - -```cs -TapBillboard.Init(config); -``` - -```java -TapBillboard.init(tapConfig); -``` - -```objectivec -[TapBillboard initWithConfig:config]; -``` - - - -### Parameters - - - -* You will be providing both the API domain and the Billboard domain when initializing the SDK. See [Domain](/sdk/domain/guide/) for more information. Please make sure you have added your domain on **Developer Center > Your game > Game Services > Configuration > Domain**. - - - -* See [Configure Properties](../features/#configure-properties) for more information about `dimensionSet`. Here we are using the two default properties: Platform and Region. - -* `client_id`, `client_token`, and `RegionType` can be found on **Developer Center > Your game > Game Services > Configuration**. - -## Opening the Billboard - - - -```cs -TapBillboard.OpenPanel((any, error) => -{ - if (error != null) - { - // Failed to open the billboard; see `error.code` and `error.errorDescription` for the reason - } else - { - // Billboard is now open - } -}, () => { - // Billboard is now closed -}); -``` - -```java -TapBillboard.openPanel(BillboardActivity.this, new Callback() { - @Override - public void onError(TapBillboardException tapBillboardException) { - // Failed to open the billboard; see `tapBillboardException.code` and `tapBillboardException.message` for the reason - } - - @Override - public void onSuccess(Void result) { - // Billboard is now open - } -},new UserInteraction() { - @Override - public void onClose() { - // Billboard is now closed; please switch to the main thread if you need to update the UI - } -}); -``` - -```objectivec -[TapBillboard openPanel:^(bool _, NSError *_Nullable error) { - if (error) { - // Failed to open the billboard; see `error.code` and `error.errorDescription` for the reason - } else { - // Billboard is now open - } -} closeCallback:^(void){ - // Billboard is now closed -}]; -``` - - - -## Obtaining Badge Statuses - -Badges can be used to tell whether there are new messages. Inside the SDK, the statuses of the badges are cached. If a request is made within 5 minutes after the last request is made, the result of the last successful request will be returned. - - - -```cs -TapBillboard.QueryBadgeDetails((badgeDetails, error) => -{ - if (error != null) - { - // Failed to obtain badge statuses; see `error.code` and `error.errorDescription` for the reason - } - else - { - // Badge statuses obtained - if (badgeDetails.showRedDot == 1) { - // New messages found - } else { - // No new messages - } - } -}); -``` - -```java -TapBillboard.getBadgeDetails(new Callback() { - @Override - public void onError(TapBillboardException tapBillboardException) { - // Failed to obtain badge statuses; see `tapBillboardException.code` and `tapBillboardException.message` for the reason - } - - @Override - public void onSuccess(BadgeDetails badgeDetails) { - if (badgeDetails.showRedDot == 1) { - // New messages found - } else { - // No new messages - } - } -}); -``` - -```objectivec -[TapBillboard getBadgeDetails:^(BadgeDetails * _Nullable result, NSError *_Nullable error) { - if (error) { - // Failed to obtain badge statuses; see `error.code` and `error.errorDescription` for the reason - } else { - if ([result.showRedDot intValue] == 1) { - // New messages found - } else { - // No new messages - } - } -}]; -``` - - - -## Registering Custom Event Listeners - -If [an announcement is configured to jump to *a module in the game*](../features/#create-announcements), or if you have added links in the content of an announcement that jumps to *a module in the game*, you can obtain the message with a custom event listener you set up and handle the message accordingly. - - - -```cs -TapBillboard.RegisterCustomLinkListener(url => -{ - // The URL returned here is the same as the one configured on the Developer Center -}); -``` - -```java -TapBillboard.registerCustomLinkListener(new CustomLinkListener() { - @Override - public void onCustomUrlClick(String url) { - // The URL returned here is the same as the one configured on the Developer Center - // Please switch to the main thread if you need to update the UI - } -}); -``` - -```objectivec -[TapBillboard registerCustomLinkListener:^(NSString * _Nullable customUrl) { - // The URL returned here is the same as the one configured on the Developer Center -}]; -``` - - - -## Unregistering Custom Event Listeners - -Use the following interface to stop listening to the custom events related to announcements: - - - -```cs -// Registered listeners should be stored and provided here to have them unregistered -TapBillboard.UnRegisterCustomLinkListener(registerdListener); -``` - -```java -// Registered listeners should be stored and provided here to have them unregistered -TapBillboard.unRegisterCustomLinkListener(registerdListener); -``` - -```objectivec -// Registered listeners should be stored and provided here to have them unregistered -[TapBillboard unRegisterCustomLinkListener:registerdListener]; -``` - - - -## Internationalization - -You can set different languages for the Billboard service: - - - -## Error Codes - -| `code` | Scenario | -|---|---| -| 400 | An error exists in the parameters used for initializing the SDK. | -| 403 | The user is not authorized to access this service. Please make sure the Billboard service has been enabled on the Developer Center. | -| 50x | There is an internal error in our server. See `description` for more information. | -| 19999 | An unknown error occurred. See `description` for more information. | - -## REST API - -Below are the REST APIs provided by the Billboard services. - -### Request Format - -For POST - -For POST and PUT requests, the request body must be in JSON and the Content-Type of the HTTP Header must be `application/json`. - -Requests are authenticated by the key-value pairs in the HTTP Header shown in the following table: - -| Key | Value | Description | Source | -| ---------- | ------------ | ----------------------------------------- | -------------- | -| `X-LC-Id` | `{{appid}}` | The `App Id` (`Client Id`) of the current app | Can be found on the Developer Center | -| `X-LC-Sign` | `{{appSign}}` | A string in the form of `sign,timestamp`. `timestamp` is the unix timestamp (UTC) of the client in milliseconds. `sign` is a string formed by concatenating the `timestamp` with the `App Key` (`Client Token`) and running it through the MD5 algorithm.| See the algorithm below. | - -To calculate the `appSign` with the `App Key`: - -```sh -md5( timestamp + App Key ) -= md5(1453014943466UtOCzqb67d3sN12Kts4URwy8) -= d5bcbb897e19b2f6633c716dfdfaf9be - --H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" -``` - -Besides setting the value of `X-LC-Id` to the `Client Id` in the HTTP Header, you will also need to provide the `Client Id` in the URL, and their values have to be the same. - -You will get an HTTP 400 error if something goes wrong, like: - -```json -{ - "success": false, - "data": { - "code": 0, - "error": "invalid_request", - "msg": "invalid params", - "error_description": "" - }, - "now": 1659084246 -} -``` - -### Base URL - -The Base URL for REST API requests (the `{{host}}` in the curl examples) is the custom API domain of your app. You can update or find it on the Developer Center. See [Domain](/sdk/storage/guide/setup-dotnet#domain) for more details. - -### Obtaining the Template - -| Parameter | Constraint | Description | -|---|---|---| -| `client_id` | Required | The `Client ID` found on **Developer Center > Game Services > Configuration**. | -| `template` | Required | The type of the template. For now, only Navigation Bar is supported. Use `navigate` for Navigation Bar and `image` for Poster. | - -```sh -curl -X GET - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - https://{{host}}/billboard/rest-api/v1/pattern/detail?client_id={{appid}}&template={{template}} -``` - -The response body is a JSON object containing all the parameters provided when the template is created: - -```json -{ - "success": true, - "data": { - "empty_img": { - "url": "https://tds-billboard.tds1.tapfiles.cn/20220727/aWkG63mpT2WeFrtT0Dxojj4QLfabWHh3.png" - }, - "highlight_text_color": "#00D9C5", - ... - }, - "now": 1659085552 -} -``` - -### Obtaining Announcements - -| Parameter | Constraint | Description | -|---|---|---| -| `client_id` | Required | The `Client ID` found on **Developer Center > Game Services > Configuration**. | -| `lang` | Required | See [Language Codes](#language-codes). | -| `template` | Required | The type of the template. For now, only Navigation Bar is supported. Use `navigate` for Navigation Bar and `image` for Poster. | -| `dimension_list` | Optional | `[{"PROPERTY_PARAMETER":"FIELD_PARAMETER"},...]`. Example: `[{"platform":"ios"},{"location":"CN"}]`. | -| `type` | Required | The type of the announcement. Use `activity` for Events and `game` for Announcements.| -| `uuid` | Optional | A unique ID identifying the device. Used for analytical purposes only. Example: `4e4105c7-781e-45c0-92ea-d595c75a3c2c`. | - -```sh -curl -X POST - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - -H 'Content-Type: application/json' \ - -d '{ - "lang":{{lang}}, - "template":{{template}}, - "type":{{type}}, - "dimension_list":{{dimension_list}} - }' \ - https://{{host}}/billboard/rest-api/v1/announcement/list?client_id={{appid}}&uuid={{uuid}} -``` - -The returned JSON object contains the announcement list, `list`, as well as the details of the latest announcement, `lastest`. - -| Parameter | Description | -|---|---| -| `id` | The ID of the announcement. | -| `type` | The type of the announcement. Could be `update`, `maintenance`, `new`, `activity`, `play_method`, `test`, or `discontinued`. | -| `expire_time` | The timestamp of the expiration time in seconds. `0` means the announcement will not expire. | -| `short_title` | The short title. | -| `jump_location` | The place to jump to. Could be `content`, `link`, or `game`. | -| `jump_link` | The link for redirection or the parameter indicating the module in the game to jump to. | -| `publish_time` | The timestamp of the publication time in seconds. | - -```json -{ - "success": true, - "data": { - "list": [ - { - "id": 2, - "type": "activity", - "expire_time": 0, - "short_title": "Hello", - "jump_location": "link", - "jump_link": "https://www.taptap.cn", - "publish_time": 1659077524 - }, - ... - ], - "lastest": { - "id": 2, - "content": "This is the content.", - "short_title": "Hello", - "long_title": "Hello world!" - } - }, - "now": 1659085756 -} -``` - -### Obtaining Announcement Details - -| Parameter | Constraint | Description | -|---|---|---| -| `client_id` | Required | The `Client ID` found on **Developer Center > Game Services > Configuration**. | -| `lang` | Required | See [Language Codes](#language-codes). | -| `id` | Required | The ID of the announcement. | -| `uuid` | Optional | A unique ID identifying the device. Used for analytical purposes only. Example: `4e4105c7-781e-45c0-92ea-d595c75a3c2c`. | - -```sh -curl -X GET - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - https://{{host}}/billboard/rest-api/v1/announcement/detail?client_id={{appid}}&lang={{lang}}&id={{id}}&uuid={{uuid}} -``` - -Returned JSON object - -| Parameter | Description | -|---|---| -| `id` | The ID of the announcement. | -| `content` | The content of the announcement. | -| `long_title` | The long title of the announcement. | -| `short_title` | The short title of the announcement. | - -```json -{ - "success": true, - "data": { - "id": 82, - "content": "This is the content.", - "short_title": "This is the short title.", - "long_title": "This is the long title." - }, - "now": 1659087990 -} -``` - -### Obtaining Badge Statuses - -| Parameter | Constraint | Description | -|---|---|---| -| `client_id` | Required | The `Client ID` found on **Developer Center > Game Services > Configuration**. | -| `uuid` | Required | A unique ID identifying the device. Example: `4e4105c7-781e-45c0-92ea-d595c75a3c2c`. | -| `template` | Required | The type of the template. For now, only Navigation Bar is supported. Use `navigate` for Navigation Bar and `image` for Poster. | -| `dimension_list` | Optional | `[{"PROPERTY_PARAMETER":"FIELD_PARAMETER"},...]`. Example: `[{"platform":"ios"},{"location":"CN"}]`. | - -```sh -curl -X POST \ - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - -H 'Content-Type: application/json' \ - -d '{ - "uuid": {{uuid}}, - "template":{{template}}, - "dimension_list":{{dimension_list}} - }' \ - 'https://{{host}}/billboard/rest-api/v1/dot/read?client_id={{client_id}}' -``` - -Returned JSON object - -| Parameter | Description | -|---|---| -| `show_red_dot` | Whether to show the badge. `0` means no and `1` means yes. | -| `close_button_img` | The URL of the image for the close button. | - -```json -{ - "success": true, - "data": { - "show_red_dot": 0, - "close_button_img": "https://tds-billboard.tds1.tapfiles.cn/20220727/aMHoqDTHT4zrXYPNprkZjXkdLK6vFg8E.png" - }, - "now": 1658896487 -} -``` - -### Updating Badge Statuses - -| Parameter | Constraint | Description | -|---|---|---| -| `client_id` | Required | The `Client ID` found on **Developer Center > Game Services > Configuration**. | -| `uuid` | Required | A unique ID identifying the device. Example: `4e4105c7-781e-45c0-92ea-d595c75a3c2c`. | -| `read_all` | Required | Whether to mark all announcements as read. `true` for yes and `false` for no. | - -```sh -curl -X POST \ - -H 'X-LC-Id: {{appid}}' \ - -H 'X-LC-Sign: {{appSign}}' \ - -H 'Content-Type: application/json' \ - -d '{ - "uuid": {{uuid}}, - "read_all":{{read_all}} - }' \ - 'https://{{host}}/billboard/rest-api/v1/dot/submit?client_id={{client_id}}' -``` - -Returned JSON object -```json -{ - "success": true, - "data": { - "msg": "ok" - }, - "now": 1658895192 -} -``` - -### Language Codes - -The REST API accepts language codes defined by ISO 639-1. For example, `en` means English and `jp` means Japanese. There are a couple of exceptions: - -1. For languages not included in ISO 639-1, the language codes defined in ISO 632-2 are used. For example, `fil` means Filipino. -2. When a language cannot be represented by a language code, the location code defined in ISO 3166-1 will be attached. For example, `zh_CN` means Simplified Chinese. - -The following language codes are supported by the REST API: - -| Code | Language | -| ------- | ----------- | -| zh_CN | Simplified Chinese | -| zh_TW | Traditional Chinese | -| en_US | English (US) | -| ja_JP | Japanese | -| ko_KR | Korean | -| pt_PT | Portuguese | -| vi_VN | Vietnamese | -| hi_IN | Hindi | -| id_ID | Indonesian | -| ms_MY | Malay | -| th_TH | Thai | -| es_ES | Spanish | -| af | Afrikaans | -| am | Amharic | -| bg | Bulgarian | -| ca | Catalan | -| hr | Croatian | -| cs | Czech | -| da | Danish | -| nl | Dutch | -| et | Estonian | -| fil | Filipino | -| fi | Finnish | -| fr | French | -| de | German | -| el | Greek | -| he | Hebrew | -| hu | Hungarian | -| is | Icelandic | -| it | Italian | -| lv | Latvian | -| lt | Lithuanian | -| no | Norwegian | -| pl | Polish | -| ro | Romanian | -| ru | Russian | -| sr | Serbian | -| sk | Slovak | -| sl | Slovenian | -| sw | Swahili | -| sv | Swedish | -| tr | Turkish | -| uk | Ukrainian | -| zu | Zulu | - -Keep in mind that some of the languages listed above are only supported by the REST API but [not the client SDK](#internationalization). - -## Video Tutorials - -You can refer to the video tutorial:[How to Integrate Billboard Functionality in Games](https://www.bilibili.com/video/BV1tp4y1L7Fe/) to learn how to access the billboard feature in Untiy project. - -For more video tutorials, see [Developer Academy](https://developer.taptap.cn/tds-tutorials/list). As the SDK features are constantly being improved, there may be inconsistencies between the video tutorials and the new SDK features, so the current documentation should prevail. \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/_category_.json deleted file mode 100644 index e777c9648..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "游戏好友", - "collapsed": true, - "position": 6 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/features.mdx deleted file mode 100644 index b4d7a6158..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/features.mdx +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: Friends Introduction -sidebar_label: Introduction -sidebar_position: 1 -slug: /sdk/friends/features ---- - - - - - - -## Introduction - -TapTap Developer Services (TDS) offers the solution for you to quickly add friending-related features into your game. - -## What We Offer - -- **Easy-to-use APIs that improve your productivity** - - The Friends service provides features like adding, deleting, and searching for friends so you don’t have to build them yourself. - -- **Real-time data tracking that allows you to perform refined operations** - - As a developer, you can utilize our tools to perform admin operations like looking up players’ connections and activity logs. - -- **Access to TapTap’s ecosystem** - - Obtain the connections among TapTap users and boost your game’s experience with them. - -## Basic Concepts - -**Game friends** - -Game friends are based on the built-in TDS account system. Interfaces applicable to them include adding, deleting, and searching for friends. There are two modes available for game friends: Follow mode and Friend mode. - -- Follow mode - - Player A can follow Player B to become their follower. Player B now becomes a followee of Player A. If Player B then follows Player A, Player A and Player B will become mutual followers. - -- Friend mode - - Player A can send a friend request to Player B. Once Player B accepts the request, Player A and Player B become friends. - -You can choose either Follow mode or Friend mode for your game, but not both. - -**TapTap friends** - -For users logged in with TapTap, you can obtain the mutual followers of these users from your game. - -:::tip -Both game friends and TapTap friends depend on the built-in TDS account system. Make sure to enable TDS Authentication before you use Friends in your game. -::: - -## Functions - -### Game Friends - -#### Follow Mode - -| **Function** | **Scenarios** | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Get followee list | Players can view the other players they are following. | -| Get mutual follower list | Players can view a list of their mutual followers. | -| Get blocklist | Players can view the other players they have blocked. | -| Follow | Players can follow other players from player lists or scoreboards. | -| Unfollow | Players can unfollow other players. | -| Block | Players can block other players from player lists or scoreboards. At this time, each player can have a maximum of 100 players on their blocklist. | -| Unblock | Players can remove other players from their blocklist. | -| Search and follow players | Players can search and follow other players with their nicknames, friend codes (a 6-digit code containing lowercase letters), and `objectId`s. | -| Share follow invites | Players can share links for follow invites on their social media. | -| Get follower list | Players can view the other players who are following them. | -| Rich presence | Display a player’s status information like their online status, current hero, and current game mode. | -| Sort by online status | A player’s followee list and mutual follower list can be sorted by online status. The follower list doesn’t support this function yet. | - -#### Friend Mode - -| **Function** | **Scenarios** | -| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Get friend list | Players can view a list of their friends. | -| Check friendship status | Check if any two players are friends. | -| Add friends | Players can send friend requests to other players from player lists or scoreboards. Players can also view the friend requests they have received. | -| Search and add friends | Players can search and send friend requests to other players with their nicknames, friend codes (a 6-digit code containing lowercase letters), and `objectId`s. | -| Share friend invites | Players can share links for friend invites on their social media. | -| Delete friends | Players can delete their friends. | -| Get pending friend requests | Players can view the friend requests they have received. | -| Accept friend requests | Players can accept other players’ requests to be their friends. | -| Decline friend requests | Players can decline other players’ requests to be their friends. | -| Get blocklist | Players can view the other players they have blocked. | -| Block | Players can block other players from player lists or scoreboards. At this time, each player can have a maximum of 100 players on their blocklist. | -| Unblock | Players can remove other players from their blocklist. | -| Sort by online status | A player’s friend list can be sorted by online status. | -| Set friend limit | You can set the maximum number of friends each player can have by going to **Developer Center > Game Services > Cloud Services > Friends**. The maximum value for this setting is 2000. | -| Rich presence | Display a player’s status information like their online status, current hero, and current game mode. | - -### TapTap Friends - -| **Function** | **Scenarios** | -| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Follow TapTap account (please contact us to enable this interface) | Players can follow other players’ TapTap accounts in your game. | -| Get TapTap mutual followers (please contact us to enable this interface) | If two players are following each other and they both authorized your game to access their friend lists, your game will be able to obtain their connection. | - -## Pricing - -- You will be billed for the Friends service according to the number of API requests made. This number will be counted into the number of API requests made for the Data Storage service. The two services together share the free quota of 30,000 requests per day. $0.04 will be charged for every 1,000 requests exceeding the free quota. - -- Access to TapTap Friends is currently free. Please contact us to enable it. - -## Q&A - -Q: Can I use the Follow mode together with the Friend mode? - -A: No. You can only pick one of them for your game. - -Q: Is there a limit for the number of followees for the Follow mode? - -A: Yes. A player can follow at most 5000 people in each game. This number is not adjustable. - -Q: Can I customize the text for sharing invites? - -A: Yes. We recommend that you use something like “xx invited you to follow them” for the Follow mode and “xx invited you to become their friend” for the Friend mode. - -If you have any other questions, feel free to submit a ticket to us from the Developer Center. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/follow.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/follow.mdx deleted file mode 100644 index ebdb59272..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/follow.mdx +++ /dev/null @@ -1,1046 +0,0 @@ ---- -title: Follow Mode -sidebar_position: 4 -slug: /sdk/friends/follow ---- - - - - - -import MultiLang from "/src/docComponents/MultiLang"; - -Before continuing, make sure you have [finished initializing the SDK](/sdk/friends/guide/). - -## Responding to Friend Status Changes - -With the Friends module, the client can listen to the status changes of the player’s friends and display them to the player in real-time. -You’ll need to register an instance for listening to friend status changes before calling the interface for getting the player online. By doing this, the player will be able to receive notifications after they get online: - - - -```cs -TDSFollows.FriendStatusChangedDelegate = new TDSFriendStatusChangedDelegate { - // The current player got online (connection established) - OnConnected = () => {}, - // Connection lost; the SDK will try to reconnect automatically - OnDisconnected = () => {}, - // Connection error - OnConnectionError = (code, message) => {}, -}; -``` - -```java -TDSFollows.registerFriendStatusChangedListener(new FriendStatusChangedListener() { - // The current player got online (connection established) - @Override - public void onConnected() {} - - // Connection lost; the SDK will try to reconnect automatically - @Override - public void onDisconnected() {} - - // Connection error - @Override - public void onConnectError(int code, String msg){}); -} -``` - -```objc -[TDSFollows registerNotificationDelegate:self]; - -// The current player got online (connection established) -- (void)onConnected {} - -// Connection lost; the SDK will try to reconnect automatically -- (void)onDisconnected {} - -// Connection error -- (void)onDisconnectedWithError:(NSError * _Nullable)error {} -``` - - - -To stop listening: - - - -```cs -TDSFollows.FriendStatusChangedDelegate = null; -``` - -```java -TDSFollows.removeFriendStatusChangedListener(); -``` - -```objc -[TDSFollows unregisterNotificationDelegate]; -``` - - - -## Getting the Player Online - -After the player logs in, you need to call this interface to establish a persistent connection between the client and the cloud. -Once the persistent connection is established, if there is an interruption to the internet connection, the SDK will automatically reconnect once the connection is restored. - - -<> - -```cs -await TDSFollows.Online(); -``` - - -<> - -```java -TDSFollows.online(new Callback() { - @Override - public void onSuccess(Void result) { - // Success - } - - @Override - public void onFail(TDSFriendError error) { - // Handle error - } -}); - -``` - -With the persistent connection established, if the player opens an invitation link, the Android SDK will automatically send a friend request to the corresponding player. - - -<> - -```objc -[TDSFollows online]; -``` - - - - -## Getting the Player Offline - -After the player logs out, you need to call this interface to disconnect the client from the cloud. - - - -```cs -await TDSFollows.Offline(); -``` - -```java -TDSFollows.offline(); -``` - -```objc -[TDSFollows offline]; -``` - - - -## Searching for Friends by Nickname - -A player can search for friends by nickname without knowing their `objectId`s. -For example, to search for friends with `Tarara` as their nickname: - - - -```cs -ReadOnlyCollection friendInfos = await TDSFollows.SearchUserByName("Tarara"); -foreach (TDSFriendInfo info in friendInfos) { - // Player data - TDSUser user = info.User; - // Rich presence data; continue reading for more information - Dictionary richPresence = RichPresence; - // Whether the friend is online - bool online = info.Online; -} -``` - -```java -TDSFollows.searchUserByName("Tarara", new ListCallback() { - @Override - public void onSuccess(List friendInfoList) { - for (TDSFriendInfo info : friendInfoList) { - // Player data - TDSUser user = info.getUser(); - // Rich presence data; continue reading for more information - TDSRichPresence richPresence = info.getRichPresence(); - // Whether the friend is online - boolean online = info.isOnline(); - } - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed search friend by nickname" + error.detailMessage); - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFollows searchUserWithNickname:@"Tarara" option:option -callback:^(NSArray * _Nullable friendInfos, NSError * _Nullable error) { - if (friendInfos) { - for (TDSFriendInfo *info in friendInfos) { - // Player data - TDSUser *user = info.user; - // Rich presence data; continue reading for more information - NSDictionary *richPresence = info.richPresence; - // Whether the friend is online - BOOL online = info.online; - } - } else if (error) { - // Handle error - } -}]; -``` - - - -Notice that **in order to use this function, the `nickname` field has to be set on the built-in account system**. -See [TDS Authentication Guide](/sdk/authentication/guide/#setting-other-user-properties) for more information. - -## Friend Code - -Each logged-in player has a friend code that can be shared with other players so these players can quickly add the current player as their friend. - -You can get the friend code of a `TDSUser` from its `shortId` field: - - - -```cs -// currentUser is a logged in TDSUser -string shortId = currentUser["shortId"]; -``` - -```java -String shortId = currentUser.getString("shortId"); -``` - -```objc -NSString *shortId = currentUser[@"shortId"]; -``` - - - -To search for a player with their friend code: - - - -```cs -TDSFriendInfo friendInfo = await TDSFollows.SearchUserByShortCode(shortId); -``` - -```java -TDSFollows.searchUserByShortCode(shortId, new Callback() { - @Override - public void onSuccess(TDSFriendInfo friendInfo) { /* See the previous section */ } - - @Override - public void onFail(TDSFriendError error) { /* See the previous section */ } -}); -``` - -```objc -[TDSFollows searchUserWithShortCode:shortId -callback:^(TDSFriendInfo * _Nullable friendInfo, NSError * _Nullable error) { - // See the previous section -}]; -``` - - - -## Searching for Friends With `objectId` - -Besides using nicknames and friend codes, a player can also search for friends with their `objectId`s. - -For example, to search for the friend with `5b0b97cf06f4fd0abc0abe35` as their `objectId`: - - - -```cs -TDSFriendInfo friendInfo = await TDSFollows.SearchUserById("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFollows.searchUserById("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(TDSFriendInfo friendInfo) { /* Other things to do */ } - - @Override - public void onFail(TDSFriendError error) { /* Other things to do */ } -}); -``` - -```objc -[TDSFollows searchUserWithObjectId:@"5b0b97cf06f4fd0abc0abe35" -callback:^(TDSFriendInfo * _Nullable friendInfo, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -## Rich Presence - -Rich presence can be used to display the player’s status information like their online status, current hero, and current game mode. - -After adding configurations for rich presence on the Developer Center, you can set the content of a player’s rich presence according to the configured fields for rich presence: - - - -```cs -await TDSFollows.SetRichPresence("score", "60"); -``` - -```java -TDSFollows.setRichPresence("score", "60", new Callback() { - @Override - public void onSuccess(Void result) { - toast("Succeed to set rich presence."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to set rich presence: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFollows setRichPresenceWithKey:@"score" value:@"60" - callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Succeed to set rich presence. - } else if (error) { - // Failed to set rich presence. - } -}]; -``` - - - -Here `score` is a rich presence field configured on the Developer Center. -There are two types available for each rich presence field: - -- `variable`: The value is a string. In the code example above, with `score` configured to be a `variable` on the Developer Center, the client sets the value of this field to be `60` when updating the rich presence data. The cloud will accordingly return `"score": "60"` to the client as the rich presence data. You need to handle the localization-related logic yourself so that the player can eventually see something like “Score: 60”. - -- `token`: The value is a string starting with `#`. In the example below, the type of `display` is `token`, and the client sets the value of this field to be `#matching` when updating the rich presence data. The cloud will handle the conversion of this value to a localized string and return the result like `"display": "Matching"` to the client. Notice that if the cloud fails to convert the value, an empty string like `"display": " "` will be returned. - -To set multiple fields at once: - - - -```cs -Dictionary info = new Dictionary(); -info.Add("score", "60"); -info.Add("display", "#matching"); -await TDSFollows.SetRichPresences(info); -``` - -```java -Map info = new HashMap<>(); -info.put("score", "60"); -info.put("display", "#matching"); -TDSFollows.setRichPresence(info, new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFollows setRichPresencesWithDictionary:@{ - @"score" : @"60", - @"display" : @"#matching", -} callback:^(BOOL succeeded, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -You can **configure at most 20 rich presence fields on the Developer Center**. The key of each field should be no longer than 128 bytes and the value of each field should be no longer than 256 bytes. - -You can use the following interface to clear a rich presence field for the current player: - - - -```cs -TDSFollows.ClearRichPresence("score"); -``` - -```java -TDSFollows.clearRichPresence("score", new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFollows clearRichPresenceWithKey:@"score" -callback:^(BOOL succeeded, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -You can also clear multiple rich presence fields at once: - - - -```cs -IEnumerable keys = new string[] {"score", "display"} -await TDSFollows.ClearRichPresences(keys); -``` - -```java -List keys = new ArrayList<>(); -keys.add("score"); -keys.add("display"); -TDSFollows.clearRichPresence(keys, new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFollows clearRichPresencesWithKeys:@[@"score", @"display"] -callback:^(BOOL succeeded, NSError * _Nullable error) { - // Other things to do -}); -``` - - - -The interfaces for setting and clearing rich presence data can each be called at most once every 30 seconds. - -There are some [REST API interfaces](/sdk/friends/guide/#rich-presence-rest-api) related to rich presence. -You can write your own scripts to perform administrative operations on the server side by interacting with these interfaces. - -## Following - -A player can follow other players by entering their [friend codes](/sdk/friends/mutual/#friend-code). - - - -```cs -await TDSFollows.FollowByShortCode(shortId); -``` - -```java -TDSFollows.followByShortCode(shortId, new Callback() { - @Override - public void onSuccess(HandleResult result) { - // Followed - } - - @Override - public void onFail(TDSFriendError error) { - // Handle error - } -}); -``` - -```objc -[TDSFollows followWithShortCode:shortId callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - if (error) { - // handle error - } else { - // handle result - } -}]; -``` - - - -Additional properties can be specified when following other players. For example, to put the other player into the `coworkers` group: - - - -```cs -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFollows.FollowByShortCode(shortId); -``` - -```java -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFollows.followByShortCode(shortId, attrs, new Callback() { - // Other things to do -}); -``` - -```objc -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFollows followWithShortCode:shortId attributes:attributes callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -A player can also follow a `TDSUser` with its `objectId`. -For example, assuming Tarara’s `objectId` is `5b0b97cf06f4fd0abc0abe35`, the current player can follow Tarara with the following code: - - - -```cs -await TDSFollows.Follow("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFollows.follow("5b0b97cf06f4fd0abc0abe35", new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFollows followWithUserId:@"5b0b97cf06f4fd0abc0abe35" -callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -Additional properties can be specified as well when following other players with `objectId`: - - - -```cs -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFollows.Follow("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFollows.follow("5b0b97cf06f4fd0abc0abe35", attrs, new Callback() { - // Other things to do -}); -``` - -```objc -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFollows followWithUserId:@"5b0b97cf06f4fd0abc0abe35" attributes:attributes -callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -Notice that a player can **follow at most 5000** other players. - -## Unfollowing - -A player can unfollow other players with their `objectId`s or friend codes. -For example, after following Tarara, the current player changes their mind and doesn’t want to follow Tarara anymore: - - - -```cs -await TDSFollows.UnFollow("5b0b97cf06f4fd0abc0abe35"); - -await TDSFollows.UnFollowByShortCode(shortIdOfTarara); -``` - -```java -TDSFollows.unfollow("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(HandleResult result) { - // Unfollowed - } - - @Override - public void onFail(TDSFriendError error) { - // Handle error - } -}); - -TDSFollows.unfollowByShortCode(shortIdOfTarara, new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFollows unfollowWithUserId:@"5b0b97cf06f4fd0abc0abe35" callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; - -[TDSFollows unfollowWithShortCode:shortIdOfTarara, callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -## Blocklist - -A player can block other players so they won’t be able to follow the current player anymore. -By blocking a player, the existing connections between the current player and the target player will be removed. -Similar to following and unfollowing, the player can block other players with their `objectId`s or friend codes. -Assuming Tarara’s `objectId` is `5b0b97cf06f4fd0abc0abe35`, to block Tarara: - - - -```cs -await TDSFollows.Block("5b0b97cf06f4fd0abc0abe35"); - -await TDSFollows.BlockByShortCode(shortIdOfTarara); -``` - -```java -TDSFollows.block("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(HandleResult result) { - // Blocked - } - - @Override - public void onFail(TDSFriendError error) { - // Handle error - } -}); - -TDSFollows.blockByShortCode(shortIdOfTarara, new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFollows blockWithObjectId:@"5b0b97cf06f4fd0abc0abe35" callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; - -[TDSFollows blockWithShortCode:shortIdOfTarara callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -A player can remove a user from their blocklist at any time: - - - -```cs -await TDSFollows.Unblock("5b0b97cf06f4fd0abc0abe35"); - -await TDSFollows.UnblockByShortCode(shortIdOfTarara); -``` - -```java -TDSFollows.unblock("5b0b97cf06f4fd0abc0abe35", new Callback() { - // Other things to do -}); - -TDSFollows.unblockByShortCode(shortIdOfTarara, new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFollows unblockWithObjectId:@"5b0b97cf06f4fd0abc0abe35" callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; - -[TDSFollows unblockWithShortCode:shortIdOfTarara callback:^(TDSFriendHandleResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -If there have been any connections between the current player and a target player before the current player blocks the target player, **these connections will not be re-established automatically if the current player unblocks the target player**. - -To retrieve the current player’s blocklist: - - - -```cs -FriendResult result = await TDSFollows.QueryBlockList(cursor, limit, sortCondition); -``` - -```java -TDSFollows.queryBlockList(config, cursor, new Callback() { - // Other things to do -} -``` - -```objc -[TDSFollows queryBlockListWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - -See [Retrieving Mutual Follower List](#retrieving-mutual-follower-list) for the meanings of the query parameters. - - - -Notice that: - -- A player can block at most 100 other players. -- Once Player A blocks Player B, not only Player B cannot follow Player A, Player A cannot follow Player B as well. If Player A has followed Player B or Player B has followed Player A, once Player A blocks Player B, they won’t be following each other anymore. - -## Retrieving Mutual Follower List - -There is an interface for players to retrieve their mutual followers. -This interface returns not only the mutual follower list but also a cursor. -You can implement pagination by specifying the cursor and the number of players returned. -The players in the result can be sorted according to their online status (those who are online will show up at the beginning). - - - -```cs -// First query -string cursor = null; -// Defaults to 50; no larger than 500 -int limit = 50; -// Sort by online status -SortCondition sortCondition = SortCondition.OnlineCondition -FriendResult result = await TDSFollows.QueryMutualList(cursor, limit, sortCondition); - -ReadOnlyCollection friendInfos = result.FriendList; -foreach (TDSFriendInfo info in friendInfos) { - // Player data - TDSUser user = info.User; - // Rich presence data - Dictionary richPresence = info.RichPresence; - // Whether the player is online - bool online = info.Online; -} - -// Pagination -string cursor = result.Cursor; -FriendResult more = await TDSFollows.QueryMutualList(cursor, limit, sortCondition); -``` - -```java -FriendRequestConfig config = new FriendRequestConfig.Builder() - .pageSize(50) /* Defaults to 50; no larger than 500 */ - .sortCondition(SortCondition.getOnlineCondition()) /* Sort by online status */ - .build(); -// First query -TDSFollows.queryMutualList(config, null, new Callback() { - @Override - public void onSuccess(FriendResult result) { - List friendInfoList = result.getFriendList(); - for (TDSFriendInfo info : friendInfoList) { - // Player data - TDSUser user = info.getUser(); - // Rich presence data - TDSRichPresence richPresence = info.getRichPresence(); - // Whether the player is online - boolean online = info.isOnline(); - } - - // Pagination - String cursor = result.getCursor(); - TDSFollows.queryMutualList(config, cursor, new Callback() { - /* Other things to do */ - } - } - @Override - public void onFail(TDSFriendError error) { - toast("query error = " + error.code + " msg = " + error.detailMessage); - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.limit = 50;// Defaults to 50; no larger than 500 -__block NSString *cursor; // Cursor -TDSFriendQuerySortCondition *sort = [TDSFriendQuerySortCondition new]; -[sort append:TDSFriendQuerySortTypeOnline error:nil]; -option.sortCondition = sort; - -[TDSFollows queryMutualListWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - for (TDSFriendInfo *info in result.friendList) - // Player data - TDSUser *user = info.user; - // Rich presence data - NSDictionary *richPresence = info.richPresence; - // Whether the player is online - BOOL online = info.online; - } - cursor = result.cursor; -}]; - -// Pagination -option.from = cursor; -[TDSFollows queryMutualListWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -Notice that to make it possible for the server to sort players by online status, you have to call the corresponding interfaces when a player gets [online](#getting-the-player-online) and [offline](#getting-the-player-offline). -Otherwise, the server won’t be able to know the online statuses of the players and won’t be able to sort them by online status. - -## Retrieving Followees - -Similarly, a player can retrieve the list of players they are following. The interface for this function is similar to that for retrieving mutual followers and the players in the result can be sorted by online status as well: - - - -```cs -FriendResult result = await TDSFollows.QueryFolloweeList(cursor, limit, sortCondition); -``` - -```java -TDSFollows.queryFolloweeList(config, cursor, new Callback() { - // Other things to do -} -``` - -```objc -[TDSFollows queryFolloweeWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -## Retrieving Followers - -A player can also retrieve the list of players who are following them. The interface for this function is similar to those for retrieving mutual followers and followees, but **it doesn’t support sorting the players in the result by online status**: - - - -```cs -FriendResult result = await TDSFollows.QueryFollowerList(cursor, limit, sortCondition); -``` - -```java -TDSFollows.queryFollowerList(config, cursor, new Callback() { - // Other things to do -} -``` - -```objc -[TDSFollows queryFollowerWithOption:option -callback:^(TDSFriendResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -If you specified to sort the result by online status, **the cloud will ignore this condition** and return the unsorted result. - -## Link Sharing - -### Landing Page - -A landing page has to be deployed before you use link sharing. -The landing page can be deployed on [Cloud Engine](/sdk/engine/overview/) or any other server that can host a static page. -If you plan to use Cloud Engine, keep in mind that the free instances provided by Cloud Engine come with auto-hibernation. Please consider purchasing standard instances. - -We provide an [open-source demo landing page] for you to use. You can build, deploy, and use it with your own configurations. -Notice that the format of the `GAME_ANDROID_LINK` environment variable of the demo is `scheme://host/path`. -The values of `host` and `path` should be consistent with those written in the `AndroidManifest.xml` of your Android project. - -[repo]: https://github.com/taptap/TapFriends-landing-page - -For example, if the `AndroidManifest.xml` of your project contains the following configurations: - -```xml - - - - - - - - - - - -``` - -The value of `GAME_ANDROID_LINK` in your landing page should be `tapsdk://APP_ID/friends`. - -The address of the landing page should be configured in the client: - -```cs -TDSFollows.SetShareLink("https://please-replace-with-your-domain.example.com"); -``` - -If the landing page is hosted on Cloud Engine, the address will be `https://YOUR_CLOUD_ENGINE_CUSTOM_DOMAIN`. - -### Generating Invitation Links - -After the landing page has been deployed and the address has been configured in the client, you can call the following interface to generate invitation links: - - - -```cs -string inviteUrl = await TDSFollows.GenerateFriendInvitationLink(); -``` - -```java -TDSFollows.generateFriendInvitationLink(new Callback() { - @Override - public void onSuccess(String inviteUrl) { - System.out.println("share this link to invite your friends: " + inviteUrl); - } - - @Override - public void onFail(TDSFriendError error) { - System.out.println("Failed to generate invite link: " + error.detailMessage); - } -}); -``` - -```objc -NSError *error; -NSString *inviteUrl = [TDSFollows generateFriendInvitationLinkWithError:&error]; -``` - - - -The default username in the link will be the `nickname` of the player. Therefore, you might want to make sure that you have set the `nickname`s of the users in the built-in account system. -See [TDS Authentication Guide](/sdk/authentication/guide/#setting-other-user-properties) for more information. -To use other names, specify them when calling the above interface. -You can also provide other parameters that can be attached to the URL of the invitation link as query parameters. -For example, if the player named Tarara wants to use “Taro” as their name and attach a `ref=taptap` parameter, you can do this: - - - -```cs -Dictionary parameters = new Dictionary { - { "ref", "taptap" } -}; -string inviteUrl = await TDSFollows.GenerateFriendInvitationLink("Taro", parameters); -``` - -```java -Map parameters = new HashMap(); -parameters.put("ref", "taptap"); -TDSFollows.generateFriendInvitationLink("Taro", parameters, new Callback() { - // Other things to do -}); -``` - -```objc -NSError *error; -TDSFriendLinkOption *option = [TDSFriendLinkOption new]; -option.roleName = @"Taro"; -option.queries = @{ - @"ref" : @"taptap", -}; -NSString *inviteUrl = [TDSFollows generateFriendInvitationLinkWithOption:option error:&error]; -``` - - - -### Handling Invitation Links - -After the player opens the game with an invitation link, you need to call the following interface to have the player follow the target player. - - - -```cs -public class DeepLinkManager : MonoBehaviour -{ - // Other things to do - private async void onDeepLinkActivated(string url) { - await TDSFollows.HandleFriendInvitationLink(url); - } -} -``` - -```java -public void onHandleLink(View view) { - TDSFollows.handFriendInvitationLink(url, new Callback() { - @Override - public void onSuccess(HandleResult result) { - // Other things to do - } - - @Override - public void onFail(TDSFriendError error) { - // Other things to do - } - }); -} -``` - -```objc -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSFollows handleFriendInvitationLink:url - callback:^(TDSFriendHandleResult * _Nullable result, TDSFriendsLinkInfo * _Nullable linkInfo, NSError * _Nullable error) { - // Other things to do - }]; -} -``` - - - -You can also parse the link with the following interface provided by the SDK and get the player’s `objectId` and name as well as other parameters. You can perform your custom logic with them. - - - -```cs -public class DeepLinkManager : MonoBehaviour -{ - // Other things to do - private async void onDeepLinkActivated(string url) { - TDSFriendLinkInfo invitation = TDSFollows.ParseFriendInvitationLink(url); - string userObjectId = invitation.Identity; - string name = invitation.RoleName; - Dictionary parameters = invitation.Queries; - await TDSFollows.Follow(userObjectId); - } -} -``` - -```java -TDSFriendLinkInfo linkInfo = TDSFollows.parseFriendInvitationLink("url"); -String userObjectId = linkInfo.getIdentity(); -String name = linkInfo.getRoleName(); -Map parameters = linkInfo.getQueries(); -``` - -```objc -TDSFriendLinkInfo *linkInfo = [TDSFollows parseFriendInvitationLink:(NSURL *)url]; -NSString *userObjectId = linkInfo.identity; -NSString *name = linkInfo.roleName; -NSDictionary *parameters = linkInfo.queries; -``` - - - -Notice that: - -The demo project uses the Friend mode by default. Please change `INVITE_TYPE` to `follow` to switch to the Follow mode. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/guide.mdx deleted file mode 100644 index 0e96c323a..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/guide.mdx +++ /dev/null @@ -1,316 +0,0 @@ ---- -title: Friends Guide -sidebar_label: Guide -sidebar_position: 2 -slug: /sdk/friends/guide ---- - - - - - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -The Friends service comes with two models: - -- [Friend mode](/sdk/friends/mutual/) -- [Follow mode](/sdk/friends/follow/) - -Depending on the requirements of your game, you can pick one of the two models for your game. -Keep in mind that: - -- You can only choose one of the two models for your game, **but not both**. -- Once you have picked one model, **you cannot switch to the other one later**. - -Your game can also obtain players’ connections [from third-party platforms](/sdk/friends/third-party/). To use this feature, please submit a ticket to us to enable it. - -Here is a recommended path for you to learn how to use Friends in your game: - -- Learn about [TDS Authentication](/sdk/authentication/features/), the built-in TDS account system, which the Friends service depends on. The “players” and “users” mentioned later in this guide all refer to `TDSUser`s. - -- Follow this guide to learn about how to initialize the SDK. - -- Pick a model for your game depending on your requirements and read the corresponding guide: - - - [Friend mode](/sdk/friends/mutual/) - - [Follow mode](/sdk/friends/follow/) - -## Initializing the SDK - -With the initialization process mentioned in [Quickstart](/sdk/start/quickstart/#initialization) completed, proceed to the [Downloads](/tap-download) page and download the TapSDK, then add the `TapFriends` module: - - - - - {`"dependencies":{ - ... - "com.taptap.tds.friends": "https://github.com/TapTap/TapFriends-Unity.git#${sdkVersions.taptap.unity}", -}`} - - - - {`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapFriend_${sdkVersions.taptap.android}', ext:'aar') -}`} - - - - {`TapFriendResource.bundle -TapFriendSDK.framework -TapFriendUISDK.framework`} - - - - -To use rich presence in your game, please first enable **Real-time synchronization of rich information interface data** on **Developer Center > Game Services > Cloud Services > Friends > Settings**, -then set the rick presence fields needed through the REST API for [configuring rich presence fields](#configuring-rich-presence-fields). - -## Rich Presence REST API - -Below are the REST API interfaces related to rich presence. -You can write your own scripts to perform administrative operations on the server side by interacting with these interfaces. - -### Request Format - -For POST and PUT requests, the request body must be in JSON, and the Content-Type of the HTTP Header should be `application/json`. - -Requests are authenticated by the following key-value pairs in the HTTP Header: - -| Key | Value | Meaning | Source | -| ---------- | ------------ | ------------------------------------------- | ----------------------------------------- | -| `X-LC-Id` | `{{appid}}` | The `App Id` (`Client Id`) of your game | Can be obtained from the Developer Center | -| `X-LC-Key` | `{{appkey}}` | The `App Key` (`Client Token`) of your game | Can be obtained from the Developer Center | - -Using the management interface requires you to provide the `Master Key`: `X-LC-Key: {{masterkey}},master`. -`Master Key`, also called `Server Secret`, can be obtained from the Developer Center as well. - -See [Credentials](/sdk/storage/guide/setup-dotnet#credentials) for more information. - -### Base URL - -The Base URL for REST API requests (the `{{host}}` in the curl examples) is the custom API domain of your app. You can update or find it on the Developer Center. -See [Domain](/sdk/storage/guide/setup-dotnet#domain) for more details. - -### Retrieving Rich Presence Configurations - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: {{sessionToken}}" \ - https://{{host}}/friend/v1/rich-presence/config -``` - -Response: - -```json -{ - "clientId": "YOUR CLIENT ID", - "enabled": 1, - "richMsgEnabled": true, - "onlineMsgEnabled": true, - "webSocketUrl": "wss://XXX.ws.tds1.tapapis.cn/ws/leancloud/v1", - "richPresenceFields": [ - { - "key": "display", - "type": "token" - }, - { - "key": "leadboard", - "type": "token" - }, - { - "key": "inviteable", - "type": "variable" - }, - { - "key": "score", - "type": "variable" - }, - { - "key": "rank", - "type": "variable" - } - ], - "richPresenceMultiLang": [ - { - "key": "display", - "lang": "en_US", - "content": { - "#playing": "Playing", - "#idle": "Idle", - "#room": "Room", - "#matching": "Matching" - } - }, - { - "key": "display", - "lang": "zh_CN", - "content": { - "#playing": "游戏中", - "#idle": "在线", - "#room": "准备中", - "#matching": "组队中" - } - }, - { - "key": "leadboard", - "lang": "en_US", - "content": { - "#score": "%score% score", - "#rank": "%rank% rank" - } - }, - { - "key": "leadboard", - "lang": "zh_CN", - "content": { - "#score": "%score% 分,排名为 %rank%", - "#rank": "%rank% 名" - } - } - ] -} -``` - -### Configuring Rich Presence Fields - -Rich presence fields can be set through the REST API. The names and types of each field needs to be provided. -This is a management interface, so the `Master Key` is required for authentication: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "enableRichMsg":true, - "enableOnlineMsg":true, - "richPresenceFields":[ - {"key":"display","type":"token"}, - {"key":"leadboard","type":"token"}, - {"key":"inviteable","type":"variable"}, - {"key":"score","type":"variable"}, - {"key":"rank","type":"variable"} - ] - }' \ - https://{{host}}/friend/v2/rich-presence/config/base-info -``` - -`enableRichMsg` and `enableOnlineMsg` are used to configure whether to enable notifications for rich presence updates and whether to enable notifications for friends’ online status updates. We recommend that you set both to be `true`. -Notice that **the version in the URL for this interface is `v2`**, which is different from other interfaces that use `v1`. - -Response: - -```json -{ - "appId": "YOUR CLIENT ID" -} -``` - -Response when there is an error: - -```json -{ - "code": 400, - "error": "Missing request header" -} -``` - -### Configuring Multi-Language Contents - -The following interface can be used to configure multi-language contents of rich presence fields. -This is a management interface, so the `Master Key` is required for authentication: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "action":"save", - "config":[ - { - "key": "display", - "lang": "en_US", - "content": { - "#playing": "Playing", - "#idle": "Idle", - "#room": "Room", - "#matching": "Matching" - } - }, - { - "key": "display", - "lang": "zh_CN", - "content": { - "#playing": "游戏中", - "#idle": "在线", - "#room": "准备中", - "#matching": "组队中" - } - }, - { - "key": "leadboard", - "lang": "en_US", - "content": { - "#score": "%score% score", - "#rank": "%rank% rank best" - } - }, - { - "key": "leadboard", - "lang": "zh_CN", - "content": { - "#score": "%score% 分,竞技排名为 %rank%", - "#rank": "%rank% 名" - } - } - ] - }' - https://{{host}}/friend/v1/rich-presence/config/lang-info -``` - -The response will be the same as the one for [configuring rich presence fields](#configuring-rich-presence-fields). - -### Retrieving Players’ Rich Presence Information - -You can retrieve the rich presence information of multiple players at once (provide a comma-separated `objectId` list): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: {{sessionToken}}" \ - -G --data-urlencode 'ids={userObjectId,anotherUserObjectId}' - https://{{host}}/friend/v1/rich-presence/users -``` - -Response - -```json -{ - "results": [ - { - "online": false, - "richPresence": { - "score": "15", - "leadboard": "15 points; ranked 150th", - "rank": "150" - } - }, - { - "online": false, - "richPresence": {} - } - ] -} -``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/mutual.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/mutual.mdx deleted file mode 100644 index 56b156102..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/mutual.mdx +++ /dev/null @@ -1,1380 +0,0 @@ ---- -title: Friend Mode -sidebar_position: 3 -slug: /sdk/friends/mutual ---- - - - - - -import MultiLang from "/src/docComponents/MultiLang"; - -Before continuing, make sure you have [finished initializing the SDK](/sdk/friends/guide/). - -## Responding to Friend Status Changes - -With the Friends module, the client can listen to the status changes of the player’s friends and display them to the player in real-time. -You’ll need to register an instance for listening to friend status changes before calling the interface for getting the player online. By doing this, the player will be able to receive notifications after they get online: - - - -```cs -TDSFriends.FriendStatusChangedDelegate = new TDSFriendStatusChangedDelegate { - // New friend added (triggered together with “Sent friend request accepted”) - OnFriendAdd = friendInfo => {}, - // New friend request received - OnNewRequestComing = req => {}, - // Sent friend request accepted - OnRequestAccepted = req => {}, - // Sent friend request declined - OnRequestDeclined = req => {}, - // Friend got online - OnFriendOnline = userId => {}, - // Friend got offline - OnFriendOffline = userId => {}, - // Friend’s rich presence information changed - OnRichPresenceChanged = (userId, richPresence) => {}, - // The current player got online (connection established) - OnConnected = () => {}, - // Connection lost; the SDK will try to reconnect automatically - OnDisconnected = () => {}, - // Connection error - OnConnectionError = (code, message) => {}, -}; -``` - -```java -TDSFriends.registerFriendStatusChangedListener(new FriendStatusChangedListener() { - // New friend added (triggered together with “Sent friend request accepted”) - @Override - public void onFriendAdd(TDSFriendInfo friendInfo) {} - - // New friend request received - @Override - public void onNewRequestComing(TDSFriendshipRequest request) {} - - // This callback will be triggered if the player entered the game with an invitation link - // You can call handFriendInvitationLink within this callback - // You can also parse the link with parseFriendInvitationLink, get the relevant parameters, and then perform your custom logic - @Override - public void onReceivedInvitationLink(String url) {} - - // Sent friend request accepted - @Override - public void onRequestAccepted(TDSFriendshipRequest request) {} - - // Sent friend request declined - @Override - public void onRequestDeclined(TDSFriendshipRequest request) {} - - // Friend got online - @Override - public void onFriendOnline(String userId) {} - - // Friend got offline - @Override - public void onFriendOffline(String userId) {} - - // Friend’s rich presence information changed - @Override - public void onRichPresenceChanged(String userId, TDSRichPresence richPresence) {} - - // The current player got online (connection established) - @Override - public void onConnected() {} - - // Connection lost; the SDK will try to reconnect automatically - @Override - public void onDisconnected() {} - - // Connection error - @Override - public void onConnectError(int code, String msg){}); -} -``` - -```objc -[TDSFriends registerNotificationDelegate:self]; - -// New friend added (triggered together with “Sent friend request accepted”) -- (void)onFriendAdd:(TDSFriendInfo *)info {} - -// New friend request received -- (void)onNewRequestComing:(TDSFriendshipRequest *)request {} - -// Sent friend request accepted -- (void)onRequestAccepted:(TDSFriendshipRequest *)request {} - -// Sent friend request declined -- (void)onRequestDeclined:(TDSFriendshipRequest *)request {} - -// Friend got online -- (void)onFriendOnline:(NSString *)userId {} - -// Friend got offline -- (void)onFriendOffline:(NSString *)userId {} - -// Friend’s rich presence information changed -- (void)onRichPresenceChanged:(NSString *)userId dictionary:(NSDictionary * _Nullable)dictionary {} - -// The current player got online (connection established) -- (void)onConnected {} - -// Connection lost; the SDK will try to reconnect automatically -- (void)onDisconnected {} - -// Connection error -- (void)onDisconnectedWithError:(NSError * _Nullable)error {} -``` - - - -The “friends” appearing in the events above refer to the friends under the Friend mode. -The SDK doesn’t support listening to the events under the Follow mode. - -To stop listening: - - - -```cs -TDSFriends.FriendStatusChangedDelegate = null; -``` - -```java -TDSFriends.removeFriendStatusChangedListener(); -``` - -```objc -[TDSFriends unregisterNotificationDelegate]; -``` - - - -## Getting the Player Online - -After the player logs in, you need to call this interface to establish a persistent connection between the client and the cloud. -Once the persistent connection is established, if there is an interruption to the internet connection, the SDK will automatically reconnect once the connection is restored. - - -<> - -```cs -await TDSFriends.Online(); -``` - - -<> - -```java -TDSFriends.online(new Callback() { - @Override - public void onSuccess(Void result) { - // Success - } - - @Override - public void onFail(TDSFriendError error) { - // Handle error - } -}); -``` - -With the persistent connection established, if the player opens an invitation link, the Android SDK will automatically send a friend request to the corresponding player. - - -<> - -```objc -[TDSFriends online]; -``` - - - - -## Getting the Player Offline - -After the player logs out, you need to call this interface to disconnect the client from the cloud. - - - -```cs -await TDSFriends.Offline(); -``` - -```java -TDSFriends.offline(); -``` - -```objc -[TDSFriends offline]; -``` - - - -## Searching for Friends by Nickname - -A player can search for friends by nickname without knowing their `objectId`s. -For example, to search for friends with `Tarara` as their nickname: - - - -```cs -ReadOnlyCollection friendInfos = await TDSFriends.SearchUserByName("Tarara"); -foreach (TDSFriendInfo info in friendInfos) { - // Player data - TDSUser user = info.User; - // Rich presence data; continue reading for more information - Dictionary richPresence = RichPresence; - // Whether the friend is online - bool online = info.Online; -} -``` - -```java -TDSFriends.searchUserByName("Tarara", new ListCallback() { - @Override - public void onSuccess(List friendInfoList) { - for (TDSFriendInfo info : friendInfoList) { - // Player data - TDSUser user = info.getUser(); - // Rich presence data; continue reading for more information - TDSRichPresence richPresence = info.getRichPresence(); - // Whether the friend is online - boolean online = info.isOnline(); - } - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed search friend by nickname" + error.detailMessage); - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends searchUserWithNickname:@"Tarara" option:option -callback:^(NSArray * _Nullable friendInfos, NSError * _Nullable error) { - if (friendInfos) { - for (TDSFriendInfo *info in friendInfos) { - // Player data - TDSUser *user = info.user; - // Rich presence data; continue reading for more information - NSDictionary *richPresence = info.richPresence; - // Whether the friend is online - BOOL online = info.online; - } - } else if (error) { - // Handle error - } -}]; -``` - - - -Notice that **in order to use this function, the `nickname` field has to be set on the built-in account system**. -See [TDS Authentication Guide](/sdk/authentication/guide/#setting-other-user-properties) for more information. - -## Friend Code - -Each logged-in player has a friend code that can be shared with other players so these players can quickly add the current player as their friend. - -You can get the friend code of a `TDSUser` from its `shortId` field: - - - -```cs -// currentUser is a logged in TDSUser -string shortId = currentUser["shortId"]; -``` - -```java -String shortId = currentUser.getString("shortId"); -``` - -```objc -NSString *shortId = currentUser[@"shortId"]; -``` - - - -To search for a player with their friend code: - - - -```cs -TDSFriendInfo friendInfo = await TDSFriends.SearchUserByShortCode(shortId); -``` - -```java -TDSFriends.searchUserByShortCode(shortId, new Callback() { - @Override - public void onSuccess(TDSFriendInfo friendInfo) { /* See the previous section */ } - - @Override - public void onFail(TDSFriendError error) { /* See the previous section */ } -}); -``` - -```objc -[TDSFriends searchUserWithShortCode:shortId -callback:^(TDSFriendInfo * _Nullable friendInfo, NSError * _Nullable error) { - // See the previous section -}]; -``` - - - -## Searching for Friends With `objectId` - -Besides using nicknames and friend codes, a player can also search for friends with their `objectId`s. - -For example, to search for the friend with `5b0b97cf06f4fd0abc0abe35` as their `objectId`: - - - -```cs -TDSFriendInfo friendInfo = await TDSFriends.SearchUserById("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFriends.searchUserById("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(TDSFriendInfo tdsFriendInfo) { - /* See the previous section */ - } - @Override - public void onFail(TDSFriendError tdsFriendError) { - /* See the previous section */ - } -}); -``` - -```objc -[TDSFriends searchUserWithObjectId:@"5b0b97cf06f4fd0abc0abe35" -callback:^(TDSFriendInfo * _Nullable friendInfo, NSError * _Nullable error) { - // See the previous section -}]; -``` - - - -## Rich Presence - -Rich presence can be used to display the player’s status information like their online status, current hero, and current game mode. - -After adding configurations for rich presence on the Developer Center, you can set the content of a player’s rich presence according to the configured fields for rich presence: - - - -```cs -await TDSFriends.SetRichPresence("score", "60"); -``` - -```java -TDSFriends.setRichPresence("score", "60", new Callback() { - @Override - public void onSuccess(Void result) { - toast("Succeed to set rich presence."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to set rich presence: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFriends setRichPresenceWithKey:@"score" value:@"60" - callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Succeed to set rich presence. - } else if (error) { - // Failed to set rich presence. - } -}]; -``` - - - -Here `score` is a rich presence field configured on the Developer Center. -There are two types available for each rich presence field: - -- `variable`: The value is a string. In the code example above, with `score` configured to be a `variable` on the Developer Center, the client sets the value of this field to be `60` when updating the rich presence data. The cloud will accordingly return `"score": "60"` to the client as the rich presence data. You need to handle the localization-related logic yourself so that the player can eventually see something like “Score: 60”. - -- `token`: The value is a string starting with `#`. In the example below, the type of `display` is `token`, and the client sets the value of this field to be `#matching` when updating the rich presence data. The cloud will handle the conversion of this value to a localized string and return the result like `"display": "Matching"` to the client. Notice that if the cloud fails to convert the value, an empty string like `"display": " "` will be returned. - -To set multiple fields at once: - - - -```cs -Dictionary info = new Dictionary(); -info.Add("score", "60"); -info.Add("display", "#matching"); -await TDSFriends.SetRichPresences(info); -``` - -```java -Map info = new HashMap<>(); -info.put("score", "60"); -info.put("display", "#matching"); -TDSFriends.setRichPresence(info, new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFriends setRichPresencesWithDictionary:@{ - @"score" : @"60", - @"display" : @"#matching", -} callback:^(BOOL succeeded, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -You can **configure at most 20 rich presence fields on the Developer Center**. The key of each field should be no longer than 128 bytes and the value of each field should be no longer than 256 bytes. - -You can use the following interface to clear a rich presence field for the current player: - - - -```cs -TDSFriends.ClearRichPresence("score"); -``` - -```java -TDSFriends.clearRichPresence("score", new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFriends clearRichPresenceWithKey:@"score" -callback:^(BOOL succeeded, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -You can also clear multiple rich presence fields at once: - - - -```cs -IEnumerable keys = new string[] {"score", "display"} -await TDSFriends.ClearRichPresences(keys); -``` - -```java -List keys = new ArrayList<>(); -keys.add("score"); -keys.add("display"); -TDSFriends.clearRichPresence(keys, new Callback() { - // Other things to do -}); -``` - -```objc -[TDSFriends clearRichPresencesWithKeys:@[@"score", @"display"] -callback:^(BOOL succeeded, NSError * _Nullable error) { - // Other things to do -}); -``` - - - -The interfaces for setting and clearing rich presence data can each be called at most once every 30 seconds. - -There are some [REST API interfaces](/sdk/friends/guide/#rich-presence-rest-api) related to rich presence. -You can write your own scripts to perform administrative operations on the server side by interacting with these interfaces. - -## Adding Friends - -A player can add friends by entering their [friend codes](/sdk/friends/mutual/#friend-code). - - - -```cs -await TDSFriends.AddFriendByShortCode(shortId); -``` - -```java -TDSFriends.addFriendByShortCode(shortId, null, new Callback() { - @Override - public void onSuccess(Void result) { - toast("Applied or added."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to add a friend: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFriends addFriendWithShortCode:shortId callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Applied or added. - } else if (error) { - // Failed to add a friend. - } -}]; -``` - - - -If the current player is already on the friend list of the other player, they will immediately become friends. -Otherwise, a friend request will be sent to the other player. - -Additional properties can be specified when adding friends. For example, to put the other player into the `coworkers` group: - - - -```cs -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFriends.AddFriendByShortCode(shortId, attrs); -``` - -```java -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFriends.addFriendByShortCode(shortId, attrs, new Callback() { - // See the example above -}); -``` - -```objc -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFriends addFriendWithShortCode:shortId attributes:attributes -callback:^(BOOL succeeded, NSError * _Nullable error) { - // See the example above -}]; -``` - - - -A player can also add a `TDSUser` as their friend with its `objectId`. -For example, assuming Tarara’s `objectId` is `5b0b97cf06f4fd0abc0abe35`, the current player can add Tarara as their friend with the following code: - - - -```cs -await TDSFriends.AddFriend("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFriends.addFriend("5b0b97cf06f4fd0abc0abe35", new Callback() { - // See the example above -}); -``` - -```objc -[TDSFriends addFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" -callback:^(BOOL succeeded, NSError * _Nullable error) { - // See the example above -}]; -``` - - - -Additional properties can be specified as well when adding friends with `objectId`: - - - -```cs -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFriends.AddFriend("5b0b97cf06f4fd0abc0abe35", attrs); -``` - -```java -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFriends.addFriend("5b0b97cf06f4fd0abc0abe35", attrs, new Callback() { - // See the example above -}); -``` - -```objc -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFriends addFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" attributes:attributes callback:^(BOOL succeeded, NSError * _Nullable error) { - // See the example above -}]; -``` - - - -## Deleting Friends - -A player can delete their existing friends. -For example, after becoming friends with Tarara, the current player changes their mind and doesn’t want to be friends with Tarara anymore: - - - -```cs -await TDSFriends.DeleteFriend("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFriends.deleteFriend("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(Boolean ok) { - toast("Deleted."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to delete a friend: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFriends deleteFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Deleted. - } else if (error) { - // Failed to delete a friend. - } -}]; -``` - - - -## Blocklist - -### Blocking Users - -A player can block a user no matter if they’re friends or not. Once a player blocks a user, the ongoing friend requests between them will be deleted, and they won’t be able to send friend requests to each other anymore. Blocked users won’t appear in search results when the player searches for friends. A player can have at most 100 users in their blocklist. - -Assuming Tarara’s `objectId` is `5b0b97cf06f4fd0abc0abe35`, to block Tarara: - - -<> - -```cs -await TDSFriends.BlockFriend("5b0b97cf06f4fd0abc0abe35"); -``` - - -<> - -```java -TDSFriends.blockFriend("5b0b97cf06f4fd0abc0abe35", new Callback(){ - @Override - public void onSuccess(Void result) { - // block user succeed. - } - @Override - public void onFail(TDSFriendError error) { - // Failed to block. - } -}); -``` - - -<> - -```objc -[TDSFriends blockFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // block user succeed. - } else if (error) { - // Failed to block. - } -}]; -``` - - - - - -### Unblocking Users - -A player can remove a user from their blocklist at any time. If the user used to be a friend of the current player, the user will be added back to the friend list of the current player. - - -<> - -```cs -await TDSFriends.UnblockFriend("5b0b97cf06f4fd0abc0abe35"); -``` - - -<> - -```java -TDSFriends.unblockFriend("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(Void result) { - // unblock succeed. - } - - @Override - public void onFail(TDSFriendError error) { - // Failed to unblock. - } -}); -``` - - -<> - -```objc -[TDSFriends unblockFriendWithUserId:@"5b0b97cf06f4fd0abc0abe35" callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // unblock succeed. - } else if (error) { - // Failed to unblock. - } -}]; -``` - - - - - -### Retrieving the Blocklist - -Use the code below to retrieve the current player’s blocklist with pagination. - - -<> - -```cs -var from = 0; -var limit = 100; -ReadOnlyCollection friendInfos = await TDSFriends.QueryBlockList(from, limit); -foreach (TDSFriendInfo info in friendInfos) { - // Player data - TDSUser user = info.User; - // Rich presence data - Dictionary richPresence = info.RichPresence; - // Whether the friend is online - bool online = info.Online; -} -``` - -In the code above: - -- `from` is the query’s offset. It will be `0` for the first page. For the next page, it will be the number of items retrieved from the last query. -- `limit` is the number of items in each page. - - -<> - -```java -TDSFriends.queryBlockList(0, 10, new ListCallback() { - @Override - public void onSuccess(List result) { - System.out.println("query blockList data, data = " + result); - } - - @Override - public void onFail(TDSFriendError error) { - System.out.println("query blockList failed, error = " + error); - } -}); -``` - -In the code above: - -- `from` is the query’s offset. It will be `0` for the first page. For the next page, it will be the number of items retrieved from the last query. -- `limit` is the number of items in each page. -- `callback` is the callback for handling the result asynchronously. The result contains the users in the blocklist. - - -<> - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends queryBlockListWithOption:option - callback:^(NSArray * _Nullable friendInfos, NSError * _Nullable error) { - if (friendInfos) { - for (TDSFriendInfo *info in friendInfos) { - // Player data - TDSUser *user = info.user; - // Rich presence data - NSDictionary *richPresence = info.richPresence; - // Whether the friend is online - BOOL online = info.online; - } - } else if (error) { - // Handle error - } -}]; -``` - -In the code above: - -- `option` is the conditions for the query, including `from` for the offset and `limit` for the number of items. -- `callback` is the callback for handling the result asynchronously. - - - - - -## Retrieving Friend Requests - -There are three possible statuses for each friend request: - -- `pending`: The target user hasn’t responded yet. This is the default status once a friend request has been created. -- `accepted`: The target user accepted the request and they are already friends with the current player. -- `declined`: The target user declined the request. - -The SDK offers an interface for retrieving friend requests. -For example, to retrieve the first 20 requests with their status being `pending`: - - -<> - -```cs -var from = 0; -var limit = 100; -ReadOnlyCollection requests = await TDSFriends.QueryFriendRequestList ( - LCFriendshipRequest.STATUS_PENDING, from, limit -); -``` - -`LCFriendshipRequest.STATUS_PENDING` means the status of the friend request is `pending`. -Similarly, `LCFriendshipRequest.STATUS_ACCEPTED` means `accepted` and `LCFriendshipRequest.STATUS_DECLINED` means `declined`. -`LCFriendshipRequest.STATUS_ANY` means any status. - - -<> - -```java -int from = 0; -int limit = 100; -TDSFriends.queryFriendRequestList(LCFriendshipRequest.STATUS_PENDING, from, limit, - new ListCallback(){ - - @Override - public void onSuccess(List requests) { - // requests is the list of pending friend requests - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to query friendship requests: " + error.detailMessage); - } -}); -``` - -In the example above, `LCFriendshipRequest.STATUS_PENDING` means the status of the friend request is `pending`. -Similarly, `LCFriendshipRequest.STATUS_ACCEPTED` means `accepted` and `LCFriendshipRequest.STATUS_DECLINED` means `declined`. -`LCFriendshipRequest.STATUS_ANY` means any status. - - -<> - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends queryFriendRequestWithStatus:TDSUserFriendshipRequestStatusPending - option:option - callback:^(NSArray * _Nullable requests, NSError * _Nullable error) { - // requests is the list of pending friend requests -}]; -``` - -In the example above, `TDSUserFriendshipRequestStatusPending` means the status of the friend request is `pending`. -Similarly, `TDSUserFriendshipRequestStatusAccepted` means `accepted` and `TDSUserFriendshipRequestStatusDeclined` means `declined`. -`TDSUserFriendshipRequestStatusAny` means any status. - - - - - -Use the following interface to include the rich presence data of the initiator of each request in the result: - - - -```cs -ReadOnlyCollection requests = await TDSFriends.QueryFriendRequestWithFriendStateList ( - LCFriendshipRequest.STATUS_PENDING, from, limit -); -foreach (TDSFriendshipRequest request in requests) { - // Friend request (see QueryFriendRequestList) - LCFriendshipRequest req = request.FriendshipRequest; - // The rich presence data of the initiator - TDSFriendInfo info = request.FriendInfo; -} -``` - -```java -TDSFriends.queryFriendRequestWithFriendStateList(LCFriendshipRequest.STATUS_PENDING, - from, limit, new ListCallback() { - @Override - public void onSuccess(List requests) { - for (TDSFriendshipRequest request : requests) { - // Friend request (see queryFriendRequestList) - LCFriendshipRequest req = request.getLcFriendshipRequest(); - // The rich presence data of the initiator - TDSFriendInfo info = request.getFriendInfo(); - } - } - - @Override - public void onFail(TDSFriendError error) { - // Handle error - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends queryFriendRequestAndStateWithStatus:TDSUserFriendshipRequestStatusPending - option:option - callback:^(NSArray * _Nullable requests, NSError * _Nullable error) { - for (TDSFriendshipRequest *request in requests) { - // Friend request (see queryFriendRequestWithStatus) - LCFriendshipRequest *req = request.lcFriendshipRequest; - // The rich presence data of the initiator - TDSFriendInfo *info = request.friendInfo; - } -}]; -``` - - - -## Handling Friend Requests - -For each new friend request, the player can accept or decline it. They can also ignore the request or even delete it. - - - -```cs -// LCFriendshipRequest request - -// Accept -await TDSFriends.AcceptFriendshipRequest(request); -// Accept and add additional attributes -Dictionary attrs = new Dictionary { - { "group", "coworkers" } -}; -await TDSFriends.AcceptFriendshipRequest(request, attrs); - -// Decline -await TDSFriends.DeclineFriendshipRequest(request); -// Delete -await request.Delete(); -``` - -```java -// LCFriendshipRequest request - -// Accept -TDSFriends.acceptFriendRequest(request, new Callback() { - @Override - public void onSuccess(Void result) { - toast("Accepted."); - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to delete a friend: " + error.detailMessage); - } -}); -// Accept and add additional attributes -Map attrs = new HashMap(); -attrs.put("group", "coworkers"); -TDSFriends.acceptFriendRequest(request, attrs, new Callback() { - // Other things to do -}); - -// Decline -TDSFriends.declineFriendRequest(request, new Callback() { - // Other things to do -}); -// Delete -TDSFriends.deleteFriendRequest(request, new Callback() { - // Other things to do -}); -``` - -```objc -// LCFriendshipRequest request - -// Accept -[TDSFriends acceptFriendRequest:request attributes:nil -callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // Accepted. - } else if (error) { - // Failed to accept a friend request. - } -}]; -// Accept and add additional attributes -NSDictionary *attributes = @{ - @"group" : @"coworkers", -}; -[TDSFriends acceptFriendRequest:request attributes:attributes -callback:^(BOOL succeeded, NSError * _Nullable error) { - // Other things to do -}]; - -// Decline -[TDSFriends declineFriendRequest:request -callback:^(BOOL succeeded, NSError * _Nullable error) { - // Other things to do -}]; - -[TDSFriends deleteFriendRequest:request -callback:^(BOOL succeeded, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -Note: - -1. If a user declines a friend request from the current player, the player will get an error when they try to send another request to the same user. -2. If a user deletes a friend request from the current user, the player will be able to send another request to the same user. - -## Retrieving Friend List - -A player can retrieve their own friend list. When performing this operation, a limit and offset can be provided: - - - -```cs -var from = 0; -var limit = 100; -ReadOnlyCollection friendInfos = await TDSFriends.QueryFriendList(from, limit); -foreach (TDSFriendInfo info in friendInfos) { - // Player data - TDSUser user = info.User; - // Rich presence data - Dictionary richPresence = info.RichPresence; - // Whether the friend is online - bool online = info.Online; -} -``` - -```java -int from = 0; -int limit = 100; -TDSFriends.queryFriendList(from, limit, - new ListCallback(){ - - @Override - public void onSuccess(List friendInfoList) { - for (TDSFriendInfo info : friendInfoList) { - // Player data - TDSUser user = info.getUser(); - // Rich presence data - TDSRichPresence richPresence = info.getRichPresence(); - // Whether the friend is online - boolean online = info.isOnline(); - } - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to query friend list" + error.detailMessage); - } -}); -``` - -```objc -TDSFriendQueryOption *option = [TDSFriendQueryOption new]; -option.from = 0; -option.limit = 100; -[TDSFriends queryFriendWithOption:option - callback:^(NSArray * _Nullable friendInfos, NSError * _Nullable error) { - if (friendInfos) { - for (TDSFriendInfo *info in friendInfos) { - // Player data - TDSUser *user = info.user; - // Rich presence data - NSDictionary *richPresence = info.richPresence; - // Whether the friend is online - BOOL online = info.online; - } - } else if (error) { - // Handle error - } -}]; -``` - - - -## Check if a User Is a Friend - -You can check if a `TDSUser` is a friend of the current player with its `objectId`. -For example, assuming Tarara’s `objectId` is `5b0b97cf06f4fd0abc0abe35`: - - - -```cs -bool isFriend = await TDSFriends.CheckFriendship("5b0b97cf06f4fd0abc0abe35"); -``` - -```java -TDSFriends.checkFriendship("5b0b97cf06f4fd0abc0abe35", new Callback() { - @Override - public void onSuccess(Boolean isFriend) { - if (isFriend) { - toast("Tarara is my friend."); - } else { - toast("Tarara is not my friend."); - } - } - - @Override - public void onFail(TDSFriendError error) { - toast("Failed to query friendship: " + error.detailMessage); - } -}); -``` - -```objc -[TDSFriends checkFriendshipWithUserId:@"5b0b97cf06f4fd0abc0abe35" - callback:^(NSNumber * _Nullable isFriend, NSError * _Nullable error) { - if (error) { - // Handle error - } - if (isFriend.boolValue) { - NSLog(@"Tarara is my friend."); - } else { - NSLog(@"Tarara is not my friend."); - } -}]; -``` - - - -## Link Sharing - -### Landing Page - -A landing page has to be deployed before you use link sharing. -The landing page can be deployed on [Cloud Engine](/sdk/engine/overview/) or any other server that can host a static page. -If you plan to use Cloud Engine, keep in mind that the free instances provided by Cloud Engine come with auto-hibernation. Please consider purchasing standard instances. - -We provide an [open-source demo landing page] for you to use. You can build, deploy, and use it with your own configurations. -Notice that the format of the `GAME_ANDROID_LINK` environment variable of the demo is `scheme://host/path`. -The values of `host` and `path` should be consistent with those written in the `AndroidManifest.xml` of your Android project. - -[repo]: https://github.com/taptap/TapFriends-landing-page - -For example, if the `AndroidManifest.xml` of your project contains the following configurations: - -```xml - - - - - - - - - - - -``` - -The value of `GAME_ANDROID_LINK` in your landing page should be `tapsdk://APP_ID/friends`. - -The address of the landing page should be configured in the client: - - - -```cs -TDSFriends.SetShareLink("https://please-replace-with-your-domain.example.com"); -``` - -```java -TDSFriends.setShareLink("https://please-replace-with-your-domain.example.com"); -``` - -```objc -[TDSFriends setShareLink:@"https://please-replace-with-your-domain.example.com"]; -``` - - - -If the landing page is hosted on Cloud Engine, the address will be `https://YOUR_CLOUD_ENGINE_CUSTOM_DOMAIN`. - -### Generating Invitation Links - -After the landing page has been deployed and the address has been configured in the client, you can call the following interface to generate invitation links: - - - -```cs -string inviteUrl = await TDSFriends.GenerateFriendInvitationLink(); -``` - -```java -TDSFriends.generateFriendInvitationLink(new Callback() { - @Override - public void onSuccess(String inviteUrl) { - System.out.println("share this link to invite your friends: " + inviteUrl); - } - - @Override - public void onFail(TDSFriendError error) { - System.out.println("Failed to generate invite link: " + error.detailMessage); - } -}); -``` - -```objc -NSError *error; -NSString *inviteUrl = [TDSFriends generateFriendInvitationLinkWithError:&error]; -``` - - - -The default username in the link will be the `nickname` of the player. -Therefore, you might want to make sure that you have set the `nickname`s of the users in the built-in account system. -See [TDS Authentication Guide](/sdk/authentication/guide/#setting-other-user-properties) for more information. -To use other names, specify them when calling the above interface. -You can also provide other parameters that can be attached to the URL of the invitation link as query parameters. -For example, if the player named Tarara wants to use “Taro” as their name and attach a `ref=taptap` parameter, you can do this: - - - -```cs -Dictionary parameters = new Dictionary { - { "ref", "taptap" } -}; -string inviteUrl = await TDSFriends.GenerateFriendInvitationLink("Taro", parameters); -``` - -```java -Map parameters = new HashMap(); -parameters.put("ref", "taptap"); -TDSFriends.generateFriendInvitationLink("Taro", parameters, new Callback() { - // Other things to do -}); -``` - -```objc -NSError *error; -TDSFriendLinkOption *option = [TDSFriendLinkOption new]; -option.roleName = @"Taro"; -option.queries = @{ - @"ref" : @"taptap", -}; -NSString *inviteUrl = [TDSFriends generateFriendInvitationLinkWithOption:option error:&error]; -``` - - - -### Handling Invitation Links - -After the player opens the game with an invitation link, you need to call the following interface. -The SDK will automatically send a friend request to the corresponding player. - - - -```cs -public class DeepLinkManager : MonoBehaviour -{ - // Other logic - private async void onDeepLinkActivated(string url) { - await TDSFriends.HandleFriendInvitationLink(url); - } -} -``` - -```java -public class FriendsActivity extends AppCompatActivity { -// Other logic - public void onHandleLink(View view) { - TDSFriends.handFriendInvitationLink(url, new Callback() { - @Override - public void onSuccess(Void result) { - // Other things to do - } - - @Override - public void onFail(TDSFriendError error) { - // Other things to do - } - }); - } -} -``` - -```objc -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TDSFriends handleFriendInvitationLink:url - callback:^(BOOL succeeded, TDSFriendsLinkInfo * _Nullable linkInfo, NSError * _Nullable error) { - if (error) { - // handle error - } - }]; -} -``` - - - -You can also parse the link with the following interface provided by the SDK and get the player’s `objectId` and name as well as other parameters. You can perform your custom logic with them. - - - -```cs -public class DeepLinkManager : MonoBehaviour -{ - // Other logic - private async void onDeepLinkActivated(string url) { - TDSFriendLinkInfo invitation = TDSFriends.ParseFriendInvitationLink(url); - string userObjectId = invitation.Identity; - string name = invitation.RoleName; - Dictionary parameters = invitation.Queries; - await TDSFriends.Follow(userObjectId); - } -} -``` - -```java -TDSFriendLinkInfo linkInfo = TDSFriends.parseFriendInvitationLink("url"); -String userObjectId = linkInfo.getIdentity(); -String name = linkInfo.getRoleName(); -Map parameters = linkInfo.getQueries(); -``` - -```objc -TDSFriendLinkInfo *linkInfo = [TDSFriends parseFriendInvitationLink:(NSURL *)url]; -NSString *userObjectId = linkInfo.identity; -NSString *name = linkInfo.roleName; -NSDictionary *parameters = linkInfo.queries; -``` - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/third-party.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/third-party.mdx deleted file mode 100644 index f5c4a8066..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/friends/third-party.mdx +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: Friends on Third-Party Platforms -sidebar_label: Friends on Third-Party Platforms -sidebar_position: 5 -slug: /sdk/friends/third-party ---- - - - - - -import { Conditional } from "/src/docComponents/conditional"; -import MultiLang from "/src/docComponents/MultiLang"; - -Before continuing, please familiarize yourself with [the general interfaces of the Friends module](/sdk/friends/guide/) - -## Retrieving Friends on Third-Party Platforms - -The following interface can be used to retrieve the current player’s friends (mutual followers) on third-party platforms (like TapTap) who have played the same game. -Besides the friend list, the interface also returns a cursor. -You can implement pagination by providing the cursor and a limit when performing the query. -The players in the result can be sorted by online status (those who are online will be placed at the beginning). - -The returned list of friends on the third-party platform will include each player’s ID, nickname, and profile picture on the platform. -As mentioned above, only those who have played the same game (i.e., logged in to the game with their third-party account) will be included in the friend list. -In general, those friends have logged in to the game with their third-party accounts that are based on the built-in account system, so the `TDSFriendInfo` of each player will be included in the friend list as well. - -In special cases, if a friend is logged in with their third-party account without using the built-in account system, the friend will still be included in the friend list, but their `TDSFriendInfo` will be `null`. -For example, assuming a game has two variations of APKs, variation A logs players in with F using the built-in account system and supports the Friends service while variation B also logs players in with F but without using the built-in account system and doesn’t support the Friends service. Player D and Player E are friends on F and they each use a different variation of the game (Player D uses the variation A) but both log in with F. Now if Player D retrieves their friends on F with the following interface, the returned list will include Player E, but the `TDSFriendInfo` of Player E will be `null`. - - - -```cs -// First query -string platform = "taptap"; -string cursor = null; -// Defaults to 50; no larger than 500 -int limit = 50; -// Sort by online status -SortCondition sortCondition = SortCondition.OnlineCondition -ThirdPartyFriendResult result = await TDSFriends.QueryThirdPartyFriendList(platform, cursor, limit, condition: sortCondition); - -ReadOnlyCollection friends = result.FriendList; -foreach (ThirdPartyFriend friend in friends) { - string thirdPartyId = friend.Id; - string thirdPartyNickName = friend.Name; - string thirdPartyAvatarUrl = friend.Avatar; - TDSFriendInfo info = friend.FriendInfo; -} - -// Pagination -string cursor = result.Cursor; -ThirdPartyFriendResult more = await TDSFriends.QueryThirdPartyFriendList(platform, cursor, limit, condition: sortCondition); -``` - -```java -ThirdPartyFriendRequestConfig config = new ThirdPartyFriendRequestConfig.Builder() - .platform(ThirdPartyFriendRequestConfig.PLATFORM_TAPTAP) - .pageSize(50) /* Defaults to 50; no larger than 500 */ - .sortCondition(SortCondition.getOnlineCondition()) /* Sort by online status */ - .build(); -// First query -TDSFollows.queryThirdPartyMutualList(config, null, new Callback() { - @Override - public void onSuccess(ThirdPartyFriendResult result) { - List friends = result.getFriendList(); - for (ThirdPartyFriend friend : friends) { - String thirdPartyId = friend.getUserId(); - String thirdPartyNickName = friend.getUserName(); - String thirdPartyAvatarUrl = friend.getUserAvatar(); - TDSFriendInfo info = friend.getTdsFriendInfo(); - } - - // Pagination - String cursor = result.getCursor(); - TDSFollows.queryThirdPartyMutualList(config, cursor, new Callback() { - /* Other things to do */ - } - } - @Override - public void onFail(TDSFriendError error) { - toast("query error = " + error.code + " msg = " + error.detailMessage); - } -}); -``` - -```objc -TDSThirdPartyFriendQueryOption *option = [TDSThirdPartyFriendQueryOption new]; -option.platform = TDSThirdPartyFriendPlatformTaptap; -option.limit = 50;// Defaults to 50; no larger than 500 -__block NSString *cursor; // Cursor - -[TDSFriends queryThirdPartyFriendListWithOption:option -callback:^(TDSThirdPartyFriendResult * _Nullable result, NSError * _Nullable error) { - for (TDSThirdPartyFriend* friend in result.friendList) { - NSString *thirdPartyId = friend.userId; - NSString *thirdPartyNickName = friend.userName; - NSString *thirdPartyAvatarUrl = friend.userAvatar; - TDSFriendInfo *info = friend.tdsFriendInfo; - } - cursor = result.cursor; -}]; - -// Pagination -option.from = cursor; -[TDSThirdPartyFriend queryThirdPartyFriendListWithOption:option -callback:^(TDSThirdPartyFriendResult * _Nullable result, NSError * _Nullable error) { - // Other things to do -}]; -``` - - - -By default, the SDK will fetch the result from the local cache. -To always fetch the result from the cloud, you can specify the caching strategy when performing the query. -The SDK will always cache the result regardless of the caching strategy you specify. -In other words, the caching strategy only affects whether the cache will be read. It won’t affect whether the result will be cached. - - - -```cs -ThirdPartyFriendResult result = await TDSFriends.QueryThirdPartyFriendList(platform, cursor, limit, - TDSFriends.ThirdPartyFriendRequestCachePolicy.OnlyNetwork, sortCondition); -``` - -```java -ThirdPartyFriendRequestConfig config = new ThirdPartyFriendRequestConfig.Builder() - .platform(ThirdPartyFriendRequestConfig.PLATFORM_TAPTAP) - .sortCondition(SortCondition.getOnlineCondition()) - .cachePolicy(ThirdPartyFriendRequestConfig.CachePolicy.ONLY_NETWORK) - .pageSize(50) - .build(); -``` - -```objc -option.cachePolicy = TDSThirdPartyFriendCachePolicyOnlyNetwork; -``` - - - -At this moment, the following third-party platforms are supported: - - - -- `taptap` (Please submit a ticket to us to enable it) - - - - - -- `taptap` (Please submit a ticket to us to enable it) -- `facebook` (The game needs to support Facebook Login) - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/_category_.json deleted file mode 100644 index 31d5ca7eb..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "推送通知", - "collapsed": true, - "position": 19 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/features.mdx deleted file mode 100644 index 3ea03273d..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/features.mdx +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Push Notification Introduction -sidebar_label: Introduction -sidebar_position: 1 -slug: /sdk/push/features/ ---- - - - - - -import {Conditional} from '/src/docComponents/conditional'; - -The Push Notification service allows you to send push notifications to your app’s users, whether they use iOS or Android. - -You can send push notifications using the iOS or Android SDK, or via our REST API. - -## Features - -You can easily integrate the Push Notification service into your app and start sending push notifications to both iOS and Android users. - -### A Variety of Message Types - -You can send push notifications that include text messages, rich media messages, and custom messages. You can also send pass-through messages. - -### Statistics - -You can view statistics such as arrival rates and open rates on the dashboard. - -### Schedule Messages and Push by Criteria - -We provide an easy-to-use dashboard that allows non-technical users to easily schedule push notifications or send push notifications to users based on specific criteria. - -## Our Benefits - -- Our service is based on WebSocket TLS, ensuring high delivery rates without compromising security. - -- We have implemented our own binary protocol together with minimalist commands that minimize power and data consumption on the clients. - -- Our service encapsulates the push notification services provided by major Chinese handset vendorsFCM, providing you with reliable and unified Android push notification solutions. - - - -- We provide dedicated and reliable connections to APNs clusters, ensuring high delivery rates for the push notifications you send out. - - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/Unreal.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/Unreal.mdx deleted file mode 100644 index 258380c8b..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/Unreal.mdx +++ /dev/null @@ -1,155 +0,0 @@ ---- -title: Unreal Push Guide -sidebar_label: Unreal Push -sidebar_position: 6 -slug: /sdk/push/guide/Unreal/ ---- - - - - - -import {Conditional} from '/src/docComponents/conditional'; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - - -This article will show you how to use push notifications in your Unreal project. We recommend that you take a look at [Push Notification Overview](/sdk/push/guide/overview/) if you haven’t already. - -Currently we only support iOS and some Android vendors (Huawei, Xiaomi, vivo, OPPO, Meizu, Honor)FCM. - -## Getting Started - -### Prerequisites - -* Install **UE 4.26** or higher on your computer -* iOS **12** or higher -* Android MinSDK is **API21** or higher - -### iOS - -Please first obtain an iOS push notification certificate by following [APNs Configuration Guide](/sdk/push/guide/ios-cert/). - -### Android - -Please first apply for push notification permissions from Android vendorsFCM by following [Android Mixpush Guide](/sdk/push/guide/android-mixpush/). - -Note that you only need to follow the guide to apply for push notification permissions from Android vendorsFCM. You **don’t** need to follow the guide to set up push notifications for Android. - -## Integrate the Push Notification Service - -### Install the Plugin - -* Download **[TapSDK.zip](https://github.com/taptap/TapSDK-UE4/releases)**, unzip it, and copy `LeanCloudPush` and `LeanCloud` to the `Plugins` directory of the project. If AndroidX configurations are missing from the project, you can also copy `AndroidX` to the project. -* Restart Unreal Editor. -* Go to **Edit > Plugins > Project > TapTap** and enable the `LeanCloudPush` module. - -### Add Dependencies - -Add the following dependencies to **Project.Build.cs**: - -```c# -PublicDependencyModuleNames.AddRange(new string[] { - "Core", - "CoreUObject", - "Engine", - "Slate", - "SlateCore", - "Http", - "Json", - "JsonUtilities", -}); - -if (Target.Platform == UnrealTargetPlatform.IOS || Target.Platform == UnrealTargetPlatform.Android) -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - // 推送接入 - "LeanCloudPush", - "LeanCloudMobile" - } - ); -} -else -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - "LeanCloud" - } - ); -} -``` - -### Project Configurations - -#### iOS - -Add the following configurations to **DefaultEngine.ini**: - -```ini -[/Script/IOSRuntimeSettings.IOSRuntimeSettings] -bEnableRemoteNotificationsSupport=True -``` - -#### Android - -* Create a directory named **app** and copy the file named **agconnect-services.json** downloaded from Huawei Developer Center into this directory -* Add the following code to the Android UPL file of the project (create the file if it doesn’t exist) and fill in the configurations - -```xml - - - - - - - - - - - - - ``` - -### Import Header Files - -```cpp -#if PLATFORM_IOS -#include "iOS/LCIOSPush.h" -#elif PLATFORM_ANDROID -#include "Android/LCAndroidPush.h" -#endif -``` - -### Usage - -Enter the configurations obtained from developer platforms into the following interface: - -```cpp -#if PLATFORM_IOS - FLCIOSPush::Register("iOS Team ID"); -#elif PLATFORM_ANDROID - FString DeviceName = FLCAndroidPush::GetDeviceName().ToLower(); - if (DeviceName.Contains("huawei")) { - FLCAndroidPush::RegisterHuaWei(); - } else if (DeviceName.Contains("oppo")) { - FLCAndroidPush::RegisterOPPO("OPPO’s AppKey", "OPPO’s AppSecret"); - } else if (DeviceName.Contains("vivo")) { - FLCAndroidPush::RegisterVIVO(); - } else if (DeviceName.Contains("meizu")) { - FLCAndroidPush::RegisterMeiZu("Meizu’s AppId", "Meizu’s AppKey"); - } else if (DeviceName.Contains("honor")) { - FLCAndroidPush::RegisterHonor(); - } else { - FLCAndroidPush::RegisterXiaoMi("Xiaomi’s AppId", "Xiaomi’s AppKey"); - } -#endif -``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/_category_.json deleted file mode 100644 index 3446da847..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 2 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/android.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/android.mdx deleted file mode 100644 index 83ce91bf8..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/android.mdx +++ /dev/null @@ -1,395 +0,0 @@ ---- -title: Android Push Guide -sidebar_label: Android Push -sidebar_position: 3 -slug: /sdk/push/guide/android/ ---- - - - - - -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import {Conditional} from '/src/docComponents/conditional'; - -Please read [Push Notification Overview](/sdk/push/guide/overview/) first to understand the concepts. - -There is a special demo for Android message push. See [Android-Push-Demo](https://github.com/leancloud/android-push-demo). - -## Introduction to the Push Process - -The Push Notification service on Android relies primarily on the PushService on the client. The PushService is an application-independent process that is created when the application is first launched and then lives (as much as possible) in the background, and is mainly responsible for maintaining a long WebSocket connection with the cloud push server. -Thus, as long as PushService is alive, any message that needs to be pushed to the current device will be pushed immediately to the push server; if PushService is killed, the push channel will be interrupted and Android devices will not receive any push messages. After establishing a long WebSocket connection with the push server, PushService will also receive multiple unsuccessful push messages cached by the server at once. - -## Integrate the Push Notification Service - -To integrate the push service, you need the realtime-android library. First, open `build.gradle` in the `app` directory and configure it like this: - - -{`dependencies {\n -implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'\n -}`} - - -Then create a new Java class called **MyLeanCloudApp** and make it inherit from the **Application** class with the following sample code: - -```java -public class MyLeanCloudApp extends Application { - - @Override - public void onCreate() { - super.onCreate(); - - // Pass this, AppId, and AppKey as initialization parameters - LeanCloud.initialize(this, "{{appid}}", "{{appkey}}", "https://please-replace-with-your-customized.domain.com"); - } -} -``` - -### Configure AndroidManifest - -Make sure your `AndroidManifest.xml` contains the following in ``: - -```xml - -``` - -Please also set the necessary permissions: - -```xml - - -``` - -In order for the application to receive pushes even when it is closed, you need to add to ``: - -```xml - - - - - - - -``` - -#### Push Wakeup - -If you want to support inter-app push wakeup mechanism, i.e. two apps using cloud push on the same device, after app A is killed, when app B is woken up it can wake up app A's push at the same time, you can configure it like this: - -```xml - -``` - -#### Complete `AndroidManifest.xml` - -The complete `AndroidManifest.xml` file configuration is shown below: - -```xml - - - - - - - - - - - - - - - - - - - - - - - -``` - - -### Save Installation - -When the application is installed on the user's device, the SDK automatically generates an Installation object if the push feature is to be used. This object is essentially the installation information generated by the application on the device and must first be stored on the cloud in order for the device to receive push notifications: - -```java -LCInstallation.getCurrentInstallation().saveInBackground(); -``` - -**This code should be called once at application startup** to ensure that the device is registered with the cloud. You can listen to the callback to get the installationId to do the data association. - -```java -LCInstallation.getCurrentInstallation().saveInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCObject avObject) { - // Operations such as associating the installationId with the user table. - String installationId = LCInstallation.getCurrentInstallation().getInstallationId(); - System.out.println("Successfully saved: " + installationId ); - } - @Override - public void onError(Throwable e) { - System.out.println("Could not save. Error message: " + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -### Enable the Push Notification Service - -Start the push service by calling the following code and also set the default Activity to open. - -```java -// Set the default Activity to open -PushService.setDefaultPushCallback(this, PushDemo.class); -``` - -### Subscribe to Channels - -Your application can subscribe to a channel by calling the `PushService.subscribe` method before saving your Installation: - -```java -// Subscribe to the channel and open the corresponding Activity when the channel message arrives -// Parameters in order: current context, channel name, class of the callback object -PushService.subscribe(this, "public", PushDemo.class); -PushService.subscribe(this, "private", Callback1.class); -PushService.subscribe(this, "protected", Callback2.class); -``` - -Note: - -- **The channel name can only contain upper and lower case English letters, numbers, underscores (`_`), hyphens (`-`), equal signs (`=`), and Chinese characters.** -- The callback object refers to the Activity page that the user enters by clicking the notification in the notification bar. - -To unsubscribe from a channel: - -```java -PushService.unsubscribe(context, "protected"); -//You must save the Installation again after unsubscribing -LCInstallation.getCurrentInstallation().saveInBackground(); -``` - -### Adapting for Android 8.0 - -After calling `LeanCloud.initialize`, you need to call `PushService.setDefaultChannelId(context, channelid)` to set the default `channel` for notification display, otherwise the message will not be displayed. See Google's official documentation [Creating a notification](https://developer.android.com/training/notify-user/channels.html) for more information about channel ID. - -In addition, our push service also supports multiple push channels. On the client side, developers can create a new notification channel by calling `PushService` with the following method (or you can call the underlying API to create it yourself): - -```java -public static void createNotificationChannel(Context context, String channelId, String channelName, - String description, int importance, - boolean enableLights, int lightColor, - boolean enableVibration, long[] vibrationPattern) -``` - -Note the `channelId` here because we will need it later when we send push notifications. When sending a push request, the custom keyword `_notificationChannel` allows you to select a different channel for the message presentation. - -For example, the following request will be displayed on the client in the channel with notification ID "1": - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "where": {"key" : "value"} - "data": { - "alert": "Message content", - "title": "Title to display in the notification bar", - "_notificationChannel": "1" - } - }' \ - https://{{host}}/1.1/push -``` - -## Send Push Notifications - - -By default, **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Settings** has **Prevent clients from sending push notifications** checked to prevent clients from pushing messages to any target device in the app without restriction. -We recommend that developers check this box to send push messages via the REST API or the dashboard. -If there is a need to send pushes from the client, you will need to uncheck this first. - -### Send to All Devices - -```java -LCPush push = new LCPush(); -Map pushData = new HashMap(); -pushData.put("alert","push message to android device directly"); -push.setPushToAndroid(true); -push.setData(pushData); -push.sendInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(JSONObject jsonObject) { - System.out.println("Push successful" + jsonObject); - } - @Override - public void onError(Throwable e) { - System.out.println("Push failed. Error message: " + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -### Send to Specific Users - -Send to users on the "public" channel: - -```java -LCQuery pushQuery = LCInstallation.getQuery(); -pushQuery.whereEqualTo("channels", "public"); -LCPush push = new LCPush(); -push.setQuery(pushQuery); -push.setMessage("Push to channel."); -push.setPushToAndroid(true); -push.sendInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(JSONObject jsonObject) { - System.out.println("Push successful" + jsonObject); - } - @Override - public void onError(Throwable e) { - System.out.println("Push failed. Error message: " + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -To send to a user with a specific Installation id, you would typically associate LCInstallation with the device's logged-in user LCUser as a property, and then you can send a message to a specific user by querying the InstallationId with the following code to achieve a private message-like function: - -```java -LCQuery pushQuery = LCInstallation.getQuery(); -// Assuming that THE_INSTALLATION_ID is the installationId stored in the user table, -// you can retrieve it and store it in the user table when the application is opened by the user -pushQuery.whereEqualTo("installationId", THE_INSTALLATION_ID); -LCPush.sendMessageInBackground("Tarara invited you to play Arc Symphony with her!",pushQuery).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(Object object) { - System.out.println("Push successful" + object); - } - @Override - public void onError(Throwable e) { - System.out.println("Push failed. Error message: " + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -## Read More: How to Reply to Push Messages - -### Message Format - -For the specific message format, see the [Push Notification REST API](/sdk/push/guide/rest/). -For Android devices, the default message content parameters support the following attributes: - -```json -{ - "alert": "Message content", - "title": "Title to display in the notification bar", - "custom-key": "A custom property added by the user; custom-key is just an example, feel free to replace it", - "silent": false, // Used to control whether to turn off the notification bar alert; default is false, i.e. do not turn off the notification bar alert - "action": "com.your_company.push" // Must be provided if using a custom receiver -} -``` -The silent property above is a flag for the push-through message and the notification bar message. If silent is true, the message is not displayed in the notification bar; if silent is false, the message is displayed in the notification bar. - -As mentioned earlier, after PushService receives a push message, it determines whether the message is expired and whether a duplicate message has been received before forwarding the message, and only non-expired and non-duplicate messages are notified to the application by sending a local notification or broadcast. - -### How Notification Bar Messages Respond to User Click Events - -When the PushService sends a notification bar message, it sets the response class for the notification bar, depending on whether the developer calls `PushService.setDefaultPushCallback(context, clazz)` or `PushService.subscribe(context, "channel", clazz)` to set the callback class that sets the response class for the notification bar. - -In the onCreate function of the callback class, the developer can then use the following code to get the specific data of the push message: - -```java -public class CallbackActivity extends Activity { - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.callback2); - - // Get the push message data - String message = this.getIntent().getStringExtra("com.avoscloud.Data"); - String channel = this.getIntent().getStringExtra("com.avoscloud.Channel"); - System.out.println("message=" + message + ", channel=" + channel); - } -} -``` - -### Customized Receiver - -If you want to push messages that don't appear in the Android notification bar, but instead execute the application's predefined logic, you need to declare your own Receiver in the `AndroidManifest.xml` of your Android project: - -```xml - - - - - - - - -``` - -Here `com.avos.avoscloud.PushDemo.MyCustomReceiver` is your Android's Receiver class, and `` must correspond to the `action` specified in the push's data. - -Your Receiver can be implemented like this: - -```java -public class MyCustomReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - // Retrieve the push message data - String message = intent.getStringExtra("com.avoscloud.Data"); - String channel = intent.getStringExtra("com.avoscloud.Channel"); - System.out.println("message=" + message + ", channel=" + channel); - } -} -``` - -Also, the request to send the push is changed accordingly, e.g: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "channels":[ "public"], - "data": { - "action": "com.avos.UPDATE_STATUS", - "name": "LeanCloud." - } - }' \ - https://{{host}}/1.1/push -``` - -Note that if you are using a custom Receiver, the message sent must have an action and its value exists in the `` list of the custom receiver configuration, for example `'com.avos.UPDATE_STATUS'`. Please use your own action and try not to confuse it with other applications. It is recommended to use the domain name to define it. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/ios-cert.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/ios-cert.mdx deleted file mode 100644 index c8e02f509..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/ios-cert.mdx +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: APNs Configuration Guide -sidebar_label: APNs Configuration -sidebar_position: 1 -slug: /sdk/push/guide/ios-cert/ ---- - - - - - -APNs is Apple’s push notification service. It allows app developers to send push notifications to apps installed on Apple devices. - -This article explains how to add APNs support to apps installed on Apple devices and what configurations you need to set up on the Developer Center. - -## Enable Push Notifications - -Follow these two steps to enable push notifications for your app: - -1. Enable the push notifications permission for your project. -2. Enable push notifications for the corresponding App ID on Apple Developer. - -### Enable the Push Notifications Permission - -To add the required permission to the app, enable push notifications for the app in Xcode. - -To do this, open the Xcode project, go to **Project > Target > Capabilities**, click the plus sign, select `Push Notifications`, and add it. The result should look like the screenshot below: - -![Project Add Capability](/img/apns_setup/project_add_capability.png) - -### Enable Push Notifications for the App ID - -To enable push notifications for an App ID, go to Apple Developers, go to **Certificates, Identifiers & Profiles**, click **Identifiers** on the sidebar, and click the corresponding App ID (which is the Bundle Identifier in the Xcode project). Now select the `Push Notifications` checkbox and click “Save” to save the change. The result should look like the screenshot below: - -![App ID Add Capability](/img/apns_setup/app_id_add_capability.png) - -## Select a Push Method - -Apple offers two ways to send push notifications, each with its own advantages and disadvantages. Both are supported by our Push Notification service, and you can choose one based on your needs. - -1. Token-Based Push Notification (Recommended). - - Technically, this is faster than certificate-based push notification. - - The same key can be used by multiple applications. - - The same key can be used to send push notifications to multiple applications under an Apple Developer account. - - The same key can be used to send push notifications to test apps and production apps. - - A key never expires, so you don’t have to regenerate it like you do with certificates. -2. Certificate-Based Push Notification. - - A certificate is associated with an Apple Developer App ID and can only be used to send push notifications to the associated app. - - APNs comes with a development environment and a production environment. You may need to set up different certificates for apps in different environments. - - A certificate has an expiration date, and you will need to regenerate and reconfigure certificates periodically. - -In general, token-based push notification is easier to set up and provides better usability and functionality than certificate-based push notification. We recommend that you use token-based push notification. - -> **Note that** you cannot use both methods simultaneously, and token-based push notification takes precedence over certificate-based push notification. Once you set up token-based push notifications, all push notifications sent from your app will use this method. - -### Token-Based Push Notification (Recommended) - -To enable token-based push notification, first generate and download an Auth Key from Apple Developer, then upload it to the Developer Center and set up the appropriate configurations. Once complete, you will be able to send push notifications from your app. - -#### Generate a Key - -To generate a key, sign in to Apple Developer, navigate to **Certificates, Identifiers & Profiles**, click **Keys** in the sidebar, and then click the plus sign (+). Enter a unique name for the key and select **Apple Push Notifications service (APNs)**: - -> **Note that** if you don’t see **Keys** in the sidebar, **your account may not have the necessary permissions**. - -![Generate Push Key](/img/apns_setup/generate_push_key.png) - -Proceed to the next page and review the key details. If everything looks good, generate the key and download it. You will get a text file with the extension `.p8`. - -> **Be sure to** keep this file (with the extension `.p8`) in a safe place. You won’t be able to download it again because the key is not stored in your Apple Developer account. If the download button is disabled, it means you have already downloaded the key. - -#### Configure the Key - -After downloading the key (as a `.p8` file), you will need to upload it to the Developer Center and complete some configurations: - -1. Go to **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Settings > iOS Token Authentications** and click on **New Token Authentication**. -2. Enter `Team ID`, `Key ID`, and `Topics` in the pop-up window and upload the key file (the `.p8` file) here. - - `Team ID` is the ID of the team to which the Apple Developer account belongs. You can find it under **Membership** in Apple Developer. - - `Key ID` is the ID of the key (the `.p8` file), which can be found by going to **Certificates, Identifiers & Profiles > Keys** in Apple Developer and clicking on the corresponding key. - - `Topics` is the IDs of your apps (the Bundle Identifiers in Xcode projects). You can specify multiple topics by separating them with **commas**, as long as **they belong to the same Team ID**. -3. Click **Add** to finish uploading and setting up the key. - -Once you have completed the above steps, you can send a test notification by going to **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Send notifications**. - -### Certificate-Based Push Notification - -To enable certificate-based push notification, a certificate must be generated for each app on Apple Developer. Each app can have **sandbox certificates** and **sandbox & production certificates**. Once you have obtained the certificates from Apple Developer, upload them to the Developer Center and you will be able to send push notifications using the certificates. - -#### Generate Certificates - -To generate certificates, sign in to Apple Developer, navigate to **Certificates, Identifiers & Profiles**, click **Certificates** in the sidebar, and then click the plus sign (+). Follow the steps below: - -1. Select a certificate type. The most common is `Apple Push Notification service SSL`. For this certificate type, you can choose between `Sandbox` and `Sandbox & Production`. `Sandbox` certificates can only be used in development environments, while `Sandbox & Production` can be used in both development and production environments. You can refer to the descriptions under each option for more information, as shown in the screenshot below: - - ![Select Cert Type](/img/apns_setup/select_cert_type.png) - -2. After selecting a certificate type, proceed to the next step and select an App ID (the Bundle Identifier in the Xcode project), then proceed to the next step. You will be prompted to upload a CSR file. -3. Generate a CSR (Certificate Signing Request) file on your Mac by following these steps: - - - Open `Keychain Access` in `/Applications/Utilities`. - - Select **Keychain Access > Certificate Assistant > Request a Certificate From a Certificate Authority…**. - - In the Certificate Assistant window, enter an email address in `User Email Address` and a name for the key in `Common Name` (e.g., Gita Kumar Dev Key). - - Leave `CA Email Address` blank. - - Select `Saved to disk` and continue. - - ![Generate CSR](/img/apns_setup/generate_csr.png) - -4. Upload your CSR file (the `.certSigningRequest` file you saved from the last step) and continue. Download the generated certificate. - -#### Set up Certificates - -After you generate a certificate, you’ll need to upload the downloaded certificate and your private key to the Developer Center. Follow the instructions below: - -1. On the Mac you used to generate the CSR, double-click the downloaded certificate. macOS will import the certificate into `Keychain` and group it with the key of the CSR you generated earlier, as shown in the screenshot below: - - ![Cert With Key](/img/apns_setup/cert_with_key.png) - -2. Go to **Keychain Access > login > My Certificates** and **right-click** the imported certificate (not the key), select **Export**, and save the certificate to disk as a `.p12` file. A popup will appear asking for a password. **Leave the two fields blank so that you don’t give the file a password**. Now click OK. You may see another popup asking for the macOS login password. Enter the password and click Allow. -3. Go to **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Settings > iOS push notification certificates** and upload the appropriate certificate (the `.p12` file exported in the last step). - - If the certificate type is `Sandbox`, the certificate can only be uploaded to the development environment; if it is `Sandbox & Production`, the certificate can be uploaded to either the development or production environment. - -Once you have completed the above steps, you can send a test notification by going to **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Send notifications**. - -#### Fail to Upload a Certificate - -If you are unable to upload a certificate, it is usually because there is a problem with the certificate. Here are some common reasons: - -1. The certificate is not a push notification certificate. You can see this from the Common Name of the certificate, which can be viewed by double-clicking the certificate in `Keychain Access`. The Common Name of a push notification certificate contains either `Push Service` or `Pass Type ID`, as shown in the screenshot below: - - ![Cert Common Name](/img/apns_setup/cert_common_name.png) - - The Developer Center checks to see if the Common Name of the certificate contains any of the following prefixes: - - - `Apple Push Services` - - `Apple Sandbox Push Services` - - `Apple Development IOS Push Services` - - `Apple Production IOS Push Services` - - `Pass Type ID` - - > Apple may change the Common Name prefix of push notification certificates in the future. When this happens, we will update the list of prefixes we use to verify certificates. - -2. The certificate is being exported in the wrong format. Currently, the Developer Center only accepts certificates in `.p12` format. Please be sure to select this format when exporting the certificate. - -#### Certificate Expired - -If you try to send a push notification with an expired certificate, you will receive the error `The iOS certificate file is expired or disabled.`. - -Each time you submit a request to send push notifications, our server checks whether the certificate of the environment specified by the `prod` parameter has expired (if `prod` is not specified, the certificate of the production environment is checked). If the certificate is expired and the queried target devices may include iOS devices, the push notification request will be rejected. - -One solution is to replace the expired certificate with a new one. Another solution is to use the `deviceType` field in the query to specify that you want to send push notifications to non-iOS devices. See *Push Notification REST API Guide* for more information. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/ios.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/ios.mdx deleted file mode 100644 index 78042ca99..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/ios.mdx +++ /dev/null @@ -1,1092 +0,0 @@ ---- -title: iOS Push Guide -sidebar_label: iOS Push -sidebar_position: 2 -slug: /sdk/push/guide/ios/ ---- - - - - - -import MultiLang from '/src/docComponents/MultiLang'; -import Mermaid from "/src/docComponents/Mermaid"; - - -This article will show you how to use push notifications in your iOS project. We recommend that you take a look at [Push Notification Overview](/sdk/push/guide/overview/) if you haven’t already. - -## Configure APNs Certificates - -To use push notifications in your iOS project, you must first configure APNs certificates. See [APNs Configuration Guide](/sdk/push/guide/ios-cert/) for more information. - -## iOS Workflow Overview - -The first step is to register with APNs and obtain a token, then store the token in the cloud: - ->APNs: 1. Call the official API and get the deviceToken -APNs-->>iOS SDK: 2. Provide the deviceToken -iOS SDK-->>Cloud: 3. The SDK stores the deviceToken in the _Installation table -`} -/> - -Then you can invoke the interface provided by the Push Notification service to send a push notification: - ->Cloud: 1. Send an HTTPS request to the Push Notification API -Cloud-->>APNs: 2. Find the appropriate deviceToken from _Installation and call the APNs API to send the push notification -APNs->>iOS device: 3. Send the push notification -`} -/> - -## Installation - -Installation is a subclass of LCObject. You can use an Installation object to store the token and other data needed to send a push notification. - -The SDK comes with a default Installation object and **will cache the data after you save the default object to the cloud**. In general, the default object is used to store the device token. The code below shows how to get the default object: - - - -```objc -LCInstallation *installation = [LCInstallation defaultInstallation]; -``` - -```swift -let installation = LCApplication.default.currentInstallation -``` - - - -In addition to using the default Installation object, you can also construct new Installation objects and store other tokens of special types (such as VoIP) in them. The following code constructs a new Installation object: - - - -```objc -LCInstallation *installation = [[LCInstallation alloc] init]; -``` -```swift -let installation = LCInstallation() -``` - - - -**The Instant Messaging service uses the device token obtained from the default Installation object. To use the push notification feature of the Instant Messaging service, make sure that the default Installation object has successfully saved the device token.** - -By default, an Installation object contains the following fields: - -Field|Type|Description ----|---|--- -deviceToken|String|The token used to send push notifications -apnsTeamId|String|The Team ID used to send push notifications -badge|Number|The number displayed in the app’s badge; primarily used to clear the badge -channels|Array|An array of subscribed channels -deviceProfile|String|Custom certificate’s name; primarily used to send push notifications with multiple certificates -deviceType|String|The device type; the SDK automatically sets the value of this field; please avoid editing it manually -apnsTopic|String|The app’s Bundle Identifier; the SDK automatically sets the value of this field; please avoid editing it manually -timeZone|String|The device’s timezone; the SDK automatically sets the value of this field; please avoid editing it manually - -### Register With APNs and Obtain a Token - -Before saving the Installation, you must first register with APNs to obtain the token needed to send push notifications. The code below uses User Notification as an example: - - - -```objc -#import -#import - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - - // Be sure to initialize the app first - [LCApplication setApplicationId:{{appid}} - clientKey:{{appkey}} - serverURLString:"https://please-replace-with-your-customized.domain.com"]; - - [[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { - switch ([settings authorizationStatus]) { - case UNAuthorizationStatusAuthorized: - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication] registerForRemoteNotifications]; - }); - break; - case UNAuthorizationStatusNotDetermined: - [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) { - if (granted) { - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication] registerForRemoteNotifications]; - }); - } - }]; - break; - default: - break; - } - }]; - - return YES; -} -``` -```swift -import LeanCloud -import UserNotifications - -func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - // Be sure to initialize the app first - do { - try LCApplication.default.set( - id: {{appid}}, - key: {{appkey}}, - // Please replace xxx.example.com with the custom API domain you have added to your app - serverURL: "https://xxx.example.com") - } catch { - print(error) - return false - } - - UNUserNotificationCenter.current().getNotificationSettings { (settings) in - switch settings.authorizationStatus { - case .authorized: - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - case .notDetermined: - UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert, .sound]) { (granted, error) in - if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - } - default: - break - } - } - - return true -} -``` - - - -### Save the Token - -Once you have successfully registered with APNs, the system returns the deviceToken with the `didRegisterForRemoteNotificationsWithDeviceToken` function. In most cases, you can save the deviceToken and apnsTeamId in this function, as shown in the code below: - - - -```objc -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { - - [[LCInstallation defaultInstallation] setDeviceTokenFromData:deviceToken - teamId:@"YOUR_APNS_TEAM_ID"]; - [[LCInstallation defaultInstallation] saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // save succeeded - } else if (error) { - NSLog(@"%@", error); - } - }]; -} -``` -```swift -func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - - LCApplication.default.currentInstallation.set( - deviceToken: deviceToken, - apnsTeamId: "YOUR_APNS_TEAM_ID") - LCApplication.default.currentInstallation.save { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} -``` - - - -The device token changes when a user erases their device, restores the app from backup, or installs the app on a new device. To solve this problem, [Apple recommends][apple-apns] that the app requests the device token of the APNs when opening, and then sets and saves the token. -Also, our system keeps track of the update times (`updatedAt`) of all Installation objects and removes the objects that haven’t been updated in a while. -Therefore, we recommend that you build your app according to Apple’s recommended way to avoid Installation objects from being removed accidentally and to avoid push notification failure due to expired device tokens. - -[apple-apns]: https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns - -## Use Multiple Certificates - -Some developers may publish different apps that share the same data and messages (for example, a carpooling service may provide different apps for drivers and riders). If that’s your case, you can upload multiple custom certificates and configure different `deviceProfile`s for different devices so you can send push notifications to different apps with different certificates. - -When you upload a custom certificate, you are prompted for a certificate type, which is also the name of the deviceProfile. If the deviceProfile is set for the Installation, our system will ignore the certificates configured for the development and production environments and use the deviceProfile instead. - -## Special Push Types - -For special push notifications such as VoIP, because they don’t use the app’s Bundle Identifier as apnsTopic, you must change apnsTopic to the specified value before saving the token. - -## Send Push Notifications - -You can send iOS push notifications through the REST API or the dashboard. - -### Environments - -An iOS app comes with a **development** environment and a **production** environment. -Apps installed using Xcode are in development environments. Apps published through the App Store, Ad-Hoc, and TestFlight are in production environments. - -When sending push notifications using the REST API or the dashboard, you can specify which environment to send push notifications to by specifying the `prod` parameter. -When sending push notifications using `Push` with the SDK, push notifications are sent to the production environment by default. -To send to the development environment instead, use the following method: - - - -```objc -[LCPush setProductionMode:false]; -``` -```swift -do { - let environment: LCApplication.Environment = [.pushDevelopment] - let configuration = LCApplication.Configuration(environment: environment) - try LCApplication.default.set( - id: {{appid}}, - key: {{appkey}}, - // Please replace xxx.example.com with the custom API domain you have added to your app - serverURL: "https://xxx.example.com", - configuration: configuration) -} catch { - print(error) -} -``` - - - -Note that if you’re using the SDK, you can only specify which environment to send push notifications to; you can’t change the environment that the app itself belongs to. -The environment the app belongs to can only be determined by how the app is distributed. - -Note that to prevent performance issues caused by massive certificate errors, when using the **development certificate**, you can send push notifications to a maximum of 20,000 devices at a time. If you exceed this limit, the system will reject the push notification request (you will see an error on **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Push records**). Please set the conditions carefully when using the development certificate. - -## Use Channels - -Channels allow you to implement a pub-sub model in your app. A device can subscribe to a channel, and when you send push notifications, you can specify which channels to send to. - -Note that a channel name can only contain uppercase and lowercase letters, numbers, underscores (`_`), hyphens (`-`), equal signs (`=`), and Chinese characters. - -### Subscribe and Unsubscribe - -To subscribe to the `Giants` channel: - - - -```objc -LCInstallation *currentInstallation = [LCInstallation defaultInstallation]; -[currentInstallation addUniqueObject:@"Giants" forKey:@"channels"]; -[currentInstallation saveInBackground]; -``` -```swift -do { - try LCApplication.default.currentInstallation.append("channels", element: "Giants", unique: true) - _ = LCApplication.default.currentInstallation.save({ (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - - - -Be sure to save after subscribing. - -To unsubscribe: - - - -```objc -LCInstallation *currentInstallation = [LCInstallation defaultInstallation]; -[currentInstallation removeObject:@"Giants" forKey:@"channels"]; -[currentInstallation saveInBackground]; -``` -```swift -do { - try LCApplication.default.currentInstallation.remove("channels", element: "Giants") - _ = LCApplication.default.currentInstallation.save({ (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - - - -Get all subscribed channels: - - - -```objc -NSArray *subscribedChannels = [LCInstallation defaultInstallation].channels; -``` -```swift -let subscribedChannels: LCArray? = LCApplication.default.currentInstallation.channels -``` - - - -### Send to Channels - -To send to the “Giants” channel: - - - -```objc -// Send a notification to all devices subscribed to the "Giants" channel. -LCPush *push = [[LCPush alloc] init]; -[push setChannel:@"Giants"]; -[push setMessage:@"Giants is cool"]; -[push sendPushInBackground]; -``` -```swift -let messageData: [String: Any] = [ - "alert": "Giants is cool" -] - -let channels: [String] = ["Giants"] - -LCPush.send(data: messageData, channels: channels) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -To send to multiple channels, specify an array for `channels`: - - - -```objc -NSArray *channels = [NSArray arrayWithObjects:@"Giants", @"Mets", nil]; -LCPush *push = [[LCPush alloc] init]; - -// Be sure to use the plural 'setChannels'. -[push setChannels:channels]; -[push setMessage:@"The Giants won against the Mets 2-3."]; -[push sendPushInBackground]; -``` -```swift -let messageData: [String: Any] = [ - "alert": "The Giants won against the Mets 2-3." -] - -let channels: [String] = ["Giants", "Mets"] - -LCPush.send(data: messageData, channels: channels) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -## Send to Specific Devices - -In most cases, using channels to specify targets to receive a push notification is sufficient. However, sometimes you may want to send push notifications to more specific devices. The Push Notification service allows you to query the Installation table using the LCQuery API and send push notifications to devices that match certain query conditions. - -Because Installation is a subclass of LCObject, you can store data of any type in an Installation object and associate the Installation with other data in your app. This gives you the ability to send customized and dynamic push notifications to your users. - -### Save Data Into Installation Objects - -To add three new fields to the Installation: - - - -```objc -// Store app language and version -LCInstallation *installation = [LCInstallation defaultInstallation]; - -[installation setObject:@(YES) forKey:@"scores"]; -[installation setObject:@(YES) forKey:@"gameResults"]; -[installation setObject:@(YES) forKey:@"injuryReports"]; -[installation saveInBackground]; -``` -```swift -do { - let currentInstallation = LCApplication.default.currentInstallation - - try currentInstallation.set("scores", value: true) - try currentInstallation.set("gameResults", value: true) - try currentInstallation.set("injuryReports", value: true) - - _ = currentInstallation.save({ (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - - - -You can set an owner for the Installation. This could be the currently logged in user: - - - -```objc -// Saving the device's owner -LCInstallation *installation = [LCInstallation defaultInstallation]; -[installation setObject:[LCUser currentUser] forKey:@"owner"]; -[installation saveInBackground]; -``` -```swift -do { - let currentInstallation = LCApplication.default.currentInstallation - - if let currentUser = LCApplication.default.currentUser { - try currentInstallation.set("owner", value: currentUser) - } - - _ = currentInstallation.save({ (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - - - -### Send Push Notifications Based on Query - -Once you start storing data with Installation, you can build a query to send push notifications to a subset of all devices registered with your app: - - - -```objc -// Create our Installation query -LCQuery *pushQuery = [LCInstallation query]; -[pushQuery whereKey:@"injuryReports" equalTo:@(YES)]; - -// Send push notification to query -LCPush *push = [[LCPush alloc] init]; -[push setQuery:pushQuery]; // Set our Installation query -[push setMessage:@"Willie Hayes injured by own pop fly."]; -[push sendPushInBackground]; -``` -```swift -let query = LCQuery(className: "_Installation") -query.whereKey("injuryReports", .equalTo(true)) - -let messageData: [String: Any] = [ - "alert": "Willie Hayes injured by own pop fly." -] - -LCPush.send(data: messageData, query: query) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -You can also perform queries on channels: - - - -```objc -// Create our Installation query -LCQuery *pushQuery = [LCInstallation query]; -[pushQuery whereKey:@"channels" equalTo:@"Giants"]; // Set channel -[pushQuery whereKey:@"scores" equalTo:@(YES)]; - -// Send push notification to query -LCPush *push = [[LCPush alloc] init]; -[push setQuery:pushQuery]; -[push setMessage:@"Giants scored against the A's! It's now 2-2."]; -[push sendPushInBackground]; -``` -```swift -let query = LCQuery(className: "_Installation") -query.whereKey("channels", .equalTo("Giants")) -query.whereKey("scores", .equalTo(true)) - -let messageData: [String: Any] = [ - "alert": "Giants scored against the A's! It's now 2-2." -] - -LCPush.send(data: messageData, query: query) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -If you have stored relations with other objects using Installation, we can also use that data. For example, to send push notifications to devices near Peking University: - - - -```objc -// Find users near a given location -LCQuery *userQuery = [LCUser query]; -[userQuery whereKey:@"location" - nearGeoPoint:beijingUniversityLocation, - withinMiles:[NSNumber numberWithInt:1]] - -// Find devices associated with these users -LCQuery *pushQuery = [LCInstallation query]; -[pushQuery whereKey:@"user" matchesQuery:userQuery]; - -// Send push notification to query -LCPush *push = [[LCPush alloc] init]; -[push setQuery:pushQuery]; // Set our Installation query -[push setMessage:@"Free hotdogs at the Tarara concession stand!"]; -[push sendPushInBackground]; -``` -```swift -let beijingUniversityLocation = LCGeoPoint(latitude: 39.9869, longitude: 116.3059) - -let userQuery = LCQuery(className: "_User") -userQuery.whereKey("location", .locatedNear(beijingUniversityLocation, minimal: nil, maximal: nil)) - -let pushQuery = LCQuery(className: "_Installation") -pushQuery.whereKey("user", .matchedQuery(userQuery)) - -let messageData: [String: Any] = [ - "alert": "Free hotdogs at the Tarara concession stand!" -] - -LCPush.send(data: messageData, query: pushQuery) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -## Options - -In addition to sending a text message, you can play a sound, set the number on the badge, or include other custom data in your push notifications. You can also set an expiration time for your push notifications. - -### Customize Notifications - -If you want to send something in addition to a text message, you can create your own notification data. Note that there are some reserved fields that have special meanings: - -Reserved field|Description ----|--- -`alert`|The text content of the push notification. -`badge`|The number on the badge above the app icon. You can set it to a specific value or choose to increase the current value. -`sound`|The name of the sound file in the app’s bundle. -`content-available`|If you use the Newsstand, set this to 1 to start a background download. - -See [Push Notification REST API Guide](/sdk/push/guide/rest/) for additional reserved fields. - -To increase the number on the badge and play a sound: - - - -```objc -NSDictionary *data = [NSDictionary dictionaryWithObjectsAndKeys: - @"The Mets scored! The game is now tied 1-1!", @"alert", - @"Increment", @"badge", - @"cheering.caf", @"sound", - nil]; -LCPush *push = [[LCPush alloc] init]; -[push setChannels:[NSArray arrayWithObjects:@"Mets", nil]]; -[push setData:data]; -[push sendPushInBackground]; -``` -```swift -let channels: [String] = ["Mets"] - -let messageData: [String: Any] = [ - "alert": "The Mets scored! The game is now tied 1-1!", - "badge": "Increment", - "sound": "cheering.caf" -] - -LCPush.send(data: messageData, channels: channels) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -You can also add other custom data. In the section about receiving push notifications, you can see that when the user opens your app from a push notification, the app can access this data. This would be helpful if you wanted to display a specific view controller when the user opens your app from a push notification. - - - -```objc -NSDictionary *data = [NSDictionary dictionaryWithObjectsAndKeys: - @"Ricky Vaughn was injured in last night's game!", @"alert", - @"Vaughn", @"name", - @"Man bites dog", @"newsItem", - nil]; -LCPush *push = [[LCPush alloc] init]; -[push setChannel:@"Indians"]; -[push setData:data]; -[push sendPushInBackground]; -``` -```swift -let channels: [String] = ["Indians"] - -let messageData: [String: Any] = [ - "alert": "Ricky Vaughn was injured in last night's game!", - "name": "Vaughn", - "newsItem": "Man bites dog" -] - -LCPush.send(data: messageData, channels: channels) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -### Set Expiration Date - -A push notification cannot be delivered if the target device is turned off or offline. If you want to send a time-sensitive push notification that you don’t want the user to see after a while, you can set an expiration time for it. - -One method is to specify an expiration time. When the time expires, the server will no longer deliver the push notification. - - - -```objc -NSDateComponents *comps = [[NSDateComponents alloc] init]; -[comps setYear:2013]; -[comps setMonth:10]; -[comps setDay:12]; -NSCalendar *gregorian = - [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; -NSDate *date = [gregorian dateFromComponents:comps]; - -// Send push notification with expiration date -LCPush *push = [[LCPush alloc] init]; -[push expireAtDate:date]; -[push setMessage:@"Season tickets on sale until October 12th"]; -[push sendPushInBackground]; -``` -```swift -let expirationDate = Date(timeIntervalSinceNow: 600) - -let messageData: [String: Any] = [ - "alert": "Season tickets on sale until October 12th" -] - -LCPush.send(data: messageData, expirationDate: expirationDate) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -There’s a problem with the above method: since the clock on the device might not be accurate, you might get a result that’s not what you expected. There’s another method where you can set an interval and the push notification will expire after the interval: - - - -```objc -NSTimeInterval interval = 60*60*24*7; // 1 week - -LCPush *push = [[LCPush alloc] init]; -[push expireAfterTimeInterval:interval]; -[push setMessage:@"Season tickets on sale until October 18th"]; -[push sendPushInBackground]; -``` -```swift -let expirationInterval: TimeInterval = 60*60*24*7 - -let messageData: [String: Any] = [ - "alert": "Season tickets on sale until October 18th" -] - -LCPush.send(data: messageData, expirationInterval: expirationInterval) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -We recommend that you set expiration times for any push notifications you send to iOS devices. This will ensure that if a user has airplane mode enabled when you send a push notification, they will be able to receive the notification when they turn off airplane mode. See [Stackoverflow - Push notification is not being delivered when iPhone comes back online](http://stackoverflow.com/questions/24026544/push-notification-is-not-being-delivered-when-iphone-comes-back-online). - -## Scheduled Push Notifications - -You can set the time at which a push notification is sent; - - - -```objc -NSDateComponents *comps = [[NSDateComponents alloc] init]; -[comps setYear:2013]; -[comps setMonth:10]; -[comps setDay:12]; -NSCalendar *gregorian = - [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; -NSDate *date = [gregorian dateFromComponents:comps]; - -LCPush *push = [[LCPush alloc] init]; -[push setPushDate:date]; -[push setMessage:@"Push this notification on 2013-10-12."]; -[push sendPushInBackground]; -``` -```swift -let pushDate = Date(timeIntervalSinceNow: 6000) -let messageData: [String: Any] = [ - "alert": "Push this notification at a later time." -] -LCPush.send(data: messageData, pushDate: pushDate) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -Scheduled push notifications can also have expiration times. To set an interval: - - - -```objc -LCPush *push = [[LCPush alloc] init]; -[push setPushDate:date]; -[push expireAfterTimeInterval:interval]; -// Other logic -``` -```swift -LCPush.send(data: messageData, pushDate: pushDate, expirationInterval: expirationInterval) { /* Other logic */ } -``` - - - -### Specify a Platform - -For cross-platform apps, you can specify a platform to send a push notification to, such as iOS or Android: - - - -```objc -LCQuery *query = [LCInstallation query]; -[query whereKey:@"channels" equalTo:@"suitcaseOwners"]; - -// Notification for Android users -[query whereKey:@"deviceType" equalTo:@"android"]; -LCPush *androidPush = [[LCPush alloc] init]; -[androidPush setMessage:@"Your suitcase has been filled with tiny robots!"]; -[androidPush setQuery:query]; -[androidPush sendPushInBackground]; - -// Notification for iOS users -[query whereKey:@"deviceType" equalTo:@"ios"]; -LCPush *iOSPush = [[LCPush alloc] init]; -[iOSPush setMessage:@"Your suitcase has been filled with tiny apples!"]; -[iOSPush setChannel:@"suitcaseOwners"]; -[iOSPush setQuery:query]; -[iOSPush sendPushInBackground]; -``` -```swift -let query = LCQuery(className: "_Installation") -query.whereKey("channels", .equalTo("suitcaseOwners")) - -// Notification for Android users -query.whereKey("deviceType", .equalTo("android")) -let messageData: [String: Any] = [ - "alert": "Your suitcase has been filled with tiny robots!" -] -LCPush.send(data: messageData, query: query) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} - -// Notification for iOS users -query.whereKey("deviceType", .equalTo("ios")) -let messageData: [String: Any] = [ - "alert": "Your suitcase has been filled with tiny apples!" -] -LCPush.send(data: messageData, query: query) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -## Receive Push Notifications - -As mentioned in [Customize Notifications](#customize-notifications), you can include any data with the push notifications you send. This data can be used to change the behavior of your app when your app is opened from a push notification. For example, when a user opens a push notification telling them they have a new friend, it would be great if they could see a picture. - -Because Apple limits the size of the message in a push notification, please reduce the size of the data you send in a push notification as much as possible, or the message may be truncated. See [APNs documentation](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html#//apple_ref/doc/uid/TP40008194-CH101-SW1) for more information. - - - -```objc -NSDictionary *data = @{ - @"alert": @"James commented on your photo!", - @"p": @"vmRZXZ1Dvo" // Photo's object id -}; -LCPush *push = [[LCPush alloc] init]; -[push setData:data]; -[push sendPushInBackground]; -``` -```swift -let messageData: [String: Any] = [ - "alert": "James commented on your photo!", - "p": "vmRZXZ1Dvo" // Photo's object id -] - -LCPush.send(data: messageData) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -## Respond to Push Notification Data - -When your app is launched from a push notification, you can access the data in the push notification from the dictionary used by the `launchOptions` parameter of the `application:didFinishLaunchingWithOptions:` method: - - - -```objc -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // ... - if ([[UIDevice currentDevice].systemVersion floatValue] < 10.0) { - NSDictionary *notificationPayload; - @try { - notificationPayload = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; - } @catch (NSException *exception) {} - - // Create a pointer to the Photo object - NSString *photoId = [notificationPayload objectForKey:@"p"]; - LCObject *targetPhoto = [LCObject objectWithoutDataWithClassName:@"Photo" - objectId:photoId]; - - // Fetch photo object - [targetPhoto fetchIfNeededInBackgroundWithBlock:^(LCObject *object, NSError *error) { - // Show photo view controller - if (!error && [LCUser currentUser]) { - PhotoVC *viewController = [[PhotoVC alloc] initWithPhoto:object]; - [self.navController pushViewController:viewController animated:YES]; - } - }]; - } -} -``` -```swift -func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - if let notification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] { - print(notification) - } - - return true -} -``` - - - -If your app is already running when a push notification arrives, you can access the data with the dictionary of the `userInfo` parameter of the `application:didReceiveRemoteNotification:fetchCompletionHandler:` method for iOS version less than 10: - - - -```objc -/*! - * Required for iOS 7+, Xcode 14.1 or later - */ -- (void)application:(UIApplication *)application - didReceiveRemoteNotification:(NSDictionary *)userInfo - fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))handler { - // Create empty photo object - NSString *photoId = [userInfo objectForKey:@"p"]; - LCObject *targetPhoto = [LCObject objectWithoutDataWithClassName:@"Photo" - objectId:photoId]; - - // Fetch photo object - [targetPhoto fetchIfNeededInBackgroundWithBlock:^(LCObject *object, NSError *error) { - // Show photo view controller - if (error) { - handler(UIBackgroundFetchResultFailed); - } else if ([LCUser currentUser]) { - PhotoVC *viewController = [[PhotoVC alloc] initWithPhoto:object]; - [self.navController pushViewController:viewController animated:YES]; - } else { - handler(UIBackgroundFetchResultNoData); - } - }]; -} -``` -```swift -func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - // handle notification -} -``` - - - -For iOS 10 and above, use the following delegate method to get the `userInfo`: - - - -```objc -/** - * Required for iOS 10+, Xcode 14.1 or later - * The method that is called when the app receives a push notification while running in the foreground - */ -- (void)userNotificationCenter:(UNUserNotificationCenter *)center - willPresentNotification:(UNNotification *)notification - withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { - NSDictionary *userInfo = notification.request.content.userInfo; - if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { - // TODO: Handle push notification content - NSLog(@"%@", userInfo); - } - // Run this method and choose whether to notify the user; you can choose Badge, Sound, or Alert - completionHandler(UNNotificationPresentationOptionAlert); -} - -/** - * Required for iOS 10+, Xcode 14.1 or later - * The method that is called when the app is in the background or not running and the user opens a push notification - */ -- (void)userNotificationCenter:(UNUserNotificationCenter *)center -didReceiveNotificationResponse:(UNNotificationResponse *)response - withCompletionHandler:(void (^)())completionHandler { - NSDictionary * userInfo = response.notification.request.content.userInfo; - if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { - // TODO: Handle push notification content - NSLog(@"%@", userInfo); - } - completionHandler(); -} -``` -```swift -func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - // handle notification -} - -func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - // handle notification -} -``` - - - -## Clear the Badge - -The badge is often cleared when the user opens or exits the app. - - - -```objc -- (void)applicationDidBecomeActive:(UIApplication *)application { - // Clear the local badge - [application setApplicationIconBadgeNumber:0]; - // Clear currentInstallation’s badge - [LCInstallation defaultInstallation].badge = 0; - [[LCInstallation defaultInstallation] saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // save succeeded - } else if (error) { - NSLog(@"%@", error); - } - }]; -} -``` -```swift -override func applicationDidBecomeActive(_ application: UIApplication) { - // Clear the local badge - application.applicationIconBadgeNumber = 0 - // Clear currentInstallation’s badge - LCApplication.default.currentInstallation.badge = 0 - LCApplication.default.currentInstallation.save { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } - } -``` - - - - -To learn more about push notifications, check out [Apple’s Local and Remote Notifications Overview](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/Introduction.html#//apple_ref/doc/uid/TP40008194-CH1-SW1). diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/overview.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/overview.mdx deleted file mode 100644 index 20f01ba4e..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/overview.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Push Notification Overview -sidebar_label: Overview -sidebar_position: 0 -slug: /sdk/push/guide/overview/ ---- - - - - - -import {Conditional} from '/src/docComponents/conditional'; - - -Push notifications allow you to deliver messages to users instantly and stay in touch with them, helping to increase user retention and improve the user experience. TapTap Developer Services provides a unified service that allows you to send push notifications to both Android and iOS users. - -In addition to sending push notifications using the iOS and Android SDK, you can also trigger them using the REST API. - - - - -Before you can use the Push Notification service, please make sure you have enabled the service on **App > Settings > Security**. You may need to wait up to 3 minutes after clicking on the button before using the service. - - - - - -Before you can use the Push Notification service, please make sure you have enabled the service on **Developer Center > Your game > Game Services > Configuration**. You may need to wait up to 3 minutes after clicking on the button before using the service. - - - -## Glossary - -### Installation - -An Installation is a unique identifier for a device that accepts push notifications from your application. Each Installation corresponds to an entry in the `_Installation` table. An Installation is essentially an LCObject that contains the following properties: - -Name|Applicable platform|Description ----|---|--- -badge|iOS|The badge at the top of the app icon that shows the number of new notifications. -channels| |The channel to which the device is subscribed. It can contain only uppercase and lowercase letters, numbers, underscores (`_`), hyphens (`-`), equal signs (`=`), and Chinese characters. -deviceProfile||deviceProfile is used to specify the name of the certificate or configuration used by the current device if there are multiple iOS certificates or Android mixpush configurations. Its value must match the certificate name or configuration name set up on **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Settings**, or push notifications won’t be delivered successfully. The value of `deviceProfile` must start with a letter and can only contain uppercase and lowercase letters, numbers, and underscores. You can also leave this field empty. deviceProfile is a special field that only supports `equals` queries. -deviceToken|iOS|A unique identifier used by APNs. -apnsTopic|iOS|This field must be configured if you are using push notification based on Token Authentication. The iOS SDK automatically uses the application’s bundle ID as apnsTopic, but you will need to set this field manually in the following situations: 1. The version of the iOS SDK you are using is below v4.2.0; 2. You are not using the iOS SDK (for example, you are using React Native); 3. You are using a different topic than the bundle ID. -deviceType| |The type of the device. Can be `ios` or `android`. -installationId|Android|A unique identifier generated by the SDK for each Android device. -timeZone| |A string representing the timezone of the device. - -### Notification - -Each Notification corresponds to a record displayed on **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Push records**, which corresponds to a push notification being sent. It contains the following properties: - -Name|Applicable platform|Description ----|---|--- -notificationId| | The ID of the push notification. -msg| |A JSON object that contains the contents of the push notification. See [消息内容参数](/sdk/push/guide/rest#消息内容参数) for more information. -invalidTokens|iOS|The number of [INVALID TOKEN](https://developer.apple.com/library/mac/technotes/tn2265/_index.html#//apple_ref/doc/uid/DTS40010376-CH1-TNTAG32) errors returned by APNs. **If this number seems high, please check the validity of the certificate.** -prod|iOS|The environment of the certificate being used. **dev** means the development environment and **prod** means the production environment. -status| |The status of the push notification. **in-queue** means the push notification is in the queue. **done** means the push notification has been delivered. **scheduled** means the push notification is scheduled and is waiting to be triggered. -devices| |The number of target devices for this push notification, which is the number of valid devices queried from the `_Installation` table when this push notification request is being processed. This may not be the same as the number of devices that actually received the push notification. A device is “valid” if its `valid` property in the `_Installation` table is `true` and its `updatedAt` is within 3 months. Target devices may include inactive devices on which your application has been uninstalled. These devices may not be able to receive the push notification. -successes| |The number of devices to which the push notification was successfully sent. For push notifications sent directly to Android devices, this means that the devices received the push notification. For push notifications sent to iOS devices or sent to Android devices via mixpush, this means that the push notification was sent to APNs or the corresponding mixpush platforms. The number of devices to which the push notification was successfully sent through a particular channel can be found in the `[lc/ios/fcm/hms/mi/oppo/vivo/meizu]Successes` field. -where| |The conditions used to query the `_Installation` table. Devices that match the conditions will receive the push notification. -errors| | If an error occurred with the push notification, this is the error message. -from-service| | **push** means the push notification is triggered directly; **rtm** means the push notification is triggered by a message sent through the Instant Messaging service. -push-time| | If this is a scheduled push notification, this is the time the push notification will be triggered. - -When a push notification is triggered, the server first looks up devices in the `_Installation` table that match the query condition, and then pushes the message to the devices. Because each `_Installation` is a key-value object that can contain custom attributes, you can implement complex conditions for sending push notifications, such as sending push notifications to users who are subscribed to specific channels or who are within a specific geographic area. You can also send push notifications to specific users. - -Note the difference between **devices** and **successes**. If **devices** is 0, it means that there are no devices that match the conditions you specified. In this case, you need to check the conditions and see if they need to be changed. If **devices** is not 0, its value indicates the number of devices found that match the conditions you specified, but it’s not guaranteed that those devices will receive push notifications. It’s likely that **successes** will be less than **devices**. If there are a large number of inactive devices, there may be a large difference between **successes** and **devices**. - -To prevent a device from receiving push notifications, change the `valid` attribute of the object for that device in the `_Installation` table to `false`. - -Note that we only keep push records for one week, and will remove push records that were created more than a week ago. Even if a push record is removed, the push notifications it triggered will still be valid (and received by the target users) as long as they haven’t expired. For more information on how to set expiration times for push notifications, see [推送 REST API 使用指南](/sdk/push/guide/rest/). - -## Unity - -See [Unity Push Notification Guide](/sdk/push/guide/unity/). - -## iOS - -See [iOS Push Notification Guide](/sdk/push/guide/ios/). - -## Android - -With the stricter permission controls enforced by Android, the deliverability of push notifications sent through the Push Notification service’s own channels has been negatively impacted. -Therefore, we recommend that you send push notifications using mixpush. It integrates the interfaces provided by major Chinese handset vendorsFCM so that you can trigger push notifications using a simple API. -See [Android Mixpush Guide](/sdk/push/guide/android-mixpush/) for more information. - -To learn more about Push Notification’s own channels, see [Android Push Notification Guide](/sdk/push/guide/android/). - -## Sending Push Notifications With REST API - -See [Push Notification REST API](/sdk/push/guide/rest/). - -## Sending Push Notifications With JavaScript SDK in Cloud Engine - -The JavaScript SDK also provides an interface for sending push notifications, although it is primarily used in Cloud Engine. See the SDK’s API documentation ([AV.Push](https://leancloud.github.io/javascript-sdk/docs/AV.Push.html)) for more information. -Here are two simple examples: - -To send push notifications to all devices subscribed to the `public` channel: - -```js -AV.Push.send({ - channels: [ 'public' ], - data: { - alert: 'public message' - } -}); -``` - -To send push notifications to devices whose corresponding objects in the `_Installation` table match certain conditions, you can specify an `AV.Query` object as the `where` condition. For example, to send a push notification to an Android device that has a specific `installationId`: - -```js -const query = new AV.Query('_Installation'); -query.equalTo('installationId', installationId); -AV.Push.send({ - where: query, - data: { - alert: 'Public message' - } -}); -``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/push-faq.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/push-faq.mdx deleted file mode 100644 index 452f6cd3f..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/push-faq.mdx +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: Push Notification FAQ -sidebar_label: FAQ -sidebar_position: 11 -slug: /sdk/push/guide/push-faq/ ---- - - - - - -### Why is the number of successful devices less than the target number of devices? - -The "target number of devices" refers to the number of valid devices that meet the conditions of this push request, and the "number of successful devices" refers to the number of devices that were successfully reached by this push. There are several situations where devices are not reached: - -- The application in the Android device is killed and is offline on the network. In this case, the number of successful devices will gradually increase after waiting a while for the devices to come online. -- The Android user deleted or reinstalled the application to generate new device data, and the previous invalid data is included in the "target device count". -- The iOS user deleted or reinstalled the app, and the previous invalid data is included in the target device count, and these numbers are the number of invalidTokens. - -### Why can I only send messages to devices that have been active in the last three months? Is it possible to send to all devices? - -We only allow developers to send messages to devices that have an **updatedAt** value of **within the last three months** in the `_Installation` table. The reason for this is as follows: - -- Devices that have been inactive for three months have a very low probability of users opening the push, and we treat these inactive devices as invalid. -- Cost reasons. Sending push notifications to all devices will tremendously increase the number of invalid devices, thus consuming a large amount of cloud resources, and one of the impacts that can be perceived by users is the push time. - -Due to the above considerations, by default we can only send messages to devices that have been active in the last three months. If you really need to send messages to all devices, you can contact us to upgrade to the Enterprise version of the service and we will create a separate cluster for your application to provide the push service. - -### Why is the number of target devices in the push record 0? - -In the push record of the dashboard, "number of target devices" refers to the number of valid devices that meet the conditions of this push request. If the value is 0, please check **the conditions of the push query and whether the target devices are valid**. - -### Why is the contents of the push record empty? - -In the push record of the dashboard, if different message contents are set for different target device types, the "Contents" column will be displayed empty. You need to click the "ID" of the message to open the push details to see the specific contents. - -### What is the delivery rate for push notifications? - -There is no industry standard for delivery rate. We have tested that the delivery rate of messages from online users is basically 100%. Our SDK has heartbeat and reconnect features to maintain a long connection to the push server as much as possible to improve the speed and reliability of messages reaching users' phones. - -### When sending push notifications with iOS Token Authentication certificates, is there a difference between test and production environments? - -Yes, the prod parameter is used to distinguish between test and production environments. The same key can send messages to both the test and production environments. - -However, the same deviceToken can only be used to successfully push to one environment (either the production environment or the test environment). When sending a push notification to a deviceToken, if it works for the dev, you will get the "invalid Tokens" error for the prod. - -### Some iOS devices are not receiving pushes, and when I check the push log in the console, I see that the number of invalidTokens is greater than 0. What is going on? - -The number of invalidTokens consists of two parts: -* The number of invalidTokens increases if the selected device does not match the selected certificate, for example, if you are using a development certificate to push to a device with a production certificate. Please check if the APNS certificate is expired and if the correct certificate type is used. -* The target device has removed or reinstalled the corresponding application. -* The invalidTokens error is also reported if the team ID is not uploaded when saving the DeviceToken. -* When uploading a certificate to the dashboard using Token Authentication, an invalidTokens error will also be reported if the TeamID or Topics is entered incorrectly (Topics is the Bundle ID of the application). - -### Can I customize the Receiver for Android notifications so that no notification is shown? - -Yes. Please refer to the [Push Notification Guide](/sdk/push/guide/rest/#消息内容参数). - -If you want to customize the receiver, you must include a custom action in the message data. When the client receives the message, it will send an intent event with the action value you defined, and your receiver must also include an `intent-filter` to catch the intent event with the action value. - -### How long is push history kept? - -Push history is kept for 7 days. Push history prior to 7 days cannot be reviewed. - -### Will both devices receive push messages if the same account is signed in on both devices? - -The push is based on the push query criteria, and the target device is found in the _Installation table. If both devices are included in the query criteria, both devices receive the push. - -For the Instant Messaging service, if single sign-on is not enabled, when a user logs in on two devices, if the user is offline, the server will send push notifications to both devices. - -### The console push log shows a successful push, but the Android device did not actually receive the push. What is the reason for this? - -If the number of successes returned is 1, it means that a response must have been received from the SDK confirming receipt of the message. This means that the push message must have reached the device. -It is recommended to check if the push uses the custom Receiver (whether there is an action field in the message) where the message arrives and the SDK passes it directly to the custom Receiver that completes the push notification. In this case, you must check the custom Receiver implementation logic to troubleshoot why the notification does not pop up when the message arrives. - -### Workaround for older versions of the Objective-C SDK not receiving pushes in the iOS 13 environment. - -In iOS 13, older versions of the Objc SDK (<= 11.6.6) are unable to upload a valid device token due to Apple's change in the API of the underlying framework. - -The workaround is to upgrade the SDK to v11.6.7 and above and save the device token as described in [Save the Token](/sdk/push/guide/ios/#save-the-token). - -The workaround for older versions of the Objective-C SDK (<= 11.6.6) is to upload the device token as follows: - -``` -NSUInteger dataLength = deviceToken.length; -if (dataLength > 0) { - const unsigned char *dataBuffer = deviceToken.bytes; - NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)]; - for (int i = 0; i < dataLength; ++i) { - [hexString appendFormat:@"%02.2hhx", dataBuffer[i]]; - } - [installation setDeviceToken:[hexString copy]]; - [installation saveInBackground]; -} -``` - -### I get the error "Some pushes are rejected by APNs due to BadDeviceToken" when sending push notifications to iOS devices. - -This error occurs because the push environment is incorrect. For example, if the push is sent to a device in the test environment using the prod parameter (i.e. `"prod": "dev"`), the device in the production environment will not receive the push. -Similarly, if you specify to push to the production environment (i.e., `"prod": "prod"`), devices in the test environment will not receive the push. -For the push environment for iOS, see [Environments](/sdk/push/guide/ios/#environments). - -### How do I select a push certificate for iOS? -The push interface has a parameter called **prod**. -**This parameter is only valid for iOS push.** - -* When sending iOS pushes using Token Authentication, this parameter is used to set whether to send the push to the development (dev) or production (prod) environment of the APNs. -* For iOS pushes using certificate authentication, this parameter sets whether to use development (dev) or production (prod) certificates. When using the certificate authentication method, if the device has a deviceProfile set in the Installation table, we give priority to pushing with the certificate specified by the deviceProfile. - -### How is the deviceToken stored correctly for iOS push? - -The device token changes when a user erases their device, restores the app from backup, or installs the app on a new device. To solve this problem, Apple recommends that the app requests the device token of the APNs when opening, and then sets and saves the token. Also, our system keeps track of the update times (`updatedAt`) of all Installation objects and removes the objects that haven’t been updated in a while. -Sample code can be found in: [iOS Push Guide](/sdk/push/guide/ios/#save-the-token). - -### For iOS, what can I do if the app does not receive a push when it is in the foreground? - -The push log will show that the push was successfully delivered, but the push is not received on the mobile side. This is because, by default, the push is not displayed in the notification bar when the iOS app is in the foreground. If the app still needs to display the push, you must use the UNUserNotificationCenterDelegate delegate method `userNotificationCenter:willPresentNotification:withCompletionHandler:` to handle how the notification is displayed. - -Available methods for displaying the push notification include: -* UNNotificationPresentationOptionBadge: Adds the value of badge to the application icon. -* UNNotificationPresentationOptionBanner: Presents the notification with a banner. -* UNNotificationPresentationOptionList: Display the notification in the notification center. -* UNNotificationPresentationOptionSound: Play the sound for the notification. - -The sample code to display the notification as a banner is as follows: - -```objc -#import "AppDelegate.h" -#import -#import - -@interface AppDelegate () -@end -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - - //The code to initialize the SDK is omitted - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - //Set the delegate object - center.delegate = self; - return YES; -} -- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{ - completionHandler(UNNotificationPresentationOptionBanner); -} -``` - -### How is _Installation associated with the user id in _User in the offline push notification service? - -After a user logs in to the Instant Messaging system, the server stores the user's `clientId` in the `channels` field of the device's `_Installation`, completing the association. If the user is offline and there are offline messages to push, the server will go to the `_Installation` table and find the device with the `channels` field containing the target `clientId` to complete the push. - -### If there are two applications on the same device, how do I push to the specified application? - -The _Installation table holds the installation information generated on the device. A new field can be added to the _Installation table to distinguish between the different applications. When you push a message, if you choose to push to all users, both applications will receive the push. If you want to push to a specific application, you can refer to the push document [Send to Specific Users](/sdk/push/guide/android/#send-to-specific-users) to push only to the specified device. - -### What does the valid field in _Installation refer to and why is it false? - -`valid` indicates whether the device record is currently valid. `false` means the record is invalid. A possible reason for this is that push has not been used for a long time, or the device is not registered. - -For example, if an Android device does not execute the following code to [Enable the Push Notification Service](/sdk/push/guide/android/#enable-the-push-notification-service), the value of valid will always be false. - -``` -// Set the default opened Activity -PushService.setDefaultPushCallback(this, PushDemo.class); -``` -If a device does not want to receive push notifications, you can also set the valid field of the corresponding installation object in the _Installation table to false. - -## Troubleshooting - -Troubleshooting push notifications can be tricky because there are many device- and network-related steps involved in the push process, and the calls are asynchronous. Here are some tips to help you troubleshoot push issues. - -### Query Push Results - -All messages sent through the `/push` interface can be viewed in the push log in the dashboard's message menu. Each time `/push` is invoked, a new push record is created to represent a push. For the meaning of the properties in this table, see [the Notification table](/sdk/push/guide/overview/#notification). - -The `/push` interface returns the `objectId` of the newly created push record, so you can look up the results of the push in the push record based on the ID. - -### Suggestions for iOS Troubleshooting - -Some suggestions for troubleshooting iOS push issues: - -* Make sure you are using the correct **Bundle Identifier** in your project's Info.plist file. -* Make sure the correct **provisioning profile** is set in **Project** > **Build Settings**. -* Try cleaning the project and restarting Xcode. -* Try going to [Apple Developer](https://developer.apple.com/account/overview.action) to regenerate the **provisioning profile**, changing the Apple ID, changing it back, and regenerating the profile. You'll need to reinstall the provisioning profile and reset it in **Project** > **Build Settings**. -* Open XCode Organizer and remove all expired and unused provisioning profiles from your computer and iOS devices. -* If the app builds and runs fine, but you still don't receive pushes, make sure your app has permission to receive pushes enabled by checking in **Settings** > **Notifications** > **Your App** on your iOS device. -* If the permissions are also OK, make sure you are using the correct **provisioning profile**. Package your application. If you uploaded a development certificate and pushed with a development certificate, you must build your application using the **Development Provisioning Profile**. If you are uploading a production certificate and pushing with a production certificate, make sure your application is packaged using the **Distribution Provisioning Profile** signature. Both **Ad Hoc** and **App Store Distribution Provisioning Profile** can be used to receive messages sent with a production certificate. -* When enabling push to an existing Apple ID, remember to regenerate the **provisioning profile** and update it in XCode Organizer. -* Production push certificates must be enabled and generated before you submit your app to the App Store, or you will need to resubmit it to the App Store. -* Please test the production environment push using the Ad Hoc Profile before submitting your app to the App Store. This is the configuration closest to the one used by the App Store. -* Check the `devices` and `status` in the push log to make sure the push status and number of receiving devices are normal. -* Check the `invalidTokens` field in the push log. If the number is abnormally high, the certificate may have been misselected and does not match the provisioning profile of the device build. -* It is recommended that you use a serial queue for the installation to avoid a crash when saving the installation due to multi-threaded writes. You can see the [VoIP project in the Swift demo][swift-voip-demo]. - -[swift-voip-demo]: https://github.com/leancloud/swift-sdk-demo#voip - -### Suggestions for Android Troubleshooting - -Some suggestions for troubleshooting Android push issues: - -* Make sure the device has called `AVInstallation` correctly to store device information in the _Installation table. -* You can check if the device is online with `installationId` in the dashboard at **Push Notification** > **Devices**. -* Make sure that `com.avos.avoscloud.PushService` is added to the AndroidManifest.xml file. -* If you are using a custom Receiver, make sure you declare your Receiver in the AndroidManifest.xml and make sure the action is consistent in the data. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/rest.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/rest.mdx deleted file mode 100644 index a6f2a00c0..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/rest.mdx +++ /dev/null @@ -1,823 +0,0 @@ ---- -title: Push Notification REST API -sidebar_position: 8 -slug: /sdk/push/guide/rest/ ---- - - - - - -import { Conditional } from "/src/docComponents/conditional"; - -Once the application is installed on the user's device, the SDK automatically generates an Installation object when you use the push feature. The Installation object contains all the information needed to send push notifications. You can use the REST API to push through the Installation object. - - - - - -The base URL for the request can be found at **App > Settings > App keys > Server URLs**. -For POST and PUT requests, the request body must be in JSON format and the Content-Type of the HTTP header must be set to `application/json`. - - - - - - -The base URL for the request can be found at **Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings**. -For POST and PUT requests, the request body must be in JSON format and the Content-Type of the HTTP header must be set to `application/json`. - - - -Requests are authenticated by the key-value pairs contained in the HTTP header, as described in the [Request Format](/sdk/storage/guide/rest/#request-format) section of the *Data Storage REST API*. - -## Installation - -You can add Installation objects to the cloud using the REST API. -Using the REST API also allows you to do things that the client SDK cannot do, such as querying all Installations to find a collection of subscribers to a channel. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    URLHTTPFunction
    /1.1/installationsPOSTUpload Installation
    /1.1/installations/<objectId>GETGet Installation
    /1.1/installations/<objectId>PUTUpdate Installation
    /1.1/installationsGETQuery Installation
    /1.1/installations/<objectId>DELETEDelete Installation
    - -### Add Installation - -Creating an Installation object is similar to creating a normal object, except that different platforms have different fields. - -Upon successful creation, the HTTP return value is **201 Created** and the Location header contains the URL of the new Installation: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -The body returned is a JSON object, including the objectId and createdAt, the timestamp of the created object. - -```json -{ - "createdAt": "2012-04-28T17:41:09.106Z", - "objectId": "51ff1808e4b074ac5c34d7fd" -} -``` - -#### DeviceToken - -iOS devices typically use DeviceToken to uniquely identify a device. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "deviceType": "ios", - "deviceToken": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", - "channels": [ - "public", "protected", "private" - ] - }' \ - https://{{host}}/1.1/installations -``` - -#### installationId - -For Android devices, the SDK will automatically generate a uuid as the installationId to store in the cloud. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "deviceType": "android", - "installationId": "12345678-4312-1234-1234-1234567890ab", - "channels": [ - "public", "protected", "private" - ] - }' \ - https://{{host}}/1.1/installations -``` - -The `installationId` must be unique within the application. - -### Get Installation - -You can get an Installation object by requesting the URL represented by the Location at the time of creation using the GET method. For example, to get the object created above: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -The returned JSON object contains all of the fields provided by the user, as well as the createdAt, updatedAt, and objectId fields: - -```json -{ - "deviceType": "ios", - "deviceToken": "abcdefghijklmnopqrstuvwzxyrandomuuidforyourdevice012345678988", - "channels": [ - "" - ], - "createdAt": "2012-04-28T17:41:09.106Z", - "updatedAt": "2012-04-28T17:41:09.106Z", - "objectId": "51ff1808e4b074ac5c34d7fd" -} -``` - -### Update Installation - -The Installation object can be updated by sending a PUT request to the appropriate URL. For example, to subscribe to a push channel, set the `channels` property to: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "deviceType": "ios", - "deviceToken": "abcdefghijklmnopqrstuvwzxyrandomuuidforyourdevice012345678988", - "channels": [ - "", - "foo" - ] - }' \ - https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -To unsubscribe from a channel: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "channels": { - "__op":"Remove", - "objects":["customer"] - } - }' \ - https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -`channels` is essentially an array property, so you can use standard array operations. - -Another example is adding custom attributes: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "userObjectId": "" - }' \ - https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -### Query Installation - -You can retrieve multiple Installations at once by sending a GET request to the root URL of installations. This feature is not available in the SDK. - -Without any URL parameters, a GET request will list all Installations: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/installations -``` - -The result field of the returned JSON object contains all results: - -```json -{ - "results": [ - { - "deviceType": "ios", - "deviceToken": "abcdefghijklmnopqrstuvwzxyrandomuuidforyourdevice012345678988", - "channels": [ - "" - ], - "createdAt": "2012-04-28T17:41:09.106Z", - "updatedAt": "2012-04-28T17:41:09.106Z", - "objectId": "51ff1808e4b074ac5c34d7fd" - }, - { - "deviceType": "ios", - "deviceToken": "876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9", - "channels": [ - "" - ], - "createdAt": "2012-04-30T01:52:57.975Z", - "updatedAt": "2012-04-30T01:52:57.975Z", - "objectId": "51fcb74ee4b074ac5c34cf85" - } - ] -} -``` - -All queries on normal objects work on Installation objects, so see the previous queries section for details. An array query of channels allows you to find all devices subscribed to a particular push channel. - -For security reasons, the Installation lookup permission is not open by default in the cloud, so this interface typically requires master key authentication. - -### Delete Installation - -To remove an Installation from LeanCloud, you can send a DELETE request to the appropriate URL, which is also not available in the client SDK. Example: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/installations/51fcb74ee4b074ac5c34cf85 -``` - -For security reasons, the Installation deletion permission is not open by default in the cloud, so this interface typically requires master key authentication. - -### Automatic Installation Expiration and Cleanup - -Whenever a user opens an application, we update the `updatedAt` timestamp in the `_Installation` table for that device. If the user has not updated the `updatedAt` timestamp in the `_Installation` table for a long time, it means that the user has not opened the application for a long time. If the user has not opened the application for more than 90 days, we will remove the user's record from the `_Installation` table. But don't worry, if the user opens the application again, a new Installation will be automatically created for pushing. - -For iOS devices, there is another expiration mechanism in addition to the one described above. When we get feedback from Apple's push service that a device's deviceToken has expired, we also remove the device from the `_Installation` table, mark the expired deviceToken as invalid, and discard any subsequent messages sent to the deviceToken. - -### API Interfaces at a Glance - -Path|Method|Description ----|---|--- -/1.1/push|POST|Send a push notification -/1.1/notifications|GET|Get push records -/1.1/notifications/:notification_id|GET|Get a push record by its ID -/1.1/notifications/:notification_id|DELETE|Delete a push record by its ID -/1.1/scheduledPushMessages|GET|Get all scheduled push notifications -/1.1/scheduledPushMessages/:id|DELETE|Delete a scheduled push notification by its ID - -### master key Verification - -If **Prevent clients from sending push notifications** is checked in **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Settings**, -you will need to pass the **master key** to send pushes, which will prevent the client from being able to push messages to any target device in the app without restriction. -This restriction is enabled by default. -We recommend that all users enable this restriction. - -### Message Content Parameters - -#### iOS Device Push Message Content Parameters - -For the specific meaning of the attributes within data and alert in iOS devices, please refer to: -1. [Apple's official documentation on the Payload Key](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification), -2. [Apple's official documentation on the Request Header](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html), and -3. [Apple's official documentation on the UserNotifications](https://developer.apple.com/documentation/usernotifications). - -Here are some specific descriptions of each property: - -#### Properties of data for iOS Devices - -Name|Format|Constraint|Description ----|---|---|--- -alert|Plain string or JSON string|Required|The content of the message. If the target device contains only iOS devices, it can also be of the JSON type; the supported properties for the JSON type are described below. -title|String|Optional|The title of the push content. If the alert field is a string, you can add the title here, but if the alert is of JSON type, you do not need to provide this field. -category|String|Optional|Notification type. -thread-id|String|Optional|Name of the notification category. -badge|Number|Optional|The number of unread messages displayed on the badge above the application icon, either a number or the string "Increment" (case sensitive). -sound|Plain string or JSON string|Optional|The sound of the push notification. See the section describing JSON objects below for more details. -content-available|Number|Optional|Set to 1 to start a background download when using the Newsstand. -mutable-content|Number|Optional|Used to support UNNotificationServiceExtension. Set to 1 to enable it. -collapse-id|String|Optional|Corresponds to the apns-collapse-id parameter of the APNs request header, which is used to collapse multiple notifications as described in Apple's official request header documentation linked below. -apns-priority|Number|Optional|Can only be 10 or 5. Corresponds to the apns-priority parameter of the APNs request header, which is used to control whether notifications are sent in power saving mode. Please click the link below for Apple's official request header documentation for details. -apns-push-type|String|Optional|Used to set the push display type. Supported on iOS 13 or watchOS 6 or higher. Can only be "background" or "alert". Default is "alert". -url-args|String|Optional|A list for Safari notifications. See the APNs documentation for details on the url-args parameter. -target-content-id|String|Optional|See the APNs documentation for details on the target-content-id parameter. -Custom attribute|Custom type|Optional|Custom attributes added by the user, with arbitrary field names and field types. - -Example: - -```json -{ - "alert": "hi" -} -``` - -#### Properties of alert for iOS Devices - -iOS devices support localized `alert` message pushing by replacing the above `alert` parameter from a string to JSON consisting of localized message pushing attributes: - -Name|Format|Constraint|Description ----|---|---| --- -title|String|Optional|The title of the notification content. -title-loc-key|String list|Optional|See Apple's push notification localization guide for details. -title-loc-args|String list|Optional|See Apple's push notification localization guide for details. -subtitle|String|Optional|The subtitle of the notification content. -subtitle-loc-key|String|Optional|See Apple's push notification localization guide for details. -subtitle-loc-args|String list|Optional|See Apple's push notification localization guide for details. -body|String|Optional|The body of the message. -action-loc-key|String|Optional|See Apple's push notification localization guide for details. -loc-key|String|Optional|See Apple's push notification localization guide for details. -loc-args|String list|Optional|See Apple's push notification localization guide for details. -launch-image|String|Optional|Sets the name of the image file to launch when the notification is clicked. -summary-arg|String|Optional|Used to set the summary. -summary-arg-count|Number|Optional|Used to set the number of summary arguments. - -Example: - -```json -{ - "alert": { - "title": "A message" - "body": "A body" - } -} -``` - -#### Properties of sound for iOS Devices - -iOS supports setting the push sound through the sound parameter, either with a string indicating the filename of a sound, pointing to a sound file that exists in the app, or with a JSON object: - -Name|Format|Constraint|Description ----|---|---|--- -name|String|Optional|Sound filename, pointing to a sound file that exists in the application. -critical|Boolean|Optional|Set to true to use the "Critical" sound. Default is false. -volume|Number|Optional|The volume of the sound. Must be a decimal number between 0 and 1. - -Example: - -```json -{ - "alert": "New weixin message.", - "badge": 9, - "sound": "biubiubiu.aiff" -} -``` - -```json -{ - "alert": "You spent $1.99", - "sound": { - "name": "ding.aiff", - "volume": "0.8" - } -} -``` - -#### Additional Notes for iOS Devices - -We support the construction of push parameters as described in the official Apple documentation above. For example: - -```json -{ - "aps": { - "alert": "New weixin message.", - "badge": 9, - "sound": "biubiubiu.aiff" - } -} -``` - -For a more detailed description, see the following example: - -```json -{ - "aps": { - "alert": { - "title": "String; the title of the push content", - "title-loc-key": "A list of strings; see Apple's note on localizing push notifications for details", - "title-loc-args": "A list of strings; see Apple's note on localizing push notifications for details", - "subtitle": "String; the subtitle of the push content", - "subtitle-loc-key": "String; see Apple's note on localizing push notifications for details", - "subtitle-loc-args": "A list of strings; see Apple's note on localizing push notifications for details", - "body": "String, the body of the message", - "action-loc-key": "String; see Apple's note on localizing push notifications for details", - "loc-key": "String; see Apple's note on localizing push notifications for details", - "loc-args": "A list of strings; see Apple's note on localizing push notifications for details", - "launch-image": "String; the name of the image file to launch when the notification is clicked", - "summary-arg": "String; used to set the summary", - "summary-arg-count": "Number; used to set the number of summary arguments" - }, - "category": "String; notification type", - "thread-id": "String; name of the notification category", - "badge": "Number; the number of unread messages displayed on the badge above the application icon, either a number or the string 'Increment' (case sensitive)", - "sound": "Plain string or JSON string; the sound of the push notification", - "content-available": "Number; set to 1 to start a background download when using the Newsstand", - "mutable-content": "Number; used to support UNNotificationServiceExtension; set to 1 to enable it" - }, - "collapse-id": "String; corresponds to the apns-collapse-id parameter of the APNs request header, which is used to collapse multiple notifications as described in Apple's official request header documentation linked below", - "apns-priority": "Number; can only be 10 or 5; corresponds to the apns-priority parameter of the APNs request header, which is used to control whether notifications are sent in power saving mode; please click the link below for Apple's official request header documentation for details", - "apns-push-type": "String; used to set the push display type; supported on iOS 13 or watchOS 6 or higher; can only be 'background' or 'alert'; default is 'alert'", - "custom-key": "Custom attributes added by the user, with arbitrary field names and field types" -} -``` - -#### Android Device Push Message Content Parameters - -For Android devices, the default notification message content parameter supports the following properties: - -Name|Format|Constraint|Description ----|---|---|--- -alert|String|Required|The content of the message. -title|String|Optional|The title of the push content. -silent|Boolean|Optional|Whether to send the push notification as a pass-through message or as a notification bar message. The default is false, i.e. `notification bar message`. -action|String|Optional|The action name provided when the Receiver was registered. Set this only if the Receiver needs to be customized. -Custom attribute|Custom type|Optional|Custom attributes added by the user, with arbitrary field names and field types. - -Example: - -```json -{ - "alert": "Hi Ming, you have guests at home. Come home for dinner!", - "title": "Ming, you received a WeChat message." -} -``` - -```json -{ - "alert": "You received $1.99.", - "my-custom-key": "my-custom-value" -} -``` - -### Push by Query Criteria - -This interface is used to send a push message to all valid device records in the _Installation table that match the query criteria, based on the query criteria provided. For example, the following code sends a push message containing `Hello from LeanCloud` to all valid devices that contain the value `public` in the `channels` field of the _Installation table. - -Note that this interface limits the requested HTTP body size to 4096 bytes, i.e., the result of JSON serialization of any parameters you pass to this interface cannot exceed this limit. - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "where": {"channels" : "public"}, - "data": {"alert" : "Hello from LeanCloud"} - }' \ - https://{{host}}/1.1/push -``` - -The parameters supported by this interface are: - -Name| Constraint | Description ----|--- | --- -data| **Required**| The content of the push. JSON object. See [Message Content Parameters](#message-content-parameters). -where| Optional | The query condition used to retrieve objects from the `_Installation` table. JSON object. If the query condition contains a special type of data that must be encoded, such as date or binary, the query condition must contain the encoded data. For example, if the `createdAt` field is greater than a certain time, the where condition must be `{"createdAt":{"$gte":{"__type":"Date","iso":"2015-06-21T18:02:52.249Z"}}}`. For more information, see the description in the *Advanced Data Types* section of the [Data Storage REST API](/sdk/storage/guide/rest/). -channels| Optional | Which channels to push to. Added to the where object as a condition. -push_time| Optional | Sets the time at which the scheduled push is to be sent, which must be in UTC time and ISO8601 format, e.g. `2019-04-01T06:19:29.000Z`. Note that if the send time is less than 1 minute from the current time, the push will be sent immediately and will not follow the push_time parameter. If you need to implement periodic push, you can refer to [Implement Periodic Push Using Cloud Engine](#implement-periodic-push-using-cloud-engine). -expiration_time| Optional | The absolute date and time when the message expires, in UTC time and ISO8601 format, for example, "2019-04-01T06:19:29.000Z". If the message is received by the client later than the message expiration time, the message is not displayed to the user. -expiration_interval| Optional | The relative time in seconds for the message to expire. Counted from `push_time`, or from the time of the API call if `push_time` is not specified. It is recommended that `expiration_time` or `expiration_interval` be set for all pushes to avoid users getting a bunch of expired pushes after a long disconnect and reconnect. -notification_id | Optional | Custom push id. Up to 16 characters long and can only be letters and numbers. If this parameter is not provided, we randomly assign a unique push id to each push request to distinguish between different pushes. The push id is used to count the number of target devices and messages reached, and is displayed in the push log in the console. Custom push ids can be used to combine multiple different requests under the same push id to get an overall count of the number of target devices and final message arrivals for a batch of push requests. -req_id | Optional | Custom request id. Can be up to 16 characters long and can only contain letters and numbers. Different push requests with the same req_id within 5 minutes are considered duplicate requests and will be sent only once. The user can resend the request once with a unique req_id in the request to avoid missing failed push requests in case of an exception such as an interface timeout. And since the req_id is the same in both requests, we automatically filter duplicate push requests to ensure that each target end user receives at most one push message. Please note that **too many or too frequent retries can interfere with normal message delivery**. -prod| Optional | ***Valid for iOS push only***. When sending iOS pushes using token authentication, this parameter is used to set whether to send the push to the development (***dev***) or production (***prod***) environment of the APNs. When sending iOS pushes using certificate authentication, this parameter is used to set whether to use a development certificate (***dev***) or a production certificate (***prod***). If `prod` is not specified and the HTTP header `X-LC-Prod` is passed without a value of 1, then it will be treated as `"prod": "dev"`, otherwise the default is `"prod": "prod"`. When using the certificate authentication method, we give priority to the certificate specified by deviceProfile if the device has deviceProfile set in the Installation record. -topic | Optional | ***Only valid for iOS push with Token Authentication***. When using token authentication to send iOS push, you need to provide the APNs topic of the device for authentication. Normally, the iOS SDK automatically reads the bundle ID of the iOS application as the topic in the apnsTopic field of the Installation record, so this parameter is not required in the push request. However, it must be specified manually in the following cases: 1. using an iOS SDK lower than v4.2.0; 2. not using the iOS SDK (e.g. using React Native); 3. using a topic different from the iOS bundle ID for the target device. -apns_team_id | Optional | ***Valid only for iOS push using Token Authentication***. If you are using Token Authentication to send iOS push, you need to provide the corresponding Team ID of the device for authentication. In general, if all APNs Topics are not duplicated under your Team ID, or if you have proactively set the apnsTeamId when saving the Installation, you do not need to provide this parameter; we will match the appropriate Team ID for each device to send the push. Otherwise, this parameter must be provided and you must ensure that the target devices of a single push request belong to the Team ID specified in this parameter through the where-query condition to ensure that the push is performed correctly. -flow_control | Optional | Whether to enable smoothing. Disabled by default. The value represents the pushing speed, i.e. the number of target end users per second. The minimum value is 1000, and any value below the minimum is treated as the minimum. -_notificationChannel | Optional | For Android 8.0 or higher, you need to pass the channel id to receive pushes properly. Please refer to the "Adapting for Android 8.0" section in the [Android Push Guide](/sdk/push/guide/android/). - -All properties in the `_Installation` table, whether built-in or custom, can be specified as query conditions by where, and all kinds of complex queries are supported. - -The objectId of a push is returned on success, and the returned objectId can be used to [query the push record](#query-push-record). - -```json -{"objectId":"i4OcyCnyjckJOtzz","createdAt":"2021-11-23T08:05:54.921Z"} -``` - -If it fails, the server will return an error code and the reason for the error: - -```json -{"code":107,"error":"Malformed json object. A json dictionary is expected."} -``` - -Some examples are given below. For more examples, see the "Queries" section of the [Data Storage REST API](/sdk/storage/guide/rest/). - -#### Push to All Devices - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "data": { - "alert": "Greetings!" - } - }' \ - https://{{host}}/1.1/push -``` - -#### Push to Android Devices - -```sh -curl -X POST \ --H "X-LC-Id: {{appid}}" \ --H "X-LC-Key: {{masterkey}},master" \ --H "Content-Type: application/json" \ --d '{ - "where":{ - "deviceType": "android" - }, - "data": { - "alert": "Greetings!" - } - }' \ -https://{{host}}/1.1/push -``` - -#### Push to Devices on the public Channel - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "channels":"public", - "data": { - "alert": "Greetings!" - } - }' \ - https://{{host}}/1.1/push -``` - -#### Push to Inactive Devices - -```sh -curl -X POST \ --H "X-LC-Id: {{appid}}" \ --H "X-LC-Key: {{masterkey}},master" \ --H "Content-Type: application/json" \ --d '{ - "where":{ - "updatedAt":{ - "$lt":{"__type":"Date","iso":"2015-06-29T11:33:53.323Z"} - } - }, - "data": { - "alert": "Greetings!" - } - }' \ -https://{{host}}/1.1/push -``` - -#### Push to Devices With Matching Custom Properties - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "where": { - "preOrder": true - }, - "data": { - "alert": "The sale is on!" - } - }' \ - https://{{host}}/1.1/push -``` - -The `where` queries are all about the properties in the `_Installation` table. This assumes that the table stores the boolean attribute `preOrder`. - -#### Push Based on Geographic Location - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "where": { - "owner": { - "$inQuery": { - "location": { - "$nearSphere": { - "__type": "GeoPoint", - "latitude": 30.0, - "longitude": -20.0 - }, - "$maxDistanceInMiles": 10.0 - } - } - } - }, - "data": { - "alert": "The highest temperature in Beijing tomorrow will be 40 degrees Celsius." - } - }' \ - https://{{host}}/1.1/push -``` - -The above example assumes that the installation has an owner attribute pointing to a record in the `_User` table, and the user has a `location` attribute of type GeoPoint, so we can do a push based on geographic location. - -### Push by Device ID List - -This interface is used to send pushes to a list of specified device IDs. The push process is faster and has lower latency than the query method because it does not query the Installation records of the target devices. For example, the following code is used to push a `Hello` message to the iOS devices with device token "device_token1", "device_token2", and "device_token3". - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "data": {"alert" : "Hello"}, - "device_type": "ios", - "device_ids": ["device_token1", "device_token2", "device_token3"] - }' \ - https://{{host}}/1.1/push/devices -``` - -The parameters of this interface consist of two parts: push-channel-independent generic parameters and push-channel-specific channel parameters. The generic parameters are channel-independent, so you can use them for either iOS or Android devices. - -The supported generic parameters are: - -Name| Constraints | Description ----|--- | --- -device_type | **Required** | Target device type. Can only be android or ios. You can only push to one device type at a time. -device_ids | **Required** | The list of target device IDs. Up to 500 IDs. For iOS devices, the device ID is the deviceToken field in the _Installation table; for Android devices, the device ID is the installationId field in the _Installation table. -data| **Required**| Same as [pushing by query criteria](#push-by-query-criteria). -expiration_interval| Optional | Same as above. -expiration_time| Optional | Same as above. -notification_id | Optional | Same as above. -req_id | Optional | Same as above. - -If the target devices are iOS devices, the following parameters can be included in addition to the generic parameters above: - -Name| Constraints | Description ----- | ---- | ---- -prod| Optional | Same as [pushing by query criteria](#push-by-query-criteria). -topic | Optional | Same as above. -apns_team_id | Optional | Same as above. -device_profile | Optional | Specifies the custom iOS push certificate to use. This parameter is not required if you are using token authentication or if the push certificate is a configured "production environment certificate" or "development environment certificate". We will use the appropriate certificate based on the value of the `prod` parameter you provide. - -If the target devices are Android devices, the following parameters can be included in addition to the general parameters mentioned above: - -Name| Constraints | Description ----- | ---- | ---- -channel| Optional | Specifies the [Android Notification Channel][android-channel]. - -[android-channel]: https://developer.android.com/develop/ui/views/notifications#ManageChannels - -### Expiration Time and Scheduled Push - -As mentioned above, you can specify the expiration time of a message using the `expiration_time` parameter: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "expiration_time": "2015-10-07T00:51:13Z", - "data": { - "alert": "Your coupon expires on October 7th." - } - }' \ - https://{{host}}/1.1/push -``` - -`expiration_interval` can also be used to specify an expiration time, usually used with a scheduled push: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "push_time": "2016-01-28T00:07:29.773Z", - "expiration_interval": 86400, - "data": { - "alert": "This push was sent on January 28th at 8:07 BST and will expire in 24 hours (86400 seconds)" - } - }' \ - https://{{host}}/1.1/push -``` - -#### Retrieving and Canceling Scheduled Pushes - -Call the `POST /scheduledPushMessages` interface to retrieve scheduled push tasks that are currently waiting to be pushed. Calling this interface requires the **master key**: - - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/scheduledPushMessages -``` - -The query will return a result similar to - -```json -{ - "results": [ - { - "id": 1, - "expire_time": 1373912050838, - "push_msg": { - "through?": null, - "app-id": "OLnulS0MaC7EEyAJ0uA7uKEF-gzGzoHsz", - "where": { - "sort": { - "createdAt": 1 - }, - "query": { - "installationId": "just-for-test", - "valid": true - } - }, - "prod": "prod", - "api-version": "1.1", - "msg": { - "message": "test msg" - }, - "id": "XRs9jmWnLd0GH2EH", - "notificationId": "mhWjvHvJARB6Q6ni" - }, - "createdAt": "2016-01-21T00:47:46.000Z" - } - ] -} -``` - -Here `push_msg` is the details of the push message and `expire_time` is the Unix timestamp of the time at which the message is set to be pushed. - -Based on the result of the query, you can cancel a scheduled push. -Note that you must use the outermost id in the returned result. -For example, to cancel the first scheduled push, use `results[0].id` instead of `results[0].push_msg.id`. -For the above example, `1` should be used instead of `XRs9jmWnLd0GH2EH`: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/scheduledPushMessages/1 -``` - -#### Implement Periodic Push Using Cloud Engine - -You can use the scheduled task feature provided by Cloud Engine to implement periodic pushes. - -## Query Push Record - -The `/push` interface returns an `objectId` representing the push message after the push, and you can use this ID to call the following API to query the push record: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/tables/Notifications/:objectId -``` - -The `:objectId` in the URL should be replaced with the objectId returned by the `/push` interface. - -The push record object will be returned. Refer to the section "Notification" in [Push Notification Overview](/sdk/push/guide/overview/) for the meaning of each field of the push record. - -## Viewing Push Status and Canceling Pushes - -During the process of sending a push, we will update the push status as the push task is executed in **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Push records**, where you can check the latest status of the push. For a description of the different push statuses, please refer to the Notification section of the [Push Notification Overview](/sdk/push/guide/overview/). - -Before the status of a push record reaches **done**, that is, before the push is complete, the "Cancel" button will appear next to the status information, and you can cancel the push by clicking it. The canceled push will be deleted from the push record. - -Note that canceling a push means canceling a push that is still in the queue and has not yet been sent. Similarly, pushes that have already been sent or stored in the offline cache cannot be canceled. Please do your best to test and confirm the content and query conditions of the target devices before sending pushes. - -## Restrictions - -* To avoid sending messages to a large number of users who are no longer active, we restrict push messages to devices with an `updatedAt` time in the `_Installation` table within the last three months. We will automatically exclude the devices that do not meet the criteria from the target devices after determining the target devices based on the push query criteria, and the excluded devices will not be counted in the target devices in **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Push records**. -* To prevent performance issues due to large numbers of certificate errors, we limit the number of devices that can be pushed using **development certificates** to a maximum of 20,000 devices at a time. If more than 20,000 devices meet the push criteria, the system will reject the push and display "Error" in the **Status** column in **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Push records** with the message "dev profile disabled for massive push". Please be sure to set the push conditions appropriately when using the developer certificate. -* Apple has a limit on the size of push messages, so please try to reduce the size of data to be sent for iOS push, otherwise it will be truncated. Please refer to the [APNs documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) for details. - -If a push fails, you will see an error message in the **Status** section of **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Push records**. - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/unity.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/unity.mdx deleted file mode 100644 index a62f7f873..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/push/guide/unity.mdx +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: Unity Push Guide -sidebar_label: Unity Push -sidebar_position: 5 -slug: /sdk/push/guide/unity/ ---- - - - - - -import {Conditional} from '/src/docComponents/conditional'; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - - -This article will show you how to use push notifications in your Unity project. We recommend that you take a look at [Push Notification Overview](/sdk/push/guide/overview/) if you haven’t already. - -Currently we only support iOS and some Android vendors (Huawei, Xiaomi, VIVO, OPPO, Meizu)FCM. - -## Getting Started - -### iOS - -Please first obtain an iOS push notification certificate by following [APNs Configuration Guide](/sdk/push/guide/ios-cert/). - -### Android - -Please first apply for push notification permissions from Android vendorsFCM by following [Android Mixpush Guide](/sdk/push/guide/android-mixpush/). - -Note that you only need to follow the guide to apply for push notification permissions from Android vendorsFCM. You **don’t** need to follow the guide to set up push notifications for Android. - -## Integrate the Push Notification Service - -### Install the SDK - -You can download the latest `unity-push.unitypackage` or `unity-push-without-gradle.unitypackage` from [SDK Releases](https://github.com/leancloud/csharp-sdk/releases). - -If your project has “no” other Android Gradle configurations, you can download `unity-push.unitypackage`. This package includes all iOS and Android configurations. You will only need to configure the parameters for different vendors. - -If your project “has” other Android Gradle configurations, you’ll need to download `unity-push-without-gradle.unitypackage`. This package doesn’t contain Android Gradle configurations related to push notifications. You will need to provide those configurations yourself. - -The SDK cannot be installed with UPM because it involves Android Gradle configurations. - - - -### Import the `LeanCloud-SDK-Realtime-Unity` Package: - -#### Method 1: Use Unity Package Manager - -Add the following dependency to the `Packages/manifest.json` file in your project: - - -{ - `"dependencies":{ - "com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-${sdkVersions.leancloud.csharp}" - }` -} - - -You can view all installed packages by going to **Window > Package Manager**. - -#### Method 2: Import Manually - -1. Locate the **LeanCloud C# SDK** download URL on [Downloads](/tap-download) and download `LeanCloud-SDK-Realtime-Unity.zip`. - -2. Unzip `LeanCloud-SDK-Realtime-Unity.zip` and drag the Plugins directory to Unity. - - - -### Set Up - -#### iOS - -Provide the iOS developer’s TeamId during initialization. See [Initialization](#initialization). - -#### Android - - - -##### Huawei - -Download the file named `agconnect-services.json` applied from Huawei Push Service and place it in `Assets/LeanCloud/Push/Android/HuaWei/hms/`. When bundling, this file will be copied to the directory specified by Huawei. - -##### VIVO - -Provide the `app_id` and `api_key` applied from VIVO Push Service as the values of `com.vivo.push.app_id` and `com.vivo.push.api_key` under `meta-data` in `Assets/Plugins/Android/AndroidMenifest.xml`. - -##### Other Platforms - -For Xiaomi, OPPO, and Meizu, provide the configurations when initializing the SDK. - -Note that `${applicationId}` is used for the package name in `AndroidMenifest.xml`. If you need different `applicationId`s for different channels, please edit it manually. - -### Initialization - -Here you need to initialize the SDK for different vendors according to the platform and device information. -You can use Xiaomi Push Service as the default option as it is able to establish connections when used under other vendors. - -```cs -using LeanCloud.Storage; -using LeanCloud; -using LeanCloud.Push; -using System.Threading.Tasks; -using LC.Newtonsoft.Json; - -LCApplication.Initialize("{{appid}}", "{{appkey}}", "https://please-replace-with-your-customized.domain.com"); - -if (Application.platform == RuntimePlatform.IPhonePlayer) { - LCIOSPushManager.RegisterIOSPush(IOS_TEAM_ID); -} else if (Application.platform == RuntimePlatform.Android) { - string deviceModel = SystemInfo.deviceModel.ToLower(); - if (deviceModel.Contains("huawei")) { - LCHuaWeiPushManager.RegisterHuaWeiPush(); - } else if (deviceModel.Contains("oppo")) { - LCOPPOPushManager.RegisterOPPOPush(OPPO_APP_KEY, OPPO_APP_SECRET); - } else if (deviceModel.Contains("vivo")) { - LCVIVOPushManager.RegisterVIVOPush(); - } else if (deviceModel.Contains("meizu")) { - LCMeiZuPushManager.RegisterMeiZuPush(MEIZU_APP_ID, MEIZU_APP_KEY); - } else /*if (deviceModel.Contains("xiaomi"))*/ { - // Try to register Xiaomi Push Service for other vendors - LCXiaoMiPushManager.RegisterXiaoMiPush(XIAOMI_APP_ID, XIAOMI_APP_KEY); - } -} -``` - - - - - -To initialize FCM: - -```cs -LCFCMPushManager.RegisterFCMPush(); -``` - - - -## Installation - -The SDK comes with Installation objects that can be used to hold tokens and other data needed for push notifications. - -```cs -LCInstallation lcInstallation = await LCInstallation.GetCurrent(); -``` - -## Send Push Notifications - -By default, **Prevent clients from sending push notifications** is checked in **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Settings** so clients won’t be able to send push notifications to other devices without restriction. -We recommend leaving this option enabled and sending push notifications through the REST API or the dashboard. - -If you need to send push notifications from clients, uncheck this option. - -### Send Push Notifications to All Devices - -```csharp -try { - LCPush push = new LCPush { - Data = new Dictionary { - { "alert", pushData } - } - }; - await push.Send(); -} catch (Exception e) { - Debug.LogError(e); -} -``` - -### Send Push Notifications to Specific Users - -Below is an example of how to send a push notification from a client to an iOS device in the test environment: - -```cs -try { - LCPush push = new LCPush { - Data = new Dictionary { - { "alert", pushData } - }, - IOSEnvironment = LCPush.IOSEnvironmentDev, - }; - LCInstallation installation = await LCInstallation.GetCurrent(); - push.Query.WhereEqualTo("objectId", installation.ObjectId); - await push.Send(); -} catch (Exception e) { - Debug.LogError(e); -} -``` - -## Respond to Push Notifications - -### Message Format - -For more information about the message format, see [Push Notification REST API Guide](/sdk/push/guide/rest#推送消息). For Android devices, the default message content parameter contains the following properties: - -```json -{ - "alert": "Message content", - "title": "The title that appears in the notification center", - "custom-key": "Custom properties; custom-key is just an example and you can use any other keys" -} -``` - -### Perform an Action When a User Taps a Push Notification - -When a user taps a push notification to open your app, the Unity scene might not be initialized. This means that Unity may not know which notification was tapped. - -To provide the **notification parameters** to Unity, the SDK will cache the parameters in the native layer when sending push notifications. It provides a C# interface for your program to retrieve the parameters: - -```cs -Dictionary launchData = await LCPushBridge.Instance.GetLaunchData(); -``` - -## Verification - -Once you have initialized the SDK in your project and run your project on a real iOS or Android device, an entry with the device information will be created in the `_Installation` table. - -You can send a test push notification with custom conditions on **Developer Center > Your game > Game Services > Cloud Services > Push Notification > Send notifications**. This will help you verify that your device can receive push notifications. -You can send the push notification by providing the objectId of the Installation. For iOS you can also provide the deviceToken and for Android you can use the registrationId. - -## FAQ -### How do I remove the push notification services provided by certain vendors? - - - -You might want your app to support only certain vendors, or to release different packages for different channels. In this case, you can remove the vendor SDKs for unused vendors to reduce the size of the packages: - - - - - -The SDK includes vendor SDKs for some Chinese Android vendors. You can remove them to reduce the size of the package. - - - -- Delete `Assets/LeanCloud/Push/Android/xx`. Here `xx` is the vendor name such as `HuaWei` and `XiaoMi`. -- Delete `dependencies` from `Assets/Plugins/Android/mainTemplate.gradle`. -- Delete vendor SDKs from `Assets/Plugins/Android/AndroidManifest.xml` (see to the comments in the file for more information). diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/rtc/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/shadow/rtc/_category_.json deleted file mode 100644 index 576865718..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/rtc/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "实时语音", - "collapsed": true, - "position": 17 -} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/rtc/features.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/rtc/features.mdx deleted file mode 100644 index e24b97b04..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/rtc/features.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: RTC Introduction -sidebar_label: Introduction -sidebar_position: 1 -slug: /sdk/rtc/features/ ---- - - - - - - -RTC is a one-stop voice chat solution that provides real-time voice chat and voice compliance services for a variety of game types such as FPS, MOBA, MMORPG, casual matchmaking, online table games, etc. All your voice chat needs can be met with one easy integration. - -## Key Benefits - -- Easy integration: RTC works with popular frameworks. With a simple integration, all your voice chat requirements can be met. -- Low latency, high quality voice: RTC supports two-player or multi-player real-time audio call with low latency and high quality, which can perform well in weak network environment. -- Compliance for English and Chinese content: RTC identifies all types of illegal content, intelligently identifying pornographic, violent, abusive, advertising, and other types of sensitive or unwanted information in real-time voice and audio files. -- Global service availability: RTC comes with massive acceleration nodes deployed globally, covering major countries and regions around the world, enabling players to access nearby servers and providing real-time voice services with low latency and no lag. - -## Features - -### Real-Time Voice Conversation - -RTC allows up to 6 people to talk at the same time and transmits data with ultra-low latency. It is ideal for multiplayer team hacking and other competitive gaming scenarios. RTC also supports: - -- Blocking the voice of others in the room -- Event callbacks for members who enter the room and speak, which the game can use to determine player status -- Enabling/disabling loopback - -### Voice Compliance - -RTC supports filtering of illegal content in English and Chinese. The game can report the voice to the compliance module in different slice lengths. If a violation is detected, the game will receive a callback, and the game can decide whether to kick the player out of the room, ban them, or take no action at all. -Note: Currently, language compliance only supports Mandarin and a few dialects. Other languages will be supported in future versions. - -### 3D Voice - -3D Voice can convert the sound without orientation into the sound with orientation, which gives the player the feeling that the sound is coming from one place in the room, suitable for creating an immersive listening experience for battle royale, FPS, and other games. - -:::info - -3D Voice is still in beta. If you encounter any problems or have any suggestions during the trial, please feel free to contact us via tickets. - -::: - -### Developer Center - -The Developer Center provides you with the following basic features: - -- Enable and configure the RTC service -- Remotely turn on/off the RTC -- Query the number of active users, maximum number of simultaneous online users, player voice hours, and other real-time voice data -- View real-time voice usage and billing details - -## Integrate the Service - -### Getting Ready - -1. Become a TapTap developer; - -2. Create a game in the TapTap Developer Center; - -3. Go to "Game Services"-"RTC" and enable the service; - -4. Configure the compliance service - - ![](https://capacity-files.lcfile.com/f7lyeyQrmn9YIwIBBlWq7HIVTqLdndQA/rtc-console.png) - - 1. Set the voice slice length: A shorter slice length sacrifices some of the contextual semantics for a relatively fast callback speed. You can choose the slice length according to your actual scenario; - 2. Set the callback address: The callback address for receiving speech compliance recognition results; - -5. Download the TapSDK (minimum supported version is v3.5.0) and integrate it into the game package; - -6. Test the game package. - diff --git a/i18n/en/docusaurus-plugin-content-docs/current/shadow/rtc/guide.mdx b/i18n/en/docusaurus-plugin-content-docs/current/shadow/rtc/guide.mdx deleted file mode 100644 index 786140715..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/shadow/rtc/guide.mdx +++ /dev/null @@ -1,1491 +0,0 @@ ---- -title: RTC Guide -sidebar_label: Guide -sidebar_position: 2 -slug: /sdk/rtc/guide/ ---- - - - - - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import Mermaid from '/src/docComponents/Mermaid'; -import {Conditional} from '/src/docComponents/conditional'; - - -## Install the SDK - -Download the SDK from the [Downloads](/tap-download) page and import the `TapRTC` module: - - - -<> - -Make sure [git-lfs] is installed on your system, then add the dependencies via UPM: - -[git-lfs]: https://git-lfs.github.com - - -{`"dependencies":{ - ... - "com.taptap.tds.rtc":"https://github.com/TapTap/TapRTC-Unity.git#${sdkVersions.taptap.unity}", -}`} - - - -<> - -{`repositories{ - flatDir { - dirs 'libs' - } -} -dependencies { - ... - implementation (name:'TapRTC_${sdkVersions.taptap.rtc}', ext:'aar') -}`} - - - -<> - -1. Select the project in Xcode, then go to Build Settings > Other Linker Flags, add `-ObjC` and `-Wl -ld_classic`. -2. Drag and drop the `TapRTC_SDK` directory into the project directory. -3. Drag the `TapRTC.framework` and `GMESDK.framework` files into the project and select `Do Not Embed`. -4. Drag the TapRTC.bundle resource file into the project. -5. Add the system libraries that the SDK depends on: libz, libresolv, libiconv, libc++, CoreMedia.framework, CoreAudio.framework, AVFoundation.framework, SystemConfiguration.framework, UIKit.framework, AudioToolbox.framework, OpenAL.framework, Security.framework. - - -TapRTC.framework - - - - - - -## Caution - -* RTC must be configured in the Developer Center before use. -* You need to implement [the appropriate signature authentication service](#server-side-authentication) on your own servers. -* C# SDK must periodically call the `TapRTC.Poll` interface to trigger relevant event callbacks. - -On Android, you need to apply for network and audio-related permissions: - -```xml - - - - - - - -``` - -For iOS, you need to request microphone permission (`Privacy - Microphone Usage Description`). - -Games usually have no need for background calls; players usually play the game while the voice is playing, and the game stays in the foreground. -If the type of game is special and needs to support background calls, then you must also request background play permission: -Configure **Capability > Background Modes > Audio, AirPlay, and Picture in Picture** in target. - -## Core Interfaces - -The RTC is initialized and configured with `TapRTCConfig`. The initialization process is asynchronous and you must wait for the initialization result to be returned before proceeding to the next step. - -* For `ClientId`, `ClientToken`, and `ServerUrl`, see the note about [application credentials](/sdk/storage/guide/setup-dotnet/#credentials). -* `UserId`: Developer-defined user ID for tagging players. -* `DeviceId`: Developer-defined device ID used to tag the device. -* `AudioPerfProfile`: Audio quality profile (`LOW`, `MID`, or `HIGH`; default is `MID`). - - - -```cs -using TapTap.RTC; - -var config = new TapRTCConfig.Builder() - .ClientID("ClientId") - .ClientToken("ClientToken") - .ServerUrl("ServerUrl") - .UserId("UserId") - .DeviceId("DeviceId") - .AudioProfile(AudioPerfProfile.MID) - .ConfigBuilder(); - -ResultCode code = await TapRTC.Init(config); - -if (code == ResultCode.OK) { - // Initialized successfully -} else { - // Failed -} -// In the RTC module of the SDK, interfaces that return ResultCode indicate success with ResultCode.OK. -``` - -```java -import android.app.Application; -import com.taptap.taprtc.Config; -import com.taptap.taprtc.Config.AudioPerfProfile; -import com.taptap.taprtc.DeviceID; -import com.taptap.taprtc.UserID; -import com.taptap.taprtc.TapRTCEngine; - -public class MyApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - Config config = new Config(); - config.appId = "AppId"; - config.appKey = "AppKey"; - config.serverUrl = "ServerUrl"; // Please remove the http/https prefix and specify a custom domain name or a domain name in the form of "xxx.cloud.tds1.tapapis.cn" or something similar. - config.userId = new UserID("UserId"); - config.deviceId = new DeviceID("DeviceId"); - // This must be set to LOW if you want to use the Range Audio feature - config.profile = AudioPerfProfile.MID; - try { - TapRTCEngine.get().init(this, config, resultCode -> { - if (resultCode == ResultCode.OK) { - // Initialized successfully - } else { - // Failed - } - }); - } catch (TapRTCException e) { - throw new RuntimeException(e); - } - } -} -// In the RTC module of the SDK, interfaces that return ResultCode indicate success with ResultCode.OK. -``` - -```objc -TapRTCConfig *config = [[TapRTCConfig alloc] initWithAppId:@"AppId" -appKey:@"AppKey" serverUrl:@"ServerUrl" -userId:@"UserId" deviceId:@"DeviceId" -profile:AudioPerfProfileMID]; -TapRTCEngine *engine = [TapRTCEngine defaultEngine]; -[engine initializeWithConfig:config resultBlock:^(NSError * _Nullable error) { - if (error) { - // handle error - } -})]; -``` - - - -### Trigger Callback Events - -For the C# SDK, you must call the `Poll` method in the `Update` method to trigger the event callback. Failure to call this method will cause the SDK to throw exceptions. - -```cs -public void Update() -{ - ResultCode code = TapRTC.Poll(); - if (code == ResultCode.OK) { - // Triggered callback successfully - } else { - // Failed - } -} -``` - -For the Java SDK and the Objective-C SDK, you do not need to call the `Poll` method regularly. - -### Resume - - - -```cs -ResultCode code = TapRTC.Resume(); -if (code == ResultCode.OK) { - // Resumed -} else { - // Failed -} -``` - -```java -import com.taptap.taprtc.TapRTCEngine; - -ResultCode code = TapRTCEngine.get().resume(); -``` - -```objc -TapRTCResultCode resultCode = [engine resume]; -if (resultCode == TapRTCResultCode_Success) { - // resumed -} else { - // failed to resume -} -``` - - - -### Pause - - - -```cs -ResultCode code = TapRTC.Pause(); -if (code == ResultCode.OK) { - // Paused -} else { - // Failed -} -``` - -```java -import com.taptap.taprtc.TapRTCEngine; - -ResultCode code = TapRTCEngine.get().pause(); -``` - -```objc -TapRTCResultCode resultCode = [engine pause]; -if (resultCode == TapRTCResultCode_Success) { - // paused -} else { - // failed to pause -} -``` - - - -## Room-Related Interfaces - -### Create Rooms - -After successful initialization, the SDK can make live voice calls only after creating a room. -The room number (`roomId`) must be specified when creating the room. -[Whether to enable range audio](#range-audio) must also be set when creating the room. The C# SDK does not enable it by default. For the Java SDK, you must specify whether to enable range audio when creating the room. The Objective-C SDK uses a separate interface to create rooms with range audio. - - - -```cs -bool enableRangeAudio = false; -var room = await TapRTC.AcquireRoom("roomId", enableRangeAudio); -``` - -```java -import com.taptap.taprtc.TapRTCEngine; -import com.taptap.taprtc.RoomID; - -RoomId roomId = new RoomID("roomId"); -boolean enableRangeAudio = false; -TapRTCRoom room = TapRTCEngine.get().acquireRoom(roomId, enableRangeAudio); -``` - -```objc -TapRTCRoom *room = [engine acquireRoomWithRoomId:@"roomID"]; -``` - - - -### Register Room-Related Callback Events - - - -```cs -room.RegisterEventAction(new TapRTCEvent() -{ - OnDisconnect = (code, message) => { label.text += "\n" + $"Disconnected code:{code} msg:{e}"; }, - OnEnterFailure = s => { label.text += "\n" + $"Failed to enter the room:{s}"; }, - OnEnterSuccess = () => { label.text += "\n" + $"Entered the room"; }, - OnExit = () => { label.text += "\n" + $"Left the room"; }, - OnUserEnter = userId => { label.text += "\n" + $"{userId} entered the room"; }, - OnUserExit = userId => { label.text += "\n" + $"{userId} left the room"; }, - OnUserSpeaker = (userId, volume) => { label.text += "\n" + $"{userId} is speaking in the room; the volume is {volume}"; }, - OnUserSpeakEnd = userId => { label.text += "\n" + $"{userId} stopped speaking"; }, - // Returns the audio quality after the switch; see the section "Switch Audio Quality" below - OnRoomTypeChanged = (i) => { label.text += "\n" + $"The audio quality is now {i}"; }, - OnRoomQualityChanged = (weight, loss, delay) => - { - Debug.Log($"Audio quality:{weight} Packet loss:{loss}% Delay:{delay}ms"); - }, - -}); -``` - -```java -import com.taptap.taprtc.UserID; -import com.taptap.taprtc.TapRTCRoom; - -room.registerCallback(new TapRTCRoom.Callback() { - // Entered the room - @Override public void onEnterSuccess() {} - - // Failed to enter the room - @Override public void onEnterFailure(String msg) {} - - // Lost connection - @Override public void onDisconnect() {} - - // Reconnected - @Override public void onReConnected() {} - - // Current player left the room - @Override public void onExit() {} - - // A player has entered the room - @Override public void onUserEnter(UserID userId) {} - - // A player has left the room - @Override public void onUserExit(UserID userId) {} - - // A player started talking - @Override public void onUserSpeakStart(UserID userId, int volume) {} - - // A player stopped talking - @Override public void onUserSpeakEnd(UserID userId) {} - - // Audio quality changed - @Override public void onRoomQualityChanged(int weight, double loss, int delay) {} -}); -``` - -```objc -// Need to implement TapRTCRoomDelegate - -// Entered the room -- (void)onEnterSuccess; - -// Failed to enter the room -- (void)onEnterFailure:(NSError *)error; - -// A player has entered the room -- (void)onUsersEnter:(NSString *)userId; - -// A player has left the room -- (void)onUsersExit:(NSString *)userId; - -// Current player left the room -- (void)onExit; - -// Lost connection -- (void)onDisconnect; - -// Reconnected -- (void)onReconnected; - -// player started talking -- (void)onUsersSpeakStart:(NSString *)userId volume:(NSInteger)volume; - -// A player stopped talking -- (void)onUsersSpeakEnd:(NSString *)userId; - -// Audio quality changed (returns audio quality, packet loss, and delay) -- (void)onQualityCallBackWithWeight:(int)weight loss:(float)loss delay:(int)delay; -``` - - - -### Join a Room - -Enter a room using the [server-side generated authentication information](#server-side-authentication). - -After successfully entering a room, a callback is made via `OnEnterSuccess` in `TapRTCEvent`. - - - -```cs -ResultCode code = await room.Join("authBuffer"); -if (code == ResultCode.OK) { - // Successfully joined the room -} -if (code == ResultCode.ERROR_ALREADY_IN_ROOM) { - // The player is already in the room -} -``` - -```java -import com.taptap.taprtc.Authority; - -Authority authBuffer = new Authority("authBuffer"); -ResultCode code = room.join(authBuffer); -if (code == ResultCode.OK) { - // Successfully joined the room -} -if (code == ResultCode.ERROR_ALREADY_IN_ROOM) { - // The player is already in the room -} -``` - -```objc -[room joinWithAuth:@"authBuffer"]; -``` - - - -The `authBuffer' is the authentication information generated on the server side, as described in the [Server-Side Authentication](#server-side-authentication) section below. - -### Exit a Room - -After leaving a room, a callback is made via `OnExit` in `TapRTCEvent`. - - - -```cs -ResultCode code = room.Exit(); -``` - -```java -ResultCode code = room.exit(); -``` - -```objc -TapRTCResultCode resultCode = [room exit]; -if (resultCode == TapRTCResultCode_Success) { - // exited -} else { - // failed exit -} -``` - - - -### Listen to Someone's Voice (Enabled by Default) - - - -```cs -ResultCode code = room.EnableUserAudio("userId"); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_USER_NOT_EXIST) { - // The player does not exist -} -``` - -```java -import com.taptap.taprtc.UserID; - -UserId userId = new UserID("userId"); -ResultCode code = room.enableUserAudio(userId); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_USER_NOT_EXIST) { - // The player does not exist -} -``` - -```objc -TapRTCResultCode resultCode = [room enableUserAudioWithUserId:@"userId"]; -``` - - - -### Disable Someone's Voice - - - -```cs -ResultCode code = room.DisableUserAudio("userId"); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_USER_NOT_EXIST) { - // The player does not exist -} -``` - -```java -import com.taptap.taprtc.UserID; - -UserId userId = new UserID("userId"); -ResultCode code = room.disableUserAudio(userId); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_USER_NOT_EXIST) { - // The player does not exist -} -``` - -```objc -TapRTCResultCode resultCode = [room disableUserAudioWithUserId:@"userId"]; -``` - - - -### Enable/Disable Voice - -This interface sets whether or not to receive audio. -In general, it is recommended that games use the [interface to turn on/off speakers](#enabledisable-speaker). - - - -```cs -// Enable -ResultCode code = room.EnableAudioReceiver(true); - -// Disable -ResultCode code = room.EnableAudioReceiver(false); -``` - -```java -// Enable -ResultCode code = room.enableAudioReceiver(true); - -// Disable -ResultCode code = room.enableAudioReceiver(false); -``` - -```objc -TapRTCResultCode resultCode = [room enableAudioReceiver:YES]; -``` - - - -### Switch Audio Quality - -There are three levels of audio quality: LOW, MID, and HIGH. - -You can change the audio quality after you enter the room. - - - -```cs -room.ChangeRoomType(AudioPerfProfile.LOW); -room.ChangeRoomType(AudioPerfProfile.MID); -room.ChangeRoomType(AudioPerfProfile.HIGH); -``` - -```java -// Not yet supported -``` - -```objc -// Not yet supported -``` - - - -Changing the audio quality triggers the `OnRoomTypeChanged` callback. - -### Get the Users in the Room - - - -```cs -HashSet userIdList = room.Users; -``` - -```java -List userIdList = room.getUsers(); -``` - -```objc -[room getUsers:^(NSArray*userIDs, NSError * _Nullable error) { - if (error) { - // Handle the error - } else { - // userIDs is the IDs of the users in the room - } -})]; -``` - - - -## Audio-Related Interfaces - -### Enable/Disable Microphone - - - -```cs -// Enable -ResultCode code = TapRTC.GetAudioDevice().EnableMic(true); - -// Disable -ResultCode code = TapRTC.GetAudioDevice().EnableMic(false); -``` - -```java -// Enable -boolean ok = TapRTCEngine.get().getAudioDevice().enableMic(true); - -// Disable -boolean ok = TapRTCEngine.get().getAudioDevice().enableMic(false); -``` - -```objc -// Enable -TapRTCResultCode code = [engine.audioDevice enableMic:YES]; - -// Disable -TapRTCResultCode code = [engine.audioDevice enableMic:NO]; -``` - - - -### Enable/Disable Speaker - - - -```cs -// Enable -ResultCode code = TapRTC.GetAudioDevice().EnableSpeaker(true); - -// Disable -ResultCode code = TapRTC.GetAudioDevice().EnableSpeaker(false); -``` - -```java -// Enable -boolean ok = TapRTCEngine.get().getAudioDevice().enableSpeaker(true); - -// Disable -boolean ok = TapRTCEngine.get().getAudioDevice().enableSpeaker(false); -``` - -```objc -// Enable -TapRTCResultCode code = [engine.audioDevice enableSpeaker:YES]; - -// Disable -TapRTCResultCode code = [engine.audioDevice enableSpeaker:NO]; -``` - - - -### Set/Get Volume - -Volume is an integer from 0 to 100. - - - -```cs -int vol = 60; - -// Set microphone volume -ResultCode code = TapRTC.GetAudioDevice().SetMicVolume(vol); -// Set speaker volume -ResultCode code = TapRTC.GetAudioDevice().SetSpeakerVolume(vol); - -// Get microphone volume -int micVolume = TapRTC.GetAudioDevice().GetMicVolume(); -// Get speaker volume -int speakerVolume = TapRTC.GetAudioDevice().GetSpeakerVolume(); -``` - -```java -int vol = 60; - -// Set microphone volume -TapRTCEngine.get().getAudioDevice().setMicVolume(vol); -// Set speaker volume -boolean ok = TapRTCEngine.get().getAudioDevice().setSpeakerVolume(vol); - -// Get microphone volume -int micVolume = TapRTCEngine.get().getAudioDevice().getMicVolume(); -// Get speaker volume -int speakerVolume = TapRTCEngine.get().getAudioDevice().getSpeakerVolume(); -``` - -```objc -int vol = 60; - -// Set microphone volume -TapRTCResultCode code = [engine.audioDevice setMicVolume:vol]; -// Set speaker volume -TapRTCResultCode code = [engine.audioDevice setSpeakerVolume:vol]; - -// Get microphone volume -int micVolume = [engine.audioDevice getMicVolume]; -// Get speaker volume -int speakerVolume = [engine.audioDevice getSpeakerVolume]; -``` - - - -### Enable/Disable Audio Play - - - -```cs -// Enable -ResultCode code = TapRTC.GetAudioDevice().EnableAudioPlay(true); -// Disable -ResultCode code = TapRTC.GetAudioDevice().EnableAudioPlay(false); -``` - -```java -// Enable -boolean ok = TapRTCEngine.get().getAudioDevice().enableAudioPlay(true); -// Disable -boolean ok = TapRTCEngine.get().getAudioDevice().enableAudioPlay(false); -``` - -```objc -// Enable -TapRTCResultCode code = [engine.audioDevice enableAudioPlay:YES]; - -// Disable -TapRTCResultCode code = [engine.audioDevice enableAudioPlay:NO]; -``` - - - -### Enable/Disable Loopback - - - -```cs -// Enable -ResultCode code = TapRTC.GetAudioDevice().EnableLoopback(true); -// Disable -ResultCode code = TapRTC.GetAudioDevice().EnableLoopback(false); -``` - -```java -// Enable -boolean ok = TapRTCEngine.get().getAudioDevice().enableLoopback(true); -// Disable -boolean ok = TapRTCEngine.get().getAudioDevice().enableLoopback(false); -``` - -```objc -// Enable -TapRTCResultCode code = [engine.audioDevice enableLoopback:YES]; - -// Disable -TapRTCResultCode code = [engine.audioDevice enableLoopback:NO]; -``` - - - -## Range Audio - -The range audio feature can support the following functions: - -- Allowing other team members within a certain range of the player to hear the player's voice; -- Support for a large number of users to turn on the microphone at the same time for voice calls in the same room. - -To use range audio, you must first specify that range audio is enabled when you create a room: - - - -```cs -bool enableRangeAudio = true; -var room = await TapRTC.AcquireRoom("roomId", enableRangeAudio); -``` - -```java -import com.taptap.taprtc.TapRTCEngine; -import com.taptap.taprtc.RoomID; - -RoomId roomId = new RoomID("roomId"); -boolean enableRangeAudio = true; -TapRTCRoom room = TapRTCEngine.get().acquireRoom(roomId, enableRangeAudio); -``` - -```objc -TapRTCRoom *room = [engine acquireRangeAudioRoomWithRoomId:@"roomID"]; -``` - - - -Also, before players enter the room, they must **change the audio quality to LOW**: - - - -```cs -room.ChangeRoomType(AudioPerfProfile.LOW); -``` - -```java -// The Java SDK does not provide an interface to switch the audio quality. -// With the Java SDK, if you want to use the range audio feature, **set the audio quality to LOW when initializing the SDK**. -``` - -```objc -// The Objective-C SDK automatically sets the audio quality to LOW when you enter a room with range audio -``` - - - -Next, set the team number and voice mode: - -- World Mode: Other team members within [a certain range] (#set-audio-reception-range) of the current player can hear the player's voice; -- Team Mode: Only team members can talk to each other. - -In both modes, team members can talk to each other regardless of distance. - - - -```cs -int teamId = 12345678; -ResultCode code = room.GetRtcRangeAudioCtrl().SetRangeAudioTeam(teamId); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} - -// World Mode -ResultCode resultCode = room.GetRtcRangeAudioCtrl().SetRangeAudioMode(RangeAudioMode.WORLD); -if (resultCode == ResultCode.OK) { - // Success -} -if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} -// Team Mode -ResultCode resultCode = room.GetRtcRangeAudioCtrl().SetRangeAudioMode(RangeAudioMode.TEAM); -if (resultCode == ResultCode.OK) { - // Success -} -if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} -``` - -```java -import com.taptap.taprtc.TapRTCRangeAudioCtrl.RangeAudioMode; - -int teamId = 12345678; -ResultCode code = room.rangeAudioCtrl().setRangeAudioTeam(new TeamID(teamId)); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} - -// World Mode -ResultCode resultCode = room.rangeAudioCtrl().setRangeAudioMode(RangeAudioMode.WORLD); -if (resultCode == ResultCode.OK) { - // Success -} -if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} - -// Team Mode -ResultCode resultCode = room.rangeAudioCtrl().setRangeAudioMode(RangeAudioMode.TEAM); -if (resultCode == ResultCode.OK) { - // Success -} -if (resultCode == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} -``` - -```objc -int teamId = 12345678; -TapRTCResultCode resultCode = [room setRangeAudioTeamId:teamId]; - -// World Mode -TapRTCResultCode code = [room setRangeAudioMode:TapRTCRangeAudioModeWorld]; -TapRTCResultCode code = [room setRangeAudioMode:TapRTCRangeAudioModeTeam]; -``` - - - -Then enter the room and [Set Audio Reception Range](#set-audio-reception-range) and [Update Source Orientation](#update-source-orientation) to make the range audio effective. - -If you want to change the voice mode after entering the room, you can call `SetRangeAudioMode` again. - -### Set Audio Reception Range - -The audio reception range controls whether or not other team members can hear your voice in world mode, and is invoked after you enter the room and usually only needs to be set once. - - - -```cs -int range = 300; -ResultCode code = room.GetRtcRangeAudioCtrl().UpdateAudioReceiverRange(range); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} -``` - -```java -int range = 300; -ResultCode code = room.rangeAudioCtrl().updateAudioReceiverRange(range); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} -``` - -```objc -int range = 300; -TapRTCResultCode code = [room updateAudioReceiverRange:range]; -``` - - - -Other team members who are out of `range` will not be able to hear the player. - -If [3D voice](#3d-Voice) is also enabled, the distance will also affect the volume level: - -| Distance | Volume decay | -| - | - | -| `N < range/10` | 1.0 (No decay)| -| `N >= range/10` | `range/10/N` | - -### Update Source Orientation - -After successfully entering the room, you must call this interface in Unity's Update method to update the orientation and direction of the sound source for the range audio to take effect. -The orientation is specified by the front, right, and top coordinates of the world coordinate system, and the direction is specified by the unit vector of the front, right, and top axes of its own coordinate system. - - - -```cs -int x = 1; -int y = 2; -int z = 3; -Position position = new Position(x, y, z); - -float[] axisForward = new float[3] {1.0, 0.0, 0.0}; -float[] axisRight = new float[3] {0.0, 1.0, 0.0}; -float[] axisUp = new float[3] {0.0, 0.0, 1.0}; -Forward forward = new Forward(axisForward, axisRight, axisUp); -ResultCode code = room.GetRtcRangeAudioCtrl().UpdateSelfPosition(position, forward); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} -``` - -```java -import com.taptap.taprtc.TapRTCRangeAudioCtrl.Position; -import com.taptap.taprtc.TapRTCRangeAudioCtrl.Forward; - -int x = 1; -int y = 2; -int z = 3; -Position position = new Position(x, y, z); - -float[] axisForward = {1.0, 0.0, 0.0}; -float[] axisRight = {0.0, 1.0, 0.0}; -float[] axisUp = {0.0, 0.0, 1.0}; -Forward forward = new Forward(axisForward, axisRight, axisUp); - -ResultCode code = room.rangeAudioCtrl().updateSelfPosition(position, forward); -if (code == ResultCode.OK) { - // Success -} -if (code == ResultCode.ERROR_NOT_RANGE_ROOM) { - // Range audio is not enabled for this room -} -``` - -```objc -TapRTCPosition position; -position.x = 1.0; -position.y = 2.0; -position.z = 3.0; - -TapRTCAxis axisForward; -axisForward.x = 1.0; -axisForward.y = 0.0; -axisForward.z = 0.0; -TapRTCAxis axisRight; -axisRight.x = 0.0; -axisRight.y = 1.0; -axisRight.z = 0.0; -TapRTCAxis axisUp; -axisUp.x = 0.0; -axisUp.y = 0.0; -axisUp.z = 1.0; - -TapRTCForward forward; -forward.forward = axisForward; -forward.rightward = axisRight; -forward.upward = axisUp; -TapRTCResultCode code = [room updateSelfPosition:position forward:forward]; -``` - - - -The orientation has no effect on whether the voice is heard or not, so if you do not enable [3D Voice](#3d-voice), the orientation parameter can be set freely when updating the orientation of the sound source. -However, when 3D Voice is enabled, the orientation must be set correctly to get accurate 3D sound effects. - -### 3D Voice - -Enabling 3D Voice allows you to convert voices without orientation to voices with source orientation to increase player immersion. -This interface takes two parameters, the first specifying whether the current player can hear the 3D sound effect, and the second specifying whether the 3D voice works [within the team] (#range-audio). - - - -```cs -bool enable3D = true; -bool applyToTeam = true; -ResultCode code = TapRTC.GetAudioDevice().EnableSpatializer(enable3D, applyToTeam); -``` - -```java -boolean enable3D = true; -boolean applyToTeam = true; -boolean ok = TapRTCEngine.get().getAudioDevice().enableSpatializer(enable3D, applyToTeam); -``` - -```objc -TapRTCResultCode code = [engine.audioDevice EnableSpatializer:YES applyTeam:YES]; -``` - - - - -## Error Codes - -Some of the operations in the above document return ResultCode, and the code examples give the error codes corresponding to some common error types. -The complete list of error codes is shown below: - - - -```cs -namespace TapTap.RTC -{ - public enum ResultCode - { - OK = 0, - ERROR_UNKNOWN = 1, - ERROR_UNIMPLEMENTED = 2, - ERROR_NOT_ON_MAIN_THREAD = 3, - ERROR_INVAIDARGS = 4, - ERROR_NOT_INIT = 5, - ERROR_CONFIG_ERROR = 11, - ERROR_NET = 21, - ERROR_NET_TIMEOUT = 22, - ERROR_USER_NOT_EXIST = 101, - ERROR_ROOM_NOT_EXIST = 102, - ERROR_DEVICE_NOT_EXIST = 103, - ERROR_TEAM_ID_NOT_NULL = 104, - ERROR_ALREADY_IN_ROOM = 105, - ERROR_NO_PERMISSION = 106, - ERROR_AUTH_FAILED = 107, - ERROR_LIB_ERROR = 108, - ERROR_NOT_RANGE_ROOM = 109, - } -} -``` - -```java -public enum ResultCode { - OK(0, "Success"), - ERROR_UNKNOWN(1, "Unknown Error"), - ERROR_UNIMPLEMENTED(2, "Unimplemented Functionality"), - ERROR_NOT_ON_MAIN_THREAD(3, "Not running on the main thread"), - ERROR_INVALID_ARGUMENT(4, "Invalid parameter"), - ERROR_NOT_INIT(5, "Uninitialized"), - ERROR_CONFIG_ERROR(11, "Configuration error"), - ERROR_NET(21, "Network error"), - ERROR_NET_TIMEOUT(22, "Network request timeout"), - ERROR_USER_NOT_EXIST(101, "User does not exist"), - ERROR_ROOM_NOT_EXIST(102, "Room does not exist"), - ERROR_DEVICE_NOT_EXIST(103, "Device does not exist"), - ERROR_TEAM_ID_NOT_NULL(104, "TeamID cannot be Zero"), - ERROR_ALREADY_IN_ROOM(105, "It's already in the other room"), - ERROR_NO_PERMISSION(106, "TapRTCCode_Error_NoPermission\tNo Permission"), - ERROR_AUTH_FAILED(107, "Authorization failure"), - ERROR_LIB_ERROR(108, "Service provider library error"), - ERROR_NOT_RANGE_ROOM(109, "Not Support Range Room"), -} -``` - -```objc -FOUNDATION_EXPORT NSString * const TapRTCNetworkErrorDomain; -FOUNDATION_EXPORT NSString * const TapRTCResultErrorDomain; - -typedef NS_ENUM(NSInteger, TapRTCResultCode) { - TapRTCCode_OK = 0, - TapRTCCode_Error_Unknown = 1, - TapRTCCode_Error_Unimplemented = 2, - TapRTCCode_Error_NotOnMainThread, - TapRTCCode_Error_InvaidArgs, - TapRTCCode_Error_NotInit, - - TapRTCCode_ConfigError_Error = 11, - TapRTCCode_ConfigError_AppID, - TapRTCCode_ConfigError_AppKey, - TapRTCCode_ConfigError_ServerUrl, - TapRTCCode_ConfigError_UserID, - TapRTCCode_ConfigError_DeviceID, - - TapRTCCode_NetError_Error = 21, - TapRTCCode_NetError_Timeout, - - TapRTCCode_Error_UserNotExist = 101, - TapRTCCode_Error_RoomNotExist, - TapRTCCode_Error_DeviceNotExist, - TapRTCCode_Error_TeamIDNotBeZero, - TapRTCCode_Error_AlreadyInOtherRoom, - TapRTCCode_Error_NoPermission, - TapRTCCode_Error_AuthFailed, - TapRTCCode_LibError, -}; -``` - - - -## Server Side - -To secure the chat channel, the RTC service must be used with the game's own authentication server. -In addition, the game's own server is used to respond to compliance callbacks and to invoke the player removal interface. - -### Server-Side Authentication - -Before a client joins a room, it must obtain a signature from your own authentication server, after which the RTC cloud verifies the signature, and only requests with valid signatures are executed, and illegal requests are blocked. - ->Authentication Server: 1. Require signature before entering room -Authentication Server-->>Client: 2. Generate signature to return to client -Client->>RTC Cloud: 3. Encode the signature in the request and send it to the real-time voice server -RTC Cloud-->>Client: 4. Verify the content of the request and the signature and perform the subsequent operations -`} /> - -1. The client requests a signature from the game's authentication server before entering the room; -2. The authentication server generates a signature to return to the client based on the [authentication key algorithm](#authentication-key-algorithm) described below; -3. The client receives the signature, encrypts it in the request, and sends it to the RTC server; -4. The RTC server performs a verification of the request content and signature, and performs the subsequent actual operation after passing the verification. - -Signatures use a combination of the **HMAC-SHA1** algorithm and Base64. -For different requests, your app generates different signatures (see format descriptions below). Overall, signing is the process of signing player and room information using a specific key (in this case, we use the application's Master Key). - -#### Authentication Key Algorithm - -The signature generation process used for authentication involves **plaintext**, **key**, and **encryption algorithm**. - -##### Plaintext - -The plaintext is a json string consisting of the following fields (in any order) - - - -| Field | Type/Length | Description | -| :--------- | :------------- | :----------------------------------------------------------- | -| `userId` | `string` | An identifier of the user entering the room | -| `appId` | `string` | The game's Client ID | -| `expireAt` | `unsigned int/4` | Expiration time (current time + expiration date (in seconds, recommended value: 300s)) | -| `roomId` | `string` | Room ID | - -##### Key - -The `Master Key` (i.e. `Server Secret`) of the game. -Both the `Client ID` and `Server Secret` can be viewed in **Developer Center > Your game > Game Services > Configuration**. - -##### Encryption Algorithm - -The encryption algorithm uses a combination of **HMAC-SHA1** algorithm and Base64, similar to the JWT format. -The generated result contains two parts: payload (plaintext) and sign (encrypted string). - -1. Construct the JSON string according to the fields in the table above. - -2. Base64-encode the JSON string from the previous step to obtain the payload. - -3. Generate the sign using **HMAC-SHA1** on the payload with **key**. - -4. Use `.` to join the payload and the token. - -Note: The JSON string itself is field-order independent, but the **spliced payload and the payload used to generate the sign must be in the same field order** or they will not pass the checksum in the RTC cloud. - -The following sample code in Java and Go is provided for reference: - -
    -Java example - -```java -import com.google.gson.Gson; -import org.junit.Test; -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import static org.junit.Assert.*; - -import java.time.Instant; -import java.util.Base64; - -public class JUnitTestSuite { - - private static final String MAC_NAME = "HmacSHA1"; - - @Test - public void testToken() throws Exception { - String masterKey = "masterKey"; - Token t = new Token(); - t.appId = "appId"; - t.userId ="user_test"; - t.roomId ="room_test";; - - int expTime = (int) Instant.now().getEpochSecond() + 5 * 60; - t.expireAt = expTime; - - // server authBuff to your SDK Client - String authBuff = genToken(t, masterKey); - assertNotNull(authBuff); - } - - private String genToken(Token token, String key) throws Exception { - Gson gson = new Gson(); - String t = gson.toJson(token); - String payload = Base64.getEncoder().encodeToString(t.getBytes(StandardCharsets.UTF_8)); - byte[] pEncryptOutBuf = hmacSHA1Encrypt(payload.getBytes(StandardCharsets.UTF_8), key); - String sign = Base64.getEncoder().encodeToString(pEncryptOutBuf); - return payload + "." + sign; - } - - - byte[] hmacSHA1Encrypt(byte[] text, String key) throws Exception { - byte[] data = key.getBytes(StandardCharsets.UTF_8); - SecretKey secretKey = new SecretKeySpec(data, MAC_NAME); - Mac mac = Mac.getInstance(MAC_NAME); - mac.init(secretKey); - return mac.doFinal(text); - } - - class Token { - String userId; - String appId; - String roomId; - long expireAt; - } -} -``` - -
    - -
    -Go example - -```go -package configs - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - - -func TestToken(t *testing.T) { - assert := assert.New(t) - t1 := &Token{ - UserId: "appId", - AppId: "user_test", - RoomId: "roomId_test", - ExpireAt: time.Now().Unix() + 5*60, - } - authBuff := GenToken(t1, "masterKey") - assert.NotEmpty(authBuff) - fmt.Println(authBuff) -} - - -const ( - sep = "." -) - -func GenToken(t *Token, masterKey string) string { - b, err := json.Marshal(t) - if err != nil { - return "" - } - payload := base64.StdEncoding.EncodeToString(b) - sign := base64.StdEncoding.EncodeToString(HmacSHA1(masterKey, payload)) - return payload + sep + sign - -} - - -func HmacSHA1(key string, data string) []byte { - mac := hmac.New(sha1.New, []byte(key)) - mac.Write([]byte(data)) - return mac.Sum(nil) -} - -type Token struct { - UserId string `json:"userId,omitempty"` - AppId string `json:"appId,omitempty"` - RoomId string `json:"roomId,omitempty"` - ExpireAt int64 `json:"expireAt,omitempty"` -} -``` - -
    - -#### Deployment Method - -Since the encryption key uses `Server Secret`, the logic of the encryption algorithm must be implemented on the server side. **Do not implement the encryption logic on the client side**. - -#### Usage - -The game's own authentication server generates the encryption string and sends it to the client. The client then passes the appropriate authentication information when calling the [join room interface](#join-a-room). - -The C# SDK also provides a `GenToken` method for testing when integrating the SDK on the client side. -For example, client-side developers can test the functionality of adding rooms on the client side before waiting for server-side developers to implement and deploy the appropriate interfaces. -Another example is that the client developer can compare the encrypted string generated by the SDK's own `GenToken` with the encrypted string generated by the server to verify that the server has implemented the encryption algorithm correctly. - -```cs -var authBuffer = AuthBufferHelper.GenToken(appId, roomId, userId, masterKey); -``` - -Note that since this method requires `Server Secret` to be passed as a parameter, **it is intended for internal testing and development only, and should not be used in published code or installation packages**. -If you are concerned about `Server Secret` being leaked into external code or installation packages due to human error, or if you want to minimize the number of internal developers having access to `Server Secret` for security reasons, it is recommended that you do not use the `GenToken` method provided by the SDK, and that you generate the encrypted string using the game's own authentication server for internal testing as well. - -### Compliance Callback - - -After you set the callback address in the RTC dashboard (**Developer Center > Your game > Game Services > RTC > Settings**), the set callback address will be called if the voice content is illegal. - -The callback address should be a URL of the HTTP(S) protocol interface, support the POST method, and use UTF-8 for data encoding. - -Example POST body for callback: - -```json -{ - "HitFlag":true, - "Msg":"Illegal message", - "ScanFinishTime":1634893736, - "ScanStartTime":1634893734, - "Scenes":[ - "default" - ], - "VoiceFilterPiece":[ - { - "Duration":14000, - "HitFlag":true, - "Info":"Illegal message", - "MainType":"abuse", - "Offset":0, - "PieceStartTime":1634893734, - "RoomId":"1234", - "UserId":"123456", - "VoiceFilterDetail":[ - { - "EndTime":0, - "KeyWord":"Illegal keyword", - "Label":"abuse", - "Rate":"0.00", - "StartTime":0 - } - ] - } - ] -} -``` - -You can get the `sign` field in the callback header to verify that the request is coming from the RTC cloud. - -#### Compliance Callback Verification Algorithm - -1. Append the `POST` prefix to the POST body of the callback to get the payload: - - ``` - POST{"HitFlag":true,"Msg":"Illegal message",/* Other logic */} - ``` - - Note: - - - Please read the JSON content directly from the HTTP request body**. Deserializing to the programming language data structure may change the order of the fields, resulting in a validation failure. - - POST and body are directly connected without spaces in between. - -2. Perform HMAC-SHA1 encryption on the payload. The key is the game's `Server Secret`. - -3. BASE64-encode the result of the previous step to get the sign. - -4. Compare it with the value of the sign field in the HTTP header of the callback. If it is the same, then the request is coming from the RTC cloud. - -Here is a sample Go code for reference: - -
    -Go example - -```go -package main - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "github.com/labstack/echo/v4" - "io/ioutil" - "net/http" -) - - -func testCallback(c echo.Context) error { - sign := c.Request().Header.Get("sign") - body, _ := ioutil.ReadAll(c.Request().Body) - checkGMESign(sign, "yourMasterKey", string(body)) - return c.NoContent(http.StatusOK) -} - - -func checkGMESign(signature, secretKey, body string) bool { - sign := genSign(secretKey, body) - return sign == signature -} - -func genSign(secretKey, body string) string { - content := "POST" + body - a := hmacSHA1(secretKey, content) - return base64.StdEncoding.EncodeToString(a) -} - -func hmacSHA1(key string, data string) []byte { - mac := hmac.New(sha1.New, []byte(key)) - mac.Write([]byte(data)) - return mac.Sum(nil) -} -``` - -
    - -### Remove Players - -In some scenarios, the game may need to kick players out of a room, such as when illegal content is involved. -You can call the RTC service's REST API from your own server to fulfill this need. - -#### Request Format - -For POST and PUT requests, the request body must be in JSON format and the Content-Type of the HTTP header must be set to `application/json`. - -The request is authenticated by the key/value pairs contained in the HTTP header, with the following parameters: - -Key|Value|Meaning|Source ----|----|---|--- -`X-LC-Id`|`{{appid}}`|The `App Id` (`Client Id`) of the current application|Can be viewed in the console -`X-LC-Key`|`{{masterkey}},master`|The `Master Key` (`Server Secret`) of the current application|Can be viewed in the console - -#### Base URL - -The Base URL for REST API requests (the `{{host}}` in the curl examples) is the custom API domain of your app. You can update or find it on the Developer Center. See [Domain](/sdk/storage/guide/setup-dotnet#domain) for more details. - -#### REST API - -```sh -curl -X DELETE \ --H "Content-Type: application/json" \ --H "X-LC-Id: {{appId}}" \ --H "X-LC-Key: {{masterKey}},master" \ --d '{"roomId":"YOUR-ROOM-ID", "userId":"YOUR-USER-ID"}' \ -https://{{host}}/rtc/v1/room/member -``` - -The HTTP status code in response to a successful removal is `200`, and the HTTP status code in response to an error is the appropriate error code, e.g. 401 if you do not have permission. - -Note that **after a player has been removed, the player can rejoin the room and speak again after joining the room. ** -**The game must also implement the appropriate blocking logic on its own authentication server** so that no signature is issued when the player rejoins the room, preventing players from circumventing the restriction by rejoining the room. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/tap-download.mdx b/i18n/en/docusaurus-plugin-content-docs/current/tap-download.mdx deleted file mode 100644 index be3d42b21..000000000 --- a/i18n/en/docusaurus-plugin-content-docs/current/tap-download.mdx +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Downloads ---- - -## SDK - -- [TapSDK Unity](https://github.com/taptap/TapSDK-Unity/releases) -- [TapSDK Android](https://github.com/taptap/TapSDK-Android/releases) -- [TapSDK iOS](https://github.com/taptap/TapSDK-iOS/releases) -- [LeanCloud C# SDK](https://github.com/leancloud/csharp-sdk/releases) - -## Demos - -- [Unity](https://github.com/taptap/TapSDK-Unity-Demo) -- [Android](https://github.com/taptap/TapSDK-Android) -- [iOS](https://github.com/taptap/TapSDK-iOS) - -## Login Button - -Click [icon.zip](https://capacity-files.lcfile.com/xLuT4Mi3gqy5K518LbRxsBo2WiwR0CHx/taptap-login-button.zip) to download the TapTap Login Button. diff --git a/leancloud/conf/docusaurus.config.js b/leancloud/conf/docusaurus.config.js deleted file mode 100644 index 85d95cfef..000000000 --- a/leancloud/conf/docusaurus.config.js +++ /dev/null @@ -1,173 +0,0 @@ -// @ts-check - -const PREVIEW = process.env.PREVIEW ?? "false"; - -/** @type {import('@docusaurus/types').Config} */ -const config = { - title: "LeanCloud 开发者文档", - url: "https://docs.leancloud.cn", - baseUrl: "/", - onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", - favicon: "img/lc-favicon.ico", - trailingSlash: true, - customFields: { - searchUrl: "https://lc-doc-search-api.leanapp.cn/search", - upItemListIndexUrl: "https://lc-doc-search-check-log.leanapp.cn/api/check-log-up", - aiSearchUrl :"https://tds-doc-search-ai-api.ap-sg.tdsapps.com/api/ai-search?type=LC", - aiSearchEnUrl :"https://tds-doc-search-ai-api.ap-sg.tdsapps.com/api/ai-search?type=LCen", - searchProviderName: "LeanDB Elasticsearch", - searchProviderWebsite: "https://docs.leancloud.cn/sdk/engine/database/es/", - mainDomainHost: "https://www.leancloud.cn", - dcDomainHost: "https://www.leancloud.cn", - }, - - i18n: { - localeConfigs: { - en: { - label: "English", - }, - "zh-Hans": { - label: "简体中文", - }, - }, - defaultLocale: "zh-Hans", - locales: ["zh-Hans", "en"], - }, - - presets: [ - [ - "classic", - /** @type {import('@docusaurus/preset-classic').Options} */ - ({ - docs: { - sidebarPath: require.resolve("./sidebars.js"), - routeBasePath: "/", - lastVersion: "current", - versions: { - current: { - label: "v3", - }, - }, - }, - theme: { - customCss: require.resolve("./src/styles/index.scss"), - }, - googleAnalytics: { - trackingID: "UA-73963350-1", - }, - }), - ], - ], - - themeConfig: - /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ - ({ - navbar: { - items: [ - { - label: "文档首页", - to: "/", - position: "right", - activeBaseRegex: "^/(?!.+)", - }, - { - label: 'API 文档', - position: 'right', - items: [ - { - label: 'Android/Java SDK API', - href: 'https://leancloud.github.io/java-unified-sdk/', - }, - { - label: 'Objective-C SDK API', - href: 'https://leancloud.github.io/objc-sdk/', - }, - { - label: 'Swfit SDK API', - href: 'https://leancloud.github.io/swift-sdk/', - }, - { - label: 'Flutter 数据存储 SDK API', - href: 'https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/leancloud_storage-library.html', - }, - { - label: 'Flutter 即时通讯 SDK API', - href: 'https://pub.dev/documentation/leancloud_official_plugin/latest/leancloud_plugin/leancloud_plugin-library.html', - }, - { - label: 'JavaScript 数据存储 SDK API', - href: 'https://leancloud.github.io/javascript-sdk/docs/', - }, - { - label: 'JavaScript 即时通讯 SDK API', - href: 'https://leancloud.github.io/js-realtime-sdk/docs/', - }, - { - label: 'JavaScript 多人在线对战 SDK API', - href: 'https://leancloud.github.io/Play-SDK-JS/doc/global.html', - }, - { - label: 'Python SDK API', - href: 'https://leancloud.github.io/python-sdk/', - }, - { - label: 'PHP SDK API', - href: 'https://leancloud.github.io/php-sdk/', - }, - { - label: 'Go SDK API', - href: 'https://pkg.go.dev/github.com/leancloud/go-sdk/leancloud', - }, - { - label: '.NET SDK API', - href: 'https://leancloud.github.io/csharp-sdk/html/', - } - ], - }, - { - label: '资源', - position: 'right', - items: [ - { - label: 'SDK', - href: '/sdk/sdk-page/', - }, - { - label: 'Demo', - to: '/demo', - } - ], - }, - { - label: "云课堂", - to: "/classroom", - position: "right", - }, - { - type: "localeDropdown", - position: "right", - } - ], - }, - prism: { - theme: require("./src/theme/prism-taptap"), - additionalLanguages: ["csharp", "java", "php", "groovy", "swift", "dart"], - }, - image: "/img/logo.svg", - metadata: [ - { - name: "keywords", - content: "leancloud 开发者 文档", - }, - ], - colorMode: { - defaultMode: "light", - disableSwitch: true, - }, - }), - - plugins: ["docusaurus-plugin-sass"], -}; - -module.exports = config; diff --git a/leancloud/conf/env.ts b/leancloud/conf/env.ts deleted file mode 100644 index ef24d4d96..000000000 --- a/leancloud/conf/env.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const BRAND: string = "leancloud"; -export const REGION: string = "cn"; - -// Cloud Engine -export const CLI_BINARY: string = BRAND === "tds" ? "tds" : "lean"; -export const HAS_SUB_DOMAIN: boolean = REGION === "global"; -export const HAS_ENGINE_CDN_DOMAIN: boolean = REGION === 'cn' diff --git a/leancloud/conf/override.scss b/leancloud/conf/override.scss deleted file mode 100644 index a682fb1f3..000000000 --- a/leancloud/conf/override.scss +++ /dev/null @@ -1,72 +0,0 @@ -/* source: ~infima/dist/css/default/default.css */ -@import "font"; - -:root { - --ifm-code-font-size: 95%; - --ifm-color-emphasis-300: var(--tap-grey2); - --ifm-color-primary: #2c97e8; - --ifm-color-primary-dark: #188ae0; - --ifm-color-primary-darker: #1782d4; - --ifm-color-primary-darkest: #136bae; - --ifm-color-primary-light: #45a3eb; - --ifm-color-primary-lighter: #51a9ec; - --ifm-color-primary-lightest: #77bcf0; - --ifm-dropdown-hover-background-color: var(--tap-grey0); - --ifm-dropdown-link-color: var(--tap-grey6); - --ifm-global-shadow-md: var(--tap-box-shadow-3); - --ifm-hr-border-color: var(--tap-grey2); - --ifm-leading: 1em; - --ifm-menu-color-background-active: #eef6fc; - --ifm-menu-color-background-hover: #fafafa; - --ifm-menu-link-padding-horizontal: 19px; - --ifm-menu-link-padding-vertical: 7px; - --ifm-menu-link-sublist-icon: url('data:image/svg+xml;utf8,'); - --ifm-navbar-height: 56px; - --ifm-navbar-link-color: var(--tap-grey6); - --ifm-navbar-padding-horizontal: 24px; - --ifm-scrollbar-size: 6px; - --ifm-scrollbar-thumb-background-color: #e4e7e8; - --ifm-scrollbar-thumb-hover-background-color: #e4e7e8; - --ifm-scrollbar-track-background-color: transparent; - --ifm-table-stripe-background: var(--ifm-table-background); - --ifm-toc-border-color: var(--tap-grey2); - --docusaurus-highlighted-code-line-bg: #303641; -} - -.table-of-contents { - @include tap-font-14; - position: relative; - - ul { - @include tap-font-12; - } - - strong { - @include tap-font-regular; - } - - &__link--active { - &:before { - content: ""; - position: absolute; - left: -1px; - margin-top: 0.25em; - width: 2px; - height: 1em; - background-color: var(--ifm-color-primary); - } - } -} - -.pagination-nav { - padding-top: 2.4rem; - - &__sublabel { - color: var(--tap-grey5); - } - - &__label { - margin-top: 4px; - color: var(--tap-grey6); - } -} diff --git a/leancloud/conf/sidebars.js b/leancloud/conf/sidebars.js deleted file mode 100644 index 9c9daf379..000000000 --- a/leancloud/conf/sidebars.js +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-check - -/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ -const sidebars = { - sdk: [ - { - type: "autogenerated", - dirName: "sdk", - }, - ], -}; - -module.exports = sidebars; diff --git a/leancloud/docs/sdk/_partials/android-package-visibility.mdx b/leancloud/docs/sdk/_partials/android-package-visibility.mdx deleted file mode 100644 index dcfd0fe87..000000000 --- a/leancloud/docs/sdk/_partials/android-package-visibility.mdx +++ /dev/null @@ -1,39 +0,0 @@ -Android 11(API level 30)之后加强了隐私保护策略,引入了大量变更和限制,其中一个重要变更——[软件包可见性](https://developer.android.com/about/versions/11/privacy/package-visibility),将会导致第三方应用无法拉起 TapTap 客户端,从而影响 TapTap 相关功能的正常使用,包括但不限于更新唤起 TapTap、购买验证等功能。 - -如果没有完成适配,Android 版本为 11 及更高版本的客户端打开游戏会提示「本游戏需要最新版 TapTap 服务支持」,无法正常进入游戏。异常呈现如下图所示: - -![图片描述](/img/android-package-visibility-android11.png) - -对此提供如下两种适配方案: - -**方案一:** - -编译时将 `targetSdkVersion` 改为 29(目前设置成 >= 30 会触发该问题)。 - -**方案二:** - -1. 将 gradle build tools 改为 4.1.0+: - - ```java - classpath 'com.android.tools.build:gradle:4.1.0' - ``` - -2. 在 AndroidManifest.xml 里添加 `` 标签中的内容: - - ```xml - - - - - - - - - - - - - ``` diff --git a/leancloud/docs/sdk/_partials/languages.mdx b/leancloud/docs/sdk/_partials/languages.mdx deleted file mode 100644 index 9433b235d..000000000 --- a/leancloud/docs/sdk/_partials/languages.mdx +++ /dev/null @@ -1,133 +0,0 @@ -import MultiLang from "/src/docComponents/MultiLang"; - - - -<> - -```cs -TapCommon.SetLanguage(TapLanguage.AUTO); -``` - -支持如下语言: - -```cs -namespace TapTap.Common -{ - public enum TapLanguage - { - AUTO = 0, // 自动 - ZH_HANS = 1, // 简体中文 - EN = 2, // 英文 - ZH_HANT = 3, // 繁体中文 - JA = 4, // 日文 - KO = 5, // 韩文 - TH = 6, // 泰文 - ID = 7, // 印尼语 - DE = 8, // 德语 - ES = 9, // 西班牙语 - FR = 10, // 法语 - PT = 11, // 葡萄牙语 - RU = 12, // 俄语 - TR = 13, // 土耳其语 - VI = 14, // 越南语 - } -} -``` - - - -<> - -```java -TapLanguage lang = 0; -TapBootstrap.setPreferredLanguage(TapLanguage.AUTO); -``` - -支持如下语言: - -``` -AUTO 跟随系统 -ZH_HANS 简体中文 -EN 英文 -ZH_HANT 繁体中文 -JA 日文 -KO 韩文 -TH 泰文 -ID 印尼语 -DE 德语 -ES 西班牙语 -FR 法语 -PT 葡萄牙语 -RU 俄语 -TR 土耳其语 -VI 越南语 -``` - - - -<> - -```objc -[TapBootstrap setPreferredLanguage:TapLanguageType_Auto]; -``` - -支持如下语言: - -```objc -typedef NS_ENUM (NSInteger, TapLanguageType) { - TapLanguageType_Auto = 0, // 自动 - TapLanguageType_zh_Hans, // 简体中文 - TapLanguageType_en, // 英文 - TapLanguageType_zh_Hant, // 繁体中文 - TapLanguageType_ja, // 日文 - TapLanguageType_ko, // 韩文 - TapLanguageType_th, // 泰文 - TapLanguageType_id, // 印度尼西亚语 - TapLanguageType_de, // 德语 - TapLanguageType_es, // 西班牙语 - TapLanguageType_fr, // 法语 - TapLanguageType_pt, // 葡萄牙语 - TapLanguageType_ru, // 俄语 - TapLanguageType_tr, // 土耳其语 - TapLanguageType_vi, // 越南语 -}; -``` - - - -<> - -```cpp -TapUECommon::SetLanguage(ELanguageType::AUTO); -``` - -支持如下语言: - -```cpp -UENUM(BlueprintType) -enum class ELanguageType : uint8 -{ - AUTO = 0, // 自动 - ZH, // 简体中文 - EN, // 英文,海外默认语言 - ZHTW, // 繁体中文 - JA, // 日语 - KO, // 韩语 - TH, // 泰文 - ID, // 印尼文 - DE, // 德语 - ES, // 西班牙语 - FR, // 法语 - PT, // 葡萄牙语 - RU, // 俄语 - TR, // 土耳其语 - VI, // 越南语 -}; -``` - - - - - -「自动」会尝试根据系统语言设置语言,如果系统语言不在上述支持的语言之中,那么会根据 [SDK 初始化时配置的区域](/sdk/start/quickstart/#初始化)设置语言。 -区域为中国大陆时会设置为简体中文,否则会设置为英文。 diff --git a/leancloud/docs/sdk/_partials/setup-domain.mdx b/leancloud/docs/sdk/_partials/setup-domain.mdx deleted file mode 100644 index a09a82153..000000000 --- a/leancloud/docs/sdk/_partials/setup-domain.mdx +++ /dev/null @@ -1,64 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - - - - - -使用 TDS 提供的云服务,初始化客户端 SDK 需要在 `server_url` 处填入 API 域名,可前往 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置** 获取 TDS 提供的 **共享域名**。 - - - - - -使用 TDS 提供的云服务需要绑定 **API 自定义域名**,以便和其他厂商的应用隔离入口,避免其他应用受到 DDoS 攻击时相互牵连。 - -初始化客户端 SDK 时 `server_url` 处填入的就是 API 域名。 - -### 绑定 API 域名 - -绑定 API 域名的前提是,你拥有一个**已经完成备案的域名**。 - -
    -点击查看 API 域名绑定步骤 - -假设你的域名为 `example.com`,API 域名绑定步骤和状态如下: - -![domain guide](https://capacity-files.lcfile.com/RonCpipde80meo5BL8fxrjHTee39Wit6/domain-guide.png) - -- 进入 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置 > API** ,点击 **「绑定新域名」** 按钮。API 域名不支持直接绑定裸域名,需要在主域名的前面添加自定义名称,也就是绑定一个子域名,比如这里你可以绑定 `api.example.com`。 -- 控制台显示 **「正在检查备案信息」**,请等待一会儿。 -- 如果域名没有完成备案,将会显示 **「绑定失败」**。 -- 域名备案检查通过,域名下方显示 **「请配置 DNS」**。 -- 此时需要到你的域名服务商控制台,进入域名解析设置页面,添加一条记录,记录类型为 A(A 记录可以将域名指向一个 IP 地址),请将前面填到开发者后台的自定义域名和 **「推荐 DNS 配置」** 下方 A 记录值复制到对应位置。 -- DNS 解析记录和证书申请(如果选择了自动管理 SSL 证书)都需要一定时间,请耐心等待。记录生效后,控制台便会显示 **「已绑定」**。 - -
    - -绑定成功后,初始化 SDK 时请传入绑定的自定义域名 `https://api.example.com`。这里用的是示例,请换成你自己绑定的 API 域名,注意不要遗漏前面的 `https://`。 - -配置域名需要一定的时间,TDS 为开发者提供了 **共享域名** 在游戏测试时使用,但共享域名没有可用性保证,容易受到 DDoS 攻击影响。游戏上线前,一定要确认使用的 API 访问域名是开发者自己绑定的域名,请勿将共享域名用于生产环境。 - -### 绑定文件域名 - -如果使用了数据存储中的文件服务,需要绑定**文件访问域名**。 - -可前往 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置 > 文件** 绑定文件域名,步骤和 API 自定义域名基本相同。但有两点不一样: - -1. API 域名解析使用 A 记录,文件域名解析使用 CNAME 记录。文件域名同样不支持绑定裸域名,需要绑定子域名。例如你的主域名是 `example.com`,可以绑定文件域名为 `files.example.com`。 -2. 绑定成功后,还需在 **开发与构建 > 数据存储 > 文件 > 设置 > 文件访问地址** 点击「修改」按钮进行切换。 - -:::info - -每个子域名只能绑定到一个游戏,且 API 域名和文件域名不可使用同样的子域名。如果你已经在 TDS 控制台绑定了某个子域名,重复绑定时控制台会显示「该域名已经被其他应用所绑定」,此时可以更换同一主域名下不同的子域名,来完成后续绑定步骤。 - -::: - -
    - -
    - - - -请参考 [域名绑定指南](/sdk/domain/guide/)。 - - diff --git a/leancloud/docs/sdk/_partials/tap-login-profile.mdx b/leancloud/docs/sdk/_partials/tap-login-profile.mdx deleted file mode 100644 index d1a31d605..000000000 --- a/leancloud/docs/sdk/_partials/tap-login-profile.mdx +++ /dev/null @@ -1,39 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -这里获取的 `TapTapAccount` 根据游戏申请的[授权范围](/sdk/taptap-login/guide/start/#不同的授权范围)有所差异。 - -其中可能会包含如下信息: - - - -| 参数 | 说明 | -| --------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `accessToken` | 用户访问凭证 | -| `openid` | 通过用户信息和游戏信息生成的**用户唯一标识**,每个玩家在每个游戏中的 `openid` 都是不一样的 | -| `unionid` | 通过用户信息加上厂商信息生成的用户唯一标识,一个玩家在同一个厂商的所有游戏中 `unionid` 都是一样的,不同厂商下 `unionid` 不同 | -| `name` | 玩家在 TapTap 平台的昵称 | -| `avatar` | 玩家在 TapTap 平台的头像 url -| `email` | 用户在 TapTap 平台注册使用的邮箱 | - - - - - -| 参数 | 说明 | -| --------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `accessToken` | 用户访问凭证 | -| `openid` | 通过用户信息和游戏信息生成的**用户唯一标识**,每个玩家在每个游戏中的 `openid` 都是不一样的 | -| `unionid` | 通过用户信息加上厂商信息生成的用户唯一标识,一个玩家在同一个厂商的所有游戏中 `unionid` 都是一样的,不同厂商下 `unionid` 不同 | -| `name` | 玩家在 TapTap 平台的昵称 | -| `avatar` | 玩家在 TapTap 平台的头像 url -| `email` | 用户在 TapTap 平台注册使用的邮箱 | - | - - - -`openid` 和 `unionid` 使用[标准的 Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4)(带 Padding)编码,包含的字符有 `A-Za-z0-9+/=`。`openid` 和 `unionid` 长度最大值为 50 个字符。 - -:::info -由于 `unionid` 与游戏所属厂商有强关联性,因此 `unionid` 适用于如下场景:厂商使用测试服进行付费删档等测试,正式服需要对于之前参与测试的老玩家进行返利等操作。因为同一个玩家在同一个厂商下的所有游戏中的 `unionid` 不变。 -一个游戏在厂商转移后同一个用户的 `unionid` 会发生改变,如果游戏使用了 `unionid`,TDS 技术支持会在转移前通过工单和游戏开发者确认相关数据的处理方案,保证迁移前后用户数据不错乱。 -::: diff --git a/leancloud/docs/sdk/_partials/unity-sdk-installation.mdx b/leancloud/docs/sdk/_partials/unity-sdk-installation.mdx deleted file mode 100644 index f1dcda6fb..000000000 --- a/leancloud/docs/sdk/_partials/unity-sdk-installation.mdx +++ /dev/null @@ -1,79 +0,0 @@ -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -SDK 可以**通过 Unity Package Manager 导入或手动导入**,二者任选其一。请根据项目需要选择。 - -#### 方法一:使用 Unity Package Manager -从 3.29.1 版本开始, SDK 修改 JSON 解析库为 `Newtonsoft-json`,如果当前工程已接入该依赖库,则不需额外处理,否则需在 `Packages/manifest.json` 添加如下依赖: - -``` -"com.unity.nuget.newtonsoft-json":"3.2.1" -``` - -##### NPMJS 安装 - -从 3.25.0 版本开始,TapSDK 支持了 NPMJS 安装,优势是只需要配置版本号,并且支持嵌套依赖。 - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - - - {`"dependencies":{ - ${props.npmDeps.map(dep => `"${dep}":"${sdkVersions.taptap.unity}",`).join('\n ')} - }`} - - -但需要注意的是,要在 `Packages/manifest.json` 中 `dependencies` 同级下声明 `scopedRegistries`: - - -{props.NoNeedLC ? - `"scopedRegistries": [ - { - "name": "NPMJS", - "url": "https://registry.npmjs.org/", - "scopes": ["com.tapsdk", "com.taptap"] - } - ] - ` : - `"scopedRegistries": [ - { - "name": "NPMJS", - "url": "https://registry.npmjs.org/", - "scopes": ["com.tapsdk", "com.taptap", "com.leancloud"] - } - ] - ` -} - - -##### GitHub 安装 - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - - - {`"dependencies":{ - ${props.githubDeps.map(dep => `"${dep.package}":"${dep.url}",`).join('\n ')} - }`} - - -在 Unity 顶部菜单中选择 **Window > Package Manager** 可查看已经安装在项目中的包。 - -#### 方法二:手动导入 - -1. 在 [下载页](/tap-download) 找到 **TapSDK Unity** 下载地址,下载 `TapSDK-UnityPackage.zip` 。 - -2. 在 Unity 项目中依次转到 **Assets > Import Packages > Custom Packages**,从解压后的 `TapSDK-UnityPackage.zip` 中,选择希望在游戏中使用的 TapSDK 包导入,其中: - -
      - {props.unitypackageModules.map(module =>
    • {module.name} {module.desc}。
    • )} -
    - -3. 如果当前项目已集成 `Newtonsoft.Json` 依赖,则忽略该步骤,否则在 `NuGet.org` [ Newtonsoft.Json ](https://www.nuget.org/packages/Newtonsoft.Json) 页面中通过点击右侧 「Download package」 下载库文件,并将下载的文件后缀从`.nupkg` 修改为 `.zip`,同时解压该文件并复制内部的 `Newtonsoft.Json.dll` 文件拷贝到工程 `Assets` 的 `Plugins` 目录下,另外为了避免导出 IL2CPP 平台时删除必要数据,需在 `Assets` 目录下创建 `link.xml` 文件(如果已有该文件,则添加如下内容),其内容如下: - -``` - - - - - - -``` \ No newline at end of file diff --git a/leancloud/docs/sdk/authentication/_category_.json b/leancloud/docs/sdk/authentication/_category_.json deleted file mode 100644 index 5da22988f..000000000 --- a/leancloud/docs/sdk/authentication/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "内建账户", - "collapsed": true, - "position": 5 -} diff --git a/leancloud/docs/sdk/authentication/faq.mdx b/leancloud/docs/sdk/authentication/faq.mdx deleted file mode 100644 index 8110d8eea..000000000 --- a/leancloud/docs/sdk/authentication/faq.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: 内建账户常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -### 应用内用户的密码需要加密吗 - -不需要加密密码,我们的服务端已使用随机生成的 salt,自动对密码做了加密。 如果用户忘记了密码,可以调用 `requestResetPassword` 方法(具体查看 SDK 的 AVUser 用法),向用户注册的邮箱发送邮件,用户以此可自行重设密码。 在整个过程中,密码都不会有明文保存的问题,密码也不会在客户端保存,只是会保存 sessionToken 来标示用户的登录状态。 - -### sessionToken 在什么情况下会失效? - -如果在控制台的存储的设置中勾选了「密码修改后,强制客户端重新登录」,则用户修改密码后, sessionToken 会变更,需要重新登录。如果没有勾选这个选项,Token 就不会改变。当新建应用时,这个选项默认是被勾上的。 - -### 在 PC 端用手机号登录,在小程序上用微信登录,如何绑定到同一个账号上? - -从逻辑上,在 PC 端登录的账号,与在小程序中用微信登录的账号,他们没有任何可以联系在一起的地方。如果都是独立创建了两个账号,只能在业务层面进行绑定(也就是将一个账号的所有关联对象全都迁移到另一个账号,然后删除原账号)。 - -如果可以在业务上加一些限制,则可以避免上面这种「创建了两个独立的账号」的情况。比如,如果手机号是账号必须设置的信息,那么我们可以在以手机号作为关联项。具体的步骤如下,首先是 `loginWithWeapp` 并带上 `failOnNotExist` 参数,这样如果该微信关联的用户已经存在则照常登录,如果没有则会失败,此时跳转到使用手机号登录/注册的页面,让用户通过手机号登录或注册,成功之后再通过 `associateWithWeapp` 接口关联当前微信账号。 - -### 不通过短信验证能否强制修改 _User 表 mobilePhoneVerified 字段,使其设置为 true? - -可以通过云引擎使用 [master key](/sdk/engine/functions/sdk/#使用超级权限) 来修改 `mobilePhoneVerified` 的值。因为云引擎运行在可信的服务器端环境中,所以可以全局开启超级权限(Master Key),这样云端会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据,当然这种方式也只允许调用一些仅供 Master Key 使用的 API。 diff --git a/leancloud/docs/sdk/authentication/guide.mdx b/leancloud/docs/sdk/authentication/guide.mdx deleted file mode 100644 index 865d680d3..000000000 --- a/leancloud/docs/sdk/authentication/guide.mdx +++ /dev/null @@ -1,3852 +0,0 @@ ---- -title: 内建账户指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; - -用户系统几乎是每款应用都要加入的功能,我们为此专门提供了一个 `LCUser` 类来方便应用使用各项用户管理的功能。 - -`LCUser` 是 `LCObject` 的子类,这意味着任何 `LCObject` 提供的方法也适用于 `LCUser`,唯一的区别就是 `LCUser` 提供一些额外的用户管理相关的功能。 - -## 用户的属性 - -`LCUser` 相比一个普通的 `LCObject` 多出了以下属性: - -- `username`:用户的用户名。 -- `password`:用户的密码。 -- `email`:用户的电子邮箱。 -- `emailVerified`:用户的电子邮箱是否已验证。 -- `mobilePhoneNumber`:用户的手机号。 -- `mobilePhoneVerified`:用户的手机号是否已验证。 - -在接下来对用户功能的介绍中我们会逐一了解到这些属性。 - -## 注册 - -用户第一次打开应用的时候,可以让用户注册一个账户。下面的代码展示了一个典型的使用用户名和密码注册的流程: - - - -<> - -```cs -// 创建实例 -LCUser user = new LCUser(); - -// 等同于 user["username"] = "Tom"; -user.Username = "Tom"; -user.Password = "cat!@#123"; - -// 可选 -user.Email = "tom@xd.com"; -user.Mobile = "+8619201680101"; - -// 设置其他属性的方法跟 LCObject 一样 -user["gender"] = "secret"; -await user.SignUp(); -``` - -新建 `LCUser` 的操作应使用 `SignUp` 而不是 `Save`,但以后的更新操作就可以用 `Save` 了。 - - - -<> - -```java -// 创建实例 -LCUser user = new LCUser(); - -// 等同于 user.put("username", "Tom") -user.setUsername("Tom"); -user.setPassword("cat!@#123"); - -// 可选 -user.setEmail("tom@xd.com"); -user.setMobilePhoneNumber("+8619201680101"); - -// 设置其他属性的方法跟 LCObject 一样 -user.put("gender", "secret"); - -user.signUpInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 注册成功 - System.out.println("注册成功。objectId:" + user.getObjectId()); - } - public void onError(Throwable throwable) { - // 注册失败(通常是因为用户名已被使用) - } - public void onComplete() {} -}); -``` - -新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 - - - -<> - -```objc -// 创建实例 -LCUser *user = [LCUser user]; - -// 等同于 [user setObject:@"Tom" forKey:@"username"] -user.username = @"Tom"; -user.password = @"cat!@#123"; - -// 可选 -user.email = @"tom@xd.com"; -user.mobilePhoneNumber = @"+8619201680101"; - -// 设置其他属性的方法跟 LCObject 一样 -[user setObject:@"secret" forKey:@"gender"]; - -[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 注册成功 - NSLog(@"注册成功。objectId:%@", user.objectId); - } else { - // 注册失败(通常是因为用户名已被使用) - } -}]; -``` - -新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 - - - -<> - -```swift -do { - // 创建实例 - let user = LCUser() - - // 等同于 user.set("username", value: "Tom") - user.username = LCString("Tom") - user.password = LCString("cat!@#123") - - // 可选 - user.email = LCString("tom@xd.com") - user.mobilePhoneNumber = LCString("+8619201680101") - - // 设置其他属性的方法跟 LCObject 一样 - try user.set("gender", value: "secret") - - _ = user.signUp { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -新建 `LCUser` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -<> - -```dart -// 创建实例 -LCUser user = LCUser(); - -// 等同于 user['username'] = 'Tom'; -user.username = 'Tom'; -user.password = 'cat!@#123'; - -// 可选 -user.email = 'tom@xd.com'; -user.mobile = '+8619201680101'; - -// 设置其他属性的方法跟 LCObject 一样 -user['gender'] = 'secret'; -await user.signUp(); -``` - -新建 `LCUser` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -<> - -```js -// 创建实例 -const user = new AV.User(); - -// 等同于 user.set('username', 'Tom') -user.setUsername("Tom"); -user.setPassword("cat!@#123"); - -// 可选 -user.setEmail("tom@xd.com"); -user.setMobilePhoneNumber("+8619201680101"); - -// 设置其他属性的方法跟 AV.Object 一样 -user.set("gender", "secret"); - -user.signUp().then( - (user) => { - // 注册成功 - console.log(`注册成功。objectId:${user.id}`); - }, - (error) => { - // 注册失败(通常是因为用户名已被使用) - } -); -``` - -新建 `AV.User` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -<> - -```python -# 创建实例 -user = leancloud.User() - -# 等同于 user.set('username', 'Tom') -user.set_username('Tom') -user.set_password('cat!@#123') - -# 可选 -user.set_email('tom@xd.com') -user.set_mobile_phone_number('+8619201680101') - -# 设置其他属性的方法跟 leancloud.Object 一样 -user.set('gender', 'secret') - -user.sign_up() -``` - -新建 `leancloud.User` 的操作应使用 `sign_up` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -<> - -```php -// 创建实例 -$user = new User(); - -// 等同于 $user->set("username", "Tom") -$user->setUsername("Tom"); -$user->setPassword("cat!@#123"); - -// 可选 -$user->setEmail("tom@xd.com"); -$user->setMobilePhoneNumber("+8619201680101"); - -// 设置其他属性的方法跟 LeanObject 一样 -$user->set("gender", "secret"); - -$user->signUp(); -``` - -新建 `User` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -```go -// 注册用户 -user, err := client.Users.SignUp("Tom", "cat!@#123") -if err != nil { - panic(err) -} - -// 设置其他属性 -if err := client.Users.ID(user.ID).Set("email", "tom@xd.com", leancloud.UseUser(user)); err != nil { - panic(err) -} -``` - - - -如果收到 `202` 错误码,意味着已经存在使用同一 `username` 的账号,此时应提示用户换一个用户名。除此之外,每个用户的 `email` 和 `mobilePhoneNumber` 也需要保持唯一性,否则会收到 `203` 或 `214` 错误。可以考虑在注册时把用户的 `username` 设为与 `email` 相同,这样用户可以直接 [用邮箱重置密码](#重置密码)。 - -采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码(云端对密码的长度、复杂度不作限制),并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调 **在客户端,应用切勿再次对密码加密,这会导致 [重置密码](#重置密码) 等功能失效**。 - -### 手机号注册 - -对于移动应用来说,允许用户以手机号注册是个很常见的需求。实现该功能大致分两步,第一步是让用户提供手机号,点击「获取验证码」按钮后,该号码会收到一个六位数的验证码: - - - -```cs -await LCSMSClient.RequestSMSCode("+8619201680101"); -``` - -```java -LCSMSOption option = new LCSMSOption(); -option.setSignatureName("sign_name"); // 设置短信签名名称 -LCSMS.requestSMSCodeInBackground("+8619201680101", option).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - } - @Override - public void onNext(LCNull avNull) { - Log.d("TAG","Result: succeed to request SMSCode."); - } - @Override - public void onError(Throwable throwable) { - Log.d("TAG","Result: failed to request SMSCode. cause:" + throwable.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -LCShortMessageRequestOptions *options = [[LCShortMessageRequestOptions alloc] init]; -options.templateName = @"template_name"; // 控制台配置好的模板名称 -options.signatureName = @"sign_name"; // 控制台配置好的短信签名名称 -[LCSMS requestShortMessageForPhoneNumber:@"+8619201680101" options:options callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - /* 请求成功 */ - } else { - /* 请求失败 */ - } -}]; -``` - -```swift -// templateName 是短信模版名称,signatureName 是短信签名名称。可以在控制台 > 短信 > 设置中查看。 -_ = LCSMSClient.requestShortMessage(mobilePhoneNumber: "+8619201680101", templateName: "template_name", signatureName: "sign_name") { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCSMSClient.requestSMSCode('+8619201680101'); -``` - -```js -AV.Cloud.requestSmsCode("+8619201680101"); -``` - -```python -leancloud.cloud.request_sms_code('+8619201680101') -``` - -```php -SMS::requestSmsCode("+8619201680101"); -``` - -```go -// 暂不支持 -``` - - - -用户填入验证码后,用下面的方法完成注册: - - - -```cs -await LCUser.SignUpOrLoginByMobilePhone("+8619201680101", "123456"); -``` - -```java -LCUser.signUpOrLoginByMobilePhoneInBackground("+8619201680101", "123456").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 注册成功 - System.out.println("注册成功。objectId:" + user.getObjectId()); - } - public void onError(Throwable throwable) { - // 验证码不正确 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser signUpOrLoginWithMobilePhoneNumberInBackground:@"+8619201680101" smsCode:@"123456" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 注册成功 - NSLog(@"注册成功。objectId:%@", user.objectId); - } else { - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.signUpOrLogIn(mobilePhoneNumber: "+8619201680101", verificationCode: "123456", completion: { (result) in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -}) -``` - -```dart - await LCUser.signUpOrLoginByMobilePhone('+8619201680101', '123456'); -``` - -```js -AV.User.signUpOrlogInWithMobilePhone("+8619201680101", "123456").then( - (user) => { - // 注册成功 - console.log(`注册成功。objectId:${user.id}`); - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -user = leancloud.User.signup_or_login_with_mobile_phone('+8619201680101', '123456') -``` - -```php -User::signUpOrLoginByMobilePhone("+8619201680101", "123456"); -``` - -```go -user, err := client.Users.SignUpByMobilePhone("+8619201680101", "123456") -if err != nil { - panic(err) -} -``` - - - -`username` 将与 `mobilePhoneNumber` 相同,`password` 会由云端随机生成。如果希望让用户指定密码,可以在客户端让用户填写手机号和密码,然后按照上一小节使用用户名和密码注册的流程,将用户填写的手机号作为 `username` 和 `mobilePhoneNumber` 的值同时提交。同时根据业务需求,在**云服务控制台 > 内建账户 > 设置**勾选**未验证手机号码的用户,禁止登录**、**已验证手机号码的用户,允许以短信验证码登录**。 - -### 手机号格式 - -云端接受的手机号以 `+` 和国家代码开头,后面紧跟着剩余的部分。手机号中不应含有任何划线、空格等非数字字符。例如,`+15559463664` 是一个合法的美国或加拿大手机号(`1` 是国家代码),`+8619201680101` 是一个合法的中国手机号(`86` 是国家代码)。 - -请参阅官网的[价格](https://www.leancloud.cn/pricing/)页面以了解支持的国家和地区。 - -## 登录 - -下面的代码用用户名和密码登录一个账户: - - - -```cs -try { - // 登录成功 - LCUser user = await LCUser.Login("Tom", "cat!@#123"); -} catch (LCException e) { - // 登录失败(可能是密码错误) - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` - -```objc -[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 登录失败(可能是密码错误) - } -}]; -``` - -```swift -_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - // 登录成功 - LCUser user = await LCUser.login('Tom', 'cat!@#123'); -} on LCException catch (e) { - // 登录失败(可能是密码错误) - print('${e.code} : ${e.message}'); -} -``` - -```js -AV.User.logIn("Tom", "cat!@#123").then( - (user) => { - // 登录成功 - }, - (error) => { - // 登录失败(可能是密码错误) - } -); -``` - -```python -user = leancloud.User() -user.login(username='Tom', password='cat!@#123') -``` - -```php -User::logIn("Tom", "cat!@#123"); -``` - -```go -user, err := client.Users.LogIn("Tom", "cat!@#123") -if err != nil { - panic(err) -} -``` - - - -### 邮箱登录 - -下面的代码用邮箱和密码登录一个账户: - - - -```cs -try { - // 登录成功 - LCUser user = await LCUser.LoginByEmail("tom@xd.com", "cat!@#123"); -} catch (LCException e) { - // 登录失败(可能是密码错误) - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.loginByEmail("tom@xd.com", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` - -```objc -[LCUser loginWithEmail:@"tom@xd.com" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 登录失败(可能是密码错误) - } -}]; -``` - -```swift -_ = LCUser.logIn(email: "tom@xd.com", password: "cat!@#123") { result in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - // 登录成功 - LCUser user = await LCUser.loginByEmail('tom@xd.com', 'cat!@#123'); -} on LCException catch (e) { - // 登录失败(可能是密码错误) - print('${e.code} : ${e.message}'); -} -``` - -```js -AV.User.loginWithEmail("tom@xd.com", "cat!@#123").then( - (user) => { - // 登录成功 - }, - (error) => { - // 登录失败(可能是密码错误) - } -); -``` - -```python -user = leancloud.User() -user.login(email='tom@xd.com', password='cat!@#123') -``` - -```php -User::logInWithEmail("tom@xd.com", "cat!@#123"); -``` - -```go -user, err := client.LoginByEmail("tom@xd.com", "cat!@#123") -if err != nil { - panic(err) -} - -fmt.Println(user) -``` - - - -### 手机号登录 - -如果应用允许用户以手机号注册,那么也可以让用户以手机号配合密码或短信验证码登录。下面的代码用手机号和密码登录一个账户: - - - -```cs -try { - // 登录成功 - LCUser user = await LCUser.LoginByMobilePhoneNumber("+8619201680101", "cat!@#123"); -} catch (LCException e) { - // 登录失败(可能是密码错误) - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.loginByMobilePhoneNumber("+8619201680101", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` - -```objc -[LCUser logInWithMobilePhoneNumberInBackground:@"+8619201680101" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 登录失败(可能是密码错误) - } -}]; -``` - -```swift -_ = LCUser.logIn(mobilePhoneNumber: "+8619201680101", password: "cat!@#123") { result in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - // 登录成功 - LCUser user = await LCUser.loginByMobilePhoneNumber('+8619201680101', 'cat!@#123'); -} on LCException catch (e) { - // 登录失败(可能是密码错误) - print('${e.code} : ${e.message}'); -} -``` - -```js -AV.User.logInWithMobilePhone("+8619201680101", "cat!@#123").then( - (user) => { - // 登录成功 - }, - (error) => { - // 登录失败(可能是密码错误) - } -); -``` - -```python -user = leancloud.User.login_with_mobile_phone('+8619201680101', 'cat!@#123') -``` - -```php -User::logInWithMobilePhoneNumber("+8619201680101", "cat!@#123"); -``` - -```go -user, err := client.LogInByMobilePhoneNumber("+8619201680101", "cat!@#123") -if err != nil { - panic(err) -} - -fmt.Println(user) -``` - - - -默认情况下,云服务允许所有关联了手机号的用户直接以手机号登录,无论手机号是否 [通过验证](#验证手机号)。为了让应用更加安全,你可以选择只允许验证过手机号的用户通过手机号登录。可以在 **控制台 > 内建账户 > 设置** 里面开启该功能。 - -除此之外,还可以让用户通过短信验证码登录,适用于用户忘记密码且不愿重置密码的情况。和 [通过手机号注册](#手机号注册) 的步骤类似,首先让用户填写与账户关联的手机号码,然后在用户点击「获取验证码」后调用下面的方法: - - - -```cs -await LCUser.RequestLoginSMSCode("+8619201680101"); -``` - -```java -LCUser.requestLoginSmsCodeInBackground("+8619201680101").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestLoginSmsCode:@"+8619201680101"]; -``` - -```swift -_ = LCUser.requestLoginVerificationCode(mobilePhoneNumber: "+8619201680101") { result in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestLoginSMSCode('+8619201680101'); -``` - -```js -AV.User.requestLoginSmsCode("+8619201680101"); -``` - -```python -leancloud.User.request_login_sms_code('+8619201680101') -``` - -```php -SMS::requestSmsCode("+8619201680101"); -``` - -```go -if err := client.Users.RequestLoginSMSCode("+8619201680101"); err != nil { - panic(err) -} -``` - - - -用户填写收到的验证码后,用下面的方法完成登录: - - - -```cs -try { - // 登录成功 - await LCUser.SignUpOrLoginByMobilePhone("+8619201680101", "123456"); -} catch (LCException e) { - // 验证码不正确 - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.signUpOrLoginByMobilePhoneInBackground("+8619201680101", "123456").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 验证码不正确 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser logInWithMobilePhoneNumberInBackground:@"+8619201680101" smsCode:@"123456" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.logIn(mobilePhoneNumber: "+8619201680101", verificationCode: "123456") { result in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - // 登录成功 - await LCUser.signUpOrLoginByMobilePhone('+8619201680101', '123456'); -} on LCException catch (e) { - // 验证码不正确 - print('${e.code} : ${e.message}'); -} -``` - -```js -AV.User.logInWithMobilePhoneSmsCode("+8619201680101", "123456").then( - (user) => { - // 登录成功 - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -user = leancloud.User.signup_or_login_with_mobile_phone('+8619201680101', '123456') -``` - -```php -User::signUpOrLoginByMobilePhone("+8619201680101", "123456"); -``` - -```go -user, err := client.Users.LogInByMobilePhoneNumber("+8619201680101", "123456") -if err != nil { - panic(err) -} -``` - - - -### 测试手机号和固定验证码 - -在开发过程中,可能会因测试目的而需要频繁地用手机号注册登录,然而运营商的发送频率限制往往会导致测试过程耗费较多的时间。 - -为了解决这个问题,可以在 **控制台 > 短信 > 设置** 里面设置一个测试手机号,而云端会为该号码生成一个固定验证码。以后进行登录操作时,只要使用的是这个号码,云端就会直接放行,无需经过运营商网络。 - -测试手机号还可用于将 iOS 应用提交到 App Store 进行审核的场景,因为审核人员可能因没有有效的手机号码而无法登录应用来进行评估审核。如果不提供一个测试手机号,应用有可能被拒绝。 - -可参阅 [短信 SMS 服务使用指南](/sdk/sms/guide/) 来了解更多有关短信发送和接收的限制。 - -### 单设备登录 - -某些场景下需要确保用户的账户在同一时间只在一台设备上登录,也就是说当用户在一台设备上登录后,其他设备上的会话全部失效。可以按照以下方案来实现: - -1. 新建一个专门用于记录用户登录信息和当前设备信息的 class。 -2. 每当用户在新设备上登录时,将该 class 中该用户对应的设备更新为该设备。 -3. 在另一台设备上打开客户端时,检查该设备是否与云端保存的一致。若不一致,则将用户 [登出](#当前用户)。 - -### 账户锁定 - -输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 `{ "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." }`,开发者可在客户端进行必要提示。 - -锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 - -## 验证邮箱 - -可以通过要求用户在登录或使用特定功能之前验证邮箱的方式防止恶意注册。默认情况下,当用户注册或变更邮箱后,`emailVerified` 会被设为 `false`。在应用的 **云服务控制台 > 内建账户 > 设置** 中,可以开启 **启用邮箱验证功能** 选项,这样当用户注册或变更邮箱时,会收到一封含有验证链接的邮件。在同一设置页面还可找到阻止未验证邮箱的用户登录的选项。 - -如果用户忘记点击链接并且在未来某一时刻需要进行验证,可以用下面的代码发送一封新的邮件: - - - -```cs -await LCUser.RequestEmailVerify("tom@xd.com"); -``` - -```java -LCUser.requestEmailVerifyInBackground("tom@xd.com").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestEmailVerify:@"tom@xd.com"]; -``` - -```swift -_ = LCUser.requestVerificationMail(email: "tom@xd.com") { result in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestEmailVerify('tom@xd.com'); -``` - -```js -AV.User.requestEmailVerify("tom@xd.com"); -``` - -```python -leancloud.User.request_email_verify('tom@xd.com') -``` - -```php -User::requestEmailVerify("tom@xd.com"); -``` - -```go -if err := client.Users.RequestEmailVerify("tom@xd.com"); err != nil { - panic(err) -} -``` - - - -用户点击邮件内的链接后,`emailVerified` 会变为 `true`。如果用户的 `email` 属性为空,则该属性永远不会为 `true`。 - -## 验证手机号 - -和 [验证邮箱](#验证邮箱) 类似,应用还可以要求用户在登录或使用特定功能之前验证手机号。默认情况下,当用户注册或变更手机号后,`mobilePhoneVerified` 会被设为 `false`。在应用的 **控制台 > 内建账户 > 设置** 中,可以开启阻止未验证手机号的用户登录的选项。 - -可以用下面的代码发送一条新的验证码(如果相应用户的 `mobilePhoneVerified` 已经为 `true`,那么验证短信不会发送): - - - -```cs -await LCUser.RequestMobilePhoneVerify("+8619201680101"); -``` - -```java -LCUser.requestMobilePhoneVerifyInBackground("+8619201680101").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestMobilePhoneVerify:@"+8619201680101" withBlock:^(BOOL succeeded, NSError * _Nullable error) { - if(succeeded){ - // 请求成功 - }else{ - // 请求失败 - } -}]; -``` - -```swift -_ = LCUser.requestVerificationCode(mobilePhoneNumber: "+8619201680101") { result in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestMobilePhoneVerify('+8619201680101'); -``` - -```js -AV.User.requestMobilePhoneVerify("+8619201680101"); -``` - -```python -leancloud.User.request_mobile_phone_verify('+8619201680101') -``` - -```php -User::requestMobilePhoneVerify("+8619201680101"); -``` - -```go -if err := client.Users.RequestMobilePhoneVerify("+8619201680101"); err != nil { - panic(err) -} -``` - - - -用户填写验证码后,调用下面的方法来完成验证。`mobilePhoneVerified` 将变为 `true`: - - - -```cs -await LCUser.VerifyMobilePhone("+8619201680101", "123456"); -``` - -```java -LCUser.verifyMobilePhoneInBackground("123456").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // mobilePhoneVerified 将变为 true - } - public void onError(Throwable throwable) { - // 验证码不正确 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser verifyMobilePhone:@"123456" withBlock:^(BOOL succeeded, NSError * _Nullable error) { - if(succeeded){ - // mobilePhoneVerified 将变为 true - }else{ - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.verifyMobilePhoneNumber(mobilePhoneNumber: "+8619201680101", verificationCode: "123456") { result in - switch result { - case .success: - // mobilePhoneVerified 将变为 true - break - case .failure(error: let error): - // 验证码不正确 - print(error) - } -} -``` - -```dart -await LCUser.verifyMobilePhone('+8619201680101','123456'); -``` - -```js -AV.User.verifyMobilePhone("123456").then( - () => { - // mobilePhoneVerified 将变为 true - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -leancloud.User.verify_mobile_phone_number('123456') -``` - -```php -User::verifyMobilePhone("123456"); -``` - -```go -// 暂不支持 -``` - - - -### 绑定、修改手机号之前先验证 - -除了在用户绑定、修改手机号**之后**进行验证,云服务也支持在用户绑定或修改手机号**之前**先通过短信验证。也就是说,绑定手机号或修改手机号时先请求发送验证码(用户需处于登录状态),再凭短信验证码完成绑定或修改操作。 - - - -```cs -await LCUser.RequestSMSCodeForUpdatingPhoneNumber("+8619201680101"); - -await LCUser.VerifyCodeForUpdatingPhoneNumber("+8619201680101", "123456"); -// 更新本地数据 -LCUser currentUser = await LCUser.GetCurrent(); -user.Mobile = "+8619201680101"; -``` - -```java -LCUser.requestSMSCodeForUpdatingPhoneNumberInBackground("+8619201680101",null).subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - } - @Override - public void onNext(@NonNull LCNull lcNull) { - // 成功调用 - } - @Override - public void onError(@NonNull Throwable e) { - // 调用出错 - } - @Override - public void onComplete() { - } -}); - -LCUser.verifySMSCodeForUpdatingPhoneNumberInBackground("123456", "+8619201680101").subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - } - @Override - public void onNext(@NonNull LCNull lcNull) { - // 更新本地数据 - LCUser currentUser = LCUser.getCurrentUser(); - currentUser.setMobilePhoneNumber("+8619201680101"); - } - @Override - public void onError(@NonNull Throwable e) { - // 验证码不正确 - } - @Override - public void onComplete() { - } -}); -``` - -```objc -[LCUser requestVerificationCodeForUpdatingPhoneNumber:@"+8619201680101" withBlock:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // 请求成功 - } else { - // 请求失败 - } -}]; - -[LCUser verifyCodeToUpdatePhoneNumber:@"+8619201680101" code:@"123456" withBlock:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // mobilePhoneNumber 变为 +8619201680101 - // mobilePhoneVerified 变为 true - } else { - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.requestVerificationCode(forUpdatingMobilePhoneNumber: "+8619201680101") { result in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} - -_ = LCUser.verifyVerificationCode("123456", toUpdateMobilePhoneNumber:"+8619201680101") { result in - switch result { - case .success: - // mobilePhoneNumber 变为 +8619201680101 - // mobilePhoneVerified 变为 true - break - case .failure(error: let error): - // 验证码不正确 - print(error) - } -} -``` - -```dart -await LCUser.requestSMSCodeForUpdatingPhoneNumber('+8619201680101'); - -await LCUser.verifyCodeForUpdatingPhoneNumber('+8619201680101', '123456'); -// 更新本地数据 -LCUser currentUser = await LCUser.getCurrent(); -user.mobile = '+8619201680101'; -``` - -```js -AV.User.requestChangePhoneNumber("+8619201680101"); - -AV.User.changePhoneNumber("+8619201680101", "123456").then( - () => { - // 更新本地数据 - const currentUser = AV.User.current(); - currentUser.setMobilePhoneNumber("+8619201680101"); - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -User.request_change_phone_number("+8619201680101") - -User.change_phone_number("123456", "+8619201680101") -# 更新本地数据 -current_user = leancloud.User.get_current() -current_user.set_mobile_phone_number("+8619201680101") -``` - -```php -User::requestChangePhoneNumber("+8619201680101"); - -User::changePhoneNumber("123456", "+8619201680101"); -// 更新本地数据 -$currentUser = User::getCurrentUser(); -$user->setMobilePhoneNumber("+8619201680101"); -``` - -```go -if err := client.Users.requestChangePhoneNumber("+8619201680101"); err != nil { - panic(err) -} - -if err := client.Users.ChangePhoneNumber("123456", "+8619201680101"); err != nil { - panic(err) -} -``` - - - -## 当前用户 - -用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```java -LCUser currentUser = LCUser.getCurrentUser(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```objc -LCUser *currentUser = [LCUser currentUser]; -if (currentUser != nil) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```swift -if let user = LCApplication.default.currentUser { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```js -const currentUser = AV.User.current(); -if (currentUser) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```python -current_user = leancloud.User.get_current() -if current_user is not None: - # 跳到首页 - pass -else: - # 显示注册或登录页面 - pass -``` - -```php -$currentUser = User::getCurrentUser(); -if ($currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```go -// 暂不支持 -``` - - - -会话信息会长期有效,直到用户主动登出: - - - -```cs -await LCUser.Logout(); - -// currentUser 变为 null -LCUser currentUser = await LCUser.GetCurrent(); -``` - -```java -LCUser.logOut(); - -// currentUser 变为 null -LCUser currentUser = LCUser.getCurrentUser(); -``` - -```objc -[LCUser logOut]; - -// currentUser 变为 nil -LCUser *currentUser = [LCUser currentUser]; -``` - -```swift -LCUser.logOut() - -// currentUser 变为 nil -let currentUser = LCApplication.default.currentUser -``` - -```dart -await LCUser.logout(); - -// currentUser 变为 null -LCUser currentUser = await LCUser.getCurrent(); -``` - -```js -AV.User.logOut(); - -// currentUser 变为 null -const currentUser = AV.User.current(); -``` - -```python -user.logout() - -current_user = leancloud.User.get_current() # None -``` - -```php -User::logOut(); - -// currentUser 变为 null -$currentUser = User::getCurrentUser(); -``` - -```go -// 暂不支持 -``` - - - -## 设置当前用户 - -用户登录后,云端会返回一个 **session token** 给客户端,它会由 SDK 缓存起来并用于日后同一用户的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个用户发起的请求了。 - -以下是一些应用可能需要用到 session token 的场景: - -- 应用根据以前缓存的 session token 登录。 -- 应用内的某个 WebView 需要知道当前登录的用户。 -- 在服务端登录后,返回 session token 给客户端,客户端根据返回的 session token 登录。 - -下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效): - - - -```cs -await LCUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf"); -``` - -```java -LCUser.becomeWithSessionTokenInBackground("anmlwi96s381m6ca7o7266pzf").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 修改 currentUser - LCUser.changeCurrentUser(user, true); - } - public void onError(Throwable throwable) { - // session token 无效 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser becomeWithSessionTokenInBackground:@"anmlwi96s381m6ca7o7266pzf" block:^(LCUser * _Nullable user, NSError * _Nullable error) { - if (user != nil) { - // 登录成功 - } else { - // session token 无效 - } -}]; -``` - -```swift -_ = LCUser.logIn(sessionToken: "anmlwi96s381m6ca7o7266pzf") { (result) in - switch result { - case .success(object: let user): - // 登录成功 - print(user) - case .failure(error: let error): - // session token 无效 - print(error) - } -} -``` - -```dart -await LCUser.becomeWithSessionToken('anmlwi96s381m6ca7o7266pzf'); -``` - -```js -AV.User.become("anmlwi96s381m6ca7o7266pzf").then( - (user) => { - // 登录成功 - }, - (error) => { - // session token 无效 - } -); -``` - -```python -user = leancloud.User.become('anmlwi96s381m6ca7o7266pzf') -``` - -```php -User::become("anmlwi96s381m6ca7o7266pzf"); -``` - -```go -user, err := client.Users.Become("anmlwi96s381m6ca7o7266pzf") -if err != nil { - panic(err) -} -``` - - - -请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。 - -如果在 **控制台 > 内建账户 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 `403 (Forbidden)` 错误。 - -下面的代码检查 session token 是否有效: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -bool isAuthenticated = await currentUser.IsAuthenticated(); -if (isAuthenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - -```java -boolean authenticated = LCUser.getCurrentUser().isAuthenticated(); -if (authenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - -```objc -LCUser *currentUser = [LCUser currentUser]; -NSString *token = currentUser.sessionToken; -[currentUser isAuthenticatedWithSessionToken:token callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // session token 有效 - } else { - // session token 无效 - } -}]; -``` - -```swift -if let sessionToken = LCApplication.default.currentUser?.sessionToken?.value { - _ = LCUser.logIn(sessionToken: sessionToken) { (result) in - if result.isSuccess { - // session token 有效 - } else { - // session token 无效 - } - } -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -bool isAuthenticated = await currentUser.isAuthenticated(); -if (isAuthenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - -```js -const currentUser = AV.User.current(); -currentUser.isAuthenticated().then((authenticated) => { - if (authenticated) { - // session token 有效 - } else { - // session token 无效 - } -}); -``` - -```python -authenticated = leancloud.User.get_current().is_authenticated() -if authenticated: - # session token 有效 - pass -else: - # session token 无效 - pass -``` - -```php -$authenticated = User::isAuthenticated(); -if ($authenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - -```go -// 暂不支持 -``` - - - -## 重置密码 - -我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了多种重置密码的方法。 - -邮箱重置密码的流程如下: - -1. 用户输入注册的电子邮箱,请求重置密码; -2. 云端向该邮箱发送一封包含重置密码的特殊链接的电子邮件; -3. 用户点击重置密码链接后,一个特殊的页面会打开,让他们输入新密码; -4. 用户的密码已被重置为新输入的密码。 - -首先让用户填写注册账户时使用的邮箱,然后调用下面的方法: - - - -```cs -await LCUser.RequestPasswordReset("tom@xd.com"); -``` - -```java -LCUser.requestPasswordResetInBackground("tom@xd.com").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestPasswordResetForEmailInBackground:@"tom@xd.com"]; -``` - -```swift -_ = LCUser.requestPasswordReset(email: "tom@xd.com") { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestPasswordReset('tom@xd.com'); -``` - -```js -AV.User.requestPasswordReset("tom@xd.com"); -``` - -```python -leancloud.User.request_password_reset('tom@xd.com') -``` - -```php -User::requestPasswordReset("tom@xd.com"); -``` - -```go -if err := client.Users.RequestPasswordReset("tom@xd.com"); err != nil { - panic(err) -} -``` - - - -上面的代码会查询是否有用户的 `email` 属性与前面提供的邮箱匹配。如果有的话,则向该邮箱发送一封密码重置邮件。之前提到过,应用可以让 `username` 与 `email` 保持一致,也可以单独收集用户的邮箱并将其存为 `email`。 - -密码重置邮件的内容可在应用的 **云服务控制台 > 内建账户 > 邮件模版** 中自定义。更多关于自定义邮件模板和验证链接的内容,请参考《自定义邮件验证和重设密码页面》。 - -除此之外,还可以用手机号重置密码: - -1. 用户输入注册的手机号,请求重置密码; -2. 云端向该号码发送一条包含验证码的短信; -3. 用户输入验证码和新密码。 - -下面的代码向用户发送含有验证码的短信: - - - -```cs -await LCUser.RequestPasswordRestBySmsCode("+8619201680101"); -``` - -```java -LCUser.requestPasswordResetBySmsCodeInBackground("+8619201680101").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestPasswordResetWithPhoneNumber:@"+8619201680101"]; -``` - -```swift -_ = LCUser.requestPasswordReset(mobilePhoneNumber: "+8619201680101") { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestPasswordRestBySmsCode('+8619201680101'); -``` - -```js -AV.User.requestPasswordResetBySmsCode("+8619201680101"); -``` - -```python -leancloud.User.request_password_reset_by_sms_code('+8619201680101') -``` - -```php -User::requestPasswordResetBySmsCode("+8619201680101"); -``` - -```go -if err := client.Users.RequestPasswordResetBySmsCode("+8619201680101"); err != nil { - panic(err) -} -``` - - - -上面的代码会查询是否有用户的 `mobilePhoneNumber` 属性与前面提供的手机号匹配。如果有的话,则向该号码发送验证码短信。 - -可以在 **云服务控制台 > 内建账户 > 设置** 中设置只有在 `mobilePhoneVerified` 为 `true` 的情况下才能用手机号重置密码。 - -用户输入验证码和新密码后,用下面的代码完成密码重置: - - - -```cs -await LCUser.ResetPasswordBySmsCode("+8619201680101", "123456", "cat!@#123"); -``` - -```java -LCUser.resetPasswordBySmsCodeInBackground("123456", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 密码重置成功 - } - public void onError(Throwable throwable) { - // 验证码不正确 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser resetPasswordWithSmsCode:@"123456" newPassword:@"cat!@#123" block:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 密码重置成功 - } else { - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.resetPassword(mobilePhoneNumber: "+8619201680101", verificationCode: "123456", newPassword: "cat!@#123") { result in - switch result { - case .success: - // 密码重置成功 - break - case .failure(error: let error): - // 验证码不正确 - print(error) - } -} -``` - -```dart -await LCUser.resetPasswordBySmsCode('+8619201680101', '123456', 'cat!@#123'); -``` - -```js -AV.User.resetPasswordBySmsCode("123456", "cat!@#123").then( - () => { - // 密码重置成功 - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -leancloud.User.reset_password_by_sms_code('123456', 'cat!@#123') -``` - -```php -User::resetPasswordBySmsCode("123456", "cat!@#123"); -``` - -```go -if err := client.Users.ResetPasswordBySmsCode("+8619201680101", "123456", "cat!@#123"); err != nil { - panic(err) -} -``` - - - -## 用户的查询 - -使用下面的代码来查询用户: - - - -```cs -LCQuery userQuery = LCUser.GetQuery(); -``` - -```java -LCQuery userQuery = LCUser.getQuery(); -``` - -```objc -LCQuery *userQuery = [LCUser query]; -``` - -```swift -let userQuery = LCQuery(className: "_User") -``` - -```dart -LCQuery userQuery = LCUser.getQuery(); -``` - -```js -const userQuery = new AV.Query("_User"); -``` - -```python -user_query = leancloud.Query('_leancloud.User') -``` - -```php -$userQuery = new Query("_User"); -``` - -```go -userQuery := client.Users.NewUserQuery() -``` - - - -为了安全起见,**新创建的应用的 `_User` 表默认关闭了 `find` 权限**,这样每位用户登录后只能查询到自己在 `_User` 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 `find` 查询权限。除此之外,还可以在 [云引擎](/sdk/engine/overview) 里封装用户查询相关的方法。 - -可以参见 [用户对象的安全](#用户对象的安全) 来了解 `_User` 表的一些限制,还可以阅读[《数据和安全》](/sdk/storage/guide/security)来了解更多 class 级权限设置的方法。 - -## 关联用户对象 - -关联用户的方法和对象是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书: - - - -```cs -LCObject book = new LCObject("Book"); -LCUser author = await LCUser.GetCurrent(); -book["title"] = "我的第五本书"; -book["author"] = author; -await book.Save(); - -LCQuery query = new LCQuery("Book"); -query.WhereEqualTo("author", author); -// books 是包含同一作者所有 Book 对象的数组 -ReadOnlyCollection books = await query.Find(); -``` - -```java -LCObject book = new LCObject("Book"); -LCUser author = LCUser.getCurrentUser(); -book.put("title", "我的第五本书"); -book.put("author", author); -book.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject book) { - // 获取所有该作者写的书 - LCQuery query = new LCQuery<>("Book"); - query.whereEqualTo("author", author); - query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List books) { - // books 是包含同一作者所有 Book 对象的数组 - } - public void onError(Throwable throwable) {} - public void onComplete() {} - }); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -```objc -LCObject *book = [LCObject objectWithClassName:@"Book"]; -LCUser *author = [LCUser currentUser]; -[book setObject:@"我的第五本书" forKey:@"title"]; -[book setObject:author forKey:@"author"]; -[book saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - // 获取所有该作者写的书 - LCQuery *query = [LCQuery queryWithClassName:@"Book"]; - [query whereKey:@"author" equalTo:author]; - [query findObjectsInBackgroundWithBlock:^(NSArray *books, NSError *error) { - // books 是包含同一作者所有 Book 对象的数组 - }]; -}]; -``` - -```swift -do { - guard let author = LCApplication.default.currentUser else { - return - } - let book = LCObject(className: "Book") - try book.set("title", value: "我的第五本书") - try book.set("author", value: author) - _ = book.save { result in - switch result { - case .success: - // 获取所有该作者写的书 - let query = LCQuery(className: "Book") - query.whereKey("author", .equalTo(author)) - _ = query.find { result in - switch result { - case .success(objects: let books): - // books 是包含同一作者所有 Book 对象的数组 - break - case .failure(error: let error): - print(error) - } - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -LCObject book = LCObject('Book'); -LCUser author = await LCUser.getCurrent(); -book['title'] = '我的第五本书'; -book['author'] = author; -await book.save(); - -LCQuery query = LCQuery('Book'); -query.whereEqualTo('author', author); -// books 是包含同一作者所有 Book 对象的数组 -List books = await query.find(); -``` - -```js -const Book = AV.Object.extend("Book"); -const book = new Book(); -const author = AV.User.current(); -book.set("title", "我的第五本书"); -book.set("author", author); -book.save().then((book) => { - // 获取所有该作者写的书 - const query = new AV.Query("Book"); - query.equalTo("author", author); - query.find().then((books) => { - // books 是包含同一作者所有 Book 对象的数组 - }); -}); -``` - -```python -Book = leancloud.Object.extend('Book') -book = Book() -author = leancloud.User.get_current() -book.set('title', '我的第五本书') -book.set('author', author) -book.save() - -# 获取所有该作者写的书 -query = Book.query -query.equal_to('author', author) -book_list = query.find() -``` - -```php -$book = new LeanObject("Book"); -$author = User::getCurrentUser(); -$book->set("title", "我的第五本书"); -$book->set("author", $author); -$book->save(); - -// 获取所有该作者写的书 -$query = new Query("Book"); -$query->equalTo("author", $author); -$books = $query->find(); -``` - -```go -// 暂不支持 -``` - - - -## 用户对象的安全 - -用户对象自带安全保障,只有通过经过鉴权的方法获取到的用户对象才能进行更新或删除操作,保证每个用户只能修改自己的数据。 - -这样设计是因为用户对象中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。 - -下面的代码展现了这种安全措施: - - - -```cs -try { - LCUser user = await LCUser.Login("Tom", "cat!@#123"); - // 试图修改用户名 - user["username"] = "Jerry"; - // 密码已被加密,这样做会获取到空字符串 - string password = user["password"]; - // 可以执行,因为用户已鉴权 - await user.Save(); - - // 绕过鉴权直接获取用户 - LCQuery userQuery = LCUser.GetQuery(); - LCUser unauthenticatedUser = await userQuery.Get(user.ObjectId); - unauthenticatedUser["username"] = "Toodle"; - - // 会出错,因为用户未鉴权 - unauthenticatedUser.Save(); -} catch (LCException e) { - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 试图修改用户名 - user.put("username", "Jerry"); - // 密码已被加密,这样做会获取到空字符串 - String password = user.getString("password"); - // 可以执行,因为用户已鉴权 - user.save(); - - // 绕过鉴权直接获取用户 - LCQuery query = new LCQuery<>("_User"); - query.getInBackground(user.getObjectId()).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser unauthenticatedUser) { - unauthenticatedUser.put("username", "Toodle"); - // 会出错,因为用户未鉴权 - unauthenticatedUser.save(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} - }); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -```objc -[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 试图修改用户名 - [user setObject:@"Jerry" forKey:@"username")]; - // 密码已被加密,这样做会获取到空字符串 - NSString *password = user[@"password"]; - // 保存更改 - [user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 可以执行,因为用户已鉴权 - - // 绕过鉴权直接获取用户 - LCQuery *query = [LCQuery queryWithClassName:@"_User"]; - [query getObjectInBackgroundWithId:user.objectId block:^(LCObject *unauthenticatedUser, NSError *error) { - [unauthenticatedUser setObject:@"Toodle" forKey:@"username"]; - [unauthenticatedUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 无法执行,因为用户未鉴权 - } else { - // 操作失败 - } - }]; - }]; - } else { - // 错误处理 - } - }]; - } else { - // 错误处理 - } -}]; -``` - -```swift -_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in - switch result { - case .success(object: let user): - // 试图修改用户名 - try! user.set("username", "Jerry") - // 密码已被加密,这样做会获取到空字符串 - let password = user.get("password") - // 可以执行,因为用户已鉴权 - user.save() - - // 绕过鉴权直接获取用户 - let query = LCQuery(className: "_User") - _ = query.get(user.objectId) { result in - switch result { - case .success(object: let unauthenticatedUser): - try! unauthenticatedUser.set("username", "Toodle") - _ = unauthenticatedUser.save { result in - switch result { - .success: - // 无法执行,因为用户未鉴权 - .failure: - // 操作失败 - } - } - case .failure(error: let error): - print(error) - } - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - LCUser user = await LCUser.login('Tom', 'cat!@#123'); - // 试图修改用户名 - user['username'] = 'Jerry'; - // 密码已被加密,这样做会获取到空字符串 - String password = user['password']; - // 可以执行,因为用户已鉴权 - await user.save(); - - // 绕过鉴权直接获取用户 - LCQuery userQuery = LCQuery('_User'); - LCUser unauthenticatedUser = await userQuery.get(user.objectId); - unauthenticatedUser['username'] = 'Toodle'; - - // 会出错,因为用户未鉴权 - unauthenticatedUser.save(); -} on LCException catch (e) { - print('${e.code} : ${e.message}'); -} -``` - -```js -const user = AV.User.logIn("Tom", "cat!@#123").then((user) => { - // 试图修改用户名 - user.set("username", "Jerry"); - // 密码已被加密,这样做会获取到空字符串 - const password = user.get("password"); - // 保存更改 - user.save().then((user) => { - // 可以执行,因为用户已鉴权 - - // 绕过鉴权直接获取用户 - const query = new AV.Query("_User"); - query.get(user.objectId).then((unauthenticatedUser) => { - unauthenticatedUser.set("username", "Toodle"); - unauthenticatedUser.save().then( - (unauthenticatedUser) => {}, - (error) => { - // 会出错,因为用户未鉴权 - } - ); - }); - }); -}); -``` - -```python -leancloud.User.login('Tom', 'cat!@#123') -current_user = leancloud.User.get_current() - -# 试图修改用户名 -current_user.set('username', 'Jerry') -# 密码已被加密,这样做会获取到空字符串 -password = current_user.get('password') -# 可以执行,因为用户已鉴权 -current_user.save() - -# 绕过鉴权直接获取用户 -query = leancloud.Query('_User') -unauthenticated_user = query.get(current_user.id) -unauthenticated_user.set('username', 'Toodle') -# 会出错,因为用户未鉴权 -unauthenticated_user.save() -``` - -```php -User::logIn("Tom", "cat!@#123"); -$currentUser = User::getCurrentUser(); - -// 试图修改用户名 -$currentUser->set("username", "Jerry"); -// 密码已被加密,这样做会获取到空字符串 -$password = $currentUser->get("password"); -// 可以执行,因为用户已鉴权 -$currentUser->save(); - -// 绕过鉴权直接获取用户 -$query = new Query("_User"); -$unauthenticatedUser = $query->get($currentUser->getObjectId()) -$unauthenticatedUser->set("username", "Toodle"); -// 会出错,因为用户未鉴权 -$unauthenticatedUser->save() -``` - -```go -user, err := client.Users.LogIn("Tom", "cat!@#123") -if err != nil { - panic(err) -} - -// 试图修改用户名,未鉴权将失败 -if err := client.User(user).Set("username", "Jerry"); err != nil { - panic(err) -} - -// 密码已被加密,这样做会获取到空字符串 -password := user.String("password") - -// 可以执行,因为用户已鉴权 -if err := client.User(user).Set("username", "Jerry", leancloud.UseUser(user)); err != nil { - panic(err) -} - -// 绕过鉴权直接获取用户 -unauthenticatedUser := User{} -if err := client.Users.NewUserQuery().EqualTo("objectId", user.ID).First(&unauthenticatedUser); err != nil { - panic(err) -} - -// 会出错,因为用户未鉴权 -if err := client.User(unauthenticatedUser).Set("username", "Toodle"); err != nil { - panic(err) -} -``` - - - -通过调用 [当前用户](#当前用户) 相关方法获取的用户总是经过鉴权的。 - -要查看一个用户对象是否经过鉴权,可以调用如下方法。通过经过鉴权的方法获取到的用户对象无需进行该检查。 - - - -```cs -IsAuthenticated -``` - -```java -isAuthenticated -``` - -```objc -isAuthenticatedWithSessionToken -``` - -```swift -// 暂不支持 -``` - -```dart -isAuthenticated -``` - -```js -isAuthenticated; -``` - -```python -is_authenticated -``` - -```php -isAuthenticated -``` - -```go -// 暂不支持 -``` - - - -注意,用户的密码只能在注册的时候进行设置,日后如需修改,只能通过 [重置密码](#重置密码) 的方式进行。密码不会被缓存在本地。如果尝试直接获取已登录用户的密码,会得到 `null`。 - -## 其他对象的安全 - -对于给定的一个对象,可以指定哪些用户有权限读取或修改它。为实现该功能,每个对象都有一个由 `ACL` 对象组成的访问控制表。请参阅[ACL 权限管理开发指南](/sdk/storage/guide/acl/)。 - -## 第三方账户登录 - -云服务支持应用层直接使用第三方社交平台(例如微信、微博、QQ 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。 - -例如以下的代码展示了终端用户使用微信登录的处理流程: - - - -```cs -Dictionary thirdPartyData = new Dictionary { - // 必须 - { "openid", "OPENID" }, - { "access_token", "ACCESS_TOKEN" }, - { "expires_in", 7200 }, - - // 可选 - { "refresh_token", "REFRESH_TOKEN" }, - { "scope", "SCOPE" } -}; -LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin"); -``` - -```java -Map thirdPartyData = new HashMap(); -// 必须 -thirdPartyData.put("expires_in", 7200); -thirdPartyData.put("openid", "OPENID"); -thirdPartyData.put("access_token", "ACCESS_TOKEN"); -// 可选 -thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); -thirdPartyData.put("scope", "SCOPE"); -LCUser.loginWithAuthData(thirdPartyData, "weixin").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) { - } - public void onNext(LCUser user) { - System.out.println("成功登录"); - } - public void onError(Throwable throwable) { - System.out.println("尝试使用第三方账号登录,发生错误。"); - } - public void onComplete() { - } -}); -``` - -```objc -NSDictionary *thirdPartyData = @{ - // 必须 - @"openid":@"OPENID", - @"access_token":@"ACCESS_TOKEN", - @"expires_in":@7200, - - // 可选 - @"refresh_token":@"REFRESH_TOKEN", - @"scope":@"SCOPE", - }; -LCUser *user = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = LeanCloudSocialPlatformWeiXin; -[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let thirdPartyData: [String: Any] = [ - // 必须 - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - - // 可选 - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" -] -let user = LCUser() -user.logIn(authData: thirdPartyData, platform: .weixin) { (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -var thirdPartyData = { - // 必须 - 'openid': 'OPENID', - 'access_token': 'ACCESS_TOKEN', - 'expires_in': 7200, - - // 可选 - 'refresh_token': 'REFRESH_TOKEN', - 'scope': 'SCOPE' -}; -LCUser currentUser = await LCUser.loginWithAuthData(thirdPartyData, 'weixin'); -``` - -```js -const thirdPartyData = { - // 必须 - openid: "OPENID", - access_token: "ACCESS_TOKEN", - expires_in: 7200, - - // 可选 - refresh_token: "REFRESH_TOKEN", - scope: "SCOPE", -}; -AV.User.loginWithAuthData(thirdPartyData, "weixin").then( - (user) => { - // 登录成功 - }, - (error) => { - // 登录失败 - } -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -`loginWithAuthData` 系列方法需要两个参数来唯一确定一个账户: - -- 第三方平台的名字,就是前例中的 `weixin`,该名字由应用层自己决定。 -- 第三方平台的授权信息,就是前例中的 `thirdPartyData`(一般包括 `uid`、`access_token`、`expires_in` 等信息,与具体的第三方平台有关)。 - -云端会使用第三方平台的鉴权信息来查询是否已经存在与之关联的账户。如果存在的话,则返回 `200 OK` 状态码,同时附上用户的信息(包括 [`sessionToken`](#设置当前用户))。如果第三方平台的信息没有和任何账户关联,客户端会收到 `201 Created` 状态码,意味着新账户被创建,同时附上用户的 `objectId`、`createdAt`、`sessionToken` 和一个自动生成的 `username`,例如: - -```json -{ - "username": "k9mjnl7zq9mjbc7expspsxlls", - "objectId": "5b029266fb4ffe005d6c7c2e", - "createdAt": "2018-05-21T09:33:26.406Z", - "updatedAt": "2018-05-21T09:33:26.575Z", - "sessionToken": "…", - // authData 通常不会返回,继续阅读以了解其中原因 - "authData": { - "weixin": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" - } - } - // … -} -``` - -这时候我们会看到 `_User` 表中出现了一条新的账户记录,账户中有一个名为 `authData` 的列,保存了第三方平台的授权信息。出于安全考虑,`authData` 不会被返回给客户端,除非它属于当前用户。 - -开发者需要自己完成第三方平台的鉴权流程(一般通过 OAuth 1.0 或 2.0),以获取鉴权信息,继而到云端来登录。 - -### Sign in with Apple - -如果你需要开发 [Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api),云服务可以帮你校验 `identityToken`,并获取 Apple 的 `access_token`。Apple Sign In 的 `authData` 结构如下: - -```json -{ - "lc_apple": { - "uid": "从 Apple 获取到的 User Identifier", - "identity_token": "从 Apple 获取到的 identityToken", - "code": "从 Apple 获取到的 Authorization Code" - } -} -``` - -`authData` 中的 key 的作用: - -- **`lc_apple`**:只有 platform 为 `lc_apple` 时,云服务才会执行 `identity_token` 和 `code` 的逻辑。 -- **`uid`**:必填。云服务通过 `uid` 判断是否存在用户。 -- **`identity_token`**:可选。`authData` 中有 `identity_token` 时云端会自动校验 `identity_token` 的有效性。开发者需要在 **云服务控制台 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 -- **`code`**:可选。`authData` 中有 `code` 时云端会自动用该 `code` 向 Apple 换取 `access_token` 和 `refresh_token`。开发者需要在 **云服务控制台 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 - -#### 获取 Client ID - -Client ID 用于校验 `identity_token` 及获取 `access_token`,指的是 Apple 应用的 identifier,也就是 `AppID` 或 `serviceID`。对于原生应用来说,指的是 Xcode 中的 Bundle Identifier,例如 `com.mytest.app`。详情请参考 [Apple 的文档](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)。 - -#### 获取 Private Key 及 Private Key ID - -Private Key 用于获取 `access_token`。登录 Apple 开发者平台,在左侧的「Certificates, Identifiers & Profiles」中选择「Keys」,添加一个用于 Apple Sign In 的 Private Key,下载 `.p8` 文件,同时在下载 Key 的页面获得 Private Key ID。详情请参考 [Apple 的文档](https://help.apple.com/developer-account/#/dev77c875b7e)。 - -将 Key ID 填写到控制台,将下载下来的 Private Key 文件上传到控制台。控制台只能上传 Private Key 文件,无法查看及下载其内容。 - -#### 获取 Team ID - -Team ID 用于获取 `access_token`。登录 Apple 开发者平台,在右上角或 Membership 页面即可看到自己所属开发团队的 Team ID。注意选择 Bundle ID 对应的 Team。 - -#### 使用 Apple Sign In 登录云服务 - -在控制台填写完成所有信息后,使用以下代码登录。 - - - -```cs -Dictionary appleAuthData = new Dictionary { - // 必须 - { "uid", "USER IDENTIFIER" }, - - // 可选 - { "identity_token", "IDENTITY TOKEN" }, - { "code", "AUTHORIZATION CODE" } -}; -LCUser currentUser = await LCUser.LoginWithAuthData(appleAuthData, "lc_apple"); -``` - -```java -// 不支持 -``` - -```objc -NSDictionary *appleAuthData = @{ - // 必须 - @"uid":@"USER IDENTIFIER", - // 可选 - @"identity_token":@"IDENTITY TOKEN", - @"code":@"AUTHORIZATION CODE", - }; -LCUser *user = [LCUser user]; -[user loginWithAuthData:appleAuthData platformId:"lc_apple" options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let appleData: [String: Any] = [ - // 必须 - "uid": "USER IDENTIFIER", - // 可选 - "identity_token": "IDENTITY TOKEN", - "code": "AUTHORIZATION CODE" -] -let user = LCUser() -user.logIn(authData: appleData, platform: .apple) { (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } -} - -``` - -```dart -var appleData = { - // 必须 - "uid": "USER IDENTIFIER", - // 可选 - "identity_token": "IDENTITY TOKEN", - "code": "AUTHORIZATION CODE" -}; -LCUser currentUser = await LCUser.loginWithAuthData(appleData, 'lc_apple'); -``` - -```js -// 不支持 -``` - -```python -# 不支持 -``` - -```php -// 不支持 -``` - -```go -// 不支持 -``` - - - -### 鉴权数据的保存 - -每个用户的 `authData` 是一个以平台名为键名,鉴权信息为键值的 JSON 对象。 - -一个关联了微信账户的用户应该会有下列对象作为 `authData`: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - } -} -``` - -而一个关联了微博账户的用户,则会有如下的 `authData`: - -```json -{ - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx" - } -} -``` - -我们允许一个账户绑定多个第三方平台的鉴权数据,这样如果某个用户同时关联了微信和微博账户,则其 `authData` 可能会是这样的: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - }, - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx" - } -} -``` - -理解 `authData` 的数据结构至关重要。一个终端用户通过如下的鉴权信息来登录的时候, - -```json -"platform": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" -} -``` - -云端首先会查找账户系统,看看是否存在 `authData.platform.openid` 等于 `OPENID` 的账户,如果存在,则返回现有账户,如果不存在那么就创建一个新账户,同时将上面的鉴权信息写入新账户的 `authData` 属性中,并将新账户的数据当成结果返回。 - -云端会自动为每个用户的 `authData..` 创建唯一索引,从而避免重复数据。 -`` 在微信等部分云服务内建支持的第三方平台上为 `openid` 字段,在其他第三方平台(包括部分云服务专门支持的第三方平台和所有云服务没有专门支持的第三方平台)上为 `uid` 字段。 - -### 自动验证第三方平台授权信息 - -为了确保账户数据的有效性,云端还支持对部分平台的 Access Token 的有效性进行自动验证,以防止伪造账户数据。如果有效性验证不通过,云端会返回 `invalid authData` 错误,关联不会被建立。对于云端无法识别的服务,开发者需要自己去验证 Access Token 的有效性。 -比如,注册、登录时分别通过云引擎的 `beforeSave` hook、`beforeUpdate` hook 来验证 Access Token 有效性。 - -如果希望使用这一功能,则在开始使用前,需要在 **云服务控制台 > 内建账户 > 设置** 配置相应平台的 **应用 ID** 和 **应用 Secret Key**。 - -如果不希望云端自动验证 Access Token,可以在 **云服务控制台 > 内建账户 > 设置** 里面取消勾选 **第三方登录时,验证用户 AccessToken 合法性**。 - -配置平台账号的目的在于创建用户对象时,云端会使用相关信息去校验请求参数 `thirdPartyData` 的合法性,确保用户对象实际对应着一个合法真实的用户,确保平台安全性。 - -### 绑定第三方账户 - -如果用户已经登录,也可以在当前账户上绑定或解绑更多第三方平台信息。 - -绑定成功后,新的第三方账户信息会被添加到用户对象的 `authData` 字段里。 - -例如,下面的代码可以关联微信账户: - - - -```cs -await currentUser.AssociateAuthData(weixinData, "weixin"); -``` - -```java -user.associateWithAuthData(weixinData, "weixin").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("绑定成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("绑定失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -[user associateWithAuthData:weixinData platformId:LeanCloudSocialPlatformWeiXin options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"成功"); - } else{ - NSLog(@"失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -currentUser.associate(authData: weixinData, platform: .weixin) { (result) in - switch result { - case .success: - // 关联成功 - case .failure(error: let error): - // 关联失败 - } -} -``` - -```dart -await currentUser.associateAuthData(weixinData, 'weixin'); -``` - -```js -user - .associateWithAuthData(weixinData, "weixin") - .then(function (user) { - // 成功绑定 - }) - .catch(function (error) { - console.error("error: ", error); - }); -``` - -```python -user.link_with("weixin", weixin_data) -``` - -```php -$user->linkWith("weixin", $weixinData); -``` - -```go -// 暂不支持 -``` - - - -为节省篇幅,上面的代码示例中没有给出具体的平台授权信息,相关内容请参考上面的[「第三方账户登录」](#第三方账户登录)一节。 - -### 解除与第三方账户的关联 - -类似地,可以解绑第三方账户。 - -例如,下面的代码可以解除用户和微信账户的关联: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -await currentUser.DisassociateWithAuthData("weixin"); -``` - -```java -LCUser user = LCUser.currentUser(); -user.dissociateWithAuthData("weixin").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("解绑成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("解绑失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -[user disassociateWithPlatform:LeanCloudSocialPlatformWeiXin callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"成功"); - } else{ - NSLog(@"失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -currentUser.disassociate(authData: .weixin) { (result) in - switch result { - case .success: - // 解除关联成功 - case .failure(error: let error): - // 解除关联失败 - } -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -await currentUser.disassociateWithAuthData('weixin'); -``` - -```js -user.dissociateAuthData("weixin").then( - (s) => { - // 解除关联成功 - }, - (error) => { - // 解除关联失败 - } -); -``` - -```python -user.unlink_from("weixin") -``` - -```php -$user->unlinkWith("weixin"); -``` - -```go -// 暂不支持 -``` - - - -
    - -扩展:第三方登录时补充完整的用户信息 - -有些产品,新用户在使用第三方账号授权拿到相关信息后,仍然需要补充设置用户名、手机号、密码等重要信息后,才被允许登录成功。 - -这时要使用 `loginWithauthData` 登录接口的 `failOnNotExist` 参数并将其设置为 `true`。服务端会判断是否已存在能匹配上的 `authData`,如果不存在则会返回 `211` 错误码和 `Could not find user` 报错信息。开发者根据这个 `211` 错误码,跳转到要求输入用户名、密码、手机号等信息的页面,实例化一个用户对象,保存上述补充数据,再次调用 `loginWithauthData` 接口进行登录,并 **不再传入 `failOnNotExist` 参数**。示例代码如下: - - - -```cs -try { - Dictionary thirdPartyData = new Dictionary { - // 必须 - { "openid", "OPENID" }, - { "access_token", "ACCESS_TOKEN" }, - { "expires_in", 7200 }, - - // 可选 - { "refresh_token", "REFRESH_TOKEN" }, - { "scope", "SCOPE" } - }; - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.FailOnNotExist = true; - LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); -} catch (LCException e) { - if (e.code == 211) { - // 不存在 authData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 - } -} - -// 跳转到输入用户名、密码、手机号等业务页面之后 -Dictionary thirdPartyData = new Dictionary { - { "expires_in", 7200 }, - { "openid", "OPENID" }, - { "access_token", "ACCESS_TOKEN" } -}; -try { - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.FailOnNotExist = true; - LCUser user = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); - user.Username = "Tom"; - user.Mobile = "+8618200008888"; - await user.Save(); -} catch (LCException e) { - //其他报错信息 -} -``` - -```java -Map thirdPartyData = new HashMap(); -thirdPartyData.put("expires_in", 7200); -thirdPartyData.put("openid", "OPENID"); -thirdPartyData.put("access_token", "ACCESS_TOKEN"); -thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); -thirdPartyData.put("scope", "SCOPE"); -Boolean failOnNotExist = true; -LCUser user = new LCUser(); -user.loginWithAuthData(thirdPartyData, "weixin", failOnNotExist).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("存在匹配的用户,登录成功"); - } - @Override - public void onError(Throwable e) { - LCException avException = new LCException(e); - int code = avException.getCode(); - if (code == 211){ - // 跳转到输入用户名、密码、手机号等业务页面 - } else { - System.out.println("发生错误:" + e.getMessage()); - } - } - @Override - public void onComplete() { - } -}); - -// 跳转到输入用户名、密码、手机号等业务页面之后 -LCUser user = new LCUser(); -user.setUsername("Tom"); -user.setMobilePhoneNumber("+8618200008888"); -Boolean failOnNotExist = false; -user.loginWithAuthData(thirdPartyData, "weixin", failOnNotExist).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("登录成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("登录失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -NSDictionary *thirdPartyData = @{ - @"access_token":@"ACCESS_TOKEN", - @"expires_in":@7200, - @"refresh_token":@"REFRESH_TOKEN", - @"openid":@"OPENID", - @"scope":@"SCOPE", - }; -LCUser *user = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = LeanCloudSocialPlatformWeiXin; -option.failOnNotExist = true; -[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // 你的逻辑 - } else if ([error.domain isEqualToString:kLeanCloudErrorDomain] && error.code == 211) { - // 不存在 thirdPartyData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 - } -}]; - -// 跳转到输入用户名、密码、手机号等业务页面之后 -LCUser *user = [LCUser user]; -user.username = @"Tom"; -user.mobilePhoneNumber = @"+8618200008888"; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = LeanCloudSocialPlatformWeiXin; -[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let thirdPartyData: [String: Any] = [ - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "openid": "OPENID", - "scope": "SCOPE" -] -let user = LCUser() -user.logIn(authData: thirdPartyData, platform: .weixin, options: [.failOnNotExist]) { (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - if error.code == 211 { - // 不存在绑定了当前 authData 的 User 的实例 - // 跳转到输入用户名、密码、手机号等业务页面 - let user = LCUser() - user.username = "Tom" - user.password = "cat!@#123" - user.mobilePhoneNumber = "+8618200008888" - user.logIn(authData: thirdPartyData, platform: .weixin, completion: { (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } - }) - } - } -} -``` - -```dart -try { - Map thirdPartyData = { - // 必须 - 'openid': 'OPENID', - 'access_token': 'ACCESS_TOKEN', - 'expires_in': 7200, - - // 可选 - 'refresh_token': 'REFRESH_TOKEN', - 'scope': 'SCOPE' - }; - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.failOnNotExist = true; - LCUser currentUser = await LCUser.loginWithAuthData(thirdPartyData, 'weixin', option: option); -} on LCException catch (e) { - if (e.code == 211) { - // 不存在 authData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 - } -} - -// 跳转到输入用户名、密码、手机号等业务页面之后 -Map thirdPartyData = { - 'expires_in': 7200, - 'openid': 'OPENID', - 'access_token': 'ACCESS_TOKEN' -}; -try { - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.failOnNotExist = true; - LCUser user = await LCUser.loginWithAuthData(thirdPartyData, 'weixin', option: option); - user.username = 'Tome'; - user.mobile = '+8618200008888'; - await user.save(); -} on LCException catch (e) { - //其他报错信息 -} -``` - -```js -const thirdPartyData = { - access_token: "ACCESS_TOKEN", - expires_in: 7200, - refresh_token: "REFRESH_TOKEN", - openid: "OPENID", - scope: "SCOPE", -}; -AV.User.loginWithAuthData(thirdPartyData, "weixin", { - failOnNotExist: true, -}).then( - (s) => { - // 登录成功 - }, - (error) => { - // 登录失败 - // 检查 error.code == 211,跳转到用户名、手机号等资料的输入页面 - } -); - -const user = new AV.User(); -// 设置用户名 -user.setUsername("Tom"); -// 设置密码 -user.setMobilePhoneNumber("+8618200008888"); -user.setPassword("cat!@#123"); -// 设置邮箱 -user.setEmail("tom@leancloud.rocks"); -user.loginWithAuthData(thirdPartyData, "weixin").then( - (loggedInUser) => { - console.log(loggedInUser); - }, - (error) => {} -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -
    - -
    - -扩展:接入 UnionID 体系,打通不同子产品的账号系统 - -随着第三方平台的账户体系变得日渐复杂,它们的第三方鉴权信息出现了一些较大的变化。下面我们以最典型的微信开放平台为例来进行说明。 - -当一个用户在移动应用内登录微信账号时,会被分配一个 OpenID;在微信小程序内登录账号时,又会被分配另一个不同的 OpenID。这样的架构会导致的问题是,使用同一个微信号的用户,也无法在微信开放平台下的移动应用和小程序之间互通。 - -微信官方为了解决这个问题,引入了 `UnionID` 的体系,以下为其官方说明: - -> 通过获取用户基本信息接口,开发者可通过 OpenID 来获取用户基本信息,而如果开发者拥有多个公众号,可使用以下办法通过 UnionID 机制来在多公众号之间进行用户帐号互通。只要是同一个微信开放平台帐号下的公众号,用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID 是相同的。 - -其他平台,如 QQ 和微博,与微信的设计也基本一致。 - -云服务支持 `UnionID` 体系。你只需要给 `loginWithauthData` 和 `associateWithauthData` 接口传入更多的第三方鉴权信息,即可完成新 UnionID 体系的集成。新增加的第三方鉴权登录选项包括: - -- unionId,指第三方平台返回的 UnionId 字符串。 -- unionId platform,指 unionId 对应的 platform 字符串,由应用层自己指定,[后面](#该如何指定-unionidplatform)会详述。 -- asMainAccount,指示是否把当前平台的鉴权信息作为主账号来使用。如果作为主账号,那么就由当前用户唯一占有该 unionId,以后其他平台使用同样的 unionId 登录的话,会绑定到当前的用户记录上来;否则,当前应用的鉴权信息会被绑定到其他账号上去。 - -下面让我们通过一个例子来说明如何使用这些参数完成 UnionID 登录。 - -假设云服务在微信开放平台上有两个应用,一个是「云服务通讯」,一个是「云服务技术支持」,这两个应用在接入第三方鉴权的时候,分别使用了 `wxleanoffice` 和 `wxleansupport` 作为 platform 来进行登录。现在我们开启 UnionID 的用户体系,希望同一个微信用户在这两个应用中都能对应到同一个账户系统(`_User` 表中的同一条记录),同时我们决定将 `wxleanoffice` 平台作为主账号平台。 - -假设对于用户 A,微信给 ta 为云服务分配的 UnionId 为 `unionid4a`,而对两个应用的授权信息分别为: - -```json -"wxleanoffice": { - "access_token": "officetoken", - "openid": "officeopenid", - "expires_in": 1384686496 -}, -"wxleansupport": { - "openid": "supportopenid", - "access_token": "supporttoken", - "expires_in": 1384686496 -} -``` - -现在,用户 A 在「云服务通讯」中通过微信登录,其调用请求为: - - - -```cs -Dictionary thirdPartyData = new Dictionary { - // 必须 - { "uid", "officeopenid" }, - { "access_token", "officetoken" }, - { "expires_in", 1384686496 }, - { "unionId", "unionid4a" }, // 新增属性 - - // 可选 - { "refresh_token", "..." }, - { "scope", "SCOPE" } -}; -LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); -option.AsMainAccount = true; -option.UnionIdPlatform = "weixin"; -LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( - thirdPartyData, "wxleanoffice", "unionid4a", - option: option); -``` - -```java -Map thirdPartyData = new HashMap(); -thirdPartyData.put("expires_in", 1384686496); -thirdPartyData.put("uid", "officeopenid"); -thirdPartyData.put("access_token", "officetoken"); -thirdPartyData.put("scope", "SCOPE"); -LCUser.loginWithAuthData(LCUser.class, thirdPartyData, "wxleanoffice", - "unionid4a", "weixin", true) // 新增参数,分别表示 uniondId,unionIdPlatform,asMainAccount - // 对于 unionIdPlatform,这里使用「weixin」来指代微信平台。 - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("登录成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("登录失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -NSDictionary *thirdPartyData = @{ - @"access_token":@"officetoken", - @"expires_in":@1384686496, - @"uid":@"officeopenid", - @"scope":@"SCOPE", - @"unionid":@"unionid4a" // 新增属性 - }; -LCUser *currentuser = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = @"weixin"; // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 -option.unionId = thirdPartyData[@"unionid"]; -option.isMainAccount = true; -[currentuser loginWithAuthData:thirdPartyData platformId:@"wxleanoffice" options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let thirdPartyData: [String: Any] = [ - "access_token": "officetoken", - "expires_in": 1384686496, - "uid": "officeopenid", - "scope": "SCOPE", - "unionid": "unionid4a" // 新增属性 -] -let user = LCUser() -user.logIn( - authData: thirdPartyData, - platform: .custom("wxleanoffice"), - unionID: thirdPartyData["unionid"] as? String, - unionIDPlatform: .weixin, // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 - options: [.mainAccount]) -{ (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -Map thirdPartyData = { - // 必须 - 'uid': 'officeopenid', - 'access_token': 'officetoken', - 'expires_in': 1384686496, - 'unionId': 'unionid4a', // 新增属性 - - // 可选 - 'refresh_token': '...', - 'scope': 'SCOPE' -}; -LCUserAuthDataLoginOption option = LCUserAuthDataLoginOption(); -option.asMainAccount = true; -option.unionIdPlatform = 'weixin'; -LCUser currentUser = await LCUser.loginWithAuthDataAndUnionId( - thirdPartyData, 'wxleanoffice', 'unionid4a', - option: option); -``` - -```js -const thirdPartyData = { - access_token: "officetoken", - expires_in: 1384686496, - uid: "officeopenid", - scope: "SCOPE", -}; - -AV.User.loginWithAuthDataAndUnionId( - thirdPartyData, - "wxleanoffice", - "unionid4a", // 新增参数 - { - unionIdPlatform: "weixin", // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 - asMainAccount: true, - } -).then( - (user) => { - // 绑定成功 - }, - (error) => { - // 绑定失败 - } -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 `authData..uid` 的唯一索引,具体可以参考《数据存储 REST API 使用详解》的[《连接用户账户和第三方平台》](/sdk/authentication/rest/#连接用户账户和第三方平台)一节。 - -如果用户 A 是第一次在「云服务通讯」中通过微信登录,那么 `_User` 表中会增加一个新用户(假设其 `objectId` 为 `ThisIsUserA`),其 `authData` 的结果如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - - // 新增键值对 - "_weixin_unionid": { - "uid": "unionid4a" - } -} -``` - -可以看到,与之前的第三方登录 API 相比,这里由于登录时指定了 `asMainAccount` 为 `true`,所以 `authData` 的第一级子目录中增加了 `_weixin_unionid` 的键值对,这里的 `weixin` 就是我们指定的 `unionIdPlatform` 的值。`_weixin_unionid` 这个增加的键值对非常重要,以后我们判断是否存在同样 UnionID 的账户就是依靠它来查找的,而是否增加这个键值对,则是由登录时指定的 `asMainAccount` 的值决定的: - -- 当 `asMainAccount` 为 `true` 时,云端会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,当前账号就会作为这一个 UnionID 对应的主账号被唯一确定。 -- 当 `asMainAccount` 为 `false` 时,云端不会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,此时如果通过提供的 UnionID 可以找到主账号,则会将当前的鉴权信息合并进主账号的 `authData` 属性里,同时返回主账号对应的 `_User` 表记录;如果通过提供的 UnionID 找不到主账号,则会根据平台的 `openid` 去查找账户,找到匹配的账户就返回匹配的,找不到就新建一个账户,此时的处理逻辑与不使用 UnionID 时的逻辑完全一致。 - -接下来,用户 A 继续在「云服务技术支持」中进行微信登录,其登录逻辑为: - - - -```cs -Dictionary thirdPartyData = new Dictionary { - // 必须 - { "uid", "supportopenid" }, - { "access_token", "supporttoken" }, - { "expires_in", 1384686496 }, - { "unionId", "unionid4a" }, - - // 可选 - { "refresh_token", "..." }, - { "scope", "SCOPE" } -}; -LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); -option.AsMainAccount = false; -option.UnionIdPlatform = "weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( - thirdPartyData, "wxleansupport", "unionid4a", - option: option); -``` - -```java -Map thirdPartyData = new HashMap(); -thirdPartyData.put("expires_in", 1384686496); -thirdPartyData.put("uid", "supportopenid"); -thirdPartyData.put("access_token", "supporttoken"); -thirdPartyData.put("scope", "SCOPE"); -LCUser.loginWithAuthData(LCUser.class, thirdPartyData, "wxleansupport", "unionid4a", - "weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 - false).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("登录成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("登录失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -NSDictionary *thirdPartyData = @{ - @"access_token":@"supporttoken", - @"expires_in":@1384686496, - @"uid":@"supportopenid", - @"scope":@"SCOPE", - @"unionid":@"unionid4a" - }; -LCUser *currentuser = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = @"weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -option.unionId = thirdPartyData[@"unionid"]; -option.isMainAccount = false; -[currentuser loginWithAuthData:thirdPartyData platformId:@"wxleansupport" options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let thirdPartyData: [String: Any] = [ - "access_token": "supporttoken", - "expires_in": 1384686496, - "uid": "supportopenid", - "scope": "SCOPE", - "unionid": "unionid4a" -] -let user = LCUser() -user.logIn( - authData: thirdPartyData, - platform: .custom("wxleansupport"), - unionID: thirdPartyData["unionid"] as? String, - unionIDPlatform: .weixin, // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 - options: [.mainAccount]) -{ (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -Map thirdPartyData = { - // 必须 - 'uid': 'supportopenid', - 'access_token': 'supporttoken', - 'expires_in': 1384686496, - 'unionId': 'unionid4a', - - // 可选 - 'refresh_token': '...', - 'scope': 'SCOPE' -}; -LCUserAuthDataLoginOption option = LCUserAuthDataLoginOption(); -option.asMainAccount = false; -option.unionIdPlatform = 'weixin'; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -LCUser currentUser = await LCUser.loginWithAuthDataAndUnionId( - thirdPartyData, 'wxleansupport', 'unionid4a', - option: option); -``` - -```js -const thirdPartyData = { - access_token: "supporttoken", - expires_in: 1384686496, - uid: "supportopenid", - scope: "SCOPE", -}; - -AV.User.loginWithAuthDataAndUnionId( - thirdPartyData, - "wxleansupport", - "unionid4a", - { - unionIdPlatform: "weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 - asMainAccount: false, - } -).then( - (user) => { - // 绑定成功 - }, - (error) => { - // 绑定失败 - } -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -与「云服务通讯」中的登录过程相比,在「云服务技术支持」这个应用中,我们在登录时只是将 `asMainAccount` 设为了 `false`。 这时我们看到,本次登录得到的还是 `objectId` 为 `ThisIsUserA` 的 `_User` 表记录(同一个账户),同时该账户的 `authData` 属性中发生了变化,多了 `wxleansupport` 的数据,如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - }, - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -在新的登录方式中,当一个用户以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录得到新的用户对象后,接下来这个用户以「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 `wxleansupport` 的新用户数据加入到已有账户的 `authData` 里了,不会再创建新的账户。 - -这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个用户对象上,实现互通。 - -### 为 UnionID 建立索引 - -云端会为 UnionID 自动建立索引,不过因为自动创建基于请求的抽样统计,可能会滞后。 -因此,我们推荐自行创建相关索引,特别是用户量(`_User` 表记录数)很大的应用,更需要预先创建索引,否则用户使用 UnionID 账号登录时可能超时失败。 -以上面的微信 UnionID 为例,建议在控制台预先创建下列唯一索引(允许缺失值): - -- `authData.wxleanoffice.uid` -- `authData.wxleansupport.uid` -- `authData._weixin_unionid.uid` - -### 该如何指定 unionIdPlatform - -从上面的例子可以看出,使用 UnionID 登录的时候,需要指定 `unionIdPlatform` 的主要目的,就是为了便于查找已经存在的唯一主账号。云端会在主账号对应账户的 `authData` 属性中增加一个 `_{unionIdPlatform}_unionid` 键值对来标记唯一性,终端用户在其他应用中登录的时候,云端会根据参数中提供的 `uniondId` + `unionIdPlatform` 的组合,在 `_User` 表中进行查找,这样来确定唯一的既存主账号。 - -本来 `unionIdPlatform` 的取值,应该是开发者可以自行决定的,但是 JavaScript SDK 基于易用性的目的,在 `loginWithAuthDataAndUnionId` 之外,还额外提供了两个接口: - -- `AV.User.loginWithQQAppWithUnionId`,这里默认使用 `qq` 作为 `unionIdPlatform`。 -- `AV.User.loginWithWeappWithUnionId`,这里默认使用 `weixin` 作为 `unionIdPlatform`。 - -从我们的统计来看,这两个接口已经被很多开发者接受,在大量线上产品中产生着实际数据。所以为了避免数据在不同平台(例如 Android 和 iOS 应用)间发生冲突,建议大家统一按照 JavaScript SDK 的默认值来设置 `unionIdPlatform`,即: - -- 微信平台的多个应用,统一使用 `weixin` 作为 `unionIdPlatform`; -- QQ 平台的多个应用,统一使用 `qq` 作为 `unionIdPlatform`; -- 微博平台的多个应用,统一使用 `weibo` 作为 `unionIdPlatform`; -- 除此之外的其他平台,开发者可以自行定义 `unionIdPlatform` 的名字,只要自己保证多个应用间统一即可。 - -### 主副应用不同登录顺序出现的不同结果 - -上面的流程是用户先登录了「云服务通讯」这个主应用,然后再登录「云服务技术支持」这个副应用,所以账号都被通过 UnionID 有效关联起来了。可能有人会想到另外一个问题,如果用户 B 先登录副应用,后登录主应用,这时候会发生什么事情呢? - -用户 B 首先登录副应用的时候,提供了「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,并且指定「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `false`」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个用户对象,该账户 `authData` 结果为: - -```json -{ - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,以及「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `true`」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个用户对象,该账户 `authData` 结果为: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - } -} -``` - -还有更复杂的情况。如果某公司的产品之前就接入了微信登录,产生了很多存量用户,并且分散在不同的子产品中,这时候怎么办?我们接下来专门讨论此时的解决方案。 - -### 存量账户如何通过 UnionID 实现关联 - -还是以我们的两个子产品「云服务通讯」(后续以「产品 1」指代)和「云服务技术支持为例」(后续以「产品 2」指代)为例,在接入 UnionID 之前,我们就接入了之前版本的微信平台登录,这时候账户系统内可能存在多种账户: - -- 只使用产品 1 的微信用户 A -- 只使用产品 2 的微信用户 B -- 同时使用两个产品的微信用户 C - -此时的存量账户表如下所示: - -| `objectId` | 微信用户 | `authData.{platform}` | `authData._{platform}_unionid` | -| ---------- | -------- | --------------------- | ------------------------------ | -| 1 | UserA | openid1(对应产品 1) | N/A | -| 2 | UserB | openid2(对应产品 2) | N/A | -| 3 | UserC | openid3(对应产品 1) | N/A | -| 4 | UserC | openid4(对应产品 2) | N/A | - -现在我们对两个子产品进行升级,接入 UnionID 体系。这时因为已经有同一个微信用户在不同子产品中创建了不同的账户(例如 `objectId` 为 3 和 4 的账户),我们需要确定以哪个平台的账号为主。比如决定使用「云服务通讯」上生成的账号为主账号,则在该应用程序更新版本时,使用 `asMainAccount=true` 参数。这个应用带着 UnionID 登录匹配或创建的账号将作为主账号,之后所有这个 UnionID 的登录都会匹配到这个账号。请注意这时 `_User` 表里会剩下一些用户数据,也就是没有被选为主账号的、其他平台的同一个用户的旧账号数据(例如 `objectId` 为 2 和 4 的账户)。这部分数据会继续服务于已经发布的但仍然使用 OpenID 登录的旧版应用。 - -接下来我们看一下,如果以产品 1 的账户作为「主账户」,按照前述的方式同时提供 openid/unionid 完成登录,则最后达到的结果是: - -1. 使用老版本的用户,不管在哪个产品里面,都可以和往常一样通过 openid 登录到正确的账户; -2. 使用产品 1 的新版本的老用户,通过 openid/unionid 组合,还是绑定到原来的账户。例如 UserC 在产品 1 中通过 openid3/unionId3 还是会绑定到 objectId=3 的账户(会增加 uniondId 记录);而 UserC 在产品 2 的新版本中,通过 openid4/unionId3 的组合则会绑定到 objectId=3 的账户,而不再是原来的 objectId=4 的账户。 -3. 使用产品 1 的新版本的新用户,通过 openid/unionid 组合,会创建新的账户;之后该用户再使用产品 2 的新版本,也会绑定到刚才创建的新账户上。 - -以上面的三个用户为例,他们分别升级到两个产品的最新版,且最终都会启用两个产品,则账户表的最终结果如下: - -| `objectId` | 微信用户 | `authData.{platform}` | `authData._{platform}_unionid` | -| ---------- | -------- | ------------------------------------------- | ------------------------------ | -| 1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A | -| 2 | UserB | openid2(对应产品 2) | N/A | -| 3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C | -| 4 | UserC | openid4(对应产品 2) | N/A | -| 5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B | - -之后有新的用户 D,分别在两个产品的新版本中登录,则账户表中会增加一条新的 objectId=6 的记录,结果如下: - -| `objectId` | 微信用户 | `authData.{platform}` | `authData._{platform}_unionid` | -| ---------- | -------- | ------------------------------------------- | ------------------------------ | -| 1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A | -| 2 | UserB | openid2(对应产品 2) | N/A | -| 3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C | -| 4 | UserC | openid4(对应产品 2) | N/A | -| 5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B | -| 6 | UserD | openid7(对应产品 1)/openid8(对应产品 2) | unionId_user_D | - -如果之后我们增加了新的子产品 3,这些用户在子产品 3 中也进行微信登录的话,那么四个用户还是会继续绑定到 objectId 为 1/3/5/6 的主账户。此时账户表的结果会变为: - -| `objectId` | 微信用户 | `authData.{platform}` | `authData._{platform}_unionid` | -| ---------- | -------- | ------------------------------------------------------------------ | ------------------------------ | -| 1 | UserA | openid1(对应产品 1)/openid6(对应产品 2)/openid9(对应产品 3) | unionId_user_A | -| 2 | UserB | openid2(对应产品 2) | N/A | -| 3 | UserC | openid3(对应产品 1)/openid4(对应产品 2)/openid10(对应产品 3) | unionId_user_C | -| 4 | UserC | openid4(对应产品 2) | N/A | -| 5 | UserB | openid5(对应产品 1)/openid2(对应产品 2)/openid11(对应产品 3) | unionId_user_B | -| 6 | UserD | openid7(对应产品 1)/openid8(对应产品 2)/openid12(对应产品 3) | unionId_user_D | - -
    - -## 匿名用户 - -将数据与用户关联需要首先创建一个用户,但有时你不希望强制用户在一开始就进行注册。使用匿名用户,可以让应用不提供注册步骤也能创建用户。下面的代码创建一个新的匿名用户: - - - -```cs -await LCUser.LoginAnonymously(); -``` - -```java -LCUser.logInAnonymously().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // user 是新的匿名用户 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -```objc -[LCUser loginAnonymouslyWithCallback:^(LCUser *user, NSError *error) { - // user 是新的匿名用户 -}]; -``` - -```swift -// 暂不支持 -``` - -```dart -await LCUser.loginAnonymously(); -``` - -```js -AV.User.loginAnonymously().then((user) => { - // user 是新的匿名用户 -}); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -可以像给普通用户设置属性那样给匿名用户设置 `username`、`password`、`email` 等属性,还可以通过走正常的注册流程来将匿名用户转化为普通用户。匿名用户能够: - -- [使用用户名和密码注册](#注册) -- [关联第三方平台](#第三方账户登录),比如微信 - -下面的代码为一名匿名用户设置用户名和密码: - - - -```cs -LCUser currentUser = await LCUser.LoginAnonymously(); -currentUser.Username = "Tom"; -currentUser.Password = "cat!@#123"; - -await currentUser.SignUp(); -``` - -```java -// currentUser 是个匿名用户 -LCUser currentUser = LCUser.getCurrentUser(); - -currentUser.setUsername("Tom"); -currentUser.setPassword("cat!@#123"); - -currentUser.signUpInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // currentUser 已经转化为普通用户 - } - public void onError(Throwable throwable) { - // 注册失败(通常是因为用户名已被使用) - } - public void onComplete() {} -}); -``` - -```objc -// currentUser 是个匿名用户 -LCUser *currentUser = [LCUser currentUser]; - -user.username = @"Tom"; -user.password = @"cat!@#123"; - -[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // currentUser 已经转化为普通用户 - } else { - // 注册失败(通常是因为用户名已被使用) - } -}]; -``` - -```swift -// 暂不支持 -``` - -```dart -LCUser currentUser = await LCUser.loginAnonymously(); -currentUser.username = 'Tom'; -currentUser.password = 'cat!@#123'; - -await currentUser.signUp(); -``` - -```js -// currentUser 是个匿名用户 -const currentUser = AV.User.current(); - -user.setUsername("Tom"); -user.setPassword("cat!@#123"); - -user.signUp().then( - (user) => { - // currentUser 已经转化为普通用户 - }, - (error) => { - // 注册失败(通常是因为用户名已被使用) - } -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -下面的代码检查当前用户是否为匿名用户: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -if (currentUser.IsAnonymous) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```java -LCUser currentUser = LCUser.getCurrentUser(); -if (currentUser.isAnonymous()) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```objc -LCUser *currentUser = [LCUser currentUser]; -if (currentUser.isAnonymous) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```swift -// 暂不支持 -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -if (currentUser.isAnonymous) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```js -const currentUser = AV.User.current(); -if (currentUser.isAnonymous()) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -如果匿名用户未能在登出前转化为普通用户,那么该用户将无法再次登录同一账户,且之前产生的数据也无法被取回。 diff --git a/leancloud/docs/sdk/domain/_category_.json b/leancloud/docs/sdk/domain/_category_.json deleted file mode 100644 index 9d023fe72..000000000 --- a/leancloud/docs/sdk/domain/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "域名", - "collapsed": true, - "position": 1 -} diff --git a/leancloud/docs/sdk/domain/faq.mdx b/leancloud/docs/sdk/domain/faq.mdx deleted file mode 100644 index 41af40335..000000000 --- a/leancloud/docs/sdk/domain/faq.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: 域名绑定常见问题 -sidebar_label: 域名绑定常见问题 -sidebar_position: 10 ---- - -import { Conditional } from "/src/docComponents/conditional"; - - - -### 开发版应用无法在 LeanCloud 备案吗? - -由于我们运营人员有限,我们无法协助开发版应用办理新增备案。 - -建议考虑通过其他方式来继续使用我们的服务,如: - -- 转入 [LeanCloud 国际版](https://leancloud.app),国际版无需绑定域名,也无需备案。 -- 到 IaaS 服务商处自行完成备案和接入。我们华北节点是和 UCloud 合作,华东节点是和腾讯云合作,大家可以按照应用的节点选择合适的 IaaS 服务商去备案。 -- 升级为商用版。 - -### 老版本的客户端怎么办? - -对于绑定了自有域名的应用,其老版本的客户端由于不能及时升级的原因,还会继续使用 LeanCloud 原来提供的共享域名。 -我们目前还支持这些请求,以便兼容老版本的客户端。 -但共享域名不保证可用性,未来我们也计划下线共享域名,还请尽快升级客户端。 - - - -### 开启 HTTPS 的域名,其 SSL 证书如何处理? - -API 自定义域名必须开启 HTTPS。 -云引擎自定义域名和文件服务自定义域名,HTTPS 是可选项。 - -如果启用 HTTPS,我们提供了两种方式 SSL 证书配置方式: - -- 开发者手动上传自己的证书,在证书到期前自行续期证书并重新上传。 -- 自动为该域名申请并维护 Let's Encrypt 证书。 - -选择哪种方式,开发者可以自行决定。 - -### 不同应用之间可以使用同一个子域名吗? - -不同应用无法使用相同的子域名。 - - - -### 应用在开发版和商用版之间切换,对于域名绑定会有什么影响吗? - -域名绑定不受开发版、商用版切换影响。 - - - -### 应用转让对于域名绑定会有什么影响吗? - -应用转让之后,如果原域名所有者不删除 DNS 解析记录,那么所有的请求还是会打到 LeanCloudTDS 后端集群上来,这些请求都会被正常处理。如果原域名所有者删除了 DNS 解析记录,而新的开发者又不绑定到新的域名,那么理论上使用原域名访问的流量是根本不会到达 LeanCloudTDS 集群的,这时候被转让的应用基本上就处于不可用状态了。 - -如果应用的新所有者需要更换域名,可以采取先新增绑定、后删除老域名的方式来操作。 -同一个应用的同一种服务支持绑定多个访问域名,以便开发者需要切换域名的时候可以平滑过渡。 - -### 使用云函数服务需要绑什么域名? - -使用云函数(包含 hook 函数)的应用,需要绑定 API 域名。 - -在云引擎托管网站,则需要绑定云引擎域名。 - -如果你实在不确定的话,可以两个都绑一下(需要绑定不同的域名,可以是同一域名的不同子域名,例如 `api.example.com` 和 `web.example.com`),这样就万无一失了。 - - - -### 文件域名因为备案信息变更,绑定域名时会报错提示「域名已冻结」。 - -如果文件域名在工信部有备案信息变更,或者有文件域名注销后重新备案的情况,绑定域名时会报错「域名已冻结」。这种情况可以通过工单联系我们申请解冻。 - - - -### 开发版应用也需要接入备案么? - -商用版应用、开发版应用使用的 API 自定义域名和云引擎自定义域名都需要接入备案(同一域名下的不同子域名无需重复接入备案)。开发版应用需要先[购买独立 IP](/sdk/domain/guide/#独立-ip),然后通过工单提交接入备案所需材料。 - -### 我是开发版应用,但不想购买独立 IP,还能办理接入备案么? - -接入备案申请中需要提交 IP 地址信息。名下有商用版应用的开发者,我们会赠送独立 IP。 -如名下无商用版应用,你需要自行购买独立 IP。 -目前我们无法协助没有独立 IP 的应用办理接入备案。 - -### 如果以后退订了备案时使用的独立 IP,会发生什么? - -如果管局或底层 IaaS 服务商抽查到 IP 与接入备案的域名不符,会限令整改甚至撤销接入备案。建议不要退订备案使用的独立 IP,除非相应域名已不再使用。 - -### 开发版应用无法提交工单? - -之前工单仅向商用版应用开放,现在为了协助开发版应用办理接入备案,工单系统也向开发版应用开放,不过开发版应用仅能在「接入备案」分类下提交工单。 - - - -### 部署云引擎的服务端项目,如果自定义域名请求接口有报错:```Provisional headers are shown``` - -该报错是大部分原因是云引擎中绑定自定义域名使用自己上传的 SSL 证书有问题,可以使用如下两个 SSL 证书检测工具输入云引擎中绑定的域名然后进行检测: -* https://www.myssl.cn/tools/check-server-cert.html -* https://myssl.com/ssl.html - -如果检查结果为: 检测结果提示“服务器缺少中间证书”或“证书链不完整”,可以进入 https://myssl.com/chain_download.html 进行证书修复; diff --git a/leancloud/docs/sdk/domain/faq_Icp.mdx b/leancloud/docs/sdk/domain/faq_Icp.mdx deleted file mode 100644 index 5bb3b4a91..000000000 --- a/leancloud/docs/sdk/domain/faq_Icp.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: 备案常见问题 -sidebar_label: 备案常见问题 -sidebar_position: 10 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -### 什么是接入备案? - -现代网站、应用的架构往往包含多个服务,这些服务可能部署在不同的云服务商上。 -在接入多个云服务商的情况下,使用的所有云服务商皆需备案或接入备案(在使用的第一家云服务商处新增备案,在其他云服务商处接入备案)。 -不过,CDN、邮件服务一般不需要接入备案。 -另外,有可能多家云服务商所用的底层 IaaS 服务商是同一家,这类情况算一家云服务商。 - -### 不需要备案的情况 - -- 单机游戏只接入广告无需备案。 -- 游戏仅仅调用 TapTap 登录 SDK 确定未使用内建账户功能无需备案。 - -### 需要备案的情况 - -游戏接入了 TapTap 联网服务,例如聊天室,云存档等要备案。 - -### 接入备案需要关站么? - -接入备案只是在备案信息中新增一个服务商,不会影响之前服务商,可以同时使用。 -和新增备案不同,接入备案可以在服务上线后进行,不要求关站或停止解析,不影响当前网站访问。 - -### 接入备案的流程有哪些?要花多久? - -接入备案的流程和新增备案类似,整个过程花费的时间也差不多,接入备案提交的备案主体等信息需与原备案信息一致。 - - -### 自定义域名已经用了很久了,为何突然收到通知需要办理接入备案? - -从合规的角度来说,都是需要接入备案的。 -文档也注明了这一点。 -考虑到有些用户可能自行在底层 IaaS 服务商办理过接入备案,底层 IaaS 服务商也没有提供查询某个域名是否办理了接入备案的 API 接口,所以控制台在绑定域名时只检查了域名是否备案,没有检查是否接入备案,但我们在收到底层 IaaS 服务商通知时都会协助用户办理接入备案。 - -### 我收到了需要办理接入备案的通知,但不想办理,会怎么样? - -超过通知载明的期限后,底层 IaaS 服务商可能会阻止域名的访问,建议尽快办理接入备案,以免应用承受不必要的停服风险。 -如有特殊情况,请通过工单联系我们,我们尽力协助您向底层 IaaS 服务商争取延长时间。 - -### 文件域名因为备案信息变更,绑定域名时会报错提示「域名已冻结」。 - -如果文件域名在工信部有备案信息变更,或者有文件域名注销后重新备案的情况,绑定域名时会报错「域名已冻结」。这种情况可以通过工单联系我们申请解冻。 - - - - - -### 开发版应用也需要接入备案么? - -商用版应用、开发版应用使用的 API 自定义域名和云引擎自定义域名都需要接入备案(同一域名下的不同子域名无需重复接入备案)。开发版应用需要先[购买独立 IP](/sdk/domain/guide/#独立-ip),然后通过工单提交接入备案所需材料。 - -### 我是开发版应用,但不想购买独立 IP,还能办理接入备案么? - -接入备案申请中需要提交 IP 地址信息。名下有商用版应用的开发者,我们会赠送独立 IP。 -如名下无商用版应用,你需要自行购买独立 IP。 -目前我们无法协助没有独立 IP 的应用办理接入备案。 - -### 如果以后退订了备案时使用的独立 IP,会发生什么? - -如果管局或底层 IaaS 服务商抽查到 IP 与接入备案的域名不符,会限令整改甚至撤销接入备案。建议不要退订备案使用的独立 IP,除非相应域名已不再使用。 - -### 开发版应用无法提交工单? - -之前工单仅向商用版应用开放,现在为了协助开发版应用办理接入备案,工单系统也向开发版应用开放,不过开发版应用仅能在「接入备案」分类下提交工单。 - - \ No newline at end of file diff --git a/leancloud/docs/sdk/domain/guide.mdx b/leancloud/docs/sdk/domain/guide.mdx deleted file mode 100644 index 8f39ca6d6..000000000 --- a/leancloud/docs/sdk/domain/guide.mdx +++ /dev/null @@ -1,730 +0,0 @@ ---- -title: 域名绑定指南 -sidebar_label: 绑定指南 -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -根据法律法规和有关部门的要求,使用云服务需要绑定一个已经在[工信部](https://beian.miit.gov.cn/?spm=a2c4g.11186623.0.0.58797cc85Mnmlh#/Integrated/index)备案过的域名。绑定自有域名也有利于从域名层面做好应用隔离,确保业务稳定。 - -这篇指南假定你了解域名解析的基本知识,如果你不太熟悉这方面的内容,可以参考[这篇文章](https://blog.taptap.dev/posts/domain-introduction)。 - -本指南面向国内版应用的开发者。 - - -## 以下服务需绑定域名 - - - -使用 LeanCloud 的数据存储和云引擎服务要求必须绑定自有域名,其他服务可选是否绑定自有域名。 -我们提供了 API 共享域名和文件共享域名,但是请注意,**共享域名没有可用性保证,容易受到 DDoS 攻击影响。我们建议在生产环境中使用绑定的自定义域名。** - - - - - -使用 TDS 的数据存储、云引擎要求必须绑定自有域名,其他服务可选是否绑定自有域名。 -我们提供了 API 共享域名和文件共享域名,但是请注意,**共享域名没有可用性保证,容易受到 DDoS 攻击影响。我们建议在生产环境中使用绑定的自定义域名。** - - - -## 自定义域名的种类 - -国内节点服务涉及以下几种自定义域名: - - - -| 域名种类 | 涉及服务 | 在工信部备案 | 在 LeanCloud 备案或接入备案 | SSL | 绑定目标 | 静态资源加速 | -|---------|--------|------------|--------------------------|-----|---------|------------| -| 文件域名 | 文件服务 | 必须 | 可选 | 可选 | 应用 | 是 | -| 云引擎域名 | 云引擎 **网站托管** | 必须 | 必须 | 可选 | 分组 | 视情况 | -| API 域名 | 存储、即时通讯、短信、推送、**云函数** | 必须 | 必须 | 必须 | 应用 | 否 | - - - - - -| 域名种类 | 涉及服务 | 在工信部备案 | 在 TDS 备案或接入备案 | SSL | 绑定目标 | 静态资源加速 | -|--------|--------|------------|---------------------|-----|---------|-----------| -| 文件域名 | 文件服务、云存档 | 必须 | 可选 | 可选 | 应用 | 是 | -| 云引擎域名 | 云引擎 **网站托管** | 必须 | 必须 | 可选 | 分组 | 视情况 | -| API 域名 | 数据存储、排行榜、内建账户、成就、**云函数**、云存档 | 必须 | 必须 | 必须 | 应用 | 否 | - - - -说明: - -1. 可以使用同一主域名下不同的子域名,每个子域名只能绑定到一个应用,且同一应用的 API 服务、云引擎网站托管服务、文件服务需绑定不同的子域名。 -2. API 域名需要启用 SSL,绑定时会自动申请 SSL 证书,当然你也可以自行上传证书。 -3. 绑定目标中的「分组」指应用下的云引擎实例分组的生产环境。 -4. 我们推荐为 API 域名和云引擎域名配置独立 IP。**API 服务只对独立(IP)入口提供可用性保证。**未配置独立 IP 的云引擎服务默认会为静态站点优化,有一些对动态内容不那么友好的限制。详见下面的 [API 域名](#api-域名)、[云引擎域名](#云引擎域名)小节。 -5. 文件域名、使用共享 IP 的 API 域名、云引擎域名使用 CNAME 配置域名解析,因此不支持绑定裸域名,以免影响该域名下的其他域名的解析以及与 MX 等记录冲突。如果需要使用裸域名作为 API 域名或云引擎域名可以配置独立 IP 后使用 A 记录绑定。 -6. 一个应用(文件域名、API 域名)或分组(云引擎域名)上可以绑定多个域名,但文件服务返回的内部文件(托管在 LeanCloudTDS 文件服务的文件)的 `url` 字段同一时间只能选择使用一个域名(可以在控制台切换)。 - -## 文件域名 - -如果你的应用使用了文件服务,请前往 **应用控制台 > 设置 > 域名绑定 > 文件访问域名开发者中心 > 游戏服务 > 应用配置 > 基本信息 > 域名配置 > 文件** 绑定文件域名。 - -注意,即使你并未使用数据存储服务,但是使用了即时通讯的多媒体消息(图像、音频、视频等),那么就有可能使用了文件服务。 -一个简单的判断方法是到 **开发者中心 > 游戏服务 > 云服务控制台 > 数据存储 > 文件 > 文件管理** 页面,如果其中有数据,就表明你的应用使用了文件服务。 - -绑定文件域名时,可以选择是否启用 HTTPS: - -- 不启用 HTTPS,则客户端只能通过 HTTP 访问。 - -- 启用 HTTPS 后,客户端同时可以通过 HTTPS 和 HTTP 访问文件。但是,受限于文件服务提供商,无论客户端使用 HTTP URL 还是 HTTPS URL 访问: - - - 启用 HTTPS 域名的自定义文件域名的流量均按照 HTTPS 流量计费。 - - 同理,应用控制台的文件流量统计也总是归入 HTTPS 流量。 - -通过 **开发者中心 > 游戏服务 > 云服务控制台 > 数据存储 > 文件 > 设置 > 文件访问地址** 更换文件域名后,之前托管在文件服务的文件 URL 会自动更新。 -即时通讯历史消息(包括富媒体消息)中的 URL 不会自动更新。 -类似地,如果开发者把文件 URL 单独保存在别的地方,更换文件域名后,需要在客户端自行实现相应的替换逻辑。 -因此,我们建议开发者在开始使用文件服务和即时通讯服务时就绑定自定义文件域名,以免给以后迁移增加困难。 - -之前使用 LeanCloud 华东节点公共文件域名的旧应用,在绑定文件域名后,现存的使用共享域名的 URL 仍然可以访问。 -如果绑定了多个文件域名,那么通过这些域名都可以访问托管在文件服务的文件(内部文件),但文件服务返回的内部文件的 URL 总是使用在前述「文件访问地址」中设置的域名。 - - - 如果在不同应用间绑定了 _File{" "} - Class,那么需要在源应用绑定自定义文件域名,目标应用无需绑定自定义文件域名。 - - -由于底层文件服务商的限制,如果你在底层文件服务商(七牛)处有账号,并且在自己的七牛账号绑定了泛域名(例如 `*.example.com`),那么该域名(`example.com`)下的所有子域名均无法绑定 LeanCloudTDS 文件服务。 -如在七牛取消泛域名绑定,下一个账期以后可以绑定该域名下的子域名至 LeanCloudTDS 文件服务。 - -## 云引擎域名 - -使用云引擎网站托管服务的应用,需要前往 **应用控制台 > 设置 > 域名绑定 > 云引擎、ClientEngine 域名开发者中心 > 游戏服务 > 应用配置 > 基本信息 > 域名配置 > 云引擎** 绑定云引擎域名。 - -如前所述,仅使用云函数(包括 hook 函数)的应用,无需绑定云引擎域名,不过需要绑定 API 域名。 - -控制台绑定域名时,可以自动申请 SSL 证书。相应地,`/.well-known/acme-challenge/` 路径用于验证,开发者无法使用该路径。 - -`stg-` 开头的自定义域名(例如 `stg-web.example.com`)会被自动地绑定到预备环境。 - -如前所述,我们推荐为云引擎配置独立 IP,未配置独立 IP 的云引擎服务默认会为静态站点优化,使用边缘节点为静态资源加速访问。但由于边缘节点的限制,对有些动态内容请求不怎么友好。下面的表格简单对比了两者: - -| | 独立入口 | 加速节点 | -| --------- | :--------------------------------------------------------------: | :-------------------------------------------------------------: | -| 域名指向 | 云引擎集群的独立 IP | 边缘节点 | -| 访问加速 | - | ✔️
    (针对可缓存资源) | -| 动态请求 | 友好 | 有限制 | -| DDoS 防护 | 2 Gbps 防护带宽
    (可更换 IP 或接入清洗服务) | 基础 DDoS 防护 | -| 流量费用 | 0.8 元/GB
    (每实例每天 1GB 免费额度) | 0.36 元/GB
    (回源流量依然按照普通域名收费) | -| DNS 解析 | A 记录 | CNAME 记录 | - -如果你在云引擎上部署的是纯静态站点,比如静态生成的网站,前后端分离应用的前端部分,以及图片、文件等静态资源,那么,使用加速节点可以提升终端用户访问速度,在流量较高的情况下还能降低流量费用。 - -如果你在云引擎上部署提供动态内容的网站或服务,例如 API 服务或者服务端渲染的页面,推荐使用独立入口。一方面,对动态内容进行加速有可能会略微影响用户访问速度,并会同时产生加速流量费用与回源流量费用。另一方面,由于边缘节点的限制,使用边缘节点的云引擎服务在使用 WebSocket 连接、HTTP PATCH 方法、获取客户端 IP 时可能遇到问题,请求超时限制可能低至 10 秒。而使用 A 记录指向独立 IP 的云引擎站点使用独立入口,不经过边缘节点,没有上述限制。 - -对于混合内容的部署,例如常见的前后端分离的单页应用,在一个分组中同时提供静态的前端页面与动态的 API 服务,可以绑定两个域名,一个域名通过 A 记录指向独立 IP,另一个域名通过 CNAME 记录指向加速节点,通过前一个域名访问动态 API 服务,通过后一个域名访问静态资源。 - -你可以随时按需通过配置 DNS 选择域名指向的入口。 -换言之,独立入口与加速节点的切换,只需在域名服务商处修改 DNS 解析,将 CNAME 记录替换为 A 记录或相反。 - -## API 域名 - -如果你使用了以下服务: - -- 结构化数据存储 - -- 云函数(包括 hook 函数) - -- 即时通讯 - -- 多人在线对战、排行榜、内建账户、好友、成就、云存档、实时语音、推送通知 - -那么建议你前往 **应用控制台 > 设置 > 域名绑定 > API 访问域名开发者中心 > 游戏服务 > 应用配置 > 基本信息 > 域名配置 > API** 绑定 API 域名。其中「结构化数据存储」和「云函数」两个模块是要求绑定自有域名的。 - - - -只使用推送和短信功能,且推送、短信均通过服务端发送,不涉及 UGC -内容的应用目前不强制要求绑定域名,但我们仍然建议用户绑定自己的 API -域名,以免受到共享域名可用性的影响。 - - - -

    - 我们推荐为 API 域名配置独立 IP。 - - 我们为每个绑定了 API 自定义域名的应用赠送了独立 IP。 - - - API - 服务入口分为独立(IP)入口与共享入口,我们只对独立(IP)入口提供可用性保证。 - -

    - -**共享入口有很多应用共同使用,风险较大,我们不对其提供可用性保证。**通过以下方式访问 API 服务将使用不提供可用性保证的共享入口: - -- 使用已绑定,但是没有指向独立 IP 的自有域名 - - 你可以在命令行 `dig` 自有域名,检查解析结果是否指向独立 IP。 - 如果没有指向独立 IP,请前往自有域名的域名服务商处修改域名解析,将 CNAME 记录替换为指向独立 IP 的 A 记录。 - -- 使用系统分配的测试域名 - - 我们为开发测试阶段的应用提供测试域名,这个域名仅供测试使用,可能被回收。正式上线的应用请绑定自有域名。测试域名可以在 **控制台 > 设置 > 应用凭证 > 服务器地址开发者中心 > 游戏服务 > 应用配置 > 基本信息 > 域名配置 > API > 共享域名** 查看。 - - - -- 使用旧版 SDK 且没有指定域名 - - 已绑定自有域名的应用,出于兼容性考虑,旧版本客户端仍可继续访问原来由 LeanCloud 提供的共享域名(旧版 SDK 会自动获取共享域名)。我们会视情况在未来合适的时候回收共享域名。我们强烈建议开发者推动用户升级到使用自定义域名的新版本客户端应用,以免业务受到不必要的影响。 - -- 使用其他已停止支持的域名 - - 一些更老的、已停止支持的 SDK 使用的域名,我们很久以前已宣布停止支持。未来我们会停止在这些域名上提供 API 服务。 - - - -目前,绑定 API 域名后,即时通讯、LiveQuery 以及多人对战的 WebSocket 连接仍会使用共享域名。 -这些共享域名虽然使用共享入口,但我们也会尽可能保证其可用。 -我们后续会支持这类 WebSocket 连接使用自有域名。 - - - 另外,用户反馈组件 API - 已弃用,但由于导出应用数据中不含用户反馈数据,为方便开发者迁移数据,用户反馈相关的 - REST API 接口仍可调用,这些接口也仍然使用共享域名。 - - -### 更新代码 - -绑定自有 API 域名后,需要更新客户端代码以使用自定义域名。请参考各服务的开发指南进行配置。 - - - -以下部分假定绑定的自定义域名是 `xxx.example.com`,且开启了 HTTPS。 - -#### REST API - -参考 [存储 REST API 使用指南](/sdk/storage/guide/rest/#base-url) 配置。 - -#### JavaScript SDK - -##### 存储 SDK - -请参考 [SDK 安装指南](/sdk/storage/guide/setup-js/#安装与引用-sdk) 配置。 - -旧版本的 SDK 请参考以下方法配置(建议使用最新版本的 SDK): - -
    - -`>=3.5.5, <3.11.1` 的版本可能会碰到仍然使用缓存默认配置的 bug,它可能会导致更新后的第一次请求失败。 - -```js -AV.init({ - // appId, appKey, - serverURLs: "https://xxx.example.com", -}); -``` - -`>= 3.0.0, <3.5.5` - -```js -AV.init({ - // appId, appKey, - serverURLs: { - push: "https://xxx.example.com", - stats: "https://xxx.example.com", - engine: "https://xxx.example.com", - api: "https://xxx.example.com", - }, -}); -``` - -`<3.0.0` 的数据存储 SDK 不支持自定义域名。 - -
    - -##### 即时通讯 SDK - -请参考 [即时通讯开发指南](/sdk/im/guide/overview/) 配置。 - - -旧版本的 SDK 请参考以下方法配置(建议使用最新版本的 SDK): - -
    - -`>=4.0.0, <=4.3.1` 的即时通讯 SDK 的 server 参数只能填写域名(不含协议),不支持未启用 HTTPS 的自定义域名: - -```js -new Realtime({ - // appId, appKey, - server: "xxx.example.com", -}); -``` - -`<4.0.0` 的即时通讯 SDK 不支持自定义域名。 - -如果使用了 LiveQuery 功能,建议使用 `>=3.14.0` 的存储 SDK。 -旧版本(`>=3.5.0, <=3.13.2`)的 SDK 还需要在初始化的时候额外配置 LiveQuery 模块的域名: - -```js -AV.init({ - // appId, appKey, - // serverURLs, - realtime: new AV._sharedConfig.liveQueryRealtime({ - appId, - appKey, - server: "xxx.example.com", - }), -}); -``` - -`>=3.5.0, <3.13.2` 的 SDK 不支持未启用 HTTPS 的自定义域名。 - -`<3.5.0` 存储 SDK 的 LiveQuery 不支持自定义域名。 - -
    - -##### 多人在线对战 - -请参考 [入门指南](/sdk/multiplayer/start/js/#初始化) 或 [开发指南](/sdk/multiplayer/guide/js/) 进行配置。 - -##### 微信小程序白名单 - -前往 **LeanCloud 控制台 > 设置 > 应用凭证 > 域名白名单**,获取域名白名单(不同应用对应不同的域名)。 - -登录[微信公众平台],前往 **设置 > 开发设置 > 服务器配置 > 「修改」** 链接,**增加**上述域名白名单中的域名。 - -[微信公众平台]: https://mp.weixin.qq.com - -#### Objective-C SDK - -请参考 [SDK 安装指南](/sdk/storage/guide/setup-objc/#初始化) 配置。 - -`<12.0.0` 的版本请参考以下方法配置: - -
    -
    
    -// 配置 SDK 储存
    -[AVOSCloud setServerURLString:@"https://xxx.example.com" forServiceModule:AVServiceModuleAPI];
    -// 配置 SDK 推送
    -[AVOSCloud setServerURLString:@"https://xxx.example.com" forServiceModule:AVServiceModulePush];
    -// 配置 SDK 云引擎(用于访问云函数,使用 API 自定义域名,而非云引擎自定义域名)
    -[AVOSCloud setServerURLString:@"https://xxx.example.com" forServiceModule:AVServiceModuleEngine];
    -// 配置 SDK 即时通讯
    -[AVOSCloud setServerURLString:@"https://xxx.example.com" forServiceModule:AVServiceModuleRTM];
    -// 配置 SDK 统计
    -[AVOSCloud setServerURLString:@"https://xxx.example.com" forServiceModule:AVServiceModuleStatistics];
    -// 初始化应用
    -[AVOSCloud setApplicationId:@"APPID" clientKey:@"APPKEY"];
    -
    - -部分旧版本 SDK(< 8.2.3)存在 SSL Pinning,它可能导致配置后的自定义服务器地址无法使用,如果出现了「证书非法」的相关错误,请至少升级 SDK 到 8.2.3,建议升级至最新版。 -
    - -`<4.6.0` 的版本不支持自定义域名。 - -#### Swift SDK - -`>= 17.0.0` 的版本请参考 [SDK 安装指南](/sdk/storage/guide/setup-swift/#初始化) 配置。 - -`>= 16.1.0, < 17.0.0` 的版本请参考以下方法配置: - -
    - -```swift -let configuration = LCApplication.Configuration( - customizedServers: [ - .api("https://xxx.example.com"), - .engine("https://xxx.example.com"), - .push("https://xxx.example.com"), - .rtm("https://xxx.example.com") - ] -) -do { - try LCApplication.default.set( - id: "APPID", - key: "APPKEY", - configuration: configuration - ) -} catch { - fatalError("\(error)") -} -``` - -
    - -`<16.1.0` 的版本不支持自定义域名。 - -#### Java Unified SDK - -Java Unified SDK(`>= 6.0.0`)请参考 [SDK 安装指南](/sdk/storage/guide/setup-java/#初始化) 配置。 - -旧版本 SDK 请参考以下方法配置: - -
    - -使用 Java Unified SDK(`< 6.0.0`)的 Android 项目,需在 `Application` 类的 `onCreate` 方法添加: - -```java -import cn.leancloud.AVOSCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - // 配置 SDK 储存 - AVOSCloud.setServer(AVOSService.API, "https://xxx.example.com"); - // 配置 SDK 云引擎(用于访问云函数,使用 API 自定义域名,而非云引擎自定义域名) - AVOSCloud.setServer(AVOSService.ENGINE, "https://xxx.example.com"); - // 配置 SDK 推送 - AVOSCloud.setServer(AVOSService.PUSH, "https://xxx.example.com"); - // 配置 SDK 即时通讯 - AVOSCloud.setServer(AVOSService.RTM, "https://xxx.example.com"); - - // 提供 this、App ID 和 App Key 作为参数 - // 注意这里千万不要调用 cn.leancloud.core.AVOSCloud 的 initialize 方法,否则会出现 NetworkOnMainThread 等错误。 - AVOSCloud.initialize(this, "APPID", "APPKEY"); - } -} -``` - -老的 Android SDK 请参考以下方法配置: - -```java -// 配置 SDK 储存 -AVOSCloud.setServer(AVOSCloud.SERVER_TYPE.API, "https://xxx.example.com"); -// 配置 SDK 云引擎 -AVOSCloud.setServer(AVOSCloud.SERVER_TYPE.ENGINE, "https://xxx.example.com"); -// 配置 SDK 推送 -AVOSCloud.setServer(AVOSCloud.SERVER_TYPE.PUSH, "https://xxx.example.com"); -// 配置 SDK 即时通讯 -AVOSCloud.setServer(AVOSCloud.SERVER_TYPE.RTM, "https://xxx.example.com"); -// 初始化应用 -AVOSCloud.initialize(this, "APPID", "APPKEY"); -``` - -`<4.4.4` 的 Android SDK 不支持自定义域名。 - -
    - -#### .NET SDK - -请参考 [SDK 安装指南](/sdk/storage/guide/setup-dotnet/#初始化) 配置。 - -`< v20190925.1` 的版本请参考以下方法配置: - -
    - -```cs -AVClient.Initialize(new AVClient.Configuration { - ApplicationId = "APPID", - ApplicationKey = "APPKEY", - ApiServer = new Uri("https://xxx.example.com"), - EngineServer = new Uri("https://xxx.example.com"), - PushServer = new Uri("https://xxx.example.com") -}); -``` - -
    - -#### PHP & Python SDK - -注意,云引擎内部访问 API 是通过内网,所以不需要也不应该配置 API 自定义域名。 -模板项目和云引擎网站托管开发指南中的示例代码均未配置 API 自定义域名,请勿设置自定义域名,以免变成公网访问,影响性能。 - -在云引擎以外的服务端使用 PHP 或 Python SDK,可以不配置 API 自定义域名。 - -#### App ID 后缀不为 `-MdYXbMMI` 的国际版应用如何初始化 SDK - -极个别国际版应用的 App ID 后缀不为 `-MdYXbMMI`(包括个别老应用以及进行过特殊配置迁移到国际版的应用)。 -对于这些应用而言,初始化 SDK 时不配置自定义域名可能会报错 -(因为较新版本的 SDK 增加了自定义域名配置参数的检查,检查时会根据 App ID 后缀判断是否为国际版应用)。 -这些应用需要这样初始化 SDK: - -
    - -注意,请使用你的应用 App ID 的前 8 位替换以下 url 地址中的 `aaaaaaaa`。 - -**JavaScript 存储** - -```js -AV.init({ - // appId, appKey, - serverURLs: { - push: "https://aaaaaaaa.push.lncldglobal.com", - stats: "https://aaaaaaaa.stats.lncldglobal.com", - engine: "https://aaaaaaaa.engine.lncldglobal.com", - api: "https://aaaaaaaa.api.lncldglobal.com", - }, -}); -``` - -**JavaScript 即时通讯** - -

    - 请参考{" "} - - 即时通讯开发指南 - {" "} - 配置。 其中 server 参数的值为{" "} - aaaaaaaa.rtm.lncldglobal.com。 -

    - -

    - JavaScript 多人在线对战 -

    - -

    - 请参考{" "} - - 入门指南 - {" "} - 或{" "} - - 开发指南 - {" "} - 进行配置。 其中 playServer 参数的值为{" "} - aaaaaaaa.play.lncldglobal.com。 -

    - -

    - Objective-C -

    - -
    -  
    -    // 配置 SDK 储存 [AVOSCloud
    -    setServerURLString:@"https://aaaaaaaa.api.lncldglobal.com"
    -    forServiceModule:AVServiceModuleAPI]; // 配置 SDK 推送 [AVOSCloud
    -    setServerURLString:@"https://aaaaaaaa.push.lncldglobal.com"
    -    forServiceModule:AVServiceModulePush]; // 配置 SDK
    -    云引擎(用于访问云函数,使用 API 自定义域名,而非云引擎自定义域名)
    -    [AVOSCloud setServerURLString:@"https://aaaaaaaa.engine.lncldglobal.com"
    -    forServiceModule:AVServiceModuleEngine]; // 配置 SDK 即时通讯 [AVOSCloud
    -    setServerURLString:@"https://aaaaaaaa.rtm.lncldglobal.com"
    -    forServiceModule:AVServiceModuleRTM]; // 配置 SDK 统计 [AVOSCloud
    -    setServerURLString:@"https://aaaaaaaa.stats.lncldglobal.com"
    -    forServiceModule:AVServiceModuleStatistics]; // 初始化应用 [AVOSCloud
    -    setApplicationId:@"APPID" clientKey:@"APPKEY"];
    -  
    -
    - -

    - Swift -

    - -```swift -let configuration = LCApplication.Configuration( - customizedServers: [ - .api("https://aaaaaaaa.api.lncldglobal.com"), - .engine("https://aaaaaaaa.engine.lncldglobal.com"), - .push("https://aaaaaaaa.push.lncldglobal.com"), - .rtm("https://aaaaaaaa.rtm.lncldglobal.com") - ] -) -do { - try LCApplication.default.set( - id: "APPID", - key: "APPKEY", - configuration: configuration - ) -} catch { - fatalError("\(error)") -} -``` - -

    - Java -

    - -```java -import cn.leancloud.AVOSCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - // 配置 SDK 储存 - AVOSCloud.setServer(AVOSService.API, "https://aaaaaaaa.api.lncldglobal.com"); - // 配置 SDK 云引擎(用于访问云函数,使用 API 自定义域名,而非云引擎自定义域名) - AVOSCloud.setServer(AVOSService.ENGINE, "https://aaaaaaaa.engine.lncldglobal.com"); - // 配置 SDK 推送 - AVOSCloud.setServer(AVOSService.PUSH, "https://aaaaaaaa.push.lncldglobal.com"); - // 配置 SDK 即时通讯 - AVOSCloud.setServer(AVOSService.RTM, "https://aaaaaaaa.rtm.lncldglobal.com"); - - // 提供 this、App ID 和 App Key 作为参数 - // 注意这里千万不要调用 cn.leancloud.core.AVOSCloud 的 initialize 方法,否则会出现 NetworkOnMainThread 等错误。 - AVOSCloud.initialize(this, "APPID", "APPKEY"); - } -} -``` - -

    - .NET -

    - -```cs -AVClient.Initialize(new AVClient.Configuration { - ApplicationId = "APPID", - ApplicationKey = "APPKEY", - ApiServer = new Uri("https://aaaaaaaa.api.lncldglobal.com"), - EngineServer = new Uri("https://aaaaaaaa.engine.lncldglobal.com"), - PushServer = new Uri("https://aaaaaaaa.push.lncldglobal.com") -}); -``` - -
    - -
    - -## 独立 IP - - - -在华北节点控制台(「右上角头像下拉菜单 > 账号设置 > 独立 IP」)可以查看当前账号使用的 API 独立 IP 和云引擎独立 IP,在同一页面也可以申请和解绑 IP。 - -申请独立 IP 后,在 LeanCloud 控制台新增域名绑定时,请按控制台提示到自有域名的域名服务商处添加 A 记录,指向独立 IP。 -已经在 LeanCloud 绑定的自定义域名,只需在域名服务商处修改解析记录,将 CNAME 记录替换为 A 记录即可切换。 - -解绑 IP 前,请确保所有指向该 IP 的自定义域名均已修改解析记录,将相应 A 记录替换回 CNAME 记录。 -由于 DNS 在整个互联网生效需要较长时间,修改解析记录后,请**至少等待 48 小时再解绑 IP**,以免影响服务。 - -独立 IP 是账户下所有应用共享的,但 API 服务和云引擎服务不可共用 IP。 -如果你希望进一步隔离各应用的入口,也可以在控制台购买更多独立 IP。 -在应用的域名绑定页会列出帐号下所有可用的 IP,你需要自行规划哪些应用使用哪些 IP。 - -每一个独立 IP 默认提供了 2 Gbps 的防护带宽,可以防护小规模的攻击,你也无需为此承担额外的费用。 -如果攻击超出了默认的防护容量,IP 会被禁用,你可以购买一个新的 IP 进行更换。 -或者你也可以将新的 IP 作为源站 IP 接入第三方清洗服务,请通过工单联系我们,我们会提供必要的支持。 - -独立 IP 的费用为人民币 50 元 / 个 / 月,购买每个 IP 时会扣除一个月的费用(50 元),从下个自然月起,每月 1 日按 50 元 / 个的标准进行扣费。 -我们给华北节点名下有商用版应用的用户都赠送了一个 API 独立 IP,可以免费使用。 - -独立 IP 与账户绑定,不会随应用的转移而发生变化。为了避免影响服务,转移的应用如果不改变自定义域名的解析配置,那么还可以继续使用原来的独立 IP。 - -华东节点暂不支持在控制台自助绑定独立 IP,如有需求,可提交工单联系我们处理。 - -如果你有多个独立 IP,在通过修改域名解析切换独立 IP 前,可能希望验证通过新 IP 可以成功访问服务。 -这种场景下可以使用 curl 的 `--resolve` 参数指定域名解析到特定的 IP(效果类似修改 `/etc/hosts` 文件)。 -比如,验证 API 独立 IP: - -```sh -curl --resolve 'api.example.com:443:YOUR-API-IP' https://api.example.com/1.1/date -``` - -正常情况下会返回包含当前时间的 JSON 格式数据。 -验证云引擎独立 IP 同理: - -```sh -curl --resolve 'engine.example.com:443:YOUR-ENGINE-IP' https://engine.example.com/ -``` - - - - -我们为每个绑定了 API 自定义域名的应用赠送了独立 IP。 - -绑定 API 自定义域名,并在客户端 SDK 初始化时指定 API 自定义域名,即可使用独立 IP。 - -[云引擎独立 IP](/sdk/engine/dedicated-IP/) 可按需购买。 - - - -## 域名绑定 - -绑定自定义域名前需要准备一个已经**备案的域名**,接下来我们详细介绍自定义域名绑定流程。 - -绑定域名过程中,控制台会提示相应的 DNS 配置,请按照控制台的提示到域名注册商或域名解析服务提供商处设置(如果自己架设域名解析服务器的话,请根据域名解析服务器文档配置)A 记录或 CNAME 记录。 - -以下使用已经备案的主域名 `example.com` 的子域名 `api.example.com` 为例,介绍自定义域名绑定流程: - -![控制台域名绑定界面](/img/domain-guide-new.png) - - - -通过**开发者管理后台 -> 游戏服务 -> 应用配置 -> 域名配置**页面开始,选择你需要绑定自定义域名的服务,然后点击绑定新域名。 - - - - -通过 **LeanCloud 管理后台 -> 设置 -> 域名绑定**页面开始,选择你需要绑定自定义域名的服务,然后点击绑定新域名。 - - -上图以 API 访问域名云服务 API 绑定为例,接下来,我们对绑定过程中的操作步骤进行单独说明: - -1. 我们要求使用二级域名(当你已备案的主域名为 `example.com` 时,可以在主域名前加上自定义字段形成新域名,这个域名就是二级域名)进行绑定。输入二级域名后,我们默认启用 SSL、强制使用 HTTPS 和 SSL 证书自动管理,如果自己有证书可以设置为手动,设置 SSL 证书可参考 [SSL 证书](#ssl-证书)。然后点击“绑定”按钮进行下一步。 - -2. 输入绑定的域名后,后台会先检查该域名是否已经被其他应用绑定过(同一个域名不可重复绑定多个应用或多个服务),然后会检查域名备案信息。 - -3. 当域名未备案时,会出现绑定失败的提示,这时需要对域名进行备案,可参考[域名备案指南](/sdk/domain/icp/),或者使用其他已备案的域名重新进行绑定。 - -4. 检查域名备案信息通过后,会提示配置 DNS,并且会显示推荐 DNS 配置,以及 CNAME 记录值。当绑定的服务选择云服务 API,推荐的 DNS 配置为 A 记录。当绑定服务选择的是云引擎、文件、公告,推荐的 DNS 配置为 CNAME 记录,并都会提供推荐的记录值。 - - -5. 根据上一步推荐的 DNS 配置到你的域名注册商或域名解析服务提供商处设置域名解析(例如:你的域名是通过腾讯云或者阿里云购买的,这时就需要你通过腾讯云或阿里云管理后台中的域名管理界面进行解析)。上图的第 5 步使用了百度云进行举例,大部分云服务商后台设置域名解析的路径大概均为控制台 -> 域名管理 -> 解析。进入域名解析页面后,按照以下说明添加解析记录即可。 - 1. 主机记录:自定义的字段 + 主域名;例如我设置的自定义域名是 `api.example.com`,所以我只需填 `api` 即可; - 2. 记录类型:根据上步推荐的 DNS 配置选择对应的记录类型;例如:云服务 API 推荐为 A 记录,因此解析时,记录类型也选择 A 记录;推荐 CNAME 为 XXX.XX.cn,因此解析时记录类型选择 CNAME 。 - 3. 记录值:直接复制粘贴推荐的 DNS 配置的记录值。 - 4. 其他解析线路、TTL 都可使用默认配置。 - -大多数域名注册商或域名解析服务提供商都提供图形化的设置界面,这样就不用直接写 DNS Zone 记录,按照其说明配置即可。如下,是通过另一个服务商进行域名解析,并设置 CNAME 记录。 - -![以 DnsPod 添加 CNAME 记录为例](/img/dnspod-add-cname-record.png) - -如果需要写 DNS Zone 记录,以 A 记录为例,假定控制台显示的 IP 为 `0.0.0.0`,那么对应的 DNS Zone 记录为: - -``` -api.example.com. 10800 IN A 0.0.0.0 -``` - -其中 10800 为 TTL,可根据自己的需要设置。 - -再举一个 CNAME 的例子,假定绑定 `xxx.example.com`,控制台显示的 CNAME 目标值为 `yyy.zzz.example`,那么对应的 DNS Zone 记录为: - -``` -xxx.example.com. 10800 IN CNAME yyy.zzz.example. -``` - -6. 以上 5 个步骤设置完成后,需要等待一段时间(一般在半小时以内),记录生效后,LeanCloudTDS 控制台会显示「已绑定」。 - -:::tip -可以使用如下命令验证绑定的域名请求是否正常 -```sh -curl https://{host}/1.1/ping -``` -::: - -如果**长时间卡在「等待配置 DNS」阶段,**那么请点击「等待配置 DNS」后的问号图标,依其提示运行相应的 dig 命令检查域名解析记录是否生效。 -如 dig 命令查不到相应的域名解析记录,请返回域名商控制台检查配置是否正确,如仍有疑问,请联系域名商客服。 -如 dig 命令能查到预期的 CNAME 记录,但控制台仍显示「等待配置 DNS」,请通过工单或论坛联系我们。 - -## SSL 证书 - -在 LeanCloudTDS 控制台绑定自定义域名时,可以选择自动管理 SSL 证书或手动管理 SSL 证书。 -如果选择自动模式,LeanCloudTDS 会自动申请、续期 [Let's Encrypt] 证书。 -如果选择手动模式,则需要上传自己的 SSL 证书(通常是 `.crt` 或 `.pem` 文件)和 SSL 私钥(通常是 `.key` 文件),并在证书过期前自行续期及再次上传。 -SSL 证书通常可以在你的域名服务商处购买,你也可以自行申请免费的证书。 -Let's Encrypt 之外,比较知名的免费 SSL 证书提供商有 [ZeroSSL]、[buypass]、[TrustAsia]。 - -如果设置完成后,显示绑定失败,并且有这个报错提示: - -``` -issueCert for {host}: Authorization not found in HTTP response from {host} ; -``` -因为工信部规定,网站接入多个云服务商时,需要在各云服务商处接入备案。这个报错是缺少对应的云服务商备案所导致的(如果是华东区的应用,很可能是缺少腾讯云备案);可以参考[域名备案指南](/sdk/domain/icp)进行备案操作; - -[let's encrypt]: https://letsencrypt.org/ -[zerossl]: https://zerossl.com/ -[buypass]: https://www.buypass.com/ssl/products/acme -[trustasia]: https://freessl.cn/ - -## 推荐阅读 - -如需了解更多域名的基本知识,可以参考以下两篇文章: - -1. [域名背后那些事](https://blog.taptap.dev/posts/domain-introduction) -2. [域名之殇](https://blog.taptap.dev/posts/domain-problems) - - -## 视频教程 - -可以参考视频教程:[如何绑定域名及如何提交备案](https://www.bilibili.com/video/BV1cu4y1V7mG/)。 - -更多视频教程见[开发者学堂](https://developer.taptap.cn/tds-tutorials/list)。因为 SDK 功能在不断完善,视频教程可能出现与新版 SDK 功能不一致的地方,以当前文档为准。 \ No newline at end of file diff --git a/leancloud/docs/sdk/domain/icp.mdx b/leancloud/docs/sdk/domain/icp.mdx deleted file mode 100644 index 108d49d24..000000000 --- a/leancloud/docs/sdk/domain/icp.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: 域名备案指南 -sidebar_label: 备案 -sidebar_position: 5 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -根据工信部规定,在国内节点绑定云引擎域名和 API 域名需要做备案。 - -## 新增备案 - -如果你的主域名没有备案(没有 ICP 备案号),那么主域名本身及其子域名均无法在控制台绑定。 -你需要先在 LeanCloudTDS 或其他云服务商办理备案。管局审核通过后,方可在 LeanCloudTDS 控制台绑定。 - -如果你的主域名没有备案,我们建议你通过 LeanCloudTDS 新增备案。 -主域名在 LeanCloudTDS 备案后,使用子域名在 LeanCloudTDS 绑定文件域名、云引擎域名、API 域名不需要额外备案或接入备案。 - - - -商用版应用请提交工单,选择 **域名与备案** 下的 **新增备案(企业)** 或 **新增备案(个人)** 分类,按照步骤填写资料,进行新增备案。 - -没有商用版应用的用户,如果不打算升级应用为商用版,可以通过以下方式新增备案: - -1. 如果你同时也是 LeanCloud 底层 IaaS 服务商(华北节点为 UCloud)的用户,那么可以自行在底层 IaaS 服务商新增备案。备案通过后,等价于在 LeanCloud 新增备案。 -2. 如果只需要绑定文件域名,可以在其他云服务商处新增备案,然后直接在 LeanCloud 控制台绑定文件域名,因为绑定文件域名不需要接入备案。 - - - - - -如需新增备案,请提交工单联系我们。 -提交工单时,请选择 **TDS 游戏服务 > ICP 备案 > 网站域名备案 > 新增备案** 分类。 - - - -注意,你也可以选择在其他云服务商处新增备案。 -但对于大部分用户而言,因为需要绑定 API 域名或云引擎域名,在其他云服务商处新增备案之后,仍然需要在 LeanCloudTDS 接入备案。 -这意味着需要重复提交许多类似的资料,经过两次审核周期,由管局先后审核两次,徒然增加时间和精力。 -因此,对于主域名没有备案的用户,我们一般推荐在 LeanCloudTDS 新增备案。 - -## 接入备案 - -工信部规定,网站接入多个云服务商时,需要在各云服务商处接入备案: - -- 接入备案只是在备案信息中新增一个服务商,不会影响之前服务商,可以同时使用。 -- 和新增备案不同,接入备案可以在服务上线后进行,不要求关站或停止解析,不影响当前网站访问。 -- 一般而言,网站使用的所有云服务商皆需备案或接入备案(在使用的第一家云服务商处新增备案,在其他云服务商接入备案)。 - -因此,如果你的主域名之前通过 LeanCloudTDS 新增备案,或者你的主域名已经自行在 LeanCloudTDS 的底层 IaaS 运营商 UCloud (华北节点)、腾讯云(华东节点)办理过新增备案或接入备案,那么不需要再操心接入备案事项。 - -如果你的主域名是通过其他云服务商新增备案,那么你在 LeanCloudTDS 控制台绑定 API 域名或云引擎域名后,需要在 LeanCloudTDS 接入备案。 -由于 CDN 服务一般不需要接入备案,因此,如果该子域名仅用于 LeanCloudTDS 文件域名、公告域名,那么无需在 LeanCloudTDS 接入备案。 - - - 商用版应用 -如需接入备案,请先在 - LeanCloud - - - TDS - 控制台完成相应域名的绑定,然后提交工单,选择 - - {" "} - 域名与备案 下的 新增备案(企业) - 新增备案(个人) - 分类 - - - {" "} - TDS 游戏服务 > ICP 备案 > 网站域名备案 > 备案接入 分类 - -,按照步骤填写资料,进行新增备案。办理接入备案期间域名、服务可以正常使用。 - - - -没有商用版应用的用户,云引擎、API 域名可以先绑定已备案的域名,进行测试开发。在产品正式上线时,升级应用为商用版,通过控制台接入备案。 -或者,如果你同时也是 LeanCloud 底层 IaaS 服务商(华北节点为 UCloud、华东节点为腾讯云)的用户,那么也可以自行在底层 IaaS 服务商接入备案。 - -华东节点底层 IaaS 服务商(腾讯云)暂不支持通过 LeanCloud 以用户主体身份提交备案资料。 -因此,希望在华东节点所在机房做备案接入或直接办理新备案的用户,可以自行创建腾讯云账号并通过腾讯云完成备案。如需授权码,商用版应用用户可提交工单获取。 - - - -## 推荐阅读 - -如需了解更多关于备案的知识,可以参考下面一篇文章: - -- [备案那些事儿](https://blog.taptap.dev/posts/icp-introduction) diff --git a/leancloud/docs/sdk/engine/_category_.json b/leancloud/docs/sdk/engine/_category_.json deleted file mode 100644 index b52074471..000000000 --- a/leancloud/docs/sdk/engine/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "云引擎", - "collapsed": true, - "position": 16 -} diff --git a/leancloud/docs/sdk/engine/_partials/building-advanced.mdx b/leancloud/docs/sdk/engine/_partials/building-advanced.mdx deleted file mode 100644 index 8f8ac891a..000000000 --- a/leancloud/docs/sdk/engine/_partials/building-advanced.mdx +++ /dev/null @@ -1,28 +0,0 @@ -除了默认的构建过程和运行命令外,开发者还可以在 `leanengine.yaml` 中进一步地调整运行命令(`run`)、依赖安装命令(`install`)和构建命令(`build`),覆盖默认的行为: - -```yaml title='leanengine.yaml' -run: echo 'run another command' -install: - - {use: 'default'} - - echo 'install additional dependencies here' -build: - - echo 'overwrite default build command here' -``` - -

    - 详细的说明见 Reference: leanengine.yaml - {props.children ? ',下面是一些具体的例子:' : '。'} -

    - -<>{props.children} - -### 系统级依赖 - -在云引擎的线上环境中,开发者可以在 `leanengine.yaml` 中定义额外的系统级依赖: - -```yaml title='leanengine.yaml' -systemDependencies: - - imagemagick -``` - -支持的完整列表见 [Reference: leanengine.yaml](/sdk/engine/deep-dive/leanengine-yaml#systemdependencies-系统级依赖)。 diff --git a/leancloud/docs/sdk/engine/_partials/building-build-logs.mdx b/leancloud/docs/sdk/engine/_partials/building-build-logs.mdx deleted file mode 100644 index 4e9054ada..000000000 --- a/leancloud/docs/sdk/engine/_partials/building-build-logs.mdx +++ /dev/null @@ -1,3 +0,0 @@ -默认情况下构建过程中产生的日志不会显示到控制台,只有构建失败时,最后一个步骤的日志才会被显示在控制台上。 - -如需打印完整的构建日志以便调试,可以在部署时勾选「打印构建日志」或命令行工具添加参数 `--options 'printBuildLogs=true'`。 diff --git a/leancloud/docs/sdk/engine/_partials/cloud-custom-domain.mdx b/leancloud/docs/sdk/engine/_partials/cloud-custom-domain.mdx deleted file mode 100644 index f32d9626e..000000000 --- a/leancloud/docs/sdk/engine/_partials/cloud-custom-domain.mdx +++ /dev/null @@ -1,17 +0,0 @@ -import { HAS_SUB_DOMAIN } from "/src/constants/env.ts"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -云引擎需要设置域名才能访问。在 ** > 管理部署 > 你的分组 > 设置 > 访问域名** 处可以绑定域名。 - - - -如果你绑定的域名以 `stg-` 开头(如 `stg-api.example.com`),会自动关联到预备环境。 - - - - - -对于测试阶段的应用,我们提供了共享域名,你可以自定义共享域名的前缀部分。 - - diff --git a/leancloud/docs/sdk/engine/_partials/cloud-environments.mdx b/leancloud/docs/sdk/engine/_partials/cloud-environments.mdx deleted file mode 100644 index 01856582c..000000000 --- a/leancloud/docs/sdk/engine/_partials/cloud-environments.mdx +++ /dev/null @@ -1,23 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -云引擎平台默认提供下列环境变量供应用使用: - -| 变量名 | 说明 | -| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `LEANCLOUD_APP_ID` | 当前应用的 `App ID`。 | -| `LEANCLOUD_APP_KEY` | 当前应用的 `App Key`。 | -| `LEANCLOUD_APP_MASTER_KEY` | 当前应用的 `Master Key`。 | -| `LEANCLOUD_APP_ENV` | 当前的应用环境:开发环境没有该环境变量,或值为 `development`(通过命令行工具启动)。预备环境值为 `stage`。生产环境值为 `production`。 | -| `LEANCLOUD_APP_PORT` | 当前应用开放给外网的端口,只有监听此端口,用户才可以访问到你的服务。 | -| `LEANCLOUD_API_SERVER` | 访问存储服务时使用的地址。该值会因为所在数据中心等原因导致不一样,所以使用 REST API 请求存储服务或其他云服务时请使用此环境变量的值。 | -| `LEANCLOUD_APP_GROUP` | 云引擎实例所在的组。当使用云引擎组管理功能时,该值为组的名称。 | -| `LEANCLOUD_REGION` | 云引擎服务所在区域,值为 `CN` 或 `US`,分别表示国内版和国际版。 | -| `LEANCLOUD_VERSION_TAG` | 云引擎实例部署的版本号。 | - - - -旧版云引擎使用的以 `LC_` 开头的环境变量(如 `LC_APP_ID`)已经被弃用。为了保证代码兼容性,`LC_` 变量在一段时间内依然有效,但未来可能会完全失效。为了避免报错,建议使用 `LEANCLOUD_` 变量来替换。 - - - -你还可以在控制台上设置自定义的环境变量来存储配置信息。 diff --git a/leancloud/docs/sdk/engine/_partials/cloud-filesystem.mdx b/leancloud/docs/sdk/engine/_partials/cloud-filesystem.mdx deleted file mode 100644 index 99c1c227b..000000000 --- a/leancloud/docs/sdk/engine/_partials/cloud-filesystem.mdx +++ /dev/null @@ -1,7 +0,0 @@ -你可以向 `/home/leanengine` 或 `/tmp` 目录写入临时文件,最多不能超过 1 GB。 - -:::caution -云引擎每次部署都会产生一个新的容器,即使不部署系统偶尔也会进行一些自动调度,这意味着你 **不能将本地文件系统当作持久的存储**,只能用作临时存储。 -::: - -如果你写入的文件体积较大,建议在使用后自动删除他们,否则如果占用磁盘空间超过 1 GB,继续写入文件可能会收到类似 `Disk quota exceeded` 的错误,这种情况下你可以重新部署一下,这样文件就会被清空了。 diff --git a/leancloud/docs/sdk/engine/_partials/cloud-health-check.mdx b/leancloud/docs/sdk/engine/_partials/cloud-health-check.mdx deleted file mode 100644 index f1ea5cbd9..000000000 --- a/leancloud/docs/sdk/engine/_partials/cloud-health-check.mdx +++ /dev/null @@ -1,27 +0,0 @@ -import Path from "/src/docComponents/path"; - -云引擎目前主要为 Web 应用优化,应用在启动后需要在环境变量 `LEANCLOUD_APP_PORT` 中指定的端口上提供 HTTP 服务,注意需要监听在 `0.0.0.0` 地址(所有接口)上,而不是一些框架默认的 `127.0.0.1`。 - -在应用部署时,云引擎的管理程序会每隔一秒去检查应用是否启动成功,如果超过启动时间限制(默认 30 秒)仍未启动成功,即认为启动失败,部署会中止。在之后的运行过程中,也会有定期的健康检查来确保应用正常运行,如果健康检查失败,云引擎管理程序会自动重启你的应用。 - -健康检查会通过 HTTP 检查应用的首页(`/`),如果返回 HTTP 2xx 的响应,就视作成功。 - -
    -点击展开健康检查与云引擎 SDK 的关联 - -云引擎还会尝试检查由 SDK 处理的 `/__engine/1/ping`,如果 SDK 接入正确,便不再要求首页(`/`)返回 HTTP 2xx。 - -如果 ** > 管理部署 > 你的分组 > 设置 > 云函数模式** 设置为「开启」或 `leanengine.yaml` 中 `functionsMode` 设置为 `strict`,云引擎会检查 SDK 是否被正确地接入,否则会视作启动失败。 - -
    - -
    -点击展开自定义启动时长(startupTimeout - -启动时间限制默认为 30 秒,可设置范围为 15–120 秒,如需延长或缩短,可以在 `leanengine.yaml` 文件中设置: - -```yaml title='leanengine.yaml' -startupTimeout: 60 -``` - -
    diff --git a/leancloud/docs/sdk/engine/_partials/cloud-internet-address.mdx b/leancloud/docs/sdk/engine/_partials/cloud-internet-address.mdx deleted file mode 100644 index 0c88542b1..000000000 --- a/leancloud/docs/sdk/engine/_partials/cloud-internet-address.mdx +++ /dev/null @@ -1,16 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -如果开发者希望在第三方服务平台(如微信开放平台)上配置 IP 白名单而需要获取云引擎的入口或出口 IP 地址,请进入 ** > 管理部署 > 云引擎分组 > 设置 > 出入口 IP** 来自助查询。 - - - -:::info -中国大陆节点的云引擎应用会默认启用加速节点,取决于底层的供应商,入口 IP 将会非常频繁地变动。如果确实需要固定入口 IP,可以开通独立 IP。 -::: - - - -我们会尽可能减少出入口 IP 的变化频率,但 IP 突然变换的可能性仍然存在。因此在遇到与出入口 IP 相关的问题,我们建议先进入控制台来核实一下 IP 列表是否有变化。 - -如需保持入口 IP 不变,建议为云引擎绑定独立 IP。 diff --git a/leancloud/docs/sdk/engine/_partials/cloud-load-balancer.mdx b/leancloud/docs/sdk/engine/_partials/cloud-load-balancer.mdx deleted file mode 100644 index 2b8432690..000000000 --- a/leancloud/docs/sdk/engine/_partials/cloud-load-balancer.mdx +++ /dev/null @@ -1,154 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; -import { HAS_ENGINE_CDN_DOMAIN } from "/src/constants/env.ts"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; - -所有对云引擎的 HTTP 或 HTTPS 请求都会经过负载均衡,负载均衡组件会处理 HTTPS 加密、重定向到 HTTPS、对响应进行压缩等一般性工作,因此云引擎上的程序不需要自己实现这些功能。同时负载均衡带来的一些限制,在云引擎程序内进行修改也无法越过,如: - -- `/.well-known/acme-challenge/` 开头的路径被用于自动管理证书,不会转发到云引擎程序。 -- 请求头(URL 和 header)每行最大 8K,总计最大 64K。 -- 请求体积(上传文件体积)最大 100M。 -- 连接或等待响应的超时时间为 60 秒。 - - - -#### 获取客户端 IP 等信息 - -云引擎的负载均衡会在 HTTP header 中传递一些有关原始请求的信息: - -- `X-Real-IP`: 请求的来源 IP。 -- `X-Forwarded-Proto`: 请求的来源协议(`http` 或 `https`)。 -- `Forwarded`: RFC 7239 规定的用于传递代理信息的头,包含 IP 和 协议。 - - - -:::caution -在使用加速节点的情况下,以上的 HTTP header 中给出的实际上是加速节点的信息,而非原始请求信息。 -::: - -在使用加速节点的情况下,还会有这些 HTTP header: - -- `X-Forwarded-For`: 逗号隔开的多个 IP,其中第一个是原始请求 IP。 - -:::caution -以上 HTTP 头中给出的信息并不可靠,云引擎无法确认其真实性,存在被伪造的可能。 -::: - - - - - - -在 Express 中: - -```js -app.get("/", function (req, res) { - console.log(req.headers["x-real-ip"]); - res.send(req.headers["x-real-ip"]); -}); -``` - - - - -Python(Flask): - -```python -from flask import Flask -from flask import request - -app = Flask(__name__) - -@app.route('/') -def index(): - print(request.headers['x-real-ip']) - return 'ok' -``` - -Python(Django): - -```python -def index(request): - print(request.META['HTTP_X_REAL_IP']) - return render(request, 'index.html', {}) -``` - - - - -```php -$app->get('/', function($req, $res) { - error_log($_SERVER['HTTP_X_REAL_IP']); - return $res; -}); -``` - - - - -```java -EngineRequestContext.getRemoteAddress(); -``` - - - - -Go(Echo): - -```go -func fetchRealIP(c echo.Context) error { - realIP = c.RealIP() - //... -} -``` - - - - - - -:::info -中国大陆节点的云引擎应用会默认启用加速节点,如果确实需要准确的原始请求 IP,可以开通独立 IP 来绕过加速节点,更多关于加速节点与独立 IP 的区别见 [域名绑定指南 § 云引擎域名](/sdk/domain/guide/#云引擎域名)。 -::: - - - - -#### 重定向到 HTTPS - -在绑定云引擎自定义域名时,可以选择「强制 HTTPS」,勾选后负载均衡组件会将 HTTP 的请求重定向到 HTTPS 的同一路径。 - - - -:::caution -在使用加速节点的情况下,「强制 HTTPS」选项无法正确工作,仍需 [在项目代码层面实现重定向](/sdk/engine/functions/sdk/#如何使用-sdk-重定向到-https)。 -::: - - - - - -#### 加速节点缓存 - -如果你将自定义域名解析到加速节点(也包括云引擎的共享域名),那么加速节点会对请求进行缓存,加速节点会有一些默认的缓存规则。 - -默认会缓存的情况: - -- 响应头中有 `Last-Modified`(通常是静态资源,其中 HTML 最多缓存 60 秒)。 - -不会缓存的情况: - -- 出错的响应(非 2xx)。 -- 非幂等请求(如 `POST`)。 -- 响应头中没有 `Last-Modified`(通常是动态接口)。 - -默认的缓存时长取决于文件类型和 `Last-Modified`(越不常修改的文件缓存越久),你可以通过自行设置 `Cache-Control` 来覆盖默认的行为,边缘节点会尽可能遵守其中的要求,比如: - -- 设置 `Cache-Control: no-cache` 来避免响应被缓存。 -- 设置 `Cache-Control: max-age=3600` 来设置缓存时长(一小时)。 - -:::info -如果希望完全避免被缓存机制影响,可以开通独立 IP 来绕过加速节点,更多关于加速节点与独立 IP 的区别见 [域名绑定指南 § 云引擎域名](/sdk/domain/guide/#云引擎域名)。 -::: - - diff --git a/leancloud/docs/sdk/engine/_partials/cloud-logs.mdx b/leancloud/docs/sdk/engine/_partials/cloud-logs.mdx deleted file mode 100644 index 02f94d3e3..000000000 --- a/leancloud/docs/sdk/engine/_partials/cloud-logs.mdx +++ /dev/null @@ -1,52 +0,0 @@ -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; - -:::note -关于如何在控制台上查看日志,以及访问日志等更多内容,请看 [云引擎平台功能 § 查看日志](/sdk/engine/platform/#查看日志)。 -::: - -云引擎会收集应用打印到标准输出(stdout)和标准错误输出(stderr)的日志: - - - - -```js -console.log("hello"); // stdout -console.error("some error"); // stderr -``` - - - - -```python -import sys - -print('hello') # stdout -print('some error', file=sys.stderr) # stderr -``` - -
    -点击展开 Python 2 打印日志的例子 - -```python -import sys - -print 'hello' # stdout -print >> sys.stderr, 'some error' # stderr -``` - -
    - -
    - - -```php -error_log("some error"); -``` - - -
    - -:::note -日志单行最大 4096 个字符,多余部分会被丢弃;日志收集速率最大 600 行每分钟,多余的部分会也被丢弃。 -::: diff --git a/leancloud/docs/sdk/engine/_partials/cloud-timezone.mdx b/leancloud/docs/sdk/engine/_partials/cloud-timezone.mdx deleted file mode 100644 index 73bae44de..000000000 --- a/leancloud/docs/sdk/engine/_partials/cloud-timezone.mdx +++ /dev/null @@ -1,13 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - - - -云引擎使用北京时间(东八区)。 - - - - - -云引擎使用 UTC+0 时区。 - - diff --git a/leancloud/docs/sdk/engine/_partials/functions-introduction.mdx b/leancloud/docs/sdk/engine/_partials/functions-introduction.mdx deleted file mode 100644 index 2e79fbc86..000000000 --- a/leancloud/docs/sdk/engine/_partials/functions-introduction.mdx +++ /dev/null @@ -1,13 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -云函数是云引擎提供的一种经过高度封装的函数计算功能,在我们的各个客户端 SDK 中也有对应的支持,可以自动地序列化 [数据存储](/sdk/storage/features)[数据存储](/sdk/storage/overview/) 服务中的各种数据类型。 - -适合使用云函数和 Hook 的场景包括: - -- 将跨平台应用(同时有 Android、iOS、浏览器客户端)中复杂的计算逻辑集中到一处,而不必每个客户端单独实现一遍。 -- 需要在服务器端对一些逻辑进行灵活调整,而不必更新客户端。 -- 需要越过 ACL 或表权限的限制,对数据进行查询或修改。 -- 需要使用 Hook 在数据存储中的对象被创建、更新、删除,或用户登录、认证时,触发自定义的逻辑、进行额外的权限检查。 -- 需要运行定时任务,如每小时关闭未支付的订单、每天凌晨运行过期数据的清理任务等。 - -你可以使用云引擎支持的所有语言(运行环境)来编写云函数,包括 Node.js、Python、Java、PHP、.NET 和 Go。其中 Node.js 支持在控制台上在线编辑,其他语言需基于我们的示例项目部署到云引擎。 diff --git a/leancloud/docs/sdk/engine/_partials/leandb-cli-access.mdx b/leancloud/docs/sdk/engine/_partials/leandb-cli-access.mdx deleted file mode 100644 index 4665defe5..000000000 --- a/leancloud/docs/sdk/engine/_partials/leandb-cli-access.mdx +++ /dev/null @@ -1,43 +0,0 @@ -import { Command } from "/src/docComponents/engine"; -import { CLI_BINARY } from "/src/constants/env.ts"; -import CodeBlock from "@theme/CodeBlock"; - -使用 [命令行工具 CLI](/sdk/engine/cli) 提供的 可以打开一个连接到云端 LeanDB 的交互式 shell,用于执行查询: - - - {`$ ${CLI_BINARY} db shell mysqldb -Welcome to the MySQL monitor. -Your MySQL connection id is 3450564 -Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement. -mysql> use test -Database changed -mysql> insert into users set name = 'leancloud'; -Query OK, 1 row affected (0.04 sec) -mysql> select * from users; -+------+-----------+ -| id | name | -+------+-----------+ -| 1 | zhenzhen | -| 2 | leancloud | -+------+-----------+ -2 rows in set (0.06 sec)`} - - -使用 可以将云端 LeanDB 导出到本地的一个端口,供本地的程序或图形化的数据库客户端连接: - - - {`$ ${CLI_BINARY} db proxy myredis -[INFO] Now, you can connect myredis via [redis-cli -h 127.0.0.1 -a hsdt9wIAuKcTZmpg -p 5678]`} - - -保持这个终端运行(不要关闭),就可以在本地的 5678 端口访问到云端的 LeanDB 了。你可以使用本地的 GUI 客户端来浏览操作云端的 LeanDB。在使用 进行开发测试时,也可以使用这个功能连接到云端的 LeanDB,设置环境变量(来自前面 的输出): - -```shell -export REDIS_URL_myredis=redis://default:hsdt9wIAuKcTZmpg@127.0.0.1:5678 -``` - -:::note - - 命令访问云端 LeanDB 实例仅用于本地开发和测试,连接会偶尔断开(部分客户端可以自动重连),请不要用于生产环境。 - -::: diff --git a/leancloud/docs/sdk/engine/_partials/leandb-create-instance.mdx b/leancloud/docs/sdk/engine/_partials/leandb-create-instance.mdx deleted file mode 100644 index db697683c..000000000 --- a/leancloud/docs/sdk/engine/_partials/leandb-create-instance.mdx +++ /dev/null @@ -1,41 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -:::caution - -<> - {props.instanceName || "LeanDB"}{" "} - 实例一旦创建就会开始计费,如余额不足可能会导致账号因欠费被停用。 - - -::: - -点击 **创建实例** 可在控制台中看到一些配置项: - -<>{props.children} - -在调整好规格后,可以在控制台上看到当前规格的价格。 - -
    -点击展开 {props.instanceName || 'LeanDB'} 计费详情 - -

    - {props.instanceName || "LeanDB"}{" "} - 按天进行扣费,使用时间不足一天也按一天计费,每天扣除前一天的费用。 - {props.instanceName || "LeanDB"} 基于开发者选择的实例规格进行计费,与实际用量无关(即使创建后未使用也会计费)。各规格的价格可以在创建{" "} - {props.instanceName || "LeanDB"} 时或 - 当前节点的 - 价格页面查看,扣费记录可在 - 云服务控制台的消费明细 - 开发者中心的账单中查看。 -

    - -:::danger - -<> - 如账号欠费停用超过一个月,{props.instanceName || "LeanDB"}{" "} - 及其中的数据会被彻底删除。 - - -::: - -
    diff --git a/leancloud/docs/sdk/engine/_partials/nodejs-setup-dependencies.mdx b/leancloud/docs/sdk/engine/_partials/nodejs-setup-dependencies.mdx deleted file mode 100644 index c62896a88..000000000 --- a/leancloud/docs/sdk/engine/_partials/nodejs-setup-dependencies.mdx +++ /dev/null @@ -1,21 +0,0 @@ -云引擎会自动安装 `package.json` 中的依赖: - -```json title='package.json' -{ - "dependencies": { - "leancloud-storage": "^3.11.0", - "leanengine": "^3.3.2" - }, - "devDependencies": { - "nodemon": "^1.18.7" - } -} -``` - -在安装依赖的过程中,云引擎会正常触发 NPM 的生命周期脚本([Life Cycle Scripts](https://docs.npmjs.com/cli/v8/using-npm/scripts#life-cycle-scripts)),如 `postinstall`、`prepare` 等。 - -因为云引擎会在云端安装依赖,所以命令行工具默认也不会上传 `node_modules` 目录;如果使用 Git 部署,也建议将 `node_modules` 目录添加到 `.gitignore` 中,使其不加入版本控制。 - -:::note -云引擎会上传 `.yarn` 文件夹,所以如果启用了 Yarn 2+ 的 [PnP(Plug'n'Play)](https://yarnpkg.com/features/pnp)但不想使用 [Zero-installs](https://yarnpkg.com/features/caching#zero-installs),请将 `.yarn/cache` 加入到 `.gitignore` 和 `.leanignore` 中 -::: diff --git a/leancloud/docs/sdk/engine/_partials/nodejs-setup-package-mamager.mdx b/leancloud/docs/sdk/engine/_partials/nodejs-setup-package-mamager.mdx deleted file mode 100644 index c7ad176eb..000000000 --- a/leancloud/docs/sdk/engine/_partials/nodejs-setup-package-mamager.mdx +++ /dev/null @@ -1,57 +0,0 @@ -云引擎目前支持以下包管理器: - -- [npm](https://docs.npmjs.com/cli/v10/commands/npm) -- [pnpm](https://pnpm.io) -- [Yarn 1](https://classic.yarnpkg.com) -- [Yarn 2+](https://yarnpkg.com) - -云引擎会按照以下条件使用包管理器: - -| 包管理器 | 条件 | 版本 | -| -------- | --------------------------------------------------------- | --------------------------- | -| pnpm | 存在合法能被解析的 `pnpm-lock.yaml` | | -| | `lockfileVersion: '6.0'` 或更高 | 8 | -| | `lockfileVersion: 5.3` 或更高 | 7 | -| | 其他 | 6 | -| Yarn 1 | 存在 `yarn.lock` | 1 | -| Yarn 2+ | 不默认支持,需通过 [Corepack](#实验性-corepack-支持) 启用 | 2+ | -| npm | 其他情况 | 使用 Node.js 默认提供的 npm | - -### 实验性 Corepack 支持 - -**由于 Corepack 还是实验性特性,云引擎不能保证对 Corepack 的支持是稳定的** - -通过给分组设置 `ENABLE_EXPERIMENTAL_COREPACK` 环境变量为任意非空字符串来启用实验性 Corepack 支持。 - -云引擎会通过调用 Corepack 读取 `package.json` 里的 `packageManager` 字段来自动识别、使用用户指定的包管理器,这也是目前唯一一种使用 Yarn 2+ 的方式。 - -假设有以下 `package.json`: - -```json title="package.json" -{ - "name": "example", - "packageManager": "yarn@4.0.2" -} -``` - -此时云引擎会自动调用 `corepack prepare --activate` 并识别包管理器为 Yarn 2+。 - -参考:[Corepack](https://nodejs.org/api/corepack.html) - -### 默认命令 - -云引擎默认运行的脚本会随着包管理器的变化而变化,如使用了 pnpm, `npm ci` 会变成 `pnpm install --frozen-lockfile`。 - -云引擎只有在没有指定 `installDevDependencies` 为 `true` 且构建脚本为空(没有手动指定,`package.json` 里的 `scripts.build` 不存在)时才会省略 devDependencies 的安装。 - -| 阶段 | 包管理器 | 条件 | 命令 | -| ------- | -------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| install | npm | Node.js 10 以上且存在 `package-lock.json` 或 `npm-shrinkwrap.json` | `npm ci` | -| | | | `npm install` 或 `npm install --omit=dev` | -| | pnpm | | `pnpm install --frozen-lockfile` 或 `pnpm install --frozen-lockfile --prod` | -| | Yarn 1 | | `yarn install` 或 `yarn install --production` | -| | Yarn 2+ | | `yarn install` | - -:::note -请注意 Yarn 1 只会使用 `yarn.lock` 内解析的 URL 下载依赖且不会遵循用户设置的源,请选择合适的源,否则可能会增加构建时间。 -::: diff --git a/leancloud/docs/sdk/engine/_partials/nodejs-setup-runtime.mdx b/leancloud/docs/sdk/engine/_partials/nodejs-setup-runtime.mdx deleted file mode 100644 index c45ecacdf..000000000 --- a/leancloud/docs/sdk/engine/_partials/nodejs-setup-runtime.mdx +++ /dev/null @@ -1,23 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -在 `package.json` 的 `engines.node` 字段可以指定 Node.js 版本: - -```json title='package.json' -{ - "engines": { - "node": "16.x" - } -} -``` - -你还可以设置为 `*` 表示总是使用最新(current)版本。 - -:::note - -对于新创建的应用,如未设置 Node.js -版本,云引擎会默认使用最新的稳定(LTS)版本。 - - 在 2021-09-02 之前创建的分组因兼容考虑会默认使用 `0.12` 版本。 - - -::: diff --git a/leancloud/docs/sdk/engine/_partials/platform-introduction.mdx b/leancloud/docs/sdk/engine/_partials/platform-introduction.mdx deleted file mode 100644 index 3818602e0..000000000 --- a/leancloud/docs/sdk/engine/_partials/platform-introduction.mdx +++ /dev/null @@ -1 +0,0 @@ -云引擎是一个托管后端程序的平台,开发者可以将 Web 应用(例如一个网站),或者 Node.js、Python、Java、PHP、.NET、Go、C++ 等语言的后端程序(例如一个 RESTful API 服务器)部署到云引擎上,云引擎会自动从源代码构建出可运行的「版本」,然后将它运行在独立的容器中,同时提供日志和监控、负载均衡、平滑发布、弹性扩容等能力。此外,云引擎还提供了定时任务、域名和证书管理和 Redis、MySQL、MongoDB、Elasticsearch 等多种托管数据库供开发者使用。 diff --git a/leancloud/docs/sdk/engine/_partials/platform-runtimes.mdx b/leancloud/docs/sdk/engine/_partials/platform-runtimes.mdx deleted file mode 100644 index 320a6a63a..000000000 --- a/leancloud/docs/sdk/engine/_partials/platform-runtimes.mdx +++ /dev/null @@ -1,8 +0,0 @@ -- [Web 应用运行环境](/sdk/engine/deploy/webapp) -- [Node.js 运行环境](/sdk/engine/deploy/nodejs) -- [Python 运行环境](/sdk/engine/deploy/python) -- [Java 运行环境](/sdk/engine/deploy/java) -- [PHP 运行环境](/sdk/engine/deploy/php) -- [.NET 运行环境](/sdk/engine/deploy/dotnet) -- [Go 运行环境](/sdk/engine/deploy/go) -- [C++ 运行环境](/sdk/engine/deploy/cpp) diff --git a/leancloud/docs/sdk/engine/_partials/quick-start-deploy.mdx b/leancloud/docs/sdk/engine/_partials/quick-start-deploy.mdx deleted file mode 100644 index a395aeba4..000000000 --- a/leancloud/docs/sdk/engine/_partials/quick-start-deploy.mdx +++ /dev/null @@ -1,6 +0,0 @@ -import { CLI_BINARY } from "/src/constants/env.ts"; -import CodeBlock from "@theme/CodeBlock"; - -直接部署到生产环境: - -{`${CLI_BINARY} deploy --prod`} diff --git a/leancloud/docs/sdk/engine/_partials/quick-start-new.mdx b/leancloud/docs/sdk/engine/_partials/quick-start-new.mdx deleted file mode 100644 index 18be0bf3d..000000000 --- a/leancloud/docs/sdk/engine/_partials/quick-start-new.mdx +++ /dev/null @@ -1,88 +0,0 @@ -import { CLI_BINARY } from "/src/constants/env.ts"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import { Conditional } from "/src/docComponents/conditional"; -import { Command } from "/src/docComponents/engine"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; - - - -请先根据 [命令行工具使用指南 § 安装](/sdk/engine/cli/#安装) 安装最新版本的命令行工具,然后根据 [命令行工具使用指南 § 登录账号](/sdk/engine/cli/#登录账号) 登录到你的账号。 - - - -如果你还没有在控制台创建过应用,请先在控制台创建应用,然后使用 创建项目: - - - {`$ ${CLI_BINARY} new ${props.appName || "my-engine-app"} -[?] Please select an app template: - 1) Node.js - Express - 2) Node.js - Koa - 3) Python - Flask - 4) Python - Django - 5) Java - Servlet - 6) Java - Spring Boot - 7) PHP - Slim - 8) .NET Core - 9) Go - Echo - 10) React Web App (via create-react-app) - 11) Vue Web App (via @vue/cli) - => 1 -[?] Please select an app: - 1) ${props.appName || "my-engine-app"} - => 1 -[INFO] Downloading templates 7.71 KiB / 7.71 KiB [==================] 100.00% 0s -[INFO] Creating project... -[INFO] Created Node.js - Express project in \`${ - props.appName || "my-engine-app" - }\` -[INFO] Lean how to use Express at https://expressjs.com`} - - -

    - 会使用你提供的名字创建一个目录,我们{" "} - cd {props.appName || "my-engine-app"} 然后安装项目依赖: -

    - - - - -```sh -npm install -``` - - - - -```sh -pip install -Ur requirements.txt -``` - - - - -```sh -composer install -``` - - - - -```sh -mvn package -``` - - - - -需要安装 global.json 文件中指定的 .NET SDK 版本。 - - - - -```sh -go mod tidy -``` - - - diff --git a/leancloud/docs/sdk/engine/cli.mdx b/leancloud/docs/sdk/engine/cli.mdx deleted file mode 100644 index 9c9baaca6..000000000 --- a/leancloud/docs/sdk/engine/cli.mdx +++ /dev/null @@ -1,610 +0,0 @@ ---- -title: 命令行工具 CLI 使用指南 -sidebar_label: 命令行工具 -sidebar_position: 8 ---- - -import CodeBlock from "@theme/CodeBlock"; -import LeandbCliAccess from "./_partials/leandb-cli-access.mdx"; -import QuickStartNew from "./_partials/quick-start-new.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import { Command } from "/src/docComponents/engine"; -import { CLI_BINARY, BRAND } from "/src/constants/env.ts"; -import Path from "/src/docComponents/path"; - -命令行工具()是用来部署云引擎应用和进行其他管理操作的客户端工具。 - -## 安装 - -### macOS - -推荐通过 [Homebrew](https://brew.sh/) 安装: - -```sh -brew update && brew install lean-cli -``` - -
    -点击展开 Homebrew 安装常见问题 - -如访问 Homebrew 网络不畅,可以 [设置 `http_proxy` 等环境变量来加速访问](https://docs.brew.sh/Manpage#using-homebrew-behind-a-proxy),或为 Homebrew 配置镜像源(如 [TUNA](https://mirror.tuna.tsinghua.edu.cn/help/homebrew/))。 - -如不希望通过 Homebrew 安装,可以在 [GitHub releases 页面] 下载二进制文件 (Apple Silicon)或 (Intel),重命名为 后移动到 `$PATH` 下的路径,并添加可执行权限()。 - -
    - -### Windows - -Windows 用户可以在 [GitHub releases 页面] 根据操作系统版本下载最新的 32 位 或 64 位 **msi** 安装包进行安装,安装成功之后在 Windows 命令提示符(或 PowerShell)下直接输入 命令即可使用。 - -也可以选择编译好的绿色版 **exe** 文件,下载后将此文件更名为 ,并将其路径加入到系统 **PATH** 环境变量([设置方法](https://www.java.com/zh-CN/download/help/path.html))中去。这样使用时在 Windows 命令提示符(或 PowerShell)下,在任意目录下输入 就可以使用命令行工具了。当然也可以将此文件直接放到已经在 PATH 环境变量中声明的任意目录中去,比如 `C:\Windows\System32` 中。 - -### Linux - -基于 Debian 的发行版可以从 [GitHub releases 页面] 下载 deb 包安装。 - -其他发行版可以从 [GitHub releases 页面] 下载预编译好的二进制文件(如 ),重命名为 后移动到 `$PATH` 下的路径,并添加可执行权限()。 - -[github releases 页面]: https://releases.leanapp.cn/#/leancloud/lean-cli/releases - -### 升级版本 - -下载最新的文件,重新执行一遍安装流程,即可把旧版本的命令行工具覆盖,升级到最新版。 - -## 命令介绍 - -安装成功之后,直接在 terminal 终端运行 ,输出帮助信息: - -
    -点击展开 的输出 - - - -``` -NAME: - lean - Command line tool to manage and deploy LeanEngine apps - -USAGE: - lean [global options] command [command options] [arguments...] - -VERSION: - 1.0.0 - -COMMANDS: - login Log in to LeanCloud - switch Change the associated LeanEngine app - info Show information about the associated user and app - up Start a development instance locally with debug console - new Create a new LeanEngine project from official examples - deploy Deploy the project to LeanEngine - publish Publish the version of staging to production - db Access to to LeanDB instances - file Manage files ('_File' class in Data Storage) - logs Show LeanEngine logs - debug Start the debug console without running the project - env Print custom environment variables on LeanEngine (secret variables not included) - cql Enter CQL interactive shell (warn: CQL is deprecated) - help Show usages of all subcommands - -GLOBAL OPTIONS: - --version, -v print the version -``` - - - - -``` -NAME: - tds - Command line tool to manage and deploy Cloud Engine apps - -USAGE: - tds [global options] command [command options] [arguments...] - -VERSION: - 1.0.0 - -COMMANDS: - login Log in to TapTap Developer Services - switch Change the associated Cloud Engine app - info Show information about the associated user and app - up Start a development instance locally with debug console - new Create a new Cloud Engine project from official examples - deploy Deploy the project to Cloud Engine - publish Publish the version of staging to production - db Access to to Database instances - file Manage files ('_File' class in Data Storage) - logs Show Cloud Engine logs - debug Start the debug console without running the project - env Print custom environment variables on Cloud Engine (secret variables not included) - cql Enter CQL interactive shell (warn: CQL is deprecated) - help Show usages of all subcommands - -GLOBAL OPTIONS: - --version, -v print the version -``` - - - -
    - -可以通过 `--version` 选项查看命令行工具的版本: - - - {`$ ${CLI_BINARY} --version -${CLI_BINARY} version 1.0.0`} - - -简单介绍下主要的子命令: - -| 命令 | 用途 | -| --------- | ------------------------------------------------------------------------ | -| `login` | 登录账号 | -| `switch` | 切换关联的云引擎应用和分组 | -| `info` | 显示当前应用和分组信息 | -| `up` | 启动本地开发调试 | -| `new` | 从项目模板创建新项目 | -| `deploy` | 部署项目至云引擎 | -| `publish` | 将预备环境的版本发布至生产环境 | -| `db` | 访问云端的 LeanCache 或 LeanDB | -| `file` | 上传文件至数据存储服务(可以在 ** > 文件** 中查看) | -| `logs` | 显示云引擎日志 | -| `debug` | 单独启动云函数调试控制台(不运行应用本身) | -| `env` | 显示或设置当前项目的环境变量 | - -用 可以进一步了解每个子命令的用法,例如: - -
    -点击展开 的输出 - - - {`NAME: - ${CLI_BINARY} deploy - Deploy the project to ${ - BRAND === "leancloud" ? "LeanEngine" : "Cloud Engine" - }\n -USAGE: - tds deploy [command options] (--prod | --staging) [--no-cache --build-logs --overwrite-functions]\n -OPTIONS: - --prod Deploy to production environment - --staging Deploy to staging environment - --build-logs Print build logs - -g Deploy from git repo - --war Deploy .war file for Java project. The first .war file in target/ is used by default - --no-cache Disable buliding cache - --overwrite-functions Overwrite cloud functions with the same name in other groups - --leanignore value Rule file for ignored files in deployment (default: ".leanignore") - --message value, -m value Comment for this version, only applicable when deploying from local files - --keep-deploy-file - --revision value, -r value Git revision or branch. Only applicable when deploying from Git (default: "master") - --options --options build-root=app Send additional deploy options to server, in urlencode format(like --options build-root=app) - --direct Upload project's tarball to remote directly`} - - -
    - -## 登录账号 - -安装完命令行工具之后,首先第一步需要登录云服务账号。 - - - -请进入开发者后台,点击左侧「创建游戏」按照需要填写基础信息和基础游戏资料,然后进入对应的游戏,依次进入**游戏服务 > 云服务 > 云引擎 > 开启 > 部署项目 > 命令行工具部署**,按照指引登录你的云服务账号。 - - - -如要切换到另一账号,重新执行 即可。 - -## 初始化项目 - -登录完成之后,可以使用 命令来初始化一个项目,并且关联到已有的云服务应用上。 - - - -## 关联应用和分组 - -命令行工具的大部分操作都是针对关联的应用进行的,使用 可以将已有的项目关联到云端的应用: - -{`${CLI_BINARY} switch`} - -如应用中有多个分组,则会需要你选择一个分组。 - -如需关联项目到其他应用,可以重新运行 。 - -另外还可以直接执行 来快速切换关联应用。 - -使用 可以查看当前项目关联的应用。 - -## 本地运行调试 - -在项目根目录运行: - -{`${CLI_BINARY} up`} - -即可开始本地调试,命令行工具会在启动你的应用同时启动一个云函数调试控制台。 - -- 在浏览器中打开 ,进入 web 应用的首页。 -- 在浏览器中打开 ,进入云引擎云函数和 Hook 函数调试控制台。 - -如果想变更启动端口号,可以使用 命令来指定。 - -命令行工具并不负责自动重启或热加载,需要由项目代码本身来实现,我们部分的示例项目提供了自动重启的能力(如 Node.js)。 - -除了使用命令行工具来启动项目之外,还可以**原生地**启动项目,比如直接使用 `node server.js` 或者 `python wsgi.py`。这样能够将云引擎开发流程更好地集成到开发者惯用的工作流程中,也可以直接和 IDE 集成。但是直接使用命令行工具创建的云引擎项目,默认会依赖一些环境变量,因此需要提前设置好这些环境变量。 - -使用命令 可以显示出这些环境变量,手动在当前终端中设置好之后,就可以不依赖命令行工具来启动项目了。另外使用兼容 `sh` shell 的用户,还可以直接使用 ,自动设置好所有的环境变量。 - -启动时还可以给启动命令增加自定义参数,在 命令后增加两个横线 `--`,所有在横线后的参数会被传递到实际执行的命令中。比如启动 node 项目时,想增加 `--inspect` 参数给 node 进程,来启动 node 自带的远程调试功能,只要用 来启动项目即可。 - -另外还可以使用 `--cmd` 来指定启动命令,这样即可使用任意自定义命令来执行项目:。 - -有些情况下,我们需要让 IDE 来运行项目,或者需要调试在虚拟机/远程机器上的项目的云函数,这时可以单独运行云函数调试功能,而不在本地运行项目本身: - - - {`$ ${CLI_BINARY} debug --remote=http://remote-url-or-ip-address:remote-port --app-id=xxxxxx`} - - -更多关于云引擎开发的内容,请参考[云引擎服务总览](/sdk/engine/overview/)。 - -## 部署 - -从 1.0 开始, 必须提供一个参数明确指定部署的目标(如不指定需交互式地选择): - -| 命令 | 体验版 | 标准版 | -| -------------------------------------- | ------------ | ---------------------------- | -| | 部署生产环境 | 部署生产环境 | -| | 不支持 | 部署预备环境 | -| | 不支持 | 将预备环境版本发布到生产环境 | - -### 从本地代码部署 - -当开发和本地测试云引擎项目通过后,你可以直接将本地源码推送到云引擎平台运行: - -{`${CLI_BINARY} deploy --prod`} - -这个命令会将本地源码部署到线上的生产环境,覆盖之前的部署(无论是从本地仓库部署、Git 部署还是在线定义)。 - -部署过程会实时打印进度: - - - {`$ ${CLI_BINARY} deploy --prod -[INFO] lean (v1.0.0) running on darwin/arm64 -[INFO] Current app: my-engine-app (xxxxxxxxxxxxxxxxxxxxxxxx), group: web, region: cn-n1 -[INFO] Deploying new verison to production -[INFO] Node.js runtime detected -[INFO] Uploading file 6.40 KiB / 6.40 KiB [=========================] 100.00% 0s -[REMOTE] 开始构建 20220328-114036 -[REMOTE] 正在下载应用代码 ... -[REMOTE] 正在解压缩应用代码 ... -[REMOTE] 运行环境: nodejs -[REMOTE] 正在下载和安装依赖项 ... -[REMOTE] 存储镜像到仓库(0B)... -[REMOTE] 镜像构建完成:20220328-114036 -[REMOTE] 开始部署 20181207-115634 到 web1 -[REMOTE] 正在创建新实例 ... -[REMOTE] 正在启动新实例 ... -[REMOTE] [Python] 使用 Python 3.7.1, Python SDK 2.1.8 -[REMOTE] 实例启动成功:{"version": "2.1.8", "runtime": "cpython-3.7.1"} -[REMOTE] 云函数和 Hook 信息已更新 -[REMOTE] 部署完成:1 个实例部署成功`} - - -默认部署备注为「从命令行工具构建」,显示在 ** > 管理部署 > 云引擎分组 > 日志** 中。你可以通过 `-m` 选项来自定义部署的备注信息: - - - {`${CLI_BINARY} deploy --prod -m 'fix #42'`} - - -部署之后需要绑定一个云引擎自定义域名,然后就可以通过 curl 命令来测试你的云引擎代码,或者通过浏览器访问相应的网址。 - -
    -点击展开如何在部署时忽略部分文件(.leanignore - -部署项目时,如果有一些临时文件或是项目源码管理软件用到的文件,不需要上传到服务器,可以将它们加入到 `.leanignore` 文件。 - -`.leanignore` 文件格式与 Git 使用的 `.gitignore` 格式基本相同(严格地说,`.leanignore` 支持的语法为 `.gitignore` 的子集),每行写一个忽略项,可以是文件或者文件夹。如果项目没有 `.leanignore` 文件,部署时会根据当前项目所使用的语言创建一个默认的 `.leanignore` 文件。请确认此文件中的 [默认配置][defaultignorepatterns] 是否与项目需求相符。 - -[defaultignorepatterns]: https://github.com/leancloud/lean-cli/blob/master/runtimes/ignorefiles.go#L13 - -
    - -### 使用预备环境 - -云引擎向标准版实例的用户提供了一个额外的预备环境,预备环境则提供了和生产环境几乎相同的环境、访问相同的数据,可以绑定单独的域名供开发者进行测试。在开发过程中,你可以先将改动部署到预备环境,使用线上数据测试通过后再发布到生产环境。 - -部署至预备环境: - -{`${CLI_BINARY} deploy --staging`} - -将预备环境的版本发布到生产环境: - - - {`$ ${CLI_BINARY} publish -[INFO] Current CLI tool version: 0.21.0 -[INFO] Retrieving app info ... -[INFO] Deploying AwesomeApp(xxxxxx) to region: cn group: web production -[REMOTE] 开始部署 20181207-115634 到 web1 -[REMOTE] 正在创建新实例 ... -[REMOTE] 正在启动新实例 ... -[REMOTE] 实例启动成功:{"version": "2.1.8", "runtime": "cpython-3.7.1"} -[REMOTE] 正在更新云函数信息 ... -[REMOTE] 部署完成:1 个实例部署成功`} - - -相比于直接部署生产环境, 可以保证将完全相同的版本(包括所有依赖和构建产物)发布到生产环境,最大限度避免不一致的情况。 - -### 使用预览环境 - -云引擎可以自动将 Pull request 部署到预览环境,每个预览环境有单独的域名,允许你在接近生产的环境中测试通过后再合并 PR。 - -预览环境目前只支持在 CI 中使用命令行工具部署。 - -使用命令 来部署预览环境,在 GitLab CI 和 GitHub Actions 中会自动读取 PR 号、分支名等信息,若使用其他 CI 或在本地部署,需要手动指定。 - -我们提供了 GitLab CI 和 GitHub Actions 的模版: - -
    -点击展开 GitLab CI (.gitlab-ci.yml) 模版 - -首先在 GitLab 点击 **Settings > CI/CD > Variables > Add variable** 添加你的 ACCESS_TOKEN,注意勾选 “Mask variable” 避免出现在日志中。 - -然后在 GitLab 左侧点击 **CI/CD > Editor** 新建一个 `.gitlab-ci.yml` 文件并添加以下内容。注意修改 `variables` 中的设置。 - - -{`variables: - REGION: MY_REGION # set this to your ${BRAND == 'tds' ? 'TDS' : 'LeanCloud'} region, e.g. ${BRAND == 'tds' ? 'cn-tds1' : 'cn-n1'} - APP_ID: MY_APP_ID # set this to your App ID on ${BRAND == 'tds' ? 'TDS' : 'LeanCloud'} - GROUP: MY_GROUP # set this to your group, e.g. web\n -before_script: - - apt-get update && apt-get install -y curl - - curl -L -o /bin/${CLI_BINARY} https://github.com/leancloud/lean-cli/releases/download/v1.2.3/${CLI_BINARY}-linux-x64 - - chmod +x /bin/${CLI_BINARY} - - ${CLI_BINARY} login --region $REGION --token $ACCESS_TOKEN - - ${CLI_BINARY} switch --region $REGION --group $GROUP $APP_ID\n -deploy_preview: - stage: deploy - script: - - PREVIEW_URL=\$(${CLI_BINARY} preview deploy) - - echo "PREVIEW_URL=$PREVIEW_URL" >> deploy.env - artifacts: - reports: - dotenv: deploy.env - environment: - name: preview/$CI_COMMIT_REF_NAME - url: $PREVIEW_URL - on_stop: delete_preview - auto_stop_in: 1 week - only: - - merge_request\n -delete_preview: - stage: deploy - script: ${CLI_BINARY} preview delete - environment: - name: preview/$CI_COMMIT_REF_NAME - action: stop - rules: - - if: $CI_MERGE_REQUEST_ID - when: manual`} - - -
    - -
    -点击展开 GitHub Actions 模版 - -首先在 GitHub 点击 **Settings > Secrets and variables > Actions** 创建一个 secret,添加你的 ACCESS_TOKEN。 - -然后在 GitHub 点击 **Actions > set up a workflow yourself** 创建两个 Workflow 文件,注意修改 `env` 中的设置: - - -{`# .github/workflows/preview-env-deploy.yml\n -name: Deploy Preview Environment\n -on: - pull_request: - types: [opened, synchronize]\n -env: - REGION: MY_REGION # set this to your ${BRAND == 'tds' ? 'TDS' : 'LeanCloud'} region, e.g. ${BRAND == 'tds' ? 'cn-tds1' : 'cn-n1'} - APP_ID: MY_APP_ID # set this to your App ID on ${BRAND == 'tds' ? 'TDS' : 'LeanCloud'} - GROUP: MY_GROUP # set this to your group, e.g. web\n -jobs: - deploy-preview-environment: - runs-on: ubuntu-latest - environment: - name: preview/\${{ github.head_ref }} - url: \${{ env.PREVIEW_URL }} - steps: - - uses: actions/checkout@v3\n - - name: Install lean-cli - run: | - sudo curl -L -o /bin/${CLI_BINARY} https://github.com/leancloud/lean-cli/releases/download/v1.2.3/${CLI_BINARY}-linux-x64 - sudo chmod +x /bin/${CLI_BINARY}\n - - name: Deploy - run: | - ${CLI_BINARY} login --region \${{ env.REGION }} --token \${{ secrets.ACCESS_TOKEN }} - ${CLI_BINARY} switch --region \${{ env.REGION }} --group \${{ env.GROUP }} \${{ env.APP_ID }} - PREVIEW_URL=\$(${CLI_BINARY} preview deploy) - echo "PREVIEW_URL=$PREVIEW_URL" >> $GITHUB_ENV -`} - - - -{`# .github/workflows/preview-env-delete.yml\n -name: Delete Preview Environment\n -on: - pull_request: - types: [closed]\n -env: - REGION: MY_REGION # set this to your ${BRAND == 'tds' ? 'TDS' : 'LeanCloud'} region, e.g. ${BRAND == 'tds' ? 'cn-tds1' : 'cn-n1'} - APP_ID: MY_APP_ID # set this to your App ID on ${BRAND == 'tds' ? 'TDS' : 'LeanCloud'} - GROUP: MY_GROUP # set this to your group, e.g. web\n -jobs: - delete-preview-environment: - runs-on: ubuntu-latest - permissions: - deployments: write - steps: - - name: Install lean-cli - run: | - sudo curl -L -o /bin/${CLI_BINARY} https://github.com/leancloud/lean-cli/releases/download/v1.2.3/${CLI_BINARY}-linux-x64 - sudo chmod +x /bin/${CLI_BINARY}\n - - name: Delete - run: | - ${CLI_BINARY} login --region \${{ env.REGION }} --token \${{ secrets.ACCESS_TOKEN }} - ${CLI_BINARY} switch --region \${{ env.REGION }} --group \${{ env.GROUP }} \${{ env.APP_ID }} - ${CLI_BINARY} preview delete\n - - uses: strumwolf/delete-deployment-environment@v2 - with: - token: \${{ secrets.GITHUB_TOKEN }} - environment: preview/\${{ github.head_ref }} - onlyDeactivateDeployments: true`} - - -
    - -### 触发 Git 部署 - -如果代码保存在某个 Git 仓库上,例如 [GitHub](https://github.com),并且在控制台已经正确设置了 git repo 地址以及 deploy key,你也可以请求云引擎从 Git 仓库获取源码并自动部署。这个操作可以在云引擎的部署菜单里完成,也可以在本地执行: - -{`${CLI_BINARY} deploy --prod -g`} - -- `-g` 选项要求从 Git 仓库部署,Git 仓库地址必须已经在云引擎菜单中保存。 -- 默认部署使用 **master** 分支的最新代码,你可以通过 `-r ` 来指定部署特定的 commit 或者 branch。 -- 设置 git repo 地址以及 deploy key 的方法可以参考 [云引擎平台功能 § Git 部署](/sdk/engine/platform/#git-部署)。 - -## 查看日志 - -使用 `logs` 命令可以查询云引擎的最新日志: - - - {`$ ${CLI_BINARY} logs - 2019-11-20 17:17:12 Deploying 20191120-171431 to web1 - 2019-11-20 17:17:12 Creating new instance ... - 2019-11-20 17:17:22 Starting new instance ... -web1 2019-11-20 17:17:22 -web1 2019-11-20 17:17:22 > node-js-getting-started@1.0.0 start /home/leanengine/app -web1 2019-11-20 17:17:22 > node server.js -web1 2019-11-20 17:17:22 -web1 2019-11-20 17:17:23 Node app is running on port: 3000 - 2019-11-20 17:17:23 Instance started: {"runtime":"nodejs-v12.13.1","version":"3.4.0"} - 2019-11-20 17:17:23 Cloud functions and hooks metadata updated - 2019-11-20 17:17:23 Deploy finished: 1 instances deployed`} - - -默认返回最新的 30 条,最新的在最下面。 - -可以加上 `-f` 选项来自动滚动更新日志,类似 `tail -f` 命令的效果: - -{`${CLI_BINARY} logs -f`} - -新的云引擎日志产生后,都会被自动填充到屏幕下方。 - -
    -点击展开 的更多用法(时间筛选等) - -可以通过 `-l` 选项设定返回的日志数目,例如返回最近的 100 条: - -{`${CLI_BINARY} logs -l 100`} - -如果想查询某一段时间的日志,可以指定 `--from` 和 `--to` 参数: - - - {`${CLI_BINARY} logs --from=2017-07-01 --to=2017-07-07`} - - -单独使用 `--from` 参数导出从某一天到现在的日志: - -{`${CLI_BINARY} logs --from=2017-07-01`} - -另外可以配合重定向功能,将一段时间内的 JSON 格式日志导出到文件,再配合本地工具进行查看: - - - {`${CLI_BINARY} logs --from=2017-07-01 --to=2017-07-07 --format=json > leanengine.logs`} - - -`--from`、`--to` 的时区为本地时区(运行 命令行工具的机器的本地时区)。 - -
    - -## 连接到云端的 LeanDB - - - -## 疑难问题 - -### 使用命令行工具部署失败怎么办? - -部署失败有多种原因,请根据显示的报错信息耐心排查。 -一般来说,如果你使用命令行工具部署,首先建议你检查命令行工具是否是最新版,如果不是最新版,请先升级到最新版再重试。 - -### 命令行工具在本地调试时提示 `Error: listen EADDRINUSE :::3000`,无法访问应用 - -`listen EADDRINUSE :::3000` 表示你的程序默认使用的 3000 端口被其他应用占用了,可以按照下面的方法找到并关闭占用 3000 端口的程序: - -- [macOS 使用 `lsof` 和 `kill`](https://stackoverflow.com/questions/3855127/find-and-kill-process-locking-port-3000-on-mac) -- [Linux 使用 `fuser`](https://stackoverflow.com/questions/11583562/how-to-kill-a-process-running-on-particular-port-in-linux) -- [Windows 使用 `netstat` 和 `taskkill`](https://stackoverflow.com/questions/6204003/kill-a-process-by-looking-up-the-port-being-used-by-it-from-a-bat) - -也可以修改命令行工具默认使用的 3000 端口: - -{`${CLI_BINARY} -p 3002`} - -### 如何通过命令行工具上传文件至文件服务? - - - {`$ ${CLI_BINARY} file upload public/index.html -Uploads /Users/dennis/programming/avos/new_app/public/index.html successfully at: http://ac-7104en0u.qiniudn.com/f9e13e69-10a2-1742-5e5a-8e71de75b9fc.html`} - - -文件上传成功后会自动生成在云端的 URL,即上例中 `successfully at:` 之后的信息。 - -上传 images 目录下的所有文件: - -{`${CLI_BINARY} file upload images/`} - -### 同一个项目如何批量部署到多个应用的云引擎? - -可以通过 切换项目所属应用,然后通过 部署。 支持通过参数以非交互的方式使用: - - - {`${CLI_BINARY} switch --region --group -${CLI_BINARY} deploy --prod`} - - -上述命令中,`` 代表应用所在区域,目前支持的值为 `cn-n1`(华北节点)、`cn-e1`(华东节点)、`us-w1`(国际版)。 -`--prod` 表示部署到生产环境,如果希望部署到预备环境,换成 即可。 -基于这两个命令可以自行编写 CI 脚本快速部署至多个应用的云引擎实例。 - -### 如何扩展命令行工具的功能? - -有时我们需要对某个应用进行特定并且频繁的操作,比如查看应用 `_User` 表的记录总数,这样可以使用命令行工具的自定义命令来实现。 - -只要在当前系统的 `PATH` 环境变量下,或者在项目目录 `.leancloud/bin` 下存在一个以 `lean-` 开头的可执行文件,比如 `lean-usercount`,那么执行 ,命令行工具就会自动调用这个可执行文件。与直接执行 `lean-usercount` 不同的是,这个命令可以获取与应用相关的环境变量,方便访问对应的数据。 - -例如将如下脚本放到当前系统的 `PATH` 环境变量中(比如 `/usr/local/bin`): - -```python -#! /bin/env python - -import sys - -import leancloud - -app_id = os.environ['LEANCLOUD_APP_ID'] -master_key = os.environ['LEANCLOUD_APP_MASTER_KEY'] - -leancloud.init(app_id, master_key=master_key) -print(leancloud.User.query.count()) -``` - -同时赋予这个脚本可执行权限 `$ chmod +x /usr/local/bin/lean-usercount`,然后执行 ,就可以看到当前应用对应的 `_User` 表中记录总数了。 - -### 命令行工具的 1.0 版本有哪些主要的变化? - -在 2022 年 3 月我们发布了 1.0.0 版本的命令行工具,并在其中按计划进行了一些不兼容的改动: - -- 必须显式指定部署的目标(`--prod` 或 `--staging`),而之前版本则是体验版默认生产环境,标准版默认预备环境。 -- 默认不再自动拉取线上的环境变量,如果需要的话可以手动添加 - `--fetch-env` 参数。 -- 删除了 命令,我们建议使用功能更强大的 来访问 LeanCache 或 LeanDB。 - -### 之前使用 `npm` 装过旧版的命令行工具,如何升级到新版? - -如果之前使用 `npm` 安装过旧版本的命令行工具,为了避免与新版本产生冲突,建议使用 `npm uninstall -g avoscloud-code leancloud-cli` 卸载旧版本命令行工具。或者直接按照 Homebrew 的提示,执行 `brew link --overwrite lean-cli` 覆盖掉之前的 `lean` 命令来解决。 diff --git a/leancloud/docs/sdk/engine/database/_category_.json b/leancloud/docs/sdk/engine/database/_category_.json deleted file mode 100644 index d740043e2..000000000 --- a/leancloud/docs/sdk/engine/database/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "数据库", - "collapsed": true, - "position": 6 -} diff --git a/leancloud/docs/sdk/engine/database/es.mdx b/leancloud/docs/sdk/engine/database/es.mdx deleted file mode 100644 index 43d6c57e0..000000000 --- a/leancloud/docs/sdk/engine/database/es.mdx +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: LeanDB Elasticsearch 使用指南 -sidebar_label: LeanDB Elasticsearch -sidebar_position: 4 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import LeandbCliAccess from "../_partials/leandb-cli-access.mdx"; -import LeandbCreateInstance from "../_partials/leandb-create-instance.mdx"; -import TabItem from "@theme/TabItem"; -import Path from "/src/docComponents/path"; - -LeanDB Elasticsearch 是云引擎提供的托管数据库,开发者可以在云引擎中使用 Elasticsearch 客户端类库或 HTTP API 连接,访问完整的 Elasticsearch 功能,更多其他托管数据库请查看 [云引擎服务总览](/sdk/engine/overview)。 - -Elasticsearch 的主要特性: - -- **高可用**:多节点集群方案,可以容忍单节点故障。 -- **在线扩容**:在线调整容量和规格,数据平滑迁移。 -- **多实例**:满足更大容量或更高性能的需求。 -- **中文分词**:内置中文分词插件并支持自定义词库。 - -## 创建和管理实例 - -开发者可以在 ** > 数据库 > Elasticsearch** 页面创建和管理 LeanDB Elasticsearch 实例。 - -### 创建实例 - - - -- **实例规格** 目前提供 `512M`、`1GB`、`2GB`、`4GB`、`8GB` 几种,代表不同的运算能力,是计费的基础。 - -每种规格有固定的存储空间限制,如需要更多存储空间需要升级到更高的规格。 - - - -### Elasticsearch 版本 - -目前 LeanDB 仅提供 Elasticsearch 7.9 版本。 - -### 在线扩容 - -目前 LeanDB Elasticsearch 不提供自助扩容的能力,如需扩容请提交工单联系我们的技术支持。 - -### 管理共享 - -可以使用控制台上的「管理共享」功能将 LeanDB 实例共享给其他应用,被共享的应用的 LeanDB 页面可能看到这个实例,相关的环境变量也会出现在其他应用的云引擎中。 - -## 在云引擎中使用 - -LeanDB 所在的应用的云引擎在部署时,会被注入几个包含 Elasticsearch 连接信息的环境变量,包括: - -- `ELASTICSEARCH_URL_` - -其中 `` 是你在创建 LeanDB 时为它指定的名字,如果你的 LeanDB 名为 `MYES` 的话,就会有名为 `ELASTICSEARCH_URL_MYES` 的环境变量。 - -该环境变量的格式是 `http://username:password@host:port`,其中包含了所有连接 Elasticsearch 所需的信息,包括认证信息。 - - - - -在 Node.js 中你可以这样连接到 Elasticsearch: - -```javascript -const { Client } = require("@elastic/elasticsearch"); -const client = new Client({ - node: process.env.ELASTICSEARCH_URL_MYES, -}); - -// promise API -const result = await client.search({ - index: "my-index", - body: { - query: { - match: { hello: "world" }, - }, - }, -}); - -// callback API -client.search( - { - index: "my-index", - body: { - query: { - match: { hello: "world" }, - }, - }, - }, - (err, result) => { - if (err) console.log(err); - } -); -``` - -- 你需要运行 `npm install @elastic/elasticsearch` 来安装上面代码中用到的依赖 -- 更多的用法请参考 [Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html) - - - - -## 中文分词 - -除了 elasticsearch 自带的分词器,我们还提供了 -[Elasticsearch ik plugin](https://github.com/medcl/elasticsearch-analysis-ik) 以支持中文分词。 -我们可以通过以下途径指定使用 IK 插件进行中文分词: - -1. 在搜索时,指定分词器 -2. 在创建索引时,为特定 `field` 指定搜索分词器 -3. 在创建索引时,指定索引的默认分词器 -4. 在创建索引时,为特定 `field` 指定分词器 - -它们的优先级依次降低,当都未指定时,会使用默认的标准分词器(standard analyzer)。具体细节及参数见 [specify an analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/specify-analyzer.html)。 - -### 自定义词库 - -除此之外,自定义词库也是支持的。用户可以在控制台上传自定义词库。 -词库文件要求为 UTF-8 编码,每个词单独一行,文件大小不能超过 10MB,例如: - -``` -面向对象编程 -函数式编程 -高阶函数 -响应式设计 -``` - -将其保存为文本文件,例如 `dict.txt`,上传即可。上传之后,分词将于 2 分钟后生效。开发者可以通过 [analyze API](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/test-analyzer.html) 来测试。需要注意使用 analyze API 时要指定 index,使用 `curl -X POST "localhost:9200/my-index/_analyze?pretty"` 的形式。 - -## 管理数据 - -除了在云引擎中通过编程的方式访问 LeanDB,我们还提供了用于进行管理、调试或一次性数据操作的方式。 - -### 使用命令行工具连接 - - diff --git a/leancloud/docs/sdk/engine/database/mongo.mdx b/leancloud/docs/sdk/engine/database/mongo.mdx deleted file mode 100644 index 75fc4e701..000000000 --- a/leancloud/docs/sdk/engine/database/mongo.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: LeanDB MongoDB 使用指南 -sidebar_label: LeanDB MongoDB -sidebar_position: 3 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import LeandbCliAccess from "../_partials/leandb-cli-access.mdx"; -import LeandbCreateInstance from "../_partials/leandb-create-instance.mdx"; -import TabItem from "@theme/TabItem"; -import Path from "/src/docComponents/path"; - -LeanDB MongoDB 是云引擎提供的托管数据库,开发者可以在云引擎中使用 MongoDB 客户端类库连接,访问完整的 MongoDB 功能,更多其他托管数据库请查看 [云引擎服务总览](/sdk/engine/overview)。 - -## 创建和管理实例 - -开发者可以在 ** > 数据库 > MongoDB** 页面创建和管理 LeanDB MongoDB 实例。 - -### 创建实例 - - - -- **实例规格** 目前提供 `512M`、`1GB`、`2GB`、`4GB`、`8GB` 几种,代表不同的运算能力,是计费的基础。 - -每种规格有固定的连接数和存储空间限制,如需要更多连接数或存储空间需要升级到更高的规格。 - - - -### MongoDB 版本 - -目前 LeanDB 仅提供 MongoDB 4.0 版本。 - -### 在线扩容 - -目前 LeanDB MongoDB 不提供自助扩容的能力,如需扩容请提交工单联系我们的技术支持。 - -### 管理共享 - -可以使用控制台上的「管理共享」功能将 LeanDB 实例共享给其他应用,被共享的应用的 LeanDB 页面可能看到这个实例,相关的环境变量也会出现在其他应用的云引擎中。 - -## 在云引擎中使用 - -LeanDB 所在的应用的云引擎在部署时,会被注入几个包含 MongoDB 连接信息的环境变量,包括: - -- `MONGODB_URL_` - -其中 `` 是你在创建 LeanDB 时为它指定的名字,如果你的 LeanDB 名为 `MYDB` 的话,就会有名为 `MONGODB_URL_MYDB` 的环境变量。 - - - - -在 Node.js 中你可以这样连接到 MongoDB(假定 LeanDB 名称为 `MYDB`): - -```js title='app.js' -const { MongoClient } = require("mongodb"); - -const mongoClient = new MongoClient(process.env["MONGODB_URL_MYDB"], { - useUnifiedTopology: true, - poolSize: 10, -}); - -mongoClient - .connect() - .then(() => { - console.log("Connected to MongoDB"); - }) - .catch((err) => { - console.eror("Connect to MongoDB failed", err.message); - }); - -app.get("/", (req, res) => { - const cats = mongoClient.collection("cats"); - - res.json(cats.find({}, { limit: 10 })); -}); -``` - -- 你需要运行 `npm install mongodb` 来安装上面代码中用到的依赖 -- 更多的用法请参考 [MongoDB Node Driver 官方文档](https://www.mongodb.com/docs/drivers/node/current/) - - - - -在 .NET 中你可以这样连接到 MongoDB(假定 LeanDB 名称为 `MYDB`): - -```cs -string url = Environment.GetEnvironmentVariable("MONGODB_URL_MYDB"); -MongoClient client = new MongoClient(url); -IMongoCollection collection = client.GetDatabase("leancloud") - .GetCollection("hello"); -FilterDefinition filter = Builders.Filter.Empty; -Console.WriteLine(collection.Find(filter).ToList().ToJson()); -``` - - - - -## 管理数据 - -除了在云引擎中通过编程的方式访问 LeanDB,我们还提供了用于进行管理、调试或一次性数据操作的方式。 - -### 使用命令行工具连接 - - diff --git a/leancloud/docs/sdk/engine/database/mysql.mdx b/leancloud/docs/sdk/engine/database/mysql.mdx deleted file mode 100644 index f47ee3c9f..000000000 --- a/leancloud/docs/sdk/engine/database/mysql.mdx +++ /dev/null @@ -1,231 +0,0 @@ ---- -title: LeanDB MySQL 使用指南 -sidebar_label: LeanDB MySQL -sidebar_position: 2 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import LeandbCliAccess from "../_partials/leandb-cli-access.mdx"; -import LeandbCreateInstance from "../_partials/leandb-create-instance.mdx"; -import TabItem from "@theme/TabItem"; -import Path from "/src/docComponents/path"; - -LeanDB MySQL 是云引擎提供的托管数据库,开发者可以在云引擎中使用 MySQL 客户端类库连接,访问完整的 MySQL 功能,更多其他托管数据库请查看 [云引擎服务总览](/sdk/engine/overview)。 - -## 创建和管理实例 - -开发者可以在 ** > 数据库 > MySQL** 页面创建和管理 LeanDB MySQL 实例。 - -### 创建实例 - - - -- **实例名称** 用于在云引擎中通过环境变量引用到该 LeanDB 实例,在账户下需唯一。 -- **内存规格** 目前提供 `0.5GB`、`1GB`、`2GB`、`4GB` 几种,代表不同的运算能力,是计费的基础。 -- **存储空间** 每个实例默认有 20G 的存储空间,可以选配 100G 或者 500G 的存储空间。 - - - -### MySQL 版本 - -目前 LeanDB 仅提供 MySQL 5.6 版本。 - -### 在线扩容 - -目前 LeanDB MySQL 不提供自助扩容的能力,如需扩容请提交工单联系我们的技术支持。 - -### 管理共享 - -可以使用控制台上的「管理共享」功能将 LeanDB 实例共享给其他应用,被共享的应用的 LeanDB 页面可能看到这个实例,相关的环境变量也会出现在其他应用的云引擎中。 - -## 在云引擎中使用 - -LeanDB 所在的应用的云引擎在部署时,会被注入几个包含 MySQL 连接信息的环境变量,包括: - -- `MYSQL_HOST_` -- `MYSQL_PORT_` -- `MYSQL_ADMIN_USER_` -- `MYSQL_ADMIN_PASSWORD_` - -其中 `` 是你在创建 LeanDB 时为它指定的名字,如果你的 LeanDB 名为 `MYRDB` 的话,就会有名为 `MYSQL_HOST_MYRDB` 的环境变量(以及其他三个)。 - - - - -在 Node.js 中你可以这样连接到 MySQL: - -```javascript -const mysql = require("mysql"); -const Promise = require("bluebird"); - -const mysqlPool = Promise.promisifyAll( - mysql.createPool({ - host: process.env["MYSQL_HOST_MYRDB"], - port: process.env["MYSQL_PORT_MYRDB"], - user: process.env["MYSQL_ADMIN_USER_MYRDB"], - password: process.env["MYSQL_ADMIN_PASSWORD_MYRDB"], - database: "test", - connectionLimit: 10, - }) -); - -mysqlPool - .queryAsync("SELECT 1 + 1 AS solution") - .then((rows) => { - console.log("The solution is", rows[0].solution); - }) - .catch((err) => { - console.error(err); - }); -``` - -- 你需要运行 `npm install --save mysql bluebird` 来安装上面代码中用到的依赖 -- 更多的用法请参考 [mysqljs/mysql 的文档](https://github.com/mysqljs/mysql) - - - - -在 Python 中你可以这样连接到 MySQL: - -```python -import os -import mysql.connector - -result = '' - -host = os.environ['MYSQL_HOST_MYRDB'] -port = os.environ['MYSQL_PORT_MYRDB'] -user = os.environ['MYSQL_ADMIN_USER_MYRDB'] -password = os.environ['MYSQL_ADMIN_PASSWORD_MYRDB'] -try: - cnx = mysql.connector.connect( - user=user, password=password, database='test', host=host, port=port) -except mysql.connector.Error as err: - if err.errno == errorcode.ER_ACCESS_DENIED_ERROR: - print("username or password error") - elif err.errno == errorcode.ER_BAD_DB_ERROR: - print("Database does not exist") - else: - print(err) - else: - cursor = cnx.cursor() - cursor.execute('SELECT 1 + 1 AS solution') - for row in cursor: - result = "The solution is {}".format(row[0]) - - cursor.close() - cnx.close() -``` - -- 上面用的是 MySQL 官方的 python driver,你需要在 `requirements.txt` 中列出这一依赖,例如:`mysql-connector-python>=8.0.16,<9.0.0` -- 更多的用法请参考 [MySQL Connector/Python 文档](https://dev.mysql.com/doc/connector-python/en/) - - - - -在 PHP 中你可以这样连接到 MySQL: - -```php -try { - $mysqlHost = getenv('MYSQL_HOST_MYRDB'); - $mysqlPort = getenv('MYSQL_PORT_MYRDB'); - $pdo = new PDO("mysql:host=$mysqlHost:$mysqlPort;dbname=test", getenv('MYSQL_ADMIN_USER_MYRDB'), getenv('MYSQL_ADMIN_PASSWORD_MYRDB')); - - foreach($pdo->query('SELECT 1 + 1 AS solution') as $row) { - print "The solution is {$row['solution']}"; - } -} catch (PDOException $e) { - print $e->getMessage(); -} -``` - -- 更多的用法请参考 [PDO 的文档](https://www.php.net/manual/zh/class.pdo.php) - - - - -在 Java 中你可以这样连接到 MySQL: - -```java -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.Statement; -import java.sql.ResultSet; -import java.sql.SQLException; - -String host = System.getenv("MYSQL_HOST_MYRDB"); -String port = System.getenv("MYSQL_PORT_MYRDB"); -String user = System.getenv("MYSQL_ADMIN_USER_MYRDB"); -String password = System.getenv("MYSQL_ADMIN_PASSWORD_MYRDB"); -try { - Class.forName("com.mysql.jdbc.Driver").newInstance(); -} catch (Exception ex) { - // 处理异常 -} -try { - Connection connection = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port + "/test?" + - "user=" + user + "&password=" + password); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT 1 + 1 AS solution"); - resultSet.first(); - System.out.format("The solution is %d", resultSet.getInt("solution")); -} catch (SQLException ex) { - // 处理异常 -} -``` - -- 需要在 `pom.xml` 中加入 mysql connector 依赖: - -```xml - - mysql - mysql-connector-java - 8.0.16 - -``` - -- 更多的用法请参考 [MySQL Connector/J 文档](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-usagenotes-connect-drivermanager.html) - - - - -在 .NET 中你可以这样连接到 MySQL: - -```cs -string host = Environment.GetEnvironmentVariable("MYSQL_HOST_mysql"); -string port = Environment.GetEnvironmentVariable("MYSQL_PORT_mysql"); -string uid = Environment.GetEnvironmentVariable("MYSQL_ADMIN_USER_mysql"); -string password = Environment.GetEnvironmentVariable("MYSQL_ADMIN_PASSWORD_mysql"); -string connectionString = $"server={host};port={port};uid={uid};pwd={password};database=leancloud"; - -MySqlConnection conn = new MySqlConnection(connectionString); -conn.Open(); - -string sql = "SELECT * FROM hello"; -MySqlCommand command = new MySqlCommand(sql, conn); -MySqlDataReader reader = command.ExecuteReader(); -StringBuilder sb = new StringBuilder(); -while (reader.Read()) { - sb.AppendLine($"{reader[0]} -- {reader[1]}"); -} -Console.WriteLine(sb.ToString()); -``` - -- 更多的用法请参考 [MySQL Connector .NET 文档](https://dev.mysql.com/doc/connector-net/en/) - - - - -## 管理数据 - -除了在云引擎中通过编程的方式访问 LeanDB,我们还提供了用于进行管理、调试或一次性数据操作的方式。 - -### 在线管理面板 - -为方便开发和调试,我们为开发者提供了一个 Web 界面来对 MySQL 进行管理,你可以在控制台上点击「管理员面板」链接来访问这个 Web 界面。 - -开发者可以在这个页面上进行 SQL 查询和更新,创建和管理数据库,创建和管理索引等操作。 - -### 使用命令行工具连接 - - diff --git a/leancloud/docs/sdk/engine/database/redis.mdx b/leancloud/docs/sdk/engine/database/redis.mdx deleted file mode 100644 index 4edba5f94..000000000 --- a/leancloud/docs/sdk/engine/database/redis.mdx +++ /dev/null @@ -1,286 +0,0 @@ ---- -title: LeanCache 使用指南 -sidebar_label: LeanCache -sidebar_position: 1 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import LeandbCliAccess from "../_partials/leandb-cli-access.mdx"; -import LeandbCreateInstance from "../_partials/leandb-create-instance.mdx"; -import TabItem from "@theme/TabItem"; -import Path from "/src/docComponents/path"; - -LeanCache 是云引擎提供的托管 Redis,开发者可以在云引擎中使用 Redis 客户端类库连接,访问完整的 Redis 功能,更多其他托管数据库请查看 [云引擎服务总览](/sdk/engine/overview)。 - -LeanCache 使用 [Redis](https://redis.io/) 提供了高性能、高可用的 Key-Value 内存存储,主要用作缓存数据的存储,也可以用作持久化数据的存储。LeanCache 采用主从架构的 **高可用** 配置,通过 AOF 和 RDB 进行数据的持久化,还提供了在不中断服务的情况下在线扩容的能力。 - -
    -点击展开 LeanCache 的使用场景 - -- 某些数据量少,但是读写比例很高,比如某些应用的菜单可以通过后台调整,所有用户会频繁读取该信息。 -- 需要同步锁或者队列处理,比如秒杀、抢红包等场景。 -- 多个云引擎节点的协同和通信。 - -恰当使用 LeanCache 不仅可以极大地提高应用的服务性能,还能 **降低成本**,因为某些高频率的查询不需要走存储服务(存储服务按调用次数收费)。你可以在 [leanengine-nodejs-demos](https://github.com/leancloud/leanengine-nodejs-demos) 中找到一些有关 LeanCache 的示例: - -- [associated-data](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/associated-data.js) 缓存关联数据 -- [leaderboard](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/leaderboard.js) 实现排行榜 -- [limited-stock-rush](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/limited-stock-rush.js) 实现秒杀抢购 -- [redlock](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/redlock.js) 实现分布式锁 - -
    - -
    -点击展开 LeanCache 高可用详情 - -每个 LeanCache 实例使用 Redis Master-Slave 主从热备,其下的多个观察节点每隔 1 秒钟观察一次主节点的状态。如果「主节点」最后一次有效响应在 5 秒之前,则该观察节点认为主节点失效。如果超过总数一半的观察节点发现主节点失效,则自动将「从节点」切换为主节点,并会有新的从节点启动重新组成主从热备。这个过程对应用完全透明,不需要修改连接字符串或者重启,整个切换过程应用只有几秒钟会出现访问中断。 - -与此同时,从节点还会以 [AOF 方式](http://www.redis.cn/topics/persistence.html) 将数据持久化存储到可靠的中央文件中,每秒刷新一次。如果很不巧主从节点同时失效,则马上会有新的 Redis 节点启动,并从 AOF 文件恢复,完成后即可再次提供服务,并且会有新的从节点与之构成主从热备。 - -当一个实例中的主节点失效,而最新的数据没有同步到对应的从节点时,主从切换会造成这部分数据丢失。当主、从节点同时失效,未同步到从节点和从节点未刷新到磁盘 AOF 文件中的数据将会丢失。 - -
    - -## 创建和管理实例 - -开发者可以在 ** > 数据库 > LeanCache (Redis)** 页面创建和管理 LeanCache 实例。 - -### 创建实例 - - - -- **实例名称** 用于在云引擎中通过环境变量引用到该 LeanDB 实例,在账户下需唯一。 -- **数据删除策略** 内存用满时的删除策略,默认为 `volatile-lru`,详见 [数据删除策略](#数据删除策略)。 -- **实例容量** 目前提供 `128M`、`256M`、`512M`、`1G`、`2G`、`4G`、`8G` 几种,代表不同的内存容量,是计费的基础。 - - - -### 数据删除策略 - -详见 [官方文档](https://redis.io/docs/manual/eviction/#eviction-policies),下面是一个简单的概括: - -- `noeviction` 不删除,当内存满时,直接返回错误。 -- `allkeys-lru` 优先删除最近最少使用的 key,以释放内存。 -- `volatile-lru` 优先删除设定了过期时间的 key 中最近最少使用的 key,以释放内存。 -- `allkeys-random` 随机删除一个 key,以释放内存。 -- `volatile-random` 从设定了过期时间的 key 中随机删除一个,以释放内存。 -- `volatile-ttl` 从设定了过期时间的 key 中删除最老的 key,以释放内存。 - -### Redis 版本 - -目前 LeanCache 仅提供 Redis 6 版本。 - -### 在线扩容 - -你可以在线扩大(或者缩小)LeanCache 实例的最大内存容量。整个过程可能会持续一段时间,在此期间 LeanCache 会中断几秒钟进行切换,其他时间都正常提供服务。如果你的应用访问量较大的话,LeanCache 中断的这几秒可能会对你的云引擎实例产生较为明显的影响(例如内存增加),可以考虑将扩容安排在低峰时刻。 - -:::caution -缩小容量之前,请务必确认现有数据体积小于目标容量,否则可能造成数据丢失。 -::: - -### 管理共享 - -可以使用控制台上的「管理共享」功能将 LeanCache 实例共享给其他应用,被共享的应用的 LeanCache 页面可能看到这个实例,相关的环境变量也会出现在其他应用的云引擎中。 - -## 在云引擎中使用 - -LeanDB 所在的应用的云引擎在部署时,会被注入几个包含 Redis 连接信息的环境变量,包括: - -- `REDIS_URL_` - -其中 `` 是你在创建 LeanDB 时为它指定的名字,如果你的 LeanDB 名为 `MYRDB` 的话,就会有名为 `REDIS_URL_MYRDB` 的环境变量。 - - - - -首先添加相关依赖到云引擎应用中: - -```json -"dependencies": { - "ioredis": "^4.9.0" -} -``` - -然后可以使用下列代码获取 Redis 连接:(假定实例名称为 `MYCACHE`) - -```js -const Redis = require("ioredis"); - -const client = new Redis(process.env["REDIS_URL_MYCACHE"]); -client.on("error", function (err) { - return console.error("redis err: ", err); -}); -``` - - - - -首先添加相关依赖到云引擎应用的 `requirements.txt` 中: - -``` -Flask>=0.10.1 -leancloud-sdk>=1.0.9 -... -redis -``` - -然后可以使用下列代码获取 Redis 连接:(假定实例名称为 `MYCACHE`) - -```python -import os -import redis - -r = redis.from_url(os.environ.get("REDIS_URL_MYCACHE")) -``` - - - - -首先添加 redis 库的依赖,比如 predis: - -```sh -composer require 'predis/predis:1.1.*' -``` - -然后在 PHP 应用中通过环境变量获取 Redis 地址并创建链接,如:(假定实例名称为 `MYCACHE`) - -```php -use Predis; -$redis = new Predis\Client(getenv("REDIS_URL_MYCACHE")); -$redis->ping(); -``` - - - - -在 `pom.xml` 中添加 redis client 的依赖: - -```xml - - redis.clients - jedis - 3.2.0 - -``` - -并引入依赖: - -```java -import redis.clients.jedis.Jedis; -``` - -从环境变量中获取链接字符串,然后再创建 redis client 实例即可。(假定实例名称为 `MYCACHE`) - -```java -String redisUrl = System.getenv("REDIS_URL_MYCACHE"); -Jedis jedis = new Jedis(redisUrl); -jedis.set("foo", "bar"); -String value = jedis.get("foo"); -jedis.close(); -``` - -并发请求较高的情况下可以考虑使用连接池: - -```java -public class RedisHelper { - private final JedisPool jedisPool; - public RedisHelper() { - // 创建应用时,先创建连接池;使用默认配置 - jedisPool = new JedisPool(System.getenv("REDIS_URL_jedis_128m")); - } - - // 使用;从连接池取出一个 jedis 连接使用 - // 注意,使用完了之后,要调用返回的 jedis 对象的 close 方法返还连接。`jedis.close()` - public Jedis getJedis() { - Jedis jedis = jedisPool.getResource(); - return jedis; - } - - public void closePool() { - // 关闭应用时关闭连接池 - jedisPool.close(); - } -} -``` - - - - -可以使用 [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/) 里面构建的方式。 - -假设在控制台创建了一个名字叫做 `dev` LeanCache 实例,如下代码将演示如何连接这个实例,并且存储、读取数据: - -```cs -string host = Environment.GetEnvironmentVariable("REDIS_HOST_dev"); -string port = Environment.GetEnvironmentVariable("REDIS_PORT_dev"); -string user = Environment.GetEnvironmentVariable("REDIS_USER_dev"); -string password = Environment.GetEnvironmentVariable("REDIS_PASSWORD_dev"); - -ConfigurationOptions config = new ConfigurationOptions { - EndPoints = { - { host, int.Parse(port) } - }, - User = user, - Password = password -}; -ConnectionMultiplexer conn = ConnectionMultiplexer.Connect(config); -IDatabase db = conn.GetDatabase(); -db.StringSet("foo", "bar"); -var bar = db.StringGet("foo"); -``` - -关于 `ConnectionMultiplexer` 的用法和相关文档请参阅:[StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/),这个库是 .NET Core 环境中比较推荐的 Redis Client。 - - - - -## 管理数据 - -除了在云引擎中通过编程的方式访问 LeanDB,我们还提供了用于进行管理、调试或一次性数据操作的方式。 - -### 使用命令行工具连接 - - - -## 常见问题 - -### 在本地调试依赖 LeanCache 的应用 - -你可以 [使用命令行工具连接](#使用命令行工具连接) 云端的 LeanCache 进行调试,或在本地安装 Redis: - -- Mac 上运行 `brew install redis` 来安装 Redis,然后使用 `redis-server` 启动服务。 -- Debian/Ubuntu 上运行 `apt-get install redis-server` -- CentOS/RHEL 运行 `yum install redis` -- Windows 尚无官方支持,可以下载 [微软的分支版本](https://github.com/microsoftarchive/redis/releases) 安装包。 - -默认情况下,在本地运行时程序没有 LeanCache 的环境变量,因此会使用本地的 Redis 服务器地址。 - -```js -// 在本地 process.env['REDIS_URL_<实例名称>'] 为 undefined,会连接默认的 127.0.0.1:6379 -const client = new Redis(process.env["REDIS_URL_MYCACHE"]); // 假定实例名称为 MYCACHE -``` - -如果部署到预备或生产环境时遇到类似 `redis err: Error: Redis connection to 127.0.0.1:6379 failed - connect ECONNREFUSED 127.0.0.1:6379` 错误,请核实以上代码中 `REDIS_URL_<实例名称>` 这个环境变量的值是否替换正确,也可参考 [在云引擎中使用(Node.js 环境)](#在云引擎中使用) 的示例。 - -更详细的 Redis 操作说明请参考 [Redis 官方文档](https://redis.io/docs/)。 - -### 与自建的 HashTable 相比较,LeanCache 有什么优势? - -与自己在程序的全局作用域中维护一个 HashTable 相比,使用 LeanCache 的优势在于: - -- **多实例之间的数据共享**:云引擎支持多实例运行,自行维护的 HashTable 数据无法[跨实例共享](#管理共享)。 -- **数据持久化存储**:在程序重启或重新部署后数据不会丢失,Redis 会帮你完成数据持久化的工作。LeanCache 还会为你的 Redis 做热备,具有非常高的可靠性。 -- **原子操作和性能**:Redis 提供了常见的数据结构和大量原子操作,其文档中列出了每个操作符的时间复杂度,而自行实现的 HashTable 的性能则很大程度依赖于具体语言的实现。 - -### 报错:Redis connection gone from end event - -LeanCache 或者任何网络程序都有可能出现连接闪断的问题,可能是因为网络波动,或是服务器负载、容量调整等等。这时只需要重建连接即可使用。而 Redis Client 一般都有断开重连的机制,未连接期间指令会保存到队列,待连接成功后再发送队列中的指令([Redis client library](https://www.npmjs.com/package/redis) 便是如此实现)。所以如果这个错误偶尔发生,一般不会有什么问题;同时建议在应用中 [增加 Redis 的 on error 事件处理](#在云引擎中使用)。 - -如果这个错误**频繁出现**,那么很可能 LeanCache 节点处于非受控状态,请提交工单联系技术支持进行处理。 - -### 多实例 - -有些时候,你可能希望在一个应用里创建多个 LeanCache 实例: - -- **需要存储的数据大于 8 GB**:目前我们提供的实例最大容量为 8 GB。如果有大于此容量的数据,建议你创建多个实例,然后根据功能来划分,比如一个用来做持久化,另一个用来做缓存。 -- **需要更高的性能**:如果单实例的性能已经成为应用的瓶颈,你可以创建多个实例,然后在云引擎中同时连接,并自己决定 key 的分片策略,使请求分散到不同的实例来获得更高的性能。 diff --git a/leancloud/docs/sdk/engine/dedicated-IP.mdx b/leancloud/docs/sdk/engine/dedicated-IP.mdx deleted file mode 100644 index 8f2b50eae..000000000 --- a/leancloud/docs/sdk/engine/dedicated-IP.mdx +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: 云引擎独立 IP -sidebar_label: 独立 IP -sidebar_position: 9 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -请参考 [域名绑定指南的云引擎域名章节](/sdk/domain/guide/#云引擎域名) 了解云引擎独立 IP 的优势。 - -前往 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置****云服务控制台 > 账号设置 > 独立 IP** 可以查看、管理当前账号使用的云引擎独立 IP。 - -申请独立 IP 后,在新增域名绑定时,请按提示到自有域名的域名服务商处添加 A 记录,指向独立 IP。 已经在开发者中心绑定的自定义域名,只需在域名服务商处修改解析记录,将 CNAME 记录替换为 A 记录即可切换。 - -解绑 IP 前,请确保所有指向该 IP 的自定义域名均已修改解析记录,将相应 A 记录替换回 CNAME 记录。 由于 DNS 在整个互联网生效需要较长时间,修改解析记录后,请**至少等待 48 小时再解绑 IP**,以免影响服务。 - -独立 IP 是账户下所有应用共享的。如果你希望进一步隔离各应用的入口,也可以购买更多独立 IP,你需要自行规划哪些应用使用哪些 IP。 - -每一个独立 IP 默认提供了 2 Gbps 的防护带宽,可以防护小规模的攻击,你也无需为此承担额外的费用。 如果攻击超出了默认的防护容量, IP 会被禁用,你可以购买一个新的 IP 进行更换。 或者你也可以将新的 IP 作为源站 IP 接入第三方清洗服务,请通过工单联系我们,我们会提供必要的支持。 - -独立 IP 的费用为人民币 50 元 / 个 / 月,购买每个 IP 时会扣除一个月的费用(50 元),从下个自然月起,每月 1 日按 50 元 / 个的标准进行扣费。 - -独立 IP 与账户绑定,不会随应用的转移而发生变化。为了避免影响服务,转移的应用如果不改变自定义域名的解析配置,那么还可以继续使用原来的独立 IP。 - -如果你有多个独立 IP,在通过修改域名解析切换独立 IP 前,可能希望验证通过新 IP 可以成功访问服务。 这种场景下可以使用 curl 的 `--resolve` 参数指定域名解析到特定的 IP(效果类似修改 `/etc/hosts` 文件): - -```sh -curl --resolve 'engine.example.com:443:YOUR-ENGINE-IP' https://engine.example.com/ -``` diff --git a/leancloud/docs/sdk/engine/deep-dive/_category_.json b/leancloud/docs/sdk/engine/deep-dive/_category_.json deleted file mode 100644 index 67ca88829..000000000 --- a/leancloud/docs/sdk/engine/deep-dive/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "深入了解", - "collapsed": true, - "position": 7 -} diff --git a/leancloud/docs/sdk/engine/deep-dive/index.mdx b/leancloud/docs/sdk/engine/deep-dive/index.mdx deleted file mode 100644 index 126cbc42d..000000000 --- a/leancloud/docs/sdk/engine/deep-dive/index.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: 深入了解云引擎 ---- - -import Mermaid from "/src/docComponents/Mermaid"; - -:::note -这篇文档希望向有经验的开发者介绍云引擎背后的更多细节,如希望快速地开始使用云引擎,请查看 [快速开始部署云引擎应用](/sdk/engine/deploy/getting-started)。 -::: - -## 云引擎适合什么样的应用 - -云引擎是一个基于容器技术实现的、进程级别的运行环境,开发者只需关注应用的进程本身,而不需要关注操作系统级别的环境。 - -在云引擎上,应用的进程可以专注在实现业务逻辑,通过标准化的接口来和云引擎交互: - -- 应用可以使用 Git 来托管代码,云引擎会从指定的仓库拉取代码 -- 应用通过依赖清单(如 `package.json`)来描述应用对环境的需求,云引擎会自动地准备好这些环境 -- 应用从环境变量中读取配置,云引擎提供了管理环境变量的能力 -- 应用本身不包含数据或共享状态无状态、通过网络来访问数据,云引擎提供了托管的数据库和缓存服务 -- 应用可以在云引擎上完成构建过程,云引擎提供了可自定义的构建机制 -- 应用可以以多进程的方式运行,以便横向扩展 -- 应用本身是完整的、可运行的(而不需要嵌入一个宿主环境),通过网络对外提供 HTTP 服务 -- 应用只需将日志写入到标准输出,云引擎会将日志收集起来以便查看 - -:::note -云引擎很大程度上受到了 [12-Factor](https://12factor.net/zh_cn/) 的影响,你可以在它的官网上了解到更多构建现代化、可移植、易于扩展和维护的后端服务的方法论。 -::: - -## 构建 - -云引擎针对多种语言提供了构建支持,我们称之为「运行环境(Runtime)」,云引擎会根据项目代码的结构去判断项目的类型,例如根目录包含 `package.json` 就会被认为是 Node.js 项目。 - -在开发者使用 Git 或命令行工具将代码上传到云引擎后,云引擎就会开始一个「构建」的过程,云引擎会根据项目的运行环境安装依赖(如 `npm install`)、构建可执行文件(如 `go build`)、执行用户自定义的命令(可在 [leanengine.yaml](/sdk/engine/deep-dive/leanengine-yaml) 中配置)。 - -|install| +Source["完整代码\\n(package.json + *.ts)"] - +Source -->|build| Version["版本\\n(bundled.js)"] - Version -->|run| Instances1([实例\\nnode ./bundled.js]) - Version -->|run| Instances2([实例\\nnode ./bundled.js]) -`} -/> - -构建过程最后会生成一个「版本」用于后续的部署,其中包含了应用的源代码、下载的依赖和构建出的可执行文件(部分运行环境),版本有一个类似 20210913-150821 的编号,在应用内是唯一的。 - -### 构建缓存 - -为了加速构建的过程,云引擎采用了「分层缓存(Layered Cache)」的缓存机制,即如果一个步骤需要执行的操作没有变化,那么就不实际执行,直接采用前一次执行的结果(缓存);但如果某个步骤需要执行的操作发生了变化,那么从此之后的所有步骤都需要重新执行。举例来说如果项目的依赖清单(如 `package.json`)没有发生变化,那么就不会重新执行依赖安装,而是采用前一次安装好的结果,整个构建过程就会快很多。 - -:::caution -构建缓存只是一种加速构建的机制,云引擎并不保证依赖没有变化就一定能使用缓存。 -::: - -:::caution -如果在项目依赖的版本指定为某个范围,且两次部署之间依赖声明文件无改动,但该依赖发布了新版本(新版本仍在指定的范围之内),那么,由于缓存机制的存在,在缓存过期前,仍会安装之前的依赖版本,而非指定范围内的最新版本。 -如果希望这种情况下,始终安装指定范围内的最新版本依赖,那么需要使用 `--no-cache` 禁用缓存。 -::: - -### 资源限制 - -**构建过程** 的资源限制为: - -- **2 个 CPU 核心**: CPU 使用率最高为 200% -- **30 分钟 CPU 时间**: 如果构建命令消耗了太多 CPU 时间会被强制停止。 -- **4 GB 内存**: 如果在构建过程中遇到了 "Out of memory" 或 "memory allocation error" 错误,可能是消耗了超过 4 GB 的内存导致的,需要调整构建命令来使用更少的内存,比如限制并发执行的编译器进程数量。 - -## 实例和部署 - -构建好的版本可以被部署到「实例」上,实例对应着服务器上的一个容器(一组进程),实际提供计算能力,对外提供服务。 - -### 平滑部署 - -标准版云引擎在进行部署、重启或自动的实例迁移时,会逐个启动新的容器,在新的容器工作正常后,旧的容器仍会保持运行至少 30 秒的时间,以便将正在处理的请求执行完。通过这样的平滑部署机制,在云引擎上部署新版本或重启时,几乎不会有请求失败,应用的处理能力也不会有明显下降。 - -应用可以接收 `SIGTERM` 信号来在退出前进行自定义的清理操作,默认情况下在收到 `SIGTERM` 信号后会有至少 10 秒的时间可以用作退出前的清理操作。 - -### 休眠和唤醒 - -云引擎的体验版实例(包括赠送的生产环境或赠送的预备环境实例)在一段时间没有外部请求后会休眠(停止运行),在有外部请求时再启动,从休眠中启动可能需要十几秒的时间,同时体验版实例每天最多保持运行 18 个小时,超过 18 小时会被强制休眠,即使有外部请求也不会启动。 - -### 自动的实例迁移 - -除了开发者进行的部署或重启,因均衡宿主机负载或宿主机需下线维护等原因,系统有时也会自动地触发实例迁移操作,这时也会使用平滑部署机制,因此对应用的影响很小,但在日志中会打印实例重启的信息、CPU 和内存图表也可能会有波动,属正常现象。 - -## 云函数 - -云函数是一种经过高度封装的函数计算功能,实际功能由云引擎 SDK 提供,本质上是与用户程序运行在同一个进程、同一个 HTTP 端口上的 HTTP 服务。 - -Hook 的实现方式和云函数非常相似,区别在于使用了特殊的命名、调用者是内网的数据存储或即时通讯服务,同时 SDK 会对调用者的来源 IP 进行检查,确保来自内网。 - -一个应用下不同分组中的云函数和 Hook 都属于同一命名空间,这意味着不同的云函数可以由不同的分组提供、使用不同的语言进行开发。在部署应用到云引擎时,云引擎会和 SDK 通讯获取云函数的列表,得出云函数名字和分组的映射关系,后续负载均衡会根据这个映射关系将请求转发到正确的分组。 diff --git a/leancloud/docs/sdk/engine/deep-dive/leanengine-yaml.mdx b/leancloud/docs/sdk/engine/deep-dive/leanengine-yaml.mdx deleted file mode 100644 index 9466dc6bc..000000000 --- a/leancloud/docs/sdk/engine/deep-dive/leanengine-yaml.mdx +++ /dev/null @@ -1,127 +0,0 @@ ---- -title: 'Reference: leanengine.yaml' -sidebar_label: 'leanengine.yaml' ---- - -import { Conditional } from "/src/docComponents/conditional"; - -`leanengine.yaml` 是一个用来自定义云引擎线上运行环境的配置文件,需要放置在项目的根目录,使用 YAML 语法。 - -## `runtime` 覆盖运行环境 - -覆盖自动识别的运行环境,可选的值: - -- `cpp` -- `dotnet` -- `go` -- `java` -- `nodejs` -- `php` -- `python` -- `static`(Web 前端) - -## `run` 覆盖运行命令 - -```yaml -run: $(npm bin)/serve -c static.json -l ${LEANCLOUD_APP_PORT} -``` - -支持 Shell 语法(如引用环境变量等)。 - -## `install` 覆盖依赖安装命令 - -覆盖默认的依赖安装命令(如 `npm install`),或在安装依赖前后运行自定义命令,支持用数组来表示多条命令,支持 Shell 语法、引用环境变量。 - -多数运行环境有默认的依赖安装命令,可以用 `use: default` 来引用默认的命令: - -```yaml -install: - - use: default - - npm run install-additional -``` - -依赖安装步骤默认只会将依赖清单(如 `package.json` 等文件)加入构建目录,如需其他文件可以用 `require` 来引入: - -```yaml -install: - - require: - - frontend/package.json - - frontend/package-lock.json - - cd frontend && npm ci -``` - -## `build` 覆盖构建命令 - -```yaml -build: NODE_ENV=production $(npm bin)/webpack -``` - -像 `install` 一样用 `use: default` 来引用默认命令,支持用数组来表示多条命令,支持 Shell 语法、引用环境变量。 - -在构建阶段全部代码文件都已经被加入了构建目录,可以使用所有的文件。 - -## `systemDependencies` 系统级依赖 - -在部署时安装额外的系统级依赖: - -```yaml -systemDependencies: - - imagemagick -``` - -- `ffmpeg` 一个音视频处理工具库。 -- `imagemagick` 一个图片处理工具库。 -- `fonts-wqy` 文泉驿点阵宋体、文泉驿微米黑字体,通常和 `chrome-headless` 配合来显示中文。 -- `fonts-noto` 思源黑体字体(体积较大),通常和 `chrome-headless` 配合来显示中文。 -- ~~`phantomjs` 一个无 UI 的 WebKit 浏览器~~(该项目已停止维护)。 -- `chrome-headless` 一个无 UI 的 Chrome 浏览器(体积很大,会显著增加部署耗时,运行时也会消耗大量 CPU 和内存;如果使用 `puppeteer` 的话,需要给 `puppeteer.launch` 传递这些参数:`{executablePath: '/usr/bin/google-chrome', args: ['--no-sandbox', '--disable-setuid-sandbox']}`)。 -- `node-canvas` 安装 [node-canvas](https://github.com/Automattic/node-canvas) 所需要的系统级依赖(你仍需要自行安装 `node-canvas`)。 -- `python-talib` [TA-Lib](https://pypi.org/project/TA-Lib/) 所需的系统依赖(你仍需要自行安装 `TA-Lib`)。 - -:::caution -注意添加系统依赖将会显著增加部署耗时,因此请不要添加未用到的依赖。 -::: - -## `buildRoot` 构建根目录 - -指定代码包或仓库中的一个子目录进行构建,相当于只上传了这一个子目录的代码。 - -## `exposeEnvironmentsOnBuild` 在构建阶段使用环境变量 - -默认情况下,应用在运行阶段才能够读取到内置环境变量和自定义环境变量,设置该选项可以在安装依赖或编译阶段读取到这些环境变量。 - -```yaml -exposeEnvironmentsOnBuild: true -``` - -云引擎运行环境默认提供的环境变量(以及 Node.js 环境变量 NODE_ENV)无法被自定义环境变量覆盖。 - -## `startupTimeout` 启动超时 - -```yaml -startupTimeout: 60 -``` - -配置程序启动的超时时间,可设置 15 - 120 的值(秒) - -## `functionsMode` 云函数功能开关 - -控制项目是否使用云函数(Hook)特性。 - -```yaml -functionsMode: strict -``` - -设置为 `strict` 表示需要使用云函数特性,如获取云函数信息失败则中断部署;设置为 `disabled` 表示不开启云函数相关功能。 - -## ~~Node.js `installDevDependencies` 安装开发依赖~~ - -:::caution -已废弃,请使用 `package-lock.json`。 -::: - -```yaml -installDevDependencies: true -``` - -安装 `package.json` 中 `devDependencies` 部分的依赖。 diff --git a/leancloud/docs/sdk/engine/deploy/_category_.json b/leancloud/docs/sdk/engine/deploy/_category_.json deleted file mode 100644 index 41ef17570..000000000 --- a/leancloud/docs/sdk/engine/deploy/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "部署应用", - "collapsed": true, - "position": 3 -} diff --git a/leancloud/docs/sdk/engine/deploy/cpp.mdx b/leancloud/docs/sdk/engine/deploy/cpp.mdx deleted file mode 100644 index a8dbc4a66..000000000 --- a/leancloud/docs/sdk/engine/deploy/cpp.mdx +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: 云引擎 C++ 运行环境 -sidebar_label: C++ -sidebar_position: 10 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; - -:::info -这篇文档是针对 C++ 运行环境的深入介绍,如希望快速地开始使用云引擎,请查看 [快速开始部署云引擎应用](/sdk/engine/deploy/getting-started)。 -::: - -云引擎目前支持构建使用 Bazel 或 Makefile(CMake)的项目。 - -C++ 运行环境提供的编译器是 GCC 9.4。 - -## Bazel 项目 - -如果项目根目录存在 `WORKSPACE`,云引擎会默认使用 `bazel build -c opt //:all` 构建,`bazel run -c opt //:all` 来运行。 - -## Makefile(CMake)项目 - -如果项目根目录存在 `Makefile`,云引擎会使用 `make` 构建。 - -如果项目根目录存在 `CMakeLists.txt`,云引擎会先使用 `cmake .` 来生成 Makefile。 - -Makefile 项目没有默认的运行命令,需要在 `leanengine.yaml` 中自行设置运行命令: - -```yaml title='leanengine.yaml' -run: ./myapp -``` - -## 上传预编译的程序 - -你也可以选择预先编译好 binary 再上传到云引擎,我们建议在 Ubuntu 20.04 的环境下编译静态链接。 - -然后在 `leanengine.yaml` 中自行设置运行命令: - -```yaml title='leanengine.yaml' -runtime: cpp -run: ./myapp -``` - -:::info -云引擎的构建环境(如发行版版本)可能会发生变化,届时可能需要调整编译参数,但已在云引擎构建好的版本会将所有运行环境固化下来,可供持续运行或随时回滚。 -::: - -## 自定义构建过程 - - - -### 构建日志 - - - -## 健康检查 - - - -## 云端环境 - -### 绑定自定义域名 - - - -### 负载均衡和加速节点 - - - -### 环境变量 - - - -### 日志 - - - -### 时区 - - - -### 文件系统 - - - -### 出入口 IP 地址 - - diff --git a/leancloud/docs/sdk/engine/deploy/dotnet.mdx b/leancloud/docs/sdk/engine/deploy/dotnet.mdx deleted file mode 100644 index 02893bd4d..000000000 --- a/leancloud/docs/sdk/engine/deploy/dotnet.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: 云引擎 .NET 运行环境 -sidebar_label: .NET -sidebar_position: 8 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; - -:::info -这篇文档是针对 .NET 运行环境的深入介绍,如希望快速地开始使用云引擎,请查看 [快速开始部署云引擎应用](/sdk/engine/deploy/getting-started)。 -::: - -所有 .NET 项目都必须在根目录包含一个 `app.sln` 文件才会被云引擎正确识别,通常一个 .NET 项目的结构如下: - -``` -├── web -| ├── StartUp.cs -| ├── Program.cs -| ├── web.csproj -| └── wwwroot -| ├── css -| ├── lib -| └── js -├── app.sln -└── global.json -``` - -如果你希望创建一个新的项目,推荐从我们的 [.NET 示例项目](https://github.com/leancloud/dotnet-core-getting-started) 开始。 - -## 启动命令 - -在完成构建后,云引擎会通过 `dotnet release/web.dll` 来启动应用。 - -## .NET 版本 - -目前云引擎仅提供 .NET 3.1.100 版本。 - -## 安装依赖和构建 - -云引擎会在云端使用 `dotnet restore app.sln` 来安装依赖;使用 `dotnet publish -o release -c Release` 来进行构建。 - -## 自定义构建过程 - - - -### 构建日志 - - - -## 健康检查 - - - -## 云端环境 - -### 绑定自定义域名 - - - -### 负载均衡和加速节点 - - - -### 环境变量 - - - -### 日志 - - - -### 时区 - - - -### 文件系统 - - - -### 出入口 IP 地址 - - diff --git a/leancloud/docs/sdk/engine/deploy/getting-started.mdx b/leancloud/docs/sdk/engine/deploy/getting-started.mdx deleted file mode 100644 index 2ad7a1ac1..000000000 --- a/leancloud/docs/sdk/engine/deploy/getting-started.mdx +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: 快速开始部署云引擎应用 -sidebar_label: 快速开始 -sidebar_position: 1 ---- - -import QuickStartNew from "../_partials/quick-start-new.mdx"; -import QuickStartDeploy from "../_partials/quick-start-deploy.mdx"; -import PlatformIntroduction from "../_partials/platform-introduction.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import PlatformRuntimes from "../_partials/platform-runtimes.mdx"; -import { CLI_BINARY } from "/src/constants/env.ts"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; - -:::info -如果仅希望使用云函数和 Hook 而不是部署通用的后端程序,请看 [快速开始部署云函数和 Hook](/sdk/engine/functions/getting-started)。 - -如希望部署 Web 前端应用,请看 [Web 前端运行环境 § 快速开始](/sdk/engine/deploy/webapp/#快速开始)。 -::: - - - -## 创建项目 - -如果你想要快速开始新项目,推荐基于我们的示例项目来开始部署第一个应用。 - - - -## 绑定已有项目 - -

    - 要将一个已有的项目关联到云引擎应用,可以使用 {CLI_BINARY} switch: -

    - - - {`$ ${CLI_BINARY} switch -[?] Please select an app: - 1) my-engine-app - => 1 -Switching to my-engine-app (group: web)`} - - - - -## 本地运行和调试 - -你可以使用这个语言的 Web 框架来定义路由,处理某一路径下的请求,在示例项目中可以看到一些例子: - - - - -```javascript title='app.js' -app.get("/", function (req, res) { - res.render("index", { currentTime: new Date() }); -}); -``` - - - - -```python title='app.py' -@app.route('/') -def index(): - return render_template('index.html') -``` - - - - -```php title='src/app.php' -$app->get('/', function (Request $request, Response $response) { - return $this->view->render($response, "index.phtml", array( - "currentTime" => new \DateTime(), - )); -}); -``` - - - - -```java title='src/main/webapp/WEB-INF/web.xml' - - index.html - -``` - - - - -```cs title='web/Startup.cs' -app.UseEndpoints(endpoints => { - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); -}) -``` - - - - -```go title='main.go' -e.GET("/", routes.Index) -``` - -```go title='routes/index.go' -func Index(c echo.Context) error { - return c.Render(http.StatusOK, "index", time.Now().String()) -} -``` - - - - -在确保所有的依赖都正确安装之后,就可以在项目根目录用我们的命令行工具来启动本地运行了: - -{`$ ${CLI_BINARY} up`} - -更多有关命令行工具和本地调试的内容请看 [云引擎命令行工具使用指南](/sdk/engine/cli/)。 - -## 部署到云引擎 - - - - - -例如你在控制台绑定了 `web.example.com` 这个域名,即可通过 `https://web.example.com` 访问你的应用(生产环境)。 - -## 更多 - -接下来可以查看 [云引擎平台功能](/sdk/engine/platform) 来了解云引擎提供的更多功能,或查看专门的页面来了解具体运行环境的详情: - - diff --git a/leancloud/docs/sdk/engine/deploy/go.mdx b/leancloud/docs/sdk/engine/deploy/go.mdx deleted file mode 100644 index 85ee5eb9c..000000000 --- a/leancloud/docs/sdk/engine/deploy/go.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: 云引擎 Go 运行环境 -sidebar_label: Go -sidebar_position: 9 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; - -:::info -这篇文档是针对 Go 运行环境的深入介绍,如希望快速地开始使用云引擎,请查看 [快速开始部署云引擎应用](/sdk/engine/deploy/getting-started)。 -::: - -云引擎目前只支持通过 Go Modules 管理依赖的项目。 - -所有 Go 项目都必须在根目录下包含一个 `go.mod` 文件才会被云引擎正确识别。 - -## 构建和启动命令 - -云引擎默认使用 `go build -o main` 进行构建,然后执行构建出的可执行文件(`main`),你可以在 `leanengine.yaml` 中来自定义构建和启动命令: - -```yaml title='leanengine.yaml' -build: go build -o myapp -run: ./myapp -``` - -## 配置 Go 版本 - -云引擎会从 `go.mod` 中读取 Go 的版本: - -```plain title='go.mod' -go 1.14 -``` - -:::note -如未设置 Go 版本,云引擎会默认使用最新的稳定版本。 -::: - -## 安装依赖(`go.mod`) - -云引擎会自动安装 `go.mod` 和 `go.sum` 中列出的依赖。 - -## 自定义构建过程 - - - -### 构建日志 - - - -## 健康检查 - - - -## 云端环境 - -### 绑定自定义域名 - - - -### 负载均衡和加速节点 - - - -### 环境变量 - - - -### 日志 - - - -### 时区 - - - -### 文件系统 - - - -### 出入口 IP 地址 - - diff --git a/leancloud/docs/sdk/engine/deploy/java.mdx b/leancloud/docs/sdk/engine/deploy/java.mdx deleted file mode 100644 index bbcd8fd4b..000000000 --- a/leancloud/docs/sdk/engine/deploy/java.mdx +++ /dev/null @@ -1,236 +0,0 @@ ---- -title: 云引擎 Java 运行环境 -sidebar_label: Java -sidebar_position: 6 ---- - -import { CLI_BINARY } from "/src/constants/env.ts"; -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; -import CodeBlock from "@theme/CodeBlock"; -import { Conditional } from "/src/docComponents/conditional"; - -:::info -这篇文档是针对 Java 运行环境的深入介绍,如希望快速地开始使用云引擎,请查看 [快速开始部署云引擎应用](/sdk/engine/deploy/getting-started)。 -::: - -云引擎目前支持使用 Maven 或 Gradle 构建出的 WAR 或 JAR 项目。也支持直接上传 WAR 包。 - -如果要开始一个新的项目,建议从我们的示例项目开始: - -- [Servlet 示例项目](https://github.com/leancloud/servlet-getting-started) -- [Spring Boot 示例项目](https://github.com/leancloud/spring-boot-getting-started) - -:::caution -Java 对内存的需求较高,体验版实例的 256M 内存可能会导致 Java 进程启动时内存不足而崩溃(OOM)导致部署失败,或运行时内存不足而频繁重启。 - -我们建议 Java 项目至少选用 512 MB 以上的内存,Spring Boot 项目至少选用 1024 MB 以上的内存,并在之后的运行过程中根据内存用量统计随时调整。调整内存规格的方法详见 [云引擎平台功能 § 调整实例规格和数量](/sdk/engine/platform/#调整实例规格和数量)。 -::: - -## 启动命令 - -在完成构建后,云引擎会在 `target` 和 `build` 目录下查找 `.war` 或者 `.jar` 文件: - -- 如果找到 `.war` 会将其放入 Servlet 容器(Jetty 9.x)来运行 -- 如果找到 `.jar` 会通过 `java -jar` 来运行 - -### 配置 JVM 参数 - -云引擎运行 Java 应用时,会自动将 `-Xmx` 参数设置为实例规格的 70%,剩下的 30% 留给堆外内存和其他开销。如果你的应用比较特殊(比如大量使用堆外内存)可以自己定制 `-Xmx` 参数。假设使用 2 GB 内存规格的实例运行,则可以在云引擎的设置页面增加「自定义环境变量」,名称为 `JAVA_OPTS`,值为 `-Xmx1500m`,这样会限制 JVM 堆最大为 1.5 GB,剩下 500 MB 留给持久代、堆外内存或者其他一些杂项使用。**注意:`-Xmx` 参数如果设置得过小可能会导致大量 CPU 消耗在反复的 GC 任务上。** - -## 配置 Java 版本 - -在项目根目录创建一个 `system.properties` 即可配置 Java 的版本: - -```plain title='system.properties' -java.runtime.version=11 -``` - -目前云引擎支持的版本有 AdoptOpenJDK `8`、`11`、`12`、`13`、`14`。 - -:::note - -对于新创建的应用,如未设置 Java -版本,云引擎会默认使用支持的版本中最新的稳定版本(LTS)。 - - 在 2021-09-02 之前创建的分组因兼容考虑会默认使用 Java `8`。 - -::: - -## 直接上传 WAR 包 - -在本地构建(如使用 `mvn package`)出 WAR 包后,可以在使用命令行工具部署时添加 `--war` 选项表示上传 WAR 包而不是源代码: - -{`${CLI_BINARY} deploy --war`} - -这种情况下在云端不会有安装依赖和构建的过程,WAR 包会被直接放入 Servlet 容器运行。 - -## 安装依赖和构建 - -如果上传了源代码,云引擎会自动安装依赖并构建。 - -云引擎会根据项目根目录是否存在 `pom.xml` 或 `gradlew` 文件来判断所使用的包管理器,然后使用对应的 Maven 或 Gradle 命令来安装依赖并构建。 - -## 自定义构建过程 - - - -#### 只上传 jar 包而不在线上进行构建 - -```yaml title='leanengine.yaml' -runtime: java -install: [] -build: [] -run: java -jar your-package.jar -``` - -在这里我们指定了使用 Java 运行时、将依赖安装和构建步骤覆盖为空,然后直接指定了运行命令(运行项目根目录下的 `your-package.jar`)。 - - - -### 构建日志 - - - -## 健康检查 - - - -## 云端环境 - -### 绑定自定义域名 - - - -### 负载均衡和加速节点 - - - -### 环境变量 - - - -### 日志 - - - -### 时区 - - - -### 文件系统 - - - -### 出入口 IP 地址 - - - -## 疑难问题 - -### 如何脱离命令行工具本地启动云引擎 Java 项目? - -设置云引擎运行需要的环境变量后,可以通过脱离命令行工具,直接运行相应命令或使用 IDE 本地启动 Java 项目。 - -通过命令行启动 Jetty 项目或 JAR 项目,先设置环境变量: - -```sh -eval "$(lean env)" -``` - -提示:命令 `lean env` 可以输出当前应用所需环境变量的设置语句,外层的 `eval` 是直接执行这些语句。 -Windows 系统下需要手动设置 `lean env` 输出的环境变量。 - -如果是 Jetty 项目,运行: - -``` -mvn jetty:run -``` - -如果是 JAR 项目,使用 Maven 打包项目并运行: - -```sh -mvn package -java -jar target/{zipped jar file} -``` - -使用 Eclipse 启动应用: - -首先确保 Eclipse 已经安装 Maven 插件,并将项目以 **Maven Project** 方式导入 Eclipse 中。 - -在 **Package Explorer** 视图右键点击项目: - -- 如果是 Jetty 项目,选择 **Run As** > **Maven build…**,将 **Main** 标签页的 **Goals** 设置为 `jetty:run`。 -- 如果是 JAR 项目,选择 **Run As** > **Run Configurations…**,选择 `Application`,设置 `Main class:`(示例项目为 `cn.leancloud.demo.todo.Application`)。 - -最后在 **Environment** 标签页增加以下环境变量和相应的值: - -| 名称 | 值 | -| -------------------------- | --------------- | -| `LEANCLOUD_APP_ENV` | `development` | -| `LEANCLOUD_APP_ID` | `{{appid}}` | -| `LEANCLOUD_APP_KEY` | `{{appkey}}` | -| `LEANCLOUD_APP_MASTER_KEY` | `{{masterkey}}` | -| `LEANCLOUD_APP_PORT` | `3000` | - -配置完成后,以后只需点击 run 按钮即可启动应用。 - -### 如何在云引擎中依赖内部 library(也称为「二方库」)? - -云引擎构建环境只能访问公开的程序库(library),如果你项目中使用了一些公司内部的依赖库,可以按照如下方式进行引用: - -1. 首先在项目根目录下新建 libs 目录,把所有依赖的 jar 文件拷贝进来; -2. 然后在项目根目录下新建 leanengine.yaml 文件,并自定义 install 环节(详见下文示例); -3. 最后修改 pom.xml 中依赖项和 `spring-boot-maven-plugin` 配置,增加 `includeSystemScope` 设置项(详见下文示例); - -最终的工程目录结构如下: - -``` -{root} -|---libs -| |- yourdependency.jar etc. -|---leanengine.yaml -\---pom.xml -``` - -leanengine.yaml 内容如下: - -```yaml -install: - - require: - - libs - - { use: "default" } -``` - -pom.xml 中增加依赖项目: - -```xml - - com.sample - sample - 1.0 - system - ${project.basedir}/libs/yourdependency.jar - -``` - -pom.xml 中对 `spring-boot-maven-plugin` 改动如下: - -```xml - - org.springframework.boot - spring-boot-maven-plugin - - true - true - - -``` diff --git a/leancloud/docs/sdk/engine/deploy/nodejs.mdx b/leancloud/docs/sdk/engine/deploy/nodejs.mdx deleted file mode 100644 index c4cc8b606..000000000 --- a/leancloud/docs/sdk/engine/deploy/nodejs.mdx +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: 云引擎 Node.js 运行环境 -sidebar_label: Node.js -sidebar_position: 4 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import NodejsSetupRuntime from "../_partials/nodejs-setup-runtime.mdx"; -import NodejsSetupPackageManager from "../_partials/nodejs-setup-package-mamager.mdx"; -import NodejsSetupDependencies from "../_partials/nodejs-setup-dependencies.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; - -:::info -这篇文档是针对 Node.js 运行环境的深入介绍,如希望快速地开始使用云引擎,请查看 [快速开始部署云引擎应用](/sdk/engine/deploy/getting-started)。 -::: - -所有 Node.js 项目都必须在根目录包含一个 [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json) 和启动脚本(如没有在控制台或 `leanengine.yaml` 内指定,请确保 `package.json` 内的 `scripts.start` 存在)才会被云引擎正确识别,云引擎也会从中读取项目对于环境的需求: - -```json title='package.json' -{ - "name": "node-js-getting-started", - "scripts": { - "start": "node server.js", - "dev": "nodemon server.js -- " - }, - "dependencies": { - "leancloud-storage": "^3.11.0", - "leanengine": "^3.3.2" - }, - "devDependencies": { - "nodemon": "^1.18.7" - }, - "engines": { - "node": "16.x" - } -} -``` - -如果你希望创建一个新的项目,推荐从我们的 [Node.js 示例项目](https://github.com/leancloud/node-js-getting-started) 开始。 - -## 启动命令 - -云引擎默认使用 `npm start` 来启动项目,你可以在 `package.json` 中修改 `scripts.start` 来使用不同的程序入口或向 `node` 添加额外的参数。 - -```json title='package.json' -{ - "scripts": { - "start": "node server.js" - } -} -``` - -:::note -在使用 [命令行工具](/sdk/engine/cli) 本地调试时,`lean up` 会优先使用 `npm run dev` 来启动项目。 -::: - -## 配置 Node.js 版本 - - - -## 配置包管理器 - - - -## 安装依赖(`package.json`) - - - -## 自定义构建过程 - - - -#### 为子项目安装依赖 - -```yaml title='leanengine.yaml' -install: - - use: default - - require: - - ./frontend/package.json - - ./frontend/package-lock.json - - cd frontend && npm ci -build: - - npm run build - - cd frontend && run build -``` - -这里我们在保留默认行为的同时,额外为 `frontend` 目录下的项目安装了依赖、运行了构建命令。 - - - -### 构建日志 - - - -## 健康检查 - - - -## 云端环境 - -### 绑定自定义域名 - - - -### 负载均衡和加速节点 - - - -### 环境变量 - - - -### 日志 - - - -### 时区 - - - -### 文件系统 - - - -### 出入口 IP 地址 - - - -## 疑难问题 - -### Node.js 项目的 `devDependencies` 没有安装? - -云引擎会在部署时用 `npm ci` 为你安装项目依赖,包括 `devDependencies`。 -不过,如果项目的 Node.js 版本小于 10,或者项目目录下没有 lockfile,则会使用 `npm install --production` 安装依赖,相应地,`devDependencies` 中列出的依赖**不会**安装,可参考 [默认命令](/sdk/engine/deploy/nodejs/#默认命令)。 -如需安装 `devDependencies`,请在项目的 `leanengine.yaml` 中指定 `installDevDependencies: true`。 - -### `npm ERR! peer dep missing` 错误怎么办? - -部署时出现类似错误: - -``` -npm ERR! peer dep missing: graphql@^0.10.0 || ^0.11.0, required by express-graphql@0.6.11 -``` - -说明有一部分 peer dependency 没有安装成功,因为 Node.js 版本小于 10 时,线上只会安装 dependencies 部分的依赖,所以请确保 dependencies 部分依赖所需要的所有依赖也都列在了 dependencies 部分(而不是 devDependencies)。 - -你可以在本地删除 node_modules,然后用 `npm install --production` 重新安装依赖来重现这个问题。 - -或者,你也可以考虑将项目升级到 Node.js 10 以上的版本。 - -#### 路由超时设置 - -因为 Node.js 的异步调用容易因运行时错误或编码疏忽中断,为了减少在这种情况下对服务器内存的占用,也为了客户端能够更早地收到错误提示,所以需要添加这个设置,一旦发生超时,服务端会返回一个 HTTP 错误码给客户端。 - -使用 Express 框架实现自定义路由的时候,请求默认的超时时间为 15 秒,该值可以在 `app.js` 中进行调整: - -```js -// 设置默认超时时间 -app.use(timeout("15s")); -``` diff --git a/leancloud/docs/sdk/engine/deploy/php.mdx b/leancloud/docs/sdk/engine/deploy/php.mdx deleted file mode 100644 index 09e8e7d8a..000000000 --- a/leancloud/docs/sdk/engine/deploy/php.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: 云引擎 PHP 运行环境 -sidebar_label: PHP -sidebar_position: 7 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; -import { Conditional } from "/src/docComponents/conditional"; - -:::info -这篇文档是针对 PHP 运行环境的深入介绍,如希望快速地开始使用云引擎,请查看 [快速开始部署云引擎应用](/sdk/engine/deploy/getting-started)。 -::: - -所有 PHP 项目必须在根目录包含一个 `composer.json` 和 `public/index.php` 才会被云引擎正确识别。 - -如果你希望创建一个新的项目,推荐从我们的 [PHP 示例项目](https://github.com/leancloud/slim-getting-started) 开始。 - -## 运行机制 - -云引擎会使用 Nginx 和 PHP-FPM 来运行你的应用,项目中的 `public` 目录会被映射为网站的根目录(document root),其中 `.php` 文件由 PHP-FPM 处理,其他静态文件由 Nginx 处理。如果被请求的路径不存在则会由 `public/index.php` 处理,这一点可以满足绝大部分框架对应用入口点的需求。 - -云引擎默认每 64 MB 内存分配一个 PHP-FPM Worker,如果希望自定义 Worker 数量,可以在云引擎设置页面的「自定义环境变量」中添加名为 `PHP_WORKERS` 的环境变量,值是一个数字。设置过低会导致收到新请求时无可用的 Worker;过高会导致内存不足、请求处理失败,建议谨慎调整。 - -## 配置 PHP 版本 - -在 `composer.json` 中可以指定 PHP 的版本: - -```json -"require": { - "php": "8.0" -} -``` - -目前云引擎支持的版本有:`5.6`、`7.0`、`7.1`、`7.2`、`7.3`、`7.4`、`8.0`。 - -:::note - -对于新创建的应用,如未设置 PHP -版本,云引擎会默认使用最新的稳定版本。 - - 在 2021-09-02 之前创建的分组因兼容考虑会默认使用 `5.6` 版本。 - - -::: - -## 安装依赖(`composer.json`) - -云引擎会自动安装 `composer.json` 中的依赖,目前云引擎云端使用的是 Composer 1.x 版本。 - -## PHP 扩展 - -所有版本的 PHP 默认开启 `fpm`、`curl`、`mysql`、`zip`、`xml`、`mbstring`、`gd`、`soap`、`sqlite3`。 - -7.0 以上版本默认开启 `mongodb`。 - -在 PHP 7.2 中官方从核心中移除了 `mcrypt` 这个拓展,云引擎以选装的方式继续提供支持,在 `composer.json` 的 `require` 中加入 `ext-mcrypt: *` 即可,使用 `mcrypt` 会增加部署耗时,如果没有用到请不要加。 - -## 自定义构建过程 - - - -### 构建日志 - - - -## 健康检查 - - - -## 云端环境 - -### 绑定自定义域名 - - - -### 负载均衡和加速节点 - - - -### 环境变量 - - - -### 日志 - - - -### 时区 - - - -### 文件系统 - - - -### 出入口 IP 地址 - - - -## 疑难问题 - -### 是否支持 `path` 类型的 composer 本地仓库? - -由于构建时会复制 `composer.json` 和 `composer.lock` 到专门的目录安装依赖,因此不支持 `path` 类型的 composer 本地仓库。 -如果你的项目使用了 `path` 类型的本地仓库,我们建议改为 `vcs` 类型。 - -### PHP 项目从 files.phpcomposer.com 下载文件失败,部署失败怎么办? - -phpcomposer.com 镜像已经停止服务,PHP 项目的 `composer.lock` 文件如果包含了这个地址的 url,会导致依赖安装失败。 -解决方法有两种: - -1. 移除 `composer.lock` 后再部署(云引擎会直接根据 `composer.json` 安装依赖)。 -2. 在本地正确配置仓库地址后,运行 `composer update --lock` 更新 `composer.lock` 文件中的下载链接(不改变具体的版本)。 diff --git a/leancloud/docs/sdk/engine/deploy/python.mdx b/leancloud/docs/sdk/engine/deploy/python.mdx deleted file mode 100644 index 9449f2b00..000000000 --- a/leancloud/docs/sdk/engine/deploy/python.mdx +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: 云引擎 Python 运行环境 -sidebar_label: Python -sidebar_position: 5 ---- - -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import { Conditional } from "/src/docComponents/conditional"; - -:::info -这篇文档是针对 Python 运行环境的深入介绍,如希望快速地开始使用云引擎,请查看 [快速开始部署云引擎应用](/sdk/engine/deploy/getting-started)。 -::: - -所有 Python 项目都必须在根目录下包含有 `wsgi.py` 和 `requirements.txt` 文件才会被云引擎正确识别。 - -云引擎默认使用 WSGI 来运行 Python 项目,运行时会首先加载 `wsgi.py` 这个模块,并将此模块的全局变量 `application` 作为 WSGI 函数进行调用。因此请保证 `wsgi.py` 文件中包含一个 `application` 的全局变量/函数/类,并且符合 [WSGI 规范](https://peps.python.org/pep-0333/): - -```python title='app.py' -from flask import Flask -app = Flask(__name__) -@app.route('/') -def index(): - return "hi" -``` - -```python title='wsgi.py' -from app import app -application = app -``` - -流行的 Python Web 框架对 WSGI 都有支持,比如 [Flask](https://flask.palletsprojects.com/en/)、[Django](https://www.djangoproject.com)、[Tornado](https://www.tornadoweb.org/en/stable/)。我们提供了 Flask 和 Django 两个框架的示例项目作为参考,你也可以直接把它们当作一个应用项目的初始化模版: - -- [Flask](https://github.com/leancloud/python-getting-started) -- [Django](https://github.com/leancloud/django-getting-started) - -## 非 WSGI 运行 - -云引擎也支持直接运行 Python 程序而不使用 WSGI(或者自行来运行 WSGI Server),你可以创建一个 `leanengine.yaml` 文件,在其中设置: - -```yaml title='leanengine.yaml' -run: python app.py -``` - -这种情况下你的应用需要自行监听环境变量 `LEANCLOUD_APP_PORT` 中的端口来提供 HTTP 服务。 - -## 配置 Python 版本 - -云引擎兼容 [pyenv](https://github.com/pyenv/pyenv) 的 `.python-version` 文件,你可以将它放在项目根目录来指定版本: - -```plain title='.python-version' -3.10 -``` - -这样将代码部署到云端时,云引擎就会自动安装对应的 Python 版本。 - -云引擎目前仅支持 CPython 版本,暂时不支持 PyPy、Jython、IronPython 等其他 Python 实现。 - -:::note - -对于新创建的应用,如未设置 Python -版本,云引擎会默认使用最新的稳定版本。 - - 在 2021-09-02 之前创建的分组因兼容考虑会默认使用 `2.7` 版本。 - -::: - -:::note -如果在本地开发时已使用了 `pyenv`,`pyenv` 也会根据此文件来自动使用对应的 Python 运行项目。我们建议本地开发使用 `pyenv`,以保证本地环境与线上相同。`pyenv` 的安装方法请参考 [pyenv 的 GitHub 仓库](https://github.com/pyenv/pyenv)。 -::: - -## 安装依赖(`requirements.txt`) - -云引擎会使用 `pip` 来安装 [requirements.txt](https://pip.pypa.io/en/stable/user_guide/#requirements-files) 中的包: - -```plain title='requirements.txt' -leancloud>=2.9.1,<3.0.0 -Flask>=1.0.0 -``` - -:::note -建议将依赖的包的版本都按照 `leancloud>=2.9.1,<3.0.0` 这种格式来明确包的大版本,防止因为包的大版本中的不兼容改动导致再次部署应用时出现问题。 -::: - -## 自定义构建过程 - - - -#### 使用 uWSGI 运行项目 - -```yaml title='leanengine.yaml' -run: uwsgi --gevent 5000 --http :3000 --wsgi-file wsgi.py --master --process=${LEANCLOUD_AVAILABLE_CPUS} --disable-log -``` - - - -### 构建日志 - - - -## 健康检查 - - - -## 云端环境 - -### 绑定自定义域名 - - - -### 负载均衡和加速节点 - - - -### 环境变量 - - - -### 日志 - - - -### 时区 - - - -### 文件系统 - - - -### 出入口 IP 地址 - - diff --git a/leancloud/docs/sdk/engine/deploy/webapp.mdx b/leancloud/docs/sdk/engine/deploy/webapp.mdx deleted file mode 100644 index 3c3d35564..000000000 --- a/leancloud/docs/sdk/engine/deploy/webapp.mdx +++ /dev/null @@ -1,157 +0,0 @@ ---- -title: 云引擎 Web 前端运行环境 -sidebar_label: Web 前端 -sidebar_position: 3 ---- - -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import NodejsSetupRuntime from "../_partials/nodejs-setup-runtime.mdx"; -import NodejsSetupPackageManager from "../_partials/nodejs-setup-package-mamager.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import NodejsSetupDependencies from "../_partials/nodejs-setup-dependencies.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -import QuickStartDeploy from "../_partials/quick-start-deploy.mdx"; - -云引擎同样对托管 Web 前端应用(例如一个网站)提供了支持,对于使用了 React、Vue 等框架的应用,云引擎可以在线上完成构建的过程,开发者不需要将构建产物提交进 Git 仓库也不需要额外的 CI 环境。云引擎还提供了自定义域名绑定、自动申请 SSL 证书、重定向到 HTTPS 等常用的功能,减轻前端开发者在部署和运维环节的工作量。 - -:::info -这篇文档是针对 Web 前端运行环境的介绍,如需了解云引擎平台提供的功能,请看 [云引擎平台功能](/sdk/engine/platform)。 -::: - -如果项目根目录包含一个 `static.json` 或 `index.html`,云引擎就会将其识别为 Web 前端项目,使用 Node.js 运行环境进行构建,然后自动使用 [serve](https://www.npmjs.com/package/serve) 来启动一个 HTTP 服务器。 - -## 快速开始 - -大多前端脚手架都可以通过简单地配置运行在云引擎上,推荐使用它们来创建新的项目。 - - - - -[create-react-app](https://create-react-app.dev/) 提供了开箱即用的 React 工具链,会自动配置好 React 的构建工具链,让开发者能专注在核心功能上: - -```sh -npx create-react-app react-for-engine --use-npm -``` - -然后切换到项目目录(上面的例子中是 `react-for-engine`)创建一个配置文件 `static.json` 将不存在的 URL 都重写到 `index.html`,以便我们的单页应用可以使用自己的前端路由(如 `react-router`): - -```json title='static.json' -{ - "public": "build", - "rewrites": [{ "source": "**", "destination": "/index.html" }] -} -``` - -再创建一个 `leanengine.yaml` 来配置构建命令: - -```yaml title='leanengine.yaml' -build: npm run build -``` - - - - -可以使用官方的 [Vue CLI](https://cli.vuejs.org/): - -```sh -npm install -g @vue/cli -vue create vue-for-engine -``` - -然后切换到项目目录(上面的例子中是 `vue-for-engine`)创建一个配置文件 `static.json` 将不存在的 URL 都重写到 `index.html`,以便我们的单页应用可以使用自己的前端路由(如 `vue-router`): - -```json title='static.json' -{ - "public": "dist", - "rewrites": [{ "source": "**", "destination": "/index.html" }] -} -``` - -再创建一个 `leanengine.yaml` 来配置构建命令: - -```yaml title='leanengine.yaml' -build: npm run build -``` - - - - -可以使用官方的 [create-next-app](https://nextjs.org/docs/api-reference/create-next-app) 创建项目: - -``` -npx create-next-app next-for-engine --use-npm -``` - -然后切换到项目目录(上面的例子中是 `next-for-engine`),创建一个 `leanengine.yaml` 来配置构建命令: - -```yaml title='leanengine.yaml' -build: npm run build -``` - -:::info - -为了能使用 Next.js 提供的 [API Routes](https://nextjs.org/docs/api-routes/introduction) 等功能,这里实际是运行在 [Node.js 运行环境](/sdk/engine/deploy/nodejs/)(使用 `npm start` 启动 [Next.js production server](https://nextjs.org/docs/api-reference/cli#production)),因此下文中关于配置 serve 的部分并不适用于 Next.js。 - -::: - - - - -云引擎可以在线上完成构建过程,开发者不需要将构建产物提交进 Git 仓库,也不需要额外的 CI 环境。 - -### 部署到云引擎 - - - -## 配置 Node.js 版本 - - - -## 配置包管理器 - - - -## 安装依赖(`package.json`) - - - -## 配置 serve - -你可以在项目根目录创建一个 `static.json` 来配置 serve 的行为。 - -```json title='static.json' -{ - "public": "build", // 在 build 目录而不是项目根目录启动网站 - "rewrites": [ - { "source": "**", "destination": "/index.html" } // 将所有不存在的文件的请求重定向到 index.html(适用大部分单页面应用) - ] -} -``` - -更多 serve 的选项和用法见 [serve-handler · Options](https://github.com/vercel/serve-handler#options)。 - -## 自定义构建过程 - - - -### 构建日志 - - - -## 云端环境 - -### 绑定自定义域名 - - - -### 负载均衡和加速节点 - - - -### 时区 - - diff --git a/leancloud/docs/sdk/engine/faq.mdx b/leancloud/docs/sdk/engine/faq.mdx deleted file mode 100644 index 306a9af79..000000000 --- a/leancloud/docs/sdk/engine/faq.mdx +++ /dev/null @@ -1,262 +0,0 @@ ---- -title: 云引擎常见问题 -sidebar_label: 常见问题 -sidebar_position: 10 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -## 云引擎功能 - -### 云引擎都支持哪些语言? - -目前支持 Node.js、Python、Java、PHP、.NET、Go 运行环境,也支持基于 Node.js 的 Web 前端项目,详见 [云引擎服务总览](/sdk/engine/overview/)。 - -如果你还需要其他运行环境的支持,欢迎向我们反馈。 - -### 云引擎支持托管纯静态网站吗? - -支持,请看 [云引擎 Web 前端应用运行环境](/sdk/engine/deploy/webapp/)。 - -### 云引擎支持 HTTPS 吗? - -支持,在绑定自定义域名时可以上传 SSL 证书或自动管理证书。 - -绑定自定义域名时还可以配置「强制 HTTPS」来实现自动跳转。 - -### 部署更新云引擎会导致服务中断吗? - -服务不会中断。在代码部署时,系统会优先启动使用新版本代码的实例,待新实例通过了健康检查,系统修改路由将请求转发至新实例后,再关闭旧版本的实例,让服务保持零中断。 - -### 云引擎和云函数是什么关系? - -云引擎是一个托管后端服务的平台,适用于所有的 Web 后端应用,对于程序内部的逻辑没有侵入。 - -在此基础上,开发者可以选择在程序内接入云引擎的 SDK,来使用云函数和 Hook 等功能,云函数与 [数据存储](/sdk/storage/features)[数据存储](/sdk/storage/overview/) 服务有深度的整合,对于已经在使用数据存储服务的开发会非常方便。 - -不接入云引擎 SDK 也可以使用云函数以外的所有功能,云引擎也提供了业界广泛使用的 [Redis](/sdk/engine/database/redis/)、[MongoDB](/sdk/engine/database/mongo/) 和 [Elasticsearch](/sdk/engine/database/es/) 供开发者存储数据。 - -## 云函数 - -### 云函数有哪些限制? - -云函数是 TDSLeanCloud 提供的一个 **相对受限** 的自定义服务器端逻辑的功能,和我们的 SDK 有比较 **深度的集成**。我们将云函数设计为一种类似 **RPC** 的机制,在云函数中你只能关注参数和结果,而不能自定义超时时间、HTTP method、URL,不能读取和设置 Header。 - -如果希望更加自由地使用这些 HTTP 的语义化功能,或者希望使用第三方的框架提供标准的 RESTful API,请在你的应用中自行来处理 HTTP 请求而不是使用云函数。 - -### 云函数可以同时存在于多个分组么? - -云引擎会自动将一个应用下的云函数请求转发到正确的分组,因此你可以同时在多个分组下使用云函数。 - -### 项目部署成功了,但云函数和 Hook 不可用? - -为了支持云引擎的云函数和 Hook 功能,云引擎的管理程序会使用 `/1.1/functions/_ops/metadatas` 这个 URL 和 SDK 交互,请确保将这个 URL 交给 SDK 处理。 -默认情况下,云引擎会尝试从 `/1.1/functions/_ops/metadatas` 获取云函数和 Hook 的元信息,如果失败,则云函数和 Hook 功能不可用,但不会中断部署。 -如果希望在获取元信息失败后中断部署,可以在 `leanengine.yaml` 文件中指定 `functionsMode` 为 `strict`。 -如果应用不使用云函数和 Hook 功能,那么你可以: - -- 在 `leanengine.yaml` 中不指定 `functionsMode`,同时 `/1.1/functions/_ops/metadatas` **返回一个 HTTP `404`** 表示不使用云函数和 Hook 相关的功能; -- 或者在 `leanengine.yaml` 中指定 `functionsMode` 为 `disabled`。注意,这种情况下,即使应用代码中定义了云函数和 Hook,Hook 也不会生效,云函数调用(通过 SDK 发起远程调用或通过 REST API 向 API 域名发起云函数调用)有可能因为被转发到错误的云引擎分组而失败。 - -### 部署中断,提示有同名云函数怎么办? - -云引擎支持多个分组。 -如果当前部署代码中部分云函数与其他组的同名,默认情况会提示错误并中断部署,防止意外重复定义云函数。 -我们建议你移除不需要的云函数,毕竟重复定义的云函数并不易于理解和维护。 -不过,你也可以通过在每次部署时额外指定 `--overwrite-functions` 参数强制替换其他组云函数的实现。 - -### 为什么 Class Hook 没有被运行? - -首先确认一下 Hook 被调用的时机是否与你的理解一致: - -- `beforeSave`:对象保存或创建之前 -- `afterSave`:对象保存或创建之后 -- `beforeUpdate`:对象更新之前 -- `afterUpdate`:对象更新之后 -- `beforeDelete`:对象删除之前 -- `afterDelete`:对象删除之后 -- `onVerified`:用户通过邮箱或手机验证后 -- `onLogin`:用户在进行登录操作时(`become(sessionToken)` 不是登录操作,因此不会调用 `onLogin`) - -还需注意在本地进行云引擎调试时,运行的会是线上预备环境的 Hook,如果没有预备环境则不会运行。 - -然后检查 Hook 函数是否被执行过: - -可以先在 Hook 函数的入口打印一行日志,然后进行操作,再到云引擎日志中检查该行日志是否被打印出来,如果没有看到日志原因可能包括: - -- 代码没有被部署到正确的应用 -- 代码没有被部署到生产环境(或没有部署成功) -- Hook 的类名不正确 - -如果日志已打出,则继续检查函数是否成功,检查控制台上是否有错误信息被打印出。如果是 before 类 Hook,需要保证 Hook 函数在 15 秒内结束,否则会被系统认为超时。 - -after 类 Hook 超时时间为 3 秒,如果你的体验实例已经休眠,很可能因为启动时间过长无法收到 after 类 Hook,建议升级到云引擎的标准实例避免休眠。 - -### 可以在云函数中未登录的情况下查询 \_User 表吗? - -在云函数里可以用 masterKey 跳过权限检查,未登录也可直接查询 \_User 表。 - -因为云引擎运行在可信的服务器端环境中,所以你可以全局开启超级权限(`Master Key`),这样云端会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据。具体细节可以参考 [云引擎 SDK 使用指南 § 使用超级权限](/sdk/engine/functions/sdk/#使用超级权限)。 - -## 部署 - -### 多次部署同一个项目时镜像大小为什么差别那么大? - -云引擎底层有一套缓存机制以加速构建过程,所以部署时显示的「存储镜像到仓库」后面的大小表示本次构建新产生的数据,可用于评估是否利用到了缓存,不代表整个项目的大小。 - -### 部署时长时间卡在「正在下载和安装依赖」怎么办? - -这个步骤对应在云端调用各个语言的包管理器(`npm`、`pip`、`composer`、`maven`)安装依赖的过程,我们有一个依赖缓存机制来加速这个安装过程,但缓存可能会因为很多原因失效(比如修改了依赖列表),在缓存失效时会比平时慢很多,请耐心等待。如果你在 `leanengine.yaml` 中指定了系统依赖也会在这个步骤中安装,因此请不要添加未用到的依赖。 - -对于 Node.js 建议检查是否在 `package-lock.json` 或 `yarn.lock` 中指定了较慢的源。 - -### 部署到多个实例时,部分实例失败需要重新部署吗? - -同一环境(预备/生产)下有多个实例时,云引擎会同时在所有实例上部署项目。如因偶然因素部分实例部署不成功,会在几分钟后自动尝试再次部署,无需手动重新部署。 - -### 云引擎实例部署后控制台多次显示「部署中」是怎么回事? - -控制台显示的「部署中」状态泛指所有运维操作,例如唤醒休眠实例、服务器偶发故障引起的重新部署,不只是用户主动进行的部署。 - -### 云引擎的健康检查是什么? - -云引擎的管理系统会每隔几分钟检查所有实例的工作状态(通过 HTTP 检查,详见具体运行环境文档的「健康检查」小节), -如果实例无法正确响应的话,管理系统会触发一次重新部署,并在控制台上打印类似下面的日志: - -> 健康检查失败:web1 检测到 Error connect ECONNREFUSED 10.19.30.220:51797 - -如果一周内发生一两次属正常现象(有可能是我们的服务器出现偶发的故障,因为会立刻重新部署,对服务影响很小),如果频繁发生可能是你的程序资源不足,或存在其他问题(运行一段时间后不再响应 HTTP 请求),需结合具体情况来分析。 - -### 实例启动失败: 无法访问应用的 Web 端口(Error: connect ECONNREFUSED),请确保程序在 30 秒内正确地启动了 HTTP 服务 - -云引擎在部署过程中会检测应用的 3000 端口是否提供了 HTTP 服务,所以请保证应用至少暴露了 3000 端口。 - - - - -## 限制和费用 - -### 云引擎的请求有哪些限制? - -云引擎的负载均衡组件限制了请求不能超过 100 MB(包括直接上传文件到云引擎)、请求处理不得超过 60 秒,WebSocket 60 秒无数据会被断开连接。 - -国内节点未绑定独立 IP 的云引擎默认为纯静态站点优化。请求会先经过边缘节点,再视缓存命中情况回源到负载均衡组件,最后到达你的应用。 -边缘节点额外限制了请求不能超过 60 MB、请求处理不得超过 10 秒,另外边缘节点不支持 WebSocket 请求和 HTTP PATCH 方法,也不支持获取客户端 IP。 -因此,如果你在国内节点云引擎托管动态网站,我们建议你绑定独立 IP,使用独立入口,不经过边缘节点,自然也就没有上述限制。 - -### 每个应用最多有几个实例? - -每个应用最多拥有 12 个实例,如果需要更多资源请通过工单联系我们的技术支持。 - -### 云引擎如何收费? - -云引擎中如果有云服务的存储等 API 调用,按数据存储收费策略照常收费。 - -云引擎的标准实例、LeanDB 实例、回源流量、加速流量都会产生费用,详见官网的价格方案页面。 - -### 流量如何计费? - -每个云引擎实例每天有 1 G 免费额度,超出部分价格可以在当前节点的价格页面查看。 - -一个应用下的流量额度会合并计算,即每天的免费额度为 `max(n, 1)` GB,其中 `n` 为该应用所有云引擎分组下的标准实例总数。 - -**云引擎不适合分发大文件之类的场景**,有此需求的开发者可以使用文件服务。 - -在** > 管理部署 > 云引擎分组 > 统计 > 流量**可以查看最近流量统计。 - -### 如果更改了实例规格或数量,当天的云引擎费用如何收取? - -云引擎资源使用量按 **当天最大的实例数量** 计算,次日凌晨从账户余额中扣费,假设某天从 0 点至 24 点之间: - -- 应用本来有 4 个 standard-512 的实例; -- 发现资源数量不足,将实例规格调整到了 standard-1024; -- 发现资源过多,减少到 2 个实例。 - -则当天费用按照 standard-1024 的价格乘上 4 个实例计算。 - -### Hook 函数算 API 请求次数吗,afterUpdate 执行一次算 1 次请求次数吗? - -AfterUpdate 是在云引擎内执行的,执行 afterUpdate 不算 API 请求,自然也不计入 API 请求数。如果 afterUpdate 里发起了 API 请求,那么照常计算 API 请求数(和客户端请求 API 一样)。 - -## 最佳实践 - -### 云引擎如何上传文件? - -托管在云引擎的网站可以使用相应 SDK 提供的接口上传文件。 -不过,一般情况下建议在客户端 SDK 上传文件,而不是通过云引擎中转,以免增加不必要的云引擎流量。 - -## 在云引擎使用文件上传,报错 failed to upload file to qiniu bcz current file has not data stream。 - -LCFile 内部会使用一个本地缓存(需要先有一个本地目录)来先保存二进制数据,然后再把它上传到文件存储服务里。在云引擎里面应该会因为权限不足的原因无法创建这样的缓存目录或文件,导致出现 `failed to upload file to qiniu bcz current file has not data stream` 这样的错误。 -我们不建议在云引擎使用文件服务,有几个原因: - -1. 上面提到的权限问题,导致云引擎里无法创建缓存目录和临时文件; -2. 所有文件数据都先到服务端,然后再调用 API 推到文件存储服务商那里,这会导致效率很低,并且用户需要支付的网络成本很高(云引擎也需要付超出标准的流量费用的)。 - -建议在客户端使用 SDK 上传文件,之后把结果(例如文件的 object id 或者 url)再给到服务端(就是云引擎代码)。在客户端上传,会使用文件存储服务商的 CDN 链路(就近上传),用户的体验会更快,并且还可以节省云引擎流量成本。 - -### 定时任务应该在预备环境还是生产环境执行? - -系统赠送的预备环境体验实例会自动休眠,可能干扰定时任务的执行,因此一般建议在预备环境测试定时任务,在生产环境正式执行定时任务。 -如果定时任务 CPU、内存占用非常高,担心影响生产环境的网站托管功能或其他云函数访问,那么可以在预备环境购买标准实例,并在预备环境执行定时任务。 - -### 如何判断当前云引擎是预备环境还是生产环境? - -默认情况,云引擎只有一个「生产环境」,对应的域名是 web.example.com。在生产环境中有一个「体验实例」来运行应用。 - -当生产环境的体验实例升级到「标准实例」后会有一个额外的「预备环境」,对应域名 stg-web.example.com,两个环境所访问的都是同样的数据,你可以用预备环境测试你的云引擎代码,每次修改先部署到预备环境,测试通过后再发布到生产环境;如果你希望有一个独立数据源的测试环境,建议单独创建一个应用。 - -另外,stg-web.example.com 域名是需要在控制台自行绑定的。 - -### 如何访问云引擎预备环境中托管的网站? - -需要在控制台手动绑定一个 `stg-` 开头的域名。`stg-` 开头的自定义域名(例如 stg-web.example.com)会被自动地绑定到预备环境。 - -### 云引擎可以绑定裸域名吗? - - - -如果希望绑定裸域名,请添加直接指向独立 IP 的 A 记录。 - - - - - -如果希望绑定裸域名,我们建议选择支持 ANAME 或 CNAME Flattening 记录的域名服务商。 - - - -## 疑难问题 - -### Application not found 错误 - -访问云引擎服务时,服务端返回错误「Application not found」或在云引擎日志中出现这个错误,可能有以下原因: - -- 调用错了环境。最常见的情况是,免费的体验实例是没有预备环境,开发者却主动设置去调用预备环境。 -- 云引擎自定义域名填错了,比如微信回调地址。 -- 因为免费版(体验版)的云引擎是有休眠的,休眠期间被调用会出现这个错误。建议升级到标准实例以保证实例一直运行。 - -### 云引擎响应时间增加怎么办 - -响应时间的增加有很多种原因:可能因为只是单纯的请求处理的数据更加复杂导致耗时变长;也有可能是因为请求量过高实例的处理能力不足从而导致响应时间增加。 -建议分析当前的代码并参考 CPU、内存占用量找出瓶颈,确定是否需要调高实例规格或增加实例数量。 -如果需要定位具体是哪些 API 或云函数响应较慢,可以下载访问日志分析。 - -### 在线上无法读取到项目中的文件怎么办? - -建议先检查文件大小写是否正确,线上的文件系统是区分大小写的,而 Windows 和 macOS 通常不区分大小写。 - -### 云引擎会重复提交请求吗? - -云引擎的负载均衡对于幂等的请求(GET、PUT),在 HTTP 层面出错或超时的情况下是会重试的,建议使用正确的谓词(例如 POST)避免此类重试。 - - - -### 云引擎的 AccessToken 在哪里? -具体位置在:**部署 > 命令行部署 > 生成新的 AccessToken**,如下图: - -![](https://capacity-files.lcfile.com/1v0cTVXTAbwHucHc5tPzOfLNlKQYMCWr/Frame%2023.png) -![](https://capacity-files.lcfile.com/A7uUNJSBUo62Y7pULYOrSh3FL6TIHfwN/Frame%202%20%282%29.png) - - diff --git a/leancloud/docs/sdk/engine/functions/_category_.json b/leancloud/docs/sdk/engine/functions/_category_.json deleted file mode 100644 index 3f294f069..000000000 --- a/leancloud/docs/sdk/engine/functions/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "云函数和 Hook", - "collapsed": true, - "position": 5 -} diff --git a/leancloud/docs/sdk/engine/functions/cloud-queue.mdx b/leancloud/docs/sdk/engine/functions/cloud-queue.mdx deleted file mode 100644 index a4e9d1572..000000000 --- a/leancloud/docs/sdk/engine/functions/cloud-queue.mdx +++ /dev/null @@ -1,208 +0,0 @@ ---- -title: 云队列(Cloud Queue)开发指南 -sidebar_label: 云队列 -sidebar_position: 4 ---- - -云队列提供了一种在云引擎之外调度云函数的能力,它基于云引擎已有的「云函数」这个概念实现了重试、去重、结果查询、延时任务、定时任务等功能,是对云函数功能的一个补充。尚未运行的任务会以一种可靠的方式暂存在云队列,即使你的云引擎因部署、过载、崩溃而重启,任务也不会丢失,云队列会等待你的云引擎实例恢复正常后继续运行它们。 - -目前云队列还是一个实验性功能,可以免费使用,在正式上线后将会是一项单独计费的功能,因为云队列被设计用于应对突发流量,所以收费的指标将会与「每小时入队任务数量(对应队列的处理能力)」和「队列内剩余任务峰值(对应队列空间的占用)」相关,按实际用量计费,不需要预估容量。 - -云队列实际上运行在云引擎之外,它通过 HTTP 接受入队等操作,同时也通过 HTTP 来调用云引擎容器中的云函数来完成实际的任务执行,我们的 SDK 会封装这些细节。云队列的入队接口(`Cloud.enqueue`)目前仅限在云引擎内使用 masterKey 权限调用(后续可能会开放客户端调用);结果查询接口(`Cloud.getTaskInfo`)则允许客户端直接使用 uniqueId 调用。 - -## 功能和使用场景 - -云队列提供的功能包括: - -- **重试** 任务在执行失败后会默认进行一次重试,可以通过选项来配置重试次数(`attempts`)和重试间隔(`backoff`),重试时 uniqueId 不会改变。 -- **去重** 可以为任务提供唯一 ID(`uniqueId`,如不提供则会随机生成),在任务存在于队列期间(包括已完成的),云队列不会接受有 uniqueId 的任务。 -- **结果查询** 任务在完成后会继续被保留在队列中一段时间(可通过 `keepResult` 配置),客户端可以使用 uniqueId 来进行高性能的结果查询。 -- **延时任务** 可以通过 `delay` 来延迟执行一个任务。 -- **定时任务** 现在定时任务是云队列的一个子功能,你可以在控制台上创建和管理任意数量的定时任务,可以使用 [CRON 表达式](#cron-表达式) 或设置间隔时间。 -- **并发控制** 云队列会将入队的任务暂存起来,以 1 个并发的速度逐步地执行,避免云引擎实例过载(后续我们会引入更智能的并发调节算法)。 -- **优先级** 可以为特定任务设置优先级(`priority`),在队列拥堵时,高优先级的任务会优先执行。 - -你可以在 [leanengine-nodejs-demos](https://github.com/leancloud/leanengine-nodejs-demos) 中找到一些有关云队列的示例: - -- [queue-delay-retry](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/queue-delay-retry.js) 延时和重试云函数 -- [queue-result-query](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/queue-result-query.js) 结果查询 -- [crawler](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/crawler.js) 抓取一个站点下所有网页的爬虫(去重和控制并发) - -## 简单使用 - -```javascript -const { Cloud } = require("leanengine"); - -// 被执行的云函数,这里只是一个例子,实际业务中注意鉴权 -Cloud.define("closeOrder", async function ({ params }) { - try { - const order = await new Query("Order").get(params.id); - // 返回值可用于后续的结果查询,也会被记录到日志中 - return await order.save({ status: "closed" }); - } catch (err) { - // 抛出异常使云函数执行失败(JavaScript 自身或依赖库的异常也会使云函数执行失败) - throw new Cloud.Error(`Some error happened: ${err.message}`); - } -}); - -// 添加任务,enqueue 本身在添加队列成功就返回一个 uniqueId(不等待任务实际被执行) -// 你可以在所有云引擎代码(包括云函数也包括网站托管中的自定义路由)使用 Cloud.enqueue -const { uniqueId } = await Cloud.enqueue("closeOrder", { id: 1234 }); - -// 查询任务结果,可将 uniqueId 发给客户端,由客户端进行查询 -// 只有存在于队列中的任务才能查询,可以用 keepResult 来调整已完成任务的保留时间 -console.log(await Cloud.getTaskInfo(uniqueId)); - -// 调整默认选项,增加重试次数,减少重试间隔 -Cloud.enqueue("closeOrder", { id: 1234 }, { attempts: 10, backoff: 10000 }); - -// 添加延时任务,closeOrder 会在一分钟之后执行 -Cloud.enqueue("closeOrder", { id: 1234 }, { delay: 600000 }); - -// 指定 uniqueId,如果已有相同 uniqueId 的任务会抛出异常 -// 只有存在于队列中的任务会参与去重,可以用 keepResult 来调整已完成任务的保留时间 -Cloud.enqueue("closeOrder", { id: 1234 }, { uniqueId: "1234" }); -``` - -如果你希望某个云函数仅限被云队列调用(而不允许客户端直接调用)的话,可以为 `Cloud.define` 加上 [`internal` 选项](https://github.com/leancloud/leanengine-node-sdk/blob/master/API.md#avclouddefine),通过这种方式定义的云函数只能被 Cloud Queue 或其他具有 masterKey 权限的代码调用。 - -## API - -目前我们只在 Node SDK(3.4 以上版本)添加了云队列支持: - -``` -Cloud.enqueue(functionName, params, options?): Promise<{uniqueId: string}> -Cloud.getTaskInfo(uniqueId): Promise -``` - -用法: - -```javascript -const { Cloud } = require("leanengine"); - -// 添加任务,enqueue 本身在添加队列成功就返回一个 uniqueId(不等待任务实际被执行) -const { uniqueId } = await Cloud.enqueue("sendMail", { userId: 1234 }); - -// 延时任务 -Cloud.enqueue("closeOrder", { id: 1234 }, { delay: 600000 }); - -// 查询任务结果 -console.log(await Cloud.getTaskInfo(uniqueId)); -``` - -`options` 的属性包括: - -- `attempts?: number`:最大重试次数,默认 `1` -- `backoff?: number`:重试间隔(毫秒),默认 `60000`(一分钟) -- `delay?: number`:延时执行(毫秒) -- `deliveryMode?: string`:超时时的行为,值是 `atLeastOnce`(至少一次,可能会重试多次)、`atMostOnce`(至多一次,不会重试),默认是 `atLeastOnce` -- `keepResult?: number`:在队列中保留结果的时间(毫秒),默认 `300000`(五分钟) -- `priority?: number`:优先级,默认是当前时间戳,设置为更小的值可以在队列拥堵时让特定任务更快地被执行 -- `timeout?: number`:超时时间(毫秒),默认 `15000`,目前最大也是 `15000`,后续会提供更长的时间 -- `uniqueId?: string`:任务的唯一 ID,会据此进行去重,最长 32 个字符,默认是随机的 UUID - -`TaskInfo` 的属性包括: - -- `uniqueId: string`:任务的唯一 ID -- `status: string`:任务的状态,包括 `queued`(等待或正在执行)、`success`(执行成功)、`failed`(执行失败) - -执行完成的 `TaskInfo` 会有: - -- `finishedAt?: string` 执行完成(成功或失败)的时间 -- `statusCode?: number` 云函数响应的 HTTP 状态码 -- `result?: object` 来自云函数的响应 - -执行失败的 `TaskInfo` 会有: - -- `error?: string` 错误提示 -- `retryAt?: string` 下次重试的时间 - -## 性能和可靠性 - -云队列的入队接口(`Cloud.enqueue`)被设计用于应对较高的突发流量(例如 1000 QPS 以上),云队列会将这些任务存储起来,以 1 个并发的速度逐步地执行,减少对于云引擎容器的压力。 - -云队列的调度并非在云引擎容器中进行,这意味着即使云引擎容器故障、重启也不会影响到云队列(任务不会丢失,失败的任务会重试),可以用于支持突发流量。同时任务本身又是以云函数的形式在云引擎容器中运行的(占用云引擎的 CPU、内存资源),和直接调用云函数的执行环境完全相同。 - -## 拥堵和排队 - -如果执行任务的并发达到了上限,那么新的任务(包括延时任务和形式任务的触发)会进入到一个抽象的「等待队列」中,每当有正在执行的任务完成了,就会从等待队列中抽取优先级最高(`priority` 值最低)的任务来执行。 - -priority 的值默认是入队时(对于定时任务则为触发时)的毫秒时间戳(例如 `2019-05-20T17:32:07.166+08:00` 的时间戳是 `1558344727166`),也就是说默认情况下等待队列中的任务会按时间顺序被执行。你可以覆盖 priority 的值,改为一个较小的值来让重要的任务尽快地被执行;或改成一个较大的值让任务更迟执行。 - -## CRON 表达式 - -CRON 表达式的基本语法为: - -``` -秒 分钟 小时 日期 day-of-month 月份 星期 day-of-week -``` - -| 位置 | 字段 | 约束 | 取值 | 可使用的特殊符号 | -| ---- | ---- | ---- | ---------------- | ---------------- | -| 1 | 秒 | 必须 | 0–59 | `, - * /` | -| 2 | 分钟 | 必须 | 0–59 | `, - * /` | -| 3 | 小时 | 必须 | 0–23(0 为午夜) | `, - * /` | -| 4 | 日期 | 必须 | 1–31 | `, - * ? /` | -| 5 | 月份 | 必须 | 1–12、JAN–DEC | `, - * /` | -| 6 | 星期 | 必须 | 1–7、SUN–SAT | `, - ? /` | - -特殊符号的用法: - -| 符号 | 含义 | 用法 | -| ---- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `*` | 所有值 | 代表一个字段的所有可能取值。如将 **分钟** 设为 `*`,表示每一分钟。 | -| `?` | 不指定值 | 用于可以使用该符号的两个字段中的一个,在一个表达式中只能出现一次。如任务执行时间为每月 10 号,星期几无所谓,那么表达式中 **日期** 设为 `10`,**星期** 设为 `?`。 | -| `-` | 范围 | 如 **小时** 为 `10-12`,即 10 点、11 点、12 点。 | -| `,` | 分隔多个值 | 如 **星期** 为 `MON,WED,FRI`,即周一、周三、周五。 | -| `/` | 间隔 | 如 **秒** 设为 `*/15`,即表示每隔 15 秒执行一次,包括 0、15、30、45 秒。 | - -各字段以空格或空白隔开。`JAN`–`DEC`、`SUN`–`SAT` 这些值不区分大小写,比如 `MON` 和 `mon` 效果一样。 - -举例如下: - -| 表达式 | 说明 | -| ------------------------ | ----------------------------------------------------------------------------------------------- | -| `0 */5 * * * ?` | 每隔 5 分钟执行一次 | -| `10 */5 * * * ?` | 每隔 5 分钟执行一次,每次执行都在分钟开始的 10 秒,例如 10:00:10、10:05:10 等等。 | -| `0 30 10-13 ? * WED,FRI` | 每周三和每周五的 10:30、11:30、12:30、13:30 执行。 | -| `0 */30 8-9 5,20 * ?` | 每个月的 5 号和 20 号的 8 点和 10 点之间每隔 30 分钟执行一次,也就是 8:00、8:30、9:00 和 9:30。 | - -Cron 表达式的时区为东八区(国内版)、UTC 零时区(国际版)。 - -## 测试期间的限制 - -在测试期间我们有一些默认的限制: - -- 入队请求限制为每应用 100 QPS -- 每天入队请求限制为每应用 10000 -- 队列中的最大任务数量限制为每应用 1000 - -如果你达到了这些限制的话可以提交工单联系我们的技术支持。 - -## FAQ - -### 接下来还会有什么功能? - -这里列出的是后续可能会实施的一些计划,如果你非常需要其中某个功能,请通过工单或论坛联系我们,让我们知道。 - -- **允许客户端调用** 我们有计划为用户提供一个「允许客户端调用云队列」的选项,开启这个选项后客户端将会可以进行入队操作(`Cloud.enqueue`),但客户端不能指定 `options` 中的任何选项,只能传递参数。 -- **超时时间** 因为之前我们的云函数一直将超时时间限制在 15 秒,所以目前云队列受限于云函数,超时也是 15 秒。我们正在调整相关的基础设施,计划在后续让从云队列调用云函数的超时时间最长可以达到 5 分钟。 -- **并发限制** 目前所有应用的并发限制都是 1,我们有计划实现一种自动控制并发的机制:在任务较多的情况下,并发会逐渐增加,直到负载体现在实例的 CPU 或内存压力上。 -- **使用单独的分组运行任务** 我们有计划为用户提供一个「在独立分组中运行定时任务」的选项,开启这个选项后会自动创建一个特殊分组,所有定时任务都在这个分组中运行。 -- **定时任务的可编程接口** 我们有计划为定时任务提供可编程接口,但优先级较低。 -- **云队列的日志** 目前云队列会将执行日志直接打印到云引擎的应用日志中,后续我们准备把云队列的日志显示在一个单独的 Tab 中。 -- **在其他 SDK 中使用云函数** 后续我们会在 Python SDK、Java SDK、PHP SDK 中添加云队列的支持。 - -### 异常处理策略(deliveryMode)具体影响哪些情况? - -异常处理策略(deliveryMode)用于在以下几种不确定任务是否已经执行或是否应该执行的情况下,来决定是否重试: - -- 云队列已将请求发到云函数,但云函数未在超时时间内给出成功或失败的响应。 -- 因云队列本身的故障导致失去对正在执行的任务的追踪。 -- 因云队列本身的故障导致定时任务没有在指定的时间触发。 - -如果选择「放弃(atMostOnce)」在出现上述情况时任务可能不会执行;如果选择「重试(atLeastOnce)」在发生上述情况时任务可能会执行多次。 - -### 如何在本地调试时使用云队列? - -你可以在本地调试时远程调用云队列相关的 API,但云队列只会在线上的云引擎中运行指定的云函数。 diff --git a/leancloud/docs/sdk/engine/functions/getting-started.mdx b/leancloud/docs/sdk/engine/functions/getting-started.mdx deleted file mode 100644 index 7f118b745..000000000 --- a/leancloud/docs/sdk/engine/functions/getting-started.mdx +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: 快速开始部署云函数和 Hook -sidebar_label: 快速开始 -sidebar_position: 1 ---- - -import QuickStartNew from "../_partials/quick-start-new.mdx"; -import QuickStartDeploy from "../_partials/quick-start-deploy.mdx"; -import FunctionsIntroduction from "../_partials/functions-introduction.mdx"; -import PlatformRuntimes from "../_partials/platform-runtimes.mdx"; -import { CLI_BINARY } from "/src/constants/env.ts"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; - - - -:::info -如果希望使用 Node.js 编写简单的云函数或 Hook,也可以尝试 [云函数和 Hook 开发指南 § 在线编写云函数](/sdk/engine/functions/guides/#在线编写云函数)。 -::: - -## 创建项目 - - - -## 编写云函数 - -在示例项目中可以看到一个云函数的例子: - - - - -```js title='cloud.js' -AV.Cloud.define("hello", function (request) { - return "Hello world!"; -}); -``` - - - - -```python title='cloud.py' -@engine.define -def hello(**params): - if 'name' in params: - return 'Hello, {}!'.format(params['name']) - else: - return 'Hello, LeanCloud!' -``` - - - - -```php title='src/cloud.php' -Cloud::define("sayHello", function($params, $user) { - return "hello {$params['name']}"; -}); -``` - - - - -```java title='src/main/java/cn/leancloud/demo/todo/Cloud.java' -@EngineFunction("hello") -public static String hello(@EngineFunctionParam("name") String name) { - if (name == null) { - return "What is your name?"; - } - - return String.format("Hello %s!", name); -} -``` - - - - -```cs title='web/HelloSample.cs' -[EngineFunction("Hello")] -public static string Hello([EngineFunctionParameter("text")]string text) -{ - return $"Hello, {text}"; -} -``` - - - - -```go title='functions/hello.go' -func init() { - leancloud.Engine.Define("hello", hello) -} - -func hello(req *leancloud.FunctionRequest) (interface{}, error) { - return map[string]string{ - "hello": "world", - }, nil -} -``` - - - - -Hook 的编写和云函数很类似,在后文中我们会详细介绍云函数和 Hook 的详细用法。 - -## 本地运行和调试 - -

    - 可以使用 {CLI_BINARY} up{" "} - 进行本地运行和调试,命令行工具会自动注入关联应用的环境变量,让云函数可以访问到线上数据存储中的数据。 -

    - - - {`$ ${CLI_BINARY} up -[INFO] The project is running at: http://localhost:3000 -[INFO] Cloud function debug console (if available) is accessible at: http://localhost:3001`} - - -

    - {CLI_BINARY} up 同时默认在 3001 - 端口启动了一个用于调试云函数的控制台( - http://localhost:3001 - ),开发者可以通过这个控制台来调试云函数和 Hook,模拟特定的输入。 -

    - -![云函数调试控制台](/img/cloud-engine/engine-cli-debug-console.png) - -## 部署到云引擎 - - - -## 更多 - -接下来可以查看 [云函数和 Hook 开发指南](/sdk/engine/functions/guides) 来了解云函数开发的详细信息、查看 [云引擎 SDK 使用指南](/sdk/engine/functions/sdk) 了解 SDK 的进阶用法、查看 [云引擎平台功能](/sdk/engine/platform) 来了解云引擎提供的更多功能,或查看专门的页面来了解具体运行环境的详情: - - diff --git a/leancloud/docs/sdk/engine/functions/guides.mdx b/leancloud/docs/sdk/engine/functions/guides.mdx deleted file mode 100644 index ea7e5de33..000000000 --- a/leancloud/docs/sdk/engine/functions/guides.mdx +++ /dev/null @@ -1,2243 +0,0 @@ ---- -title: 云函数和 Hook 开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import FunctionsIntroduction from "../_partials/functions-introduction.mdx"; -import Mermaid from "/src/docComponents/Mermaid"; -import TabItem from "@theme/TabItem"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -:::info -这篇文档专注在「云函数和 Hook」这种云引擎上的特殊的应用场景,如需部署通用的后端应用,或需要了解云引擎平台提供的更多功能,请看 [云引擎平台功能](/sdk/engine/platform)。 -::: - - - -## 云函数 - -现在让我们看一个更复杂的例子,在一个应用中我们允许用户对电影进行评分,一个评分对象(`Review`)大概是这样: - -```json -{ - "movie": "夏洛特烦恼", - "stars": 5, - "comment": "夏洛一梦,笑成麻花" -} -``` - -`stars` 表示评分,是 1 至 5 的数字。通过云引擎,我们可以简单地传入电影名称,然后返回电影的平均分。 - -云函数接收 JSON 格式的请求对象,我们可以用它来传入电影名称。云函数中可以直接使用对应语言的数据存储 SDK,所以我们可以使用它来查询所有的评分。结合在一起,我们可以实现一个 `averageStars` 函数: - - - - -```js -AV.Cloud.define("averageStars", function (request) { - var query = new AV.Query("Review"); - query.equalTo("movie", request.params.movie); - return query.find().then(function (results) { - var sum = 0; - for (var i = 0; i < results.length; i++) { - sum += results[i].get("stars"); - } - return sum / results.length; - }); -}); -``` - -`AV.Cloud.define` 还接受一个可选参数 `options`(位置在函数名称和调用函数之间),这个 `options` 对象上的属性包括: - -- `fetchUser?: boolean`:是否自动抓取客户端的用户信息,默认为 `true`。设置为假时,`Request` 将不会有 `currentUser` 属性。 -- `internal?: boolean`:是否只允许在云引擎内(使用 `AV.Cloud.run` 且未开启 `remote` 选项)或使用 `Master Key`(使用 `AV.Cloud.run` 时传入 `useMasterKey`)调用,不允许客户端直接调用。默认为 `false`。 - -例如,假设我们不希望客户端直接调用上述函数,也不关心客户端用户信息,那么上述函数的定义可以改写为: - -```js -AV.Cloud.define( - "averageStars", - { fetchUser: false, internal: true }, - function (request) { - // 内容同上 - } -); -``` - - - - -```python -@engine.define -def averageStars(movie, **params): - reviews = leancloud.Query(Review).equal_to('movie', movie).find() - result = sum(x.get('stars') for x in reviews) - return result -``` - -客户端 SDK 调用时,云函数的名称默认为 Python 代码中函数的名称。有时需要设置云函数的名称与 Python 代码中的函数名称不相同,可以在 `engine.define` 后面指定云函数名称,比如: - -```python -@engine.define('averageStars') -def my_custom_average_start(movie, **params): - pass -``` - - - - -```java -@EngineFunction("averageStars") -public static float getAverageStars(@EngineFunctionParam("movie") String movie) throws LCException { - LCQuery query = new LCQuery("Review"); - query.whereEqualTo("movie", movie); - List reviews = query.find(); - int sum = 0; - if (reviews == null && reviews.isEmpty()) { - return 0; - } - for (LCObject review : reviews) { - sum += review.getInt("star"); - } - return sum / reviews.size(); -} -``` - - - - -```php -use \LeanCloud\Engine\Cloud; -use \LeanCloud\Query; -use \LeanCloud\CloudException; - -Cloud::define("averageStars", function($params, $user) { - $query = new Query("Review"); - $query->equalTo("movie", $params["movie"]); - try { - $reviews = $query->find(); - } catch (CloudException $ex) { - // 查询失败,将错误输出到日志 - error_log($ex->getMessage()); - return 0; - } - $sum = 0; - forEach($reviews as $review) { - $sum += $review->get("stars"); - } - if (count($reviews) > 0) { - return $sum / count($reviews); - } else { - return 0; - } -}); -``` - - - - -```cs -[LCEngineFunction("averageStars")] -public static float AverageStars([LCEngineFunctionParam("movie")] string movie) { - if (movie == "夏洛特烦恼") { - return 3.8f; - } - return 0; -} -``` - - - - -```go -type Review struct { - leancloud.Object - Movie string `json:"movie"` - Stars int `json:"stars"` - Comment string `json:"comment"` -} - -leancloud.Engine.Define("averageStars", func(req *leancloud.FunctionRequest) (interface{}, error) { - reviews := make([]Review, 10) // 预留一小部分空间 - if err := client.Class("Review").NewQuery().EqualTo("movie", req.Params["movie"].(string)).Find(&reviews); err != nil { - return nil, err - } - - sum := 0 - for _, v := range reviews { - sum += v.Stars - } - - return sum / len(reviews), nil -}) -``` - - - - -### 参数和返回值 - - - - -`Request` 会作为参数传入到云函数中,`Request` 上的属性包括: - -- `params: object`:客户端发送的参数对象,当使用 `rpc` 调用时,也可能是 `AV.Object`。 -- `currentUser?: AV.User`:客户端所关联的用户(根据客户端发送的 `X-LC-Session` 头)。 -- `sessionToken?: string`:客户端发来的 `sessionToken`(`X-LC-Session` 头)。 -- `meta: object`:有关客户端的更多信息,目前只有一个 `remoteAddress` 属性表示客户端的 IP。 - -如果云函数返回了一个 Promise,那么云函数会使用 Promise 成功结束后的结果作为成功响应;如果 Promise 中发生了错误,云函数会使用这个错误作为错误响应,对于使用 `AV.Cloud.Error` 构造的异常对象,我们认为是客户端错误,不会在标准输出打印消息,对于其他异常则会在标准输出打印调用栈,以便排查错误。 - -我们推荐大家使用链式的 Promise 写法来完成业务逻辑,这样会极大地方便异步任务的处理和异常处理,**请注意一定要将 Promise 串联起来并在云函数中 return** 以保证上述逻辑正确工作,推荐阅读 [JavaScript Promise 迷你书](http://liubin.org/promises-book/) 来深入地了解 Promise。 - -
    -点击展开 Node.js SDK 早期版本详情 - -在 2.0 之前的 Node.js 中,云函数接受 `request` 和 `response` 两个参数,我们会继续兼容这种用法到下一个大版本,希望开发者尽快迁移到 Promise 风格的云函数上。之前版本的文档见 [Node SDK v1 API 文档](https://github.com/leancloud/leanengine-node-sdk/blob/v1/API.md)。 - -
    - -
    - - -调用云函数时的参数会直接传递给云函数,因此直接声明这些参数即可。另外调用云函数时可能会根据不同情况传递不同的参数,这时如果定义云函数时没有声明这些参数,会触发 Python 异常,因此建议声明一个额外的关键字参数(关于关键字参数,请参考 [此篇文档](https://www.liaoxuefeng.com/wiki/1016959663602400/1017261630425888) 中「关键字参数」一节)来保存多余的参数。 - -```python -@engine.define -def my_cloud_func(foo, bar, baz, **params): - pass -``` - -除了调用云函数的参数之外,还可以通过 `engine.current` 对象,来获取到调用此云函数的客户端的其他信息。`engine.current` 对象上的属性包括: - -- `engine.current.user: leancloud.User`:客户端所关联的用户(根据客户端发送的 `X-LC-Session` 头)。 -- `engine.current.session_token: str`:客户端发来的 `sessionToken`(`X-LC-Session` 头)。 -- `engine.current.meta: dict`:有关客户端的更多信息,目前只有一个 `remote_address` 属性表示客户端的 IP。 - - - - -传递给云函数的参数依次为: - -- `$params: array`:客户端发送的参数。 -- `$user: User`:客户端所关联的用户(根据客户端发送的 `X-LC-Session` 头)。 -- `$meta: array`:有关客户端的更多信息,目前只有一个 `$meta['remoteAddress']` 属性表示客户端的 IP。 - - - - -云函数中可以获取的参数和上下文信息有: - -- `@EngineFunctionParam`:客户端发送的参数。 -- `EngineRequestContext`:有关客户端的更多信息,其中 `EngineRequestContext.getSessionToken()` 会返回客户端所关联用户的 sessionToken(根据客户端发送的 `X-LC-Session` 头),`EngineRequestContext.getRemoteAddress()` 会返回客户端的实际地址。 - - - - -云函数中可以获取的参数和上下文信息有: - -- `LCEngineFunctionParam`:客户端发送的参数。 -- `LCEngineRequestContext`:有关客户端的更多信息,其中 `LCEngineRequestContext.SessionToken` 会返回客户端所关联用户的 sessionToken(根据客户端发送的 `X-LC-Session` 头),`LCEngineRequestContext.RemoteAddress` 会返回客户端的实际地址。 - - - - -`leancloud.FunctionRequest` 会作为参数传入到云函数中,`leancloud.FunctionRequest` 上的属性包括: - -- `Params` 包含客户端发送的参数。 -- `CurrentUser` 包含客户端所关联的用户(根据客户端发送的 `X-LC-Session` 头)。可以在 `Define` 定义云函数时,在最后传入可选参数 `WithoutFetchUser()` 禁止获取当前调用的用户。 -- `SessionToken` 包含客户端发来的 `sessionToken`(根据客户端发送的 `X-LC-Session` 头)。可以在 `Define` 定义云函数时,在最后传入可选参数 `WithoutFetchUser()` 禁止获取当前调用的 `sessionToken`。 -- `Meta` 包含有关客户端的更多信息,目前只有一个 `remoteAddress` 属性表示客户端的 IP。 - - -
    - -### 客户端 SDK 调用云函数 - -各个客户端 SDK 都提供了调用云函数的功能: - - - -```cs -try { - Dictionary response = await LCCloud.Run("averageStars", parameters: new Dictionary { - { "movie", "夏洛特烦恼" } - }); - // 处理结果 -} catch (LCException e) { - // 处理异常 -} -``` - -<> - -```java -// 构建传递给服务端的参数字典 -Map dicParameters = new HashMap(); -dicParameters.put("movie", "夏洛特烦恼"); - -// 调用指定名称的云函数 averageStars,并且传递参数 -LCCloud.callFunctionInBackground("averageStars", dicParameters).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - - } - - @Override - public void onNext(Object object) { - // succeed. - } - - @Override - public void onError(Throwable throwable) { - // failed. - } - - @Override - public void onComplete() { - - } -}); -``` - -Java SDK 还提供了一个支持缓存的 `callFunctionWithCacheInBackground`,和 `LCQuery` 一样,开发者在请求的时候,可以指定 `CachePolicy` 以及缓存的最长期限,这样可以避免短时间一直直接请求云端服务器。 - - - -```objc -// 构建传递给服务端的参数字典 -NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"夏洛特烦恼" - forKey:@"movie"]; - -// 调用指定名称的云函数 averageStars,并且传递参数 -[LCCloud callFunctionInBackground:@"averageStars" - withParameters:dicParameters - block:^(id object, NSError *error) { - if(error == nil){ - // 处理结果 - } else { - // 处理报错 - } -}]; -``` - -```swift -LCEngine.run("averageStars", parameters: ["movie": "夏洛特烦恼"]) { (result) in - switch result { - case .success(value: let resultValue): - print(resultValue) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - Map response = await LCCloud.run('averageStars', params: { 'movie': '夏洛特烦恼' }); - // 处理结果 -} on LCException catch (e) { - // 处理异常 -} -``` - -```js -var paramsJson = { - movie: "夏洛特烦恼", -}; -AV.Cloud.run("averageStars", paramsJson).then( - function (data) { - // 处理结果 - }, - function (err) { - // 处理报错 - } -); -``` - -```python -from leancloud import cloud - -cloud.run('averageStars', movie='夏洛特烦恼') -``` - -```php -use \LeanCloud\Engine\Cloud; -$params = array( - "movie" => "夏洛特烦恼" -); -Cloud::run("averageStars", $params); -``` - -```go -// ... -averageStars, err := leancloud.Run("averageStars", map[string]string{"movie": "夏洛特烦恼"}) -if err != nil { - panic(err) -} -// ... -``` - - - -云函数调用(Run)默认将请求参数和响应结果作为 JSON 对象来处理,如果需要在请求或响应中传递 LCObject 对象,则可以使用 RPC 方式来调用云函数,SDK 将会完成 LCObject 类型的序列化和反序列化,在云函数和客户端代码中都可以直接获取到 LCObject 对象: - - - -```cs -try { - LCObject response = await LCCloud.RPC("averageStars", parameters: new Dictionary { - { "movie", "夏洛特烦恼" } - }); - // 处理结果 -} catch (LCException e) { - // 处理异常 -} -``` - -<> - -```java -// 构建参数 -Map dicParameters = new HashMap<>(); -dicParameters.put("movie", "夏洛特烦恼"); - -LCCloud.callRPCInBackground("averageStars", dicParameters).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - - } - - @Override - public void onNext(LCObject avObject) { - // succeed. - } - - @Override - public void onError(Throwable throwable) { - // failed - } - - @Override - public void onComplete() { - - } -}); -``` - -Java SDK 还提供了一个支持缓存的 `callRPCWithCacheInBackground`,和 `LCQuery` 一样,开发者在请求的时候,可以指定 `CachePolicy` 以及缓存的最长期限,这样可以避免短时间一直直接请求云端服务器。 - - - -```objc -NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"夏洛特烦恼" - forKey:@"movie"]; - -[LCCloud rpcFunctionInBackground:@"averageStars" - withParameters:parameters - block:^(id object, NSError *error) { - if(error == nil){ - // 处理结果 - } - else { - // 处理报错 - } -}]; -``` - -```swift -LCEngine.call("averageStars", parameters: ["movie": "夏洛特烦恼"]) { (result) in - switch result { - case .success(object: let object): - if let object = object { - print(object) - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - LCObject response = await LCCloud.rpc('averageStars', params: { 'movie': '夏洛特烦恼' }); - // 处理结果 -} on LCException catch (e) { - // 处理异常 -} -``` - -```js -var paramsJson = { - movie: "夏洛特烦恼", -}; - -AV.Cloud.rpc("averageStars", paramsJson).then( - function (object) { - // 处理结果 - }, - function (error) { - // 处理报错 - } -); -``` - -```python -from leancloud import cloud - -cloud.rpc('averageStars', movie='夏洛特烦恼') -``` - -```php -// 暂不支持 -``` - -```go -// ... -averageStars := 0 -if err := leancloud.RPC("averageStars", Review{Movie: "夏洛特烦恼"}, &averageStars); err != nil { - panic(err) -} -// .. -``` - - - -RPC 会处理以下形式的请求和响应: - -- 单个 LCObject -- 包含 LCObject 的散列表(HashMap) -- 包含 LCObject 的数组(Array) - -其他形式的数据 SDK 会保持原样,不进行处理。 - -### 云函数内部调用云函数 - - - - -云引擎 Node.js 环境下,默认会直接进行一次本地的函数调用,而不会像客户端一样发起一个 HTTP 请求。 - -```js -AV.Cloud.run("averageStars", { - movie: "夏洛特烦恼", -}).then( - function (data) { - // 调用成功,得到成功的应答 data - }, - function (error) { - // 处理调用失败 - } -); -``` - -如果你希望发起 HTTP 请求来调用云函数,可以传入一个 `remote: true` 的选项。当你在云引擎之外运行 Node.js SDK(包括调用位于其他分组上的云函数)时这个选项非常有用: - -```js -AV.Cloud.run("averageStars", { movie: "夏洛特烦恼" }, { remote: true }).then( - function (data) { - // 成功 - }, - function (error) { - // 处理调用失败 - } -); -``` - -上面的 `remote` 选项实际上是作为 `AV.Cloud.run` 的可选参数 options 对象的属性传入的。这个 `options` 对象包括以下参数: - -- `remote?: boolean`:上面的例子用到的 `remote` 选项,默认为假。 -- `user?: AV.User`:以特定的用户运行云函数(建议在 `remote` 为假时使用)。 -- `sessionToken?: string`:以特定的 `sessionToken` 调用云函数(建议在 `remote` 为真时使用)。 -- `req?: http.ClientRequest | express.Request`:为被调用的云函数提供 `remoteAddress` 等属性。 - - - - -云引擎 Python 环境下,默认会进行远程调用。 -例如,以下代码会发起一次 HTTP 请求,去请求部署在云引擎上的云函数。 - -```python -from leancloud import cloud - -cloud.run('averageStars', movie='夏洛特烦恼') -``` - -如果想要直接调用本地(当前进程)中的云函数,或者发起调用就是在云引擎中,想要省去一次 HTTP 调用的开销,可以使用 `leancloud.cloud.run.local` 来取代 `leanengine.cloud.run`,这样会直接在当前进程中执行一个函数调用,而不会发起 HTTP 请求来调用此云函数。 - - - - -Java SDK 不支持本地调用云函数。 -如有代码复用需求,建议将公共逻辑提取成普通函数(Java 方法),在多个云函数中调用。 - - - - -云引擎中默认会直接进行一次本地的函数调用,而不是像客户端一样发起一个 HTTP 请求。 - -```php -try { - $result = Cloud::run("averageStars", array("movie" => "夏洛特烦恼")); -} catch (\Exception $ex) { - // 云函数错误 -} -``` - -如果想要通过 HTTP 调用,可以使用 `runRemote` 方法: - -```php -try { - $token = User::getCurrentSessionToken(); // 以特定的 `sessionToken` 调用云函数,可选 - $result = Cloud::runRemote("averageStars", array("movie" => "夏洛特烦恼"), $token); -} catch (\Exception $ex) { - // 云函数错误 -} -``` - - - - -.NET SDK 不支持本地调用云函数。 -如有代码复用需求,建议将公共逻辑提取成普通函数,在多个云函数中调用。 - - - - -使用 `Engine.Run` 即是本地调用: - -```go -averageStars, err := leancloud.Engine.Run("averageStars", Review{Movie: "夏洛特烦恼"}) -if err != nil { - panic(err) -} -``` - -如果希望发起 HTTP 请求来调用云函数,请使用 `Client.Run`。 - -`Run` 的可选参数如下: - -- `WithSessionToken(token)` 为当前的调用请求传入 `sessionToken` -- `WithUser(user)` 为当前的调用请求传入对应的用户对象 - - - - -### 云函数错误响应码 - -可以根据 [HTTP status codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) 自定义错误响应码。 - - - - -```js -AV.Cloud.define("customErrorCode", function (request) { - throw new AV.Cloud.Error("自定义错误信息。", { code: 123 }); -}); -``` - - - - -```python -from leancloud import LeanEngineError - -@engine.define -def custom_error_code(**params): - raise LeanEngineError(123, '自定义错误信息。') -``` - - - - -```java -@EngineFunction() -public static void customErrorCode() throws Exception { - throw new LCException(123, "自定义错误信息。"); -} -``` - - - - -```php -Cloud::define("customErrorCode", function($params, $user) { - throw new FunctionError("自定义错误信息。", 123); -}); -``` - - - - -```cs -[LCEngineFunction("throwLCException")] -public static void ThrowLCException() { - throw new LCException(123, "自定义错误信息。"); -} -``` - - - - -```go -leancloud.Engine.Define("customErrorCode", func(req *leancloud.FunctionRequest) (interface{}, error) { - return nil, leancloud.CloudError{123, "自定义错误信息。"} -}) -``` - - - - -客户端收到的响应:`{ "code": 123, "error": "自定义错误信息。" }`。 - -### 云函数超时 - -云函数超时时间为 15 秒,如果超过阈值,客户端将收到 HTTP status code 为 `503` 的响应,body 为 `The request timed out on the server`。 -注意即使已经响应,此时云函数可能仍在执行,但执行完毕后的响应是无意义的(不会发给客户端,会在日志中打印一个 `Can't set headers after they are sent` 的异常)。 -除了 `503` 错误外,有些情况下客户端也可能收到其他报错,如 `524` 或 `141`。 - -#### 超时的处理方案 - -我们建议将代码中的任务转化为异步队列处理,以优化运行时间,避免云函数或定时任务发生超时。 - -例如: - -1. 在存储服务中创建一个队列表,包含 `status` 列; -2. 接到任务后,向队列表保存一条记录,`status` 值设置为 `处理中`,然后将请求结束掉,将队列对象的 `id` 发给客户端。 -3. 当业务处理完毕,根据处理结果更新刚才的队列对象状态,将 `status` 字段设置为 `完成` 或者 `失败`; -4. 在任何时候,在控制台通过队列 `id` 可以获取某个任务的执行结果,判断任务状态。 - -不过,对于 before 类 hook 函数,改为异步处理通常没有意义。 -虽然改为异步后能保证 before 类函数能够运行完成,不会因超时而报错。 -但是,只要 before 类 hook 函数不能及时抛出异常,就无法起到中断操作执行的作用。 -对于超时的 before 类 hook 函数,如果无法优化代码,压缩执行时间的话,那只能改为 after 类函数。 -例如,假设某个 beforeSave 函数需要调用耗时较久的第三方的自然语言处理接口判断用户提交的评论是否来自真实用户,导致超时, -那么可以改为 afterSave 函数,在保存评论后再调用第三方接口,如果判断是水军评论,那么再进行删除。 - -## 数据存储 Hook - -Hook 函数本质上是云函数,但它有固定的名称,定义之后会 **由系统** 在特定事件或操作(如数据保存前、保存后,数据更新前、更新后等等)发生时 **自动触发**,而不是由开发者来控制其触发时机。需要注意: - -- 通过控制台进行数据导入时不会触发任何 hook 函数。 -- 使用 Hook 函数需要 [防止死循环调用](#防止死循环调用)。 -- `_Installation` 表暂不支持 Hook 函数。 -- Hook 函数只对当前应用的 Class 生效,对绑定后的目标 Class 无效。 - -对于 `before` 类的 Hook,如果返回了一个错误的话,这个操作就会被中断,因此你可以在这些 Hook 中主动抛出一个错误来拒绝掉某些操作。对于 `after` 类的 Hook,返回错误并不会影响操作的执行(因为其实操作已经执行完了)。 - -D{object} -D-->E(new) -E-->|beforeSave|H{error?} -H-->N(No) -N-->B[create new object on the cloud] -B -->|afterSave|C((done)) -H-->Y(Yes) -Y-->Z((interrupted)) -D-->F(existing) -F-->|beforeUpdate|I{error?} -I-->Y -I-->V(No) -V-->G[update existing object on the cloud] -G-->|afterUpdate| C -`} -/> - -|beforeDelete|H{error?} -H-->Y(Yes) -Y-->Z((interrupted)) -H-->N(No) -N-->B[delete object on the cloud] -B -->|afterDelete|C((done)) -`} -/> - -为了认证 Hook 调用者的身份,我们的 SDK 内部会确认请求确实是从云引擎内网的云存储组件发出的,如果认证失败,可能会出现 `Hook key check failed` 的提示,如果在本地调试时出现这样的错误,请确保是通过命令行工具启动调试的。 - -### BeforeSave - -在创建新对象之前,可以对数据做一些清理或验证。例如,一条电影评论不能过长,否则界面上显示不开,需要将其截断至 140 个字符: - - - - -```js -AV.Cloud.beforeSave("Review", function (request) { - var comment = request.object.get("comment"); - if (comment) { - if (comment.length > 140) { - // 截断并添加 '…' - request.object.set("comment", comment.substring(0, 140) + "…"); - } - } else { - // 不保存数据,并返回错误 - throw new AV.Cloud.Error("No comment provided!"); - } -}); -``` - -上面的代码示例中,`request.object` 是被操作的 `AV.Object`。除了 `object` 之外,`request` 上还有一个属性: - -- `currentUser?: AV.User`:发起操作的用户。 - -类似地,其他 hook 的 `request` 参数上也包括 `object` 和 `currentUser` 这两个属性。 - - - - -```python -@engine.before_save('Review') # Review 为需要 hook 的 class 的名称 -def before_review_save(review): - comment = review.get('comment') - if not comment: - raise leancloud.LeanEngineError(message='No comment provided!') - if len(comment) > 140: - review.comment.set('comment', comment[:140] + '…') -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.beforeSave) -public static LCObject reviewBeforeSaveHook(LCObject review) throws Exception { - if (StringUtil.isEmpty(review.getString("comment"))) { - throw new Exception("No comment provided!"); - } else if (review.getString("comment").length() > 140) { - review.put("comment", review.getString("comment").substring(0, 140) + "…"); - } - return review; -} -``` - - - - -```php -Cloud::beforeSave("Review", function($review, $user) { - $comment = $review->get("comment"); - if ($comment) { - if (strlen($comment) > 140) { - // 截断并添加 '…' - $review->set("comment", substr($comment, 0, 140) . "…"); - } - } else { - // 不保存数据,并返回错误 - throw new FunctionError("No comment provided!", 101); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeSave)] -public static LCObject ReviewBeforeSave(LCObject review) { - if (string.IsNullOrEmpty(review["comment"])) { - throw new Exception("No comment provided!"); - } - string comment = review["comment"] as string; - if (comment.Length > 140) { - review["comment"] = string.Format($"{comment.Substring(0, 140)}..."); - } - return review; -} -``` - - - - -```go -leancloud.Engine.BeforeSave("Review", func(req *ClassHookRequest) (interface{}, error) { - review := new(Review) - if err := req.Object.Clone(review); err != nil { - return nil, err - } - - if len(review.Comment) > 140 { - review.Comment = review.Comment[:140] - } - - return review, nil -}) -``` - - - - -### AfterSave - -在创建新对象后触发指定操作,比如当一条留言创建后再更新一下所属帖子的评论总数: - - - - -```js -AV.Cloud.afterSave("Comment", function (request) { - var query = new AV.Query("Post"); - return query.get(request.object.get("post").id).then(function (post) { - post.increment("comments"); - return post.save(); - }); -}); -``` - - - - -```python -import leancloud - -@engine.after_save('Comment') # Comment 为需要 hook 的 class 的名称 -def after_comment_save(comment): - post = leancloud.Query('Post').get(comment.id) - post.increment('commentCount') - try: - post.save() - except leancloud.LeanCloudError: - raise leancloud.LeanEngineError(message='An error occurred while trying to save the post.') -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.afterSave) -public static void reviewAfterSaveHook(LCObject review) throws Exception { - LCObject post = review.getLCObject("post"); - post.fetch(); - post.increment("comments"); - post.save(); -} -``` - - - - -```php -Cloud::afterSave("Comment", function($comment, $user) { - $query = new Query("Post"); - $post = $query->get($comment->get("post")->getObjectId()); - $post->increment("commentCount"); - try { - $post->save(); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred while trying to save the post: " . $ex->getMessage()); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.AfterSave)] -public static async Task ReviewAfterSave(LCObject review) { - LCObject post = review["post"] as LCObject; - await post.Fetch(); - post.Increment("comments", 1); - await post.Save(); -} -``` - - - - -```go -leancloud.Engine.AfterSave("Review", func(req *ClassHookRequest) error { - review := new(Review) - if err := req.Object.Clone(review); err != nil { - return err - } - - if err := client.Object(review.Post).Update(map[string]interface{}{ - "comment": leancloud.OpIncrement(1), - }); err != nil { - return leancloud.CloudError{Code: 500, Message: err.Error()} - } - - return nil -}) -``` - - - - -再如,在用户注册成功之后,给用户增加一个新的属性 `from` 并保存: - - - - -```js -AV.Cloud.afterSave("_User", function (request) { - console.log(request.object); - request.object.set("from", "LeanCloud"); - return request.object.save().then(function (user) { - console.log("Success!"); - }); -}); -``` - -虽然对于 `after` 类的 Hook 我们并不关心返回值,但我们仍建议你返回一个 Promise,这样如果发生了非预期的错误,会自动在标准输出中打印异常信息和调用栈。 - - - - -```python -@engine.after_save('_User') -def after_user_save(user): - print(user) - user.set('from', 'LeanCloud') - try: - user.save() - except LeanCloudError, e: - print('Error: ', e) -``` - - - - -```java -@EngineHook(className = "_User", type = EngineHookType.afterSave) -public static void userAfterSaveHook(LCUser user) throws Exception { - user.put("from", "LeanCloud"); - user.save(); -} -``` - - - - -```php -Cloud::afterSave("_User", function($userObj, $currentUser) { - $userObj->set("from", "LeanCloud"); - try { - $userObj->save(); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred while trying to save the user: " . $ex->getMessage()); - } -}); -``` - - - - -```cs -[LCEngineClassHook("_User", LCEngineObjectHookType.AfterSave)] -public static async Task UserAfterSave(LCObject user) { - user["from"] = "LeanCloud"; - await user.Save(); -} -``` - - - - -```go -leancloud.Engine.AfterSave("_User", func(req *ClassHookRequest) error{ - if req.User != nil { - if err := client.User(req.User).Set("from", "LeanCloud"); err != nil { - return err - } - } - return nil -}) -``` - - - - -### BeforeUpdate - -在更新已存在的对象前执行操作,这时你可以知道哪些字段已被修改,还可以在特定情况下拒绝本次修改: - - - - -```js -AV.Cloud.beforeUpdate("Review", function (request) { - // 如果 comment 字段被修改了,检查该字段的长度 - if (request.object.updatedKeys.indexOf("comment") != -1) { - if (request.object.get("comment").length > 140) { - // 拒绝过长的修改 - throw new AV.Cloud.Error("comment 长度不得超过 140 字符。"); - } - } -}); -``` - - - - -```python -@engine.before_update('Review') -def before_hook_object_update(obj): - # 如果 comment 字段被修改了,检查该字段的长度 - if 'comment' in obj.updated_keys and len(obj.get('comment')) > 140: - # 拒绝过长的修改 - raise leancloud.LeanEngineError(message='comment 长度不得超过 140 字符。') -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.beforeUpdate) -public static LCObject reviewBeforeUpdateHook(LCObject review) throws Exception { - List updateKeys = EngineRequestContext.getUpdateKeys(); - for (String key : updateKeys) { - // 如果 comment 字段被修改了,检查该字段的长度 - if ("comment".equals(key) && review.getString("comment").length()>140) { - throw new Exception("comment 长度不得超过 140 字符。"); - } - } - return review; -} -``` - - - - -```php -Cloud::beforeUpdate("Review", function($review, $user) { - // 如果 comment 字段被修改了,检查该字段的长度 - if (in_array("comment", $review->updatedKeys) && - strlen($review->get("comment")) > 140) { - throw new FunctionError("comment 长度不得超过 140 字符。"); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeUpdate)] -public static LCObject ReviewBeforeUpdate(LCObject review) { - ReadOnlyCollection updatedKeys = review.GetUpdatedKeys(); - if (updatedKeys.Contains("comment")) { - string comment = review["comment"] as string; - if (comment.Length > 140) { - throw new Exception("comment 长度不得超过 140 字符。"); - } - } - return review; -} -``` - - - - -```go -leancloud.Engine.BeforeUpdate("Review", func(req *ClassHookRequest) (interface{}, error) { - updatedKeys = req.UpdatedKeys() - for _, v := range updatedKeys { - if v == "comment" { - comment, ok := req.Object.Raw()["comment"].(string) - if !ok { - return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"} - } - - if len(comment) > 140 { - return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"} - } - } - } - - return nil, nil -}) -``` - - - - -对传入对象直接进行的修改不会被保存。如需拒绝修改,可以让函数返回一个错误。 - -传入的对象是一个尚未保存到数据库的临时对象,并不保证与最终储存到数据库的对象完全相同,这是因为修改中可能包含自增、数组增改、关系增改等原子操作。 - -### AfterUpdate - -本 Hook 使用不当可能会造成死循环,导致数据存储 API 的调用次数暴涨,甚至产生更多的费用。因此请仔细阅读 [防止死循环调用](#防止死循环调用) 部分,做出必要的调整和预防措施。 - -在更新已存在的对象后执行特定的动作。和 BeforeUpdate 一样,你可以知道哪些字段已被修改。 - - - - -```js -AV.Cloud.afterUpdate("Review", function (request) { - if (request.object.updatedKeys.indexOf("comment") != -1) { - if (request.object.get("comment").length < 5) { - console.log(review.ObjectId + " 看起来像灌水评论:" + comment); - } - } -}); -``` - - - - -```python -@engine.after_update('Review') -def after_review_update(article): - if 'comment' in obj.updated_keys and len(obj.get('comment')) < 5: - print(review.ObjectId + " 看起来像灌水评论:" + comment) -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.afterUpdate) -public static void reviewAfterUpdateHook(LCObject review) throws Exception { - List updateKeys = EngineRequestContext.getUpdateKeys(); - for (String key : updateKeys) { - if ("comment".equals(key) && review.getString("comment").length()<5) { - LOGGER.d(review.ObjectId + " 看起来像灌水评论:" + comment); - } - } -} -``` - - - - -```php -Cloud::afterUpdate("Review", function($review, $user) { - if (in_array("comment", $review->updatedKeys) && - strlen($review->get("comment")) < 5) { - error_log(review.ObjectId . " 看起来像灌水评论:" . comment); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.AfterUpdate)] -public static void ReviewAfterUpdate(LCObject review) { - ReadOnlyCollection updatedKeys = review.GetUpdatedKeys(); - if (updatedKeys.Contains("comment")) { - string comment = review["comment"] as string; - if (comment.Length < 5) { - Console.WriteLine($"{review.ObjectId} 看起来像灌水评论:{comment}"); - } - } -} -``` - - - - -```go -leancloud.Engine.AfterUpdate("Review", func(req *ClassHookRequest) error { - updatedKeys := req.UpdatedKeys() - for _, v := range updatedKeys { - if v == "comment" { - comment, ok := req.Object.Raw()["comment"].(string) - if !ok { - return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"} - } - - if len(comment) < 5 { - fmt.Println(req.Object.ID, " 看起来像灌水评论:", comment)) - } - } - } - - return nil -}) -``` - - - - -### BeforeDelete - -在删除一个对象之前做一些检查工作,比如在删除一个相册 `Album` 前,先检查一下该相册中还有没有照片 `Photo`: - - - - -```js -AV.Cloud.beforeDelete("Album", function (request) { - // 查询 Photo 中还有没有属于这个相册的照片 - var query = new AV.Query("Photo"); - var album = AV.Object.createWithoutData("Album", request.object.id); - query.equalTo("album", album); - return query.count().then( - function (count) { - if (count > 0) { - // delete 操作会被丢弃 - throw new AV.Cloud.Error( - "Cannot delete an album if it still has photos in it." - ); - } - }, - function (error) { - throw new AV.Cloud.Error( - "Error " + - error.code + - " occurred when finding photos: " + - error.message - ); - } - ); -}); -``` - - - - -```python -import leancloud - -@engine.before_delete('Album') # Album 为需要 hook 的 class 的名称 -def before_album_delete(album): - query = leancloud.Query('Photo') - query.equal_to('album', album) - try: - matched_count = query.count() - except leancloud.LeanCloudError: - raise engine.LeanEngineError(message='An error occurred with LeanEngine.') - if count > 0: - # delete 操作会被丢弃 - raise engine.LeanEngineError(message='Cannot delete an album if it still has photos in it.') -``` - - - - -```java -@EngineHook(className = "Album", type = EngineHookType.beforeDelete) -public static LCObject albumBeforeDeleteHook(LCObject album) throws Exception { - LCQuery query = new LCQuery("Photo"); - query.whereEqualTo("album", album); - int count = query.count(); - if (count > 0) { - // delete 操作会被丢弃 - throw new Exception("Cannot delete an album if it still has photos in it."); - } else { - return album; - } -} -``` - - - - -```php -Cloud::beforeDelete("Album", function($album, $user) { - $query = new Query("Photo"); - $query->equalTo("album", $album); - try { - $count = $query->count(); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred when getting photo count: {$ex->getMessage()}"); - } - if ($count > 0) { - // delete 操作会被丢弃 - throw new FunctionError("Cannot delete an album if it still has photos in it."); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Album", LCEngineObjectHookType.BeforeDelete)] -public static async Task AlbumBeforeDelete(LCObject album) { - LCQuery query = new LCQuery("Photo"); - query.WhereEqualTo("album", album); - int count = await query.Count(); - if (count > 0) { - throw new Exception("Cannot delete an album if it still has photos in it."); - } - return album; -} -``` - - - - -```go -leancloud.Engine.BeforeDelete("Album", func(req *ClassHookRequest) (interface{}, error) { - photo := new(Photo) - if err := req.Object.Clone(photo); err != nil { - return nil, err - } - - count, err := client.Class("Photo").NewQuery().EqualTo("album", photo.Album).Count() - if err != nil { - return nil, err - } - - if count > 0 { - return nil, leancloud.CloudError{Code: 500, Message: "Cannot delete an album if it still has photos in it."} - } - - fmt.Println("Deleted.") - - return nil, nil -}) -``` - - - - -### AfterDelete - -在一个对象被删除后执行操作,例如递减计数、删除关联对象等等。同样以相册为例,这次我们不在删除相册前检查是否还有照片,而是在删除后,同时删除相册中的照片: - - - - -```js -AV.Cloud.afterDelete("Album", function (request) { - var query = new AV.Query("Photo"); - var album = AV.Object.createWithoutData("Album", request.object.id); - query.equalTo("album", album); - return query - .find() - .then(function (posts) { - return AV.Object.destroyAll(posts); - }) - .catch(function (error) { - console.error( - "Error " + - error.code + - " occurred when finding photos: " + - error.message - ); - }); -}); -``` - - - - -```python -import leancloud - -@engine.after_delete('Album') # Album 为需要 hook 的 class 的名称 -def after_album_delete(album): - query = leancloud.Query('Photo') - query.equal_to('album', album) - try: - query.destroy_all() - except leancloud.LeanCloudError: - raise leancloud.LeanEngineError(message='An error occurred with LeanEngine.') -``` - - - - -```java -@EngineHook(className = "Album", type = EngineHookType.afterDelete) -public static void albumAfterDeleteHook(LCObject album) throws Exception { - LCQuery query = new LCQuery("Photo"); - query.whereEqualTo("album", album); - List result = query.find(); - if (result != null && !result.isEmpty()) { - LCObject.deleteAll(result); - } -} -``` - - - - -```php -Cloud::afterDelete("Album", function($album, $user) { - $query = new Query("Photo"); - $query->equalTo("album", $album); - try { - $photos = $query->find(); - LeanObject::destroyAll($photos); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred when getting photo count: {$ex->getMessage()}"); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Album", LCEngineObjectHookType.AfterDelete)] -public static async Task AlbumAfterDelete(LCObject album) { - LCQuery query = new LCQuery("Photo"); - query.WhereEqualTo("album", album); - ReadOnlyCollection result = await query.Find(); - if (result != null && result.Count > 0) { - await LCObject.DeleteAll(result); - } -} -``` - - - - -```go -leancloud.Engine.AfterDelete("Album", func(req *ClassHookRequest) error { - photo := new(Photo) - if err := req.Object.Clone(photo); err != nil { - return nil, err - } - - count, err := client.Class("Photo").NewQuery().EqualTo("album", photo.Album).Count() - if err != nil { - return nil, err - } - - if count > 0 { - return nil, leancloud.CloudError{Code: 500, Message: "An error occurred with LeanEngine."} - } - - fmt.Println("Deleted.") - - return nil, nil -}) -``` - - - - -### OnVerified - -当用户通过邮箱或者短信验证时,对该用户执行特定操作。比如: - - - - -```js -AV.Cloud.onVerified("sms", function (request) { - console.log("User " + request.object + " is verified by SMS."); -}); -``` - -上面的代码示例中的 `object` 换成 `currentUser` 也可以。因为这里被操作的对象正好是发起操作的用户。 -下面的 `onLogin` 函数同理。 - - - - -```python -@engine.on_verified('sms') -def on_sms_verified(user): - print(user) -``` - - - - -```java -@EngineHook(className = "_User", type = EngineHookType.onVerifiedSMS) -public static void userOnVerifiedHook(LCUser user) throws Exception { - LOGGER.d("用户 " + user.getObjectId() + " 已通过短信验证。"); -} - -@EngineHook(className = "_User", type = EngineHookType.onVerifiedEmail) -public static void userOnVerifiedHook(LCUser user) throws Exception { - LOGGER.d("用户 " + user.getObjectId() + " 已通过邮箱验证。"); -} -``` - - - - -```php -Cloud::onVerifed("sms", function($user, $meta) { - error_log("User {$user->getUsername()} is verified by SMS."); -}); -``` - - - - -```cs -[LCEngineUserHook(LCEngineUserHookType.OnSMSVerified)] -public static void OnVerifiedSMS(LCUser user) { - Console.WriteLine($"用户 {user.ObjectId} 已通过短信验证。"); -} - -[LCEngineUserHook(LCEngineUserHookType.OnEmailVerified)] -public static void OnVerifiedEmail(LCUser user) { - Console.WriteLine($"用户 {user.ObjectId} 已通过邮箱验证。"); -} -``` - - - - -```go -leancloud.Engine.OnVerified("sms", func(req *ClassHookRequest) error { - fmt.Println("用户 ", req.User.ID, " 已通过短信验证。") -}) - -leancloud.Engine.OnVerified("email", func(req *ClassHookRequest) error { - fmt.Println("用户 ", req.User.ID, " 已通过邮箱验证。") -}) -``` - - - - -数据库中相关的验证字段,如 `emailVerified` 不需要修改,系统会自动更新。 - -该 hook 属于 after 类 hook。 - -### OnLogin - -在用户登录之时执行指定操作,比如禁止在黑名单上的用户登录: - - - - -```js -AV.Cloud.onLogin(function (request) { - // 因为此时用户还没有登录,所以用户信息是保存在 request.object 对象中 - console.log("User " + request.object + " is trying to log in."); - if (request.object.get("username") === "noLogin") { - // 如果是 error 回调,则用户无法登录(收到 401 响应) - throw new AV.Cloud.Error("Forbidden"); - } -}); -``` - - - - -```python -@engine.on_login -def on_login(user): - print(user) - if user.get('username') == 'noLogin': - # 如果抛出 LeanEngineError,则用户无法登录(收到 401 响应) - raise LeanEngineError('Forbidden') - # 没有抛出异常,函数正常执行完毕的话,用户可以登录 -``` - - - - -```java -@EngineHook(className = "_User", type = EngineHookType.onLogin) -public static LCUser userOnLoginHook(LCUser user) throws Exception { - if ("noLogin".equals(user.getUsername())) { - throw new Exception("Forbidden"); - } else { - return user; - } -} -``` - - - - -```php -Cloud::onLogin(function($user) { - error_log("User {$user->getUsername()} is trying to log in."); - if ($user->get("blocked")) { - // 如果抛出异常,则用户无法登录(收到 401 响应) - throw new FunctionError("Forbidden"); - } - // 没有抛出异常,函数正常执行完毕的话,用户可以登录 -}); -``` - - - - -```cs -[LCEngineUserHook(LCEngineUserHookType.OnLogin)] -public static LCUser OnLogin(LCUser user) { - if (user.Username == "noLogin") { - throw new Exception("Forbidden"); - } - return user; -} -``` - - - - -```go -leancloud.Engine.OnLogin(func(req *ClassHookRequest) error { - fmt.Println("用户 ", req.User.ID, " 已登录。") -}) -``` - - - - -该 hook 属于 before 类 hook。 - -### OnAuthData - -在云存储处理第三方登录的 `authData` 时触发,开发者可以在这个 Hook 中进行对 `authData` 的校验或转换。比如: - - - - -```js -AV.Cloud.onAuthData(function (request) { - let authData = request.authData; - console.log(authData); - - if (authData.weixin.code === "12345") { - authData.weixin.accessToken = "45678"; - } else { - // 校验失败,抛出异常,则用户无法登录 - throw new AV.Cloud.Error("invalid code"); - } - // 校验成功,返回校验或转换之后的 authData,用户继续登录流程 - return authData; -}); -``` - - - - -```python -@engine.on_auth_data -def on_auth_data(auth_data): - if auth_data['weixin']['code'] == '12345': - # 校验成功,返回校验或转换之后的 auth_data,用户继续登录流程 - auth_data['weixin']['code'] = '45678' - return auth_data - else: - # 校验失败,抛出异常,则用户无法登录 - raise LeanEngineError('invalid code') -``` - - - - -```cs -[LCEngineUserHook(LCEngineUserHookType.OnAuthData)] -public static Dictionary OnAuthData(Dictionary authData) { - if (authData.TryGetValue("fake_platform", out object tokenObj)) { - if (tokenObj is Dictionary token) { - // 模拟校验 - if (token["openid"] as string == "123" && token["access_token"] as string == "haha") { - LCLogger.Debug("Auth data Verified OK."); - } else { - throw new Exception("Invalid auth data."); - } - } else { - throw new Exception("Invalid auth data"); - } - } - return authData; -``` - - - - -该 hook 属于 before 类 hook。 - -### 防止死循环调用 - -你也许会好奇为什么可以在 AfterUpdate 中保存 `post` 而不会再次触发该 hook。 -这是因为云引擎对所有传入对象做了处理,以阻止死循环调用的产生。 - -不过请注意,以下情况还需要开发者自行处理: - -- 对传入对象进行 `fetch` 操作。 -- 重新构造传入的对象。 - -对于使用上述方式产生的对象,请根据需要自行调用禁用 hook 的接口: - - - - -```js -// 直接修改并保存对象不会再次触发 afterUpdate Hook 函数 -request.object.set("foo", "bar"); -request.object.save().then(function (obj) { - // 你的业务逻辑 -}); - -// 如果有 fetch 操作,则需要在新获得的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 -request.object - .fetch() - .then(function (obj) { - obj.disableAfterHook(); - obj.set("foo", "bar"); - return obj.save(); - }) - .then(function (obj) { - // 你的业务逻辑 - }); - -// 如果是其他方式构建对象,则需要在新构建的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 -var obj = AV.Object.createWithoutData("Post", request.object.id); -obj.disableAfterHook(); -obj.set("foo", "bar"); -obj.save().then(function (obj) { - // 你的业务逻辑 -}); -``` - - - - -```python -@engine.after_update('Post') -def after_post_update(post): - # 直接修改并保存对象不会再次触发 after_update Hook 函数 - post.set('foo', 'bar') - post.save() - - # 如果有 fetch 操作,则需要在新获得的对象上调用 disable_after_hook 来确保不会再次触发 Hook 函数 - post.fetch() - post.disable_after_hook() - post.set('foo', 'bar') - - # 如果是其他方式构建对象,则需要在新构建的对象上调用 disable_after_hook 来确保不会再次触发 Hook 函数 - post = leancloud.Object.extend('Post').create_without_data(post.id) - post.disable_after_hook() - post.save() -``` - - - - -```java -@EngineHook(className="Post", type = EngineHookType.afterUpdate) -public static void afterUpdatePost(LCObject post) throws LCException { - // 直接修改并保存对象不会再次触发 afterUpdate Hook 函数 - post.put("foo", "bar"); - post.save(); - - // 如果有 fetch 操作,则需要在新获得的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 - post.fetch(); - post.disableAfterHook(); - post.put("foo", "bar"); - - // 如果是其他方式构建对象,则需要在新构建的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 - post = LCObject.createWithoutData("Post", post.getObjectId()); - post.disableAfterHook(); - post.save(); -} -``` - - - - -```php -Cloud::afterUpdate("Post", function($post, $user) { - // 直接修改并保存对象不会再次触发 afterUpdate Hook 函数 - $post->set('foo', 'bar'); - $post->save(); - - // 如果有 fetch 操作,则需要在新获得的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 - $post->fetch(); - $post->disableAfterHook(); - $post->set('foo', 'bar'); - $post->save(); - - // 如果是其他方式构建对象,则需要在新构建的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 - $post = LeanObject::create("Post", $post->getObjectId()); - $post->disableAfterHook(); - $post->save(); -}); -``` - - - - -```cs -// 直接修改并保存对象不会再次触发 afterUpdate Hook 函数 -post["foo"] = "bar"; -await post.Save(); - -// 如果有 fetch 操作,则需要在新获得的对象上调用 DisableAfterHook 来确保不会再次触发 Hook 函数 -await post.Fetch(); -post.DisableAfterHook(); -post["foo"] = "bar"; - -// 如果是其他方式构建对象,则需要在新构建的对象上调用 DisableAfterHook 来确保不会再次触发 Hook 函数 -post = LCObject.CreateWithoutData("Post", post.ObjectId); -post.DisableAfterHook(); -await post.Save(); -``` - - - - -### Hook 错误响应码 - -为 `BeforeSave` 这类的 hook 函数定义错误码,需要这样: - - - - -```js -AV.Cloud.beforeSave("Review", function (request) { - // 使用 JSON.stringify() 将 object 变为字符串 - throw new AV.Cloud.Error( - JSON.stringify({ - code: 123, - message: "An error occurred.", - }) - ); -}); -``` - - - - -```python -@engine.before_save('Review') # Review 为需要 hook 的 class 的名称 -def before_review_save(review): - comment = review.get('comment') - if not comment: - raise leancloud.LeanEngineError( - code=123, - message='An error occurred.' - ) -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.beforeSave) -public static LCObject reviewBeforeSaveHook(LCObject review) throws Exception { - throw new LCException(123, "An error occurred."); -} -``` - - - - -```php -Cloud::beforeSave("Review", function($review, $user) { - $comment = $review->get("comment"); - if (!$comment) { - throw new FunctionError(json_encode(array( - "code" => 123, - "message" => "An error occurred.", - ))); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeDelete)] -public static void ReviewBeforeDelete(LCObject review) { - throw new LCException(123, "An error occurred."); -} -``` - - - - -```go -leancloud.Engine.BeforeSave("Review", func(req *ClassHookRequest) (interface{}, error) { - return nil, leancloud.CloudError{Code: 123, Message: "An error occurred."} -}) -``` - - - - -客户端收到的响应为 `Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." }`。可通过 **截取字符串** 的方式取出错误信息,再转换成需要的对象。 - -### Hook 超时 - -Before 类 Hook 函数的超时时间为 10 秒,其他类 Hook 函数的超时时间为 3 秒。如果 Hook 函数被其他的云函数调用(比如因为保存对象而触发 `BeforeSave` 和 `AfterSave`),那么它们的超时时间会进一步被其他云函数调用的剩余时间限制。 - -例如,如果一个 `BeforeSave` 函数是被一个已经运行了 13 秒的云函数触发,那么它就只剩下 2 秒的时间来运行。同时请参考 [云函数超时及处理方案](#云函数超时)。 - -## 即时通讯 Hook - -参见**即时通讯指南**中[万能的 Hook 机制](/sdk/im/guide/systemconv/#万能的-hook-机制)章节。 - -## 在线编写云函数 - -很多人使用云引擎是为了在服务端提供一些个性化的方法供各终端调用,而不希望关心诸如代码托管、npm 依赖管理等问题。为此我们提供了在线维护云函数的功能。使用此功能需要注意: - -- 在线定义的函数会覆盖你之前用 Git 或命令行部署的项目。 -- 目前只能在线编写云函数和 Hook,不支持托管静态网页、编写动态路由。 -- 只能使用 JavaScript SDK 和一些内置的 Node.js 模块(详见下节表格),无法引入其他模块作为依赖。 - - -**功能地址** - -![](https://capacity-files.lcfile.com/5DF59OXNHAQFIygBz9KCRchixpyLRnQf/Frame%202.png) - -![在线编写云函数](https://capacity-files.lcfile.com/6ltgdbheLhTKTFs78vxVENin0i6d7nWM/engine-snippets-list.png) - - - - -在 ** > 管理部署 > 云引擎分组 > 部署 > 在线编辑** 页,可以: - -- **创建函数**:指定函数类型、函数名称、函数体的具体代码、注释等信息,点击「创建」即可创建一个云函数。函数类型包括 Function(普通云函数)、Hook、Global(这里可以定义多个云函数的公共逻辑)。 -- **部署**:选择要部署的环境,点击「部署」即可看到部署过程和结果。 -- **预览**:会将所有函数汇总并生成一个完整的代码段,可以确认代码,或者将其保存为 `cloud.js` 覆盖项目模板的同名文件,即可快速地转换为使用项目部署。 -- **维护云函数**:可以编辑已有云函数,查看保存历史,以及删除云函数。 - -云函数编辑之后需要点击 **部署** 才能生效。 - -目前在线编辑仅支持 Node.js,最新的 `v3` 版本使用 Node.js 8.x 和 3.x 的 Node.js SDK,使用 Promise 写法,默认提供的依赖包有:async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js。 - -
    -点击展开在线编辑 SDK 版本详情 - -| 在线编辑版本 | Node.js SDK | JS SDK | Node.js | 备注 | 可用依赖 | -| ------------ | ----------- | ------ | ------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| v0 | 0.x | 0.x | 0.12 | 已不推荐使用 | moment, request, underscore | -| v1 | 1.x | 1.x | 4 | | async, bluebird, co, ejs, handlebars, joi, lodash, marked, moment, q, request, superagent, underscore | -| v2 | 2.x | 2.x | 6 | 需要使用 Promise 写法 | async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js | -| v3 | 3.x | 3.x | 8 | 需要使用 Promise 写法 | async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js | - -**从 v0 升级到 v1:** - -- JS SDK 升级到了 [1.0](https://github.com/leancloud/javascript-sdk/releases/tag/v1.0.0)。 -- 需要从 `request.currentUser` 获取用户,而不是 `AV.User.current`。 -- 在调用 `AV.Cloud.run` 时需要手动传递 user 对象。 - -**从 v1 升级到 v2:** - -- JS SDK 升级到 [2.0](https://github.com/leancloud/javascript-sdk/releases/tag/v2.0.0)(必须使用 Promise,不再支持 callback 风格)。 -- 删除了 `AV.Cloud.httpRequest`。 -- 在云函数中 **必须** 返回 Promise 作为云函数的值,抛出 `AV.Cloud.Error` 来表示错误。 - -**从 v2 升级到 v3:** - -- JS SDK 升级到了 [3.0](https://github.com/leancloud/javascript-sdk/releases/tag/v3.0.0)(`AV.Object.toJSON` 的行为变化等)。 - -
    - -
    -点击展开在线编辑和项目部署的关系 - -「在线编辑」的产生是为了方便大家初次体验云引擎,或者只是需要一些简单 hook 方法的应用使用。我们的实现方式就是把定义的函数拼接起来,生成一个云引擎项目然后部署。 - -所以可以认为「在线编辑」和「项目部署」最终是一样的,都是一个完整的项目。 - -定义函数是一个单独功能,可以让你不用使用基础包、git 等工具就能快速生成和编辑云引擎项目。 - -当然,你也可以使用基础包,自己写代码并部署项目。 - -这两条路是分开的,使用任何一种方式部署都会导致另一种方式失效。 - -
    - -
    -点击展开如何从在线编辑迁移到项目部署 - -1. 按照[云引擎命令行工具使用指南](/sdk/engine/cli/)安装命令行工具,使用 `lean new` 初始化项目,模板选择 `Node.js > Express`(我们的 Node.js 示例项目)。 -2. 在 ** > 管理部署 > 云引擎分组 > 部署 > 在线编辑** 点击 **预览**,将全部函数的代码拷贝到新建项目中的 `cloud.js`(替换掉原有内容)。 -3. 运行 `lean up`,在 的调试界面中测试云函数和 Hook,然后运行 `lean deploy` 部署代码到云引擎(使用标准实例的用户还需要执行 `lean publish`)。 -4. 部署后请留意云引擎控制台上是否有错误产生。 - -如果在线编辑使用的是 0.x 版本的 Node.js SDK,那么还需要修改不兼容的代码。 -比如将 `AV.User.current()` 改为 `request.currentUser`。 -详见 [升级到云引擎 Node.js SDK 1.0](https://leancloud.cn/docs/leanengine-node-sdk-upgrade-1.html)。 - -
    - -## 查看和运行云函数 - -** > 管理部署 > 云引擎分组 > 部署** 页面以表格的形式展示了应用各分组上定义的云函数(包括 Hook)的信息,包括云函数名称、所属分组、QPM(每分钟请求数)。在云函数表格中,点击 **运行** 按钮可以通过控制台调用云函数。 - -这里显示的是应用下所有分组中的云函数,包括在线编辑也包括项目部署。 - -![云函数列表](https://capacity-files.lcfile.com/Ci9L7WpfA8rHzBhwAOFvJqYhai1uTUSu/engine-functions-list.png) - -## 生产环境和预备环境 - -云引擎应用有「生产环境」和「预备环境」之分。在云引擎通过 SDK 调用云函数时,包括显式调用以及隐式调用(由于触发 hook 条件导致 hook 函数被调用),SDK 会根据云引擎所属环境(预备、生产)调用相应环境的云函数。例如,假定定义了 `beforeDelete` 云函数,在预备环境通过 SDK 删除一个对象,会触发预备环境的 `beforeDelete` hook 函数。 - -在云引擎以外的环境通过 SDK 显式或隐式调用云函数时,`X-LC-Prod` 的默认值一般为 `1`,也就是调用生产环境。但由于历史原因,各 SDK 的具体行为有一些差异: - -- 在 Node.js、PHP、Java、C# 这四个 SDK 下,默认总是调用生产环境的云函数。 -- 在 Python SDK 下,配合 lean-cli 本地调试时,且应用存在预备环境时,默认调用预备环境的云函数,其他情况默认调用生产环境的云函数。 -- 云引擎 Java 环境的模板项目 [java-war-getting-started] 和 [spring-boot-getting-started] 做了处理,配合 lean-cli 本地调试时,且应用存在预备环境时,默认调用预备环境的云函数,其他情况默认调用生产环境的云函数(与 Python SDK 的行为一致)。 - -[java-war-getting-started]: https://github.com/leancloud/java-war-getting-started/ -[spring-boot-getting-started]: https://github.com/leancloud/spring-boot-getting-started/ - -你还可以在 SDK 中指定客户端将请求所发往的环境: - - - -```cs -LCCloud.IsProduction = true; // production (default) -LCCloud.IsProduction = false; // staging -``` - -```java -LCCloud.setProductionMode(true); // production -LCCloud.setProductionMode(false); // staging -``` - -```objc -[LCCloud setProductionMode:YES]; // production (default) -[LCCloud setProductionMode:NO]; // staging -``` - -```swift -// production by default - -// staging -do { - let environment: LCApplication.Environment = [.cloudEngineDevelopment] - let configuration = LCApplication.Configuration(environment: environment) - try LCApplication.default.set( - id: {{appid}}, - key: {{appkey}}, - serverURL: "https://please-replace-with-your-customized.domain.com", - configuration: configuration) -} catch { - print(error) -} -``` - -```dart -LCCloud.setProduction(true); // production (default) -LCCloud.setProduction(false); // staging -``` - -```js -AV.setProduction(true); // production (default) -AV.setProduction(false); // staging -``` - -```python -leancloud.use_production(True) # production (default) -leancloud.use_production(False) # staging -# 需要在 SDK 初始化语句 `leancloud.init` 之前调用 -``` - -```php -Client::useProduction(true); // production (default) -Client::useProduction(false); // staging -``` - -```go -// 暂不支持(总是使用生产环境) -``` - - - -体验版云引擎应用只有「生产环境」,因此请不要切换到预备环境。 - -## 定时任务 - -定时任务可以按照设定,以一定间隔自动完成指定动作,比如半夜清理过期数据,每周一向所有用户发送推送消息等等。定时任务的最小时间单位是 **秒**,正常情况下时间误差都可以控制在秒级别。 - -定时任务是普通的云函数,也会遇到 [超时问题](#云函数超时),具体请参考 [超时处理方案](#超时的处理方案)。 - -一个定时任务如果在 24 小时内收到了超过 30 次的 `400`(Bad Request)或 `502`(Bad Gateway)的应答,它将会被云引擎禁用,同时系统会向开发者发出相关的禁用通知邮件。在控制台的日志中,对应的错误信息为 `timerAction short-circuited and no fallback available`。 - -部署云引擎之后,进入 ** > 管理部署 > 云引擎分组 > 定时任务**,点击 **创建定时任务**,然后设定执行的函数名称、执行环境等等。例如定义一个打印循环打印日志的任务 `logTimer`: - - - - -```js -AV.Cloud.define("logTimer", function (request) { - console.log("This log is printed by logTimer."); -}); -``` - - - - -```python -@engine.define -def logTimer(movie, **params): - print('This log is printed by logTimer.') -``` - - - - -```java -@EngineFunction("logTimer") -public static float logTimer throws Exception { - LogUtil.avlog.d("This log is printed by logTimer."); -} -``` - - - - -```php -Cloud::define("logTimer", function($params, $user) { - error_log("This log is printed by logTimer."); -}); -``` - - - - -```cs -[LCEngineFunction("logTimer")] -public static void LogTimer() { - Console.WriteLine("This log is printed by logTimer."); -} -``` - - - - -```go -leancloud.Engine.Define("logTimer", func(req *FunctionRequest) (interface{}, error) { - fmt.Println("This log is printed by logTimer.") - return nil, nil -}) -``` - - - - -![定时任务列表](https://capacity-files.lcfile.com/CDEURgOio8jgEHMVwKHDqLiR5LrVROql/engine-cronjobs-list.png) - -云引擎支持两种定时任务: - -- 使用 Cron 表达式安排调度 -- 以分钟为单位的简单循环调度 - -以 Cron 表达式为例,比如每周一早上 8 点打印日志(运行之前定义的 `logTimer` 函数),创建定时任务的时候,选择 **Cron 表达式** 并填入 `0 0 8 ? * MON`。 - -Cron 表达式的语法可以参考 [云队列(Cloud Queue)开发指南 § CRON 表达式](/sdk/engine/functions/cloud-queue/#cron-表达式)。 - -在配置定时任务时可以指定一些额外的非必填选项: - -- 运行参数:传递给云函数的参数(JSON 对象)。 -- 异常策略:任务因云函数超时失败后重试执行还是放弃执行,详见 [云队列指南 § 异常处理策略](/sdk/engine/functions/cloud-queue/#异常处理策略deliverymode具体影响哪些情况)。 - -「最近一次执行」会显示最近一次执行的时间和详情,但目前这个数据仅保留 5 分钟,在查看详情中: - -- `status` 任务的状态,包括 `success`(成功)、`failed`(失败) -- `uniqueId` 任务的唯一 ID -- `finishedAt` 执行完成的精确时间(仅限成功任务) -- `statusCode` 云函数响应的 HTTP 状态码(仅限成功任务) -- `result` 来自云函数的响应(仅限成功任务) -- `error` 错误提示(仅限失败任务) -- `retryAt` 下次重试时间(仅限失败任务) - -定时任务的结果(执行日志)可以在 ** > 管理部署 > 云引擎分组 > 日志** 中查看。 diff --git a/leancloud/docs/sdk/engine/functions/rest-api.mdx b/leancloud/docs/sdk/engine/functions/rest-api.mdx deleted file mode 100644 index 01c75b096..000000000 --- a/leancloud/docs/sdk/engine/functions/rest-api.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: 云函数 REST API -sidebar_label: REST API -sidebar_position: 5 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -:::info -这篇文档会是关于 REST API 的深入介绍,如需了解云函数和 Hook 的用法请看 [云函数和 Hook 开发指南](/sdk/engine/functions/guides)。 -::: - -云服务提供了统一的访问云函数的 REST API 接口,所有的客户端 SDK 也都是封装了这个接口从而实现对云函数的调用。 - -我们推荐使用 [Postman](https://www.postman.com/) 来调试 REST API。 - -## Base URL - -REST API 请求的 Base URL(下文 curl 示例中用 `{{host}}` 表示)即应用绑定的 API 自定义域名,可以在控制台绑定、查看。详见文档关于[域名](/sdk/storage/guide/setup-dotnet/#域名)[域名](/sdk/domain/guide/)的说明。 - -请求格式请参考 [这里](/sdk/storage/guide/rest/#请求格式)。 - -## 概览 - -| URL | HTTP | 功能 | -| ----------------------------------- | ---- | ---------------------------------------- | -| /1.1/functions/<functionName> | POST | 调用云函数 | -| /1.1/call/<functionName> | POST | 调用云函数,支持 LCObject 作为参数和结果 | - -## 预备环境和生产环境 - -在客户端通过 REST API 调用云函数时,可以设置 HTTP 头 `X-LC-Prod` 来区分调用的环境。 - -- `X-LC-Prod: 0` 表示调用预备环境 -- `X-LC-Prod: 1` 表示调用生产环境 - -通过 SDK 调用云函数时,SDK 会根据当前环境设置 `X-LC-Prod` HTTP 头,详见 [云函数开发指南 § 生产环境和预备环境](/sdk/engine/functions/guides/#生产环境和预备环境) 中的说明。 - -## 云函数 - -通过 `POST /functions/:name` 可以调用云函数,参数和结果都是 JSON 格式。 -例如,我们传入电影的名字来获取电影的目前的评分: - -```sh -curl -X POST -H "Content-Type: application/json; charset=utf-8" \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -d '{"movie":"夏洛特烦恼"}' \ -https://{{host}}/1.1/functions/averageStars -``` - -响应: - -```json -{ - "result": { - "movie": "夏洛特烦恼", - "stars": "2.5" - } -} -``` - -如果调用的云函数需要关联用户,那么可以通过 `X-LC-Session` 传入相应的 `sessionToken`: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{}' \ - https://{{host}}/1.1/functions/hello -``` - -有些时候我们希望使用 LCObject 作为云函数的参数,或者希望以 LCObject 为云函数的返回值,这时我们可以使用 `POST /1.1/call/:name` 这个 RPC 调用的 API,云函数 SDK 会将参数解释为一个 LCObject,同时在返回 LCObject 时提供必要的元信息: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"__type": "Object", "className": "Post", "pubUser": "LeanCloud 官方客服"}' \ - https://{{host}}/1.1/call/addPost -``` - -响应: - -```json -{ - "result": { - "__type": "Object", - "className": "Post", - "pubUser": "官方客服" - } -} -``` - -RPC 调用时,不仅可以返回单个 LCObject,还可以返回包含 LCObject 的数据结构。 -例如,假设有一个云函数返回一个数组,其中包含一个数字和一个 Todo 对象,那么 RPC 调用的结果为: - -```json -{ - "result": [ - 1, - { - "title": "工程师周会", - "createdAt": { - "__type": "Date", - "iso": "2019-04-28T08:34:12.932Z" - }, - "updatedAt": { - "__type": "Date", - "iso": "2019-04-28T08:34:12.932Z" - }, - "objectId": "5cc5658443e78cb53fe7b731", - "__type": "Object", - "className": "Todo" - } - ] -} -``` - -在通过 SDK 进行 RPC 调用时,SDK 会据此自动反序列化。 - -如果云函数超时,客户端会收到 HTTP status code 为 503、524、141 等的响应。 diff --git a/leancloud/docs/sdk/engine/functions/sdk.mdx b/leancloud/docs/sdk/engine/functions/sdk.mdx deleted file mode 100644 index 8498cdfce..000000000 --- a/leancloud/docs/sdk/engine/functions/sdk.mdx +++ /dev/null @@ -1,741 +0,0 @@ ---- -title: 云引擎 SDK 使用指南 -sidebar_label: 云引擎 SDK -sidebar_position: 3 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; -import { Conditional } from "/src/docComponents/conditional"; - -:::info -这篇文档是关于云引擎 SDK 的深入介绍,如需了解云函数和 Hook 的用法请看 [云函数和 Hook 开发指南](/sdk/engine/functions/guides)。 -::: - -云引擎 SDK 通常基于 [数据存储](/sdk/storage/features)[数据存储](/sdk/storage/overview/) 服务的 SDK,并提供了云函数和 Hook 等额外能力,供开发者在云引擎上更方便地开发后端应用。 - -## 接入云引擎 SDK - -我们的示例项目默认已经接入了 SDK,如果你需要将云引擎 SDK 接入到已有的项目中,可以按照下面的步骤操作: - - - - -```sh -npm install leanengine leancloud-storage -``` - -然后在代码中将云引擎中间件挂载到 Express 框架上: - -```js title='app.js' -var express = require("express"); -var AV = require("leanengine"); - -AV.init({ - appId: process.env.LEANCLOUD_APP_ID, - appKey: process.env.LEANCLOUD_APP_KEY, - masterKey: process.env.LEANCLOUD_APP_MASTER_KEY, -}); - -var app = express(); -app.use(AV.express()); -app.listen(process.env.LEANCLOUD_APP_PORT); -``` - -其中,`AV.express` 接受一个可选参数 `options`,`options` 是一个对象,目前支持以下两个可选属性: - -- `onError`:全局错误处理函数,云函数(包括 Hook 函数)抛出异常时会调用该函数。该函数的使用场景包括统一发送错误报告。 -- `ignoreInvalidSessionToken`:布尔值,为真时忽略客户端发来的错误的 `sessionToken`(`X-LC-session` 头),为假时抛出 `401` 错误 `{"code": 211, "error": "Verify sessionToken failed, maybe login expired: ..."}`。客户端 SDK 发送请求时会统一发送 `X-LC-session` 头(其中指定了 `sessionToken`),`sessionToken` 可能因种种原因失效,而云函数在很多情况下并不关心 `sessionToken`。因此,云引擎提供了 `ignoreInvalidSessionToken` 这个选项,设为真时忽略 `sessionToken` 错误。反之,如果该选项设为假,客户端收到相应报错时,需要重新登录。 - -关于云引擎 SDK 的详细 API 文档见 [LeanEngine Node SDK · API.md](https://github.com/leancloud/leanengine-node-sdk/blob/master/API.md)。 - -
    -点击展开 Koa 框架接入方法 - -```js title='app.js' -var koa = require("koa"); -var AV = require("leanengine"); - -AV.init({ - appId: process.env.LEANCLOUD_APP_ID, - appKey: process.env.LEANCLOUD_APP_KEY, - masterKey: process.env.LEANCLOUD_APP_MASTER_KEY, -}); - -var app = koa(); -app.use(AV.koa()); -app.listen(process.env.LEANCLOUD_APP_PORT); -``` - -
    - -
    -点击展开 Node SDK 不同版本的差异 - -Node SDK 的历史版本: - -- `1.x`:彻底废弃了全局的 `currentUser`,依赖的 JavaScript 也升级到了 1.x 分支,支持了 Koa 和 Node.js 4.x 及以上版本。 -- `2.x`:提供了对 Promise 风格的云函数、Hook 写法的支持,移除了一些被弃用的特性(`AV.Cloud.httpRequest`),不再支持 Backbone 风格的回调函数。 -- `3.x`:**推荐使用** 的版本,指定 JavaScript SDK 为 peer dependency(允许自定义 JS SDK 的版本),升级 JS SDK 到 3.x。 - -详见 Node.js SDK 的 [更新日志](https://github.com/leancloud/leanengine-node-sdk/releases)。 - -
    - -你可以在 [GitHub](https://github.com/leancloud/leanengine-node-sdk) 上找到 Node SDK 的源代码。 - -
    - - -将 `leancloud` 添加到 `requirements.txt` 中: - -``` -leancloud>=2.9.4,<3.0.0 -``` - -在本地运行和调试项目的时候,可以在项目目录下使用如下命令进行依赖安装: - -```sh -pip install -r requirements.txt -``` - -然后在代码中加载 SDK,因为 `wsgi.py` 是项目最先被执行的文件,推荐在此文件进行 Python SDK 的初始化: - -```python -import os -import leancloud - -APP_ID = os.environ['LEANCLOUD_APP_ID'] -APP_KEY = os.environ['LEANCLOUD_APP_KEY'] -MASTER_KEY = os.environ['LEANCLOUD_APP_MASTER_KEY'] - -leancloud.init(APP_ID, app_key=APP_KEY, master_key=MASTER_KEY) - -leancloud.use_master_key(True) -``` - -SDK 默认开启了 masterKey 权限,会跳过 ACL 和其他权限限制,详见 [使用超级权限](#使用超级权限)。 - -
    -点击展开 PyPI 上 leancloud-sdkleancloud 两个包的差别 - -`leancloud-sdk` 是旧版的 Python SDK,已经不再维护,请使用 `leancloud`。 - -不同版本的差别详见 Python SDK 的 [更新日志](https://github.com/leancloud/python-sdk/blob/master/changelog)。 - -
    - -你可以在 [GitHub](https://github.com/leancloud/python-sdk) 上找到 Python SDK 的源代码。 - -
    - - -在 `pom.xml` 中增加依赖配置来增加 LeanEngine Java SDK 的依赖: - -```xml - - - cn.leancloud - engine-core - 8.2.1 - - -``` - -在程序中初始化 SDK: - -```java -import cn.leancloud.LCCloud; -import cn.leancloud.LCObject; -import cn.leancloud.core.GeneralRequestSignature; -import cn.leancloud.LeanEngine; - -String appId = System.getenv("LEANCLOUD_APP_ID"); -String appKey = System.getenv("LEANCLOUD_APP_KEY"); -String appMasterKey = System.getenv("LEANCLOUD_APP_MASTER_KEY"); -String hookKey = System.getenv("LEANCLOUD_APP_HOOK_KEY"); - -LeanEngine.initialize(appId, appKey, appMasterKey); - -GeneralRequestSignature.setMasterKey(appMasterKey); -``` - -上面的代码默认开启了 masterKey 权限,会跳过 ACL 和其他权限限制,详见 [使用超级权限](#使用超级权限)。 - -你可以在 [GitHub](https://github.com/leancloud/java-unified-sdk) 上找到 Java SDK 的源代码。 - - - - -安装依赖: - -```sh -composer require leancloud/leancloud-sdk -``` - -初始化 SDK: - -```php -use \LeanCloud\Client; - -Client::initialize( - getenv("LEANCLOUD_APP_ID"), - getenv("LEANCLOUD_APP_KEY"), - getenv("LEANCLOUD_APP_MASTER_KEY") -); - -Client::useMasterKey(true); -``` - -上面的代码默认开启了 masterKey 权限,会跳过 ACL 和其他权限限制,详见 [使用超级权限](#使用超级权限)。 - -你可以在 [GitHub](https://github.com/leancloud/php-sdk) 上找到 PHP SDK 的源代码。 - - - - -添加依赖: - -```sh -dotnet add package LeanCloud.Storage -``` - -初始化 SDK: - -```cs -LCEngine.Initialize(services); -``` - -你可以在 [GitHub](https://github.com/leancloud/csharp-sdk) 上找到 .NET SDK 的源代码。 - - - - -添加依赖: - -```go -import "github.com/leancloud/go-sdk/leancloud" -``` - -初始化 SDK: - -```go -client := leancloud.NewEnvClient() -leancloud.Engine.Init(client) -``` - -Go SDK 以标准库 HTTP 方法的形式提供了可供任意框架接入的接口,以 **echo** 为示例: - -```go -// ./adapters/echo.go -//... -func Echo(e *echo.Echo) { - e.Any("/1/*", echo.WrapHandler(leancloud.Engine.Handler()), setResponseContentType) - e.Any("/1.1/*", echo.WrapHandler(leancloud.Engine.Handler()), setResponseContentType) - e.Any("/__engine/*", echo.WrapHandler(leancloud.Engine.Handler()), setResponseContentType) -} - -func setResponseContentType(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - c.Response().Header().Set("Content-Type", "application/json; charset=UTF-8") - return next(c) - } -} -``` - -函数 **Echo** 接收 echo 实例对象,将 Go SDK 中提供 LeanEngine 相关功能的接口绑定到 `/1/` `/1.1/` 和 `/__engine/` 开头的路由前缀上,保证 LeanEngine 相关的底层功能正常。 - -大多数 Go Web 框架均提供将标准库 HTTP Handler 转换为特有 Handler 的方法,只要保证能够在其他框架中接入以上两个部件,即可将 LeanEngine 集成入你的 Go Web 框架中。 - -你可以在 [GitHub](https://github.com/leancloud/go-sdk) 上找到 Go SDK 的源代码。 - - -
    - -## 使用数据存储服务 - -接入 SDK 后,在云引擎中你就可以调用 [数据存储](/sdk/storage/features)[数据存储](/sdk/storage/overview/) 服务作为数据库来存储数据,或者使用文件、短信、推送等功能。可以查看数据存储服务对应语言的文档了解详情。 - -数据存储相关功能可以在云函数和 Hook 中使用,也可以在程序的其他部分(如自行选用的 Web 框架)中使用。 - -## 使用超级权限 - -因为云引擎运行在可信的服务器端环境中,所以可以使用 Master Key(超级权限)跳过 ACL 和 Class 权限的检查,没有限制地修改数据存储中的数据;还可以使用一些仅限 Master Key 调用的管理员接口,如 [遍历 Class(scan)](/sdk/storage/guide/rest/#遍历-class)。 - -所以你可以全局开启超级权限(`Master Key`),这样会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据,也允许调用一些仅供 `Master Key` 使用的 API。 - - - - -全局开启 Master Key: - -```js title='sever.js' -AV.Cloud.useMasterKey(); -``` - -如果没有添加这些代码,默认是没有超级权限的,这意味着在云引擎中你也不能修改被 ACL 保护的数据,你需要在进行操作时手动指定 `sessionToken`,让操作以这个用户的权限来执行: - -```js -const post = new Post(); -post.save( - { author: user } - // 或者使用 request.sessionToken(网站托管中需启用 `Cloud.CookieSession`) - // { - // sessionToken: user.getSessionToken() - // } -); -``` - -或者你也可单独对某一个操作使用 `Master Key`,跳过权限检查: - -```js -post.destroy({ useMasterKey: true }); -``` - -当然你也可以在启用了 Master Key 的情况下使用 `useMasterKey: false` 来对单个操作关掉 Master Key。 - - - - -```python -# 通常位于 wsgi.py -leancloud.use_master_key(True) -``` - - - - -```java -// 通常位于 src/…/AppInitListener.java -RequestSignImplementation.setMasterKey(appMasterKey); -``` - - - - -```php -// 通常位于 src/app.php -Client::useMasterKey(true); -``` - - - - -```cs -LCApplication.UseMasterKey = true; -``` - - - - -Go SDK 中每个请求都可以使用 `UseMasterKey()` 为请求带上 `Master Key` 来开启超级权限,只需要作为可选参数传入最后即可,例如 `Create` `Set` `Update` 等操作。 - - - - -那么究竟是否应该使用 Master Key 呢,我们的建议如下: - -- 如果你的云引擎代码中特权操作比较多、操作不属于用户的全局数据比较多,那么建议全局开启 `Master Key`,并自行做好对于用户请求的权限检查。 -- 如果你的云引擎代码中的请求通常和单个用户自己的数据相关、需要遵守 ACL,那么建议不开启 `Master Key`,将用户请求的 `sessionToken` 传入数据修改的相关操作。 - -关于云引擎上的权限问题,还可以参考 [ACL 权限管理开发指南](/sdk/storage/guide/acl/) 和 [在云引擎中使用 ACL](/sdk/storage/guide/engine-acl/)。 - -## 用户状态管理 - -:::caution -因云引擎属于多主机、多进程的运行环境,因此内存型的 Session 是无法正确工作的(如 Node.js 的 [express-session](https://github.com/expressjs/session) 默认的 MemoryStore、PHP 内建的 `$_SESSION`)。 -::: - -### 使用 HTTP Header - -如果你的页面主要是由浏览器端渲染,那么建议在前端使用 SDK 登录用户,调用 SDK 的接口获取 Session Token,通过 HTTP Header 等方式将 Session Token 发送给后端。 - - - - -例如,在前端登录用户并通过 `user.getSessionToken()` 获取 Session Token 并发送给后端: - -```js -AV.User.login(user, pass).then((user) => { - return fetch("/profile", { - headers: { - "X-LC-Session": user.getSessionToken(), - }, - }); -}); -``` - -相应的后端 Node.js 代码: - -```js -app.get("/profile", function (req, res) { - AV.User.become(req.headers["x-lc-session"]) - .then((user) => { - res.send(user); - }) - .catch((err) => { - res.send({ error: err.message }); - }); -}); - -app.post("/todos", function (req, res) { - var todo = new Todo(); - todo - .save(req.body, { sessionToken: req.headers["x-lc-session"] }) - .then(() => { - res.send(todo); - }) - .catch((err) => { - res.send({ error: err.message }); - }); -}); -``` - - - - -### CookieSession - -如果你的页面主要由服务端渲染,可以使用我们在部分 SDK 中提供的 Cookie Session 组件,它可以将数据存储服务中的 Session Token 存储在 Cookie 中,简化服务器端对于用户登录状态的管理。 - -:::danger -使用 Cookie 作为鉴权方式需要注意防范 [防御 CSRF 攻击](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)。 - -业界通常使用 CSRF Token 来防御 CSRF 攻击,你需要传递给客户端一个随机字符串(即 CSRF Token,可通过 Cookie 传递),客户端在每个有副作用的请求中都要将 CSRF 包含在请求正文或 Header 中,服务器端需要校验这个 CSRF Token 是否正确。 -::: - - - - -如果你的页面主要是由服务器端渲染(例如使用 EJS、Pug),在前端不需要使用 JavaScript SDK 进行数据操作,那么可以使用 `AV.Cloud.CookieSession` 中间件,在 Cookie 中维护用户状态: - -```js -// Express -app.use( - AV.Cloud.CookieSession({ - secret: "my secret", - maxAge: 3600000, - fetchUser: true, - }) -); -// Koa -app.use( - AV.Cloud.CookieSession({ - framework: "koa", - secret: "my secret", - maxAge: 3600000, - fetchUser: true, - }) -); -``` - -你需要传入一个 `secret` 用于签名 Cookie(必须提供),这个中间件会将 `AV.User` 的登录状态信息记录到 Cookie 中,用户下次访问时自动检查用户是否已经登录,如果已经登录,可以通过 `req.currentUser` 获取当前登录用户。 - -`AV.Cloud.CookieSession` 支持的选项包括: - -- **fetchUser**:是否自动 `fetch` 当前登录的 `AV.User` 对象。默认为 `false`。如果设置为 `true`,每个 HTTP 请求都将发起一次 API 调用来 `fetch` 用户对象。如果设置为 `false`,默认只可以访问 `req.currentUser` 的 `id`(`_User` 表记录的 `objectId`)和 `sessionToken` 属性,你可以在需要时再手动 `fetch` 整个用户。 -- **name**:Cookie 的名字,默认为 `avos.sess`。 -- **maxAge**:Cookie 的过期时间。单位为毫秒。 - -在 Node SDK 中不再允许通过 `AV.User.current()` 获取登录用户的信息,而是需要你: - -- 通过 `request.currentUser` 获取用户信息。 -- 在后续的方法调用显式传递 user 对象。 - -
    -点击展开一个具有登录功能的站点的例子 - -```js -app.post("/login", function (req, res) { - AV.User.logIn(req.body.username, req.body.password).then( - function (user) { - res.saveCurrentUser(user); // save cookie - res.redirect("/profile"); - }, - function (error) { - res.redirect("/login"); - } - ); -}); - -app.get("/profile", function (req, res) { - if (req.currentUser) { - res.send(req.currentUser); - } else { - res.redirect("/login"); - } -}); - -app.get("/logout", function (req, res) { - req.currentUser.logOut(); - res.clearCurrentUser(); // clear cookie - res.redirect("/profile"); -}); -``` - -
    - -
    -点击展开浏览器对跨域 Cookie 的限制(SameSite - -Chrome 80 起 `SameSite` 的默认值为 `Lax`,如果你的应用的前端没部署在云引擎上,又需要向云引擎发送携带 Cookie 的 POST 请求,那么需要设置 `SameSite` 为 `none`。 - -`AV.Cloud.CookieSession` 会将所有参数都传递给浏览器的 `cookies.set()`,所以你可以将 `sameSite` 传入: - -```js -AV.Cloud.CookieSession({ sameSite: "none" }); -``` - -注意: - -- `SameSite` 要求与 `Secure` 标记一同发送,因此请确保你的客户端是通过 HTTPS 协议访问云引擎的。 -- 请仅在有必要的时候设置 `SameSite` 为 `none`,以免平白增加 CSRF 风险。 - -
    - -
    - - -Python SDK 提供了一个 `leancloud.engine.CookieSessionMiddleware` 的 WSGI 中间件,使用 Cookie 来维护用户(`leancloud.User`)的登录状态。要使用这个中间件,可以在 `wsgi.py` 中将: - -```python -application = engine -``` - -替换为: - -```python -application = leancloud.engine.CookieSessionMiddleware(engine, secret=YOUR_APP_SECRET) -``` - -你需要传入一个 `secret` 的参数用于签名 Cookie(必须提供),这个中间件会将 `AV.User` 的登录状态信息记录到 Cookie 中,用户下次访问时自动检查用户是否已经登录,如果已经登录,可以通过 `leancloud.User.get_current()` 获取当前登录用户。 - -`leancloud.engine.CookieSessionMiddleware` 初始化时支持的非必须选项包括: - -- **name**:在 cookie 中保存的 session token 的 key 的名称,默认为 `leancloud:session`。 -- **excluded_paths**:指定哪些 URL path 不处理 session token,比如在处理静态文件的 URL path 上不进行处理,防止无谓的性能浪费。接受参数类型 `list`。 -- **fetch_user**:处理请求时是否要从存储服务获取用户数据,如果为 `False` 的话,`leancloud.User.get_current()` 获取到的用户数据上除了 `session_token` 之外没有任何其他数据,需要自己调用 `fetch()` 来获取。为 `True` 的话,会自动在用户对象上调用 `fetch()`,这样将会产生一次数据存储的 API 调用。默认为 `False`。 -- **expires**:设置 cookie 的失效日期(参考 [Werkzeug Document](https://werkzeug.palletsprojects.com/en/2.1.x/http/#werkzeug.http.dump_cookie))。 -- **max_age**:设置 cookie 在多少秒后失效(参考 [Werkzeug Document](https://werkzeug.palletsprojects.com/en/2.1.x/http/#werkzeug.http.dump_cookie))。 - - - - -云引擎提供了一个 `LeanCloud\Storage\CookieStorage` 模块,用 Cookie 来维护用户(`User`)的登录状态,要使用它可以在 `app.php` 中添加下列代码: - -```php -use \LeanCloud\Storage\CookieStorage; -Client::setStorage(new CookieStorage(60 * 60 * 24, "/")); -``` - -`CookieStorage` 支持传入秒作为过期时间,以及路径作为 cookie 的作用域。默认过期时间为 7 天。 - -可以通过 `User::getCurrentUser()` 来获取当前登录用户。你可以这样简单地实现一个具有登录功能的站点: - -```php -$app->get('/login', function($req, $res) { - // login page -}); - -$app->post('/login', function($req, $res) { - $params = $req->getQueryParams(); - try { - User::logIn($params["username"], $params["password"]); - return $res->withRedirect('/profile'); - } catch (Exception $ex) { - return $res->withRedirect('/login'); - } -}); - -$app->get('/profile', function($req, $res) { - $user = User::getCurrentUser(); - if ($user) { - return $res->getBody()->write($user->getUsername()); - } else { - return $res->withRedirect('/login'); - } -}); - -$app->get('/logout', function($req, $res) { - User::logOut(); - return $res->redirect("/"); -}); -``` - -一个简单的登录页面可以是这样: - -```html - - - -
    - - - - - -
    - - -``` - -`CookieStorage` 也支持保存其他属性: - -```php -$cookieStorage = Client::getStorage(); -$cookieStorage->set("key", "val"); -``` - -
    -
    - -## FAQ - -### 如何使用 SDK 重定向到 HTTPS? - -我们目前推荐在绑定自定义域名时勾选「强制 HTTPS」而不是使用 SDK 中的重定向中间件。 - - - -:::info -不过,「强制 HTTPS」选项目前只支持独立 IP。 -如果使用加速节点,仍需在项目代码层面实现重定向。 -::: - - - -
    -点击展开关于 SDK 中重定向到 HTTPS 的用法(不推荐) - -一些 SDK 提供了重定向至 HTTPS 的中间件,部署并发布到生产环境之后,访问你的 LeanEngine 网站都会强制通过 HTTPS 访问。 - - - - -Node.js(Express): - -```js -app.enable("trust proxy"); -app.use(AV.Cloud.HttpsRedirect()); -``` - -Node.js(Koa): - -```js -app.proxy = true; -app.use(AV.Cloud.HttpsRedirect({ framework: "koa" })); -``` - - - - -```python -import leancloud - -application = get_your_wsgi_func() - -application = leancloud.HttpsRedirectMiddleware(application) -``` - - - - -```php -SlimEngine::enableHttpsRedirect(); -$app->add(new SlimEngine()); -``` - - - - -```java -LeanEngine.setHttpsRedirectEnabled(true); -``` - - - - -```cs -app.UseHttpsRedirection(); -``` - - - -
    - -### 如何打印 SDK 发出的网络请求? - - - - -你可以通过设置一个 `DEBUG=leancloud:request` 的环境变量来打印由 SDK 发出的网络请求。在本地调试时你可以通过这样的命令启动程序: - -```sh -env DEBUG=leancloud:request lean up -``` - -当有对服务端的调用时,你可以看到类似这样的日志: - -```sh -leancloud:request request(0) +0ms GET https://{{host}}/1.1/classes/Todo?&where=%7B%7D&order=-createdAt { where: '{}', order: '-createdAt' } -leancloud:request response(0) +220ms 200 {"results":[{"content":"1","createdAt":"2016-08-09T06:18:13.028Z","updatedAt":"2016-08-09T06:18:13.028Z","objectId":"57a975a55bbb5000643fb690"}]} -``` - -我们不建议在线上生产环境开启这个日志,否则将会打印大量的日志。如有必要,可以指定 `DEBUG=leancloud:request:error`,只打印出错的网络请求。 - - - - -### 为什么 Pointer 字段中的数据没有完整地发给客户端? - - - - -> 将 JavaScript SDK 和 Node SDK 升级到 3.0 以上版本可以彻底解决该问题。 - -云函数在响应时会调用到 `AV.Object#toJSON` 方法,将结果序列化为 JSON 对象返回给客户端。在早期版本中 `AV.Object#toJSON` 方法为了防止循环引用,当遇到属性是 Pointer 类型会返回 Pointer 元信息,不会将 include 的其他字段添加进去,我们在 [JavaScript SDK 3.0](https://github.com/leancloud/javascript-sdk/releases/tag/v3.0.0) 中对序列化相关的逻辑做了重新设计,**将 JavaScript SDK 和 Node SDK 升级到 3.0 以上版本便可以彻底解决该问题**。 - -如果暂时无法升级 SDK 版本,可以通过这样的方式绕过: - -```javascript -AV.Cloud.define("querySomething", function (req, res) { - var query = new AV.Query("Something"); - // user 是 Something 表的一个 Pointer 字段 - query.include("user"); - query - .find() - .then(function (results) { - // 手动进行一次序列化 - results.forEach(function (result) { - result.set( - "user", - result.get("user") ? result.get("user").toJSON() : null - ); - }); - // 再返回查询结果给客户端 - res.success(results); - }) - .catch(res.error); -}); -``` - - - - -Python SDK 只会返回 Pointer 元信息,因此也需要额外进行一次查询并手动进行序列化(见 Node.js 的代码)。 - - - - -### RPC 调用云函数时,为什么会返回预期之外的空对象? - - - - -使用 Node SDK 定义的云函数,如果返回一个不是 AVObject 的值,比如字符串、数字,RPC 调用得到的是空对象(`{}`)。 -类似地,如果返回一个包含非 AVObject 成员的数组,RPC 调用的结果中该数组的相应成员也会被序列化为 `{}`。 -这个问题将在 Node SDK 的下一个大版本(4.0)中修复。 -目前绕过这一个问题的方法是将返回结果放在对象(`{}`)中返回。 - - - diff --git a/leancloud/docs/sdk/engine/overview.mdx b/leancloud/docs/sdk/engine/overview.mdx deleted file mode 100644 index dcd426f27..000000000 --- a/leancloud/docs/sdk/engine/overview.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: 云引擎服务总览 -sidebar_label: 总览 -sidebar_position: 1 ---- - -import PlatformIntroduction from "./_partials/platform-introduction.mdx"; -import { Conditional } from "/src/docComponents/conditional"; - - - -## 部署应用 - -:::tip -请从 [快速开始部署云引擎应用](/sdk/engine/deploy/getting-started/) 开始部署你的第一个应用,然后阅读特定语言的文档来了解更多有关云引擎运行环境的信息。 -::: - -| 运行环境 | 支持版本 | 支持包管理器 | 文档页面 | 示例项目 | -| -------- | --------------- | ------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Web 前端 | Node.js >= 0.12 | NPM / Yarn | [Web 前端运行环境](/sdk/engine/deploy/webapp/) | [Web 前端运行环境 § 快速开始](/sdk/engine/deploy/webapp/#快速开始) | -| Node.js | >= 0.12 | NPM / Yarn | [Node.js 运行环境](/sdk/engine/deploy/nodejs/) | [node-js-getting-started](https://github.com/leancloud/node-js-getting-started/) (Express) | -| Python | >= 2.7 | pip | [Python 运行环境](/sdk/engine/deploy/python/) | [python-getting-started](https://github.com/leancloud/python-getting-started) (Flask) | -| Java | 8, 11–15 | Maven / Gradle | [Java 运行环境](/sdk/engine/deploy/java/) | [servlet-getting-started](https://github.com/leancloud/servlet-getting-started)
    [spring-boot-getting-started](https://github.com/leancloud/spring-boot-getting-started) | -| PHP | 5.6, 7.0–8.0 | Composer (v1) | [PHP 运行环境](/sdk/engine/deploy/php/) | [slim-getting-started](https://github.com/leancloud/slim-getting-started) | -| .NET | 3.1 | dotnet | [.NET 运行环境](/sdk/engine/deploy/dotnet/) | [dotnet-core-getting-started](https://github.com/leancloud/dotnet-core-getting-started) | -| Go | >= 1.10 | go mod | [Go 运行环境](/sdk/engine/deploy/go/) | [golang-getting-started](https://github.com/leancloud/golang-getting-started) (Echo) | -| C++ | GCC 9.4 | Bazel | [C++ 运行环境](/sdk/engine/deploy/cpp/) | [cpp-socket](https://github.com/leancloud/leanengine-unit-test/tree/cpp-socket-bazel) (Bazel) | - -## 云函数和 Hook - -云函数是云引擎提供的一种经过高度封装的函数计算功能,可以自动地序列化 [数据存储](/sdk/storage/features)[数据存储](/sdk/storage/overview/) 服务中的各种数据类型,在我们的各个客户端 SDK 中也有对应的支持。Hook 功能则允许开发者在数据存储中的对象被创建、更新、删除,或用户登录、认证,实时通讯发送消息、创建对话、客户端上下线时触发自定义的逻辑,进行额外的权限检查。 - -:::tip -使用云函数几乎不需要你有传统后端开发经验,可以帮助开发者专注在业务逻辑上,请从 [快速部署云函数和 Hook](/sdk/engine/functions/getting-started/) 开始编写你的第一个云函数。 -::: - -基于云函数我们还提供了 [定时任务](/sdk/engine/functions/guides/#定时任务) 和 [云队列](/sdk/engine/functions/cloud-queue/) 等功能,可以对云函数进行更复杂的调度。如在特定时间或基于一定时间间隔来运行云函数,还有重试、去重、结果查询、延时任务等功能。 - -## LeanDB 数据库 - -除了使用 [数据存储](/sdk/storage/features)[数据存储](/sdk/storage/overview/) 服务外,云引擎也提供了业界使用广泛的一些数据库的托管方案: - -| 数据库 | 集群配置 | 集群可用性 | 文档页面 | -| ------------- | ------------------ | ---------------------------------- | --------------------------------------------------------- | -| Redis | 主从结构(1M/1S) | 默认高可用,自动切换 | [LeanCache 使用指南](/sdk/engine/database/redis/) | -| MongoDB | 副本集(1P/1S/1A) | 默认高可用,自动切换 | [LeanDB MongoDB 使用指南](/sdk/engine/database/mongo/) | -| MySQL | 主从结构(1M/1S) | 默认高可用,自动切换 | [LeanDB MySQL 使用指南](/sdk/engine/database/mysql/) | -| Elasticsearch | 单节点 / 三个节点 | 默认高可用,自动切换(三个节点时) | [LeanDB Elasticsearch 使用指南](/sdk/engine/database/es/) | - -## 更多 - - - -- **命令行工具** 可以用来部署、调试云函数项目,详见 [命令行工具 CLI 使用指南](/sdk/engine/cli/)。 -- 除了使用数据存储 SDK,云函数也提供了 **REST API**,详见 [云引擎 REST API 指南](/sdk/engine/functions/rest-api/)。 -- 云引擎支持绑定 **独立 IP**,详见 [云引擎独立 IP 指南](/sdk/engine/dedicated-IP)。 -- 在 [深入了解云引擎](/sdk/engine/deep-dive/) 中我们会向有经验的开发者介绍云引擎背后的一些细节。 - - - - - -- **命令行工具** 可以用来部署、调试云函数项目,详见 [命令行工具 CLI 使用指南](/sdk/engine/cli/)。 -- 除了使用数据存储 SDK,云函数也提供了 **REST API**,详见 [云引擎 REST API 指南](/sdk/engine/functions/rest-api/)。 -- 在 [深入了解云引擎](/sdk/engine/deep-dive/) 中我们会向有经验的开发者介绍云引擎背后的一些细节。 - - diff --git a/leancloud/docs/sdk/engine/platform.mdx b/leancloud/docs/sdk/engine/platform.mdx deleted file mode 100644 index 94a7fee7d..000000000 --- a/leancloud/docs/sdk/engine/platform.mdx +++ /dev/null @@ -1,287 +0,0 @@ ---- -title: 云引擎平台功能 -sidebar_label: 平台功能 -sidebar_position: 2 ---- - -import CloudCustomDomain from "./_partials/cloud-custom-domain.mdx"; -import PlatformIntroduction from "./_partials/platform-introduction.mdx"; -import PlatformRuntimes from "./_partials/platform-runtimes.mdx"; -import { CLI_BINARY } from "/src/constants/env.ts"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - - - -:::info - -这篇文档会帮助了解云引擎平台提供的功能,有关具体运行环境的详情请查看专门的文档页面: - - - -::: - -## 云引擎可以部署什么 - -云引擎是一个进程级别的运行环境,开发者只需关注应用的进程本身(如 `node` 进程或 Go 项目编译出的可执行文件)、专注于实现业务逻辑,而不需要关注操作系统级别的环境。 - -部署到云引擎的应用只需遵循业界通常的项目结构(例如 Node.js 项目需要有 `package.json`),云引擎就可以自动地为其准备运行环境,可以在 [总览](/sdk/engine/overview) 中查看特定运行环境的文档来了解详情。 - -云引擎对应用进程内部几乎没有干预,你可以选用你喜欢的开发框架和第三方组件库、自行组织项目的目录结构。云引擎的负载均衡会转发绑定域名下的所有 HTTP 请求,你可以在应用内使用 Web 框架来自行设计 HTTP API 中的路径、请求和响应的格式。 - -目前云引擎主要为无状态的 HTTP 服务而优化,不支持在文件系统上持久地存储数据,应用可以将数据存储到 [数据存储](/sdk/storage/features)[数据存储](/sdk/storage/overview/) 或 LeanDB 提供的 [Redis](/sdk/engine/database/redis/)、[MongoDB](/sdk/engine/database/mongo/) 和 [Elasticsearch](/sdk/engine/database/es/) 中。 - -

    - 要将一个已有的项目关联到云引擎应用,可以使用 {CLI_BINARY} switch: -

    - - - {`$ ${CLI_BINARY} switch -[?] Please select an app: - 1) my-engine-app - => 1 -Switching to my-engine-app (group: web)`} - - -## 查看日志 - -### 运行日志 - -在 ** > 管理部署 > 云引擎分组 > 日志** 中可以查看云引擎的部署和运行日志,还可以通过环境(预备环境、生产环境)、类型(标准输出、标准错误)、实例、日期时间进行筛选。 - -![云引擎日志](https://capacity-files.lcfile.com/CwCqJjVAKo64RaQUdwF40LWRmzPFx36Y/engine-logs.png) - -可以使用命令行工具将运行日志导出至本地,详见 [命令行工具使用指南 § 查看日志](/sdk/engine/cli/#查看日志)。 - -## 查看统计数据 - -** > 管理部署 > 云引擎分组 > 统计** 页面显示出当前应用下所有实例资源的使用情况,开发者由此可以判断实例资源是否即将或已经超限。 - -![云引擎统计](https://capacity-files.lcfile.com/n1JLtkSPrixFDUVYD5lqDqTUhpmvFPga/engine-metrics.png) - -#### 每分钟请求数 - -一段时间内云引擎每分钟处理的请求数。可以通过左上角的下拉菜单选择按不同请求类型(网站托管、云函数)和不同 HTTP 状态码分别查看。 - -#### 响应时间 - -若曲线图升高,说明 CPU 使用量达到或超过限制,实例的 CPU 资源紧张。同样可以按不同请求类型(网站托管、云函数)和不同 HTTP 状态码分别查看。 - -#### CPU - -曲线图展现出在一段时间内应用实际 CPU 的使用量。如果 CPU 接近或达到限制,应用表现为请求响应时间延长。 - -#### 内存 - -曲线图展现出在一段时间内应用内存的实际使用量。如果内存使用量达到限制,则相关的业务进程(比如 Node.js 进程或者 Python 进程)会因为内存溢出(OOM)而重启,从而导致业务处理中断,并且该实例在启动期间服务不可用。如果内存曲线图频繁地接近限制然后忽然下降,就说明内存使用超限导致了进程重启。 - -**显示各实例详情(勾选框)** 勾选后会在 CPU、内存图表中为每个实例单独绘制一条线(不勾选默认显示所有实例合计的数值)。 - -图表右上角可以切换时间范围:今天、昨天、过去 7 天。 - -## 部署与发布 - -### 预备环境和生产环境 - -标准版云引擎对每个分组提供了「生产环境」和「预备环境」,生产环境设计用来接收和处理线上请求;预备环境则提供了和生产环境几乎相同的环境、访问相同的数据,可以绑定单独的域名供开发者进行测试。TDSLeanCloud 的客户端 SDK 在调用云函数或进行可能触发 Hook 的数据存储操作时,可以设置请求的环境(`X-LC-Prod`)。 - -在开发过程中,你可以先将改动部署到预备环境,使用线上数据测试通过后再发布到生产环境。如果你希望有一个独立数据源的测试环境,建议单独创建一个应用。 - -体验版云引擎没有预备环境。 - -### 预览环境 - -云引擎可以自动将 Pull request 部署到预览环境,每个预览环境有单独的域名,允许你在接近生产的环境中测试通过后再合并 PR。 - -预览环境的生命周期和 PR 绑定,在创建 PR 后,云引擎会自动创建一个预览环境并部署改动,在 PR 有更新后会自动部署最新的改动,PR 合并或关闭一段时间后,预览环境也会自动删除。 - -和预备环境类似,预览环境使用和生产环境相同的环境变量、访问相同的数据;和预备环境不同的是,预览环境可以同时存在多个,且自动和 PR 保持同步,而预备环境每个分组最多只有一个,且需要手动部署要测试的代码。 - -在使用预览环境之前,你需要先在 ** > 管理部署 > 你的分组 > 设置** 中绑定一个泛域名,如 `*.previews.example.com`。所有预览环境都会自动分配这个泛域名下的子域名,类似 `pr-123.previews.example.com`。 - -预览环境目前只支持在 GitLab CI 或 GitHub Actions 中使用命令行工具部署,需要配置 CI 才能使用,详见 [命令行工具使用指南 § 使用预览环境](/sdk/engine/cli/#使用预览环境)。后续会支持不用配置 CI 自动同步的功能。 - -### 命令行部署 - -在你的项目根目录运行: - -{`${CLI_BINARY} deploy`} - -就会开始上传本地的代码,会交互式地询问要部署到生产环境还是预备环境,如需直接部署生产环境可以加上 `--prod` 参数,更多命令行工具的用法请查看 [命令行工具使用指南 § 从本地代码部署](/sdk/engine/cli/#从本地代码部署)。 - -### Git 部署 - -云引擎还支持从 Git 仓库拉取代码进行部署,包括 [GitHub](https://github.com/) 或 [Gitee](https://gitee.com/) 这样的第三方托管平台,和 [GitLab](https://about.gitlab.com/install/) 这样自己搭建的 Git 托管服务。你可以在 ** > 管理部署 > 你的分组 > 部署 > Git 部署** 中设置 Git 仓库的地址。 - -云引擎支持 SSH 协议的私有仓库(如 `git@github.com:leancloud/node-js-getting-started.git`),你需要在设置 Git 仓库地址后在 Git 托管服务处为云引擎配置 deploy key。如果你的仓库是公开的,则推荐使用 HTTPS 形式的仓库地址。 - -设置好 Git 仓库后,就可以在部署界面点击「部署」了,默认使用 `master` 分支,你也可以手动填写分支、标签或 commit hash。 - -如果希望 push 到项目的 Git 仓库的特定分支后自动触发云引擎部署,可以在应用的 ** > 管理部署 > 你的分组 > 设置 > 配置自动部署** 中生成一个 deploy token,然后查看页面上显示的 Webhook 地址。当该地址收到任意 POST 请求后,会部署指定分支的代码到指定的环境,通过 GitHub Action 配置自动部署的具体方法请参考控制台上的指引。 - -### 部署历史与回滚 - -在 ** > 管理部署 > 云引擎分组 > 部署** 页面,展开一个环境右上角的菜单,进入「版本」,可以查看这个环境的历史部署。 -每个历史部署版本会显示简短描述(基于 git 提交日志等信息)、部署版本号、部署时间。 -历史部署按部署时间倒序排列,当前部署排在最前。 -点击 **部署到** 按钮,可以回滚至相应的部署版本。 -除了按环境查看历史部署外,还可通过 **时间线** 按照时间顺序(最新部署在前)查看部署到云引擎的各个版本。 - -除了查看部署历史外,这里还会显示各个环境的部署状态(「休眠中」、「部署中」、「运行中」)。 -通过右上角的按钮还可以 **重启**(重新部署当前版本)或 **清除部署**(移除部署,注销相应的云函数和 Hook)。 -预备环境还可以点击右上角的 **部署到生产环境** 按钮将最近部署到预备环境的版本发布到生产环境。 -当部署状态为「部署中」时,控制台会显示部署进行中的一些信息,也会显示一个 **取消部署** 按钮,点击可以取消部署。 - -### 分组管理 - -云引擎支持在同一个应用下创建多个「分组」,运行不同的后端程序,在访问同一数据源的情况下,部署多套不同的服务器端业务代码,并对每个分组绑定不同的自定义域名,实现更复杂的业务需求: - -- 将用户界面和管理后台拆分为不同的项目,使用不同的域名。 -- 单独部署主系统之外的边缘支持系统,以免边缘系统出现问题时影响主系统。 -- 使用不同的服务器端语言来编写云函数和网站,例如你可以使用 Node.js 编写云函数,而用 PHP 来实现网站。 - -每个分组都有独立的预备环境用于测试代码、独立的域名供外部访问,每个分组的环境变量、代码仓库等设置也是独立的,你可以单独对一个组部署代码。 - -每个分组都可以部署云函数、Hook 和定时任务。但如果当前部署的代码中有和其他组同名的云函数,会中断部署,需勾选[「强制覆盖同名云函数」选项](#部署选项) 强制部署。 - -每个应用默认有一个 `web` 分组,你可以在 ** > 管理部署** 中创建额外的分组。新建的分组默认不包含任何实例(无法响应请求),你需要 [调整实例规格和数量](#调整实例规格和数量)。 - -### 部署选项 - -云引擎在进行部署时提供了一些选项: - -#### 不使用缓存(`--no-cache`) - -在遇到与依赖安装有关的问题时,可以尝试开启该选项,禁用加速构建的 [缓存机制](/sdk/engine/deep-dive/#构建)。 - -#### 打印构建日志(`--options 'printBuildLogs=true'`) - -在遇到与构建有关的问题时,可以尝试开启该选项来在日志中打印构建过程的详细日志。 - -#### 强制覆盖同名云函数(`--overwrite-functions`) - -在多个分组的云函数或 Hook 重复时,可以开启该选项强制进行部署(以最后一次部署的分组中的云函数或 Hook 为准),通常用于在分组之间移动云函数或 Hook。 - -## 管理资源和容量 - -### 体验版云引擎 - -云引擎对于每个应用都默认赠送一个 0.5 CPU、256 MB 的体验实例,可以免费使用,供开发者学习和测试云引擎。 - -**体验实例会执行休眠策略**,没有请求时会休眠,有请求时启动(首次启动可能需要十几秒的时间),每天最多运行 18 个小时。 - -
    -点击展开体验实例休眠详情 - -- 如果应用最近一段时间(半小时)没有任何外部请求,则休眠。 -- 休眠后如果有新的外部请求实例则马上启动。访问者的体验是第一个请求响应时间是 5 ~ 30 秒(视实例启动时间而定),后续访问响应速度恢复正常。 -- 强制休眠:如果最近 24 小时内累计运行超过 18 小时,则强制休眠。此时新的请求会收到 503 的错误响应码,该错误可在 ** > 管理部署 > 云引擎分组 > 统计** 中查看。 - -
    - -### 标准版云引擎 - - - -云引擎的计费独立于开发版、商用版方案,开通或取消商用版不影响云引擎的实例和计费。 - - - -:::tip -对于商业项目和正式上线的产品,我们建议开发者升级到标准版云引擎,并使用至少两个实例,来保证业务的可用性。 -::: - -标准实例不会像体验实例一样在没有请求时休眠,会一直保持运行;标准实例配有预备环境方便测试;如果购买了两个或更多的实例,还能进行负载均衡和故障切换,充分保障服务的可用性。 - -标准版的功能包括: - -#### 负载均衡(高可用) - -云引擎的网关会将客户端的请求轮流分配给每个实例,随着业务请求量的增加,开发者可以简单地通过增加实例数量来提升处理能力。 - -在同一分组的同一环境中有两个或更多的实例时,便可实现故障切换。当其中一个实例出现故障无法工作时,云引擎的网关会自动将接下来的请求转发到其他可以正常工作的实例上,等待故障的实例恢复后复原。 - -#### 预备环境 - -云引擎会为每个标准版分组赠送一个预备环境,它有着和生产环境几乎完全一样的运行环境。在正式上线前,开发者可以先将代码发布到预备环境,使用线上的环境和数据进行模拟测试。 - -预备环境规格与生产环境相同,与体验实例一样执行休眠策略。 - -#### 平滑部署 - -在部署新版本或其他运维操作时,系统会让新旧版本的实例同时运行一段时间,再关闭旧版本的实例,让服务保持零中断。 - -#### 多分组 - -多实例分组可以实现在访问同一数据源的情况下,部署多份云引擎代码,满足不同的业务需求。每个组可以绑定独立域名。详见 [分组管理](#分组管理)。 - -### 调整实例规格和数量 - -我们为标准版实例提供了几种不同的规格,差别主要体现在可供使用的内存上: - -| 规格 | 内存 | CPU | -| ------------- | ------- | ------ | -| standard-512 | 512 MB | 1 Core | -| standard-1024 | 1024 MB | 1 Core | -| standard-2048 | 2048 MB | 1 Core | -| standard-4096 | 4096 MB | 1 Core | - -在 ** > 管理部署 > 你的分组 > 部署** 页面下,可以修改实例的规格和数量。 - -:::tip -我们建议根据自己的程序运行时所需要的最大内存来选择实例规格,然后通过调整实例数量来应对请求量的增加。对于商业项目和正式上线的产品,我们建议使用至少两个实例,来保证业务的可用性。 -::: - -为了防止实例因为资源使用超限而受到影响,我们建议开发者经常 [查看统计数据](#查看统计数据): - -- 一天内平均 **内存** 使用超过可用资源的 **70%**(例如对于 1 个 standard-1024 来说就是 717 MB)就建议提高实例规格。 -- 一天内平均 **CPU** 使用超过可用资源的 **30%**(例如对于 1 个 standard-1024 来说就是 30% CPU)就建议增加实例数量。 - -
    -点击展开多实例运行详情 - -当一个分组中的一个环境里有多个实例时,我们称之为「多实例运行」。在部署或者运维操作(如重启)期间,也会有多个实例短暂地同时运行。 - -多个实例的内存和文件系统是独立的,这意味着在一个实例中写入全局变量或文件,其他实例无法读取,建议在首次切换到多实例运行时进行充分的测试。 - -如果需要在多个实例间低延迟地共享数据,可以使用 LeanCache。 - -
    - -### 自动伸缩 - -除了固定数量的实例外,标准版云引擎还可以设置自动伸缩,云引擎会按需动态调整实例的数量,在满足突发流量的同时减少成本。 - -云引擎使用实例的 CPU 使用量来决定是否需要伸缩,如果 CPU 用量超过一定范围就扩容一个实例,若 CPU 用量低于一定范围就缩容一个实例。 - -在控制台上可以设置实例数量的上下限,云引擎只会在限制的区间内扩容或缩容。设置下限可以保证总是存在一些实例提供服务,设置上限可以控制每天的成本在可接受的范围内。 - -通过配置决定是否伸缩的 CPU 用量范围,可以设置自动伸缩的策略: - -* 成本优先:保守的扩容策略,尽量降低成本 -* 性能优先:激进的扩容策略,保障高性能 -* 均衡:均衡的扩容策略 - -不管是哪种策略,云引擎在缩容时都比扩容保守,以避免在缩容后马上遇到需要扩容的情况。 - -### 实例计费规则 - -标准版云引擎按照所选择的规格乘以数量来进行计费,每天会按照前一天最大的实例数量来扣除前一天的费用。各规格的价格可以在当前节点的价格页面查看,扣费记录可在云服务控制台的消费明细开发者中心的账单中查看。 - -如果不想继续付费,可以在所有分组中将实例规格修改为「免费版」,标准实例会被删除,并在最后一个分组下赠送一个体验实例。在次日会进行最后一次扣费,之后便不会再产生费用。 - -## 调整设置 - -### 环境变量 - -我们推荐使用环境变量向运行在云引擎上的应用注入配置,你可以在 ** > 管理部署 > 你的分组 > 设置 > 自定义环境变量** 中添加自定义环境变量,修改环境变量后会在下一次部署时生效。 - -勾选了 **Secret** 的环境变量在保存后便不会在控制台显示,可以用在类似密钥、密码的字段上减少意外泄漏的可能。 - -云引擎运行环境默认提供的环境变量无法被自定义环境变量覆盖。 - -### 绑定自定义域名 - - diff --git a/leancloud/docs/sdk/im/_category_.json b/leancloud/docs/sdk/im/_category_.json deleted file mode 100644 index 7e7e6520d..000000000 --- a/leancloud/docs/sdk/im/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "即时通讯", - "collapsed": true, - "position": 18 -} diff --git a/leancloud/docs/sdk/im/best-practice/_category_.json b/leancloud/docs/sdk/im/best-practice/_category_.json deleted file mode 100644 index 91b3df724..000000000 --- a/leancloud/docs/sdk/im/best-practice/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "最佳实践", - "collapsed": true, - "position": 5 -} diff --git a/leancloud/docs/sdk/im/best-practice/hook-text-moderation.mdx b/leancloud/docs/sdk/im/best-practice/hook-text-moderation.mdx deleted file mode 100644 index 6a4b24361..000000000 --- a/leancloud/docs/sdk/im/best-practice/hook-text-moderation.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: 即时通讯文本检测 -sidebar_label: 文本检测 -slug: /sdk/im/best-practice/hook-text-moderation ---- - - - - - -本文介绍了如何通过即时通讯 hook 接入第三方的文本检测服务。 - -## 预备知识 - -本文涉及到即时通讯服务 hook 以及云引擎在线编辑云函数功能,首先请阅读相关文档了解这些功能: - -1. [详解消息 hook 与系统对话](/sdk/im/guide/systemconv/) -2. [云函数和 Hook 开发指南 § 在线编写云函数](/sdk/engine/functions/guides#在线编写云函数) - -## 操作步骤 - -1. 开启云引擎服务,选择「云引擎 -> 部署管理 -> web -> 部署 -> 在线编辑 -> 创建函数」,于弹出编辑窗口中选择类型 `Hook`,名称 `_messageReceived`,在下方编辑区域填入自行编写的函数。创建成功后点击「部署」,等待部署成功。 -![](https://capacity-files.lcfile.com/5DF59OXNHAQFIygBz9KCRchixpyLRnQf/Frame%202.png) - -2. 现在使用即时通讯服务发送消息,可以看到内容会根据敏感词情况做出相应的变化了。 - -## 代码示范 - -下面是一段 Node.js 版的云引擎 Hook 代码,供参考: - -```javascript -const https = require("https"); - -// 假设第三方文本检测服务通过 HTTP Header 鉴权 -const authToken = "第三方文本检测服务的鉴权 token"; - -const params = request.params; - -// 向第三方文本检测服务提交聊天内容和聊天用户的 ID -// 不同服务接受的参数不同,请根据实际情况修改 -const postData = JSON.stringify({ - data: { - text: params.content, - user_id: params.fromPeer, - }, -}); - -const options = { - hostname: "third-party-text-moderation.example.com", - port: 443, - path: "/path/to/text/moderation/interface", - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Third-Party-Auth": authToken, - }, -}; - -return new Promise((resolve, reject) => { - const req = https.request(options, (res) => { - if (res.statusCode != 200) { - // or resolve(null) - reject(new Error(`BAD STATUS: ${res.statusCode}`)); - return; - } - let body = ""; - res.setEncoding("utf8"); - res.on("data", (chunk) => { - body += chunk; - }); - res.on("end", () => { - json = JSON.parse(body); - - if (json.result == 0) { - resolve(null); - } else { - // 假设第三方文本检测服务会在 filtered_text 字段中返回过滤后的文本 - if (json.filtered_text) { - resolve({ content: json.filtered_text }); - } else { - resolve({ drop: true }); - } - } - }); - }); - - req.on("error", (e) => { - // or resolve(null) - reject(e); - }); - - req.write(postData); - req.end(); -}); -``` - -以上代码简单展示了如何在即时通讯服务中使用第三方文本检测服务,代码仅供参考,你需要根据实际业务场景来处理请求和响应结果。 -涉及到的即时通讯 hook 参数和文本检测服务请求与响应参数,请参考[即时通讯 \_messageReceived Hook](/sdk/im/guide/systemconv/#_messagereceived)和第三方文本检测服务的 API 文档。 diff --git a/leancloud/docs/sdk/im/features.mdx b/leancloud/docs/sdk/im/features.mdx deleted file mode 100644 index 67d23f53a..000000000 --- a/leancloud/docs/sdk/im/features.mdx +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: 即时通讯功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 -slug: /sdk/im/features ---- - - - - - -快速将即时通讯功能集成到你的游戏中去,借助丰富灵活的 API 轻松实现社交通讯、直播互动、在线客服、游戏对战等场景中的常见需求。 - -## 实用功能 - -即时通讯服务提供的主要功能有: - -### 基本功能 - -创建单聊、群聊、聊天室、公众号等多种类型对话;发送文本、语音、视频、位置以及其他自定义消息。 - -### 操作鉴权与签名 - -可以启用第三方应用服务端鉴权机制,实时控制用户行为,确保聊天过程中数据安全可靠。 - -### 单点登录与离线推送 - -默认支持多设备单点登录,可以将聊天消息转为推送通知送达离线用户。 - -### 消息 hook - -开发者可实时处理消息事件,根据业务需求改变默认路由。 - -### @、撤回和修改 - -支持在对话中提及某人;消息投递之后,还可以撤回和修改;支持本地缓存。 - -## 优势和特色 - -- 功能全面,接口灵活。提供敏感词等管理功能,支持公众号、客服号等特定对话形态,更有可编程消息 Hook 机制可灵活满足各类业务需求。 - -- 稳定可靠。已稳定服务数万开发团队,支撑日活千万的产品,99.9% 的可用性承诺。 - -- 支持消息下发峰值超过 1.2 亿条 / 分钟,完美应对高并发和爆发式活动推广。 - -- 精准对接。客户端 SDK 与 demo 代码完全开源,研发工程师直接回复工单和社区提问,7x24 小时紧急状态电话支持。 diff --git a/leancloud/docs/sdk/im/guide/_category_.json b/leancloud/docs/sdk/im/guide/_category_.json deleted file mode 100644 index 3446da847..000000000 --- a/leancloud/docs/sdk/im/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 2 -} diff --git a/leancloud/docs/sdk/im/guide/beginner.mdx b/leancloud/docs/sdk/im/guide/beginner.mdx deleted file mode 100644 index cfde4ebd8..000000000 --- a/leancloud/docs/sdk/im/guide/beginner.mdx +++ /dev/null @@ -1,5093 +0,0 @@ ---- -title: 一,从简单的单聊、群聊、收发图文消息开始 -sidebar_label: 基础功能 -sidebar_position: 1 -slug: /sdk/im/guide/beginner ---- - - - - - -import MultiLang from "/src/docComponents/MultiLang"; -import Mermaid from "/src/docComponents/Mermaid"; -import { Conditional } from "/src/docComponents/conditional"; - - -## 阅读准备 - -在阅读本章之前,如果你还不太了解即时通讯服务的总体架构,建议先阅读[即时通讯服务总览](/sdk/im/guide/overview/)。 -另外,如果你还没有下载对应开发环境(语言)的 SDK,请参考相应语言的 SDK 配置指南完成 SDK 安装与初始化: - - - -- [C# SDK 配置](/sdk/storage/guide/setup-dotnet/) -- [Java SDK 配置](/sdk/storage/guide/setup-java/) -- [Objective-C SDK 配置](/sdk/storage/guide/setup-objc/) - - - - - -- [C# SDK 配置](/sdk/storage/guide/setup-dotnet/) -- [Java SDK 配置](/sdk/storage/guide/setup-java/) -- [Objective-C SDK 配置](/sdk/storage/guide/setup-objc/) -- [JavaScript SDK 配置](/sdk/storage/guide/setup-js/) -- [Swift SDK 配置](/sdk/storage/guide/setup-swift/) -- [Flutter SDK 配置](/sdk/storage/guide/setup-flutter/) - - - -## 本章导读 - -在很多产品里面,都存在让用户实时沟通的需求,例如: - -- 员工与客户之间的实时交流,如房地产行业经纪人与客户的沟通,商业产品客服与客户的沟通,等等。 -- 企业内部沟通协作,如内部的工作流系统、文档/知识库系统,增加实时互动的方式可能就会让工作效率得到极大提升。 -- 直播互动,不论是文体行业的大型电视节目中的观众互动、重大赛事直播,娱乐行业的游戏现场直播、网红直播,还是教育行业的在线课程直播、KOL 知识分享,在支持超大规模用户积极参与的同时,也需要做好内容审核管理。 -- 应用内社交,游戏公会嗨聊,等等。社交产品要能长时间吸引住用户,除了实时性之外,还需要更多的创新玩法,对于标准化通讯服务会存在更多的功能扩展需求。 - -根据功能需求的层次性和技术实现的难易程度不同,我们分为多篇文档来一步步地讲解如何利用即时通讯服务实现不同业务场景需求: - -- 本篇文档,我们会从实现简单的单聊/群聊开始,演示创建和加入「对话」、发送和接收富媒体「消息」的流程,同时让大家了解历史消息云端保存与拉取的机制,希望可以满足在成熟产品中快速集成一个简单的聊天页面的需求。 -- [离线消息文档](/sdk/im/guide/intermediate/)会介绍一些特殊消息的处理,例如 @ 成员提醒、撤回和修改、消息送达和被阅读的回执通知等,离线状态下的推送通知和消息同步机制,多设备登录的支持方案,以及如何扩展自定义消息类型,希望可以满足一个社交类产品的多方面需求。 -- [权限与聊天室文档](/sdk/im/guide/senior/)会介绍一下系统的安全机制,包括第三方的操作签名,同时也会介绍直播聊天室和临时对话的用法,希望可以帮助开发者提升产品的安全性和易用性,并满足特殊场景的需求。 -- [Hook 与系统对话文档](/sdk/im/guide/systemconv/)会介绍即时通讯服务端 Hook 机制,系统对话的用法,以及给出一个基于这两个功能打造一个属于自己的聊天机器人的方案,希望可以满足业务层多种多样的扩展需求。 - -希望开发者最终顺利完成产品开发的同时,也对即时通讯服务的体系结构有一个清晰的了解,以便于产品的长期维护和定制化扩展。 - - -## 一对一单聊 - -在开始讨论聊天之前,我们需要介绍一下在即时通讯 SDK 中的 `IMClient` 对象: - -> `IMClient` 对应实体的是一个用户,它代表着一个用户以客户端的身份登录到了即时通讯的系统。 - -具体可以参考[即时通讯服务总览](/sdk/im/guide/overview/)中《clientId、用户和登录》一节的说明。 - -### 创建 `IMClient` - -假设我们产品中有一个叫「Tom」的用户,首先我们在 SDK 中创建出一个与之对应的 `IMClient` 实例(创建实例前请确保已经成功初始化了 SDK): - - - -```cs -LCIMClient tom = new LCIMClient("Tom"); -``` - -```java -// clientId 为 Tom -LCIMClient tom = LCIMClient.getInstance("Tom"); -``` - -```objc -// 定义一个常驻内存的属性变量 -@property (nonatomic) LCIMClient *tom; -// 初始化 -NSError *error; -tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error]; -if (error) { - NSLog(@"init failed with error: %@", error); -} else { - NSLog(@"init succeeded"); -} -``` - -```js -// Tom 用自己的名字作为 clientId 来登录即时通讯服务 -realtime - .createIMClient("Tom") - .then(function (tom) { - // 成功登录 - }) - .catch(console.error); -``` - -```swift -// 定义一个常驻内存的全局变量 -var tom: IMClient -// 初始化 -do { - tom = try IMClient(ID: "Tom") -} catch { - print(error) -} -``` - -```dart -// clientId 为 Tom -Client tom = Client(id: 'Tom'); -``` - - - -注意这里一个 `IMClient` 实例就代表一个终端用户,我们需要把它全局保存起来,因为后续该用户在即时通讯上的所有操作都需要直接或者间接使用这个实例。 - -### 登录即时通讯服务器 - -创建好了「Tom」这个用户对应的 `IMClient` 实例之后,我们接下来需要让该实例「登录」即时通讯服务器。 -只有登录成功之后客户端才能开始与其他用户聊天,也才能接收到云端下发的各种事件通知。 - -这里需要说明一点,有些 SDK(比如 C# SDK)在创建 `IMClient` 实例的同时会自动进行登录,另一些 SDK(比如 iOS 和 Android SDK)则需要调用开发者手动执行 `open` 方法进行登录: - - - -```cs -await tom.Open(); -``` - -```java -// Tom 创建了一个 client,用自己的名字作为 clientId 登录 -LCIMClient tom = LCIMClient.getInstance("Tom"); -// Tom 登录 -tom.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient client, LCIMException e) { - if (e == null) { - // 成功打开连接 - } - } -}); -``` - -```objc -// 定义一个常驻内存的属性变量 -@property (nonatomic) LCIMClient *tom; -// 初始化,然后登录 -NSError *error; -tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error]; -if (error) { - NSLog(@"init failed with error: %@", error); -} else { - [tom openWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // open succeeded - } - }]; -} -``` - -```js -// Tom 用自己的名字作为 clientId 登录,并且获取 IMClient 对象实例 -realtime - .createIMClient("Tom") - .then(function (tom) { - // 成功登录 - }) - .catch(console.error); -``` - -```swift -// 定义一个常驻内存的全局变量 -var tom: IMClient -// 初始化,然后登录 -do { - tom = try IMClient(ID: "Tom") - tom.open { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// Tom 创建了一个 client,用自己的名字作为 clientId 登录 -Client tom = Client(id: 'Tom'); -// Tom 登录 -await tom.open(); -``` - - - -### 使用 `_User` 登录 - -除了应用层指定 `clientId` 登录之外,我们也支持直接使用 `_User` 对象来创建 `IMClient` 并登录。这种方式能直接利用云端内置的用户鉴权系统而省掉登录签名操作,更方便地将存储和即时通讯这两个模块结合起来使用。示例代码如下: - - - -```cs -var user = await LCUser.Login("USER_NAME", "PASSWORD"); -var client = new LCIMClient(user); -``` - -```java -// 以 LCUser 的用户名和密码登录到存储服务 -LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功,与服务器连接 - LCIMClient client = LCIMClient.getInstance(user); - client.open(new LCIMClientCallback() { - @Override - public void done(final LCIMClient avimClient, LCIMException e) { - // 执行其他逻辑 - } - }); - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` - -```objc -// 定义一个常驻内存的属性变量 -@property (nonatomic) LCIMClient *client; -// 登录 User,然后使用登录成功的 User 初始化 Client 并登录 -[LCUser logInWithUsernameInBackground:USER_NAME password:PASSWORD block:^(LCUser * _Nullable user, NSError * _Nullable error) { - if (user) { - NSError *err; - client = [[LCIMClient alloc] initWithUser:user error:&err]; - if (err) { - NSLog(@"init failed with error: %@", err); - } else { - [client openWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // open succeeded - } - }]; - } - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -// 以 AVUser 的用户名和密码登录即时通讯服务 -AV.User.logIn("username", "password") - .then(function (user) { - return realtime.createIMClient(user); - }) - .catch(console.error.bind(console)); -``` - -```swift -// 定义一个常驻内存的全局变量 -var client: IMClient -// 登录 User,然后使用登录成功的 User 初始化 Client 并登录 -LCUser.logIn(username: USER_NAME, password: PASSWORD) { (result) in - switch result { - case .success(object: let user): - do { - client = try IMClient(user: user) - client.open { (result) in - // handle result - } - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -// 暂不支持 -``` - - - -### 创建对话 `Conversation` - -用户登录之后,要开始与其他人聊天,需要先创建一个「对话」。 - -对话(`Conversation`)是消息的载体,所有消息都是发送给对话,即时通讯服务端会把消息下发给所有在对话中的成员。 - -Tom 完成了登录之后,就可以选择用户聊天了。现在他要给 Jerry 发送消息,所以需要先创建一个只有他们两个成员的 `Conversation`: - - - -```cs -var conversation = await tom.CreateConversation(new string[] { "Jerry" }, name: "Tom & Jerry", unique: true); -``` - -```java -tom.createConversation(Arrays.asList("Jerry"), "Tom & Jerry", null, false, true, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if(e == null) { - // 创建成功 - } - } -}); -``` - -```objc -// 创建与 Jerry 之间的对话 -[self createConversationWithClientIds:@[@"Jerry"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) { - // handle callback -}]; -``` - -```js -// 创建与 Jerry 之间的对话 -tom - .createConversation({ - // tom 是一个 IMClient 实例 - // 指定对话的成员除了当前用户 Tom(SDK 会默认把当前用户当做对话成员)之外,还有 Jerry - members: ["Jerry"], - // 对话名称 - name: "Tom & Jerry", - unique: true, - }) - .then(/* 略 */); -``` - -```swift -do { - try tom.createConversation(clientIDs: ["Jerry"], name: "Tom & Jerry", isUnique: true, completion: { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - // 创建与 Jerry 之间的对话 - Conversation conversation = await tom.createConversation( - isUnique: true, members: {'Jerry'}, name: 'Tom & Jerry'); -} catch (e) { - print('创建会话失败:$e'); -} -``` - - - -`createConversation` 这个接口会直接创建一个对话,并且该对话会被存储在 `_Conversation` 表内,可以打开 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 数据存储 > 结构化数据** 查看数据。不同 SDK 提供的创建对话接口如下: - - - -```cs -/// -/// Creates a conversation -/// -/// The list of clientIds of participants in this conversation (except the creator) -/// The name of this conversation -/// Whether this conversation is unique; -/// if it is true and an existing conversation contains the same composition of members, -/// the existing conversation will be reused, otherwise a new conversation will be created. -/// Custom attributes of this conversation -/// -public async Task CreateConversation( - IEnumerable members, - string name = null, - bool unique = true, - Dictionary properties = null) { - return await ConversationController.CreateConv(members: members, - name: name, - unique: unique, - properties: properties); -} -``` - -```java -/** - * 创建或查询一个已有 conversation - * - * @param members 对话的成员 - * @param name 对话的名字 - * @param attributes 对话的额外属性 - * @param isTransient 是否是聊天室 - * @param isUnique 如果已经存在符合条件的会话,是否返回已有回话 - * 为 false 时,则一直为创建新的回话 - * 为 true 时,则先查询,如果已有符合条件的回话,则返回已有的,否则,创建新的并返回 - * 为 true 时,仅 members 为有效查询条件 - * @param callback 结果回调函数 - */ -public void createConversation(final List members, final String name, - final Map attributes, final boolean isTransient, final boolean isUnique, - final LCIMConversationCreatedCallback callback); -/** - * 创建一个聊天对话 - * - * @param members 对话参与者 - * @param attributes 对话的额外属性 - * @param isTransient 是否为聊天室 - * @param callback 结果回调函数 - */ -public void createConversation(final List members, final String name, - final Map attributes, final boolean isTransient, - final LCIMConversationCreatedCallback callback); -/** - * 创建一个聊天对话 - * - * @param conversationMembers 对话参与者 - * @param name 对话名称 - * @param attributes 对话属性 - * @param callback 结果回调函数 - * @since 3.0 - */ -public void createConversation(final List conversationMembers, String name, - final Map attributes, final LCIMConversationCreatedCallback callback); -/** - * 创建一个聊天对话 - * - * @param conversationMembers 对话参与者 - * @param attributes 对话属性 - * @param callback 结果回调函数 - * @since 3.0 - */ -public void createConversation(final List conversationMembers, - final Map attributes, final LCIMConversationCreatedCallback callback); -``` - -```objc -/// The option of conversation creation. -@interface LCIMConversationCreationOption : NSObject -/// The name of the conversation. -@property (nonatomic, nullable) NSString *name; -/// The attributes of the conversation. -@property (nonatomic, nullable) NSDictionary *attributes; -/// Create or get an unique conversation, default is `true`. -@property (nonatomic) BOOL isUnique; -/// The time interval for the life of the temporary conversation. -@property (nonatomic) NSUInteger timeToLive; -@end - -/// Create a Normal Conversation. Default is a Normal Unique Conversation. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned. -/// @param callback Result callback. -- (void)createConversationWithClientIds:(NSArray *)clientIds - callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback; - -/// Create a Normal Conversation. Default is a Normal Unique Conversation. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned. -/// @param option See `LCIMConversationCreationOption`. -/// @param callback Result callback. -- (void)createConversationWithClientIds:(NSArray *)clientIds - option:(LCIMConversationCreationOption * _Nullable)option - callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback; - -/// Create a Chat Room. -/// @param callback Result callback. -- (void)createChatRoomWithCallback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback; - -/// Create a Chat Room. -/// @param option See `LCIMConversationCreationOption`. -/// @param callback Result callback. -- (void)createChatRoomWithOption:(LCIMConversationCreationOption * _Nullable)option - callback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback; - -/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID. -/// @param callback Result callback. -- (void)createTemporaryConversationWithClientIds:(NSArray *)clientIds - callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback; - -/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID. -/// @param option See `LCIMConversationCreationOption`. -/// @param callback Result callback. -- (void)createTemporaryConversationWithClientIds:(NSArray *)clientIds - option:(LCIMConversationCreationOption * _Nullable)option - callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback; -``` - -```js -/** - * 创建一个对话 - * @param {Object} options 除了下列字段外的其他字段将被视为对话的自定义属性 - * @param {String[]} options.members 对话的初始成员列表,必要参数,默认包含当前 client - * @param {String} [options.name] 对话的名字,可选参数,如果不传默认值为 null - * @param {Boolean} [options.transient=false] 是否为聊天室,可选参数 - * @param {Boolean} [options.unique=false] 是否唯一对话,当其为 true 时,如果当前已经有相同成员的对话存在则返回该对话,否则会创建新的对话 - * @param {Boolean} [options.tempConv=false] 是否为临时对话,可选参数 - * @param {Integer} [options.tempConvTTL=0] 可选参数,如果 tempConv 为 true,这里可以指定临时对话的生命周期。 - * @return {Promise.} - */ -async createConversation({ - members: m, - name, - transient, - unique, - tempConv, - tempConvTTL, - // 可添加更多属性 -}); -``` - -```swift -/// Create a Normal Conversation. Default is a Unique Conversation. -/// -/// - Parameters: -/// - clientIDs: The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned. -/// - name: The name of the conversation. -/// - attributes: The attributes of the conversation. -/// - isUnique: True means create or get a unique conversation, default is true. -/// - completion: callback. -public func createConversation(clientIDs: Set, name: String? = nil, attributes: [String : Any]? = nil, isUnique: Bool = true, completion: @escaping (LCGenericResult) -> Void) throws - -/// Create a Chat Room. -/// -/// - Parameters: -/// - name: The name of the chat room. -/// - attributes: The attributes of the chat room. -/// - completion: callback. -public func createChatRoom(name: String? = nil, attributes: [String : Any]? = nil, completion: @escaping (LCGenericResult) -> Void) throws - -/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle. -/// -/// - Parameters: -/// - clientIDs: The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID. -/// - timeToLive: The time interval for the life of the temporary conversation. -/// - completion: callback. -public func createTemporaryConversation(clientIDs: Set, timeToLive: Int32, completion: @escaping (LCGenericResult) -> Void) throws -``` - -```dart -/// To create a normal [Conversation]. -/// -/// [isUnique] is a special parameter, default is `true`, it affects the creation behavior and property [Conversation.isUnique]. -/// * When it is `true` and the relevant unique [Conversation] not exists in the server, this method will create a new unique [Conversation]. -/// * When it is `true` and the relevant unique [Conversation] exists in the server, this method will return that existing unique [Conversation]. -/// * When it is `false`, this method always create a new non-unique [Conversation]. -/// -/// [members] is the [Conversation.members]. -/// [name] is the [Conversation.name]. -/// [attributes] is the [Conversation.attributes]. -/// -/// Returns an instance of [Conversation]. -Future createConversation({ - bool isUnique = true, - Set members, - String name, - Map attributes, -}) async {} - -/// To create a new [ChatRoom]. -/// -/// [name] is the [Conversation.name]. -/// [attributes] is the [Conversation.attributes]. -/// -/// Returns an instance of [ChatRoom]. -Future createChatRoom({ - String name, - Map attributes, -}) async {} - -/// To create a new [TemporaryConversation]. -/// -/// [members] is the [Conversation.members]. -/// [timeToLive] is the [TemporaryConversation.timeToLive]. -/// -/// Returns an instance of [TemporaryConversation]. -Future createTemporaryConversation({ - Set members, - int timeToLive, -}) async {} -``` - - - -虽然不同语言/平台接口声明有所不同,但是支持的参数是基本一致的。在创建一个对话的时候,我们主要可以指定: - -1. `members`:必要参数,包含对话的初始成员列表,请注意当前用户作为对话的创建者,是默认包含在成员里面的,所以 `members` 数组中可以不包含当前用户的 `clientId`。 - -2. `name`:对话名字,可选参数,上面代码指定为了「Tom & Jerry」。 - -3. `attributes`:对话的自定义属性,可选。上面示例代码没有指定额外属性,开发者如果指定了额外属性的话,以后其他成员可以通过 `LCIMConversation` 的接口获取到这些属性值。附加属性在 `_Conversation` 表中被保存在 `attr` 列中。 - -4. `unique`/`isUnique` 或者是 `LCIMConversationOptionUnique`:唯一对话标志位,可选。 - - - 如果设置为唯一对话,云端会根据完整的成员列表先进行一次查询,如果已经有正好包含这些成员的对话存在,那么就返回已经存在的对话,否则才创建一个新的对话。 - - 如果指定 `unique` 标志为假,那么每次调用 `createConversation` 接口都会创建一个新的对话。 - - 未指定 `unique` 时,SDK 默认值为真。 - - 从通用的聊天场景来看,不管是 Tom 发出「创建和 Jerry 单聊对话」的请求,还是 Jerry 发出「创建和 Tom 单聊对话」的请求,或者 Tom 以后再次发出创建和 Jerry 单聊对话的请求,都应该是同一个对话才是合理的,否则可能因为聊天记录的不同导致用户混乱。 - -5. 对话类型的其他标志,可选参数,例如 `transient`/`isTransient` 表示「聊天室」,`tempConv`/`tempConvTTL` 和 `LCIMConversationOptionTemporary` 用来创建「临时对话」等等。什么都不指定就表示创建普通对话,对于这些标志位的含义我们先不管,以后会有说明。 - -创建对话之后,可以获取对话的内置属性,云端会为每一个对话生成一个全局唯一的 ID 属性:`Conversation.id`,它是其他用户查询对话时常用的匹配字段。 - -### 发送消息 - -对话已经创建成功了,接下来 Tom 可以在这个对话中发出第一条文本消息了: - - - -```cs -var textMessage = new LCIMTextMessage("Jerry,起床了!"); -await conversation.Send(textMessage); -``` - -```java -LCIMTextMessage msg = new LCIMTextMessage(); -msg.setText("Jerry,起床了!"); -// 发送消息 -conversation.sendMessage(msg, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - Log.d("Tom & Jerry", "发送成功!"); - } - } -}); -``` - -```objc -LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"耗子,起床!" attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` - -```js -var { TextMessage } = require("leancloud-realtime"); -conversation - .send(new TextMessage("Jerry,起床了!")) - .then(function (message) { - console.log("Tom & Jerry", "发送成功!"); - }) - .catch(console.error); -``` - -```swift -do { - let textMessage = IMTextMessage(text: "Jerry,起床了!") - try conversation.send(message: textMessage) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - TextMessage textMessage = TextMessage(); - textMessage.text = 'Jerry,起床了!'; - await conversation.send(message: textMessage); -} catch (e) { - print(e); -} -``` - - - -上面接口实现的功能就是向对话中发送一条消息,同一对话中其他在线成员会立刻收到此消息。 - -现在 Tom 发出了消息,那么接收者 Jerry 他要在界面上展示出来这一条新消息,该怎么来处理呢? - -### 接收消息 - -在另一个设备上,我们用 `Jerry` 作为 `clientId` 来创建一个 `IMClient` 并登录即时通讯服务(与前两节 Tom 的处理流程一样): - - - -```cs -var jerry = new LCIMClient("Jerry"); -``` - -```java -// Jerry 登录 -LCIMClient jerry = LCIMClient.getInstance("Jerry"); -jerry.open(new LCIMClientCallback(){ - @Override - public void done(LCIMClient client,LCIMException e){ - if(e==null){ - // 登录成功后的逻辑 - } - } -}); -``` - -```objc -NSError *error; -jerry = [[LCIMClient alloc] initWithClientId:@"Jerry" error:&error]; -if (!error) { - [jerry openWithCallback:^(BOOL succeeded, NSError *error) { - // handle callback - }]; -} -``` - -```js -var { Event } = require("leancloud-realtime"); -// Jerry 登录 -realtime - .createIMClient("Jerry") - .then(function (jerry) {}) - .catch(console.error); -``` - -```swift -do { - let jerry = try IMClient(ID: "Jerry") - jerry.open { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -Client jerry = Client(id: 'Jerry'); -await jerry.open(); -``` - - - -Jerry 作为消息的被动接收方,他不需要主动创建与 Tom 的对话,可能也无法知道 Tom 创建好的对话信息,Jerry 端需要通过设置即时通讯客户端事件的回调函数,才能获取到 Tom 那边操作的通知。 - -即时通讯客户端事件回调能处理多种服务端通知,这里我们先关注这里会出现的两个事件: - -- 用户被邀请进入某个对话的通知事件。Tom 在创建和 Jerry 的单聊对话的时候,Jerry 这边就能立刻收到一条通知,获知到类似于「Tom 邀请你加入了一个对话」的信息。 -- 已加入对话中新消息到达的通知。在 Tom 发出「Jerry,起床了!」这条消息之后,Jerry 这边也能立刻收到一条新消息到达的通知,通知中带有消息具体数据以及对话、发送者等上下文信息。 - -现在,我们看看具体应该如何响应服务端发过来的通知。Jerry 端会分别处理「加入对话」的事件通知和「新消息到达」的事件通知: - - - -```cs -jerry.OnInvited = (conv, initBy) => { - WriteLine($"{initBy} 邀请 Jerry 加入 {conv.Id} 对话"); -}; -jerry.OnMessage = (conv, msg) => { - if (msg is LCIMTextMessage textMessage) { - // textMessage.ConversationId 是该条消息所属于的对话 ID - // textMessage.Text 是该文本消息的文本内容 - // textMessage.FromClientId 是消息发送者的 clientId - } -}; -``` - -```java -// Java/Android SDK 通过定制自己的对话事件 Handler 处理服务端下发的对话事件通知 -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本方法来处理当前用户被邀请到某个聊天对话事件 - * - * @param client - * @param conversation 被邀请的聊天对话 - * @param operator 邀请你的人 - * @since 3.0 - */ - @Override - public void onInvited(LCIMClient client, LCIMConversation conversation, String invitedBy) { - // 当前 clientId(Jerry)被邀请到对话,执行此处逻辑 - } -} -// 设置全局的对话事件处理 handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); - -// Java/Android SDK 通过定制自己的消息事件 Handler 处理服务端下发的消息通知 -public static class CustomMessageHandler extends LCIMMessageHandler{ - /** - * 重载此方法来处理接收消息 - * - * @param message - * @param conversation - * @param client - */ - @Override - public void onMessage(LCIMMessage message,LCIMConversation conversation,LCIMClient client){ - if(message instanceof LCIMTextMessage){ - Log.d(((LCIMTextMessage)message).getText()); // Jerry,起床了 - } - } - } -// 设置全局的消息处理 handler -LCIMMessageManager.registerDefaultMessageHandler(new CustomMessageHandler()); -``` - -```objc -// Objective-C SDK 通过实现 LCIMClientDelegate 代理来处理服务端通知 -// 不了解 Objective-C 代理(delegate)概念的读者可以参考: -// https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/DelegatesandDataSources/DelegatesandDataSources.html -jerry.delegate = delegator; - -/*! - 当前用户被邀请加入对话的通知。 - @param conversation - 所属对话 - @param clientId - 邀请者的 ID - */ -- (void)conversation:(LCIMConversation *)conversation invitedByClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"当前 clientId(Jerry)被 %@ 邀请,加入了对话",clientId]); -} - -/*! - 接收到新消息(使用内置消息格式)。 - @param conversation - 所属对话 - @param message - 具体的消息 - */ -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - NSLog(@"%@", message.text); // Jerry,起床了! -} -``` - -```js -// JS SDK 通过在 IMClient 实例上监听事件回调来响应服务端通知 - -// 当前用户被添加至某个对话 -jerry.on(Event.INVITED, function invitedEventHandler(payload, conversation) { - console.log(payload.invitedBy, conversation.id); -}); - -// 当前用户收到了某一条消息,可以通过响应 Event.MESSAGE 这一事件来处理。 -jerry.on(Event.MESSAGE, function (message, conversation) { - console.log("收到新消息:" + message.text); -}); -``` - -```swift -let delegator: Delegator = Delegator() -jerry.delegate = delegator - -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - print(message) - default: - break - } - default: - break - } -} -``` - -```dart -jerry.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - if (message.stringContent != null) { - print('收到的消息是:${message.stringContent}'); - } -}; -``` - - - -Jerry 端实现了上面两个事件通知函数之后,就顺利收到 Tom 发送的消息了。之后 Jerry 也可以回复消息给 Tom,而 Tom 端实现类似的接收流程,那么他们俩就可以开始愉快的聊天了。 - -我们现在可以回顾一下 Tom 和 Jerry 发送第一条消息的过程中,两方完整的处理时序: - ->Cloud: 1. Tom 将 Jerry 加入对话 -Cloud-->>Jerry: 2. 下发通知:你被邀请加入对话 -Jerry-->>UI: 3. 加载聊天的 UI 界面 -Tom->>Cloud: 4. 发送消息 -Cloud-->>Jerry: 5. 下发通知:接收到有新消息 -Jerry-->>UI: 6. 显示收到的消息内容 -`} -/> - -在聊天过程中,接收方除了响应新消息到达通知之外,还需要响应多种对话成员变动通知,例如「新用户 XX 被 XX 邀请加入了对话」、「用户 XX 主动退出了对话」、「用户 XX 被管理员剔除出对话」,等等。 -云端会实时下发这些事件通知给客户端,具体细节可以参考后续章节:[成员变更的事件通知总结](#成员变更的事件通知总结)。 - -## 多人群聊 - -上面我们讨论了一对一单聊的实现流程,假设我们还需要实现一个「朋友群」的多人聊天,接下来我们就看看怎么完成这一功能。 - -从即时通讯云端来看,多人群聊与单聊的流程十分接近,主要差别在于对话内成员数量的多少。群聊对话支持在创建对话的时候一次性指定全部成员,也允许在创建之后通过邀请的方式来增加新的成员。 - -### 创建多人群聊对话 - -在 Tom 和 Jerry 的对话中(假设对话 ID 为 `CONVERSATION_ID`,这只是一个示例,并不代表实际数据),后来 Tom 又希望把 Mary 也拉进来,他可以使用如下的办法: - - - -```cs -// 首先根据 ID 获取 Conversation 实例 -var conversation = await tom.GetConversation("CONVERSATION_ID"); -// 邀请 Mary 加入对话 -await conversation.AddMembers(new string[] { "Mary" }); -``` - -```java -// 首先根据 ID 获取 Conversation 实例 -final LCIMConversation conv = client.getConversation("CONVERSATION_ID"); -// 邀请 Mary 加入对话 -conv.addMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() { - @Override - public void done(LCIMException e, List successfulClientIds, List failures) { - // 添加成功 - } -}); -``` - -```objc -// 首先根据 ID 获取 Conversation 实例 -LCIMConversationQuery *query = [self.client conversationQuery]; -[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) { - // 邀请 Mary 加入对话 - [conversation addMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"邀请成功!"); - } - }]; -}]; -``` - -```js -// 首先根据 ID 获取 Conversation 实例 -tom - .getConversation("CONVERSATION_ID") - .then(function (conversation) { - // 邀请 Mary 加入对话 - return conversation.add(["Mary"]); - }) - .then(function (conversation) { - console.log("添加成功", conversation.members); - // 此时对话成员为:['Mary', 'Tom', 'Jerry'] - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let conversationQuery = client.conversationQuery - try conversationQuery.getConversation(by: "CONVERSATION_ID") { (result) in - switch result { - case .success(value: let conversation): - do { - try conversation.add(members: ["Mary"], completion: { (result) in - switch result { - case .allSucceeded: - break - case .failure(error: let error): - print(error) - case let .slicing(success: succeededIDs, failure: failures): - if let succeededIDs = succeededIDs { - print(succeededIDs) - } - for (failedIDs, error) in failures { - print(failedIDs) - print(error) - } - } - }) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -List conversations; -try { -// 首先根据 ID 获取 Conversation 实例 - ConversationQuery query = tom.conversationQuery(); - query.whereEqualTo('objectId', 'CONVERSATION_ID'); - conversations = await query.find(); -} catch (e) { - print(e); -} -try { - Conversation conversation = conversations.first; -// 邀请 Mary 加入对话 - MemberResult addResult = await conversation.addMembers( - members: {'Mary'}, - ); -} catch (e) { - print(e); -} -``` - - - -而 Jerry 端增加「新成员加入」的事件通知处理函数,就可以及时获知 Mary 被 Tom 邀请加入当前对话了: - - -<> - -```cs -jerry.OnMembersJoined = (conv, memberList, initBy) => { - WriteLine($"{initBy} 邀请了 {memberList} 加入了 {conv.Id} 对话"); -} -``` - -其中 `AVIMOnInvitedEventArgs` 参数包含如下内容: - -1. `InvitedBy`:该操作的发起者 -2. `JoinedMembers`:此次加入对话的包含的成员列表 -3. `ConversationId`:被操作的对话 - - -<> - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本方法以处理聊天对话中的参与者加入事件 - * - * @param client - * @param conversation - * @param members 加入的参与者 - * @param invitedBy 加入事件的邀请人,有可能是加入的参与者本身 - * @since 3.0 - */ - @Override - public void onMemberJoined(LCIMClient client, LCIMConversation conversation, - List members, String invitedBy) { - // 手机屏幕上会显示一小段文字:Mary 加入到 551260efe4b01608686c3e0f;操作者为:Tom - Toast.makeText(LeanCloud.applicationContext, - members + " 加入到 " + conversation.getConversationId() + ";操作者为:" - + invitedBy, Toast.LENGTH_SHORT).show(); - } -} -// 设置全局的对话事件处理 handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); -``` - - -<> - -```objc -jerry.delegate = delegator; - -#pragma mark - LCIMClientDelegate -/*! - 对话中有新成员加入时所有成员都会收到这一通知。 - @param conversation - 所属对话 - @param clientIds - 加入的新成员列表 - @param clientId - 邀请者的 ID - */ -- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"%@ 加入到对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]); -} -``` - - -<> - -```js -// 有用户被添加至某个对话 -jerry.on( - Event.MEMBERS_JOINED, - function membersjoinedEventHandler(payload, conversation) { - console.log(payload.members, payload.invitedBy, conversation.id); - } -); -``` - -其中 `payload` 参数包含如下内容: - -1. `members`:字符串数组,被添加的用户 `clientId` 列表 -2. `invitedBy`:字符串,邀请者 `clientId` - - -<> - -```swift -jerry.delegate = delegator - -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .joined(byClientID: byClientID, at: atDate): - print(byClientID) - print(atDate) - case let .membersJoined(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` - - -<> - -```dart -// 加入成员通知 -jerry.onMembersJoined = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('成员 ${members.toString()} 加入会话'); -}; -``` - - - - -这一流程的时序图如下: - ->Cloud: 1. 添加 Mary -Cloud->>Tom: 2. 下发通知:Mary 被你邀请加入了对话 -Cloud-->>Mary: 2. 下发通知:你被 Tom 邀请加入对话 -Cloud-->>Jerry: 2. 下发通知:Mary 被 Tom 邀请加入了对话 -`} -/> - -而 Mary 端如果要能加入到 Tom 和 Jerry 的对话中来,Ta 可以参照 [一对一单聊](#一对一单聊) 中 Jerry 侧的做法监听 `INVITED` 事件,就可以自己被邀请到了一个对话当中。 - -而 **重新创建一个对话,并在创建的时候指定全部成员** 的方式如下: - - - -```cs -var conversation = await tom.CreateConversation(new string[] { "Jerry","Mary" }, name: "Tom & Jerry & friends", unique: true); -``` - -```java -tom.createConversation(Arrays.asList("Jerry","Mary"), "Tom & Jerry & friends", null, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if (e == null) { - // 创建成功 - } - } - }); -``` - -```objc -// Tom 建立了与朋友们的会话 -[tom createConversationWithClientIds:@[@"Jerry", @"Mary"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) { - if (!error) { - NSLog(@"创建成功!"); - } -}]; -``` - -```js -tom - .createConversation({ - // 创建的时候直接指定 Jerry 和 Mary 一起加入多人群聊,当然根据需求可以添加更多成员 - members: ["Jerry", "Mary"], - // 对话名称 - name: "Tom & Jerry & friends", - unique: true, - }) - .catch(console.error); -``` - -```swift -do { - try tom.createConversation(clientIDs: ["Jerry", "Mary"], name: "Tom & Jerry & friends", isUnique: true, completion: { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - Conversation conversation = await jerry.createConversation( - isUnique: true, - members: {'Jerry', 'Mary'}, - name: 'Tom & Jerry & friends'); -} catch (e) { - print(e); -} -``` - - - -### 群发消息 - -多人群聊中一个成员发送的消息,会实时同步到所有其他在线成员,其处理流程与单聊中 Jerry 接收消息的过程是一样的。 - -例如,Tom 向好友群发送了一条欢迎消息: - - - -```cs -var textMessage = new LCIMTextMessage("大家好,欢迎来到我们的群聊对话!"); -await conversation.Send(textMessage); -``` - -```java -LCIMTextMessage msg = new LCIMTextMessage(); -msg.setText("大家好,欢迎来到我们的群聊对话!"); -// 发送消息 -conversation.sendMessage(msg, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - Log.d("群聊", "发送成功!"); - } - } -}); -``` - -```objc -[conversation sendMessage:[LCIMTextMessage messageWithText:@"大家好,欢迎来到我们的群聊对话!" attributes:nil] callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` - -```js -conversation.send(new TextMessage("大家好,欢迎来到我们的群聊对话")); -``` - -```swift -do { - let textMessage = IMTextMessage(text: "大家好,欢迎来到我们的群聊对话!") - try conversation.send(message: textMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage textMessage = TextMessage(); - textMessage.text = '大家好,欢迎来到我们的群聊对话!'; - await conversation.send(message: textMessage); -} catch (e) { - print(e); -} -``` - - - -而 Jerry 和 Mary 端都会有 `Event.MESSAGE` 事件触发,利用它来接收群聊消息,并更新产品 UI。 - -### 将他人踢出对话 - -三个好友的群其乐融融不久,后来 Mary 出言不逊,惹恼了群主 Tom,Tom 直接把 Mary 踢出了对话群。Tom 端想要踢人,该怎么实现呢? - - - -```cs -await conversation.RemoveMembers(new string[] { "Mary" }); -``` - -```java -conv.kickMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() { - @Override - public void done(LCIMException e, List successfulClientIds, List failures) { - } -}); -``` - -```objc -[conversation removeMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"踢人成功!"); - } -}]; -``` - -```js -conversation - .remove(["Mary"]) - .then(function (conversation) { - console.log("移除成功", conversation.members); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - try conversation.remove(members: ["Mary"], completion: { (result) in - switch result { - case .allSucceeded: - break - case .failure(error: let error): - print(error) - case let .slicing(success: succeededIDs, failure: failures): - if let succeededIDs = succeededIDs { - print(succeededIDs) - } - for (failedIDs, error) in failures { - print(failedIDs) - print(error) - } - } - }) -} catch { - print(error) -} -``` - -```dart -try { - MemberResult removeMemberResult = await conversation.removeMembers(members: {'Mary'}); -} catch (e) { - print(e); -} -``` - - - -Tom 端执行了这段代码之后会触发如下流程: - ->Cloud: 1. 对话中移除 Mary -Cloud-->>Mary: 2. 下发通知:你被 Tom 从对话中剔除了 -Cloud-->>Jerry: 2. 下发通知:Mary 被 Tom 移除 -Cloud-->>Tom: 2. 下发通知:Mary 被移除了对话 -`} -/> - -这里出现了两个新的事件:当前用户被踢出对话 `KICKED`(Mary 收到的),成员 XX 被踢出对话 `MEMBERS_LEFT`(Jerry 和 Tom 收到的)。其处理方式与邀请人的流程类似: - - - -```cs -jerry.OnMembersLeft = (conv, leftIds, kickedBy) => { - WriteLine($"{leftIds} 离开对话 {conv.Id};操作者为:{kickedBy}"); -} -jerry.OnKicked = (conv, initBy) => { - WriteLine($"你已经离开对话 {conv.Id};操作者为:{initBy}"); -}; -``` - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本方法以处理聊天对话中的参与者离开事件 - * - * @param client - * @param conversation - * @param members 离开的参与者 - * @param kickedBy 离开事件的发动者,有可能是离开的参与者本身 - * @since 3.0 - */ - @Override - public abstract void onMemberLeft(LCIMClient client, - LCIMConversation conversation, List members, String kickedBy) { - Toast.makeText(LeanCloud.applicationContext, - members + " 离开对话 " + conversation.getConversationId() + ";操作者为:" - + kickedBy, Toast.LENGTH_SHORT).show(); - } - /** - * 实现本方法来处理当前用户被踢出某个聊天对话事件 - * - * @param client - * @param conversation - * @param kickedBy 踢出你的人 - * @since 3.0 - */ - @Override - public abstract void onKicked(LCIMClient client, LCIMConversation conversation, - String kickedBy) { - Toast.makeText(LeanCloud.applicationContext, - "你已离开对话 " + conversation.getConversationId() + ";操作者为:" - + kickedBy, Toast.LENGTH_SHORT).show(); - } -} -// 设置全局的对话事件处理 handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); -``` - -```objc -jerry.delegate = delegator; - -#pragma mark - LCIMClientDelegate -/*! - 对话中有成员离开时所有剩余成员都会收到这一通知。 - @param conversation - 所属对话 - @param clientIds - 离开的成员列表 - @param clientId - 操作者的 ID - */ -- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray * _Nullable)clientIds byClientId:(NSString * _Nullable)clientId { - ; -} -/*! - 当前用户被踢出对话的通知。 - @param conversation - 所属对话 - @param clientId - 操作者的 ID - */ -- (void)conversation:(LCIMConversation *)conversation kickedByClientId:(NSString * _Nullable)clientId { - ; -} -``` - -```js -// 有成员被从某个对话中移除 -jerry.on( - Event.MEMBERS_LEFT, - function membersjoinedEventHandler(payload, conversation) { - console.log(payload.members, payload.kickedBy, conversation.id); - } -); -// 有用户被踢出某个对话 -jerry.on( - Event.KICKED, - function membersjoinedEventHandler(payload, conversation) { - console.log(payload.kickedBy, conversation.id); - } -); -``` - -```swift -jerry.delegate = delegator - -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .left(byClientID: byClientID, at: atDate): - print(byClientID) - print(atDate) - case let .membersLeft(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` - -```dart -// 有成员被从某个对话中移除 -jerry.onMembersLeft = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('成员 ${members.toString()} 离开会话,操作者为:$byClientID'); -}; -// 有用户被踢出某个对话 -jerry.onKicked = ({ - Client client, - Conversation conversation, - String byClientID, - DateTime atDate, -}) { - print('你已离开对话,操作者为:$byClientID'); -}; -``` - - - -### 用户主动加入对话 - -把 Mary 踢走之后,Tom 嫌人少不好玩,所以他找到了 William,说他和 Jerry 有一个很好玩的聊天群,并且把群的 ID(或名称)告知给了 William。William 也很想进入这个群看看他们究竟在聊什么,他自己主动加入了对话: - - - -```cs -var conv = await william.GetConversation("CONVERSATION_ID"); -await conv.Join(); -``` - -```java -LCIMConversation conv = william.getConversation("CONVERSATION_ID"); -conv.join(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // 加入成功 - } - } -}); -``` - -```objc -LCIMConversationQuery *query = [william conversationQuery]; -[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) { - [conversation joinWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"加入成功!"); - } - }]; -}]; -``` - -```js -william - .getConversation("CONVERSATION_ID") - .then(function (conversation) { - return conversation.join(); - }) - .then(function (conversation) { - console.log("加入成功", conversation.members); - // 此时对话成员为:['William', 'Tom', 'Jerry'] - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let conversationQuery = client.conversationQuery - try conversationQuery.getConversation(by: "CONVERSATION_ID") { (result) in - switch result { - case .success(value: let conversation): - do { - try conversation.join(completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -List conversations; -try { - ConversationQuery query = william.conversationQuery(); - query.whereEqualTo('objectId', 'CONVERSATION_ID'); - conversations = await query.find(); -} catch (e) { - print(e); -} - -try { - Conversation conversation = conversations.first; - MemberResult joinResult = await conversation.join(); -} catch (e) { - print(e); -} -``` - - - -执行了这段代码之后会触发如下流程: - ->Cloud: 1. 加入对话 -Cloud-->>William: 2. 下发通知:你已加入对话 -Cloud-->>Tom: 2. 下发通知:William 加入对话 -Cloud-->>Jerry: 2. 下发通知:William 加入对话 -`} -/> - -其他人则通过订阅 `MEMBERS_JOINED` 来接收 William 加入对话的通知 : - - - -```cs -jerry.OnMembersJoined = (conv, memberList, initBy) => { - WriteLine($"{memberList} 加入了 {conv.Id} 对话;操作者为:{initBy}"); -} -``` - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - @Override - public void onMemberJoined(LCIMClient client, LCIMConversation conversation, - List members, String invitedBy) { - // 手机屏幕上会显示一小段文字:William 加入到 551260efe4b01608686c3e0f;操作者为:William - Toast.makeText(LeanCloud.applicationContext, - members + " 加入到 " + conversation.getConversationId() + ";操作者为:" - + invitedBy, Toast.LENGTH_SHORT).show(); - } -} -``` - -```objc -- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"%@ 加入到对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]); -} -``` - -```js -jerry.on( - Event.MEMBERS_JOINED, - function membersJoinedEventHandler(payload, conversation) { - console.log(payload.members, payload.invitedBy, conversation.id); - } -); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .membersJoined(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` - -```dart -jerry.onMembersJoined = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('成员 ${members.toString()} 加入会话'); -}; -``` - - - -### 用户主动退出对话 - -随着 Tom 邀请进来的人越来越多,Jerry 觉得跟这些人都说不到一块去,他不想继续呆在这个对话里面了,所以选择自己主动退出对话,这时候可以调用下面的方法完成退群的操作: - - - -```cs -await conversation.Quit(); -``` - -```java -conversation.quit(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // 退出成功 - } - } -}); -``` - -```objc -[conversation quitWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"退出成功!"); - } -}]; -``` - -```js -conversation - .quit() - .then(function (conversation) { - console.log("退出成功", conversation.members); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - try conversation.leave(completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - MemberResult quitResult = await conversation.quit(); -} catch (e) { - print(e); -} -``` - - - -执行了这段代码 Jerry 就离开了这个聊天群,此后群里所有的事件 Jerry 都不会再知晓。各个成员接收到的事件通知流程如下: - ->Cloud: 1. 离开对话 -Cloud-->>Jerry: 2. 下发通知:你已离开对话 -Cloud-->>Mary: 2. 下发通知:Jerry 已离开对话 -Cloud-->>Tom: 2. 下发通知:Jerry 已离开对话 -`} -/> - -而其他人需要通过订阅 `MEMBERS_LEFT` 来接收 Jerry 离开对话的事件通知: - - - -```cs -mary.OnMembersLeft = (conv, members, initBy) => { - WriteLine($"{members} 离开了 {conv.Id} 对话;操作者为:{initBy}"); -} -``` - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - @Override - public void onMemberLeft(LCIMClient client, LCIMConversation conversation, List members, - String kickedBy) { - // 有其他成员离开时,执行此处逻辑 - } -} -``` - -```objc -// Mary 登录之后,Jerry 退出了对话,在 Mary 所在的客户端就会激发以下回调 -- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray *)clientIds byClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"%@ 离开了对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]); -} -``` - -```js -mary.on( - Event.MEMBERS_LEFT, - function membersLeftEventHandler(payload, conversation) { - console.log(payload.members, payload.kickedBy, conversation.id); - } -); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .membersLeft(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` - -```dart -mary.onMembersLeft = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('成员 ${members.toString()} 离开会话'); -}; -``` - - - -### 成员变更的事件通知总结 - -前面的时序图和代码针对成员变更的操作做了逐步的分析和阐述,为了确保开发者能够准确的使用事件通知,如下表格做了一个统一的归类和划分: - -假设 Tom 和 Jerry 已经在对话内了: - - -<> - -| 操作 | Tom | Jerry | Mary | William | -| -------------- | ----------------- | ----------------- | ----------- | ----------------- | -| Tom 添加 Mary | `OnMembersJoined` | `OnMembersJoined` | `OnInvited` | / | -| Tom 剔除 Mary | `OnMembersLeft` | `OnMembersLeft` | `OnKicked` | / | -| William 加入 | `OnMembersJoined` | `OnMembersJoined` | / | `OnMembersJoined` | -| Jerry 主动退出 | `OnMembersLeft` | `OnMembersLeft` | / | `OnMembersLeft` | - - -<> - -| 操作 | Tom | Jerry | Mary | William | -| -------------- | ---------------- | ---------------- | ----------- | ---------------- | -| Tom 添加 Mary | `onMemberJoined` | `onMemberJoined` | `onInvited` | / | -| Tom 剔除 Mary | `onMemberLeft` | `onMemberLeft` | `onKicked` | / | -| William 加入 | `onMemberJoined` | `onMemberJoined` | / | `onMemberJoined` | -| Jerry 主动退出 | `onMemberLeft` | `onMemberLeft` | / | `onMemberLeft` | - - - -<> - -| 操作 | Tom | Jerry | Mary | William | -| -------------- | ---------------- | ------------------ | ------------------- | ---------------- | -| Tom 添加 Mary | `membersAdded` | `membersAdded` | `invitedByClientId` | / | -| Tom 剔除 Mary | `membersRemoved` | `membersRemoved` | `kickedByClientId` | / | -| William 加入 | `membersAdded` | `membersAdded` | / | `membersAdded` | -| Jerry 主动退出 | `membersRemoved` | `kickedByClientId` | / | `membersRemoved` | - - - - -## 文本之外的聊天消息 - -上面的示例都是发送文本消息,但是实际上可能图片、视频、位置等消息也是非常常见的消息格式,接下来我们就看看如何发送这些富媒体类型的消息。 - -即时通讯服务默认支持文本、文件、图像、音频、视频、位置、二进制等不同格式的消息,除了二进制消息之外,普通消息的收发接口都是字符串,但是文本消息和文件、图像、音视频消息有一点区别: - -- 文本消息发送的就是本身的内容 -- 而其他的多媒体消息,例如一张图片,实际上即时通讯 SDK 会首先调用存储服务的 `AVFile` 接口,将图像的二进制文件上传到存储服务云端,再把图像下载的 URL 放入即时通讯消息结构体中,所以 **图像消息不过是包含了图像下载链接的固定格式文本消息**。 - -图像等二进制数据不随即时通讯消息直接下发的主要原因在于,文件存储服务默认都是开通了 CDN 加速选项的,通过文件下载对于终端用户来说可以有更快的展现速度,同时对于开发者来说也能获得更低的存储成本。 - -### 默认消息类型 - -即时通讯服务内置了多种结构化消息用来满足常见的需求: - -- `TextMessage` 文本消息 -- `ImageMessage` 图像消息 -- `AudioMessage` 音频消息 -- `VideoMessage` 视频消息 -- `FileMessage` 普通文件消息(.txt/.doc/.md 等各种) -- `LocationMessage` 地理位置消息 - -所有消息均派生自 `LCIMMessage`,每种消息实例都具备如下属性: - - -<> - -| 属性 | 类型 | 描述 | -| ------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `content` | `String` | 消息内容。 | -| `clientId` | `String` | 消息发送者的 `clientId`。 | -| `conversationId` | `String` | 消息所属对话 ID。 | -| `messageId` | `String` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `timestamp` | `long` | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 | -| `receiptTimestamp` | `long` | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 | -| `status` | `AVIMMessageStatus` 枚举 | 消息状态,有五种取值:

    `AVIMMessageStatusNone`(未知)
    `AVIMMessageStatusSending`(发送中)
    `AVIMMessageStatusSent`(发送成功)
    `AVIMMessageStatusReceipt`(被接收)
    `AVIMMessageStatusFailed`(失败) | -| `ioType` | `AVIMMessageIOType` 枚举 | 消息传输方向,有两种取值:

    `AVIMMessageIOTypeIn`(发给当前用户)
    `AVIMMessageIOTypeOut`(由当前用户发出) | - - -<> - -| 属性 | 类型 | 描述 | -| ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `content` | `String` | 消息内容。 | -| `clientId` | `String` | 消息发送者的 `clientId`。 | -| `conversationId` | `String` | 消息所属对话 ID。 | -| `messageId` | `String` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `timestamp` | `long` | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 | -| `receiptTimestamp` | `long` | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 | -| `status` | `MessageStatus` 枚举 | 消息状态,有五种取值:

    `StatusNone`(未知)
    `StatusSending`(发送中)
    `StatusSent`(发送成功)
    `StatusReceipt`(被接收)
    `StatusFailed`(失败) | -| `ioType` | `MessageIOType` 枚举 | 消息传输方向,有两种取值:

    `TypeIn`(发给当前用户)
    `TypeOut`(由当前用户发出) | - - -<> - -| 属性 | 类型 | 描述 | -| -------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `content` | `NSString` | 消息内容。 | -| `clientId` | `NSString` | 消息发送者的 `clientId`。 | -| `conversationId` | `NSString` | 消息所属对话 ID。 | -| `messageId` | `NSString` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `sendTimestamp` | `int64_t` | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 | -| `deliveredTimestamp` | `int64_t` | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 | -| `status` | `AVIMMessageStatus` 枚举 | 消息状态,有五种取值:

    `LCIMMessageStatusNone`(未知)
    `LCIMMessageStatusSending`(发送中)
    `LCIMMessageStatusSent`(发送成功)
    `LCIMMessageStatusDelivered`(被接收)
    `LCIMMessageStatusFailed`(失败) | -| `ioType` | `LCIMMessageIOType` 枚举 | 消息传输方向,有两种取值:

    `LCIMMessageIOTypeIn`(发给当前用户)
    `LCIMMessageIOTypeOut`(由当前用户发出) | - - -<> - -| 属性 | 类型 | 描述 | -| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `from` | `String` | 消息发送者的 `clientId`。 | -| `cid` | `String` | 消息所属对话 ID。 | -| `id` | `String` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `timestamp` | `Date` | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 | -| `deliveredAt` | `Date` | 消息送达时间。 | -| `status` | `Symbol` | 消息状态,其值为枚举 [`MessageStatus`](https://leancloud.github.io/js-realtime-sdk/docs/module-leancloud-realtime.html#.MessageStatus) 的成员之一:

    `MessageStatus.NONE`(未知)
    `MessageStatus.SENDING`(发送中)
    `MessageStatus.SENT`(发送成功)
    `MessageStatus.DELIVERED`(已送达)
    `MessageStatus.FAILED`(失败) | - - -<> - -| 属性 | 类型 | 描述 | -| -------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `content` | `IMMessage.Content` | 消息内容,支持 `String` 和 `Data` 两种格式。 | -| `fromClientID` | `String` | 消息发送者的 `clientId`。 | -| `currentClientID` | `String` | 消息接收者的 `clientId`。 | -| `conversationID` | `String` | 消息所属对话 ID。 | -| `ID` | `String` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `sentTimestamp` | `int64_t` | 消息发送的时间。消息发送成功之后,云端赋予的全局的时间戳。 | -| `deliveredTimestamp` | `int64_t` | 消息被对方接收到的时间戳。 | -| `readTimestamp` | `int64_t` | 消息被对方阅读的时间戳。 | -| `patchedTimestamp` | `int64_t` | 消息被修改的时间戳。 | -| `isAllMembersMentioned` | `Bool` | @ 所有会话成员。 | -| `mentionedMembers` | `[String]` | @ 会话成员。 | -| `isCurrentClientMentioned` | `Bool` | 当前 `Client` 是否被 @。 | -| `status` | `IMMessage.Status` | 消息状态,有 6 种取值:

    `none`(无状态)
    `sending`(发送中)
    `sent`(发送成功)
    `delivered`(已被接收)
    `read`(已被读)
    `failed`(发送失败) | -| `ioType` | `IMMessage.IOType` | 消息传输方向,有两种取值:

    `in`(当前用户接收到的)
    `out`(由当前用户发出的) | - - -
    - -我们为每一种富媒体消息定义了一个消息类型,即时通讯 SDK 自身使用的类型是负数(如下面列表所示),所有正数留给开发者自定义扩展类型使用,`0` 作为「没有类型」被保留起来。 - -| 消息 | 类型 | -| -------- | ---- | -| 文本消息 | `-1` | -| 图像消息 | `-2` | -| 音频消息 | `-3` | -| 视频消息 | `-4` | -| 位置消息 | `-5` | -| 文件消息 | `-6` | - -### 图像消息 - -#### 发送图像文件 - -即时通讯 SDK 支持直接通过二进制数据,或者本地图像文件的路径,来构造一个图像消息并发送到云端。其流程如下: - ->Local: 1. 获取图像实体内容 -Tom-->>Storage: 2. SDK 后台上传文件(LCFile)到云端 -Storage-->>Tom: 3. 返回图像的云端地址 -Tom-->>Cloud: 4. SDK 将图像消息发送给云端 -Cloud->>Jerry: 5. 收到图像消息,在对话框里面做 UI 展现 -`} -/> - -图解: - -1. Local 可能是来自于 `localStorage`/`camera`,表示图像的来源可以是本地存储例如 iPhone 手机的媒体库或者直接调用相机 API 实时地拍照获取的照片。 -2. `LCFile` 是云服务提供的文件存储对象。 - -对应的代码并没有时序图那样复杂,因为调用 `send` 接口的时候,SDK 会自动上传图像,不需要开发者再去关心这一步: - - - -```cs -var image = new LCFile("screenshot.png", new Uri("http://example.com/screenshot.png")); -var imageMessage = new LCIMImageMessage(image); -imageMessage.Text = "发自我的 Windows"; -await conversation.Send(imageMessage); -``` - -```java -LCFile file = LCFile.withAbsoluteLocalPath("San_Francisco.png", Environment.getExternalStorageDirectory() + "/San_Francisco.png"); -// 创建一条图像消息 -LCIMImageMessage m = new LCIMImageMessage(file); -m.setText("发自我的小米手机"); -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` - -```objc -NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); -NSString *documentsDirectory = [paths objectAtIndex:0]; -NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:@"Tarara.png"]; -NSError *error; -LCFile *file = [LCFile fileWithLocalPath:imagePath error:&error]; -LCIMImageMessage *message = [LCIMImageMessage messageWithText:@"萌妹子一枚" file:file attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` - -```js -// 图像消息等富媒体消息依赖存储 SDK 和富媒体消息插件, -// 具体的引用和初始化步骤请参考 SDK 配置指南 - -var fileUploadControl = $("#photoFileUpload")[0]; -var file = new AV.File("avatar.jpg", fileUploadControl.files[0]); -file - .save() - .then(function () { - var message = new ImageMessage(file); - message.setText("发自我的 Ins"); - message.setAttributes({ location: "旧金山" }); - return conversation.send(message); - }) - .then(function () { - console.log("发送成功"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - if let imageFilePath = Bundle.main.url(forResource: "image", withExtension: "jpg")?.path { - let imageMessage = IMImageMessage(filePath: imageFilePath, format: "jpg") - try conversation.send(message: imageMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` - -```dart -import 'package:flutter/services.dart' show rootBundle; - -// 假设项目根目录有 assets 文件夹存放图片,并且在 pubspec.yaml 中已经将 assets 文件夹添加到工程中。 -ByteData imageData = await rootBundle.load('assets/test.png'); -// image message -ImageMessage imageMessage = ImageMessage.from( - binaryData: imageData.buffer.asUint8List(), - format: 'png', - name: 'image.png', -); -try { - conversation.send(message: imageMessage); -} catch (e) { - print(e); -} -``` - - - -#### 发送图像链接 - -除了上述这种从本地直接发送图片文件的消息之外,在很多时候,用户可能从网络上或者别的应用中拷贝了一个图像的网络连接地址,当做一条图像消息发送到对话中,这种需求可以用如下代码来实现: - - - -```cs -var image = new LCFile("girl.gif", new Uri("http://example.com/girl.gif")); -var imageMessage = new LCIMImageMessage(image); -imageMessage.Text = "发自我的 Windows"; -await conversation.Send(imageMessage); -``` - -```java -LCFile file = new LCFile("萌妹子","http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif", null); -LCIMImageMessage m = new LCIMImageMessage(file); -m.setText("萌妹子一枚"); -// 创建一条图像消息 -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` - -```objc -// Tom 发了一张图片给 Jerry -LCFile *file = [LCFile fileWithURL:[self @"http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif"]]; -LCIMImageMessage *message = [LCIMImageMessage messageWithText:@"萌妹子一枚" file:file attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -var { ImageMessage } = initPlugin(AV, IM); -// 从网络链接直接构建一个图像消息 -var file = new AV.File.withURL( - "萌妹子", - "http://pic2.zhimg.com/6c10e6053c739ed0ce676a0aff15cf1c.gif" -); -file - .save() - .then(function () { - var message = new ImageMessage(file); - message.setText("萌妹子一枚"); - return conversation.send(message); - }) - .then(function () { - console.log("发送成功"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - if let url = URL(string: "http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif") { - let imageMessage = IMImageMessage(url: url, format: "gif") - try conversation.send(message: imageMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` - -```dart -ImageMessage imageMessage = ImageMessage.from( - url: 'http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif', - format: 'png', - name: 'image.png', -); -try { - conversation.send(message: imageMessage); -} catch (e) { - print(e); -} -``` - - - -#### 接收图像消息 - -图像消息的接收机制和之前是一样的,只需要修改一下接收消息的事件回调逻辑,根据消息类型来做不同的 UI 展现即可,例如: - - - -```cs -client.OnMessage = (conv, msg) => { - if (e.Message is LCIMImageMessage imageMessage) { - WriteLine(imageMessage.Url); - } -} -``` - -```java -LCIMMessageManager.registerMessageHandler(LCIMImageMessage.class, - new LCIMTypedMessageHandler() { - @Override - public void onMessage(LCIMImageMessage msg, LCIMConversation conv, LCIMClient client) { - // 只处理 Jerry 这个客户端的消息 - // 并且来自 conversationId 为 55117292e4b065f7ee9edd29 的 conversation 的消息 - if ("Jerry".equals(client.getClientId()) && "55117292e4b065f7ee9edd29".equals(conv.getConversationId())) { - String fromClientId = msg.getFrom(); - String messageId = msg.getMessageId(); - String url = msg.getFileUrl(); - Map metaData = msg.getFileMetaData(); - if (metaData.containsKey("size")) { - int size = (Integer) metaData.get("size"); - } - if (metaData.containsKey("width")) { - int width = (Integer) metaData.get("width"); - } - if (metaData.containsKey("height")) { - int height = (Integer) metaData.get("height"); - } - if (metaData.containsKey("format")) { - String format = (String) metaData.get("format"); - } - } - } -}); -``` - -```objc -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - LCIMImageMessage *imageMessage = (LCIMImageMessage *)message; - - // 消息的 ID - NSString *messageId = imageMessage.messageId; - // 图像文件的 URL - NSString *imageUrl = imageMessage.file.url; - // 发该消息的 clientId - NSString *fromClientId = message.clientId; -} -``` - -```js -var { Event, TextMessage } = require('leancloud-realtime'); -var { ImageMessage } = initPlugin(AV, IM); - -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - var file; - switch (message.type) { - case ImageMessage.TYPE: - file = message.getFile(); - console.log('收到图像消息,URL:' + file.url()); - break; - } -} -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - switch message { - case let imageMessage as IMImageMessage: - print(imageMessage) - default: - break - } - default: - break - } - default: - break - } -} -``` - -```dart -lient.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - if (message is ImageMessage) { - print('收到图像消息,URL:${message.url}'); - } -}; -``` - - - -### 发送音频消息/视频/文件 - -#### 发送流程 - -对于图像、音频、视频和文件这四种类型的消息,SDK 均采取如下的发送流程: - -如果文件是从 **客户端 API 读取的数据流(Stream)**,步骤为: - -1. 从本地构造 `LCFile` -2. 调用 `LCFile` 的上传方法将文件上传到云端,并获取文件元信息(`metaData`) -3. 把 `LCFile` 的 `objectId`、URL、文件元信息都封装在消息体内 -4. 调用接口发送消息 - -如果文件是 **外部链接的 URL**,则: - -1. 直接将 URL 封装在消息体内,不获取元信息(例如,音频消息的时长),不包含 `objectId` -2. 调用接口发送消息 - -以发送音频消息为例,基本流程是:读取音频文件(或者录制音频)> 构建音频消息 > 消息发送。 - - - -```cs -var audio = new LCFile("tante.mp3", Path.Combine(Application.persistentDataPath, "tante.mp3")); -var audioMessage = new LCIMAudioMessage(audio); -audioMessage.Text = "听听人类的神曲"; -await conversation.Send(audioMessage); -``` - -```java -LCFile file = LCFile.withAbsoluteLocalPath("忐忑.mp3",localFilePath); -LCIMAudioMessage m = new LCIMAudioMessage(file); -m.setText("听听人类的神曲"); -// 创建一条音频消息 -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` - -```objc -NSError *error = nil; -LCFile *file = [LCFile fileWithLocalPath:localPath error:&error]; -if (!error) { - LCIMAudioMessage *message = [LCIMAudioMessage messageWithText:@"听听人类的神曲" file:file attributes:nil]; - [conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } - }]; -} -``` - -```js -var AV = require("leancloud-storage"); -var { AudioMessage } = initPlugin(AV, IM); - -var fileUploadControl = $("#musicFileUpload")[0]; -var file = new AV.File("忐忑.mp3", fileUploadControl.files[0]); -file - .save() - .then(function () { - var message = new AudioMessage(file); - message.setText("听听人类的神曲"); - return conversation.send(message); - }) - .then(function () { - console.log("发送成功"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - if let filePath = Bundle.main.url(forResource: "audio", withExtension: "mp3")?.path { - let audioMessage = IMAudioMessage(filePath: filePath, format: "mp3") - audioMessage.text = "听听人类的神曲" - try conversation.send(message: audioMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` - -```dart -import 'package:flutter/services.dart' show rootBundle; - -// 假设项目根目录有 assets 文件夹存放 mp3 文件,并且在 pubspec.yaml 中已经将 assets 文件夹添加到工程中。 -ByteData audioData = await rootBundle.load('assets/test.mp3'); -AudioMessage audioMessage = AudioMessage.from( - binaryData: audioData.buffer.asUint8List(), - format: 'mp3', -); -audioMessage.text = '听听人类的神曲'; -try { - await conversation.send(message: audioMessage); -} catch (e) { - print(e); -} -``` - - - -与图像消息类似,音频消息也支持从 URL 构建: - - - -```cs -var audio = new LCFile("apple.aac", new Uri("https://some.website.com/apple.aac")); -var audioMessage = new LCIMAudioMessage(audio); -audioMessage.Text = "来自苹果发布会现场的录音"; -await conversation.Send(audioMessage); -``` - -```java -LCFile file = new LCFile("apple.aac", "https://some.website.com/apple.aac", null); -LCIMAudioMessage m = new LCIMAudioMessage(file); -m.setText("来自苹果发布会现场的录音"); -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` - -```objc -LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"https://some.website.com/apple.aac"]]; -LCIMAudioMessage *message = [LCIMAudioMessage messageWithText:@"来自苹果发布会现场的录音" file:file attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -var { AudioMessage } = initPlugin(AV, IM); - -var file = new AV.File.withURL( - "apple.aac", - "https://some.website.com/apple.aac" -); -file - .save() - .then(function () { - var message = new AudioMessage(file); - message.setText("来自苹果发布会现场的录音"); - return conversation.send(message); - }) - .then(function () { - console.log("发送成功"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - if let url = URL(string: "https://some.website.com/apple.aac") { - let audioMessage = IMAudioMessage(url: url, format: "aac") - audioMessage.text = "来自苹果发布会现场的录音" - try conversation.send(message: audioMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` - -```dart -AudioMessage audioMessage = AudioMessage.from( - url: 'https://some.website.com/apple.aac', - name: 'apple.aac', -); -try { - await conversation.send(message: audioMessage); -} catch (e) { - print(e); -} -``` - - - -### 发送地理位置消息 - -地理位置消息构建方式如下: - - - -```cs -var location = new LCGeoPoint(31.3753285, 120.9664658); -var locationMessage = new LCIMLocationMessage(location); -await conversation.Send(locationMessage); -``` - -```java -final LCIMLocationMessage locationMessage = new LCIMLocationMessage(); -// 开发者可以通过设备的 API 获取设备的具体地理位置,此处设置了 2 个经纬度常量作为演示 -locationMessage.setLocation(new LCGeoPoint(31.3753285,120.9664658)); -locationMessage.setText("蛋糕店的位置"); -conversation.sendMessage(locationMessage, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (null != e) { - e.printStackTrace(); - } else { - // 发送成功 - } - } -}); -``` - -```objc -LCIMLocationMessage *message = [LCIMLocationMessage messageWithText:@"蛋糕店的位置" latitude:31.3753285 longitude:120.9664658 attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -var { LocationMessage } = initPlugin(AV, IM); - -var location = new AV.GeoPoint(31.3753285, 120.9664658); -var message = new LocationMessage(location); -message.setText("蛋糕店的位置"); -conversation - .send(message) - .then(function () { - console.log("发送成功"); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let locationMessage = IMLocationMessage(latitude: 31.3753285, longitude: 120.9664658) - try conversation.send(message: locationMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -LocationMessage locationMessage = LocationMessage.from( - latitude: 22, - longitude: 33, -); -try { - await conversation.send(message: locationMessage); -} catch (e) { - print(e); -} -``` - - - -### 再谈接收消息 - - -<> - -C# SDK 通过 `OnMessage` 事件回调来通知新消息: - -```cs -jerry.OnMessage = (conv, msg) => { - if (msg is LCIMImageMessage imageMessage) { - - } else if (msg is LCIMAudioMessage audioMessage) { - - } else if (msg is LCIMVideoMessage videoMessage) { - - } else if (msg is LCIMFileMessage fileMessage) { - - } else if (msg is AVIMLocationMessage locationMessage) { - - } else if (msg is InputtingMessage) { - WriteLine($"收到自定义消息 {inputtingMessage.TextContent} {inputtingMessage.Ecode}"); - } -} -``` - - -<> - -Java/Android SDK 中定义了 `LCIMMessageHandler` 接口来通知应用层新消息到达事件发生,开发者通过调用 `LCIMMessageManager.registerDefaultMessageHandler` 方法来注册自己的消息处理函数。`LCIMMessageManager` 提供了两个不同的方法来注册默认的消息处理函数,或特定类型的消息处理函数: - -```java -/** - * 注册默认的消息 handler - * - * @param handler - */ -public static void registerDefaultMessageHandler(LCIMMessageHandler handler); -/** - * 注册特定消息格式的处理单元 - * - * @param clazz 特定的消息类 - * @param handler - */ -public static void registerMessageHandler(Class clazz, MessageHandler handler); -/** - * 取消特定消息格式的处理单元 - * - * @param clazz - * @param handler - */ -public static void unregisterMessageHandler(Class clazz, MessageHandler handler); -``` - -消息处理函数需要在应用初始化时完成设置,理论上我们支持为每一种消息(包括应用层自定义的消息)分别注册不同的消息处理函数,并且也支持取消注册。 - -多次调用 `LCIMMessageManager` 的 `registerDefaultMessageHandler`,只有最后一次调用有效;而通过 `registerMessageHandler` 注册的 `LCIMMessageHandler`,则是可以同存的。 - -当客户端收到一条消息的时候,SDK 内部的处理流程为: - -- 首先解析消息的类型,然后找到开发者为这一类型所注册的处理响应 handler chain,再逐一调用这些 handler 的 `onMessage` 函数。 -- 如果没有找到专门处理这一类型消息的 handler,就会转交给 `defaultHandler` 处理。 - -这样一来,在开发者为 `AVIMTypedMessage`(及其子类)指定了专门的 handler,也指定了全局的 `defaultHandler` 了的时候,如果发送端发送的是通用的 `LCIMMessage` 消息,那么接收端就是 `LCIMMessageManager.registerDefaultMessageHandler()` 中指定的 handler 被调用;如果发送的是 `LCIMTypedMessage`(及其子类)的消息,那么接收端就是 `LCIMMessageManager.registerMessageHandler()` 中指定的 handler 被调用。 - -```java -// 1. 注册默认 handler,只有其他 handle 都没有被调用到时才会调用 -LCIMMessageManager.registerDefaultMessageHandler(new LCIMMessageHandler(){ - public void onMessage(LCIMMessage message, LCIMConversation conversation, LCIMClient client) { - // 接收消息 - } - - public void onMessageReceipt(LCIMMessage message, LCIMConversation conversation, LCIMClient client) { - // 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。 - // 因此别忘了在这里处理未知类型,例如提示用户升级客户端至最新版本。 - } -}); -// 2. 为每一种消息类型注册 handler -LCIMMessageManager.registerMessageHandler(LCIMTypedMessage.class, new LCIMTypedMessageHandler(){ - public void onMessage(LCIMTypedMessage message, LCIMConversation conversation, LCIMClient client) { - switch (message.getMessageType()) { - case LCIMMessageType.TEXT_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMTextMessage textMessage = (LCIMTextMessage)message; - break; - case LCIMMessageType.IMAGE_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMImageMessage imageMessage = (LCIMImageMessage)message; - break; - case LCIMMessageType.AUDIO_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMAudioMessage audioMessage = (LCIMAudioMessage)message; - break; - case LCIMMessageType.VIDEO_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMVideoMessage videoMessage = (LCIMVideoMessage)message; - break; - case LCIMMessageType.LOCATION_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMLocationMessage locationMessage = (LCIMLocationMessage)message; - break; - case LCIMMessageType.FILE_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMFileMessage fileMessage = (LCIMFileMessage)message; - break; - case LCIMMessageType.RECALLED_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMRecalledMessage recalledMessage = (LCIMRecalledMessage)message; - break; - case 123: - // 这是一个自定义消息类型 - // 执行其他逻辑 - CustomMessage customMessage = (CustomMessage)message; - break; - } - } - - public void onMessageReceipt(LCIMTypedMessage message, LCIMConversation conversation, LCIMClient client) { - // 执行收到消息后的逻辑 - } -}); -``` - - -<> - -Objective-C SDK 是通过实现 `LCIMClientDelegate` 代理来响应新消息到达通知的,并且,分别使用了两个方法来分别处理普通的 `LCIMMessage` 消息和内建的多媒体消息 `LCIMTypedMessage`(包括应用层由此派生的自定义消息: - -```objc -/*! - 接收到新的普通消息。 - @param conversation - 所属对话 - @param message - 具体的消息 - */ -- (void)conversation:(LCIMConversation *)conversation didReceiveCommonMessage:(LCIMMessage *)message; - -/*! - 接收到新的富媒体消息。 - @param conversation - 所属对话 - @param message - 具体的消息 - */ -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message; -``` - -```objc -// 处理默认类型消息 -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - if (message.mediaType == LCIMMessageMediaTypeImage) { - LCIMImageMessage *imageMessage = (LCIMImageMessage *)message; // 处理图像消息 - } else if(message.mediaType == LCIMMessageMediaTypeAudio){ - // 处理音频消息 - } else if(message.mediaType == LCIMMessageMediaTypeVideo){ - // 处理视频消息 - } else if(message.mediaType == LCIMMessageMediaTypeLocation){ - // 处理位置消息 - } else if(message.mediaType == LCIMMessageMediaTypeFile){ - // 处理文件消息 - } else if(message.mediaType == LCIMMessageMediaTypeText){ - // 处理文本消息 - } else if(message.mediaType == 123){ - // 处理自定义的消息类型 - } -} - -// 处理未知消息类型 -- (void)conversation:(LCIMConversation *)conversation didReceiveCommonMessage:(LCIMMessage *)message { - // 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。 - // 因此别忘了在这里处理未知类型,例如提示用户升级客户端至最新版本。 -} -``` - - -<> - -不管消息类型如何,JavaScript SDK 都是是通过 `IMClient` 上的 `Event.MESSAGE` 事件回调来通知新消息的,应用层只需要在一个地方,统一对不同类型的消息使用不同方式来处理即可。 - -```js -// 在初始化 Realtime 时,需加载 TypedMessagesPlugin -var { Event, TextMessage } = require("leancloud-realtime"); -var { FileMessage, ImageMessage, AudioMessage, VideoMessage, LocationMessage } = - initPlugin(AV, IM); -// 注册 MESSAGE 事件的 handler -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - // 请按自己需求改写 - var file; - switch (message.type) { - case TextMessage.TYPE: - console.log( - "收到文本消息,内容:" + message.getText() + ",ID:" + message.id - ); - break; - case FileMessage.TYPE: - file = message.getFile(); // file 是 AV.File 实例 - console.log( - "收到文件消息,URL:" + file.url() + ",大小:" + file.metaData("size") - ); - break; - case ImageMessage.TYPE: - file = message.getFile(); - console.log( - "收到图像消息,URL:" + file.url() + ",宽度:" + file.metaData("width") - ); - break; - case AudioMessage.TYPE: - file = message.getFile(); - console.log( - "收到音频消息,URL:" + - file.url() + - ",长度:" + - file.metaData("duration") - ); - break; - case VideoMessage.TYPE: - file = message.getFile(); - console.log( - "收到视频消息,URL:" + - file.url() + - ",长度:" + - file.metaData("duration") - ); - break; - case LocationMessage.TYPE: - var location = message.getLocation(); - console.log( - "收到位置消息,纬度:" + - location.latitude + - ",经度:" + - location.longitude - ); - break; - case 1: - console.log("OperationMessage 是自定义消息类型"); - default: - // 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。 - // 因此别忘了在默认分支中处理未知类型,例如提示用户升级客户端至最新版本。 - console.warn("收到未知类型消息"); - } -}); - -// 同时,对应的 conversation 上也会派发 `MESSAGE` 事件: -conversation.on(Event.MESSAGE, function messageEventHandler(message) { - // 这里补充业务逻辑 -}); -``` - - -<> - -Swift SDK 是通过实现 `IMClientDelegate` 代理来响应新消息到达通知的: - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - print(message) - default: - break - } - default: - break - } -} -``` - -```swift -// 处理默认类型消息 -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - if let categorizedMessage = message as? IMCategorizedMessage { - switch categorizedMessage { - case let textMessage as IMTextMessage: - print(textMessage) - case let imageMessage as IMImageMessage: - print(imageMessage) - case let audioMessage as IMAudioMessage: - print(audioMessage) - case let videoMessage as IMVideoMessage: - print(videoMessage) - case let fileMessage as IMFileMessage: - print(fileMessage) - case let locationMessage as IMLocationMessage: - print(locationMessage) - case let recalledMessage as IMRecalledMessage: - print(recalledMessage) - case let customMessage as CustomMessage: - print("customMessage 是自定义消息类型") - default: - break - } else { - // 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。 - // 因此别忘了在默认分支中处理未知类型,例如提示用户升级客户端至最新版本。 - print("收到未知类型消息") - } - default: - break - } - default: - break - } -} -``` - - -<> - -```dart -jerry.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - if (message.binaryContent != null) { - print('收到二进制消息:${message.binaryContent.toString()}'); - } else if (message is TextMessage) { - print('收到文本类型消息:${message.text}'); - } else if (message is LocationMessage) { - print('收到地理位置消息,坐标:${message.latitude},${message.longitude}'); - } else if (message is FileMessage) { - if (message is ImageMessage) { - print('收到图像消息,图像 URL:${message.url}'); - } else if (message is AudioMessage) { - print('收到音频消息,消息时长:${message.duration}'); - } else if (message is VideoMessage) { - print('收到视频消息,消息时长:${message.duration}'); - } else { - print('收到 .txt/.doc/.md 等各种类型的普通文件消息,URL:${message.url}'); - } - } else if (message is CustomMessage) { - // CustomMessage 是自定义的消息类型 - print('收到自定义类型消息'); - } else { - // 这里可以继续添加自定义类型的判断条件 - print('收到未知消息类型'); - if (message.stringContent != null) { - print('收到普通消息:${message.stringContent}'); - } - } -}; -``` - - - - -上面的代码示例中涉及到接收自定义消息。 -我们将在[即时通讯开发指南第二篇](/sdk/im/guide/intermediate/)的《自定义消息类型》一节介绍。 - -## 扩展对话:支持自定义属性 - -「对话(`Conversation`)」是即时通讯的核心逻辑对象,它有一些内置的常用的属性,与控制台中 `_Conversation` 表是一一对应的。默认提供的 **内置** 属性的对应关系如下: - - - -| `AVIMConversation` 属性名 | `_Conversation` 字段 | 含义 | -| ------------------------- | -------------------- | ------------------------------------------------------------ | -| `CurrentClient` | N/A | 对话所属的 `AVIMClient` 对象 | -| `ConversationId` | `objectId` | 全局唯一的 ID | -| `Name` | `name` | 成员共享的统一的名字 | -| `MemberIds` | `m` | 成员列表 | -| `MuteMemberIds` | `mu` | 静音该对话的成员 | -| `Creator` | `c` | 对话创建者 | -| `IsTransient` | `tr` | 是否为聊天室 | -| `IsSystem` | `sys` | 是否为系统对话 | -| `IsUnique` | `unique` | 是否为相同成员的唯一对话 | -| `IsTemporary` | N/A | 是否为临时对话(临时对话数据不保存到 `_Conversation` 表中 ) | -| `CreatedAt` | `createdAt` | 创建时间 | -| `UpdatedAt` | `updatedAt` | 最后更新时间 | -| `LastMessageAt` | `lm` | 该对话最后一条消息,也可以理解为最后一次活跃时间 | - -| `LCIMConversation` get 方法名 | `_Conversation` 字段 | 含义 | -| ----------------------------- | -------------------- | ------------------------------------------------------------ | -| `getAttributes` | `attr` | 自定义属性 | -| `getConversationId` | `objectId` | 全局唯一的 ID | -| `getCreatedAt` | `createdAt` | 创建时间 | -| `getCreator` | `c` | 对话创建者 | -| `getLastDeliveredAt` | N/A | (仅限单聊)最后一条已送达对方的消息时间 | -| `getLastMessage` | N/A | 最后一条消息,可能会空 | -| `getLastMessageAt` | `lm` | 该对话最后一条消息,也可以理解为最后一次活跃时间 | -| `getLastReadAt` | N/A | (仅限单聊)最后一条对方已读的消息时间 | -| `getMembers` | `m` | 成员列表 | -| `getName` | `name` | 成员共享的统一的名字 | -| `getTemporaryExpiredat` | N/A | 临时对话存活时间 | -| `getUniqueId` | `uniqueId` | `Unique Conversation` 全局唯一的 `ID` | -| `getUnreadMessagesCount` | N/A | 未读消息数 | -| `getUpdatedAt` | `updatedAt` | 最后更新时间 | -| `isSystem` | `sys` | 是否为系统对话 | -| `isTemporary` | N/A | 是否为临时对话(临时对话数据不保存到 `_Conversation` 表中 ) | -| `isTransient` | `tr` | 是否为聊天室 | -| `isUnique` | `unique` | 是否是 `Unique Conversation` | -| `unreadMessagesMentioned` | N/A | 未读消息是否 @ 了当前的 `Client` | - -| `LCIMConversation` 属性名 | `_Conversation` 字段 | 含义 | -| ----------------------------- | -------------------- | ------------------------------------------------------------ | -| `clientID` | N/A | 会话所属的 `Client` 的 `ID` | -| `conversationId` | `objectId` | 全局唯一的 ID | -| `creator` | `c` | 对话创建者 | -| `createdAt` | `createdAt` | 创建时间 | -| `updatedAt` | `updatedAt` | 最后更新时间 | -| `lastMessage` | N/A | 最后一条消息,可能会空 | -| `lastMessageAt` | `lm` | 最后一条消息发送时间,也可以理解为最后一次活跃时间 | -| `lastReadAt` | N/A | (仅限单聊)最后一条对方已读的消息时间 | -| `lastDeliveredAt` | N/A | (仅限单聊)最后一条已送达对方的消息时间 | -| `unreadMessagesCount` | N/A | 未读消息数 | -| `unreadMessageContainMention` | N/A | 未读消息是否 @ 了当前的 `Client` | -| `name` | `name` | 成员共享的统一的名字 | -| `members` | `m` | 成员列表 | -| `attributes` | `attr` | 自定义属性 | -| `uniqueId` | `uniqueId` | `Unique Conversation` 全局唯一的 `ID` | -| `unique` | `unique` | 是否是 `Unique Conversation` | -| `transient` | `tr` | 是否为暂态会话 | -| `system` | `sys` | 是否为系统对话 | -| `temporary` | N/A | 是否为临时对话(临时对话数据不保存到 `_Conversation` 表中 ) | -| `temporaryTTL` | N/A | 临时对话存活时间 | -| `muted` | N/A | 当前用户是否静音该对话 | -| `imClient` | N/A | 对话所属的 `LCIMClient` 对象 | - -| `Conversation` 属性名 | `_Conversation` 字段 | 含义 | -| --------------------- | -------------------- | -------------------------------------------------- | -| `createdAt` | `createdAt` | 创建时间 | -| `creator` | `c` | 对话创建者 | -| `id` | `objectId` | 全局唯一的 ID | -| `lastDeliveredAt` | N/A | (仅限单聊)最后一条已送达对方的消息时间 | -| `lastMessage` | N/A | 最后一条消息,可能会空 | -| `lastMessageAt` | `lm` | 最后一条消息发送时间,也可以理解为最后一次活跃时间 | -| `lastReadAt` | N/A | (仅限单聊)最后一条对方已读的消息时间 | -| `members` | `m` | 成员列表 | -| `muted` | N/A | 当前用户是否静音该对话 | -| `mutedMembers` | `mu` | 静音该对话的成员 | -| `name` | `name` | 成员共享的统一的名字 | -| `system` | `sys` | 是否为服务号(系统对话) | -| `transient` | `tr` | 是否为暂态会话 | -| `unreadMessagesCount` | N/A | 未读消息数 | -| `updatedAt` | `updatedAt` | 最后更新时间 | - -| `IMConversation` 属性名 | `_Conversation` 字段 | 含义 | -| ------------------------------- | -------------------- | ---------------------------------------------------------- | -| `client` | N/A | 会话所属的 `Client` | -| `ID` | `objectId` | 会话的全局唯一 `ID` | -| `clientID` | N/A | 会话所属的 `Client` 的 `ID` | -| `isUnique` | `unique` | 是否是 `Unique Conversation` | -| `uniqueID` | `uniqueId` | `Unique Conversation` 全局唯一的 `ID` | -| `name` | `name` | 会话的名称 | -| `creator` | `c` | 会话的创建者 | -| `createdAt` | `createdAt` | 会话的创建时间 | -| `updatedAt` | `updatedAt` | 会话的最后更新时间 | -| `attributes` | `attr` | 会话的自定义属性 | -| `members` | `m` | 会话的成员列表 | -| `isMuted` | N/A | 当前用户是否静音该对话 | -| `isOutdated` | N/A | 会话的属性是否过期,可以根据该属性来决定是否更新会话的数据 | -| `lastMessage` | N/A | 最新一条消息,可能会空 | -| `unreadMessageCount` | N/A | 未读消息数 | -| `isUnreadMessageContainMention` | N/A | 未读消息是否 @ 了当前的 `Client` | -| `memberInfoTable` | N/A | 成员信息表 | - -| `Conversation` 属性名 | `_Conversation` 字段 | 含义 | -| ------------------------- | -------------------- | ---------------------------------------- | -| `attributes` | `attr` | 自定义属性 | -| `client` | N/A | 对话所属的 `Client` 对象 | -| `createdAt` | `createdAt` | 创建时间 | -| `creator` | `c` | 对话创建者 | -| `id` | `objectId` | 全局唯一的 ID | -| `isMuted` | N/A | 当前用户是否静音该对话 | -| `isUnique` | `unique` | 是否是 `Unique Conversation` | -| `lastDeliveredAt` | N/A | (仅限单聊)最后一条已送达对方的消息时间 | -| `lastMessage` | N/A | 最后一条消息,可能会空 | -| `lastReadAt` | N/A | (仅限单聊)最后一条对方已读的消息时间 | -| `members` | `m` | 成员列表 | -| `name` | `name` | 成员共享的统一的名字 | -| `uniqueID` | `uniqueId` | `Unique Conversation` 的唯一 ID | -| `unreadMessagesCount` | N/A | 未读消息数 | -| `unreadMessagesMentioned` | N/A | 未读消息是否 @ 了当前的 `Client` | -| `updatedAt` | `updatedAt` | 最后更新时间 | - - - -不过,我们不建议直接对 `_Conversation` 进行写操作,因为: - -- 客户端 SDK 查询会话数据是走 websocket 长连接,会首先从即时通讯服务器的内存缓存中查。直接操作 `_Conversation` 表,不会更新即时通讯服务器的缓存,这就带来了缓存不一致问题。 -- 直接操作 `_Conversation` 表的情况下,即时通讯服务器不会下发相应的事件通知客户端,客户端自然也就无从响应。 -- 如果定义了即时通讯服务的 hook 函数,直接操作 `_Conversation` 表不会触发这些 hook。 - -如有管理需求,我们推荐调用专门的即时通讯 REST API 接口。 - -另外,我们可以通过「自定义属性」来在「对话」中保存更多业务层数据。 - -### 创建自定义属性 - -在最开始介绍 [创建单聊对话](#创建对话-conversation) 的时候,我们提到过 `IMClient#createConversation` 接口支持附加自定义属性,现在我们就来演示一下如何使用自定义属性。 - -假如在创建对话的时候,我们需要添加两个额外的属性值对 `{ "type": "private", "pinned": true }`,那么在调用 `IMClient#createConversation` 方法时可以把附加属性传进去: - - - -```cs -var properties = new Dictionary { - { "type", "private" }, - { "pinned", true } -}; -var conversation = await tom.CreateConversation("Jerry", name: "Tom & Jerry", unique: true, properties: properties); -``` - -```java -HashMap attr = new HashMap(); -attr.put("type","private"); -attr.put("pinned",true); -client.createConversation(Arrays.asList("Jerry"),"猫和老鼠", attr, false, true, - new LCIMConversationCreatedCallback(){ - @Override - public void done(LCIMConversation conv,LCIMException e){ - if(e==null){ - // 创建成功 - } - } - }); -``` - -```objc -// Tom 创建名称为「猫和老鼠」的会话,并附加会话属性 -LCIMConversationCreationOption *option = [LCIMConversationCreationOption new]; -option.name = @"猫和老鼠"; -option.attributes = @{ - @"type": @"private", - @"pinned": @(YES) -}; -[self createConversationWithClientIds:@[@"Jerry"] option:option callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"创建成功!"); - } -}]; -``` - -```js -tom - .createConversation({ - members: ["Jerry"], - name: "猫和老鼠", - unique: true, - type: "private", - pinned: true, - }) - .then(function (conversation) { - console.log("创建成功。ID:" + conversation.id); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - try tom.createConversation(clientIDs: ["Jerry"], name: "猫和老鼠", attributes: ["type": "private", "pinned": true], isUnique: true, completion: { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - Conversation conversation = await jerry.createConversation( - members: {'client1.id', 'client2.id'}, - attributes: { - 'members': ['Jerry'], - 'name': '猫和老鼠', - 'unique': true, - 'type': 'private', - 'pinned': true, - }, - ); -} catch (e) { - print(e); -} -``` - - - -**自定义属性在 SDK 级别是对所有成员可见的。**我们也支持通过自定义属性来查询对话,请参见 [使用复杂条件来查询对话](#使用复杂条件来查询对话)。 - -### 修改和使用属性 - -在 `Conversation` 对象中,系统默认提供的属性,例如对话的名字(`name`),如果业务层没有限制的话,所有成员都是可以修改的,示例代码如下: - - - -```cs -await conversation.UpdateInfo(new Dictionary { - { "name", "聪明的喵星人" } -}); -``` - -```java -LCIMConversation conversation = client.getConversation("55117292e4b065f7ee9edd29"); -conversation.setName("聪明的喵星人"); -conversation.updateInfoInBackground(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // 更新成功 - } - } -}); -``` - -```objc -conversation[@"name"] = @"聪明的喵星人"; -[conversation updateWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"修改成功!"); - } -}]; -``` - -```js -conversation.name = "聪明的喵星人"; -conversation.save(); -``` - -```swift -do { - try conversation.update(attribution: ["name": "聪明的喵星人"], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - await conversation.updateInfo(attributes: { - 'name': '聪明的喵星人', - }); -} catch (e) { - print(e); -} -``` - - - -而 `Conversation` 对象中自定义的属性,即时通讯服务也是允许对话内其他成员来读取、使用和修改的,示例代码如下: - - - -```cs -// 获取自定义属性 -var type = conversation["type"]; -// 为 pinned 属性设置新的值 -await conversation.UpdateInfo(new Dictionary { - { "pinned", false } -}); -``` - -```java -// 获取自定义属性 -String type = conversation.get("attr.type"); -// 为 pinned 属性设置新的值 -conversation.set("attr.pinned",false); -// 保存 -conversation.updateInfoInBackground(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // 更新成功 - } - } -}); -``` - -```objc -// 获取自定义属性 -NSString *type = conversation.attributes[@"type"]; -// 为 pinned 属性设置新的值 -[conversation setObject:@(NO) forKey:@"attr.pinned"]; -// 保存 -[conversation updateWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"修改成功!"); - } -}]; -``` - -```js -// 获取自定义属性 -var type = conversation.get("attr.type"); -// 为 pinned 属性设置新的值 -conversation.set("attr.pinned", false); -// 保存 -conversation.save(); -``` - -```swift -do { - let type = conversation.attributes?["type"] as? String - try conversation.update(attribution: ["attr.pinned": false]) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { -// 获取自定义属性 - String type = conversation.attributes['type']; -// 为 pinned 属性设置新的值 - await conversation.updateInfo(attributes: { - 'pinned': false, - }); -} catch (e) { - print(e); -} -``` - - - -对自定义属性名的说明 - -在 `IMClient#createConversation` 接口中指定的自定义属性,会被存入 `_Conversation` 表的 `attr` 字段,所以在之后对这些属性进行读取或修改的时候,属性名需要指定完整的路径,例如上面的 `attr.type`,这一点需要特别注意。 - -### 对话属性同步 - -对话的名字以及应用层附加的其他属性,一般都是需要全员共享的,一旦有人对这些数据进行了修改,那么就需要及时通知到全部成员。在前一个例子中,有一个用户对话名称改为了「聪明的喵星人」,那其他成员怎么能知道这件事情呢? - -即时通讯云端提供了实时同步的通知机制,会把单个用户对「对话」的修改同步下发到所有在线成员(对于非在线的成员,他们下次登录上线之后,自然会拉取到最新的完整的对话数据)。对话属性更新的通知事件声明如下: - - - -```cs -jerry.OnConversationInfoUpdated = (conv, attrs, initBy) => { - WriteLine($"对话:${conv.Id} 被更新"); -}; -``` - -```java -// 在 LCIMConversationEventHandler 接口中有如下定义 -/** - * 对话自身属性变更通知 - * - * @param client - * @param conversation - * @param attr 被更新的属性 - * @param operator 该操作的发起者 ID - */ -public void onInfoChanged(LCIMClient client, LCIMConversation conversation, JSONObject attr, - String operator) -``` - -```objc -/// Notification for conversation's attribution updated. -/// @param conversation Updated conversation. -/// @param date Updated date. -/// @param clientId Client ID which do this update. -/// @param updatedData Updated data. -/// @param updatingData Updating data. -- (void)conversation:(LCIMConversation *)conversation didUpdateAt:(NSDate * _Nullable)date byClientId:(NSString * _Nullable)clientId updatedData:(NSDictionary * _Nullable)updatedData updatingData:(NSDictionary * _Nullable)updatingData; -``` - -```js -/** - * 对话信息被更新 - * @event IMClient#CONVERSATION_INFO_UPDATED - * @param {Object} payload - * @param {Object} payload.attributes 被更新的属性 - * @param {String} payload.updatedBy 该操作的发起者 ID - */ -var { Event } = require("leancloud-realtime"); -client.on(Event.CONVERSATION_INFO_UPDATED, function (payload) {}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .dataUpdated(updatingData: updatingData, updatedData: updatedData, byClientID: byClientID, at: atDate): - print(updatingData) - print(updatedData) - print(byClientID) - print(atDate) - default: - break - } -} -``` - -```dart -jerry.onInfoUpdated = ({ - Client client, - Conversation conversation, - Map updatingAttributes, - Map updatedAttributes, - String byClientID, - DateTime atDate, -}) { - print('会话:${conversation.id} 被更新'); -}; -``` - - - -使用提示: - -应用层在该事件的响应函数中,可以获知当前什么属性被修改了,也可以直接从 SDK 的 `Conversation` 实例中获取最新的合并之后的属性值,然后依据需要来更新产品 UI。 - -### 获取群内成员列表 - -群内成员列表是作为对话的属性持久化保存在云端的,所以要获取一个 `Conversation` 对象的成员列表,我们可以在调用这个对象的更新方法之后,直接获取成员属性即可。 - - - -```cs -await conversation.Fetch(); -``` - -```java -// fetchInfoInBackground 方法会执行一次刷新操作,以获取云端最新对话数据。 -conversation.fetchInfoInBackground(new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - conversation.getMembers(); - } - } -}); -``` - -```objc -// fetchWithCallback 方法会执行一次刷新操作,以获取云端最新对话数据。 -[conversation fetchWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"", conversation.members); - } -}]; -``` - -```js -// fetch 方法会执行一次刷新操作,以获取云端最新对话数据。 -conversation.fetch().then(function(conversation) { - console.log('members: ', conversation.members); -).catch(console.error.bind(console)); -``` - -```swift -do { - try conversation.refresh { (result) in - switch result { - case .success: - if let members = conversation.members { - print(members) - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// 暂不支持 -``` - - - -使用提示: - -成员列表是对 **_普通对话_** 而言的,对于像「聊天室」「系统对话」这样的特殊对话,并不存在「成员列表」属性。 - -## 使用复杂条件来查询对话 - -除了在事件通知接口中获得 `Conversation` 实例之外,开发者也可以根据不同的属性和条件来查询 `Conversation` 对象。例如有些产品允许终端用户根据名字或地理位置来匹配感兴趣聊天室,也有些业务场景允许查询成员列表中包含特定用户的所有对话,这些都可以通过对话查询的接口实现。 - -### 根据 ID 查询 - -ID 对应就是 `_Conversation` 表中的 `objectId` 的字段值,这是一种最简单也最高效的查询(因为云端会对 ID 建立索引): - - - -```cs -var query = tom.GetQuery(); -var conversation = await query.Get("551260efe4b01608686c3e0f"); -``` - -```java -LCIMConversationsQuery query = tom.getConversationsQuery(); -query.whereEqualTo("objectId","551260efe4b01608686c3e0f"); -query.findInBackground(new LCIMConversationQueryCallback(){ - @Override - public void done(List convs,LCIMException e){ - if(e==null){ - if(convs!=null && !convs.isEmpty()){ - // convs.get(0) 就是想要的 conversation - } - } - } -}); -``` - -```objc -LCIMConversationQuery *query = [tom conversationQuery]; -[query getConversationById:@"551260efe4b01608686c3e0f" callback:^(LCIMConversation *conversation, NSError *error) { - if (succeeded) { - NSLog(@"查询成功!"); - } -}]; -``` - -```js -tom - .getConversation("551260efe4b01608686c3e0f") - .then(function (conversation) { - console.log(conversation.id); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let conversationQuery = tom.conversationQuery - try conversationQuery.getConversation(by: "551260efe4b01608686c3e0f") { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// 我们建议开发者首先尝试从内存中获取对话,以减少不必要的网络请求。 - -String convID = '551260efe4b01608686c3e0f'; -Conversation conversation = tom.conversationMap[convID]; -if (conversation == null) { - try { - ConversationQuery query = tom.conversationQuery(); - query.whereEqualTo('objectId', convID); - conversation = await query.find(); - } catch (e) { - print(e); - } -} -``` - - - -### 基础的条件查询 - -即时通讯 SDK 提供了丰富的条件查询方式,可以满足各种复杂的业务需求。 - -我们首先从最简单的 `equalTo` 开始。例如查询所有自定义属性 `type`(字符串类型)为 `private` 的对话,需要如下代码: - - - -```cs -var query = tom.GetQuery() - .WhereEqualTo("type", "private"); -await query.Find(); -``` - -```java -LCIMConversationsQuery query = tom.getConversationsQuery(); -query.whereEqualTo("attr.type","private"); -// 执行查询 -query.findInBackground(new LCIMConversationQueryCallback(){ - @Override - public void done(List convs,LCIMException e){ - if(e == null){ - // convs 就是想要的结果 - } - } -}); -``` - -```objc -LCIMConversationQuery *query = [tom conversationQuery]; -[query whereKey:@"attr.type" equalTo:@"private"]; -// 执行查询 -[query findConversationsWithCallback:^(NSArray *objects, NSError *error) { - NSLog(@"找到 %ld 个对话!", [objects count]); -}]; -``` - -```js -var query = client.getQuery(); -query.equalTo("attr.type", "private"); -query - .find() - .then(function (conversations) { - // conversations 就是想要的结果 - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let conversationQuery = tom.conversationQuery - try conversationQuery.where("attr.type", .equalTo("private")) - try conversationQuery.findConversations { (result) in - switch result { - case .success(value: let conversations): - print(conversations) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - ConversationQuery query = jerry.conversationQuery(); - query.whereEqualTo('attr.type', 'private'); -// conversations 就是想要的结果 - List conversations = await query.find(); -} catch (e) { - print(e); -} -``` - - - -熟悉数据存储服务的开发者可以更容易理解对话的查询构建,因为对话查询和数据存储服务的对象查询在接口上是十分接近的: - -- 可以通过 `find` 获取当前结果页数据 -- 支持通过 `count` 获取结果数 -- 支持通过 `first` 获取第一个结果 -- 支持通过 `skip` 和 `limit` 对结果进行分页 - -与 `equalTo` 类似,针对 `Number` 和 `Date` 类型的属性还可以使用大于、大于等于、小于、小于等于等,详见下表: - - - -| 逻辑比较 | `AVIMConversationQuery` 方法 | -| -------- | ---------------------------- | -| 等于 | `WhereEqualTo` | -| 不等于 | `WhereNotEqualsTo` | -| 大于 | `WhereGreaterThan` | -| 大于等于 | `WhereGreaterThanOrEqualsTo` | -| 小于 | `WhereLessThan` | -| 小于等于 | `WhereLessThanOrEqualsTo` | - -| 逻辑比较 | `LCIMConversationsQuery` 方法 | -| -------- | ----------------------------- | -| 等于 | `whereEqualTo` | -| 不等于 | `whereNotEqualsTo` | -| 大于 | `whereGreaterThan` | -| 大于等于 | `whereGreaterThanOrEqualsTo` | -| 小于 | `whereLessThan` | -| 小于等于 | `whereLessThanOrEqualsTo` | - -| 逻辑比较 | `LCIMConversationQuery` 方法 | -| -------- | ---------------------------- | -| 等于 | `equalTo` | -| 不等于 | `notEqualTo` | -| 大于 | `greaterThan` | -| 大于等于 | `greaterThanOrEqualTo` | -| 小于 | `lessThan` | -| 小于等于 | `lessThanOrEqualTo` | - -| 逻辑比较 | `IMConversationQuery` 的 `Constraint` | -| -------- | ------------------------------------- | -| 等于 | `equalTo` | -| 不等于 | `notEqualTo` | -| 大于 | `greaterThan` | -| 大于等于 | `greaterThanOrEqualTo` | -| 小于 | `lessThan` | -| 小于等于 | `lessThanOrEqualTo` | - -| 逻辑比较 | `ConversationQuery` 方法 | -| -------- | ------------------------ | -| 等于 | `equalTo` | -| 不等于 | `notEqualTo` | -| 大于 | `greaterThan` | -| 大于等于 | `greaterThanOrEqualTo` | -| 小于 | `lessThan` | -| 小于等于 | `lessThanOrEqualTo` | - - - -使用注意:默认查询条件 - -为了防止用户无意间拉取到所有的对话数据,在客户端不指定任何 `where` 条件的时候,`ConversationQuery` 会默认查询包含当前用户的对话。如果客户端添加了任一 `where` 条件,那么 `ConversationQuery` 会忽略默认条件而严格按照指定的条件来查询。如果客户端要查询包含某一个 `clientId` 的对话,那么使用下面的 [数组查询](#数组查询) 语法对 `m` 属性列和 `clientId` 值进行查询即可,不会和默认查询条件冲突。 - -### 正则匹配查询 - -`ConversationsQuery` 也支持在查询条件中使用正则表达式来匹配数据。比如要查询所有 `language` 是中文的对话: - - - -```cs -query.WhereMatches("language", "[\\u4e00-\\u9fa5]"); // language 是中文字符 -``` - -```java -query.whereMatches("language","[\\u4e00-\\u9fa5]"); // language 是中文字符 -``` - -```objc -[query whereKey:@"language" matchesRegex:@"[\\u4e00-\\u9fa5]"]; // language 是中文字符 -``` - -```js -query.matches("language", /[\\u4e00-\\u9fa5]/); // language 是中文字符 -``` - -```swift -try conversationQuery.where("language", .matchedRegularExpression("[\\u4e00-\\u9fa5]", option: nil)) -``` - -```dart -// 暂不支持 -``` - - - -### 字符串查询 - -**前缀查询** 类似于 SQL 的 `LIKE 'keyword%'` 条件。例如查询名字以「教育」开头的对话: - - - -```cs -query.WhereStartsWith("name", "教育"); -``` - -```java -query.whereStartsWith("name","教育"); -``` - -```objc -[query whereKey:@"name" hasPrefix:@"教育"]; -``` - -```js -query.startsWith("name", "教育"); -``` - -```swift -try conversationQuery.where("name", .prefixedBy("教育")) -``` - -```dart -// 暂不支持 -``` - - - -**包含查询** 类似于 SQL 的 `LIKE '%keyword%'` 条件。例如查询名字中包含「教育」的对话: - - - -```cs -query.WhereContains("name", "教育"); -``` - -```java -query.whereContains("name","教育"); -``` - -```objc -[query whereKey:@"name" containsString:@"教育"]; -``` - -```js -query.contains("name", "教育"); -``` - -```swift -try conversationQuery.where("name", .matchedSubstring("教育")) -``` - -```dart -// 暂不支持 -``` - - - -**不包含查询** 则可以使用 [正则匹配查询](#正则匹配查询) 来实现。例如查询名字中不包含「教育」的对话: - - - -```cs -query.WhereMatches("name", "^((?!教育).)* $ "); -``` - -```java -query.whereMatches("name","^((?!教育).)* $ "); -``` - -```objc -[query whereKey:@"name" matchesRegex:@"^((?!教育).)* $ "]; -``` - -```js -var regExp = new RegExp("^((?!教育).)*$", "i"); -query.matches("name", regExp); -``` - -```swift -try conversationQuery.where("name", .matchedRegularExpression("^((?!教育).)* $ ", option: nil)) -``` - -```dart -// 暂不支持 -``` - - - -### 数组查询 - -可以使用 `containsAll`、`containedIn`、`notContainedIn` 来对数组进行查询。例如查询成员中包含「Tom」的对话: - - - -```cs -var members = new List { "Tom" }; -query.WhereContainedIn("m", members); -``` - -```java -query.whereContainedIn("m", Arrays.asList("Tom")); -``` - -```objc -[query whereKey:@"m" containedIn:@[@"Tom"]]; -``` - -```js -query.containedIn("m", ["Tom"]); -``` - -```swift -try conversationQuery.where("m", .containedIn(["Tom"])) -``` - -```dart -// 暂不支持 -``` - - - -### 空值查询 - -空值查询是指查询相关列是否为空值的方法,例如要查询 `lm` 列为空值的对话: - - - -```cs -query.WhereDoesNotExist("lm"); -``` - -```java -query.whereDoesNotExist("lm"); -``` - -```objc -[query whereKeyDoesNotExist:@"lm"]; -``` - -```js -query.doesNotExist("lm"); -``` - -```swift -try conversationQuery.where("lm", .notExisted) -``` - -```dart -// 暂不支持 -``` - - - -反过来,如果要查询 `lm` 列不为空的对话,则替换为如下条件即可: - - - -```cs -query.WhereExists("lm"); -``` - -```java -query.whereExists("lm"); -``` - -```objc -[query whereKeyExists:@"lm"]; -``` - -```js -query.exists("lm"); -``` - -```swift -try conversationQuery.where("lm", .existed) -``` - -```dart -// 暂不支持 -``` - - - -**注意,系统消息(服务号)没有 lm 列,可以替换为 updatedAt。** - -### 组合查询 - -查询年龄小于 18 岁,并且关键字包含「教育」的对话: - - - -```cs -query.WhereContains("keywords", "教育") - .WhereLessThan("age", 18); -``` - -```java -query.whereContains("keywords", "教育"); -query.whereLessThan("age", 18); -``` - -```objc -[query whereKey:@"keywords" containsString:@"教育"]; -[query whereKey:@"age" lessThan:@(18)]; -``` - -```js -query.contains("keywords", "教育").lessThan("age", 18); -``` - -```swift -try conversationQuery.where("keywords", .matchedSubstring("教育")) -try conversationQuery.where("age", .lessThan(18)) -``` - -```dart -// 暂不支持 -``` - - - -另外一种组合的方式是,两个查询采用 `or` 或者 `and` 的方式构建一个新的查询。 - -查询年龄小于 18 或者关键字包含「教育」的对话: - - - -```cs -// 暂不支持 -``` - -```java -LCIMConversationsQuery ageQuery = tom.getConversationsQuery(); -ageQuery.whereLessThan('age', 18); - -LCIMConversationsQuery keywordsQuery = tom.getConversationsQuery(); -keywordsQuery.whereContains('keywords', '教育'); - -LCIMConversationsQuery query = LCIMConversationsQuery.or(Arrays.asList(priorityQuery, statusQuery)); -``` - -```objc -LCIMConversationQuery *ageQuery = [tom conversationQuery]; -[ageQuery whereKey:@"age" greaterThan:@(18)]; - -LCIMConversationQuery *keywordsQuery = [tom conversationQuery]; -[keywordsQuery whereKey:@"keywords" containsString:@"教育"]; - -LCIMConversationQuery *query = [LCIMConversationQuery orQueryWithSubqueries:[NSArray arrayWithObjects:ageQuery,keywordsQuery,nil]]; -``` - -```js -// JavaScript SDK 暂不支持 -``` - -```swift -do { - let ageQuery = tom.conversationQuery - try ageQuery.where("age", .greaterThan(18)) - - let keywordsQuery = tom.conversationQuery - try keywordsQuery.where("keywords", .matchedSubstring("教育")) - - let conversationQuery = try ageQuery.or(keywordsQuery) -} catch { - print(error) -} -``` - -```dart -// 暂不支持 -``` - - - -### 结果排序 - -可以指定查询结果按照部分属性值的升序或降序来返回。例如: - - - -```cs -query.OrderByDescending("createdAt"); -``` - -```java -query.orderByDescending("createdAt"); -``` - -```objc -[query orderByDescending:@"createdAt"]; -``` - -```js -// 对查询结果按照 name 升序,然后按照创建时间降序排序 -query.addAscending("name").addDescending("createdAt"); -``` - -```swift -try conversationQuery.where("createdAt", .descending) -``` - -```dart -// 暂不支持 -``` - - - -### 不带成员信息的精简模式 - -普通对话最多可以容纳 500 个成员,在有些业务逻辑不需要对话的成员列表的情况下,可以使用「精简模式」进行查询,这样返回结果中不会包含成员列表(`members` 字段为空数组),有助于提升应用的性能同时减少流量消耗。 - - - -```cs -query.Compact = true; -``` - -```java -query.setCompact(true); -``` - -```objc -query.option = LCIMConversationQueryOptionCompact; -``` - -```js -query.compact(true); -``` - -```swift -conversationQuery.options = [.notContainMembers] -``` - -```dart -query.excludeMembers = true; -``` - - - -### 让查询结果附带一条最新消息 - -对于一个聊天应用,一个典型的需求是在对话的列表界面显示最后一条消息,默认情况下,针对对话的查询结果是不带最后一条消息的,需要单独打开相关选项: - - - -```cs -query.WithLastMessageRefreshed = true; -``` - -```java -query.setWithLastMessagesRefreshed(true); -``` - -```objc -query.option = LCIMConversationQueryOptionWithMessage; -``` - -```js -// withLastMessagesRefreshed 方法可以指定让查询结果带上最后一条消息 -query.withLastMessagesRefreshed(true); -``` - -```swift -conversationQuery.options = [.containLastMessage] -``` - -```dart -query.includeLastMessage = true; -``` - - - -需要注意的是,这个选项真正的意义是「刷新对话的最后一条消息」,这意味着由于 SDK 缓存机制的存在,将这个选项设置为 `false` 查询得到的对话也还是有可能会存在最后一条消息的。 - -### 查询缓存 - - -<> - -.NET SDK 暂不支持缓存功能。 - - -<> - -通常,将查询结果缓存到磁盘上是一种行之有效的方法,这样就算设备离线,应用刚刚打开,网络请求尚未完成时,数据也能显示出来。或者为了节省用户流量,在应用打开的第一次查询走网络,之后的查询可优先走本地缓存。 - -值得注意的是,默认的策略是先走本地缓存的再走网络的,缓存时间是一小时。`LCIMConversationsQuery` 中有如下方法: - -```java -// 设置 LCIMConversationsQuery 的查询策略 -public void setQueryPolicy(LCQuery.CachePolicy policy); -``` - -有时你希望先走网络查询,发生网络错误的时候,再从本地查询,可以这样: - -```java -LCIMConversationsQuery query = client.getConversationsQuery(); -query.setQueryPolicy(LCQuery.CachePolicy.NETWORK_ELSE_CACHE); -query.findInBackground(new LCIMConversationQueryCallback() { - @Override - public void done(List conversations, LCIMException e) { - - } -}); -``` - - -<> - -通常,将查询结果缓存到磁盘上是一种行之有效的方法,这样就算设备离线,应用刚刚打开,网络请求尚未完成时,数据也能显示出来。或者为了节省用户流量,在应用打开的第一次查询走网络,之后的查询可优先走本地缓存。 - -值得注意的是,默认的策略是先走本地缓存的再走网络的,缓存时间是一小时。`LCIMConversationQuery` 中有如下方法: - -```objc -// 设置缓存策略,默认是 kLCCachePolicyCacheElseNetwork -@property (nonatomic) LCCachePolicy cachePolicy; - -// 设置缓存的过期时间,默认是 1 小时(1 * 60 * 60) -@property (nonatomic) NSTimeInterval cacheMaxAge; -``` - -有时你希望先走网络查询,发生网络错误的时候,再从本地查询,可以这样: - -```objc -LCIMConversationQuery *query = [client conversationQuery]; -query.cachePolicy = kLCCachePolicyNetworkElseCache; -[query findConversationsWithCallback:^(NSArray *objects, NSError *error) { - -}]; -``` - -各种查询缓存策略的行为可以参考[数据存储指南](/sdk/storage/guide/dotnet/)的《缓存查询》一节。 - - -<> - -JavaScript SDK 会对按照对话 ID 对对话进行内存字典缓存,但不会进行持久化的缓存。 - - -<> - -Swift SDK 提供了会话的缓存功能,包括内存缓存和持久化缓存。 - -会话的内存缓存: - -```swift -client.getCachedConversation(ID: "CONVERSATION_ID") { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } -} - -client.removeCachedConversation(IDs: ["CONVERSATION_ID"]) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -会话的持久化缓存。**注意,使用「查询持久存储会话」以及「删除持久存储会话」的功能前,需调用 `prepareLocalStorage` 方法且回调结果为成功;`prepareLocalStorage` 方法只需要调用一次(返回成功),一般在 `IMClient.init()` 和 `IMClient.open()` 之间调用**: - -```swift -// Switch for Local Storage of IM Client -do { - // Client init with Local Storage feature - let clientWithLocalStorage = try IMClient(ID: "CLIENT_ID") - - // Client init without Local Storage feature - var options = IMClient.Options.default - options.remove(.usingLocalStorage) - let clientWithoutLocalStorage = try IMClient(ID: "CLIENT_ID", options: options) -} catch { - print(error) -} - -// Preparation for Local Storage of IM Client -do { - try client.prepareLocalStorage { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} - -// Get and Load Stored Conversations to Memory -do { - try client.getAndLoadStoredConversations(completion: { (result) in - switch result { - case .success(value: let conversations): - print(conversations) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} - -// Delete Stored Conversations and Messages belong to them -do { - try client.deleteStoredConversationAndMessages(IDs: ["CONVERSATION_ID"], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -注意: - -1. 聊天室和临时会话没有本地缓存机制。 -2. 会话有内存缓存和持久化(磁盘)缓存。消息只有持久化缓存,且只支持消息查询的结果(查询结果小于 3 时不缓存)。 - - -<> - -Flutter SDK 暂不支持缓存功能。 - - - - - -### 性能优化建议 - -`Conversation` 数据是存储在云端数据库中的,与存储服务中的对象查询类似,我们需要尽可能利用索引来提升查询效率,这里有一些优化查询的建议: - -- `Conversation` 的 `objectId`、`updatedAt`、`createdAt` 等属性上是默认建了索引的,所以通过这些条件来查询会比较快。 -- 虽然 `skip` 搭配 `limit` 的方式可以翻页,但是在结果集较大的时候不建议使用,因为数据库端计算翻页距离是一个非常低效的操作,取而代之的是尽量通过 `updatedAt` 或 `lastMessageAt` 等属性来限定返回结果集大小,并以此进行翻页。 -- 使用 `m` 列的 `contains` 查询来查找包含某人的对话时,也尽量使用默认的 `limit` 大小 10,再配合 `updatedAt` 或者 `lastMessageAt` 来做条件约束,性能会提升较大。 -- 整个应用对话如果数量太多,可以考虑在云引擎封装一个云函数,用定时任务启动之后,周期性地做一些清理,例如可以归档或删除一些不活跃的对话。 - -## 聊天记录查询 - -消息记录默认会在云端保存 **180** 天,开发者可以通过额外付费来延长这一期限(有需要的用户请提工单联系技术支持),也可以通过 REST API 将聊天记录同步到自己的服务器上。 - -SDK 提供了多种方式来拉取历史记录,iOS 和 Android SDK 还提供了内置的消息缓存机制,以减少客户端对云端消息记录的查询次数,并且在设备离线情况下,也能展示出部分数据保障产品体验不会中断。 - -### 从新到旧获取对话的消息记录 - -在终端用户进入一个对话的时候,最常见的需求就是由新到旧、以翻页的方式拉取并展示历史消息,这可以通过如下代码实现: - - - -```cs -// limit 取值范围 1~100,默认 20 -var messages = await conversation.QueryMessages(limit: 10); -foreach (var message in messages) { - if (message is LCIMTextMessage textMessage) { - - } -} -``` - -```java -// limit 取值范围 1~100,如调用 queryMessages 时不带 limit 参数,默认获取 20 条消息记录 -int limit = 10; -conv.queryMessages(limit, new LCIMMessagesQueryCallback() { - @Override - public void done(List messages, LCIMException e) { - if (e == null) { - // 成功获取最新 10 条消息记录 - } - } -}); -``` - -```objc -// 查询对话中最后 10 条消息,limit 取值范围 1~100,值为 0 时获取 20 条消息记录(使用服务端默认值) -[conversation queryMessagesWithLimit:10 callback:^(NSArray *objects, NSError *error) { - NSLog(@"查询成功!"); -}]; -``` - -```js -conversation - .queryMessages({ - limit: 10, // limit 取值范围 1~100,默认 20 - }) - .then(function (messages) { - // 最新的十条消息,按时间增序排列 - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - try conversation.queryMessage(limit: 10) { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// limit 取值范围 1~100,如调用 queryMessage 时不带 limit 参数,默认获取 20 条消息记录 -try { - List messages = await conversation.queryMessage( - limit: 10, - ); -} catch (e) { - print(e); -} -``` - - - -`queryMessage` 接口也是支持翻页的。即时通讯云端通过消息的 `messageId` 和发送时间戳来唯一定位一条消息,因此要从某条消息起拉取后续的 N 条记录,只需要指定起始消息的 `messageId` 和发送时间戳作为锚定就可以了,示例代码如下: - - - -```cs -// limit 取值范围 1~1000,默认 100 -var messages = await conversation.QueryMessages(limit: 10); -var oldestMessage = messages[0]; -var start = new LCIMMessageQueryEndpoint { - MessageId = oldestMessage.Id, - SentTimestamp = oldestMessage.SentTimestamp -}; -var messagesInPage = await conversation.QueryMessages(start: start); -``` - -```java -// limit 取值范围 1~1000,默认 100 -conv.queryMessages(10, new LCIMMessagesQueryCallback() { - @Override - public void done(List messages, LCIMException e) { - if (e == null) { - // 成功获取最新 10 条消息记录 - // 返回的消息一定是时间增序排列,也就是最早的消息一定是第一个 - LCIMMessage oldestMessage = messages.get(0); - - conv.queryMessages(oldestMessage.getMessageId(), oldestMessage.getTimestamp(),20, - new LCIMMessagesQueryCallback(){ - @Override - public void done(List messagesInPage,LCIMException e){ - if(e== null){ - // 查询成功返回 - Log.d("Tom & Jerry", "got " + messagesInPage.size()+" messages"); - } - } - }); - } - } -}); -``` - -```objc -// 查询对话中最后 10 条消息 -[conversation queryMessagesWithLimit:10 callback:^(NSArray *messages, NSError *error) { - NSLog(@"第一次查询成功!"); - // 以第一页的最早的消息作为开始,继续向前拉取消息 - LCIMMessage *oldestMessage = [messages firstObject]; - [conversation queryMessagesBeforeId:oldestMessage.messageId timestamp:oldestMessage.sendTimestamp limit:10 callback:^(NSArray *messagesInPage, NSError *error) { - NSLog(@"第二次查询成功!"); - }]; -}]; -``` - -```js -// JS SDK 通过迭代器隐藏了翻页的实现细节,开发者通过不断的调用 next 方法即可获得后续数据。 -// 创建一个迭代器,每次获取 10 条历史消息 -var messageIterator = conversation.createMessagesIterator({ limit: 10 }); -// 第一次调用 next 方法,获得前 10 条消息,还有更多消息,done 为 false -messageIterator - .next() - .then(function (result) { - // result: { - // value: [message1, ..., message10], - // done: false, - // } - }) - .catch(console.error.bind(console)); -// 第二次调用 next 方法,获得第 11~20 条消息,还有更多消息,done 为 false -// 迭代器内部会记录起始消息的数据,无需开发者显示指定 -messageIterator - .next() - .then(function (result) { - // result: { - // value: [message11, ..., message20], - // done: false, - // } - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - let start = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID", - sentTimestamp: 31415926, - isClosed: false - ) - try conversation.queryMessage(start: start, limit: 10, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -List messages; -try { -// 第一次查询成功 - messages = await conversation.queryMessage( - limit: 10, - ); -} catch (e) { - print(e); -} - -try { - // 返回的消息一定是时间增序排列,也就是最早的消息一定是第一个 - Message oldMessage = messages.first; - // 以第一页的最早的消息作为开始,继续向前拉取消息 - List messages2 = await conversation.queryMessage( - startTimestamp: oldMessage.sentTimestamp, - startMessageID: oldMessage.id, - startClosed: true, - limit: 10, - ); -} catch (e) { - print(e); -} -``` - - - -### 按照消息类型获取 - -除了按照时间先后顺序拉取历史消息之外,即时通讯服务云端也支持按照消息的类型来拉取历史消息,这一功能可能对某些产品来说非常有用,例如我们需要展现某一个聊天群组里面所有的图像。 - -`queryMessage` 接口还支持指定特殊的消息类型,其示例代码如下: - - - -```cs -// 传入泛型参数,SDK 会自动读取类型的信息发送给服务端,用作筛选目标类型的消息 -var imageMessages = await conversation.QueryMessages(messageType: -2); -``` - -```java -int msgType = LCIMMessageType.IMAGE_MESSAGE_TYPE; -conversation.queryMessagesByType(msgType, limit, new LCIMMessagesQueryCallback() { - @Override - public void done(List messages, LCIMException e){ - } -}); -``` - -```objc -[conversation queryMediaMessagesFromServerWithType:LCIMMessageMediaTypeImage limit:10 fromMessageId:nil fromTimestamp:0 callback:^(NSArray *messages, NSError *error) { - if (!error) { - NSLog(@"查询成功!"); - } -}]; -``` - -```js -conversation - .queryMessages({ type: ImageMessage.TYPE }) - .then((messages) => { - console.log(messages); - }) - .catch(console.error); -``` - -```swift -do { - try conversation.queryMessage(limit: 10, type: IMTextMessage.messageType, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - List messages = await conversation.queryMessage(type: -2); -} catch (e) { - print(e); -} -``` - - - -如要获取更多图像消息,可以效仿前一章节中的示例代码,继续翻页查询即可。 - -### 从旧到新反向获取历史消息 - -即时通讯云端支持的历史消息查询方式是非常多的,除了上面列举的两个最常见需求之外,还可以支持按照由旧到新的方向进行查询。如下代码演示从对话创建的时间点开始,从前往后查询消息记录: - - - -```cs -var earliestMessages = await conversation.QueryMessages(direction: LCIMMessageQueryDirection.OldToNew); -``` - -```java -LCIMMessageInterval interval = new LCIMMessageInterval(null, null); -conversation.queryMessages(interval, DirectionFromOldToNew, limit, - new LCIMMessagesQueryCallback(){ - public void done(List messages, LCIMException exception) { - // 处理结果 - } -}); -``` - -```objc -[conversation queryMessagesInInterval:nil direction:LCIMMessageQueryDirectionFromOldToNew limit:20 callback:^(NSArray * _Nullable messages, NSError * _Nullable error) { - if (messages.count) { - // 处理结果 - } -}]; -``` - -```js -var { MessageQueryDirection } = require('leancloud-realtime'); -conversation.queryMessages({ - direction: MessageQueryDirection.OLD_TO_NEW, -}).then(function(messages) { - // 处理结果 -}.catch(function(error) { - // 处理异常 -}); -``` - -```swift -do { - try conversation.queryMessage(direction: .oldToNew, limit: 10, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - List messages = await conversation.queryMessage( - direction: MessageQueryDirection.oldToNew, - ); -} catch (e) { - print(e); -} -``` - - - -这种情况下要实现翻页,接口会稍微复杂一点,请继续阅读下一节。 - -### 从某一时间戳往某一方向查询 - -即时通讯服务云端支持以某一条消息的 ID 和时间戳为准,往一个方向查: - -- 从新到旧:以某一条消息为基准,查询它 **之前** 产生的消息 -- 从旧到新:以某一条消息为基准,查询它 **之后** 产生的消息 - -这样我们就可以在不同方向上实现消息翻页了。 - - - -```cs -var earliestMessages = await conversation.QueryMessages(direction: LCIMMessageQueryDirection.OldToNew, limit: 1); -// 获取 earliestMessages.Last() 之后的消息 -var lastMessage = earliestMessages.Last(); -var start = new LCIMMessageQueryEndpoint { - MessageId = lastMessage.Id, - SentTimestamp = lastMessage.SentTimestamp -}; -var nextPageMessages = await conversation.QueryMessages(start: start); -``` - -```java -LCIMMessageIntervalBound start = LCIMMessageInterval.createBound(messageId, timestamp, false); -LCIMMessageInterval interval = new LCIMMessageInterval(start, null); -LCIMMessageQueryDirection direction; -conversation.queryMessages(interval, direction, limit, - new LCIMMessagesQueryCallback(){ - public void done(List messages, LCIMException exception) { - // 处理结果 - } -}); -``` - -```objc -LCIMMessageIntervalBound *start = [[LCIMMessageIntervalBound alloc] initWithMessageId:nil timestamp:timestamp closed:false]; -LCIMMessageInterval *interval = [[LCIMMessageInterval alloc] initWithStartIntervalBound:start endIntervalBound:nil]; -[conversation queryMessagesInInterval:interval direction:direction limit:20 callback:^(NSArray * _Nullable messages, NSError * _Nullable error) { - if (messages.count) { - // 处理结果 - } -}]; -``` - -```js -var { MessageQueryDirection } = require('leancloud-realtime'); -conversation.queryMessages({ - startTime: timestamp, - startMessageId: messageId, -startClosed: false, - direction: MessageQueryDirection.OLD_TO_NEW, -}).then(function(messages) { - // 处理结果 -}.catch(function(error) { - // 处理异常 -}); -``` - -```swift -do { - let start = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID", - sentTimestamp: 31415926, - isClosed: true - ) - try conversation.queryMessage(start: start, direction: .oldToNew, limit: 10, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - List messages = await conversation.queryMessage( - startTimestamp: textMessage.sentTimestamp, - startMessageID: textMessage.id, - startClosed: true, - direction: MessageQueryDirection.oldToNew, - limit: 10, - ); -} catch (e) { - print(e); -} -``` - - - -### 获取指定区间内的消息 - -除了顺序查找之外,我们也支持获取特定时间区间内的消息。假设已知 2 条消息,这 2 条消息以较早的一条为起始点,而较晚的一条为终点,这个区间内产生的消息可以用如下方式查询: - -注意:**每次查询也有 100 条限制,如果想要查询区间内所有产生的消息,替换区间起始点的参数即可。** - - - -```cs -var earliestMessage = await conversation.QueryMessages(direction: LCIMMessageQueryDirection.OldToNew, limit: 1); -var latestMessage = await conversation.QueryMessages(limit: 1); -var start = new LCIMMessageQueryEndpoint { - MessageId = earliestMessage[0].Id -}; -var end = new LCIMMessageQueryEndpoint { - MessageId = latestMessage[0].Id -}; -// messagesInInterval 最多可包含 100 条消息 -var messagesInInterval = await conversation.QueryMessages(start: start, end: end); -``` - -```java -LCIMMessageIntervalBound start = LCIMMessageInterval.createBound(messageId, timestamp, false); -LCIMMessageIntervalBound end = LCIMMessageInterval.createBound(endMessageId, endTimestamp, false); -LCIMMessageInterval interval = new LCIMMessageInterval(start, end); -LCIMMessageQueryDirection direction; -conversation.queryMessages(interval, direction, limit, - new LCIMMessagesQueryCallback(){ - public void done(List messages, LCIMException exception) { - // 处理结果 - } -}); -``` - -```objc -LCIMMessageIntervalBound *start = [[LCIMMessageIntervalBound alloc] initWithMessageId:nil timestamp:startTimestamp closed:false]; -LCIMMessageIntervalBound *end = [[LCIMMessageIntervalBound alloc] initWithMessageId:nil timestamp:endTimestamp closed:false]; -LCIMMessageInterval *interval = [[LCIMMessageInterval alloc] initWithStartIntervalBound:start endIntervalBound:end]; -[conversation queryMessagesInInterval:interval direction:direction limit:100 callback:^(NSArray * _Nullable messages, NSError * _Nullable error) { - if (messages.count) { - // 处理结果 - } -}]; -``` - -```js -conversation.queryMessages({ - startTime: timestamp, - startMessageId: messageId, - endTime: endTimestamp, - endMessageId: endMessageId, -}).then(function(messages) { - // 处理结果 -}.catch(function(error) { - // 处理异常 -}); -``` - -```swift -do { - let start = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID_1", - sentTimestamp: 31415926, - isClosed: true - ) - let end = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID_2", - sentTimestamp: 31415900, - isClosed: true - ) - try conversation.queryMessage(start: start, end: end, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - List messages = await conversation.queryMessage( - startTimestamp: textMessage.sentTimestamp, - startMessageID: textMessage.id, - startClosed: true, - endTimestamp: fileMessage.sentTimestamp, - endMessageID: fileMessage.id, - endClosed: true, - ); -} catch (e) { - print(e); -} -``` - - - -### 客户端消息缓存 - -iOS 和 Android SDK 针对移动设备的特殊性,实现了客户端消息的缓存。开发者无需进行特殊设置,只要接收或者查询到的新消息,默认都会进入被缓存起来,该机制给开发者提供了如下便利: - -1. 客户端可以在未联网的情况下进入对话列表之后,可以获取聊天记录,提升用户体验 -2. 减少查询的次数和流量的消耗 -3. 极大地提升了消息记录的查询速度和性能 - -客户端缓存是默认开启的,如果开发者有特殊的需求,SDK 也支持关闭缓存功能。例如有些产品在应用层进行了统一的消息缓存,无需 SDK 层再进行冗余存储,可以通过如下接口来关闭消息缓存: - - - -```cs -// 暂不支持 -``` - -```java -// 需要在调用 LCIMClient.open(callback) 函数之前设置,关闭历史消息缓存开关。 -LCIMOptions.getGlobalOptions().setMessageQueryCacheEnabled(false); -``` - -```objc -// 需要在调用 [avimClient openWithCallback:callback] 函数之前设置,关闭历史消息缓存开关。 -avimClient.messageQueryCacheEnabled = false; -``` - -```js -// 暂不支持 -``` - -```swift -// Switch for Local Storage of IM Client -do { - // Client init with Local Storage feature - let clientWithLocalStorage = try IMClient(ID: "CLIENT_ID") - - // Client init without Local Storage feature - var options = IMClient.Options.default - options.remove(.usingLocalStorage) - let clientWithoutLocalStorage = try IMClient(ID: "CLIENT_ID", options: options) -} catch { - print(error) -} - -// Message Query Policy -enum MessageQueryPolicy { - case `default` - case onlyNetwork - case onlyCache - case cacheThenNetwork -} - -do { - try conversation.queryMessage(policy: .default, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -// 暂不支持 -``` - - - -## 用户退出与网络状态变化 - -### 用户退出即时通讯服务 - -如果产品层面设计了用户退出登录或者切换账号的接口,对于即时通讯服务来说,也是需要完全注销当前用户的登录状态的。在 SDK 中,开发者可以通过调用 `LCIMClient` 的 `close` 系列方法完成即时通讯服务的「退出」: - - - -```cs -await tom.Close(); -``` - -```java -tom.close(new LCIMClientCallback(){ - @Override - public void done(LCIMClient client,LCIMException e){ - if(e==null){ - // 登出成功 - } - } -}); -``` - -```objc -[tom closeWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"退出即时通讯服务"); - } -}]; -``` - -```js -tom - .close() - .then(function () { - console.log("Tom 退出登录"); - }) - .catch(console.error.bind(console)); -``` - -```swift -tom.close { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await tom.close(); -``` - - - -调用该接口之后,客户端就与即时通讯服务云端断开连接了,从云端查询前一 `clientId` 的状态,会显示「离线」状态。 - -### 客户端事件与网络状态响应 - -即时通讯服务与终端设备的网络连接状态休戚相关,如果网络中断,那么所有的消息收发和对话操作都会失败,这时候产品层面需要在 UI 上给予用户足够的提示,以免影响使用体验。 - -我们的 SDK 内部和即时通讯云端会维持一个「心跳」机制,能够及时感知到客户端的网络变化,同时将底层网络变化事件通知到应用层。具体来讲,当网络连接出现中断、恢复等状态变化时,SDK 会派发以下事件: - - -<> - -`LCIMClient` 上会有如下事件通知: - -- `OnPaused` 指网络连接断开事件发生,此时聊天服务不可用 -- `OnResume` 指网络连接恢复正常,此时聊天服务变得可用 -- `OnClose` 指连接关闭,且不会自动重连 - - -<> - -`LCIMClientEventHandler` 上会有如下事件通知: - -- `onConnectionPaused()` 指网络连接断开事件发生,此时聊天服务不可用。 -- `onConnectionResume()` 指网络连接恢复正常,此时聊天服务变得可用。 -- `onClientOffline()` 指单点登录被踢下线的事件。 - - -<> - -在 `LCIMClientDelegate` 里,可以接收到如下所示的事件通知: - -- `imClientResumed`:连接自动恢复了 -- `imClientPaused`:连接断开了;该事件被触发的常见场景:网络无法访问、应用进入后台 -- `imClientResuming`:正在重新建立连接 -- `imClientClosed`:连接关闭,且不会自动重连;该事件被触发的常见场景:单设备登录冲突、后台主动把该 client 下线 - -```objc -- (void)imClientResumed:(LCIMClient *)imClient -{ - -} - -- (void)imClientResuming:(LCIMClient *)imClient -{ - -} - -- (void)imClientPaused:(LCIMClient *)imClient error:(NSError * _Nullable)error -{ - -} - -- (void)imClientClosed:(LCIMClient *)imClient error:(NSError * _Nullable)error -{ - -} -``` - - -<> - -- `DISCONNECT`:与服务端连接断开,此时聊天服务不可用。 -- `OFFLINE`:网络不可用。 -- `ONLINE`:网络恢复。 -- `SCHEDULE`:计划在一段时间后尝试重连,此时聊天服务仍不可用。 -- `RETRY`:正在重连。 -- `RECONNECT`:与服务端连接恢复,此时聊天服务可用。 - -```js -var { Event } = require("leancloud-realtime"); - -realtime.on(Event.DISCONNECT, function () { - console.log("服务器连接已断开"); -}); -realtime.on(Event.OFFLINE, function () { - console.log("离线(网络连接已断开)"); -}); -realtime.on(Event.ONLINE, function () { - console.log("已恢复在线"); -}); -realtime.on(Event.SCHEDULE, function (attempt, delay) { - console.log(delay + " ms 后进行第 " + (attempt + 1) + " 次重连"); -}); -realtime.on(Event.RETRY, function (attempt) { - console.log("正在进行第 " + (attempt + 1) + " 次重连"); -}); -realtime.on(Event.RECONNECT, function () { - console.log("与服务端连接恢复"); -}); -``` - - -<> - -在 `IMClientDelegate` 的 `IMClientDelegate.client(_:event:)` 函数里,可以接收到如下所示的事件通知: - -- `sessionDidOpen`:连接自动恢复了 -- `sessionDidPause`:连接断开了;该事件被触发的常见场景:网络无法访问、应用进入后台 -- `sessionDidResume`:正在重新建立连接 -- `sessionDidClose`:连接关闭,且不会自动重连;该事件被触发的常见场景:单设备登录冲突、后台主动把该 client 下线 - -```swift -func client(_ client: IMClient, event: IMClientEvent) { - switch event { - case .sessionDidOpen: - break - case .sessionDidPause(error: let error): - print(error) - case .sessionDidResume: - break - case .sessionDidClose(error: let error): - print(error) - } -} -``` - - -<> - -`Client` 有如下事件通知: - -- `onOpened` 用户登录即时通信的服务。 -- `onClosed` 用户退出即时通信服务。 -- `onResuming` 指网络正在尝试重连,此时聊天服务不可用。 -- `onDisconnected` 指网络连接断开事件发生,此时聊天服务不可用。 - - - - - -## 其他开发建议 - -### 如何根据活跃度来展示对话列表 - -不管是当前用户参与的「对话」列表,还是全局热门的开放聊天室列表展示出来了,我们下一步要考虑的就是如何把最活跃的对话展示在前面,这里我们把「活跃」定义为最近有新消息发出来。我们希望有最新消息的对话可以展示在对话列表的最前面,甚至可以把最新的那条消息也附带显示出来,这时候该怎么实现呢? - -我们专门为 `LCIMConversation` 增加了一个动态的属性 `lastMessageAt`(对应 `_Conversation` 表里的 `lm` 字段),记录了对话中最后一条消息到达即时通讯云端的时间戳,这一数字是服务器端的时间(精确到秒),所以不用担心客户端时间对结果造成影响。另外,`LCIMConversation` 还提供了一个方法可以直接获取最新的一条消息。这样在界面展现的时候,开发者就可以自己决定展示内容与顺序了。 - -### 自动重连 - -如果开发者没有明确调用退出登录的接口,但是客户端网络存在抖动或者切换(对于移动网络来说,这是比较常见的情况),我们 iOS 和 Android SDK 默认内置了断线重连的功能,会在网络恢复的时候自动建立连接,此时 `IMClient` 的网络状态可以通过底层的网络状态响应接口得到回调。 - -### 更多「对话」类型 - -即时通讯服务提供的功能就是让一个客户端与其他客户端进行在线的消息互发,对应不同的使用场景,除了前两章节介绍的 [一对一单聊](#一对一单聊) 和 [多人群聊](#多人群聊) 之外,我们也支持其他形式的「对话」模型: - -- 开放聊天室,例如直播中的弹幕聊天室,它与普通的「多人群聊」的主要差别是允许的成员人数以及消息到达的保证程度不一样。有兴趣的开发者可以参考[即时通讯开发指南第三篇](/sdk/im/guide/senior/)的《玩转直播聊天室》一节。 - -- 临时对话,例如客服系统中用户和客服人员之间建立的临时通道,它与普通的「一对一单聊」的主要差别在于对话总是临时创建并且不会长期存在,在提升实现便利性的同时,还能降低服务使用成本(能有效减少存储空间方面的花费)。有兴趣的开发者可以参考[即时通讯开发指南第三篇](/sdk/im/guide/senior/)的《使用临时对话》一节。 - -- 系统对话,例如在微信里面常见的公众号/服务号,系统全局的广播账号,与普通「多人群聊」的主要差别,在于「服务号」是以订阅的形式加入的,也没有成员限制,并且订阅用户和服务号的消息交互是一对一的,一个用户的上行消息不会群发给其他订阅用户。有兴趣的开发者可以参考[即时通讯开发指南第四篇](/sdk/im/guide/systemconv/)《「系统对话」的使用》一节。 - -## 进一步阅读 - -- 《即时通讯开发指南》第二篇[消息收发的更多方式,离线推送与消息同步,多设备登录](/sdk/im/guide/intermediate/) -- 《即时通讯开发指南》第三篇[安全与签名、玩转聊天室和临时对话](/sdk/im/guide/senior/) -- 《即时通讯开发指南》第四篇[详解消息 hook 与系统对话](/sdk/im/guide/systemconv/) diff --git a/leancloud/docs/sdk/im/guide/intermediate.mdx b/leancloud/docs/sdk/im/guide/intermediate.mdx deleted file mode 100644 index 86ea4415c..000000000 --- a/leancloud/docs/sdk/im/guide/intermediate.mdx +++ /dev/null @@ -1,2268 +0,0 @@ ---- -title: 二,消息收发的更多方式,离线推送与消息同步,多设备登录 -sidebar_label: 离线消息 -sidebar_position: 2 -slug: /sdk/im/guide/intermediate ---- - - - - - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; - -## 本章导读 - -在前一章[从简单的单聊、群聊、收发图文消息开始](/sdk/im/guide/beginner/)里面,我们说明了如何在产品中增加一个基本的单聊/群聊页面,并响应服务端实时事件通知。接下来,在本篇文档中我们会讲解如何实现一些更复杂的业务需求,例如: - -- 支持消息被接收和被阅读的状态回执,实现「Ding」一下的效果 -- 发送带有成员提醒的消息(@ 某人),在超多用户群聊的场合提升目标用户的响应积极性 -- 支持消息的撤回和修改 -- 解决成员离线状态下的推送通知与重新上线后的消息同步,确保不丢消息 -- 支持多设备登录,或者强制用户单点登录 -- 扩展新的消息类型 - -## 消息收发的更多方式 - -在一个偏重工作协作或社交沟通的产品里,除了简单的消息收发之外,我们还会碰到更多需求,例如: - -- 在消息中能否直接提醒某人,类似于很多 IM 工具中提供的 @ 消息,这样接收方能更明确地知道哪些消息需要及时响应; -- 消息发出去之后才发现内容不对,这时候能否修改或者撤回? -- 除了普通的聊天内容之外,是否支持发送类似于「XX 正在输入」这样的状态消息? -- 消息是否被其他人接收、读取,这样的状态能否反馈给发送者? -- 客户端掉线一段时间之后,可能会错过一批消息,能否提醒并同步一下未读消息? - -等等,所有这些需求都可以通过即时通讯服务解决,下面我们来逐一看看具体的做法。 - -### @ 成员提醒消息 - -在一些多人聊天群里面,因为消息量较大,很容易就导致某一条重要的消息没有被目标用户看到就被刷下去了,所以在消息中「@成员」是一种提醒接收者注意的有效办法。在微信这样的聊天工具里面,甚至会在对话列表页对有提醒的消息进行特别展示,用来通知消息目标高优先级查看和处理。 - -一般提醒消息都使用「@ + 人名」来表示目标用户,但是这里「人名」是一个由应用层决定的属性,可能有的产品使用全名,有的使用昵称,并且这个名字和即时通讯服务里面标识一个用户使用的 `clientId` 可能根本不一样(毕竟一个是给人看的,一个是给机器读的)。使用「人名」来圈定用户,也存在一种例外,就是聊天群组里面的用户名是可以改变的,如果消息发送的时候「王五」还叫「王五」,等发送出来之后他恰好同步改成了「王大麻子」,这时候接收方的处理就比较麻烦了。还有第三个原因,就是「提醒全部成员」的表示方式,可能「@all」、「@group」、「@所有人」都会被选择,这是一个完全依赖应用层 UI 的选项。 - -所以「@ 成员」提醒消息并不能简单在文本消息中加入「@ + 人名」,解决方案是给普通消息(`LCIMMessage`)增加两个额外的属性: - -- `mentionList`,是一个字符串的数组,用来单独记录被提醒的 `clientId` 列表; -- `mentionAll`,是一个 `Bool` 型的标志位,用来表示是否要提醒全部成员。 - -带有提醒信息的消息,有可能既有提醒全部成员的标志,也还单独设置了 `mentionList`,这由应用层去控制。发送方在发送「@ 成员」提醒消息的时候,如何输入、选择成员名称,这是业务方 UI 层面需要解决的问题,即时通讯 SDK 不关心其实现逻辑,SDK 只要求开发者在发送一条「@ 成员」消息的时候,调用 `mentionList` 和 `mentionAll` 的 setter 方法,设置正确的成员列表即可。示例代码如下: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("@Tom 早点回家") { - MentionIdList = new string[] { "Tom" } -}; -await conversation.Send(textMessage); -``` - -```java -String content = "@Tom 早点回家"; -LCIMTextMessage message = new LCIMTextMessage(); -message.setText(content); -List list = new ArrayList<>(); // 部分用户的 mention list,你可以像下面代码这样来填充 -list.add("Tom"); -message.setMentionList(list); -imConversation.sendMessage(message, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` - -```objc -LCIMMessage *message = [LCIMTextMessage messageWithText:@"@Tom 早点回家" attributes:nil]; -message.mentionList = @[@"Tom"]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError * _Nullable error) { - /* 一条提及 Tom 的消息已发出 */ -}]; -``` - -```js -const message = new TextMessage(`@Tom 早点回家`).setMentionList(["Tom"]); -conversation - .send(message) - .then(function (message) { - console.log("发送成功!"); - }) - .catch(console.error); -``` - -```swift -do { - let message = IMTextMessage(text: "@Tom 早点回家") - message.mentionedMembers = ["Tom"] - try conversation.send(message: message, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = '@Tom 早点回家'; - message.mentionMembers = ['Tom']; - await conversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - -或者也可以通过设置 `mentionAll` 属性值提醒所有人: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("@all") { - MentionAll = true -}; -await conv.Send(textMessage); -``` - -```java -String content = "@all"; -LCIMTextMessage message = new LCIMTextMessage(); -message.setText(content); - -boolean mentionAll = true; // 指示是否提及了所有人 -message.mentionAll(mentionAll); - -imConversation.sendMessage(message, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` - -```objc -LCIMMessage *message = [LCIMTextMessage messageWithText:@"@all" attributes:nil]; -message.mentionAll = YES; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError * _Nullable error) { - /* 一条提及所有用户的消息已发出 */ -}]; -``` - -```js -const message = new TextMessage(`@all`).mentionAll(); -conversation - .send(message) - .then(function (message) { - console.log("发送成功!"); - }) - .catch(console.error); -``` - -```swift -do { - let message = IMTextMessage(text: "@all") - message.isAllMembersMentioned = true - try conversation.send(message: message, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = 'content'; - message.mentionAll = true; - await conversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - -对于消息的接收方来说,可以通过调用 `mentionList` 和 `mentionAll` 的 getter 方法来获得提醒目标用户的信息,示例代码如下: - - - -```cs -jerry.onMessage = (conv, msg) => { - List mentionIds = msg.MentionIdList; -}; -``` - -```java -@Override -public void onMessage(LCIMAudioMessage msg, LCIMConversation conv, LCIMClient client) { - // 读取消息 @ 的 clientId 列表 - List currentMsgMentionUserList = message.getMentionList(); -} -``` - -```objc -// 示例代码演示 LCIMTypedMessage 接收时,获取该条消息提醒的 clientId 列表,同理可以用类似的代码操作 LCIMMessage 的其他子类 -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - // 读取消息 @ 的 clientId 列表 - NSArray *mentionList = message.mentionList; -} -``` - -```js -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - var mentionList = receivedMessage.getMentionList(); -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - if let mentionedMembers = message.mentionedMembers { - print(mentionedMembers) - } - if let isAllMembersMentioned = message.isAllMembersMentioned { - print(isAllMembersMentioned) - } - default: - break - } - default: - break - } -} -``` - -```dart -jerry.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - List mentionList = message.mentionMembers; -}; -``` - - - -此外,为了方便应用层 UI 展现,我们特意为 `LCIMMessage` 增加了两个标识位,用来显示被提醒的状态: - -- 一个是 `mentionedAll` 标识位,用来表示该消息是否提醒了当前对话的全体成员。只有 `mentionAll` 属性为 `true`,这个标识位才为 `true`,否则就为 `false`。 -- 另一个是 `mentioned` 标识位,用来快速判断该消息是否提醒了当前登录用户。如果 `mentionList` 属性列表中包含有当前登录用户的 `clientId`,或者 `mentionAll` 属性为 `true`,那么 `mentioned` 方法都会返回 `true`,否则返回 `false`。 - -调用示例如下: - - - -```cs -client.OnMessage = (conv, msg) => { - bool mentioned = msg.MentionAll || msg.MentionList.Contains("Tom"); -}; -``` - -```java -@Override -public void onMessage(LCIMAudioMessage msg, LCIMConversation conv, LCIMClient client) { - // 读取消息是否 @ 了对话的所有成员 - boolean currentMsgMentionAllUsers = message.isMentionAll(); - // 读取消息是否 @ 了当前用户 - boolean currentMsgMentionedMe = message.mentioned(); -} -``` - -```objc -// 示例代码演示 LCIMTypedMessage 接收时,获取该条消息是否 @ 了当前对话里的所有成员或当前用户,同理可以用类似的代码操作 LCIMMessage 的其他子类 -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - // 读取消息是否 @ 了对话的所有成员 - BOOL mentionAll = message.mentionAll; - // 读取消息是否 @ 了当前用户 - BOOL mentionedMe = message.mentioned; -} -``` - -```js -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - var mentionedAll = receivedMessage.mentionedAll; - var mentionedMe = receivedMessage.mentioned; -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - print(message.isCurrentClientMentioned) - default: - break - } - default: - break - } -} -``` - -```dart -// 暂不支持 -``` - - - -### 修改消息 - -在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 即时通讯设置** 启用「允许通过 SDK 编辑消息」后,终端用户可以对自己已经发送的消息进行修改(`Conversation#updateMessage` 方法)。目前即时通讯服务端并没有在时效性上进行限制,不过只允许用户修改自己发出去的消息,不允许修改别人的消息。 - -修改已经发送的消息,并不是直接在老的消息对象上修改,而是像发新消息一样创建一个消息实例,然后调用 `Conversation#updateMessage(oldMessage, newMessage)` 方法来向云端提交请求,示例代码如下: - - - -```cs -LCIMTextMessage newMessage = new LCIMTextMessage("修改后的消息内容"); -await conversation.UpdateMessage(oldMessage, newMessage); -``` - -```java -LCIMTextMessage textMessage = new LCIMTextMessage(); -textMessage.setContent("修改后的消息"); -imConversation.updateMessage(oldMessage, textMessage, new LCIMMessageUpdatedCallback() { - @Override - public void done(LCIMMessage avimMessage, LCException e) { - if (null == e) { - // 消息修改成功,avimMessage 即为被修改后的最新的消息 - } - } -}); -``` - -```objc -LCIMMessage *oldMessage = <#MessageYouWantToUpdate#>; -LCIMMessage *newMessage = [LCIMTextMessage messageWithText:@"Just a new message" attributes:nil]; - -[conversation updateMessage:oldMessage - toNewMessage:newMessage - callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"消息已被修改。"); - } -}]; -``` - -```js -var newMessage = new TextMessage("new message"); -conversation - .update(oldMessage, newMessage) - .then(function () { - // 修改成功 - }) - .catch(function (error) { - // 异常处理 - }); -``` - -```swift -do { - let newMessage = IMTextMessage(text: "Just a new message") - try conversation.update(oldMessage: oldMessage, to: newMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - Message updatedMessage = await conversation.updateMessage( - oldMessage: oldMessage, - newMessage: newMessage, - ); -} catch (e) { - print(e); -} -``` - - - -消息修改成功之后,对话内的其他成员会立刻接收到 `MESSAGE_UPDATE` 事件: - - - -```cs -tom.OnMessageUpdated = (conv, msg) => { - if (msg is LCIMTextMessage textMessage) { - WriteLine($"内容 {textMessage.Text}, 消息 ID {textMessage.Id}"); - } -}; -``` - -```java -void onMessageUpdated(LCIMClient client, LCIMConversation conversation, LCIMMessage message) { - // message 即为被修改的消息 -} -``` - -```objc -/* 实现 delegate 方法,以处理消息修改的事件 */ -- (void)conversation:(LCIMConversation *)conversation messageHasBeenUpdated:(LCIMMessage *)message reason:(LCIMMessagePatchedReason * _Nullable)reason { - /* 有消息被修改 */ -} -``` - -```js -var { Event } = require("leancloud-realtime"); -conversation.on(Event.MESSAGE_UPDATE, function (newMessage, reason) { - // newMessage 为修改后的消息 - // 在视图层可以通过消息的 ID 找到原来的消息并用 newMessage 替换 - // reason (可选)对象表示消息修改的原因, - // reason 不存在表示发送者主动修改。 - // reason 的 code 属性为正数时,表示因触发云引擎 hook 而导致消息修改 - // (具体数值由开发者在 hook 函数定义中自行指定), - // reason 的 code 属性为负数时,表示因触发系统内置机制而导致消息修改, - // 例如 -4408 表示因敏感词过滤被修改。 - // reason 的 detail 属性是一个字符串,指明具体的修改原因。 -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .updated(updatedMessage: updatedMessage, reason: _): - print(updatedMessage) - default: - break - } - default: - break - } -} -``` - -```dart -tom.onMessageUpdated = ({ - Client client, - Conversation conversation, - Message updatedMessage, - int patchCode, - String patchReason, -}) { - // updatedMessage 即为被修改的消息 -}; -``` - - - -对于 Android 和 iOS SDK 来说,如果开启了消息缓存的选项的话(默认开启),SDK 内部会先从缓存中修改这条消息记录,然后再通知应用层。所以对于开发者来说,收到这条通知之后刷新一下目标聊天页面,让消息列表更新即可(这时候消息列表会出现内容变化)。 - -如果系统修改了消息(例如触发了内置的敏感词过滤功能,或者云引擎的 hook 函数),发送者会收到 `MESSAGE_UPDATE` 事件,其他对话成员接收到的是修改过的消息。 - -### 撤回消息 - -除了修改消息,终端用户还可以撤回一条自己之前发送过的消息。 -和修改消息类似,这一功能需要在控制台启用(**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 即时通讯设置** 启用「允许通过 SDK 撤回消息」)。 -同样,即时通讯服务端并没有在时效性上进行限制,不过只允许用户撤回自己发出去的消息,不允许撤回别人的消息。 - -撤回消息调用 `Conversation#recallMessage` 方法,示例代码如下: - - - -```cs -await conversation.RecallMessage(message); -``` - -```java -conversation.recallMessage(message, new LCIMMessageRecalledCallback() { - @Override - public void done(LCIMRecalledMessage recalledMessage, LCException e) { - if (null == e) { - // 消息撤回成功,可以更新 UI - } - } -}); -``` - -```objc -LCIMMessage *oldMessage = <#MessageYouWantToRecall#>; - -[conversation recallMessage:oldMessage callback:^(BOOL succeeded, NSError * _Nullable error, LCIMRecalledMessage * _Nullable recalledMessage) { - if (succeeded) { - NSLog(@"消息已被撤回。"); - } -}]; -``` - -```js -conversation - .recall(oldMessage) - .then(function (recalledMessage) { - // 撤回成功 - // recalledMessage 是一个 RecalledMessage - }) - .catch(function (error) { - // 异常处理 - }); -``` - -```swift -do { - try conversation.recall(message: oldMessage, completion: { (result) in - switch result { - case .success(value: let recalledMessage): - print(recalledMessage) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - RecalledMessage recalledMessage = await conversation.recallMessage( - message: oldMessage, - ); -} catch (e) { - print(e); -} -``` - - - -成功撤回消息后,对话内的其他成员会接收到 `MESSAGE_RECALL` 的事件: - - - -```cs -tom.OnMessageRecalled = (conv, recalledMsg) => { - // recalledMsg 即为被撤回的消息 -}; -``` - -```java -void onMessageRecalled(LCIMClient client, LCIMConversation conversation, LCIMMessage message) { - // message 即为被撤回的消息 -} -``` - -```objc -/* 实现 delegate 方法,以处理消息撤回的事件 */ -- (void)conversation:(LCIMConversation *)conversation messageHasBeenRecalled:(LCIMRecalledMessage *)message reason:(LCIMMessagePatchedReason * _Nullable)reason { - /* 有消息被撤回 */ -} -``` - -```js -var { Event } = require("leancloud-realtime"); -conversation.on(Event.MESSAGE_RECALL, function (recalledMessage, reason) { - // recalledMessage 为已撤回的消息 - // 在视图层可以通过消息的 ID 找到原来的消息并用 recalledMessage 替换 - // reason (可选) 为撤回消息的原因,详见下文修改消息部分的说明。 -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .updated(updatedMessage: updatedMessage, reason: _): - if let recalledMessage = updatedMessage as? IMRecalledMessage { - print(recalledMessage) - } - default: - break - } - default: - break - } -} -``` - -```dart -tom.onMessageRecalled = ({ - Client client, - Conversation conversation, - RecalledMessage recalledMessage, -}) { - // recalledMessage 即为被撤回的消息 -}; -``` - - - -对于 Android 和 iOS SDK 来说,如果开启了消息缓存的选项的话(默认开启),SDK 内部需要保证数据的一致性,所以会先从缓存中删除这条消息记录,然后再通知应用层。对于开发者来说,收到这条通知之后刷新一下目标聊天页面,让消息列表更新即可(此时消息列表中的消息会直接变少,或者显示撤回提示)。 - -### 暂态消息 - -有时候我们需要发送一些特殊的消息,譬如聊天过程中「某某正在输入…」这样的实时状态信息,或者当群聊的名称修改以后给该群成员发送「群名称被某某修改为 XX」这样的通知信息。这类消息与终端用户发送的消息不一样,发送者不要求把它保存到历史记录里,也不要求一定会被送达(如果成员不在线或者现在网络异常,那么没有下发下去也无所谓),这种需求可以使用「暂态消息」来实现。 - -「暂态消息」是一种特殊的消息,与普通消息相比有以下几点不同: - -- 它不会被自动保存到云端,以后在历史消息中无法找到它 -- 只发送给当时在线的成员,不支持延迟接收,离线用户更不会收到推送通知 -- 对当时在线成员也不保证百分百送达,如果因为当时网络原因导致下发失败,服务端不会重试 - -我们可以用「暂态消息」发送一些实时的、频繁变化的状态信息,或者用来实现简单的控制协议。 - -暂态消息的数据和构造方式与普通消息是一样的,只是其发送方式与普通消息有一些区别。到目前为止,我们演示的 `LCIMConversation` 发送消息接口都是这样的: - - - -```cs -public async Task Send(LCIMMessage message, LCIMMessageSendOptions options = null); -``` - -```java -/** - * 发送一条消息 - */ -public void sendMessage(LCIMMessage message, final LCIMConversationCallback callback) -``` - -```objc -/*! - 往对话中发送消息。 - */ -- (void)sendMessage:(LCIMMessage *)message - callback:(void (^)(BOOL succeeded, NSError * _Nullable error))callback; -``` - -```js -/** - * 发送消息 - * @param {Message} message 消息,Message 及其子类的实例 - * @return {Promise.} 发送的消息 - */ -async send(message) -``` - -```swift -/// Send Message. -/// -/// - Parameters: -/// - message: The message to be sent. -/// - options: @see `MessageSendOptions`. -/// - priority: @see `IMChatRoom.MessagePriority`. -/// - pushData: The push data of APNs. -/// - progress: The file uploading progress. -/// - completion: callback. -public func send(message: IMMessage, options: MessageSendOptions = .default, priority: IMChatRoom.MessagePriority? = nil, pushData: [String : Any]? = nil, progress: ((Double) -> Void)? = nil, completion: @escaping (LCBooleanResult) -> Void) throws -``` - -```dart -Future send({ - @required Message message, -}) async {} -``` - - - -其实即时通讯 SDK 还允许在发送一条消息的时候,指定额外的参数 `LCIMMessageOption`,`LCIMConversation` 完整的消息发送接口如下: - - - -```cs -/// -/// Sends a message in this conversation. -/// -/// The message to send. -/// -public async Task Send(LCIMMessage message, LCIMMessageSendOptions options = null); -``` - -```java -/** - * 发送消息 - * @param message - * @param messageOption - * @param callback - */ -public void sendMessage(final LCIMMessage message, final LCIMMessageOption messageOption, final LCIMConversationCallback callback); -``` - -```objc -/*! - 往对话中发送消息。 - @param message - 消息对象 - @param option - 消息发送选项 - @param callback - 结果回调 - */ -- (void)sendMessage:(LCIMMessage *)message - option:(nullable LCIMMessageOption *)option - callback:(void (^)(BOOL succeeded, NSError * _Nullable error))callback; -``` - -```js -/** - * 发送消息 - * @param {Message} message 消息,Message 及其子类的实例 - * @param {Object} [options] since v3.3.0,发送选项 - * @param {Boolean} [options.transient] since v3.3.1,是否作为暂态消息发送 - * @param {Boolean} [options.receipt] 是否需要回执,仅在普通对话中有效 - * @param {Boolean} [options.will] since v3.4.0,是否指定该消息作为「遗愿消息」发送, - * 「遗愿消息」会延迟到当前用户掉线后发送,常用来实现「下线通知」功能 - * @param {MessagePriority} [options.priority] 消息优先级,仅在聊天室中有效, - * see: {@link module:leancloud-realtime.MessagePriority MessagePriority} - * @param {Object} [options.pushData] 消息对应的离线推送内容,如果消息接收方不在线,会推送指定的内容。其结构说明参见: {@link https://url.leanapp.cn/pushData 推送消息内容 } - * @return {Promise.} 发送的消息 - */ -async send(message, options) -``` - -```swift -/// Message Sending Option -public struct MessageSendOptions: OptionSet { - /// Get Receipt when other client received message or read message. - public static let needReceipt = MessageSendOptions(rawValue: 1 << 0) - - /// Indicates whether this message is transient. - public static let isTransient = MessageSendOptions(rawValue: 1 << 1) - - /// Indicates whether this message will be auto delivering to other client when this client disconnected. - public static let isAutoDeliveringWhenOffline = MessageSendOptions(rawValue: 1 << 2) -} - -/// Send Message. -/// -/// - Parameters: -/// - message: The message to be sent. -/// - options: @see `MessageSendOptions`. -/// - priority: @see `IMChatRoom.MessagePriority`. -/// - pushData: The push data of APNs. -/// - progress: The file uploading progress. -/// - completion: callback. -public func send(message: IMMessage, options: MessageSendOptions = .default, priority: IMChatRoom.MessagePriority? = nil, pushData: [String : Any]? = nil, progress: ((Double) -> Void)? = nil, completion: @escaping (LCBooleanResult) -> Void) throws -``` - -```dart -Future send({ - @required Message message, - bool transient, - bool receipt, - bool will, - MessagePriority priority, - Map pushData, -}) async {} -``` - - - -通过 `LCIMMessageOption` 参数我们可以指定: - -- 是否作为暂态消息发送(设置 `transient` 属性); -- 服务端是否需要通知该消息的接收状态(设置 `receipt` 属性,消息回执,后续章节会进行说明); -- 消息的优先级(设置 `priority` 属性,后续章节会说明); -- 是否为「遗愿消息」(设置 `will` 属性,后续章节会说明); -- 消息对应的离线推送内容(设置 `pushData` 属性,后续章节会说明),如果消息接收方不在线,会推送指定的内容。 - -如果我们需要让 Tom 在聊天页面的输入框获得焦点的时候,给群内成员同步一条「Tom 正在输入…」的状态信息,可以使用如下代码: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("Tom 正在输入…"); -LCIMMessageSendOptions option = new LCIMMessageSendOptions() { - Transient = true -}; -await conversation.Send(textMessage, option); -``` - -```java -String content = "Tom 正在输入…"; -LCIMTextMessage message = new LCIMTextMessage(); -message.setText(content); - -LCIMMessageOption option = new LCIMMessageOption(); -option.setTransient(true); - -imConversation.sendMessage(message, option, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` - -```objc -LCIMMessage *message = [LCIMTextMessage messageWithText:@"Tom 正在输入…" attributes:nil]; -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.transient = true; -[conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - /* 一条暂态消息已发出 */ -}]; -``` - -```js -const message = new TextMessage("Tom 正在输入…"); -conversation.send(message, { transient: true }); -``` - -```swift -do { - let message = IMTextMessage(text: "Tom 正在输入…") - try conversation.send(message: message, options: [.isTransient], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = 'Tom 正在输入…'; -// 发送一条暂态消息 - await conversation.send(message: message, transient: true); -} catch (e) { - print(e); -} -``` - - - -暂态消息的接收逻辑和普通消息一样,开发者可以按照消息类型进行判断和处理,这里不再赘述。上面使用了内建的文本消息只是一种示例,从展现端来说,我们如果使用特定的类型来表示「暂态消息」,是一种更好的方案。即时通讯 SDK 并没有提供固定的「暂态消息」类型,可以由开发者根据自己的业务需要来实现专门的自定义,具体可以参考后述章节:[扩展自己的消息类型](#扩展自己的消息类型)。 - -### 消息回执 - -即时通讯服务端在进行消息投递的时候,会按照消息上行的时间先后顺序下发(先收到的消息先下发,保证顺序性),且内部协议上会要求 SDK 对收到的每一条消息进行确认(ack)。如果 SDK 收到了消息,但是在发送 ack 的过程中出现网络丢包,即时通讯服务端还是会认为消息没有投递下去,之后会再次投递,直到收到 SDK 的应答确认为止。与之对应,SDK 内部也进行了消息去重处理,保证在上面这种异常条件下应用层也不会收到重复的消息。所以我们的消息系统从协议上是可以保证不丢任何一条消息的。 - -不过,有些业务场景会对消息投递的细节有更高的要求,例如消息的发送方要能知道什么时候接收方收到了这条消息,什么时候 ta 又点开阅读了这条消息。有一些偏重工作写作或者私密沟通的产品,消息发送者在发送一条消息之后,还希望能看到消息被送达和阅读的实时状态,甚至还要提醒未读成员。这样「苛刻」的需求,就依赖于我们的「消息回执」功能来实现。 - -与上一节「暂态消息」的发送类似,要使用消息回执功能,需要在发送消息时在 `LCIMMessageOption` 参数中标记「需要回执」选项: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("一条非常重要的消息。"); -LCIMMessageSendOptions option = new LCIMMessageSendOptions { - Receipt = true -}; -await conversation.Send(textMessage, option); -``` - -```java -LCIMMessageOption messageOption = new LCIMMessageOption(); -messageOption.setReceipt(true); -imConversation.sendMessage(message, messageOption, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` - -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.receipt = true; -[conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!需要回执。"); - } -}]; -``` - -```js -var message = new TextMessage("一条非常重要的消息。"); -conversation.send(message, { - receipt: true, -}); -``` - -```swift -do { - let message = IMTextMessage(text: "一条非常重要的消息。") - try conversation.send(message: message, options: [.needReceipt], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = '一条非常重要的消息。'; - await conversation.send(message: message, receipt: true); -} catch (e) { - print(e); -} -``` - - - -> 注意: -> -> 只有在发送时设置了「需要回执」的标记,云端才会发送回执,默认不发送回执,且目前消息回执只支持单聊对话(成员不超过 2 人)。 - -那么发送方后续该如何响应回执的通知消息呢? - -#### 送达回执 - -当接收方收到消息之后,云端会向发送方发出一个回执通知,表明消息已经送达。**请注意与「已读回执」区别开。** - - - -```cs -// Tom 用自己的名字作为 clientId 建立了一个 LCIMClient -LCIMClient client = new LCIMClient("Tom"); -// Tom 登录到系统 -await client.Open(); - -// 设置送达回执 -client.OnMessageDelivered = (conv, msgId) => { - // 在这里可以书写消息送达之后的业务逻辑代码 -}; -// 发送消息 -LCIMTextMessage textMessage = new LCIMTextMessage("夜访蛋糕店,约吗?"); -await conversation.Send(textMessage); -``` - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本地方法来处理对方已经接收消息的通知 - */ - public void onLastDeliveredAtUpdated(LCIMClient client, LCIMConversation conversation) { - ; - } -} - -// 设置全局的对话事件处理 handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); -``` - -```objc -// 监听消息是否已送达实现 `conversation:messageDelivered` 即可。 -- (void)conversation:(LCIMConversation *)conversation messageDelivered:(LCIMMessage *)message { - NSLog(@"%@", @"消息已送达。"); // 打印消息 -} -``` - -```js -var { Event } = require("leancloud-realtime"); -conversation.on(Event.LAST_DELIVERED_AT_UPDATE, function () { - console.log(conversation.lastDeliveredAt); - // 在 UI 中将早于 lastDeliveredAt 的消息都标记为「已送达」 -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .delivered(toClientID: toClientID, messageID: messageID, deliveredTimestamp: deliveredTimestamp): - if messageID == message.ID { - message.deliveredTimestamp = deliveredTimestamp - } - default: - break - } - default: - break - } -} -``` - -```dart -tom.onMessageDelivered = ({ - Client client, - Conversation conversation, - String messageID, - String toClientID, - DateTime atDate, -}) { - // 消息已送达,在这里可以书写消息送达之后的业务逻辑代码 -}; -``` - - - -请注意这里送达回执的内容,不是某一条具体的消息,而是当前对话内最后一次送达消息的时间戳(`lastDeliveredAt`)。最开始我们有过解释,服务端在下发消息的时候,是能够保证顺序的,所以在送达回执的通知里面,我们不需要对逐条消息进行确认,只给出当前确认送达的最新消息的时间戳,那么在这之前的所有消息就都是已经送达的状态。在 UI 层展示的时候,可以将早于 `lastDeliveredAt` 的消息都标记为「已送达」。 - -#### 已读回执 - -消息送达只是即时通讯服务端和客户端之间的投递行为完成了,可能终端用户并没有进入对话聊天页面,或者根本没有激活应用(Android 平台应用在后台也是可以收到消息的),所以「送达」并不等于终端用户真正「看到」了这条消息。 - -即时通讯服务还支持「已读」消息的回执,不过这首先需要接收方显式完成消息「已读」的确认。 - -由于即时通讯服务端是顺序下发新消息的,客户端不需要对每一条消息单独进行「已读」确认。我们设想的场景如下图所示: - -![在一个标题为「欢迎回来」的对话框中写着「好久不见!你有 5002 条未读消息。是否跳过这些消息?(选择「是」将清除所有未读消息标记)」。对话框的底部有两个按钮,分别为「是,跳过」和「否」。](/img/realtime_read_confirm.png) - -用户在进入一个对话的时候,一次性清除当前对话的所有未读消息即可。`Conversation` 的清除接口如下: - - - -```cs -/// -/// Mark the last message of this conversation as read. -/// -/// -public Task Read(); -``` - -```java -/** - * 清除未读消息 - */ -public void read(); -``` - -```objc -/*! - 将对话标记为已读。 - 该方法将本地对话中其他成员发出的最新消息标记为已读,该消息的发送者会收到已读通知。 - */ -- (void)readInBackground; -``` - -```js -/** - * 将该会话标记为已读 - * @return {Promise.} self - */ -async read(); -``` - -```swift -/// Clear unread messages that its sent timestamp less than the sent timestamp of the parameter message. -/// -/// - Parameter message: The default is the last message. -public func read(message: IMMessage? = nil) -``` - -```dart -await conversation.read(); -``` - - - -对方「阅读」了消息之后,云端会向发送方发出一个回执通知,表明消息已被阅读。 - -Tom 和 Jerry 聊天,Tom 想及时知道 Jerry 是否阅读了自己发去的消息,这时候双方的处理流程是这样的: - -1. Tom 向 Jerry 发送一条消息,且标记为「需要回执」: - - - - ```cs - LCIMTextMessage textMessage = new LCIMTextMessage("一条非常重要的消息。"); - LCIMMessageSendOptions options = new LCIMMessageSendOptions { - Receipt = true - }; - await conversation.Send(textMessage); - ``` - - ```java - LCIMClient tom = LCIMClient.getInstance("Tom"); - LCIMConversation conv = client.getConversation("551260efe4b01608686c3e0f"); - - LCIMTextMessage textMessage = new LCIMTextMessage(); - textMessage.setText("Hello, Jerry!"); - - LCIMMessageOption option = new LCIMMessageOption(); - option.setReceipt(true); /* 将消息设置为需要回执。 */ - - conv.sendMessage(textMessage, option, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - /* 发送成功 */ - } - } - }); - ``` - - ```objc - LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; - option.receipt = YES; /* 将消息设置为需要回执。 */ - - LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"Hello, Jerry!" attributes:nil]; - - [conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (!error) { - /* 发送成功 */ - } - }]; - ``` - - ```js - var message = new TextMessage("一条非常重要的消息。"); - conversation.send(message, { - receipt: true, - }); - ``` - - ```swift - do { - let message = IMTextMessage(text: "Hello, Jerry!") - try conversation.send(message: message, options: [.needReceipt], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } catch { - print(error) - } - ``` - - ```dart - try { - TextMessage message = TextMessage(); - message.text = '一条非常重要的消息。'; - await conversation.send(message: message, receipt: true); - } catch (e) { - print(e); - } - ``` - - - -2. Jerry 阅读 Tom 发的消息后,调用对话上的 `read` 方法把「对话中最近的消息」标记为已读: - - - - ```cs - await conversation.Read(); - ``` - - ```java - conversation.read(); - ``` - - ```objc - [conversation readInBackground]; - ``` - - ```js - conversation - .read() - .then(function (conversation) {}) - .catch(console.error.bind(console)); - ``` - - ```swift - conversation.read() - ``` - - ```dart - await conversation.read(); - ``` - - - -3. Tom 将收到一个已读回执,对话的 `lastReadAt` 属性会更新。此时可以更新 UI,把时间戳小于 `lastReadAt` 的消息都标记为已读: - - - - ```cs - tom.OnLastReadAtUpdated = (conv) => { - // Jerry 阅读了你的消息。可以通过调用 conversation.LastReadAt 来获得对方已经读取到的时间 - }; - ``` - - ```java - public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本地方法来处理对方已经阅读消息的通知 - */ - public void onLastReadAtUpdated(LCIMClient client, LCIMConversation conversation) { - /* Jerry 阅读了你的消息。可以通过调用 conversation.getLastReadAt() 来获得对方已经读取到的时间点 */ - } - } - - // 设置全局的对话事件处理 handler - LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); - ``` - - ```objc - // Tom 可以在 client 的 delegate 方法中捕捉到 lastReadAt 的更新 - - (void)conversation:(LCIMConversation *)conversation didUpdateForKey:(LCIMConversationUpdatedKey)key { - if ([key isEqualToString:LCIMConversationUpdatedKeyLastReadAt]) { - NSDate *lastReadAt = conversation.lastReadAt; - /* Jerry 阅读了你的消息。可以使用 lastReadAt 更新 UI,例如把时间戳小于 lastReadAt 的消息都标记为已读。 */ - } - } - ``` - - ```js - var { Event } = require("leancloud-realtime"); - conversation.on(Event.LAST_READ_AT_UPDATE, function () { - console.log(conversation.lastReadAt); - // 在 UI 中将早于 lastReadAt 的消息都标记为「已读」 - }); - ``` - - ```swift - func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .read(byClientID: byClientID, messageID: messageID, readTimestamp: readTimestamp): - if messageID == message.ID { - message.readTimestamp = readTimestamp - } - default: - break - } - default: - break - } - } - ``` - - ```dart - jerry.onLastReadAtUpdated = ({ - Client client, - Conversation conversation, - }) { - // 在 UI 中将早于 lastReadAt 的消息都标记为「已读」 - }; - ``` - - - -注意: - -要使用已读回执,应用需要在初始化的时候开启 [未读消息数更新通知](#未读消息数更新通知) 选项。 - -### 消息免打扰 - -假如某一用户不想再收到某对话的消息提醒,但又不想直接退出对话,可以使用静音操作,即开启「免打扰模式」。具体可以参考[即时通讯开发指南第三篇](/sdk/im/guide/senior/)的《消息免打扰》一节。 - -### Will(遗愿)消息 - -即时通讯服务还支持一类比较特殊的消息:Will(遗愿)消息。「Will 消息」是在一个用户突然掉线之后,系统自动通知对话的其他成员关于该成员已掉线的消息,好似在掉线后要给对话中的其他成员一个妥善的交待,所以被戏称为「遗愿」消息,如下图中的「Tom 已断线,无法收到消息」: - -![在一个名为「Tom & Jerry」的对话中,Jerry 收到内容为「Tom 已断线,无法收到消息」的 Will 消息。这条消息看起来像一条系统通知,与普通消息的样式不同。](/img/lastwill-message.png) - -要发送 Will 消息,用户需要设定好消息内容发给云端,云端并不会将其马上发送给对话的成员,而是缓存下来,一旦检测到该用户掉线,云端立即将这条遗愿消息发送出去。开发者可以利用它来构建自己的断线通知的逻辑。 - - - -```cs -LCIMTextMessage message = new LCIMTextMessage("我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。"); -LCIMMessageSendOptions options = new LCIMMessageSendOptions { - Will = true -}; -await conversation.Send(message, options); -``` - -```java -LCIMTextMessage message = new LCIMTextMessage(); -message.setText("我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。"); - -LCIMMessageOption option = new LCIMMessageOption(); -option.setWill(true); - -conversation.sendMessage(message, option, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` - -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.will = YES; - -LCIMMessage *willMessage = [LCIMTextMessage messageWithText:@"我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。" attributes:nil]; - -[conversation sendMessage:willMessage option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"遗愿消息已发出。"); - } -}]; -``` - -```js -var message = new TextMessage( - "我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。" -); -conversation - .send(message, { will: true }) - .then(function () { - // 发送成功,当前 client 掉线的时候,这条消息会被下发给对话里面的其他成员 - }) - .catch(function (error) { - // 异常处理 - }); -``` - -```swift -do { - let message = IMTextMessage(text: "我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。") - try conversation.send(message: message, options: [.isAutoDeliveringWhenOffline], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = '我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。'; - await conversation.send(message: message, will: true); -} catch (e) { - print(e); -} -``` - - - -客户端发送完毕之后就完全不用再关心这条消息了,云端会自动在发送方异常掉线后通知其他成员,接收端则根据自己的需求来做 UI 的展现。 - -Will 消息有 **如下限制**: - -- Will 消息是与当前用户绑定的,并且只对最后一次设置的「对话 + 消息」生效。如果用户在多个对话中设置了 Will 消息,那么只有最后一次设置有效;如果用户在同一个对话中设置了多条 Will 消息,也只有最后一次设置有效。 -- Will 消息不会进入目标对话的消息历史记录。 -- 当用户主动退出即时通讯服务时,系统会认为这是计划性下线,不会下发 Will 消息(如有)。 - -### 消息内容过滤 - - - -对于多人参与的聊天群组来说,内容的审核和实时过滤是产品运营上的基本要求。我们即时通讯服务默认提供了敏感词过滤的功能,具体请参考[即时通讯开发指南第三篇](/sdk/im/guide/senior/)的《消息内容的实时过滤》一节。 - - - - - -请参考[即时通讯开发指南第三篇](/sdk/im/guide/senior/)的《消息内容的实时过滤》一节。 - - - -### 本地发送失败的消息 - -有时你可能需要将发送失败的消息临时保存到客户端本地的缓存中,等到合适时机再进行处理。例如,将由于网络突然中断而发送失败的消息先保留下来,在消息列表中展示这种消息时,额外添加出错的提示符号和重发按钮,待网络恢复后再由用户选择是否重发。 - -即时通讯 Android 和 iOS SDK 默认提供了消息本地缓存的功能,消息缓存中保存的都是已经成功上行到云端的消息,并且能够保证和云端的数据同步。为了方便开发者,SDK 也支持将一时失败的消息加入到缓存中。 - -将消息加入缓存的代码如下: - - - -```cs -// 暂不支持 -``` - -```java -conversation.addToLocalCache(message); -``` - -```objc -[conversation addMessageToCache:message]; -``` - -```js -// 暂不支持 -``` - -```swift -do { - try conversation.insertFailedMessageToCache(failedMessage) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// 暂不支持 -``` - - - -将消息从缓存中删除: - - - -```cs -// 暂不支持 -``` - -```java -conversation.removeFromLocalCache(message); -``` - -```objc -[conversation removeMessageFromCache:message]; -``` - -```js -// 暂不支持 -``` - -```swift -do { - try conversation.removeFailedMessageFromCache(failedMessage) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// 暂不支持 -``` - - - -从缓存中取出来的消息,在 UI 展示的时候可以根据 `message.status` 的属性值来做不同的处理,`status` 属性为 `LCIMMessageStatusFailed` 时即表示是发送失败了的本地消息,这时可以在消息旁边显示一个重新发送的按钮。通过将失败消息加入到 SDK 缓存中,还有一个好处就是,消息从缓存中取出来再次发送,不会造成服务端消息重复,因为 SDK 有做专门的去重处理。 - -## 离线推送通知 - -对于移动设备来说,在聊天的过程中部分客户端难免会临时下线,如何保证离线用户也能及时收到消息,是我们需要考虑的重要问题。即时通讯云端会在用户下线的时候,主动通过「Push Notification」这种外部方式来通知客户端新消息到达事件,以促使用户尽快打开应用查看新消息。 - -iOS 和 Android 分别提供了内置的离线消息推送通知服务,但是使用的前提是按照推送文档配置 iOS 的推送证书和开启 Android 推送的开关,详细请阅读如下文档: - -1. [即时通讯总览](/sdk/im/guide/overview/)。 -2. [Android 混合推送开发指南](/sdk/push/guide/android-mixpush/) / [iOS 推送开发指南](/sdk/push/guide/ios/)。 - -云端会将用户的即时通讯 `clientId` 与推送服务的设备数据 `_Installation` 自动进行关联。当用户 A 发出消息后,如果对话中部分成员当前不在线,而且这些成员使用的是 iOS 设备,或者是成功开通混合推送功能的 Android 设备的话,云端会自动将即时通讯消息转成特定的推送通知发送至客户端,同时我们也提供扩展机制,允许开发者对接第三方的消息推送服务。 - -要有效使用本功能,关键在于 **自定义推送的内容**。我们提供三种方式允许开发者来指定推送内容: - -1. 静态配置提醒消息 - - 用户可以在控制台中为应用设置一个全局的静态 JSON 字符串,指定固定内容来发送通知。例如,我们进入 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 离线推送**,填入: - - ```json - { "alert": "您有新的消息", "badge": "Increment" } - ``` - - 那么在有新消息到达的时候,符合条件的离线用户会收到一条「您有新的消息」的通知栏消息。 - - 注意,这里 `badge` 参数为 iOS 设备专用,且 `Increment` 大小写敏感,表示自动增加应用 badge 上的数字计数。 - 通常需要在打开或退出应用时,通过设置 Installation 的 badge 字段清零 badge 计数。 - - 此外,对于 iOS 设备您还可以设置声音等推送属性,具体的字段可以参考 [推送 REST API 使用指南](/sdk/push/guide/rest/) 的《消息内容参数》一节。 - -2. 客户端发送消息的时候额外指定推送信息 - - 第一种方法虽然发出去了通知,但是因为通知文本与实际消息内容完全无关,存在一些不足。有没有办法让推送消息的内容与即时通讯消息动态相关呢? - - 还记得我们发送「暂态消息」时的 `LCIMMessageOption` 参数吗?即时通讯 SDK 允许客户端在发送消息的时候,指定附加的推送信息(在 `LCIMMessageOption` 中设置 `pushData` 属性),这样在需要离线推送的时候我们就会使用这里设置的内容来发出推送通知。示例代码如下: - - - -```cs -LCIMTextMessage message = new LCIMTextMessage("Jerry,今晚有比赛,我约了 Kate,咱们仨一起去酒吧看比赛啊?!"); -LCIMMessageSendOptions sendOptions = new LCIMMessageSendOptions { - PushData = new Dictionary { - { "alert", "您有一条未读的消息"}, - { "category", "消息"}, - { "badge", 1}, - { "sound", "message.mp3"}, // 声音文件名,前提在应用里存在 - { "custom-key", "由用户添加的自定义属性,custom-key 仅是举例,可随意替换"} - } -}; -``` - -```java -LCIMTextMessage msg = new LCIMTextMessage(); -msg.setText("Jerry,今晚有比赛,我约了 Kate,咱们仨一起去酒吧看比赛啊?!"); - -LCIMMessageOption messageOption = new LCIMMessageOption(); -String pushMessage = "{\"alert\":\"您有一条未读的消息\", \"category\":\"消息\"," - + "\"badge\":1,\"sound\":\"message.mp3\"," - + "\"custom-key\":\"由用户添加的自定义属性,custom-key 仅是举例,可随意替换\"}"; -messageOption.setPushData(pushMessage); -conv.sendMessage(msg, messageOption, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` - -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.pushData = @{@"alert" : @"您有一条未读消息", @"sound" : @"message.mp3", @"badge" : @1, @"custom-key" : @"由用户添加的自定义属性,custom-key 仅是举例,可随意替换"}; -[conversation sendMessage:[LCIMTextMessage messageWithText:@"Jerry,今晚有比赛,我约了 Kate,咱们仨一起去酒吧看比赛啊?!" attributes:nil] option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - // 在这里处理发送失败或者成功之后的逻辑 -}]; -``` - -```js -const message = new TextMessage('Jerry,今晚有比赛,我约了 Kate,咱们仨一起去酒吧看比赛啊?!'); -conversation.send(message), { - pushData: { - "alert": "您有一条未读的消息", - "category": "消息", - "badge": 1, - "sound": "message.mp3", // 声音文件名,前提在应用里存在 - "custom-key": "由用户添加的自定义属性,custom-key 仅是举例,可随意替换" - } -}); -``` - -```swift -do { - let message = IMTextMessage(text: "Jerry,今晚有比赛,我约了 Kate,咱们仨一起去酒吧看比赛啊?!") - let pushData: [String: Any] = [ - "alert": "您有一条未读的消息", - "category": "消息", - "badge": 1, - "sound": "message.mp3", - "custom-key": "由用户添加的自定义属性,custom-key 仅是举例,可随意替换" - ] - try conversation.send(message: message, pushData: pushData, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = 'Jerry,今晚有比赛,我约了 Kate,咱们仨一起去酒吧看比赛啊?!'; - await conversation.send(message: message, pushData: { - "alert": "您有一条未读的消息", - "category": "消息", - "badge": 1, - "sound": "message.mp3", // 声音文件名,前提在应用里存在 - "custom-key": "由用户添加的自定义属性,custom-key 仅是举例,可随意替换" - }); -} catch (e) { - print(e); -} -``` - - - -3. 服务端动态生成通知内容 - - 第二种方法虽然动态,但是需要在客户端发送消息的时候提前准备好推送内容,这对于开发阶段的要求比较高,并且在灵活性上有比较大的限制,所以看上去也不够完美。 - - 我们还提供了第三种方式,让开发者在推送动态内容的时候,也不失实现上的灵活性。这种方式需要使用即时通讯 Hook 机制在服务端来统一指定离线推送消息内容,感兴趣的开发者可以参阅[详解消息 hook 与系统对话](/sdk/im/guide/systemconv/)。 - -三种方式之间的优先级如下:**服务端动态生成通知 > 客户端发送消息的时候额外指定推送信息 > 静态配置提醒消息**。 - -也就是说如果开发者同时采用了多种方式来指定消息推送,那么有服务端动态生成的通知的话,最后以它为准进行推送。其次是客户端发送消息的时候额外指定推送内容,最后是静态配置的提醒消息。 - -### 实现原理与限制 - -同时使用了推送服务和即时通讯服务的应用,客户端在成功登录即时通讯服务时,SDK 会自动关联当前的 `clientId` 和设备数据(推送服务中的 `Installation` 表)。关联的方式是通过让目标设备 **订阅** 名为 `clientId` 的 Channel 实现的。开发者可以在数据存储的 `_Installation` 表中的 `channels` 字段查到这组关联关系。在实际离线推送时,云端系统会根据用户 `clientId` 找到对应的关联设备进行推送。 - -由于即时通讯触发的推送量比较大,内容单一,所以推送服务云端不会保留这部分记录,开发者在 **开发者中心** > **你的游戏** > **游戏服务** > **云服务** > **推送通知** > **推送记录** 中也无法找到这些记录。 - -推送服务的通知过期时间是 7 天,也就是说,如果一个设备 7 天内没有连接到 APNs、MPNs 或设备对应的混合推送平台,系统将不会再给这个设备推送通知。 - -### 其他推送设置 - -iOS 环境下,离线消息默认推送至 APNs 的生产环境。 -推送时使用 `"_profile": "dev"` 可以切换至 APNs 的开发环境(如果基于证书鉴权方式进行推送,此时会使用开发证书): - -```json -{ - "alert": "您有一条未读消息", - "_profile": "dev" -} -``` - -基于 Apple 推荐使用的 Token Authentication 方式进行推送时,如果应用配置了多个不同 Team ID 的 Private Key,请确认目标用户设备使用的 APNs Team ID 并将其填写在 `_apns_team_id` 参数内,以保证推送正常进行,只有指定 Team ID 的设备能收到推送(Apple 不允许在一次推送请求中向多个从属于不同 Team ID 的设备发推送)。如: - -```json -{ - "alert": "您有一条未读消息", - "_apns_team_id": "my_fancy_team_id" -} -``` - -`_profile` 和 `_apns_team_id` 属性为推送服务内部使用,均不会实际推送。 -指定附加推送信息时,支持为不同种类的设备(比如 `ios`、`android`)附加不同的推送信息,需要特别注意的是,`_profile` 和 `_apns_team_id` 这两个内部属性不要在 `ios` 对象内部指定,否则不会生效。 -例如,这样的附加推送消息会导致离线消息被推送至 APNs 的生产环境: - -```json -{ - "ios": { - "badge": "Increment", - "category": "NEW_CHAT_MESSAGE", - "sound": "default", - "thread-id": "chat", - "alert": { - "title": "您有一条未读消息", - "body": "因为 _profile 内部属性的位置错误,这条消息仍然会被推送到 APNs 的生产环境" - }, - "_profile": "dev" - }, - "android": { - "title": "您有一条未读消息", - "alert": "" - } -} -``` - -这样才能推送至开发环境: - -```json -{ - "_profile": "dev", - "ios": { - /* 略 */ - }, - "android": { - /* 略 */ - } -} -``` - -目前,**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 离线推送** 这里的推送内容也支持一些内置变量,你可以将上下文信息直接设置到推送内容中: - -- `${convId}` 推送相关的对话 ID -- `${timestamp}` 触发推送的时间戳(Unix 时间戳) -- `${fromClientId}` 消息发送者的 `clientId` - -## 离线消息同步 - -离线推送通知是一种非常有效的提醒用户的手段,但是如果用户不上线,即时通讯的消息就总是无法下发,客户端如果长时间下线,会导致大量消息堆积在云端,此后如果用户再上线,我们该如何处理才能保证消息完全不丢失呢? - -即时通讯服务提供客户端主动从云端「拉」的方式。云端会记录下用户在每一个参与对话中接收的最后一条消息的位置,在用户重新登录上线后,实时计算出用户离线期间产生未读消息的对话列表及对应的未读消息数,以「未读消息数更新」的事件通知到客户端,然后客户端在需要的时候来主动拉取这些离线消息。 - -### 未读消息数更新通知 - -在客户端重新登录上线后,即时通讯云端会实时计算下线时间段内当前用户参与过的对话中的新消息数量。 - -客户端只有设置了主动拉取的方式,云端才会在必要的时候下发这一通知。如前所述,对于 JavaScript / Android / iOS SDK 来说,仅支持客户端主动拉取未读消息,所以不需要再做什么设置。 - -客户端 SDK 会在 `IMConversation` 上维护一个 `unreadMessagesCount` 字段,来统计当前对话中存在有多少未读消息。 - -客户端用户登录之后,云端会以「未读消息数更新」事件的形式,将当前用户所在的多对 `` 数据通知到客户端,这就是客户端维护的 `` 初始值。之后 SDK 在收到新的在线消息的时候,会自动增加对应的 `unreadMessageCount` 计数。直到用户把某一个对话的未读消息清空,这时候云端和 SDK 的 `` 计数都会清零。 - -> 注意:开启未读消息数后,在开发者没有主动重置未读消息的情况下,未读消息数将一直累计。 -> 客户端再次离线并不会重置未读消息数。 -> 包括客户端在线时收到的消息,也会导致未读消息数增加。 -> 因此开发者需要在合适时机通过将对话标记为已读主动清除未读消息数。 - -客户端 SDK 在 `` 数字变化的时候,会通过 `IMClient` 派发「未读消息数量更新(`UNREAD_MESSAGES_COUNT_UPDATE`)」事件到应用层。开发者可以监听 `UNREAD_MESSAGES_COUNT_UPDATE` 事件,在对话列表界面上更新这些对话的未读消息数量。建议开发者在应用层面对未读计数的结果进行持久化缓存,如果同一个对话有两个不同的未读数,则使用新数据直接覆盖老数据,这样对话列表里面展示的未读数会比较准确。 - - - -```cs -tom.OnUnreadMessagesCountUpdated = (convs) => { - foreach (LCIMConversation conv in convs) { - // conv.Unread 即该 conversation 的未读消息数量 - } -}; -``` - -```java -// 实现 LCIMConversationEventHandler 的代理方法 onUnreadMessagesCountUpdated 来得到未读消息的数量变更的通知 -onUnreadMessagesCountUpdated(LCIMClient client, LCIMConversation conversation) { - // conversation.getUnreadMessagesCount() 即该 conversation 的未读消息数量 -} -``` - -```objc -// 使用代理方法 conversation:didUpdateForKey: 来观察对话的 unreadMessagesCount 属性 -- (void)conversation:(LCIMConversation *)conversation didUpdateForKey:(LCIMConversationUpdatedKey)key { - if ([key isEqualToString:LCIMConversationUpdatedKeyUnreadMessagesCount]) { - NSUInteger unreadMessagesCount = conversation.unreadMessagesCount; - /* 有未读消息产生,请更新 UI,或者拉取对话。 */ - } -} -``` - -```js -var { Event } = require("leancloud-realtime"); -client.on(Event.UNREAD_MESSAGES_COUNT_UPDATE, function (conversations) { - for (let conv of conversations) { - console.log(conv.id, conv.name, conv.unreadMessagesCount); - } -}); -``` - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .unreadMessageCountUpdated: - print(conversation.unreadMessageCount) - default: - break - } -} -``` - -```dart -tom.onUnreadMessageCountUpdated = ({ - Client client, - Conversation conversation, -}) { - // conversation.unreadMessageCount 即该 conversation 的未读消息数量 -}; -``` - - - -对开发者来说,在 `UNREAD_MESSAGES_COUNT_UPDATE` 事件响应的时候,SDK 传给应用层的 `Conversation` 对象,其 `lastMessage` 应该是当前时间点当前用户在当前对话里面接收到的最后一条消息,开发者如果要展示更多的未读消息,就需要通过消息拉取的接口来主动获取了(参见[即时通讯开发指南第一篇](/sdk/im/guide/beginner/)的《聊天记录查询》一节。 - -清除对话未读消息数的唯一方式是调用 `Conversation#read` 方法将对话标记为已读,一般来说开发者至少需要在下面两种情况下将对话标记为已读: - -- 在对话列表点击某对话进入到对话页面时 -- 用户正在某个对话页面聊天,并在这个对话中收到了消息时 - -iOS 和 Android 应用层需要持久化缓存未读计数的细节说明 - -对于未读通知的下发时机和数量,iOS 和 Java/Android 两个平台的 SDK 在内部处理上稍有差异:iOS SDK(Objective-C 和 Swift 都包括)在每次登录即时通讯云端的时候,都会获得云端下发的**大量**未读通知;而 Java/Android SDK 由于内部持久化缓存了通知的时间戳(能减轻服务端压力),所以登录即时通讯云端之后客户端只会收到上次通知时间戳之后发生了变化的**部分**未读数通知。 - -因此 Java SDK 的开发者需要在应用层缓存收到的未读数通知(同一个对话的未读数采用覆盖的方式来更新),而 iOS SDK 这里收到的**大量未读通知并不等于全量数据(云端追踪的有未读消息的对话数不超过 50 个)**,所以也是一样需要在应用层面缓存收到的未读计数结果,这样才能保证对话列表超过 50 个之后未读计数值的准确性。 - -## 多端登录与单设备登录 - -一个用户可以使用相同的账号在不同的客户端上登录(例如 QQ 网页版和手机客户端可以同时接收到消息和回复消息,实现多端消息同步),而有一些场景下,需要禁止一个用户同时在不同客户端登录,例如我们不能用同一个微信账号在两个手机上同时登录。即时通讯服务提供了灵活的机制,来满足 **_多端登录_** 和 **_单设备登录_** 这两种完全相反的需求。 - -即时通讯 SDK 在生成 `IMClient` 实例的时候,允许开发者在 `clientId` 之外,增加一个额外的 `tag` 标记。云端在用户主动登录的时候,会检查 `` 组合的唯一性。如果当前用户已经在其他设备上使用同样的 `tag` 登录了,那么云端会强制让之前登录的设备下线。如果多个 `tag` 不发生冲突,那么云端会把他们当成独立的设备进行处理,应该下发给该用户的消息会分别下发给所有设备,不同设备上的未读消息计数则是合并在一起的(各端之间消息状态是同步的);该用户在单个设备上发出来的上行消息,云端也会默认同步到其他设备。 - -基于以上机制,即时通讯可以支持应用实现多种业务需求: - -1. 无限制的多端登录:不设置 `tag`,默认对用户的多端登录不作限制。用户可以在多个设备上登录,比如在手机和平板上同时登录,甚至在两台不同的手机上登录,多个设备可以同时接收和回复消息。 -2. 单设备登录:在所有客户端都设置同一个 `tag`,限制用户只能在一台设备上登录。 -3. 有限制的多端登录:通过设置不同的 `tag`,允许用户在多台不同类型的设备上登录。例如,我们可以设计三种 `tag`:`Mobile`、`Pad`、`Web`,分别对应三种类型的设备:手机、平板和电脑,那么用户分别在三种设备上登录就都是允许的,但是却不能同时在两台电脑上登录。详见下面的代码示例。 - -### 设置登录标记 - -按照上面的方案,以手机端登录为例,在创建 `IMClient` 实例的时候,我们增加 `tag: Mobile` 这样的标记: - - - -```cs -LCIMClient client = new LCIMClient(clientId, "Mobile", "your-device-id"); -``` - -```java -// 第二个参数:登录标记 tag -LCIMClient currentClient = LCIMClient.getInstance(clientId, "Mobile"); -currentClient.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient avimClient, LCIMException e) { - if(e == null){ - // 与云端建立连接成功 - } - } -}); -``` - -```objc -NSError *error; -LCIMClient *currentClient = [[LCIMClient alloc] initWithClientId:@"Tom" tag:@"Mobile" error:&error]; -if (!error) { - [currentClient openWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 与云端建立连接成功 - } - }]; -} -``` - -```js -realtime.createIMClient("Tom", { tag: "Mobile" }).then(function (tom) { - console.log("Tom 登录"); -}); -``` - -```swift -do { - let client = try IMClient(ID: "CLIENT_ID", tag: "Mobile") - client.open { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - Client tom = Client(id: 'Tom', tag: 'Mobile'); - await tom.open(); -} catch (e) { - print(e); -} -``` - - - -之后如果同一个用户在另一个手机上再次登录,则较早前登录系统的客户端会被强制下线。 - -### 处理登录冲突 - -即时通讯云端在登录用户的 `` 相同的时候,总是踢掉较早登录的设备,这时候较早登录设备端会收到被云端下线(`CONFLICT`)的事件通知: - - - -```cs -tom.OnClose = (code, detail) => { - -}; -``` - -```java -public class AVImClientManager extends LCIMClientEventHandler { - /** - * 实现本方法以处理当前登录被踢下线的情况 - * - * - * @param client - * @param code 状态码说明被踢下线的具体原因 - */ - @Override - public void onClientOffline(LCIMClient avimClient, int i) { - if(i == 4111){ - // 适当地弹出友好提示,告知当前用户的 clientId 在其他设备上登录了 - } - } -} - -// 自定义实现的 LCIMClientEventHandler 需要注册到 SDK 后,SDK 才会通过回调 onClientOffline 来通知开发者 -LCIMClient.setClientEventHandler(new AVImClientManager()); -``` - -```objc -- (void)imClientClosed:(LCIMClient *)imClient error:(NSError * _Nullable)error -{ - if ([error.domain isEqualToString:kLeanCloudErrorDomain] && - error.code == 4111) { - // 适当的弹出友好提示,告知当前用户的 clientId 在其他设备上登录了 - } -} -``` - -```js -var { Event } = require("leancloud-realtime"); -tom.on(Event.CONFLICT, function () { - // 弹出提示,告知当前用户的 clientId 在其他设备上登录了 -}); -``` - -```swift -func client(_ client: IMClient, event: IMClientEvent) { - switch event { - case .sessionDidClose(error: let error): - if error.code == 4111 { - // 弹出提示,告知当前用户的 clientId 在其他设备上登录了 - } - default: - break - } -} -``` - -```dart -tom.onClosed = ({ - Client client, - RTMException exception, -}) { - if (exception.code == '4111') { - // 适当的弹出友好提示,告知当前用户的 clientId 在其他设备上登录了 - } -}; -``` - - - -如上述代码中,被动下线的时候,云端会告知原因,因此客户端在做展现的时候也可以做出类似于 QQ 一样友好的通知。 - -以上提到的登录均指用户主动进行登录操作。已登录用户在应用启动、网络中断等场景下,SDK 会自动重新登录。这种情况下,如果触发登录冲突,云端并不会踢掉较早登录的设备,自动重新登录的设备则会收到登录冲突的报错,登录失败。 - -相应地,应用开发者如果希望在用户主动登录触发冲突时,不踢掉较早登录的设备,而提示用户登录失败,可以在登录时传入参数指明这一点: - - - -```cs -await tom.Open(false); -``` - -```java -LCIMClientOpenOption openOption = new LCIMClientOpenOption(); -openOption.setReconnect(true); -LCIMClient currentClient = LCIMClient.getInstance(clientId, "Mobile"); -currentClient.open(openOption, new LCIMClientCallback() { - @Override - public void done(LCIMClient avimClient, LCIMException e) { - if(e == null){ - // 与云端建立连接成功 - } - } -}); -``` - -```objc -NSError *err; -LCIMClient *currentClient = [[LCIMClient alloc] initWithClientId:@"Tom" tag:@"Mobile" error:&err]; -if (err) { - NSLog(@"init failed with error: %@", err); -} else { - [currentClient openWithOption:LCIMClientOpenOptionReopen callback:^(BOOL succeeded, NSError * _Nullable error) { - if ([error.domain isEqualToString:kLeanCloudErrorDomain] && - error.code == 4111) { - // 冲突时登录失败,不会踢掉较早登录的设备 - } - }]; -} -``` - -```js -realtime - .createIMClient("Tom", { tag: "Mobile", isReconnect: true }) - .then(function (tom) { - console.log("冲突时登录失败,不会踢掉较早登录的设备"); - }); -``` - -```swift -do { - let client = try IMClient(ID: "Tom", tag: "Mobile") - client.open(options: [.reconnect]) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - if error.code == 4111 { - // 冲突时登录失败,不会踢掉较早登录的设备 - } - } - } -} catch { - print(error) -} -``` - -```dart -try { - Client tom = Client(id: 'Tom', tag: 'Mobile'); - // 冲突时登录失败,不会踢掉较早登录的设备 - await tom.open(reconnect: true); -} catch (e) { - print(e); -} -``` - - - -## 扩展自己的消息类型 - -尽管即时通讯服务默认已经包含了丰富的消息类型,但是我们依然支持开发者根据业务需要扩展自己的消息类型,例如允许用户之间发送名片、红包等等。这里「名片」和「红包」就可以是应用层定义的自己的消息类型。 - -### 自定义消息属性 - -即时通讯 SDK 默认提供了多种消息类型用来满足常见的需求: - -- `TextMessage` 文本消息 -- `ImageMessage` 图像消息 -- `AudioMessage` 音频消息 -- `VideoMessage` 视频消息 -- `FileMessage` 普通文件消息(.txt/.doc/.md 等各种) -- `LocationMessage` 地理位置消息 - -这些消息类型还支持应用层设置若干 key-value 自定义属性来实现扩展。譬如有一条文本消息需要附带城市信息,这时候开发者使用消息类中预留的 `attributes` 属性就可以保存额外信息了。 - - - -```cs -LCIMTextMessage messageWithCity = new LCIMTextMessage("天气太冷了"); -messageWithCity["city"] = "北京"; -``` - -```java -LCIMTextMessage messageWithCity = new LCIMTextMessage(); -messageWithCity.setText("天气太冷了"); -HashMap attr = new HashMap(); -attr.put("city", "北京"); -messageWithCity.setAttrs(attr); -``` - -```objc -NSDictionary *attributes = @{ @"city": @"北京" }; -LCIMTextMessage *messageWithCity = [LCIMTextMessage messageWithText:@"天气太冷了" attributes:attributes]; -``` - -```js -var messageWithCity = new TextMessage("天气太冷了"); -messageWithCity.setAttributes({ city: "北京" }); -``` - -```swift -let messageWithCity = IMTextMessage(text: "天气太冷了") -messageWithCity.attributes = ["city": "北京"]; -``` - -```dart -TextMessage message = TextMessage(); -message.text = '天气太冷了'; -message.attributes = {'city': '北京'}; -``` - - - -### 自定义消息类型 - -在默认的消息类型完全无法满足需求的时候,可以实现和使用自定义的消息类型。 - - -<> - -继承于 `LCIMTypedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -- 首先定义一个自定义的子类继承自 `LCIMTypedMessage`。 -- 然后在初始化的时候注册这个子类。 - -```cs -class EmojiMessage : LCIMTypedMessage { - public const int EmojiMessageType = 1; - - public override int MessageType => EmojiMessageType; - - public string Ecode { - get { - return data["ecode"] as string; - } set { - data["ecode"] = value; - } - } -} - -// 注册子类 -LCIMTypedMessage.Register(EmojiMessage.EmojiMessageType, () => new EmojiMessage()); -``` - - -<> - -继承于 `LCIMTypedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -- 实现新的消息类型,继承自 `LCIMTypedMessage`。这里需要注意: - - 在 class 上增加一个 `@LCIMMessageType(type=123)` 的 Annotation
    具体消息类型的值(这里是 `123`)由开发者自己决定。内建消息类型使用负数,所有正数都预留给开发者扩展使用。 - - 在消息内部声明字段属性时,要增加 `@LCIMMessageField(name="")` 的 Annotation
    `name` 为可选字段,同时自定义的字段要有对应的 getter/setter 方法。 - - **请不要遗漏空的构造方法**(参考下面的示例代码),否则会造成类型转换失败。 -- 调用 `LCIMMessageManager.registerLCIMMessageType()` 函数进行类型注册。 -- 调用 `LCIMMessageManager.registerMessageHandler()` 函数进行消息处理 handler 注册。 - -注意:如果你是使用 Kotlin 来开发,由于 Kotlin 对反射的处理方式与 Java 有细微差异,导致 `LCIMMessageField` 注释不能产生作用,所以 SDK 实际发送的自定义消息数据不全。我们已经在 `6.4.4` 版本的 SDK 中对这一问题进行了优化,请 Kotlin 开发者升级到 6.4.4 及其后续版本来定制子类化消息。 - -```java -@LCIMMessageType(type = 123) -public class CustomMessage extends LCIMTypedMessage { - // 空的构造方法,不可遗漏 - public CustomMessage() { - - } - - @LCIMMessageField(name = "_lctext") - String text; - @LCIMMessageField(name = "_lcattrs") - Map attrs; - - public String getText() { - return this.text; - } - - public void setText(String text) { - this.text = text; - } - - public Map getAttrs() { - return this.attrs; - } - - public void setAttrs(Map attr) { - this.attrs = attr; - } -} - -// 注册自定义类型 -LCIMMessageManager.registerLCIMMessageType(CustomMessage.class); -``` - - -<> - -继承于 `LCIMTypedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -- 实现 `LCIMTypedMessageSubclassing` 协议; -- 子类将自身类型进行注册,一般可在子类的 `+load` 方法或者 `UIApplication` 的 `-application:didFinishLaunchingWithOptions:` 方法里面调用 `[YourClass registerSubclass]`。 - -```objc -// 定义 - -@interface CustomMessage : LCIMTypedMessage - -+ (LCIMMessageMediaType)classMediaType; - -@end - -@implementation CustomMessage - -+ (LCIMMessageMediaType)classMediaType { - return 123; -} - -@end - -// 注册子类 -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [CustomMessage registerSubclass]; -} -``` - - -<> - -通过继承 `TypedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -- 申明新的消息类型,继承自 `TypedMessage` 或其子类,然后: - - 对 class 使用 `messageType(123)` 装饰器,具体消息类型的值(这里是 `123`)由开发者自己决定(内建消息类型使用负数,所有正数都预留给开发者扩展使用)。 - - 对 class 使用 `messageField(['fieldName'])` 装饰器来声明需要发送的字段。 -- 调用 `Realtime#register()` 函数注册这个消息类型。 - -举个例子,实现一个在 [暂态消息](#暂态消息) 中提出的 `OperationMessage`: - -```js -// TypedMessage, messageType, messageField 都是由 leancloud-realtime 这个包提供的 -// 在浏览器中则是 var { TypedMessage, messageType, messageField } = AV; -var { TypedMessage, messageType, messageField } = require("leancloud-realtime"); -// 定义 OperationMessage 类,用于发送和接收所有的用户操作消息 -export class OperationMessage extends TypedMessage {} -// 指定 type 类型,可以根据实际换成其他正整数 -messageType(1)(OperationMessage); -// 声明需要发送 op 字段 -messageField("op")(OperationMessage); -// 注册消息类,否则收到消息时无法自动解析为 OperationMessage -realtime.register(OperationMessage); -``` - - -<> - -继承于 `IMCategorizedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -- 实现 `IMMessageCategorizing` 协议; -- 子类将自身类型进行注册,一般可在 `AppDelegate` 的 `application(_:didFinishLaunchingWithOptions:)` 方法里面调用 `try CustomMessage.register()`。 - -```swift -// 定义 CustomMessage 类 -class CustomMessage: IMCategorizedMessage { - - // 指定 type 类型,可以根据实际换成其他正整数 - class override var messageType: MessageType { - return 1 - } -} - -// 注册消息类型 -func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - do { - try CustomMessage.register() - } catch { - print(error) - return false - } - - return true -} -``` - - -<> - -继承于 `TypedMessage`,开发者也可以扩展自己的富媒体消息。步骤是: - -```dart -// 自定义消息类型 CustomMessage -class CustomMessage extends TypedMessage { - @override - - int get type => 123; - CustomMessage() : super(); - CustomMessage.from({ - @required String text, - //... - }) { - this.text = text; - } -} -TypedMessage.register(() => CustomMessage()); -``` - - -
    - -自定义消息的接收,可以参看[即时通讯开发指南第一篇](/sdk/im/guide/beginner/)的《再谈接收消息》。 - -## 进一步阅读 - -- 《即时通讯开发指南》第三篇[安全与签名、玩转聊天室和临时对话](/sdk/im/guide/senior/) -- 《即时通讯开发指南》第四篇[详解消息 hook 与系统对话](/sdk/im/guide/systemconv/) diff --git a/leancloud/docs/sdk/im/guide/overview.mdx b/leancloud/docs/sdk/im/guide/overview.mdx deleted file mode 100644 index 091ad7c09..000000000 --- a/leancloud/docs/sdk/im/guide/overview.mdx +++ /dev/null @@ -1,311 +0,0 @@ ---- -title: 即时通讯总览 -sidebar_label: 总览 -sidebar_position: 0 -slug: /sdk/im/guide/overview ---- - - - - - -即时通讯是主要解决产品内即时通信(Instant Messaging)、实时数据同步等需求,其设计上的主要目标是: - -- **支持为现有应用快速加入多种通讯能力** - - 我们很多客户的产品都已经达到一个比较稳定的形态,即时通讯只是其中一个锦上添花的功能,所以如何和现有系统无缝集成,是我们设计上一个重要的出发点。即时通讯服务可以在应用账户系统独立的情况下,快速接入并稳定安全地运行。 - - 我们支持了多种典型通讯场景,提供了丰富的 UI 库和脚手架来帮助开发者快速接入。并且考虑到业务运行环境,我们提供了全平台支持的 SDK。 - -- **强大的自定义机制满足业务各种扩展需求** - - 我们默认支持了文本、图片、音视频、地理位置、二进制等多种类型的消息收发,同时也允许产品开发者来扩展自己的消息类型和 UI 样式。并且在基本功能之外,我们也支持更多高阶需求,例如消息撤回与修改、@ 成员提醒、暂态消息、「已读」回执、离线推送、敏感内容过滤等等与消息收发相关的功能,或者超大规模用户参与的「开放聊天室」,以及类似于微信公众号的「系统对话」,在这里都可以得到满足。 - -- **安全和权限控制** - - 我们始终把系统安全性放在首要位置,客户端与云端使用 WebSocket 全双工通讯,全程 TLS 加密传输。在用户登录和操作权限的控制上,我们专门设计了第三方操作签名的机制,让应用层在快速接入的同时,也可以实时、完整地控制用户在即时通讯系统内的所有活动。 - -- **最大限度降低客户的生产运维成本** - - 我们提供专业的技术支持服务,富有经验的资深工程师 7 × 24 小时对接,以帮助开发者快速、有效地完成产品集成,缩短开发周期。此外在产品运营阶段,让开发者彻底摆脱后端系统日常的运维细节和突发的软硬件故障处理,也不用关心用户量和流量的变化。帮助客户在享受高品质技术服务的同时,也可以最大限度降低生产运维成本,并且以更快的速度推进产品迭代,是我们始终追求的目标。 - -即时通讯服务现在已经被广泛使用在应用内社交、工作协同、客服系统、超大型赛事和电视直播、以及游戏状态同步等多种业务场景之中。 - -## 功能和特性 - -即时通讯服务提供的主要功能有: - -- **基本聊天功能**,包括: - - - 支持多种聊天场景。除了普通的单聊、群聊之外,我们还提供不限人数的「**开放聊天室**」,适合活动直播、公开课、游戏中的世界聊天等海量用户在一个群里互动的场合,也提供了可用来实现应用内的公众号、服务号的「**系统对话**」,还有专为客服系统准备的「**临时对话**」。通过提炼不同场景的共通需求,我们提供了功能各异但接口一致的解决方案。 - - 用户之间可以发送多种多样的消息,如文本、图片、语音、音视频、地理位置等,也可以发送 **二进制消息**,以及更多的应用层自定义消息。 - - 聊天消息自动保存在云端,支持各种复杂的查找和翻页方式。 - -- **特殊的消息收发需求**。除了普通的消息收发之外,我们还支持: - - - 带有提醒功能的 **@ 消息**(如微信里面的 @ 某人) - - 消息的 **撤回和修改** - - 消息送达和对方已读的 **回执通知** - - 群聊里面为了避免过于干扰,允许用户开启 **消息免打扰** 开关 - - 发送譬如聊天过程中「某某正在输入…」这样的 **状态信息** - - 在消息接收方离线时,自动转为 **推送通知**(Push Notification) - - 对于大型的开放聊天室,为了防止重要消息被淹没或丢弃,支持不同 **优先级** 的发送选项,确保重要的消息能优先、迅速送达客户端 - -- **多端登录与消息同步** - - 现在一个用户使用多个设备登录已经是比较常见的需求,我们既支持单个账号在多个设备同时登录、即时通讯同步到所有设备,也支持 **单点登录**,可由业务层自主选择。 - 移动设备网络不稳定也是常态,聊天过程中用户难免会偶尔掉线,我们的 **消息同步机制** 可以确保用户消息及时得到同步,重要消息从不丢失。 - -- **管理与运营支持** - - 对于聊天消息,我们默认提供了 **实时过滤敏感内容** 的能力,允许各个产品设置自己的敏感词列表,并且也支持开发者实现自己的敏感词过滤插件,来确保产品运营层面合规合法。 - -- **安全控制** - - 任何终端用户要开启即时通讯服务,只需要提供一个唯一标识自己的 `clientId` 即可,这种与产品自有账户系统解耦合的方式,带来了集成的便利,也可以促使通讯服务商专注做好底层的「信使」角色。 - 同时我们也提供 **第三方鉴权** 的机制,通过在聊天流程中加入开发者服务器签名授权这一环节,来确保通讯操作的安全。 - 而且,即时通讯 SDK 与云端是 WebSocket 全双工通讯,且全程使用 TLS 安全加密。 - -- **强大的业务扩展能力** - - 对于很多典型的需求,我们提供了默认的实现,而为了支持业务的多样性和特殊性,我们也提供了丰富的扩展机制: - - - 为了和产品自有用户系统进行对接,我们提供了第三方操作鉴权的扩展接口,确保在用户登录、创建/加入/退出对话群组、以及拉取聊天记录时,所有操作都得到了授权。 - - 同时我们还支持开发者对消息传递的过程进行 **hook 处理**,在消息到达云端但是还没有投递之前和投递之后,分别完成自定义的处理逻辑,例如过滤掉竞品的品牌,以及自定义离线推送消息,等等。 - - 我们也支持通过简单的 **web hook** 来完成云端和应用后端的消息同步。 - - 在提供移动端的 SDK 之外,我们还提供了 REST API,以帮助产品在可信环境下更好地实现业务处理。 - - 我们相信灵活性和扩展性也是云服务的核心竞争力。 - -## 全平台 SDK 和 Demo 支持 - -目前,我们提供了主流平台的客户端 SDK,而且它们源代码都是公开的,开发者可以在我们的 [GitHub 账户](https://github.com/leancloud) 下自由下载,可以与我们工程师同步讨论遇到的问题和需求。 - -在 SDK 之外,我们也公开了一些 Demo 项目来帮助开发者快速熟悉我们的产品,详见页眉导航栏的 **下载 > Demos** 链接。 - -## 核心概念说明 - -在深入了解之前,我们先跟大家解释几个核心概念,这些概念在 API 或者后面的开发指南中都会出现,了解它们会让后续的文档阅读变得简单和轻松很多。 - -### `clientId`、用户和登录 - -即时通讯服务中的每一个终端称为一个「Client」。Client 拥有一个在应用内唯一标识自己的 ID(`clientId`)。这个 ID 由应用自己定义,必须是 **只由英文字母、数字、半角下划线与半角短横线组成,不以数字开头,不超过 64 个字符的字符串**。在大部分场合,Client 都可以对应到应用中的某个「用户」,但是并不是只有真的用户才能作为「Client」,你完全可以把一个探测器当成一个「Client」,把它收集到的数据通过即时通讯服务广播给更多「人」。 - -要使用即时通讯服务,每一个终端设备需要首先建立与即时通讯云端的 WebSocket 长连接,并使用唯一的 `clientId` 来加入即时通讯服务,我们把这一过程称为「登录」。请注意这里的登录仅仅指客户端登录即时通讯服务,与应用层面的用户账户注册登录是不一样的。 - -默认情况下,即时通讯服务允许一个 `clientId` 在多个不同的设备上登录,也允许一个设备上有多个 `clientId` 同时登录。如果使用场景中需要限制用户只在一处登录,可以在登录时明确设置当前设备的 tag,当云端检测到同一个 tag 的设备出现冲突时,会自动踢出已存在设备上的登录状态。开发者可以根据自己的应用场景选择合适的登录方式。 - -客户端通过即时通讯 SDK 完成登录后,开发者就不必再关心底层的网络连接状态,SDK 会自动为开发者保持连接状态,并在网络状态变化之后进行自动重连。当应用在前台时 SDK 会保持连接,而当应用退到后台时连接会自动断开。我们默认会激活平台原生的推送服务,来保证消息及时送达。 - -### 对话(Conversation) - -用户登录之后,与其他人进行消息沟通,即为开启了一个「对话(`Conversation`)」。在即时通讯服务中,「对话」包含了沟通的用户群体(成员),也是所有消息依托的媒介:消息都是由某一个 Client 发往一个「对话」。终端用户在开始聊天之前,需要先创建或者加入一个对话,然后再邀请其他人进来(可选),之后所有参与者在这个对话内进行交流。 - -用户每创建一个对话,就会在云端的 `_Conversation` 表中增加一条记录,可以进入 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 数据存储 > 结构化数据** 来查看该数据。一个「对话」在创建之后,我们还可以给他指定一些应用层的属性,例如名字、成员、以及自定义的扩展属性,等等。对话的各个属性与 `_Conversation` 表中字段的对应关系为: - -| 属性名 | 表字段 | 类型 | 约束 | 说明 | -| ---------------- | ---------- | --------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `name` | `name` | `String` | 可选 | 对话的名字,可为群组命名。 | -| `attributes` | `attr` | `Object` | 可选 | 自定义属性,供开发者扩展使用。 | -| `conversationId` | `objectId` | `String` | | 对话 ID(只读),由云端为该对话生成的一个全局唯一的 ID。 | -| `creator` | `c` | `String` | | 对话创建者的 `clientId`(只读)。 | -| `members` | `m` | `Array` | | 普通对话的所有参与者(仅针对普通对话,聊天室和系统对话并不支持持久化的成员列表,具体的开发指南中会有详细解释)。 | -| `mute` | `mu` | `Array` | | 将对话设为静音的参与者,这部分参与者不会收到推送(仅对 iOS 以及开启了混合推送的 Android 用户有效,具体的开发指南中会有详细解释)。 | -| `lastMessageAt` | `lm` | `Date` | | 对话中最后一条消息的发送或接收时间。 | -| `transient` | `tr` | `Boolean` | 可选 | 对话类型标志,是否为聊天室,后面会说明。 | -| `system` | `sys` | `Boolean` | 可选 | 对话类型标志,是否是系统对话,后面会说明。 | -| `unique` | `unique` | `Boolean` | 可选 | 内部字段,对话类型标志,标记根据成员原子创建的对话,后面会说明。 | - -虽然我们统一使用 `_Conversation` 表来存储所有对话,但是根据业务场景的不同我们 SDK 里面提供了多种不同的对话类型可供选择。 - -#### 业务场景的需求 - -在解释对话类型之前,我们先列举一下即时通讯可能的使用场景。 - -- **单聊/私聊** - - 就是两个 Client 之间的对话,公开与否(能否让其他人看到这个对话存在)由应用层自己控制。通常的业务场景里它是私密的,并且加入新的成员之后,会切换成新的群聊(当然,也可以依然不离开当前对话,这一点还是由应用层来决定)。 - -- **群聊** - - 就是两个(含)以上 Client 之间的对话,通常可以添加和删除成员,并且会赋予群聊一个名字,例如「家人群」、「朋友群」、「部门同事群」等等。随着成员的减少,群聊也可能只有两个甚至一个成员(成员的多少并不是区分群聊和单聊的关键)。群聊能否公开(譬如支持名字搜索),由应用自己决定。 - -- **聊天室** - - 游戏中的世界聊天,直播产品使用的开放聊天室、弹幕、网页直播等都可以抽象成开放「聊天室」,它与群聊类似,都是多人参与的群组,但是也有一些区别:其一在于聊天室人数可能远大于群聊人数;其二在于聊天室强调的是在线人数,所有参与者进入聊天界面就算加入,关闭界面就算退出,所以聊天室不需要离线消息和推送通知,在线成员数比具体成员列表更有意义。 - -- **公众号、机器人** - - 很多产品都会加入类似微信的公众号、服务号的功能,让一些特殊的账号可以给订阅用户发全员广播,也可以和特定用户进行一对一的信息交流。也有一些业务需要实现一个智能机器人的功能,可以自动与其他用户进行交流,解决客服或者问答类的需求。 - -- **临时客服通道** - - 有一些客服系统,会为每一个反馈问题的终端用户建立一个与在席客服的临时沟通渠道,在通道内用户和客服人员如单聊一般进行沟通,随着问题的解决自动关闭该通道。 - -即时通讯系统设计了四种类型的「对话」来满足不同的需求,下面我们看看如何把上面的业务场景映射到具体的「对话」类型。 - -#### 普通对话(Conversation) - -这是我们使用最多的「对话」,一般的单聊和群聊都可以通过它来实现。普通对话支持的功能有: - -- 成员之间发送和接收消息 -- 允许增加、删除成员(最大成员数不超过 500),且会全局通知成员变动事件 -- 支持查询成员在线状态 -- 支持更多的消息收发选项,例如 @ 成员提醒消息、撤回和修改消息、暂态消息、消息送达和已读的回执通知、Will 消息、离线推送通知等等 -- 部分成员离线状态下,可以收到消息推送通知,并且上线之后会进行消息同步,确保不丢消息 -- 消息记录自动保存在云端,支持多种查询方式 - -建议:开发者将单聊/群聊、私密/公开等属性存入到 `Conversation.attributes` 之中,在应用层进行区分。 - -#### 聊天室(Chat Room) - -专门用来处理不限人数的「聊天室」的这种需求的「对话」(在老版本 SDK 中也叫 Transient Conversation,在 `_Conversation` 表中,以 `tr` 为 `true` 来标记)。与普通对话一样,它支持创建、自身主动加入、自身主动退出对话等操作,消息记录会被保存并可供获取,但其不同之处在于: - -- **不限成员上限**,没有固定成员概念,加入即为成员,断线即为退出(`m` 列将被忽略) -- 不支持查询成员列表,你可以通过相关 API **查询在线人数** -- 不支持离线消息、离线推送通知、消息回执等功能 -- 没有成员加入、离开的通知 -- 不支持邀请加入、踢出成员这两个操作 -- 一个用户一次登录只能加入一个聊天室,加入新的聊天室后会自动离开旧的聊天室 -- 加入之后如果半小时内断网重连会自动加入原聊天室,超过这个时间则需要重新加入 - -建议:虽然「聊天室」不限制成员数量,但从实际经验来看,如果人数过多,那么聊天室内消息被放大的效果会非常明显,对于终端用户而言即表现为过量消息不断刷屏,反而影响用户体验。我们建议每个聊天室的上限人数控制在 **5000** 人左右。开发者可以考虑从应用层面将大聊天室拆分成多个较小的聊天室。 - -#### 系统对话(System Conversation) - -这是用于实现智能机器人、公众号、服务账号等场景的「对话」,也可以用作发送应用内通知的通道(在 `_Conversation` 表中,以 `sys` 为 `true` 来标记)。这种对话具有以下特点: - -- 加入即订阅,离开即退订,订阅人数没有限制(`m` 列将被忽略) -- 系统对话的创建必须由服务端发起,在客户端仅允许订阅/取消订阅一个已经存在的系统对话 -- 可以通过系统对话给所有订阅者发送全局消息,也可以单独某一个或者某几个用户发定向消息 -- 用户给系统对话发送的上行消息是单向的,消息和相关信息会存储在数据存储中的 `_SysMessage` 表,并不会被其他订阅用户收到 -- 开发者可以配置 Hook 地址接收用户发给系统对话的消息,并利用 REST API 发消息回复 - -#### 临时对话(Temporary Conversation) - -临时对话的数据不会被保存到 `_Conversation` 表中,它解决的是一种特殊的聊天场景: - -- 对话存续时间短 -- 聊天参与的人数较少(最多为 10 个 Client) -- 聊天记录的存储不是强需求 - -这种对话场景,诸如电商售前和售后在线聊天的客服系统,我们推荐使用临时对话。与普通对话相比,它具有如下特点: - -- 不支持消息静音/取消静音的操作 -- 无法更新对话属性 -- 其他消息收发与成员查询操作,和普通对话完全一样 - -建议:临时对话在使用上与普通对话类似,其最大特点是较短的有效期(不会被保存到 `_Conversation` 表中),这带来的优势是可以 **减轻对话的持久化存储在服务端占用的存储资源规模**,从而 **降低开发者的使用成本**。 - -#### 不同类型的对比总结 - -| 对话类型 | 使用场景 | 成员管理 | 收发消息 | 消息记录 | -| ------------ | ------------------------------------------ | ------------------------------------------------------------ | ----------------------------------------------------------------------- | -------- | -| **普通对话** | 单聊、群聊 | 成员持久化保存,最高支持 500 个成员 | 只有成员可以收发消息 | 支持 | -| **聊天室** | 聊天室、弹幕、网页实时评论 | 没有持久化的成员数据,自主加入,不支持邀请,成员数量没有限制 | 所有用户都可以发消息,当前在线的成员可以收到消息 | 支持 | -| **系统对话** | 公众号、机器人、下发加好友通知、自定义消息 | 没有成员概念,开发者维护订阅关系,订阅人数没有上限 | 开发者通过 API 给特定用户发消息,支持业务方配置 Web Hook 来备份处理消息 | 支持 | -| **临时对话** | 临时客服通道 | 成员固定,无法增加/删除成员 | 只有成员可以收发消息 | 不支持 | - -### 消息(Message) - -即时通讯服务中单次交互的数据单元。用户可以一次传输不超过 **5 KB** 的消息数据。即时通讯系统对消息格式没有任何要求,允许开发者传输任何基于文本的消息数据,开发者可以在文本协议基础上定义自己的应用层协议。 - -根据发送参数的不同,消息可分为「普通消息」和「暂态消息」。云端对于普通消息会提供接收回执、自动持久化存储、离线推送等功能。但对暂态消息,则不会被自动保存,也不支持延迟接收,离线用户更不会收到推送通知。譬如聊天过程中「某某正在输入中…」这样的状态信息,就适合按照暂态消息来发送,而用户输入的正式消息,则应该用普通消息来发送。 - -我们对普通消息提供「至少一次」的到达保证,并且在官方 SDK 中支持对消息的去重,开发者无需关心。开发者可以通过 SDK 或 REST API 发送消息。SDK 通常用于最终用户发送消息,而 REST API 是开发者从云端发送消息的接口。当从 REST API 发送消息时,开发者可以指定消息的发送者、对话 ID,对于系统对话还可以指定消息的接收者。 - -#### 富媒体消息 - -为了方便开发者的使用,我们提供了几种封装好的基于 JSON 格式的富媒体消息类型(`TypedMessage`),譬如: - -- 文本(`TextMessage`) -- 图片(`ImageMessage`) -- 音频(`AudioMessage`) -- 视频(`VideoMessage`) -- 位置(`LocationMessage`) - -这些消息类型的层次关系为: - -![TypedMessage 继承自 Message。TextMessage、ImageMessage、AudioMessage、VideoMessage、LocationMessage 和其他消息类型继承自 TypedMessage。](/img/realtime_v2_message_types.svg) - -富媒体消息基于 JSON 格式,通过 REST API 发送时需要传入[符合特定格式的 JSON 字符串](/sdk/im/guide/rest/#富媒体消息格式)。 -使用客户端 SDK 发送消息时,SDK 会自动完成相应的转换。 - -| 属性 | 约束 | 说明 | -| ---------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `_lctype` | | 富媒体消息的类型
    消息类型
    文本消息-1
    图像消息-2
    音频消息-3
    视频消息-4
    位置消息-5
    文件消息-6
    以上类型均使用负数,所有正数留给自定义扩展类型使用,0 作为「没有类型」被保留起来。 | -| `_lctext` | | 富媒体消息的文字说明 | -| `_lcattrs` | | JSON 字符串,用来给开发者存储自定义属性。 | -| `_lcfile` | | 如果是包含了文件(图像、音频、视频、通用文件)的消息 ,`_lcfile` 就包含了它的文件实体的相关信息。 | -| `url` | | 文件在上传之后的物理地址(注意,绑定或换绑自定义域名后,历史消息中的 url 不会更新) | -| `objId` | 可选 | 文件服务中对应文件的 objectId | -| `metaData` | 可选 | 文件的元数据 | - -以上为所有类型的富媒体消息共有的属性。 - -开发者可以基于我们的框架,方便地扩展出自己的消息类型。 - -#### 与消息相关的其他功能需求 - -前面章节说明功能特性的时候,我们提到过,在正常收发消息之外我们还支持: - -- 带有提醒功能的 @ 消息(如微信里面的 @ 某人) -- 消息的撤回和修改 -- 消息内容的实时过滤 -- 消息送达和对方已读的回执通知 -- 群聊里面为了避免过于干扰,允许用户开启消息免打扰开关 -- 发送譬如聊天过程中「某某正在输入…」这样的状态信息 -- 在消息接收方离线时,自动转为推送通知(Push Notification) -- 对于大型的开放聊天室,为了防止重要消息被淹没或丢弃,支持不同优先级的发送选项,确保重要的消息能优先、迅速送达客户端 - -具体的使用方法可以参考文档[即时通讯开发指南第二篇](/sdk/im/guide/intermediate/)的《消息收发的更多方式》一节和[第三篇](/sdk/im/guide/senior/)的《消息的内容过滤》一节。 - -## 系统限制 - -- 对于客户端主动发起的操作会按照操作类型限制其频率。发消息操作限制为 **每分钟 60 次**,历史消息查询操作限制为 **每分钟 120 次**,其他类型操作包括加入对话、离开对话、登录服务、退出服务等均限制为 **每分钟 30 次**。当调用超过限制时,云端会拒绝响应这些超限的操作,这样如果操作本由 SDK 发起则表现为不会走回调。如果使用 REST API 发起各种操作,则不会受到上述频率的限制。 -- 应用全局服务器下发消息速度默认最高可达到每秒钟 160000 次,超过部分会被服务器丢弃。如果你的应用会超过此限制,请提交工单联系我们。 -- 客户端发送的单条消息大小不得超过 5 KB(`pushData` 等附加信息也计入消息大小)。 -- 单个普通对话的成员上限为 500 个,如果你通过数据存储 API 向 `m` 字段加入了超过 500 个 ID,我们只会使用其中的前 500 个。 -- 请不要使用相同的 ID 在大量设备上同时登录,如果系统检测到某个 ID 同时在超过 5 个不同的 IP 上登录,会认为此 ID 是重复使用的 ID,之后此 ID 当日的每次登录会按照「ID + IP」的组合作为计费的独立用户。 -- 如果单个用户有超过 50 个的对话存在未接收的离线消息,那么当该用户登录时服务端只会 **随机** 下发 50 个对话的离线消息或未读消息数量。也就是说服务端不会再下发超出对话数量限制的那部分离线消息,也不会下发离线消息数量,离线消息不会丟失但需要从历史记录中拉取得到。 -- 单个对话未接收的离线消息数最多 100 条,超过后,系统会以先入先出方式存储新的离线消息,同时移除当前对话存储的最早的一条离线消息。被移除的离线消息可以通过历史消息记录查询,但不会产生离线消息提醒,也不会计入对话的未读消息计数。 -- 切换文件访问地址(**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 数据存储 > 文件 > 设置**)不会自动更新历史消息(包括富媒体消息)中的 URL。 -- 调用消息操作有关的 REST API 有请求频率以及总量的限制,详见[即时通讯 REST API](/sdk/im/guide/rest/)。 - -### 对话的有效期 - -一个对话(包括普通、暂态、系统对话)如果 **6 个月内** 没有通过 SDK 或者 REST API 发送过新的消息,或者它在 `_Conversation` 表中的任意字段没有被更新过,即被视为 **不活跃对话**,云端会自动将其删除。(查询对话的消息记录并不会更新 `_Conversation` 表,所以只查询不发送消息的对话仍会被视为不活跃对话。) - -不活跃的对话被删除后,当客户端再次通过 SDK 或 REST API 对其发送消息时,会遇到 `4401 INVALID_MESSAGING_TARGET` 错误,表示该对话已经不存在了。同时,与该对话相关的消息历史也无法获取。 - -反之,活跃的对话会一直保存在云端。 - -### 消息的有效期 - -一个对话的消息记录会在云端保留 **6 个月**,也就是说一个对话可以查询到半年之内的历史消息记录。开发者可以付费来延长这一期限,如有需要,请提交工单联系技术支持。 -你也随时可以通过 REST API 将聊天记录同步到自己的服务器上。 - -## 即时通讯 Hook 机制 - -详见[即时通讯开发指南第四篇](/sdk/im/guide/systemconv/)的《万能的 Hook 机制》一节。 - -## 价格 - -客户端调用即时通讯服务 SDK,标准版应用按登录用户计费,**低于 500 人 / 天 免费,超出部分,则每日 15 元 / 万人**。 - -* 使用 REST API 发送即时通讯消息也是收费的。计费标准就是数据存储 API 调用费用标准(每万次 1.0 元)。此项计费在控制台付费账单明细中对应扣费项目是:「数据存储(API 请求)」。 -* 多媒体消息如果使用到文件服务,正常收取文件费用,文件服务收费标准参考[官网价格页面](https://developer.taptap.cn/product-intro/price)。 -* 无论使用 SDK 调用即时通讯服务,还是使用 REST API 调用即时通讯服务,所有访问 `_Conversation` 表(即会话表)的请求,都会产生数据存储费用。即 `_Conversation` 表的请求按照数据存储 API 调用标准收费(每万次 1.0 元)。此项计费在控制台付费账单明细中对应扣费项目是:「数据存储(API 请求)」。 - -## 开发指南 - -按功能区分,可以参考如下文档: - -- [一,从简单的单聊、群聊、收发图文消息开始](/sdk/im/guide/beginner/) -- [二,消息收发的更多方式,离线推送与消息同步,多设备登录](/sdk/im/guide/intermediate/) -- [三,安全与签名、玩转聊天室和临时对话](/sdk/im/guide/senior/) -- [四,详解消息 hook 与系统对话](/sdk/im/guide/systemconv/) - -具体的 REST API 规范,可以参考: - -- [即时通讯 REST API 使用指南](/sdk/im/guide/rest/) diff --git a/leancloud/docs/sdk/im/guide/rest.mdx b/leancloud/docs/sdk/im/guide/rest.mdx deleted file mode 100644 index 2ce47d8b7..000000000 --- a/leancloud/docs/sdk/im/guide/rest.mdx +++ /dev/null @@ -1,1751 +0,0 @@ ---- -title: 即时通讯 REST API -sidebar_position: 5 -slug: /sdk/im/guide/rest ---- - - - - - -## 概览 - -请求的 Base URL 可以在**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置**查看。 -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 -请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,详见[数据存储 REST API 使用详解](/sdk/storage/guide/rest/)中《请求格式》一节的说明。 - -`_Conversation` 表包含一些内置的关键字段定义了对话的属性、成员等,单聊、群聊、聊天室、服务号均在此表中,详见[即时通讯总览](/sdk/im/guide/overview/)的《对话》一节。 -不过为了避免出现数据不一致问题,我们不推荐调用数据存储相关的 API 直接操作 `_Conversation` 表中的数据。 - -当前的 API 版本为 `1.2`: - -- 单聊、群聊相关 API 以 `rtm/conversations` 标示 -- 聊天室相关 API 以 `rtm/chatrooms` 标示,在 `_Conversation` 表内用字段 `tr` 为 true 标示。 -- 服务号相关 API 以 `rtm/service-conversations` 标示,在 `_Conversation` 表内用字段 `sys` 为 true 标示。 - -除此之外,与 client 相关的请求以 `rtm/clients` 标示。 -最后,一些全局性质的 API 直接以 `rtm/{function}` 标示,如 `rtm/all-conversations` 可查询所有类型的对话。 - -## 单聊、群聊 - -### 创建对话 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"My First Conversation", "m": ["BillGates", "SteveJobs"], "unique": true}' \ - https://{{host}}/1.2/rtm/conversations -``` - -上面的例子会创建一个最简单的对话,包括两个 client ID 为 BillGates 和 SteveJobs 的初始成员。对话创建成功会返回 objectId,即即时通讯中的对话 ID,客户端就可以通过这个 ID 发送消息了。新创建的对话可以在 `_Conversation` 表内找到。 -对话的字段可参考[即时通讯总览](/sdk/im/guide/overview/)的《对话》一节。 -传入 `"unique": true` 参数可以保证对话的唯一性。 - -返回 - -```json -{ - "unique": true, - "updatedAt": "2020-05-26T06:42:31.492Z", - "name": "My First Conversation", - "objectId": "5eccba570d3a42c5fd4e25c3", - "m": ["BillGates", "SteveJobs"], - "createdAt": "2020-05-26T06:42:31.482Z", - "uniqueId": "6c7b0e5afcae9aa1139a0afa25833dec" -} -``` - -需要注意,群聊与单聊的唯一区别是 client 数量,API 层面是一致的。 - -### 查询对话 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'where={"name": "first conversation"}' \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/conversations -``` - -| 参数 | 约束 | 说明 | -| ----- | ---- | ------------------------------------------------------------------------ | -| skip | 可选 | -| limit | 可选 | 与 skip 联合使用实现分页 | -| where | 可选 | 参见[数据存储 REST API 使用详解](/sdk/storage/guide/rest/)的《查询》一节 | - -返回 - -```json -{ - "results": [ - { - "name": "test conv1", - "m": ["tom", "jerry"], - "createdAt": "2018-01-17T04:15:33.386Z", - "updatedAt": "2018-01-17T04:15:33.386Z", - "objectId": "5a5ecde6c3422b738c8779d7" - } - ] -} -``` - -### 更新对话 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"Updated Conversation"}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id} -``` - -`_Conversation` 表除 m 字段均可通过这个接口更新。 - -返回 - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### 删除对话 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id} -``` - -返回 - -```json -{} -``` - -### 增加成员 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/members -``` - -返回 - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### 移除成员 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/members -``` - -返回 - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### 查询成员 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/members -``` - -返回 - -```json -{ "result": ["client1", "client2"] } -``` - -### 增加静音用户 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/mutes -``` - -| 参数 | 说明 | -| ---------- | -------------------------- | -| client_ids | 要静音的 `Client ID`,数组 | - -返回 - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### 移除静音用户 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/mutes -``` - -返回 - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### 查询静音用户 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/mutes -``` - -返回 - -```json -{ "result": ["client1", "client2"] } -``` - -### 单聊、群聊-发消息 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages -``` - -**注意**,由于这个接口的管理性质,当你通过这个接口发送消息时,我们不会检查 **from_client** 是否有权限给这个对话发送消息,而是统统放行,请谨慎使用这个接口。 -如果你在应用中使用了我们内部定义的富媒体消息格式,在发送消息时 **message** 字段需要遵守相应的格式要求。 - -| 参数 | 约束 | 说明 | -| ------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| from_client | 必填 | 消息的发件人 client Id | -| message | 必填 | 消息内容(这里的消息内容的本质是字符串,但是我们对字符串内部的格式没有做限定,理论上开发者可以随意发送任意格式,只要大小不超过 5 KB 限制即可。) | -| transient | 可选 | 是否为暂态消息,默认 false | -| no_sync | 可选 | 默认情况下消息会被同步给在线的 from_client 用户的客户端,设置为 true 禁用此功能。 | -| push_data | 可选 | 以消息附件方式设置本条消息的离线推送通知内容。如果目标接收者使用的是 iOS 设备并且当前不在线,我们会按照该参数填写的内容来发离线推送。请参看[即时通讯开发指南第二篇](/sdk/im/guide/intermediate/)的《离线推送通知》一节的说明。 | -| priority | 可选 | 定义消息优先级,可选值为 high、normal、low,分别对应高、中、低三种优先级。该参数大小写不敏感,默认为中优先级 normal。本参数仅对暂态消息或聊天室的消息有效,高优先级下在服务端与用户设备的连接拥塞时依然排队。 | -| mention_all | 可选 | 布尔类型,用于提醒对话内所有成员注意本消息。 | -| mention_client_ids | 可选 | 数组类型,表示需要提醒注意本消息的对话内成员 client_id 列表,最多能包含 20 个 client Id。 | - -返回说明: - -默认情况下发送消息 API 使用异步的方式,调用后返回消息 id 和接收消息的服务器时间戳,例如 `{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151}`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 查询历史消息 - -该接口要求使用 master key。 -为了保证获取聊天记录的安全性,可以开启签名认证,具体可以参考[即时通讯开发指南第三篇](/sdk/im/guide/senior/)的《安全与签名》一节。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages -``` - -| 参数 | 约束 | 说明 | -| -------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------ | -| msgid | 可选 | 起始的消息 id,**使用时必须加上对应消息的时间戳 timestamp 参数,作为查询的起点** | -| timestamp | 可选 | 查询起始的时间戳。默认是当前时间,单位是毫秒 | -| till_msgid | 可选 | 查询终止的消息 id。**使用时必须加上消息的时间戳 till_timestamp 参数,作为查询的终点** | -| till_timestamp | 可选 | 查询终止的时间戳,默认为 0,单位是毫秒 | -| include_start | 可选 | 是否包含由 timestamp 与 msgid 确定的起始消息。布尔值,默认为 false | -| include_stop | 可选 | 是否包含由 till_timestamp 与 till_msgid 确定的终止消息。布尔值,默认为 false | -| reversed | 可选 | 以默认排序(默认按时间降序)相反的方向返回结果,这时 till_timestamp 默认为当前时间戳,timestamp 默认为 0。布尔值,默认为 false | -| limit | 可选 | 返回条数限制,可选,默认 100 条,最大 1000 条 | -| client_id | 可选 | 查看者 id(签名参数) | -| nonce | 可选 | 签名随机字符串(签名参数) | -| signature_ts | 可选 | 签名时间戳(签名参数),单位是毫秒 | -| signature | 可选 | 签名(签名参数) | - -本接口时间参数较多,这里举一示例供大家参考。比如某对话内有三条消息,id 分别为 id1、id2、id3,发消息的时间分别是 t1、t2、t3(t1 < t2 < t3),下面列举出不同参数组合的查询结果(空白表示使用默认值): - -| timestamp | msgid | till_timestamp | till_msgid | include_start | include_stop | reversed | 结果 | -| --------- | ----- | -------------- | ---------- | ------------- | ------------ | -------- | ------- | -| t3 | id3 | t1 | id1 | | | | id2 | -| t3 | id3 | t1 | id1 | true | | | id3 id2 | -| t3 | id3 | t1 | id1 | | true | | id2 id1 | -| t1 | id1 | t3 | id3 | | | true | id2 | -| t1 | id1 | t3 | id3 | true | | true | id1 id2 | -| t1 | id1 | t3 | id3 | | true | true | id2 id3 | - -返回数据格式,JSON 数组,默认按消息记录从新到旧排序,设置请求参数 `reversed` 后以相反的方向排序。 - -返回: - -```json -[ - { - "timestamp": 1408008498571, - "conv-id": "219946ef32e40c515d33ae6975a5c593", - "data": "今天天气不错!", - "from": "u111872755_9d0461adf9c267ae263b3742c60fa", - "msg-id": "vdkGm4dtRNmhQ5gqUTFBiA", - "is-conv": true, - "is-room": false, - "to": "5541c02ce4b0f83f4d44414e", - "bin": false, - "from-ip": "202.117.15.217" - } - // ... -] -``` - -如需查询某个用户发出的消息,可以调用 `GET /rtm/clients/{client_id}/messages` 这个接口。 -如需查询整个应用的历史消息,可以调用 `GET /rtm/messages` 这个接口。 - -### 单聊、群聊-修改消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages/{message_id} -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| message | 必填 | 消息体 | -| timestamp | 必填 | 消息的时间戳 | - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 单聊、群聊-撤回消息 - -该接口要求使用 master key。需要相应 SDK 的支持,具体可参考之前的修改消息接口。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages/{message_id}/recall -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| timestamp | 必填 | 消息的时间戳 | - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 删除消息 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'from_client=some-client-id' \ - --data-urlencode 'timestamp=123' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages/{message_id} -``` - -注意,该接口仅删除服务端的消息,对客户端无影响。 - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| timestamp | 必填 | 消息的时间戳 | - -返回: - -```json -{} -``` - -## 聊天室 - -### 创建聊天室 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"My First Chatroom"}' \ - https://{{host}}/1.2/rtm/chatrooms -``` - -对话的字段可参考[即时通讯总览](/sdk/im/guide/overview/)的《对话》一节。 - -返回 - -```json -{ - "objectId": "5a5d7432c3422b31ed845e75", - "createdAt": "2018-01-16T03:40:32.814Z" -} -``` - -### 查询聊天室 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'where={"name": "chatroom"}' \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/chatrooms -``` - -| 参数 | 约束 | 说明 | -| ----- | ---- | ---------------------------------------------------------------------------- | -| skip | 可选 | -| limit | 可选 | 与 skip 联合使用实现分页 | -| where | 可选 | 请参考[数据存储 REST API 使用详解](/sdk/storage/guide/rest/)的《查询》一节。 | - -返回 - -```json -{ - "results": [ - { - "name": "My First Chatroom", - "createdAt": "2018-01-17T04:15:33.386Z", - "updatedAt": "2018-01-17T04:15:33.386Z", - "objectId": "5a5ecde6c3422b738c8779d7" - } - ] -} -``` - -### 更新聊天室 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"Updated Chatroom"}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id} -``` - -返回 - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### 删除聊天室 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id} -``` - -返回 - -```json -{} -``` - -### 随机获取在线成员 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/members -``` - -返回 - -```json -{ "result": ["clientid1", "clientid2", "clientid3"] } -``` - -### 查询在线成员数 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/members/online-count -``` - -返回 - -```json -{ "result": 3 } -``` - -### 聊天室-发消息 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages -``` - -**注意**,由于这个接口的管理性质,当你通过这个接口发送消息时,我们不会检查 **from_client** 是否有权限给这个聊天室发送消息,而是统统放行,请谨慎使用这个接口。 -如果你在应用中使用了我们内部定义的富媒体消息格式,在发送消息时 **message** 字段需要遵守相应的格式要求。 -此外,聊天室目前**不支持**将消息同步发送给在线的 **from_client**。 - -| 参数 | 约束 | 说明 | -| ------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| from_client | 必填 | 消息的发件人 client Id | -| message | 必填 | 消息内容(这里的消息内容的本质是字符串,但是我们对字符串内部的格式没有做限定,理论上开发者可以随意发送任意格式,只要大小不超过 5 KB 限制即可。) | -| transient | 可选 | 是否为暂态消息,默认 false | -| priority | 可选 | 定义消息优先级,可选值为 high、normal、low,分别对应高、中、低三种优先级。该参数大小写不敏感,默认为中优先级 normal。本参数仅对暂态消息或聊天室的消息有效,高优先级下在服务端与用户设备的连接拥塞时依然排队。 | -| mention_all | 可选 | 布尔类型,用于提醒对话内所有成员注意本消息。 | -| mention_client_ids | 可选 | 数组类型,表示需要提醒注意本消息的对话内成员 client_id 列表,最多能包含 20 个 client Id。 | - -返回说明: - -默认情况下发送消息 API 使用异步的方式,调用后返回消息 id 和接收消息的服务器时间戳,例如 `{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151}`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 查询历史消息 - -该接口要求使用 master key。 -为了保证获取聊天记录的安全性,可以开启签名认证,具体可以参考[即时通讯开发指南第三篇](/sdk/im/guide/senior/)的《安全与签名》一节。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/messages -``` - -| 参数 | 约束 | 说明 | -| -------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------ | -| msgid | 可选 | 起始的消息 id,**使用时必须加上对应消息的时间戳 timestamp 参数,作为查询的起点** | -| timestamp | 可选 | 查询起始的时间戳。默认是当前时间,单位是毫秒 | -| till_msgid | 可选 | 查询终止的消息 id。**使用时必须加上消息的时间戳 till_timestamp 参数,作为查询的终点** | -| till_timestamp | 可选 | 查询终止的时间戳,默认为 0,单位是毫秒 | -| include_start | 可选 | 是否包含由 timestamp 与 msgid 确定的起始消息。布尔值,默认为 false | -| include_stop | 可选 | 是否包含由 till_timestamp 与 till_msgid 确定的终止消息。布尔值,默认为 false | -| reversed | 可选 | 以默认排序(默认按时间降序)相反的方向返回结果,这时 till_timestamp 默认为当前时间戳,timestamp 默认为 0。布尔值,默认为 false | -| limit | 可选 | 返回条数限制,可选,默认 100 条,最大 1000 条 | -| client_id | 可选 | 查看者 id(签名参数) | -| nonce | 可选 | 签名随机字符串(签名参数) | -| signature_ts | 可选 | 签名时间戳(签名参数),单位是毫秒 | -| signature | 可选 | 签名(签名参数) | - -本接口时间参数较多,这里举一示例供大家参考。比如某对话内有三条消息,id 分别为 id1、id2、id3,发消息的时间分别是 t1、t2、t3(t1 < t2 < t3),下面列举出不同参数组合的查询结果(空白表示使用默认值): - -| timestamp | msgid | till_timestamp | till_msgid | include_start | include_stop | reversed | 结果 | -| --------- | ----- | -------------- | ---------- | ------------- | ------------ | -------- | ------- | -| t3 | id3 | t1 | id1 | | | | id2 | -| t3 | id3 | t1 | id1 | true | | | id3 id2 | -| t3 | id3 | t1 | id1 | | true | | id2 id1 | -| t1 | id1 | t3 | id3 | | | true | id2 | -| t1 | id1 | t3 | id3 | true | | true | id1 id2 | -| t1 | id1 | t3 | id3 | | true | true | id2 id3 | - -返回数据格式,JSON 数组,默认按消息记录从新到旧排序,设置请求参数 `reversed` 后以相反的方向排序。 - -返回: - -```json -[ - { - "timestamp": 1408008498571, - "conv-id": "219946ef32e40c515d33ae6975a5c593", - "data": "今天天气不错!", - "from": "u111872755_9d0461adf9c267ae263b3742c60fa", - "msg-id": "vdkGm4dtRNmhQ5gqUTFBiA", - "is-conv": true, - "is-room": false, - "to": "5541c02ce4b0f83f4d44414e", - "bin": false, - "from-ip": "202.117.15.217" - } - // ... -] -``` - -如需查询某个用户发出的消息,可以调用 `GET /rtm/clients/{client_id}/messages` 这个接口。 -如需查询整个应用的历史消息,可以调用 `GET /rtm/messages` 这个接口 - -### 聊天室-修改消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages/{message_id} -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| message | 必填 | 消息体 | -| timestamp | 必填 | 消息的时间戳 | - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 聊天室-撤回消息 - -该接口要求使用 master key。需要相应 SDK 的支持,具体可参考上面的「修改消息」接口。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages/{message_id}/recall -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| timestamp | 必填 | 消息的时间戳 | - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 删除消息 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'from_client=some-client-id' \ - --data-urlencode 'timestamp=123' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages/{message_id} -``` - -注意,该接口仅删除服务端的消息,对客户端无影响。 - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| timestamp | 必填 | 消息的时间戳 | - -返回: - -```json -{} -``` - -## 服务号 - -### 创建服务号 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"My First Service-conversation"}' \ - https://{{host}}/1.2/rtm/service-conversations -``` - -对话的字段可参考[即时通讯总览](/sdk/im/guide/overview/)的《对话》一节。 - -返回 - -```json -{ - "objectId": "5a5d7432c3422b31ed845e75", - "createdAt": "2018-01-16T03:40:32.814Z" -} -``` - -### 查询服务号 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'where={"name": "service"}' \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/service-conversations -``` - -| 参数 | 约束 | 说明 | -| ----- | ---- | ---------------------------------------------------------------------------- | -| skip | 可选 | -| limit | 可选 | 与 skip 联合使用实现分页 | -| where | 可选 | 请参考[数据存储 REST API 使用详解](/sdk/storage/guide/rest/)的《查询》一节。 | - -返回 - -```json -{ - "results": [ - { - "name": "My First Service-conversation", - "createdAt": "2018-01-17T04:15:33.386Z", - "updatedAt": "2018-01-17T04:15:33.386Z", - "objectId": "5a5ecde6c3422b738c8779d7" - } - ] -} -``` - -### 更新服务号 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"Updated Service-conversation"}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id} -``` - -返回 - -```json -{ - "updatedAt": "2018-01-16T03:40:37.683Z", - "objectId": "5a5d7433c3422b31ed845e76" -} -``` - -### 删除服务号 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id} -``` - -返回 - -```json -{} -``` - -### 订阅服务号 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_id":"client_id"}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers -``` - -返回 - -```json -{} -``` - -### 取消订阅 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/{client_id} -``` - -返回 - -```json -{} -``` - -### 遍历查询订阅者 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers -``` - -| 参数 | 约束 | 说明 | -| --------- | ---- | ------------------------------------------------------------------------------------------------------------ | -| limit | 可选 | 返回条数限制,默认是 50 条,最大 50 条。 | -| client_id | 可选 | 查询起始订阅者 client id,不填则从订阅者列表起始位置开始遍历。查询结果不会再包含当前指定的订阅者 client id。 | - -返回 - -```json -[ - { - "timestamp": 1491467841116, - "subscriber": "client id 1", - "conv_id": "55b871" - }, - { - "timestamp": 1491467852768, - "subscriber": "client id 2", - "conv_id": "55b872" - } - // ... -] -``` - -其中 timestamp 表示用户订阅系统对话的时间,subscriber 是订阅用户的 client id。如果一次没有获取完,需要从结果列表中取最后一个订阅者的 client id,作为 client_id 参数再次调用本接口以获取下一批订阅者列表。 - -### 查询订阅者数 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/count -``` - -返回 - -```json -{ "count": 100 } -``` - -### 给所有订阅者发消息 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/broadcasts -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------------------------------------------------------------------------- | -| from_client | 必选 | 消息的发件人 client ID | -| message | 必选 | 消息体 | -| push | 可选 | 附带的推送内容,如果设置,所有 iOS 和 Android 用户会收到这条推送通知。字符串或 JSON 对象 | - -返回: - -```json -{ "msg-id": "qNkRkFWOeSqP65S9fDyHJw", "timestamp": 1495431811151 } -``` - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 修改给所有订阅者发送的消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id} -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| message | 必填 | 消息体 | -| timestamp | 必填 | 消息的时间戳 | - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 撤回给所有订阅者发送的消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id}/recall -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| timestamp | 必填 | 消息的时间戳 | - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 给任意用户单独发消息 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages -``` - -**注意**,由于这个接口的管理性质,当你通过这个接口发送消息时,我们不会检查 **from_client** 是否有权限给这个服务号发送消息,而是统统放行,请谨慎使用这个接口。 -如果你在应用中使用了我们内部定义的富媒体消息格式,在发送消息时 **message** 字段需要遵守相应的格式要求。 - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| from_client | 必填 | 消息的发件人 client Id | -| to_clients | 必填 | 数组类型,表示接收消息的 client Id 列表,最多能包含 20 个 client Id | -| message | 必填 | 消息内容(这里的消息内容的本质是字符串,但是我们对字符串内部的格式没有做限定,
    理论上开发者可以随意发送任意格式,只要大小不超过 5 KB 限制即可。) | -| transient | 可选 | 是否为暂态消息,默认 false | -| no_sync | 可选 | 默认情况下消息会被同步给在线的 from_client 用户的客户端,设置为 true 禁用此功能。 | -| push_data | 可选 | 以消息附件方式设置本条消息的离线推送通知内容。如果目标接收者使用的是 iOS 设备并且当前不在线,我们会按照该参数填写的内容来发离线推送。请参看[即时通讯开发指南第三篇](/sdk/im/guide/senior/)的《离线推送通知》一节。 | -| priority | 可选 | 定义消息优先级,可选值为 high、normal、low,分别对应高、中、低三种优先级。该参数大小写不敏感,默认为中优先级 normal。本参数仅对暂态消息或聊天室的消息有效,高优先级下在服务端与用户设备的连接拥塞时依然排队。 | - -返回说明: - -默认情况下发送消息 API 使用异步的方式,调用后返回消息 id 和接收消息的服务器时间戳,例如 `{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151}`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 修改给用户单独发送的消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123, "to_clients":["a","b","c"]}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id} -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ----------------------------------------------------------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| message | 必填 | 消息体 | -| timestamp | 必填 | 消息的时间戳 | -| to_clients | 必填 | 数组类型,表示接收目标消息的 client Id 列表,最多能包含 20 个 client Id | - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 撤回给用户单独发送的消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123, "to_clients":["a","b","c"]}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id}/recall -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ----------------------------------------------------------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| timestamp | 必填 | 消息的时间戳 | -| to_clients | 必填 | 数组类型,表示接收目标消息的 client Id 列表,最多能包含 20 个 client Id | - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 删除给用户单独发送的消息 - -本接口要求使用 master key,并且只能删除订阅消息或给用户单独发送的消息,无法删除广播消息。 -广播消息请调用 `DELETE /1.2/rtm/broadcasts/{message_id}` 接口删除。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'from_client=some-client-id' \ - --data-urlencode 'timestamp=123' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/{client_id}/messages/{message_id} -``` - -注意,该接口仅删除服务端的消息,对客户端无影响。 - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| timestamp | 必填 | 消息的时间戳 | - -返回: - -```json -{} -``` - -### 查询服务号给某用户发的消息 - -该接口要求使用 master key。查询结果包含服务号发送的订阅广播消息也包含单独发送的消息。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/{client_id}/messages -``` - -参数和返回值与查询历史消息接口相同。 - -## 用户 - -### 查询在线成员 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/clients/check-online -``` - -| 参数 | 约束 | 说明 | -| ---------- | ---- | ----------------------------------- | -| client_ids | 必选 | 要查询的 client ID 列表,最多 20 个 | - -返回在线的 ID 列表 - -```json -{ "results": ["client1"] } -``` - -注意,该接口不判断用户是否存在,「用户不存在」视同「用户不在线」。 - -### 查询未读消息数 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'conv_id=...' \ - https://{{host}}/1.2/rtm/clients/{client_id}/unread-count -``` - -| 参数 | 约束 | 说明 | -| ------- | ---- | ----------------------------------------------------------- | -| conv_id | 可选 | 对话 ID,若不传此参数,查询 client 在所有对话中的未读消息数 | - -返回 - -```json -{ "count": 1 } -``` - -### 强制下线 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"reason": "why"}' \ - https://{{host}}/1.2/rtm/clients/{client_id}/kick -``` - -| 参数 | 约束 | 说明 | -| ------ | ---- | ---------------------------------- | -| reason | 可选 | 下线原因,字符串,不超过 20 个字符 | - -返回 - -```json -{} -``` - -### 查询订阅的服务号 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'conv_id=...' \ - --data-urlencode 'timestamp=...' \ - --data-urlencode 'limit=...' \ - --data-urlencode 'direction=...' \ - https://{{host}}/1.2/rtm/clients/{client_id}/service-conversations -``` - -| 参数 | 约束 | 类型 | 说明 | -| --------- | ---- | ------ | -------------------------------------------------------------------------------------------------------------------------------- | -| conv_id | 可选 | 字符串 | 查询起始服务号 id,不填则从订阅列表起始位置开始遍历。查询结果不会再包含本对话 | -| timestamp | 可选 | 数字 | 查询起始对话被订阅时间。虽然是可选字段但当提供 conv_id 时本字段必填,值必须为订阅 conv_id 参数所指定系统对话的时间,单位是毫秒 | -| limit | 可选 | 数字 | 返回条数限制,默认是 50 条 | -| direction | 可选 | 字符串 | 查询结果按时间排序方式,old 表示降序,new 表示升序,默认是 new。使用 old 则先返回最近订阅的对话,使用 new 则先返回最早订阅的对话 | - -返回目标用户订阅系统对话的列表: - -```json -[ - { "timestamp": 1482994126561, "subscriber": "XXX", "conv_id": "convId1" }, - { "timestamp": 1491467945277, "subscriber": "XXX", "conv_id": "convId2" } - // ... -] -``` - -其中 `timestamp` 表示用户订阅系统对话的时间,`subscriber` 是订阅用户的 client id。如果一次没有获取完,需要从结果列表中取最后一个服务号 ID 和订阅时间,分别作为 conv_id 和 timestamp 参数再次调用本接口以获取下一批订阅的系统对话。 - -### 查询用户发送消息 - -该接口要求使用 master key。 -使用这个接口可以查询某 client_id 在单聊、群聊与聊天室里发的消息。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/clients/{client_id}/messages -``` - -参数与返回值可以参考 `GET /1.2/rtm/conversations/{conv_id}/messages` 接口。 - -### 获取登录签名 - -本接口可以让使用了 LCUser 的应用方便快捷地实现登录认证。 -登录认证默认关闭,可以进入 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 即时通讯设置**,勾选 **登录启用签名认证** 进行开启。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'session_token=some-token' \ - https://{{host}}/1.2/rtm/clients/sign -``` - -| 参数 | 约束 | 说明 | -| ------------- | ---- | ---------------------- | -| session_token | 必选 | LCUser 的 sessionToken | - -返回 - -```json -{ - "signature": "bc884dbb617aab1efc228229210e487330abfc7d", - "nonce": "akywke3f28", - "client_id": "5fb4ff18d0deed36ea501c8a", - "timestamp": 1614237989966 -} -``` - -注意,虽然这是一个 GET 请求,但并不是幂等的,每次调用返回的签名都不相同。 - -为了方便用户进行细粒度控制,实现自定义功能(如黑名单),本接口提供了一个 hook `_rtmClientSign`,在验证 sessionToken 后去调用,传入的参数为 LCUser 构成的 JSON 对象: - -```json -{ - "email": "", - "sessionToken": "", - "updatedAt": "", // 格式:2017-07-11T07:58:10.149Z - "phone": "", - "objectId": "", - "username": "", - "createdAt": "", // 格式:2017-07-11T07:58:10.149Z - "emailVerified": true, // true/false - "mobilePhoneVerified": true // true/false -} -``` - -可以返回两类结果: - -```json -{"result": true} // 允许签名 -{"result": false, "error": "error message"} // 拒绝签名 -``` - -## 全局 API - -### 查询用户数 - -本接口会返回应用当前在线用户总数,以及当天有登录记录的独立用户总数。本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/stats -``` - -返回 - -```json -{ "result": { "online_user_count": 10212, "user_count_today": 1002324 } } -``` - -其中 `online_user_count` 表示当前应用在线用户总数,`user_count_today` 表示当天有登录记录的独立用户总数。 - -### 查询所有对话 - -本接口会返回所有的 单聊群聊/聊天室/服务号。在 `_Conversation` 表默认 ACL 权限下要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/all-conversations -``` - -| 参数 | 约束 | 说明 | -| ----- | ---- | ------------------------------------------ | -| skip | 可选 | -| limit | 可选 | 与 skip 联合使用实现分页 | -| where | 可选 | 参考《存储 REST API 指南》中的《查询》一节 | - -返回 - -```json -{ - "results": [ - { - "name": "conversation", - "createdAt": "2018-01-17T04:15:33.386Z", - "updatedAt": "2018-01-17T04:15:33.386Z", - "objectId": "5a5ecde6c3422b738c8779d7" - } - ] -} -``` - -### 全局广播 - -该接口可以给该应用所有 client 广播一条消息,每天最多 30 条。本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "1a", "message": "{\"_lctype\":-1,\"_lctext\":\"这是一个纯文本消息\",\"_lcattrs\":{\"a\":\"_lcattrs 是用来存储用户自定义的一些键值对\"}}", "conv_id": "..."}' \ - https://{{host}}/1.2/rtm/broadcasts -``` - -| 参数 | 约束 | 类型 | 说明 | -| ----------- | ---- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| from_client | 必填 | 字符串 | 消息的发件人 id | -| conv_id | 必填 | 字符串 | 发送到对话 id,仅限于服务号 | -| message | 必填 | 字符串 | 消息内容(这里的消息内容的本质是字符串,但是我们对字符串内部的格式没有做限定,理论上开发者可以随意发送任意格式,只要大小不超过 5 KB 限制即可。) | -| valid_till | 可选 | 数字 | 过期时间,UTC 时间戳(毫秒),最长为 1 个月之后。默认值为 1 个月后。 | -| push | 可选 | 字符串或 JSON 对象 | 附带的推送内容,如果设置,**所有** iOS 和 Android 用户会收到这条推送通知。 | -| transient | 可选 | 布尔值 | 默认为 false。该字段用于表示广播消息是否为暂态消息,暂态消息只会被当前在线的用户收到,不在线的用户再次上线后也收不到该消息。 | - -Push 的格式与《推送 REST API 指南》的《消息内容参数》一节中 `data` 下面的部分一致。如果你需要指定开发证书推送,需要在 push 的 json 中设置 `"_profile": "dev"`,例如: - -```json -{ - "alert": "消息内容", - "category": "通知分类名称", - "badge": "Increment", - "_profile": "dev" -} -``` - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 修改广播消息 - -该接口要求使用 master key。 - -广播消息修改仅对当前还未收到该广播消息的设备生效,如果目标设备已经收到了该广播消息则无法修改。请慎重发送广播消息。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id} -``` - -| 参数 | 约束 | 说明 | -| ----------- | ---- | ---------------------- | -| from_client | 必填 | 消息的发件人 client ID | -| message | 必填 | 消息体 | -| timestamp | 必填 | 消息的时间戳 | - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 删除广播消息 - -调用此 API 将删除已发布的广播消息,仅对还未收到广播消息的设备生效,已收到广播消息的设备无法删除消息。本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/broadcasts/{message_id} -``` - -| 参数 | 约束 | 说明 | -| ---------- | ---- | ----------------------- | -| message_id | 必填 | 要删除的消息 id,字符串 | - -成功则返回状态码 `200 OK`。 - -### 查询广播消息 - -调用此 API 可查询目前有效的广播消息。本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/broadcasts?conv_id={conv_id} -``` - -| 参数 | 约束 | 说明 | -| ------- | ---- | ---------------------- | -| conv_id | 必填 | 服务号 id | -| limit | 可选 | 返回消息条数 | -| skip | 可选 | 跳过消息条数,用于翻页 | - -### 查询应用内所有历史消息 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/messages -``` - -参数与返回值可以参考 `GET /1.2/rtm/conversations/{conv_id}/messages` 接口。 - -## 富媒体消息格式 - -[富媒体消息](/sdk/im/guide/overview/#富媒体消息)的参数格式相对于普通文本来说,仅仅是将 `message` 参数换成了一个 JSON 字符串。 -其中 JSON 字段的含义见[即时通讯总览·富媒体消息](/sdk/im/guide/overview/#富媒体消息)中的说明。 -下面给出内置富媒体消息类型序列化为 JSON 的例子。 - -### 文本消息 - -```json -{ - "_lctype": -1, - "_lctext": "这是一个纯文本消息", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - } -} -``` - -### 图像消息 - -```json -{ - "_lctype": -2, // 必要参数 - "_lctext": "图像的文字说明", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对", - "b": true, - "c": 12 - }, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", // 必要参数 - "objId": "54699d87e4b0a56c64f470a4", // 文件对应的 LCFile.objectId - "metaData": { - "name": "IMG_20141223.jpeg", // 图像的名称 - "format": "png", // 图像的格式 - "height": 768, // 单位:像素 - "width": 1024, // 单位:像素 - "size": 18 // 单位:b - } - } -} -``` - -上面是完整的例子,如果只想简单的发送图像 URL: - -```json -{ - "_lctype": -2, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25" - } -} -``` - -### 音频消息 - -```json -{ - "_lctype": -3, - "_lctext": "这是一个音频消息", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - }, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", - "objId": "54699d87e4b0a56c64f470a4", // 文件对应的 LCFile.objectId - "metaData": { - "name": "我的滑板鞋.wav", - "format": "wav", - "duration": 26, // 单位:秒 - "size": 2738 // 单位:b - } - } -} -``` - -简略版: - -```json -{ - "_lctype": -3, - "_lcfile": { - "url": "http://www.somemusic.com/x.mp3" - } -} -``` - -### 视频消息 - -```json -{ - "_lctype": -4, - "_lctext": "这是一个视频消息", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - }, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/99de0f45-171c-4fdd-82b8-1877b29bdd12", - "objId": "54699d87e4b0a56c64f470a4", // 文件对应的 LCFile.objectId - "metaData": { - "name": "录制的视频.mov", - "format": "avi", - "duration": 168, // 单位:秒 - "size": 18689 // 单位:b - } - } -} -``` - -简略版: - -```json -{ - "_lctype": -4, - "_lcfile": { - "url": "http://www.somevideo.com/Y.flv" - } -} -``` - -### 通用文件消息 - -```json -{ - "_lctype": -6, - "_lctext": "这是一个普通文件类型", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - }, - "_lcfile": { - "url": "http://www.somefile.com/jianli.doc", - "name": "我的简历.doc", - "size": 18689 // 单位:b - } -} -``` - -简略版: - -```json -{ - "_lctype": -6, - "_lcfile": { - "url": "http://www.somefile.com/jianli.doc", - "name": "我的简历.doc" - } -} -``` - -### 地理位置消息 - -```json -{ - "_lctype": -5, - "_lctext": "这是一个地理位置消息", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - }, - "_lcloc": { - "longitude": 23.2, - "latitude": 45.2 - } -} -``` - -简略版: - -```json -{ - "_lctype": -5, - "_lcloc": { - "longitude": 23.2, - "latitude": 45.2 - } -} -``` - -## 接口请求频率限制 - -本文档中和消息操作有关的 REST API 有请求频率以及总量的限制(**即时通讯客户端 SDK 的 API 不受此限制影响**),具体如下: - -### 普通消息 - -1.1 版本: - -- 发送消息、系统对话给用户发消息(`/1.1/rtm/messages`) -- 修改与撤回消息 (`/1.1/rtm/patch/message`) - -1.2 版本: - -- [单聊、群聊-发消息](#单聊、群聊-发消息) -- [单聊、群聊-修改消息](#单聊、群聊-修改消息) -- [单聊、群聊-撤回消息](#单聊、群聊-撤回消息) -- [聊天室-发消息](#聊天室-发消息) -- [聊天室-修改消息](#聊天室-修改消息) -- [聊天室-撤回消息](#聊天室-撤回消息) -- [服务号-给任意用户单独发消息](#给任意用户单独发消息) -- [服务号-修改给用户单独发送的消息](#修改给用户单独发送的消息) -- [服务号-撤回给用户单独发送的消息](#撤回给用户单独发送的消息) - -#### 限制 - -| 商用版(每应用) | 开发版(每应用) | -| -------------------------------------- | ---------------- | -| 最大 9000 次/分钟,默认 1800 次/分钟 | 120 次/分钟 | - -所有接口共享额度。超过额度限制后一分钟内 LeanCloud 会拒绝请求持续返回 429 错误码,一分钟后会重新处理请求。 - -商用版应用的上限可以在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 服务阈值 > 普通消息 API 调用频率上限** 修改。 -按照每日调用频率峰值实行阶梯收费,如下表所示: - -| 每分钟调用频率 | 费用 | -| -------------- | ----------- | -| 0 ~ 1800 | 免费 | -| 1801 ~ 3600 | ¥20 元 / 天 | -| 3601 ~ 5400 | ¥30 元 / 天 | -| 5401 ~ 7200 | ¥40 元 / 天 | -| 7201 ~ 9000 | ¥50 元 / 天 | - -国际版 - -| 每分钟调用频率 | 费用 | -| -------------- | ------------ | -| 0 ~ 1800 | 免费 | -| 1801 ~ 3600 | $6 USD / 天 | -| 3601 ~ 5400 | $9 USD / 天 | -| 5401 ~ 7200 | $12 USD / 天 | -| 7201 ~ 9000 | $15 USD / 天 | - -每日调用频率峰值可以在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 统计 > REST API QPM 峰值** 中查看。 - -### 订阅消息 - -1.1 版本: - -- 系统对话发送订阅消息 (`/1.1/rtm/broadcast/subscriber`) - -1.2 版本: - -- [给所有订阅者发消息](#给所有订阅者发消息) -- [修改给所有订阅者发送的消息](#修改给所有订阅者发送的消息) -- [撤回给所有订阅者发送的消息](#撤回给所有订阅者发送的消息) - -#### 限制 - -| 限制 | 商用版 | 开发版 | -| -------- | ------------------ | ------------------ | -| 频率限制 | 每应用 30 次/分钟 | 每应用 10 次/分钟 | -| 总量限制 | 全天最多 1000 次 | 全天最多 100 次 | - -所有接口共享额度。超过频率限制后 1 分钟内云端会拒绝请求持续返回 429 错误码,一分钟后会重新处理请求;超过总量限制后当天会拒绝之后的所有请求并返回 429 错误码。 - -### 广播消息 - -1.1 版本: - -- 发送广播消息 (`/1.1/rtm/broadcast`) - -1.2 版本: - -- [全局广播](#全局广播) -- [修改广播消息](#修改广播消息) - -#### 限制 - -| 限制 | 商用版 | 开发版 | -| -------- | ------------------ | ----------------- | -| 频率限制 | 每应用 10 次/分钟 | 每应用 1 次/分钟 | -| 总量限制 | 全天最多 30 次 | 全天最多 10 次 | - -所有接口共享以上额度。超过频率限制后 1 分钟内云端会拒绝请求持续返回 429 错误码,一分钟后会重新处理请求;超过总量限制后当天会拒绝之后的所有请求并返回 429 错误码。 diff --git a/leancloud/docs/sdk/im/guide/senior.mdx b/leancloud/docs/sdk/im/guide/senior.mdx deleted file mode 100644 index 5c59891eb..000000000 --- a/leancloud/docs/sdk/im/guide/senior.mdx +++ /dev/null @@ -1,1130 +0,0 @@ ---- -title: 三,安全与签名、玩转聊天室和临时对话 -sidebar_label: 权限与聊天室 -sidebar_position: 3 -slug: /sdk/im/guide/senior ---- - - - - - -import MultiLang from "/src/docComponents/MultiLang"; -import Mermaid from "/src/docComponents/Mermaid"; -import { Conditional } from "/src/docComponents/conditional"; - -## 本章导读 - -在前一篇[消息收发的更多方式,离线推送与消息同步,多设备登录](/sdk/im/guide/intermediate/)中,我们演示了与消息相关的更多特殊需求的实现方法,现在,我们会更进一步,从系统安全和成员权限管理的角度,给大家详细说明: - -- 如何通过第三方鉴权来控制客户端登录与操作 -- 如何对成员权限进行限制,以保证聊天流程能被运营人员很好管理起来 -- 如何实现一个不限人数的直播聊天室 -- 如何对大型群聊中的消息进行实时内容过滤 -- 如何使用临时对话 - -## 安全与签名 - -即时通讯服务有一大特色就是让应用账户系统和聊天服务解耦,终端用户只需要登录应用账户系统就可以直接使用即时通讯服务,同时从系统安全角度出发,我们还提供了第三方操作签名的机制来保证聊天通道的安全性。 - -该机制的工作架构是,在客户端和即时通讯云端之间,增加应用自己的鉴权服务器(也就是即时通讯服务之外的「第三方」),在客户端开始一些有安全风险的操作命令(如登录聊天服务、建立对话、加入群组、邀请他人等)之前,先通过鉴权服务器获取签名,之后即时通讯云端会依据它和第三方鉴权服务之间的协议来验证该签名,只有附带有效签名的请求才会被执行,非法请求全部会被阻止下来。 - -使用操作签名可以保证聊天通道的安全,这一功能默认是关闭的,可以在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 即时通讯设置** 中进行开启: - -- **登录启用签名认证**,用于控制所有的用户登录 -- **对话操作启用签名认证**,用于控制新建或加入对话、邀请/踢出对话成员等操作 -- **聊天记录查询启用签名认证**,用于控制聊天记录查询操作 - -开发者可根据实际需要进行选择。一般来说,**登录认证** 是最基本的安全机制,我们强烈建议开发者开启登录认证。 - ->应用鉴权服务器: 1. 携登录、新建会话、加入群组、邀请他人、踢出成员等行为请求签名 -应用鉴权服务器-->>终端: 2. 生成时间戳、随机字符串和签名返回给客户端 -终端->>即时通讯服务云端: 3. 将签名编码到请求中发给即时通讯服务器 -即时通讯服务云端-->>终端: 4. 对请求的内容和签名进行验证,执行后续操作 -`} -/> - -1. 客户端进行登录或新建对话等操作,SDK 会调用 `SignatureFactory` 的实现,并携带用户信息和用户行为(登录、新建对话或群组操作)请求签名; -2. 应用自有的权限系统,或应用在云引擎上的签名程序收到请求,进行权限验证,如果通过则利用下文所述的 [签名算法](#用户登录签名) 生成时间戳、随机字符串和签名返回给客户端; -3. 客户端获得签名后,编码到请求中,发给即时通讯服务器; -4. 即时通讯服务器对请求的内容和签名做一遍验证,确认这个操作是被应用服务器允许的,进而执行后续的实际操作。 - -签名采用 **HMAC-SHA1** 算法,输出字节流的十六进制字符串(hex dump)。针对不同的请求,开发者需要拼装不同组合的字符串,加上 UTC timestamp 以及随机字符串作为签名的消息(参见后续格式说明)。总体上,签名就是使用特定的密钥(在这里我们使用应用的 `Master Key`),对输入的消息(即「签名的消息」)进行哈希计算,得到一串十六进制的字符串,这就是最终的「签名」。 - -对于使用 `LCUser` 的应用,可使用 REST API 获取登录签名进行登录认证。 - -### 签名格式说明 - -下面我们详细说明一下不同操作的签名消息格式。 - -#### 用户登录签名 - -签名的消息格式如下,注意 `clientid` 与 `timestamp` 之间是两个冒号: - -``` -appid:clientid::timestamp:nonce -``` - -| 参数 | 说明 | -| ----------- | ---------------------------------------------- | -| `appid` | 应用的 ID。 | -| `clientid` | 登录时使用的 `clientId`。 | -| `timestamp` | 当前的 UTC 时间距离 Unix epoch 的 **毫秒数**。 | -| `nonce` | 随机字符串。 | - -> 注意:签名的 key **必须** 是应用的 `Master Key`,你可以在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 里找到。**请保护好 Master Key,不要泄露给任何无关人员。** - -开发者可以实现自己的 `SignatureFactory`,调用远程服务器的签名接口获得签名。如果你没有自己的服务器,可以直接在云引擎上通过 **网站托管** 来实现自己的签名接口。在移动应用中直接进行签名的做法 **非常危险**,它可能导致你的 **Master Key** 泄漏。 - -签名的有效期是 6 个小时,强制下线后签名立即失效。 -签名失效不影响当前在线的 client。 - -#### 开启对话签名 - -新建一个对话的时候,签名的消息格式为: - -``` -appid:clientid:sorted_member_ids:timestamp:nonce -``` - -- `appid`、`clientid`、`timestamp` 和 `nonce` 的含义 [同上](#用户登录签名)。 -- `sorted_member_ids` 是以半角冒号(`:`)分隔、**升序排序** 的 `clientId`,即邀请参与该对话的成员列表。 - -#### 群组功能的签名 - -在群组功能中,我们对 **加群**、**邀请** 和 **踢出群** 这三个动作也允许加入签名,签名的消息格式是: - -``` -appid:clientid:convid:sorted_member_ids:timestamp:nonce:action -``` - -- `appid`、`clientid`、`sorted_member_ids`、`timestamp` 和 `nonce` 的含义同上。对创建群的情况,这里 `sorted_member_ids` 是空字符串。 -- `convid` 是此次行为关联的对话 ID。 -- `action` 是此次行为的动作,`invite` 表示加群和邀请,`kick` 表示踢出群。 - -#### 查询聊天记录的签名 - -``` -appid:client_id:convid:nonce:timestamp -``` - -各参数的含义同上。 - -注意,此签名仅用于通过 REST API 查询历史消息,客户端 SDK 不适用。 - -### 云引擎签名范例 - -为了帮助开发者理解云端签名的算法,我们开源了一个用「Node.js + 云引擎」实现签名的云端,供开发者学习和使用:[即时通讯云引擎签名 Demo](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/rtm-signature.js)。 - -### 客户端如何支持操作签名 - -上面的签名算法,都是对第三方鉴权服务器如何进行签名的协议说明,在开启了操作签名的前提下,客户端这边的使用流程需要进行相应的改变,增加请求签名的环节,才能让整套机制顺利运行起来。 - -即时通讯 SDK 为每一个 `AVIMClient` 实例都预留了一个 `Signature` 工厂接口,这个接口默认不设置就表示不使用签名,启动签名的时候,只需要在客户端实现这一接口,调用远程服务器的签名接口获得签名,并把它绑定到 `AVIMClient` 实例上即可: - - - -```cs -public class LocalSignatureFactory : ILCIMSignatureFactory { - const string MasterKey = "pyvbNSh5jXsuFQ3C8EgnIdhw"; - - public Task CreateConnectSignature(string clientId) { - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, string.Empty, timestamp.ToString(), nonce); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - public Task CreateStartConversationSignature(string clientId, IEnumerable memberIds) { - string sortedMemberIds = string.Empty; - if (memberIds != null) { - List sortedMemberList = memberIds.ToList(); - sortedMemberList.Sort(); - sortedMemberIds = string.Join(":", sortedMemberList); - } - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, sortedMemberIds, timestamp.ToString(), nonce); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - public Task CreateConversationSignature(string conversationId, string clientId, IEnumerable memberIds, string action) { - string sortedMemberIds = string.Empty; - if (memberIds != null) { - List sortedMemberList = memberIds.ToList(); - sortedMemberList.Sort(); - sortedMemberIds = string.Join(":", sortedMemberList); - } - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, conversationId, sortedMemberIds, timestamp.ToString(), nonce, action); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - private static string SignSHA1(string key, string text) { - HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key)); - byte[] bytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(text)); - string signature = BitConverter.ToString(bytes).Replace("-", string.Empty); - return signature; - } - - private static string NewNonce() { - byte[] bytes = new byte[10]; - using (RandomNumberGenerator generator = RandomNumberGenerator.Create()) { - generator.GetBytes(bytes); - } - return Convert.ToBase64String(bytes); - } - - private static string GenerateSignature(params string[] args) { - string text = string.Join(":", args); - string signature = SignSHA1(MasterKey, text); - return signature; - } -} - -// 设置签名工厂 -LCIMClient tom = new LCIMClient("tom", signatureFactory: new LocalSignatureFactory()); -``` - -```java -// 这是一个依赖云引擎完成签名的示例 -public class KeepAliveSignatureFactory implements SignatureFactory { - @Override - public Signature createSignature(String peerId, List watchIds) throws SignatureException { - Map params = new HashMap(); - params.put("self_id",peerId); - params.put("watch_ids",watchIds); - - try{ - Object result = LCCloud.callFunction("sign",params); - if(result instanceof Map){ - Map serverSignature = (Map) result; - Signature signature = new Signature(); - signature.setSignature((String)serverSignature.get("signature")); - signature.setTimestamp((Long)serverSignature.get("timestamp")); - signature.setNonce((String)serverSignature.get("nonce")); - return signature; - } - }catch(LCException e){ - throw (SignatureFactory.SignatureException) e; - } - return null; - } - - @Override - public Signature createConversationSignature(String convId, String peerId, - List targetPeerIds,String action) throws SignatureException{ - Map params = new HashMap(); - params.put("client_id",peerId); - params.put("conv_id",convId); - params.put("members",targetPeerIds); - params.put("action",action); - - try{ - Object result = LCCloud.callFunction("sign2",params); - if(result instanceof Map){ - Map serverSignature = (Map) result; - Signature signature = new Signature(); - signature.setSignature((String)serverSignature.get("signature")); - signature.setTimestamp((Long)serverSignature.get("timestamp")); - signature.setNonce((String)serverSignature.get("nonce")); - return signature; - } - }catch(LCException e){ - throw (SignatureFactory.SignatureException) e; - } - return null; - } -} - -// 将签名工厂类的实例绑定到 LCIMClient 上 -LCIMOptions.getGlobalOptions().setSignatureFactory(new KeepAliveSignatureFactory()); -``` - -```objc -// 实现 LCIMSignatureDataSource 协议 -- (void)client:(LCIMClient *)client - action:(LCIMSignatureAction)action - conversation:(LCIMConversation * _Nullable)conversation - clientIds:(NSArray * _Nullable)clientIds -signatureHandler:(void (^)(LCIMSignature * _Nullable))handler -{ - if ([action isEqualToString:LCIMSignatureActionOpen]) { - // 开启了签名认证的模块,需返回对应的签名 - LCIMSignature *signature; - /* - ... - ... - 具体实现可以参考章节「云引擎签名范例」 - */ - handler(signature); - } else { - // 没有开启签名认证的模块,需返回 nil - handler(nil); - } -} - -// 设置协议代理者 -NSError *error; -LCIMClient *imClient = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error]; -if (!error) { - imClient.signatureDataSource = signatureDelegator; -} -``` - -```js -// 基于云引擎进行登录签名的 signature 工厂方法 -var signatureFactory = function (clientId) { - return AV.Cloud.rpc("sign", { clientId: clientId }); // AV.Cloud.rpc 返回一个 Promise -}; -// 基于云引擎进行对话创建/加入、邀请成员、踢出成员等操作签名的 signature 工厂方法 -var conversationSignatureFactory = function ( - conversationId, - clientId, - targetIds, - action -) { - return AV.Cloud.rpc("sign-conversation", { - conversationId: conversationId, - clientId: clientId, - targetIds: targetIds, - action: action, - }); -}; - -realtime - .createIMClient("Tom", { - signatureFactory: signatureFactory, - conversationSignatureFactory: conversationSignatureFactory, - }) - .then(function (tom) { - console.log("Tom 登录"); - }) - .catch(function (error) { - // 如果 signatureFactory 抛出了异常,或者签名没有验证通过,会在这里被捕获 - }); -``` - -```swift -class SignatureDelegator: IMSignatureDelegate { - - // 基于云引擎的获取客户端登录签名的函数 - func getClientOpenSignature(completion: (IMSignature) -> Void) { - // 具体实现可以参考章节「云引擎签名范例」 - } - - func client(_ client: IMClient, action: IMSignature.Action, signatureHandler: @escaping (IMClient, IMSignature?) -> Void) { - switch action { - case .open: - // 开启了签名认证的模块,需返回对应的签名 - self.getClientOpenSignature { (signature) in - signatureHandler(client, signature) - } - default: - // 没有开启签名认证的模块,需返回 nil - signatureHandler(client, nil) - } - } -} - -do { - let signatureDelegator = SignatureDelegator() - let client = try IMClient(ID: "Tom", signatureDelegate: signatureDelegator) -} catch { - print(error) -} -``` - -```dart - -``` - - - -需要强调的是:开发者切勿在客户端直接使用 `Master Key` 进行签名操作,因为 `Master Key` 一旦泄露,会造成应用的数据处于高危状态,后果不容小视。因此,强烈建议开发者将签名的具体代码托管在安全性高稳定性好的云端服务器上(例如云引擎)。 - -### 内建账户系统(`User`)的签名机制 - -`User` 是存储服务提供的默认账户系统,对于使用了它来完成用户注册、登录的产品来说,终端用户通过 `User` 账户系统的登录认证之后,转到即时通讯服务上,是无需再进行登录签名操作的。使用 `User` 账号系统登录即时通讯服务的示例如下: - - - -```cs -LCUser user = await LCUser.Login("username", "password"); -CIMClient client = new LCIMClient(user); -await client.Open(); -``` - -```java -// 以 LCUser 的用户名和密码登录到内建账户系统 -LCUser.logInInBackground("username", "password", new LogInCallback() { - @Override - public void done(LCUser user, LCException e) { - if (null != e) { - return; - } - // 以 LCUser 实例创建了一个 client - LCIMClient client = LCIMClient.getInstance(user); - // 登录即时通讯云端 - client.open(new LCIMClientCallback() { - @Override - public void done(final LCIMClient avimClient, LCIMException e) { - // 执行其他逻辑 - } - }); - } -}); -``` - -```objc -// 以 LCUser 的用户名和密码登录到内建账户系统 -[LCUser logInWithUsernameInBackground:username password:password block:^(LCUser * _Nullable user, NSError * _Nullable error) { - // 以 LCUser 实例创建了一个 client - NSError *err; - LCIMClient *client = [[LCIMClient alloc] initWithUser:user error:&err]; - if (!err) { - // 登录即时通讯云端 - [client openWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - // 执行其他逻辑 - }]; - } -}]; -``` - -```js -var AV = require("leancloud-storage"); -// 以用户名和密码登录内建账户系统 -AV.User.logIn("username", "password") - .then(function (user) { - // 直接使用 LCUser 实例登录即时通讯服务 - return realtime.createIMClient(user); - }) - .catch(console.error.bind(console)); -``` - -```swift -_ = LCUser.logIn(username: "username", password: "password") { (result) in - switch result { - case .success(object: let user): - do { - let client = try IMClient(user: user) - client.open(completion: { (result) in - // 执行其他逻辑 - }) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -// 暂不支持 -``` - - - -内置账户系统与即时通讯服务可以共享登录签名信息,这里我们直接用 `logIn` 成功之后的 `LCUser` 实例来创建 `IMClient`,在即时通讯服务的用户登录环节,云端会自动关联账户系统来确认用户身份的合法性,这样可以省掉 SDK 向第三方申请登录签名的操作,进一步简化开发流程。 - -`IMClient` 完成即时通讯系统登录之后,其他功能的使用就和之前的介绍没有任何区别了。 - -## 玩转直播聊天室 - -在即时通讯服务总览中,我们比较了不同的业务场景与对话类型,现在就来看看如何使用「聊天室」完成一个直播弹幕的需求。 - -### 创建聊天室 - -`IMClient` 提供了专门的 `createChatRoom` 方法来创建聊天室: - - - -```cs -// 最直接的方式,传入 name 即可 -tom.CreateChatRoom("聊天室"); -``` - -```java -tom.createChatRoom("聊天室", null, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conv, LCIMException e) { - if (e == null) { - // 创建成功 - } - } -}); -``` - -```objc -[client createChatRoomWithCallback:^(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error) { - if (chatRoom && !error) { - LCIMTextMessage *textMessage = [LCIMTextMessage messageWithText:@"这是一条消息" attributes:nil]; - [chatRoom sendMessage:textMessage callback:^(BOOL success, NSError *error) { - if (success && !error) { - - } - }]; - } -}]; -``` - -```js -tom.createChatRoom({ name: "聊天室" }).catch(console.error); -``` - -```swift -do { - try client.createChatRoom(name: "聊天室", attributes: nil) { (result) in - switch result { - case .success(value: let chatRoom): - print(chatRoom) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -ChatRoom chatRoom = await jerry.createChatRoom(name: '聊天室'); -``` - - - -在创建聊天室的时候,开发者可以指定聊天室的名字和附加属性(非必须),与创建普通对话的接口相比,有如下差异: - -- 聊天室因为没有成员列表,所以创建的时候指定 `members` 是没有意义的 -- 同样的原因,创建聊天室的时候指定 `unique` 标志也是没有意义的(云端无需根据成员 ID 来去重) - -> 尽管我们调用 `createConversation` 接口,通过传递合适的参数(`{ transient: true }`),也可以创建一个聊天室,但是还是建议大家直接使用 `createChatRoom` 方法。 - -### 查找聊天室 - -在即时通讯开发指南第一篇中,我们已经了解了构造复杂条件来查询对话的方法,`ConversationsQuery` 依然适用于查询聊天室,只需要添加 `transient = true` 的限制条件即可。 - - - -```cs -LCIMConversationQuery query = new LCIMConversationQuery(tom); -query.WhereEqualTo("tr", true); -``` - -```java -LCIMConversationsQuery query = tom.getChatRoomQuery(); -query.findInBackground(new LCIMConversationQueryCallback() { - @Override - public void done(List conversations, LCIMException e) { - if (null == e) { - // 获取成功 - } else { - // 获取失败 - } - } -}); -``` - -```objc -LCIMConversationQuery *query = [tom conversationQuery]; -[query whereKey:@"tr" equalTo:@(YES)]; -``` - -```js -var query = tom.getQuery().equalTo("tr", true); // 聊天室对象 -query - .find() - .then(function (conversations) { - // conversations 就是想要的结果 - }) - .catch(console.error); -``` - -```swift -do { - let query = client.conversationQuery - try query.where("tr", .equalTo(true)) - try query.findConversations { (result) in - switch result { - case .success(value: let conversations): - guard conversations is [IMChatRoom] else { - return - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - ConversationQuery query = tom.conversationQuery(); - query.whereEqualTo('tr', true); - // conversations 就是想要的结果 - List conversations = await query.find(); -} catch (e) { - print(e); -} -``` - - - -> 上面示例中 Java / Android SDK 专门提供了 `LCIMClient#getChatRoomQuery` 方法来生成聊天室查询对象,屏蔽了 `transient` 属性的细节,建议开发者优先使用这种高层 API。 - -### 加入和离开聊天室 - -查询到聊天室之后,加入和离开聊天室与普通对话的对应接口没有区别,详细请参考[即时通讯开发指南第一篇](/sdk/im/guide/beginner/)《多人群聊》。 - -在成员管理与变更通知方面,聊天室与普通对话的最大区别就是: - -- 在聊天室内无法邀请或者踢出成员,只能由用户主动加入和退出; -- 除了用户主动退出之外,客户端断线也会被认为是退出了聊天室。为了防止网络抖动,如果客户端临时异常断线,只要在半小时内重新上线,都会自动加入原聊天室(主动退出的除外); -- 云端不会下发成员加入、退出的变更通知; -- 不支持查询成员列表,但提供专门的 API 来查询实时在线人数。 - -另外,也请注意 **_聊天室也不支持离线推送通知、离线消息同步、消息回执等功能_**。 - -### 查询成员数量 - -`LCIMConversation#memberCount` 方法可以用来查询普通对话的成员总数,在聊天室中,它返回的就是实时在线的人数: - - - -```cs -int membersCount = await conversation.GetMembersCount(); -``` - -```java -private void TomQueryWithLimit() { - LCIMClient tom = LCIMClient.getInstance("Tom"); - tom.open(new LCIMClientCallback() { - - @Override - public void done(LCIMClient client, LCIMException e) { - if (e == null) { - // 登录成功 - LCIMConversationsQuery query = tom.getConversationsQuery(); - query.setLimit(1); - // 获取第一个对话 - query.findInBackground(new LCIMConversationQueryCallback() { - @Override - public void done(List convs, LCIMException e) { - if (e == null) { - if (convs != null && !convs.isEmpty()) { - LCIMConversation conv = convs.get(0); - // 获取第一个对话的在线人数 - conv.getMemberCount(new LCIMConversationMemberCountCallback() { - - @Override - public void done(Integer count, LCIMException e) { - if (e == null) { - Log.d("Tom & Jerry 对话的在线人数为 " + count); - } - } - }); - } - } - } - }); - } - } - }); -} -``` - -```objc -// 查询在线人数 -[conversation countMembersWithCallback:^(NSInteger number, NSError *error) { - NSLog(@"%ld",number); -}]; -``` - -```js -chatRoom - .count() - .then(function (count) { - console.log("在线人数:" + count); - }) - .catch(console.error.bind(console)); -``` - -```swift -do { - chatRoom.getOnlineMembersCount { (result) in - switch result { - case .success(count: let count): - print(count) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -int count = await chatRoom.countMembers(); -``` - - - -### 消息等级 - -为了保证消息的时效性,当聊天室消息过多导致客户端连接堵塞时,服务器端会选择性地丢弃部分非高等级的消息。目前支持的消息等级有: - -| 消息等级 | 描述 | -| ------------------------ | ------------------------------------------------------------------ | -| `MessagePriority.HIGH` | 高等级,针对时效性要求较高的消息,比如直播聊天室中的礼物、打赏等。 | -| `MessagePriority.NORMAL` | 中等级,比如普通非重复性的文本消息。 | -| `MessagePriority.LOW` | 低等级,针对时效性要求较低的消息,比如直播聊天室中的弹幕。 | - -消息等级默认为 `NORMAL`。 - -消息等级在发送接口的参数中设置。以下代码演示了如何发送一个高等级的消息: - - - -```cs -LCIMTextMessage message = new LCIMTextMessage("现在比分是 0:0,下半场中国队肯定要做出人员调整"); -LCIMMessageSendOptions options = new LCIMMessageSendOptions { - Priority = LCIMMessagePriority.High -}; -await chatRoom.Send(message, options); -``` - -```java -LCIMClient tom = LCIMClient.getInstance("Tom"); - tom.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient client, LCIMException e) { - if (e == null) { - // 创建名为「猫和老鼠」的对话 - client.createConversation(Arrays.asList("Jerry"), "猫和老鼠", null, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conv, LCIMException e) { - if (e == null) { - LCIMTextMessage msg = new LCIMTextMessage(); - msg.setText("耗子,起床!"); - - LCIMMessageOption messageOption = new LCIMMessageOption(); - messageOption.setPriority(LCIMMessageOption.MessagePriority.High); - conv.sendMessage(msg, messageOption, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } - }); - } - } - }); - } - } - }); -``` - -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.priority = LCIMMessagePriorityHigh; -[chatRoom sendMessage:[LCIMTextMessage messageWithText:@"耗子,起床!" attributes:nil] option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - // 在这里处理发送失败或者成功之后的逻辑 -}]; -``` - -```js -var { Realtime, TextMessage, MessagePriority } = require("leancloud-realtime"); -var realtime = new Realtime({ - appId: "GDBz24d615WLO5e3OM3QFOaV-gzGzoHsz", - appKey: "dlCDCOvzMnkXdh2czvlbu3Pk", -}); -realtime - .createIMClient("host") - .then(function (host) { - return host.createConversation({ - members: ["broadcast"], - name: "2094 世界杯决赛梵蒂冈对阵中国比赛直播间", - transient: true, - }); - }) - .then(function (conversation) { - console.log(conversation.id); - return conversation.send( - new TextMessage("现在比分是 0:0,下半场中国队肯定要做出人员调整"), - { priority: MessagePriority.HIGH } - ); - }) - .then(function (message) { - console.log(message); - }) - .catch(console.error); -``` - -```swift -do { - let message = IMTextMessage(text: "现在比分是 0:0,下半场中国队肯定要做出人员调整") - try chatRoom.send(message: message, priority: .high) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -try { - TextMessage message = TextMessage(); - message.text = '现在比分是 0:0,下半场中国队肯定要做出人员调整'; - await chatRoom.send(message: message, priority: MessagePriority.high); -} catch (e) { - print(e); -} -``` - - - -> 注意: -> -> 此功能仅针对聊天室消息有效。普通对话的消息不需要设置等级,即使设置了也会被系统忽略,因为普通对话的消息不会被丢弃。 - -### 消息免打扰 - -假如某一用户不想再收到某对话的消息提醒,但又不想直接退出对话,可以使用静音操作,即开启「免打扰模式」。 - -比如 Tom 工作繁忙,对某个对话设置了静音: - - - -```cs -await chatRoom.Mute(); -``` - -```java -LCIMClient tom = LCIMClient.getInstance("Tom"); -tom.open(new LCIMClientCallback(){ - - @Override - public void done(LCIMClient client,LCIMException e){ - if(e==null){ - // 登录成功 - LCIMConversation conv = client.getConversation("551260efe4b01608686c3e0f"); - conv.mute(new LCIMConversationCallback(){ - - @Override - public void done(LCIMException e){ - if(e==null){ - // 设置成功 - } - } - }); - } - } -}); -``` - -```objc -// Tom 将会话设置为静音 -[conversation muteWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"修改成功!"); - } -}]; -``` - -```js -tom - .getConversation("CONVERSATION_ID") - .then(function (conversation) { - return conversation.mute(); - }) - .then(function (conversation) { - console.log("静音成功"); - }) - .catch(console.error.bind(console)); -``` - -```swift -conversation.mute { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await chatRoom.mute(); -``` - - - -设置静音之后,iOS 及启用混合推送的 Android 用户就不会收到推送消息了。与之对应的就是取消静音的操作(`Conversation#unmute` 方法),即取消免打扰模式。 - -> 使用建议: -> -> - 对话内消息的静音/取消静音操作不光对聊天室有效,普通的群聊对话也可以执行该操作。 -> - `mute` 和 `unmute` 操作会修改云端 `_Conversation` 里面的 `mu` 属性。**开发者切勿在控制台中对 `mu` 随意进行修改**,否则可能会引起即时通讯云端的离线推送功能失效。 - -### 消息内容的实时过滤 - - - -对于开放聊天室来说,内容的审核和实时过滤是产品运营上的一个基本要求。即时通讯服务提供了敏感词过滤的功能,多人的普通对话、聊天室和系统对话里面的消息都会进行实时过滤。开发者可以在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置** 中对一对一单聊启用敏感词过滤。 - - - - - -即时通讯服务提供了敏感词过滤的功能。开发者可以在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置** 中对一对一单聊启用敏感词过滤。 - - - -命中的敏感词将会被替换为 `***`。 - -消息内容实时过滤属于系统层面的修改消息,发送者会收到 `MESSAGE_UPDATE` 事件。应用可以在客户端监听该事件,实现相应的业务逻辑,相关代码示例可以参考[即时通讯开发指南第二篇](/sdk/im/guide/intermediate/)的《修改消息》一节。 - - - -过滤的词库由即时通讯服务统一提供。商用版应用支持开发者使用自定义敏感词词库,只需在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置** 中上传敏感词文件。敏感词文件为 UTF-8 编码的纯文本文件,一行一个敏感词。开发者上传的自定义敏感词词库会替换默认提供的词库。 - - - - - -商用版应用支持开发者使用自定义敏感词词库,只需在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置** 中上传敏感词文件。敏感词文件为 UTF-8 编码的纯文本文件,一行一个敏感词。**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置** 中,「群聊启用敏感词过滤」总是选中状态,目前暂不可取消,但国际版应用的内置敏感词词库为空。因此,如果国际版应用没有上传自定义敏感词词库,那么实际上聊天消息不会过滤。 - - - -敏感词的过滤规则:如果消息是富媒体消息类型(`_lctype` 属性有值),那么敏感词只过滤 `_lctext` 这个字段的内容。如果非富媒体消息类型(消息没有 `_lctype` 属性),则全部消息体过滤。 - -如果开发者有较为复杂的过滤需求,可以使用云引擎 hook `_messageReceived` 来实现过滤,在 hook 中开发者对消息的内容有完全的控制力。 - -## 使用临时对话 - -临时对话是一个全新的概念,它解决的是一种特殊的聊天场景: - -- 对话存续时间短 -- 聊天参与的人数较少(最多为 10 个 `clientId`) -- 聊天记录的存储不是强需求 - -临时对话最大的特点是 **较短的有效期**,这个特点可以解决对话的持久化存储在服务端占用的存储资源越来越大、开发者需要支付的成本越来越高的问题,也可以应对一些临时聊天的场景。诸如电商售前和售后在线聊天的客服系统,我们推荐使用临时对话。 - -### 临时对话实例 - -`IMConversation` 有专门的 `createTemporaryConversation` 方法用于创建临时对话: - - - -```cs -LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" }); -``` - -```java -tom.createTemporaryConversation(Arrays.asList(members), 3600, new LCIMConversationCreatedCallback(){ - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if (null == e) { - LCIMTextMessage msg = new LCIMTextMessage(); - msg.setText("这里是临时对话"); - conversation.sendMessage(msg, new LCIMConversationCallback(){ - @Override - public void done(LCIMException e) { - } - }); - } - } -}); -``` - -```objc -[self createTemporaryConversationWithClientIds:@[@"Jerry", @"William"] callback:^(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error) { - if (temporaryConversation) { - // success - } -}]; -``` - -```js -realtime - .createIMClient("Tom") - .then(function (tom) { - return tom.createTemporaryConversation({ - members: ["Jerry", "William"], - }); - }) - .then(function (conversation) { - return conversation.send(new AV.TextMessage("这里是临时对话")); - }) - .catch(console.error); -``` - -```swift -do { - try client.createTemporaryConversation(clientIDs: ["Jerry", "William"], timeToLive: 3600) { (result) in - switch result { - case .success(value: let tempConversation): - print(tempConversation) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -TemporaryConversation temporaryConversation; -try { - temporaryConversation = await jerry.createTemporaryConversation( - members: {'Jerry', 'William'}, - ); -} catch (e) { - print(e); -} - -try { - TextMessage message = TextMessage(); - message.text = '这里是临时对话'; - await temporaryConversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - -与其他对话类型不同的是,临时对话有一个 **重要** 的属性:TTL。它标记着这个对话的有效期,系统默认是 1 天,但是在创建对话的时候是可以指定这个时间的,最高不超过 30 天。如果你的需求是一定要超过 30 天,请使用普通对话。传入 TTL 创建临时对话的代码如下: - - - -```cs -LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" }, - ttl: 3600); -``` - -```java -LCIMClient client = LCIMClient.getInstance("Tom"); -client.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient avimClient, LCIMException e) { - if (null == e) { - String[] members = {"Jerry", "William"}; - avimClient.createTemporaryConversation(Arrays.asList(members), 3600, new LCIMConversationCreatedCallback(){ - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if (null == e) { - LCIMTextMessage msg = new LCIMTextMessage(); - msg.setText("这里是临时对话,一小时之后,这个对话就会消失"); - conversation.sendMessage(msg, new LCIMConversationCallback(){ - @Override - public void done(LCIMException e) { - } - }); - } - } - }); - } - } -}); -``` - -```objc -LCIMConversationCreationOption *option = [LCIMConversationCreationOption new]; -option.timeToLive = 3600; -[self createTemporaryConversationWithClientIds:@[@"Jerry", @"William"] option:option callback:^(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error) { - if (temporaryConversation) { - // success - } -}]; -``` - -```js -realtime - .createIMClient("Tom") - .then(function (tom) { - return tom.createTemporaryConversation({ - members: ["Jerry", "William"], - ttl: 3600, - }); - }) - .then(function (conversation) { - return conversation.send( - new AV.TextMessage("这里是临时对话,一小时之后,这个对话就会消失") - ); - }) - .catch(console.error); -``` - -```swift -do { - try client.createTemporaryConversation(clientIDs: ["Jerry", "William"], timeToLive: 3600) { (result) in - switch result { - case .success(value: let tempConversation): - print(tempConversation) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -TemporaryConversation temporaryConversation; -try { - temporaryConversation = await jerry.createTemporaryConversation( - members: {'Jerry', 'William'}, - timeToLive: 3600, - ); -} catch (e) { - print(e); -} - -try { - TextMessage message = TextMessage(); - message.text = '这里是临时对话,一小时之后,这个对话就会消失'; - await temporaryConversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - -临时对话的其他操作与普通对话无异。 - -## 进一步阅读 - -- 《即时通讯开发指南》第四篇[详解消息 hook 与系统对话](/sdk/im/guide/systemconv/) diff --git a/leancloud/docs/sdk/im/guide/systemconv.mdx b/leancloud/docs/sdk/im/guide/systemconv.mdx deleted file mode 100644 index f8d352e44..000000000 --- a/leancloud/docs/sdk/im/guide/systemconv.mdx +++ /dev/null @@ -1,1433 +0,0 @@ ---- -title: 四,详解消息 hook 与系统对话 -sidebar_label: Hook 与系统对话 -sidebar_position: 4 -slug: /sdk/im/guide/systemconv ---- - - - - -import MultiLang from "/src/docComponents/MultiLang"; -import Mermaid from "/src/docComponents/Mermaid"; - -## 本章导读 - -在前一篇[安全与签名、玩转聊天室和临时对话](/sdk/im/guide/senior/)中,我们解释了一些第三方鉴权方面的问题,在这里我们会更进一步,给大家说明: - -- 即时通讯的消息 Hook 机制 -- 系统对话的使用方法 - -## 万能的 Hook 机制 - -完全开放的架构,支持强大的业务扩展能力,是即时通讯服务的特色之一,这种优势的体现就是这里将要给大家介绍的「Hook 机制」。 - -### Hook 与即时通讯服务的关系 - -Hook 也可以称为「钩子」,是一种特殊的消息处理机制,与 Windows 平台下的中断机制类似,允许应用方拦截并处理即时通讯过程中的多种事件和消息,从而达到实现自定义业务逻辑的目的。 - -以 **\_messageRecieved** Hook 为例,它在消息送达服务器后会被调用,在 Hook 内可以捕获消息内容、消息发送者、消息接收者等信息,这些信息均能在 Hook 内做修改并将修改后的值转交回服务器,服务器会使用修改后的消息继续完成消息投递工作。最终收消息用户收到的会是被 Hook 修改过后的消息,而不再是最初送达服务器的原始消息。Hook 也可以选择拒绝消息发送,服务器会在给客户端回复消息被 Hook 拒绝后丢弃消息不再完成后续消息处理及转发流程。 - -**需要注意的是,默认情况下如果 Hook 调用失败,例如超时、返回状态码非 200 的结果等,服务器会忽略 Hook 的错误继续处理原始请求**。如果你需要改变这个行为,可以在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 即时通讯设置** 内开启「Hook 调用失败时返回错误给客户端并放弃继续处理请求」。开启后如果 Hook 调用失败,服务器会返回错误信息给客户端告知 Hook 调用错误,并拒绝继续处理请求。 - -### 消息类 Hook - -一条消息,在即时通讯的流程中,从终端用户 A 发送开始,到其他用户接收到为止,考虑到存在接收方在线/不在线的可能,会经历多个不同阶段,这里每一个阶段都会触发 Hook 函数: - -- **\_messageReceived**
    - 消息达到服务器,群组成员已解析完成之后,发送给收件人之前调用。开发者在这里还可以修改消息内容,实时改变消息接收者的列表,以及其他类似操作。 -- **\_messageSent**
    - 消息发送完成后调用。开发者在这里可以完成业务统计,或将消息中转备份到己方服务器,以及其他类似操作。 -- **\_receiversOffline**
    - 消息发送完成,存在离线的收件人,在发推送给收件人之前调用。开发者在这里可以动态修改离线推送的通知内容,或通知目的设备的列表,以及其他类似操作。 -- **\_messageUpdate**
    - 收到消息修改请求,发送修改后的消息给收件人之前调用。与新发消息一样,开发者在这里可以再次修改消息内容,实时改变消息接收者的列表,以及其他类似操作。 - -### 对话类 Hook - -在对话创建和成员变动等更改性操作前后,都可以触发 Hook 函数,进行额外的处理: - -- **\_conversationStart**
    - 创建对话,在签名校验(如果开启)之后,实际创建之前调用。开发者在这里可以为新的「对话」添加其他内部属性,或完成操作鉴权,以及其他类似操作。 -- **\_conversationStarted**
    - 创建对话完成后调用。开发者在这里可以完成业务统计,或将对话数据中转备份到己方服务器,以及其他类似操作。 -- **\_conversationAdd**
    - 向对话添加成员,在签名校验(如果开启)之后,实际加入之前调用,包括主动加入和被其他用户加入两种情况。开发者可以在这里根据内部权限设置批准或驳回这一请求,以及其他类似操作。 -- **\_conversationRemove**
    - 从对话中踢出成员,在签名校验(如果开启)之后,实际踢出之前调用,用户自己退出对话不会调用。开发者可以在这里根据内部权限设置批准或驳回这一请求,以及其他类似操作。 -- **\_conversationAdded**
    - 用户加入对话,在加入成功后调用。 -- **\_conversationRemoved**
    - 用户离开对话,在离开成功后调用。 -- **\_conversationUpdate**
    - 修改对话名称、自定义属性,设置或取消对话消息提醒,在实际修改之前调用。开发者在这里可以为新的「对话」添加其他内部属性,或完成操作鉴权,以及其他类似操作。 - -### 客户端上下线 Hook - -在客户端上线和下线的时候,可以触发 Hook 函数: - -- **\_clientOnline**
    - 客户端上线,客户端登录成功后调用。 -- **\_clientOffline**
    - 客户端下线,客户端登出成功或意外下线后调用。 - -开发者可以利用这两个 Hook 函数,结合 LeanCache 来完成一组客户端实时状态查询的 endpoint,具体可以参考文档[《即时通讯中的在线状态查询》](https://docs.leancloud.app/sdk/im/best-practice/realtime-guide-onoff-status/)。 - -### Hook 与云引擎的关系 - -因为 Hook 发生在即时通讯的在线处理环节,而即时通讯服务端每秒钟需要处理的消息和对话事件数量远超大家的想象,出于性能考虑,我们要求开发者使用云引擎来实现 Hook 函数。 - -即时通讯的云引擎 Hook 要求云引擎部署在云引擎的 **生产环境**,测试环境仅用于开发者手动调用测试。由于缓存的原因,首次部署的云引擎 Hook 需要至多三分钟来正式生效,后续修改会实时生效。 - -### Hook API 细节与使用场景详解 - -与 `conversation` 相关的 hook 可以在应用签名之外增加额外的权限判断,控制对话是否允许被建立、某些用户是否允许被加入对话等。你可以用这一 hook 实现黑名单功能。 - -#### `_messageReceived` - -这个 hook 发生在消息到达云端之后。如果是群组消息,我们会解析出所有消息收件人。 - -你可以通过返回参数控制消息是否需要被丢弃,删除个别收件人,还可以修改消息内容,例如过滤应用中的敏感词。返回空对象(`response.success({})`)则会执行系统默认的流程。 - -请注意,在这个 hook 的代码实现的任何分支上 **请确保最终会调用 `response.success` 返回结果**,使得消息可以尽快投递给收件人。这个 hook 将 **阻塞发送流程**,因此请尽量减少无谓的代码调用,提升效率。 - -如果你使用了默认提供的富媒体消息格式,云引擎参数中的 `content` 接收的是 JSON 结构的字符串形式。关于这个结构的详细说明,请参考[即时通讯 REST API 使用指南](/sdk/im/guide/rest/)的《富媒体消息格式说明》一节。 - -参数: - -| 参数 | 说明 | -| ----------- | ---------------------------------------------------------------------------------- | -| `fromPeer` | 消息发送者的 ID。 | -| `convId` | 消息所属对话的 ID。 | -| `toPeers` | 解析出的对话相关的 `clientId`。 | -| `transient` | 是否是 transient 消息。 | -| `bin` | 原始消息内容是否为二进制消息。 | -| `content` | 消息体字符串。如果 `bin` 为 `true`,则该字段为原始消息内容做 Base64 转码后的结果。 | -| `receipt` | 是否要求回执。 | -| `timestamp` | 服务器收到消息的时间戳(毫秒)。 | -| `system` | 是否属于系统对话消息。 | -| `sourceIP` | 消息发送者的 IP。 | - -参数示例: - -```json -{ - "fromPeer": "Tom", - "receipt": false, - "groupId": null, - "system": null, - "content": "{\"_lctext\":\"来我们去 XX 传奇玩吧\",\"_lctype\":-1}", - "convId": "5789a33a1b8694ad267d8040", - "toPeers": ["Jerry"], - "bin": false, - "transient": false, - "sourceIP": "121.239.62.103", - "timestamp": 1472200796764 -} -``` - -返回值: - -| 参数 | 约束 | 说明 | -| --------- | ---- | --------------------------------------------------------------------------------------------------------------------------- | -| `drop` | 可选 | 如果返回真值,消息将被丢弃。 | -| `code` | 可选 | 当 `drop` 为 `true` 时可以下发一个应用自定义的整型错误码。 | -| `detail` | 可选 | 当 `drop` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 | -| `bin` | 可选 | 返回的 `content` 内是否为二进制消息,如果不提供则为请求中的 `bin` 值。 | -| `content` | 可选 | 修改后的 `content`,如果不提供则保留原消息。如果 `bin` 为 `true`,则 `content` 需要是二进制消息内容做 Base64 转码后的结果。 | -| `toPeers` | 可选 | 数组,修改后的收件人,如果不提供则保留原收件人。 | - -示例代码: - - - -```js -AV.Cloud.onIMMessageReceived((request) => { - let content = request.params.content; - let processedContent = content.replace("XX 传奇", "**"); - // 必须含有以下语句给服务端一个正确的返回,否则会引起异常 - return { - content: processedContent, - }; -}); -``` - -```python -import json - -@engine.define -def _messageReceived(**params): - content = json.loads(params['content']) - text = content['_lctext'] - content['_lctext'] = text.replace('XX 传奇', '**') - # 必须含有以下语句给服务端一个正确的返回,否则会引起异常 - return { - 'content': json.dumps(content) - } -``` - -```php -Cloud::define("_messageReceived", function($params, $user) { - $content = json_decode($params["content"], true); - $text = $content["_lctext"]; - $content["_lctext"] = preg_replace("XX 传奇", "**", $text); - // 必须含有以下语句给服务端一个正确的返回,否则会引起异常 - return array("content" => json_encode($content)); -}); -``` - -```java -@IMHook(type = IMHookType.messageReceived) - public static Map onMessageReceived(Map params) { - Map result = new HashMap(); - String content = (String)params.get("content"); - String processedContent = content.replace("XX 传奇", "**"); - result.put("content", processedContent); - // 必须含有以下语句给服务端一个正确的返回,否则会引起异常 - return result; - } -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.MessageReceived)] -public static object OnMessageReceived(Dictionary parameters) { - string content = parameters["content"] as string; - string processedContent = content.Replace("XX 中介", "**"); - return new Dictionary { - { "content", processedContent } - }; -} -``` - -```go -// 暂不支持 -``` - - - -而实际上启用上述代码之后,一条消息的时序图如下: - ->RTM: 1. 发送消息 -RTM-->>Engine: 2. 触发 _messageReceived hook 调用 -Engine-->>RTM: 3. 返回 hook 函数处理结果 -RTM-->>SDK: 4. 将 hook 函数处理结果发送给接收方 -`} -/> - -- 上图假设的是对话所有成员都在线,而如果有成员不在线,流程有些不一样,下一节会做介绍。 -- RTM 表示即时通讯服务集群,Engine 表示云引擎服务集群,它们基于内网通讯。 - -#### `_receiversOffline` - -这个 hook 发生在有收件人离线的情况下,你可以通过它来自定义离线推送行为,包括推送内容、被推送用户或略过推送。你也可以直接在 hook 中触发自定义的推送。发往聊天室的消息不会触发此 hook。 - -参数: - -| 参数 | 说明 | -| --------------------- | --------------------------------------------------------------------------------------------------------------------- | -| `fromPeer` | 消息发送者 ID。 | -| `convId` | 消息所属对话的 ID。 | -| `offlinePeers` | 数组,离线的收件人列表。 | -| `content` | 消息内容。 | -| `timestamp` | 服务器收到消息的时间戳(毫秒)。 | -| `mentionAll` | 布尔类型,表示本消息是否 @ 了所有成员。 | -| `mentionOfflinePeers` | 被本消息 @ 且离线的成员 ID。如果 `mentionAll` 为 `true`,则该参数为空,表示所有 `offlinePeers` 参数内的成员全部被 @。 | - -返回值: - -| 参数 | 约束 | 说明 | -| -------------- | ---- | -------------------------------------------------------------------- | -| `skip` | 可选 | 如果为真将跳过推送(比如已经在云引擎里触发了推送或者其他通知)。 | -| `offlinePeers` | 可选 | 数组,筛选过的推送收件人。 | -| `pushMessage` | 可选 | 推送内容,支持自定义 JSON 结构。 | -| `force` | 可选 | 如果为真将强制推送给 `offlinePeers` 里 `mute` 的用户,默认 `false`。 | - -示例代码: - - - -```js -AV.Cloud.onIMReceiversOffline((request) => { - let params = request.params; - let content = params.content; - - // params.content 为消息的内容 - let shortContent = content; - - if (shortContent.length > 6) { - shortContent = content.slice(0, 6); - } - - console.log("shortContent", shortContent); - - return { - pushMessage: JSON.stringify({ - // 自增未读消息的数目,不想自增就设为数字 - badge: "Increment", - sound: "default", - // 使用开发证书 - _profile: "dev", - alert: shortContent, - }), - }; -}); -``` - -```python -@engine.define -def _receiversOffline(**params): - print('_receiversOffline start') - # params['content'] 为消息内容 - content = params['content'] - short_content = content[:6] - print('short_content:', short_content) - payloads = { - # 自增未读消息的数目,不想自增就设为数字 - 'badge': 'Increment', - 'sound': 'default', - # 使用开发证书 - '_profile': 'dev', - 'alert': short_content, - } - print('_receiversOffline end') - return { - 'pushMessage': json.dumps(payloads), - } -``` - -```php -Cloud::define('_receiversOffline', function($params, $user) { - error_log('_receiversOffline start'); - // content 为消息的内容 - $shortContent = $params["content"]; - if (strlen($shortContent) > 6) { - $shortContent = substr($shortContent, 0, 6); - } - - $json = array( - // 自增未读消息的数目,不想自增就设为数字 - "badge" => "Increment", - "sound" => "default", - // 使用开发证书 - "_profile" => "dev", - "alert" => shortContent - ); - - $pushMessage = json_encode($json); - return array( - "pushMessage" => $pushMessage, - ); -}); -``` - -```java -@IMHook(type = IMHookType.receiversOffline) - public static Map onReceiversOffline(Map params) { - // content 为消息内容 - String alert = (String)params.get("content"); - if(alert.length() > 6){ - alert = alert.substring(0, 6); - } - System.out.println(alert); - Map result = new HashMap(); - JSONObject object = new JSONObject(); - // 自增未读消息的数目 - // 不想自增就设为数字 - object.put("badge", "Increment"); - object.put("sound", "default"); - // 使用开发证书 - object.put("_profile", "dev"); - object.put("alert", alert); - result.put("pushMessage", object.toString()); - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ReceiversOffline)] -public static Dictionary OnReceiversOffline(Dictionary parameters) { - string alert = parameters["content"] as string; - if (alert.Length > 6) { - alert = alert.Substring(0, 6); - } - Dictionary pushMessage = new Dictionary { - { "badge", "Increment" }, - { "sound", "default" }, - { "_profile", "dev" }, - { "alert", alert }, - }; - return new Dictionary { - { "pushMessage", JsonSerializer.Serialize(pushMessage) } - }; -} -``` - -```go -// 暂不支持 -``` - - - -#### `_messageSent` - -在消息发送完成后执行,对消息发送性能没有影响,可以用来执行相对耗时的逻辑。 - -参数: - -| 参数 | 说明 | -| -------------- | -------------------------------- | -| `fromPeer` | 消息发送者的 ID。 | -| `convId` | 消息所属对话的 ID。 | -| `msgId` | 消息 ID。 | -| `onlinePeers` | 当前在线发送的用户 ID。 | -| `offlinePeers` | 当前离线的用户 ID。 | -| `transient` | 是否是 transient 消息。 | -| `system` | 是否是 system conversation。 | -| `bin` | 是否是二进制消息。 | -| `content` | 消息体字符串。 | -| `receipt` | 是否要求回执。 | -| `timestamp` | 服务器收到消息的时间戳(毫秒)。 | -| `sourceIP` | 消息发送者的 IP。 | - -参数示例: - -```json -{ - "fromPeer": "Tom", - "receipt": false, - "onlinePeers": [], - "content": "12345678", - "convId": "5789a33a1b8694ad267d8040", - "msgId": "fptKnuYYQMGdiSt_Zs7zDA", - "bin": false, - "transient": false, - "sourceIP": "114.219.127.186", - "offlinePeers": ["Jerry"], - "timestamp": 1472703266522 -} -``` - -返回值: - -这个 hook 不会对返回值进行检查。只需返回 `{}` 即可。 - -示例代码: - -下面代码演示了日志记录相关的操作(在消息发送完后,在云引擎中打印一下日志): - - - -```js -AV.Cloud.onIMMessageSent((request) => { - console.log("params", request.params); -}); -``` - -```python -@engine.define -def _messageSent(**params): - print('_messageSent start') - print('params:', params) - print('_messageSent end') - return {} -``` - -```php -Cloud::define('_messageSent', function($params, $user) { - error_log('_messageSent start'); - error_log('params' . json_encode($params)); - return array(); -}); -``` - -```java -@IMHook(type = IMHookType.messageSent) - public static Map onMessageSent(Map params) { - System.out.println(params); - Map result = new HashMap(); - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.MessageSent)] -public static Dictionary OnMessageSent(Dictionary parameters) { - Console.WriteLine(JsonSerializer.Serialize(parameters)); - return default; -} -``` - -```go -// 暂不支持 -``` - - - -#### `_messageUpdate` - -这个 hook 发生在修改消息请求到达云端,云端正式修改消息之前。 - -你可以通过返回参数控制修改消息请求是否需要被丢弃,删除个别收件人,或再次修改这个修改消息请求中的消息内容。 - -请注意,在这个 hook 的代码实现的任何分支上 **请确保最终会调用 `response.success` 返回结果**,使得修改消息可以尽快投递给收件人。这个 hook 将 **阻塞发送流程**,因此请尽量减少无谓的代码调用,提升效率。 - -如果你使用了默认提供的富媒体消息格式,云引擎参数中的 `content` 接收的是 JSON 结构的字符串形式。关于这个结构的详细说明,请参考[即时通讯 REST API 使用指南](/sdk/im/guide/rest/)的《富媒体消息》一节。 - -参数: - -| 参数 | 说明 | -| ----------- | ---------------------------------------------------------------------------------- | -| `fromPeer` | 消息发送者的 ID。 | -| `convId` | 消息所属对话的 ID。 | -| `toPeers` | 解析出的对话相关的 `clientId`。 | -| `bin` | 原始消息内容是否为二进制消息。 | -| `content` | 消息体字符串。如果 `bin` 为 `true`,则该字段为原始消息内容做 Base64 转码后的结果。 | -| `timestamp` | 服务器收到消息的时间戳(毫秒)。 | -| `msgId` | 被修改的消息 ID。 | -| `sourceIP` | 消息发送者的 IP。 | -| `recall` | 是否撤回消息。 | -| `system` | 是否属于系统对话消息。 | - -返回值: - -| 参数 | 约束 | 说明 | -| --------- | ---- | --------------------------------------------------------------------------------------------------------------------------- | -| `drop` | 可选 | 如果返回真值,修改消息请求将被丢弃。 | -| `code` | 可选 | 当 `drop` 为 `true` 时可以下发一个应用自定义的整型错误码。 | -| `detail` | 可选 | 当 `drop` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 | -| `bin` | 可选 | 返回的 `content` 内是否为二进制消息,如果不提供则为请求中的 `bin` 值。 | -| `content` | 可选 | 修改后的 `content`,如果不提供则保留原消息。如果 `bin` 为 `true`,则 `content` 需要是二进制消息内容做 Base64 转码后的结果。 | -| `toPeers` | 可选 | 数组,修改后的收件人,如果不提供则保留原收件人。 | - -#### `_conversationStart` - -在创建对话时调用,发生在签名验证(如果开启)之后、创建对话之前。 - -参数: - -| 参数 | 说明 | -| --------- | ---------------------------- | -| `initBy` | 由谁发起的 `clientId`。 | -| `members` | 初始成员数组,包含初始成员。 | -| `attr` | 创建对话时的额外属性。 | - -参数示例: - -``` -{ - "initBy": "Tom", - "members": ["Tom", "Jerry"], - "attr": { - "name": "Tom & Jerry" - } -} -``` - -返回值: - -| 参数 | 约束 | 说明 | -| -------- | ---- | ---------------------------------------------------------------- | -| `reject` | 可选 | 是否拒绝,默认为 `false`。 | -| `code` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的整型错误码。 | -| `detail` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 | - -例如,初始成员不足四人,不允许创建对话: - - - -```js -AV.Cloud.onIMConversationStart((request) => { - if (request.params.members.length < 4) { - return { - reject: true, - code: 1234, - detail: "至少邀请 3 人开启对话", - }; - } else { - return {}; - } -}); -``` - -```python -@engine.define -def _conversationStart(**params): - if len(params["members"]) < 4: - return { - "reject": True, - "code": 1234, - "detail": "至少邀请 3 人开启对话", - } - else: - return {} -``` - -```php -Cloud::define('_conversationStart', function($params, $user) { - if (count($params["members"]) < 4) { - return [ - "reject" => true, - "code" => 1234, - "detail" => "至少邀请 3 人开启对话", - ]; - } else { - return array(); - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationStart) -public static Map onConversationStart(Map params) { - String[] members = (String[])params.get("members"); - Map result = new HashMap(); - if (members.length < 4) { - result.put("reject", true); - result.put("code", 1234); - result.put("detail", "至少邀请 3 人开启对话"); - } - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationStart)] -public static object OnConversationStart(Dictionary parameters) { - List members = parameters["members"] as List; - if (members.Count < 4) { - return new Dictionary { - { "reject", true }, - { "code", 1234 }, - { "detail", "至少邀请 3 人开启对话" } - }; - } - return default; -} -``` - -```go -// 暂不支持 -``` - - - -#### `_conversationStarted` - -对话创建后调用。 - -参数: - -| 参数 | 说明 | -| -------- | ----------------- | -| `convId` | 新生成的对话 ID。 | - -返回值: - -这个 hook 不对返回值进行处理,只需返回 `{}` 即可。 - -例如,创建对话后,将对话的 ID 存储到 LeanCache 的最近创建对话列表: - - - -```js -AV.Cloud.onIMConversationStarted((request) => { - redisClient.lpush("recent_conversations", request.params.convId); - return {}; -}); -``` - -```python -@engine.define -def _conversationStarted(**params): - redis_client.lpush("recent_conversations", params["convId"]) - return {} -``` - -```php -Cloud::define('_conversationStarted', function($params, $user) { - $redis->lpush("recent_conversations", $params["convId"]); - return array(); -}); -``` - -```java -@IMHook(type = IMHookType.conversationStarted) -public static Map onConversationStarted(Map params) throws Exception { - String convId = (String)params.get("convId"); - jedis.lpush("recent_conversations", params.get("convId")); - Map result = new HashMap(); - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationStarted)] -public static object OnConversationStarted(Dictionary parameters) { - string convId = parameters["convId"] as string; - Console.WriteLine($"{convId} started"); - return default; -} -``` - -```go -// 暂不支持 -``` - - - -#### `_conversationAdd` - -在将用户加入到对话时调用,发生在签名验证(如果开启)之后、加入对话之前,包括主动加入和被其他用户加入两种情况都会触发。**注意如果在创建对话时传入了其他用户的 `clientId` 作为成员,则不会触发该 hook。**如果是自己加入,那么 `initBy` 和 `members` 的唯一元素是一样的。 - -参数: - -| 参数 | 说明 | -| --------- | ----------------------- | -| `initBy` | 由谁发起的 `clientId`。 | -| `members` | 要加入的成员,数组。 | -| `convId` | 对话 ID。 | - -返回值: - -| 参数 | 约束 | 说明 | -| -------- | ---- | ---------------------------------------------------------------- | -| `reject` | 可选 | 是否拒绝,默认为 `false`。 | -| `code` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的整型错误码。 | -| `detail` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 | - -例如,不允许某成员创建的对话新增成员: - - - -```js -AV.Cloud.onIMConversationAdd((request) => { - if (request.params.initBy === "Tom") { - return { - reject: true, - code: 9890, - detail: "会话已封闭,不允许新增成员。", - }; - } else { - return {}; - } -}); -``` - -```python -@engine.define -def _conversationAdd(**params): - if params["initBy"] == "Tom": - return { - "reject": True, - "code": 9890, - "detail": "会话已封闭,不允许新增成员。" - } - else: - return {} -``` - -```php -Cloud::define('_conversationAdd', function($params, $user) { - if ($params["initBy"] === "Tom") { - return [ - "reject" => true, - "code" => 9890, - "detail" => "会话已封闭,不允许新增成员。", - ]; - } else { - return array(); - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationAdd) -public static Map onConversationAdd(Map params) { - Map result = new HashMap(); - if ("Tom".equals(params.get("initBy"))) { - result.put("reject", true); - result.put("code", 9890); - result.put("detail", "会话已封闭,不允许新增成员。") - } - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationAdd)] -public static object OnConversationAdd(Dictionary parameters) { - if ("Tom".Equals(parameters["initBy"])) { - return new Dictionary { - { "reject", true }, - { "code", 9890 }, - { "detail", "会话已封闭,不允许新增成员。" } - }; - } - return default; -} -``` - -```go -// 暂不支持 -``` - - - -#### `_conversationRemove` - -从对话中移除成员,在签名校验(如果开启)之后、实际踢出之前触发,用户自己退出对话不会触发。 - -参数: - -| 参数 | 说明 | -| --------- | -------------------- | -| `initBy` | 由谁发起。 | -| `members` | 要踢出的成员,数组。 | -| `convId` | 对话 ID。 | - -返回值: - -| 参数 | 约束 | 说明 | -| -------- | ---- | ---------------------------------------------------------------- | -| `reject` | 可选 | 是否拒绝,默认为 `false`。 | -| `code` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的整型错误码。 | -| `detail` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 | - -例如,某个应用会让官方运营人员加入每个聊天群,希望加上群主无法踢掉官方运营的限制: - - - -```js -AV.Cloud.onIMConversationRemove(async (request) => { - const supporters = ["Bast", "Hypnos", "Kthanid"]; - const members = request.params.members; - for (const member of members) { - if (supporters.includes(member)) { - return { - "reject": true, - "code": 1928, - "detail": `不允许移除官方运营人员 ${member}`, - }; - } - } - return {}; -} -``` - -```python -@engine.define -def _conversationRemove(**params): - supporters = ["Bast", "Hypnos", "Kthanid"] - members = params["members"] - for member in members: - if member in supporters: - return { - "reject": True, - "code": 1928, - "detail": f"不允许移除官方运营人员 {member}" - } - return {} -``` - -```php -Cloud::define('_conversationRemove', function($params, $user) { - $supporters = array("Bast", "Hypnos", "Kthanid"); - $members = $params["members"]; - foreach ($members as $member) { - if (in_array($member, $supporters)) { - return [ - "reject" => true, - "code" => 1928, - "detail" => "不允许移除官方运营人员 $member", - ]; - } - } - return array(); -}); -``` - -```java -@IMHook(type = IMHookType.conversationRemove) -public static Map onConversationRemove(Map params) { - String[] supporters = {"Bast", "Hypnos", "Kthanid"}; - String[] members = (String[])params.get("members"); - Map result = new HashMap(); - for (String member : members) { - if (Arrays.asList(supporters).contains(member)) { - result.put("reject", true); - result.put("code", 1928); - result.put("detail", "不允许移除官方运营人员 " + member); - } - } - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationRemove)] -public static object OnConversationRemove(Dictionary parameters) { - List supporters = new List { "Bast", "Hypnos", "Kthanid" }; - List members = parameters["members"] as List; - foreach (object member in members) { - if (supporters.Contains(member as string)) { - return new Dictionary { - { "reject", true }, - { "code", 1928 }, - { "detail", $"不允许移除官方运营人员 {member}" } - }; - } - } - return default; -} -``` - -```go -// 暂不支持 -``` - - - -#### `_conversationAdded` - -成员成功加入对话后调用。 - -参数: - -| 参数 | 说明 | -| --------- | -------------------- | -| `initBy` | 由谁发起。 | -| `convId` | 对话 ID。 | -| `members` | 新加入的用户 ID 数组 | - -返回值: - -这个 hook 不会对返回值进行检查。 - -例如,如果一个群一次性加入了 10 个以上的成员,那么给运营人员发送一条通知短信: - - - -```js -AV.Cloud.onIMConversationAdded((request) => { - if (request.params.members.length > 10) { - AV.Cloud.requestSmsCode({ - mobilePhoneNumber: "18200008888", - template: "Group_Notice", - sign: "sign_example", - conv_id: request.params.convId, - }).then( - function () { - /* 调用成功 */ - }, - function (err) { - /* 调用失败 */ - } - ); - } -}); -``` - -```python -@engine.define -def _conversationAdded(**params): - if len(params["members"]) > 10: - cloud.request_sms_code( - "18200008888", - template="Group_Notice", sign: "sign_example", - params={"conv_id": params["convId"]} - ) -``` - -```php -Cloud::define('_conversationAdded', function($params, $user) { - if (count($params["members"]) > 10) { - $options = [ - "template" => "Group_Notice", - "name" => "sign_example", - "conv_id" => $params["convId"], - ]; - SMS::requestSmsCode("18200008888", $options); - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationAdded) -public static void onConversationAdded(Map params) { - String[] members = (String[])params.get("members"); - if (members.length > 10) { - LCSMSOption option = new LCSMSOption(); - option.setTemplateName("Group_Notice"); - option.setSignatureName("sign_example"); - Map parameters = new HashMap(); - parameters.put("conv_id", params.get("convId")); - option.setEnvMap(parameters); - LCSMS.requestSMSCodeInBackground("18200008888", option).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) {} - @Override - public void onNext(LCNull avNull) { - Log.d("TAG","Result: Successfully sent text message."); - } - @Override - public void onError(Throwable throwable) { - Log.d("TAG","Result: Failed to send text message. Reason: " + throwable.getMessage()); - } - @Override - public void onComplete() {} - }); - } -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationAdded)] -public static async Task OnConversationAdded(Dictionary parameters) { - List members = (parameters["members"] as List) - .Cast() - .ToList(); - if (members.Count > 10) { - Dictionary variables = new Dictionary { - { "conv_id", request.Params["convId"] } - }; - try { - await LCSMSClient.RequestSMSCode("18200008888", "Group_Notice", "sign_example", variables: variables); - Console.WriteLine("Successfully sent text message."); - } catch (Exception e) { - Console.WriteLine($"Failed to send text message. Reason: {e.Message}"); - } - } -} -``` - -```go -// 暂不支持 -``` - - - -#### `_conversationRemoved` - -成员成功离开对话后调用。 - -参数: - -| 参数 | 说明 | -| --------- | -------------------- | -| `initBy` | 由谁发起。 | -| `convId` | 对话 ID。 | -| `members` | 离开的用户 ID 数组 | - -返回值: - -这个 hook 不会对返回值进行检查。 - -例如,如果用户自行离开了对话,那么将这个对话的 ID 存储到 LeanCache(应用可以利用这些数据实现展示「最近离开的对话」乃至重新加入的功能): - - - -```js -AV.Cloud.onIMConversationRemoved((request) => { - const initBy = request.params.initBy; - const members = request.params.members; - if (members.length === 1) { - if (members[0] === initBy) { - redisClient.lpush(initBy, request.params.convId); - } - } -}); -``` - -```python -@engine.define -def _conversationRemoved(**params): - init_by = params["initBy"] - members = params["members"] - if len(members) == 1: - if members[0] == init_by: - redis_client.lpush(init_by, params["convId"]) -``` - -```php -Cloud::define('_conversationRemoved', function($params, $user) { - $initBy = $params['initBy']; - $members = $params['members']; - if (count($members) === 1) { - if (members[0] === $initBy) { - $redis->lpush($initBy, $params["convId"]); - } - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationRemoved) -public static void onConversationRemoved(Map params) { - String[] members = (String[])params.get("members"); - String initBy = (String)params.get("initBy"); - if (members.length == 1) { - if (initBy.equals(members[0])) { - jedis.lpush(initBy, params.get("convId")); - } - } -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationRemoved)] -public static void OnConversationRemoved(Dictionary parameters) { - List members = (parameters["members"] as List) - .Cast() - .ToList(); - string initBy = parameters["initBy"] as string; - if (members.Count == 1 && members[0].Equals(initBy)) { - Console.WriteLine($"{parameters["convId"]} removed."); - } -} -``` - -```go -// 暂不支持 -``` - - - -#### `_conversationUpdate` - -在修改对话名称、自定义属性,设置或取消对话消息提醒之前调用。 - -参数: - -| 参数 | 说明 | -| -------- | ---------------------- | -| `initBy` | 由谁发起。 | -| `convId` | 对话 ID。 | -| `mute` | 是否关闭当前对话提醒。 | -| `attr` | 待设置的对话属性。 | - -`mute` 和 `attr` 参数互斥,不会同时传递。 - -返回值: - -| 参数 | 约束 | 说明 | -| -------- | ---- | ------------------------------------------------------------------ | -| `reject` | 可选 | 是否拒绝,默认为 `false`。 | -| `code` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的整型错误码。 | -| `detail` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 | -| `attr` | 可选 | 修改后的待设置对话属性,如果不提供则保持原参数中的对话属性。 | -| `mute` | 可选 | 修改后的关闭对话提醒设置,如果不提供则保持原参数中的关闭提醒设置。 | - -`mute` 和 `attr` 参数互斥,不能同时返回。并且返回值必须与请求对应,请求中如果带着 `attr`,则返回值中只有 `attr` 参数有效,返回 `mute` 会被丢弃。同理,请求中如果带着 `mute`,返回值中如果有 `attr` 则 `attr` 会被丢弃。 - -例如,不允许修改对话名称: - - - -```js -AV.Cloud.onIMConversationUpdate((request) => { - if ("attr" in request.params && "name" in request.params.attr) { - return { - reject: true, - code: 1949, - detail: "对话名称不可修改", - }; - } -}); -``` - -```python -@engine.define -def _conversationUpdate(**params): - if ('attr' in params) and ('name' in params['attr']): - return { - "reject": True, - "code": 1949, - "detail": "对话名称不可修改" - } -``` - -```php -Cloud::define('_conversationUpdate', function($params, $user) { - if (array_key_exists('attr', $params) && array_key_exists('name', $params["attr"])) { - return [ - "reject" => true, - "code" => 1949, - "detail" => "对话名称不可修改", - ]; - } -}); -``` - -```java -@IMHook(type = IMHookType.conversationUpdate) -public static Map onConversationUpdate(Map params) { - Map result = new HashMap(); - Map attr = (Map)params.get("attr"); - if (attr != null && attr.containsKey("name")) { - result.put("reject", true); - result.put("code", 1949); - result.put("detail", "对话名称不可修改"); - } - return result; -} -``` - -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationUpdate)] -public static object OnConversationUpdate(Dictionary parameters) { - Dictionary attr = parameters["attr"] as Dictionary; - if (attr != null && attr.ContainsKey("name")) { - return new Dictionary { - { "reject", true }, - { "code", 1949 }, - { "detail", "对话名称不可修改" } - }; - } - return default; -} -``` - -```go -// 暂不支持 -``` - - - -#### `_clientOnline` - -客户端上线,客户端登录成功后调用。 - -请注意本 Hook 仅作为用户上线后的通知。如果用户快速的进行上下线切换或有多个设备同时进行上下线,则上线和下线 Hook 调用顺序并不做严格保证,即可能出现某用户 `_clientOffline` Hook 先于 `_clientOnline` Hook 而调用。 - -参数: - -| 参数 | 说明 | -| --------- | ----------------------------------------------------------------------------------------------------------- | -| peerId | 登录者的 ID | -| sourceIP | 登录者的 IP | -| tag | 无值或值为 "default" 表示主动登录时不会根据 tag 踢其他设备下线,其他情况下会踢当前登录的相同 tag 的设备下线 | -| reconnect | 标识客户端本次登录是否是自动重连,无值或值为 0 表示主动登录,值为 1 表示自动重连 | - -返回: - -这个 hook 不会对返回值进行检查。 - -例如,客户端上线后更新 LeanCache,供查询客户端的实时在线状态: - - - -```js -AV.Cloud.onIMClientOnline((request) => { - // 1 表示在线 - redisClient.set(request.params.peerId, 1); -}); -``` - -```python -@engine.define -def _clientOnline(**params): - # 1 表示在线 - redis_client.set(params["peerId"], 1) -``` - -```php -Cloud::define('_clientOnline', function($params, $user) { - // 1 表示在线 - $redis->set($params["peerId"], 1); -} -``` - -```java -@IMHook(type = IMHookType.clientOnline) -public static void onClientOnline(Map params) { - // 1 表示在线 - jedis.set(params.get("peerId"), 1); -} -``` - -```cs -// 注意,C# 代码示例中没有更新 LeanCache,仅仅输出了用户状态 -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ClientOnline)] -public static void OnClientOnline(Dictionary parameters) { - Console.WriteLine($"{parameters["peerId"]} online."); -} -``` - -```go -// 暂不支持 -``` - - - -#### `_clientOffline` - -在客户端登出成功或意外下线后调用。 - -请注意本 Hook 仅作为用户下线后的通知。如果用户快速的进行上下线切换或有多个设备同时进行上下线,则上线和下线 Hook 调用顺序并不做严格保证,即可能出现某用户 `_clientOffline` Hook 先于 `_clientOnline` Hook 而调用。 - -参数: - -| 参数 | 说明 | -| --------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| peerId | 登出者的 ID | -| closeCode | 登出的方式,1 代表用户主动登出,2 代表连接断开,3 代表用户由于 `tag` 冲突被踢下线,4 代表用户被 API 踢下线 | -| closeMsg | 对登出方式的描述信息 | -| sourceIP | 执行关闭会话操作的用户的 IP,连接断开时不传递此参数 | -| tag | 由用户在会话创建时传递而来,无值或值为 `default` 表示主动登录时不会根据 tag 踢其他设备下线,其他情况下会踢当前登录的相同 tag 的设备下线 | -| errorCode | 导致连接断开的错误码,可选 | -| errorMsg | 导致连接断开的错误信息,可选 | - -可能出现的错误信息说明: - -| 错误码 | 错误信息 | 解释 | -| ------ | ------------------- | ------------------------------------ | -| 4107 | READ_TIMEOUT | 长时间未发送消息或心跳包导致连接超时 | -| 4108 | LOGIN_TIMEOUT | 一定时间内未登录而导致超时 | -| 4109 | FRAME_TOO_LONG | WebSocket 帧过长 | -| 4114 | UNPARSEABLE_RAW_MSG | 消息格式错误无法解析 | -| 4200 | INTERNAL_ERROR | 服务器内部错误 | - -返回: - -这个 hook 不会对返回值进行检查。 - -例如,客户端下线后更新 LeanCache,供查询客户端的实时在线状态: - - - -```js -AV.Cloud.onIMClientOffline((request) => { - // 0 表示下线 - redisClient.set(request.params.peerId, 0); -}); -``` - -```python -@engine.define -def _clientOffline(**params): - # 0 表示下线 - redis_client.set(params["peerId"], 0) -``` - -```php -Cloud::define('_clientOffline', function($params, $user) { - // 0 表示下线 - $redis->set($params["peerId"], 0); -} -``` - -```java -@IMHook(type = IMHookType.clientOffline) -public static void onClientOffline(Map params) { - // 0 表示下线 - jedis.set(params.get("peerId"), 0); -} -``` - -```cs -// 注意,C# 代码示例中没有更新 LeanCache,仅仅输出了用户状态 -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ClientOffline)] -public static void OnClientOffline(Dictionary parameters) { - Console.WriteLine($"{parameters["peerId"]} offline"); -} -``` - -```go -// 暂不支持 -``` - - - -[《即时通讯中的在线状态查询》](https://docs.leancloud.app/sdk/im/best-practice/realtime-guide-onoff-status/)提供了完整的 Node.js 示例(包括 LeanCache 连接,久未上线的客户端清理,配套的返回在线状态的云函数,以及如何在客户端调用),可以参考。 - -## 「系统对话」的使用 - -系统对话可以用于实现机器人自动回复、公众号、服务账号等功能。在我们的 [官方聊天 Demo](https://leancloud.github.io/leanmessage-demo/) 中就有一个使用系统对话 hook 实现的机器人 MathBot,它能计算用户发送来的数学表达式并返回结果,[其服务端源码](https://github.com/leancloud/leanmessage-demo/tree/master/server) 可以从 GitHub 上获取。 - -### 系统对话的创建 - -系统对话也是对话的一种,创建后也是在 `_Conversation` 表中增加一条记录,只是该记录 `sys` 列的值为 `true`,从而与普通会话进行区别。具体创建方法请参考[即时通讯 REST API 使用指南](/sdk/im/guide/rest/)的《创建服务号》一节。 - -### 系统对话消息的发送 - -系统对话给用户发消息请参考[即时通讯 REST API 使用指南](/sdk/im/guide/rest/)的《给任意用户单独发消息》一节。用户给系统对话发送消息跟用户给普通对话发消息方法一致。 - -你还可以利用系统对话发送广播消息给全部用户。相比遍历所有用户 ID 逐个发送,广播消息只需要调用一次 REST API。广播消息具有以下特征: - -- 广播消息必须与系统对话关联,广播消息和一般的系统对话消息会混合在系统对话的记录中 -- 用户离线时发送的广播消息,上线后会作为离线消息收到 -- 广播消息具有实效性,可以设置过期时间;过期的消息不会作为离线消息发送给用户,不过仍然可以在历史消息中获取到 -- 新用户第一次登录后,会收到最近一条未过期的系统广播消息 - -除此以外广播消息与普通消息的处理完全一致。广播消息的发送可以参考[即时通讯 REST API 使用指南](/sdk/im/guide/rest/)的《全局广播》一节。 - -### 获取系统对话消息记录 - -获取系统对话给用户发送的消息记录请参考:[即时通讯 REST API 使用指南](/sdk/im/guide/rest/)的《查询服务号给某用户发的消息》一节。 - -获取用户给系统对话发送的消息记录有以下两种方式实现: - -- `_SysMessage` 表方式,在应用首次有用户发送消息给某系统对话时自动创建,创建后我们将所有由用户发送到系统对话的消息都存储在该表中。 -- [Web Hook](#web-hook) 方式,这种方式需要开发者自行定义 [Web Hook](#web-hook),用于实时接收用户发给系统对话的消息。 - -### 系统对话消息结构 - -#### `_SysMessage` - -存储用户发给系统对话的消息,各字段含义如下: - -| 字段 | 说明 | -| ----------- | ------------------------------ | -| `ackAt` | 消息送达的时间。 | -| `bin` | 是否为二进制类型消息。 | -| `conv` | 消息关联的系统对话 `Pointer`。 | -| `data` | 消息内容。 | -| `from` | 发消息用户的 `clientId`。 | -| `fromIp` | 发消息用户的 IP。 | -| `msgId` | 消息的内部 ID。 | -| `timestamp` | 消息创建的时间。 | - -#### Web Hook - -需要开发者自行在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 服务号消息回调** 定义,来实时接收用户发给系统对话的消息,消息的数据结构与上文所述的 `_SysMessage` 一致。 - -当有用户向系统对话发送消息时,我们会通过 HTTP POST 请求将 JSON 格式的数据发送到用户设置的 Web Hook 上。请注意,我们调用 Web Hook 时并不是一次调用只发送一条消息,而是会以批量的形式将消息发送过去。从下面的发送消息格式中能看到,JSON 的最外层是个 `Array`。 - -超时时间为 5 秒,当用户 hook 地址超时没有响应,我们会重试至多 3 次。 - -发送的消息格式为: - -```json -[ - { - "fromIp": "121.238.214.92", - "conv": { - "__type": "Pointer", - "className": "_Conversation", - "objectId": "55b99ad700b0387b8a3d7bf0" - }, - "msgId": "nYH9iBSBS_uogCEgvZwE7Q", - "from": "A", - "bin": false, - "data": "你好,sys", - "createdAt": { - "__type": "Date", - "iso": "2015-07-30T14:37:42.584Z" - }, - "updatedAt": { - "__type": "Date", - "iso": "2015-07-30T14:37:42.584Z" - } - } -] -``` - -## 即时通讯开发指南一览 - -- [服务总览](/sdk/im/guide/overview/) -- 《即时通讯开发指南》第一篇[从简单的单聊、群聊、收发图文消息开始](/sdk/im/guide/beginner/) -- 《即时通讯开发指南》第二篇[消息收发的更多方式,离线推送与消息同步,多设备登录](/sdk/im/guide/intermediate/) -- 《即时通讯开发指南》第三篇[安全与签名、玩转聊天室和临时对话](/sdk/im/guide/senior/) diff --git a/leancloud/docs/sdk/leaderboard/_category_.json b/leancloud/docs/sdk/leaderboard/_category_.json deleted file mode 100644 index a13c3aec4..000000000 --- a/leancloud/docs/sdk/leaderboard/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "排行榜", - "collapsed": true, - "position": 15.1 -} diff --git a/leancloud/docs/sdk/leaderboard/features.mdx b/leancloud/docs/sdk/leaderboard/features.mdx deleted file mode 100644 index 1a1907e8e..000000000 --- a/leancloud/docs/sdk/leaderboard/features.mdx +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: 排行榜功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -:::tip - -在游戏中设立排行榜功能,可以推动玩家之间的趣味性竞争,从而帮助提升游戏的玩家活跃度。 - -::: - -**排行榜**服务提供的功能包括: - -- **自动计算排名:**排行榜中玩家数据更新时,系统自动重新计算排名。 -- **获取前排数据:**获取当前排行榜中排名前 N 个玩家数据。 -- **获取当前玩家的排名:**即使当前玩家没有名列前茅,也可以获取他在排行中的具体名次,以此了解到当前玩家和前排玩家的差距。 -- **获取当前玩家附近排名的玩家:**例如为当前玩家寻找水平相当的对手或好友。 -- **数据重置:**支持自动定期重置或手动重置数据,比如在一个赛季结束之时或在指定天、周、月后自动重置排行榜,或是在应用测试阶段、应用数据出现误差的情况下进行手动重置。 -- **简便的数据更新模式:**提供三种分数更新模式,better 模式会保留玩家的最好成绩,last 会保留玩家的最新成绩,sum 会累加当前成绩。 - -## 创建排行榜 - -:::note - -我们推荐在 TapTap 开发者中心的排行榜控制台提前创建排行榜,客户端指定相应排行榜名称 `statisticName` 即可更新成绩。 - -::: - -每一个排行榜由名称 `statisticName` 及成员的成绩 `statisticValue` 组成,并可以设置排序、更新策略及重置周期。 - -### 名称 - -`statisticName` 是排行榜名称,不可和应用下的其他排行榜重复,不可修改,只能包含字母、数字、下划线,并且以字母开头。 - -### 成员类型 - -`memberType` 是排行榜的成员类型,目前支持三种类型的成员: - -- user 类型:值为 `_User`。排行榜成员对应内建账户系统(`_User` 表)中某一个用户的 `objectId`。这是唯一可以在客户端更新成绩(仅限玩家本人的成绩)的排行榜类型。 -- object 类型:值为数据存储中除了 `_User` 之外的 class 名称,排行榜成员对应表中一个对象的 `objectId`。例如你有一个 `Weapon` 表,可以在 memberType 中填入 `Weapon`,这样可以构建一个武器排行榜。 -- entity 类型:填入的值为 `_Entity`。排行榜成员是开发者自行指定的字符串数据,只能包含字母、数字、下划线。 - -注意: - -- 查询排行榜时,可以指定相关参数直接获取 user 及 object 的更多数据。 -- object 和 entity 类型的排行榜需要**使用超级权限在服务端**才能更新。 -- 在控制台勾选「只允许使用 Master Key 更新分数」后(默认未勾选),user 类型的排行榜同样需要使用超级权限在服务端更新。从反作弊的角度出发,建议勾选此项。 - -## 上传成绩 - -`statisticValue` 是用户在客户端产生的成绩(数字),比如得分、击杀数、用时。 - -### 设置排序策略 - -- `descending`:降序,排名按成绩由高到低排列,在许多游戏中,玩家得分越高,排名越高。 -- `ascending`:升序,排名按成绩由低到高排列,例如在某些竞速游戏中,完成任务的时间越短排名越高。 - -### 更新策略 - -`updateStrategy` 代表成绩的更新策略,一个排行榜可以选择下列更新策略之一: - -- `better`:保留玩家**最好的**成绩。也就是说,排序策略为降序时,成绩高于之前成绩时更新,排序策略为升序时,成绩低于之前成绩时更新。 -- `last`:保留玩家**最新的**成绩。也就是说,每次玩家更新成绩时都会覆盖掉之前的成绩。 -- `sum`:累加玩家的成绩。每次更新成绩时都会将本次成绩累加到当前的总成绩。 - -## 数据重置 - -排行榜可以被重置,重置后线上所有数据清零,例如在赛季结束后清零所有数据。从重置时间开始的一刻起,所有老数据会被清理,排行版的版本号(`Version`)会更新(加一),客户端再发起的更新分数的请求将自动进入新版本的数据中。 - -`versionChangeInterval` 代表数据重置周期,共有以下选项: - -- `day`:每天凌晨 00:00 重置。 -- `week`:每周一凌晨 00:00 重置。 -- `month`:每月 1 日凌晨 00:00 重置。 -- `never`:不自动重置,在必要的时候手动重置。 - - - -:::info -请注意,国内版和国际版重置时间不同,对于需要在凌晨 00:00 重置的周期(day/week/month): - -- 国内版应用会在北京时间 00:00 重置。 -- 国际版应用会在 UTC 标准时间 00:00 重置。 -::: - - - -## 查询历史数据 - -在排行榜重置后,客户端通过 SDK 只能查询当前版本及前一版本的数据,更前版本的数据无法查询。 -如果在排行榜控制台未勾选「保留前一版本」(默认未勾选),则排行榜重置后,客户端通过 SDK 只能查询当前版本的数据。 -重置周期为 `never` 的排行榜还有一条额外限制,手动重置那一刻起 7 天内可以查询前一版本的数据,超过 7 天后无法查询历史数据。 -例如: - -- 假设排行榜重置周期为 `month`,当前为 3 月,3 月 1 日排行榜重置后当前版本为 3,那么可以查询版本为 2 (2 月份)的历史数据,无法查询版本为 1 (1 月份)的数据。 -- 假设排行榜重置周期为 `never`,现在手动重置排行榜,排行版版本变为 3,那么 7 日内可以查询版本为 2 的历史数据,超过 7 日后无法查询历史数据。 - -虽然更前版本的数据无法通过 SDK 查询,但可以通过 REST API 获取归档的 CSV 文件。 -不过,每个排行榜最多保存 60 个历史数据归档,**超出后最老的版本会被删除**。 -因此,如需长期保存,请注意及时下载归档后另行备份。 diff --git a/leancloud/docs/sdk/leaderboard/guide/_category_.json b/leancloud/docs/sdk/leaderboard/guide/_category_.json deleted file mode 100644 index bbd78eabb..000000000 --- a/leancloud/docs/sdk/leaderboard/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 3 -} diff --git a/leancloud/docs/sdk/leaderboard/quick-start/_category_.json b/leancloud/docs/sdk/leaderboard/quick-start/_category_.json deleted file mode 100644 index 9ff402a45..000000000 --- a/leancloud/docs/sdk/leaderboard/quick-start/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "快速入门", - "collapsed": true, - "position": 2 -} diff --git a/leancloud/docs/sdk/multiplayer/_category_.json b/leancloud/docs/sdk/multiplayer/_category_.json deleted file mode 100644 index 73a44656d..000000000 --- a/leancloud/docs/sdk/multiplayer/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "多人在线对战", - "collapsed": true, - "position": 17 -} diff --git a/leancloud/docs/sdk/multiplayer/client-engine/_category_.json b/leancloud/docs/sdk/multiplayer/client-engine/_category_.json deleted file mode 100644 index 7bf7181d3..000000000 --- a/leancloud/docs/sdk/multiplayer/client-engine/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Client Engine", - "collapsed": true, - "position": 4 -} diff --git a/leancloud/docs/sdk/multiplayer/client-engine/client-engine.mdx b/leancloud/docs/sdk/multiplayer/client-engine/client-engine.mdx deleted file mode 100644 index e88777bf4..000000000 --- a/leancloud/docs/sdk/multiplayer/client-engine/client-engine.mdx +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Client Engine 总览 -sidebar_label: 总览 -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -:::info -**在阅读本文档之前,请先阅读[多人在线对战服务功能介绍](/sdk/multiplayer/features/)及 [MasterClient](/sdk/multiplayer/guide/js/#masterclient) ,了解多人在线对战开发的基础结构。** -::: - -## Client Engine 解决了什么问题 - -多人在线对战服务(即下图中的 Multiplayer 服务)很好的解决了「房间」抽象与房间之间玩家交换消息的问题。我们以「剪刀石头布」游戏为例,游戏的流程是这样的: - -![image](/img/client-engine/client-engine1.png) - -其中 Multiplayer 服务扮演的是一个纯粹的消息转发角色,为了方便之后的讨论,我们可以把这个图简化一下(虚线代表消息是通过 Multiplayer 服务传递的): - -![image](/img/client-engine/client-engine2.png) - -这个流程很简单,但也存在一些问题: - -1. 所有的玩家都是一样的上帝视角,能看到所有的状态,后出拳的玩家(如图中的 B)可以根据 A 的选择对应出拳达到必胜的效果。 -2. 最终结果是由客户端上报的,客户端可以伪造结果上报。 -3. A 作为 MasterClient 可以操纵一些涉及随机的操作,例如洗牌、掷骰子等(「剪刀石头布」里并没有类似的机制)。 - -不同的游戏类型对于这些问题的容忍度是不同的。在不改变上图流程的前提下,这些问题也都能找到各自改进的方案。但 Client Engine 方案试图用一个釜底抽薪的方法解决这些问题:把 MasterClient 运行到服务端。在 Client Engine 方案里,游戏的流程是这样的: - -![image](/img/client-engine/client-engine3.png) - -在这个流程中,在服务端运行的 MasterClient 是唯一拥有上帝视角的裁判。所有玩家都只与 MasterClient 交换信息,MasterClient 只会给客户端同步部分信息(比如只告诉 B「A 出拳了」,但出了什么未知)。游戏逻辑的运算(包括随机、胜负判定)以及最终结果的上报都在服务端进行。 - -**Multiplayer 服务是基础。玩家客户端并不与 MasterClient 直接通信,图中的虚线表示消息仍然是通过 Multiplayer 服务转发的。** - -## Client Engine 介绍 - -Client Engine 是多人在线游戏 Client 托管方案。[多人在线对战服务](/sdk/multiplayer/features/)提供了 MasterClient 机制来控制游戏逻辑:MasterClient 是一个特殊的 Client,它接收和处理游戏内的所有事件与消息,进行实时处理之后将结果下发给其他游戏客户端,用以控制游戏向下执行。开发者可以基于多人在线对战的 SDK 开发出一套完整的 MasterClient 逻辑,继而将这样的「客户端」托管到 Client Engine,省去程序部署、运维的负担。如图所示: - -![image](/img/client-engine/structure.png) - -除了 MasterClient 的托管外,开发者还可以在 Client Engine 中 - -* 托管普通的虚拟玩家,增加游戏的趣味性和活跃度。 -* 自定义 REST API 来完成其他逻辑的开发。 - -将游戏逻辑托管在 Client Engine 有以下优势: - -* 网络延迟更低。游戏运行过程中涉及到「游戏玩家」-「多人对战云端」- MasterClient 三方非常频繁的消息交互, Client Engine 与多人对战云端处于同一物理网络,可以大幅减少公网的传输延迟。 -* 运维支持更成熟。Client Engine 提供了完善的日志收集、状态监控、负载均衡以及自动容错恢复机制,可以提供更高的稳定性保障。 -* 自由伸缩更有弹性。Client Engine 提供了庞大的资源池,可以快速响应单个游戏产品临时的、突发的扩容需求,无需手动调整实例,自动完成扩容。 - -## 文档及 Demo - -详细的使用方式请参考文档: - -* [Client Engine 快速入门 · Node.js](/sdk/multiplayer/client-engine/quick-start-node/) 介绍了从初始项目开始,如何本地开发调试,以及部署到云端。 -* [你的第一个 Client Engine 小游戏 · Node.js](/sdk/multiplayer/client-engine/first-game-node/) 该文档帮助您快速上手,通过 Client Engine 实现一个剪刀石头布的猜拳小游戏。完成本文档教程后,您会对 Client Engine 的基础使用流程有初步的理解。 -* [Client Engine 开发指南 · Node.js](/sdk/multiplayer/client-engine/guide-node/) 在初始项目的基础上深入讲解 Client Engine SDK。 - -示例 Demo: - -* [回合制 Demo](/sdk/multiplayer/client-engine/demo/#回合制-demo)。 - -## 价格 - -### 开发版 - -开发版下提供「体验版」供开发者免费使用: - -* 免费 100 CCU 和 50% CPU。 -* 不提供预备环境。 -* 不支持自动扩容及负载均衡。 -* 强制休眠。 - -休眠策略: - -标准实例不会休眠。 - -体验实例会执行休眠策略: - -* 如果应用最近一段时间(半小时)没有任何外部请求,则休眠。 -* 休眠后如果有新的外部请求实例则马上启动。访问者的体验是第一个请求响应时间是 5 ~ 30 秒(视实例启动时间而定),后续访问响应速度恢复正常。 -* 强制休眠:如果最近 24 小时内累计运行超过 18 小时,则强制休眠。此时新的请求会收到 503 的错误响应码,该错误可在 **云服务控制台 > Play > Client Engine > 统计** 中查看。 - -如果不希望预备环境的体验实例因为强制休眠而中断服务,或需要多个实例来完整模拟生产环境,可以在预备环境根据需要购买标准实例。 - -### 商用版 - -商用版下可通过控制台从「体验版」升级到「标准版」,「标准版」提供预备环境,支持自动扩容及负载均衡,不会休眠。 - -「标准版」按照「计算单元」扩容与计费。一个计算单元包含 100 CCU 和 50% CPU,我们会在任意一个指标用尽的时候自动增加计算单元。例如在某一时刻 Client Engine 消耗了 80 CCU 与 90% CPU,则此时系统会为其分配 2 个计算单元。 - - - - -系统会按照每天的计算单元用量峰值计费,单个计算单元的价格请参考[官网](https://developer.taptap.io/product-intro/price)。例如国内节点的一个应用某天最多使用了 2 个计算单元,当天收费为 2 * 国内计算单元价格。 - - - - - -系统会按照每天的计算单元用量峰值计费,单个计算单元的价格请参考[官网](https://developer.taptap.cn/product-intro/price)。例如国内节点的一个应用某天最多使用了 2 个计算单元,当天收费为 2 * 国内计算单元价格。 - - - - -:::info -注意:升级为「标准版」后不论是否使用,都会按照最低 1 个计算单元来提供服务并计费。 -::: diff --git a/leancloud/docs/sdk/multiplayer/client-engine/demo.mdx b/leancloud/docs/sdk/multiplayer/client-engine/demo.mdx deleted file mode 100644 index 6fedafe21..000000000 --- a/leancloud/docs/sdk/multiplayer/client-engine/demo.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: 游戏 Demo 示例 -sidebar_label: Demo -sidebar_position: 5 ---- - -这篇文档集合了游戏相关的所有 Demo,供您在开发自己项目时进行参考。 - -## 多人在线对战 - -### 回合制对战 Demo - -这款 Demo 使用[多人在线对战](/sdk/multiplayer/features/)和 [Client Engine](/sdk/multiplayer/client-engine/) 来实现,实现语言为 JavaScript,全部服务端及客户端的代码总共花费了约 7 天的时间。其中主要实现的功能有:快速开始、设置人物属性、房间内对战等,详情请点击[项目地址](https://github.com/leancloud/multiplayer-turn-based-game-demo)。 - -### 实时对战 Demo - -这款 Demo 是类似于《球球大作战》这种类型游戏的精简版,使用[多人在线对战](/sdk/multiplayer/features/)来实现,分别使用 Cocos Creator(JavaScript) 和 Unity(C#) 实现,花费时间约 8 天。该 Demo 主要演示了移动同步相关的逻辑。 - -详情请点击: - -- [Cocos Creator 项目地址](https://github.com/onerain88/BallBattle) - -- [Unity 项目地址](https://github.com/onerain88/BallBattle-Unity) - -## 弱联网单机游戏 - -《LeanCloud 周年游戏》是一款微信小游戏,玩家只要点击下落的蛋糕就可以获得分数,得分最高者可以获得奖品。服务端主要使用了[云引擎](/sdk/engine/overview/)及[排行榜](/sdk/leaderboard/features/)来实现。详情请点击[项目地址](https://github.com/leancloud/LeanCloudBirthday)。 - -微信扫码试玩: - -![image](/img/client-engine/leancloud-birthday-game.jpg) diff --git a/leancloud/docs/sdk/multiplayer/client-engine/first-game-node.mdx b/leancloud/docs/sdk/multiplayer/client-engine/first-game-node.mdx deleted file mode 100644 index 195fa0220..000000000 --- a/leancloud/docs/sdk/multiplayer/client-engine/first-game-node.mdx +++ /dev/null @@ -1,473 +0,0 @@ ---- -title: 你的第一个 Client Engine 小游戏 · Node.js -sidebar_label: 第一个小游戏 -sidebar_position: 3 ---- - -该文档帮助您快速上手,通过 Client Engine 实现一个剪刀石头布的猜拳小游戏。完成本文档教程后,您会对 Client Engine 的基础使用流程有初步的理解。 - -## 准备初始项目 - -这个小游戏分为服务端和客户端两部分,其中服务端使用 Client Engine 来实现,客户端则是一个简单的 Web 页面。在这个教程中我们着重教您一步一步写 Client Engine 中的代码,客户端的代码请您查看示例项目。 - -### Client Engine 项目 - -请先阅读 [Client Engine 快速入门:运行及部署项目](/sdk/multiplayer/client-engine/quick-start-node/) 获得初始项目,了解如何本地运行及部署项目。 - -`./src` 中主要源文件及用途如下: - -``` -├── configs.ts // 配置文件 -├── index.ts // 项目入口 -├── reception.ts // Reception 类实现文件,GameManager 的子类,负责管理 Game,在这个文件中撰写了创建 Game 的自定义方法 -└── rps-game.ts // RPSGame 类实现文件,Game 的子类,在这个文件中撰写了具体猜拳游戏的逻辑 -``` - -该项目中的 `Game` 及 `GameManager` 使用的是 Client Engine SDK 的功能,关于 SDK 的详细用法请参考 [Client Engine 开发指南](/sdk/multiplayer/client-engine/guide-node/)。 - -您可以从 `index.ts` 文件入手来了解整个项目,该文件是项目启动的入口,它通过 express 框架定义了名为 `/reservation` 的 Web API,供客户端快速开始时为客户端下发新的房间名称。 - -`reception.ts` 及 `rps-game.ts` 里面有本教程的全部代码。您可以选择备份这两个文件,清空这两个文件后根据本文档撰写自己的代码,同时也可以查看已经写好的代码以做对比。 - -### 客户端项目 - -[点击下载客户端项目](https://github.com/leancloud/client-engine-demo-webapp)。**打开 `./src` 中的 `config.ts`,将 appId 和 appKey 修改为自己应用的信息**,按照 README 启动项目后观察界面的变化。游戏相关的逻辑位于 `./src/components` 下的文件中,在有需要的时候您可以打开这里的文件查看代码。 - -## 核心流程 - -在多人对战服务中,房间的创建者为 MasterClient,因此在这个小游戏中,每一个房间都是由 Client Engine 管理的 MasterClient 调用在线对战服务相关的接口来创建的。Client Engine 中会有多个 MasterClient,每一个 MasterClient 管理着自己房间内的游戏逻辑。 - -这个小游戏的核心逻辑为:**Client Engine 中的 MasterClient 及客户端玩家 Client 加入到同一个房间,在通信过程中由 MasterClient 控制游戏内的逻辑。**具体拆解步骤如下: - -1. 玩家客户端连接[多人在线对战服务](/sdk/multiplayer/features/),向 Client Engine 提供的 `/reservation` 接口请求快速开始游戏。 -2. Client Engine 每次收到请求后会检查是否有可用的房间,如果有则返回已有的 roomName 给客户端;如果没有则创建新的 MasterClient 并创建一个新的房间,返回 roomName 给客户端。 -3. 客户端通过 Client Engine 返回的 roomName 加入房间。 -4. MasterClient 和客户端在同一房间内,每次客户端出拳时会将消息发送给 MasterClient,MasterClient 将消息转发给其他客户端,并最终判定游戏结果。 -5. MasterClient 判定游戏结束,客户端离开房间,Client Engine 销毁游戏。 - -## 代码开发 - -### 自定义 Game - -我们的目标是让 MasterClient 和客户端 Client 进入同一个房间,第一步在 Client Engine 中我们先准备好房间。在 Client Engine SDK 中,每一个房间都对应一个 `Game` 对象,每一个 `Game` 对象都对应一个自己的 MasterClient。接下来我们创建一个继承 `Game` 的子类 `RPSGame` ,在 `RPSGame` 中撰写猜拳小游戏的房间内逻辑。 - -在 `rpg-game.ts` 文件中初始化自定义的 `RPSGame`: - -```js -import { Game } from "@leancloud/client-engine"; -import { Event, Play, Room } from "@leancloud/play"; -export default class RPSGame extends Game { - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - } -} -``` - -### 管理 Game - -Client Engine SDK 中,`GameManager` 负责 `Game` 的创建及销毁,具体的原理及结构介绍请参考 [Client Engine 开发指南](/sdk/multiplayer/client-engine/guide-node/)。在这篇文档中,我们通过简单的配置就可以使用 `GameManager` 的管理功能。 - -#### 自定义 GameManager - -首先创建一个子类 `Reception` 继承自 `GameManager`,在这个子类中我们就可以使用 `GameManager` 提供的方法来帮我们撰写自己的逻辑。 - -在 `reception.ts` 文件中初始化自定义的 `Reception`: - -```js -import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine"; -export default class Reception extends GameManager { - -} -``` - -这个自定义的类`Reception` 用于管理 T 类型的 `Game` 对象,在实际游戏中会是您自定义的 `Game` 类型的实例。接下来,我们在 `reception` 中使用 `GameManager` 的方法来实现自己的自定义逻辑:快速开始。 - -#### 实现逻辑:「快速开始」 - -这里我们要实现的快速开始的逻辑是:随便找一个有空位的房间返回给客户端,如果当前的 Client Engine 实例没有可用的房间,那么就创建一个房间返回给客户端。我们在 `Reception` 类中撰写名为 `makeReservation()` 的自定义方法来实现这个逻辑并供[入口 API ](#入口 API:快速开始)调用。 - -```js -import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine"; -export default class Reception extends GameManager { - - /** - * 为指定玩家预约游戏,如果没有可用的游戏会创建一个新的游戏。 - * @param playerId 预约的玩家 ID - * @return 预约成功的游戏的房间 name - */ - public async makeReservation(playerId: string) { - let game: T; - const availableGames = this.getAvailableGames(); - if (availableGames.length > 0) { - game = availableGames[0]; - this.reserveSeats(game, playerId); - } else { - game = await this.createGame(playerId); - } - return game.room.name; - } - -} -``` - -在这段代码中,我们调用了 `GameManager` 的 `getAvailableGames()` 来获得当前 Client Engine 实例管理的 `Game`: - -* 如果有房间内还有空位的 `Game`,使用 `GameManager` 的 `reserveSeats()` 方法为玩家占位并返回 roomName。 -* 如果所有 `Game` 房间都已经满员了,使用 `GameManager` 的 `createGame()` 方法创建一个新的房间并返回 roomName。 - -#### 实现逻辑:「创建新游戏」 - -如果您希望自己先创建房间,再邀请朋友加入该房间,可以在 `reception` 中写一个创建新游戏的方法供供[入口 API ](#入口 API:创建新游戏)调用。同样我们自定义的 `createGameAndGetName()` 方法内用到的 `createGame()` 是由 SDK 中的 `GameManager` 提供的。 - -```js -export default class Reception extends GameManager { - - public async makeReservation(playerId: string) { - ...... - } - - /** - * 创建一个新的游戏。 - * @param playerId 预约的玩家 ID - * @param options 创建新游戏时可以指定的一些配置项 - * @return 创建的游戏的房间 name - */ - public async createGameAndGetName(playerId: string, options?: ICreateGameOptions) { - const game = await this.createGame(playerId, options); - return game.room.name; - } - -} -``` - -#### 绑定 GameManager 及 Game - -当 `GameManager` 的子类 `Reception` 及 `Game` 的子类 `RPSGame` 都准备好后,我们要在整个项目入口把 `RPSGame` 给到 `Reception`,由 `Reception` 来管理 `RPSGame`。 - -在 `index.ts` 文件创建 `Reception` 对象的方法中可以看到,第一个参数已经传入了 `RPSGame`,如果您的自定义 `Game` 使用的是其他的名字,可以将 `RPSGame` 换成您自定义的 `Game` 类。 - -```js -import PRSGame from "./rps-game"; -const reception = new Reception( - PRSGame, - APP_ID, - APP_KEY, - { - concurrency: 2, - }, -); -``` - -在这里配置完成后,`reception` 会在合适的时机创建并管理 `PRSGame` 和对应的 MasterClient。 - -#### 配置负载均衡 - -由于 `GameManager` 中的逻辑会直接被外部请求所调用,因此需要为在入口处为 `GameManager` 配置负载均衡。关于负载均衡详细的介绍可以参考 [Client Engine 开发指南](/sdk/multiplayer/client-engine/guide-node/#负载均衡),在这里我们先简单的查看 `index.ts` 文件中的这些代码,了解如何配置即可: - -```js -import { ICreateGameOptions,LoadBalancerFactory } from "@leancloud/client-engine"; - -// 创建负责负载均衡的对象,此处代码不需要改动,只需要复制粘贴即可 -const loadBalancerFactory = new LoadBalancerFactory({ - poolId: `${APP_ID.slice(0, 5)}-${process.env.LEANCLOUD_APP_ENV || "development"}`, - redisUrl: process.env.REDIS_URL__CLIENT_ENGINE, -}); - -// 将 reception 及我们自定义的方法 makeReservation 配置负载均衡。 -loadBalancerFactory.bind(reception, ["makeReservation", "createGameAndGetName"]) -``` - -到这里,管理 `RPSGame` 的 `reception` 我们已经准备完成,接下来开始撰写具体的房间内游戏逻辑。 - -### 设定房间内玩家数量 - -在这个猜拳小游戏中,我们设定只允许两个玩家玩,满两个玩家后就不允许新的玩家再进入房间,可以这样设置 `Game` 的静态属性 `defaultSeatCount`: - -```js -export default class RPSGame extends Game { - public static defaultSeatCount = 2; -} -``` - -在这里配置完成后,Client Engine 初始项目每次请求多人对战服务创建房间时,都会根据这里的值限定房间内的玩家数量。 - -对设置房间内玩家数量的详细讲解请参考 [Client Engine 开发指南](/sdk/multiplayer/client-engine/guide-node/#设置房间内玩家数量)。 - -### MasterClient 及客户端进入同一房间 - -在完成 Game 的基础配置之后,MasterClient 和客户端就可以准备加入同一个房间了。 - -#### 入口 API:快速开始 - -`index.ts` 文件的入口 API `/reservation` ,当客户端调用这个接口时,该接口会调用 `Reception` 自定义的 `makeReservation()` 方法来帮助客户端快速开始游戏。 - -```js -app.post("/reservation", async (req, res, next) => { - try { - const { - playerId, - } = req.body as { - playerId: any - }; - if (typeof playerId !== "string") { - throw new Error("Missing playerId"); - } - debug(`Making reservation for player[${playerId}]`); - // 调用我们在 Reception 类中准备好的 makeReservation() 方法 - const roomName = await reception.makeReservation(playerId); - debug(`Seat reserved, room: ${roomName}`); - return res.json({ - roomName, - }); - } catch (error) { - next(error); - } -}); -``` - -客户端可以调用这个 API 来快速开始,用该接口的示例代码如下 **(非 Client Engine 代码)**: - -```js -// 这里在客户端通过 HTTP 调用在 Client Engine 中实现的 `/reservation` 接口 -const { roomName } = await (await fetch( - `${CLIENT_ENGINE_SERVER}/reservation`, - { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - playerId: play.userId, - }) - } -)).json(); -// 加入房间 -return play.joinRoom(roomName); -``` - -当客户端调用 `/reservation` 并加入房间成功后,意味着客户端 Client 及 MasterClient 进入了同一个房间内,当房间人数足够时,就可以开始游戏了。 - -客户端项目中已经帮您写好了调用 `/reservation` 的代码,无需您自己再写代码,您可以在 `/src/components/Lobby.vue` 中查看相关代码。 - -#### 入口 API:创建新游戏 - -该入口 API 撰写方式与「快速开始」相同,不再重复说明,可以参考 index.ts 文件中的 `/game` 方法。 - -### 宣布游戏开始 - -在这个小游戏中,人满后我们就可以开始游戏了。我们可以在 Game 的人满事件中宣布游戏开始: - -```js -import { AutomaticGameEvent, Game, watchRoomFull } from "@leancloud/client-engine"; -import { Play, Room } from "@leancloud/play"; - -enum Event { - Start = 10, -}; - -@watchRoomFull() -export default class RPSGame extends Game { - public static defaultSeatCount = 2; - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - // 监听 ROOM_FULL 事件,收到此事件后调用 `start() 方法` - this.once(AutomaticGameEvent.ROOM_FULL, this.start); - } - - protected start = async () => { - // 标记房间不再可加入 - this.masterClient.setRoomOpened(false); - // 向客户端广播游戏开始事件 - this.broadcast(Event.Start); - } -} -``` - -在这段代码中,`watchRoomFull` 装饰器在人满时会使 `Game` 抛出 `AutomaticGameEvent.ROOM_FULL` 事件,在这个事件中我们选择调用自定义的 `start` 方法。在 `start` 方法中我们将房间打开,然后向所有客户端广播游戏开始。 - -到了这一步,您可以启动当前 Client Engine 项目,启动客户端并开启两个客户端 Web 页面,在界面上点击「快速开始」,可以观察到第一个点击「快速开始」的界面显示出了日志:`xxxx 加入了房间`。 - -### 猜拳逻辑 - -接下来我们开始开发具体的游戏中逻辑。具体步骤分为以下几步: - -1. 玩家 A 选择手势,发送出拳事件给 MasterClient。 -2. MasterClient 收到事件,转发事件给玩家 B。 -3. 玩家 B 收到 MasterClient 转发来的事件,界面展示:对方已选择。 -4. 玩家 B 选择手势,发送出拳事件给 MasterClient。 -5. MasterClient 收到事件,转发事件给玩家 A。 -6. 玩家 A 收到 MasterClient 转发来的事件,界面展示:对方已选择。 -7. MasterClient 发现双方都已经出拳,判断结果,公布答案并宣布游戏结束。 - -这三者之间的交互可以用这张图来表示: - -![image](/img/client-engine/rps-game-flow.png) - -接下来我们对每一步进行拆解并撰写代码: - - -#### 玩家 A 选择手势,发送出拳事件给 MasterClient - -这一部分代码是客户端的,**不需要您写在 Client Engine 中**,您可以在客户端项目中 `./src/components/Game.vue` 找到相关代码。 - -```js -enum Event { - Start = 10, - Play = 11, -}; -choices = ["✊", "✌️", "✋"]; - -// 当用户选择时,我们把对应选项的 index 发送给服务端 -play.sendEvent(Event.Play, {index}, {receiverGroup: ReceiverGroup.MasterClient}); -``` - -#### MasterClient 收到事件,转发事件给玩家 B - -这部分代码写在 Client Engine 中,您可以根据下方的示例代码写在自己的 `RPSGame` 中。我们在 `start` 方法中注册自定义事件,并在收到 `play` 事件后,将玩家 A 的动作内容抹去,转发给玩家 B。 - -```js -protected start = async () => { - ...... - // 接收自定义事件 - this.masterClient.on(PlayEvent.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { - if (eventId === Event.Play) { - // 收到其他玩家的事件,转发事件 - this.forwardToTheRests({ eventId, eventData, senderId }, (eventData) => { - return {} - }) - } - }); -} -``` - -在这段代码中,Game 中的 MasterClient 对象注册了多人对战的自定义事件,当玩家 A 发送 `play` 事件给 MasterClient 时,这个事件会被触发。我们在这个事件中使用了 `Game` 的转发事件方法 `forwardToTheRests()`,这个方法第一个参数是原始的事件,第二个参数是原始事件的 eventData 数据处理,我们将原始的 eventData 数据,也就是玩家 A 发来的 `{index}`,修改为空数据 `{}`,这样当玩家 B 收到事件后无法获知玩家 A 的详细动作。 - -#### 玩家 B 收到 MasterClient 转发来的事件,界面展示:对方已选择 - -这部分代码是客户端的,**不需要您写在 Client Engine 中**,您可以在客户端项目中 `./src/components/Game.vue` 找到相关代码。 - -```js -play.on(PlayEvent.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { - ...... - switch (eventId) { - ...... - case Event.Play: - this.log(`对手已选择`); - break; - ..... - } -}); -``` - -#### 玩家 B 选择手势,发送出拳事件给 MasterClient - -这部分逻辑和上文「玩家 A 选择手势,发送出拳事件给 MasterClient」相同,使用的也是相同部分的代码,您可以在客户端项目中 `./src/components/Game.vue` 找到相关代码。 - -#### MasterClient 收到事件,转发事件给玩家 A - -这部分逻辑和上文「MasterClient 收到事件,转发事件给玩家 B」相同,使用的也是相同部分的代码,不需要再额外在 Client Engine 中写代码。 - -#### 玩家 A 收到 MasterClient 转发来的事件,界面展示:对方已选择 - -这部分逻辑和上文「玩家 A 收到 MasterClient 转发来的事件,界面展示:对方已选择」相同,使用的也是相同部分的代码,您可以在客户端项目中 `./src/components/Game.vue` 找到相关代码。 - -到这一步时,您可以运行项目,打开两个界面猜拳,观察双方的动作同步到各自的界面中,但各自分别不知道对方选了什么。 - -#### MasterClient 发现双方都已经出拳,判断结果,公布答案并宣布游戏结束 - -每次 MasterClient 收到玩家选择事件时,我们要把玩家的选择存起来,并判断两位玩家是不是都已经做出选择了: - -```js -protected start = async () => { - ...... - // [this.player[0] 的选择, this.player[1] 的选择]。当两个玩家都没有选择时,假定双方的选择都为 -1 - const choices = [-1, -1]; - - this.masterClient.on(PlayEvent.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { - if (eventId === Event.Play) { - // 收到其他玩家的事件,转发事件 - ...... - // 存储当前玩家的选择 - if (this.players[0].actorId === senderId) { - // 如果是 player[0] 就存储到 choices[0]中 - choices[0] = eventData.index; - } else { - // 如果是 player[1] 就存储到 choices[1]中 - choices[1] = eventData.index; - } - } - }); -} -``` - -在上面这段代码中,我们构造了一个 Array 类型的 choice 来存储玩家的选择,当收到出拳事件时会将用户的选择存储起来,接下来我们判断两个玩家是不是都做出选择了,如果做出选择了则广播游戏结果,并广播游戏结束: - -```js -enum Event { - Start = 10, - Play = 11, - Over = 20, -}; -...... -protected start = async () => { - ...... - // [this.player[0] 的选择, this.player[1] 的选择]。当两个玩家都没有选择时,假定双方的选择都为 -1 - const choices = [-1, -1]; - - this.masterClient.on(PlayEvent.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { - if (eventId === Event.Play) { - // 收到其他玩家的事件,转发事件 - ...... - // 存储当前玩家的选择 - ...... - // 检查两个玩家是不是都已经做出选择了 - if (choices.every((choice) => choice > 0)) { - // 两个玩家都已经做出选择,游戏结束,向客户端广播游戏结果 - const winner = this.getWinner(choices); - this.broadcast(Event.Over, { - choices, - winnerId: winner ? winner.userId : null, - }); - } - } - }); -} - -``` - -在上面的代码中,使用了 `getWinner()` 方法来获取游戏结果,这个是我们自定义的判断胜负的方法,您可以直接复制粘贴下方的代码到自己的 `RPSGame` 文件中: - -```js -// 客户端的出拳数组为:[✊, ✌️, ✋], -// 出 ✊ (index 为 0 )时,赢 ✌️(index 为 1),因此 wins[0] = 1,以此类推 -const wins = [1, 2, 0]; - -@watchRoomFull() -export default class RPSGame extends Game { - ...... - - /** - * 根据玩家的选择计算赢家 - * @return 返回胜利的 Player,或者 null 表示平局 - */ - private getWinner([player1Choice, player2Choice]: number[]) { - if (player1Choice === player2Choice) { return null; } - if (wins[player1Choice] === player2Choice) { return this.players[0]; } - return this.players[1]; - } -} -``` - -客户端在收到 MasterClient 广播结束事件后在界面上做相应的结果展示。到这里基础逻辑都已经开发完成,您可以运行项目,打开两个页面,愉快的开始自己和自己的对战了。 - -### 离开房间 - -当所有客户端离开房间后,`GameManager` 会帮助我们把空房间销毁,因此在我们这个小游戏的代码中不需要再额外写这部分的代码。 - -### RxJS - -当您查看示例 Demo 时,会发现代码和本文档中的代码相比更精简一些,原因是示例 Demo 中使用了 RxJS。如果您感兴趣,可以自行研究 [RxJS](https://rxjs-dev.firebaseapp.com/) 及相关接口的 [API 文档](https://rxjs-dev.firebaseapp.com/api)。 - -## 开发指南 - -当您按照本文档的说明一步一步开发出猜拳小游戏后,对 Client Engine SDK 和初始项目一定有了初步的感受,接下来您可以参考 [Client Engine 开发指南](/sdk/multiplayer/client-engine/guide-node/)更深入的了解整体结构及用法。 diff --git a/leancloud/docs/sdk/multiplayer/client-engine/guide-node.mdx b/leancloud/docs/sdk/multiplayer/client-engine/guide-node.mdx deleted file mode 100644 index cd46b0db2..000000000 --- a/leancloud/docs/sdk/multiplayer/client-engine/guide-node.mdx +++ /dev/null @@ -1,386 +0,0 @@ ---- -title: Client Engine 开发指南 · Node.js -sidebar_label: 开发指南 -sidebar_position: 4 ---- - -请先阅读 [Client Engine 快速入门 · Node.js](/sdk/multiplayer/client-engine/quick-start-node/) 及[你的第一个 Client Engine 小游戏](/sdk/multiplayer/client-engine/first-game-node/)来初步了解如何使用初始项目来开发游戏。本文档将在初始项目的基础上深入讲解 Client Engine SDK。 - -Client Engine 初始项目依赖了专门的 Client Engine SDK, Client Engine SDK 在多人在线对战 SDK 的基础上进行了封装,帮助您更好的撰写服务端游戏逻辑。您可以通过[快速入门](/sdk/multiplayer/client-engine/quick-start-node/)安装依赖。 - -## 组件 - -SDK 提供以下组件: - -* **`Game` :**负责房间内游戏的具体逻辑。Client Engine 维护了许多游戏房间,每一个游戏房间都是一个 Game 实例,即每个 Game 实例对应一个唯一的 Play Room 与 MasterClient。游戏房间内的逻辑由 Game 中的代码来控制,因此**房间内的游戏逻辑必须继承自该类**。 -* **`GameManager` :**负责创建、管理及分配具体的 Game 对象。Game 的管理及销毁由 SDK 负责,不需要您再自己额外写代码。 - -### GameManager - -#### GameManager 实例化 - -`GameManager` 会帮您自动创建、管理并销毁 Game,因此在项目启动时,您需要实例化 `GameManager`。相关示例代码如下: - -##### 自定义 GameManager - -首先需要自定义一个 Class 继承自 `GameManager`,例如示例代码中的 `SampleGameManager`: - -```js -import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine"; -export default class SampleGameManager extends GameManager { - -} -``` - -##### 在 GameManager 中自定义方法 - -Client Engine 的核心用法之一是负责创建 Game 并返回 roomName 给客户端,因此在 `SampleGameManager` 这个类中,我们需要撰写创建 `Game` 的方法提供给 Web API 使用。例如示例项目中的[快速开始](/sdk/multiplayer/client-engine/quick-start-node/#实现逻辑快速开始)和[创建新游戏](/sdk/multiplayer/client-engine/first-game-node/#实现逻辑创建新游戏)。这里的示例代码我们以创建新游戏为例: - -```js -import { Game, GameManager, ICreateGameOptions } from "@leancloud/client-engine"; -export default class SampleGameManager extends GameManager { - /** - * 创建一个新的游戏。 - * @param playerId 预约的玩家 ID - * @param options 创建新游戏时可以指定的一些配置项 - * @return 创建的游戏的房间 name - */ - public async createGameAndGetName(playerId: string, options?: ICreateGameOptions) { - const game = await this.createGame(playerId, options); - return game.room.name; - } -} -``` - -写完自定义方法之后,在下文我们还需要为这里的方法配置[负载均衡](#负载均衡),需要注意的是,受[负载均衡系统](#负载均衡)的要求,`GameManager` 子类中的 `public` 方法,其参数与返回值必须是 `string`、`number`、`boolean`、`null`、`Object`、`Array` 中的一种。以上代码中可以看到 `GameManager` 的 `createGame()` 方法返回的是一个 `Game`,不符合负载均衡的要求,因此我们在这里封装为自己的方法 `createGameAndGetName()`。 - -##### 创建 GameManager 子类对象 - -接下来创建 `GameManager` 的子类对象,在创建 `SampleGameManager` 的时候,需要在第一个参数内传入[自定义的 Game ](#实现自己的 Game),这里使用的是[示例 Demo](/sdk/multiplayer/client-engine/first-game-node/) 猜拳游戏中的 `RPSGame`。 - -```js -import PRSGame from "./rps-game"; -const gameManager = new SampleGameManager( - gameConstructor: PRSGame, - appId: {{appid}}, - appKey: {{appkey}}, - playServer: "https://please-replace-with-your-customized.domain.com", - concurrency: 2, -); -``` - -##### 设置负载均衡 - -`GameManager` 需要配置负载均衡,以确保 `GameManager` 创建的 `Game` 能够尽可能均匀分配到每一个 Client Engine 实例中。[负载均衡](#负载均衡)的详细文档请看下文,在这里,我们先讲解如何配置。 - -在这里我们创建一个[负载均衡](#负载均衡)对象,然后将上面的 `gameManager` 绑定到负载均衡中: - -```js -import { ICreateGameOptions,LoadBalancerFactory } from "@leancloud/client-engine"; - -// 创建负责负载均衡的对象,请不要更改,使用时复制粘贴即可 -const loadBalancerFactory = new LoadBalancerFactory({ - poolId: `${APP_ID.slice(0, 5)}-${process.env.LEANCLOUD_APP_ENV || "development"}`, - redisUrl: process.env.REDIS_URL__CLIENT_ENGINE, -}); - -// 将 reception 及我们自定义的方法 makeReservation 配置负载均衡。 -const loadBalancer = loadBalancerFactory.bind(gameManager, ["createGameAndGetName"]); -``` - -`loadBalancerFactory` 的 `bind()` 方法中,第一个参数是 `gameManager` 对象,第二个参数是一个数组,传入需要进行负载均衡的方法名 `["createGameAndGetName"]`。 - -到这里,`gameManager` 的配置就完成了,您可以在自己定义的 Web API 处这样调用相关方法: `gameManager.createGameAndGetName()`。 - -#### 创建房间 - -在 [GameManager 实例化](#gamemanager-实例化)这一节中,我们在子类中使用了 `GameManager` 的 `createGame()` 来创建房间。 - -`createGame()` 接受以下参数: - -* playerId:发起请求的客户端在多人对战服务中的 [userId](/sdk/multiplayer/guide/js/#初始化)。 -* createGameOptions(可选):创建指定条件的房间。 - * roomName(可选):创建指定 roomName 的房间。例如您需要和好友一起玩时,可以用这个接口创建房间后,把 roomName 分享给好友。如果您不关心 roomName,可以不指定这个参数。 - * roomOptions(可选):通过这个参数,客户端在请求 Client Engine 创建房间时,可以设置 `customRoomProperties`,`customRoomPropertyKeysForLobby`,`visible`,对这三个参数的说明请参考[创建房间](/sdk/multiplayer/guide/js/#创建房间)。 - * seatCount(可选):创建房间时,指定本次游戏需要多少人,这个值需要在[设置房间内玩家数量](#设置房间内玩家数量)的 `minSeatCount` 和 `maxSeatCount` 之间,否则 Client Engine 会拒绝创建房间。如果不指定,则以 `defaultSeatCount` 为准。 - -例如创建一个带有匹配条件的新房间时,可以这样调用 `createGame()`: - -```js -// 您可以从客户端发来的请求中获得 playerId 和 createGameOptions -const props = { - level: 2, -}; - -const roomOptions = { - customRoomPropertyKeysForLobby: ['level'], - customRoomProperties: props, -}; - -const createGameOptions = { - roomOptions -}; - -gameManager.createGame(playerId, createGameOptions); -``` - -在[你的第一个 Client Engine 小游戏](/sdk/multiplayer/client-engine/first-game-node/)中,`reception.ts` 中使用 `createGame()` 撰写了两个自定义方法用于「快速开始」和「创建新游戏」,并通过 `index.ts` 中的 Web API `/reservation` 和 `/game` 来调用相关逻辑,如果没有自定义需求您可以直接使用示例 Demo 中的接口并传入以上参数。 - -#### 获取当前可用的房间 - -GameManager 提供了 `getAvailableGames()` 方法来获取当前 GameManager 对象所在的 Client Engine 实例中的可用游戏列表。这里的可用指的是房间还有空位。使用示例代码如下: - -```js -var games = gameManager.getAvailableGames(); -``` - -需要注意的是,这个方法获取的不是多人对战服务中所有的可用房间,**仅限于当前所在 Client Engine 实例中的可用房间**,Client Engine 多实例负载均衡请参考[负载均衡](#负载均衡)。 - -#### 匹配 - -`GameManager` 暂时没有提供匹配机制,如果客户端只需要随机加入某个房间,可以参考[示例项目](/sdk/multiplayer/client-engine/first-game-node/)中「快速开始」的实现方案。这个实现方案会在负载最低的实例中寻找可用房间或创建房间,最终返回给客户端一个可加入的房间名称。 - -如果您希望实现有条件的匹配,可以这样实现: - -1. 客户端向多人对战服务请求[有条件的匹配](/sdk/multiplayer/guide/js/#随机加入房间),如果有空余的房间,则会触发加入成功事件。 -2. 如果的多人对战服务此时没有空余的房间,客户端会收到「加入房间失败」事件,在这个事件中,发现错误码是 [4301](/sdk/multiplayer/error-code/#4301),则向 Client Engine 请求创建房间。 -3. Client Engine 收到请求后创建房间并返回 roomName 给客户端。这一部分的逻辑可以使用[示例项目](/sdk/multiplayer/client-engine/first-game-node/)中的 `/game` 入口。 -4. 客户端拿到 Client Engine 返回的 roomName 后加入房间,等待其他人匹配加入。 - -这个流程在客户端中的示例代码如下(**非 Client Engine**): - -客户端首先向多人对战服务发起有条件的加入房间请求: - -```js -const matchProps = {level: 2}; -play.joinRandomRoom({matchProperties: matchProps}); -``` - -如果多人对战服务此时有可以加入的新房间,您会自动加入到新房间中,并触发加入房间成功事件: - -```js -play.on(Event.ROOM_JOINED, () => { - // TODO 可以做跳转场景之类的操作 -}); -``` - -如果没有可以加入的房间,会触发加入房间失败事件。在这个事件中,[4301](/sdk/multiplayer/error-code/#4301) 错误码代表着没有可以加入的空房间,此时我们向 Client Engine 请求创建一个新的房间,获得新房间的 roomName 后加入新房间: - -```js -// 加入房间失败后请求 Client Engine 创建房间 -play.on(Event.ROOM_JOIN_FAILED, (error) => { - if (error.code === 4301) { - // 设置创建带有匹配属性的房间 - const props = {level: 2}; - const options = {customRoomPropertyKeysForLobby: ['level']}; - // 这里通过 HTTP 调用在 Client Engine 中实现的 `/game` 接口 - const { roomName } = await (await fetch( - `${CLIENT_ENGINE_SERVER}/game`, - { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - playerId: play.userId, - options - }) - } - )).json(); - // 加入房间 - return play.joinRoom(roomName); - } else { - console.log(error); - } -}); -``` - -### Game - -#### Game 生命周期 - -1. **创建:** `Game` 由 SDK 中的 `GameManager` 管理,`GameManager` 会在收到创建房间的请求时根据情况创建 `Game`。 -2. **运行:** 创建后,`Game` 的控制权从 SDK 中 `GameManager` 移交给 `Game` 本身。从这个时刻开始,会有玩家陆续加入游戏房间。 -3. **销毁:** 所有玩家离开房间后,意味着游戏结束,`Game` 将控制权交回 `GameManager`,`GameManager` 做最后的清理工作,包括断开并销毁该房间的 masterClient、将 `Game` 从管理的游戏列表中删除等。 - -#### Game 通用属性 - -在实现游戏逻辑的过程中,`Game` 类提供下面这些属性来简化常见需求的实现,您可以在继承 `Game` 的自己的类中方便的获得以下属性: - -* `room` 属性:游戏对应的房间,这是一个 Play SDK 中的 Room 实例。 -* `masterClient` 属性:游戏对应的 masterClient,这是一个 Play SDK 中的 Play 实例。 -* `players` 属性:不包含 masterClient 的玩家列表。注意,如果您通过 Play SDK Room 实例的 `playerList` 属性获取的房间成员列表是包括 masterClient 的。 - -#### Game 通用方法 - -Game 类在多人对战 SDK 的基础上封装了以下方法,使得 MasterClient 可以更便利的发送自定义事件: - -* `broadcast()` 方法:向所有玩家广播自定义事件。示例代码请参考[广播自定义事件](#广播自定义事件)。 -* `forwardToTheRests()` 方法:将一个玩家发送的自定义事件转发给其他玩家。示例代码请参考[转发自定义事件](#转发自定义事件)。 - -#### 实现自己的 Game - -实现自己的房间内游戏逻辑时,您需要创建一个继承自 `Game` 的类来撰写自己的游戏逻辑,示例方法如下: - -```js -import { Game } from "@leancloud/client-engine"; -export default class SampleGame extends Game { - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - } -} -``` - -#### 设置房间内玩家数量 - -这里的玩家数量指的是不包括 MasterClient 的玩家数量,根据多人对战服务的限制,最多不能超过 9 个人。 - -在 `Game` 中需要指定 `defaultSeatCount` 静态属性作为默认的玩家数量,Client Engine 会根据这个值向多人对战服务请求创建房间。例如斗地主需要 3 个人才能玩,可以这样设置: - -```js -export default class SampleGame extends Game { - public static defaultSeatCount = 3; // 最大不能超过 9 -} -``` - -如果您的游戏需要的玩家数量在某个范围内,除了设置 `defaultSeatCount` 外,还需要使用 `minSeatCount` 静态属性限定最小玩家数量,`maxSeatCount` 静态属性设定最大玩家数量。例如三国杀要求至少 2 个人,最多 8 个人才能玩,默认 5 个人可以玩,可以这样设置: - -```js -export default class SampleGame extends Game { - public static minSeatCount = 2; - public static maxSeatCount = 8; // 最大不能超过 9 - public static defaultSeatCount = 5; -} -``` - -在[创建房间](#创建房间)的接口中,可以将客户端 request 请求中的 `seatCount` 参数来动态覆盖掉 `defaultSeatCount`。 - -当房间人数达到 `seatCount` 时,您可以选择配置触发[房间人满事件](#房间人满事件),如果您的客户端没有指定 `seatCount`,人满事件时将以 `defaultSeatCount` 的值为准。 - -#### 加入房间事件 - -当客户端成功加入房间后,位于 Client Engine 的 MasterClient 会收到[新玩家加入事件](/sdk/multiplayer/guide/js/#新玩家加入事件),如果您需要监听此事件,可以在自定义的 `Game` 中的 `constructor()` 方法中撰写监听的代码: - -```js -import { Game } from "@leancloud/client-engine"; -export default class SampleGame extends Game { - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - this.masterClient.on(Event.PLAYER_ROOM_JOINED, () => { - console.log('有人来了'); - }); - } -} -``` - -#### 房间人满事件 - -当房间的人数满足[设置房间内玩家数量](#设置房间内玩家数量)的人满逻辑时,`watchRoomFull` 装饰器会让您收到 Game 抛出的 `AutomaticGameEvent.ROOM_FULL` 事件,您可以在这个事件中撰写相应的游戏逻辑,例如关闭房间,向客户端广播游戏开始: - -```js -import { AutomaticGameEvent, Game, watchRoomFull } from "@leancloud/client-engine"; - -enum Event { - GameStart = 15, -}; - -@watchRoomFull() -export default class SampleGame extends Game { - constructor(room: Room, masterClient: Play) { - super(room, masterClient); - // 监听 ROOM_FULL 事件,收到此事件后调用 `start() 方法` - this.once(AutomaticGameEvent.ROOM_FULL, this.start); - } - - protected start = async () => { - // 在这里撰写自己房间人满后的逻辑 - // 标记房间不再可加入 - this.masterClient.setRoomOpened(false); - // 向客户端广播游戏开始事件 - this.broadcast(Event.GameStart); - } -} -``` - -#### 广播自定义事件 - -在[房间人满事件](#房间人满事件)中,`Game` 向房间内所有成员广播了游戏开始: - -```js -enum Event { - GameStart = 15, -}; -this.broadcast(Event.GameStart); -``` - -在广播事件时您还可以带有一些数据: - -```js -enum Event { - GameStart = 15, -}; -const gameData = {someGameData}; -this.broadcast(Event.GameStart, gameData); -``` - -此时客户端的[接收自定义事件](/sdk/multiplayer/guide/js/#接收自定义事件)方法会被触发,如果发现是 `game-start` 事件,客户端可以在 UI 上展示对战开始。 - -#### 转发自定义事件 - -MasterClient 可以转发某个客户端发来的事件给其他客户端,在转发时还可以处理数据: - -```js -enum Event { - SomeEvent = 15, -}; -this.forwardToTheRests(event, (eventData) => { - // 准备要转发的数据 - const actUserId = event.senderId; - const result = {actUserId}; - return result; - // Event.SomeEvent 是自定义事件的 ID,如果省略则使用原 event 事件的 ID -}, Event.SomeEvent) -``` - -在这个代码中,`event` 参数是某个客户端发来的原始事件,`eventData` 是原始事件的数据,您可以在转发事件给其他客户端时处理该数据,例如抹去或增加一些信息。MasterClient 发送该事件后,客户端的[接收自定义事件](/sdk/multiplayer/guide/js/#接收自定义事件)会被触发。 - -#### MasterClient 与客户端通信 - -除了上方初始项目提供的[广播自定义事件](#广播自定义事件)及[转发自定义事件](#转发自定义事件)外,您依然可以使用多人对战服务中的[自定义属性](/sdk/multiplayer/guide/js/#自定义属性及同步)、[自定义事件](/sdk/multiplayer/guide/js/#自定义事件)进行通信。 - -除此之外,`Game` 还提供了以下 [RxJS](http://reactivex.io/rxjs) 方法方便您对事件进行流处理,进而精简自己的代码及逻辑: - -* `getStream()` 方法:获取玩家发送的自定义事件的流,这是一个 RxJS 中的 Observable 对象。接口说明请参考 [API 文档](https://leancloud.github.io/client-engine-nodejs-sdk/classes/game.html#getstream)。 -* `takeFirst()` 方法:获取玩家发送的指定条件的从现在开始算的第一条自定义事件的流,返回一个 RxJS 中的 Observable 对象。接口说明请参考 [API 文档](https://leancloud.github.io/client-engine-nodejs-sdk/classes/game.html#takefirst)。 - -注意,以上两个方法需要您了解 [RxJS](http://reactivex.io/rxjs) 才能使用,如果您不了解 [RxJS](http://reactivex.io/rxjs),依然可以使用多人对战服务中的[事件方法](/sdk/multiplayer/guide/js/#自定义事件)进行通信。 - -#### 游戏结束 - -当所有玩家都离开后,`GameManager` 会自动帮您销毁当前房间及相关的 MasterClient。此时如果您没有其他的逻辑要做,则不需要关心本节文档。如果您希望自己做一些清理工作,例如保存用户数据等,可以使用 `autoDestroy` 装饰器,这个装饰器会在所有玩家离开后自动触发 `Game` 子类中的 `destroy()` 方法,您可以将相关逻辑写在这个方法中。 - -```js -import { autoDestroy, Game } from "@leancloud/client-engine"; - -@autoDestroy() -export default class SampleGame extends Game { - protected destroy() { - super.destroy(); - console.log('在这里可以做额外的清理工作'); - } -} -``` - -## 负载均衡 - -Client Engine 会根据整体实例负载的高低自动对实例数量进行调整。 - -在 Client Engine 中,需要负载均衡的情况有两种:第一种是客户端通过 REST API 发起的请求,第二种是每一个实例运行的 `Game` 数量的负载。对于客户端通过 REST API 发起的请求,Client Engine 会自动将请求均匀的分配给当前的所有实例,不需要我们再做任何配置工作。对于第二种情况,每个 `Game` 对象(每局游戏)一般都会持续存在一段时间,为了让每个实例承载的 `Game` 对象尽可能达到均衡,我们需要额外配置 `GameManager` 到负载均衡系统中。 - -这个特性由 SDK 提供的 `LoadBalancerFactory` 类实现。在 [GameManager 实例化](#GameManager 实例化)中我们可以看到,`LoadBalancerFactory` 通过绑定 `gameManager` 生成一个 `LoadBalancer` 的对象,每一个 Client Engine 实例中都会有这样一个对象。 - -当 Client Engine 的某个实例接收到来自客户端的 REST API 请求,并调用 `gameManager` 中的方法时,接收请求的实例中的负载均衡节点 `LoadBalancer` 会找出集群中承载 `Game` 数量最小的实例,将指定的 `gameManager` 的 API 调用转发给该实例的 `gameManager` 运行并将结果返回。在这里,`LoadBalancer` 只负责请求的转发,不关心如何处理请求。 - -## API 文档 - -您可以在 API 文档中找到更多 SDK 的类、方法及属性说明,[点击查看 Client Engine SDK API 文档](https://leancloud.github.io/client-engine-nodejs-sdk/)。 diff --git a/leancloud/docs/sdk/multiplayer/client-engine/quick-start-node.mdx b/leancloud/docs/sdk/multiplayer/client-engine/quick-start-node.mdx deleted file mode 100644 index b7e875e2e..000000000 --- a/leancloud/docs/sdk/multiplayer/client-engine/quick-start-node.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: Client Engine 快速入门 · Node.js -sidebar_label: 快速入门 -sidebar_position: 2 ---- - -该文档帮助你快速了解如何创建一个 Client Engine 项目,该项目是一个简单的双人剪刀石头布游戏,游戏逻辑的开发依赖于多人在线对战 JavaScript SDK。 - -我们在这篇文档中先了解如何本地启动项目,简单试玩一下,然后部署项目到云端。之后在[开发指南](/sdk/multiplayer/client-engine/guide-node/)中会介绍详细的游戏逻辑以及如何开发自己的游戏。 - -## 启动项目 - -### 安装命令行工具 - -请查看命令行工具**[安装部分](/sdk/engine/cli/#安装)**的文档,安装命令行工具,并执行**[登录](/sdk/engine/cli/#登录账号)**命令登录。 - -### 创建项目 - -从 Github 获取示例项目,请将该项目作为你的项目基础: - -```js -git clone https://github.com/leancloud/client-engine-nodejs-getting-started -cd client-engine-nodejs-getting-started -``` - -添加应用 App ID 等信息到该项目: - -```sh -lean switch -``` - -在第一步选择 App 中,选择您的游戏对应的 LeanCloud 应用。在第二步选择云引擎分组时,必须选择 `_client-engine` 分组,LeanCloud 仅对该分组提供专门针对 Client Engine 的优化维护及各种支持,如图所示: - -![image](/img/client-engine/lean-switch.png) - -### 本地运行 - -首先在当前项目的目录下安装必要的依赖,执行如下命令行: - -```sh -npm install -``` - -如果同时会使用到数据存储服务,执行如下命令: - -```sh -npm install leancloud-storage -``` - -启动应用时打开调试日志: - -```sh -DEBUG=ClientEngine*,RPS*,Play lean up -``` - -如果您不需要调试日志,可以直接使用以下命令启动: - -```sh -lean up -``` - -启动后在浏览器中打开 `http://localhost:3000/` ,检测项目是否正常启动。 - -### 访问站点 - -#### 感受游戏 - -服务端项目启动完成后,如果希望体验 Demo 游戏,需要额外同时打开两个[客户端示例 Demo](https://client-engine-app.leanapp.cn/)页面,在这两个页面中做如下配置: - -点击 Configs,APP_ID 、APP_KEY 和 PLAY_SERVER 填入之前选择的应用的 App ID 、App Key 及服务器地址: - -```sh -# 如果您的浏览器已经登录 LeanCloud,请在下方选择相关应用,复制粘贴相关信息到 Configs 中: - APP_ID: "your-app-id" - APP_KEY: "your-app-key" -``` - -接着在 Client Engine Server 中输入 `http://localhost:3000`。如图所示: - -![image](/img/client-engine/browser-demo.png) - -信息填写完成后,点击「Login to Play」就可以开始游戏了。 - -#### 客户端代码 - -如果您希望查看详细的客户端代码,可以访问位于 github 的[客户端示例代码](https://github.com/leancloud/client-engine-demo-webapp)。 - -## 部署到云端 - -部署至预备环境,在根目录中运行: - -```sh -lean deploy --staging -``` - -在浏览器中登录 LeanCloud 控制台,绑定[云引擎域名](/sdk/domain/guide/#云引擎域名)(`stg-` 开头的自定义域名会被自动地绑定到预备环境),然后访问相应网址可以看到 Client Engine 服务端正在运行的文本。 - -其他详细的部署方式请参考命令行工具文档中的[部署](/sdk/engine/cli/#部署)。 - -## 你的第一个 Client Engine 小游戏 - -接下来请查看文档[你的第一个 Client Engine 小游戏](/sdk/multiplayer/client-engine/first-game-node/),了解如何根据该初始项目一步一步开发出来一个剪刀石头布小游戏。 diff --git a/leancloud/docs/sdk/multiplayer/error-code.mdx b/leancloud/docs/sdk/multiplayer/error-code.mdx deleted file mode 100644 index 03a4d759c..000000000 --- a/leancloud/docs/sdk/multiplayer/error-code.mdx +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: 多人在线对战错误码详解 -sidebar_label: 错误码详解 -sidebar_position: 5 ---- - -## 应用级别错误码 - -### 4001 - -- 信息 - APP_NOT_FOUND -- 含义 - 找不到 App,请确认 App ID 及 App Key 输入正确,并且设置了正确的节点。 - -### 4002 - -- 信息 - INSUFFICIENT_BALANCE -- 含义 - 应用所属账户欠费,请充值后再使用。 - - - -### 4004 - -- 信息 - CCU_QUOTA_EXCEEDED -- 含义 - CCU 超过当前额度限制,请升级商用版或企业版。 - - - -### 4006 - -- 信息 - JOIN_OR_CREATE_ROOM_NOT_ALLOWED_DUE_TO_APP_MSG_QUOTA_EXCEEDED -- 含义 - 应用房间中最大消息发送速率超过每秒 500 条,禁止创建新房间,禁止加入其他房间。 - -## 服务故障错误码 - -### 4200 - -- 信息 - INTERNAL_ERROR -- 含义 - 服务器内部错误,请联系技术支持。 - -### 4202 - -- 信息 - SERVICE_TEMPORARILY_UNAVAILABLE -- 含义 - 游戏服务临时不可用,请联系技术支持。 - -## Room 错误码 - -### 4301 - -- 信息 - ROOM_NOT_FOUND -- 含义 - 找不到 Room,以下情况会导致该错误: - - 随机匹配时没有符合条件的房间,此时您可以创建一个新的房间。 - - 指定 Room 名称加入房间时,该 Room 已经销毁或名称不正确。 - -### 4302 - -- 信息 - ROOM_FULL -- 含义 - Room 已满,请加入其他的房间。 - - - -### 4308 - -- 信息 - ROOM_MEMBERSHIP_REQUIRED -- 含义 - 需要当前玩家在该房间内才能进行操作。 - -### 4309 - -- 信息 - ROOM_GAME_VERSION_OR_SDK_VERSION_NOT_MATCH -- 含义 - 创建 Room 用户的 Game Version 或 SDK Version 和当前加入 Room 用户的 Game Version 或 SDK Version 不匹配。当使用 Room 名称强制加入一个和自己 Game Version 或 SDK Version 不一致的房间时会出现这个错误。 - - - -### 4311 - -- 信息 - ROOM_ALREADY_EXISTS -- 含义 - Room 已经存在,请使用其他 Room 名称创建房间。 - -### 4312 - -- 信息 - ROOM_ATTRS_FULL -- 含义 - Room 的属性已经达到最大长度限制。 - -### 4313 - -- 信息 - INVALID_ROOM_ATTR -- 含义 - Room 的属性不符合要求。 - -### 4314 - -- 信息 - ROOM_CLOSED -- 含义 - Room 已经关闭。主动加入一个已经 close 的房间会出现这个错误。 - -### 4315 - -- 信息 - TARGET_MASTER_CLIENT_OFFLINE -- 含义 - 转移 Master 时目标 Master 不在线。 - -### 4316 - -- 信息 - INVALID_ROOM_ID -- 含义 - Room id 格式不合要求。 - -### 4317 - -- 信息 - SHOULD_LEAVE -- 含义 - 用户在线时未退出当前房间强制加入其他房间时报错,请先调用 leave 方法再加入其他房间。 - - - - - - - - - - - -### 4324 - -- 信息 - SHOULD_JOIN -- 含义 - Client 当前未加入任何 Room,却试图做一些 Room 内的操作,请先加入一个 Room 再进行操作。 - - - -### 4326 - -- 信息 - ROOM_ATTR_NOT_MATCHED -- 含义 - 按 Room 属性匹配方式加入 Room 时,没有符合相关属性的房间,此时您可以创建新的符合相关属性的房间。 - - - -### 4328 - -- 信息 - OPERATION_NOT_ALLOWED -- 含义 - 没有权限做本次操作。 - -### 4329 - -- 信息 - PLAYER_PROPERTIES_FULL -- 含义 - 用户属性已满,请减少属性的大小。 - - - -## RPC 消息相关错误 - - - -### 4406 - -- 信息 - NO_VALID_MESSAGE_RECEIVER -- 含义 - 消息没有合法的接收人。 - - - -## 其他错误 - -### 4101 - -- 信息 - DUPLICATE_LOGIN -- 含义 - 在一个已经有用户登录的链接上,再次收到另一个用户的登录请求。 - -### 4102 - -- 信息 - DUPLICATE_CONNECTIONS -- 含义 - 同一用户在不同链接上登录。 - -### 4013 - -- 信息 - SIGNATURE_VERIFICATION_FAILED -- 含义 - 签名错误。 - -### 4104 - -- 信息 - INVALID_APP_ID_OR_CLIENT_ID -- 含义 - App id 或者 Client id 格式不合法。 - -### 4105 - -- 信息 - SESSION_REQUIRED -- 含义 - 用户未登录就发请求。 - - - - - - - -### 4110 - -- 信息 - FRAME_TOO_LONG -- 含义 - 收到的数据包过大,超过限制。 - - - - - -### 4121 - -- 信息 - INVALID_PARAMS -- 含义 - 参数传错,请查看详细的 detail 信息。 - - - - - - - - diff --git a/leancloud/docs/sdk/multiplayer/features.mdx b/leancloud/docs/sdk/multiplayer/features.mdx deleted file mode 100644 index a306092d0..000000000 --- a/leancloud/docs/sdk/multiplayer/features.mdx +++ /dev/null @@ -1,815 +0,0 @@ ---- -title: 多人在线对战功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; - -多人在线对战是 TDSLeanCloud 专门针对多人在线类型的游戏推出的后端服务。开发者不需要自己搭建后端系统,利用云服务就可以轻松实现游戏内玩家匹配、在线对战消息同步等功能。 - -## 核心功能 - -- **玩家匹配**:随机或按指定条件将玩家匹配到一起玩游戏。在线对战的匹配功能会将即将一起游戏的玩家匹配到同一个房间(Room)中。例如《第五人格》、《王者荣耀》、《吃鸡》等对战类手游,玩家只需点击「自由匹配」就可以迅速匹配到其他玩家,大家进入到同一个房间中准备开始游戏;玩家也可以自己新开房间邀请好友一起玩。 -- **对战消息快速同步**:客户端与服务端使用 WebSocket 通道进行实时双向通信,确保游戏内所有消息能够快速同步。 -- **游戏逻辑运算**:在线对战提供了 [MasterClient](/sdk/multiplayer/guide/js/#masterclient) 作为客户端主机控制游戏逻辑。游戏内的所有逻辑都交给 MasterClient 来判断运转,如果 MasterClient 意外掉线,我们会自动将网络状态最好的客户端切换为 MasterClient,确保游戏顺畅进行;开发者也可以选择在服务端编写游戏逻辑(服务端游戏逻辑支持尚在开发中)。 -- **多平台支持**:完美适配游戏引擎 Unity 及 Cocos Creator,支持多个平台。 - -## 特性 - -- 支持动态扩容,从容应对海量并发。 -- 在久经考验的底层架构上进行了深度优化与改进,可以稳定承接每秒亿级的消息下发量。 - -## 核心概念 - -### Client 和 UserId - -多人在线对战服务中的每一个终端称为一个「Client」,每一个 Client 在整个对战服务中都有一个唯一标识 `UserId`,这个 `UserId` 只允许英文、数字与下划线,长度不能超过 32 个字符,在一个应用内全局唯一。每一个游戏玩家都肯定是一个 Client,但并不是所有的 Client 都是真正的玩家,例如托管在 Client Engine 中管理房间的 MasterClient 或自己写的 AI 玩家。 - -多人在线对战服务仅允许一个 Client 同时和服务器建立一条连接,如果已登录 `UserId` 再次尝试登录,第二次登录会把之前的登录踢掉。 - -### 房间和 ActorId - -当玩家匹配成功后,会进入到同一个房间内进行游戏,对战的消息会在这个房间内快速同步。每一个玩家在该房间内有一个自己的专属 ActorId,房间内的通信全部通过 ActorId 来传递。玩家退出房间后,ActorId 失效,当玩家进入下一个房间后,会获得新的房间内的 ActorId。 - -**一个房间最多支持 10 人同时在线。** - -## 游戏核心流程 - -这里给出简单的示例代码使您更快地了解到整体流程,详细的开发指南请参考: - -- [多人在线对战开发指南 · JavaScript](/sdk/multiplayer/guide/js/) -- [多人在线对战开发指南 · C#](/sdk/multiplayer/guide/cs/) - -### 连接服务器 - - -<> - - - -```js -const client = new Client({ - // 设置 APP ID - appId: {{appid}}, - // 设置 APP Key - appKey: {{appkey}}, - // 设置 Server (请将 xxx.example.com 替换为你的应用绑定的自定义 API 域名) - playServer: 'https://xxx.example.com', - // 设置用户 id - userId: 'tarara', - // 设置游戏版本号,选填,默认 0.0.1,不同版本的玩家不会匹配到同一个房间 - gameVersion: '0.0.1' -}); - -client.connect().then(()=> { - // 连接成功 -}).catch(console.error); -``` - - - - - -```js -const client = new Client({ - appId: 'your-client-id', // 游戏的 Client ID - appKey: 'your-client-token', // 游戏的 Client Token - playServer: 'https://your_server_url', // 游戏的 API 域名 - userId: 'tarara', // 设置用户 id - gameVersion: '0.0.1' // 设置游戏版本号,选填,默认 0.0.1,不同版本的玩家不会匹配到同一个房间 -}); - -client.connect().then(()=> { - // 连接成功 -}).catch(console.error); -``` - -- 在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 可以查看游戏的 `Client ID` 和 `Client Token`。 -- API 域名在 **应用配置 > 域名配置 > API** 处查看,参考文档关于[域名](/sdk/domain/guide/)的说明。 - - - - -<> - -```cs -Play.UserID = "tarara"; -// 连接服务器时可以声明游戏版本,不同版本的玩家不会匹配到同一个房间 -Play.Connect("0.0.1"); -``` - - - - -### 玩家匹配 - -#### 随机匹配 - -单人玩游戏时,最常见的场景是随机匹配其他玩家迅速开始。具体实现步骤如下: - -1、调用 `JoinRandomRoom` 开始匹配。 - - -<> - -```js -client - .joinRandomRoom() - .then(() => { - // 成功加入房间 - }) - .catch(console.error); -``` - - -<> - -```cs -Play.JoinRandomRoom(); -``` - - - - -2、在顺利情况下,会进入某个有空位的房间开始游戏。 - - -<> - -```js -// JavaScript SDK 通过 joinRandomRoom 的 Promise 判断是否加入房间成功 -``` - - -<> - -```cs -play.On(Event.ROOM_JOINED, (evtData) => { - // 成功加入房间 - -}); -``` - - - - -3、如果没有空房间,就会加入失败。此时在失败触发的回调中建立一个房间等待其他人加入,建立房间时: - -- 不需要关心房间名称。 -- 默认一个房间内最大人数是 10,可以通过设置 MaxPlayerCount 来限制最大人数。 -- 设置 [玩家掉线后的保留时间](/sdk/multiplayer/guide/cs/#在房间内保留断线用户),在有效时间内如果该玩家加回房间,房间内依然保留该玩家的自定义属性。 - - -<> - -```js -client - .joinRandomRoom() - .then() - .catch((error) => { - if (error.code === 4301) { - const options = { - // 设置最大人数,当房间满员时,服务端不会再匹配新的玩家进来。 - maxPlayerCount: 4, - // 设置玩家掉线后的保留时间为 120 秒 - playerTtl: 120, - }; - // 创建房间 - client - .createRoom({ - roomOptions: options, - }) - .then(() => { - // 创建房间成功 - }); - } - }); -``` - - -<> - -```cs -// 加入失败时,这个回调会被触发 -play.On(Event.ROOM_JOIN_FAILED, (evtData) => -{ - var options = new RoomOptions() - { - // 设置最大人数,当房间满员时,服务端不会再匹配新的玩家进来。 - MaxPlayerCount = 4, - // 设置玩家掉线后的保留时间为 120 秒 - PlayerTtl = 120, - }; - play.CreateRoom(roomOptions: options); -}); -``` - - - - -#### 自定义房间匹配规则 - -有的时候我们希望将水平差不多的玩家匹配到一起。例如当前玩家 5 级,他只能和 0-10 级的玩家匹配,10 以上的玩家无法被匹配到。这个场景可以通过给房间设置属性来实现,具体实现逻辑如下: - -1、确定匹配属性,例如 0-10 级是 level-1,10 以上是 level-2。 - - -<> - -```js -var matchLevel = 0; -if (level < 10) { - matchLevel = 1; -} else { - matchLevel = 2; -} -``` - - -<> - -```cs -int matchLevel = 0; -if (level < 10) { - matchLevel = 1; -} else { - matchLevel = 2; -} -``` - - - - -2、根据匹配属性加入房间 - - -<> - -```js -const matchProps = { - level: matchLevel, -}; - -client - .joinRandomRoom({ matchProperties: matchProps }) - .then(() => { - // 成功加入房间 - }) - .catch(console.error); -``` - - -<> - -```cs -Hashtable matchProp = new Hashtable(); -matchProp.Add("matchLevel", matchLevel); -Play.JoinRandomRoom(matchProp); -``` - - - - -3、如果随机加入房间失败,则创建具有匹配属性的房间等待其他同水平的人加入。 - - -<> - -```js -const matchProps = { - level: matchLevel, -}; - -client - .joinRandomRoom({ matchProperties: matchProps }) - .then() - .catch((error) => { - if (error.code === 4301) { - const options = { - // 设置最大人数,当房间满员时,服务端不会再匹配新的玩家进来。 - maxPlayerCount: 4, - // 设置玩家掉线后的保留时间为 120 秒 - playerTtl: 120, - // 房间的自定义属性 - customRoomProperties: matchProps, - // 从房间的自定义属性中选择匹配用的 key - customRoomPropertyKeysForLobby: ["level"], - }; - - client - .createRoom({ - roomOptions: options, - }) - .then() - .catch(console.error); - } - }); -``` - - -<> - -```cs -play.On(Event.ROOM_JOIN_FAILED, (error) => { - if (error["code"] == 4301) - { - var props = new Dictionary(); - props.Add("level", 2); - var options = new RoomOptions() - { - // 设置最大人数,当房间满员时,服务端不会再匹配新的玩家进来。 - MaxPlayerCount = 3, - // 设置玩家掉线后的保留时间为 120 秒 - PlayerTtl = 120, - // 房间的自定义属性 - CustomRoomProperties = props, - // 从房间的自定义属性中选择匹配用的 key - CustoRoomPropertyKeysForLobby = new List() { "level" }, - }; - play.CreateRoom(roomOptions: options); - } -}); -``` - - - - -#### 和好友一起玩 - -假设 PlayerA 希望能和好基友 PlayerB 一起玩游戏,这时又分以下两种情况: - -- 只是两个人一起玩,不允许陌生人加入 -- 好友和陌生人一起玩 - -##### 不允许陌生人加入 - -1、PlayerA 创建房间,设置房间不可见,这样其他人就不会被随机匹配到 PlayerA 创建的房间中。 - - -<> - -```js -const options = { - // 房间不可见 - visible: false, -}; -client - .createRoom({ - roomOptions: options, - }) - .then() - .catch(console.error); -``` - - -<> - -```cs -var options = new RoomOptions() -{ - Visible = false, -}; -play.CreateRoom(roomOptions: options); -``` - - - - -2、PlayerA 通过某种通信方式(例如 [即时通讯](/sdk/im/features/))将房间名称告诉 PlayerB。 - -3、PlayerB 根据房间名称加入到房间中。 - - - -<> - -```js -client.joinRoom("LiLeiRoom").then().catch(console.error); -``` - - -<> - -```cs -Play.JoinRoom(roomName); -``` - - - - -##### 好友和陌生人一起玩 - -PlayerA 通过某种通信方式(例如 [即时通讯](/sdk/im/features/))邀请 PlayerB,PlayerB 接受邀请。 - -1、PlayerA 设置和 PlayerB 一起匹配进入某个房间 - - -<> - -```js -client - .joinRandomRoom({ expectedUserIds: ["playerB"] }) - .then(() => { - // 加入成功 - }) - .catch(console.error); -``` - - -<> - -```cs -Play.JoinRandomRoom(expectedUserIds: new string[] {"playerB"}); -``` - - - - -2、如果有足够空位的房间,PlayerA 加入成功。 - - -<> - -```js -// JavaScript SDK 通过 joinRandomRoom 的 Promise 判断是否加入房间成功 -``` - - -<> - -```cs -play.On(Event.ROOM_JOINED, (evtData) => { - // TODO 可以做跳转场景之类的操作 - -}); -``` - - - - -PlayerA 通过某种通信方式(例如 [即时通讯](/sdk/im/features/))告诉 PlayerB 已经加入房间的 roomName,PlayerB 根据 roomName 加入房间。 - - -<> - -```js -client.joinRoom("LiLeiRoom").then().catch(console.error); -``` - - -<> - -```cs -Play.JoinRoom(roomName); -``` - - - - -3、如果没有合适的房间则创建并加入房间: - - -<> - -```js -const expectedUserIds = ["playerB"]; -client - .joinRandomRoom({ expectedUserIds }) - .then() - .catch((error) => { - // 没有空房间或房间位置不够 - if (error.code === 4301 || error.code === 4302) { - client - .createRoom({ - expectedUserIds: expectedUserIds, - }) - .then() - .catch(console.error); - } - }); -``` - - -<> - -```cs -play.On(Event.ROOM_JOIN_FAILED, (error) => { - var expectedUserIds = new List() { "cr3_2" }; - Play.CreateRoom(expectedUserIds: expectedUserIds); -}); -``` - - - - -PlayerA 创建房间后,通过某种通信方式(例如 [即时通讯](/sdk/im/features/))告诉 PlayerB 已经加入房间的 roomName,PlayerB 根据 roomName 加入房间。 - - -<> - -```js -client.joinRoom("LiLeiRoom").then().catch(console.error); -``` - - -<> - -```cs -Play.JoinRoom(roomName); -``` - - - - -更多匹配接口请参考房间匹配文档:[JavaScript](/sdk/multiplayer/guide/js/#房间匹配)、[C#](/sdk/multiplayer/guide/cs/#房间匹配)。 - -### 游戏中 - -#### 相关概念 - -- **MasterClient**:多人在线对战使用 [MasterClient](/sdk/multiplayer/guide/js/#masterclient) 在客户端担任运算主机,由 MasterClient 来控制游戏逻辑,例如判定游戏开始还是结束、下一轮由谁操作、扣除玩家多少金币等等。 -- **自定义属性**:自定义属性又分为 [房间自定义属性](/sdk/multiplayer/guide/js/#房间自定义属性) 和 [玩家自定义属性](/sdk/multiplayer/guide/js/#玩家自定义属性)。我们建议将游戏数据加入到自定义属性中,例如房间的当前地图、下注总金币、每个人的手牌等数据,这样当 MasterClient 转移时新的 MasterClient 可以拿到当前游戏的最新数据继续进行运算。 - -#### 开始游戏 - -游戏开始前,我们建议为每个玩家设立一个准备状态,当所有玩家准备完毕后,MasterClient 开始游戏。开始游戏前需要将房间设置为不可见,防止游戏期间有其他玩家被匹配进来。 - -Player A 通过设置自定义属性的方式设置准备状态: - - -<> - -```js -// 玩家设置准备状态 -const props = { - ready: true, -}; -// 请求设置玩家属性 -play.player - .setCustomProperties(props) - .then(() => { - // 设置属性成功 - }) - .catch(console.error); -``` - - -<> - -```cs -// 玩家设置准备状态 -Hashtable prop = new Hashtable(); -prop.Add("ready", true); -play.Player.SetCustomProperties(props); -``` - - - - -所有玩家(包括 PlayerA)都会收到事件回调通知: - - -<> - -```js -play.on(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, (data) => { - // MasterClient 才会执行这个运算 - if (play.player.isMaster) { - // 在自己写的方法中检查已经准备的玩家数量,可以通过 play.room.playerList 获取玩家列表。 - const readyPlayerCount = getReadyPlayerCount(); - // 如果都准备好了就开始游戏 - if ( - readyPlayersCount > 1 && - readyPlayersCount == play.room.playerList.length() - ) { - // 设置房间不可见,避免其他玩家被匹配进来 - play.setRoomVisible(false); - // 开始游戏 - start(); - } - } -}); -``` - - -<> - -```cs - -play.On(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, (evtData) => { - // MasterClient 才会执行这个运算 - if (play.Player.IsMaster) - { - // 在自己写的方法中检查已经准备的玩家数量,可以通过 play.Room.playerList 获取玩家列表。 - var readyPlayerCount = getReadyPlayerCount(); - // 如果都准备好了就开始游戏 - if (readyPlayersCount > 1 && readyPlayersCount == Play.Players.Count()) - { - // 设置房间不可见,避免其他玩家被匹配进来 - play.SetRoomVisible(false); - // 开始游戏 - start(); - } - } -}); -``` - - - - -#### 游戏中发送消息 - -游戏中的大部分消息都发给 [MasterClient](/sdk/multiplayer/guide/js/#masterclient),由 MasterClient 运算后再判定下一步操作。假设有这样一个场景:玩家 A 跟牌完成后,告诉 MasterClient 跟牌完成,MasterClient 收到消息后通知所有人当前需要下一个玩家 B 操作。 - -具体发消息流程如下: - -1、Player A 发送自定义事件 `follow` ,通知 MasterClient 自己跟牌完成。 - - -<> - -```js -// 设置事件的接收组为 Master -const options = { - receiverGroup: ReceiverGroup.MasterClient, -}; - -// 设置要发送的信息 -const eventData = { - actorId: play.player.actorId, -}; - -// 设置事件 Id -const FOLLOW_EVENT_ID = 1; - -// 发送事件 -play.sendEvent(FOLLOW_EVENT_ID, eventData, options); -``` - - -<> - -```cs -// 设置事件的接收组为 Master -var options = new SendEventOptions() { - ReceiverGroup = ReceiverGroup.MasterClient -}; -// 设置要发送的信息 -var eventData = new Dictionary(); -eventData.Add("actorId", play.player.actorId); - -// 设置事件 Id -byte followEventId = 1; - -// 发送事件 -play.SendEvent(followEventId, eventData, options); -``` - - - - -2、 MasterClient 中的相关方法会被触发。MasterClient 计算出下一位操作的玩家是 PlayerB,然后调用 `next` 方法,通知所有玩家当前需要 PlayerB 操作。 - - -<> - -```js -// Event.CUSTOM_EVENT 方法会被触发 -play.on(Event.CUSTOM_EVENT, event => { - const { eventId } = event; - - if (eventId === FOLLOW_EVENT_ID) { - // follow 自定义事件 - // 判断下一步需要 PlayerB 操作 - int PlayerBId = getNextPlayerId(); - - // 通知所有玩家下一步需要 PlayerB 操作。 - const options = { - receiverGroup: ReceiverGroup.All, - }; - const eventData = { - actorId: PlayerBId, - }; - const NEXT_EVENT_ID = 2; - play.sendEvent(NEXT_EVENT_ID, eventData, options); - } -}); - -``` - - -<> - -```cs -// Event.CUSTOM_EVENT 方法会被触发 -play.On(Event.CUSTOM_EVENT, (evtData) => { - // 获取事件参数 - var eventId = evtData["eventId"]; - if (eventId == followEventId) { - byte nextEventId = 2; - - // 事件内容 - var eventData = new Dictionary(); - eventData.Add("actorId", PlayerBId); - - // 发送给所有人 - var options = new SendEventOptions() - { - ReceiverGroup = ReceiverGroup.All - }; - - play.SendEvent(nextEventId, eventData, options); - } -}); -``` - - - - -3、所有玩家的相关方法被触发。 - - -<> - -```js -// Event.CUSTOM_EVENT 方法会被触发 -play.on(Event.CUSTOM_EVENT, event => { - const { eventId, eventData } = event; - if (eventId === FOLLOW_EVENT_ID) { - ...... - }; - if (eventId === NEXT_EVENT_ID) { - // next 事件逻辑 - console.log('Next Player:' + eventData.actorId); - } -}); - -``` - - -<> - -```cs -play.On(Event.CUSTOM_EVENT, (evtData) => { - if (eventId == followEventId) - { - ...... - } - - if (eventId == nextEventId) - { - // next 事件逻辑 - var actorId = evtData["actorId"]; - } -}); -``` - - - - -更详细的用法及介绍,请参考 : - -- [JavaScript - 自定义事件](/sdk/multiplayer/guide/js/#自定义事件) -- [C# - 自定义事件](/sdk/multiplayer/guide/cs/#自定义事件) - -#### 游戏中断线重连 - -如果 MasterClient 位于客户端,MasterClient 断线后,多人对战的服务会重新挑选其他成员成为新的 MasterClient,原来的 MasterClient 重新返回房间后会成为一名普通成员。具体请参考 [断线重连](/sdk/multiplayer/guide/js/#断线重连)。 - -#### 退出房间 - - -<> - -```js -play - .leaveRoom() - .then(() => { - // 成功退出房间 - }) - .catch(console.error); -``` - - -<> - -```cs -Play.LeaveRoom(); -``` - - - - -## 文档 - -### JavaScript - -- [快速入门](/sdk/multiplayer/start/js/):快速接入多人在线对战,并运行一个小 Demo -- [多人在线对战开发指南 · JavaScript](/sdk/multiplayer/guide/js/):对多人在线对战的所有功能及接口进行详细介绍。 - -### C# - -- [快速入门](/sdk/multiplayer/start/cs/):快速接入多人在线对战,并运行一个小 Demo -- [多人在线对战开发指南 · C#](/sdk/multiplayer/guide/cs/):对多人在线对战的所有功能及接口进行详细介绍。 diff --git a/leancloud/docs/sdk/multiplayer/guide/_category_.json b/leancloud/docs/sdk/multiplayer/guide/_category_.json deleted file mode 100644 index bbd78eabb..000000000 --- a/leancloud/docs/sdk/multiplayer/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 3 -} diff --git a/leancloud/docs/sdk/multiplayer/guide/cs.mdx b/leancloud/docs/sdk/multiplayer/guide/cs.mdx deleted file mode 100644 index ba6a972c5..000000000 --- a/leancloud/docs/sdk/multiplayer/guide/cs.mdx +++ /dev/null @@ -1,893 +0,0 @@ ---- -title: 多人在线对战开发指南 · C# -sidebar_label: C# -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -## 前言 - -多人在线对战是一款基于 C# 编写的游戏 SDK,它为有强联网需求的网络游戏提供了一整套的客户端 SDK 解决方案,因此开发团队不再需要自建服务端,从而节省大部分开发和运维成本。多人在线对战提供的主要功能如下: - -- 获取房间列表 -- 创建房间 -- 加入房间 -- 随机加入(符合条件的)房间 -- 获取房间玩家 -- 获取、设置、同步房间的属性 -- 获取、设置、同步玩家的属性 -- 发送和接收「自定义事件」 -- 离开房间 - -## SDK 导入 - -请阅读 [安装指南](/sdk/multiplayer/start/cs/#安装),获取 dll 库文件。 - -## 初始化 - -导入需要的命名空间。 - -```cs -using LeanCloud.Play; -``` - -接着我们需要实例化一个在线对战 SDK 的客户端对象。 - - - -```cs -var client = new Client("your-app-id", "your-app-key", userId, playServer: "https://xxx.example.com", gameVersion: "0.0.1"); -// 请将 xxx.example.com 替换为你的应用绑定的自定义 API 域名 -``` - - - - - -```cs -var client = new Client( - "your-client-id", // 游戏的 Client ID - "your-client-token", // 游戏的 Client Token - "tarara", // 设置用户 id - playServer: "https://your_server_url", // 游戏的 API 域名 - gameVersion: "0.0.1" // 设置游戏版本号,选填,默认 0.0.1,不同版本的玩家不会匹配到同一个房间 -); -``` - -- 在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 可以查看游戏的 `Client ID` 和 `Client Token`。 -- API 域名在 **应用配置 > 域名配置 > API** 处查看,参考文档关于[域名](/sdk/domain/guide/)的说明。 - - - -其中, -`userId` 作为客户端的唯一标识连接至服务器。 -需要注意,这个 `userId` 有如下限制: - -- 只允许英文、数字与下划线 -- 长度不能超过 32 个字符 -- 一个应用内全局唯一 - -`gameVersion` 表示客户端的版本号,如果允许多个版本的游戏共存,则可以根据这个版本号路由到不同的游戏服务器。 - -## 连接 - -### 建立连接 - -通过下面的代码将当前玩家连接到多人对战服务: - -```cs -try { - await client.Connect(); - // 连接成功 -} catch (PlayException e) { - // 连接失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -## 大厅 - -我们推荐您**「不要」将玩家加入大厅**,因为在大厅中,服务端会不停地下发最新的全量房间列表,由玩家自行选择其中一个房间进行游戏,这种方式不仅对玩家体验不友好,同时来会带来很大的带宽压力。 -我们推荐您像现在的绝大部分手游一样,**直接通过[房间匹配](#房间匹配)的方式,快速匹配开始游戏**。 - -如果您有特殊的游戏场景需要获取房间列表,可以调用以下方法: - -```cs -try { - await client.JoinLobby(); - // 加入大厅成功 -} catch (PlayException e) { - // 加入大厅失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -当玩家加入到大厅后,服务端会将当前大厅的房间列表推送给客户端,开发者可以根据需求显示房间列表,或加入房间参与游戏。 - -```cs -client.OnLobbyRoomListUpdated += () => { - var roomList = client.LobbyRoomList; - // TODO 可以做房间列表展示的逻辑 -}; -``` - -更多关于 `LobbyRoom`,请参考 [API 文档](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1LobbyRoom.htm)。 - -### 相关事件 - -| 事件 | 参数 | 描述 | -| ---------------------- | ---- | ---------------- | -| OnLobbyRoomListUpdated | 无 | 大厅房间列表更新 | - -## 房间匹配 - -房间,是指产生玩家「战斗交互」的单位。比如斗地主的牌桌、MMO 的副本、微信游戏的对战等,广义上都属于房间的范畴。 -玩家之间的战斗交互都是在房间内完成的。所以,玩家「如何进入房间」就成了房间匹配的关键。下面我们将从「创建房间」和「加入房间」两方面来分析一下常用的「房间匹配」功能。 - -### 创建房间 - -我们可以这样简单地创建一个房间。创建房间的玩家为房主([MasterClient](#masterclient)),房主在创建房间成功的同时也意味着成功加入了该房间。 - -```cs -try { - await client.CreateRoom(); - // 创建房间成功也意味着自己已经成功加入了该房间 -} catch (PlayException e) { - // 创建房间失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -我们也可以创建一个设定相关信息的房间。 - -```cs -// 房间的自定义属性 -var props = new PlayObject { - { "title", "room title" }, - { "level", 2 } -}; -var roomOptions = new RoomOptions { - Visible = false, - EmptyRoomTtl = 10000, - PlayerTtl = 600, - MaxPlayerCount = 2, - CustomRoomProperties = props, - CustomRoomPropertyKeysForLobby = new List { "level" }, - Flag = CreateRoomFlag.MasterSetMaster | CreateRoomFlag.MasterUpdateRoomProperties, -}; -var expectedUserIds = new List { "cr3_2" }; -try { - await client.CreateRoom(roomName, roomOptions, expectedUserIds); - // 创建房间成功也意味着自己已经成功加入了该房间 -} catch (PlayException e) { - // 创建房间失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -其中 `roomName`、`roomOptions` 和 `expectedUserIds` 这些参数都是可选参数。 - -#### roomName - -房间名称必须保证唯一;如果不设置,则由服务端生成唯一房间 Id 并返回。 - -#### roomOptions - -创建房间时的指定参数,包括: - -- `Opened`:房间是否开放。如果设置为 false,则不允许其他玩家加入。 -- `Visible`:房间是否可见。如果设置为 false,则不会出现在大厅的房间列表中,但是其他玩家可以通过指定「房间名称」加入房间。 -- `EmptyRoomTtl`:当房间中没有玩家时,房间保留的时间(单位:秒)。默认为 0,即 房间中没有玩家时,立即销毁房间数据。最大值为 300,即 5 分钟。 -- `PlayerTtl`:当玩家掉线时,保留玩家在房间内的数据的时间(单位:秒)。默认为 0,即 玩家掉线后,立即销毁玩家数据。最大值为 300,即 5 分钟。 -- `MaxPlayerCount`:房间允许的最大玩家数量。 -- `CustomRoomProperties`:房间的自定义属性。 -- `CustomRoomPropertyKeysForLobby`:房间的自定义属性 `CustomRoomProperties` 中「键」的数组,包含在 `CustomRoomPropertyKeysForLobby` 中的属性将会出现在大厅的房间属性中(`client.LobbyRoomList`),而全部属性要在加入房间后的 `room.CustomProperties` 中查看。这些属性将会在匹配房间时用到。 -- `Flag`:创建房间标志位,详情请看下文中 [MasterClient 掉线不转移](#masterclient-掉线不转移),[指定其他成员为 MasterClient](#指定其他成员为-masterclient),[只允许 MasterClient 修改房间属性](#只允许-masterclient-修改房间属性)。 - -#### expectedUserIds - -指定的玩家 ID 数组。这个参数主要用于为某些能加入到房间中的特定玩家「占位」。 - -注意:这些「特定的玩家」并不会真地加入到房间里来,而只会在房间的「空位」上预留出位置,只允许「特定的玩家」加入。如果开发者要做「邀请加入」的功能,还需要通过其他途径,例如 IM、微信分享等将「房间名称」发送给好友,好友再通过 `JoinRoom(roomName)` 接口加入房间。 - -更多关于 `CreateRoom`,请参考 [API 文档](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1Client.htm#a0478f278b300dd4ae4c4e1fe311d3c7c)。 - -### 加入房间 - -当房间创建好后,其他的玩家可以通过「加入房间」参与到游戏中。 - -#### 加入指定房间 - -通过指定「房间名称」加入房间。 - -```cs -try { - // 玩家在加入 'LiLeiRoom' 房间 - await client.JoinRoom("LiLeiRoom"); - // 加入房间成功 -} catch (PlayException e) { - // 加入房间失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -在加入房间时,也可以为其他玩家占位,如果房间剩余空位小于占位数量,会加入房间失败。 - -```cs -var expectedUserIds = new List() { "LiLei", "Jim" }; -try { - // 玩家在加入 "game" 房间,并为 LiLei 和 Jim 玩家占位 - await client.JoinRoom("game", expectedUserIds); -} catch (PlayException e) { - // 加入房间失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} - -``` - -更多关于 `JoinRoom`,请参考 [API 文档](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1Client.htm#ab19049bfb2cf5e62c746f5e4a4ce79b2)。 - -#### 随机加入房间 - -有时候,我们不需要加入指定某个房间,而是随机加入「符合某些条件的房间」(甚至可以是没有条件),比如快速开始、快速匹配等,这时我们可以通过调用 `JoinRandomRoom` 方法随机加入房间。 - -```cs -try { - await client.JoinRandomRoom(); - // 加入房间成功 -} catch (PlayException e) { - // 加入房间失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -也可以为随机加入设置「条件」,例如随机加入到 level = 2 的房间。 - -```cs -// 设置匹配属性 -var matchProps = new PlayObject { - { "level", 2 } -}; -try { - await client.JoinRandomRoom(matchProps); -} catch (PlayException e) { - // 加入房间失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -### 加入或创建指定房间 - -很多游戏没有强制指定房主的场景,只要一部分玩家能进入到同一个房间游戏,谁作为房主都可以,这时我们可以用 `JoinOrCreateRoom()` 来实现。如果有相关房间,这个方法会将当前玩家直接加入房间,如果在房间不存在,则创建一个新房间。 - -```cs -try { - // 例如,有 4 个玩家同时加入一个房间名称为 「room1」 的房间,如果不存在,则创建并加入 - await client.JoinOrCreateRoom("room1"); -} catch (PlayException e) { - // 加入房间失败,也没有成功创建房间 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -更多关于 `JoinOrCreateRoom`,请参考 [API 文档](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1Client.htm#ad4a489ac7663ee9eee812cba2659a187)。 - -### 新玩家加入事件 - -对于已经在房间的玩家,当有新玩家加入到房间时,服务端会派发 `OnPlayerRoomJoined`(新玩家加入)事件通知客户端,客户端可以通过新玩家的属性,做一些显示逻辑。 - -```cs -// 注册新玩家加入事件 -client.OnPlayerRoomJoined += newPlayer => { - // TODO 新玩家加入逻辑 -}; -``` - -可以通过 `client.Room.PlayerList` 获取房间内的所有玩家。 - -### 判断本地玩家 - -拿到房间内的所有玩家后,可以判断某一个 `Player` 是否是当前本地玩家。 - -```cs -var players = client.Room.PlayerList; -var player = players[0]; -var isLocal = player.isLocal; -``` - -### 离开房间 - -当玩家想要「主动」离开房间时,可以调用下面的接口。 - -```cs -try { - await client.LeaveRoom(); - // 离开房间成功,可以执行跳转场景等逻辑 -} catch (PlayException e) { - // 离开房间失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -房间里的其他玩家将会接收到 `OnPlayerRoomLeft`(有玩家离开房间)事件。 - -```cs -// 注册有玩家离开房间事件 -client.OnPlayerRoomLeft += leftPlayer => { - // TODO 可以执行玩家离开的销毁工作 -} -``` - -### 相关事件 - -| 事件 | 参数 | 描述 | -| ------------------ | ---------- | -------------- | -| OnPlayerRoomJoined | newPlayer | 新玩家加入房间 | -| OnPlayerRoomLeft | leftPlayer | 玩家离开房间 | - -## MasterClient - -多人对战服务默认房间的创建者房主为 MasterClient。在游戏中他可以关闭房间、设置房间不可见、踢人等,同时他所在的设备还承担了「逻辑运算功能」,例如它在游戏中可以负责以下场景: - -- 在卡牌游戏中为用户发牌; -- 控制刷怪的时机及逻辑; -- 游戏结束时判断胜负; -- …等等。 - -### MasterClient 位于玩家客户端 - -您可以将 MasterClient 的逻辑写在客户端中,作为房间创建者的玩家终端会承担游戏运算。在这种情况下,多人对战为 MasterClient 提供了以下方便的功能: - -#### MasterClient 掉线转移 - -当 MasterClient 掉线后,多人对战服务会指任一名新的玩家客户端为 MasterClient。即使当原 MasterClient 恢复上线之后,也不会成为新的 MasterClient。当 MasterClient 变化时,SDK 会派发 `OnMasterSwitched`(主机切换)事件通知客户端。 - -```cs -// 注册主机切换事件 -client.OnMasterSwitched += newMaster => { - // TODO 可以做主机切换的展示 - - // 可以根据判断当前客户端是否是 Master,来确定是否执行逻辑处理。 - if (client.Player.IsMaster) { - - } -} -``` - -#### 相关事件 - -| 事件 | 参数 | 描述 | -| ---------------- | --------- | ----------- | -| OnMasterSwitched | newMaster | Master 更换 | - -#### 指定其他成员为 MasterClient - -在游戏过程中,MasterClient 如果不想再承担运算功能,可以调用以下方法主动将自己的角色转移给其他玩家客户端: - -```cs -try { - // 通过玩家的 Id 指定 MasterClient - await client.SetMaster(newMasterId); -} catch (PlayException e) { - // 设置 Master 失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -转移 MasterClient 身份后,房间内所有玩家都会收到 `OnMasterSwitched` 事件。 - -### MasterClient 位于服务端 - -为了确保游戏安全,防止用户作弊等,我们推荐您将 MasterClient 托管在 TDS 提供的服务端 Client Engine 中,同时做以下权限配置: - -#### MasterClient 掉线不转移 - -将 MasterClient 放在服务端后,MasterClient 意外掉线时也不应当转移角色,此时需要在创建房间时指定 `FixedMaster` Flag: - -```cs -var options = new RoomOptions { - Flag = CreateRoomFlag.FixedMaster -}; -await client.CreateRoom(roomOptions: options); -``` - -### MasterClient 的操作 - -#### 设置房间是否开放 - -MasterClient 可以设置房间是否开放,当房间关闭时,不允许其他玩家加入。 - -```cs -try { - // 设置房间关闭 - await client.SetRoomOpen(false); - Debug.Log(client.Room.Open); -} catch (PlayException e) { - // 设置失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -#### 设置房间是否可见 - -MasterClient 可以设置房间是否可见,当房间不可见时,这个房间将不会出现在玩家的大厅房间列表中,但是**其他玩家可以通过指定 roomName 加入**。 - -```cs -try { - // 设置房间不可见 - await client.SetRoomVisible(false); - Debug.Log(client.Room.Visible); -} catch (PlayException e) { - // 设置失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -#### 踢人 - -MasterClient 可以把房间内的其他玩家踢出房间: - -```cs -try { - // 可以传入踢人的 code 和 msg - await client.KickPlayer(otherPlayer.ActorId, 1, "你已被踢出房间"); -} catch (PlayException e) { - // 踢人失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -踢出房间后,被踢的玩家会收到 `OnRoomKicked` 事件。 - -```cs -client.OnRoomKicked += (code, msg) => { - // code 和 msg 就是 MasterClient 在踢人时传递的信息 -}; -``` - -同时除 MasterClient 之外,其他还存在房间的玩家会收到 `OnPlayerRoomLeft` 事件: - -```cs -client.OnPlayerRoomLeft += leftPlayer => { - -}; -``` - -## 自定义属性及同步 - -为了满足开发者不同的游戏需求,多人对战 SDK 允许开发者设置「自定义属性」。 - -自定义属性同步的主要作用包括: - -- 使每个客户端的数据保持一致。 -- 自定义属性由服务端管理,当有玩家进入房间后,即可得到所有的自定义属性。 - -自定义属性又分为「房间自定义属性」和「玩家自定义属性」。 - -### 房间自定义属性 - -可以给房间设置一个 `PlayObject` 类型的自定义属性,比如战斗的回合数、所有棋牌等。 - -```cs -// 设置想要修改的自定义属性 -var props = new PlayObject { - { "gold", 1000 } -}; -try { - // 设置 gold 属性为 1000 - await client.Room.SetCustomProperties(props); - var newProperties = client.Room.CustomProperties; -} catch (PlayException e) { - // 设置属性错误 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -注意:这个接口并不是直接设置「客户端中自定义属性的内存值」,而是发送修改自定义属性的消息,由服务端最终确定是否修改。 - -当房间属性变化时,SDK 将派发 `OnRoomCustomPropertiesChanged`(房间自定义属性)事件通知所有玩家客户端(包括自己)。 - -```cs -// 注册房间属性变化事件 -client.OnRoomCustomPropertiesChanged += changedProps => { - var props = client.Room.CustomProperties; - var gold = props.GetInt("gold"); -}; -``` - -注意:`changedProps` 参数只表示当前修改的参数,不是「全部属性」。如需获得全部属性,请通过 `client.Room.CustomProperties` 获得。 - -#### 只允许 MasterClient 修改房间属性 - -默认情况下房间内所有 Client 都可以修改房间的自定义属性,如果您希望只允许 MasterClient 修改,可以在创建房间时指定 `MasterUpdateRoomProperties` Flag: - -```cs -var options = new RoomOptions { - Flag = MasterUpdateRoomProperties -}; -await client.CreateRoom(roomOptions: options); -``` - -### 玩家自定义属性 - -玩家自定义属性与 [房间自定义属性](#房间自定义属性) 基本一致。 - -```cs -// 扑克牌对象 -var poker = new PlayObject { - { "flower", 1 }, - { "num", 13 } -}; -var props = new PlayObject { - { "nickname", "Li Lei" }, - { "gold", 1000 }, - { "poker", poker } -}; -try { - // 请求设置玩家属性 - await client.Player.SetCustomProperties(props); -} catch (PlayException e) { - // 设置属性错误 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -房间内任意一玩家(包括自己)修改自己的自定义属性后,会触发玩家自定义属性变化事件: - -```cs -// 注册玩家自定义属性变化事件 -client.OnPlayerCustomPropertiesChanged += (player, changedProps) => { - // 得到玩家所有自定义属性 - var props = player.CustomProperties; - var title = props.GetString("title"); - var gold = props.GetInt("gold"); -}; -``` - -### CAS - -CAS 全称为 Compare And Swap,即「检测并更新」的意思,用于避免一些「并发问题」。 - -当调用 `SetCustomProperties` 时,服务器会接收所有客户端提交的值,这无法满足某些场景下的需求。 - -比如,一个属性保存着这个房间的某件物品的持有者,即属性的 key 是物品,value 是持有者。 -任何客户端都可以在任何时候设置这个属性,如果多个客户端同时调用,**服务端会将最后收到的调用作为最终的值**,这样通常是不符合逻辑的,一般来说,应该属于第一个获得的人。 - -`SetCustomProperties` 有一个可选参数 `expectedValues` 可作为判断条件。服务器只会更新当前和 `expectedValues` 匹配的属性,使用过期的 `expectedValues` 的更新将会被忽略。 - -假设一个房间里有 10 名玩家,但是只有 1 把屠龙刀,屠龙刀只能被第一个「抢到」的人获得。 - -我们可以设置 `expectedValues` 中 `tulong` 值: - -```cs -var props = new PlayObject { - // X 表示当前的客户端 - { "tulong", X } -}; -var expectedValues = new PlayObject { - // 屠龙刀当前拥有者 - { "tulong", null } -}; -await client.Room.SetCustomProperties(props, expectedValues); -``` - -这样,在「第一个玩家」获得屠龙刀之后,`tulong` 对应的值为「第一个玩家」,后续的请求 `expectedValues = { tulong: null }` 将会失败。 - -### 相关事件 - -| 事件 | 参数 | 描述 | -| ------------------------------- | ---------------------- | ------------------ | -| OnRoomCustomPropertiesChanged | changedProps | 房间自定义属性变化 | -| OnPlayerCustomPropertiesChanged | (player, changedProps) | 玩家自定义属性变化 | - -## 自定义事件 - -在[自定义属性](#自定义属性及同步)中,我们介绍了如何根据游戏需求来自定义游戏的数据结构和数据类型。 -但是,只有数据是不够的,开发者还需要通过自定义事件互相通信。 - -### 发送自定义事件 - -我们可以通过「自定义事件」发送各种事件,比如游戏开始、抓牌、释放 X 技能、游戏结束等等。 - -```cs -var options = new SendEventOptions { - // 设置事件的接收组为 Master - ReceiverGroup = ReceiverGroup.MasterClient - // 也可以指定接收者 actorId - // TargetActorIds = new List() { 1 }, -}; -// 设置技能 Id -var eventData = new PlayObject { - { "skillId", 123 } -}; -// 设置 skill 事件 id -byte SKILL_EVENT_ID = 1; -try { - // 发送自定义事件 - await client.SendEvent(SKILL_EVENT_ID, eventData, options); -} catch (PlayException e) { - // 发送事件错误 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -其中 `options` 是指事件发送参数,包括「接收组」和「接收者 ID 数组」。 - -- 接收组(ReceiverGroup)是接收事件的目标的枚举值,包括 Others(房间内除自己之外的所有人)、All(房间内的所有人)、MasterClient(主机)。 -- 接收者 ID 数组是指接收事件的目标的具体值,即玩家的 `ActorId` 数组。`ActorId` 可以通过 `player.ActorId` 获得。 - -注意:如果同时设置「接收组」和「接收者 ID 数组」,则「接收者 ID 数组」将会覆盖「接收组」。 - -### 接收自定义事件 - -当事件发送成功后,事件接收者中的自定义事件 `OnCustomEvent` 会被触发,此时可以根据 `eventId`(事件 ID)来处理不同事件。 - -```cs -// 注册自定义事件 -client.OnCustomEvent += (eventId, eventData, senderId) => { - if (eventId == SKILL_EVENT_ID) { - // 如果是 skill 事件 - var skillId = eventData.GetString("skillId"); - // TODO 处理释放技能的表现 - - } -}; -``` - -### `event` 参数 - -| 参数 | 类型 | 描述 | -| --------- | ---------- | ------------------------------- | -| eventId | byte | 事件 Id,用于表示事件 | -| eventData | PlayObject | 事件参数 | -| senderId | int | 事件发送者 Id(玩家的 actorId) | - -## 断线 - -在网络不稳定的情况下,可能会被动断开连接,被动断开连接时 SDK 会向客户端派发 `OnDisconnected`(断开连接)事件,开发者可以在这里在 UI 上对玩家进行提示: - -```cs -// 注册断开连接事件 -client.OnDisconnected += () => { - // TODO 如果需要,可以选择重连 -}; -``` - -## 断线重连 - -### 在房间内保留断线用户 - -网路不稳定、玩家将游戏置于后台等情况可能导致玩家掉线,玩家掉线时,其他在线玩家的 `OnPlayerActivityChanged` 事件会被触发: - -```cs -// 注册玩家掉线 / 上线事件 -client.OnPlayerActivityChanged += player => { - // 获得用户是否「活跃」状态 - Debug.Log(player.IsActive); - // TODO 根据玩家的在线状态可以做显示和逻辑处理 -}; -``` - -如果玩家长时间内没有回到游戏,服务器会将玩家移除房间并销毁玩家数据,房间里的其他玩家将会接收到 `OnPlayerRoomLeft`(有玩家离开房间)事件。 -在创建房间时,我们可以通过 `PlayerTtl` 来设置「玩家掉线后的保留时间」,这样当玩家掉线时,在 `PlayerTtl` 时间内服务端会保留掉线玩家在房间内的数据,并等待该玩家恢复上线。 - -```cs -var options = new RoomOptions { - // 将 PlayerTtl 设置为 300 秒 - PlayerTtl = 300 -} -await client.CreateRoom(roomOptions: options); -``` - -掉线玩家在 `PlayerTtl` 时间内回到房间时,其他玩家的 `OnPlayerActivityChanged` 事件会被再次触发,开发者可以在这个事件中判断玩家的当前状态。 - -### 重新连接 - -用户断线后,可以通过下面的接口重新连接至服务器。 - -```cs -client.OnDisconnected += async () => { - // 重连 - await client.Reconnect(); -}; -``` - -注意:这个接口只是重新连接至服务器,如果之前在「房间内游戏」,并不会直接回到房间。 - -**推荐**[重连后获取房间](#重连后获取房间),再由玩家选择是否回到房间。 - -也可以直接[重连并回到房间](#重连并回到房间)。 - -### 重连后获取房间 - -玩家重连后可以通过 `FetchMyRoom` 接口获取掉线前所在房间: - -- 如果玩家仍在房间中会返回房间 id,游戏此时可告知玩家这个状态,玩家若选择回到掉线前所在房间,调用 `RejoinRoom` 接口即可。 -- 如果玩家不在房间,或房间已被销毁,会返回 [4301](/sdk/multiplayer/error-code/#4301) 的异常,游戏再做相应处理。 - -```cs -string roomName = await client.FetchMyRoom(); -``` - -### 回到房间 - -当玩家连接至服务器以后,可以通过 `Rejoin` 接口「再次回到某房间」。当该玩家在 `PlayerTtl` 时间内调用此接口,可以成功回到房间,如果超过 `PlayerTtl` 时间,则重回房间失败。 - -```cs -try { - // 重连 - await client.Reconnect(); - // 重连成功,回到之前的某房间 - if (roomName) { - try { - await client.RejoinRoom(roomName); - // 回到房间成功,房间内其他玩家会收到 `OnPlayerActivityChanged` 事件。 - } catch (PlayException e) { - // 返回房间失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); - } - } -} catch (PlayException e) { - // 重连失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -### 重连并回到房间 - -这个接口相当于 `Reconnect()` 和 `RejoinRoom()` 的合并。通过这个接口,可以直接重新连接并回到「之前的房间」。 - -```cs -try { - await client.ReconnectAndRejoin(); - // 回到房间成功,更新数据和界面 -} catch (PlayException e) { - // 重连或返回失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -## 关闭 - -开发者也可以通过下面的接口主动关闭 SDK。关闭后如果需要再次使用,需要重新实例化 Client。 - -```cs -client.Close(); -``` - -## 错误处理 - -在我们发起请求时,可以通过 try catch 具体的 exception 信息,例如创建房间时: - -```cs -try { - await client.createRoom(); - // 创建房间成功 -} catch (PlayException e) { - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -## 重大错误事件 - -如果当前发生重大错误,这个事件轻易不会被触发,一旦触发请联系技术支持来处理。 - -```cs -client.OnError += (code, detail) => { - // 联系技术支持 - -}; -``` - -## 序列化 - -在新版 Play 中,我们提供了更丰富的同步数据的方式。主要包括容器类型(PlayObject/PlayArray)和自定义类型。 - -### PlayObject - -`PlayObject` 是用来替换旧版本中的 `Dictionary` 类型的。 - -`PlayObject` 实现了 `IDictionary` 接口,在满足 `IDictionary` 接口的基础上,还提供了更方便的获取接口。 - -常用接口如下: - -```csharp -// 基本类型 -public bool GetBool(object key); -public int GetInt(object key); -public float GetFloat(object key); -... -// 容器类型 -public PlayObject GetPlayObject(object key); // PlayObject 支持嵌套 -public PlayArray GetPlayArray(object key); -public T Get(object key); -``` - -[更多接口请参考](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1PlayObject.htm) - -### PlayArray - -`PlayArray` 实现了 `IList` 接口,主要用于数组对象的同步,与 `PlayObject` 类似。 - -常用接口如下: - -```csharp -// 基本类型 -public bool GetBool(int index); -public int GetInt(int index); -public float GetFloat(int index); -... -// 容器类型 -public PlayObject GetPlayObject(int index); -public PlayArray GetPlayArray(int index); -public T Get(int index); -// 转换接口 -public List ToList(); -``` - -[更多接口请参考](https://leancloud.github.io/Play-SDK-CSharp/html/classLeanCloud_1_1Play_1_1PlayArray.htm) - -### 自定义类型 - -`Play` 除了支持上述两种容器类型,还支持同步「自定义类型」的数据。 - -假设我们有一个 Hero 类型,包含 id, name, hp,定义如下: - -```csharp -class Hero { - int id; - string name; - int hp; -} -``` - -要同步 Hero 类型的数据,需要以下两步: - -#### 实现序列化 / 反序列化方法 - -序列化方法实现由开发者自由实现,可以使用 protobuf, thrift 等。只要满足 `Play` 支持的序列化和反序列化接口即可。 - -```csharp -public delegate byte[] SerializeMethod(object obj); -public delegate object DeserializeMethod(byte[] bytes); -``` - -以下是通过 `Play` 提供的序列化 `PlayObject` 的方式的示例 - -```csharp -// 序列化方法 -public static byte[] Serialize(object obj) { - Hero hero = obj as Hero; - var playObject = new PlayObject { - { "id", hero.id }, - { "name", hero.name }, - { "hp", hero.hp }, - }; - return CodecUtils.SerializePlayObject(playObject); -} -// 反序列化方法 -public static object Deserialize(byte[] bytes) { - var playObject = CodecUtils.DeserializePlayObject(bytes); - Hero hero = new Hero { - id = playObject.GetInt("id"), - name = playObject.GetString("name"), - hp = playObject.GetInt("hp"), - }; - return hero; -} -``` - -#### 注册自定义类型 - -当实现了序列化方法,记得在使用前要先进行自定义类型的注册。 - -```csharp -CodecUtils.RegisterType(typeof(Hero), typeCode, Hero.Serialize, Hero.Deserialize); -``` - -其中 `typeCode` 是表示自定义类型的数字编码,在反序列化时会根据这个编码确定自定义类型。 - -## API 文档 - -更多接口及详情,请参考 [API 接口](https://leancloud.github.io/Play-SDK-CSharp/html/)。 diff --git a/leancloud/docs/sdk/multiplayer/guide/js.mdx b/leancloud/docs/sdk/multiplayer/guide/js.mdx deleted file mode 100644 index dda5d6e76..000000000 --- a/leancloud/docs/sdk/multiplayer/guide/js.mdx +++ /dev/null @@ -1,909 +0,0 @@ ---- -title: 多人在线对战开发指南 · JavaScript -sidebar_label: JavaScript -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -## 前言 - -多人在线对战是一款基于 JavaScript 编写的游戏 SDK,它为有强联网需求的网络游戏提供了一整套的客户端 SDK 解决方案,因此开发团队不再需要自建服务端,从而节省大部分开发和运维成本。多人在线对战提供的主要功能如下: - -- 获取房间列表 -- 创建房间 -- 加入房间 -- 随机加入(符合条件的)房间 -- 获取房间玩家 -- 获取、设置、同步房间的属性 -- 获取、设置、同步玩家的属性 -- 发送和接收「自定义事件」 -- 离开房间 - -## SDK 导入 - -请阅读 [安装指南](/sdk/multiplayer/start/js/#安装),获取 JS 库文件。 - -## 初始化 - -首先,需要引入 SDK 中常用的类型和常量。 - -```javascript -const { - // SDK - Client, - // Play SDK 事件常量 - Event, - // 事件接收组 - ReceiverGroup, - // 创建房间标志 - CreateRoomFlag, -} = Play; -``` - -注意:Cocos Creator 在构建「微信小游戏」项目时,无法将 `Play` 正常加载到全局变量中,因此需要先导入 `Play` 模块。 - -```javascript -const Play = require("./play"); -``` - -接着我们需要实例化一个在线对战 SDK 的客户端对象。 - - - -```javascript -const client = new Client({ - // 设置 APP ID - appId: "your-app-id", - // 设置 APP Key - appKey: "your-app-key", - // 设置 Server (将 xxx.example.com 替换为你的应用绑定的自定义 API 域名) - playServer: 'https://xxx.example.com', - // 设置用户 id - userId: 'tarara', - // 设置游戏版本号(选填,默认 0.0.1) - gameVersion: '0.0.1' -}); -``` - - - - - -```js -const client = new Client({ - appId: 'your-client-id', // 游戏的 Client ID - appKey: 'your-client-token', // 游戏的 Client Token - playServer: 'https://your_server_url', // 游戏的 API 域名 - userId: 'tarara', // 设置用户 id - gameVersion: '0.0.1' // 设置游戏版本号,选填,默认 0.0.1,不同版本的玩家不会匹配到同一个房间 -}); -``` - -- 在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 可以查看游戏的 `Client ID` 和 `Client Token`。 -- API 域名在 **应用配置 > 域名配置 > API** 处查看,参考文档关于[域名](/sdk/domain/guide/)的说明。 - - - -其中, -`userId` 作为客户端的唯一标识连接至服务器。 -需要注意,这个 `userId` 有如下限制: - -- 只允许英文、数字与下划线 -- 长度不能超过 32 个字符 -- 一个应用内全局唯一 - -`gameVersion` 表示客户端的版本号,如果允许多个版本的游戏共存,则可以根据这个版本号路由到不同的游戏服务器。 - -## 连接 - -### 建立连接 - -通过下面的代码将当前玩家连接到多人对战服务: - -```javascript -client - .connect() - .then(() => { - // 连接成功 - }) - .catch((error) => { - // 连接失败 - console.error(error.code, error.detail); - }); -``` - -## 大厅 - -我们推荐您**「不要」将玩家加入大厅**,因为在大厅中,服务端会不停地下发最新的全量房间列表,由玩家自行选择其中一个房间进行游戏,这种方式不仅对玩家体验不友好,同时来会带来很大的带宽压力。 -我们推荐您像现在的绝大部分手游一样,**直接通过[房间匹配](#房间匹配)的方式,快速匹配开始游戏**。 - -如果您有特殊的游戏场景需要获取房间列表,可以调用以下方法: - -```javascript -client - .joinLobby() - .then(() => { - // 加入大厅成功 - }) - .catch((error) => { - // 加入大厅失败 - console.error(error.code, error.detail); - }); -``` - -当玩家加入到大厅后,服务端会将当前大厅的房间列表推送给客户端,开发者可以根据需求显示房间列表,或加入房间参与游戏。 - -```javascript -client.on(Event.LOBBY_ROOM_LIST_UPDATED, () => { - const roomList = client.lobbyRoomList; - // TODO 可以做房间列表展示的逻辑 -}); -``` - -更多关于 `LobbyRoom`,请参考 [API 文档](https://leancloud.github.io/Play-SDK-JS/doc/LobbyRoom.html)。 - -### 相关事件 - -| 事件 | 参数 | 描述 | -| ----------------------- | ---- | ---------------- | -| LOBBY_ROOM_LIST_UPDATED | 无 | 大厅房间列表更新 | - -## 房间匹配 - -房间,是指产生玩家「战斗交互」的单位。比如斗地主的牌桌、MMO 的副本、微信游戏的对战等,广义上都属于房间的范畴。 -玩家之间的战斗交互都是在房间内完成的。所以,玩家「如何进入房间」就成了房间匹配的关键。下面我们将从「创建房间」和「加入房间」两方面来分析一下常用的「房间匹配」功能。 - -### 创建房间 - -我们可以这样简单地创建一个房间。创建房间的玩家为房主([MasterClient](#masterclient)),房主在创建房间成功的同时也意味着成功加入了该房间。 - -```javascript -client - .createRoom() - .then(() => { - // 创建房间成功也意味着自己已经成功加入了该房间 - }) - .catch((error) => { - // 创建房间失败 - console.error(error.code, error.detail); - }); -``` - -我们也可以创建一个设定相关信息的房间。 - -```javascript -// 房间的自定义属性 -const props = { - title: "room title", - level: 2, -}; -const options = { - // 房间不可见 - visible: false, - // 房间空后保留的时间,单位:秒 - emptyRoomTtl: 300, - // 允许的最大玩家数量 - maxPlayerCount: 2, - // 玩家离线后,保留玩家数据的时间,单位:秒 - playerTtl: 300, - customRoomProperties: props, - // 用于做房间匹配的自定义属性键,即房间匹配条件为 level = 2 - customRoomPropertyKeysForLobby: ["level"], - // 给 MasterClient 设置权限 - flag: - CreateRoomFlag.MasterSetMaster | CreateRoomFlag.MasterUpdateRoomProperties, -}; -const expectedUserIds = ["world"]; -client - .createRoom({ - roomName, - roomOptions: options, - expectedUserIds: expectedUserIds, - }) - .then(() => { - // 创建房间成功也意味着自己已经成功加入了该房间 - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -其中 `roomName`、`roomOptions` 和 `expectedUserIds` 这些参数都是可选参数。 - -#### roomName - -房间名称必须保证唯一;如果不设置,则由服务端生成唯一房间 Id 并返回。 - -#### roomOptions - -创建房间时的指定参数,包括: - -- `opened`:房间是否开放。如果设置为 false,则不允许其他玩家加入。 -- `visible`:房间是否可见。如果设置为 false,则不会出现在大厅的房间列表中,但是其他玩家可以通过指定「房间名称」加入房间。 -- `emptyRoomTtl`:当房间中没有玩家时,房间保留的时间(单位:秒)。默认为 0,即 房间中没有玩家时,立即销毁房间数据。最大值为 300,即 5 分钟。 -- `playerTtl`:当玩家掉线时,保留玩家在房间内的数据的时间(单位:秒)。默认为 0,即 玩家掉线后,立即销毁玩家数据。最大值为 300,即 5 分钟。 -- `maxPlayerCount`:房间允许的最大玩家数量。 -- `customRoomProperties`:房间的自定义属性。 -- `customRoomPropertyKeysForLobby`:房间的自定义属性 `customRoomProperties` 中「键」的数组,包含在 `customRoomPropertyKeysForLobby` 中的属性将会出现在大厅的房间属性中(`client.lobbyRoomList`),而全部属性要在加入房间后的 `room.customProperties` 中查看。这些属性将会在匹配房间时用到。 -- `flag`:创建房间标志位,详情请看下文中 [MasterClient 掉线不转移](#masterclient-掉线不转移),[指定其他成员为 MasterClient](#指定其他成员为-masterclient),[只允许 MasterClient 修改房间属性](#只允许-masterclient-修改房间属性)。 - -#### expectedUserIds - -指定的玩家 ID 数组。这个参数主要用于为某些能加入到房间中的特定玩家「占位」。 - -注意:这些「特定的玩家」并不会真地加入到房间里来,而只会在房间的「空位」上预留出位置,只允许「特定的玩家」加入。如果开发者要做「邀请加入」的功能,还需要通过其他途径,例如 IM、微信分享等将「房间名称」发送给好友,好友再通过 `joinRoom(roomName)` 接口加入房间。 - -更多关于 `createRoom`,请参考 [API 文档](https://leancloud.github.io/Play-SDK-JS/doc/Client.html#createRoom)。 - -### 加入房间 - -当房间创建好后,其他的玩家可以通过「加入房间」参与到游戏中。 - -#### 加入指定房间 - -通过指定「房间名称」加入房间。 - -```javascript -// 玩家在加入 'LiLeiRoom' 房间 -client - .joinRoom("LiLeiRoom") - .then(() => { - // 加入房间成功 - }) - .catch((error) => { - // 加入房间失败 - console.error(error.code, error.detail); - }); -``` - -在加入房间时,也可以为其他玩家占位,如果房间剩余空位小于占位数量,会加入房间失败。 - -```javascript -const expectedUserIds = ["LiLei", "Jim"]; -// 玩家在加入 'game' 房间,并为 hello 和 world 玩家占位 -client - .joinRoom("game", { - expectedUserIds, - }) - .then(() => { - // 加入房间成功 - }) - .catch((error) => { - // 加入房间失败 - console.error(error.code, error.detail); - }); -``` - -更多关于 `joinRoom`,请参考 [API 文档](https://leancloud.github.io/Play-SDK-JS/doc/Client.html#joinRoom)。 - -#### 随机加入房间 - -有时候,我们不需要加入指定某个房间,而是随机加入「符合某些条件的房间」(甚至可以是没有条件),比如快速开始、快速匹配等,这时我们可以通过调用 `joinRandomRoom` 方法随机加入房间。 - -```javascript -client - .joinRandomRoom() - .then(() => { - // 加入房间成功 - }) - .catch((error) => { - // 加入房间失败 - console.error(error.code, error.detail); - }); -``` - -也可以为随机加入设置「条件」,例如随机加入到 level = 2 的房间。 - -```javascript -// 设置匹配属性 -const matchProps = { - level: 2, -}; -client - .joinRandomRoom({ - matchProperties: matchProps, - }) - .then(() => { - // 加入房间成功 - }) - .catch((error) => { - // 加入房间失败 - console.error(error.code, error.detail); - }); -``` - -### 加入或创建指定房间 - -很多游戏没有强制指定房主的场景,只要一部分玩家能进入到同一个房间游戏,谁作为房主都可以,这时我们可以用 `joinOrCreateRoom()` 来实现。如果有相关房间,这个方法会将当前玩家直接加入房间,如果在房间不存在,则创建一个新房间。 - -```javascript -// 例如,有 4 个玩家同时加入一个房间名称为 「room1」 的房间,如果不存在,则创建并加入 -client - .joinOrCreateRoom("room1") - .then(() => { - // 加入或创建房间成功 - }) - .catch((error) => { - // 加入房间失败,也没有成功创建房间 - console.error(error.code, error.detail); - }); -``` - -更多关于 `joinOrCreateRoom`,请参考 [API 文档](https://leancloud.github.io/Play-SDK-JS/doc/Client.html#joinOrCreateRoom)。 - -### 新玩家加入事件 - -对于已经在房间的玩家,当有新玩家加入到房间时,服务端会派发 `PLAYER_ROOM_JOINED`(新玩家加入)事件通知客户端,客户端可以通过新玩家的属性,做一些显示逻辑。 - -```javascript -// 注册新玩家加入事件 -client.on(Event.PLAYER_ROOM_JOINED, ({ newPlayer }) => { - // TODO 新玩家加入逻辑 -}); -``` - -可以通过 `client.room.playerList` 获取房间内的所有玩家。 - -### 判断本地玩家 - -拿到房间内的所有玩家后,可以判断某一个 `Player` 是否是当前本地玩家。 - -```javascript -const players = client.room.playerList; -const player = players[0]; -const isLocal = player.isLocal; -``` - -### 离开房间 - -当玩家想要「主动」离开房间时,可以调用下面的接口。 - -```javascript -client - .leaveRoom() - .then(() => { - // 离开房间成功,可以执行跳转场景等逻辑 - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -房间里的其他玩家将会接收到 `PLAYER_ROOM_LEFT`(有玩家离开房间)事件。 - -```javascript -// 注册有玩家离开房间事件 -client.on(Event.PLAYER_ROOM_LEFT, ({ leftPlayer }) => { - // TODO 可以执行玩家离开的销毁工作 -}); -``` - -### 相关事件 - -| 事件 | 参数 | 描述 | -| ------------------ | -------------- | -------------- | -| PLAYER_ROOM_JOINED | { newPlayer } | 新玩家加入房间 | -| PLAYER_ROOM_LEFT | { leftPlayer } | 玩家离开房间 | - -## MasterClient - -多人对战服务默认房间的创建者房主为 MasterClient。在游戏中他可以关闭房间、设置房间不可见、踢人等,同时他所在的设备还承担了「逻辑运算功能」,例如它在游戏中可以负责以下场景: - -- 在卡牌游戏中为用户发牌; -- 控制刷怪的时机及逻辑; -- 游戏结束时判断胜负; -- …等等。 - -### MasterClient 位于玩家客户端 - -您可以将 MasterClient 的逻辑写在客户端中,作为房间创建者的玩家终端会承担游戏运算。在这种情况下,多人对战为 MasterClient 提供了以下方便的功能: - -#### MasterClient 掉线转移 - -当 MasterClient 掉线后,多人对战服务会指任一名新的玩家客户端为 MasterClient。即使当原 MasterClient 恢复上线之后,也不会成为新的 MasterClient。当 MasterClient 变化时,SDK 会派发 `MASTER_SWITCHED`(主机切换)事件通知客户端。 - -```javascript -// 注册主机切换事件 -client.on(Event.MASTER_SWITCHED, ({ newMaster }) => { - // TODO 可以做主机切换的展示 - - // 可以根据判断当前客户端是否是 Master,来确定是否执行逻辑处理。 - if (client.player.isMaster) { - } -}); -``` - -#### 相关事件 - -| 事件 | 参数 | 描述 | -| --------------- | ------------- | ----------- | -| MASTER_SWITCHED | { newMaster } | Master 更换 | - -#### 指定其他成员为 MasterClient - -在游戏过程中,MasterClient 如果不想再承担运算功能,可以调用以下方法主动将自己的角色转移给其他玩家客户端: - -```javascript -// 通过玩家的 Id 指定 MasterClient -client - .setMaster(otherActorId) - .then(() => { - // 此时这里的值为 false - console.log(client.player.isMaster); - }) - .catch(console.error); -``` - -转移 MasterClient 身份后,房间内所有玩家都会收到 `MASTER_SWITCHED` 事件。 - -### MasterClient 位于服务端 - -为了确保游戏安全,防止用户作弊等,我们推荐您将 MasterClient 托管在 TDS 提供的服务端 Client Engine 中,同时做以下权限配置: - -#### MasterClient 掉线不转移 - -将 MasterClient 放在服务端后,MasterClient 意外掉线时也不应当转移角色,此时需要在创建房间时指定 `FixedMaster` Flag: - -```js -client.createRoom({ - roomOptions: { - flag: CreateRoomFlag.FixedMaster, - }, -}); -``` - -### MasterClient 的操作 - -#### 设置房间是否开放 - -MasterClient 可以设置房间是否开放,当房间关闭时,不允许其他玩家加入。 - -```javascript -// 设置房间关闭 -client - .setRoomOpen(false) - .then(() => { - console.log(client.room.open); - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -#### 设置房间是否可见 - -MasterClient 可以设置房间是否可见,当房间不可见时,这个房间将不会出现在玩家的大厅房间列表中,但是**其他玩家可以通过指定 roomName 加入**。 - -```javascript -// 设置房间不可见 -client - .setRoomVisible(false) - .then(() => { - console.log(client.room.visible); - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -#### 踢人 - -MasterClient 可以把房间内的其他玩家踢出房间: - -```javascript -// 可以传入一个 Object 对象传递信息,该对象仅支持 code 和 msg 两个 key。 -var info = { code: 1, msg: "你已被踢出房间" }; -client.kickPlayer(otherPlayer.actorId, info).then(() => { - console.log("成功踢出房间"); -}); -``` - -踢出房间后,被踢的玩家会收到 `ROOM_KICKED` 事件。 - -```javascript -client.on(Event.ROOM_KICKED, ({ code, msg }) => { - // code 和 msg 就是 MasterClient 在踢人时传递的信息 -}); -``` - -同时除 MasterClient 之外,其他还存在房间的玩家会收到 `PLAYER_ROOM_LEFT` 事件: - -```javascript -client.on(Event.PLAYER_ROOM_LEFT, ({ leftPlayer }) => {}); -``` - -## 自定义属性及同步 - -为了满足开发者不同的游戏需求,在线对战 SDK 允许开发者设置「自定义属性」。自定义属性接口参数定义为 JavaScript 的 `Object` 类型,支持的数据类型包括: - -- Boolean -- Number -- String -- Object -- Array - -自定义属性同步的主要作用包括: - -- 使每个客户端的数据保持一致。 -- 自定义属性由服务端管理,当有玩家进入房间后,即可得到所有的自定义属性。 - -自定义属性又分为「房间自定义属性」和「玩家自定义属性」。 - -### 房间自定义属性 - -可以给房间设置一个 `Object` 类型的自定义属性,比如战斗的回合数、所有棋牌等。 - -```javascript -// 设置想要修改的自定义属性 -const props = { - gold: 1000, -}; -// 设置 gold 属性为 1000 -client.room - .setCustomProperties(props) - .then(() => { - var newProperties = client.room.customProperties; - }) - .catch(console.error); -``` - -注意:这个接口并不是直接设置「客户端中自定义属性的内存值」,而是发送修改自定义属性的消息,由服务端最终确定是否修改。 - -当房间属性变化时,SDK 将派发 `ROOM_CUSTOM_PROPERTIES_CHANGED`(房间自定义属性)事件通知所有玩家客户端(包括自己)。 - -```javascript -// 注册房间属性变化事件 -client.on(Event.ROOM_CUSTOM_PROPERTIES_CHANGED, ({ changedProps }) => { - // 可以从这个方法中获得房间的全部属性 - const properties = client.room.customProperties; - const gold = properties.gold; - // TODO 可以做属性变化的界面展示 -}); -``` - -注意:`changedProps` 参数只表示增量修改的参数,不是「全部属性」。如需获得全部属性,请通过 `client.room.customProperties` 获得。 - -#### 只允许 MasterClient 修改房间属性 - -默认情况下房间内所有 Client 都可以修改房间的自定义属性,如果您希望只允许 MasterClient 修改,可以在创建房间时指定 `MasterUpdateRoomProperties` Flag: - -```js -client - .createRoom({ - roomOptions: { - flag: CreateRoomFlag.MasterUpdateRoomProperties, - }, - }) - .then(() => { - // 创建房间成功 - }) - .catch(console.error); -``` - -### 玩家自定义属性 - -玩家自定义属性与 [房间自定义属性](#房间自定义属性) 基本一致。 - -```javascript -// 扑克牌对象 -const poker = { - // 花色 - flower: 1, - // 数值 - num: 13, -}; -const props = { - nickname: "Li Lei", - gold: 1000, - poker: poker, -}; -// 请求设置玩家属性 -client.player - .setCustomProperties(props) - .then(() => { - // 设置属性成功 - }) - .catch(console.error); -``` - -房间内任意一玩家(包括自己)修改自己的自定义属性后,会触发玩家自定义属性变化事件: - -```javascript -// 注册玩家自定义属性变化事件 -client.on(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, ({ player }) => { - // 得到玩家所有自定义属性 - const props = player.customProperties; - const { title, gold } = props; - // TODO 可以做属性变化的界面展示 -}); -``` - -### CAS - -CAS 全称为 Compare And Swap,即「检测并更新」的意思,用于避免一些「并发问题」。 - -当调用 `setCustomProperties` 时,服务器会接收所有客户端提交的值,这无法满足某些场景下的需求。 - -比如,一个属性保存着这个房间的某件物品的持有者,即属性的 key 是物品,value 是持有者。 -任何客户端都可以在任何时候设置这个属性,如果多个客户端同时调用,**服务端会将最后收到的调用作为最终的值**,这样通常是不符合逻辑的,一般来说,应该属于第一个获得的人。 - -`setCustomProperties` 有一个可选参数 `expectedValues` 可作为判断条件。服务器只会更新当前和 `expectedValues` 匹配的属性,使用过期的 `expectedValues` 的更新将会被忽略。 - -假设一个房间里有 10 名玩家,但是只有 1 把屠龙刀,屠龙刀只能被第一个「抢到」的人获得。 - -我们可以设置 `expectedValues` 中 `tulong` 值: - -```javascript -const props = { - tulong: X, // X 分别表示当前的客户端 -}; -const expectedValues = { - tulong: null, // 屠龙刀当前拥有者 -}; -client.room - .setCustomProperties(props, { expectedValues }) - .then(() => {}) - .catch(console.error); -``` - -这样,在「第一个玩家」获得屠龙刀之后,`tulong` 对应的值为「第一个玩家」,后续的请求(`const expectedValues = { tulong: null }`)将会失败。 - -### 相关事件 - -| 事件 | 参数 | 描述 | -| -------------------------------- | ------------------------ | ------------------ | -| ROOM_CUSTOM_PROPERTIES_CHANGED | { changedProps } | 房间自定义属性变化 | -| PLAYER_CUSTOM_PROPERTIES_CHANGED | { player, changedProps } | 玩家自定义属性变化 | - -## 自定义事件 - -在[自定义属性](#自定义属性及同步)中,我们介绍了如何根据游戏需求来自定义游戏的数据结构和数据类型。 -但是,只有数据是不够的,开发者还需要通过自定义事件互相通信。 - -### 发送自定义事件 - -我们可以通过「自定义事件」发送各种事件,比如游戏开始、抓牌、释放 X 技能、游戏结束等等。 - -```javascript -const options = { - // 设置事件的接收组为 MasterClient - receiverGroup: ReceiverGroup.MasterClient, - // 也可以指定接收者 actorId - // options.targetActorIds: [1], -}; -// 设置技能 Id -const eventData = { - skillId: 123, -}; - -// 设置事件 Id -const SKILL_EVENT_ID = 1; -// 发送自定义事件 -client - .sendEvent(SKILL_EVENT_ID, eventData, options) - .then(() => {}) - .catch(console.error); -``` - -其中 `options` 是指事件发送参数,包括「接收组」和「接收者 ID 数组」。 - -- 接收组(ReceiverGroup)是接收事件的目标的枚举值,包括 Others(房间内除自己之外的所有人)、All(房间内的所有人)、MasterClient(房主)。 -- 接收者 ID 数组是指接收事件的目标的具体值,即玩家的 `actorId` 数组。`actorId` 可以通过 `player.actorId` 获得。 - -注意:如果同时设置「接收组」和「接收者 ID 数组」,则「接收者 ID 数组」将会覆盖「接收组」。 - -### 接收自定义事件 - -当事件发送成功后,事件接收者中的自定义事件 `CUSTOM_EVENT` 会被触发,此时可以根据 `eventId`(事件 ID)来处理不同事件。 - -```javascript -// 注册自定义事件 -client.on(Event.CUSTOM_EVENT, ({ eventId, eventData }) => { - if (eventId === SKILL_EVENT_ID) { - // 如果是 skill 事件,则 解构事件数据 - const { skillId, targetId } = eventData; - // TODO 处理释放技能的表现 - } -}); -``` - -### `event` 参数 - -| 参数 | 类型 | 描述 | -| --------- | ------ | ------------------------------- | -| eventId | Number | 事件 Id,用于表示事件 | -| eventData | Object | 事件参数 | -| senderId | Number | 事件发送者 Id(玩家的 actorId) | - -## 断线 - -在网络不稳定的情况下,可能会被动断开连接,被动断开连接时 SDK 会向客户端派发 `DISCONNECTED`(断开连接)事件,开发者可以在这里在 UI 上对玩家进行提示: - -```javascript -// 注册断开连接事件 -client.on(Event.DISCONNECTED, () => { - // TODO 如果需要,可以选择检测网络并重连 -}); -``` - -## 断线重连 - -### 在房间内保留断线用户 - -网路不稳定、玩家将游戏置于后台等情况可能导致玩家掉线,玩家掉线时,其他在线玩家的 `PLAYER_ACTIVITY_CHANGED` 事件会被触发: - -```javascript -// 注册玩家掉线 / 上线事件 -client.on(Event.PLAYER_ACTIVITY_CHANGED, ({ player }) => { - // 获得用户是否「活跃」状态 - cc.log(player.isActive()); - // TODO 根据玩家的在线状态可以做显示和逻辑处理 -}); -``` - -如果玩家长时间内没有回到游戏,服务器会将玩家移除房间并销毁玩家数据,房间里的其他玩家将会接收到 `PLAYER_ROOM_LEFT`(有玩家离开房间)事件。 -在创建房间时,我们可以通过 `playerTtl` 来设置「玩家掉线后的保留时间」,这样当玩家掉线时,在 `playerTtl` 时间内服务端会保留掉线玩家在房间内的数据,并等待该玩家恢复上线。 - -```javascript -const options = { - // 将 playerTtl 设置为 300 秒 - playerTtl: 300, -}; -client - .createRoom({ - roomOptions: options, - }) - .then(() => {}) - .catch(console.error); -``` - -掉线玩家在 `playerTtl` 时间内回到房间时,其他玩家的 `PLAYER_ACTIVITY_CHANGED` 事件会被再次触发,开发者可以在这个事件中判断玩家的当前状态。 - -### 重新连接 - -用户断线后,可以通过下面的接口重新连接至服务器。 - -```javascript -client - .reconnect() - .then(() => { - // 重连成功,此时可以选择是否回到房间 - }) - .catch(console.error); -``` - -注意:这个接口只是重新连接至服务器,如果之前在「房间内游戏」,并不会直接回到房间。如需重连并回到断线前的房间,请参考[重连并回到房间](#重连并回到房间)。 - -### 回到房间 - -当玩家连接至服务器以后,可以通过 `rejoin` 接口「再次回到某房间」。当该玩家在 `playerTtl` 时间内调用此接口,可以成功回到房间,如果超过 `playerTtl` 时间,则重回房间失败。 - -```javascript -client - .reconnect() - .then(() => { - // 重连成功,回到之前的某房间 - return client.rejoinRoom(roomName); // 房间的 roomName 需要自己缓存到客户端 - }) - .then(() => { - // 回到房间成功,房间内其他玩家会收到 `PLAYER_ACTIVITY_CHANGED` 事件。 - }) - .catch(console.error); -``` - -### 重连并回到房间 - -这个接口相当于 `reconnect()` 和 `rejoinRoom()` 的合并。通过这个接口,可以直接重新连接并回到「之前的房间」。 - -```javascript -client - .reconnectAndRejoin() - .then(() => { - // 回到房间成功,更新数据和界面 - }) - .catch(console.error); -``` - -## 关闭 - -开发者也可以通过下面的接口主动关闭 SDK。关闭后如果需要再次使用,需要重新实例化 Client。 - -```javascript -client.close().then(() => { - // 断开连接成功 -}); -``` - -## 错误处理 - -在我们发起请求时,可以通过 Promise catch 具体的 error 信息,例如创建房间时: - -```javascript -client - .createRoom() - .then(() => { - // 创建房间成功 - }) - .catch((error) => { - console.error(error.code, error.detail); - }); -``` - -## 重大错误事件 - -如果当前发生重大错误,这个事件轻易不会被触发,一旦触发请联系技术支持来处理。 - -```javascript -client.on(Event.Error, ({ code, detail }) => { - // 联系技术支持 -}); -``` - -## 序列化 - -JavaScript 是弱类型语言,所以只要是 `Object/Array` 类型的数据,都可进行数据同步。 - -### 自定义类型 - -假设我们有一个 Hero 类型,包含 id, name, hp,定义如下: - -```javascript -class Hero { - constructor(id, name, hp) { - this._id = id; - this._name = name; - this._hp = hp; - } -} -``` - -要同步 Hero 类型的数据,需要以下两步: - -#### 实现序列化 / 反序列化方法 - -序列化方法实现由开发者自由实现,可以使用 protobuf, thrift 等。只要满足 `Play` 支持的序列化和反序列化接口即可。 - -以下是通过 `Play` 提供的序列化 `Object` 的方式的示例: - -```javascript -const { - serializeObject, - deserializeObject, -} = Play; - -// 序列化 -static serialize(hero) { - // 可以筛选要序列化的字段 - const obj = { - id: hero._id, - name: hero._name, - hp: hero._hp, - }; - return serializeObject(obj); -} - -// 反序列化 -static deserialize(bytes) { - const obj = deserializeObject(bytes); - const { id, name, hp } = obj; - const hero = new Hero(id, name, hp); - return hero; -} -``` - -#### 注册自定义类型 - -当实现了序列化方法,记得在使用前要先进行自定义类型的注册。 - -```javascript -const { registerType } = Play; - -registerType(Hero, typeCode, Hero.serialize, Hero.deserialize); -``` - -其中 `typeCode` 是表示自定义类型的数字编码,在反序列化时会根据这个编码确定自定义类型。 - -## API 文档 - -更多接口及详情,请参考 [API 接口](https://leancloud.github.io/Play-SDK-JS/doc/)。 diff --git a/leancloud/docs/sdk/multiplayer/start/_category_.json b/leancloud/docs/sdk/multiplayer/start/_category_.json deleted file mode 100644 index 9ff402a45..000000000 --- a/leancloud/docs/sdk/multiplayer/start/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "快速入门", - "collapsed": true, - "position": 2 -} diff --git a/leancloud/docs/sdk/multiplayer/start/cs.mdx b/leancloud/docs/sdk/multiplayer/start/cs.mdx deleted file mode 100644 index cd9eeffd5..000000000 --- a/leancloud/docs/sdk/multiplayer/start/cs.mdx +++ /dev/null @@ -1,201 +0,0 @@ ---- -title: 多人在线对战入门教程 · C# -sidebar_label: C# -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import CodeBlock from "@theme/CodeBlock"; - -欢迎使用多人在线对战。本教程将通过模拟一个比较玩家分数大小的场景,来讲解 SDK 的核心使用方法。 - -## 安装 - -Play 客户端 SDK 是开源的,源码地址请访问 [csharp-sdk](https://github.com/leancloud/csharp-sdk)。 - -也可以直接下载 [Release 版本](https://github.com/leancloud/csharp-sdk/releases)。 - -### Unity 项目 - -SDK 可以通过 Unity Package Manager 导入或手动导入,二者任选其一: - -* **UPM**:请在项目的 Packages/manifest.json 中添加依赖项: - - - {`"dependencies": { - "com.leancloud.play": "https://github.com/leancloud/csharp-sdk-upm.git#play-${sdkVersions.leancloud.csharp}", - }`} - - -* **直接导入**:解压下载的 LeanCloud-SDK-Play-Unity.zip,将 `Plugins` 目录拖拽至 Unity 工程。如果项目中已有 `Plugins` 目录,则合并至项目中的 `Plugins` 目录。 - -### 开启调试日志 - -为方便调试,你可以通过注册回调获取日志。在 Unity 中,可以参考如下设置: - -```cs -// 设置 SDK 日志委托 -LeanCloud.Common.Logger.LogDelegate = (level, log) => -{ - if (level == LogLevel.Debug) { - Debug.LogFormat("[DEBUG] {0}", log); - } else if (level == LogLevel.Warn) { - Debug.LogWarningFormat("[WARN] {0}", log); - } else if (level == LogLevel.Error) { - Debug.LogErrorFormat("[ERROR] {0}", log); - } -}; -``` - -## 初始化 - -导入需要的命名空间 - -```cs -using LeanCloud.Play; -``` - - - -```cs -var client = new Client("your-app-id", "your-app-key", "tarara", playServer: "https://xxx.example.com"); -// 请将 xxx.example.com 替换为你的应用绑定的自定义 API 域名 -``` - - - - - -```cs -var client = new Client( - "your-client-id", // 游戏的 Client ID - "your-client-token", // 游戏的 Client Token - "tarara", // 设置用户 id - playServer: "https://your_server_url" // 游戏的 API 域名 -); -``` - -- 在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 可以查看游戏的 `Client ID` 和 `Client Token`。 -- API 域名在 **应用配置 > 域名配置 > API** 处查看,参考文档关于[域名](/sdk/domain/guide/)的说明。 - - - -## 连接多人对战服务器 - -```cs -try { - await client.Connect(); -} catch (PlayException e) { - // 连接失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -## 创建或加入房间 - -默认情况下 Play SDK 不需要加入「大厅」,即可创建 / 加入指定房间。 - -```cs -try { - await client.JoinOrCreateRoom(roomName); -} catch (PlayException e) { - // 创建或加入房间失败 - Debug.LogErrorFormat("{0}, {1}", e.Code, e.Detail); -} -``` - -`JoinOrCreateRoom` 通过相同的 roomName 保证两个客户端玩家可以进入到相同的房间。请参考 [开发指南](/sdk/multiplayer/guide/cs/#加入或创建指定房间) 获取更多关于 `JoinOrCreateRoom` 的用法。 - -## 通过 CustomPlayerProperties 同步玩家属性 - -当有新玩家加入房间时,Master 为每个玩家分配一个分数,这个分数通过「玩家自定义属性」同步给玩家。 -(这里没有做更复杂的算法,只是为 Master 分配了 10 分,其他玩家分配了 5 分)。 - -```cs -// 注册新玩家加入房间事件 -client.OnPlayerRoomJoined += (newPlayer) => { - Debug.LogFormat("new player: {0}", newPlayer.UserId); - if (client.Player.IsMaster) { - // 获取房间内玩家列表 - var playerList = client.Room.PlayerList; - for (int i = 0; i < playerList.Count; i++) { - var player = playerList[i]; - var props = new PlayObject(); - // 判断如果是房主,则设置 10 分,否则设置 5 分 - if (player.IsMaster) { - props.Add("point", 10); - } else { - props.Add("point", 5); - } - player.SetCustomProperties(props); - } - var data = new PlayObject { - { "winnerId", client.Room.Master.ActorId } - }; - var opts = new SendEventOptions { - ReceiverGroup = ReceiverGroup.All - }; - client.SendEvent(GAME_OVER_EVENT, data, opts); - } -}; -``` - -玩家得到分数后,显示自己的分数。 - -```cs -// 注册「玩家属性变更」事件 -client.OnPlayerCustomPropertiesChanged += (player, changedProps) => { - // 判断如果玩家是自己,则做 UI 显示 - if (player.IsLocal) { - // 得到玩家的分数 - long point = player.CustomProperties.GetInt("point"); - Debug.LogFormat("{0} : {1}", player.UserId, point); - scoreText.text = string.Format("Score: {0}", point); - } -}; -``` - -## 通过「自定义事件」通信 - -当分配完分数后,将获胜者(Master)的 ID 作为参数,通过自定义事件发送给所有玩家。 - -```cs -if (client.Player.IsMaster) { - // ... - var data = new PlayObject { - { "winnerId", client.Room.Master.ActorId } - }; - var opts = new SendEventOptions { - ReceiverGroup = ReceiverGroup.All - }; - client.SendEvent(GAME_OVER_EVENT, data, opts); -} -``` - -根据判断胜利者是不是自己,做不同的 UI 显示。 - -```cs -// 注册自定义事件 -client.OnCustomEvent += (eventId, eventData, senderId) => { - if (eventId == GAME_OVER_EVENT) { - // 得到胜利者 Id - int winnerId = eventData.GetInt("winnerId"); - // 如果胜利者是自己,则显示胜利 UI;否则显示失败 UI - if (client.Player.ActorId == winnerId) { - Debug.Log("win"); - resultText.text = "Win"; - } else { - Debug.Log("lose"); - resultText.text = "Lose"; - } - client.Close(); - } -}; -``` - -## Demo - -我们通过 Unity 完成了这个 Demo,供大家运行参考。 - -[QuickStart 工程](https://github.com/leancloud/Play-CSharp-Quick-Start)。 diff --git a/leancloud/docs/sdk/multiplayer/start/js.mdx b/leancloud/docs/sdk/multiplayer/start/js.mdx deleted file mode 100644 index 8b1d42347..000000000 --- a/leancloud/docs/sdk/multiplayer/start/js.mdx +++ /dev/null @@ -1,332 +0,0 @@ ---- -title: 多人在线对战入门教程 · JavaScript -sidebar_label: JavaScript -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -欢迎使用多人在线对战。本教程将通过模拟一个比较玩家分数大小的场景,来讲解 SDK 的核心使用方法。 - -## 安装 - -多人在线对战客户端 SDK 是开源的,您可以直接下载 [Release 版本](https://github.com/leancloud/Play-SDK-JS/releases)。源码请访问 [Play-SDK-JS](https://github.com/leancloud/Play-SDK-JS)。 - -## 支持开发平台 - -微信开发者工具:微信小程序 / 微信小游戏 - -CocosCreator:Mac、Web、微信小游戏、Facebook Instant Game、iOS、Android。 - -LayaAir:微信小游戏 - -Egret:Web - -### Cocos Creator - -[下载 `play.js`](https://github.com/leancloud/Play-SDK-JS/releases) 并拖拽至 Cocos Creator 工程中,选择「插件」模式导入。可参考 [Cocos Creator 插件脚本](https://docs.cocos.com/creator/manual/zh/scripting/external-scripts.html)。 - -在 Cocos Creator 中选中刚刚导入的 play.js 文件,在「属性检查器」选中以下所有选项: - -- 导入为插件 -- 允许 Web 平台加载 -- 允许编辑器加载 -- 允许 Native 平台加载 - -如图所示: - -![image](/img/multiplayer/cocos-creator-multiplayer-install.png) - -### LayaAir - -[下载 `play-laya.js`](https://github.com/leancloud/Play-SDK-JS/releases) 至 Laya 工程的 bin/libs 目录下。 - -在 bin/index.html 中项目「IDE 生成的 UI 文件」之前引入刚下载的 SDK 文件: - -```diff - - - - -+ - - - -``` - -### Egret - -[下载 `play-egret.zip`](https://github.com/leancloud/Play-SDK-JS/releases) 并解压至 Egret 工程的 libs 目录下。 - -在 Egret 工程中的 egretProperties.json 文件中添加 SDK 配置: - -```diff -{ - "engineVersion": "5.2.13", - "compilerVersion": "5.2.13", - "template": {}, - "target": { - "current": "web" - }, - "modules": [ - { - "name": "egret" - }, - ... -+ { -+ "name": "Play", -+ "path": "./libs/play" -+ } - ] -} -``` - -在 Egret 工程下,执行 `Egret build -e` 命令,如果在 manifest.json 中生成了 SDK 引用,说明 SDK 安装成功。 - -```diff -{ - "initial": [ - "libs/modules/egret/egret.js", - ... -+ "libs/play/Play.js" - ], - "game": [ - ... - ] -} -``` - -可参考 [Egret 第三方库使用方法](https://docs.egret.com/engine/docs/extension/threes/instructions)。 - -### 微信小程序 - -[下载 `play-weapp.js`](https://github.com/leancloud/Play-SDK-JS/releases) 并拖拽至微信小程序的工程目录下即可。 - -### Node.js 安装 - -安装与引用 SDK: - -```sh -npm install @leancloud/play --save -``` - -## 日志 - -日志可以方便我们追踪问题,SDK 支持在浏览器和 Node.js 环境下打开日志调试。调试日志开启后,SDK 会把网络请求、错误消息等信息输出到 console 中。 - -### 浏览器 - -浏览器环境下,请打开浏览器的控制台,运行以下命令: - -```shell -localStorage.debug = 'Play' -``` - -### Node.js - -Node.js 环境下,需要将环境变量 DEBUG 设置为 `Play`,你可以在启动某个命令之前设置环境变量。 -下面以本地启动云引擎调试的命令 `lean up` 为例: - -```sh -# Unix -DEBUG='Play' lean up -# Windows cmd -set DEBUG=Play lean up -``` - -## 初始化 - -导入 SDK - -```javascript -const { - Client, - Region, - Event, - ReceiverGroup, - setAdapters, - LogLevel, - setLogger, -} = Play; -``` - - - -```javascript -const client = new Client({ - // 设置 APP ID - appId: "your-app-id", - // 设置 APP Key - appKey: "your-app-key", - // 设置 Server (请将 xxx.example.com 替换为你的应用绑定的自定义 API 域名) - playServer: 'https://xxx.example.com', - // 设置用户 id - userId: 'tarara', - // 设置游戏版本号(选填,默认 0.0.1) - gameVersion: '0.0.1' -}); -``` - - - - - -```js -const client = new Client({ - appId: 'your-client-id', // 游戏的 Client ID - appKey: 'your-client-token', // 游戏的 Client Token - playServer: 'https://your_server_url', // 游戏的 API 域名 - userId: 'tarara', // 设置用户 id - gameVersion: '0.0.1' // 设置游戏版本号,选填,默认 0.0.1,不同版本的玩家不会匹配到同一个房间 -}); -``` - -- 在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 可以查看游戏的 `Client ID` 和 `Client Token`。 -- API 域名在 **应用配置 > 域名配置 > API** 处查看,参考文档关于[域名](/sdk/domain/guide/)的说明。 - - - -## 连接多人对战服务器 - -```javascript -client - .connect() - .then(() => { - // 连接成功 - }) - .catch((error) => { - // 连接失败 - console.error(error.code, error.detail); - }); -``` - -## 创建或加入房间 - -默认情况下 Play SDK 不需要加入「大厅」,即可创建 / 加入指定房间。 - -```javascript -// 例如,有 4 个玩家同时加入一个房间名称为「room1」的房间,如果不存在,则创建并加入 -client - .joinOrCreateRoom("room1") - .then(() => { - // 加入或创建房间成功 - }) - .catch((error) => { - // 加入房间失败,也没有成功创建房间 - console.error(error.code, error.detail); - }); -``` - -`joinOrCreateRoom` 通过相同的 roomName 保证两个客户端玩家可以进入到相同的房间。请参考 [开发指南](/sdk/multiplayer/guide/js/#加入或创建指定房间) 获取更多关于 `joinOrCreateRoom` 的用法。 - -## 通过 CustomPlayerProperties 同步玩家属性 - -当有新玩家加入房间时,Master 为每个玩家分配一个分数,这个分数通过「玩家自定义属性」同步给玩家。 -(这里没有做更复杂的算法,只是为 Master 分配了 10 分,其他玩家分配了 5 分)。 - -```javascript -// 注册新玩家加入房间事件 -client.on(Event.PLAYER_ROOM_JOINED, (data) => { - const { newPlayer } = data; - console.log(`new player: ${newPlayer.userId}`); - if (client.player.isMaster) { - // 获取房间内玩家列表 - const playerList = client.room.playerList; - for (let i = 0; i < playerList.length; i++) { - const player = playerList[i]; - // 判断如果是房主,则设置 10 分,否则设置 5 分 - if (player.isMaster) { - player.setCustomProperties({ - point: 10, - }); - } else { - player.setCustomProperties({ - point: 5, - }); - } - } - // ... - } -}); -``` - -玩家得到分数后,显示自己的分数。 - -```javascript -// 注册「玩家属性变更」事件 -client.on(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, (data) => { - const { player } = data; - // 解构得到玩家的分数 - const { point } = player.customProperties; - console.log(`${player.userId}: ${point}`); - if (player.isLocal) { - // 判断如果玩家是自己,则做 UI 显示 - this.scoreLabel.string = `score:${point}`; - } -}); -``` - -## 通过「自定义事件」通信 - -当分配完分数后,将获胜者(Master)的 ID 作为参数,通过自定义事件发送给所有玩家。 - -```javascript -if (client.player.isMaster) { - const WIN_EVENT_ID = 2; - client.sendEvent( - WIN_EVENT_ID, - { winnerId: client.room.masterId }, - { receiverGroup: ReceiverGroup.All } - ); -} -``` - -根据判断胜利者是不是自己,做不同的 UI 显示。 - -```javascript -// 注册自定义事件 -client.on(Event.CUSTOM_EVENT, (event) => { - // 解构事件参数 - const { eventId, eventData } = event; - if (eventId === "win") { - // 解构得到胜利者 Id - const { winnerId } = eventData; - console.log(`winnerId: ${winnerId}`); - // 如果胜利者是自己,则显示胜利 UI;否则显示失败 UI - if (client.player.actorId === winnerId) { - this.resultLabel.string = "win"; - } else { - this.resultLabel.string = "lose"; - } - client.close().then(() => { - // 断开连接成功 - }); - } -}); -``` - -## Demo - -我们在 Cocos Creator、LayaAir、Egret Wing 中都完成了这个 Demo,供大家运行和参考。 - -[QuickStart 工程](https://github.com/leancloud/Play-Quick-Start-JS)。 - -## 构建注意事项 - -你可以通过 Cocos Creator 构建出其支持的工程。 - -其中仅在构建 Android 工程时需要做一点额外的配置,需要在初始化 `play` 之前添加如下代码: - -```js -onLoad() { - const { setAdapters } = Play; - if (cc.sys.platform === cc.sys.ANDROID) { - const caPath = cc.url.raw('resources/cacert.pem'); - setAdapters({ - WebSocket: (url) => new WebSocket(url, 'protobuf.1', caPath) - }); - } -} -``` - -这样做的原因是 SDK 使用了基于 WebSocket 的 wss 进行安全通信,需要通过以上代码适配 Android 平台的 CA 证书机制。 diff --git a/leancloud/docs/sdk/push/_category_.json b/leancloud/docs/sdk/push/_category_.json deleted file mode 100644 index 31d5ca7eb..000000000 --- a/leancloud/docs/sdk/push/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "推送通知", - "collapsed": true, - "position": 19 -} diff --git a/leancloud/docs/sdk/push/features.mdx b/leancloud/docs/sdk/push/features.mdx deleted file mode 100644 index a03e726c8..000000000 --- a/leancloud/docs/sdk/push/features.mdx +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: 推送通知功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 -slug: /sdk/push/features/ ---- - - - - - -import {Conditional} from '/src/docComponents/conditional'; - -推送通知,使得开发者可以即时地向其应用程序的用户推送通知或者消息,与用户保持互动,从而有效地提高留存率,提升用户体验。平台提供整合了 Android 推送、iOS 推送的统一推送服务。 - -除了通过 iOS、Android SDK 做推送服务之外,你还可以通过 REST API 来发送推送请求。 - -## 实用功能 - -iOS / Android,统一的全平台推送解决方案,快速集成,轻松提升用户粘性。 - -### 多种消息形式 - -文本通知、富媒体、自定义消息,支持消息透传。 - -### 推送打开统计 - -完备的推送效果统计,到达率 + 用户打开率展示。 - -### 定时推送与按条件推送 - -简单直观的控制台,方便产品运营人员直接完成业务操作。 - -## 优势和特色 - -- 基于 WebSocket TLS 通道,穿透性好,消息传递安全可靠。 - -- 极简指令,自主二进制协议,省电省流量。 - -- Android 混合推送。完美对接国内主流手机厂商 FCM 的推送服务,提供可靠、统一的安卓推送解决方案。 - - - -- APNs 专线连接。提供跨区域专线直连苹果推送服务集群,通道稳定可靠,保证消息全天候可达。 - - diff --git a/leancloud/docs/sdk/push/guide/Unreal.mdx b/leancloud/docs/sdk/push/guide/Unreal.mdx deleted file mode 100644 index fd721ed4a..000000000 --- a/leancloud/docs/sdk/push/guide/Unreal.mdx +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: Unreal 推送指南 -sidebar_label: Unreal 推送 -sidebar_position: 6 -slug: /sdk/push/guide/Unreal/ ---- - - - - - -import {Conditional} from '/src/docComponents/conditional'; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - - -本文介绍了如何在 Unreal 中使用推送通知功能。建议先阅读 [推送通知服务总览](/sdk/push/guide/overview/) 了解相关概念。 - -由于 Android 系统对于第三方推送管控越来越严格,所以目前只支持 iOS 及 Android 厂商(华为、小米、vivo、OPPO、魅族、荣耀)FCM 推送。 - -## 准备工作 - -### 前提条件 - -* 安装 **UE 4.26** 及以上版本 -* iOS **12** 或更高版本 -* Android MinSDK 为 **API21** 或更高版本 - -### iOS - -请参考 [iOS 推送设置指南](/sdk/push/guide/ios-cert/)申请 iOS 推送证书。 - -### Android - -请参考 [Android 混合推送开发指南](/sdk/push/guide/android-mixpush/)申请各厂商 FCM Android 推送权限。 - -注意:这里只需要参考混合推送指南申请各厂商 FCM 的推送权限,**不需要** 参考混合推送指南中 Android 相关配置的内容。 - -## 接入推送服务 - -### 安装插件 - -* 下载 **[TapSDK.zip](https://github.com/taptap/TapSDK-UE4/releases)** 解压后将 `LeanCloudPush`、`LeanCloud` 文件夹 `Copy` 到项目的 `Plugins` 目录中(如果项目中缺少关于 AndroidX 的配置,可以将 `AndroidX` 也 `Copy` 到项目中) -* 重启 Unreal Editor -* 打开 **编辑 > 插件 > 项目 > TapTap**,开启 `LeanCloudPush` 模块 - -### 依赖所需模块 - -在 **Project.Build.cs** 中添加所需模块: - -```c# -PublicDependencyModuleNames.AddRange(new string[] { - "Core", - "CoreUObject", - "Engine", - "Slate", - "SlateCore", - "Http", - "Json", - "JsonUtilities", -}); -if (Target.Platform == UnrealTargetPlatform.IOS || Target.Platform == UnrealTargetPlatform.Android) -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - // 推送接入 - "LeanCloudPush", - "LeanCloudMobile" - } - ); -} -else -{ - PublicDependencyModuleNames.AddRange( - new string[] - { - "LeanCloud" - } - ); -} -``` - -### 项目所需配置 - -#### iOS 所需配置项 - -在文件 **DefaultEngine.ini** 添加如下配置: - -```ini -[/Script/IOSRuntimeSettings.IOSRuntimeSettings] -bEnableRemoteNotificationsSupport=True -``` - -#### Android 所需配置项 - -* 新建一个名叫 **app** 的文件夹,将从华为开发者中心下载到的 **agconnect-services.json** 文件拷贝到文件夹里 -* 在项目安卓的 UPL 文件中加入如下代码(没有 UPL 新建一个),填写正确的配置 - -```xml - - - - - - - - - - - - - ``` - -### 导入头文件 - -```cpp -#if PLATFORM_IOS -#include "iOS/LCIOSPush.h" -#elif PLATFORM_ANDROID -#include "Android/LCAndroidPush.h" -#endif -``` - -### 如何使用 - -将开发者平台获得的配置填入下面接口中: - -```cpp -#if PLATFORM_IOS - FLCIOSPush::Register("iOS Team ID"); -#elif PLATFORM_ANDROID - FString DeviceName = FLCAndroidPush::GetDeviceName().ToLower(); - if (DeviceName.Contains("huawei")) { - FLCAndroidPush::RegisterHuaWei(); - } else if (DeviceName.Contains("oppo")) { - FLCAndroidPush::RegisterOPPO("OPPO的AppKey", "OPPO的AppSecret"); - } else if (DeviceName.Contains("vivo")) { - FLCAndroidPush::RegisterVIVO(); - } else if (DeviceName.Contains("meizu")) { - FLCAndroidPush::RegisterMeiZu("Meizu的AppId", "Meizu的AppKey"); - } else if (DeviceName.Contains("honor")) { - FLCAndroidPush::RegisterHonor(); - } else { - FLCAndroidPush::RegisterXiaoMi("小米的AppId", "小米的AppKey"); - } -#endif -``` diff --git a/leancloud/docs/sdk/push/guide/_category_.json b/leancloud/docs/sdk/push/guide/_category_.json deleted file mode 100644 index 3446da847..000000000 --- a/leancloud/docs/sdk/push/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 2 -} diff --git a/leancloud/docs/sdk/push/guide/android-mixpush.mdx b/leancloud/docs/sdk/push/guide/android-mixpush.mdx deleted file mode 100644 index 9695d29b7..000000000 --- a/leancloud/docs/sdk/push/guide/android-mixpush.mdx +++ /dev/null @@ -1,1409 +0,0 @@ ---- -title: Android 混合推送指南 -sidebar_label: Android 混合推送 -sidebar_position: 4 -slug: /sdk/push/guide/android-mixpush/ ---- - - - - - -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import {Conditional} from '/src/docComponents/conditional'; - -## 混合推送概述 - -自 Android 8.0 之后,系统权限控制越来越严,第三方推送通道的生命周期受到较大限制;同时,国内主流厂商也开始推出自己独立的推送服务,而厂商间千差万别的繁杂接口徒增了开发和代码维护的难度。为此,我们推出混合推送方案。我们逐一对接国内主流厂商,将它们不同的接口隐藏起来,让开发者通过统一的 API 完成推送任务。这保障了主流 Android 系统上的推送到达率,也大幅降低了开发复杂度。 - -在混合推送方案里,消息下发时使用的通道不再是我们自己维持的 WebSocket 长连接,而是借用厂商和 OS 层的系统通道进行通信。 -一条推送消息下发的步骤如下: - -1. 开发者调用云服务 Push API 请求对全部或特定设备进行推送; -2. 云推送服务端将请求转发给厂商的推送接口; -3. 厂商通过手机端的系统通道下发推送消息,同时手机端系统消息接收器将推送消息展示到通知栏; -4. 终端用户点击消息之后唤起目标应用或者页面。 - -整个流程与苹果的 APNs 推送类似,SDK 在客户端基本不会得到调用(具体依赖于厂商的实现方案),消息的下发和展示都依赖厂商客户端的行为。所以**如果部分厂商在某些推送中夹带了其他非开发者提交的消息,或者在服务启用的时候,有额外营销性质的弹窗,这都是厂商自己的行为,与我们完全无关**,还请大家了解。另外,如果开发者碰到厂商 SDK 的问题,我们也无法深入调查,还请大家自行到厂商的论坛或技术支持渠道咨询解决。 - - - -Android 混合推送功能仅对商用版应用开放,如果希望使用该功能,请进入 **应用 > 推送 > 设置 > 混合推送**,打开混合推送的开关。 - - - - - -Android 混合推送功能仅对商用版应用开放,如果希望使用该功能,请进入 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > 混合推送**,打开混合推送的开关。 - - - -注意,混合推送可以随时按需开关。当该选项关闭后,下一次 Android 推送会与普通推送一样自动选择自有通道送达客户端,除了会再次遇到上面提到的自有通道在部分 ROM 上会受到限制的问题之外,不会有别的影响。而当该选项再次开启后,Android 推送又会去选择厂商推送渠道。 - -开启了混合推送之后,Installation 表中每一个设备对应的记录,会增加 `registrationId` 字段,用于记录厂商分配的注册 id(类似于 APNs 的 device token),同时还会增加一个 `vendor` 字段(如果没有这一字段,则说明客户端集成有问题),其值为 - -vendor | 厂商 ----|--- -`HMS` | 华为 HMS 推送 -`mi` | 小米推送 -`mz` | 魅族推送 -`oppo`| OPPO 推送 -`vivo`| vivo 推送 -`honor`| 荣耀推送 - -注意,混合推送对接的是厂商各自的推送服务,需要单独配置,不支持混用。 -通常情况下,需要提交不同的版本(分别对接厂商的推送服务)到相应厂商的应用商店。 -如果希望使用统一版本,那么需要自行判断手机型号,在手机上开启对应的推送。 - -### 推送提醒的红点或角标展示 - -很多开发者都希望可以在应用桌面开启角标或者小红点,以达到更好的提醒效果。国内厂商对此功能的开放程度不一,详见下表: - -厂商 | 是否支持角标/红点 | 是否需要配置 | 适配说明 ---- | --- | --- | --- -华为 | 支持角标 | 是 | 请参考下文[华为角标适配说明](#华为角标适配说明) -小米 | 支持角标 | 否 | 遵从系统默认逻辑,感应通知栏通知数目,按 1 自动增减 -OPPO | 支持红点 | 否 | 圆点展示需由用户在通知设置中手动开启,遵从系统默认逻辑,有通知则展示,无则不展示;数值展示只对指定应用开启,例如 QQ、微信,需向官方进行权限申请,暂无明确适配说明。 -vivo | 支持角标 | 是 | 参考下文[vivo 手机角标适配说明](#vivo-手机角标适配说明) -魅族 | 支持红点 | 否 | 遵从系统默认逻辑,仅支持红点展示,有通知则展示,无则不展示 -荣耀 | 支持角标 | 否 | 客户端无需配置,服务端推送时通过 badge 参数进行设置,详情可参考:[荣耀服务端推送](https://developer.hihonor.com/cn/kitdoc?category=%E5%9F%BA%E7%A1%80%E6%9C%8D%E5%8A%A1&kitId=11002&navigation=guides&docId=cloud-base-api.md&token=#%E6%A1%8C%E9%9D%A2%E8%A7%92%E6%A0%87). - -### 通知栏消息与透传消息 - -很多开发者会关心通知栏消息和透传消息是否支持,因为应用状态不同,可能接收到消息的途径不一样,产品层面希望的处理方式也有差异。不同厂商对透传消息的支持不一样,详见下表: - -厂商 | 是否支持透传消息 ---- | --- -华为 | 是 -小米 | 是 -OPPO | 否 -vivo | 否(老版本有透传接口,新版本已不建议使用) -魅族 | 否 -荣耀 | 是 - -注意,如果指定了 `silent` 为真,但厂商不支持透传,那么这条消息会被丢弃,推送记录中会记录相应的报错信息。 - -### 即时通讯的离线推送 - -在即时通讯服务中,在 iOS 平台上如果用户下线,是可以启动离线消息推送机制的,对于 Android 用户来说,如果只是使用云推送自有通道,那么是不存在离线推送的,因为聊天和推送共享同一条 WebSocket 长链接,在即时通讯服务中用户下线了的话,那么推送也必然是不可达的。但是如果启用了混合推送,因为推送消息走的是厂商通道,这一点和 iOS 基本一致,所以这时候 Android 用户就存在离线推送的通知路径了。 -也就是说,如果开启了混合推送,那么即时通讯里面的离线推送和静音机制,对使用了混合推送的 Android 用户也是有效的。 - -### 受限说明 - -推送消息长度限制: - -- 消息中的应用包名最大支持 128 字节,消息内容最大支持 4KB 字节。 - -最低 Android 版本要求: - -- 华为推送需要用户手机上安装 HMS Core(APK)6.11.0.300 及以上版本,最低 Android 版本为 4.4(minSdkVersion 19)。 -- 小米推送服务 SDK 支持的最低安卓版本为 4.4(minSdkVersion:19)。 -- vivo 推送服务支持的最低 Android 版本为 6.0(minSdkVersion:23)。 -- OPPO 推送只支持 Android 4.4 或以上版本的手机系统(minSdkVersion:19)。 -- 魅族(flyme)推送只支持 Android 4.2 或以上版本的手机系统(minSdkVersion:17)。 -- 荣耀 推送支持国内Magic UI 4.0及以上,海外Magic UI 4.2及以上(minSdkVersion 19)。 - - -影响送达率的因素说明: - -- 终端设备是否在线。如果设备离线,推送服务会缓存消息,待设备上线后,再将消息推送给设备。 -- 终端设备上集成推送服务 SDK 的应用是否被卸载。 -- 终端设备的网络状况是否稳定。 -- 终端设备的安全控制策略。 -- 透传消息的送达受 Android 系统和应用是否驻留在后台影响。 - -下面我们逐一看看如何对接华为、小米、魅族等厂商的推送服务。 - -## 推荐的接入方式 - -混合推送本质上还是依赖于各厂商的 SDK 和服务端能力,我们的客户端 SDK 只是对厂商 SDK 的包装,而实际的推送请求也是通过 LeanCloud 中转之后发送到厂商后台。因为是一对多的关系,我们的客户端 SDK 更新速度可能跟不上厂商的迭代速度,因此建议大家直接对接厂商 SDK,然后在客户端把厂商分配的「注册 id」与厂商标识(见上一章 vendor 的说明)保存到设备信息(`Installation`)中,这样之后一样可以通过我们的推送 API 来给所有设备正确发送推送信息。 - -### 客户端接入方法 - -不同厂商获取「注册 id」的流程和接口会有不同,可以参考厂商平台的开发指南,这里我们说一下集成厂商 SDK 获取到「注册 id」之后如何按照规范来保存设备信息。 - -#### 华为推送(HMS) - -开发者从 `HmsMessageService` 继承自己的实现类,然后在 `onNewToken(String token)` 回调函数中调用如下代码进行保存: - -``` - public static void updateInstallation(String hwToken) { - if (StringUtil.isEmpty(hwToken)) { - return; - } - LCInstallation installation = LCInstallation.getCurrentInstallation(); - if (!VENDOR.equals(installation.getString(LCInstallation.VENDOR))) { - installation.put(LCInstallation.VENDOR, "HMS"); - } - if (!hwToken.equals(installation.getString(LCInstallation.REGISTRATION_ID))) { - installation.put(LCInstallation.REGISTRATION_ID, hwToken); - } - installation.saveInBackground().subscribe(ObserverBuilder.buildSingleObserver(new SaveCallback() { - @Override - public void done(LCException e) { - if (null != e) { - LOGGER.e("update installation error!", e); - } else { - LOGGER.d("Huawei push registration successful!"); - } - } - })); - } -``` - -示例代码可以参考[LCHMSMessageService](https://github.com/leancloud/java-unified-sdk/blob/master/android-sdk/mixpush-hms/src/main/java/cn/leancloud/LCHMSMessageService.java#L61)。 - -#### 小米推送 - -开发者从 `PushMessageReceiver` 继承自己的实现类,然后在 `onReceiveRegisterResult` 回调函数中调用如上例代码进行保存(记得将 `vendor` 换成 `mi`)。示例代码可以参考[LCMiPushMessageReceiver](https://github.com/leancloud/java-unified-sdk/blob/master/android-sdk/mixpush-xiaomi/src/main/java/cn/leancloud/LCMiPushMessageReceiver.java#L119)。 - -#### OPPO 推送 - -开发者从 `ICallBackResultService` 继承自己的实现类,然后在 `onRegister` 回调函数中调用如上例代码进行保存(记得将 `vendor` 换成 `oppo`)。示例代码可以参考[LCOPPOPushAdapter](https://github.com/leancloud/java-unified-sdk/blob/master/android-sdk/mixpush-oppo/src/main/java/cn/leancloud/LCOPPOPushAdapter.java#L45)。 - -#### vivo 推送 - -开发者从 `OpenClientPushMessageReceiver` 继承自己的实现类,然后在 `onReceiveRegId` 回调函数中调用如上例代码进行保存(记得将 `vendor` 换成 `vivo`)。示例代码可以参考[LCVIVOPushMessageReceiver](https://github.com/leancloud/java-unified-sdk/blob/master/android-sdk/mixpush-vivo/src/main/java/cn/leancloud/LCVIVOPushMessageReceiver.java#L39)。 - -#### 魅族推送 - -开发者从 `MzPushMessageReceiver` 继承自己的实现类,然后在 `onRegisterStatus` 回调函数中调用如上例代码进行保存(记得将 `vendor` 换成 `mz`)。示例代码可以参考[LCFlymePushMessageReceiver](https://github.com/leancloud/java-unified-sdk/blob/master/android-sdk/mixpush-meizu/src/main/java/cn/leancloud/LCFlymePushMessageReceiver.java#L101)。 - -#### 荣耀推送 - -开发者从 `HonorMessageService` 继承自己的实现类,然后在 `onNewToken` 回调函数中调用如上例代码进行保存(记得将 `vendor` 换成 `honor`)。示例代码可以参考[LCHonorMessageService](https://github.com/leancloud/java-unified-sdk/blob/master/android-sdk/mixpush-honor/src/main/java/cn/leancloud/LCHonorMessageService.java#L20)。 - - -### 发送混合推送的服务端 API - -可以参考这里的说明来发送推送请求:[推送 REST API 使用指南](/sdk/push/guide/rest/)。 - -如果开发者要集成我们封装的混合推送 SDK,可以继续往下阅读,如果自行接入厂商 SDK,则可以忽略下文。 - -## 混合推送 library 的构成 - -我们提供了一个 all-in-one 的混合推送模块,统一支持华为(HMS)、小米、OPPO、vivo、魅族、荣耀推送,开发者依赖如下: -cn.leancloud:mixpush-android:{sdkVersions.leancloud.java}@aar - -从 6.5.1 版本开始,我们额外提供了单一厂商的推送 library,以支持不希望全部集成的产品之需求,新 library 与厂商的对应关系如下: - -
      -
    • 华为(HMS) cn.leancloud:mixpush-hms:{sdkVersions.leancloud.java}
    • -
    • 小米 cn.leancloud:mixpush-xiaomi:{sdkVersions.leancloud.java}
    • -
    • 魅族 cn.leancloud:mixpush-meizu:{sdkVersions.leancloud.java}
    • -
    • OPPO cn.leancloud:mixpush-oppo:{sdkVersions.leancloud.java}
    • -
    • vivo cn.leancloud:mixpush-vivo:{sdkVersions.leancloud.java}
    • -
    • 荣耀 cn.leancloud:mixpush-honor:{sdkVersions.leancloud.java}
    • -
    - -两组 library 的使用方法基本相同,开发者可以根据自己的需要选取合适的 library。有一点需要注意的是,在 6.5.1 及后续版本的 library 中,由于小米、OPPO、vivo 、荣耀并没有将他们的 SDK 包发布到公开源供开发者引用,所以如果是使用这几个厂商的推送,需要开发者将厂商的 SDK 包手动加入工程中。 - -### 混合推送 SDK 的获取方法 - -我们已经把所有的 library 推送到了 maven 中央仓库,开发者可以直接在 build.gradle 中进行引用(maven 依赖方式)。 - -但是对于华为和荣耀推送(从 **8.2.16** 版本开始支持),因为厂商是将他们的基础 sdk 发布到私有仓库,所以我们在使用 `mixpush-android`(all in one 版本) 或者 `mixpush-hms`(华为推送)/`mixpush-honor`(荣耀推送)时,需要在项目级 build.gradle 文件的 `allprojects/repositories` 和 `buildscript/repositories` 中增加仓库地址(分别对应不同的推送目标): - -``` -maven {url 'http://developer.huawei.com/repo/'} -maven {url 'https://developer.hihonor.com/repo/'} -``` - -开发者可以参考我们的 [all-in-one demo](https://github.com/leancloud/mixpush-demos/tree/master/allin1) 来进行集中配置。 - -## 华为推送-HMS 版本 - -### 环境配置 - -1. **注册华为账号**:在 [华为开发者联盟](https://developer.huawei.com/consumer/cn/) 注册华为开发者账号。 -2. **开发前准备**:接入华为 PUSH 之前,需要创建应用并配置应用签名,具体可参考华为官方文档:[开发准备](https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/android-config-agc-0000001050170137)。 -3. **打开推送服务开关**:登录华为开发者联盟,按照开发准备一文中的提示开通推送服务。 -4. **将华为 App 信息保存到 开发者中心 控制台**:将上面创建的华为 App 信息(主要有 AppId 和 AppSecret),通过 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > 混合推送** 与应用关联。 - -### 接入 SDK - -#### 获取 HMS SDK - -当前版本 SDK 依赖华为 PushKit V5 版本,开发者可以参考[华为官方文档](https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/android-integrating-sdk-0000001050040084)完成 HMS SDK 的接入。 -其主要步骤有: - -- 在 AndroidStudio 开发环境中添加当前应用的 AppGallery Connect 配置文件,如下图所示: - - ![image](/img/hms_appgallery_connect.png) - -- 配置 HMS SDK 的 maven 仓库地址。 - - - 在项目级 build.gradle 文件的 `allprojects/repositories` 和 `buildscript/repositories` 中增加仓库地址: - - ``` - maven {url 'http://developer.huawei.com/repo/'} - ``` - -- 在项目级 `build.gradle` 的 `buildscript/dependencies` 里面增加配置: - - ```groovy - dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' - classpath 'com.huawei.agconnect:agcp:1.6.0.300' - } - ``` - -- 添加编译依赖。 - - - 在应用级的 `build.gradle` 头部增加如下配置: - - ```groovy - apply plugin: 'com.huawei.agconnect' - ``` - - 如下图所示: - - ![applyImage](/img/hms_build_plugin_agconnect.png) - - - 在应用级的 `build.gradle` 中增加如下编译依赖: - - ```groovy - dependencies { - //其他已存在的依赖不要删除 - implementation 'com.huawei.hms:push:6.11.0.300' - } - ``` - -- 在 android 中配置签名 - - 将生成签名证书指纹步骤中生成的签名文件拷贝到工程的 app 目录下,在 build.gradle 文件中配置签名: - - ```groovy - android { - signingConfigs { - config { - keyAlias 'pushdemo' - keyPassword '123456789' - storeFile file('demo.keystore') - storePassword '123456789' - } - } - - buildTypes { - debug { - signingConfig signingConfigs.config - } - release { - signingConfig signingConfigs.config - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - } - ``` - -做完这些修改后,Android Studio 右上方出现 `Sync Now` 链接。点击 `Sync Now` 等待同步完成。 - -#### 修改应用 manifest 配置 - -首先导入 `mixpush-hms` 包,修改 `build.gradle` 文件,在 `dependencies` 中添加依赖: - - -{`dependencies { - //混合推送需要的包 - implementation 'cn.leancloud:mixpush-hms:${sdkVersions.leancloud.java}' - //即时通信与推送需要的包 - implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'\n - implementation 'com.huawei.hms:push:6.11.0.300' -}`} - - -> 如果希望一次性接入所有厂商推送,可以将 `mixpush-hms` 替换为 `mixpush-android`。 - -然后配置相关 AndroidManifest,添加 Permission(开发者要将其中的 `<包名>` 替换为自己的应用的 package): - -```xml - - - - -``` - -集成最新的 HMS Core Push SDK 版本后要在 AndroidManifest.xml 文件的 application 节点下参照以下步骤注册 Service,用于接收华为推送的消息与令牌。 - -```xml - - - - - -``` - -### 具体使用 - -1. 在 application 的 `onCreate` 方法中调用 `LeanCloud.initialize` 完成初始化之后,进行混合推送 library 的初始化: - - - 使用 `mixpush-android` 的开发者,调用 `cn.leancloud.LCMixPushManager.registerHMSPush(context, profile)` 完成 HMS 推送的初始化。 - - 使用 `mixpush-hms` 的开发者,调用 `cn.leancloud.hms.LCMixPushManager.registerHMSPush(context, profile)` 完成 HMS 推送的初始化。 - - 这里参数 `profile` 的用法可以参考[推送 REST API 使用指南](/sdk/push/guide/rest/)的《Android 混合推送多配置区分》一节。 - -2. 务必在应用启动的首个 activity 的 `onCreate` 方法中调用 `LCMixPushManager.connectHMS(activity)` ,确保 HMS SDK 连接成功。如果开发者不通过 AppGallery Connect 配置文件来集成,我们也提供了 `LCMixPushManager.connectHMS(activity, huaweiAppId)` 来显式指定华为应用 id 完成连接。 - -云端只有在**满足以下全部条件**的情况下才会使用华为推送: - - - EMUI 系统 - - 在华为后台正确配置应用签名 - - manifest 正确填写 - -检测华为推送是否集成成功,可以检查 Installation 表中该设备对应的记录是否增加一个 vendor 字段,vendor 字段值为 HMS 表示设备成功注册为华为 HMS 推送。 - -### 提升透传消息到达率 - -透传消息是由客户端应用负责处理的消息。终端设备收到透传消息后不直接展示,而是将数据传递给应用,由应用自主解析内容,并触发相关动作(如跳转网页、应用内页面等等)。透传消息的常用场景包括好友邀请、VoIP 呼叫、语音播报等。 -按照华为官方说明,透传消息的到达率受 Android 系统和应用是否驻留在后台影响,推送服务不保证透传消息的高到达率,并且会让应用层处理变得复杂,所以还是推荐大家使用普通的通知栏消息。 - -> 当使用华为推送发透传消息时,如果目标设备上应用进程被杀,会出现推送消息无法接收的情况。这个是华为 ROM 对透传消息广播的限制导致的,需要引导用户在华为「权限设置」中对应用开启自启动权限来避免。 -### 应用在前台时自己处理通知栏消息 - -华为手机可以支持这一需求,但需要在调用 REST API 发送消息时,指定 `message.android.notification.foreground_show` 值为 `false`,同时客户端 Manifest 中声明 HmsMessageService 子类,并声明 `queries` 节点(针对 Android 11 以上系统)。 - -REST API 请求示例: - -```json -{ - "hms": { - "message": { - "notification": { - "title": "message title", - "body": "message body" - }, - "android": { - "notification": { - "foreground_show": false, - "click_action": { - "type": 1, - "action": "com.huawei.codelabpush.intent.action.test" - } - } - } - } - } -} -``` - -manifest 配置示例: - -```xml - - ... - - - - - - - - ... - - - - - - ... - -``` - -### 使用特定 activity 响应推送消息 - -华为推送消息,在用户点击了通知栏信息之后,默认是打开应用,用户也可以指定特定的 activity 来响应推送启动事件。 - -打开应用自定义页面有两种方式,一种是通过服务端 API 指定 intent 参数,另一种是指定 action 参数,我们混合推送 SDK 选择使用 intent 参数(具体可参考华为文档[服务端发送 push 消息](https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/android-server-dev-0000001050040110)),开发者也可以在发送请求时自行指定 action 参数(按照华为的 REST API 规范指定即可)。 - -对于目标 activity,开发者需要在 manifest 文件的 application 中增加 intent-filter 的定义,例如: - -```xml - - - - - - - - -``` - -在目标 activity 的 `onCreate` 函数中可以从 intent extra data 中通过 `content` key 可以获得推送内容(JSON 格式,包含 push 消息中所有自定义属性)。 -可以参考我们的示例:[PushTargetActivity](https://github.com/leancloud/mixpush-demos/blob/master/huawei/app/src/main/java/cn/leancloud/demo/hmspush/PushTargetActivity.java))。 - -一般情况下,这里 intent-filter 的内容都不需要修改。 -如果同一开发者有多个应用都使用了我们的 HMS 混合推送,或者终端用户安装了多个使用我们的 HMS 混合推送的应用,那么在同一个终端上,推送消息在通知栏被点击之后,因为多个应用都响应同样的 intent-filter,所以会出现要选择应用来打开的情况。 -这可以通过在 intent-filter 中配置不一样的 `android:host` 解决。 - -在云服务控制台,增加华为 HMS 推送配置的时候,开发者可以指定自己的 Android Intent Hostname(不指定就使用默认值 `cn.leancloud.push`),然后在这里的 intent-filter 中填上**同样**的值,客户端就可以区分不同应用的通知栏消息了。 - -云端在调用 HMS 推送接口的时候,会把开发者自定义的属性,使用固定的 intentUri pattern 来封装成 `intent` 数据,其中 `intentUri` 的固定格式为: - -``` -intent://HOST/notify_detail#Intent;scheme=lcpushscheme;S.content=XXXX;launchFlags=0x10000000;end -``` - -其中 `HOST` 就是上面配置的 Android Intent Hostname,默认值为 `cn.leancloud.push`;`XXX` 就是开发者自定义参数的 JSON 字符串做了 URL Encode 之后的值,只有这部分内容是开发者可以指定的。 - -云端发送这种推送的例子如下: - -```sh -curl -X POST \ - -H "X-LC-Id: {your app id}" \ - -H "X-LC-Key: {your app key}" \ - -H "Content-Type: application/json" \ - -d '{ - "where": {"channels" : "public"} - "data": {"alert" : "消息内容", - "title": "显示在通知栏的标题", - "k1" : "v1", - "k2" : "v2"} - }' \ - https://{{host}}/1.1/push -``` - -云端最终发送给 HMS Server 的请求中 payload 字段为(其中 `{"k1" : "v1","k2" : "v2"}` 这个 JSON 串经过了 URLEncode 处理): - -```json -{ - "hms": { - "msg": { - "type": 3, - "body": { - "title": "显示在通知栏的标题", - "content": "消息内容" - }, - "action": { - "type": 1, - "param": { - "intent": "intent://cn.leancloud.push/notify_detail?S.content=%7B%22k1%22%3A%22v1%22%2C%22k2%22%3A%22v2%22%7D#Intent;scheme=lcpushscheme;launchFlags=0x10000000;end" - } - } - } - } -} -``` - -到目前为止,我们只支持一种 intentUri 格式,所以所有的推送请求都会被同一个 activity 响应。如果开发者需要最终显示不同的页面,可以由这个接收 activity 进行一次转发。 - -### 华为推送自定义 Receiver - -如果你想推送消息,但不显示在 Android 系统的通知栏中,而是执行应用程序预定义的逻辑,可以自定义 Receiver。 -华为混合推送自定义 Receiver 需要继承 LCHMSMessageService,在收到透传消息的回调方法 `onMessageReceived` 获取推送消息数据。 -你的 Receiver 可以按照如下方式实现: - -```java -public class MyHuaweiReceiver extends LCHMSMessageService { - @Override - public boolean onMessageReceived(RemoteMessage remoteMessage) { - try { - String message = remoteMessage.getData(); - String content = "--- 收到推送消息:--- " + new String(message, "UTF-8"); - System.out.println("TAG:" + content); - } catch (Exception e) { - e.printStackTrace(); - } - return false; - } -} -``` - -`AndroidManifest.xml` 中把 LCHMSMessageService 替换为你自定义的 MyHuaweiReceiver。 - -```xml - - - - - -``` - -修改 HMS 推送注册函数。特别注意一点,使用自定义 Receiver 的时候,需要调用 `LCMixPushManager.registerHMSPush(context, profile, receiverClazz)` 或者 `LCMixPushManager.registerHMSPush(context, receiverClazz)` 来完成 HMS 推送的初始化,否则会导致 `LCMixPushManager.registerHMSPush` 调用失败。 - -推送的内容示例如下: - -```json -{ - "alert": "消息内容", - "title": "显示在通知栏的标题", - "custom-key": "由用户添加的自定义属性,custom-key 仅是举例,可随意替换", - "silent": true //silent 属性,是透传消息与通知栏消息的标志 -} -``` - -### 华为角标适配说明 -#### 使用限制 -华为手机角标展示支持 EMUI 8.0 及以上手机。 -受限于华为手机角标能力的开放程度,在不同的推送场景下角标功能有所不同,详见下表。 - -推送形式 | 角标能力 | 实现方式 ---- | --- | --- -通知栏消息 | 支持角标自动加 1、直接设置或不变,支持通知点击的自动减 1,不支持通知清除的自动减 1 | 通过管理台或 Push API 关键字设置 -透传消息 | 开发者自行处理设置、加减逻辑 | 调用 HMS SDK 开放接口 - -#### 配置说明 -##### 应用内角标设置权限申请 -为能实现角标修改的正确效果,请首先为应用添加华为手机上的角标读写权限,具体实现为在应用 AndroidManifest.xml 文件的 manifest 标签下添加以下权限配置: - -```xml - -``` - -##### 华为手机终端设置角标自增减 -华为手机支持角标自动增减 1(负数为减,正数为增),需要在客户端通过代码实现,示例如下: - -```java -Bundle extra = new Bundle(); -extra.putString("package", "xxxxxx"); -extra.putString("class", "yyyyyyy"); -extra.putInt("badgenumber", i); -context.getContentResolver().call(Uri.parse("content://com.huawei.android.launcher.settings/badge/"), "change_badge", null, extra); -``` - -### 推送消息智能分类 - -华为推送服务会将推送消息自动分为服务与通讯、资讯营销两类,不同类别的提醒方式有所差异,因此也会影响混合推送内容中一些属性的效果。 -例如,服务与通讯分类默认使用系统铃声,资讯营销分类默认无铃声,这一设定只能由用户在手机的通知中心自行修改,`sound` 属性中指定的自定义铃声无效。 -如果想要使用自定义铃声,需要创建 Channel,在创建时设置相应的铃声,这样指定 Channel 的推送就可以使用专门的铃声了。 -不过,某些情况下,华为推送服务仍会将自定义 Channel 的推送归类为服务与通讯或资讯营销。 - -请参考[华为的开发者文档][huawei-classification]了解服务与通讯、资讯营销两个分类的具体差异。 - -[huawei-classification]: https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/message-classification-management-solution-0000001149358835 -### 参考 demo - -我们提供了一个 [最新的华为推送 demo](https://github.com/leancloud/mixpush-demos/tree/master/huawei),可供你在接入过程中参考。 - -### 华为推送测试 - -华为推送在每日单设备推送数量上有一些限制,详细信息可以见[华为官方文档](https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/message-restriction-description-0000001361648361#section104849311415);一般来说,资讯营销类消息的每日单设备的推送数量为 2 条。 - -所以在单设备测试华为推送时,需要添加一个参数用来标识此次推送是测试推送,测试推送不受每日单设备推送数量上限要求,详细信息可见[华为官方文档的 target_user_type 参数](https://developer.huawei.com/consumer/cn/doc/HMSCore-References/https-send-api-0000001050986197);当设置 `target_user_type` 参数为 1 时,即表明此次推送为测试推送。 - -华为测试推送也存在一些限制,如下: -* 每个应用每日可发送该测试消息 500 条 -* 单次推送不能超过 10 台设备 - -可以在推送的 data 中加入 `target_user_type` 参数,样例如下: -```json -{ - "alert" : "Hello", - "hms" : { - "target_user_type" : 1 - } -} -``` - -**注意:请不要在正式推送中使用 `target_user_type` 参数。** - -## 小米推送 - -### 环境配置 - -1. **注册小米账号**:在 [小米开放平台][xiaomi] 上注册小米开发者账号并完成实名认证([详细流程](https://dev.mi.com/console/doc/detail?pId=848))。 -2. **创建小米推送服务应用**([详细流程](https://dev.mi.com/console/doc/detail?pId=68))。 -3. **设置小米的 AppId 及 AppSecret**:在 [小米开放平台][xiaomi] > **管理控制台** > **消息推送** > **相关应用** 可以查到具体的小米推送服务应用的 AppId 及 AppSecret。将此 AppId 及 AppSecret 通过 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > 混合推送** 与云服务应用关联。 - -[xiaomi]: https://dev.mi.com/console/ - -### 接入 SDK - -我们混合推送基于小米 5.9.9 版本 SDK 进行开发。开发者需要首先导入 `mixpush-android` 包:修改 `build.gradle` 文件,在 **dependencies** 中添加依赖: - - -{`dependencies { - //混合推送需要的包 - implementation fileTree(dir: 'libs', include: ['*.aar']) // 需将 MiPush_SDK_Client_5_9_9-C_3rd.aar 放入应用的 libs 目录下 - implementation 'cn.leancloud:mixpush-android:${sdkVersions.leancloud.java}' - //即时通信与推送需要的包 - implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' -}`} - - -如果只希望接入小米推送,可以将 `mixpush-android` 替换为 `mixpush-xiaomi`。 -如果是通过 aar 包导入,则需要手动下载 aar 包 [小米 Push SDK](http://dev.xiaomi.com/mipush/downpage/)。 - -然后配置相关 AndroidManifest。添加 Permission: - -```xml - - - - - - -``` - -添加 service 与 receiver。开发者要将其中的 `<包名>` 替换为自己的应用对应的 package: - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### 具体使用 - -在 `LeanCloud.initialize` 之后调用以下函数进行混合推送 library 的初始化: - -- 使用 `mixpush-android` 的开发者,调用 `cn.leancloud.LCMixPushManager.registerXiaomiPush(context, miAppId, miAppKey, profile)` 。 -- 使用 `mixpush-xiaomi` 的开发者,调用 `cn.leancloud.mi.LCMixPushManager.registerXiaomiPush(context, miAppId, miAppKey, profile)` 。 - -这里: - -- 参数 `miAppKey` 需要的是 AppKey,而在控制台的混合推送配置中 Profile 的第二个参数是 AppSecret,请注意区分,并分别正确填写。 -- 参数 `profile` 的用法可以参考 [推送 REST API 使用指南](/sdk/push/guide/rest/)的《Android 混合推送多配置区分》一节。 - -云端只有在**满足以下全部条件**的情况下才会使用小米推送: - -- MIUI 系统 -- manifest 正确填写 -- appId、appKey、appSecret 有效 - -### 小米推送通知栏消息的点击事件 - -当小米通知栏消息被点击后,如果已经设置了自定义 Receiver,则 SDK 会发送一个 action 为 `com.avos.avoscloud.mi_notification_action` 的 broadcast。如有需要,开发者可以通过订阅此消息获取点击事件,否则 SDK 会默认打开启动推送服务时设置的 Activity。 - - - -### 其它 - -如果遇到报错「应用在黑名单中,禁止发送消息」,则原因可能是没在小米应用商店上架 APP。详细信息可以参见小米推送服务在 2022 年更新的[小米推送技术服务协议](https://dev.mi.com/console/doc/detail?pId=860)。 - -## 魅族推送 - -### 环境配置 - -1. **注册魅族账号**:在 [Flyme开放平台](https://open.flyme.cn) 上注册魅族开发者账号并完成开发者认证([详细流程](http://open-wiki.flyme.cn/doc-wiki/index#id?8))。 -2. **创建魅族推送服务应用**([详细流程](http://open.res.flyme.cn/fileserver/upload/file/202102/d2e53035310b407cb3f21f1b0433202d.pdf))。 -3. **设置魅族的 AppId 及 AppSecret**:在 [魅族推送平台](http://push.meizu.com/) > **应用列表** > **打开应用** > **配置管理** 可以查到具体的魅族推送服务应用的 AppId 及 AppSecret。将此 AppId 及 AppSecret 通过 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > 混合推送**,与云服务应用关联。 - -### 接入 SDK - -首先导入 `mixpush-android` 包。修改 `build.gradle` 文件,在 **dependencies** 中添加依赖: - - -{`dependencies { - //魅族推送需要的包 - implementation 'com.meizu.flyme.internet:push-internal:4.3.0' - //混合推送需要的包 - implementation 'cn.leancloud:mixpush-android:${sdkVersions.leancloud.java}' - //即时通信与推送需要的包 - implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' -}`} - - -如果只希望接入魅族推送,可以将 `mixpush-android` 替换为 `mixpush-meizu`。 - -如果是通过 jar 包导入,则需要手动下载 jar 包 [魅族 Push SDK](https://github.com/MEIZUPUSH/PushDemo-Eclipse/releases)。 - -然后配置相关 AndroidManifest。添加 Permission: - -```xml - - - - - - - - - -``` - -添加 service 与 receiver。开发者要将其中的 `<包名>` 替换为自己的应用对应的 package: - -```xml - - - - - - - - - - - - - - -``` - -### 具体使用 - -在 `LeanCloud.initialize` 之后调用以下函数进行混合推送 library 的初始化: - -- 使用 `mixpush-android` 的开发者,调用 `cn.leancloud.LCMixPushManager.registerFlymePush(context, flymeId, flymeKey, profile)` 。 -- 使用 `mixpush-meizu` 的开发者,调用 `cn.leancloud.flyme.LCMixPushManager.registerFlymePush(context, flymeId, flymeKey, profile)` 。 - -这里参数 `profile` 的用法可以参考[推送 REST API 使用指南](/sdk/push/guide/rest/)的《Android 混合推送多配置区分》一节。 - -注意,云端只有在以下三个条件都满足的情况下,才会使用魅族推送。 - -- Flyme 系统 -- manifest 正确填写 -- flymeId、flymeKey 有效 - -#### 魅族推送通知栏消息的点击事件 - -当魅族通知栏消息被点击后,如果已经设置了自定义 Receiver,则 SDK 会发送一个 action 为 `com.avos.avoscloud.flyme_notification_action` 的 broadcast。如有需要,开发者可以通过订阅此消息获取点击事件,否则 SDK 会默认打开启动推送服务时对应设置的 Activity。 - -### 魅族推送测试 - -测试魅族推送时,需提前将设备加入到魅族推送后台的「常用设备」列表中。登录魅族开发者账号,进入推送服务后台管理,然后进入「配置管理」页面,最后进入「常用设备」页面,添加常用设备。 - -## vivo 推送 - -vivo 混合推送 demo:可参照 [这里](https://github.com/leancloud/mixpush-demos/tree/master/vivo)。 -### 环境配置 - -要使用 vivo 官方推送服务,需要在 [vivo 开发者平台](https://dev.vivo.com.cn/home)注册一个账号,并创建好应用。 -这里假设大家已经完成上述操作,创建好了应用,并获取了 `appId`、`appKey`、`appSecret`(请保存好这几个值,下一步接入的时候会用到。) - -### 接入 SDK - -当前版本的 SDK 是基于 vivo 官方文档 [push SDK 接入文档](https://dev.vivo.com.cn/documentCenter/doc/365) 封装而来,使用的 vivo push SDK 基线版本是 `3.0.0.7`。我们会结合 demo([源码](https://github.com/leancloud/mixpush-demos/tree/master/vivo/))来解释整个接入流程。 - -首先将 demo 工程 `app/libs` 目录下的所有 jar 包(如有)拷贝到目标工程的 libs 目录下,然后修改 `build.gradle` 文件,在 `dependencies` 中添加依赖: - - -{`dependencies { - //混合推送需要的包 - implementation files("libs/vivo_pushsdk-v3.0.0.7_488.aar") // 将 vivo_pushsdk-v3.0.0.7_488.aar 置于应用 libs 目录下 - implementation 'cn.leancloud:mixpush-vivo:${sdkVersions.leancloud.java}' - //即时通信与推送需要的包 - implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' -}`} - - -接下来配置 AndroidManifest,添加权限声明: - -```xml - - - -``` - -最后在 AndroidManifest 中添加必须的 service 与 receiver(开发者要将其中的 `com.vivo.push.app_id` 和 `com.vivo.push.app_key` 替换为自己的应用的信息): - -```xml - - - - - - - - - - - - - - - - -``` - -接下来我们看看代码上要怎么做。 - -#### 初始化 - -与其他推送的初始化方法一样,我们在 `Application#onCreate` 方法中进行 vivo 推送的初始化: - -```java -import cn.leancloud.LeanCloud; -import cn.leancloud.LCMixPushManager; // 使用 mixpush-android 的场合 -//import cn.leancloud.vivo.LCMixPushManager; // 使用 mixpush-vivo 的场合 - -public class MyApp extends Application { - // 请替换成你自己的 appId 和 appKey - private static final String LC_APP_ID = "xxx"; - private static final String LC_APP_KEY = "xxx"; - - @Override - public void onCreate() { - super.onCreate(); - - //开启调试日志 - LeanCloud.setLogLevel(LCLogger.Level.DEBUG); - - // LeanCloud SDK 初始化 - LeanCloud.initialize(this, "{{appid}}", "{{appkey}}", "https://please-replace-with-your-customized.domain.com"); - - // vivo 推送初始化 - // 使用 mixpush-android 的场合,引用 cn.leancloud.LCMixPushManager - // 使用 mixpush-vivo 的场合,引用 cn.leancloud.vivo.LCMixPushManager - // 注意这里第二个参数,表示用户是否同意了应用的隐私声明。true:用户同意了隐私声明,默认同意;false:未同意隐私声明。 - LCMixPushManager.registerVIVOPush(this, true); - - LCMixPushManager.turnOnVIVOPush(new LCCallback() { - @Override - protected void internalDone0(Boolean aBoolean, LCException e) { - if (null != e) { - System.out.println("failed to turn on vivo push. cause:"); - e.printStackTrace(); - } else { - System.out.println("succeed to turn on vivo push."); - } - } - }); - } -} -``` - -开发者也可以在 `onCreate` 方法中调用 LCMixPushManager 的其他方法,以使用 vivo 推送的全部客户端功能: - -```java -public class LCMixPushManager { - // 判断当前设备是否支持 vivo 推送 - public static boolean isSupportVIVOPush(Context context); - - // 关闭 vivo 推送 - public static void turnOffVIVOPush(final LCCallback callback); - - public static void bindVIVOAlias(Context context, String alias, final LCCallback callback); - - public static void unbindVIVOAlias(Context context, String alias, final LCCallback callback); - - public static String getVIVOAlias(Context context); - - public static void setVIVOTopic(Context context, String topic, final LCCallback callback); - - public static void delVIVOTopic(Context context, String topic, final LCCallback callback); - - public static List getVIVOTopics(Context context); -} -``` - - - -#### 添加 vivo 推送配置 - -在**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置** 页面开启混合推送服务,并且在「vivo 推送配置」一节设置好准备阶段申请好的 `vivo appId`、`vivo appKey`、`vivo Secret`,就可以了。 - - - -#### 响应通知栏消息的点击事件 - -与其他厂商的混合推送机制一样,vivo 混合推送也是通过系统通道来下发消息,开发者调用 push API 发送消息时,其流程为: - -- 用户服务器向云推送服务器发送推送请求; -- 云推送服务器向 vivo 服务器 转发请求; -- vivo 服务器通过系统长链接下发推送通知到手机端; -- 手机端操作系统将消息展示在通知栏; -- 用户点击通知栏消息。 - -新版的 vivo 推送已经不再通过 SDK 来响应通知栏的点击回调,而是在消息中直接指定了响应的 Activity 与附带数据,这要求我们在发送推送请求的时候就按照 vivo 的规则组织好参数,同时客户端 AndroidManifest 里需要声明好响应 Activity 支持的 filter,例如: - -```xml - - - - - - - - -``` - -具体的客户端和 REST API 请求示例,可以参看我们的 demo。 - -### vivo 手机角标适配说明 -#### 使用限制 -vivo 的通知栏消息,并不支持角标显示,而透传消息(官方已建议不再继续使用),则可以由开发者自己处理设置逻辑。 -vivo 桌面图标角标默认是关闭的,开发者接入完成后还需要终端用户手动开启,开启完成后收到新消息时,在已安装的应用桌面图标右上角显示「数字角标」。终端用户开启角标设置的路径是:「设置 > 通知与状态栏 > 应用通知管理 > 应用名称 > 桌面图标角标」。 - -> 视 OS 版本差异,「桌面图标角标」名称可能为「应用图标标记」或「桌面角标」。 - -#### 配置说明 - -- 添加权限 - - 为能实现角标修改的正确效果,请首先为应用添加 vivo 手机上的角标读写权限,具体实现为在应用 AndroidManifest.xml 文件的 manifest 标签下添加以下权限配置: - - ```xml - - ``` - -- 应用在需要显示桌面角标的场景,通过广播将信息发送给 vivoLauncher: - - 广播参数: - - - `action`:`launcher.action.CHANGE_APPLICATION_NOTIFICATION_NUM` - - `packageName`:应用包名 - - `className`:主类名 - - `notificationNum`:未读消息数目 - - 简单示例: - - ```java - Intent intent = new Intent(); - int missedCalls = 10; - intent.setAction("launcher.action.CHANGE_APPLICATION_NOTIFICATION_NUM"); - intent.putExtra("packageName", "com.android.xxxx"); - intent.putExtra("className", "com.android.xxxx.Mainxxxx"); - intent.putExtra("notificationNum", missedCalls); - intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); - sendBroadcast(intent); - ``` - - > 注意: 在 8.0 系统上,还需要给 Intent 加上下面的 flag:`Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND` - -## OPPO 推送 - -混合推送 OPPO 模块基于 oppo Push SDK v3.4.0 版本,支持 Android 4.4 或以上版本的手机系统,服务支持信息如下: - -- 支持平台:ColorOS 3.1 及以上的系统的 OPPO 机型,一加 5/5t 及以上机型,realme 所有机型。 -- 通知消息类型:只支持通知栏消息的推送。消息下发到 OS 系统模块并由系统通知模块展示,在用户点击通知前,不启动应用。具体限制可参考 [OPPO 官方文档](https://open.oppomobile.com/wiki/doc#id=10743)。 - -在接入时,开发者可以参考我们的 [demo](https://github.com/leancloud/mixpush-demos/tree/master/oppo)。 - -### 环境配置 - -在开始接入之前,有两项准备工作: - -- 在 [OPPO 开放平台](https://open.oppomobile.com/)注册一个账号,并创建好应用。 -- 从 OPPO 官网下载 aar 资源。OPPO 官方 aar 下载地址为:。 - -这里假设大家已经完成上述操作,创建好了应用,并获取了 `appKey`、`appSecret`、`masterSecret`,请保存好这三个值,下一步接入的时候会用到: - -- SDK 初始化需要使用 `appKey` 和 `appSecret` 。 -- 服务端设置需要使用 `appKey`和 `masterSecret`。 - -### 接入 SDK - -#### 下载 SDk - -- 将之前下载的 SDK(`com.heytap.msp_3.4.0.aar`)复制到工程 libs/ 目录下,然后修改 `build.gradle` 文件,在 `dependencies` 中添加依赖: - - -{`dependencies { - //混合推送需要的包 - implementation fileTree(dir: 'libs', include: ['*.aar']) - implementation 'cn.leancloud:mixpush-oppo:${sdkVersions.leancloud.java}' - //即时通信与推送需要的包 - implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - implementation 'commons-codec:commons-codec:1.6' -}`} - - -#### 配置 AndroidManifest.xml - -注意:OPPO 推送服务SDK 3.4.0 版本支持的最低安卓版本为 Android 4.4 系统(`minSdkVersion="19"`)。 - -- 增加权限列表(如果应用无透传权限,则不用配置) - - ```xml - - - - - - - - ``` - -- 推送服务组件注册(如果应用无透传权限,则不用配置) - - ```xml - - - - - - - - - - - . - ``` - - -接下来我们看看代码上要怎么做。 - -#### 初始化 - -与其他推送的初始化方法一样,我们在 `Application#onCreate` 方法中进行 OPPO 推送的初始化: - -```java -import cn.leancloud.LeanCloud; -import cn.leancloud.LCOPPOPushAdapter; -import cn.leancloud.oppo.LCMixPushManager; // 使用 mixpush-oppo 的场合 -//import cn.leancloud.LCMixPushManager; // 使用 mixpush-android 的场合 - -// Customized Application. -public class MyApp extends Application { - // 请替换成你自己的 appId 和 appKey - private static final String LC_APP_ID = "xxx"; - private static final String LC_APP_KEY = "xxx"; - private String OPPO_APPKEY = "your OPPO app id"; - private String OPPO_APPSECRET = "your OPPO app secret"; - - @Override - public void onCreate() { - super.onCreate(); - - //开启调试日志 - LeanCloud.setLogLevel(LCLogger.Level.DEBUG); - - // LeanCloud SDK 初始化 - LeanCloud.initialize(this, "{{appid}}", "{{appkey}}", "https://please-replace-with-your-customized.domain.com"); - - // OPPO 推送初始化 - // 使用 mixpush-android 的场合,引用 cn.leancloud.LCMixPushManager - // 使用 mixpush-oppo 的场合,引用 cn.leancloud.oppo.LCMixPushManager - LCMixPushManager.registerOppoPush(this, OPPO_APPKEY, OPPO_APPSECRET, new LCOPPOPushAdapter()); - } -} -``` - -开发者也可以在 `onCreate` 方法中调用 LCMixPushManager 的其他方法,以使用 OPPO 推送的全部客户端功能,具体可以参看 LCMixPushManager 的接口文档,或参考[官方文档-详细 API 说明](https://open.oppomobile.com/wiki/doc#id=10688) 来了解具体信息。 - - -#### 添加 OPPO 推送配置 - -在**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置**页面开启混合推送服务,并且在「OPPO 推送配置」一节设置好准备阶段申请好的「OPPO app key」和「OPPO master secret」,就可以了。 - -#### 响应通知栏消息的点击事件 - -与其他厂商的混合推送机制一样,OPPO 混合推送也是通过系统通道来下发消息,开发者调用 push API 发送消息时,其流程为: - -- 用户服务器向云推送服务器发送推送请求; -- 云推送服务器 向 OPPO 服务器 转发请求; -- OPPO 服务器 通过系统长链接下发推送通知到手机端; -- 手机端操作系统将消息展示在通知栏; -- 用户点击通知栏消息。此时 OS 会根据 push 参数执行不同的动作(默认值为 0): - - 0. 启动应用; - 1. 打开应用内页(activity 的 intent action); - 2. 打开网页; - 3. 打开应用内页(activity); - 4. Intent scheme URL - -客户端响应用户点击的过程不需要 SDK 参与,全部都是由系统通过消息里面附带的信息来自行处理。混合推送现在可支持所有动作方式,具体可参考我们的 demo。 - - -## 荣耀推送 - -### 环境配置 - -1. **注册荣耀账号**:在 [荣耀开发者平台](https://developer.hihonor.com/cn/) 注册荣耀开发者账号。 -2. **开发前准备**:接入荣耀 PUSH 之前,需要申请开通推送服务,开通流程可以参考 [推送服务开通流程](https://developer.hihonor.com/cn/kitdoc?category=%E5%9F%BA%E7%A1%80%E6%9C%8D%E5%8A%A1&kitId=11002&navigation=guides&docId=app-registration.md&token=) ,其中配置 SHA256 证书指纹 可以参考 [指纹证书生成](https://developer.hihonor.com/cn/kitdoc?category=%E5%9F%BA%E7%A1%80%E6%9C%8D%E5%8A%A1&kitId=11002&navigation=guides&docId=android-generate-appsign.md&token=)。 -3. 配置消息回执,设置步骤可参考 [消息回执](https://developer.hihonor.com/cn/kitdoc?category=%E5%9F%BA%E7%A1%80%E6%9C%8D%E5%8A%A1&kitId=11002&navigation=guides&docId=cloud-meassage-return.md&token=),回调地址请设置为:**`https://callback.tds1.tapapis.cn/push/v1/callback/honor`**。 -4. **将荣耀 App 信息保存到 开发者中心 控制台**:将上面创建的荣耀 App 信息(主要有 APP ID、APP Secret、Client ID、Client Secret),通过 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > 混合推送** 与应用关联。 - - -### 接入 SDK - -#### 获取荣耀推送 SDK - -开发者可以参考[荣耀推送官方文档](https://developer.hihonor.com/cn/kitdoc?category=%E5%9F%BA%E7%A1%80%E6%9C%8D%E5%8A%A1&kitId=11002&navigation=guides&docId=intergrate.md&token=),完成荣耀推送 SDK 的接入。其主要步骤有: - -- 配置荣耀 SDK 的 maven 仓库地址。在项目级 build.gradle 文件的 `allprojects/repositories` 和 `buildscript/repositories` 中增加仓库地址: - - ``` - maven { url 'https://developer.hihonor.com/repo/' } - ``` - -- 打开应用级的 build.gradle 文件,在 dependencies 中添加如下编译依赖。 - - ```groovy - dependencies { - implementation 'com.hihonor.mcs:push:7.0.41.301' - } - ``` - -- 在 android 中配置签名。将生成签名证书指纹步骤中生成的签名文件拷贝到工程的 app 目录下,在 build.gradle 文件中配置签名: - ```groovy - android { - signingConfigs { - config { - keyAlias 'pushdemo' - keyPassword '123456789' - storeFile file('demo.keystore') - storePassword '123456789' - } - } - - buildTypes { - debug { - signingConfig signingConfigs.config - } - release { - signingConfig signingConfigs.config - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - } - ``` - -做完这些修改后,Android Studio 右上方出现 Sync Now 链接。点击 Sync Now 等待同步完成。 - -#### 修改应用 manifest 配置 - -首先导入 `mixpush-honor` 包,修改 build.gradle 文件,在 dependencies 中添加依赖: - - -{`dependencies { - //混合推送需要的包 - implementation 'cn.leancloud:mixpush-honor:${sdkVersions.leancloud.java}' - //即时通信与推送需要的包 - implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'\n - implementation 'com.hihonor.mcs:push:7.0.41.301' -}`} - - -> 如果希望一次性接入所有厂商推送,可以将 `mixpush-honor` 替换为 `mixpush-android`。 - -然后配置相关 AndroidManifest,添加 Permission(开发者要将其中的 `<包名>` 替换为自己的应用的包名): - -```xml - - - - -``` - -集成最新的荣耀 Push SDK 版本后要在 AndroidManifest.xml 文件的 application 节点下参照以下步骤注册 service,用于接收荣耀推送的消息与令牌。 - -```xml - - - - - - -``` - -配置 AppId 信息:需要同样在 AndroidManifest.xml 文件下,添加 `meta-data` 标签,并在 `com.hihonor.push.app_id` 下添加 AppId(AppId 在注册完成后获取)。 - -```xml - - - - - -``` - -### 初始化 - -与其他推送的初始化方法一样,在 `Application#onCreate` 方法中进行荣耀推送的初始化: - -```java -import cn.leancloud.LeanCloud; -import cn.leancloud.honor.LCMixPushManager; // 使用 mixpush-honor 的场合 -import cn.leancloud.LCMixPushManager; // 使用 mixpush-android 的场合 - -// Customized Application. -public class MyApp extends Application { - // 请替换成你自己的 appId 和 appKey - private static final String LC_APP_ID = "xxx"; - private static final String LC_APP_KEY = "xxx"; - - - @Override - public void onCreate() { - super.onCreate(); - - //开启调试日志 - LeanCloud.setLogLevel(LCLogger.Level.DEBUG); - - // LeanCloud SDK 初始化 - LeanCloud.initialize(this, "{{appid}}", "{{appkey}}", "https://please-replace-with-your-customized.domain.com"); - - // 荣耀推送初始化 - // 使用 mixpush-android 的场合,引用 cn.leancloud.LCMixPushManager - // 使用 mixpush-honor 的场合,引用 cn.leancloud.honor.LCMixPushManager - LCMixPushManager.registerHonorPush(this); - } -} -``` - -### 点击通知打开自定义页面 - -您可以自定义处理点击通知栏消息通知的动作,包括:打开应用首页、打开特定 URL 和打开自定义页面。 - -打开应用自定义页面有两种方式,一种是通过服务端 API 指定 intent 参数,另一种是指定 action 参数,混合推送 SDK 选择使用 intent 参数(具体可参考荣耀文档 [服务端发送 push 消息 ](https://developer.hihonor.com/cn/kitdoc?category=%E5%9F%BA%E7%A1%80%E6%9C%8D%E5%8A%A1&kitId=11002&navigation=ref&docId=downlink-message.md&token=)),开发者也可以在发送请求时自行指定 action 参数(按照荣耀的 REST API 规范指定即可)。 - -对于目标 activity,开发者需要在 manifest 文件的 application 中增加 intent-filter 的定义,例如: - -```xml - - - - - - - - - - - - - - - - - - - -``` - -### 荣耀推送测试 - -荣耀推送在每日单设备推送数量上有一些限制(和华为推送类似)。 - -所以在单设备测试荣耀推送时,需要添加一个参数用来标识此次推送是测试推送,测试推送不受每日单设备推送数量上限要求,详细信息可见[荣耀官方文档的 targetUserType 参数](https://developer.hihonor.com/cn/kitdoc?category=%E5%9F%BA%E7%A1%80%E6%9C%8D%E5%8A%A1&kitId=11002&navigation=ref&docId=downlink-message.md&token=);当设置 `targetUserType` 参数为 1 时,即表明此次推送为测试推送。 - -荣耀测试推送也存在一些限制,如下: -* 每个应用每日可发送该测试消息 1000 条 -* 单次推送不能超过 10 台设备 - -可以在推送的 data 中加入 `targetUserType` 参数,样例如下: -```json -{ - "alert" : "Hello", - "honor" : { - "targetUserType" : 1 - } -} -``` - -**注意:请不要在正式推送中使用 `targetUserType` 参数。** - -## 取消混合推送注册 - -对于已经注册了混合推送的用户,如果想取消混合推送的注册而改走云服务自有的 WebSocket 的话,可以调用如下函数: - -```java -LCMixPushManager.unRegisterMixPush(); -``` - -此函数为异步函数,如果取消成功会有 `Registration canceled successfully` 的日志输出,万一取消注册失败的话会有类似 `unRegisterMixPush error` 的日志输出。 - -## 错误排查建议 - -- 只要注册时有条件不符合,SDK 会在日志中输出导致注册失败的原因,例如 `register error, mainifest is incomplete` 代表 manifest 未正确填写。如果注册成功,`_Installation` 表中的相关记录应该具有 **vendor** 这个字段并且不为空值。 - -- 如果注册一直失败的话,请提交工单或去论坛发帖,提供相关日志、具体机型以及系统版本号,我们会跟进协助来排查。 - -- 查看魅族机型的设置,并打开「信任此应用」、「开机自启动」、「自启动管理」和「权限管理」等相关选项。 diff --git a/leancloud/docs/sdk/push/guide/android.mdx b/leancloud/docs/sdk/push/guide/android.mdx deleted file mode 100644 index 4e5de19f7..000000000 --- a/leancloud/docs/sdk/push/guide/android.mdx +++ /dev/null @@ -1,493 +0,0 @@ ---- -title: Android 推送指南 -sidebar_label: Android 推送 -sidebar_position: 3 -slug: /sdk/push/guide/android/ ---- - - - - - -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; -import {Conditional} from '/src/docComponents/conditional'; - -请先阅读[推送通知总览](/sdk/push/guide/overview/)了解相关概念。 - -Android 消息推送有专门的 Demo,请见 [Android-Push-Demo](https://github.com/leancloud/android-push-demo) 项目。 - -## 推送流程简介 - -Android 的推送主要依赖客户端的 PushService 服务。PushService 是一个独立于应用程序的进程,在应用程序第一次启动时顺带创建,其后则(尽量)一直存活于后台,它主要负责维持与云推送服务器的 WebSocket 长链接。 -所以,只要 PushService 存活,那么推送服务器上有任何需要下发到当前设备的消息,都会立刻推送下来;如果 PushService 被杀死,那推送通道中断,Android 设备就收不到任何推送消息(混合推送除外,后述会有说明)。PushService 第一次启动,建立起与推送服务器的 WebSocket 长链接之后,也会一次性收到多条服务端缓存的未成功下发的历史消息。 - -## 接入推送服务 - -:::caution 注意 - -当前文档介绍的推送服务是我们平台自带推送,在 Android 8.0 之后,自带推送使用的后台服务无法存活太长时间(OS 层面的限制),自带推送长链接基本就不可用了,这会影响到推送消息的到达率。**所以当前文档的推送服务不建议继续使用,请使用各个厂商的推送,即使用[混合推送](/sdk/push/guide/android-mixpush/)**。 - -**如果 APP 内集成当前推送,将无法通过应用商店审核**。例如小米、华为等应用商店因为检测到 LCBroadcastReceiver 存在自启动行为而拒绝上架 APP,应用商店的拒绝提示一般是:**APP 存在自启动行为**。所以我们强烈建议弃用当前文档的推送服务,建议使用[混合推送](/sdk/push/guide/android-mixpush/)。 - -::: - -要接入推送服务,需要依赖 realtime-android library。首先打开 `app` 目录下的 `build.gradle` 进行如下配置: - - -{`dependencies {\n -implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'\n -}`} - - -然后新建一个 Java Class ,名字叫做 **MyLeanCloudApp**,让它继承自 **Application** 类,实例代码如下: - - - -```java -public class MyLeanCloudApp extends Application { - - @Override - public void onCreate() { - super.onCreate(); - // 初始化参数依次为 this, AppID, appkey - LeanCloud.initialize(this, "{{AppID}}", "{{appkey}}", "https://please-replace-with-your-customized.domain.com"); - - } -} -``` - - - - - -```java -public class MyLeanCloudApp extends Application { - - @Override - public void onCreate() { - super.onCreate(); - // 初始化参数依次为 this, ClientID, clientToken - LeanCloud.initialize(this, "{{clientID}}", "{{clientToken}}", "https://please-replace-with-your-customized.domain.com"); - } -} -``` - - -### 配置 AndroidManifest - -请确保你的 `AndroidManifest.xml` 的 `` 中包含如下内容: - -```xml - -``` - -同时设置了必要的权限: - -```xml - - -``` - -为了让应用能在关闭的情况下也可以收到推送,你需要在 `` 中加入: - -```xml - - - - - - - -``` - -#### 推送唤醒 - -如果希望支持应用间的推送唤醒机制,即在同一设备上有两个使用了云推送的应用,应用 A 被杀掉后,当应用 B 被唤醒时可以同时唤醒应用 A 的推送,可以这样配置: - -```xml - -``` - -#### 完整的 `AndroidManifest.xml` - -完整的 `AndroidManifest.xml` 文件配置示意如下: - -```xml - - - - - - - - - - - - - - - - - - - - - - - -``` - - -### 保存 Installation - -当应用在用户设备上安装好以后,如果要使用推送功能,SDK 会自动生成一个 Installation 对象。该对象本质上是应用在设备上生成的安装信息,需要首先将它保存到云端设备才能收到推送: - -```java -LCInstallation.getCurrentInstallation().saveInBackground(); -``` - -**这段代码应该在应用启动的时候调用一次**,保证设备注册到云端。你可以监听调用回调,获取 installationId 做数据关联。 - -```java -LCInstallation.getCurrentInstallation().saveInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCObject avObject) { - // 关联 installationId 到用户表等操作。 - String installationId = LCInstallation.getCurrentInstallation().getInstallationId(); - System.out.println("保存成功:" + installationId ); - } - @Override - public void onError(Throwable e) { - System.out.println("保存失败,错误信息:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -### 启动推送服务 - -通过调用以下代码启动推送服务,同时设置默认打开的 Activity。 - -```java -// 设置默认打开的 Activity -PushService.setDefaultPushCallback(this, PushDemo.class); -``` - -### 订阅频道 - -你的应用可以订阅某个频道(channel)的消息,只要在保存 Installation 之前调用 `PushService.subscribe` 方法: - -```java -// 订阅频道,当该频道消息到来的时候,打开对应的 Activity -// 参数依次为:当前的 context、频道名称、回调对象的类 -PushService.subscribe(this, "public", PushDemo.class); -PushService.subscribe(this, "private", Callback1.class); -PushService.subscribe(this, "protected", Callback2.class); -``` - -注意: - -- **频道名称只能包含大小写英文字母、数字、下划线(`_`)、连字符(`-`)、等号(`=`)、汉字(中日韩统一表意文字)。** -- 回调对象指用户点击通知栏的通知进入的 Activity 页面。 - -退订频道也很简单: - -```java -PushService.unsubscribe(context, "protected"); -//退订之后需要重新保存 Installation -LCInstallation.getCurrentInstallation().saveInBackground(); -``` - -### Android 8.0 推送适配 - -在调用 `LeanCloud.initialize` 之后,需要调用 `PushService.setDefaultChannelId(context, channelid)` 设置通知展示的默认 `channel`,否则消息无法展示。Channel ID 的解释请阅读 Google 官方文档 [Creating a notification](https://developer.android.com/training/notify-user/channels.html)。 - -另外,我们的推送服务也支持多个推送 Channel。在客户端,开发者可以通过调用 `PushService` 的如下方法创建新的通知 Channel(也可以自己调用底层 API 创建): - -```java -public static void createNotificationChannel(Context context, String channelId, String channelName, - String description, int importance, - boolean enableLights, int lightColor, - boolean enableVibration, long[] vibrationPattern) -``` - -记下这里的 `channelId`,因为之后发送推送通知的时候,我们还需要用到它。在发送推送请求的时候,通过 `_notificationChannel` 这个自定义的关键字可以选择不同的 channel 进行消息展示。 - -例如如下的请求会在客户端通知 id 为「1」的通道进行显示: - - - - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "where": {"key" : "value"} - "data": { - "alert": "消息内容", - "title": "显示在通知栏的标题", - "_notificationChannel": "1" - } - }' \ - https://{{host}}/1.1/push -``` - - - - - -```sh -curl -X POST \ - -H "X-LC-Id: {{ClientID}}" \ - -H "X-LC-Key: {{ClientToken}}" \ - -H "Content-Type: application/json" \ - -d '{ - "where": {"key" : "value"} - "data": { - "alert": "消息内容", - "title": "显示在通知栏的标题", - "_notificationChannel": "1" - } - }' \ - https://{{host}}/1.1/push -``` - - - - -## 推送消息 - - -**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置** 中默认选中了 **禁止从客户端进行消息推送**,避免客户端可以不经限制地给应用内任意目标设备推送消息。 -我们建议开发者都勾选此项,通过 REST API 或云服务控制台发送推送消息。 -如果有从客户端发推送的需求,需要先取消勾选此项。 - -### 推送给所有的设备 - -```java -LCPush push = new LCPush(); -Map pushData = new HashMap(); -pushData.put("alert","push message to android device directly"); -push.setPushToAndroid(true); -push.setData(pushData); -push.sendInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(JSONObject jsonObject) { - System.out.println("推送成功" + jsonObject); - } - @Override - public void onError(Throwable e) { - System.out.println("推送失败,错误信息:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -### 发送给特定的用户 - -发送给「public」频道的用户: - -```java -LCQuery pushQuery = LCInstallation.getQuery(); -pushQuery.whereEqualTo("channels", "public"); -LCPush push = new LCPush(); -push.setQuery(pushQuery); -push.setMessage("Push to channel."); -push.setPushToAndroid(true); -push.sendInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(JSONObject jsonObject) { - System.out.println("推送成功" + jsonObject); - } - @Override - public void onError(Throwable e) { - System.out.println("推送失败,错误信息:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -发送给某个 Installation id 的用户,通常来说,你会将 LCInstallation 关联到设备的登录用户 LCUser 上作为一个属性,然后就可以通过下列代码查询 InstallationId 的方式来发送消息给特定用户,实现类似私信的功能: - -```java -LCQuery pushQuery = LCInstallation.getQuery(); -// 假设 THE_INSTALLATION_ID 是保存在用户表里的 installationId, -// 可以在应用启动的时候获取并保存到用户表 -pushQuery.whereEqualTo("installationId", THE_INSTALLATION_ID); -LCPush.sendMessageInBackground("Tarara invited you to play Arc Symphony with her!",pushQuery).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(Object object) { - System.out.println("推送成功" + object); - } - @Override - public void onError(Throwable e) { - System.out.println("推送失败,错误信息:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -## 深入阅读:如何响应推送消息 - -注意,以下内容不适用于混合推送。 -混合推送中,需要使用各厂商提供的机制指定特定的 activity 来响应推送消息,详见[Android 混合推送指南](/sdk/push/guide/android-mixpush/)。 - -### 消息格式 - -具体的消息格式,可参考[推送 REST API 使用指南](/sdk/push/guide/rest/)的《推送消息》一节。 -对于 Android 设备,默认的消息内容参数支持下列属性: - -```json -{ - "alert": "消息内容", - "title": "显示在通知栏的标题", - "custom-key": "由用户添加的自定义属性,custom-key 仅是举例,可随意替换", - "silent": false, // 用于控制是否关闭推送通知栏提醒,默认为 false,即不关闭通知栏提醒 - "action": "com.your_company.push" // 在使用自定义 receiver 时必须提供 -} -``` -上面 silent 属性,是透传消息与通知栏消息的标志。silent 为 true,表示消息不展示在通知栏;silent 为 false,表示消息会展示在通知栏。 - -前面讲过,在 PushService 里面接收到推送消息之后,在转发消息之前,会判断消息是否过期,以及是否收到了重复的消息,只有非过期、非重复的消息,才会会通过发送本地通知或者广播,将事件告知应用程序。 - -### 通知栏消息如何响应用户点击事件 - -PushService 在发出通知栏消息的时候,会根据开发者调用 `PushService.setDefaultPushCallback(context, clazz)` 或者 `PushService.subscribe(context, "channel", clazz)` 设置的回调类,设置通知栏的响应类。 - -在回调类的 onCreate 函数中开发者则可以通过如下代码获取推送消息的具体数据: - -```java -public class CallbackActivity extends Activity { - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.callback2); - - // 获取推送消息数据 - String message = this.getIntent().getStringExtra("com.avoscloud.Data"); - String channel = this.getIntent().getStringExtra("com.avoscloud.Channel"); - System.out.println("message=" + message + ", channel=" + channel); - } -} -``` - -### 自定义 Receiver - -如果你想推送消息,但不显示在 Android 系统的通知栏中,而是执行应用程序预定义的逻辑,你需要在你的 Android 项目中的 `AndroidManifest.xml` 中声明你自己的 Receiver: - -```xml - - - - - - - - -``` - -其中 `com.avos.avoscloud.PushDemo.MyCustomReceiver` 是你的 Android 的 Receiver 类。而 `` 需要与 push 的 data 中指定的 `action` 相对应。 - -你的 Receiver 可以按照如下方式实现: - -```java -public class MyCustomReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - // 获取推送消息数据 - String message = intent.getStringExtra("com.avoscloud.Data"); - String channel = intent.getStringExtra("com.avoscloud.Channel"); - System.out.println("message=" + message + ", channel=" + channel); - } -} -``` - -同时,要求发送推送的请求也做相应更改,例如: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "channels":[ "public"], - "data": { - "action": "com.avos.UPDATE_STATUS", - "name": "LeanCloud." - } - }' \ - https://{{host}}/1.1/push -``` - -注意,如果你使用自定义的 Receiver,发送的消息必须带 action,并且其值在自定义的 Receiver 配置的 `` 列表里存在,比如这里的 `'com.avos.UPDATE_STATUS'`,请使用自己的 action,尽量不要跟其他应用混淆,推荐采用域名来定义。 - - -## 混合推送 - -自 Android 8.0 之后,系统权限控制越来越严,第三方推送通道的生命周期受到较大限制,同时国内主流厂商也开始推出自己独立的推送服务。因此我们提供「混合推送」的方案来提升推送到达率,具体请参考[Android 混合推送指南](/sdk/push/guide/android-mixpush/)。 - -### 让 PushService 前台运行 - -对于高版本的 Android OS 系统来说,后台 Service 受到越来越严格的限制,为了尽量保证 PushService 的生命周期,让 PushSevice 成为前台 Service(foreground Service)来运行也是一个可以考虑的选择。 - -为了支持这一选项,从 6.4.0 版本开始,我们在 `realtime-android` library 中,给 PushService 增加了一个新的配置项,以便开发者自己选择是否让 PushService 在前台运行: - -```java -/** -* 设置 PushService 以前台进程的方式运行(默认是 background service)。 -* Android 前台 Service 必须要显示一个 Notification 在通知栏,详见说明: -* https://developer.android.com/guide/components/services -* -* @param enableForeground enable PushService run on foreground or not. -* @param identifier The identifier for this notification as per -* {@link NotificationManager#notify(int, Notification) -* NotificationManager.notify(int, Notification)}; must not be 0. -* @param notification The Notification to be displayed. -*/ -public static void setForegroundMode(boolean enableForeground, int identifier, Notification notification); -``` - -PushService 默认是始终在后台运行的,如果切换到前台运行,从我们测试的结果来看,PushService 可以保持长时间不被系统杀死。设置前台运行的示例代码如下: - -```java -String channelId = "cn.leancloud.simpleapp"; -PushService.setDefaultChannelId(this, channelId); - -NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, channelId); -Notification notification = notificationBuilder.setOngoing(true) - .setSmallIcon(R.mipmap.ic_launcher) - .setContentTitle("App is running in background") - .setPriority(NotificationManager.IMPORTANCE_MIN) - .setCategory(Notification.CATEGORY_SERVICE) - .build(); -PushService.setForegroundMode(true, 101, notification); -``` diff --git a/leancloud/docs/sdk/push/guide/ios-cert.mdx b/leancloud/docs/sdk/push/guide/ios-cert.mdx deleted file mode 100644 index ecb778de2..000000000 --- a/leancloud/docs/sdk/push/guide/ios-cert.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: APNs(苹果推送通知服务)设置指南 -sidebar_label: APNs 设置 -sidebar_position: 1 -slug: /sdk/push/guide/ios-cert/ ---- - - - - - - -APNs 是 Apple 的推送通知服务,它使第三方应用程序开发人员能够向安装在 Apple 设备上的应用程序发送通知数据。 - -此文主要介绍如何让 Apple 设备上的应用程序支持 APNs 以及如何在开发者中心设置相关的配置。 - -## 启用推送功能 - -应用要启用推送功能,分为两步: - -1. 开发项目开启推送的权限。 -2. 在苹果开发者网站为对应的 App ID 启用推送功能。 - -### 开启开发项目的推送权限 - -要在应用程序中添加所需的权限,请在 Xcode 项目中启用推送通知功能。 - -打开 Xcode 项目,在 **Project > Target > Capabilities** 页面中点击红框中的加号按钮,然后选择并添加 `Push Notifications`,添加后的结果如图中黄框所示: - -![Project Add Capability](/img/apns_setup/project_add_capability.png) - -### 启用苹果 App ID 的推送功能 - -登录苹果开发者网站,进入 **Certificates, Identifiers & Profiles** 页面,点击侧边栏中的 **Identifiers**,然后在列表中找到项目对应的 App ID(即 Xcode 项目中的 Bundle Identifier),点击并进入配置编辑页面,然后选择 `Push Notifications` 旁边的复选框,最后点击右上的 Save(保存)按钮,结果如下图所示: - -![App ID Add Capability](/img/apns_setup/app_id_add_capability.png) - -## 选择推送方式 - -苹果提供了两种方式来发送通知,这两种方式各有优点和缺点,云服务对这两种方式都支持,可以根据需要选择其中一种推送方式。 - -1. 基于 Token 的推送方式(推荐)。 - - 理论上它比基于证书的方式更快。 - - 支持多个云服务应用使用同一个 Key。 - - 支持用同一个 Key 给苹果开发者账号下的多个应用推送通知。 - - 支持用同一个 Key 给苹果开发者账号下的测试、正式应用推送通知。 - - 生成的 Key 不再有过期时间,无需像证书方式那样需要定期重新生成证书。 -2. 基于证书的推送方式。 - - 证书和苹果的 App ID 绑定,一个证书只能向其绑定的苹果应用推送通知。 - - APNs 有开发、生产两个环境,可能需要为不同环境下的苹果应用配置对应的证书。 - - 证书有过期时间,需要定期重新生成并配置。 - -总的来说,基于 Token 的推送方式在配置步骤、易用性以及功能性上,都要优于基于证书的推送方式,因此我们推荐使用基于 Token 的推送方式。 - -> **注意**,这两种方式在云服务平台上是互斥的,且基于 Token 的方式优先级高于基于证书的方式,即如果设置了 Token 方式的配置,则该云服务应用下的所有推送都会使用基于 Token 的方式。 - -### 基于 Token 的推送方式(推荐) - -要使用基于 Token 的推送方式,首先需从苹果开发者网站上生成并下载密钥(**Auth Key**),之后在开发者中心上传该密钥并设置对应信息,成功后即可使用该方式推送通知。 - -#### 生成密钥 - -登录苹果开发者网站,进入 **Certificates, Identifiers & Profiles** 页面,点击侧边栏中的 **Keys**,然后点击左上方的添加按钮(+)。在密钥名称下,为密钥输入一个独特的名称,接着选择 **Apple Push Notifications service (APNs)** 旁边的复选框,如下图所示: - -> **注意**,如果侧边栏中没有 **Keys** 这一项,则可能是**苹果开发者账号无对应的权限**。 - -![Generate Push Key](/img/apns_setup/generate_push_key.png) - -接着点击继续,在下一个页面审查密钥配置,确认无误后点击确认。最后点击下载密钥,成功下载的密钥将被保存为一个文本文件,文件扩展名为 `.p8`。 - -> **注意**,请把这个文件(扩展名为 `.p8`)保存在一个安全的地方,因为密钥不会保存在你的苹果开发者账户中,你将无法再次下载它。如果下载按钮被禁用,说明你之前下载了密钥。 - -#### 设置密钥 - -下载了密钥(`.p8` 文件)后,需要在开发者中心将其上传,并配置相关信息。具体步骤如下: - -1. 在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > iOS 推送 Token Authentication** 点击 **新增 Token Authentication** 按钮。 -2. 在弹出的对话框内填入 `Team ID`、`Key ID`、`Topics`,并将之前下载的密钥文件(`.p8` 文件)上传。 - - `Team ID` 是苹果开发者账号所属团队的 ID,在苹果开发者账号网站的 **Membership** 中可查看到。 - - `Key ID` 是之前生成的推送密钥(`.p8` 文件)的 ID,在苹果开发者账号网站的 **Certificates, Identifiers & Profiles > Keys** 中点击对应的密钥,进入详细页面后可查看到。 - - `Topics` topic 指苹果应用的 ID(即 Xcode 项目中的 Bundle Identifier),此栏支持填入多个 topic,每个 topic 用**英文半角逗号**分隔,且**所有 topic 必须从属于同一个 Team ID**。 -3. 点击**添加**,完成上传以及设置密钥的工作。 - -完成以上操作后,可以通过 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 在线发送** 测试推送的发送功能。 - -### 基于证书的推送方式 - -要使用基于证书的推送方式,需要从苹果开发者网站上为每个苹果应用生成单独的推送证书,每个苹果应用的推送证书又可分为**开发环境推送证书**和**开发&生产环境推送证书**,之后在开发者中心上传对应的推送证书,成功后即可使用该方式推送通知。 - -#### 生成证书 - -登录苹果开发者网站,进入 **Certificates, Identifiers & Profiles** 页面,点击侧边栏中的 **Certificates**,然后点击左上方的添加按钮(+),接着按以下步骤操作: - -1. 选择对应的证书类型。一般选择 `Apple Push Notification service SSL` 类型的推送证书,这类证书又分为 `Sandbox` 和 `Sandbox & Production` 两种,其中 `Sandbox` 证书只能用于开发环境的苹果应用,而 `Sandbox & Production` 证书既可用于开发环境,也可用于生产环境的苹果应用。具体区别可以看选项下的描述,如下图所示: - - ![Select Cert Type](/img/apns_setup/select_cert_type.png) - -2. 选择完证书后,点击继续,接着选择 App ID(即 Xcode 项目中的 Bundle Identifier),选完后点击继续进入下一页面,会要求上传一个 CSR 文件。 -3. 在你的 Mac 上生成一个 CSR(Certificate Signing Request)文件,生成步骤如下: - - - 启动位于 `/Applications/Utilities` 的 `Keychain Access`(钥匙串访问)。 - - 选择 **Keychain Access > Certificate Assistant > Request a Certificate From a Certificate Authority…**(钥匙串访问 > 证书助理 > 从证书颁发机构请求证书…)。 - - 证书助理对话框中,在 `User Email Address`(用户电子邮件地址)栏中输入一个电子邮件地址;在 `Common Name`(通用名称)字段中,输入密钥的名称(例如,Gita Kumar Dev Key)。 - - 将 `CA Email Address`(CA 电子邮件地址)字段留空。 - - 选择 `Saved to disk`(保存到磁盘),然后点击继续。 - - ![Generate CSR](/img/apns_setup/generate_csr.png) - -4. 上传你的 CSR 文件(上一步中保存到本地的 `.certSigningRequest` 文件)并点击继续,最后下载生成的证书。 - -#### 设置证书 - -证书生成后,需要将下载的证书和私钥上传到开发者中心。请按以下步骤操作: - -1. 在生成 CSR 文件的 Mac 上,双击下载的证书,macOS 会将其导入到 `Keychain` 中并和之前创建的 CSR 的密钥归为一组,如下图所示: - - ![Cert With Key](/img/apns_setup/cert_with_key.png) - -2. 在 **Keychain Access > login > My Certificates** 中**右键点击**导入的证书(点击证书,不要点击对应的密钥),选择 **Export(导出)**,将证书以 `.p12` 格式保存到磁盘,这时会有弹窗提示输入密码来保护导出的证书,**请不要设置密码,将两个输入框留空**,点击 OK;之后可能还有一个弹窗,要求输入 macOS 的 login(登录)密码以允许让证书导出,请输入密码并点击 Allow。 -3. 在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > iOS 推送证书** 上传对应的证书(上一步导出的 `.p12` 文件)。 - - 如果证书的类型为 `Sandbox`,则只能上传到开发环境;如果证书的类型为 `Sandbox & Production`,则开发、生产环境都可上传。 - -完成以上操作后,可以通过 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 在线发送** 测试推送的发送功能。 - -#### 上传证书失败 - -如果上传推送证书失败,通常是因为证书有问题,一般由下列原因导致: - -1. 证书不是推送证书。可以通过证书的 Common Name(常用名称)来判断(在 `Keychain Access` 中双击证书可查看到),推送证书的 Common Name 中会包含 `Push Service` 或者 `Pass Type ID`,如下图所示: - - ![Cert Common Name](/img/apns_setup/cert_common_name.png) - - 上传程序会检查证书的 Common Name(常用名称)是否包含以下前缀: - - - `Apple Push Services` - - `Apple Sandbox Push Services` - - `Apple Development IOS Push Services` - - `Apple Production IOS Push Services` - - `Pass Type ID` - - > Apple 未来可能会修改推送证书的 Common Name(常用名称)前缀,我们会及时更新前缀列表,同时也欢迎大家来反馈。 - -2. 证书导出格式有误。目前开发者中心只接受 `.p12` 格式的证书,因此在导出证书时,必须选择 `.p12` 作为导出格式。 - -#### 证书过期 - -开发者如果使用过期证书进行推送,会遇到 `The iOS certificate file is expired or disabled.` 的错误提示。 - -云服务后端在收到推送请求时都会去检查 `prod` 参数指明的证书是否过期,没有 `prod` 则默认检查生产环境证书,如果发现过期并且 query 条件查出的目标设备可能存在 iOS 设备,就直接拒绝本次推送。 - -一种解决方法是替换过期的证书,另一种方法是在 query 条件中通过 `deviceType` 字段明确指定设备类型为非 iOS 设备来推送,方法参见《推送 REST API 使用指南》的《通过查询条件发推送》一节。 diff --git a/leancloud/docs/sdk/push/guide/ios.mdx b/leancloud/docs/sdk/push/guide/ios.mdx deleted file mode 100644 index ab3a8643b..000000000 --- a/leancloud/docs/sdk/push/guide/ios.mdx +++ /dev/null @@ -1,1092 +0,0 @@ ---- -title: iOS 推送指南 -sidebar_label: iOS 推送 -sidebar_position: 2 -slug: /sdk/push/guide/ios/ ---- - - - - - -import MultiLang from '/src/docComponents/MultiLang'; -import Mermaid from "/src/docComponents/Mermaid"; - - -本文介绍了如何在 iOS 设备中使用推送通知功能。建议先阅读[推送通知总览](/sdk/push/guide/overview/)了解相关概念。 - -## 配置 APNs 推送证书 - -配置 APNs 证书是使用推送服务的前提,详情请参考[iOS 推送设置指南](/sdk/push/guide/ios-cert/)。 - -## iOS 流程简介 - -首先,注册 APNs 申请 Token,并将其保存到云端: - ->APNs: 1. 调用官方 API,申请 deviceToken -APNs-->>iOS SDK: 2. 下发 deviceToken -iOS SDK-->>云端: 3. SDK 将 deviceToken 存储在云端 _Installation 表中 -`} -/> - -然后,调用推送通知提供的接口发送推送消息: - ->云端: 1. HTTPS 请求推送 API -云端-->>APNs: 2. 找出 _Installation 记录中对应的 deviceToken,调用 APNs API 发送推送 -APNs->>iOS 设备: 3. 发送推送 -`} -/> - -## Installation - -Installation 是 LCObject 的子类,使用 Installation 对象来保存推送所需的 token 以及其他数据。 - -SDK 提供默认的 Installation 对象,并**会在默认对象保存成功后持久缓存其数据**。一般情况下,使用默认对象保存 device token。默认对象的获取方式如下: - - - -```objc -LCInstallation *installation = [LCInstallation defaultInstallation]; -``` - -```swift -let installation = LCApplication.default.currentInstallation -``` - - - -除了默认的 Installation 对象,你也可以构造新的 Installation 对象,用来存储其他特殊类型的 token(诸如 VoIP 等),构造方式如下: - - - -```objc -LCInstallation *installation = [[LCInstallation alloc] init]; -``` -```swift -let installation = LCInstallation() -``` - - - -**SDK 即时通讯模块会使用默认 Installation 对象的 device token。如需使用即时通讯的离线推送功能,请确保默认 Installation 对象成功保存了 device token。** - -Installation 对象的默认字段如下所示: - -字段|类型|说明 ----|---|--- -deviceToken|String|推送所需的 Token -apnsTeamId|String|推送所需的 Team ID -badge|Number|对应应用通知的标记,主要用于通知标记清零 -channels|Array|订阅频道数组 -deviceProfile|String|自定义证书名称,主要用于多证书推送 -deviceType|String|设备类型,SDK 会自动设置该属性,一般情况下不要随意更改 -apnsTopic|String|应用的 Bundle Identifier,SDK 会自动设置该属性,一般情况下不要随意更改 -timeZone|String|设备所处时区,SDK 会自动设置该属性,一般情况下不要随意更改 - -### 注册 APNs 获取 Token - -在保存 Installation 前,要先注册 APNs 来获取推送所需的 token,以 User Notification 为例,具体步骤如下: - - - -```objc -#import -#import - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - - // 首先需要初始化应用 - [LCApplication setApplicationId:{{appid}} - clientKey:{{appkey}} - serverURLString:"https://please-replace-with-your-customized.domain.com"]; - - [[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { - switch ([settings authorizationStatus]) { - case UNAuthorizationStatusAuthorized: - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication] registerForRemoteNotifications]; - }); - break; - case UNAuthorizationStatusNotDetermined: - [[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) { - if (granted) { - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication] registerForRemoteNotifications]; - }); - } - }]; - break; - default: - break; - } - }]; - - return YES; -} -``` -```swift -import LeanCloud -import UserNotifications - -func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - // 首先需要初始化应用 - do { - try LCApplication.default.set( - id: {{appid}}, - key: {{appkey}}, - // 请将 xxx.example.com 替换为你的应用绑定的自定义 API 域名 - serverURL: "https://xxx.example.com") - } catch { - print(error) - return false - } - - UNUserNotificationCenter.current().getNotificationSettings { (settings) in - switch settings.authorizationStatus { - case .authorized: - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - case .notDetermined: - UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert, .sound]) { (granted, error) in - if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - } - default: - break - } - } - - return true -} -``` - - - -### 保存 Token - -注册 APNs 成功后,系统会通过 `didRegisterForRemoteNotificationsWithDeviceToken` 函数返回 deviceToken。一般情况,在该函数里保存 deviceToken 和 apnsTeamId 即可。保存方式如下: - - - -```objc -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { - - [[LCInstallation defaultInstallation] setDeviceTokenFromData:deviceToken - teamId:@"YOUR_APNS_TEAM_ID"]; - [[LCInstallation defaultInstallation] saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // save succeeded - } else if (error) { - NSLog(@"%@", error); - } - }]; -} -``` -```swift -func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - - LCApplication.default.currentInstallation.set( - deviceToken: deviceToken, - apnsTeamId: "YOUR_APNS_TEAM_ID") - LCApplication.default.currentInstallation.save { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} -``` - - - -iOS 系统重装、从备份恢复应用、在新设备上安装应用都会导致 device token 变化,因此 [Apple 推荐][apple-apns]在应用每次启动时都去请求 APNs 的 device token,获取 token 后进行设置并保存 token。 -除此以外,云服务后端会统计 Installation 的更新时间(`updatedAt`),据此清理长期未更新的 Installation 数据。 -所以我们建议开发者遵循 Apple 的推荐方式开发应用,以免有效 Installation 数据被意外清理,以及因为 device token 过期无效而推送失败。 - -[apple-apns]: https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns - -## 多证书场景 - -对于一些应用,他们在发布和上架时分为不同的版本(司机版、乘客版),但数据和消息是互通的,这种场景下我们允许应用上传多个自定义证书并对不同的设备设置 `deviceProfile`,从而可以用合适的证书给不同版本的应用推送。 - -当你上传自定义证书时会被要求输入「证书类型」,即 deviceProfile 的名字。当 Installation 上保存了 deviceProfile 时,我们将忽略原先的开发和生产证书设置,而直接按照 deviceProfile 推送。 - -## 特殊推送类型 - -对于诸如 VoIP 这类的特殊推送,由于其使用的 apnsTopic 并不是应用的 Bundle Identifier,所以在保存 token 前,需要修改 apnsTopic 为其指定的值。 - -## 发送推送消息 - -可以通过 REST API 或云服务控制台发送 iOS 推送消息。 - -### 推送环境 - -iOS 应用的推送环境有**测试**和**生产**两种。 -通过 Xcode 安装的 App 处于测试环境,通过 App Store、Ad-Hoc、TestFlight 发布的正式版 App 处于生产环境。 - -通过 REST API 和控制台推送消息时可以通过 `prod` 参数指定推送到处于哪个环境的 iOS 应用。 -通过 SDK 使用 `Push` 发起推送,默认发往生产环境的 iOS 应用。 -如果要推送到测试环境,需要采用如下方式设置: - - - -```objc -[LCPush setProductionMode:false]; -``` -```swift -do { - let environment: LCApplication.Environment = [.pushDevelopment] - let configuration = LCApplication.Configuration(environment: environment) - try LCApplication.default.set( - id: {{appid}}, - key: {{appkey}}, - // 请将 xxx.example.com 替换为你的应用绑定的自定义 API 域名 - serverURL: "https://xxx.example.com", - configuration: configuration) -} catch { - print(error) -} -``` - - - -注意,SDK 只能指定推送给哪种环境的 iOS 应用,无法切换 iOS 应用本身所处的推送环境。 -iOS 应用所处的推送环境完全由 App 的分发方式决定。 - -注意,为防止由于大量证书错误所产生的性能问题,我们对使用 **开发证书** 的推送做了设备数量的限制,即一次至多可以向 20,000 个设备进行推送。如果满足推送条件的设备超过了 20,000 个,系统会拒绝此次推送(**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 推送记录** 页面会显示相应信息)。因此,在使用开发证书推送时,请合理设置推送条件。 - -## 使用频道 - -使用频道(channel)可以实现「发布—订阅」的模型。设备订阅某个频道,然后发送消息的时候指定要发送的频道即可。 - -注意,频道名称只能包含大小写英文字母、数字、下划线(`_`)、连字符(`-`)、等号(`=`)、汉字(中日韩统一表意文字)。 - -### 订阅和退订 - -订阅 `Giants` 频道: - - - -```objc -LCInstallation *currentInstallation = [LCInstallation defaultInstallation]; -[currentInstallation addUniqueObject:@"Giants" forKey:@"channels"]; -[currentInstallation saveInBackground]; -``` -```swift -do { - try LCApplication.default.currentInstallation.append("channels", element: "Giants", unique: true) - _ = LCApplication.default.currentInstallation.save({ (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - - - -订阅后要记得保存。 - -退订: - - - -```objc -LCInstallation *currentInstallation = [LCInstallation defaultInstallation]; -[currentInstallation removeObject:@"Giants" forKey:@"channels"]; -[currentInstallation saveInBackground]; -``` -```swift -do { - try LCApplication.default.currentInstallation.remove("channels", element: "Giants") - _ = LCApplication.default.currentInstallation.save({ (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - - - -获取所有订阅的频道: - - - -```objc -NSArray *subscribedChannels = [LCInstallation defaultInstallation].channels; -``` -```swift -let subscribedChannels: LCArray? = LCApplication.default.currentInstallation.channels -``` - - - -### 发送消息到频道 - -发送消息到刚才订阅的「Giants」频道: - - - -```objc -// Send a notification to all devices subscribed to the "Giants" channel. -LCPush *push = [[LCPush alloc] init]; -[push setChannel:@"Giants"]; -[push setMessage:@"Giants 太牛掰了"]; -[push sendPushInBackground]; -``` -```swift -let messageData: [String: Any] = [ - "alert": "Giants 太牛掰了" -] - -let channels: [String] = ["Giants"] - -LCPush.send(data: messageData, channels: channels) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -如果你想发送到多个频道,可以指定 `channels` 数组: - - - -```objc -NSArray *channels = [NSArray arrayWithObjects:@"Giants", @"Mets", nil]; -LCPush *push = [[LCPush alloc] init]; - -// Be sure to use the plural 'setChannels'. -[push setChannels:channels]; -[push setMessage:@"The Giants won against the Mets 2-3."]; -[push sendPushInBackground]; -``` -```swift -let messageData: [String: Any] = [ - "alert": "The Giants won against the Mets 2-3." -] - -let channels: [String] = ["Giants", "Mets"] - -LCPush.send(data: messageData, channels: channels) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -## 高级定向发送 - -频道对于大多数应用来说可能就足够了。但是某些情况下,你可能需要更高精度的定向推送。推送通知允许你通过 LCQuery API 查询 Installation 列表,并向指定条件的 query 推送消息。 - -因为 Installation 同时是 LCObject 的子类,因此你可以保存任何数据类型到 Installation,并将它和你的其他应用数据对象关联起来,这样一来,你可以非常灵活地向你用户群做定制化、动态的推送。 - -### 保存 Installation 数据 - -为 Installation 添加三个新字段: - - - -```objc -// Store app language and version -LCInstallation *installation = [LCInstallation defaultInstallation]; - -[installation setObject:@(YES) forKey:@"scores"]; -[installation setObject:@(YES) forKey:@"gameResults"]; -[installation setObject:@(YES) forKey:@"injuryReports"]; -[installation saveInBackground]; -``` -```swift -do { - let currentInstallation = LCApplication.default.currentInstallation - - try currentInstallation.set("scores", value: true) - try currentInstallation.set("gameResults", value: true) - try currentInstallation.set("injuryReports", value: true) - - _ = currentInstallation.save({ (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - - - -你可以给 Installation 添加 owner 属性,比如当前的登录用户: - - - -```objc -// Saving the device's owner -LCInstallation *installation = [LCInstallation defaultInstallation]; -[installation setObject:[LCUser currentUser] forKey:@"owner"]; -[installation saveInBackground]; -``` -```swift -do { - let currentInstallation = LCApplication.default.currentInstallation - - if let currentUser = LCApplication.default.currentUser { - try currentInstallation.set("owner", value: currentUser) - } - - _ = currentInstallation.save({ (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - - - -### 根据查询来推送消息 - -一旦 Installation 保存了你的应用数据,你可以使用 Query 来查询出设备的一个子集做推送。 - - - -```objc -// Create our Installation query -LCQuery *pushQuery = [LCInstallation query]; -[pushQuery whereKey:@"injuryReports" equalTo:@(YES)]; - -// Send push notification to query -LCPush *push = [[LCPush alloc] init]; -[push setQuery:pushQuery]; // Set our Installation query -[push setMessage:@"Willie Hayes injured by own pop fly."]; -[push sendPushInBackground]; -``` -```swift -let query = LCQuery(className: "_Installation") -query.whereKey("injuryReports", .equalTo(true)) - -let messageData: [String: Any] = [ - "alert": "Willie Hayes injured by own pop fly." -] - -LCPush.send(data: messageData, query: query) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -你也可以在查询中添加 channels 的条件: - - - -```objc -// Create our Installation query -LCQuery *pushQuery = [LCInstallation query]; -[pushQuery whereKey:@"channels" equalTo:@"Giants"]; // Set channel -[pushQuery whereKey:@"scores" equalTo:@(YES)]; - -// Send push notification to query -LCPush *push = [[LCPush alloc] init]; -[push setQuery:pushQuery]; -[push setMessage:@"Giants scored against the A's! It's now 2-2."]; -[push sendPushInBackground]; -``` -```swift -let query = LCQuery(className: "_Installation") -query.whereKey("channels", .equalTo("Giants")) -query.whereKey("scores", .equalTo(true)) - -let messageData: [String: Any] = [ - "alert": "Giants scored against the A's! It's now 2-2." -] - -LCPush.send(data: messageData, query: query) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -如果你在 Installation 还保存了其他对象的关系,我们同样可以在查询条件中使用这些数据,例如,向靠近北京大学的设备推送消息: - - - -```objc -// Find users near a given location -LCQuery *userQuery = [LCUser query]; -[userQuery whereKey:@"location" - nearGeoPoint:beijingUniversityLocation, - withinMiles:[NSNumber numberWithInt:1]] - -// Find devices associated with these users -LCQuery *pushQuery = [LCInstallation query]; -[pushQuery whereKey:@"user" matchesQuery:userQuery]; - -// Send push notification to query -LCPush *push = [[LCPush alloc] init]; -[push setQuery:pushQuery]; // Set our Installation query -[push setMessage:@"Free hotdogs at the Tarara concession stand!"]; -[push sendPushInBackground]; -``` -```swift -let beijingUniversityLocation = LCGeoPoint(latitude: 39.9869, longitude: 116.3059) - -let userQuery = LCQuery(className: "_User") -userQuery.whereKey("location", .locatedNear(beijingUniversityLocation, minimal: nil, maximal: nil)) - -let pushQuery = LCQuery(className: "_Installation") -pushQuery.whereKey("user", .matchedQuery(userQuery)) - -let messageData: [String: Any] = [ - "alert": "Free hotdogs at the Tarara concession stand!" -] - -LCPush.send(data: messageData, query: pushQuery) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -## 发送选项 - -除了发送一个文本信息之外,你还可以播放一个声音,设置 badge 数字或者其他想自定义的数据。你还可以设置一个消息的过期时间,如果对消息的时效性特别敏感的话。 - -### 定制通知 - -如果你不仅想发送一条文本消息,你可以构建自定义的推送数据。这里有一些保留字段具有特殊含义: - -保留字段|说明 ----|--- -`alert`|推送消息的文本内容。 -`badge`|应用图标右上角的数字。可以设置一个值或者递增当前值。 -`sound`|应用 bundle 里的声音文件名称。 -`content-available`|如果使用了 Newsstand,设置为 1 来开始一次后台下载。 - -更多可用的保留字段,请参考[推送 REST API 使用指南](/sdk/push/guide/rest/)的《消息内容参数》一节。 - -递增 badge 数字并播放声音: - - - -```objc -NSDictionary *data = [NSDictionary dictionaryWithObjectsAndKeys: - @"The Mets scored! The game is now tied 1-1!", @"alert", - @"Increment", @"badge", - @"cheering.caf", @"sound", - nil]; -LCPush *push = [[LCPush alloc] init]; -[push setChannels:[NSArray arrayWithObjects:@"Mets", nil]]; -[push setData:data]; -[push sendPushInBackground]; -``` -```swift -let channels: [String] = ["Mets"] - -let messageData: [String: Any] = [ - "alert": "The Mets scored! The game is now tied 1-1!", - "badge": "Increment", - "sound": "cheering.caf" -] - -LCPush.send(data: messageData, channels: channels) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -当然,你还可以添加其他自定义的数据。你会在接收推送一节看到,当应用通过推送打开你的应用的时候,你就可以访问这些数据。当你要在用户打开通知的时候显示一个不同的 view controller 的时候,这特别有用。 - - - -```objc -NSDictionary *data = [NSDictionary dictionaryWithObjectsAndKeys: - @"Ricky Vaughn was injured in last night's game!", @"alert", - @"Vaughn", @"name", - @"Man bites dog", @"newsItem", - nil]; -LCPush *push = [[LCPush alloc] init]; -[push setChannel:@"Indians"]; -[push setData:data]; -[push sendPushInBackground]; -``` -```swift -let channels: [String] = ["Indians"] - -let messageData: [String: Any] = [ - "alert": "Ricky Vaughn was injured in last night's game!", - "name": "Vaughn", - "newsItem": "Man bites dog" -] - -LCPush.send(data: messageData, channels: channels) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -### 设置过期日期 - -当设备关闭或者无法连接到网络的时候,推送通知就无法被送达。如果你有一条时间敏感的推送通知,不希望在太长时间后被用户读到,那么可以设置一个过期时间来避免打扰用户。 - -首先是指定过期时间来告诉云服务不要再去发送通知。 - - - -```objc -NSDateComponents *comps = [[NSDateComponents alloc] init]; -[comps setYear:2013]; -[comps setMonth:10]; -[comps setDay:12]; -NSCalendar *gregorian = - [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; -NSDate *date = [gregorian dateFromComponents:comps]; - -// Send push notification with expiration date -LCPush *push = [[LCPush alloc] init]; -[push expireAtDate:date]; -[push setMessage:@"Season tickets on sale until October 12th"]; -[push sendPushInBackground]; -``` -```swift -let expirationDate = Date(timeIntervalSinceNow: 600) - -let messageData: [String: Any] = [ - "alert": "Season tickets on sale until October 12th" -] - -LCPush.send(data: messageData, expirationDate: expirationDate) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -这个方法有个隐患,因为设备的时钟是无法保证精确的,你可能得到错误的结果。因此,我们还提供了指定时间间隔方法,通知将在指定间隔时间后失效: - - - -```objc -NSTimeInterval interval = 60*60*24*7; // 1 week - -LCPush *push = [[LCPush alloc] init]; -[push expireAfterTimeInterval:interval]; -[push setMessage:@"Season tickets on sale until October 18th"]; -[push sendPushInBackground]; -``` -```swift -let expirationInterval: TimeInterval = 60*60*24*7 - -let messageData: [String: Any] = [ - "alert": "Season tickets on sale until October 18th" -] - -LCPush.send(data: messageData, expirationInterval: expirationInterval) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -注意,我们建议给 iOS 设备的推送都设置过期时间,才能保证推送的当时,如果用户设置了飞行模式,在关闭飞行模式之后可以收到推送消息,可以参考 [Stackoverflow - Push notification is not being delivered when iPhone comes back online](http://stackoverflow.com/questions/24026544/push-notification-is-not-being-delivered-when-iphone-comes-back-online)。 - -## 定时推送 - -我们提供了设置推送时间的方法,可以在指定的时间进行推送: - - - -```objc -NSDateComponents *comps = [[NSDateComponents alloc] init]; -[comps setYear:2013]; -[comps setMonth:10]; -[comps setDay:12]; -NSCalendar *gregorian = - [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; -NSDate *date = [gregorian dateFromComponents:comps]; - -LCPush *push = [[LCPush alloc] init]; -[push setPushDate:date]; -[push setMessage:@"Push this notification on 2013-10-12."]; -[push sendPushInBackground]; -``` -```swift -let pushDate = Date(timeIntervalSinceNow: 6000) -let messageData: [String: Any] = [ - "alert": "Push this notification at a later time." -] -LCPush.send(data: messageData, pushDate: pushDate) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -定时推送同样可以设置过期时间,例如同时指定时间间隔: - - - -```objc -LCPush *push = [[LCPush alloc] init]; -[push setPushDate:date]; -[push expireAfterTimeInterval:interval]; -// 下略 -``` -```swift -LCPush.send(data: messageData, pushDate: pushDate, expirationInterval: expirationInterval) { /* 略 */ } -``` - - - -### 指定设备平台 - -跨平台的应用,可能想指定发送的平台,比如 iOS 或者 Android: - - - -```objc -LCQuery *query = [LCInstallation query]; -[query whereKey:@"channels" equalTo:@"suitcaseOwners"]; - -// Notification for Android users -[query whereKey:@"deviceType" equalTo:@"android"]; -LCPush *androidPush = [[LCPush alloc] init]; -[androidPush setMessage:@"Your suitcase has been filled with tiny robots!"]; -[androidPush setQuery:query]; -[androidPush sendPushInBackground]; - -// Notification for iOS users -[query whereKey:@"deviceType" equalTo:@"ios"]; -LCPush *iOSPush = [[LCPush alloc] init]; -[iOSPush setMessage:@"Your suitcase has been filled with tiny apples!"]; -[iOSPush setChannel:@"suitcaseOwners"]; -[iOSPush setQuery:query]; -[iOSPush sendPushInBackground]; -``` -```swift -let query = LCQuery(className: "_Installation") -query.whereKey("channels", .equalTo("suitcaseOwners")) - -// Notification for Android users -query.whereKey("deviceType", .equalTo("android")) -let messageData: [String: Any] = [ - "alert": "Your suitcase has been filled with tiny robots!" -] -LCPush.send(data: messageData, query: query) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} - -// Notification for iOS users -query.whereKey("deviceType", .equalTo("ios")) -let messageData: [String: Any] = [ - "alert": "Your suitcase has been filled with tiny apples!" -] -LCPush.send(data: messageData, query: query) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -## 接收推送通知 - -正如 [定制通知](#定制通知) 一节提到,你可以随通知发送任意的数据。我们使用这些数据修改应用的行为,当应用是通过通知打开的时候。例如,当打开一条通知告诉你有一个新朋友的时候,这时候如果显示一张图片会非常好。 - -由于 Apple 对消息大小的限制,请尽量缩小要发送的数据大小,否则会被截断。详情请参看 [APNs 文档](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html#//apple_ref/doc/uid/TP40008194-CH101-SW1)。 - - - -```objc -NSDictionary *data = @{ - @"alert": @"James commented on your photo!", - @"p": @"vmRZXZ1Dvo" // Photo's object id -}; -LCPush *push = [[LCPush alloc] init]; -[push setData:data]; -[push sendPushInBackground]; -``` -```swift -let messageData: [String: Any] = [ - "alert": "James commented on your photo!", - "p": "vmRZXZ1Dvo" // Photo's object id -] - -LCPush.send(data: messageData) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - - - -## 响应通知数据 - -当应用是被通知打开的时候,你可以通过 `application:didFinishLaunchingWithOptions:` 方法的 `launchOptions` 参数所使用的 dictionary 访问到数据: - - - -```objc -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // ... - if ([[UIDevice currentDevice].systemVersion floatValue] < 10.0) { - NSDictionary *notificationPayload; - @try { - notificationPayload = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; - } @catch (NSException *exception) {} - - // Create a pointer to the Photo object - NSString *photoId = [notificationPayload objectForKey:@"p"]; - LCObject *targetPhoto = [LCObject objectWithoutDataWithClassName:@"Photo" - objectId:photoId]; - - // Fetch photo object - [targetPhoto fetchIfNeededInBackgroundWithBlock:^(LCObject *object, NSError *error) { - // Show photo view controller - if (!error && [LCUser currentUser]) { - PhotoVC *viewController = [[PhotoVC alloc] initWithPhoto:object]; - [self.navController pushViewController:viewController animated:YES]; - } - }]; - } -} -``` -```swift -func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - if let notification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] { - print(notification) - } - - return true -} -``` - - - -如果当通知到达的时候,你的应用已经在运行,对于 iOS 10 以下,你可以通过 `application:didReceiveRemoteNotification:fetchCompletionHandler:` 方法的 `userInfo` 参数所使用 dictionary 访问到数据: - - - -```objc -/*! - * Required for iOS 7+ - */ -- (void)application:(UIApplication *)application - didReceiveRemoteNotification:(NSDictionary *)userInfo - fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))handler { - // Create empty photo object - NSString *photoId = [userInfo objectForKey:@"p"]; - LCObject *targetPhoto = [LCObject objectWithoutDataWithClassName:@"Photo" - objectId:photoId]; - - // Fetch photo object - [targetPhoto fetchIfNeededInBackgroundWithBlock:^(LCObject *object, NSError *error) { - // Show photo view controller - if (error) { - handler(UIBackgroundFetchResultFailed); - } else if ([LCUser currentUser]) { - PhotoVC *viewController = [[PhotoVC alloc] initWithPhoto:object]; - [self.navController pushViewController:viewController animated:YES]; - } else { - handler(UIBackgroundFetchResultNoData); - } - }]; -} -``` -```swift -func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - // handle notification -} -``` - - - -iOS 10 以上需要使用下面代理方法来获得 `userInfo`: - - - -```objc -/** - * Required for iOS 10+ - * 在前台收到推送内容,执行的方法 - */ -- (void)userNotificationCenter:(UNUserNotificationCenter *)center - willPresentNotification:(UNNotification *)notification - withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { - NSDictionary *userInfo = notification.request.content.userInfo; - if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { - // TODO: 处理远程推送内容 - NSLog(@"%@", userInfo); - } - // 需要执行这个方法,选择是否提醒用户,有 Badge、Sound、Alert 三种类型可以选择设置 - completionHandler(UNNotificationPresentationOptionAlert); -} - -/** - * Required for iOS 10+ - * 在后台和启动之前收到推送内容,点击推送内容后,执行的方法 - */ -- (void)userNotificationCenter:(UNUserNotificationCenter *)center -didReceiveNotificationResponse:(UNNotificationResponse *)response - withCompletionHandler:(void (^)())completionHandler { - NSDictionary * userInfo = response.notification.request.content.userInfo; - if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { - // TODO: 处理远程推送内容 - NSLog(@"%@", userInfo); - } - completionHandler(); -} -``` -```swift -func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - // handle notification -} - -func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - // handle notification -} -``` - - - -## 清除 Badge - -一般需要在打开应用或者退出应用时,将 badge 数目清零。 - - - -```objc -- (void)applicationDidBecomeActive:(UIApplication *)application { - // 本地清空角标 - [application setApplicationIconBadgeNumber:0]; - // currentInstallation 的角标清零 - [LCInstallation defaultInstallation].badge = 0; - [[LCInstallation defaultInstallation] saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // save succeeded - } else if (error) { - NSLog(@"%@", error); - } - }]; -} -``` -```swift -override func applicationDidBecomeActive(_ application: UIApplication) { - // 本地清空角标 - application.applicationIconBadgeNumber = 0 - // currentInstallation 的角标清零 - LCApplication.default.currentInstallation.badge = 0 - LCApplication.default.currentInstallation.save { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } - } -``` - - - - -你可以阅读 [Apple 本地化和推送的文档](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/Introduction.html#//apple_ref/doc/uid/TP40008194-CH1-SW1) 来更多地了解推送通知。 diff --git a/leancloud/docs/sdk/push/guide/overview.mdx b/leancloud/docs/sdk/push/guide/overview.mdx deleted file mode 100644 index ba4af0f99..000000000 --- a/leancloud/docs/sdk/push/guide/overview.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: 推送通知总览 -sidebar_label: 总览 -sidebar_position: 0 -slug: /sdk/push/guide/overview/ ---- - - - - - -import {Conditional} from '/src/docComponents/conditional'; - - -推送通知,使得开发者可以即时地向其应用程序的用户推送通知或者消息,与用户保持互动,从而有效地提高留存率,提升用户体验。平台提供整合了 Android 推送、iOS 推送的统一推送服务。 - -除了通过 iOS、Android SDK 做推送服务之外,你还可以通过 REST API 来发送推送请求。 - - - -使用推送服务前,需在 **应用 > 设置 > 安全中心** 开启推送服务,此开关有三分钟的延迟。 - - - - - -使用推送服务前,需在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 开启推送服务,此开关有三分钟的延迟。 - - - -## 基本概念 - -### Installation - -Installation 表示一个允许推送的设备的唯一标示,对应数据管理平台中的 `_Installation` 表。它就是一个普通的 LCObject,主要属性包括: - -名称|适用平台|描述 ----|---|--- -badge|iOS|呈现在应用图标右上角的红色圆形数字提示,例如待更新的应用数、未读信息数目等。 -channels| |设备订阅的频道。频道名称只能包含大小写英文字母、数字、下划线(`_`)、连字符(`-`)、等号(`=`)、汉字(中日韩统一表意文字)。 -deviceProfile||在应用有多个 iOS 推送证书或多个 Android 混合推送配置的场景下,deviceProfile 用于指定当前设备使用的证书名或配置名。其值需要与 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置** 内配置的证书名或配置名对应,否则将无法完成推送。`deviceProfile` 的值必须以字母开头,由大小写字母、数字和下划线组成的字符串,或为空值。deviceProfile 是特殊字段,只支持 `equals` 查询。 -deviceToken|iOS|APNs 推送的唯一标识符 -apnsTopic|iOS|基于 Token Authentication 的推送需要设置该字段。iOS SDK 会自动读取 iOS 应用的 bundle ID 作为 apnsTopic。但以下情况需要手工指定:1. 使用低于 v4.2.0 的 iOS SDK 版本;2. 不使用 iOS SDK(如 React Native);3. 使用不同于 bundle ID 的 topic。 -deviceType| |设备类型,目前支持 `ios`、`android`。 -installationId|Android|SDK 为每个 Android 设备产生的唯一标识符 -timeZone| |字符串,设备设定的时区 - -### Notification - -对应 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 推送记录** 里的一条记录,表示一条推送消息,它包括下列属性: - -名称|适用平台|描述 ----|---|--- -notificationId| | 推送消息 ID -msg| |本次推送的消息内容,JSON 对象,详见[消息内容参数](/sdk/push/guide/rest#消息内容参数)。 -invalidTokens|iOS|本次推送遇到多少次由 APNs 返回的 [INVALID TOKEN](https://developer.apple.com/library/mac/technotes/tn2265/_index.html#//apple_ref/doc/uid/DTS40010376-CH1-TNTAG32) 错误。**如果这个数字过大,请留意证书是否正常。** -prod|iOS|使用什么环境证书。**dev** 表示开发证书,**prod** 表示生产环境证书。 -status| |本次推送的状态,**in-queue** 表示仍然在队列,**done** 表示完成,**scheduled** 表示定时推送任务等待触发中。 -devices| |本次推送目标设备数。这个数字不是实际送达数,而是处理本次推送请求时在 `_Installation` 表中符合查询条件且有效的总设备数。有效是指 `_Installation` 表的 valid 字段为 true 且 updatedAt 字段时间在最近三个月以内。目标设备数可能会包含大量的非活跃用户(如已卸载 App 的用户),这部分用户可能无法收到推送。 -successes| |本次推送成功设备数。推送成功对普通 Android 设备来说指目标设备收到了推送,对 iOS 设备或使用了混合推送的 Android 设备来说指消息成功推送给了 Apple APNs 或设备对应的混合推送平台。此外还有 `[lc/ios/fcm/hms/mi/oppo/vivo/meizu]Successes` 等分别代表通过各渠道推送成功的设备数。 -where| |本次推送查询 `_Installation` 表的条件,符合这些查询条件的设备将接收本条推送消息。 -errors| | 本次推送过程中的错误信息。 -from-service| | **push** 表示是直接发送的推送消息,**rtm** 表示是 RTM 离线推送消息。 -push-time| | 定时推送的发送时间 - -推送本质上是根据一个 query 条件来查询 `_Installation` 表里符合条件的设备,然后将消息推送给设备。因为 `_Installation` 是一个可以完全自定义属性的 Key-Value Object,因此可以实现各种复杂条件推送,例如频道订阅、地理位置信息推送、特定用户推送等。 - -对于 **devices** 和 **successes** 这两个属性,当 **devices** 值为 0 时,表示没有找到任何符合目标条件的设备,需要检查一下推送查询条件,这时没有设备能收到推送通知;当 **devices** 值不为 0 时,该值仅仅说明找到了这么多符合条件的设备,但并不保证这些设备都能收到推送通知,所以 **successes** 很可能是会小于 **devices** 的。特别是当查询出来的设备中含有大量的非活跃设备时,**successes** 可能会和 **devices** 有很大差距。 - -如果某个设备不想收到推送提醒,可以将 `_Installation` 表中相应安装对象的 `valid` 字段修改为 `false`。 - -注意:我们只保留最近一周的推送记录,并会对过期的推送记录定时进行清理。推送记录清理和推送消息过期时间无关,也就是说即使推送记录被清理,没有过期的推送消息依然是有效的,目标用户依然是能够收到消息。推送过期时间设置请参考[推送 REST API 使用指南](/sdk/push/guide/rest/)的《过期时间和定时推送》一节。 - -## Unity 推送 - -请阅读 [Unity 推送指南](/sdk/push/guide/unity/)。 - -## Unreal 推送 - -请阅读 [Unreal 推送指南](/sdk/push/guide/Unreal/)。 - -## iOS 推送 - -请阅读 [iOS 推送指南](/sdk/push/guide/ios/)。 - -## Android 推送 - -由于 Android 系统权限控制越来越严,推送服务自有通道的推送到达率受到影响。 -因此,建议商用版应用使用我们的「混合推送」方案,该方案对接了国内主流厂商 FCM 的接口,让开发者通过统一的 API 完成推送任务。 -详见 [Android 混合推送指南](/sdk/push/guide/android-mixpush/)。 - -如果想要使用推送服务自有通道推送,请阅读 [Android 推送指南](/sdk/push/guide/android/)。 - -## 使用 REST API 推送消息 - -请阅读[推送 REST API 使用指南](/sdk/push/guide/rest/)。 - -## 云引擎下通过 JavaScript SDK 创建推送 - -JavaScript SDK 也提供了创建推送的接口,使用场景主要面向云引擎。请参考 SDK 的 API 文档 [AV.Push](https://leancloud.github.io/javascript-sdk/docs/AV.Push.html)。 -这里举两个简单的例子: - -推送给所有订阅了 `public` 频道的设备: - -```js -AV.Push.send({ - channels: [ 'public' ], - data: { - alert: 'public message' - } -}); -``` - -如果希望按照某个 `_Installation` 表的查询条件来推送,例如推送给某个 `installationId` 的 Android 设备,可以传入一个 `AV.Query` 对象作为 `where` 条件: - -```js -const query = new AV.Query('_Installation'); -query.equalTo('installationId', installationId); -AV.Push.send({ - where: query, - data: { - alert: 'Public message' - } -}); -``` diff --git a/leancloud/docs/sdk/push/guide/push-faq.mdx b/leancloud/docs/sdk/push/guide/push-faq.mdx deleted file mode 100644 index d489680f7..000000000 --- a/leancloud/docs/sdk/push/guide/push-faq.mdx +++ /dev/null @@ -1,210 +0,0 @@ ---- -title: 推送常见问题 -sidebar_label: 常见问题 -sidebar_position: 11 -slug: /sdk/push/guide/push-faq/ ---- - - - - - -### 为什么成功设备数小于目标设备数? - -「目标设备数」是指符合本次推送查询条件的有效设备数量,「成功设备数」是指这次推送成功到达的设备数量。没有到达设备一般有以下几种情况: - -- Android 设备中的应用被杀掉,在网络中处于离线状态。对于这种情况,稍等一段时间待设备上线后,成功设备数就会逐渐增加。同时推荐集成 [混合推送](/sdk/push/guide/android-mixpush/) 来提升推送到达率。 -- Android 用户删掉或重装应用生成了新的设备数据,之前的无效数据被包含到了「目标设备数」中。 -- iOS 用户删掉或重装了应用,之前的无效数据被包含到了「目标设备数」中,这些数量就是 invalidTokens 数量。 - -### 为什么只能给三个月内活跃过的设备发消息?能否发全量设备? - -我们只允许开发者给 `_Installation` 表中 **updatedAt** 值为**最近三个月以内**的设备推送消息。原因如下: - -- 三个月没有活跃过的设备,用户打开推送的概率已经非常小了,我们将这些不活跃的设备视为无效设备。 -- 成本原因。全量推送需要处理的无效设备数量会成倍增长,因而会消耗大量云端资源,其中一个能被用户感知到的影响就是推送时间,目标设备数成倍增长也意味着单次全量推送的时间也成倍增长。 - -基于以上考虑,我们默认只能向三个月以内活跃过的设备发消息。如果您确实需要给全量设备发消息,可以联系我们升级为企业版服务,我们将为您的应用创建独立集群来提供推送服务。 - -### 为什么推送记录的目标设备数为 0? - -在控制台的推送记录中,「目标设备数」是指符合本次推送查询条件的有效设备数量。该值为 0 时请检查**推送查询条件及目标设备是否有效**。 - -### 为什么推送记录的内容为空? - -在控制台的推送记录中,当针对不同目标设备类型设置了不同消息内容时,「内容」一栏会显示为空,需要点击消息的「ID」打开推送详情来查看具体的内容。 - -### 推送的到达率如何 - -关于到达率这个概念,业界并没有统一的标准。我们测试过,在线用户消息的到达率基本达到 100%。我们的 SDK 做了心跳和重连等功能,尽量维持对推送服务器的长连接存活,提升消息到达用户手机的实时性和可靠性。 - -### 使用 iOS 的 Token Authentication 证书推送是否区分测试环境和生产环境? - -使用 Token Authentication 也是区分生产和测试环境的,是使用 prod 参数用来区分。同一个 key 可以给测试环境发消息,也能给正式环境发消息。 - -但是同一个设备的 deviceToken 只能成功发送一个环境。要么是正式环境,要么是测试环境。现象是给一个 deviceToken 推送,如果 dev 成功了,prod 就会报 invalid Tokens,不会在两个环境同时发送成功。 - -### 有一些 iOS 设备收不到推送,到控制台查看推送记录,发现 invalidTokens 的数量大于 0,是怎么回事? - -invalidTokens 的数量由以下两部分组成: -* 选择的设备与选择的证书不匹配时,会增加 invalidTokens 的数量,例如使用开发证书给生产证书的设备推送。请检查 APNS 证书是否过期,并检查是否使用了正确的证书类型。 -* 目标设备移除或重装了对应的 App。 -* 保存 DeviceToken 时没有上传 team ID 也会报错 invalidTokens。 -* 使用 Token Authentication 的方式在控制台上传证书时,TeamID 或者 Topics 输入错误(Topics 是 App 的 Bundle ID)也会报错 invalidTokens。 - -### Android 消息接收能不能自定义 Receiver 不弹出通知。 - -可以。请参考 [消息推送开发指南](/sdk/push/guide/rest/#消息内容参数)。 - -如果要自定义 receiver,必须在消息的 data 里带上自定义的 action。客户端在接收到消息后,将广播 action 为您定义的值的 intent 事件,您的 receiver 里也必须带上 `intent-filter` 来捕获该 action 值的 intent 事件。 - -### Android 应用进程被杀掉后无法收到推送消息 - -iOS 能做到这点,是因为当应用进程关闭后,Apple 和设备的系统之间还会存在连接,这条连接跟应用无关,所以无论应用是否被杀掉,消息都能发送到 APNs 再通过这条连接发送到设备。但对 Android 来说,如果是国内用户,因为众所周知的原因,Google 和设备之间的这条连接是无法使用的,所以应用只能自己去保持连接并在后台持续运行,一旦后台进程被杀掉,就无法收到推送消息了。虽然 Android SDK 已经采取了各种办法保持应用在后台运行,但随着 Android 系统版本的升级,权限控制越来越严,第三方推送通道的生命周期受到较大限制。因此 Android SDK 推出了混合推送的方案,对接国内主流厂商,保障了主流 Android 系统上的推送到达率。详见 [Android 混合推送开发指南](/sdk/push/guide/android-mixpush/)。 - -### 推送记录会保留多久? - -推送记录会保留 7 天,7 天之前的推送记录无法查询。 - -### 同一个账号在两个设备登录过,两个设备都会收到推送信息吗? - -推送的时候是根据推送查询条件,在 _Installation 表中查找符合条件的目标设备来推送。只要查询条件能包含这两个设备,则两个设备都能收到推送。 - -如果是登录即时通讯系统,如果没有开启单点登录,用户在登录两个设备后,如果用户不在线会尝试给这两个设备都发离线消息推送。 - -### Android 非混合推送,控制台推送记录中显示推送成功,但 Android 设备实际没有收到推送,是什么原因? - -对于 Android 非混合推送设备,当返回的记录成功数为 1 时,表示一定收到了 SDK 确认收到该消息的回应。即此条推送消息一定是到达了设备。 -建议检查推送是否使用了自定义 Receiver 功能(消息中是否有 action 字段),消息到达后 SDK 会直接将消息转交给自定义 Receiver,由自定义 Receiver 完成推送提醒。这种情况需要检查自定义 Receiver 实现逻辑排查消息到达后为什么没有弹出提醒。 - -### 旧版本 Objective-C SDK 在 iOS 13 环境下,无法接收推送的解决办法。 - -在 iOS 13 环境下,由于苹果更改了基础框架的 API,导致旧版本的 Objc SDK(<= 11.6.6)无法上传有效的 device token。 - -解决办法是升级 SDK 版本到 v11.6.7 及以上,保存 deviceToken 的方法参见 [保存 Token](/sdk/push/guide/ios/#保存-token)。 - -旧版本的 Objective-C SDK(<= 11.6.6)的解决办法是按如下方式上传 device token: - -``` -NSUInteger dataLength = deviceToken.length; -if (dataLength > 0) { - const unsigned char *dataBuffer = deviceToken.bytes; - NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)]; - for (int i = 0; i < dataLength; ++i) { - [hexString appendFormat:@"%02.2hhx", dataBuffer[i]]; - } - [installation setDeviceToken:[hexString copy]]; - [installation saveInBackground]; -} -``` - -### iOS 推送报错 「部分推送被 APNs 拒绝,拒绝原因是: BadDeviceToken」 - -这个报错是因为推送环境不正确,例如推送时通过 prod 参数设置推送给测试环境的设备(即 `"prod": "dev"`),则生产环境的设备无法收到推送。 -同理,如果指定推送给生产环境的设备(即 `"prod": "prod"`),则测试环境的设备无法收到推送。 -关于 iOS 的推送环境参考:[推送环境](/sdk/push/guide/ios/#推送环境)。 - -### iOS 推送如何选择推送证书? -推送消息接口有一个参数:**prod**。 -**这个参数仅对 iOS 推送有效。** - -* 当使用 Token Authentication 鉴权方式发 iOS 推送时,该参数用于设置将推送发至 APNs 的开发环境(dev)还是生产环境(prod)。 -* 当使用证书鉴权方式发 iOS 推送时,该参数用于设置使用开发证书(dev)还是生产证书(prod)。在使用证书鉴权方式下,当设备在 Installation 记录中设置了 deviceProfile 时我们优先按照 deviceProfile 指定的证书推送。 - -### iOS 推送如何正确保存 deviceToken? - -iOS 系统重装、从备份恢复应用、在新设备上安装应用都会导致 device token 变化,因此 Apple 推荐 在应用每次启动时都去请求 APNs 的 device token,获取 token 后进行设置并保存 token。 除此以外,后端会统计 installation 的更新时间(updatedAt),据此清理长期未更新的 installation 数据。 所以我们建议开发者遵循 Apple 的推荐方式开发应用,以免有效 installation 数据被意外清理,以及因为 device token 过期无效而推送失败。 -示例代码可以参考:[iOS 消息推送开发指南](/sdk/push/guide/ios/#保存-token)。 - -### iOS 推送,APP 在前台时收不到推送如何处理? - -在推送记录中显示推送已经成功送达,但是手机端没有收到推送。这是因为 iOS 推送应用在前台时,推送默认不会显示在通知栏。如果应用在前端仍需要显示推送,需要使用 UNUserNotificationCenterDelegate 的代理方法 `userNotificationCenter:willPresentNotification:withCompletionHandler:` 来处理如何显示推送。 - -可选的推送展示方式有: -* UNNotificationPresentationOptionBadge:应用图标上增加 badge 的值。 -* UNNotificationPresentationOptionBanner:横幅展示推送。 -* UNNotificationPresentationOptionList:在通知中心展示推送。 -* UNNotificationPresentationOptionSound:播放推送的声音。 - -横幅展示推送的示例代码如下: - -```objc -#import "AppDelegate.h" -#import -#import - -@interface AppDelegate () -@end -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - - //此处省略 SDK 的初始化 - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - //设置代理对象 - center.delegate = self; - return YES; -} -- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{ - completionHandler(UNNotificationPresentationOptionBanner); -} -``` - -### 离线推送通知服务里,_Installation 是如何与 _User 里的用户 id 关联的? - -用户登录即时通信系统以后,服务器会将用户的 `clientId` 保存在登录设备的 `_Installation` 表的 `channels` 字段里,从而完成关联。当用户离线,有离线消息需要推送时,服务端会去 `_Installation` 表内找到 `channels` 字段包含目标 `clientId` 的设备来完成推送。 - -### 如果同一个设备上有两个应用,如何推送到指定的应用? - -_Installation 表记录了设备上生成的安装信息。可以在 _Installation 表新增一个字段来区分不同的应用。在推送消息的时候,如果选择推送全部用户,则两个应用都会收到推送。如果想要针对某一个应用推送,可以参考 [发送给特定的用户](/sdk/push/guide/android/#发送给特定的用户) 这个推送文档,只发送给指定的设备。 - -### _Installation 中的 valid 字段指的是什么,valid 为什么是 false? - -valid 表示当前这条设备记录是否有效,是 false 表示这条记录失效了,比如长时间未使用推送或者推送设备并未登录。 - -比如 Android 设备未执行以下代码打开[启动推送服务](/sdk/push/guide/android/#启动推送服务),valid 的值就会一直是 false。 - -``` -// 设置默认打开的 Activity -PushService.setDefaultPushCallback(this, PushDemo.class); -``` -如果某个设备不想收到推送提醒,也可以将 _Installation 表中相应安装对象的 valid 字段修改为 false。 - -## 推送问题排查 - -推送因为环节较多,跟设备和网络都相关,并且调用都是异步化,因此问题比较难以查找,这里提供一些有用助于排查消息推送问题的技巧。 - -### 推送结果查询 - -所有经过 `/push` 接口发出的消息的都可以在控制台的消息菜单里的推送记录看到。每次调用 `/push` 都将产生一条新的 推送记录表示一次推送。该表的各属性含义请参考 [Notification 表详解](/sdk/push/guide/overview/#notification) 。 - -`/push` 接口会返回新建的推送记录的 `objectId`,你就可以在推送记录里根据 ID 查询消息推送的结果。 - -### iOS 推送排查建议 - -iOS 推送问题的一些排查建议: - -* 请确保在项目 Info.plist 文件中使用了正确的 **Bundle Identifier**。 -* 请确保在 **Project** > **Build Settings** 设置了正确的 **provisioning profile**。 -* 尝试 clean 项目并重启 Xcode -* 尝试到 [Apple Developer](https://developer.apple.com/account/overview.action) 重新生成 **provisioning profile**,修改 Apple ID,再改回来,然后重新生成。你需要重新安装 provisioning profile,并在 **Project** > **Build Settings** 里重新设置。 -* 打开 XCode Organizer,从电脑和 iOS 设备里删除所有过期和不用的 provisioning profile。 -* 如果编译和运行都没有问题,但是你仍然收不到推送,请确保你的应用打开了接收推送权限,在 iOS 设备的 **设置** > **通知** > **你的应用** 里确认。 -* 如果权限也没有问题,请确保使用了正确的 **provisioning profile**。 打包你的应用。如果你上传了开发证书并使用开发证书推送,那么必须使用 **Development Provisioning Profile** 构建你的应用。如果你上传了生产证书,并且使用生产证书推送,请确保你的应用使用 **Distribution Provisioning Profile** 签名打包。**Ad Hoc** 和 **App Store Distribution Provisioning Profile** 都可以接收使用生产证书发送的消息。 -* 当在一个已经存在的 Apple ID 上启用推送,请记得重新生成 **provisioning profile**,并到 XCode Organizer 更新。 -* 生产环境的推送证书必须在提交到 App Store 之前启用推送并生成,否则你需要重新提交 App Store。 -* 请在提交 App Store 之前,使用 Ad Hoc Profile 测试生产环境推送,这种情况下的配置最接近于 App Store。 -* 检查消息菜单里的推送记录中的 `devices` 和 `status`,确认推送状态和接收设备数目正常。 -* 检查消息菜单里的推送记录中的 `invalidTokens` 字段,如果该数字异常大,可能证书选择错误,跟设备 build 的 provisioning profile 不匹配。 -* 建议使用串行队列操作 installation,以免保存 installation 时因为多线程写入而崩溃。可以参考 [Swift Demo 中 VoIP 项目的做法][swift-voip-demo]。 - -[swift-voip-demo]: https://github.com/leancloud/swift-sdk-demo#voip - -### Android 排查建议 - -Android 推送问题的一些建议和提示: - -* 请确保设备正确调用了 `AVInstallation` 保存了设备信息到 _Installation 表。 -* 可以在控制台的 **推送** > **设备** 根据 `installationId` 查询设备是否在线。 -* 请确保 `com.avos.avoscloud.PushService` 添加到 AndroidManifest.xml 文件中。 -* 如果使用自定义 Receiver,请确保在 AndroidManifest.xml 中声明你的 Receiver,并且保证 data 里的 action 一致。 diff --git a/leancloud/docs/sdk/push/guide/rest.mdx b/leancloud/docs/sdk/push/guide/rest.mdx deleted file mode 100644 index 4f9775106..000000000 --- a/leancloud/docs/sdk/push/guide/rest.mdx +++ /dev/null @@ -1,991 +0,0 @@ ---- -title: 推送 REST API -sidebar_position: 8 -slug: /sdk/push/guide/rest/ ---- - - - - - -import { Conditional } from "/src/docComponents/conditional"; - -当 App 安装到用户设备后,如果要使用推送功能,云服务 SDK 会自动生成一个 Installation 对象。Installation 对象包含了推送所需要的所有信息。你可以使用 REST API,通过 Installation 对象进行推送。 - - - -请求的 Base URL 可以在**应用 > 设置 > 应用凭证 > 服务器地址**查看。 -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 - - - - - -请求的 Base URL 可以在**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置**查看。 -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 - - - - -请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,详见《存储 REST API》中 [请求格式](/sdk/storage/guide/rest/#请求格式) 一节的说明。 - -## Installation - -你可以通过 REST API 在云端增加安装对象。 -使用 REST API 还可以达成一些客户端 SDK 无法完成的操作,比如查询所有的 installation 来找到一个 channel 的订阅者的集合。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    URLHTTP功能
    /1.1/installationsPOST上传安装数据
    /1.1/installations/<objectId>GET获取安装数据
    /1.1/installations/<objectId>PUT更新安装数据
    /1.1/installationsGET查询安装数据
    /1.1/installations/<objectId>DELETE删除安装数据
    - -### 增加 Installation - -创建一个安装对象和普通的对象差不多,只是不同平台有不同的字段。 - -创建成功后,HTTP 的返回值为 **201 Created**,Location header 包括了新的安装的 URL: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -返回的 body 是一个 JSON 对象,包括了 objectId 和 createdAt 这个创建对象的时间戳。 - -```json -{ - "createdAt": "2012-04-28T17:41:09.106Z", - "objectId": "51ff1808e4b074ac5c34d7fd" -} -``` - -#### DeviceToken - -iOS 设备通常使用 DeviceToken 来唯一标识一台设备。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "deviceType": "ios", - "deviceToken": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", - "channels": [ - "public", "protected", "private" - ] - }' \ - https://{{host}}/1.1/installations -``` - -#### installationId - -对于 Android 设备,SDK 会自动生成 uuid 作为 installationId 保存到云端。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "deviceType": "android", - "installationId": "12345678-4312-1234-1234-1234567890ab", - "channels": [ - "public", "protected", "private" - ] - }' \ - https://{{host}}/1.1/installations -``` - -`installationId` 必须在应用内唯一。 - -### 获取 Installation - -你可以通过 GET 方法请求创建的时候 Location 表示的 URL 来获取 Installation 对象。比如,获取上面创建的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -返回的 JSON 对象包含所有用户提供的字段,加上 createdAt、updatedAt 和 objectId 字段: - -```json -{ - "deviceType": "ios", - "deviceToken": "abcdefghijklmnopqrstuvwzxyrandomuuidforyourdevice012345678988", - "channels": [ - "" - ], - "createdAt": "2012-04-28T17:41:09.106Z", - "updatedAt": "2012-04-28T17:41:09.106Z", - "objectId": "51ff1808e4b074ac5c34d7fd" -} -``` - -### 更新 Installation - -安装对象可以向相应的 URL 发送 PUT 请求来更新。例如,通过设置 `channels` 属性来订阅某个推送频道: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "deviceType": "ios", - "deviceToken": "abcdefghijklmnopqrstuvwzxyrandomuuidforyourdevice012345678988", - "channels": [ - "", - "foo" - ] - }' \ - https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -再比如退订一个频道: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "channels": { - "__op":"Remove", - "objects":["customer"] - } - }' \ - https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -`channels` 本质上是数组属性,因此可以使用标准的数组操作。 - -又比如添加自定义属性: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "userObjectId": "<用户的 objectId>" - }' \ - https://{{host}}/1.1/installations/51ff1808e4b074ac5c34d7fd -``` - -### 查询 Installation - -你可以一次通过 GET 请求到 installations 的根 URL 来获取多个安装对象。这项功能在 SDK 中不可用。 - -没有任何 URL 参数的话,一个 GET 请求会列出所有安装: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/installations -``` - -返回的 JSON 对象的 results 字段包含了所有的结果: - -```json -{ - "results": [ - { - "deviceType": "ios", - "deviceToken": "abcdefghijklmnopqrstuvwzxyrandomuuidforyourdevice012345678988", - "channels": [ - "" - ], - "createdAt": "2012-04-28T17:41:09.106Z", - "updatedAt": "2012-04-28T17:41:09.106Z", - "objectId": "51ff1808e4b074ac5c34d7fd" - }, - { - "deviceType": "ios", - "deviceToken": "876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9", - "channels": [ - "" - ], - "createdAt": "2012-04-30T01:52:57.975Z", - "updatedAt": "2012-04-30T01:52:57.975Z", - "objectId": "51fcb74ee4b074ac5c34cf85" - } - ] -} -``` - -所有对普通的对象的查询都对 installation 对象起作用,所以可以查看之前的查询部分以获取详细信息。通过做 channels 的数组查询,你可以查找一个订阅了给定的推送频道的所有设备。 - -出于安全性考虑,云端默认未开放 installation 查找权限,所以通常这个接口需要使用 master key 鉴权。 - -### 删除 Installation - -为了从 LeanCloud 中删除一个安装对象,可以发送 DELETE 请求到相应的 URL。这个功能在客户端 SDK 也不可用。举例: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/installations/51fcb74ee4b074ac5c34cf85 -``` - -出于安全性考虑,云端默认未开放 installation 删除权限,所以通常这个接口需要使用 master key 鉴权。 - -### Installation 自动过期和清理 - -每当用户打开应用,我们都会更新该设备的 `_Installation` 表中的 `updatedAt` 时间戳。如果用户长期没有更新 `_Installation` 表的 `updatedAt` 时间戳,也就意味着该用户长期没有打开过应用。当超过 90 天没有打开过应用时,我们会将这个用户在 `_Installation` 表中的记录删除。不过请不要担心,当用户再次打开应用的时候,仍然会自动创建一个新的 Installation 用于推送。 - -对于 iOS 设备,除了上述过期机制外还多拥有一套过期机制。当我们根据 Apple 推送服务的反馈获取到某设备的 deviceToken 已过期时,我们也会将这个设备在 `_Installation` 表中的信息删除,并标记这个已过期的 deviceToken 为无效,丢弃后续所有发送到该 deviceToken 的消息。 - -### API 接口一览 - -Path|Method|描述 ----|---|--- -/1.1/push|POST|推送通知 -/1.1/notifications|GET|查询推送记录 -/1.1/notifications/:notification_id|GET|根据 ID 查推送记录 -/1.1/notifications/:notification_id|DELETE|根据 ID 删推送记录 -/1.1/scheduledPushMessages|GET|查询应用下所有的定时推送 -/1.1/scheduledPushMessages/:id|DELETE|根据 ID 删定时推送 - -### master key 校验 - -当在**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置**中点选了 **禁止从客户端进行消息推送** 后, -必须通过 **master key** 才能发送推送,从而避免了客户端可以不经限制的给应用内任意目标设备推送消息的可能。 -这一限制默认为启用状态。 -我们建议用户都将此限制启用。 - -### 消息内容参数 - -#### iOS 设备推送消息内容参数 - -iOS 设备中 data 和 alert 内属性的具体含义请参考: -1. [Apple 官方关于 Payload Key 的文档](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification), -2. [Apple 官方关于 Request Header 的文档](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html),以及 -3. [Apple 官方关于 UserNotifications 的文档](https://developer.apple.com/documentation/usernotifications)。 - -下面是对各属性的一些具体说明: - -#### iOS 设备 data 各属性说明 - -名称|格式|约束|描述 ----|---|---|--- -alert|普通字符串或 JSON 字符串|必填|表示消息内容。如果目标设备中只包含 iOS 设备则还可以是 JSON 类型,下面详述 JSON 类型时支持的属性, -title|字符串|可选|表示推送内容标题,如果 alert 字段为字符串可以在此补充提供 title,如果 alert 是 JSON 类型则无需再提供本字段, -category|字符串|可选|通知类型, -thread-id|字符串|可选|通知分类名称, -badge|数字|可选|未读消息数目,应用图标边上的小红点数字,可以是数字,也可以是字符串 "Increment"(大小写敏感), -sound|普通字符串或 JSON 字符串|可选|指定推送声音信息,下面详述 JSON 类型时支持的属性, -content-available|数字|可选|如果使用 Newsstand,设置为 1 来开始一次后台下载, -mutable-content|数字|可选|用于支持 UNNotificationServiceExtension 功能,设置为 1 时启用, -collapse-id|字符串|可选|对应 APNs request header 的 apns-collapse-id 参数,用于多条推送合并展示,具体请点击下面 Apple 官方关于 Request Header 的文档链接进行查阅, -apns-priority|数字|可选|只能是 10 或 5,对应 APNs request header 的 apns-priority 参数,用于控制是否以节电模式发推送,具体请点击下面 Apple 官方关于 Request Header 的文档链接进行查阅, -apns-push-type|字符串|可选|用于设置推送展示类型,在 iOS 13 或 watchOS 6 以上设备支持,只能为 "background" 或 "alert",默认为 "alert", -url-args|字符串|可选|列表类型,用于 Safari 推送,详情见 APNs 文档关于 url-args 参数的描述, -target-content-id|字符串|可选|详情见 APNs 文档关于 target-content-id 参数的描述, -自定义属性|自定义类型|可选|由用户添加的自定义属性,字段名和字段类型任意。 - -示例: - -```json -{ - "alert": "hi" -} -``` - -#### iOS 设备 alert 各属性说明 - -iOS 设备支持 `alert` 本地化消息推送,只需要将上面 `alert` 参数从 String 替换为一个由本地化消息推送属性组成的 JSON 即可: - -名称|格式|约束|描述 ----|---|---| --- -title|字符串|可选|表示推送内容标题, -title-loc-key|字符串列表|可选|详情请参看 Apple 关于推送提醒本地化的说明, -title-loc-args|字符串列表|可选|详情请参看 Apple 关于推送提醒本地化的说明, -subtitle|字符串|可选|表示推送内容副标题, -subtitle-loc-key|字符串|可选|详情请参看 Apple 关于推送提醒本地化的说明, -subtitle-loc-args|字符串列表|可选|详情请参看 Apple 关于推送提醒本地化的说明, -body|字符串|可选|表示消息内容, -action-loc-key|字符串|可选|详情请参看 Apple 关于推送提醒本地化的说明, -loc-key|字符串|可选|详情请参看 Apple 关于推送提醒本地化的说明, -loc-args|字符串列表|可选|详情请参看 Apple 关于推送提醒本地化的说明, -launch-image|字符串|可选|设置点击推送后启动图片文件名, -summary-arg|字符串|可选|用于设置 Summary, -summary-arg-count|数字|可选|用于设置 Summary 参数数量 - -示例: - -```json -{ - "alert": { - "title": "A message" - "body": "A body" - } -} -``` - -#### iOS 设备 sound 各属性说明 - -iOS 支持通过 sound 参数设置推送声音,可以是字符串类型的声音文件名,指向一个在应用内存在的声音文件,或是 JSON 类型: - -名称|格式|约束|描述 ----|---|---|--- -name|字符串|可选|声音文件名,指向一个在应用内存在的声音文件 -critical|布尔类型|可选|true 表示使用 "Critical" 提示音,默认为 false -volume|数字类型|可选|指定声音大小,必须为 0 到 1 之间的小数 - -示例: - -```json -{ - "alert": "New weixin message.", - "badge": 9, - "sound": "biubiubiu.aiff" -} -``` - -```json -{ - "alert": "消费 13 元", - "sound": { - "name": "ding.aiff", - "volume": "0.8" - } -} -``` - -#### iOS 设备其他说明 - -我们也支持按照上述 Apple 官方文档的方式构造推送参数,例如: - -```json -{ - "aps": { - "alert": "New weixin message.", - "badge": 9, - "sound": "biubiubiu.aiff" - } -} -``` - -更详细的描述如下面例子描述: - -```json -{ - "aps": { - "alert": { - "title": "字符串类型,表示推送内容标题", - "title-loc-key": "字符串列表,详情请参看 Apple 关于推送提醒本地化的说明", - "title-loc-args": "字符串列表类型,详情请参看 Apple 关于推送提醒本地化的说明", - "subtitle": "字符串类型,表示推送内容副标题", - "subtitle-loc-key": "字符串类型,详情请参看 Apple 关于推送提醒本地化的说明", - "subtitle-loc-args": "字符串列表类型,详情请参看 Apple 关于推送提醒本地化的说明", - "body": "字符串类型,表示消息内容", - "action-loc-key": "字符串类型,详情请参看 Apple 关于推送提醒本地化的说明", - "loc-key": "字符串类型,详情请参看 Apple 关于推送提醒本地化的说明", - "loc-args": "字符串列表类型,详情请参看 Apple 关于推送提醒本地化的说明", - "launch-image": "字符串类型,设置点击推送后启动图片文件名", - "summary-arg": "字符串类型,用于设置 Summary", - "summary-arg-count": "数字类型,用于设置 Summary 参数数量" - }, - "category": "字符串类型,通知类型", - "thread-id": "字符串类型,通知分类名称", - "badge": "数字类型,未读消息数目,应用图标边上的小红点数字,可以是数字,也可以是字符串 Increment(大小写敏感)", - "sound": "普通字符串或 JSON 字符串类型,指定推送声音信息", - "content-available": "数字类型,如果使用 Newsstand,设置为 1 来开始一次后台下载", - "mutable-content": "数字类型,用于支持 UNNotificationServiceExtension 功能,设置为 1 时启用" - }, - "collapse-id": "字符串类型,对应 APNs request header 的 apns-collapse-id 参数,用于多条推送合并展示,具体请点击下面 Apple 官方关于 Request Header 的文档链接进行查阅", - "apns-priority": "数字类型,只能是 10 或 5,对应 APNs request header 的 apns-priority 参数,用于控制是否以节电模式发推送,具体请点击上面 Apple 官方关于 Request Header 的文档链接进行查阅", - "apns-push-type": "字符串类型,用于设置推送展示类型,只能为 background,voip,complication,fileprovider,mdm,alert,默认为 alert", - "custom-key": "由用户添加的自定义属性,字段名和字段类型任意,custom-key 仅是举例,可随意替换。" -} -``` - -#### Android 设备通用推送消息内容参数 - -如果是 Android 设备,默认的消息栏通知消息内容参数支持下列属性: - -名称|格式|约束|描述 ----|---|---|--- -alert|字符串|必填|表示消息内容。 -title|字符串|可选|表示显示在通知栏的标题。 -silent|布尔|可选|指定透传消息或通知栏消息,默认为 false,即 `通知栏消息`。 -action|字符串|可选|注册 Receiver 时提供的 action name,仅当需要自定义 Receiver 时设置。仅支持自有推送,不支持厂商推送。 -自定义名称|任意类型|可选|由用户添加的自定义属性,字段名和字段类型任意。 - -示例: - -```json -{ - "alert": "你好小明,家里来客人了,快回家吃饭!", - "title": "小明,您收一条微信消息" -} -``` - -```json -{ - "alert": "支付宝到账 13 元!", - "my-custom-key": "my-custom-value" -} -``` - -关于 `silent` 参数请参看 [Android 推送区分透传和通知栏消息](#android-推送区分透传和通知栏消息),关于自定义 Receiver 请参看《Android 消息推送开发指南》的《自定义 Receiver》一节。 - -#### 为多种类型设备设置不同推送内容 - -单次推送中,如果查询条件覆盖的目标推送设备包含多种类型,如既包含 iOS 设备,又包含云服务自有渠道的 Android 设备,又有混合推送的小米华为设备等,可以为不同推送设备单独填写推送内容参数,我们会按照设备类型取出对应设备类型的推送内容来发推送,例如: - -```json -{ - "alert": "Body default", - "title": "Title default", - "mi": { - "title": "Title for xiaomi" - }, - "hms": { - "title": "Title for huawei" - }, - "vivo": { - "title": "Title for vivo", - "alert": "body for vivo", - "pushMode": 1 - } -} -``` - -上述示例中,我们向不同类型的设备推送了不同的标题。 -另外,每个厂商所支持的参数都不尽相同,我们对最常用的参数做了适配,为不同设备设置不同的推送内容也意味着可以针对不同的设备使用相应厂商特有的推送参数。 -比如上面我们在推送给 vivo 设备时设定了 `pushMode` 参数,这个参数是 vivo 特有的参数,用来标记推送模式是正式推送还是测试推送。 - -其中属性名称和推送平台对应关系如下: - -属性名称 | 平台 --------- | ---- -ios | Apple APNs -android | 云服务自有 Android 平台 -mi | 小米推送 -hms | 华为 HMS 推送 (仅国内版适用) -mz | 魅族推送 (仅国内版适用) -vivo | vivo 推送 (仅国内版适用) -oppo | oppo 推送 (仅国内版适用) -fcm | FCM 推送(仅国际版适用) - -### 通过查询条件发推送 - -本接口用于根据提供的查询条件,给在 _Installation 表内所有符合查询条件的有效设备记录发推送消息。例如下面是给所有在 _Installation 表中 `channels` 字段包含 `public` 值的有效设备推送一条内容为 `Hello from LeanCloud` 的消息。 - -请注意,本接口限制请求的 HTTP Body 大小必须小于 4096 个字节,即你调用本接口传递的所有参数做 JSON 序列化后得到的结果不能超过此限制。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "where": {"channels" : "public"}, - "data": {"alert" : "Hello from LeanCloud"} - }' \ - https://{{host}}/1.1/push -``` - -本接口支持的参数如下: - -名称| 约束 | 描述 ----|--- | --- -data| **必填**| 推送的内容数据,JSON 对象,请参考 [消息内容](#消息内容参数)。 -where| 可选 | 检索 `_Installation` 表使用的查询条件,JSON 对象。如果查询条件内包含日期或二进制等需要做编码的特殊类型数据,查询条件内需要包含编码后的数据。如查询 `createdAt` 字段大于某个时间的设备,where 条件需要为 `{"createdAt":{"$gte":{"__type":"Date","iso":"2015-06-21T18:02:52.249Z"}}}`。更多信息请参看[数据存储 REST API 使用详解](/sdk/storage/guide/rest/)的《数据类型》一节的说明。 -channels| 可选 | 推送给哪些频道,将作为条件加入 where 对象。 -push_time| 可选 | 设置定时推送的发送时间,需为 UTC 时间且符合 ISO8601 格式要求,例如:`2019-04-01T06:19:29.000Z`。请注意发送时间与当前时间如果小于 1 分钟则推送会立即发出,不会遵循 push_time 参数要求。如果需要实现周期推送,可以参照 [使用云引擎实现周期推送](#使用云引擎实现周期推送) 实现。 -expiration_time| 可选 | 消息过期的绝对日期时间,需为 UTC 时间且符合 ISO8601 格式要求,例如:"2019-04-01T06:19:29.000Z"。如果客户端收到消息的时间超过消息过期的时间,那么消息将不显示给用户。 -expiration_interval| 可选 | 消息过期的相对时间,单位是秒。从 `push_time` 开始算起,未指定 `push_time` 时从调用 API 的时间开始算起。建议推送都设置 `expiration_time` 或 `expiration_interval`,避免用户长时间断网并重新联网后收到一大堆已失效的推送信息。 -notification_id | 可选 | 自定义推送 id,最长 16 个字符且只能由英文字母和数字组成,不提供该参数时我们会为每个推送请求随机分配一个唯一的推送 id,用于区分不同推送。我们会根据推送 id 来统计推送的目标设备数和最终消息到达数,并展示在云服务控制台的推送记录中。用户自定义推送 id 可以将多个不同的请求并入同一个推送 id 下从而整体统计出这一批推送请求的目标设备数和最终消息到达数。 -req_id | 可选 | 自定义请求 id,最长 16 个字符且只能由英文字母和数字组成。5 分钟内带有相同 req_id 的不同推送请求我们认为是重复请求,只会发一次推送。用户可以在请求中带着唯一的 req_id 从而在接口出现超时等异常时将请求重发一次,以避免漏掉失败的推送请求。并且由于前后两次请求中 req_id 相同,我们会自动过滤重复的推送请求以保证每个目标终端用户最多只会收到一次推送消息。**重发过频或次数过多会影响正常的消息推送**,请注意控制。 -prod| 可选 | ***仅对 iOS 推送有效***。当使用 Token Authentication 鉴权方式发 iOS 推送时,该参数用于设置将推送发至 APNs 的开发环境(***dev***)还是生产环境(***prod***)。当使用证书鉴权方式发 iOS 推送时,该参数用于设置使用开发证书(***dev***)还是生产证书(***prod***)。未传入 `prod` 时,如果传入了 `X-LC-Prod` HTTP 头,且其值不为 1,那么视同 `"prod": "dev"`,否则默认 `"prod": "prod"`。在使用证书鉴权方式下,当设备在 Installation 记录中设置了 deviceProfile 时我们优先按照 deviceProfile 指定的证书推送。 -topic | 可选 | ***仅对使用 Token Authentication 鉴权方式的 iOS 推送有效***。当使用 Token Authentication 鉴权方式发 iOS 推送时需要提供设备对应的 APNs Topic 做鉴权。一般情况下,iOS SDK 会自动读取 iOS app 的 bundle ID 作为 topic 存入 Installation 记录的 apnsTopic 字段,所以推送请求中无需带有该参数。但以下情况需要手工指定: 1. 使用低于 v4.2.0 的 iOS SDK; 2. 不使用 iOS SDK (如 React Native);3. 推送目标设备使用的 topic 与 iOS Bundle ID 不同。 -apns_team_id | 可选 | ***仅对使用 Token Authentication 鉴权方式的 iOS 推送有效***。当使用 Token Authentication 鉴权方式发 iOS 推送时需要提供设备对应的 Team ID 做鉴权。一般情况下如果你配置的所有 Team ID 下的 APNs Topic 均不重复,或在存储 Installation 时主动设置过 apnsTeamId 值,则无需提供本参数,我们会为每个设备匹配对应的 Team ID 来发推送。否则必须提供本参数且需要通过 where 查询条件保证单次推送请求的目标设备均属于本参数指定的 Team ID,以保证推送正常进行。 -flow_control | 可选 | 是否开启平缓发送,默认不开启。其值代表推送的速度,即每秒推送的目标终端用户数。最低值 1000,低于最低值按最低值计算。 -_notificationChannel | 可选 | Android 8.0 以上设备在推送时需要传递 channel id 才能正常接收推送,请参看 [Android 推送指南](/sdk/push/guide/android/)的《Android 8.0 推送适配》一节。 - -`_Installation` 表中的所有属性,无论是内置的还是自定义的,都可以作为查询条件通过 where 来指定,并且支持各种复杂查询。 - -成功时会返回本次推送的 objectId,后续可以用返回的 objectId [查询推送记录](#推送记录查询)。 - -```json -{"objectId":"i4OcyCnyjckJOtzz","createdAt":"2021-11-23T08:05:54.921Z"} -``` - -失败时会返回错误码和错误原因: - -```json -{"code":107,"error":"Malformed json object. A json dictionary is expected."} -``` - -下面会举一些例子,更多例子请参考[数据存储 REST API 使用详解](/sdk/storage/guide/rest/)的《查询》一节。 - -#### 推送给所有的设备 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "data": { - "alert": "问好!" - } - }' \ - https://{{host}}/1.1/push -``` - -#### 推送给 android 设备 - -```sh -curl -X POST \ --H "X-LC-Id: {{appid}}" \ --H "X-LC-Key: {{masterkey}},master" \ --H "Content-Type: application/json" \ --d '{ - "where":{ - "deviceType": "android" - }, - "data": { - "alert": "问好!" - } - }' \ -https://{{host}}/1.1/push -``` - -#### 推送给 public 频道的设备 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "channels":"public", - "data": { - "alert": "问好!" - } - }' \ - https://{{host}}/1.1/push -``` - -#### 推送给不活跃的设备 - -```sh -curl -X POST \ --H "X-LC-Id: {{appid}}" \ --H "X-LC-Key: {{masterkey}},master" \ --H "Content-Type: application/json" \ --d '{ - "where":{ - "updatedAt":{ - "$lt":{"__type":"Date","iso":"2015-06-29T11:33:53.323Z"} - } - }, - "data": { - "alert": "问好!" - } - }' \ -https://{{host}}/1.1/push -``` - -#### 推送给自定义属性符合条件的设备 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "where": { - "preOrder": true - }, - "data": { - "alert": "抢购开始!" - } - }' \ - https://{{host}}/1.1/push -``` - -用 `where` 查询的都是 `_Installation` 表中的属性。这里假设该表存储了 `preOrder` 的布尔属性。 - -#### 根据地理信息位置做推送 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "where": { - "owner": { - "$inQuery": { - "location": { - "$nearSphere": { - "__type": "GeoPoint", - "latitude": 30.0, - "longitude": -20.0 - }, - "$maxDistanceInMiles": 10.0 - } - } - } - }, - "data": { - "alert": "北京明日最高气温 40 摄氏度。" - } - }' \ - https://{{host}}/1.1/push -``` - -上面的例子假设 installation 有个 owner 属性指向 `_User` 表的记录,并且用户有个 `location` 属性是 GeoPoint 类型,我们就可以根据地理信息位置做推送。 - -### 通过设备 ID 列表发推送 - -本接口用于给一批指定的设备 ID 发推送,推送过程因为不用查询目标设备的 Installation 记录,推送速度相对查询方式来说会更快,延迟相对更低。例如下面是给 device token 为 "device_token1", "device_token2", "device_token3" 的 iOS 设备推送一条内容为 `Hello` 的消息。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "data": {"alert" : "Hello"}, - "device_type": "ios", - "device_ids": ["device_token1", "device_token2", "device_token3"] - }' \ - https://{{host}}/1.1/push/devices -``` - -本接口的参数由推送渠道无关的通用参数和推送渠道相关的渠道参数两部分组成。通用参数因为跟具体推送渠道无关,无论是给 iOS 设备还是混合推送、非混合推送的 Android 设备发推送都可以带上。 - -支持的通用参数如下: - -名称| 约束 | 描述 ----|--- | --- -device_type | **必填** | 目标设备类型,目前只能为 android、ios 两种。一次推送只能给一种类型的设备发推送。 -device_ids | **必填** | 目标设备 ID 列表,最多包含 500 个 ID 。对于 iOS 设备来说,设备 ID 是 _Installation 表中的 deviceToken 字段;对于使用混合推送的 Android 设备来说,设备 ID 是 _Installation 表中的 registrationId 字段;对于非混合推送的 Android 设备来说,设备 ID 是 _Installation 表中的 installationId 字段。 -data| **必填**| 同[通过查询条件发推送](#通过查询条件发推送)。 -expiration_interval| 可选 | 同上。 -expiration_time| 可选 | 同上。 -notification_id | 可选 | 同上。 -req_id | 可选 | 同上。 - -如果目标设备为 iOS 设备,则在上述通用参数之外,还可以附带如下参数: - -名称 | 约束 | 描述 ----- | ---- | ---- -prod| 可选 | 同[通过查询条件发推送](#通过查询条件发推送)。 -topic | 可选 | 同上。 -apns_team_id | 可选 | 同上。 -device_profile | 可选 | 用于指定使用的 iOS 自定义推送证书。如果使用 Token Authentication 鉴权方式,或者使用的推送证书为配置的「生产环境证书」或「开发环境证书」则无需提供本参数。我们会根据你填写的 `prod` 参数值来使用对应的证书。 - -如果目标为 Android 设备,则在前述通用参数之外,还可以附带如下参数: - -名称 | 约束 | 描述 ----- | ---- | ---- -channel| 可选 | 指定 [Android 通知渠道][android-channel]。 -vendor | 可选 | ***仅对开启混合推送的设备有效*** 对应混合推送设备在 _Installation 表中的 vendor 字段。一次推送接口调用只能将推送发送给相同 vendor 的设备。 -device_profile | 可选 | ***仅对开启混合推送的设备有效*** 当目标混合推送平台下配置了多份配置时需要通过该参数指定配置名。默认值为 _default - -[android-channel]: https://developer.android.com/guide/topics/ui/notifiers/notifications.html?hl=zh-cn#ManageChannels - -### Android 混合推送多配置区分 - -如果使用了混合推送功能,并且在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > 混合推送** 增加了多个混合推送配置,那么在向 `_Installation` 表保存设备信息时就需要将当前设备所对应的混合推送配置名存入 `deviceProfile` 字段。系统会按照该字段指定的唯一配置名为每个目标设备进行混合推送。 - -如果 `deviceProfile` 字段为空,系统会默认使用名为 `_default` 的混合推送配置来进行推送,所以一定要保证在控制台的混合推送设置中,存在以 `_default` 命名的 Profile 并且已被正确配置,否则系统会**拒绝推送。** - -`deviceProfile` 的值必须以字母开头,由大小写字母、数字和下划线组成的字符串,或为空值。 - -### Android 推送区分透传和通知栏消息 - -Android 推送(包括 Android 混合推送)支持透传和通知栏两种消息类型。透传消息是指消息到达设备后会先交给 LeanCloud Android SDK,再由 SDK 将消息通过自定义 Receiver 传递给开发者,收到消息后的行为由开发者定义的 Receiver 来决定,SDK 不会自动弹出通知栏提醒。而通知栏消息是指消息到达设备后会立即自动弹出通知栏提醒。 - -推送服务通过推送请求中 `data` 参数内的 `silent` 字段区分透传和通知栏消息。 -`silent` 为 `true` 表示这个消息是透传消息,为 `false` 表示消息是通知栏消息。 -如果不传递 `silent` 则默认其值为 `false`。 -另外请注意,如果希望接收透传消息请不要忘记自行实现自定义 Receiver,参见 [Android 推送指南](/sdk/push/guide/android/)的《自定义 Receiver》一节的说明。 - -### 过期时间和定时推送 - -如上所述,可以用 `expiration_time` 参数指定消息的过期时间: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "expiration_time": "2015-10-07T00:51:13Z", - "data": { - "alert": "你的优惠券将于 10 月 7 日到期。" - } - }' \ - https://{{host}}/1.1/push -``` - -`expiration_interval` 也可以用于指定过期时间,一般结合定时推送使用: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{ - "push_time": "2016-01-28T00:07:29.773Z", - "expiration_interval": 86400, - "data": { - "alert": "北京时间 1 月 28 日 8:07 发送这条推送,24 小时(86400 秒)后过期" - } - }' \ - https://{{host}}/1.1/push -``` - -#### 定时推送任务查询和取消 - -调用 `POST /scheduledPushMessages` 接口可以查询当前正在等待推送的定时推送任务,调用这个接口需要使用 **master key**: - - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/scheduledPushMessages -``` - -查询出来的结果类似: - -```json -{ - "results": [ - { - "id": 1, - "expire_time": 1373912050838, - "push_msg": { - "through?": null, - "app-id": "OLnulS0MaC7EEyAJ0uA7uKEF-gzGzoHsz", - "where": { - "sort": { - "createdAt": 1 - }, - "query": { - "installationId": "just-for-test", - "valid": true - } - }, - "prod": "prod", - "api-version": "1.1", - "msg": { - "message": "test msg" - }, - "id": "XRs9jmWnLd0GH2EH", - "notificationId": "mhWjvHvJARB6Q6ni" - }, - "createdAt": "2016-01-21T00:47:46.000Z" - } - ] -} -``` - -其中 `push_msg` 就是该推送消息的详情,`expire_time` 是消息设定推送时间的 unix 时间戳。 - -根据查询的结果,就可以取消一个定时推送任务。 -注意需要使用返回结果中最外层的 id。 -比如取消第一个定时推送,需要使用 `results[0].id`,而不是 `results[0].push_msg.id`。 -就上面的例子而言,应该使用 `1` 而不是 `XRs9jmWnLd0GH2EH`: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/scheduledPushMessages/1 -``` - -#### 使用云引擎实现周期推送 - -可以配合云引擎的定时任务功能实现周期推送,非常方便。请阅读《在云引擎中使用推送服务》。 - -## 推送记录查询 - -`/push` 接口在推送后会返回代表该条推送消息的 `objectId`,你可以使用这个 ID 调用下列 API 查询推送记录: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/tables/Notifications/:objectId -``` - -其中 URL 里的 `:objectId` 替换成 `/push` 接口返回的 objectId 。 - -将返回推送记录对象,推送记录各字段含义参考[推送通知总览](/sdk/push/guide/overview/)的《Notification》一节。 - -## 推送状态查看和取消 - -在发推送的过程中,我们会随着推送任务的执行更新推送状态到 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 推送记录** 中,可以在这里查看推送的最新状态。对不同推送状态的说明请参看[推送通知总览](/sdk/push/guide/overview/)的《Notification》一节。 - -在一条推送记录状态到达 **done** 即完成推送之前,其状态信息旁边会显示「取消推送」按钮,点击后就能将本次推送取消。并且取消了的推送会从推送记录中删除。 - -请注意取消推送的意思是取消还排在队列中未发出的推送,已经发出或已存入离线缓存的推送是无法取消的。同理,推送状态显示已经完成的推送也无法取消。请尽量在发推送前做好测试,确认好发送内容和目标设备查询条件。 - -## 限制与费用 - -### 推送消息接口 - -[推送消息接口](#推送消息)的调用受频率限制,限制如下: - - - -|商用版(每应用)|开发版(每应用)| -|----------|---------------| -|最大 9600 次/分钟,默认 600 次/分钟|60 次/分钟| - - - - - -| 每个应用 | -|----------| -|最大 9600 次/分钟,默认 600 次/分钟| - - - -超过频率限制后 1 分钟内云端会拒绝请求持续返回 429 错误码,一分钟后会重新处理请求。 - -商用版应用默认上限可以在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > 消息推送 API 调用频率上限** 修改。 -按照每日调用频率峰值实行阶梯收费,费用如下: - -| 每分钟调用频率 | 费用 | -| - | - | -| 0 ~ 600 | 免费 | -| 601 ~ 1200 | ¥60 元 / 天 | -| 1201 ~ 1800 | ¥90 元 / 天 | -| 1801 ~ 2400 | ¥120 元 / 天 | -| 2401 ~ 3000 | ¥150 元 / 天 | -| 3001 ~ 3600 | ¥180 元 / 天 | -| 3601 ~ 4200 | ¥210 元 / 天 | -| 4201 ~ 4800 | ¥240 元 / 天 | -| 4801 ~ 5400 | ¥270 元 / 天 | -| 5401 ~ 6000 | ¥300 元 / 天 | -| 6001 ~ 6600 | ¥330 元 / 天 | -| 6601 ~ 7200 | ¥360 元 / 天 | -| 7201 ~ 7800 | ¥390 元 / 天 | -| 7801 ~ 8400 | ¥420 元 / 天 | -| 8401 ~ 9000 | ¥450 元 / 天 | -| 9001 ~ 9600 | ¥480 元 / 天 | - -国际版 - -| 每分钟调用频率 | 费用 | -| - | - | -| 0 ~ 600 | 免费 | -| 601 ~ 1200 | $20 USD / 天 | -| 1201 ~ 1800 | $30 USD / 天 | -| 1801 ~ 2400 | $40 USD / 天 | -| 2401 ~ 3000 | $50 USD / 天 | -| 3001 ~ 3600 | $60 USD / 天 | -| 3601 ~ 4200 | $70 USD / 天 | -| 4201 ~ 4800 | $80 USD / 天 | -| 4801 ~ 5400 | $90 USD / 天 | -| 5401 ~ 6000 | $100 USD / 天 | -| 6001 ~ 6600 | $110 USD / 天 | -| 6601 ~ 7200 | $120 USD / 天 | -| 7201 ~ 7800 | $130 USD / 天 | -| 7801 ~ 8400 | $140 USD / 天 | -| 8401 ~ 9000 | $150 USD / 天 | -| 9001 ~ 9600 | $160 USD / 天 | - -每日调用频率峰值可在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 统计 > REST API QPM 峰值** 中查看。 - -### 每日推送人次 - -限制如下: - - - -|商用版(每应用每天)|开发版(每应用每天)| -|----------|---------------| -|最大无上限,默认 100 万人次|1 万人次| - - - - - -| 每个应用 | -|----------| -|最大无上限,默认 100 万人次| - - - -达到限制后,当天将无法再推送消息。 - -商用版应用默认上限可以在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置 > 每日推送上限** 修改。 -费用如下(其中不足一万人次的部分,按一万人次计算): - -| 每日推送人次 | 费用 | -| - | - | -| 小于等于 100 万的部分 | 免费 | -| 大于 100 万的部分 | ¥0.5 元 / 万人次 | - -国际版 - -| 每日推送人次 | 费用 | -| - | - | -| 小于等于 10 万的部分 | 免费 | -| 大于 10 万的部分 | $0.02 USD / 万人次 | - -每日推送人次可在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 统计 > 推送人次** 中查看。 - -### 其他 - -* 为避免给大量早已不再活跃的用户发消息,我们限制只能给 `_Installation` 表内 `updatedAt` 时间在最近三个月以内的设备推送消息。我们会在根据推送查询条件查出目标设备后自动将不符合条件的设备从目标设备中剔除,并且被剔除的设备不会计入 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 推送记录** 内的目标设备数中。商用版用户如有特别需求,可提交工单联系我们付费延长有效期(最长可延长至一年)。 -* 为防止由于大量证书错误所产生的性能问题,我们对使用 **开发证书** 的推送做了设备数量的限制,即一次至多可以向 20,000 个设备进行推送。如果满足推送条件的设备超过了 20,000 个,系统会拒绝此次推送,在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 推送记录** 中的 **状态** 一栏显示「错误」,提示信息为「dev profile disabled for massive push」。因此,在使用开发证书推送时,请合理设置推送条件。 -* Apple 对推送消息大小有限制,对 iOS 推送请尽量缩小要发送的数据大小,否则会被截断。详情请参看 [APNs 文档](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html)。 -* 如果使用了 Android 的混合推送,请注意华为推送对消息大小有限制。为保证推送消息能被正常发送,我们要求 data + channels 参数须小于 4096 字节,超过限制会导致推送无法正常发送,请尽量减小发送数据大小。 -* 每个开发版应用处在待发队列中的定时推送数量最多 10 条,每个商用版应用处在待发队列中的定时推送数量最多 1000 条。每个应用处在待发队列中的定时推送数量最多 1000 条。 - -如果推送失败,在 **开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 推送记录** 的 **状态** 一栏中会看到错误提示。 - diff --git a/leancloud/docs/sdk/push/guide/unity.mdx b/leancloud/docs/sdk/push/guide/unity.mdx deleted file mode 100644 index f601ed0b3..000000000 --- a/leancloud/docs/sdk/push/guide/unity.mdx +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: Unity 推送指南 -sidebar_label: Unity 推送 -sidebar_position: 5 -slug: /sdk/push/guide/unity/ ---- - - - - - -import {Conditional} from '/src/docComponents/conditional'; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - - -本文介绍了如何在 Unity 中使用推送通知功能。建议先阅读 [推送通知服务总览](/sdk/push/guide/overview/) 了解相关概念。 - -由于 Android 系统对于第三方推送管控越来越严格,所以目前只支持 iOS 及 Android 厂商(华为、小米、VIVO、OPPO、魅族)FCM 推送。 - -## 准备工作 - -### iOS - -请参考 [iOS 推送设置指南](/sdk/push/guide/ios-cert/)申请 iOS 推送证书。 - -### Android - -请参考 [Android 混合推送开发指南](/sdk/push/guide/android-mixpush/)申请各厂商 FCM Android 推送权限。 - -注意:这里只需要参考混合推送指南申请各厂商 FCM 的推送权限,**不需要** 参考混合推送指南中 Android 相关配置的内容。 - -## 接入推送服务 - -### 安装 - -在 [SDK Releases](https://github.com/leancloud/csharp-sdk/releases) 下载最新版本的 `unity-push.unitypackage` 或 `unity-push-without-gradle.unitypackage`。 - -如果项目中**没有**使用其他 Android Gradle 配置,可以直接下载 `unity-push.unitypackage`,其中包括了完整的 iOS/Android 配置,开发者只需配置各厂商参数即可。 - -如果项目中**有**其他 Android Gradle 配置,则需要下载 `unity-push-without-gradle.unitypackage`,这个包中不包含推送相关的 Android Gradle 配置,需要开发者自行补充。 - -因为涉及到 Android Gradle 配置,目前不提供 UPM 方式安装。 - - - -### 导入 `LeanCloud-SDK-Realtime-Unity` 包: - -#### 方法一:使用 Unity Package Manager - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - - -{ -`"dependencies":{ - "com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-${sdkVersions.leancloud.csharp}" -}` -} - - -在 Unity 顶部菜单中选择 **Window > Package Manager** 可查看已经安装在项目中的包。 - -#### 方法二:手动导入 - -1. 在 [下载页](/tap-download) 找到 **LeanCloud C# SDK** 下载地址,下载 `LeanCloud-SDK-Realtime-Unity.zip`。 - -2. 解压后的 `LeanCloud-SDK-Realtime-Unity.zip` 为 Plugins 文件夹,拖拽至 Unity 即可。 - - - -### 配置 - -#### iOS - -只需要在初始化时传入 iOS 开发者的 TeamId,见[初始化](#初始化)。 - -#### Android - - - -##### 华为 - -下载华为推送后台申请的 `agconnect-services.json`,放置在项目的 `Assets/LeanCloud/Push/Android/HuaWei/hms/` 下(在打包时会将其拷贝到华为要求的目录下)。 - -##### VIVO - -将 VIVO 推送后台申请的 `app_id` 和 `api_key` 填入 `Assets/Plugins/Android/AndroidMenifest.xml` 中的 `meta-data` 的 `com.vivo.push.app_id` 和 `com.vivo.push.api_key`。 - -##### 其他平台 - -小米、OPPO、魅族的配置在初始化时传入。 - -注意:`AndroidMenifest.xml` 中会用 `${applicationId}` 作为包名配置,如果 `applicationId` 需要划分渠道,则需要手动修改。 - -### 初始化 - -这里需要开发者根据平台及设备信息,进行不同厂商 SDK 的初始化。 -小米推送有能力在其他厂商下建立推送连接,可以作为缺省厂商使用。 - -```cs -using LeanCloud.Storage; -using LeanCloud; -using LeanCloud.Push; -using System.Threading.Tasks; -using LC.Newtonsoft.Json; - -LCApplication.Initialize("{{appid}}", "{{appkey}}", "https://please-replace-with-your-customized.domain.com"); - -if (Application.platform == RuntimePlatform.IPhonePlayer) { - LCIOSPushManager.RegisterIOSPush(IOS_TEAM_ID); -} else if (Application.platform == RuntimePlatform.Android) { - string deviceModel = SystemInfo.deviceModel.ToLower(); - if (deviceModel.Contains("huawei")) { - LCHuaWeiPushManager.RegisterHuaWeiPush(); - } else if (deviceModel.Contains("oppo")) { - LCOPPOPushManager.RegisterOPPOPush(OPPO_APP_KEY, OPPO_APP_SECRET); - } else if (deviceModel.Contains("vivo")) { - LCVIVOPushManager.RegisterVIVOPush(); - } else if (deviceModel.Contains("meizu")) { - LCMeiZuPushManager.RegisterMeiZuPush(MEIZU_APP_ID, MEIZU_APP_KEY); - } else /*if (deviceModel.Contains("xiaomi"))*/ { - // 其他的厂商可以尝试注册小米推送 - LCXiaoMiPushManager.RegisterXiaoMiPush(XIAOMI_APP_ID, XIAOMI_APP_KEY); - } -} -``` - - - - - -初始化 FCM 进行推送: - -```cs -LCFCMPushManager.RegisterFCMPush(); -``` - - - -## Installation - -SDK 提供默认的 Installation 对象,用来保存推送所需的 token 以及其他数据。 - -```cs -LCInstallation lcInstallation = await LCInstallation.GetCurrent(); -``` - -## 推送消息 - -**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 设置** 中默认选中了 **禁止从客户端进行消息推送**,避免客户端可以不经限制地给应用内任意目标设备推送消息。 -我们建议开发者都勾选此项,通过 REST API 或云服务控制台发送推送消息。 - -如果有从客户端发推送的需求,需要先取消勾选此项。 - -### 推送给所有设备 - -```csharp -try { - LCPush push = new LCPush { - Data = new Dictionary { - { "alert", pushData } - } - }; - await push.Send(); -} catch (Exception e) { - Debug.LogError(e); -} -``` - -### 发送给特定的用户 - -下面是一个从客户端推送的例子,给某一台测试环境的 iOS 设备发推送: - -```cs -try { - LCPush push = new LCPush { - Data = new Dictionary { - { "alert", pushData } - }, - IOSEnvironment = LCPush.IOSEnvironmentDev, - }; - LCInstallation installation = await LCInstallation.GetCurrent(); - push.Query.WhereEqualTo("objectId", installation.ObjectId); - await push.Send(); -} catch (Exception e) { - Debug.LogError(e); -} -``` - -## 深入阅读:如何响应推送消息 - -### 消息格式 - -具体的消息格式,可参考[推送 REST API 使用指南的推送消息](/sdk/push/guide/rest#推送消息)一节。 对于 Android 设备,默认的消息内容参数支持下列属性: - -```json -{ - "alert": "消息内容", - "title": "显示在通知栏的标题", - "custom-key": "由用户添加的自定义属性,custom-key 仅是举例,可随意替换" -} -``` - -### 通知栏消息如何响应用户点击事件 - -Unity 环境有别于 iOS/Android 环境,当点击通知时,Unity 场景不一定能初始化完成,所以从 Native 到 Unity 的通知可能无法到达。 - -SDK 为了能够将**通知参数**顺利的发送到 Unity,会在通知时将参数缓存在 Native 层,提供一个统一获取启动参数的 C# 接口: - -```cs -Dictionary launchData = await LCPushBridge.Instance.GetLaunchData(); -``` - -## 推送验证 - -初始化 SDK 后,在 iOS/Android 真机运行项目,`_Installation` 表会生成一条设备信息的数据。 - -在**开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 推送通知 > 在线发送**可以自定义推送条件发送一条推送,测试当前设备能否正常收到推送。 -这里可以根据 Installation 的 objectId 推送,iOS 设备也可以根据 deviceToken 推送,Android 设备可以根据 registrationId 推送。 - -## 其他 -### 如何剔除某些厂商推送服务 - - - -应用可能只计划支持部分手机厂商,或针对不同渠道分别打包,这时可以手动剔除不需要的厂商 SDK,以节省打包体积: - - - - - -SDK 中包含了一些中国 Android 手机厂商的 SDK,国际版应用可以手动剔除以节省打包体积。 - - - -- 删掉 `Assets/LeanCloud/Push/Android/xx`,`xx` 代表厂商,如 `HuaWei`、`XiaoMi` 等 -- 删除 `Assets/Plugins/Android/mainTemplate.gradle` 中的 `dependencies` 部分 -- 删掉 `Assets/Plugins/Android/AndroidManifest.xml` 中厂商 SDK 组件部分(有厂商相关的注释) diff --git a/leancloud/docs/sdk/start/_category_.json b/leancloud/docs/sdk/start/_category_.json deleted file mode 100644 index 7d914738b..000000000 --- a/leancloud/docs/sdk/start/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "入门指南", - "collapsed": true, - "position": 0 -} diff --git a/leancloud/docs/sdk/start/compliance.mdx b/leancloud/docs/sdk/start/compliance.mdx deleted file mode 100644 index 77d7757b4..000000000 --- a/leancloud/docs/sdk/start/compliance.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: SDK 合规使用说明 -sidebar_position: 3 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -发布日期:2023 年 10 月 13 日 - -生效日期:2023 年 10 月 13 日 - - -为有效治理 App 强制授权、过度索权、超范围收集个人信息等现象,落实《网络安全法》《消费者权益保护法》的要求,保障个人信息安全,2019 年 1 月,中央网信办、工信部、公安部、市场监管总局等四部委发布了《关于开展 App 违法违规收集使用个人信息专项治理的公告》,在全国范围组织开展 App 违法违规收集使用个人信息专项治理,并陆续出台完善了《App 违法违规收集使用个人信息行为认定方法》、《GB/T 35273-2020 信息安全技术 个人信息安全规范》等标准规范。 - -2020 年以来,随着工信部纵深推进 App 专项整治行动,以及陆续检测通报 App 违规情况,监管部门、各行业参与方、终端用户都越来越关注 App 和 SDK 的安全问题。 - -为帮助 LeanCloud SDK的使用者们更清楚地了解监管要求、高效落实个人信息保护相关事宜,LeanCloud(公司全称:美味书签(北京)信息技术有限公司)特编写了《LeanCloud SDK合规指南》(以下简称“指南”),供开发者们参考。 - -## 一、App 个人信息保护的合规要求 - -1. App 首先需制定一份《隐私政策》,并确保在产品界面中显著展示。《隐私政策》须单独成文,而不是作为用户协议、用户说明等文件中的一部分存在。App 应在《隐私政策》中明示收集使用个人信息的目的、方式和范围,并确保《隐私政策》链接正常有效,易于访问和阅读。 -2. App 应在《隐私政策》中将收集个人信息的业务功能以及每个业务功能所收集的个人信息类型进行逐项列举,不应使用“等、例如” 等方式概括说明;同时,App 须对个人敏感信息类型进行显著标识(如字体加粗、标星号、下划线、斜体、颜色等)。如果通过嵌入第三方代码、插件等方式将个人信息传输至第三方服务器,应通过弹窗提示等方式明确告知用户。 - - -## 二、App 使用 LeanCloud SDK 时的合规指引 -1. 您应确保在 App 首次运行时通过明显方式提示终端用户阅读您的《隐私政策》,并取得终端用户的合法授权后,再初始化 SDK 进行信息收集与处理。如果终端用户不同意您的隐私政策,则不能初始化 LeanCloud 的各项 SDK,也无法使用相应 SDK 对应功能。 -2. 如果您的 App 在终端用户首次运行时,需要注册用户账号才能使用,则可以在账号注册环节提示终端用户同意您的《隐私政策》,之后完成注册;如果您的 App 并不一定需要终端用户注册用户账号才能使用,那么如果终端用户不同意您的《隐私政策》,按照最新合规政策,您不应停止让终端用户使用您的 App,仍需保留用户的基本使用权利。若终端用户将使用到需收集相关个人信息才能使用的功能(比如混合推送服务),可以再次提醒终端用户需要同意您的《隐私政策》才能正常使用相关功能,若终端用户仍不同意,则无法提供对应功能,可在终端用户下次需要使用时再提示同意您的《隐私政策》。 - -### 对混合推送的特别说明 - -若您使用了 LeanCloud 混合推送 SDK,针对各类厂商的推送 SDK,建议您在 APP 自身隐私政策中添加各项 SDK 的隐私政策文本模版如下: - -| SDK 名称 | SDK 厂商 | 合作目的 | 收集个人信息 | SDK 隐私政策链接 | -| ----------------- | ----------------- | ----------------- | ---------------- | ---------------- | -| 华为推送 SDK | 华为软件技术有限公司 | 用于华为手机的消息推送 | 设备信息【包括:设备型号、设备名称、SIM卡序列号、设备唯一标识符(IMEI、IMSI、AndroidID、IDFA、OAID),以下同】、网络信息 | https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/sdk-data-security-0000001050042177 | -| 荣耀推送 SDK | 荣耀终端有限公司 | 用于荣耀手机的消息推送 | 设备信息、网络信息 | https://www.hihonor.com/cn/privacy/privacy-policy/ | -| 小米推送 SDK | 北京小米移动软件有限公司 | 用于小米手机的消息推送 | 设备标识符(如 Android ID、OAID、GAID)、设备信息 | https://dev.mi.com/console/doc/detail?pId=1822 | -| Oppo 推送 SDK | 广东欢太科技有限公司 | 用于 Oppo 手机的消息推送 | 设备标识符(如 IMEI、ICCID、IMSI、Android ID、GAID)、应用信息(如应用包名、版本号和运行状态)、网络信息(如 IP 或域名连接结果,当前网络类型) | https://open.oppomobile.com/wiki/doc#id=10288 | -| Vivo 推送 SDK | 维沃移动通信有限公司 | 用于 Vivo 手机的消息推送 | 设备信息 | https://www.vivo.com.cn/about-vivo/privacy-policy | -| 魅族推送 SDK | 珠海市魅族通讯设备有限公司 | 用于魅族手机的消息推送 | 设备标识信息、位置信息、网络状态信息、运营商信息 | https://i.flyme.cn/privacy | - -### LeanCloud 隐私政策模版 -在使用 LeanCloud 各项 SDK 产品时,开发者需在 App《隐私政策》的 “与授权合作伙伴共享”条款中,将 LeanCloud 的用户隐私政策 加入其中,并向终端用户逐一明示您嵌入的 SDK 收集使用个人信息的目的、方式和范围。 - -在 APP 自身隐私政策中添加 LeanCloud 各项 SDK 的隐私政策文本模版如下: - -| SDK 名称 | SDK 厂商 | 合作目的 | 收集个人信息 | SDK 隐私政策链接 | -| ----------------- | ----------------- | ----------------- | ---------------- | ---------------- | -| LeanCloud 存储 SDK | 美味书签(北京)信息技术有限公司 | 为 App 用户提供结构化数据存储技术服务 | 无 | /sdk/privacy.html | -| LeanCloud RTM SDK | 美味书签(北京)信息技术有限公司 | 为 App 用户提供实时聊天技术服务 | 无 | /sdk/privacy.html | -| LeanCloud 混合推送 SDK | 美味书签(北京)信息技术有限公司 | 为 App 用户提供消息推送技术服务 | 除厂商收集信息外,本 SDK 不收集个人信息 | /sdk/privacy.html | -| LeanCloud 多人对战 SDK | 美味书签(北京)信息技术有限公司 | 为 App 用户提供多人对战技术服务 | 无 | /sdk/privacy.html | - -## 三、合规文件指引 -为了更及时高效地落实合规要求,我们建议各位开发者充分了解现有以及陆续将发布的有关个人信息保护的法律、法规、政策、标准等,以下资料供您参考: - -- [《GB/T 35273-2020 信息安全技术 个人信息安全规范》](http://c.gb688.cn/bzgk/gb/showGb?type=online&hcno=4568F276E0F8346EB0FBA097AA0CE05E) -- [《网络安全标准实践指南—移动互联网应用程序(App)个人信息保护常见问题及处置指南》](https://www.tc260.org.cn/front/postDetail.html?id=20200918162332) -- [《网络安全实践指南—移动互联网应用基本业务功能必要信息规范》](https://www.tc260.org.cn/front/postDetail.html?id=20190531230315) -- [《网络安全标准实践指南—移动互联网应用程序(App)收集使用个人信息自评估指南(征求意见稿)》](https://www.tc260.org.cn/front/postDetail.html?id=20200319113609) -- [《网络安全标准实践指南—移动互联网应用程序(App)个人信息安全防范指引(征求意见稿)》](https://www.tc260.org.cn/front/postDetail.html?id=20200330091643) -- [《网络安全标准实践指南—移动互联网应用程序(App)系统权限申请使用指南》](https://www.tc260.org.cn/front/postDetail.html?id=20200918163359) -- [《网络安全标准实践指南—移动互联网应用程序(App)中的第三方软件开发工具包(SDK)安全指引(征求意见稿)》](https://www.tc260.org.cn/front/postDetail.html?id=20200918155732) -- [《App 违法违规收集使用个人信息行为认定方法》](http://www.cac.gov.cn/2019-12/27/c_1578986455686625.htm) - - -如有任何问题,您可通过以下方式与我们联系: - -邮箱:leancloud-support@xd.com - diff --git a/leancloud/docs/sdk/start/faq.mdx b/leancloud/docs/sdk/start/faq.mdx deleted file mode 100644 index d436f65a3..000000000 --- a/leancloud/docs/sdk/start/faq.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: 账户和控制台常见问题 -sidebar_label: 常见问题 -sidebar_position: 5 ---- - -:::tip -这篇文档仅介绍**账户和控制台**相关的常见问题,使用具体的服务遇到问题可参考对应文档目录下列出的常见问题,如[数据存储常见问题](/sdk/storage/faq/)、[云引擎常见问题](/sdk/engine/faq/)等。 -::: - -## 平台 - -### LeanCloud 部署在哪个云平台上 - -LeanCloud 部署在国内多个云计算平台上,并采用在双线机房内同时使用虚拟机和实体机的混合部署策略,来保证应用的访问体验和可靠性。 - -### 哪里获取平台的更新信息 - -通常情况下,我们新版本的更新周期为一到两周。获取更新信息可以通过: - -* [官方博客](https://leancloudblog.com/)(每次更新的详细信息都会发布在那里) -* [官方微博](https://weibo.com/avoscloud) -* 官方微信公众号:LeanCloud通讯 -* 每月初,我们会将每月的更新摘要发送到您的注册邮箱。 -* 在控制台页面的右上方有消息中心,请注意查看新通知。 - -## 节点选择 - -### 华北节点、华东节点与国际版如何选择,各节点的区别是什么? - -如果用户主体在国内,可以选择国内的华北和华东节点;如果用户主体在国外,则可以选择国际版。跨节点访问(例如国内访问国际版,或者国外访问国内节点)会有网络延迟,所以建议根据用户所在地选择节点。 - -华北节点和华东节点在国内不同地区的访问速度基本没有差别。目前华北节点的功能更完善一些,有一些功能在华东还不支持,例如,华东节点暂不支持在控制台自助绑定独立 IP、暂时没有 API 访问日志功能、混合推送中不支持 FCM 推送。如果需要使用这些服务可以选择华北节点。 - -### 应用如何切换节点? - -应用不支持一键切换节点,「应用转让」功能也仅限在相同节点转让。如果一定要切换节点,可以通过数据导入导出的方式实现,即在另外的节点新建一个应用,将当前节点应用数据导出后导入到新创建的应用。 - -需要注意: - -* 只有数据存储服务的结构化数据支持导出与导入。 -* 即时通信服务的会话记录与聊天记录无法导出。 -* 云引擎需要重新部署。 -* 短信签名与模板需要重新审核。 -* 推送厂商、开发证书、敏感词库等应用维度的设置信息都需要重新配置。 - -## 技术支持 - -### 获取客服支持有哪些途径 - -* 到免费的 [用户社区](https://forum.leancloud.cn/) 进行提问。 -* 商用版应用的所有者可以进入 [工单系统](https://leanticket.cn) 来提交问题。 -* 账号相关事项,可发送邮件到 获取帮助。 -* 紧急故障拨打客服电话:+8618625038918。 -* 售前咨询请致电 +8613011098244。 - -## 费用 - -### 如何付费 - -* 支付宝充值 - - 进入 **控制台 > 财务 > 概览**,点击「余额充值」按钮时将会出现「支付宝充值」窗口。我们将每天自动从您的账户余额里扣除前一日的费用。每次扣费优先使用充值金额,其次是赠送金额。 - -* 对公账户汇款 - - 账户信息请通过 **控制台 > 财务 > 概览** 查看(点击「余额充值」按钮后显示)。 - -:::caution -请务必在汇款附言里中注明以下信息,以便我们账务确认汇款的来源和用途,及时入账。 - -1. 您的 LeanCloud 用户名 -2. (或)注册邮箱 -::: - -### 如何申请开具发票 - -请参考[控制台指南 > 账单和发票](/sdk/start/dashboard/#账单和发票)一节的说明。 - -### 如果没有缴费会怎么样 - -账户余额小于 0 时,账户服务将被停止,即云端会拒绝所有的请求,因此应用的统计数据也无法生成;应用数据被置于不可见模式,但仍会在 LeanCloud 云端保留 30 天。如需要恢复服务和访问应用数据,请登录控制台充值。 - -:::caution -欠费超过 30 天,应用内的数据(包括文件)会被删除且无法恢复。 -::: - -你可以在控制台设置告警余额,当账户余额小于设置的值时,我们会发送短信、邮件通知。请开发者务必关注账户的余额情况,以免对业务造成影响。 - -### 欠费期间应用产生的数据和请求能否找回 - -不能。因为欠费时应用处于禁用阶段,所有的请求都会被云端丢弃,与统计相关的数据也不会生成,所以当应用服务恢复后也无法找回这一期间的数据。为避免统计数据出现断档,请在控制台中设置告警余额,及时充值,保证服务的持续。 - -## 隐私政策 - -### LeanCloud SDK 收集哪些数据 - -LeanCloud SDK 不采集个人信息,例如,不会收集设备 Mac 地址,不会采集唯一设备识别码(如 IMEI / android ID / IDFA / OPENUDID / GUID、SIM 卡 IMSI 信息等)对用户进行唯一标识,不会访问其他应用的数据信息。 - -* [LeanCloud Android SDK 个人信息采集说明](https://www.leancloud.cn/privacy/sdk-data-collection/android/) -* [LeanCloud Objective-C SDK Data Collection Practices](https://www.leancloud.cn/privacy/sdk-data-collection/objc/) -* [LeanCloud Swift SDK Data Collection Practices](https://www.leancloud.cn/privacy/sdk-data-collection/swift/) - -LeanCloud SDK 都是开源的,开发者可以审查代码以确定 SDK 是否有其他收集用户数据的行为。 - -## 数据存储 - -### 如何重命名 Class? - -LeanCloud 不支持重命名 Class。 -你可以新建一个 Class,利用 [数据导入导出](/sdk/storage/guide/rest/#数据导出-api) 功能从旧 Class 导出数据,然后导入到新 Class。 -导出导入期间不要往旧 Class 写数据,或者在旧 Class 和新 Class 双写数据,以避免数据不一致。 - -### 如何导入或者导出数据? - -* [导入数据](/sdk/start/dashboard/#导入数据) -* [导出数据](/sdk/start/dashboard/#导出数据) - -### 导出数据后没收到邮件? - -首先请确认邮箱能够正常接收邮件。 -如果导出数据的数据量较大,导出任务可能需要一定时间才能完成,请耐心等待。 -另外,12 点之后无法导出数据,指的是 12 点后无法提交数据导出任务。 -也就是说,在 12 点前提交的数据导出任务,可能在 12 点之后才实际完成导出。 - -### 如何在 App 邮件内完全使用自己的品牌 - -请参考 [自定义邮件验证和重设密码页面](/sdk/storage/guide/custom-reset-verify-page/)。 - -### 创建唯一索引失败 - -请确认想要创建索引的列没有已经存在的重复值。 - -### 如何上传文件 - -任何一个 Class 如果有 File 类型的列,就可以直接在 **数据** 管理平台中将文件上传到该列。如果没有,请自行创建列,类型指定为 File。 - -## 应用 - -### 如何在应用之间共享数据 - -请参考 [应用间数据共享](/sdk/storage/guide/app-data-share/) 绑定 Class。 - -### 应用什么情况下会自动归档? - -连续 30 天无 API 请求的开发版应用会自动归档,应用归档后,存储以及依赖存储的服务不可用。云引擎服务仍可以正常访问。归档的应用可以在控制台 > 查看所有应用页面手动激活。如果开发版应用连续 30 天无 API 请求但应用仍在计费(例如数据存储空间使用超过免费额度 1 GB),则不会被归档。 - -### 海外用户访问应用速度很慢 - -由于众所周知的原因,许多海外地区访问国内服务的网络速度不尽如人意。 -如果应用主要面向海外用户,建议使用 [LeanCloud 国际版](https://leancloud.app/)(机房位于北美)。如果应用同时面向国内用户和海外用户,可以考虑购买专线方案改善海外用户访问速度。专线方案仅面向商用版应用,如有需要,可以提交工单联系我们。 diff --git a/leancloud/docs/sdk/start/quickstart.mdx b/leancloud/docs/sdk/start/quickstart.mdx deleted file mode 100644 index 639ed8c69..000000000 --- a/leancloud/docs/sdk/start/quickstart.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: 快速开始 -sidebar_label: 快速开始 -slug: /sdk/start/quickstart/ -sidebar_position: 1 ---- -## 注册账号 - -### 账号体系 - -LeanCloud 国内版()和国际版()独立运营,使用独立的账号系统。 - -国内版、国际版的功能大体相同,使用哪个版本主要取决于应用面向的用户。 -如果你的应用主要面向国内用户,我们建议在国内版注册账号;相应地,如果应用主要面向海外用户,建议在国际版注册账号。 -另外,根据有关部门规定,国内版需要绑定手机、实名认证、绑定已备案的域名才能正常使用。 - - -### 注册邮箱 - -注册邮箱用于重置密码、受让应用、接收导出数据、接收余额告警等重要通知,十分关键,请**确保使用长期有效、能正常接收邮件的邮箱**注册账号。 -如代表公司注册账号,建议使用公司邮箱注册,以免因离职时遗忘交接而导致不必要的麻烦。 - -使用邮箱注册账号后,会收到欢迎邮件,请点击邮件中的「验证邮箱」按钮完成邮箱验证。 -注册邮箱每个月会收到 LeanCloud 月报(包括产品进展、常见问题等内容),如不想接收此类邮件,可以点击欢迎邮件底部的「立即退订」,或在「控制台 > 账号设置 > Email」修改「邮件选项」。 -重大变更、余额告警等重要邮件无法退订。 - -## 实名认证 - -注册国内版账号后,控制台会自动跳转至实名认证页面,点击「开始认证」按钮按照提示完成认证即可。 -请根据实际情况选择实名认证类型(个人认证、企业认证)。 - -个人认证在填写姓名和身份证号后会显示二维码,通过支付宝应用扫描二维码即可完成认证。 -企业认证请根据控制台提示提交相应的企业信息和文件,我们会在 2 个工作日内审核,审核通过后会收到邮件通知。 - -## 账号安全 - -为保证账号安全,邮箱、手机发生变更时,请及时在控制台更新。 -在 **控制台 > 账号设置 > 开发者信息**可以修改手机号,**控制台 > 账号设置 > Email**可以修改邮箱。 - -为增强账号安全性,建议定期更换密码并开启二次认证。 -在**控制台 > 账号设置 > 账号安全**可以修改密码及开启二次认证。 -开启二次认证后,登录控制台以及进行敏感操作时需要提供 Google Authenticator 等应用生成的验证码。 - -如你选择开启二次认证,**务必妥善保管恢复码**,以免因为手机丢失等原因无法访问账号。 - -## 创建应用 - -无论你打算使用 LeanCloud 的哪些服务,首先都需要创建一个应用。 -点击控制台左上角的 LeanCloud 图标,即可返回控制台首页。 -在控制台首页点击**创建应用**按钮,在创建应用对话框中填写应用名称并选择应用类型即可新建应用。 - -:::caution -每个账号最多可以创建 50 个应用。未验证手机号的账号无法创建应用。 -::: - -新创建的应用会以卡片形式显示在控制台,卡片左上角显示应用名称,右上角显示应用类型(开发版、商用版),下方显示用户数、昨日 API 请求数、当月 API 请求数三项统计数据,点击中间的图标则可直达相应的服务:结构化数据存储、云引擎、即时通讯、推送、短信、游戏、设置。 - -点击应用名称会进入应用概览页面,这个页面会显示应用的基本信息数据,例如请求数、新增用户数、消息数等。 - -左栏菜单显示各项服务,我们会在后文具体说明。 -对于刚刚创建的应用,通常最先查看的页面是**控制台 > 设置 > 应用凭证**。 -在初始化 SDK 时需要用到其中的 App ID、App Key、服务器地址等信息。 -根据有关部门规定,使用 LeanCloud 国内版的服务需要绑定域名。 -我们建议你首先[绑定域名](/sdk/domain/guide/),这样设置页面显示的服务器地址就会是你绑定的域名,SDK 初始化时可以直接使用,后续无需修改。 -如果暂不绑定域名(比如域名正在办理备案),设置页面会显示供测试使用的临时共享域名,该域名无可用性保证,可能被回收,正式上线前需要改用自有域名。 - -创建应用、绑定域名后,就可以[参考文档](/sdk/storage/overview/)基于 SDK 或 REST API 上手开发项目了。 - -## 财务信息 - -控制台首页右侧会显示财务信息,包括账户余额和最近消费情况,请多加留意,以免因为欠费而导致服务停止。 - -余额旁有**充值**、**告警**两个链接。 - -* 点击**充值**即可通过支付宝、银行汇款两种方式进行充值,其中以银行汇款方式充值需要一定时间才能到账(2 个工作日或更久),请提前安排。 -* 点击**告警**可以设置告警余额,账户余额小于设定值时会发送告警短信和告警邮件。 - -:::info -* 名下无商用版应用的用户,余额告警的默认值为 0 -* 名下有商用版应用的用户,余额告警的默认值为 100 -::: - -当前账号绑定邮箱、手机始终会收到告警短信、邮件,还可以额外添加接受告警的邮件地址和手机号,比如添加负责财务的同事的联系方式。 - diff --git a/leancloud/docs/sdk/storage/_category_.json b/leancloud/docs/sdk/storage/_category_.json deleted file mode 100644 index 0b22b23d7..000000000 --- a/leancloud/docs/sdk/storage/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "数据存储", - "collapsed": true, - "position": 15 -} diff --git a/leancloud/docs/sdk/storage/_partials/app-config.mdx b/leancloud/docs/sdk/storage/_partials/app-config.mdx deleted file mode 100644 index ac4ef6fdd..000000000 --- a/leancloud/docs/sdk/storage/_partials/app-config.mdx +++ /dev/null @@ -1,23 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - - - -在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 可以查看应用的基本信息: - -- **Client ID**:又称 `App ID`,在 SDK 初始化时用到。 -- **Client Token**:又称 `App Key`,客户端对服务端的调用凭证,在 SDK 初始化时用到。 -- **域名配置 > 云服务 API**:又称 API 域名或 **Server URL**,在客户端 SDK 初始化时用到。域名配置参考下一节[域名](#域名)。 -- **Server Secret**:又称 `Master Key`,用于在自有服务器、云引擎等**受信任环境**调用管理接口,具备跳过一切权限验证的超级权限。所以**一定注意保密,千万不要在客户端代码中使用该凭证**。 - - - - - -在 **云服务控制台 > 设置 > 应用凭证** 可以查看应用的基本信息: - -- **App ID**:在 SDK 初始化时用到。 -- **App Key**:客户端对服务端的调用凭证,在 SDK 初始化时用到。 -- **Master Key**:用于在自有服务器、云引擎等**受信任环境**调用管理接口,具备跳过一切权限验证的超级权限。所以**一定注意保密,千万不要在客户端代码中使用该凭证**。 -- **服务器地址**:又称 API 域名或 **Server URL**,在客户端 SDK 初始化时用到。域名配置参考下一节[域名](#域名)。 - - diff --git a/leancloud/docs/sdk/storage/acl.mdx b/leancloud/docs/sdk/storage/acl.mdx deleted file mode 100644 index a3ea7f988..000000000 --- a/leancloud/docs/sdk/storage/acl.mdx +++ /dev/null @@ -1,937 +0,0 @@ ---- -title: ACL 权限管理开发指南 -sidebar_label: ACL 权限管理 -slug: /sdk/storage/guide/acl/ -sidebar_position: 12 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -:::info -ACL 是 Access Control List 的缩写,称为访问控制列表,包含了对一个对象或一条记录可进行何种操作的权限定义。 -::: - -

    TDSLeanCloud 云端使用的 ACL 机制是将每个操作授权给特定的 User 用户或者 Role 角色,只允许这些用户或角色对一个对象执行这些操作。

    - -例如如下 ACL 定义: - -```json -{ - "*":{ - "read":true, - "write":false - }, - "role:admin":{ - "read":true, - "write":true - }, - "58113fbda0bb9f0061ddc869":{ - "read":true, - "write":true - } -} -``` - -- 所有人可读,但不能写(`*` 代表所有人)。 -- 角色为 admin(包含子角色)的用户可读可写。 -- ID 为 `58113fbda0bb9f0061ddc869` 的用户可读可写。 - -我们使用内建数据表 `_User` 来维护 [用户/账户系统](/sdk/authentication/guide/),以及内建数据表 `_Role` 来维护**角色**。 -角色既可以包含用户,也可以包含其他角色,也就是说角色有层次关系,将权限授予一个角色代表该角色所包含的其他角色也会得到相应的权限。 - -云端对客户端发过来的每一个请求都要进行用户身份鉴别和 ACL 访问授权的严格检查。因此,使用 ACL 可以灵活且最大程度地保护应用数据,提升访问安全。 - -## 默认 ACL - -每个 Class 的初始默认 ACL 为所有人可读可写: - -```json -{ - "*":{ - "read":true, - "write":true - } -} -``` - -在创建 Class 对话框可以设置 Class 的默认 ACL: - -![创建 Class 对话框](/img/security/class-acl.png) - -你可以设置 read 和 write 权限开放给哪些用户,其中: - -- 「所有用户」、「指定用户」可以参考数据安全指南的 [Class 层面的访问权限](/sdk/storage/guide/security/#class-权限)一节的说明。 -- 「数据创建者(Owner)」指创建数据的用户。也就是说,新增对象(create)时附带的 `X-LC-Session` HTTP 头对应的用户。 - -除了分别设置 read 和 write 权限开放给哪些用户外,对话框中还提供了一些**常用设定的快捷方式**: - -- **限制写入**:数据创建者可读、可写,其他用户可读、不可写。 -- **限制读取**:数据创建者可读、可写,其他用户不可读、不可写。 -- **限制所有**:数据创建者可读、不可写,其他用户不可读、不可写。 -- **无限制**:所有人可读、可写。 - -对于已经存在的 Class,你可以**更新默认 ACL 和访问权限**。 -进入** > 结构化数据**,选择一个 Class,再点击**权限**标签页。 -不过,修改默认 ACL 只作用于新增的对象,现存对象的 ACL 值保持不变。 - -除了设置默认 ACL 外,在控制台也可以**设置单个对象的 ACL**。 -不过在控制台手工为每个对象设置 ACL 过于繁琐,并不现实。 -所以通常我们在控制台设置**默认 ACL**,确保所有新创建的对象都有合适的初始 ACL 值。 -为单个对象设置更复杂、更精细的 ACL,则通过代码设置其 `ACL` 字段。 - -例如,`Post` Class 的默认 ACL 可能是这样的: - -- read:所有用户 -- write:数据创建者(Owner) - -也就是说,所有用户都可以查看帖子,用户只能修改或者删除自己发的帖子。 - -这里数据创建者指创建对象的请求的 HTTP 头中携带的 session token 对应的用户,在我们的例子中是帖子的作者。 - -## 基于用户的权限管理 - -继续上面的例子,假设某个帖子的发布人允许另一个特定的用户(比如两个人合作编写一篇文章)修改帖子,除此之外的其他人不可修改,那么我们可以这样设置 ACL: - - - -```cs -try { - LCQuery userQuery = LCUser.GetQuery(); - LCUser otherUser = await userQuery.Get("55f1572460b2ce30e8b7afde"); - // 新建一个帖子对象 - LCObject post = LCObject("Post"); - post["title"] = "这是我的第二条发言,谢谢大家!"; - post["content"] = "我最近喜欢看足球和篮球了。"; - - //新建一个 ACL 实例 - LCACL acl = new LCACL(); - acl.PublicReadAccess = true; // 设置公开的「读」权限,任何人都可阅读 - LCUser currentUser = await LCUser.GetCurrent(); - acl.SetUserWriteAccess(currentUser, true); // 为当前用户赋予「写」权限,有且仅有当前用户可以修改这条 Post - acl.SetUserWriteAccess(otherUser, true); - post.ACL = acl; - - await post.Save(); -} catch (LCException e) { - print($"{e.Code} : {e.Message}"); -} -``` - -```java -LCQuery query = LCUser.getQuery(); -query.getInBackground("55f1572460b2ce30e8b7afde").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser anotherUser) { - // 新建一个帖子对象 - LCObject post= new LCObject("Post"); - post.put("title","这是我的第二条发言,谢谢大家!"); - post.put("content","我最近喜欢看足球和篮球了。"); - - //新建一个 ACL 实例 - LCACL acl = new LCACL(); - acl.setPublicReadAccess(true);// 设置公开的「读」权限,任何人都可阅读 - acl.setWriteAccess(LCUser.getCurrentUser(), true);//为当前用户赋予「写」权限 - acl.setWriteAccess(anotherUser, true); - - // 将 ACL 实例赋予 Post对象 - post.setACL(acl); - - //保存到云端 - post.saveInBackground(); - } - @Override - public void onError(Throwable e) { - System.out.println("errorMessage:" + e.getMessage()); - } - @Override - public void onComplete() { - } -} -``` - -```objc -LCQuery *query = [LCUser query]; -[query getObjectInBackgroundWithId:@"55f1572460b2ce30e8b7afde" block:^(LCObject * _Nullable object, NSError * _Nullable error) { - if (error == nil) { - // 新建一个帖子对象 - LCObject *post = [LCObject objectWithClassName:@"Post"]; - [post setObject:@"这是我的第二条发言,谢谢大家!" forKey:@"title"]; - [post setObject:@"我最近喜欢看足球和篮球了。" forKey:@"content"]; - - //新建一个 ACL 实例 - LCACL *acl = [LCACL ACL]; - [acl setPublicReadAccess:YES];// 设置公开的「读」权限,任何人都可阅读 - [acl setWriteAccess:YES forUser:[LCUser currentUser]];// 为当前用户赋予「写」权限 - [acl setWriteAccess:YES forUser:otherUser]; - - post.ACL = acl;// 将 ACL 实例赋予 Post 对象 - - [post save]; - } else { - NSLog(@"error"); - } -}]; -``` - -```swift -let query = LCQuery(className: LCUser.objectClassName()) - -_ = query.get("55f1572460b2ce30e8b7afde") { result in - switch result { - case .success(object: let object): - do { - let post = LCObject(className: "Post") - - try post.set("title", value: "这是我的第二条发言,谢谢大家!") - try post.set("content", value: "我最近喜欢看足球和篮球了。") - - let acl = LCACL() - - acl.setAccess([.read], allowed: true) - if let currentUserID = LCApplication.default.currentUser?.objectId?.value { - acl.setAccess([.write], allowed: true, forUserID: currentUserID) - } - if let anotherUserID = (object as? LCUser)?.objectId?.value { - acl.setAccess([.write], allowed: true, forUserID: anotherUserID) - } - - post.ACL = acl - - assert(post.save().isSuccess) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - LCQuery userQuery = LCUser.getQuery(); - LCUser otherUser = await userQuery.get('55f1572460b2ce30e8b7afde'); - // 新建一个帖子对象 - LCObject post = LCObject("Post"); - post['title'] = '这是我的第二条发言,谢谢大家!'; - post['content'] = '我最近喜欢看足球和篮球了。'; - - //新建一个 ACL 实例 - LCACL acl = LCACL(); - acl.setPublicReadAccess(true); // 设置公开的「读」权限,任何人都可阅读 - LCUser currentUser = await LCUser.getCurrent(); - acl.setUserWriteAccess(currentUser, true); // 为当前用户赋予「写」权限,有且仅有当前用户可以修改这条 Post - acl.setUserWriteAccess(otherUser, true); - post.acl = acl; //将 ACL 实例赋予 Post对象 - - await post.save(); -} on LCException catch (e) { - print('${e.code} : ${e.message}'); -} -``` - -```js - // 创建一个针对 User 的查询 - var query = new AV.Query(AV.User); - query.get('55f1572460b2ce30e8b7afde').then(function(otherUser) { - var post = new AV.Object('Post'); - post.set('title', '这是我的第二条发言,谢谢大家!'); - post.set('content','我最近喜欢看足球和篮球了。'); - - // 新建一个 ACL 实例 - var acl = new AV.ACL(); - acl.setPublicReadAccess(true); - acl.setWriteAccess(AV.User.current(), true); - acl.setWriteAccess(otherUser, true); - - // 将 ACL 实例赋予 Post 对象 - post.setACL(acl); - - // 保存到云端 - return post.save(); - }).then(function() { - // 保存成功 - }).catch(function(error) { - // 错误信息 - console.log(error); - }); -``` - -```python -import leancloud - -# 登录一个用户 -user = leancloud.User() -user.login('my_user_name', 'my_password') - -# 新建一个 Post 对象 -Post = leancloud.Object.extend('Post') -post = Post() -post.set('title', '这是我的第二条发言,谢谢大家!') -post.set('content', '我最近喜欢看足球和篮球了。') - -# 新建一个 leancloud.ACL 实例 -acl = leancloud.ACL() -acl.set_public_read_access(True) -# 设置当前登录用户的的可写权限 -acl.set_write_access(leancloud.User.get_current().id, True) -# 设定指定 objectId 用户的可写权限 -acl.set_write_access('55f1572460b2ce30e8b7afde', True) -post.set_acl(acl) -post.save() -``` - -```php -// 登录一个用户 -User::logIn("Tom", "cat!@#123"); - -// 新建一个帖子 -$post = new LeanObject("Post"); -$post->set("title", "这是我的第二条发言,谢谢大家!"); -$post->set("content", "我最近喜欢看足球和篮球了。"); - -// 新建一个 ACL 实例 -$acl = new ACL(); -// 设置公开的「读」权限,任何人都可阅读 -$acl->setPublicReadAccess(true); -// 为当前用户赋予「写」权限,有且仅有当前用户可以修改这条 Post -$acl->setWriteAccess(User::getCurrentUser(), true); -$otherUser = LeanObject::create("_User", "55f1572460b2ce30e8b7afde"); -$acl->setWriteAccess($otherUser, true); -$post->setACL($acl); -$post->save(); -``` - - - -执行完毕上面的代码,回到控制台,可以看到,该条 Post 记录里面的 ACL 列的内容如下: - -```json -{ - "*":{ - "read":true - }, - "55b9df0400b0f6d7efaa8801":{ - "write":true - }, - "55f1572460b2ce30e8b7afde":{ - "write":true - } -} -``` - -从结果可以看出,该条 Post 已经允许 `objectId` 为 `55b9df0400b0f6d7efaa8801` 以及 `55f1572460b2ce30e8b7afde` 的两个用户(User)修改,他们拥有写权限 `"write": ture`。 - -:::tip 注意 -为了避免示例过于冗长,重点不突出,从下面的示例开始,都不再给出可以直接执行的完整代码,只保留关键部分。 -::: - -假设论坛有一个管理员,可以编辑、删除所有帖子,那么我们可以用类似的方法给每个帖子加上相应的权限: - - - -```cs -acl.SetUserWriteAccess(anAdministrator, true); -``` - -```java -acl.setWriteAccess(anAdministrator, true); -``` - -```objc -[acl setWriteAccess:YES forUser:anAdministrator]; -``` - -```swift -acl.setAccess([.write], allowed: true, forUserID: anAdministratorID) -``` - -```dart -acl.setUserWriteAccess(anAdministrator, true); -``` - -```js -acl.setWriteAccess(anAdministrator, true); -``` - -```python -acl.set_write_access(an_administrator_id, True) -``` - -```php -$acl->setReadAccess($anAdministrator, true); -``` - - - -但是,未来可能会有新的管理员加入,老的管理员也可能卸任,所以单纯基于用户进行权限管理,任何人员变动都需要批量订正数据(修改 ACL 字段),太不灵活了。 - -因此,我们需要引入角色这一概念。 - -## 基于角色的权限管理 - -「角色」相当于用户组,并且可以嵌套。 -换言之,一个角色的成员要么是用户,要么是另一个角色。 - -角色的名字由字母、数字、下划线组成,不可变更,在应用内唯一。 - -继续上面的例子,让我们看看如何赋予管理员角色写权限(假定已经存在一个名为 `admin` 的角色): - - - -```cs -LCRole admin = LCRole.CreateWithoutData("_Role", "55fc0eb700b039e44440016c"); -acl.SetRoleReadAccess(admin, true); -``` - -```java -acl.setRoleWriteAccess("admin", true); -``` - -```objc -LCRole *admin = [LCRole objectWithClassName:@"_Role" objectId:@"55fc0eb700b039e44440016c"]; -[acl setWriteAccess:YES forRole:admin]; -``` - -```swift -acl.setAccess([.write], allowed: true, forRoleName:"admin") -``` - -```dart -LCRole admin = LCRole.createWithoutData('_Role', '55fc0eb700b039e44440016c'); -acl.setRoleWriteAccess(admin, true); -``` - -```js -acl.setRoleWriteAccess('admin', true); -``` - -```python -admin = leancloud.Role('admin') -acl.set_role_write_access(admin, True) -``` - -```php -$acl->setRoleWriteAccess("admin", true); -``` - - - -### 角色的创建 - -下面让我们看看如何创建一个角色。 - -这里有一个需要特别注意的地方,因为 `Role` 本身也是一个 `Object`,它自身也有 ACL 控制,并且它的权限控制应该更严谨。 -所以通常我们在创建角色的时候会显式地设定该角色的 ACL。 -如果不指定,那么 SDK 会默认设定角色的 ACL 为所有人可读、所有人不可写。 -换言之,在不显式指定 ACL 的情况下,SDK 的默认设定会导致角色一经创建,未来无法在客户端修改,以后添加成员等操作都需要在控制台进行或在服务端使用 masterKey 进行。 -为了便于测试,我们在下面的示例代码中暂且将角色的写权限赋予了创建该角色的用户(当前用户),**实际项目中请根据具体需求设定合适的 ACL**。 - - - -```cs -try { - // 角色本身的 ACL - LCACL acl = new LCACL(); - acl.PublicReadAccess = true; - LCUser currentUser = await LCUser.GetCurrent(); - acl.SetUserWriteAccess(currentUser, true); - - LCRole admin = LCRole.Create(name, acl); - await admin.Save(); -} catch (LCException e) { - print($"{e.Code} : {e.Message}"); -} -``` - -```java -// 角色本身的 ACL -LCACL roleACL = new LCACL(); -roleACL.setPublicReadAccess(true); -roleACL.setWriteAccess(LCUser.getCurrentUser(),true); - -LCRole admin = new LCRole("admin", roleACL); -admin.saveInBackground().blockingSubscribe(); -``` - -```objc -// 角色本身的 ACL -LCACL *roleACL = [LCACL ACL]; -[roleACL setPublicReadAccess:YES]; -[roleACL setWriteAccess:YES forUser:[LCUser currentUser]]; - -LCRole *admin = [LCRole roleWithName:@"admin" acl:roleACL]; -[admin save]; -``` - -```swift -do { - // 角色本身的 ACL - let roleACL = LCACL() - roleACL.setAccess([.read], allowed: true) - if let currentUserID = LCApplication.default.currentUser?.objectId?.value { - roleACL.setAccess([.write], allowed: true, forUserID: currentUserID) - } - - let admin = LCRole(name: "admin") - admin.ACL = roleACL - assert(admin.save().isSuccess) -} catch { - print(error) -} -``` - -```dart -try { - // 角色本身的 ACL - LCACL acl = LCACL(); - acl.setPublicReadAccess(true); - LCUser currentUser = await LCUser.getCurrent(); - acl.setUserWriteAccess(currentUser, true); - - LCRole admin = LCRole.create('admin', acl); - await admin.save(); -} on LCException catch (e) { - print('${e.code} : ${e.message}'); -} -``` - -```js -// 角色本身的 ACL -const roleAcl = new AV.ACL(); -roleAcl.setPublicReadAccess(true); -roleAcl.setWriteAccess(AV.User.current(), true); - -const admin = new AV.Role('admin', roleAcl); -await admin.save(); -``` - -```python -# 角色本身的 ACL -role_acl = leancloud.ACL() -role_acl.set_public_read_access(True) -current_user_id = leancloud.User.get_current().id -role_acl.set_write_access(current_user_id) - -admin = leancloud.Role('admin', role_acl) -admin.save() -``` - -```php -// 角色本身的 ACL -$roleACL = new ACL(); -$roleACL->setPublicReadAccess(true); -$roleACL->setWriteAccess(User::getCurrentUser(), true); - -$admin = new Role(); -$admin->setName("admin"); -$admin->setACL($roleACL) -$admin->save(); -``` - - - -现在这个 `admin` 角色是空的,我们接下来把当前用户**添加**到这个角色: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -admin.AddRelation("users", currentUser); -``` - -```java -admin.getUsers().add(LCUser.getCurrentUser()); -``` - -```objc -[[admin users] addObject: [LCUser currentUser]]; -``` - -```swift -if let currentUser = LCApplication.default.currentUser { - if let users = admin.users { - try? users.insert(currentUser) - } else { - let users = admin.relationForKey("users") - if (try? users.insert(currentUser)) != nil { - admin.users = users - } - } -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -admin.addRelation('users', currentUser); -``` - -```js -admin.getUsers().add(AV.User.current()); -``` - -```python -admin.get_users().add(leancloud.User.get_current()) -``` - -```php -$admin->getUsers().add(User::getCurrentUser()); -``` - - - -如果我们又想从角色中**移除**用户: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -admin.RemoveRelation("users", currentUser); -``` - -```java -admin.getUsers().remove(LCUser.getCurrentUser()); -``` - -```objc -[[admin users] removeObject:[LCUser currentUser]]; -``` - -```swift -if let currentUser = LCApplication.default.currentUser { - try? admin.users?.remove(currentUser) -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -admin.removeRelation('users', currentUser); -``` - -```js -admin.getUsers().remove(AV.User.current()); -``` - -```python -admin.get_users().remove(leancloud.User.get_current()) -``` - -```php -$admin->getUsers().remove(User::getCurrentUser()); -``` - - - -如前所述,角色的成员可以是另一个角色。 -假定有两个角色,一个「管理员」(`admin`),一个「版主」(`moderator`),我们想让 `admin` 成为 `moderator` 的子角色,因为管理员同时拥有版主的全部权限。 - - - -```cs -moderator.AddRelation("roles", admin); -``` - -```java -moderator.getRoles().add(admin); -``` - -```objc -[[moderator roles] addObject:admin]; -``` - -```swift -if let subroles = moderator.roles { - try? subroles.insert(admin) -} else { - let subroles = moderator.relationForKey("roles") - if (try? subroles.insert(currentUser)) != nil { - admin.roles = subroles - } -} -``` - -```dart -moderator.addRelation('roles', admin); -``` - -```js -moderator.getRoles().add(admin); -``` - -```python -moderator.get_roles().add(admin) -``` - -```php -$moderator->getRoles().add(admin); -``` - - - -偶尔可能想要移除子角色,比如后来我们改变了主意,管理员应该专注于全局性的管理任务,帖子编辑、删除之类的任务全部由版主负责。 - - - -```cs -moderator.RemoveRelation("roles", admin); -``` - -```java -moderator.getRoles().remove(admin); -``` - -```objc -[[moderator roles] removeObject:admin]; -``` - -```swift -try? moderator.roles?.remove(admin) -``` - -```dart -moderator.removeRelation('roles', admin); -``` - -```js -moderator.getRoles().remove(admin); -``` - -```python -moderator.get_roles().remove(admin) -``` - -```php -$moderator->getRoles().remove(admin); -``` - - - -### 角色的查询 - -查询某个用户有哪些角色: - - - -```cs -try { - LCUser currentUser = await LCUser.GetCurrent(); - LCQuery roleQuery = LCRole.GetQuery(); - roleQuery.WhereEqualTo("users", currentUser); - ReadOnlyCollection roles = await roleQuery.Find(); -} catch (LCException e) { - print($"{e.Code} : {e.Message}"); -} -``` - -```java -LCUser user = LCUser.getCurrentUser(); -user.getRolesInBackground().subscribe(new Observer>() { - @Override public void onSubscribe(Disposable d) {} - @Override public void onNext(List avRoles) { - // avRoles 是查询结果 - } - @Override public void onError(Throwable e) {} - @Override public void onComplete() {} -}); -``` - -```objc -LCUser *user = [LCUser currentUser]; -[user getRolesInBackgroundWithBlock:^(NSArray * _Nullable avRoles, NSError * _Nullable error) { - // avRoles 是查询结果 -}]; -``` - -```swift -if let user = LCApplication.default.currentUser { - let roleQuery = LCQuery(className: LCRole.objectClassName()) - roleQuery.whereKey("users", .equalTo(user)) - _ = roleQuery.find { result in - switch result { - case .success(objects: let roles): - print(roles) - case .failure(error: let error): - print(error) - } - } -} -``` - -```dart -try { - LCUser currentUser = await LCUser.getCurrent(); - LCQuery roleQuery = LCRole.getQuery(); - roleQuery.whereEqualTo('users', currentUser); - List roles = await roleQuery.find(); -} on LCException catch (e) { - print('${e.code} : ${e.message}'); -} -``` - -```js -const roles = await AV.User.current().getRoles(); -``` - -```python -roles = leancloud.User.get_current().get_roles() -``` - -```php -$roles = User::getCurrentUser().getRoles(); -``` - - - -查询某个角色包含的用户(这里只给出构建查询的代码): - - - -```cs -LCQuery userQuery = moderator.Users.Query; -``` - -```java -LCQuery userQuery = moderator.getUsers().getQuery(); -``` - -```objc -LCUser *userQuery = [[moderator users] query]; -``` - -```swift -let *userQuery = moderator.users?.query -``` - -```dart -LCQuery userQuery = moderator.users.query(); -``` - -```js -const userQuery = moderator.getUsers().query(); -``` - -```python -user_query = moderator.get_users().query -``` - -```php -$userQuery = $moderator->getUsers().getQuery(); -``` - - - -当然,上面的代码没有考虑子角色中包含的用户。 -如果想要查询角色中包含的所有用户(直接包含和间接包含),需要递归地查出角色的所有子角色(包括子角色的子角色,以此类推),接着查询所有这些角色包含的用户。 -限于篇幅,就不在这里列出完整的代码了,只列出如何构建子角色查询的代码: - - - -```cs -LCQuery subroleQuery = moderator.Roles.Query; -``` - -```java -LCQuery subroleQuery = moderator.getRoles().getQuery(); -``` - -```objc -LCRole *subroleQuery = [[moderator roles] query]; -``` - -```swift -let *subRoleQuery = moderator.roles?.query -``` - -```dart -LCQuery subroleQuery = moderator.roles.query(); -``` - -```js -const subroleQuery = moderator.getRoles().query(); -``` - -```python -subrole_query = moderator.get_roles().query -``` - -```php -$subRoleQuery = $moderator->getRoles().getQuery(); -``` - - - -由于角色继承自结构化存储的对象,你也可以根据角色的其他属性执行各种查询,方式和一般的对象查询是一样的。 - -## 特殊规则 - -因为用户相关信息比较敏感,所以 `_User` 表会忽略 ACL 的设置,任何用户都无法修改其他用户的属性,比如当前登录的用户是 A,而他想通过请求去修改 B 用户的用户名、密码或者其他自定义属性,是不会生效的,即使 B 用户的 ACL 中赋予了 A 写权限也不行。 - -因为 LiveQuery 设计的使用场景是客户端,而客户端不应使用 MasterKey,否则会有极大的安全隐患。 -所以,LiveQuery 订阅事件时会忽略 MasterKey。换言之,LiveQuery 订阅事件时不应使用 MasterKey,即使使用也不会跳过 ACL 等权限检查。 - -## 获取对象的 ACL 值 - -查询数据时,SDK 默认不会返回对象的 ACL 值。如果想在获取对象的同时返回对象的 ACL 值,需要同时满足下面两个条件: - -1. 进入 ** > 服务设置 > 查询设置**,勾选「查询时返回值包括 ACL」。 -2. 客户端查询对象时指定返回 ACL。 - -代码如下: - - - -```cs -LCQuery query = new LCQuery("Todo"); -query.IncludeACL = true; -``` - -```java -LCQuery query = new LCQuery<>("Todo"); -query.includeACL(true); -``` - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -query.includeACL = YES; -``` - -```swift -let query = LCQuery(className: "Todo") -query.includeACL = true -``` - -```dart -LCQuery query = LCQuery('Todo'); -query.includeACL(true); -``` - -```js -var query = new AV.Query('Todo'); -query.includeACL(true); -``` - -```python -query = leancloud.Object.extend('Todo').query.include_acl(True) -``` - -```php -// 暂不支持 -``` - - - -## 最佳实践 - -如果应用的权限控制需求比较简单,我们推荐在控制台恰当设置 [Class 权限](/sdk/storage/guide/security/#class-权限)、[字段权限](/sdk/storage/guide/security/#字段权限)、[默认 ACL](#默认-acl),然后通过客户端代码设置个别需要精细权限控制的 ACL。 - -对于权限控制需求复杂的应用,我们推荐在控制台设置 Class 权限、字段权限、默认 ACL 后,在云引擎统一处理 ACL 相关的逻辑。 -一方面,这样免去了在 iOS、Android、Web 等各处不断升级和维护逻辑十分类似的客户端代码。 -另一方面,云引擎除了处理 ACL 外,还可以通过 hook 基于更复杂的条件进行权限控制,比如不允许发布超过一定字数的帖子。 -详见 [在云引擎中使用 ACL](/sdk/storage/guide/engine-acl/)。 - -对于储存敏感数据、安全性要求非常严苛的 Class,开发者也可以考虑将对应的 [Class 权限](/sdk/storage/guide/security/#class-权限)的写权限乃至读权限完全关闭,客户端所有请求都通过云引擎中转,这样与自己搭建后端具有同样的数据安全性保障。 diff --git a/leancloud/docs/sdk/storage/best-practice/_category_.json b/leancloud/docs/sdk/storage/best-practice/_category_.json deleted file mode 100644 index 4e75adc71..000000000 --- a/leancloud/docs/sdk/storage/best-practice/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "最佳实践", - "collapsed": true, - "position": 18 -} diff --git a/leancloud/docs/sdk/storage/best-practice/dot-notation.md b/leancloud/docs/sdk/storage/best-practice/dot-notation.md deleted file mode 100644 index ecfc8ae2d..000000000 --- a/leancloud/docs/sdk/storage/best-practice/dot-notation.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: 点号使用指南 -slug: /sdk/storage/guide/dot-notation/ -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -许多编程语言中都可以通过点号(`.`)访问对象的属性。 -TDSLeanCloud 的数据存储 SDK 和 REST API 也支持类似的功能。 -善用点号常常可以让代码看上去更简短,降低网络通讯的开销。 - -## 在查询对象时使用点号 - -AVObject / LCObject 字段允许的类型包括对象(`Object`),如果我们想要根据对象的属性发起查询,那么可以使用 `字段名.属性名` 的格式进行查询。 - -例如,假设有一个 `Member` 类,其 `occupation` 字段的结构如下: - -```ts -{ - "profession": string; - "remark": string; - // ... 其他属性 -} -``` - -那么我们可以像这样查询职业为「Other」(其他)的成员: - -```js -query.equalTo("occupation.profession", "Other") -``` - -注意,如果需要频繁查询 `occupation.profession`,可以考虑双写一个 `occupation_profession` 字段,然后为该字段添加索引,以加速查询。 - -在获取对象时,我们可以通过 `select` 指定需要返回的属性。 -当指定的属性的类型是对象时,我们可以在 `select` 条件中使用点号,指定只获取该属性的某个属性。 - -例如,假设包含 `query.select(["occupation"])` 语句的某个查询返回如下对象: - -```js -{ - "occupation": { - "profession": "Other", - "remark": "Feng Shui Consultant" - }, - // objectId、createdAt、updatedAt -} -``` - -那么包含 `query.select(["occupation.profession"])` 语句的相应查询将返回如下对象: - -```js -{ - "occupation": { - "profession": "Other" - }, - // objectId、createdAt、updatedAt -} -``` - -## 在设置对象属性时使用点号 - -同理,我们也可以使用点号直接设置某个对象的属性。例如: - -```js -member.set("occupation.profession", "Software Developer") -``` - -**更新对象**时,无论 `member` 对象上是否存在 `occupation` 字段,均可直接使用 `occupation.profession`。 - -注意,使用点号**创建对象**会报错。这种情况下,可以使用以下方式设置属性: - -```js -member.set("occupation", { "profession": "Software Developer" }) -``` - -更新数组的操作(`add`、`addUnique`、`remove`)同样适用于内嵌于字段的数组,可以搭配点号使用。 -例如,假设 `Article` 类的 `section` 字段的类型为对象(object),其中有一个属性 `tags` 是一个数组,那么我们可以通过以下方式新增标签: - -```js -const article = new AV.Object.createWithoutData('Article', '582570f38ac247004f39c24b'); -article.addUnique('section.tags', ['LeanStorage', 'JavaScript']); -``` - -## Pointer 和点号 - -无需额外查询即可获取来自另一个 Class 的数据的 `include` 语句,也支持点号。 -例如,假设 `Comment` 类的 `post` 字段是一个指向 `Post` 类的 Pointer,而 `Post` 类又有一个指向 `Author` 类的 Pointer 字段 `author`,那么我们可以使用如下语句同时获取评论所属文章的作者: - -```js -query.include('post.author') -``` - -Pointer 中的点号看起来和前面提到的对象中的点号很相似,但两者是独立的功能。例如: - -- `include` 语句中的点号只是在查询到结果后,「展开」指定的 Pointer 字段,而不是在查询前就「展开」Pointer 字段。因此,无法像查询对象属性一样,通过点号对 Pointer 的属性发起查询。 -- 假设 `Comment` 类的 `post` 字段是一个指向 `Post` 类的 Pointer,而 `Post` 类又有一个指向 `Author` 类的 Pointer 字段 `author`, 然后 `Author` 类还有一个指向 `Group` 类的 Pointer 字段 `group`,那么 `query.include('post.author.group')` 可以同时获取评论所属文章的作者所属的群组。但如果 `post` 字段是一个 Pointer,它指向的 `Post` 类的 `author` 字段是一个**对象**,其中包含一个指向 `Group` 类的 `group` 字段,那么无法使用 `query.include('post.author.group')`。 diff --git a/leancloud/docs/sdk/storage/best-practice/engine-acl.mdx b/leancloud/docs/sdk/storage/best-practice/engine-acl.mdx deleted file mode 100644 index 9f808fd33..000000000 --- a/leancloud/docs/sdk/storage/best-practice/engine-acl.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: 在云引擎中使用 ACL -slug: /sdk/storage/guide/engine-acl/ -sidebar_position: 3 ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -**[云引擎](https://developer.taptap.cn/docs/sdk/engine/overview/)** 提供给开发者自定义云端逻辑的接口,例如开发者想记录每一个用户登录系统的时间,打印一下日志,以备之后的查询和分析,那么云引擎提供的接口就可以实现这一需求,详细的操作请查看 **[云函数和 Hook](/sdk/engine/functions/getting-started/)** 文档。 - -## 需求 - -提到以上这个需求是为了让开发者更好地理解以下的需求描述: - -假如开发者的应用有 iOS、Android、Web(JavaScript)各种版本,针对权限管理的逻辑代码会遍布各个客户端,开发者会重复书写逻辑极其类似的代码,只不过是不同语言的版本而已。因此,假如有一个云端的接口,让开发者可以在创建对象或者更新对象的时候,为其添加 ACL 相关的代码,那么各个客户端就无需再重复编码。 - -## 示例 - -我们从一个简单的实例入手: - -我们希望每发一篇帖子,不管是从 iOS 还是 Android,还是任意客户端发出的,都希望管理员具备对它读写的权限。 - -第一步,我们需要编写我们的云引擎 Hook 函数(关于云引擎 Hook 函数介绍请查看 [Save 前执行操作](/sdk/engine/functions/guides/#beforesave)): - - - - - -```js -AV.Cloud.beforeSave('Post', (request) => { - const post = request.object; - if (post) { - var acl = new AV.ACL(); - acl.setPublicReadAccess(true); - // 假定已经存在一个 `admin` 角色 - acl.setRoleWriteAccess('admin', true); - post.setACL(acl); - } else { - throw new AV.Cloud.Error('Invalid Post object.'); - } -}); -``` - - - - - -```python -@engine.before_save('Post') -def before_post_save(post): - acl = leancloud.ACL() - acl.set_public_read_access(True) - # 假定已经存在一个 `admin` 角色 - admin = leancloud.Role('admin') - acl.set_role_write_access(admin, True) - post.set_acl(acl) -``` - - - - - -```php -Cloud::beforeSave("Post", function($post, $user) { - $acl = new ACL(); - $acl->setPublicReadAccess(true); - // 假定已经存在一个 `admin` 角色 - $acl->setRoleWriteAccess("admin", true); - $post->setACL($acl); -}); -``` - - - - - -```java -@EngineHook(className = "Post", type = EngineHookType.beforeSave) -public static AVObject postBeforeSaveHook(AVObject post) throws Exception { - AVACL acl = new AVACL(); - acl.setPublicReadAccess(true); - // 假定已经存在一个 `admin` 角色 - acl.setRoleWriteAccess("admin", true); - post.setACL(acl); - return post; -} -``` - - - - - -[部署代码到云端](/sdk/engine/functions/getting-started/#部署到云引擎)后,每个在客户端创建的帖子,保存后会自动添加如下的 ACL: - -```json -{"*":{"read":true},"role:admin":{"write":true}} -``` diff --git a/leancloud/docs/sdk/storage/best-practice/relation-guide.md b/leancloud/docs/sdk/storage/best-practice/relation-guide.md deleted file mode 100644 index f4a077f54..000000000 --- a/leancloud/docs/sdk/storage/best-practice/relation-guide.md +++ /dev/null @@ -1,1288 +0,0 @@ ---- -title: 数据模型设计 -slug: /sdk/storage/guide/relation/ -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import MultiLang from "/src/docComponents/MultiLang"; - -多年以来,关系型数据库已经成为了企业数据管理的基础,很多工程师对于关系模型和 6 个范式都比较了解,但是如今来构建和运行一个应用,随着数据来源的越发多样和用户量的不断增长,关系数据库的限制逐渐成为业务的瓶颈,因此越来越多的公司开始向其他 NoSQL 数据库进行迁移。 - -

    TDSLeanCloud 的数据存储后台大量采用了 MongoDB 这种文档数据库来存储结构化数据,正因如此我们才能提供面向对象的、海量的、无需创建数据表结构即存即用的存储能力。从传统的关系型数据库转换到 TDSLeanCloud 或者 MongoDB 存储系统,最基础的改变就是「数据建模 Schema 设计」。

    - -

    首先来梳理一下关系型数据库、MongoDB 和 TDSLeanCloud 数据存储的对应术语:

    - -RDBMS | MongoDB | TDSLeanCloud 数据存储 --------- | ---------- | ----------------- -Database | Database | Application -Table | Collection | Class -Row | Document | Object -Index | Index | Index -JOIN | Embedded,Reference | Embedded Object, Pointer - -在 TDSLeanCloud 上进行数据建模设计需要数据架构师、开发人员和 DBA 在观念上做一些转变:之前是传统的关系型数据模型,所有数据都会被映射到二维的表结构「行」和「列」;现在是丰富、动态的对象模型,即 MongoDB 的「文档模型」,包括内嵌子对象和数组。 - -## 文档模型 - -:::info -后文中我们有时候采用 TDSLeanCloud 数据存储的核心概念 **Object(对象)**,有时候提到 MongoDB 中的名词 **Document(文档)**,它们是等同的。 -::: - -我们现在使用的大部分数据都有比较复杂的结构,用「JSON 对象」来建模比用「表」会更加高效。通过内嵌子对象和数组,JSON 对象可以和应用层的数据结构完全对齐。这对开发者来说,会更容易将应用层的数据映射到数据库里的对象。相反,将应用层的数据映射到关系数据库的表,则会降低开发效率。而比较普遍的增加额外的对象关系映射(ORM)层的做法,也同时降低了 schema 扩展和查询优化的灵活性,引入了新的复杂度。 - -例如,在 RDBMS 中有父子关系的两张表,通常就会变成 TDSLeanCloud 里面含有内嵌子对象的单文档结构。以下图的数据为例: - -**PERSON 表** - -Person_ID | Surname | First_Name | City -------- | ------- | ---------- | ---- -0 | 柳 | 红 | 伦敦 -1 | 杨 | 真 | 北京 -2       |   王      | 新         | 苏黎世 - -**CAR 表** - -Car_ID | Model | Year | Value | Person_ID ------- | ----- | ---- | ----- | ------- -101 | 大众迈腾 | 2015 | 180000 | 0 -102 | 丰田汉兰达 | 2016 | 240000 | 0 -103 | 福特翼虎 | 2014 | 220000 | 1 -104 | 现代索纳塔 | 2013 | 150000 | 2 - -RDBMS 中通过 Person_ID 域来连接 PERSON 表和 CAR 表,以此支持应用中显示每辆车的拥有者信息。使用文档模型,通过内嵌子对象和数组可以将相关数据提前合并到一个单一的数据结构中,传统的跨表的行和列现在都被存储到了一个文档内,完全省略掉了 join 操作。 - -换成 TDSLeanCloud 来对同样的数据建模,则允许我们创建这样的 schema:一个单一的 Person 对象,里面通过一个子对象数组来保存该用户所拥有的每一部 Car,例如: - -```json -{ - "first_name":"红", - "surname":"柳", - "city":"伦敦", - "location":[ - 45.123, - 47.232 - ], - "cars":[ - { - "model":"大众迈腾", - "year":2015, - "value":180000 - }, - { - "model":"丰田汉兰达", - "year":2016, - "value":240000 - } - ] -} -``` - -文档数据库里的一篇文档,就相当于 TDSLeanCloud 平台里的一个对象。这个例子里的关系模型虽然只由两张表组成(现实中大部分应用可能需要几十、几百甚至上千张表),但是它并不影响我们思考数据的方式。 - -为了更好地展示关系模型和文档模型的区别,我们用一个博客平台来举例。从下图中可以看出,依赖 RDBMS 的应用需要 join 五张不同的表来获得一篇博客的完整数据,而在 TDSLeanCloud 中所有的博客数据都包含在一个文档中,博客作者和评论者的用户信息则通过一个到 User 的引用(指针)进行关联。 - -![](/img/storage/rdbm-vs-mongodb.png) - -## 文档模型的优点 - -除了数据表现更加自然之外,文档模型还有性能和扩展性方面的优势: - -- 通过单一调用即可获得完整的文档,避免了多表 join 的开销。TDSLeanCloud 的 Object 物理上作为一个单一的块进行存储,只需要一次内存或者磁盘的读操作即可。RDBMS 与此相反,一个 join 操作需要从不同地方多次读取操作才可完成。 -- 文档是自包含的,将数据库内容分布到多个节点(也叫 Sharding)会更简单,同时也更容易通过普通硬件的水平扩展获得更高性能。DBA 们不再需要担心跨节点进行 join 操作可能带来的性能恶化问题。 - -## 定义文档 Schema - -应用的数据访问模式决定了 schema 设计,因此我们需要特别明确以下几点: - -- 数据库读写操作的比例以及是否需要重点优化某一方的性能; -- 对数据库进行查询和更新的操作类型; -- 数据生命周期和文档的增长率; - -以此来设计更合适的 schema 结构。 - -对于普通的「属性名:值」来说,设计比较简单,和 RDBMS 中平坦的表结构差别不大。对于「一对一」或「一对多」的关系会很自然地考虑使用内嵌对象: - -- 数据「所有」和「包含」的关系,都可以通过内嵌对象来进行建模。 -- 同时,在架构上也可以把那些经常需要同时、原子改动的属性作为一个对象嵌入到一个单独的属性中。 - -例如,为了记录每个学生的家庭住址,我们可以把住址信息作为一个整体嵌入 Student 类里面。 - - - - -```cs - LCObject studentTom = new LCObject("Student"); - studentTom["name"] = "Tom"; - var addr = new Dictionary(); - addr["city"] = "北京"; - addr["address"] = "西城区西长安街 1 号"; - addr["postcode"] = "100017"; - studentTom["address"] = addr; - await studentTom.Save(); -``` - -```java - AVObject studentTom = new AVObject("Student"); - studentTom.put("name", "Tom"); - HashMap addr = new HashMap<>(); - addr.put("city", "北京"); - addr.put("address", "西城区西长安街 1 号"); - addr.put("postcode", "100017"); - studentTom.put("address", addr); - studentTom.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(AVObject studentTom) { - // 成功保存之后,执行其他逻辑 - System.out.println("保存成功。objectId:" + studentTom.getObjectId()); - } - public void onError(Throwable throwable) { - // 异常处理 - } - public void onComplete() {} -}); -``` - -```objc - LCObject *studentTom = [LCObject objectWithClassName:@"Student"]; - [studentTom setObject:@"Tom" forKey:@"name"]; - NSDictionary *addr = [NSDictionary dictionaryWithObjectsAndKeys: - @"北京", @"city", - @"西城区西长安街 1 号", @"address", - @"100017", @"postcode", - nil]; - [studentTom setObject:addr forKey:@"address"]; - [studentTom saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 成功保存之后,执行其他逻辑 - NSLog(@"保存成功。objectId:%@", studentTom.objectId); - } else { - // 异常处理 - } -}]; -``` - -```swift -do { - let studentTom = LCObject(className: "Student") - - try studentTom.set("name", "Tom") - try studentTom.set("address", [ - "city" : "北京", - "address" : "西城区西长安街 1 号", - "postcode" : "100017" - ] - - _ = studentTom.save { result in - switch result { - case .success: - // 成功保存之后,执行其他逻辑 - break - case .failure(error: let error): - // 异常处理 - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -// 构建对象 -LCObject studentTom = LCObject('Student'); -studentTom['name'] = 'Tom'; -Map address = { - 'city': '北京', - 'address': '西城区西长安街 1 号', - 'postcode': '100017' -}; -studentTom['address'] = address; -await studentTom.save(); -``` - -```js - // 学生 Tom - const studentTom = new AV.Object('Student'); - studentTom.set('name', 'Tom'); - const addr = { "city": "北京", "address": "西城区西长安街 1 号", "postcode":"100017" }; - studentTom.set('address', addr); - studentTom.save().then((studentTom) => { - // 成功保存之后,执行其他逻辑 - console.log(`保存成功。objectId:${studentTom.id}`); - }, (error) => { - // 异常处理 - }); -``` - -```python - student_tom = leancloud.Object.extend("Student")() - student_tom.set('name', 'Tom') - - addr = { "city": "北京", "address": "西城区西长安街 1 号", "postcode":"100017" } - student_tom.set('address', addr) - - student_tom.save() -``` - -```php -$studentTom = new LeanObject("Student"); -$studentTom->set("name", "Tom"); -$addr = array("city" => "北京", "address" => "西城区西长安街 1 号", "postcode" => "100017"); -$studentTom->set("address", $addr); -$studentTom->save(); -``` - - - -但并不是所有的一对一关系都适合内嵌的方式,对下面的情况后文介绍的「引用」(等同于 MongoDB 的 reference)方式会更加合适: - -- 一个对象被频繁地读取,但是内嵌的子对象却很少会被访问。 -- 对象的一部分属性频繁地被更新,数据大小持续增长,但是剩下的一部分属性基本稳定不变。 -- 对象大小超过了 TDSLeanCloud 当前最大 16 MB 限制。 - -接下来我们重点讨论一下在 TDSLeanCloud 上如何通过「引用」机制来实现复杂的关系模型。 - -数据对象之间存在 3 种类型的关系:「一对一」将一个对象与另一个对象关联,「一对多」是一个对象关联多个对象,「多对多」则用来实现大量对象之间的复杂关系。 - -我们支持 3 种方式来构建对象之间的关系,这些都是通过 MongoDB 的文档引用来实现的: - -1. Pointers(适合一对一、一对多关系) -2. 中间表(多对多) -3. ~~Arrays(一对多、多对多)~~ 不建议使用,请参考 [何时使用数组](#何时使用数组)。 - -## 一对多关系 - -### Pointers 存储 - -中国的「省份」与「城市」具有典型的一对多的关系。深圳和广州(城市)都属于广东省(省份),而朝阳区和海淀区(行政区)只能属于北京市(直辖市)。广东省对应着多个一级行政城市,北京对应着多个行政区。下面我们使用 Pointers 来存储这种一对多的关系。 - -:::info -为了表述方便,后文中提及城市都泛指一级行政市以及直辖市行政区,而省份也包含了北京、上海等直辖市。 -::: - - - -```cs - LCObject guangZhou = new LCObject("City"); - guangZhou["name"] = "广州"; - - LCObject guangDong = new LCObject("Province"); - guangDong["name"] = "广东"; - - guangZhou["dependent"] = guangDong; - - // 广东无需单独保存,因为在保存广州时自动保存了广东 - await guangZhou.Save(); -``` - -```java -AVObject guangZhou = new AVObject("City"); -guangZhou.put("name", "广州"); -AVObject guangDong = new AVObject("Province"); -guangDong.put("name", "广东"); -guangZhou.put("dependent", guangDong); -guangZhou.saveInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(AVObject guangZhou) { - // 广州、广东保存成功(广东无需单独保存) - } - @Override - public void onError(Throwable e) { - } - @Override - public void onComplete() { - } -}); -``` - -```objc - LCObject *GuangZhou = [LCObject objectWithClassName:@"City"]; - [GuangZhou setObject:@"广州" forKey:@"name"]; - - LCObject *GuangDong = [LCObject objectWithClassName:@"Province"]; - [GuangDong setObject:@"广东" forKey:@"name"]; - - [GuangZhou setObject:GuangDong forKey:@"dependent"]; - - [GuangZhou saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 广州、广东保存成功(广东无需单独保存) - } - }]; -``` - -```swift -do { - let guangZhou = LCObject(className: "City") - try guangZhou.set("name", value:"广州") - - let guangDong = LCObject(className: "Province") - try guangDong.set("name", value:"广东") - - try guangZhou.set("dependent", guangDong) - - _ = guangZhou.save { result in - switch result { - case .success: - // 广州、广东保存成功(广东无需单独保存) - break - case .failure(error: let error): - // 异常处理 - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -LCObject guangZhou = LCObject('City'); -guangZhou['name'] = '广州'; - -LCObject guangDong = LCObject('Province'); -guangDong['name'] = '广东'; - -guangZhou['dependent'] = guangDong; - -// 广东无需单独保存,因为在保存广州时自动保存了广东 -await guangZhou.save(); -``` - -```js - // 新建一个 AV.Object - const GuangZhou = new AV.Object('City'); - GuangZhou.set('name', '广州'); - const GuangDong = new AV.Object('Province'); - GuangDong.set('name', '广东'); - GuangZhou.set('dependent', GuangDong); - GuangZhou.save().then(function (guangZhou) { - // 广州、广东保存成功(广东无需单独保存) - console.log(guangZhou.id); - }); -``` - -```python - guang_zhou = leancloud.Object.extend('City')() - guang_zhou.set('name', '广州') - - guang_dong = leancloud.Object.extend('Province')() - guang_dong.set('name', '广东') - - guang_zhou.set('dependent', guang_dong) - - # 广东无需单独保存,因为在保存广州时自动保存了广东 - guang_zhou.save() -``` - -```php -$guangZhou = new LeanObject("City"); -$guangZhou->set("name", "广州"); - -$guangDong = new LeanObject("Province"); -$guangDong->set("name", "广东"); - -$guangZhou->set("dependent", $guangDong); - -// 广东无需单独保存,因为在保存广州时自动保存了广东 -$guangZhou->save(); -``` - - - -保存关联对象的同时,被关联的对象也会随之被保存到云端。 -执行上述代码后,在应用控制台可以看到 `dependent` 字段显示为 Pointer 数据类型,而它本质上存储的是一个指向 `Province` 这张表的某个 AVObject 的指针。 - -要关联一个已经存在于云端的对象,例如将「东莞市」添加至「广东省」(假设广东的 objectId 为 `56545c5b00b09f857a603632`),方法如下: - - - -```cs - LCObject guangDong = LCObject.CreateWithoutData("Province", "56545c5b00b09f857a603632"); - LCObject dongGuan = new LCObject("City"); - dongGuan["name"] = "东莞"; -``` - -```java - AVObject guangDong = AVObject.createWithoutData("Province", "56545c5b00b09f857a603632"); - AVObject dongGuan = new AVObject("City"); - dongGuan.put("name", "东莞"); - dongGuan.put("dependent", guangDong); -``` - -```objc - LCObject *GuangDong = [LCObject objectWithClassName:@"Province" objectId:@"56545c5b00b09f857a603632"]; - LCObject *DongGuan = [LCObject objectWithClassName:@"City"]; - [DongGuan setObject:@"东莞" forKey:@"name"]; - [DongGuan setObject:GuangDong forKey:@"dependent"]; -``` - -```swift - let guangDong = LCObject(className: "Province", objectId: "56545c5b00b09f857a603632") - let dongGuan = LCObject(className: "City") - try dongGuan.set("name", value:"东莞") - dongGuan["dependent"] = guangDong -``` - -```dart -LCObject guangDong = LCObject.createWithoutData('Province', '56545c5b00b09f857a603632'); -LCObject DongGuan = LCObject('City'); -DongGuan['name'] = '东莞'; -DongGuan['dependent'] = guangDong; -``` - -```js - const GuangDong = AV.Object.createWithoutData('Province', '56545c5b00b09f857a603632'); - const DongGuan = new AV.Object('City'); - DongGuan.set('name', '东莞'); - DongGuan.set('dependent', GuangDong); -``` - -```python - Province = leancloud.Object.extend('Province') - guang_dong = Province.create_without_data('574416af79bc44005c61bfa3') - dong_guan = leancloud.Object.extend('City')() - dong_guan.set('name', '东莞') - dong_guan.set('dependent', guang_dong) -``` - -```php -$guangDong = LeanObject::create("Province", "574416af79bc44005c61bfa3"); -$dongGuan = new LeanObject("City"); -$dongGuan->set("name", "东莞"); -$dongGuan->set("dependent", $guangDong); -``` - - - -注意,为了节约篇幅,以上代码中省略了保存对象的代码。 - -### Pointers 查询 - -想知道广州属于哪个省份: - - - -```cs - LCQuery query = new LCQuery("City"); - // 查询名字是广州的城市 - query = query.WhereEqualTo("name", "广州"); - // 告知云端还要一并获取对应城市的省份 - query = query.Include("dependent"); - LCObject city = await query.First(); - LCObject province = city["dependent"] as LCObject; -``` - -```java -AVQuery query = new AVQuery<>("City"); -query.whereEqualTo("name", "广州"); -query.include("dependent"); -query.getFirstInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List city) { - AVObject province = city.getAVObject("dependent"); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -```objc - LCQuery *query = [LCQuery queryWithClassName:@"City"]; - [query whereKey:@"name" equalTo:@"广州"]; - [query includeKey:@"dependent"]; - - [query getFirstObjectInBackgroundWithBlock:^(LCObject *city, NSError *error) { - LCObject *province = [city objectForKey:@"dependent"]; - }]; -``` - -```swift - let query = LCQuery(className: "City") - query.whereKey("name", .equalTo: "广州") - query.whereKey("dependent", .included) - _ = query.getFirst { result in - switch result { - case .success(object: let city): - let province = city["dependent"] as? LCObject - case .failure(error: let error): - print(error) - } - } -``` - -```dart -LCQuery query = LCQuery('City'); -query.whereEqualTo('name', '广州'); -query.include('dependent'); -LCObject city = await query.First(); -LCObject province = city['dependent']; -``` - -```js - const query = new AV.Query('City'); - query.equalTo('name', '广州'); - query.include(['dependent']); - query.first().then((city) => { - const province = city.get('dependent'); - }); -``` - -```python - query = leancloud.Query("City") - query.equal_to('name', '广州') - query.include('dependent') - city = query.first() - province = city.get('dependent') -``` - -```php -$query = new Query("City"); -$query->equalTo("name", "广州"); -$query->_include("dependent"); -$city = $query->first(); -$province = $city->get("dependent"); -``` - - - -想知道哪些城市属于广东省(假定代表广东省的对象的 objectId 是 `56545c5b00b09f857a603632`) ? - - - -```cs - LCObject guangDong = LCObject.CreateWithoutData("Province", "56545c5b00b09f857a603632"); - LCQuery query = new LCQuery("City"); - query.WhereEqualTo("dependent", guangDong); - // cities 为广东省下辖的所有城市 - ReadOnlyCollection cities = await query.Find(); -``` - -```java - AVObject guangDong = AVObject.createWithoutData("Province", "56545c5b00b09f857a603632"); - AVQuery query = new AVQuery<>("City"); - query.whereEqualTo("dependent", guangDong); - query.findInBackground().subscribe(new Observer>() { - @Override - public void onSubscribe(Disposable d) {} - @Override - public void onNext(List cities) { - for (AVObject city : cities) { - // cities 为广东省下辖的所有城市 - } - } - @Override - public void onError(Throwable e) {} - @Override - public void onComplete() {} - }); -``` - -```objc - LCObject *GuangDong = [LCObject objectWithClassName:@"Province" objectId:@"56545c5b00b09f857a603632"]; - LCQuery *query = [LCQuery queryWithClassName:@"City"]; - [query whereKey:@"dependent" equalTo:GuangDong]; - [query findObjectsInBackgroundWithBlock:^(NSArray *cities, NSError *error) { - for (LCObject *city in cities) { - // cities 为广东省下辖的所有城市 - } - }]; -``` - -```swift - let guangDong = LCObject(className: "Province", objectId: "56545c5b00b09f857a603632") - let query = LCQuery(className: "City") - query.whereKey("dependent", .equalTo(GuangDong)) - _ = query.find { (result) in - switch result { - case .success(objects: let cities): - for city in cities { - // cities 为广东省下辖的所有城市 - } - case .failure(error: let error): - print(error) - } - } -``` - -```dart -LCObject guangDong = LCObject.createWithoutData('Province', '56545c5b00b09f857a603632'); -LCQuery query = LCQuery('City'); -query.whereEqualTo('dependent', guangDong); -// cities 为广东省下辖的所有城市 -List cities = await query.find(); -``` - -```js - const GuangDong = AV.Object.createWithoutData('Province', '56545c5b00b09f857a603632'); - const query = new AV.Query('City'); - query.equalTo('dependent', GuangDong); - query.find().then((cities) => { - // cities 为广东省下辖的所有城市 - }); -``` - -```python - Province = leancloud.Object.extend('Province') - guang_dong = Province.create_without_data('574416af79bc44005c61bfa3') - query = leancloud.Query('City') - query.equal_to('dependent', guang_dong) - # cities 为广东省下辖的所有城市 - cities = query.find(): -``` - -```php -$guangDong = LeanObject::create("Province", "574416af79bc44005c61bfa3"); -$query = new Query("City"); -$query->equalTo("dependent", $guangDong); -// cities 为广东省下辖的所有城市 -$cities = $query->find(); -``` - - - -大多数场景下,Pointers 是实现一对多关系的最好选择。 - -## 多对多关系 - -假设有选课应用,我们需要为 `Student` 对象和 `Course` 对象建模。一个学生可以选多门课程,一个课程也有多个学生,这是一个典型的多对多关系。我们必须使用 Arrays 或创建自己的中间表来实现这种关系。决策的关键在于**是否需要为这个关系附加一些属性**。 - -如果不需要附加属性,使用 Arrays 最为简单。通常情况下,使用 Arrays 可以使用更少的查询并获得更好的性能。 - -:::tip -如果多对多关系中任何一方对象数量可能达到或超过 100,使用中间表是更好的选择。 -::: - -反之,若需要为关系附加一些属性,就创建一个独立的表(中间表)来存储两端的关系。记住,附加的属性是描述这个关系的,不是描述关系中的任何一方。所附加的属性可以是: - -- 关系创建的时间 -- 关系创建者 -- 某人查看此关系的次数 - -### 使用中间表实现多对多关系(推荐) - -有时我们需要知道更多关系的附加信息,比如在一个学生选课系统中,我们要了解学生打算选修的这门课的课时有多长,或者学生选修是通过手机选修还是通过网站操作的,此时我们可以使用传统的数据模型设计方法「中间表」。 - -为此,我们创建一个独立的表 `StudentCourseMap` 来保存 `Student` 和 `Course` 的关系: - -字段|类型|说明 ----|---|--- -`course`|Pointer|Course 指针实例 -`student`|Pointer|Student 指针实例 -`duration`|Array|所选课程的开始和结束时间点,如 `["2016-02-19","2016-04-21"]`。 -`platform`|String|操作时使用的设备,如 `iOS`。 - -如此,实现选修功能的代码如下: - - - -```cs - LCObject studentTom = new LCObject("Student"); - studentTom["name"] = "Tom"; - - LCObject courseLinearAlgebra = new LCObject("Course"); - courseLinearAlgebra["name"] = "线性代数"; - - LCObject studentCourseMapTom = new LCObject("StudentCourseMap"); - - // 设置关联 - studentCourseMapTom["student"] = studentTom; - studentCourseMapTom["course"] = courseLinearAlgebra; - - // 设置学习周期 - studentCourseMapTom["duration"] = new string[] { "2016-02-19", "2016-04-21" }); - // 获取操作平台 - studentCourseMapTom["platform"] = "iOS"; - - // 保存选课表对象 - await studentCourseMapTom.Save(); -``` - -```java - AVObject studentTom = new AVObject("Student"); - studentTom.put("name", "Tom"); - - AVObject courseLinearAlgebra = new AVObject("Course"); - courseLinearAlgebra.put("name", "线性代数"); - - AVObject studentCourseMapTom = new AVObject("StudentCourseMap"); - - // 设置关联 - studentCourseMapTom.put("student", studentTom); - studentCourseMapTom.put("course", courseLinearAlgebra); - - // 设置学习周期 - studentCourseMapTom.put("duration", Arrays.asList("2016-02-19", "2016-04-21")); - // 获取操作平台 - studentCourseMapTom.put("platform", "iOS"); - - // 保存选课表对象 - studentCourseMapTom.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(AVObject studentCourseMapTom) { - // 成功保存之后,执行其他逻辑 - } - public void onError(Throwable throwable) { - // 异常处理 - } - public void onComplete() {} -}); -``` - -```objc - LCObject *studentTom = [LCObject objectWithClassName:@"Student"]; - [studentTom setObject:@"Tom" forKey:@"name"]; - - LCObject *courseLinearAlgebra = [LCObject objectWithClassName:@"Course"]; - [courseLinearAlgebra setObject:@"线性代数" forKey:@"name"]; - - LCObject *studentCourseMapTom = [LCObject objectWithClassName:@"StudentCourseMap"]; - - // 设置关联 - [studentCourseMapTom setObject:studentTom forKey:@"student"]; - [studentCourseMapTom setObject:courseLinearAlgebra forKey:@"course"]; - - // 设置学习周期 - [studentCourseMapTom setObject:[NSArray arrayWithObjects:@"2016-02-19",@"2016-04-21", nil] forKey:@"duration"]; - // 获取操作平台 - [studentCourseMapTom setObject:@"iOS" forKey:@"platform"]; - - // 保存选课表对象 - [studentCourseMapTom saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 成功保存之后,执行其他逻辑 - } else { - // 异常处理 - } -}]; -``` - -```swift -do { - let studentTom = LCObject(className: "Student") - try studentTom.set("name", value: "Tom") - - let courseLinearAlgebra = LCObject(className: "Course") - try courseLinearAlgebra.set("name"), value: "线性代数") - - let studentCourseMapTom = LCObject(className: "StudentCourseMap") - - try studentCourseMapTom.set("student", value: studentTom) - try studentCourseMapTom.set("course", value: courseLinearAlgebra) - try studentCourseMapTom.set("duration", value: ["2016-02-19", "2016-04-21"]) - try studentCourseMapTom.set("platform", value: "iOS") - - _ = studentCourseMapTom.save { result in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -LCObject studentTom = LCObject('Student'); -studentTom['name'] = 'Tom'; - -LCObject courseLinearAlgebra = LCObject('Course'); -courseLinearAlgebra['name'] = '线性代数'; - -LCObject studentCourseMapTom = LCObject('StudentCourseMap'); - -// 设置关联 -studentCourseMapTom['student'] = studentTom; -studentCourseMapTom['course'] = courseLinearAlgebra; -// 设置学习周期 -studentCourseMapTom['duration'] = ['2016-02-19', '2016-04-21']; -// 获取操作平台 -studentCourseMapTom['platform'] = 'iOS'; -// 保存选课表对象 -await studentCourseMapTom.save(); -``` - -```js - const studentTom = new AV.Object('Student'); - studentTom.set('name', 'Tom'); - - const courseLinearAlgebra = new AV.Object('Course'); - courseLinearAlgebra.set('name', '线性代数'); - - const studentCourseMapTom = new AV.Object('StudentCourseMap'); - - // 设置关联 - studentCourseMapTom.set('student', studentTom); - studentCourseMapTom.set('course', courseLinearAlgebra); - - // 设置学习周期 - studentCourseMapTom.set('duration', ["2016-02-19", "2016-04-12"]); - - // 设置操作平台 - studentCourseMapTom.set('platform', 'web'); - - // 保存选课表对象 - studentCourseMapTom.save().then((studentCourseMapTom) => { - // 成功保存之后,执行其他逻辑 - console.log(`保存成功。objectId:${todo.id}`); - }, (error) => { - // 异常处理 - }); -``` - -```python - student_tom = leancloud.Object.extend('Student')() - student_tom.set('name', 'Tom') - - course_linear_algebra = leancloud.Object.extend('Course')() - course_linear_algebra.set('name', '线性代数') - - student_course_map_tom = leancloud.Object.extend('Student_course_map')() - - # 设置关联 - student_course_map_tom.set('student', student_tom) - student_course_map_tom.set('course', course_linear_algebra) - - # 设置学习周期 - student_course_map_tom.set('duration', ["2016-02-19", "2016-04-12"]) - - # 设置操作平台 - student_course_map_tom.set('platform', 'ios') - - # 保存选课表对象 - student_course_map_tom.save() -``` - -```php -$studentTom = new LeanObject("Student"); -$studentTom->set("name", "Tom"); - -$courseLinearAlgebra = new LeanObject("Course"); -$courseLinearAlgebra->set("name", "线性代数"); - -$studentCourseMapTom = new LeanObject("StudentCourseMap"); - -// 设置关联 -$studentCourseMapTom->set("student", $studentTom); -$studentCourseMapTom->set("course", $courseLinearAlgebra); - -// 设置学习周期 -$studentCourseMapTom->set("duration", array("2016-02-19", "2016-04-12")); - -// 设置操作平台 -$studentCourseMapTom->set("platform", "ios"); - -// 保存选课表对象 -$studentCourseMapTom->save(); -``` - - - -查询选修了某一课程的所有学生: - - - -```cs - // 线性代数课程 - LCObject courseLinearAlgebra = LCObject.CreateWithoutData("Course", "562da3fdddb2084a8a576d49"); - - // 构建 StudentCourseMap 的查询 - LCQuery query = new LCQuery("StudentCourseMap"); - - // 查询所有选择了线性代数的学生 - query.WhereEqualTo("course", courseLinearAlgebra); - - ReadOnlyCollection studentCourseMaps = await query.Find(); - foreach (var studentCourseMap in studentCourseMaps) - { - LCObject student = studentCourseMap["student"] as LCObject; - List duration = studentCourseMap["duration"] as List; - string platform = studentCourseMap["platform"] as string; - } -``` - -```java -// 线性代数课程 -AVObject courseLinearAlgebra = AVObject.createWithoutData("Course", "562da3fdddb2084a8a576d49"); -// 构建 StudentCourseMap 的查询 -AVQuery query = new AVQuery<>("StudentCourseMap"); -// 查询所有选择了线性代数的学生 -query.whereEqualTo("course", courseLinearAlgebra); -// 执行查询 -query.findInBackground().subscribe(new Observer>() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(List list) { - // list 是所有 course 等于线性代数的选课对象 - // 然后遍历过程中可以访问每一个选课对象的 student,course,duration,platform 等属性 - for (AVObject studentCourseMap : list) { - AVObject student = studentCourseMap.getAVObject("student"); - ArrayList duration = (ArrayList) studentCourseMap.getList("duration"); - String platform = studentCourseMap.getString("platform"); - } - } - @Override - public void onError(Throwable e) { - } - @Override - public void onComplete() { - } -}); -``` - -```objc - // 线性代数课程 - LCObject *courseLinearAlgebra = [LCObject objectWithClassName:@"Course" objectId:@"562da3fdddb2084a8a576d49"]; - - // 构建 StudentCourseMap 的查询 - LCQuery *query = [LCQuery queryWithClassName:@"StudentCourseMap"]; - - // 查询所有选择了线性代数的学生 - [query whereKey:@"course" equalTo:courseLinearAlgebra]; - - // 执行查询 - [query findObjectsInBackgroundWithBlock:^(NSArray *studentCourseMaps, NSError *error) { - // studentCourseMaps 是所有 course 等于线性代数的选课对象 - // 然后遍历过程中可以访问每一个选课对象的 student,course,duration,platform 等属性 - for (LCObject *studentCourseMap in studentCourseMaps) { - LCObject *student =[studentCourseMap objectForKey:@"student"]; - NSArray *duration = [studentCourseMap objectForKey:@"duration"]; - NSLog(@"platform: %@", [studentCourseMap objectForKey:@"platform"]); - } - }]; -``` - -```swift - let query = LCQuery(className: "StudentCourseMap") - let courseLinearAlgebra = LCObject(className: "Course", objectId: "562da3fdddb2084a8a576d49") - - query.whereKey("course", equalTo: courseLinearAlgebra) - _ = query.find { (result) in - switch result { - case .success(objects: let studentCourseMaps): - for studentCourseMap in studentCourseMaps { - let student = studentCourseMap["student"] as? LCObject - let duration = studentCourseMap["duration"] as? LCArray - let platform = studentCourseMap["platform"] as? LCString - } - case .failure(error: let error): - print(error) - } - } -``` - -```dart -// 线性代数课程 -LCObject courseLinearAlgebra = LCObject.createWithoutData('Course', '562da3fdddb2084a8a576d49'); -// 构建 StudentCourseMap 的查询 -LCQuery query = LCQuery('StudentCourseMap'); -// 查询所有选择了线性代数的学生 -query.whereEqualTo('course', courseLinearAlgebra); -List list = await query.find(); -// list 是所有 course 等于线性代数的选课对象 -// 然后遍历过程中可以访问每一个选课对象的 student,course,duration,platform 等属性 -for (LCObject studentCourseMap in list) { - LCObject student = studentCourseMap['student']; - LCObject course = studentCourseMap['course']; - List duration = studentCourseMap['duration']; - String platform = studentCourseMap['platform']; -} -``` - -```js - // 线性代数课程 - const courseLinearAlgebra = AV.Object.createWithoutData('Course', '562da3fdddb2084a8a576d49'); - - // 构建 StudentCourseMap 的查询 - const query = new AV.Query('StudentCourseMap'); - - // 查询所有选择了线性代数的学生 - query.equalTo('course', courseLinearAlgebra); - - // 执行查询 - query.find().then(function (studentCourseMaps) { - // studentCourseMaps 是所有 course 等于线性代数的选课对象 - // 然后遍历过程中可以访问每一个选课对象的 student,course,duration,platform 等属性 - studentCourseMaps.forEach(function (scm, i, a) { - const student = scm.get('student'); - const duration = scm.get('duration'); - const platform = scm.get('platform'); - }); - }); -``` - -```python - Course = leancloud.Object.extend('Course') - course_linear_algebra = Course.create_without_data('57448184c26a38006b8d4761') - query = leancloud.Query('Student_course_map') - query.equal_to('course', course_linear_algebra) - - # 查询所有选择了线性代数的学生 - student_course_map_list = query.find() - - # list 是所有 course 等于线性代数的选课对象, - # 然后遍历过程中可以访问每一个选课对象的 student,course,duration,platform 等属性 - for student_course_map in student_course_map_list: - student = student_course_map.get('student') - duration = student_course_map.get('duration') - platform = student_course_map.get('platform') -``` - -```php -$courseLinearAlgebra = LeanObject::create("Course", "57448184c26a38006b8d4761"); -$query = new Query("Student_course_map"); -$query->equalTo("course", $courseLinearAlgebra); - -// 查询所有选择了线性代数的学生 -$studentCourseMaps = $query->find(); - -foreach ($studentCourseMaps as $studentCourseMap) { - $student = $studentCourseMap->get("student"); - $duration = $studentCourseMap->get("duration"); - $platform = $studentCourseMap->get("platform"); -} -``` - - - -同样我们也可以很简单地查询某一个学生选修的所有课程,只需将上述代码变换查询条件即可: - - - -```objc - LCQuery *query = [LCQuery queryWithClassName:@"StudentCourseMap"]; - LCObject *studentTom = [LCObject objectWithClassName:@"Student" objectId:@"562da3fc00b0bf37b117c250"]; - [query whereKey:@"student" equalTo:studentTom]; -``` - -```swift - let query = LCQuery(className: "StudentCourseMap") - let studentTom = LCObject(className: "Student", objectId: "562da3fc00b0bf37b117c250") - - query.whereKey("student", equalTo: studentTom) -``` - -```java - AVQuery query = new AVQuery<>("StudentCourseMap"); - AVObject studentTom = AVObject.createWithoutData("Student", "562da3fc00b0bf37b117c250"); - query.whereEqualTo("student", studentTom); -``` - -```js - const studentTom = AV.Object.createWithoutData('Student', '579f0441128fe10054420d49'); - const query = new AV.Query('StudentCourseMap'); - query.equalTo('student', studentTom); -``` - -```python - Student = leancloud.Object.extend('Student') - student_tom = Student.create_without_data("562da3fc00b0bf37b117c250") - query.whereEqualTo("student", student_tom) -``` - -```php -$studentTom = LeanObject::create("Student", "562da3fc00b0bf37b117c250"); -$query->equalTo("student", $studentTom); -``` - -```cs - LCQuery query = new LCQuery("StudentCourseMap"); - LCObject studentTom = LCObject.CreateWithoutData("Student", "562da3fc00b0bf37b117c250"); - query.WhereEqualTo("student", studentTom); -``` - -```dart -LCQuery query = LCQuery('StudentCourseMap'); -LCObject studentTom = LCObject.createWithoutData('Student', '562da3fc00b0bf37b117c250'); -query.whereEqualTo('student', studentTom); -``` - - - -我们在上面的代码中省略了执行查询的语句,以节省篇幅。 - -## 一对一关系 - -当你需要将一个对象拆分成两个对象时,一对一关系是一种重要的需求,例如: - -#### 限制部分用户数据的权限 - -在这个场景中,你可以将此对象拆分成两部分,一部分包含所有用户可见的数据,另一部分包含所有仅自己可见的数据(通过 [ACL 控制](/sdk/storage/guide/security/#class-权限) )。同样你也可以实现一部分包含所有用户可修改的数据,另一部分包含所有仅自己可修改的数据。 - -#### 避免大对象 - -原始对象大小超过了对象的 128 KB 的上限值,此时你可以创建另一个对象来存储额外的数据。当然通常的作法是更好地设计和优化数据模型来避免出现大对象,但如果确实无法避免,则可以考虑使用 AVFile 存储大数据。 - -#### 更灵活的文件对象 - -AVFile 可以方便地存取文件,但对对象进行查询和修改等操作就不是很方便了。此时可以使用 AVObject 构造一个自己的文件对象并与 AVFile 建立一对一关联,将文件属性存于 AVObject 中,这样既可以方便查询修改文件属性,也可以方便存取文件。 - -## 关联数据的删除 - -当表中有一个 Pointer 指向的源数据被删除时,这个源数据对应的 Pointer **不会**被自动删除。所以建议用户在删除源数据时自行检查是否有 Pointer 指向这条数据,基于业务场景有必要做数据清理的话,可以调用对应的对象上的删除接口将 Pointer 关联的对象删除。 - -## 索引 - -在任何一个数据库系统中,索引都是优化性能的重要手段,同时它与 Schema 设计也是密不可分的。TDSLeanCloud 数据存储也支持索引,其索引与关系数据库中基本相同。在索引的选择上,应用查询操作的模式和频率起决定性作用。 - -同时也要注意,索引不是没有代价的。在加速查询的同时,它也会降低写入速度、消耗更多存储空间。是否建索引,如何建索引,建多少索引,我们需要综合权衡后来下决定。 - -### 索引类型 - -数据存储的索引可以包含任意的属性(包括数组),下面是一些索引选项: - -#### 复合索引 - -在多个属性域上构建一个单独的索引结构。例如,以一个存储客户数据的应用为例,我们可能需要根据姓、名和居住地来查询客户信息。通过在「姓、名、居住地」上建立复合索引,TDSLeanCloud 可以快速给出满足这三个条件的结果。此外,这一复合索引也能加速任何前置属性的查询。例如根据「姓」或者根据「姓+名」的查询都会使用到这个复合索引。 - -:::caution -注意,如果单按照「名」来查询,此复合索引会失效。 -::: - -#### 唯一索引 - -通过给索引加上唯一性约束,TDSLeanCloud 就会拒绝含有相同索引值的对象插入和更新。所有的索引默认都不是唯一索引,如果把复合索引指定为唯一索引,那么应用层必须保证索引列的组合值是唯一的。 -#### 数组索引 - -对数组属性也能创建索引。 - -#### 地理空间索引 - -TDSLeanCloud 会自动为 GeoPoint 类型的属性建立地理空间索引,但是要求一个 Object 内 GeoPoint 的属性不能超过一个。 - -#### 稀疏索引 - -这种索引只包含那些含有指定属性的文档,如果文档不含有目标属性,那么就不会进入索引。稀疏索引体积更小,查询性能更高。TDSLeanCloud 数据存储默认都会创建稀疏索引。 - -TDSLeanCloud 数据存储的索引可以在任何域上建立,包括内嵌对象和数组类型,这使它带来了比 RDBMS 更强大的功能。 - -### 通过索引优化性能 - -TDSLeanCloud 后台会根据每天的访问日志,自动归纳和学习频繁使用的访问模式,并自动创建合适的索引。不过如果你对索引优化比较有经验,也可以在控制台为每一个 Class 手动创建索引。 - -## 持续优化 Schema - -在 TDSLeanCloud 的存储系统里,Class 可以在没有完整的结构定义(包含哪些属性、数据类型如何等)时就提前创建好,一个 Class 下的对象(Object)也无需包含所有属性域,我们可以随时往对象中增减新的属性。 - -这种灵活、动态的 Schema 机制,使 Schema 的持续优化变得非常简单。相比之下,关系数据库的开发人员和 DBA 在开始一个新项目的时候,写下第一行代码之前,就需要制定好数据库 Schema,这至少需要几天,有的需要数周甚至更长。而 TDSLeanCloud 则允许开发者通过不断迭代和敏捷过程,持续优化 Schema。开发者可以开始写代码并将他们创建的对象持久化存储起来,以后当需要增加新的功能,TDSLeanCloud 可以继续存储新的对象而不需要对原来的 Class 做 ALTER TABLE 操作,这会给我们的开发带来很大的便利。 - -## 最佳的建模方式 - -选择最适合的建模方式,首先要确定关系是否: - -> 存在附加属性? - -例如学生选课,学生在选课的时候有的从电脑浏览器里选,有的从手机上选,手机系统还区分 iOS、Android 等等。那么假设要统计一下学生选课的来源,那么建立选课关系的时候就需要记录一下附加属性,这时只有中间表可以满足这个需求。 - -其次判断: - -> 是一对多还是多对多? - -- 一对多:Pointer -- 多对多:中间表 - -决定了用哪种方式之后,就按照前文介绍的一对多或多对多的实例代码构建自己的业务逻辑代码。但是 Pointer 可以实现的,中间表也可以实现,并且开发者可控的余地更多。具体可以参照如下伪代码: - -``` -if(存在附加属性){ - return 中间表; -} else { - switch(mode){ - case 一对多:{ - return Pointer; - } - case 多对多:{ - return 中间表; - } - } -} -``` - -### 案例分析 - -#### 婴儿与监护人之间的关系 - -做一个应用统计婴儿吃饭、睡觉、玩耍的时间分布,而婴儿的监护人可能会有多个,爷爷奶奶外公外婆还有可能是月嫂。首先按照我们之前设定的思路来逐步分析应该采用哪种方式建模: - -> 是否存在附加属性? - -一个婴儿和一个监护人之间的关系是否有附加属性?答案是有附加属性,父子关系跟母子关系是不一样的关系,因为对于婴儿监护人的身份称呼就是这个关系的附加属性。因此不用多想,果断使用中间表。不用再纠结是一对多还是多对多。 - -![relation-guide-1](/img/storage/relation-guide-1.png) - -#### 用户与设备之间的关系 - -婴儿的状态改变,可以通过推送服务做到实时推送给监护人,比如孩子的爸爸设备较多,他会在 iPad、iPhone 以及 Windows 上安装这个应用,那么他一般情况下会有 3 台设备。监护人在每一台设备上登录后,每当孩子的状态发生改变,服务端都会向这 3 台设备推送,那么我们接着按照之前的思路来分析应该采用哪种方式建模。 - -> 是否存在附加属性? - -一般来说这种情况是可以省略附加属性的,因为 TDSLeanCloud 的 `_Installation` 表里有一个字段是专门用来存储客户端设备的 deviceType,因此设备的信息是不需要存储在中间表的。 -而除非有其他特殊的属性条件,设备和用户之间的关系就是一个简单的一对多的关系,并且并不需要附加属性。 - -其次判断: - -> 是一对多还是多对多? - -一个用户在多个设备上登录一般就可以定义为一对多,而很少会出现类似 iPhone/iPad 版 QQ 那样内置了多账户管理系统,因此定义为一对多比较满足我们现在的需求。 - -### 何时使用数组 - -当要关联的数据是简单数据并且查询多于修改的时候,用数组比较合适。比如社交类应用里给朋友加标签,就可以使用 string 数组来存储这个属性。 - - - -```cs - LCObject beckham = new LCObject("Boy"); - beckham["tags"] = new string[] { "颜值爆表", "明星范儿" }); -``` - -```java - AVObject beckham = new AVObject("Boy"); - beckham.put("tags", Arrays.asList("颜值爆表", "明星范儿")); -``` - -```objc - LCObject *beckham = [LCObject objectWithClassName:@"Boy"]; - [beckham setObject: [NSArray arrayWithObjects:@"颜值爆表",@"明星范儿",nil] forKey:@"tags"]; -``` - -```swift - let beckham = LCObject(className: "Boy") - try beckham.set("tags", value: ["颜值爆表", "明星范儿"]) -``` - -```dart -LCObject beckham = LCObject('Boy'); -beckham['tags'] = ['颜值爆表', '明星范儿']; -``` - -```js - const beckham = new AV.Object('Boy'); - beckham.set('tags', ['颜值爆表','明星范儿']); -``` - -```python - beckham = leancloud.Object.create('Boy')() - beckham.set('tags', ['颜值爆表', '明星范儿']) -``` - -```php -$beckham = new LeanObject("Boy"); -$beckham->set("tags", array("颜值爆表", "明星范儿")); -``` - - - -为节省篇幅,上面的代码省略了保存对象的语句。 - -数组过长可能会影响性能,建议数组长度控制在 1000 以下。 -数组长度达到 10000 后,云端会拒绝向数组追加元素的请求。 diff --git a/leancloud/docs/sdk/storage/datalake.mdx b/leancloud/docs/sdk/storage/datalake.mdx deleted file mode 100644 index 55b80655e..000000000 --- a/leancloud/docs/sdk/storage/datalake.mdx +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: 数据仓库使用指南 -sidebar_label: 数据仓库 -sidebar_position: 14 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -基于现代的数据库 ClickHouse ,我们构建了新一代的数据仓库。与云服务紧密集成,支持将存储数据,日志表数据自动入库,从而为业务运营的统计分析提供强大的数据支持。 - -新的数据仓库将提供以下独特的功能: - -1. 基于列的存储,支持高比率的压缩,可以大幅度缩减存储成本; -1. 基于列的遍历和向量的运算支持高效的查询效率,可以将原本需要分钟级别的复杂查询,缩短到秒级别; -1. 直观的数据视图功能,支持存储中间结果,配合 join 查询,可支持高效率且复杂的二阶查询; -1. 更多样的函数,支持更复杂的 SQL 查询,可参考下述「[常见用例](#常见用例推荐)」小节; -1. 无缝对接日志表,可支持多样的数据源实时入库和查询,从而更灵活地集成外部数据源,为业务提供更完备的数据分析能力。 - -## 数据入库 - -在对数据进行查询前,我们首先需要将数据入库。目前,我们主要支持数据存储 Class 的入库。数据存储 Class 又具体分为两类,一类是日志表,另一类则是普通 Class 。 - -### 日志表 - -日志表是数据存储中的一类特殊表,是为了满足业务存储事件,日志等「不变型」数据的需求而设计,这类数据的特点是写入之后不会再修改。这些数据在被收集起来之后,可以应用于业务审计与运营分析,开发者对事件进行追踪等场景。由于该类数据只有追加,没有修改,我们能够提供更大的并发吞吐写入。 - -#### 开启和接入日志表 - -在控制台「数据存储」页面,点击创建 Class ,勾选「日志表」选项即可创建日志表。比如我们可以创建一个名为 `EventLog` 的日志表。 - -在 SDK 层面,日志表对象的创建与普通对象的创建一致: - -```java -// for Android -AVObject event = new AVObject("EventLog"); -event.put("eventType", "buttonClick"); -event.put("eventName", "orderSubmit"); -event.put("eventDate", new Date()); -event.saveInBackground(); -``` - -日志提交后会直接入库数据仓库,并且实时可查。 - -### 普通 Class 的同步 - -普通 Class 对象,由于支持更新,同步到数据仓库的流程要稍微复杂一些。 - -在控制台「数据存储」-「数据仓库」中,点击创建 Class 同步,选择需要在数据仓库中查询分析的字段,即可开启 Class 到数据仓库的同步。存量数据会即刻开始同步,取决于数据量的大小,同步可能持续较长时间,请耐心等待。 - -开启同步的 Class ,我们会在次日凌晨同步前一日更新过的数据。因此更新的对象需要在次日才可见。我们还会持续优化该流程,以期实现更实时的同步。 - -### 数据类型的转换 - -数据仓库中的字段类型与存储有着显著的差异,在数据入库的过程中我们会进行数据转换。其中需要特别注意的是, - -1. 数据仓库不支持 `null` 类型,对于字段缺失的情况以零值存储。字符类型的零值是空字符串,数字字段的零值则是数值 0。 -1. `Boolean` 类型会被转换为 `UInt8` 存储。0 值表示 false, 1 表示 true 。加之上述零值特性,默认缺失值为 0 表示 false 。 -1. 不支持嵌套的 `Object` 类型,实际会以 JSON 字符串的形式存储,使用时需要使用 JSON 函数来提取。 -1. 对于数组类型,要求元素是同一类型。在入库的过程中会转换成 `Array(String)`,使用时需要用类型转换函数转换回原类型。 - -具体的类型转换说明见下表: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    存储类型数据仓库类型转换说明使用示例
    ArrayArray(String)元素类型会以字符存储,在提取元素后需要使用类型转换回原始类型toInt64(xs[1])
    BooleanUInt80 表示 false, 1 表示 trueemailVerified = 1
    BytesN/A不支持
    DateDateTime
    File - *.className String
    - *.objectId String -
    会被展开为 className 和 objectId 两个字段
    GeoPointPoint试验性支持
    NumberFloat64
    ObjectString以 JSON 字符串的形式存储,需要用相关函数提取其中的字段visitParamExtractInt64(object, 'id')
    Pointer - *.className String
    - *.objectId String -
    会被展开为 className 和 objectId 两个字段
    - -数据仓库中所支持的类型列表,可参考 [ClickHouse 数据类型](https://clickhouse.com/docs/en/sql-reference/data-types/) 文档。如何在类型之间转换,可参考 [类型转换](https://clickhouse.com/docs/en/sql-reference/functions/type-conversion-functions/) 文档。 - -## 查询语法 - -数据仓库仅支持 `select` 查询。语法结构大致如下, - -```sql -SELECT column1, column2, expr ... -FROM table | (subquery) -[INNER|LEFT|RIGHT|FULL|CROSS] JOIN (subquery)|table (ON ) -[WHERE expr] -[GROUP BY expr_list] -[HAVING expr] -[ORDER BY expr_list] -[LIMIT [skip, ]n] -[UNION ...] -``` - -注意字符串,日期等值必需用**单引号**引用;而对于字段或表名中有特殊字符的情况,可使用反引号进行引用。比如 - -```sql -WHERE order_status = 'delivered' - AND `from` = 'example.com' -``` - -更多查询语法请参考 [ClickHouse 文档指南](https://clickhouse.com/docs/en/sql-reference/statements/select/)。 - -## 常见用例推荐 - -#### 使用 visitParamExtract* 提取 json 字段 - -在数据同步的过程中,多层嵌套的 object 对象会以 json 字符串的形式入库。为了提取 json 字符串中的字段,推荐使用 `visitParamExtract*` 来提取字段,比如 - -```sql -SELECT visitParamExtractString('{"abc":"\\u263a"}', 'abc') = '☺'; -``` - -在嵌套复杂,且出现有重名字段时,可使用 `JSONExtract*`,比如 - -```sql -SELECT JSONExtractFloat('{"a": "hello", "b": [-100, 200.0, 300]}', 'b', 2) = 200.0 -``` - -更多的 JSON 提取函数可参考[官方文档](https://clickhouse.com/docs/en/sql-reference/functions/json-functions/#visitparamextractuintparams-name)。 - -#### 使用 toTimeZone 进行时区转换 - -日期的展示默认以服务端时区展示,国内节点为东八区时间 (Asia/Shanghai)。如果不符合预期,可以使用 `toTimeZone` 将日期转换到特定的时区展示。 - -```sql -toTimeZone(createdAt, 'Asia/Shanghai') -``` - -#### 使用 toYYYYMMDD 进行按日期统计 - -在按照日期进行统计分析时,可以使用 `toYYYYMMDD` 将日期字符串化后进行 group by 统计。同时需要注意时区的处理,比如, - -```sql -GROUP BY toYYYYMMDD(toTimeZone(createdAt, 'Asia/Shanghai')) -``` - -#### 使用 argMax 提取「最新版本」数据 - -在遇到重复数据时,可以使用 argMax 来提取最后一条数据作为「最新版本」的数据。比如我们可以提取某一个订单的最新状态, - -```sql -SELECT - orderId, - argMax(status, updatedAt) AS status -FROM my_class -GROUP BY orderId -``` - - -## 其他限制 - -* 基于安全原因,`_User` 表的 `sessionToken`、`password`、`salt`、`authData` 字段,以及所有表的 `ACL` 字段都不支持同步。 -* 基于性能考量,`_Conversation` 表的 `m` 和 `mu` 这样的大数组字段暂不支持同步。 -* 为了更好的隔离应用之间的影响,我们会对来自同一个应用的慢查询进行配额限制。比如,我们仅允许最多不超过 3 个慢查询同时进行。如果一个查询耗时超过 5 秒,则为慢查询。 - -## REST API - -敬请期待。 - -## 计费 - -数据仓库功能仅对商用版应用对所有游戏开放,且目前处于 Beta 预览阶段,暂不收费。进入正式阶段,我们将对「存储空间」和「计算资源」两个维度进行计费。 diff --git a/leancloud/docs/sdk/storage/dotnet-guide/_category_.json b/leancloud/docs/sdk/storage/dotnet-guide/_category_.json deleted file mode 100644 index 61bdfc91e..000000000 --- a/leancloud/docs/sdk/storage/dotnet-guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": ".NET", - "collapsed": true, - "position": 2 -} diff --git a/leancloud/docs/sdk/storage/dotnet-guide/dotnet.mdx b/leancloud/docs/sdk/storage/dotnet-guide/dotnet.mdx deleted file mode 100644 index 74163c380..000000000 --- a/leancloud/docs/sdk/storage/dotnet-guide/dotnet.mdx +++ /dev/null @@ -1,1164 +0,0 @@ ---- -title: 数据存储开发指南 · .NET -sidebar_label: .NET 开发指南 -slug: /sdk/storage/guide/dotnet/ -sidebar_position: 2 ---- - -import Path from "/src/docComponents/path"; -import { Conditional } from "/src/docComponents/conditional"; - - -数据存储是云服务提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端: - -```cs -// 构建对象 -LCObject todo = new LCObject("Todo"); -// 为属性赋值 -todo["title"] = "工程师周会"; -todo["content"] = "周二两点,全体成员"; -// 将对象保存到云端 -await todo.Save(); -``` - -我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。 - -## SDK 安装与初始化 - -请阅读 [数据存储 .NET SDK 配置指南](/sdk/storage/guide/setup-dotnet/)。 - -## 对象 - -### `LCObject` - -`LCObject` 是云服务对复杂对象的封装,每个 `LCObject` 包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个 `LCObject` 上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。 - -比如说,一个保存着单个 Todo 的 `LCObject` 可能包含如下数据: - -```json -title: "给小林发邮件确认会议时间", -isComplete: false, -priority: 2, -tags: ["工作", "销售"] -``` - -### 数据类型 - -`LCObject` 支持的数据类型包括 `String`、`Number`、`Boolean`、`Object`、`Array`、`Date` 等等。你可以通过嵌套的方式在 `Object` 或 `Array` 里面存储更加结构化的数据。 - -`LCObject` 还支持两种特殊的数据类型 `Pointer` 和 `File`,可以分别用来存储指向其他 `LCObject` 的指针以及二进制数据。 - -`LCObject` 同时支持 `GeoPoint`,可以用来存储地理位置信息。参见 [GeoPoint](#geopoint)。 - -以下是一些示例: - -```cs -// 基本类型 -int numberValue = 2018; -bool boolValue = true; -string stringValue = "hello, world"; -DateTime now = DateTime.Now; -List intList = new List { 1, 2, 3 }; -Dictionary dict = new Dictionary { - { "year", 1780 }, - { "first", "partridge" }, - { "second", "turtledoves" }, - { "fifth", "golden rings" } -}; - -// 构建对象 -LCObject object = new LCObject("Hello"); -object["numberValue"] = numberValue; -object["boolValue"] = boolValue; -object["stringValue"] = stringValue; -object["time"] = now; -object["intList"] = intList; -object["dictValue"] = dict; -``` - -我们不推荐通过 `byte[]` 在 `LCObject` 里面存储图片、文档等大型二进制数据。每个 `LCObject` 的大小不应超过 **128 KB**。如需存储大型文件,可创建 `LCFile` 实例并将其关联到 `LCObject` 的某个属性上。参见 [文件](#文件)。 - -注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。 - -** > 结构化数据** 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。 - -若想了解云服务是如何保护应用数据的,请阅读[数据和安全](/sdk/storage/guide/security/)。 - -### 构建对象 - -下面的代码构建了一个 class 为 `Todo` 的 `LCObject`: - -```cs -LCObject object = new LCObject("Todo"); -``` - -在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。 - -### 保存对象 - -下面的代码将一个 class 为 `Todo` 的对象存入云端: - -```cs -// 构建对象 -LCObject todo = new LCObject("Todo"); -// 为属性赋值 -todo["title"] = "马拉松报名"; -todo["priority"] = 2; -// 将对象保存到云端 -await todo.Save(); -``` - -为了确认对象已经保存成功,我们可以到 ** > 结构化数据 > `Todo`** 里面看一下,应该会有一行新的数据产生。点一下这个数据的 `objectId`,应该能看到类似这样的内容: - -```json -{ - "title": "马拉松报名", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -注意,无需在 ** > 结构化数据** 里面创建新的 `Todo` class 即可运行前面的代码。如果 class 不存在,它将自动创建。 - -以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定: - -| 内置属性 | 类型 | 描述 | -| ----------- | ---------- | -------------------------------------------------------------- | -| `objectId` | `string` | 该对象唯一的 ID 标识。 | -| `ACL` | `LCACL` | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 | -| `createdAt` | `DateTime` | 该对象被创建的时间。 | -| `updatedAt` | `DateTime` | 该对象最后一次被修改的时间。 | - -这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的 `LCObject` 不存在这些属性。 - -属性名(**keys**)只能包含字母、数字和下划线。自定义属性不得以双下划线(`__`)开头或与任何系统保留字段和内置属性(`ACL`、`className`、`createdAt`、`objectId` 和 `updatedAt`)重名,无论大小写。 - -属性值(**values**)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 [数据类型](#数据类型)。 - -我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 `CustomData`。属性,采用小驼峰法,如 `imageUrl`。 - -### 获取对象 - -对于已经保存到云端的 `LCObject`,可以通过它的 `objectId` 将其取回: - -```cs -LCQuery query = new LCQuery("Todo"); -LCObject todo = await query.Get("582570f38ac247004f39c24b"); -// todo 就是 ObjectId 为 582570f38ac247004f39c24b 的 Todo 实例 -string title = todo["title"] as string; -int priority = (int)(todo["priority"]); - -// 获取内置属性 -string objectId = todo.ObjectId; -DateTime updatedAt = todo.UpdatedAt; -DateTime createdAt = todo.CreatedAt; -``` - -如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 `null`。 - -#### 同步对象 - -当云端数据发生更改时,你可以调用 `Fetch` 方法来刷新对象,使之与云端数据同步: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -await todo.Fetch(); -// todo 已刷新 -``` - -刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,**`Fetch` 操作会丢弃这些修改**。为避免这种情况,你可以在刷新时指定 **需要刷新的属性**,这样只有指定的属性会被刷新(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`),其他属性不受影响。 - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -await todo.Fetch(includes: new string[] { "priority", "location" }); -// 只有 priority 和 location 会被获取和刷新 -``` - -### 更新对象 - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `Save` 方法。例如: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -todo["content"] = "这周周会改到周三下午三点。"; -await todo.Save(); -``` - -云服务会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。 - -#### 有条件更新对象 - -通过传入 `query` 选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回 `305` 错误。 - -例如,用户的账户表 `Account` 有一个余额字段 `balance`,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求: - -```cs -try { - LCObject account = LCObject.CreateWithoutData("Account", "5745557f71cfe40068c6abe0"); - // 对 balance 原子减少 100 - int amount = -100; - account.Increment("balance", amount); - // 设置条件 - LCQuery query = new LCQuery("Account"); - query.WhereGreaterThanOrEqualTo("balance", -amount); - // 操作结束后,返回最新数据。 - // 如果是新对象,则所有属性都会被返回, - // 否则只有更新的属性会被返回。 - await account.Save(fetchWhenSave: true, query: query); - print($"当前余额为:{account["balance"]}"); -} catch (LCException e) { - if (e.code == 305) { - print("余额不足,操作失败!"); - } -} -``` - -**`query` 选项只对已存在的对象有效**,不适用于尚未存入云端的对象。 - -`query` 选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过 `LCQuery` 查询 `LCObject` 再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。 - -#### 更新计数器 - -设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 **原子操作** 来增加或减少一个属性内保存的数字: - -```cs -post.Increment("likes", 1); -``` - -可以指定需要增加或减少的值。若未指定,则默认使用 `1`。 - -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,需要原子增减的字段建议使用整数以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 - -#### 更新数组 - -更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据: - -- `Add(key, value)` 将指定对象附加到数组末尾。 -- `AddAll(key, values)` 将一组对象附加到数组末尾。 -- `AddUnique(key, value)` 将指定对象附加到数组末尾,确保对象唯一。 -- `AddAllUnique(key, values)` 将指定对象数组附加到数组末尾,确保对象唯一。 -- `Remove(key, value)` 从数组字段中删除指定对象的所有实例。 -- `RemoveAll(key, values)` 从数组对象中删除指定数组中的所有对象。 - -例如,`Todo` 用一个 `alarms` 属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性: - -```cs -DateTime alarm1 = DateTime.Parse("2018-04-30 07:10:00Z"); -DateTime alarm2 = DateTime.Parse("2018-04-30 07:20:00Z"); -DateTime alarm3 = DateTime.Parse("2018-04-30 07:30:00Z"); - -LCObject todo = new LCObject("Todo"); -todo.AddAllUnique("alarms", new object[] { alarm1, alarm2, alarm3 }); -await todo.Save(); -``` - -### 删除对象 - -下面的代码从云端删除一个 `Todo` 对象: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -await todo.Delete(); -``` - -如果只需删除对象的一个属性,可以用 `Unset`: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); - -// priority 属性会被删除 -todo.Unset("priority"); - -// 保存对象 -await todo.Save(); -``` - -注意,删除对象是一个较为敏感的操作,我们建议你阅读[ACL 权限管理开发指南](/sdk/storage/guide/acl/) 来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。 - -### 批量操作 - -```cs -// 批量构建和更新 -LCObject.SaveAll(); - -// 批量删除 -LCObject.DeleteAll(); - -// 批量同步 -LCObject.FetchAll(); -``` - -下面的代码将所有 `Todo` 的 `isComplete` 设为 `true`: - -```cs -LCQuery query = new LCQuery("Todo"); -ReadOnlyCollection results = await query.Find(); -// 获取需要更新的 todo -foreach (LCObject todo in results) { - // 更新属性值 - todo["isComplete"] = true; -} -await LCObject.SaveAll(results); -``` - -虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。 - -### 数据模型 - -对象之间可以产生关联。拿一个博客应用来说,一个 `Post` 对象可以与许多个 `Comment` 对象产生关联。云服务支持三种关系:一对一、一对多、多对多。 - -#### 一对一、一对多关系 - -一对一、一对多关系可以通过将 `LCObject` 保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个 `Comment` 指向一个 `Post`。 - -下面的代码会创建一个含有单个 `Comment` 的 `Post`: - -```cs -// 创建 post -LCObject post = new LCObject("Post"); -post["title"] = "饿了……"; -post["content"] = "中午去哪吃呢?"; - -// 创建 comment -LCObject comment = new LCObject("Comment"); -comment["content"] = "当然是肯德基啦!"; - -// 将 post 设为 comment 的一个属性值 -comment["parent"] = post; - -// 保存 comment 会同时保存 post -await comment.Save(); -``` - -云端存储时,会将被指向的对象用 `Pointer` 的形式存起来。你也可以用 `objectId` 来指向一个对象: - -```cs -LCObject post = LCObject.CreateWithoutData("Post", "57328ca079bc44005c2472d0"); -comment["post"] = post; -``` - -请参阅 [关系查询](#关系查询) 来了解如何获取关联的对象。 - -#### 多对多关系 - -想要建立多对多关系,最简单的办法就是使用 **数组**。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 **中间表** 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。 - -我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。 - -### 序列化和反序列化 - -在实际的开发中,把 `LCObject` 当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此 `LCObject` 也提供了序列化和反序列化的方法。 - -序列化: - -```cs -LCObject object = new LCObject("Todo"); -object["title"] = "马拉松报名"; -object["priority"] = 2; -object["owner"] = await LCUser.GetCurrent(); -string serializedString = object.ToString(); -``` - -反序列化: - -```cs -LCObject newObject = LCObject.ParseObject(json); -await newObject.Save(); -``` - -## 查询 - -我们已经了解到如何从云端获取单个 `LCObject`,但你可能还会有一次性获取多个符合特定条件的 `LCObject` 的需求,这时候就需要用到 `LCQuery` 了。 - -### 基础查询 - -执行一次基础查询通常包括这些步骤: - -1. 构建 `LCQuery`; -2. 向其添加查询条件; -3. 执行查询并获取包含满足条件的对象的数组。 - -下面的代码获取所有 `lastName` 为 `Smith` 的 `Student`: - -```cs -LCQuery query = new LCQuery("Student"); -query.WhereEqualTo("lastName", "Smith"); -// students 是包含满足条件的 Student 对象的数组 -ReadOnlyCollection students = await query.Find(); -``` - -### 查询条件 - -可以给 `LCObject` 添加不同的条件来改变获取到的结果。 - -下面的代码查询所有 `firstName` 不为 `Jack` 的对象: - -```cs -query.WhereNotEqualTo("firstName", "Jack"); -``` - -对于能够排序的属性(比如数字、字符串),可以进行比较查询: - -```cs -// 限制 age < 18 -query.WhereLessThan("age", 18); - -// 限制 age <= 18 -query.WhereLessThanOrEqualTo("age", 18); - -// 限制 age > 18 -query.WhereGreaterThan("age", 18); - -// 限制 age >= 18 -query.WhereGreaterThanOrEqualTo("age", 18); -``` - -可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是 `AND` 的关系: - -```cs -query.WhereEqualTo("firstName", "Jack"); -query.WhereGreaterThan("age", 18); -``` - -可以通过指定 `limit` 限制返回结果的数量(默认为 `100`): - -```cs -// 最多获取 10 条结果 -query.Limit(10); -``` - -由于性能原因,`limit` 最大只能设为 `1000`。即使将其设为大于 `1000` 的数,云端也只会返回 1,000 条结果。 - -如果只需要一条结果,可以直接用 `First`: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("priority", 2); -// todo 是第一个满足条件的 Todo 对象 -LCObject todo = await query.First(); -``` - -可以通过设置 `skip` 来跳过一定数量的结果: - -```cs -query.Skip(20); -``` - -把 `skip` 和 `limit` 结合起来,就能实现翻页功能: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("priority", 2); -query.Limit(10); -query.Skip(20); -``` - -需要注意的是,`skip` 的值越高,查询所需的时间就越长。作为替代方案,可以通过设置 `createdAt` 或 `updatedAt` 的范围来实现更高效的翻页,因为它们都自带索引。 -同理,也可以通过设置自增字段的范围来实现翻页。 - -对于能够排序的属性,可以指定结果的排序规则: - -```cs -// 按 createdAt 升序排列 -query.OrderByAscending("createdAt"); - -// 按 createdAt 降序排列 -query.OrderByDescending("createdAt"); -``` - -还可以为同一个查询添加多个排序规则: - -```cs -query.AddAscendingOrder("priority"); -query.AddDescendingOrder("createdAt"); -``` - -可以通过 `Select` 指定需要返回的属性。下面的代码只获取每个对象的 `title` 和 `content`(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`): - -```cs -LCQuery query = new LCQuery("Todo"); -query.Select("title"); -query.Select("content"); -LCObject todo = await query.First(); - -string title = todo["title"] as string; // √ -string content = todo["content"] as string; // √ -string notes = todo["notes"] as string; // null -``` - -`Select` 支持点号(`author.firstName`),详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)。 - -另外,字段名前添加减号前缀表示反向选择,例如 `-author` 表示不返回 `author` 字段。 -反向选择同样适用于内置字段,比如 `-objectId`,也可以和点号组合使用,比如 `-pubUser.createdAt`。 - -对于未获取的属性,可以通过对结果中的对象进行 `Fetch` 操作来获取。参见 [同步对象](#同步对象)。 - -### 字符串查询 - -可以用 `WhereStartsWith` 来查找某一属性值以特定字符串开头的对象。和 SQL 中的 `LIKE` 一样,你可以利用索引带来的优势: - -```cs -LCQuery query = new LCQuery("Todo"); -// 相当于 SQL 中的 title LIKE 'lunch%' -query.WhereStartsWith("title", "lunch"); -``` - -可以用 `WhereContains` 来查找某一属性值包含特定字符串的对象: - -```cs -LCQuery query = new LCQuery("Todo"); -// 相当于 SQL 中的 title LIKE '%lunch%' -query.WhereContains("title", "lunch"); -``` - -和 `WhereStartsWith` 不同,`WhereContains` 无法利用索引,因此不建议用于大型数据集。 - -注意 `WhereStartsWith` 和 `WhereContains` 都是 **区分大小写** 的,所以上述查询会忽略 `Lunch`、`LUNCH` 等字符串。 - -如果想查找某一属性值不包含特定字符串的对象,可以使用 `WhereMatches` 进行基于正则表达式的查询: - -```cs -LCQuery query = new LCQuery("Todo"); -// "title" 不包含 "ticket"(不区分大小写) -query.WhereMatches("title", "^((?!ticket).)*\$", modifiers: "i"); -``` - -不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class, -因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以使用全文搜索服务。 - -使用查询时如果遇到性能问题,可参阅 [查询性能优化](#查询性能优化)。 - -### 数组查询 - -下面的代码查找所有数组属性 `tags` 包含 `工作` 的对象: - -```cs -query.WhereEqualTo("tags", "工作"); -``` - -下面的代码查询数组属性长度为 3(正好包含 3 个标签)的对象: - -```cs -query.WhereSizeEqualTo("tags", 3); -``` - -下面的代码查找所有数组属性 `tags` **同时包含** `工作`、`销售` 和 `会议` 的对象: - -```cs -query.WhereContainsAll("tags", new string[] { "工作", "销售", "会议" }); -``` - -如需获取某一属性值包含一列值中任意一个值的对象,可以直接用 `WhereContainedIn` 而无需执行多次查询。下面的代码构建的查询会查找所有 `priority` 为 `1` **或** `2` 的 todo 对象: - -```cs -// 单个查询 -LCQuery priorityOneOrTwo = new LCQuery("Todo"); -priorityOneOrTwo.WhereContainedIn("priority", new string[] { 1, 2 }); -// 这样就可以了 :) - -// --------------- -// vs. -// --------------- - -// 多个查询 -LCQuery priorityOne = new LCQuery("Todo"); -priorityOne.WhereEqualTo("priority", 1); - -LCQuery priorityTwo = new LCQuery("Todo"); -priorityTwo.WhereEqualTo("priority", 2); - -LCQuery priorityOneOrTwo = LCQuery.Or(new LCQuery[] { priorityOne, priorityTwo }); -ReadOnlyCollection results = await priorityOneOrTwo.Find(); -// 好像有些繁琐 :( -``` - -反过来,还可以用 `WhereNotContainedIn` 来获取某一属性值不包含一列值中任何一个的对象。 - -### 关系查询 - -查询关联数据有很多种方式,常见的一种是查询某一属性值为特定 `LCObject` 的对象,这时可以像其他查询一样直接用 `WhereEqualTo`。比如说,如果每一条博客评论 `Comment` 都有一个 `post` 属性用来存放原文 `Post`,则可以用下面的方法获取所有与某一 `Post` 相关联的评论: - -```cs -LCObject post = LCObject.CreateWithoutData("Post", "57328ca079bc44005c2472d0"); -LCQuery query = new LCQuery("Comment"); -query.WhereEqualTo("post", post); -// comments 包含与 post 相关联的评论 -ReadOnlyCollection comments = await query.Find(); -``` - -如需获取某一属性值为另一查询结果中任一 `LCObject` 的对象,可以用 `WhereMatchesQuery`。下面的代码构建的查询可以找到所有包含图片的博客文章的评论: - -```cs -LCQuery innerQuery = new LCQuery("Post"); -innerQuery.WhereExists("image"); - -LCQuery query = new LCQuery("Comment"); -query.WhereMatchesQuery("post", innerQuery); -``` - -如需获取某一属性值不是另一查询结果中任一 `LCObject` 的对象,则使用 `WhereDoesNotMatchQuery`。 - -有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用 `Include`。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章: - -```cs -LCQuery query = new LCQuery("Comment"); - -// 获取最新发布的 -query.OrderByDescending("createdAt"); - -// 只获取 10 条 -query.Limit(10); - -// 同时包含博客文章 -query.Include("post"); - -// comments 包含最新发布的 10 条评论,包含各自对应的博客文章 -ReadOnlyCollection comments = await query.Find(); -foreach (LCObject comment in comments) { -// 该操作无需网络连接 - LCObject post = comment["post"] as LCObject; -} -``` - -可以用 dot 符号(`.`)来获取多级关系,例如 `post.author`,详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)的《在查询对象时使用点号》一节。 - -可以在同一查询上应用多次 `Include` 以包含多个属性。 - -通过 `Include` 进行多级查询的方式不适用于数组属性内部的 `LCObject`,只能包含到数组本身。 - -#### 关系查询的注意事项 - -云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌 / 子查询(和普通查询一样,`limit` 默认为 `100`,最大 `1000`),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出 `limit`,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有 `limit` 数量以内的结果会被填入主查询。 - -我们建议采用以下方案进行改进: - -- 确保子查询的结果在 100 条以下,如果在 100 至 1,000 条之间的话请将子查询的 `limit` 设为 `1000`。 -- 将需要查询的字段冗余到主查询所在的表上。 -- 进行多次查询,每次在子查询上设置不同的 `skip` 值来遍历所有记录(注意 `skip` 的值较大时可能会引发性能问题,因此不是很推荐)。 - -### 统计总数量 - -如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用 `Count` 来代替 `Find`。比如说,查询有多少个已完成的 todo: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("isComplete", true); -int count = await query.Count(); -print($"{count} 个 todo 已完成"); -``` - -### 组合查询 - -组合查询就是把诸多查询条件用一定逻辑合并到一起(`OR` 或 `AND`)再交给云端去查询。 - -组合查询不支持在子查询中包含 `GeoPoint` 或其他非过滤性的限制(例如 `near`、`withinGeoBox`、`limit`、`skip`、`ascending`、`descending`、`include`)。 - -#### OR 查询 - -OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于 `3` 或者已经完成了的 todo: - -```cs -LCQuery priorityQuery = new LCQuery("Todo"); -priorityQuery.WhereGreaterThanOrEqualTo("priority", 3); - -LCQuery isCompleteQuery = new LCQuery("Todo"); -isCompleteQuery.WhereEqualTo("isComplete", true); - -LCQuery priorityOneOrTwo = LCQuery.Or(new LCQuery[] { priorityQuery, isCompleteQuery }); -ReadOnlyCollection results = await priorityOneOrTwo.Find(); -``` - -使用 OR 查询时,子查询中不能包含 `GeoPoint` 相关的查询。 - -#### AND 查询 - -使用 AND 查询的效果等同于往 `LCQuery` 添加多个条件。下面的代码构建的查询会查找创建时间在 `2016-11-13` 和 `2016-12-02` 之间的 todo: - -```cs -LCQuery startDateQuery = new LCQuery("Todo"); -startDateQuery.WhereGreaterThanOrEqualTo("createdAt", DateTime.Parse("2016-11-13 00:00:00Z")); - -LCQuery endDateQuery = new LCQuery("Todo"); -endDateQuery.WhereLessThan("createdAt", DateTime.Parse("2016-12-03 00:00:00Z")); - -LCQuery query = LCQuery.And(new LCQuery[] { startDateQuery, endDateQuery }); -ReadOnlyCollection results = await query.Find(); -``` - -单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询: - -```cs -LCQuery createdAtQuery = new LCQuery("Todo"); -createdAtQuery.WhereGreaterThanOrEqualTo("createdAt", DateTime.Parse("2018-04-30 00:00:00Z")); -createdAtQuery.WhereLessThan("createdAt", DateTime.Parse("2018-05-01 00:00:00Z")); - -LCQuery locationQuery = new LCQuery("Todo"); -locationQuery.WhereDoesNotExist("location"); - -LCQuery priority2Query = new LCQuery("Todo"); -priority2Query.WhereEqualTo("priority", 2); - -LCQuery priority3Query = new LCQuery("Todo"); -priority3Query.WhereEqualTo("priority", 3); - -LCQuery priorityQuery = LCQuery.Or(new LCQuery[] { priority2Query, priority3Query }); -LCQuery timeLocationQuery = LCQuery.Or(new LCQuery[] { locationQuery, createdAtQuery }); -LCQuery query = LCQuery.And(new LCQuery[] { priorityQuery, timeLocationQuery }); -``` - -### 查询性能优化 - -影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。 - -- 不等于和不包含查询(无法使用索引) -- 通配符在前面的字符串查询(无法使用索引) -- 有条件的 `count`(需要扫描所有数据) -- `skip` 跳过较多的行数(相当于需要先查出被跳过的那些行) -- 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引) -- 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据) - - - -## LiveQuery - -LiveQuery 衍生于 [`LCQuery`](#查询),并为其带来了更强大的功能。它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。 - -设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用 `LCQuery` 并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。 - -想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的 `LCQuery`。订阅成功后,一旦有符合 `LCQuery` 的 `LCObject` 发生变化,云端就会主动、实时地将信息通知到客户端。 - -LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。 - -### 启用 LiveQuery - -进入 ** > 服务设置**,在 **安全设置** 里面勾选 **启用 LiveQuery**: - -```cs -using LeanCloud.LiveQuery; -``` - -### Demo - -下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果: - - - -使用我们的「LeanTodo」微信小程序和网页应用,可以实际体验以上视频所演示的效果,步骤如下: - -1. 微信扫码,添加小程序「LeanTodo」; - - ![LeanTodo mini program](/img/leantodo-weapp-qr.jpg) - -2. 进入小程序,点击首页左下角 **设置** > **账户设置**,输入便于记忆的用户名和密码; - -3. 使用浏览器访问 ,输入刚刚在小程序中更新好的账户信息,点击 **Login**; - -4. 随意添加更改数据,查看两端的同步状态。 - -注意按以上顺序操作。在网页应用中使用 **Signup** 注册的账户无法与小程序创建的账户相关联,所以如果颠倒以上操作顺序,则无法观测到数据同步效果。 - -[LiveQuery 公开课](http://www.bilibili.com/video/av11291992/) 涵盖了许多开发者关心的问题和解答。 - -### 构建订阅 - -首先创建一个普通的 `LCQuery` 对象,添加查询条件(如有),然后进行订阅操作: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("isComplete", true); -await query.Subscribe(); -// 订阅成功 -``` - -LiveQuery 不支持内嵌查询,也不支持返回指定属性。 - -订阅成功后,就可以接收到和 `LCObject` 相关的更新了。假如在另一个客户端上创建了一个 `Todo` 对象,对象的 `title` 设为 `更新作品集`,那么下面的代码可以获取到这个新的 `Todo`: - -```cs -LCQuery query = new LCQuery("Todo"); -LCLiveQuery liveQuery = await query.Subscribe(); -liveQuery.OnCreate = (obj) => { - print(obj["title"]); // 更新作品集 -}; -``` - -此时如果有人把 `Todo` 的 `content` 改为 `把我最近画的插画放上去`,那么下面的代码可以获取到本次更新: - -```cs -liveQuery.OnUpdate = (updatedTodo, updatedKeys) => { - print(updatedTodo["content"]); // 把我最近画的插画放上去 -}; -``` - -### 事件处理 - -订阅成功后,可以选择监听如下几种数据变化: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` 事件 - -当有新的满足 `LCQuery` 查询条件的 `LCObject` 被创建时,`create` 事件会被触发。下面的 `obj` 就是新建的 `LCObject`: - -```cs -liveQuery.OnCreate = (obj) => { - print("对象被创建。"); -}; -``` - -#### `update` 事件 - -当有满足 `LCQuery` 查询条件的 `LCObject` 被更新时,`update` 事件会被触发。下面的 `obj` 就是有更新的 `LCObject`: - -```cs -liveQuery.OnUpdate = (obj, updatedKeys) => { - print("对象被更新。"); -}; -``` - -#### `enter` 事件 - -当一个已存在的、原本不符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后符合查询条件,`enter` 事件会被触发。下面的 `obj` 就是进入 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```cs -liveQuery.OnEnter = (obj, updatedKeys) => { - print("对象进入。"); -}; -``` - -注意区分 `create` 和 `enter` 的不同行为。如果一个对象已经存在,在更新之前不符合查询条件,而在更新之后符合查询条件,那么 `enter` 事件会被触发。如果一个对象原本不存在,后来被构建了出来,那么 `create` 事件会被触发。 - -#### `leave` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后不符合查询条件,`leave` 事件会被触发。下面的 `obj` 就是离开 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```cs -liveQuery.OnLeave = (obj, updatedKeys) => { - print("对象离开。"); -}; -``` - -#### `delete` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 被删除,`delete` 事件会被触发。下面的 `objId` 就是被删除的 `LCObject` 的 `objectId`: - -```cs -liveQuery.OnDelete = (objId) => { - print("对象被删除。"); -}; -``` - -### 取消订阅 - -如果不再需要接收有关 `LCQuery` 的更新,可以取消订阅。 - -```cs -await liveQuery.Unsubscribe(); -// 成功取消订阅 -``` - -### 断开连接 - -断开连接有几种情况: - -1. 网络异常或者网络切换,非预期性断开。 -2. 退出应用、关机或者打开飞行模式等,用户在应用外的操作导致断开。 - -如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。 - -而另外一种极端情况——**当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下**,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。 - -### LiveQuery 的注意事项 - -因为 LiveQuery 的实时性,很多用户会陷入一个误区,试着用 LiveQuery 来实现一个简单的聊天功能。 -我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,后期维护的难度非常大(聊天记录、对话维护之类的代码会很混乱),并且云服务已经提供了即时通讯的服务。 -LiveQuery 的核心还是提供一个针对查询的推拉结合的用法,脱离设计初衷容易造成前端的模块混乱。 - - - -## 文件 - -有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用 `LCObject` 保存,此时文件对象 `LCFile` 便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。 - -### 构建文件 - -可以通过字符串构建文件: - -```cs -LCFile file = new LCFile("resume.txt", UTF8.GetBytes("LeanCloud")); -``` - -除此之外,还可以通过 URL 构建文件: - -```cs -LCFile file = new LCFile("logo.png", new Uri("https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png")); -``` - -通过 URL 构建文件时,SDK 并不会将原本的文件转储到云端,而是会将文件的物理地址存储为字符串,这样也就不会产生任何文件上传流量。使用其他方式构建的文件会被保存在云端。 - -云端会根据文件扩展名自动检测文件类型。如果需要的话,也可以手动指定 `Content-Type`(一般称为 MIME 类型): - -```cs -LCFile file = new LCFile("resume.txt", UTF8.GetBytes("LeanCloud")); -file.MimeType = "application/json"; -``` - -与前面提到的方式相比,一个更常见的文件构建方式是从本地路径上传。 - -```cs -LCFile file = new LCFile("avatar.jpg", "./avatar.jpg"); -``` - -这里上传的文件名字叫做 `avatar.jpg`。需要注意: - -- 每个文件会被分配到一个独一无二的 `objectId`,所以在一个应用内是允许多个文件重名的。 -- 文件必须有扩展名才能被云端正确地识别出类型。比如说要用 `LCFile` 保存一个 PNG 格式的图像,那么扩展名应为 `.png`。 -- 如果文件没有扩展名,且没有手动指定类型,那么云服务将默认使用 `application/octet-stream`。 - -### 保存文件 - -将文件保存到云端后,便可获得一个永久指向该文件的 URL: - -```cs -await file.Save(); -print(file.Url); -``` - -文件上传后,可以在 ** > 文件** 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的 `objectId` 和 URL。 - -已经保存到云端的文件可以关联到 `LCObject`: - -```cs -LCObject todo = new LCObject("Todo"); -todo["title"] = "买蛋糕"; -// attachments 是一个 LCFile[] 类型 -todo.Add("attachments", file); -await todo.Save(); -``` - -也可以通过构建 `LCQuery` 进行[查询](#查询): - -```cs -LCQuery query = LCFile.GetQuery(); -``` - -需要注意的是,内部文件(上传到文件服务的文件)的 `url` 字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 -因此,通过 `url` 字段查询文件仅适用于外部文件(直接保存外部 URL 到文件服务创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。 - -注意,如果文件被保存到了 `LCObject` 的一个数组属性中,那么在查询 `LCObject` 时如果需要包含文件,则要用到 `LCQuery` 的 `Include` 方法。比如说,在获取所有标题为 `买蛋糕` 的 todo 的同时获取附件中的文件: - -```cs -// 获取同一标题且包含附件的 todo -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("title", "买蛋糕"); -query.WhereExists("attachments"); - -// 同时获取附件中的文件 -query.Include("attachments"); -ReadOnlyCollection todos = await query.Find(); -foreach (LCObject todo in todos) { - // 获取每个 todo 的 attachments 数组 - List attachments = todo["attachments"] as List; -} -``` - -### 上传进度监听 - -上传过程中可以实时向用户展示进度: - -```cs -await file.Save((count, total) => { - print($"{count}/{total}"); - if (count == total) { - print("done"); - } -}); -``` - -### 文件元数据 - -上传文件时,可以用 `metaData` 添加额外的属性。文件一旦保存,`metaData` 便不可再修改。 - -```cs -file.AddMetaData("size", 1024); -file.AddMetaData("width", 128); -file.AddMetaData("height", 256); -file.MimeType = "image/jpg"; -await file.Save(); -``` - -保存文件时,SDK 会自动将文件大小写入元信息的 `size` 字段,开发者无需手动指定 `size` 字段。 -如果开发者通过 AddMetaData 手动指定了文件大小,那么 SDK 会将手动指定的 `size` 值保存到云端。 - -### 图像缩略图 - -成功保存图像后,除了可以获取指向该文件的 URL 外,还可以获取图像的缩略图 URL,并且可以指定缩略图的宽度和高度: - -```cs -// 获得宽度为 100 像素,高度为 200 像素的缩略图 url -string url = file.GetThumbnailUrl(100, 200); -``` - -图片最大不超过 **20 MB** 才可以获取缩略图。 - -国际版不支持图片缩略图。 - -### 下载文件 - -SDK 没有提供专门的下载文件的接口。 -如前所述,文件保存到云端后,开发者可以获取到文件的 URL,可以据此自行实现文件下载功能。 - -### 删除文件 - -下面的代码从云端删除一个文件: - -```cs -LCFile file = LCObject.CreateWithoutData("_File", "552e0a27e4b0643b709e891e"); -await file.Delete(); -``` - -默认情况下,文件的删除权限是关闭的。 -如需删除文件,一般建议在服务端使用 Master Key 调用 REST API 删除。 -取决于产品的具体需求,也可以考虑在 ** > 文件 > 权限** 向特定用户或特定角色开启删除权限。 - -### 文件审核 - -当前**文件审核**功能支持检测**图片**文件。 - -你可以在 **数据存储 > 文件 > 文件审核** 标签下勾选「自动审核新上传图片」,还可以批量审核指定时间范围内的图片,图片审核结果将在 **文件管理** 标签页展示。 - -如果你需要人工二次审核,可以点击每一行记录,在文件详情中选择「通过」或「封禁」。 - -## GeoPoint - -云服务允许你通过将 `LCGeoPoint` 关联到 `LCObject` 的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。 - -要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个 `LCGeoPoint` 并将其纬度(`latitude`)设为 `39.9`,经度(`longitude`)设为 `116.4`: - -```cs -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -``` - -现在可以将这个地理位置存储为一个对象的属性: - -```cs -todo["location"] = point; -``` - -### 地理位置查询 - -给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 `LCQuery` 添加 `WhereNear` 条件。下面的代码查找 `location` 属性值离某一点最近的 `Todo` 对象: - -```cs -LCQuery query = new LCQuery("Todo"); -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -query.WhereNear("location", point); - -// 限制为 10 条结果 -query.Limit(10); -// todos 是包含满足条件的 Todo 对象的数组 -ReadOnlyCollection todos = await query.Find(); -``` - -像 `OrderByAscending` 和 `OrderByDescending` 这样额外的排序条件会获得比默认的距离排序更高的优先级。 - -若要限制结果和给定地点之间的距离,可以参考 API 文档中的参数。 - -若要查询在某一矩形范围内的对象,可以用 `WhereWithinGeoBox`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```cs -LCQuery query = new LCQuery("Todo"); -LCGeoPoint southwest = new LCGeoPoint(30, 115); -LCGeoPoint northeast = new LCGeoPoint(40, 118); -query.WhereWithinGeoBox("location", southwest, northeast); -``` - -### GeoPoint 的注意事项 - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -## 用户 - -请参阅[内建账户指南](/sdk/authentication/guide/)。 - -## 角色 - -随着用户量的增长,你可能会发现相比于为每一名用户单独设置权限,将预先设定好的权限直接分配给一部分用户是更好的选择。为了迎合这种需求,云服务支持基于角色的权限管理。请参阅[ACL 权限管理开发指南](/sdk/storage/guide/acl/)。 - -## 子类化 - -子类化推荐给进阶的开发者在进行代码重构的时候做参考。你可以用 `LCObject` 重载符 `[]` 访问/赋值任意字段;你也可以使用子类化的属性来封装获取字段的方法,增强编码体验。 -子类化有很多优势,包括减少代码的编写量,具有更好的扩展性,和支持自动补全等等。 - -### 实现 - -要实现子类化,需要下面三个步骤: - -1. 继承 `LCObject` -2. 重载构造方法,传入 `类名` -3. 注册子类 - -下面是实现 `Student` 子类化的例子: - -```cs -// 定义 Student 类型 -class Student : LCObject { - internal string Name { - get => this["name"] as string; - set { - this["objectValue"] = value; - } - } - internal Student() : base("Student") { } -} -// 注册 Student 子类 -LCObject.RegisterSubclass("Student", () => new Student()); -``` - -### 使用 - -如下所示,两段代码对 name 字段的赋值方式等价。 - -```cs -LCObject student = new LCObject("Student"); -student["name"] = "小明"; -await student.Save(); -``` - -```cs -Student student = new Student(); -student.Name = "小明"; -await student.Save(); -``` - -## 全文搜索 - -全文搜索是一个针对应用数据进行全局搜索的接口,它基于搜索引擎构建,提供更强大的搜索功能。要深入了解其用法和阅读示例代码,请阅读[全文搜索指南](/sdk/storage/guide/fulltext-search/)。 diff --git a/leancloud/docs/sdk/storage/dotnet-guide/setup-dotnet.mdx b/leancloud/docs/sdk/storage/dotnet-guide/setup-dotnet.mdx deleted file mode 100644 index ea4865a63..000000000 --- a/leancloud/docs/sdk/storage/dotnet-guide/setup-dotnet.mdx +++ /dev/null @@ -1,214 +0,0 @@ ---- -title: 数据存储 .NET SDK 配置指南 -sidebar_label: .NET SDK 配置 -slug: /sdk/storage/guide/setup-dotnet/ -sidebar_position: 1 ---- - -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import DomainBinding from "../../_partials/setup-domain.mdx"; -import AppConfig from "../_partials/app-config.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -:::info -.NET SDK 基于 .NET Standard 2.0 接口标准实现,支持框架如下: - -- Unity 2018.1+ -- .NET Core 2.0+ -- .NET Framework 4.6.1+ -- Mono 5.4+ - -更多支持框架可参考 [.NET Standard](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) 文档。 - -::: - -## 获取 SDK - -通过 GitHub 仓库 [Releases](https://github.com/leancloud/csharp-sdk/releases) 下载最新版本 SDK。 - -各模块及依赖关系如下: - -| 名称 | 模块描述 | -| ------------------------ | ------------------------------------------------ | -| `LeanCloud-SDK-Storage` | 存储服务。 | -| `LeanCloud-SDK-Realtime` | 即时通信、LiveQuery 服务,依赖于存储服务。 | -| `LeanCloud-SDK-Engine` | 云引擎服务,依赖于存储,适用于云引擎服务端环境。 | - -如只需使用某种服务,可下载最小依赖包,减小程序体积。 - -### Unity 项目安装 - -- 直接导入:请下载 LeanCloud-SDK-XXX-Unity.zip,解压后为 Plugins 文件夹,拖拽至 Unity 即可。 - -- UPM:请在项目的 Packages/manifest.json 中添加依赖项 - - {`"dependencies": { -"com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -"com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-${sdkVersions.leancloud.csharp}" -}`} - - -注意:仅支持 Unity 2018+,即 Unity Api Compatibility Level 支持 .NET Standard 2.0 的版本。 - -### 非 Unity 项目安装 - -.NET Core 或其他支持 .NET Standard 2.0 的项目请下载 LeanCloud-SDK-XXX-Standard.zip,解压后设置依赖即可(XXX 指云服务,包括存储 Storage,云引擎 Engine)。 - -## 初始化 - - - -:::info -[TapSDK 初始化](/sdk/start/quickstart/#初始化)时,会自动执行这一节的初始化方法。 - -如果已经参考 **快速开始** 文档完成了 TapSDK 初始化,则**不需要**参考这里的初始化。 -::: - - - -导入模块: - -```cs -// 导入基础模块 -using LeanCloud; -// 导入存储模块 -using LeanCloud.Storage; -using LeanCloud.Realtime; -``` - -在使用服务前,先调用如下代码初始化 SDK: - - - -```cs -LCApplication.Initialize("your-client-id", "your-client-token", "https://your_server_url"); -``` - - - - - -```cs -LCApplication.Initialize("your-app-id", "your-app-key", "https://your_server_url"); -``` - - - -### 应用凭证 - - - -## 域名 - - - -## 开启调试日志 - -在应用开发阶段,你可以选择开启 SDK 的调试日志(debug log)来方便追踪问题。调试日志开启后,SDK 会把网络请求、错误消息等信息输出到 IDE 的日志窗口,或是浏览器 Console 或是云引擎日志(如果在云引擎下运行 SDK)。 - - - - -```cs -LCLogger.LogDelegate = (LCLogLevel level, string info) => { - switch (level) { - case LCLogLevel.Debug: - Debug.Log($"[DEBUG] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Warn: - Debug.Log($"[WARNING] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Error: - Debug.Log($"[ERROR] {DateTime.Now} {info}\n"); - break; - default: - Debug.Log(info); - break; - } -} -``` - - - - - -```cs -LCLogger.LogDelegate = (LCLogLevel level, string info) => { - switch (level) { - case LCLogLevel.Debug: - TestContext.Out.WriteLine($"[DEBUG] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Warn: - TestContext.Out.WriteLine($"[WARNING] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Error: - TestContext.Out.WriteLine($"[ERROR] {DateTime.Now} {info}\n"); - break; - default: - TestContext.Out.WriteLine(info); - break; - } -} -``` - - - - -:::caution -在应用发布之前,请关闭调试日志,以免暴露敏感数据。 -::: - -## 验证 - -首先,确认本地网络环境是可以访问云端服务器的,可以执行以下命令: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` 为绑定的 API 自定义域名。 - -如果当前网络正常会返回当前时间: - -```json -{ "__type": "Date", "iso": "2020-10-12T06:46:56.000Z" } -``` - -然后在项目中编写如下测试代码: - -```cs -LCObject testObject = new LCObject("TestObject"); -testObject["words"] = "Hello world!"; -await testObject.Save(); -``` - -保存后运行程序。 - -然后打开 ** > 结构化数据 > `TestObject`**,如果看到数据表中出现一行「words」列的值为「Hello world!」的数据,说明 SDK 已经正确地执行了上述代码,配置完毕。 - -如果控制台没有发现对应的数据,请参考 [问题排查](#问题排查)。 - -## 问题排查 - -SDK 安装指南基于当前最新版本的 SDK 编写,所以排查问题前,请先检查下安装的 SDK 是不是最新版本。 - -### `401 Unauthorized` - -如果 SDK 抛出 `401` 异常或者查看本地网络访问日志存在: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -则可认定为 App ID 或者 `App Key` 输入有误,或者是不匹配,很多开发者同时注册了多个应用,导致拷贝粘贴的时候,用 A 应用的 App ID 匹配 B 应用的 `App Key`,这样就会出现服务端鉴权失败的错误。 - -### 客户端无法访问网络 - -客户端尤其是手机端,应用在访问网络的时候需要申请一定的权限。 diff --git a/leancloud/docs/sdk/storage/faq.mdx b/leancloud/docs/sdk/storage/faq.mdx deleted file mode 100644 index a27132e6e..000000000 --- a/leancloud/docs/sdk/storage/faq.mdx +++ /dev/null @@ -1,326 +0,0 @@ ---- -title: 数据存储常见问题 -sidebar_label: 常见问题 -slug: /sdk/storage/faq/ -sidebar_position: 18 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -## API - -### API 调用次数有什么限制吗 - -使用 开发版 [标准版](https://developer.taptap.cn/product-intro/price),每天有 API 读写请求三万次的免费额度。 - -推送服务免费使用,并不占用免费额度,推送消息接口的调用受频率限制,具体参考:[推送消息接口的限制](/sdk/push/guide/rest/#推送消息接口) 文档。 - - - -默认情况下,商用版应用同一时刻最多可使用的工作线程数为 60,即同一时刻最多可以同时处理 60 个数据请求。**我们会根据应用运行状况以及运维需要调整该值**。如果需要提高这一上限,请 [提交工单](https://www.leanticket.cn) 进行申请。 - - - -### API 调用次数的计算 - -对于数据存储来说,每次 `create` 和 `update` 一个对象的数据算 1 次请求,如调用 1 次 `object.saveInBackground` 算 1 次 API 请求。在 API 调用失败的情况下,如果是由于**应用流控超限**(错误码 429)而被云端拒绝,则**不会**算成 1 次请求;如果是其他原因,例如**权限不够**(错误码 430),那么仍会算为 1 次请求。 - -#### 一次请求 - -- `create` -- `save` -- `fetch` -- `find` -- `delete` -- `deleteAll` - -调用一次 `fetch` 或 `find` 通过 `include` 返回了 100 个关联对象,算 1 次 API 请求。调用一次 `find` 或 `deleteAll` 来查找或删除 500 条记录,只算 1 次 API 请求。 - -#### 多次请求 - -- `saveAll` -- `fetchAll` - -调用一次 `saveAll` 或 `fetchAll` 来保存或获取 array 里面 100 个 对象,算 100 次 API 请求。 - - - -对于 [社交信息流](https://docs.leancloud.cn/sdk/other/openview/status_system/),`create` 和 `update` 按照 Status 和 Follower/Followee 的对象数量来计费。 - - - -对于 query 则是按照请求数来计费,与结果的大小无关。`query.count` 算 1 次 API 请求。collection fetch 也是按照请求次数来计费。 - -### 如何获取 API 的访问日志 - - - -进入 ** > API 访问日志**,开启日志服务,稍后刷新页面,就可以看到从开启到当前时间产生的日志了。 - - - - - -开发者中心后台暂不支持查看 API 访问日志。 - - - -### 其他语言调用 REST API 如何对参数进行编码 - -REST API 文档使用 curl 作为示范,其中 `--data-urlencode` 表示要对参数进行 URL encode 编码。 - -如果是 GET 请求,直接将经过 URL encode 的参数通过 `&` 连接起来,放到 URL 的问号后。如 `https://API_BASE_URL/1.1/login?username=xxxx&password=xxxxx`。 - -## 查询 - -### 如何实现大小写不敏感的查询 - -目前不提供直接支持,可采用正则表达式查询的办法,具体参考 [StackOverflow - MongoDB: Is it possible to make a case-insensitive query](http://stackoverflow.com/questions/1863399/mongodb-is-it-possible-to-make-a-case-insensitive-query)。 - -使用各平台 SDK 的 AVQuery 对象提供的 `matchesRegex` 方法(Android SDK 用 `whereMatches` 方法)。 - -### 查询最多能返回多少结果? - -- 查询最多返回 1000 条数据。 -- 返回的查询结果不止一个时,总大小不超过 6 MB(为了兼容旧客户端,超过 6 MB 时不会报错,会返回截断的结果);返回一个查询结果时,大小不超过 16 MB。 - -### 查询结果最多只能返回 1000 条数据,当我需要的数据量超过了 1000 该怎么办? - -可以通过每次变更查询条件,来继续从上一次的断点获取新的结果,譬如: -- 第一次查询,createdAt 时间在 2015-12-01 00:00:00 之后的 1000 条数据(最后一条的 createdAt 值是 x); -- 第二次查询,createdAt 在 x 之后的 1000 条数据(最后一条的 createdAt 是 y); -- 第三次查询,createdAt 在 y 之后的 1000 条数据(最后一条是 z); - -以此类推。 - -### 可以通过指定 objectId 范围来实现分页吗? - -当前数据存储服务自动生成 `objectId` 的算法是基于时间的,因此确实可以通过指定 `objectId` 范围来实现分页。 -但是,从代码可读性及严谨性出发(比如导入数据时可以指定不同格式的 `objectId`),推荐通过指定 `createdAt` 范围来实现分页。 - -### 当数据量越来越大时,怎么加快查询速度? - -与使用传统的数据库一样,查询优化主要靠**索引**实现。索引就像字典里的目录,能帮助你在海量的文字中更快速地查词。 - -原则:数据量少时,不建索引。多的时候请记住,建立索引后,写入时还需要更新索引,以此来换取更少的查询时间。所以,一般来说,写少读多就多建索引,写多读少就少建索引。 - -索引可以在 **** 中选择对应 Class 后进入「性能与索引」自助创建,详见[控制台使用指南](/sdk/start/dashboard/#给某个-class-数据建索引)。 - -### LeanCloud 查询支持 `Sum`、`Group By`、`Distinct` 这种函数吗? - -不支持。`Group By` 查询往往涉及大量对象的遍历,数据存储并不适用这样的场景。 - -为此我们推荐使用「[数据仓库](/sdk/storage/datalake/)」功能,它支持大量和高效的数据遍历,为数据分析这一场景量身定制。 - -### 默认值的查询结果为什么不对 - -这是默认值的限制。MongoDB 本身是不支持默认值,我们提供的默认值只是应用层面的增强,老数据如不存在相应的 key,设置默认值后并不会订正数据,只是在查询后做了展现层的优化。相应地,变更默认值后,这些 key 也会显示为新的默认值。有两种解决方案: - -1. 对老的数据做一次更新,查询出 key 不存在(whereDoesNotExist)的记录,再更新回去。 -2. 查询条件加上 or 查询,or key 不存在(whereDoesNotExist)。 - -### 如何查询非空值? - -空值的具体语义取决于字段的类型和项目的实际情况。以字符串字段为例,空值可能意味着: - -1. 该字段不存在 -2. 该字段为 `null` -3. 该字段为空字符串 `""` - -相应地,查询该字段非空的条件为 `where={"some_field": {"$exists": true, "$nin": ["", null]}}`。 - -在项目的数据模型设计阶段确定严谨的规范,有助于简化查询条件,避免因为不小心遗漏情况而出错。 -例如,如果在设计上确保字符串字段为空时干脆不设置字段(不存在),永不将其设为 `""` 和 `null`(强烈不推荐将字段设为 `null`,SDK 未提供将字段设为 `null` 的方法),那么只需查询 `{"$exists": true}` 即可。 - -### 地理位置查询错误 - -如果错误信息类似于 `can't find any special indices: 2d (needs index), 2dsphere (needs index), for 字段名`,就代表用于查询的字段没有建立 2D 索引,可以在 Class 管理的 **其他** 菜单里找到 **索引** 管理,点击进入,找到字段名称,选择并创建「2dsphere」索引类型。 - -![image](/img/storage/geopoint_faq.png) - -### 如何提升标志位的查询效率? - -当数据表中有很多布尔类型的数据时,可以考虑使用二进制存储提高查询效率。例如需要存储是否开启推送、是否静音、是否为会员等多个状态,可以这样表示: - -`111`:开启推送、静音、是会员 - -`101`:开启推送、未静音、是会员 - -在 LeanCloud 存储为整型字段,操作这个字段的方法可以参考位运算的接口文档:[REST API - 位运算](/sdk/storage/guide/rest/#位运算)。 - -### 应用内搜索的关键字查询, 查出来的结果是两月前的数据,最新数据查询不到怎么办? - -这种情况可以在 ** > 全文搜索** 尝试重建索引,索引创建完后会有邮件提醒。 - -一般来说当用户上传了新的词典,或者有批量删除过数据等情况都需要执行一次「重建索引」操作。当发现搜索与存储数据不一致时,也可以尝试重建索引来解决。 - -### 某张表单个对象过大,导致请求速度较慢怎么办? - -单个对象的数据过大,需要在业务层去优化代码,比如 data 是大数据字段: - -- 在查询的时候,通过 AVQuery.Select 去选取需要的字段(不要 select data 字段)。 -- 在 GET 对象的时候,才把 data 字段获取到客户端。 -- 或者考虑将 data 字段的数据做为「文件」保存,然后在表中通过 Pointer 关联起来。 -- 如果 data 是一个巨大的对象且只需要获取其中某个或某几个属性的话,可以考虑在 select 时用点号指定相应的属性,参考 [在查询对象时使用点号](/sdk/storage/guide/dot-notation/#在查询对象时使用点号)。 - -## 文件存储 - -### 文件存储有 CDN 加速吗? - -中国节点的文件存储服务自带 CDN 加速访问,但不包括海外 CDN 加速。 - -商用版应用可发工单申请开启海外 CDN 加速(开启后海外访问文件 http/https 流量费用分别上调为 0.40 元/GB、0.60 元/GB)。 - -国际版没有现成的 CDN 加速访问服务,需要用户自行配置。 - -以 CloudFront 加速服务为例,配置过程如下: - -- 阅读官方指南 [Getting Started with CloudFront](http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.html)。 -- 创建一个 AWS 账户,开始使用 CloudFront 服务和付费。 -- S3 的公共访问权限(read permission)已经被配置好,可以跳过指南中[有关 S3 配置的部分](http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.html#GettingStartedUploadContent)。 -- CloudFront 配置中的 **Origin Domain Name** 请从 AVFile 的 URL 中获取,其他均可保持默认。 - -### 文件存储有大小限制吗? - -没有。 - -除了在浏览器里通过 JavaScript SDK 上传文件,或者通过我们网站直接上传文件,有 10 MB 的大小限制之外,其他 SDK 都没有限制。 JavaScript SDK 在 Node.js 环境中也没有大小限制。 - -### 存储图片可以做缩略图等处理吗? - -可以。默认我们的 `AVFile` 类提供了缩略图获取方法,可以参见各个 SDK 的开发指南。如果要自己处理,可以通过获取 `AVFile` 的 `URL` 属性。 - -使用 [七牛图片处理 API](https://developer.qiniu.com/dora/manual/1279/basic-processing-images-imageview2) 执行处理,例如添加水印、裁剪等。 - -### 文件存储支持访问控制吗? - -由于知道文件 URL 的任何用户都可以访问文件,因此需要为 `_File` Class 和引用文件的 Class 设置合适的 Class 权限和 ACL,限制未授权用户获取到文件 URL。 - -如果希望能够更精细地控制文件访问权限,比如在一定时间后重新检查是否有权访问文件,建议使用支持访问权限控制的第三方文件存储服务商,然后直接在 LeanCloud 文件服务中保存一个外部的 URL。 -注意 `_File` 表是不可变的,这意味着已经写入的 URL 不能修改。 -但通常支持访问权限控制的文件存储服务商,实现访问权限的方式都是在现有的 URL,比如 `https://file.example.com/some-url` 后额外传入一个 token 参数(这个 token 通常有时效性),比如 `https://file.example.com/some-url?token=xxxxxx`。 -所以客户端需要访问文件时,可以先从 `LCFile` 获取 `url` 属性后,再加上这个 `token` 参数,拼成最终的 URL 下载文件。 -`token` 的获取需要自行在云引擎或自己的服务器上部署一个简单的生成访问 `token` 的 API 服务,客户端访问该 API 服务获取 `token`。 - -### 如何修改文件存储中已有的文件? - -文件存储对应的 `_File` Class 是不可变数据,文件内容和相关元数据一经写入就无法修改,只能删除后重新上传或重新通过 URL 构建。 -在使用外部文件(通过 URL 构建文件)的情况下,如果有频繁修改文件或相关元数据的需求,可以考虑如下方案: - -- 自己实现一个存储外部文件相关信息的 Class,比如 `ExternalFile`。在引用该文件的对象中设置 Pointer 字段,指向 `ExternalFile`。注意,这种情况下,查询包含文件的对象时,如果希望同时获取文件信息,需要 include 相应字段(使用内置 `_File` Class 的情况下会自动 include,不用另外指定)。 -- 在需要引用文件的对象中,直接使用字符串类型保存 URL。这种情况下文件相关功能都需要自行实现。如果想要保存这个文件的相关元信息,还需要另外设计。比如在对象的其他字段上保存,或者将相应字段的类型从字符串改成 object,同时保存 URL 和元信息。注意,如此设计会导致应用的文件信息分散在需要用到文件的各个 Class 中,要对整个应用的文件进行批量查询和批量处理会比较麻烦。 - -## SDK - -### iOS 项目打包后的大小 - -创建一个全新的空白项目,使用 CocoaPod 安装了 AVOSCloud 和 AVOSCloudIM 模块,此时项目大小超过了 80 MB。打包之后体积会不会缩小?大概会有多大呢? - -数据存储 iOS SDK 二进制中包含了 i386、armv7、arm64 等 5 个 CPU slices。发布过程中,non-ARM 的符号和没有参与连接的符号会被 strip 掉。因此,最终应用体积不会增加超过 10 MB,请放心使用。 - -### Java SDK 对 AVObject 对象使用 getDate("createdAt") 方法读取创建时间为什么会返回 null - -请用 `AVObject` 的 `getCreatedAt` 方法;获取 `updatedAt` 用 `getUpdatedAt`。 -这两个方法会返回 Date 类型。 - -如果希望返回字符串类型,可以使用 `getUpdatedAtString()` 和 `getCreatedAtString()`。 - -### Android 设备每次启动时,installationId 为什么总会改变?如何才能不改变? - -可能有以下两种原因导致这种情况: - -- SDK 版本过旧,installationId 的生成逻辑在版本更迭中有修改。请更新至最新版本。 -- 代码混淆引起的,注意在 proguard 文件中添加 [SDK 的混淆排除](/sdk/storage/guide/setup-java/#android-代码混淆)。 - -### Android 代码混淆怎么做 - -参考 [Android 代码混淆](/sdk/storage/guide/setup-java/#android-代码混淆) 文档。 - -### Java SDK 出现 already has one request sending 错误是什么原因 - -日志中出现了 `com.avos.avoscloud.AVException: already has one request sending` 的错误信息,这说明存在对同一个 `AVObject` 实例对象同时进行了 2 次异步的 `save` 操作。为防止数据错乱,数据存储 SDK 对于这种同一数据的并发写入做了限制,所以抛出了这个异常。 - -需要检查代码,通过打印 log 和断点的方式来定位究竟是由哪一行 `save` 所引发的。 - -### 使用 Android SDK 的数据存储功能,APP 上架时被厂商检测存在自启动问题。 - -解决办法:将 Android SDK 升级到 v8.2.19 及以上版本。 - -自启动问题是因为 LiveQuery 需要在后台有一个长链接,这样才能及时收到云端的事件通知,而这个长链接是放在一个后台 Service 里面的,并且有自动重连的能力——这会监听网络状态变化,在网络有效的时候启动这个 service。这个逻辑可能会被一些平台判定为「自启动」。 - -老版本 SDK 因为没有适配 Android 新版本,所以有时候会抛出异常。8.2.19 版本 SDK 对 Android 新系统进行了适配,但是也并没有完全禁止这个自动重连的逻辑,所以可能还是会被误判。如果已经使用新版 SDK 但仍遇到这个问题,可以向厂商申诉。 - -### JavaScript SDK 有没有同步 API - -JavaScript SDK 由于平台的特殊性(运行在单线程运行的浏览器或者 Node.js 环境中),不提供同步 API,所有需要网络交互的 API 都需要以 callback 的形式调用。我们提供了 [Promise 模式](/sdk/storage/guide/js/#promise) 来减少 callback 嵌套过多的问题。 - -### JavaScript SDK 在 AV.init 中用了 Master Key,但发出去的 AJAX 请求返回 206 - -目前 JavaScript SDK 在浏览器(而不是 Node)中工作时,是不会发送 Master Key 的,因为我们不鼓励在浏览器中使用 Master Key,Master Key 代表着对数据的最高权限,只应当在后端程序中使用。 - -如果你的应用的确是内部应用(做好了相关的安全措施,外部访问不到),可以在 `AV.init`之后增加下面的代码来让 JavaScript SDK 发送 Master Key: - -```js -AV.Cloud.useMasterKey(true); -``` - -### JavaScript SDK 会暴露 App Key 和 App Id,怎么保证安全性? - -首先请阅读 [**安全总览**](/sdk/storage/guide/security/) 来了解数据存储服务完整的安全体系。其中提到,可以使用「安全域名」,在没有域名的情况下,可以使用 [ACL](/sdk/storage/guide/acl/)。 - -理论上所有客户端都是不可信任的,所以需要在服务端对安全性进行设计。如果需要高级安全,可以使用 [ACL](/sdk/storage/guide/acl/) 方式来管理,如果需要更高级的自定义方式,可以使用 [云引擎](/sdk/engine/overview/)。 - -## 其他 - -### 如何防止 DDoS 攻击,或者高频次的 CC 攻击? - - - -无论是 DDoS 攻击,还是高频次的 CC 攻击,均需要在业务所请求的独立 IP 前架设高防资源。 -具体步骤如下: - -1. 在控制台绑定已备案的域名。使用数据存储与即时通讯服务需绑定 API 域名,使用云引擎服务需绑定云引擎域名。SDK 初始化的时候,指定 Server URL 为控制台绑定的 API 域名地址。绑定域名在控制台 > 应用 > 设置 > 域名绑定处操作。 -2. 绑定域名时将 DNS 解析设置为独立的 IP(设置 IP 地址为 A 记录)。独立 IP 在控制台个人中心 > 账号设置 > 独立 IP 处可以查看,API 与云引擎分别对应不同的 IP。商用版用户会免费赠送一个 API 独立 IP,购买的第二个 API 独立 IP 开始收费,云引擎没有免费 IP。每个 IP 每月收费 50 元。 -3. 在第三方购买高防资源,并使用上面新购的 IP 来回源。 - -由于很多网络攻击的目的地都是特定 IP,如果独立 IP 已随业务公开,可以另外购买一个独立 IP 来做为高防资源的回源 IP,以避免高防资源因源 IP 不可用而失效。 - - - - - -详见 [TDS 云服务带高防吗](/sdk/start/faq/#tds-云服务带高防吗)。 - - - -### 保存数据时报错 The key is too long,是什么问题? - -可以检查要保存或更新的字段是否设置了索引,有索引的字段长度限制不能超过 1 KB。 - -### 如何解决数据一致性或事务需求? - -数据存储服务目前并不提供完整的事务功能,但提供了一些保证数据一致性的特性,可以解决大部分的一致性需求: - - - - -- 在单个对象的一次 save 操作中,对多个字段的更新操作是原子地完成的。 -- 使用 [increment](/sdk/storage/guide/js/#更新计数器)(原子计数器)可以原子地更新数字字段。 -- [唯一索引](https://docs.leancloud.cn/sdk/start/dashboard/#给某个-class-数据建索引) 可以保证在一个字段上有同样值的对象只有一个。 -- [有条件更新对象](/sdk/storage/guide/js/#有条件更新对象) 可以仅在满足某个查询条件时进行更新操作;在这个特性的基础上,你可以自己实现更加复杂的 [两阶段提交](http://www.howardliu.cn/translation-perform-two-phase-commits-in-mongodb/)。 -- 在云引擎上还可以借助 [LeanCache](/sdk/engine/database/redis/) 来实现自定义的 [排他锁](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/redlock.js)。 - - - - - -- 在单个对象的一次 save 操作中,对多个字段的更新操作是原子地完成的。 -- 使用 [increment](/sdk/storage/guide/js/#更新计数器)(原子计数器)可以原子地更新数字字段。 -- [唯一索引](/sdk/start/dashboard/#给某个-class-数据建索引) 可以保证在一个字段上有同样值的对象只有一个。 -- [有条件更新对象](/sdk/storage/guide/js/#有条件更新对象) 可以仅在满足某个查询条件时进行更新操作;在这个特性的基础上,你可以自己实现更加复杂的 [两阶段提交](http://www.howardliu.cn/translation-perform-two-phase-commits-in-mongodb/)。 -- 在云引擎上还可以借助 [LeanCache](/sdk/engine/database/redis/) 来实现自定义的 [排他锁](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/redlock.js)。 - - - -关于这个话题我们还录制了一期[公开课视频](https://www.bilibili.com/video/av12823801/),其中有对上面这些特性的详细介绍,和解决常见场景的实例教程(包括实现两阶段提交)。 diff --git a/leancloud/docs/sdk/storage/fulltext-search/_category_.json b/leancloud/docs/sdk/storage/fulltext-search/_category_.json deleted file mode 100644 index 3f12d528f..000000000 --- a/leancloud/docs/sdk/storage/fulltext-search/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "全文搜索", - "collapsed": true, - "position": 11 -} diff --git a/leancloud/docs/sdk/storage/fulltext-search/fulltext-search-guide.mdx b/leancloud/docs/sdk/storage/fulltext-search/fulltext-search-guide.mdx deleted file mode 100644 index 95480fa6e..000000000 --- a/leancloud/docs/sdk/storage/fulltext-search/fulltext-search-guide.mdx +++ /dev/null @@ -1,240 +0,0 @@ ---- -title: 全文搜索指南 -slug: /sdk/storage/guide/fulltext-search/ -sidebar_position: 1 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -在应用内使用全文搜索是一个很常见的需求。例如一个阅读类的应用,里面有很多有价值的文章,开发者会提供一个搜索框,让用户键入关键字后就能查找到应用内相关的文章,并按照相关度排序,就好像我们打开浏览器用 Google 搜索关键字一样。 -虽然使用正则查询也可以实现全文搜索功能,但数据量较大的时候正则查询会有性能问题,因此我们提供了专门的全文搜索功能。 - -## 为 Class 启用搜索 - -你需要选择至少一个 Class 为它开启全文搜索。开启后,该 Class 的数据会自动建立索引,并且可以调用我们的搜索组件或者 [API](#搜索-api) 搜索到内容。 - -**请注意,启用了搜索的 Class 数据,其搜索结果仍然遵循 ACL。如果你为 Class 里的 Object 设定了合理的 ACL,那么搜索结果也将遵循这些 ACL 值,保护你的数据安全。** - -你可以在 ** > 全文搜索** 为 Class 启用搜索,点击「添加搜索 Class」: - -- **Class**:选择需要启用搜索的 Class。开发版应用最多允许 5 个 Class 启用全文搜索,商用版应用最多允许 10 个 Class 启用全文搜索。 -- **开放的列**:你可以选择将哪些字段加入搜索索引。默认情况下,`objectId`、`createdAt`、`updatedAt` 三个字段将无条件加入开放字段列表。除了这三个字段外,开发版应用每个 Class 最多允许索引 5 个字段,商用版应用每个 Class 最多允许索引 10 个字段。请仔细挑选要索引的字段。 - -**如果一个 Class 启用了全文搜索,但是超过两周没有任何搜索调用,我们将自动禁用该 Class 的搜索功能。** - -## 搜索 API - -我们提供了 [全文搜索的 REST API 接口](/sdk/storage/guide/fulltext-search-rest/)。 -SDK 封装了这一接口。 - -假设你对 `GameScore` 类 [启用了全文搜索](#为-class-启用搜索),你就可以尝试传入关键字来搜索: - - - -```cs -LCSearchQuery query = new LCSearchQuery("GameScore"); -query.QueryString("dennis") - .OrderByDescending("score") - .Limit(10); -LCSearchResponse response = await query.Find(); -// 符合查询条件的文档总数 -Debug.Log(response.Hits); -// 符合查询条件的结果文档 -foreach (GameScore score in response.Results) { - -} -// 标记本次查询结果,下次查询继续传入这个 sid 用于查找后续的数据,用来支持翻页查询 -Debug.Log(response.Sid); -``` - -```java -LCSearchQuery searchQuery = new LCSearchQuery("dennis"); -searchQuery.setClassName("GameScore"); -searchQuery.setLimit(10); -searchQuery.orderByAscending("score"); // 根据 score 字段升序排序。 -searchQuery.findInBackground().subscribe(new Observer>() { - @Override - public void onSubscribe(Disposable disposable) {} - - @Override - public void onNext(List results) { - for (LCObject o:results) { - System.out.println(o); - } - testSucceed = true; - latch.countDown(); - } - - @Override - public void onError(Throwable throwable) { - throwable.printStackTrace(); - testSucceed = true; - latch.countDown(); - } - - @Override - public void onComplete() {} -}); -``` - -```objc -LCSearchQuery *searchQuery = [LCSearchQuery searchWithQueryString:@"test-query"]; -searchQuery.className = @"GameScore"; -searchQuery.highlights = @"field1,field2"; -searchQuery.limit = 10; -searchQuery.cachePolicy = kLCCachePolicyCacheElseNetwork; -searchQuery.maxCacheAge = 60; -searchQuery.fields = @[@"field1", @"field2"]; -[searchQuery findInBackground:^(NSArray *objects, NSError *error) { - for (LCObject *object in objects) { - NSString *appUrl = [object objectForKey:@"_app_url"]; - NSString *deeplink = [object objectForKey:@"_deeplink"]; - NSString *highlight = [object objectForKey:@"_highlight"]; - // other fields - // code is here - } -}]; -``` - -```dart -LCSearchQuery query = new LCSearchQuery('GameScore'); -query.queryString('dennis'); -query.orderByDescending('score'); -query.limit(10); -LCSearchResponse response = await query.find(); -// 符合查询条件的文档总数 -print(response.hits); -// 符合查询条件的结果文档 -for (GameScore score in response.results) { - -} -// 标记本次查询结果,下次查询继续传入这个 sid 用于查找后续的数据,用来支持翻页查询 -print(response.sid); -``` - -```js -const query = new AV.SearchQuery("GameScore"); -query.queryString("dennis"); -// 高亮玩家字段中匹配到的 dennis 字符串,如要匹配多个字段,可传入一个数组 -query.highlights("player"); -query.highlights("player"); -query - .find() - .then(function (results) { - console.log("Find " + query.hits() + " docs."); - // 打印输出:Find 4 docs. - // 打印带高亮的第一个匹配结果,剩余匹配结果的处理同理 - console.log(results[0].get("_highlight").player); - // 打印输出:[ 'dennis ZX' ] - }) - .catch(function (err) { - // 处理 err - }); -``` - - - -有关查询语法,可以参考 [q 查询语法](/sdk/storage/guide/fulltext-search-rest#q-查询语法)。 - -因为每次请求都有 limit 限制,所以一次请求可能并不能获取到所有满足条件的记录。 -`SearchQuery` 的 `hits()` 标示所有满足查询条件的记录数。 -你可以多次调用同一个 `SearchQuery` 的 `find()` 获取余下的记录。 - -如果在不同请求之间无法保存查询的 query 对象,可以利用 sid 做到翻页,一次查询是通过 `SearchQuery` 的 `_sid` 属性来标示的。 -你可以通过 `SearchQuery` 的 `sid()` 来重建查询 query 对象,继续翻页查询。 -sid 在 5 分钟内有效。 - -复杂排序可以使用 `SearchSortBuilder`,例如,假设 `scores` 是由分数组成的数组,现在需要根据分数的平均分倒序排序,并且没有分数的排在最后: - - - -```cs -LCSearchSortBuilder sortBuilder = new LCSearchSortBuilder(); -sortBuilder.OrderByAscending("balance", "avg", "last"); -searchQuery.SortBy(sortBuilder); -``` - -```java -LCSearchSortBuilder builder = LCSearchSortBuilder.newBuilder(); -builder.orderByDescending("scores","avg","last"); -searchQuery.setSortBuilder(builder); -``` - -```objc -LCSearchSortBuilder *builder = [LCSearchSortBuilder newBuilder]; -[builder orderByDescending:@"scores" withMode:@"max" andMissing:@"last"]; -searchQuery.sortBuilder = builder; -``` - -```dart -LCSearchSortBuilder sortBuilder = new LCSearchSortBuilder(); -sortBuilder.orderByAscending('scores', mode: 'avg', missing: 'last'); -searchQuery.sortBy(sortBuilder); -``` - -```js -searchQuery.sortBy( - new AV.SearchSortBuilder().descending("scores", "avg", "last") -); -``` - - - -更多 API 请参考 SDK API 文档: - - - -<> - -- [LCSearchQuery](https://leancloud.github.io/csharp-sdk/html/classLeanCloud_1_1Storage_1_1LCSearchQuery.html) -- [LCSearchSortBuilder](https://leancloud.github.io/csharp-sdk/html/classLeanCloud_1_1Storage_1_1LCSearchSortBuilder.html) -- [LCSearchResponse](https://leancloud.github.io/csharp-sdk/html/classLeanCloud_1_1Storage_1_1LCSearchResponse.html) - - -<> - -- [LCSearchQuery](https://leancloud.github.io/java-unified-sdk) -- [LCSearchSortBuilder](https://leancloud.github.io/java-unified-sdk) - - -<> - -- [LCSearchQuery](https://leancloud.github.io/objc-sdk/Classes/LCSearchQuery.html) -- [LCSearchSortBuilder](https://leancloud.github.io/objc-sdk/Classes/LCSearchSortBuilder.html) - - -<> - -- [LCSearchQuery](https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/LCSearchQuery-class.html) -- [LCSearchSortBuilder](https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/LCSearchSortBuilder-class.html) -- [LCSearchResponse](https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/LCSearchResponse-class.html) - - -<> - -- [AV.SearchQuery](https://leancloud.github.io/javascript-sdk/docs/AV.SearchQuery.html) -- [AV.SearchSortBuilder](https://leancloud.github.io/javascript-sdk/docs/AV.SearchSortBuilder.html) - - - - - -## 自定义分词 - -默认情况下, String 类型的字段都将被自动执行分词处理,我们使用的分词组件是 [mmseg](https://github.com/medcl/elasticsearch-analysis-mmseg),词库来自搜狗。但是很多用户由于行业或者专业的特殊性,一般都有自定义词库的需求,因此我们提供了自定义词库的功能。应用创建者可以通过 ** > 全文搜索 > 自定义词库** 上传词库文件。 - -词库文件要求为 UTF-8 编码,每个词单独一行,文件大小不能超过 512 K,例如: - -``` -面向对象编程 -函数式编程 -高阶函数 -响应式设计 -``` - -将其保存为文本文件,如 `words.txt`,上传即可。上传后,分词将于 3 分钟后生效。开发者可以通过 [`analyze` API](/sdk/storage/guide/fulltext-search-rest#分词结果查询)(要求使用 master key)来测试。 - -自定义词库生效后,**仅对新添加或者更新的文档/记录才有效**,如果需要对原有的文档也生效的话,需要在 **数据存储** > **全文搜索** 点击「重建索引」按钮,重建原有索引。 -同样,如果更新了自定义词库(包括删除自定义词库),也需要重建索引。 diff --git a/leancloud/docs/sdk/storage/fulltext-search/fulltext-search-rest.mdx b/leancloud/docs/sdk/storage/fulltext-search/fulltext-search-rest.mdx deleted file mode 100644 index b3b0c4f7f..000000000 --- a/leancloud/docs/sdk/storage/fulltext-search/fulltext-search-rest.mdx +++ /dev/null @@ -1,345 +0,0 @@ ---- -title: 全文搜索 REST API -slug: /sdk/storage/guide/fulltext-search-rest/ -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -全文搜索服务提供以下 REST API 接口: - -| URL | HTTP | 功能 | -| ------------------- | ---- | ----------------------- | -| /1.1/search/select | GET | 条件查询 | -| /1.1/search/mlt | GET | moreLikeThis 相关性查询 | -| /1.1/search/analyze | GET | 分词结果查询 | - -在调用全文搜索的 REST API 接口前,需要首先为相应的 Class 启用搜索。 -另外也请参考[数据存储 REST API 使用详解](/sdk/storage/guide/rest/)中关于 API Base URL、请求格式、响应格式的说明,以及《全文搜索指南》的[《自定义分词》](/sdk/storage/guide/fulltext-search/#自定义分词)章节。 - -## 条件查询 - -`GET /1.1/search/select` REST API 接口提供全文搜索功能。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/select?q=dennis&limit=200&clazz=GameScore&order=-score" -``` - -返回类似: - -```json -{ - "hits": 1, - "results": [ - { - "_app_url": "http://stg.pass.com//1/go/com.leancloud/classes/GameScore/51e3a334e4b0b3eb44adbe1a", - "_deeplink": "com.leancloud.appSearchTest://leancloud/classes/GameScore/51e3a334e4b0b3eb44adbe1a", - "_highlight": null, - "updatedAt": "2011-08-20T02:06:57.931Z", - "playerName": "Sean Plott", - "objectId": "51e3a334e4b0b3eb44adbe1a", - "createdAt": "2011-08-20T02:06:57.931Z", - "cheatMode": false, - "score": 1337 - } - ], - "sid": "cXVlcnlUaGVuRmV0Y2g7Mzs0NDpWX0NFUmFjY1JtMnpaRDFrNUlBcTNnOzQzOlZfQ0VSYWNjUm0yelpEMWs1SUFxM2c7NDU6Vl9DRVJhY2NSbTJ6WkQxazVJQXEzZzswOw==" -} -``` - -查询的参数支持: - -| 参数 | 约束 | 说明 | -| ------------ | ---- | ---------------------------------------------------------------------------------------------------------- | -| `q` | 必须 | 查询文本,支持 Elasticsearch 的 query string 语法。参见 [q 查询语法](#q-查询语法)。 | -| `skip` | 可选 | 跳过的文档数目,默认为 0。 | -| `limit` | 可选 | 返回集合大小,默认 100,最大 1000。 | -| `sid` | 可选 | 之前查询结果中返回的 sid 值,用于分页,对应于 Elasticsearch 中的 [scroll id]。 | -| `fields` | 可选 | 逗号隔开的字段列表,查询的字段列表。 | -| `highlights` | 可选 | 高亮字段,可以是通配符 `*`,也可以是字段列表(逗号隔开的字符串)。 | -| `clazz` | 可选 | 类名,如果没有指定或者为空字符串,则搜索所有启用了全文搜索的 class。 | -| `include` | 可选 | 关联查询内联的 Pointer 字段列表,逗号隔开,形如 `user,comment` 的字符串。**仅支持 include Pointer 类型**。 | -| `order` | 可选 | 排序字段,形如 `-score,createdAt` 逗号隔开的字段,负号表示倒序,可以多个字段组合排序。 | -| `sort` | 可选 | 复杂排序字段,例如地理位置信息排序,见下文描述。 | - -[scroll id]: https://www.elastic.co/guide/en/elasticsearch/reference/7.4/search-request-body.html#request-body-search-scroll - -返回结果属性介绍: - -- `results`:符合查询条件的结果文档。 -- `hits`:符合查询条件的文档总数 -- `sid`:标记本次查询结果,下次查询继续传入这个 sid 用于查找后续的数据,用来支持翻页查询。 - -返回结果 `results` 列表里是一个一个的对象,字段是你在全文搜索设置里启用的字段列表,并且有三个特殊字段: - -- `_app_url`:全文搜索结果在网站上的链接。 -- `_deeplink`:全文搜索的程序调用 URL,也就是 deeplink。 -- `_highlight`:高亮的搜索结果内容,关键字用 `em` 标签括起来。如果搜索时未传入 `highlights` 参数,则该字段为 null。 - -最外层的 `sid` 用来标记本次查询结果,下次查询继续传入这个 sid 将翻页查找后 200 条数据: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/select?q=dennis&limit=200&clazz=GameScore&order=-score&sid=cXVlcnlUaGVuRmV0Y2g7Mzs0NDpWX0NFUmFjY1JtMnpaRDFrNUlBcTNnOzQzOlZfQ0VSYWNjUm0yelpEMWs1SUFxM2c7NDU6Vl9DRVJhY2NSbTJ6WkQxazVJQXEzZzswOw" -``` - -直到返回结果为空。 - -### q 查询语法 - -q 参数遵循 Elasticsearch 的 [query string 语法](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-query-string-query.html#query-string-syntax)。建议详细阅读这个文档。这里简单做个举例说明。 - -如果你非常熟悉 Elasticsearch 的 query string 语法,那么可以跳至[地理位置信息查询](#地理位置信息查询)一节(地理位置查询是我们在 Elasticsearch 上添加的扩展功能)。 - -查询的关键字保留字符包括:`+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /`,当出现这些字符的时候,请对这些保留字符做 URL Escape 转义。 - -#### 基础查询语法 - -- 查询某个关键字,例如 `可乐`。 -- 查询**多个关键字**,例如 `可口 可乐`,空格隔开,返回的结果默认按照文本相关性排序,其他排序方法请参考上文中的 [order](#条件查询) 和下文中的 [sort](#复杂排序)。 -- 查询某个**短语**,例如 `"lady gaga"`,注意用双引号括起来,这样才能保证查询出来的相关对象里的相关内容的关键字也是按照 `lady gaga` 的顺序出现。 -- 根据**字段查询**,例如根据 nickname 字段查询:`nickname:逃跑计划`。 -- 根据字段查询,也可以是短语,记得加双引号在短语两侧:`nickname:"lady gaga"`。 -- **复合查询**,AND 或者 OR,例如 `nickname:(逃跑计划 OR 夜空中最亮的星)`。 -- 假设 book 字段是 object 类型,那么可以根据**内嵌字段**来查询,例如 `book.name:clojure OR book.content:clojure`,也可以用通配符简写为 `book.*:clojure`。 -- 查询没有 title 的对象:`_missing_:title`。 -- 查询有 title 字段并且不是 null 的对象:`_exists_:title`。 - -**上面举例根据字段查询,前提是这些字段在 class 的全文搜索设置里启用了索引。** - -#### 通配符和正则查询 - -`qu?ck bro*` 就是一个通配符查询,`?` 表示一个单个字符,而 `*` 表示 0 个或者多个字符。 - -通配符其实是正则的简化,可以使用正则查询: - -``` -name:/joh?n(ath[oa]n)/ -``` - -正则的语法参考 [正则语法](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-regexp-query.html#regexp-syntax)。 - -#### 模糊查询 - -根据文本距离相似度(Fuzziness)来查询。例如 `quikc~`,默认根据 [Damerau–Levenshtein 文本距离算法](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance)来查找最多两次变换的匹配项。 - -例如这个查询可以匹配 `quick`、`qukic`、`qukci` 等。 - -#### 范围查询 - -``` -// 数字 1 到 5: -count:[1 TO 5] - -// 2012 年内 -date:[2012-01-01 TO 2012-12-31] - -// 2012 年之前 -{* TO 2012-01-01} -``` - -`[]` 表示闭区间,`{}` 表示开区间。 - -还可以采用比较运算符: - -``` -age:>10 -age:>=10 -age:<10 -age:<=10 -``` - -#### 查询分组 - -查询可以使用括号分组: - -``` -(quick OR brown) AND fox -``` - -#### 特殊类型字段说明 - -- objectId 在全文搜索的类型为 string,因此可以按照字符串查询:`objectId: 558e20cbe4b060308e3eb36c`,不过这个没有特别必要了,你可以直接走 SDK 查询,效率更好。 -- createdAt 和 updatedAt 映射为 date 类型,例如 `createdAt:["2015-07-30T00:00:00.000Z" TO "2015-08-15T00:00:00.000Z"]` 或者 `updatedAt: [2012-01-01 TO 2012-12-31]`。 -- 除了 createdAt 和 updatedAt 之外的 Date 字段类型,需要加上 `.iso` 后缀做查询:`birthday.iso: [2012-01-01 TO 2012-12-31]`。 -- Pointer 类型,可以采用 `字段名.objectId` 的方式来查询:`player.objectId: 558e20cbe4b060308e3eb36c and player.className: Player`,pointer 只有这两个属性,全文搜索不会 include 其他属性。 -- Relation 字段的查询不支持。 -- File 字段,可以根据 url 或者 id 来查询:`avatar.url: "https://developer.taptap.cn/docs/sdk/storage/guide/fulltext-search-rest/"`,无法根据文件内容做全文搜索。 - -### 复杂排序 - -假设你要排序的字段是一个数组,比如分数数组 `scores`,你想根据平均分来倒序排序,并且没有分数的排最后,那么可以传入: - -```sh ---data-urlencode 'sort=[{"scores":{"order":"desc","mode":"avg","missing":"_last"}}]' -``` - -也就是 `sort` 可以是一个 JSON 数组,其中每个数组元素是一个 JSON 对象: - -```json -{ "scores": { "order": "desc", "mode": "avg", "missing": "_last" } } -``` - -排序的字段作为 key,字段可以设定下列选项: - -- `order`:`asc` 表示升序,`desc` 表示降序。 -- `mode`:如果该字段是多值属性或者数组,那么可以选择按照最小值 `min`、最大值 `max`、总和 `sum` 或者平均值 `avg` 来排序。 -- `missing`:决定缺失该字段的文档排序在开始还是最后,可以选择 `_last` 或者 `_first`,或者指定一个默认值。 - -多个字段排序就类似: - -```json -[ - { - "scores": { "order": "desc", "mode": "avg", "missing": "_last" } - }, - { - "updatedAt": { "order": "asc" } - } -] -``` - -### 地理位置信息查询 - -如果 class 里某个列是 `GeoPoint` 类型,那么可以根据这个字段的地理位置远近来排序,例如假设字段 `location` 保存的是 `GeoPoint` 类型,那么查询 `[39.9, 116.4]` 附近的玩家可以通过设定 sort 为: - -```json -{ - "_geo_distance": { - "location": [39.9, 116.4], - "order": "asc", - "unit": "km", - "mode": "min" - } -} -``` - -`order` 和 `mode` 含义跟上述复杂排序里的一致,`unit` 用来指定距离单位,例如 `km` 表示千米,`m` 表示米,`cm` 表示厘米等。 - -## moreLikeThis 相关性查询 - -除了 `/1.1/search/select` 之外,我们还提供了 `/1.1/search/mlt` API 接口,用于相似文档的查询,可以用来实现相关性推荐。 - -假设我们有一个 Class 叫 `Post` 是用来保存博客文章的,我们想基于它的标签字段 `tags` 做相关性推荐: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/mlt?like=clojure&clazz=Post&fields=tags" -``` - -我们设定了 `like` 参数为 `clojure`,查询的相关性匹配字段 `fields` 是 `tags`,也就是从 `Post` 里查找 `tags` 字段跟 `clojure` 这个文本相似的对象,返回类似: - -```json -{ - "results": [ - { - "tags": ["clojure", "数据结构与算法"], - "updatedAt": "2016-07-07T08:54:50.268Z", - "_deeplink": "cn.leancloud.qfo17qmvr8w2y6g5gtk5zitcqg7fyv4l612qiqxv8uqyo61n://leancloud/classes/Article/577e18b50a2b580057469a5e", - "_app_url": "https://leancloud.cn/1/go/cn.leancloud.qfo17qmvr8w2y6g5gtk5zitcqg7fyv4l612qiqxv8uqyo61n/classes/Article/577e18b50a2b580057469a5e", - "objectId": "577e18b50a2b580057469a5e", - "_highlight": null, - "createdAt": "2016-07-07T08:54:13.250Z", - "className": "Article", - "title": "clojure persistent vector" - } - // …… - ], - "sid": null -} -``` - -除了可以通过指定 `like` 这样的相关性文本来指定查询相似的文档之外,还可以通过 likeObjectIds 指定一个对象的 objectId 列表,来查询相似的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/mlt?likeObjectIds=577e18b50a2b580057469a5e&clazz=Post&fields=tags" -``` - -这次我们换成了查找和 `577e18b50a2b580057469a5e` 这个 objectId 指代的对象相似的对象。 - -更详细的查询参数说明: - -| 参数 | 约束 | 说明 | -| --------------- | ---- | -------------------------------------------------------------------------------------------------------------- | -| `clazz` | 必须 | 类名 | -| `like` | 可选 | **和 `likeObjectIds` 参数二者必须提供其中之一**。代表相似的文本关键字。 | -| `likeObjectIds` | 可选 | **和 `like` 参数二者必须提供其中之一**。代表相似的对象 objectId 列表,用逗号隔开。 | -| `min_term_freq` | 可选 | **文档中一个词语至少出现次数,小于这个值的词将被忽略,默认是 2**,如果返回文档数目过少,可以尝试调低此值。 | -| `min_doc_freq` | 可选 | **词语至少出现的文档个数,少于这个值的词将被忽略,默认值为 5**,同样,如果返回文档数目过少,可以尝试调低此值。 | -| `max_doc_freq` | 可选 | 词语最多出现的文档个数,超过这个值的词将被忽略,防止一些无意义的热频词干扰结果,默认无限制。 | -| `skip` | 可选 | 跳过的文档数目,默认为 0。 | -| `limit` | 可选 | 返回集合大小,默认 100,最大 1000。 | -| `fields` | 可选 | 相似搜索匹配的字段列表,用逗号隔开,默认为所有索引字段 `_all`。 | -| `include` | 可选 | 关联查询内联的 Pointer 字段列表,逗号隔开,形如 `user,comment` 的字符串。**仅支持 include Pointer 类型**。 | - -更多内容参考 [Elasticsearch 文档](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-mlt-query.html)。 - -## 分词结果查询 - -全文搜索会对 String 类型的字段自动进行分词处理。 -如果发现搜索结果不符合预期,推荐先通过 `analyze` API 检查分词结果(要求使用 master key)。 -`analyze` API 也用于验证自定义词库是否生效。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - "https://{{host}}/1.1/search/analyze?clazz=GameScore&text=响应式设计" -``` - -参数包括 `clazz` 和 `text`。`text` 就是测试的文本段,返回结果: - -```json -{ - "tokens": [ - { - "token": "响应", - "start_offset": 0, - "end_offset": 2, - "type": "word", - "position": 0 - }, - { - "token": "式", - "start_offset": 2, - "end_offset": 3, - "type": "word", - "position": 1 - }, - { - "token": "设计", - "start_offset": 3, - "end_offset": 5, - "type": "word", - "position": 2 - } - ] -} -``` - -可以看到,分词系统将「响应式设计」分为了三个词。 -如果分词系统认为「响应式设计」是一个词(比如上传了包含「响应式设计」一词的自定义词库),那么返回结果会是: - -```json -{ - "tokens": [ - { - "token": "响应式设计", - "start_offset": 0, - "end_offset": 5, - "type": "word", - "position": 0 - } - ] -} -``` diff --git a/leancloud/docs/sdk/storage/java-guide/_category_.json b/leancloud/docs/sdk/storage/java-guide/_category_.json deleted file mode 100644 index 6c29c0a34..000000000 --- a/leancloud/docs/sdk/storage/java-guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Java", - "collapsed": true, - "position": 3 -} diff --git a/leancloud/docs/sdk/storage/java-guide/java.mdx b/leancloud/docs/sdk/storage/java-guide/java.mdx deleted file mode 100644 index 1fd4c4973..000000000 --- a/leancloud/docs/sdk/storage/java-guide/java.mdx +++ /dev/null @@ -1,1584 +0,0 @@ ---- -title: 数据存储开发指南 · Java -sidebar_label: Java 开发指南 -slug: /sdk/storage/guide/java/ -sidebar_position: 4 ---- - -import Path from "/src/docComponents/path"; -import { Conditional } from "/src/docComponents/conditional"; - - -数据存储是云服务提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端: - -```java -// 构建对象 -LCObject todo = new LCObject("Todo"); - -// 为属性赋值 -todo.put("title", "工程师周会"); -todo.put("content", "周二两点,全体成员"); - -// 将对象保存到云端 -todo.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // 成功保存之后,执行其他逻辑 - System.out.println("保存成功。objectId:" + todo.getObjectId()); - } - public void onError(Throwable throwable) { - // 异常处理 - } - public void onComplete() {} -}); -``` - -我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。 - -## SDK 安装与初始化 - -请阅读[数据存储 Java SDK 配置指南](/sdk/storage/guide/setup-java/)。 - -## 对象 - -### `LCObject` - -`LCObject` 是云服务对复杂对象的封装,每个 `LCObject` 包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个 `LCObject` 上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。 - -比如说,一个保存着单个 Todo 的 `LCObject` 可能包含如下数据: - -```json -title: "给小林发邮件确认会议时间", -isComplete: false, -priority: 2, -tags: ["工作", "销售"] -``` - -### 数据类型 - -`LCObject` 支持的数据类型包括 `String`、`Number`、`Boolean`、`Object`、`Array`、`Date` 等等。你可以通过嵌套的方式在 `Object` 或 `Array` 里面存储更加结构化的数据。 - -`LCObject` 还支持两种特殊的数据类型 `Pointer` 和 `File`,可以分别用来存储指向其他 `LCObject` 的指针以及二进制数据。 - -`LCObject` 同时支持 `GeoPoint`,可以用来存储地理位置信息。参见 [GeoPoint](#geopoint)。 - -以下是一些示例: - -```java -// 基本类型 -boolean bool = true; -int number = 2018; -String string = number + " 流行音乐榜单"; -Date date = new Date(); -byte[] data = "Hello world!".getBytes(); -ArrayList arrayList = new ArrayList<>(); -arrayList.add(number); -arrayList.add(string); -HashMap hashMap = new HashMap<>(); -hashMap.put("number", number); -hashMap.put("string", string); - -// 构建对象 -LCObject testObject = new LCObject("TestObject"); -testObject.put("testBoolean", bool); -testObject.put("testInteger", number); -testObject.put("testDate", date); -testObject.put("testData", data); -testObject.put("testArrayList", arrayList); -testObject.put("testHashMap", hashMap); -testObject.save(); -``` - -我们不推荐通过 `byte[]` 在 `LCObject` 里面存储图片、文档等大型二进制数据。每个 `LCObject` 的大小不应超过 **128 KB**。如需存储大型文件,可创建 `LCFile` 实例并将其关联到 `LCObject` 的某个属性上。参见 [文件](#文件)。 - -注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。 - -** > 结构化数据** 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。 - -若想了解云服务是如何保护应用数据的,请阅读[数据和安全](/sdk/storage/guide/security/)。 - -### 构建对象 - -下面的代码构建了一个 class 为 `Todo` 的 `LCObject`: - -```java -LCObject todo = new LCObject("Todo"); -``` - -在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。 - -### 保存对象 - -下面的代码将一个 class 为 `Todo` 的对象存入云端: - -```java -// 构建对象 -LCObject todo = new LCObject("Todo"); - -// 为属性赋值 -todo.put("title", "马拉松报名"); -todo.put("priority", 2); - -// 将对象保存到云端 -todo.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // 成功保存之后,执行其他逻辑 - System.out.println("保存成功。objectId:" + todo.getObjectId()); - } - public void onError(Throwable throwable) { - // 异常处理 - } - public void onComplete() {} -}); -``` - -为了确认对象已经保存成功,我们可以到 ** > 结构化数据 > `Todo`** 里面看一下,应该会有一行新的数据产生。点一下这个数据的 `objectId`,应该能看到类似这样的内容: - -```json -{ - "title": "马拉松报名", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -注意,无需在 ** > 结构化数据** 里面创建新的 `Todo` class 即可运行前面的代码。如果 class 不存在,它将自动创建。 - -以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定: - -| 内置属性 | 类型 | 描述 | -| ----------- | -------- | -------------------------------------------------------------- | -| `objectId` | `String` | 该对象唯一的 ID 标识。 | -| `ACL` | `LCACL` | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 | -| `createdAt` | `Date` | 该对象被创建的时间。 | -| `updatedAt` | `Date` | 该对象最后一次被修改的时间。 | - -这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的 `LCObject` 不存在这些属性。 - -属性名(**keys**)只能包含字母、数字和下划线。自定义属性不得以双下划线(`__`)开头或与任何系统保留字段和内置属性(`ACL`、`className`、`createdAt`、`objectId` 和 `updatedAt`)重名,无论大小写。 - -属性值(**values**)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 [数据类型](#数据类型)。 - -我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 `CustomData`。属性,采用小驼峰法,如 `imageUrl`。 - -### 获取对象 - -对于已经保存到云端的 `LCObject`,可以通过它的 `objectId` 将其取回: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.getInBackground("582570f38ac247004f39c24b").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // todo 就是 objectId 为 582570f38ac247004f39c24b 的 Todo 实例 - String title = todo.getString("title"); - int priority = todo.getInt("priority"); - - // 获取内置属性 - String objectId = todo.getObjectId(); - Date updatedAt = todo.getUpdatedAt(); - Date createdAt = todo.getCreatedAt(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 `null`。 - -#### 同步对象 - -当云端数据发生更改时,你可以调用 `fetchInBackground` 方法来刷新对象,使之与云端数据同步: - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.fetchInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // todo 已刷新 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,**`fetchInBackground` 操作会丢弃这些修改**。为避免这种情况,你可以在刷新时指定 **需要刷新的属性**,这样只有指定的属性会被刷新(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`),其他属性不受影响。 - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -String keys = "priority, location"; -todo.fetchInBackground(keys).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // 只有 priority 和 location 会被获取和刷新 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -如果想要确保只刷新已经保存到云端的对象,那么可以用 `fetchIfNeededInBackground` 替换 `fetchInBackground`。 -调用 `fetchIfNeededInBackground` 方法时,如果是本地构造尚未保存的对象,那么不会访问云端,onNext 方法中传入的是本地对象。 - -### 更新对象 - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `saveInBackground` 方法。例如: - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.put("content", "这周周会改到周三下午三点。"); -todo.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject savedTodo) { - System.out.println("保存成功"); - } - public void onError(Throwable throwable) { - System.out.println("保存失败!"); - } - public void onComplete() {} -});; -``` - -云服务会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。 - -#### 有条件更新对象 - -通过传入 `query` 选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回 `305` 错误。 - -例如,用户的账户表 `Account` 有一个余额字段 `balance`,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求: - -```java -LCObject account = LCObject.createWithoutData("Account", "5745557f71cfe40068c6abe0"); -// 对 balance 原子减少 100 -final int amount = -100; -account.increment("balance", amount); -// 设置条件 -LCSaveOption option = new LCSaveOption(); -option.query(new LCQuery<>("Account").whereGreaterThanOrEqualTo("balance", -amount)); -// 操作结束后,返回最新数据。 -// 如果是新对象,则所有属性都会被返回, -// 否则只有更新的属性会被返回。 -option.setFetchWhenSave(true); -account.saveInBackground(option).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject account) { - System.out.println("当前余额为:" + account.get("balance")); - } - public void onError(Throwable throwable) { - System.out.println("余额不足,操作失败!"); - } - public void onComplete() {} -}); -``` - -**`query` 选项只对已存在的对象有效**,不适用于尚未存入云端的对象。 - -`query` 选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过 `LCQuery` 查询 `LCObject` 再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。 - -#### 更新计数器 - -设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 **原子操作** 来增加或减少一个属性内保存的数字: - -```java -post.increment("likes", 1); -``` - -可以指定需要增加或减少的值。若未指定,则默认使用 `1`。 - -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,需要原子增减的字段建议使用整数以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 - -#### 更新数组 - -更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据: - -- `add()` 将指定对象附加到数组末尾。 -- `addUnique()` 如果数组中不包含指定对象,则将该对象加入数组。对象的插入位置是随机的。 -- `removeAll()` 从数组对象中删除指定数组中的所有对象。 - -例如,`Todo` 用一个 `alarms` 属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性: - -```java -Date getDateWithDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date date = dateFormat.parse(dateString); - return date; -} - -Date alarm1 = getDateWithDateString("2018-04-30 07:10:00"); -Date alarm2 = getDateWithDateString("2018-04-30 07:20:00"); -Date alarm3 = getDateWithDateString("2018-04-30 07:30:00"); - -LCObject todo = new LCObject("Todo"); -todo.addAllUnique("alarms", Arrays.asList(alarm1, alarm2, alarm3)); -todo.save(); -``` - -### 删除对象 - -下面的代码从云端删除一个 `Todo` 对象: - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.deleteInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) {} - - @Override - public void onNext(LCNull response) { - // succeed to delete a todo. - } - - @Override - public void onError(@NonNull Throwable e) { - System.out.println("failed to delete a todo: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); -``` - -注意,删除对象是一个较为敏感的操作,我们建议你阅读[ACL 权限管理开发指南](/sdk/storage/guide/acl/)来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。 - -### 批量操作 - -```java -// 批量构建和更新 -saveAll() -saveAllInBackground() - -// 批量删除 -deleteAll() -deleteAllInBackground() - -// 批量同步 -fetchAll() -fetchAllInBackground() -``` - -下面的代码将所有 `Todo` 的 `isComplete` 设为 `true`: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List todos) { - // 获取需要更新的 todo - for (LCObject todo : todos) { - // 更新属性值 - todo.put("isComplete", true); - } - // 批量更新 - LCObject.saveAll(todos); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。 - -### 后台运行 - -细心的开发者已经发现,在所有的示例代码中几乎都是用了异步来访问云端,形如 `xxxxInBackground` 的用法都是提供给开发者在主线程调用用以实现后台运行的方法,因此开发者在主线程可以放心地调用这种命名方式的函数。 - -### 离线存储对象 - -大多数保存功能可以立刻执行,并通知应用「保存完毕」。不过若不需要知道保存完成的时间,则可使用 `saveEventually` 来代替。 - -它的优点在于:如果用户目前尚未接入网络,`saveEventually` 会缓存设备中的数据,并在网络连接恢复后上传。如果应用在网络恢复之前就被关闭了,那么当它下一次打开时,SDK 会再次尝试保存操作。 - -所有 `saveEventually`(或 `deleteEventually`)的相关调用,将按照调用的顺序依次执行。因此,多次对某一对象使用 `saveEventually` 是安全的。 - -### 数据模型 - -对象之间可以产生关联。拿一个博客应用来说,一个 `Post` 对象可以与许多个 `Comment` 对象产生关联。云服务支持三种关系:一对一、一对多、多对多。 - -#### 一对一、一对多关系 - -一对一、一对多关系可以通过将 `LCObject` 保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个 `Comment` 指向一个 `Post`。 - -下面的代码会创建一个含有单个 `Comment` 的 `Post`: - -```java -// 创建 post -LCObject post = new LCObject("Post"); -post.put("title", "饿了……"); -post.put("content", "中午去哪吃呢?"); - -// 创建 comment -LCObject comment = new LCObject("Comment"); -comment.put("content", "当然是肯德基啦!"); - -// 将 post 设为 comment 的一个属性值 -comment.put("parent", post); - -// 保存 comment 会同时保存 post -comment.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // 成功保存之后,执行其他逻辑 - Log.e(TAG, "保存成功。objectId:" + todo.getObjectId()); - } - public void onError(Throwable throwable) { - // 异常处理 - } - public void onComplete() {} - }); -``` - -云端存储时,会将被指向的对象用 `Pointer` 的形式存起来。你也可以用 `objectId` 来指向一个对象: - -```java -LCObject post = LCObject.createWithoutData("Post", "57328ca079bc44005c2472d0"); -comment.put("post", post); -``` - -请参阅 [关系查询](#关系查询) 来了解如何获取关联的对象。 - -#### 多对多关系 - -想要建立多对多关系,最简单的办法就是使用 **数组**。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 **中间表** 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。 - -我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。 - -### 序列化和反序列化 - -在实际的开发中,把 `LCObject` 当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此 `LCObject` 也提供了序列化和反序列化的方法。 - -序列化: - -```java -LCObject todo = new LCObject("Todo"); // 构建对象 -todo.put("title", "马拉松报名"); // 设置名称 -todo.put("priority", 2); // 设置优先级 -todo.put("owner", LCUser.getCurrentUser()); // 这里就是一个 Pointer 类型,指向当前登录的用户 -String serializedString = todo.toString(); -``` - -反序列化: - -```java -LCObject deserializedObject = LCObject.parseLCObject(serializedString); -deserializedObject.save(); // 保存到服务端 -``` - -## 查询 - -我们已经了解到如何从云端获取单个 `LCObject`,但你可能还会有一次性获取多个符合特定条件的 `LCObject` 的需求,这时候就需要用到 `LCQuery` 了。 - -### 基础查询 - -执行一次基础查询通常包括这些步骤: - -1. 构建 `LCQuery`; -2. 向其添加查询条件; -3. 执行查询并获取包含满足条件的对象的数组。 - -下面的代码获取所有 `lastName` 为 `Smith` 的 `Student`: - -```java -LCQuery query = new LCQuery<>("Student"); -query.whereEqualTo("lastName", "Smith"); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List students) { - // students 是包含满足条件的 Student 对象的数组 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### 查询条件 - -可以给 `LCObject` 添加不同的条件来改变获取到的结果。 - -下面的代码查询所有 `firstName` 不为 `Jack` 的对象: - -```java -query.whereNotEqualTo("firstName", "Jack"); -``` - -对于能够排序的属性(比如数字、字符串),可以进行比较查询: - -```java -// 限制 age < 18 -query.whereLessThan("age", 18); - -// 限制 age <= 18 -query.whereLessThanOrEqualTo("age", 18); - -// 限制 age > 18 -query.whereGreaterThan("age", 18); - -// 限制 age >= 18 -query.whereGreaterThanOrEqualTo("age", 18); -``` - -可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是 `AND` 的关系: - -```java -query.whereEqualTo("firstName", "Jack"); -query.whereGreaterThan("age", 18); -``` - -可以通过指定 `limit` 限制返回结果的数量(默认为 `100`): - -```java -// 最多获取 10 条结果 -query.limit(10); -``` - -由于性能原因,`limit` 最大只能设为 `1000`。即使将其设为大于 `1000` 的数,云端也只会返回 1,000 条结果。 - -如果只需要一条结果,可以直接用 `getFirstInBackground`: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("priority", 2); -query.getFirstInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // todo 是第一个满足条件的 Todo 对象 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -可以通过设置 `skip` 来跳过一定数量的结果: - -```java -// 跳过前 20 条结果 -query.skip(20); -``` - -把 `skip` 和 `limit` 结合起来,就能实现翻页功能: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("priority", 2); -query.limit(10); -query.skip(20); -``` - -需要注意的是,`skip` 的值越高,查询所需的时间就越长。作为替代方案,可以通过设置 `createdAt` 或 `updatedAt` 的范围来实现更高效的翻页,因为它们都自带索引。 -同理,也可以通过设置自增字段的范围来实现翻页。 - -对于能够排序的属性,可以指定结果的排序规则: - -```java -// 按 createdAt 升序排列 -query.orderByAscending("createdAt"); - -// 按 createdAt 降序排列 -query.orderByDescending("createdAt"); -``` - -还可以为同一个查询添加多个排序规则: - -```java -query.addAscendingOrder("priority"); -query.addDescendingOrder("createdAt"); -``` - -下面的代码可用于查找包含或不包含某一属性的对象: - -```java -// 查找包含 "images" 的对象 -query.whereExists("images"); - -// 查找不包含 "images" 的对象 -query.whereDoesNotExist("images"); -``` - -可以通过 `selectKeys` 指定需要返回的属性。下面的代码只获取每个对象的 `title` 和 `content`(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`): - -```java -LCQuery query = new LCQuery<>("Todo"); -query.selectKeys(Arrays.asList("title", "content")); -query.getFirstInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - String title = todo.getString("title"); // √ - String content = todo.getString("content"); // √ - String notes = todo.getString("notes"); // 会报错 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -`selectKeys` 支持点号(`author.firstName`),详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)。 -另外,字段名前添加减号前缀表示反向选择,例如 `-author` 表示不返回 `author` 字段。 -反向选择同样适用于内置字段,比如 `-objectId`,也可以和点号组合使用,比如 `-pubUser.createdAt`。 - -对于未获取的属性,可以通过对结果中的对象进行 `fetchInBackground` 操作来获取。参见 [同步对象](#同步对象)。 - -### 字符串查询 - -可以用 `whereStartsWith` 来查找某一属性值以特定字符串开头的对象。和 SQL 中的 `LIKE` 一样,你可以利用索引带来的优势: - -```java -LCQuery query = new LCQuery<>("Todo"); -// 相当于 SQL 中的 title LIKE 'lunch%' -query.whereStartsWith("title", "lunch"); -``` - -可以用 `whereContains` 来查找某一属性值包含特定字符串的对象: - -```java -LCQuery query = new LCQuery<>("Todo"); -// 相当于 SQL 中的 title LIKE '%lunch%' -query.whereContains("title", "lunch"); -``` - -和 `whereStartsWith` 不同,`whereContains` 无法利用索引,因此不建议用于大型数据集。 - -注意 `whereStartsWith` 和 `whereContains` 都是 **区分大小写** 的,所以上述查询会忽略 `Lunch`、`LUNCH` 等字符串。 - -如果想查找某一属性值不包含特定字符串的对象,可以使用 `whereMatches` 进行基于正则表达式的查询: - -```java -LCQuery query = new LCQuery<>("Todo"); -// "title" 不包含 "ticket"(不区分大小写) -query.whereMatches("title", "^((?!ticket).)*$", "i"); -``` - -不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class, -因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以使用全文搜索服务。 - -使用查询时如果遇到性能问题,可参阅 [查询性能优化](#查询性能优化)。 - -### 数组查询 - -下面的代码查找所有数组属性 `tags` 包含 `工作` 的对象: - -```java -query.whereEqualTo("tags", "工作"); -``` - -下面的代码查询数组属性长度为 3(正好包含 3 个标签)的对象: - -```java -query.whereSizeEqual("tags", 3); -``` - -下面的代码查找所有数组属性 `tags` **同时包含** `工作`、`销售` 和 `会议` 的对象: - -```java -query.whereContainsAll("tags", Arrays.asList("工作", "销售", "会议")); -``` - -如需获取某一属性值包含一列值中任意一个值的对象,可以直接用 `whereContainedIn` 而无需执行多次查询。下面的代码构建的查询会查找所有 `priority` 为 `1` **或** `2` 的 todo 对象: - -```java -// 单个查询 -LCQuery priorityOneOrTwo = new LCQuery<>("Todo"); -priorityOneOrTwo.whereContainedIn("priority", Arrays.asList(1, 2)); -// 这样就可以了 :) - -// --------------- -// vs. -// --------------- - -// 多个查询 -final LCQuery priorityOne = new LCQuery<>("Todo"); -priorityOne.whereEqualTo("priority", 1); - -final LCQuery priorityTwo = new LCQuery<>("Todo"); -priorityTwo.whereEqualTo("priority", 2); - -LCQuery priorityOneOrTwo = LCQuery.or(Arrays.asList(priorityOne, priorityTwo)); -// 好像有些繁琐 :( -``` - -反过来,还可以用 `whereNotContainedIn` 来获取某一属性值不包含一列值中任何一个的对象。 - -### 关系查询 - -查询关联数据有很多种方式,常见的一种是查询某一属性值为特定 `LCObject` 的对象,这时可以像其他查询一样直接用 `whereEqualTo`。比如说,如果每一条博客评论 `Comment` 都有一个 `post` 属性用来存放原文 `Post`,则可以用下面的方法获取所有与某一 `Post` 相关联的评论: - -```java -LCObject post = LCObject.createWithoutData("Post", "57328ca079bc44005c2472d0"); -LCQuery query = new LCQuery<>("Comment"); -query.whereEqualTo("post", post); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List comments) { - // comments 包含与 post 相关联的评论 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -如需获取某一属性值为另一查询结果中任一 `LCObject` 的对象,可以用 `whereMatchesQuery`。下面的代码构建的查询可以找到所有包含图片的博客文章的评论: - -```java -LCQuery innerQuery = new LCQuery<>("Post"); -innerQuery.whereExists("image"); - -LCQuery query = new LCQuery<>("Comment"); -query.whereMatchesQuery("post", innerQuery); -``` - -如需获取某一属性值不是另一查询结果中任一 `LCObject` 的对象,则使用 `whereDoesNotMatchQuery`。 - -有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用 `include`。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章: - -```java -LCQuery query = new LCQuery<>("Comment"); - -// 获取最新发布的 -query.orderByDescending("createdAt"); - -// 只获取 10 条 -query.limit(10); - -// 同时包含博客文章 -query.include("post"); - -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List comments) { - // comments 包含最新发布的 10 条评论,包含各自对应的博客文章 - for (LCObject comment : comments) { - // 该操作无需网络连接 - LCObject post = comment.getLCObject("post"); - } - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -可以用 dot 符号(`.`)来获取多级关系,例如 `post.author`,详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)的《在查询对象时使用点号》一节。 - -可以在同一查询上应用多次 `include` 以包含多个属性。通过这种方法获取到的对象同样接受 `getFirst` 等 `LCQuery` 辅助方法。 - -通过 `include` 进行多级查询的方式不适用于数组属性内部的 `LCObject`,只能包含到数组本身。 - -#### 关系查询的注意事项 - -云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌/子查询(和普通查询一样,`limit` 默认为 `100`,最大 `1000`),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出 `limit`,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有 `limit` 数量以内的结果会被填入主查询。 - -我们建议采用以下方案进行改进: - -- 确保子查询的结果在 100 条以下,如果在 100 至 1,000 条之间的话请将子查询的 `limit` 设为 `1000`。 -- 将需要查询的字段冗余到主查询所在的表上。 -- 进行多次查询,每次在子查询上设置不同的 `skip` 值来遍历所有记录(注意 `skip` 的值较大时可能会引发性能问题,因此不是很推荐)。 - -### 统计总数量 - -如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用 `countInBackground` 来代替 `findInBackground`。比如说,查询有多少个已完成的 todo: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("isComplete", true); -query.countInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(Integer count) { - System.out.println(count + " 个 todo 已完成。"); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### 组合查询 - -组合查询就是把诸多查询条件用一定逻辑合并到一起(`OR` 或 `AND`)再交给云端去查询。 - -组合查询不支持在子查询中包含 `GeoPoint` 或其他非过滤性的限制(例如 `near`、`withinGeoBox`、`limit`、`skip`、`ascending`、`descending`、`include`)。 - -#### OR 查询 - -OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于 `3` 或者已经完成了的 todo: - -```java -final LCQuery priorityQuery = new LCQuery<>("Todo"); -priorityQuery.whereGreaterThanOrEqualTo("priority", 3); - -final LCQuery isCompleteQuery = new LCQuery<>("Todo"); -isCompleteQuery.whereEqualTo("isComplete", true); - -LCQuery query = LCQuery.or(Arrays.asList(priorityQuery, isCompleteQuery)); -``` - -使用 OR 查询时,子查询中不能包含 `GeoPoint` 相关的查询。 - -#### AND 查询 - -使用 AND 查询的效果等同于往 `LCQuery` 添加多个条件。下面的代码构建的查询会查找创建时间在 `2016-11-13` 和 `2016-12-02` 之间的 todo: - -```java -Date getDateWithDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - Date date = dateFormat.parse(dateString); - return date; -} - -final LCQuery startDateQuery = new LCQuery<>("Todo"); -startDateQuery.whereGreaterThanOrEqualTo("createdAt", getDateWithDateString("2016-11-13")); - -final LCQuery endDateQuery = new LCQuery<>("Todo"); -endDateQuery.whereLessThan("createdAt", getDateWithDateString("2016-12-03")); - -LCQuery query = LCQuery.and(Arrays.asList(startDateQuery, endDateQuery)); -``` - -单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询: - -```java -Date getDateWithDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - Date date = dateFormat.parse(dateString); - return date; -} - -final LCQuery createdAtQuery = new LCQuery<>("Todo"); -createdAtQuery.whereGreaterThanOrEqualTo("createdAt", getDateWithDateString("2018-04-30")); -createdAtQuery.whereLessThan("createdAt", getDateWithDateString("2018-05-01")); - -final LCQuery locationQuery = new LCQuery<>("Todo"); -locationQuery.whereDoesNotExist("location"); - -final LCQuery priority2Query = new LCQuery<>("Todo"); -priority2Query.whereEqualTo("priority", 2); - -final LCQuery priority3Query = new LCQuery<>("Todo"); -priority3Query.whereEqualTo("priority", 3); - -LCQuery priorityQuery = LCQuery.or(Arrays.asList(priority2Query, priority3Query)); -LCQuery timeLocationQuery = LCQuery.or(Arrays.asList(locationQuery, createdAtQuery)); -LCQuery query = LCQuery.and(Arrays.asList(priorityQuery, timeLocationQuery)); -``` - -### 查询性能优化 - -影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。 - -- 不等于和不包含查询(无法使用索引) -- 通配符在前面的字符串查询(无法使用索引) -- 有条件的 `count`(需要扫描所有数据) -- `skip` 跳过较多的行数(相当于需要先查出被跳过的那些行) -- 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引) -- 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据) - - - -## LiveQuery - -LiveQuery 衍生于 [`LCQuery`](#查询),并为其带来了更强大的功能。它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。 - -设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用 `LCQuery` 并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。 - -想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的 `LCQuery`。订阅成功后,一旦有符合 `LCQuery` 的 `LCObject` 发生变化,云端就会主动、实时地将信息通知到客户端。 - -LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。 - -### 启用 LiveQuery - -进入 ** > 服务设置**,在 **安全设置** 里面勾选 **启用 LiveQuery** 即可。确保即时通信模块已被添加到 `AndroidManifest.xml`: - -```xml - - - - - - - - -``` - -可以在 [SDK 安装与初始化](#sdk-安装与初始化) 中找到完整设置方法。 - -### Demo - -下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果: - - - -使用我们的「LeanTodo」微信小程序和网页应用,可以实际体验以上视频所演示的效果,步骤如下: - -1. 微信扫码,添加小程序「LeanTodo」; - - ![LeanTodo mini program](/img/leantodo-weapp-qr.jpg) - -2. 进入小程序,点击首页左下角 **设置** > **账户设置**,输入便于记忆的用户名和密码; - -3. 使用浏览器访问 ,输入刚刚在小程序中更新好的账户信息,点击 **Login**; - -4. 随意添加更改数据,查看两端的同步状态。 - -注意按以上顺序操作。在网页应用中使用 **Signup** 注册的账户无法与小程序创建的账户相关联,所以如果颠倒以上操作顺序,则无法观测到数据同步效果。 - -[LiveQuery 公开课](http://www.bilibili.com/video/av11291992/) 涵盖了许多开发者关心的问题和解答。 - -### 构建订阅 - -首先创建一个普通的 `LCQuery` 对象,添加查询条件(如有),然后进行订阅操作: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("isComplete", true); -LCLiveQuery liveQuery = LCLiveQuery.initWithQuery(query); -liveQuery.subscribeInBackground(new LCLiveQuerySubscribeCallback() { - @Override - public void done(LCException e) { - if (e == null) { - // 订阅成功 - } - } -}); -``` - -LiveQuery 不支持内嵌查询,也不支持返回指定属性。 - -订阅成功后,就可以接收到和 `LCObject` 相关的更新了。假如在另一个客户端上创建了一个 `Todo` 对象,对象的 `title` 设为 `更新作品集`,那么下面的代码可以获取到这个新的 `Todo`: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("isComplete", true); -LCLiveQuery liveQuery = LCLiveQuery.initWithQuery(query); -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectCreated(LCObject newTodo) { - System.out.println(newTodo.getString("title")); // 更新作品集 - } -}); -liveQuery.subscribeInBackground(new LCLiveQuerySubscribeCallback() { - @Override - public void done(LCException e) { - if (e == null) { - // 订阅成功 - } - } -}); -``` - -此时如果有人把 `Todo` 的 `content` 改为 `把我最近画的插画放上去`,那么下面的代码可以获取到本次更新: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectUpdated(LCObject updatedTodo, List updatedKeys) { - System.out.println(updatedTodo.getString("content")); // 把我最近画的插画放上去 - } -}); -``` - -### 事件处理 - -订阅成功后,可以选择监听如下几种数据变化: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` 事件 - -当有新的满足 `LCQuery` 查询条件的 `LCObject` 被创建时,`create` 事件会被触发。下面的 `object` 就是新建的 `LCObject`: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectCreated(LCObject object) { - System.out.println("对象被创建。"); - } -}); -``` - -#### `update` 事件 - -当有满足 `LCQuery` 查询条件的 `LCObject` 被更新时,`update` 事件会被触发。下面的 `object` 就是有更新的 `LCObject`: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectUpdated(LCObject object, List updatedKeys) { - System.out.println("对象被更新。"); - } -}); -``` - -#### `enter` 事件 - -当一个已存在的、原本不符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后符合查询条件,`enter` 事件会被触发。下面的 `object` 就是进入 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectEnter(LCObject object, List updatedKeys) { - System.out.println("对象进入。"); - } -}); -``` - -注意区分 `create` 和 `enter` 的不同行为。如果一个对象已经存在,在更新之前不符合查询条件,而在更新之后符合查询条件,那么 `enter` 事件会被触发。如果一个对象原本不存在,后来被构建了出来,那么 `create` 事件会被触发。 - -#### `leave` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后不符合查询条件,`leave` 事件会被触发。下面的 `object` 就是离开 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectLeave(LCObject object, List updatedKeys) { - System.out.println("对象离开。"); - } -}); -``` - -#### `delete` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 被删除,`delete` 事件会被触发。下面的 `object` 就是被删除的 `LCObject` 的 `objectId`: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectDeleted(String object) { - System.out.println("对象被删除。"); - } -}); -``` - -### 取消订阅 - -如果不再需要接收有关 `LCQuery` 的更新,可以取消订阅。 - -```java -liveQuery.unsubscribeInBackground(new LCLiveQuerySubscribeCallback() { - @Override - public void done(LCException e) { - if (e == null) { - // 成功取消订阅 - } - } -}); -``` - -### 断开连接 - -断开连接有几种情况: - -1. 网络异常或者网络切换,非预期性断开。 -2. 退出应用、关机或者打开飞行模式等,用户在应用外的操作导致断开。 - -如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。 - -而另外一种极端情况——**当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下**,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。 - -### 网络状态响应 - -可以调用 `setConnectionHandler` 静态方法监听 LiveQuery 连接的建立、断开、异常: - -```java -LCLiveQuery.setConnectionHandler(new LCLiveQueryConnectionHandler() { - @Override - public void onConnectionOpen() { - System.out.println("============ LiveQuery Connection opened ============"); - } - - @Override - public void onConnectionClose() { - System.out.println("============ LiveQuery Connection closed ============"); - } - - @Override - public void onConnectionError(int code, String reason) { - System.out.println("============ LiveQuery Connection error. code:" + code - + ", reason:" + reason + " ============"); - } -}); -``` - -### LiveQuery 的注意事项 - -因为 LiveQuery 的实时性,很多用户会陷入一个误区,试着用 LiveQuery 来实现一个简单的聊天功能。 -我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,后期维护的难度非常大(聊天记录、对话维护之类的代码会很混乱),并且云服务已经提供了即时通讯的服务。 -LiveQuery 的核心还是提供一个针对查询的推拉结合的用法,脱离设计初衷容易造成前端的模块混乱。 - -如果你的应用只使用 LiveQuery(不使用即时通讯和其他推送服务),那么可以在初始化 SDK 时使用 `PushService` 的静态方法 `startIfRequired` 来创建 WebSocket 连接: - -```java -PushService.startIfRequired(android.content.Context context); -``` - - -## 文件 - -有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用 `LCObject` 保存,此时文件对象 `LCFile` 便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。 - -### 构建文件 - -可以通过字符串构建文件: - -```java -// resume.txt 是文件名 -LCFile file = new LCFile("resume.txt", "LeanCloud".getBytes()); -``` - -除此之外,还可以通过 URL 构建文件: - -```java -LCFile file = new LCFile( - "logo.png", - "https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png", - new HashMap() -); -``` - -通过 URL 构建文件时,SDK 并不会将原本的文件转储到云端,而是会将文件的物理地址存储为字符串,这样也就不会产生任何文件上传流量。使用其他方式构建的文件会被保存在云端。 - -云端会根据文件扩展名自动检测文件类型。如果需要的话,也可以手动指定 `Content-Type`(一般称为 MIME 类型): - -```java -Map meta = new HashMap(); -meta.put("mime_type", "application/json"); -LCFile file = new LCFile("resume.txt", "LeanCloud".getBytes(), meta); -``` - -与前面提到的方式相比,一个更常见的文件构建方式是从本地路径上传。 - -```java -LCFile file = LCFile.withAbsoluteLocalPath("avatar.jpg", "/tmp/avatar.jpg"); -``` - -这里上传的文件名字叫做 `avatar.jpg`。需要注意: - -- 每个文件会被分配到一个独一无二的 `objectId`,所以在一个应用内是允许多个文件重名的。 -- 文件必须有扩展名才能被云端正确地识别出类型。比如说要用 `LCFile` 保存一个 PNG 格式的图像,那么扩展名应为 `.png`。 -- 如果文件没有扩展名,且没有手动指定类型,那么云服务将默认使用 `application/octet-stream`。 - -### 保存文件 - -将文件保存到云端后,便可获得一个永久指向该文件的 URL: - -```java -file.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCFile file) { - System.out.println("文件保存完成。URL:" + file.getUrl()); - } - public void onError(Throwable throwable) { - // 保存失败,可能是文件无法被读取,或者上传过程中出现问题 - } - public void onComplete() {} -}); -``` - -文件上传后,可以在 ** > 文件** 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的 `objectId` 和 URL。 - -已经保存到云端的文件可以关联到 `LCObject`: - -```java -LCObject todo = new LCObject("Todo"); -todo.put("title", "买蛋糕"); -// attachments 是一个 Array 属性 -todo.add("attachments", file); -todo.save(); -``` - -也可以通过构建 `LCQuery` 进行[查询](#查询): - -```java -LCQuery query = new LCQuery<>("_File"); -``` - -需要注意的是,内部文件(上传到文件服务的文件)的 `url` 字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 -因此,通过 `url` 字段查询文件仅适用于外部文件(直接保存外部 URL 到文件服务创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。 - -注意,如果文件被保存到了 `LCObject` 的一个数组属性中,那么在查询 `LCObject` 时如果需要包含文件,则要用到 `LCQuery` 的 `include` 方法。比如说,在获取所有标题为 `买蛋糕` 的 todo 的同时获取附件中的文件: - -```java -// 获取同一标题且包含附件的 todo -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("title", "买蛋糕"); -query.whereExists("attachments"); - -// 同时获取附件中的文件 -query.include("attachments"); - -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List todos) { - for (LCObject todo : todos) { - // 获取每个 todo 的 attachments 数组 - } - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### 上传进度监听 - -上传过程中可以实时向用户展示进度: - -```java -file.saveInBackground(new ProgressCallback() { - @Override - public void done(Integer percent) { - System.out.println("上传进度:" + percent + "%"); - } -}); -``` - -percent 是一个 0 到 100 之间的整数,表示上传进度。 -percent 为 100 表示上传完成。 -如果上传过程中出现问题,会抛出 LCException 异常。 -这个接受 ProgressCallback 参数的 saveInBackground 重载是一个 `void` 方法,不会返回 `Observable`。 - -### 文件元数据 - -上传文件时,可以用 `metaData` 添加额外的属性。文件一旦保存,`metaData` 便不可再修改。 - -```java -// 设置元数据 -file.addMetaData("author", "LeanCloud"); -file.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCFile file) { - // 获取 author 属性 - String author = (String) file.getMetaData("author"); - // 获取文件名 - String fileName = file.getName(); - // 获取大小(不适用于通过 base64 编码的字符串或者 URL 保存的文件) - int size = file.getSize(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### 图像缩略图 - -成功保存图像后,除了可以获取指向该文件的 URL 外,还可以获取图像的缩略图 URL,并且可以指定缩略图的宽度和高度: - -```java -LCFile file = new LCFile("test.jpg", "文件-url", new HashMap()); -file.getThumbnailUrl(true, 100, 100); -``` - -图片最大不超过 **20 MB** 才可以获取缩略图。 - -国际版不支持图片缩略图。 - -### 下载文件 - -可以调用 LCFile 的 `getDataInBackground` 或 `getDataStreamInBackground` 方法下载文件: - -```java -file.getDataInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) {} - - @Override - public void onNext(byte[] bytes) { - Log.d("LCFile", "file data length: " + bytes.length); - } - - @Override - public void onError(Throwable e) { - Log.d("LCFile", "failed to get data. cause: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); - -file.getDataStreamInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) {} - - @Override - public void onNext(InputStream inputStream) { - try { - byte[] buffer = new byte[102400]; - int read = inputStream.read(buffer); - Log.d("LCFile", "file data length: " + read); - inputStream.close(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - @Override - public void onError(Throwable e) { - Log.d("LCFile", "failed to get data. cause: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); -``` - -除了使用 SDK 提供的方法外,也可以获取 LCFile 的 url 后调用标准库和第三方库下载文件。 - -### 删除文件 - -下面的代码从云端删除一个文件: - -```java -LCObject file = LCObject.createWithoutData("_File", "552e0a27e4b0643b709e891e"); -file.deleteInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) {} - - @Override - public void onNext(LCNull response) { - // succeed to delete the file - } - - @Override - public void onError(@NonNull Throwable e) { - System.out.println("failed to delete the file: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); -``` - -默认情况下,文件的删除权限是关闭的。 -如需删除文件,一般建议在服务端使用 Master Key 调用 REST API 删除。 -取决于产品的具体需求,也可以考虑在 ** > 文件 > 权限** 向特定用户或特定角色开启删除权限。 - -### 文件审核 - -当前**文件审核**功能支持检测**图片**文件。 - -你可以在 **数据存储 > 文件 > 文件审核** 标签下勾选「自动审核新上传图片」,还可以批量审核指定时间范围内的图片,图片审核结果将在 **文件管理** 标签页展示。 - -如果你需要人工二次审核,可以点击每一行记录,在文件详情中选择「通过」或「封禁」。 - -## GeoPoint - -云服务允许你通过将 `LCGeoPoint` 关联到 `LCObject` 的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。 - -要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个 `LCGeoPoint` 并将其纬度(`latitude`)设为 `39.9`,经度(`longitude`)设为 `116.4`: - -```java -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -``` - -现在可以将这个地理位置存储为一个对象的属性: - -```java -todo.put("location", point); -``` - -### 地理位置查询 - -给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 `LCQuery` 添加 `whereNear` 条件。下面的代码查找 `location` 属性值离某一点最近的 `Todo` 对象: - -```java -LCQuery query = new LCQuery<>("Todo"); -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -query.whereNear("location", point); - -// 限制为 10 条结果 -query.limit(10); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List todos) { - // todos 是包含满足条件的 Todo 对象的数组 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -像 `orderByAscending` 和 `orderByDescending` 这样额外的排序条件会获得比默认的距离排序更高的优先级。 - -若要限制结果和给定地点之间的距离,可以参考 API 文档中的 `whereWithinKilometers`、`whereWithinMiles` 和 `whereWithinRadians` 参数。 - -若要查询在某一矩形范围内的对象,可以用 `whereWithinGeoBox`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```java -LCQuery query = new LCQuery<>("Todo"); -LCGeoPoint southwest = new LCGeoPoint(30, 115); -LCGeoPoint northeast = new LCGeoPoint(40, 118); -query.whereWithinGeoBox("location", southwest, northeast); -``` - -### GeoPoint 的注意事项 - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -## 用户 - -请参阅[内建账户指南](/sdk/authentication/guide/)。 - -## 角色 - -随着用户量的增长,你可能会发现相比于为每一名用户单独设置权限,将预先设定好的权限直接分配给一部分用户是更好的选择。为了迎合这种需求,云服务支持基于角色的权限管理。请参阅[ACL 权限管理开发指南](/sdk/storage/guide/acl/)。 - -## 子类化 - -子类化推荐给进阶的开发者在进行代码重构的时候做参考。你可以用 `LCObject#get` 访问任意字段;你也可以使用子类化的属性来封装获取字段的方法,增强编码体验。 -子类化有很多优势,包括减少代码的编写量,具有更好的扩展性,和支持自动补全等等。 - -### 实现 - -要实现子类化,需要下面 4 个步骤: - -1. 首先声明一个子类继承自 `LCObject`; -2. 添加 `@LCClassName` 注解。它的值必须是一个字符串,也就是你过去传入 `LCObject` 构造函数的类名。这样一来,后续就不需要再在代码中出现这个字符串类名; -3. 确保你的子类有一个 public 的默认(参数个数为 0)的构造函数。切记不要在构造函数里修改任何 `LCObject` 的字段; -4. 在你的应用初始化的地方,在调用 `LeanCloud.initialize()` 之前注册子类 `LCObject.registerSubclass(YourClass.class)`。 - -下面是实现 `Student` 子类化的例子: - -```java -// Student.java -import com.tapsdk.lc.LCClassName; -import com.tapsdk.lc.LCObject; - -@LCClassName("Student") -public class Student extends LCObject { - // 添加访问器 - public String getContent() { - return getString("content"); - } - // 添加修改器 - public void setContent(String value) { - put("content", value); - } - // 添加自定义方法 - public void handleReport() { - // 处理用户举报,当达到某个条数的时候,自动打上屏蔽标志 - increment("report", 1); - if (getReport() > 50) { - setSpam(true); - } - } -} - -// App.java -import com.tapsdk.lc.LeanCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - LCObject.registerSubclass(Student.class); - LeanCloud.initialize(this, "your-client-id", "your-client-token", "https://please-replace-with-your-customized.domain.com"); - } -} -``` - -### 初始化子类 - -你可以使用你自定义的构造函数来创建你的子类对象。你的子类必须定义一个公开的默认构造函数,并且不修改任何父类 LCObject 中的字段,这个默认构造函数将会被 SDK 使用来创建子类的强类型的对象。 - -要创建一个到现有对象的引用,可以使用 `LCObject.createWithoutData()`: - -```java -Student postReference = LCObject.createWithoutData(Student.class, student.getObjectId()); -``` - -### 子类的序列化与反序列化 - -如果希望 LCObject 子类也支持 Parcelable,则需要至少满足以下几个要求: - -1. 确保子类有一个 public 并且参数为 Parcel 的构造函数,并且在内部调用父类的该构造函数。 -2. 内部需要有一个静态变量 CREATOR 实现 `Parcelable.Creator`。 - -```java -// Stduent.java -@LCClassName("Student") -public class Student extends LCObject { - public Student(){ - super(); - } - - public Student(Parcel in){ - super(in); - } - //此处为我们的默认实现,当然你也可以自行实现 - public static final Creator CREATOR = AVObjectCreator.instance; -} -``` - -### 查询子类 - -你可以通过 `LCObject.getQuery()` 或者 `LCQuery.getQuery` 的静态方法获取特定的子类的查询对象。下面的例子就查询了用户发表的所有微博列表: - -```java -LCQuery query = LCObject.getQuery(Student.class); -query.whereEqualTo("pubUser", LCUser.getCurrentUser().getUsername()); -List results = query.find(); -for (Student a : results) { - // ... -} -``` - -### LCUser 的子类化 - -LCUser 作为 AVObject 的子类,同样允许子类化,你可以定义自己的 User 对象,不过比起 LCObject 子类化会更简单一些,只要继承 LCUser 就可以了: - -```java -import com.tapsdk.lc.LCObject; -import com.tapsdk.lc.LCUser; - -public class MyUser extends LCUser { - public void setNickName(String name) { - this.put("nickName", name); - } - - public String getNickName() { - return this.getString("nickName"); - } -} -``` - -不需要添加 @LCClassname 注解,所有 LCUser 的子类的类名都是内建的 `_User`。同样也不需要注册 MyUser。 - -当用户子类化 LCUser 后,如果希望以后查询 LCUser 所得到的对象会自动转化为用户子类化的对象,则需要在调用 LeanCloud.initialize() 之前添加: - -```java -LCUser.alwaysUseSubUserClass(subUser.class); -``` - -注册跟普通的 LCUser 对象没有什么不同,但是登录如果希望返回自定义的子类,必须这样: - -```java -MyUser cloudUser = LCUser.logIn(username, password, MyUser.class); -``` - -## 全文搜索 - -全文搜索是一个针对应用数据进行全局搜索的接口,它基于搜索引擎构建,提供更强大的搜索功能。要深入了解其用法和阅读示例代码,请阅[全文搜索指南](/sdk/storage/guide/fulltext-search/)。 diff --git a/leancloud/docs/sdk/storage/java-guide/setup-java.mdx b/leancloud/docs/sdk/storage/java-guide/setup-java.mdx deleted file mode 100644 index 4dd7a15e6..000000000 --- a/leancloud/docs/sdk/storage/java-guide/setup-java.mdx +++ /dev/null @@ -1,496 +0,0 @@ ---- -title: 数据存储 Java SDK 配置指南 -sidebar_label: Java SDK 配置 -slug: /sdk/storage/guide/setup-java/ -sidebar_position: 3 ---- - -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import DomainBinding from "../../_partials/setup-domain.mdx"; -import AppConfig from "../_partials/app-config.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -## 获取 SDK - -获取 SDK 有多种方式,较为推荐的方式是通过包依赖管理工具下载最新版本。 - -### 使用包管理工具下载 - -我们已经将所有的 library 发布到了 maven 中心仓库,开发者可以用以下任意包管理工具来安装 SDK。 - - - -对于数据存储服务: - - - - - - -如果是 Android 项目,使用这些包: - - -{`implementation 'com.taptap:lc-storage-android:${sdkVersions.leancloud.java}' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'`} - - - - - - - {` - com.taptap - lc-storage-core - ${sdkVersions.leancloud.java} -`} - - - - - - - - {``} - - - - - - - - {`libraryDependencies += "com.taptap" %% "lc-storage-core" % "${sdkVersions.leancloud.java}"`} - - - - - - - - {`implementation 'com.taptap:lc-storage-core:${sdkVersions.leancloud.java}'`} - - - - - - - - -对于即时通讯与推送服务: - - - - -如果是 Android 项目,使用这些包: - - - {`implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'`} - - -使用混合推送服务: - - - {`implementation 'cn.leancloud:mixpush-android:${sdkVersions.leancloud.java}' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'`} - - - - - - - - {` - cn.leancloud - realtime-core - ${sdkVersions.leancloud.java} -`} - - - - - - - - {``} - - - - - - - - {`libraryDependencies += "cn.leancloud" %% "realtime-core" % "${sdkVersions.leancloud.java}"`} - - - - - - - - {`implementation 'cn.leancloud:realtime-core:${sdkVersions.leancloud.java}'`} - - - - - - - - -
    -点击查看对 maven 源的特别说明 - -我们发现有时候 Maven 源的 CDN 缓存同步策略出现问题,可能会导致我们某个版本或者该版本下某种格式的 library 文件无法下载,这时候你可以在配置文件中显式增加一个 Sonatype 的源,就可以解决找不到文件的问题。 - -Maven pom.xml 的修改示例如下: - -```xml - - - oss-sonatype - oss-sonatype - https://oss.sonatype.org/content/groups/public/ - - -``` - -Gradle build.gradle 的修改示例如下: - -```groovy -buildscript { - repositories { - google() - jcenter() - // 增加下面的配置 - maven { - url "https://oss.sonatype.org/content/groups/public/" - } - } -} - -allprojects { - repositories { - google() - jcenter() - // 增加下面的配置 - maven { - url "https://oss.sonatype.org/content/groups/public/" - } - } -} -``` - -
    - -### 手动安装 - -可以执行以下命令获取 Java SDK 并安装: - -```sh -$ git clone https://github.com/taptap/TapSDK-LC-Java.git -$ cd TapSDK-LC-Java/ -$ mvn clean install -``` - -获取和安装 Android SDK: - -```sh -$ cd TapSDK-LC-Java/ -$ cd android-sdk/ -$ gradle clean assemble -``` - -## 初始化 - -### 应用凭证 - - - -### Android 平台初始化 - - - -:::info -[TapSDK 初始化](/sdk/start/quickstart/#初始化)时,会自动执行这一节的初始化方法。 - -如果已经参考 **快速开始** 文档完成了 TapSDK 初始化,则**不需要**参考这里的初始化。 -::: - - - -如果是一个 Android 项目,则向 `Application` 类的 `onCreate` 方法添加: - - - -```java -import com.tapsdk.lc.LeanCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // 注意这里千万不要调用 com.tapsdk.lc.core.LeanCloud 的 initialize 方法,否则会出现 NetworkOnMainThread 等错误。 - LeanCloud.initialize(this, "your-client-id", "your-client-token", "https://your_server_url"); - } -} -``` - - - - - -```java -import cn.leancloud.LeanCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // 注意这里千万不要调用 cn.leancloud.core.LeanCloud 的 initialize 方法,否则会出现 NetworkOnMainThread 等错误。 - LeanCloud.initialize(this, "your-app-id", "your-app-key", "https://your_server_url"); - } -} -``` - - - -然后指定 SDK 需要的权限并在 `AndroidManifest.xml` 里面声明 `MyLeanCloudApp` 类: - - - -```xml - - - - - - - - -``` - - - - - -```xml - - - - - - - - - - - - - - - - - - - -``` - -注意:对于**只使用即时通讯服务**,而**不使用推送服务**的应用来说,在初始化的时候设置 `LCIMOptions#disableAutoLogin4Push` 选项,可以加快即时通讯用户登录的过程。设置方法如下: - -```java -// 在 LeanCloud.initialize 之后调用,禁止自动发送推送服务的 login 请求。 -LCIMOptions.getGlobalOptions().setDisableAutoLogin4Push(true); -``` - - - - - -#### 更安全的客户端初始化方法 - -对 Android 开发者来说,从 6.1.0 版本开始,除了支持通过 appId + appKey 完成初始化,我们还提供一种更加安全的使用方式,支持仅仅通过 appId 来初始化应用,例如: - -```java -import cn.leancloud.LeanCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // 提供 this、App ID、绑定的自定义 API 域名作为参数 - LeanCloud.initializeSecurely(this, "{{appid}}", "https://your_server_url"); - } -} -``` - -这时候程序初始化不再需要 appKey,避免了核心配置信息在客户端泄漏可能带来的潜在风险。具体的集成方法可参看文档[《Android SDK 更安全的接入和初始化方式》](/sdk/storage/guide/setup-android-securely/)。 - - - -### Java 平台初始化 - -如果是一个普通 Java 项目,则在代码开头添加: - - - -```java -import com.tapsdk.lc.core.LeanCloud; - -LeanCloud.initialize("your-client-id", "your-client-token", "https://your_server_url"); -``` - - - - - -```java -import cn.leancloud.core.LeanCloud; - -LeanCloud.initialize("your-app-id", "your-app-key", "https://your_server_url"); -``` - - - -注意,云引擎内部访问 API 是通过内网,所以不需要也不应该配置 API 自定义域名(`serverUrl`)。 -模板项目和示例代码均未配置 API 自定义域名, -请勿调用 `setServer`,否则会变成公网访问,影响性能。 - - - -realtime-core library 也支持在纯 Java Application 中使用,但是与 Android 的调用方式有细微差异,Java Application 中需要开发者显式建立与云端的长链接(Android 平台是通过 PushService 自动建立的)。建立长链接的方法如下: - -```java -LCConnectionManager.getInstance().startConnection(new LCCallback() { - @Override - protected void internalDone0(Object o, LCException e) { - if (e == null) { - System.out.println("成功建立 WebSocket 链接"); - } else { - System.out.println("建立 WebSocket 链接失败:" + e.getMessage()); - } - } -}); -``` - -只有长链接成功建立之后,后续的聊天请求才能开始。 - - - -## 域名 - - - -## 开启调试日志 - -在应用开发阶段,你可以选择开启 SDK 的调试日志(debug log)来方便追踪问题。调试日志开启后,SDK 会把网络请求、错误消息等信息输出到 IDE 的日志窗口,或是浏览器 Console 或是云引擎日志(如果在云引擎下运行 SDK)。 - -```java -// 在初始化之前调用 -LeanCloud.setLogLevel(LCLogger.Level.DEBUG); -``` - -详细调试流程可以参考[Android SDK 调试指南][android-debug-guide]。 - -[android-debug-guide]: https://forum.leancloud.cn/t/leancloud-sdk-android-sdk/21829 - -:::caution -在应用发布之前,请关闭调试日志,以免暴露敏感数据。 -::: - -## 验证 - -首先,确认本地网络环境是可以访问云端服务器的,可以执行以下命令: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` 为绑定的 API 自定义域名。 - -如果当前网络正常会返回当前时间: - -```json -{ "__type": "Date", "iso": "2020-10-12T06:46:56.000Z" } -``` - -然后在项目中编写如下测试代码: - -```java -LCObject testObject = new LCObject("TestObject"); -testObject.put("words", "Hello world!"); -testObject.saveInBackground().blockingSubscribe(); -``` - -保存后运行程序。 - -然后打开 ** > 结构化数据 > `TestObject`**,如果看到数据表中出现一行「words」列的值为「Hello world!」的数据,说明 SDK 已经正确地执行了上述代码,配置完毕。 - -如果控制台没有发现对应的数据,请参考 [问题排查](#问题排查)。 - -## 问题排查 - -SDK 安装指南基于当前最新版本的 SDK 编写,所以排查问题前,请先检查下安装的 SDK 是不是最新版本。 - -### `401 Unauthorized` - -如果 SDK 抛出 `401` 异常或者查看本地网络访问日志存在: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -则可认定为 `App ID` 或者 `App Key` 输入有误,或者是不匹配,很多开发者同时注册了多个应用,导致拷贝粘贴的时候,用 A 应用的 `App ID` 匹配 B 应用的 `App Key`,这样就会出现服务端鉴权失败的错误。 - -### 客户端无法访问网络 - -客户端尤其是手机端,应用在访问网络的时候需要申请一定的权限。 - -## Android 代码混淆 - -为了保证 SDK 在代码混淆后能正常运作,需要保证部分类和第三方库不被混淆,参考下列配置: - -``` -# proguard.cfg --keepattributes Signature --dontwarn com.jcraft.jzlib.** --keep class com.jcraft.jzlib.** { *;} --dontwarn sun.misc.** --keep class sun.misc.** { *;} --dontwarn retrofit2.** --keep class retrofit2.** { *;} --dontwarn io.reactivex.** --keep class io.reactivex.** { *;} --dontwarn sun.security.** --keep class sun.security.** { *; } --dontwarn com.google.** --keep class com.google.** { *;} --dontwarn com.tapsdk.lc.** --keep class com.tapsdk.lc.** { *;} --keep public class android.net.http.SslError --keep public class android.webkit.WebViewClient --dontwarn android.webkit.WebView --dontwarn android.net.http.SslError --dontwarn android.webkit.WebViewClient --dontwarn android.support.** --dontwarn org.apache.** --keep class org.apache.** { *;} --dontwarn okhttp3.** --keep class okhttp3.** { *;} --keep interface okhttp3.** { *; } --dontwarn okio.** --keep class okio.** { *;} --keepattributes *Annotation* -``` diff --git a/leancloud/docs/sdk/storage/js-guide/_category_.json b/leancloud/docs/sdk/storage/js-guide/_category_.json deleted file mode 100644 index ec86fd4a6..000000000 --- a/leancloud/docs/sdk/storage/js-guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "JavaScript", - "collapsed": true, - "position": 5 -} diff --git a/leancloud/docs/sdk/storage/js-guide/js.mdx b/leancloud/docs/sdk/storage/js-guide/js.mdx deleted file mode 100644 index 545b5b6dd..000000000 --- a/leancloud/docs/sdk/storage/js-guide/js.mdx +++ /dev/null @@ -1,1546 +0,0 @@ ---- -title: 数据存储开发指南 · JavaScript -sidebar_label: JavaScript 开发指南 -slug: /sdk/storage/guide/js/ -sidebar_position: 2 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import Path from "/src/docComponents/path"; -import { Conditional } from "/src/docComponents/conditional"; - - -数据存储是云服务提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端: - -```js -// 声明 class -const Todo = AV.Object.extend("Todo"); - -// 构建对象 -const todo = new Todo(); - -// 为属性赋值 -todo.set("title", "工程师周会"); -todo.set("content", "周二两点,全体成员"); - -// 将对象保存到云端 -todo.save().then( - (todo) => { - // 成功保存之后,执行其他逻辑 - console.log(`保存成功。objectId:${todo.id}`); - }, - (error) => { - // 异常处理 - } -); -``` - -我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。 - -## SDK 安装与初始化 - -请阅读 [数据存储、即时通讯 JavaScript SDK 配置指南](/sdk/storage/guide/setup-js/)。 - -## 对象 - -### `AV.Object` - -`AV.Object` 是云服务对复杂对象的封装,每个 `AV.Object` 包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个 `AV.Object` 上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。 - -比如说,一个保存着单个 Todo 的 `AV.Object` 可能包含如下数据: - -```json -title: "给小林发邮件确认会议时间", -isComplete: false, -priority: 2, -tags: ["工作", "销售"] -``` - -### 数据类型 - -`AV.Object` 支持的数据类型包括 `String`、`Number`、`Boolean`、`Object`、`Array`、`Date` 等等。你可以通过嵌套的方式在 `Object` 或 `Array` 里面存储更加结构化的数据。 - -`AV.Object` 还支持两种特殊的数据类型 `Pointer` 和 `File`,可以分别用来存储指向其他 `AV.Object` 的指针以及二进制数据。 - -`AV.Object` 同时支持 `GeoPoint`,可以用来存储地理位置信息。参见 [GeoPoint](#geopoint)。 - -以下是一些示例: - -```js -// 基本类型 -const bool = true; -const number = 2018; -const string = `${number} 流行音乐榜单`; -const date = new Date(); -const array = [string, number]; -const object = { - number: number, - string: string, -}; - -// 构建对象 -const TestObject = AV.Object.extend("TestObject"); -const testObject = new TestObject(); -testObject.set("testNumber", number); -testObject.set("testString", string); -testObject.set("testDate", date); -testObject.set("testArray", array); -testObject.set("testObject", object); -testObject.save(); -``` - -我们不推荐在 `AV.Object` 里面存储图片、文档等大型二进制数据。每个 `AV.Object` 的大小不应超过 **128 KB**。如需存储大型文件,可创建 `AV.File` 实例并将其关联到 `AV.Object` 的某个属性上。参见 [文件](#文件)。 - -注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。 - -** > 结构化数据** 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。 - -若想了解云服务是如何保护应用数据的,请阅读[数据和安全](/sdk/storage/guide/security/)。 - -### 构建对象 - -下面的代码构建了一个 class 为 `Todo` 的 `AV.Object`: - -```js -// 为 AV.Object 创建子类 -const Todo = AV.Object.extend("Todo"); - -// 为该类创建一个新实例 -const todo = new Todo(); - -// 你还可以直接使用 AV.Object 的构造器 -const todo = new AV.Object("Todo"); -``` - -在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。 - -如果你使用的是 ES6,还可以通过 `extends` 关键字来创建 `AV.Object` 的子类,然而 SDK 无法自动识别你创建的子类。你需要通过这种方式手动注册一下: - -```js -class Todo extends AV.Object { - // 自定义属性和方法 -} - -// 注册子类 -AV.Object.register(Todo); -``` - -这样你就能在 `AV.Object` 的子类中添加额外的方法和属性了。 - -### 保存对象 - -下面的代码将一个 class 为 `Todo` 的对象存入云端: - -```js -// 声明 class -const Todo = AV.Object.extend("Todo"); - -// 构建对象 -const todo = new Todo(); - -// 为属性赋值 -todo.set("title", "马拉松报名"); -todo.set("priority", 2); - -// 将对象保存到云端 -todo.save().then( - (todo) => { - // 成功保存之后,执行其他逻辑 - console.log(`保存成功。objectId:${todo.id}`); - }, - (error) => { - // 异常处理 - } -); -``` - -为了确认对象已经保存成功,我们可以到 ** > 结构化数据 > `Todo`** 里面看一下,应该会有一行新的数据产生。点一下这个数据的 `objectId`,应该能看到类似这样的内容: - -```json -{ - "title": "马拉松报名", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -注意,无需在 ** > 结构化数据** 里面创建新的 `Todo` class 即可运行前面的代码。如果 class 不存在,它将自动创建。 - -以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定: - -| 内置属性 | 类型 | 描述 | -| ----------- | -------- | -------------------------------------------------------------- | -| `objectId` | `String` | 该对象唯一的 ID 标识。 | -| `ACL` | `AV.ACL` | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 | -| `createdAt` | `Date` | 该对象被创建的时间。 | -| `updatedAt` | `Date` | 该对象最后一次被修改的时间。 | - -这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的 `AV.Object` 不存在这些属性。 - -属性名(**keys**)只能包含字母、数字和下划线。自定义属性不得以双下划线(`__`)开头或与任何系统保留字段和内置属性(`ACL`、`className`、`createdAt`、`objectId` 和 `updatedAt`)重名,无论大小写。 - -属性值(**values**)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 [数据类型](#数据类型)。 - -我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 `CustomData`。属性,采用小驼峰法,如 `imageUrl`。 - -### 获取对象 - -对于已经保存到云端的 `AV.Object`,可以通过它的 `objectId` 将其取回: - -```js -const query = new AV.Query("Todo"); -query.get("582570f38ac247004f39c24b").then((todo) => { - // todo 就是 objectId 为 582570f38ac247004f39c24b 的 Todo 实例 - const title = todo.get("title"); - const priority = todo.get("priority"); - - // 获取内置属性 - const objectId = todo.id; - const updatedAt = todo.updatedAt; - const createdAt = todo.createdAt; -}); -``` - -如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 `undefined`。 - -如果需要一次性获取返回对象的所有属性(比如进行数据绑定)而非显式地调用 `get`,可以利用 `AV.Object` 实例的 `toJSON` 方法: - -```js -const query = new AV.Query("Todo"); -query.get("582570f38ac247004f39c24b").then((todo) => { - console.log(todo.toJSON()); - // { - // createdAt: "2017-03-08T11:25:07.804Z", - // objectId: "582570f38ac247004f39c24b", - // priority: 2, - // title: "工程师周会", - // updatedAt: "2017-03-08T11:25:07.804Z" - // } -}); -``` - -#### 同步对象 - -当云端数据发生更改时,你可以调用 `fetch` 方法来刷新对象,使之与云端数据同步: - -```js -const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.fetch().then((todo) => { - // todo 已刷新 -}); -``` - -刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,**`fetch` 操作会丢弃这些修改**。为避免这种情况,你可以在刷新时指定 **需要刷新的属性**,这样只有指定的属性会被刷新(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`),其他属性不受影响。 - -```js -const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo - .fetch({ - keys: "priority, location", - }) - .then((todo) => { - // 只有 priority 和 location 会被获取和刷新 - }); -``` - -### 更新对象 - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `save` 方法。例如: - -```js -const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.set("content", "这周周会改到周三下午三点。"); -todo.save(); -``` - -云服务会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。 - -如果想要查看哪些属性有尚未保存的修改,可以调用 `dirtyKeys` 方法: - -```js -todo.dirtyKeys(); // ['content'] -``` - -如果希望撤销尚未保存的修改,可以调用 `revert` 方法。直接调用 `revert()` 将撤销所有尚未保存的修改。还可以额外传入 `keys` 数组作为参数,指定需要撤销的属性,例如: - -```js -todo.revert(["content"]); -``` - -#### 有条件更新对象 - -通过传入 `query` 选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回 `305` 错误。 - -例如,用户的账户表 `Account` 有一个余额字段 `balance`,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求: - -```js -const account = AV.Object.createWithoutData( - "Account", - "5745557f71cfe40068c6abe0" -); -// 对 balance 原子减少 100 -const amount = -100; -account.increment("balance", amount); -account - .save(null, { - // 设置条件 - query: new AV.Query("Account").greaterThanOrEqualTo("balance", -amount), - // 操作结束后,返回最新数据。 - // 如果是新对象,则所有属性都会被返回, - // 否则只有更新的属性会被返回。 - fetchWhenSave: true, - }) - .then( - (account) => { - console.log(`当前余额为:${account.get("balance")}`); - }, - (error) => { - if (error.code === 305) { - console.error("余额不足,操作失败!"); - } - } - ); -``` - -**`query` 选项只对已存在的对象有效**,不适用于尚未存入云端的对象。 - -`query` 选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过 `AV.Query` 查询 `AV.Object` 再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。 - -#### 更新计数器 - -设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 **原子操作** 来增加或减少一个属性内保存的数字: - -```js -post.increment("likes", 1); -``` - -可以指定需要增加或减少的值。若未指定,则默认使用 `1`。 - -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,需要原子增减的字段建议使用整数以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 - -#### 更新数组 - -更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据: - -- `AV.Object.add('arrayKey', value)` 将指定对象附加到数组末尾。 -- `AV.Object.addUnique('arrayKey', value)` 如果数组中不包含指定对象,则将该对象加入数组。对象的插入位置是随机的。 -- `AV.Object.remove('arrayKey', value)` 从数组字段中删除指定对象的所有实例。 - -例如,`Todo` 用一个 `alarms` 属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性: - -```js -const alarm1 = new Date("2018-04-30T07:10:00"); -const alarm2 = new Date("2018-04-30T07:20:00"); -const alarm3 = new Date("2018-04-30T07:30:00"); - -const alarms = [alarm1, alarm2, alarm3]; - -const todo = new AV.Object("Todo"); -todo.addUnique("alarms", alarms); -todo.save(); -``` - -### 删除对象 - -下面的代码从云端删除一个 `Todo` 对象: - -```js -const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.destroy(); -``` - -如果只需删除对象的一个属性,可以用 `unset`: - -```js -const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b"); - -// priority 属性会被删除 -todo.unset("priority"); - -// 保存对象 -todo.save(); -``` - -注意,删除对象是一个较为敏感的操作,我们建议你阅读[ACL 权限管理开发指南](/sdk/storage/guide/acl/) 来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。 - -### 批量操作 - -```js -// 创建一个保存所有 AV.Object 的数组 -const object1 = new AV.Object("Todo"); -object1.set("content", "更新文档"); -const object2 = new AV.Object("Todo"); -object2.set("content", "回复论坛帖子"); -const objects = [object1, object2]; - -// 批量构建和更新 -AV.Object.saveAll(objects).then( - function (savedObjects) { - // 成功保存所有对象时进入此 resolve 函数,savedObjects 是包含所有 AV.Object 的数组 - }, - function (error) { - // 只要有一个对象保存错误就会进入此 reject 函数 - } -); - -// 批量删除 -AV.Object.destroyAll(objects).then( - function (deletedObjects) { - // 成功删除所有对象时进入此 resolve 函数,deletedObjects 是包含所有的 AV.Object 的数组 - }, - function (error) { - // 只要有一个对象删除错误就会进入此 reject 函数 - } -); - -// 批量同步 -AV.Object.fetchAll(objects).then( - function (fetchedObjects) { - // 成功同步所有对象时进入此 resolve 函数,fetchedObjects 是包含所有的 AV.Object 的数组 - }, - function (error) { - // 只要有一个对象同步错误就会进入此 reject 函数 - } -); -``` - -下面的代码将所有 `Todo` 的 `isComplete` 设为 `true`: - -```js -const query = new AV.Query("Todo"); -query.find().then((todos) => { - // 获取需要更新的 todo - todos.forEach((todo) => { - // 更新属性值 - todo.set("isComplete", true); - }); - // 批量更新 - AV.Object.saveAll(todos); -}); -``` - -虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。 - -### 数据模型 - -对象之间可以产生关联。拿一个博客应用来说,一个 `Post` 对象可以与许多个 `Comment` 对象产生关联。云服务支持三种关系:一对一、一对多、多对多。 - -#### 一对一、一对多关系 - -一对一、一对多关系可以通过将 `AV.Object` 保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个 `Comment` 指向一个 `Post`。 - -下面的代码会创建一个含有单个 `Comment` 的 `Post`: - -```js -// 创建 post -const post = new AV.Object("Post"); -post.set("title", "饿了……"); -post.set("content", "中午去哪吃呢?"); - -// 创建 comment -const comment = new AV.Object("Comment"); -comment.set("content", "当然是肯德基啦!"); - -// 将 post 设为 comment 的一个属性值 -comment.set("parent", post); - -// 保存 comment 会同时保存 post -comment.save(); -``` - -云端存储时,会将被指向的对象用 `Pointer` 的形式存起来。你也可以用 `objectId` 来指向一个对象: - -```js -const post = AV.Object.createWithoutData("Post", "57328ca079bc44005c2472d0"); -comment.set("post", post); -``` - -请参阅 [关系查询](#关系查询) 来了解如何获取关联的对象。 - -#### 多对多关系 - -想要建立多对多关系,最简单的办法就是使用 **数组**。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 **中间表** 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。 - -我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。 - -### 序列化和反序列化 - -在实际的开发中,把 `AV.Object` 当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此 `AV.Object` 也提供了序列化和反序列化的方法。 - -序列化: - -```js -const todo = new AV.Object("Todo"); // 构建对象 -todo.set("title", "马拉松报名"); // 设置名称 -todo.set("priority", 2); // 设置优先级 -todo.set("owner", AV.User.current()); // 这里就是一个 Pointer 类型,指向当前登录的用户 - -// 将 AV.Object 对象序列化成 JSON 对象 -const json = todo.toFullJSON(); -// 将 JSON 对象序列化为字符串 -const serializedString = JSON.stringify(json); -``` - -`AV.Object` 还提供了另一个方法 `toJSON()`。它们的区别是 `toJSON()` 得到的对象仅包含对象的 payload,一般用于展示,而 `toFullJSON()` 得到的对象包含了元数据,一般用于传输。在使用时请注意区分。 - -反序列化: - -```js -// 将字符串反序列化为 JSON 对象 -const json = JSON.parse(serializedString); -// 将 JSON 对象反序列化成 AV.Object 对象 -const todo = AV.parseJSON(json); -``` - -## 查询 - -我们已经了解到如何从云端获取单个 `AV.Object`,但你可能还会有一次性获取多个符合特定条件的 `AV.Object` 的需求,这时候就需要用到 `AV.Query` 了。 - -### 基础查询 - -执行一次基础查询通常包括这些步骤: - -1. 构建 `AV.Query`; -2. 向其添加查询条件; -3. 执行查询并获取包含满足条件的对象的数组。 - -下面的代码获取所有 `lastName` 为 `Smith` 的 `Student`: - -```js -const query = new AV.Query("Student"); -query.equalTo("lastName", "Smith"); -query.find().then((students) => { - // students 是包含满足条件的 Student 对象的数组 -}); -``` - -### 查询条件 - -可以给 `AV.Object` 添加不同的条件来改变获取到的结果。 - -下面的代码查询所有 `firstName` 不为 `Jack` 的对象: - -```js -query.notEqualTo("firstName", "Jack"); -``` - -对于能够排序的属性(比如数字、字符串),可以进行比较查询: - -```js -// 限制 age < 18 -query.lessThan("age", 18); - -// 限制 age <= 18 -query.lessThanOrEqualTo("age", 18); - -// 限制 age > 18 -query.greaterThan("age", 18); - -// 限制 age >= 18 -query.greaterThanOrEqualTo("age", 18); -``` - -可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是 `AND` 的关系: - -```js -query.equalTo("firstName", "Jack"); -query.greaterThan("age", 18); -``` - -可以通过指定 `limit` 限制返回结果的数量(默认为 `100`): - -```js -// 最多获取 10 条结果 -query.limit(10); -``` - -由于性能原因,`limit` 最大只能设为 `1000`。即使将其设为大于 `1000` 的数,云端也只会返回 1,000 条结果。 - -如果只需要一条结果,可以直接用 `first`: - -```js -const query = new AV.Query("Todo"); -query.equalTo("priority", 2); -query.first().then((todo) => { - // todo 是第一个满足条件的 Todo 对象 -}); -``` - -可以通过设置 `skip` 来跳过一定数量的结果: - -```js -// 跳过前 20 条结果 -query.skip(20); -``` - -把 `skip` 和 `limit` 结合起来,就能实现翻页功能: - -```js -const query = new AV.Query("Todo"); -query.equalTo("priority", 2); -query.limit(10); -query.skip(20); -``` - -需要注意的是,`skip` 的值越高,查询所需的时间就越长。作为替代方案,可以通过设置 `createdAt` 或 `updatedAt` 的范围来实现更高效的翻页,因为它们都自带索引。 -同理,也可以通过设置自增字段的范围来实现翻页。 - -对于能够排序的属性,可以指定结果的排序规则: - -```js -// 按 createdAt 升序排列 -query.ascending("createdAt"); - -// 按 createdAt 降序排列 -query.descending("createdAt"); -``` - -还可以为同一个查询添加多个排序规则: - -```js -query.addAscending("priority"); -query.addDescending("createdAt"); -``` - -下面的代码可用于查找包含或不包含某一属性的对象: - -```js -// 查找包含 'images' 的对象 -query.exists("images"); - -// 查找不包含 'images' 的对象 -query.doesNotExist("images"); -``` - -可以用 `matchesKeyInQuery` 查找某一属性值为另一查询返回结果的对象。 - -比如说,你有一个用于存储国家和语言对应关系的 `Country` class,还有一个用于存储学生国籍的 `Student` class: - -| name | language | -| ----- | -------- | -| US | English | -| UK | English | -| China | Chinese | - -| fullName | nationality | -| ---------- | ----------- | -| John Doe | US | -| Tom Sawyer | UK | -| Ming Li | China | - -下面的代码可以找到所有来自英语国家的学生: - -```js -const studentQuery = new AV.Query("Student"); -const countryQuery = new AV.Query("Country"); -// 获取所有的英语国家 -countryQuery.equalTo("language", "English"); -// 把 Student 的 nationality 和 Country 的 name 关联起来 -studentQuery.matchesKeyInQuery("nationality", "name", countryQuery); -studentQuery.find().then((students) => { - // students 包含 John Doe 和 Tom Sawyer -}); -``` - -可以通过 `select` 指定需要返回的属性。下面的代码只获取每个对象的 `title` 和 `content`(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`): - -```js -const query = new AV.Query("Todo"); -query.select(["title", "content"]); -query.first().then((todo) => { - const title = todo.get("title"); // √ - const content = todo.get("content"); // √ - const notes = todo.get("notes"); // undefined -}); -``` - -`select` 支持点号(`author.firstName`),详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)。 - -另外,字段名前添加减号前缀表示反向选择,例如 `-author` 表示不返回 `author` 字段。 -反向选择同样适用于内置字段,比如 `-objectId`,也可以和点号组合使用,比如 `-pubUser.createdAt`。 - -对于未获取的属性,可以通过对结果中的对象进行 `fetch` 操作来获取。参见 [同步对象](#同步对象)。 - -### 字符串查询 - -可以用 `startsWith` 来查找某一属性值以特定字符串开头的对象。和 SQL 中的 `LIKE` 一样,你可以利用索引带来的优势: - -```js -const query = new AV.Query("Todo"); -// 相当于 SQL 中的 title LIKE 'lunch%' -query.startsWith("title", "lunch"); -``` - -可以用 `contains` 来查找某一属性值包含特定字符串的对象: - -```js -const query = new AV.Query("Todo"); -// 相当于 SQL 中的 title LIKE '%lunch%' -query.contains("title", "lunch"); -``` - -和 `startsWith` 不同,`contains` 无法利用索引,因此不建议用于大型数据集。 - -注意 `startsWith` 和 `contains` 都是 **区分大小写** 的,所以上述查询会忽略 `Lunch`、`LUNCH` 等字符串。 - -如果想查找某一属性值不包含特定字符串的对象,可以使用 `matches` 进行基于正则表达式的查询: - -```js -const query = new AV.Query("Todo"); -// 'title' 不包含 'ticket'(不区分大小写) -const regExp = new RegExp("^((?!ticket).)*$", "i"); -query.matches("title", regExp); -``` - -不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class, -因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以使用全文搜索服务。 - -使用查询时如果遇到性能问题,可参阅 [查询性能优化](#查询性能优化)。 - -### 数组查询 - -下面的代码查找所有数组属性 `tags` 包含 `工作` 的对象: - -```js -query.equalTo("tags", "工作"); -``` - -下面的代码查询数组属性长度为 3(正好包含 3 个标签)的对象: - -```js -query.sizeEqualTo("tags", 3); -``` - -下面的代码查找所有数组属性 `tags` **同时包含** `工作`、`销售` 和 `会议` 的对象: - -```js -query.containsAll("tags", ["工作", "销售", "会议"]); -``` - -如需获取某一属性值包含一列值中任意一个值的对象,可以直接用 `containedIn` 而无需执行多次查询。下面的代码构建的查询会查找所有 `priority` 为 `1` **或** `2` 的 todo 对象: - -```js -// 单个查询 -const priorityOneOrTwo = new AV.Query("Todo"); -priorityOneOrTwo.containedIn("priority", [1, 2]); -// 这样就可以了 :) - -// --------------- -// vs. -// --------------- - -// 多个查询 -const priorityOne = new AV.Query("Todo"); -priorityOne.equalTo("priority", 1); - -const priorityTwo = new AV.Query("Todo"); -priorityTwo.equalTo("priority", 2); - -const priorityOneOrTwo = AV.Query.or(priorityOne, priorityTwo); -// 好像有些繁琐 :( -``` - -反过来,还可以用 `notContainedIn` 来获取某一属性值不包含一列值中任何一个的对象。 - -### 关系查询 - -查询关联数据有很多种方式,常见的一种是查询某一属性值为特定 `AV.Object` 的对象,这时可以像其他查询一样直接用 `equalTo`。比如说,如果每一条博客评论 `Comment` 都有一个 `post` 属性用来存放原文 `Post`,则可以用下面的方法获取所有与某一 `Post` 相关联的评论: - -```js -const post = AV.Object.createWithoutData("Post", "57328ca079bc44005c2472d0"); -const query = new AV.Query("Comment"); -query.equalTo("post", post); -query.find().then((comments) => { - // comments 包含与 post 相关联的评论 -}); -``` - -如需获取某一属性值为另一查询结果中任一 `AV.Object` 的对象,可以用 `matchesQuery`。下面的代码构建的查询可以找到所有包含图片的博客文章的评论: - -```js -const innerQuery = new AV.Query("Post"); -innerQuery.exists("image"); - -const query = new AV.Query("Comment"); -query.matchesQuery("post", innerQuery); -``` - -如需获取某一属性值不是另一查询结果中任一 `AV.Object` 的对象,则使用 `doesNotMatchQuery`。 - -有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用 `include`。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章: - -```js -const query = new AV.Query("Comment"); - -// 获取最新发布的 -query.descending("createdAt"); - -// 只获取 10 条 -query.limit(10); - -// 同时包含博客文章 -query.include("post"); - -query.find().then((comments) => { - // comments 包含最新发布的 10 条评论,包含各自对应的博客文章 - comments.forEach((comment) => { - // 该操作无需网络连接 - const post = comment.get("post"); - }); -}); -``` - -可以用 dot 符号(`.`)来获取多级关系,例如 `post.author`,详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)的《在查询对象时使用点号》一节。 - -可以在同一查询上应用多次 `include` 以包含多个属性。 - -通过 `include` 进行多级查询的方式不适用于数组属性内部的 `AV.Object`,只能包含到数组本身。 - -#### 关系查询的注意事项 - -云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌 / 子查询(和普通查询一样,`limit` 默认为 `100`,最大 `1000`),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出 `limit`,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有 `limit` 数量以内的结果会被填入主查询。 - -我们建议采用以下方案进行改进: - -- 确保子查询的结果在 100 条以下,如果在 100 至 1,000 条之间的话请将子查询的 `limit` 设为 `1000`。 -- 将需要查询的字段冗余到主查询所在的表上。 -- 进行多次查询,每次在子查询上设置不同的 `skip` 值来遍历所有记录(注意 `skip` 的值较大时可能会引发性能问题,因此不是很推荐)。 - -### 统计总数量 - -如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用 `count` 来代替 `find`。比如说,查询有多少个已完成的 todo: - -```js -const query = new AV.Query("Todo"); -query.equalTo("isComplete", true); -query.count().then((count) => { - console.log(`${count} 个 todo 已完成。`); -}); -``` - -### 组合查询 - -组合查询就是把诸多查询条件用一定逻辑合并到一起(`OR` 或 `AND`)再交给云端去查询。 - -组合查询不支持在子查询中包含 `GeoPoint` 或其他非过滤性的限制(例如 `near`、`withinGeoBox`、`limit`、`skip`、`ascending`、`descending`、`include`)。 - -#### OR 查询 - -OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于 `3` 或者已经完成了的 todo: - -```js -const priorityQuery = new AV.Query("Todo"); -priorityQuery.greaterThanOrEqualTo("priority", 3); - -const isCompleteQuery = new AV.Query("Todo"); -isCompleteQuery.equalTo("isComplete", true); - -const query = AV.Query.or(priorityQuery, isCompleteQuery); -``` - -使用 OR 查询时,子查询中不能包含 `GeoPoint` 相关的查询。 - -#### AND 查询 - -使用 AND 查询的效果等同于往 `AV.Query` 添加多个条件。下面的代码构建的查询会查找创建时间在 `2016-11-13` 和 `2016-12-02` 之间的 todo: - -```js -const startDateQuery = new AV.Query("Todo"); -startDateQuery.greaterThanOrEqualTo( - "createdAt", - new Date("2016-11-13 00:00:00") -); - -const endDateQuery = new AV.Query("Todo"); -endDateQuery.lessThan("createdAt", new Date("2016-12-03 00:00:00")); - -const query = AV.Query.and(startDateQuery, endDateQuery); -``` - -单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询: - -```js -const createdAtQuery = new AV.Query("Todo"); -createdAtQuery.greaterThanOrEqualTo("createdAt", new Date("2018-04-30")); -createdAtQuery.lessThan("createdAt", new Date("2018-05-01")); - -const locationQuery = new AV.Query("Todo"); -locationQuery.doesNotExist("location"); - -const priority2Query = new AV.Query("Todo"); -priority2Query.equalTo("priority", 2); - -const priority3Query = new AV.Query("Todo"); -priority3Query.equalTo("priority", 3); - -const priorityQuery = AV.Query.or(priority2Query, priority3Query); -const timeLocationQuery = AV.Query.or(locationQuery, createdAtQuery); -const query = AV.Query.and(priorityQuery, timeLocationQuery); -``` - -### 查询性能优化 - -影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。 - -- 不等于和不包含查询(无法使用索引) -- 通配符在前面的字符串查询(无法使用索引) -- 有条件的 `count`(需要扫描所有数据) -- `skip` 跳过较多的行数(相当于需要先查出被跳过的那些行) -- 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引) -- 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据) - - - -## LiveQuery - -LiveQuery 衍生于 [`AV.Query`](#查询),并为其带来了更强大的功能。它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。 - -设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用 `AV.Query` 并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。 - -想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的 `AV.Query`。订阅成功后,一旦有符合 `AV.Query` 的 `AV.Object` 发生变化,云端就会主动、实时地将信息通知到客户端。 - -LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。 - -### 启用 LiveQuery - -进入 ** > 服务设置**,在 **安全设置** 里面勾选 **启用 LiveQuery**,然后将下面的 npm 模块添加到项目中即可: - -```js -// 无需加载 leancloud-storage -const AV = require("leancloud-storage/live-query"); -``` - -或者使用 `script` 标签: - - -{` -`} - - -### Demo - -下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果: - - - -使用我们的「LeanTodo」微信小程序和网页应用,可以实际体验以上视频所演示的效果,步骤如下: - -1. 微信扫码,添加小程序「LeanTodo」; - - ![LeanTodo mini program](/img/leantodo-weapp-qr.jpg) - -2. 进入小程序,点击首页左下角 **设置** > **账户设置**,输入便于记忆的用户名和密码; - -3. 使用浏览器访问 ,输入刚刚在小程序中更新好的账户信息,点击 **Login**; - -4. 随意添加更改数据,查看两端的同步状态。 - -注意按以上顺序操作。在网页应用中使用 **Signup** 注册的账户无法与小程序创建的账户相关联,所以如果颠倒以上操作顺序,则无法观测到数据同步效果。 - -[LiveQuery 公开课](http://www.bilibili.com/video/av11291992/) 涵盖了许多开发者关心的问题和解答。 - -### 构建订阅 - -首先创建一个普通的 `AV.Query` 对象,添加查询条件(如有),然后进行订阅操作: - -```js -const query = new AV.Query("Todo"); -query.subscribe().then((liveQuery) => { - // 订阅成功 -}); -``` - -LiveQuery 不支持内嵌查询,也不支持返回指定属性。 - -订阅成功后,就可以接收到和 `AV.Object` 相关的更新了。假如在另一个客户端上创建了一个 `Todo` 对象,对象的 `title` 设为 `更新作品集`,那么下面的代码可以获取到这个新的 `Todo`: - -```js -const query = new AV.Query("Todo"); -query.subscribe().then((liveQuery) => { - liveQuery.on("create", (newTodo) => { - console.log(newTodo.get("title")); // 更新作品集 - }); -}); -``` - -此时如果有人把 `Todo` 的 `content` 改为 `把我最近画的插画放上去`,那么下面的代码可以获取到本次更新: - -```js -liveQuery.on("update", (updatedTodo, updatedKeys) => { - console.log(updatedTodo.get("content")); // 把我最近画的插画放上去 -}); -``` - -### 事件处理 - -订阅成功后,可以选择监听如下几种数据变化: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` 事件 - -当有新的满足 `AV.Query` 查询条件的 `AV.Object` 被创建时,`create` 事件会被触发。下面的 `object` 就是新建的 `AV.Object`: - -```js -liveQuery.on("create", (object) => { - console.log("对象被创建。"); -}); -``` - -#### `update` 事件 - -当有满足 `AV.Query` 查询条件的 `AV.Object` 被更新时,`update` 事件会被触发。下面的 `object` 就是有更新的 `AV.Object`: - -```js -liveQuery.on("update", (object, updatedKeys) => { - console.log("对象被更新。"); -}); -``` - -#### `enter` 事件 - -当一个已存在的、原本不符合 `AV.Query` 查询条件的 `AV.Object` 发生更新,且更新后符合查询条件,`enter` 事件会被触发。下面的 `object` 就是进入 `AV.Query` 的 `AV.Object`,其内容为该对象最新的值: - -```js -liveQuery.on("enter", (object, updatedKeys) => { - console.log("对象进入。"); -}); -``` - -注意区分 `create` 和 `enter` 的不同行为。如果一个对象已经存在,在更新之前不符合查询条件,而在更新之后符合查询条件,那么 `enter` 事件会被触发。如果一个对象原本不存在,后来被构建了出来,那么 `create` 事件会被触发。 - -#### `leave` 事件 - -当一个已存在的、原本符合 `AV.Query` 查询条件的 `AV.Object` 发生更新,且更新后不符合查询条件,`leave` 事件会被触发。下面的 `object` 就是离开 `AV.Query` 的 `AV.Object`,其内容为该对象最新的值: - -```js -liveQuery.on("leave", (object, updatedKeys) => { - console.log("对象离开。"); -}); -``` - -#### `delete` 事件 - -当一个已存在的、原本符合 `AV.Query` 查询条件的 `AV.Object` 被删除,`delete` 事件会被触发。下面的 `object` 就是被删除的 `AV.Object` 的 `objectId`: - -```js -liveQuery.on("delete", (object) => { - console.log("对象被删除。"); -}); -``` - -### 取消订阅 - -如果不再需要接收有关 `AV.Query` 的更新,可以取消订阅。 - -```js -liveQuery.unsubscribe().then(() => { - // 成功取消订阅 -}); -``` - -### 断开连接 - -断开连接有几种情况: - -1. 网络异常或者网络切换,非预期性断开。 -2. 退出应用、关机或者打开飞行模式等,用户在应用外的操作导致断开。 - -如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。 - -而另外一种极端情况——**当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下**,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。 - -### LiveQuery 的注意事项 - -因为 LiveQuery 的实时性,很多用户会陷入一个误区,试着用 LiveQuery 来实现一个简单的聊天功能。 -我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,后期维护的难度非常大(聊天记录、对话维护之类的代码会很混乱),并且云服务已经提供了即时通讯的服务。 -LiveQuery 的核心还是提供一个针对查询的推拉结合的用法,脱离设计初衷容易造成前端的模块混乱。 - - - -## 文件 - -有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用 `AV.Object` 保存,此时文件对象 `AV.File` 便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。 - -### 构建文件 - -可以通过 base64 编码的字符串构建文件: - -```js -const data = { base64: "TGVhbkNsb3Vk" }; -// resume.txt 是文件名 -const file = new AV.File("resume.txt", data); -``` - -还可以通过字节数组构建文件: - -```js -const data = [0x4c, 0x65, 0x61, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64]; -const file = new AV.File("resume.txt", data); -``` - -此外,浏览器环境下还可以通过 Blob 构建文件,Node.js 环境下还可以通过 Buffer、Stream 构建文件。 - -React Native 环境下,可以通过本地路径构建文件: - -```js -const data = { blob: { uri: localFileUri } }; -const file = new AV.File("resume.txt", data); -``` - -除此之外,还可以通过 URL 构建文件: - -```js -const file = AV.File.withURL( - "logo.png", - "https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png" -); -``` - -通过 URL 构建文件时,SDK 并不会将原本的文件转储到云端,而是会将文件的物理地址存储为字符串,这样也就不会产生任何文件上传流量。使用其他方式构建的文件会被保存在云端。 - -云端会根据文件扩展名自动检测文件类型。如果需要的话,也可以手动指定 `Content-Type`(一般称为 MIME 类型): - -```js -const file = new AV.File("resume.txt", data, "application/json"); -``` - -注意:目前微信小程序环境下不支持指定文件类型。 - -与前面提到的方式相比,一个更常见的文件构建方式是从本地路径上传。 - -```html - -``` - -然后在一个点击事件处理函数中获取这个文件: - -```js -const avatarUpload = document.getElementById("avatar-upload"); -if (avatarUpload.files.length) { - const localFile = avatarUpload.files[0]; - const file = new AV.File("avatar.jpg", localFile); -} -``` - -这里上传的文件名字叫做 `avatar.jpg`。需要注意: - -- 每个文件会被分配到一个独一无二的 `objectId`,所以在一个应用内是允许多个文件重名的。 -- 文件必须有扩展名才能被云端正确地识别出类型。比如说要用 `AV.File` 保存一个 PNG 格式的图像,那么扩展名应为 `.png`。 -- 如果文件没有扩展名,且没有手动指定类型,那么云服务将默认使用 `application/octet-stream`。 - -如果希望指定文件上传后在云端的路径,可以设置文件的 `key` 属性。 -例如,上传 robots.txt 文件,限制搜索引擎抓取自定义文件域名下的 URL: - -```js -file.save({ key: "robots.txt" }); -``` - -### 保存文件 - -将文件保存到云端后,便可获得一个永久指向该文件的 URL: - -```js -file.save().then( - (file) => { - console.log(`文件保存完成。URL:${file.url}`); - }, - (error) => { - // 保存失败,可能是文件无法被读取,或者上传过程中出现问题 - } -); -``` - -```js -const data = [0x4c, 0x65, 0x61, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64]; -const file = new AV.File("resume.txt", data); -file.save({ keepFileName: true }).then( - (file) => { - console.log(file.url); // https://your-file-domain/5112b94e0536e995741c/resume.txt - }, - (error) => { - // 保存失败,可能是文件无法被读取,或者上传过程中出现问题 - } -); -``` - -文件上传后,可以在 ** > 文件** 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的 `objectId` 和 URL。 - -已经保存到云端的文件可以关联到 `AV.Object`: - -```js -const Todo = AV.Object.extend("Todo"); -const todo = new Todo(); -todo.set("title", "买蛋糕"); -// attachments 是一个 Array 属性 -todo.add("attachments", file); -todo.save(); -``` - -也可以通过构建 `AV.Query` 进行[查询](#查询): - -```js -const query = new AV.Query("_File"); -``` - -需要注意的是,内部文件(上传到文件服务的文件)的 `url` 字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 -因此,通过 `url` 字段查询文件仅适用于外部文件(直接保存外部 URL 到文件服务创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。 - -注意,如果文件被保存到了 `AV.Object` 的一个数组属性中,那么在查询 `AV.Object` 时如果需要包含文件,则要用到 `AV.Query` 的 `include` 方法。比如说,在获取所有标题为 `买蛋糕` 的 todo 的同时获取附件中的文件: - -```js -// 获取同一标题且包含附件的 todo -const query = new AV.Query("Todo"); -query.equalTo("title", "买蛋糕"); -query.exists("attachments"); - -// 同时获取附件中的文件 -query.include("attachments"); - -query.find().then((todos) => { - todos.forEach((todo) => { - // 获取每个 todo 的 attachments 数组 - const attachments = todo.get("attachments"); - attachments.forEach((attachment) => { - // 每个附件都是一个 AV.File 实例 - console.log(`附件 URL:${attachment.get("url")}`); - }); - }); -}); -``` - -### 上传进度监听 - -上传过程中可以实时向用户展示进度: - -```js -file - .save({ - onprogress: (progress) => { - console.log(progress); - // { - // loaded: 1024, - // total: 2048, - // percent: 50 - // } - }, - }) - .then((file) => { - // 保存后的操作 - }); -``` - -### 文件元数据 - -上传文件时,可以用 `metaData` 添加额外的属性。文件一旦保存,`metaData` 便不可再修改。 - -```js -// 设置元数据 -file.metaData("author", "LeanCloud"); -file.save().then((file) => { - // 获取全部元数据 - const metadata = file.metaData(); - // 获取 author 属性 - const author = file.metaData("author"); - // 获取文件名 - const fileName = file.get("name"); - // 获取大小(不适用于通过 base64 编码的字符串或者 URL 保存的文件) - const size = file.size(); -}); -``` - -### 图像缩略图 - -成功保存图像后,除了可以获取指向该文件的 URL 外,还可以获取图像的缩略图 URL,并且可以指定缩略图的宽度和高度: - -```js -// 获得宽度为 100 像素,高度 200 像素的缩略图 -const url = file.thumbnailURL(100, 200); -``` - -图片最大不超过 **20 MB** 才可以获取缩略图。 - -国际版不支持图片缩略图。 - -### 删除文件 - -下面的代码从云端删除一个文件: - -```js -const file = AV.File.createWithoutData("552e0a27e4b0643b709e891e"); -file.destroy(); -``` - -默认情况下,文件的删除权限是关闭的。 -如需删除文件,一般建议在服务端使用 Master Key 调用 REST API 删除。 -取决于产品的具体需求,也可以考虑在 ** > 文件 > 权限** 向特定用户或特定角色开启删除权限。 - -### 文件审核 - -当前**文件审核**功能支持检测**图片**文件。 - -你可以在 **数据存储 > 文件 > 文件审核** 标签下勾选「自动审核新上传图片」,还可以批量审核指定时间范围内的图片,图片审核结果将在 **文件管理** 标签页展示。 - -如果你需要人工二次审核,可以点击每一行记录,在文件详情中选择「通过」或「封禁」。 - -## Promise - -每一个在 JavaScript SDK 中的异步方法都会返回一个 `Promise`,可以用于处理该异步方法的完成与异常。下面的示例代码在查询到一个 `AV.Object` 后对其进行更新: - -```js -const query = new AV.Query("Todo"); -query.equalTo("priority", 1); -// find 方法是一个异步方法,会返回一个 Promise,之后可以使用 then 方法 -query - .find() - .then((todos) => { - // 返回符合条件的对象组成的数组 - const todo = todos[0]; - todo.set("notes", "今天需要完成。"); - // save 方法也是一个异步方法,会返回一个 Promise,所以在此处,你可以直接 return 出去,后续操作就可以支持链式 Promise 调用 - return todo.save(); - }) - .then(() => { - // 这里是 save 方法返回的 Promise - console.log("成功更新 todo。"); - }) - .catch((error) => { - // catch 方法写在 Promise 链式的最后,可以捕捉到全部 error - console.error(error); - }); -``` - -### `then` 方法 - -每一个 `Promise` 都有一个叫 `then` 的方法,这个方法接受一对 callback。第一个 callback 在 `Promise` 被解决(`resolved`,也就是正常运行)的时候调用,第二个会在 `Promise` 被拒绝(`rejected`,也就是遇到错误)的时候调用: - -```js -todo.save().then( - (todo) => { - console.log("成功更新 todo。"); - }, - (error) => { - console.error(error); - } -); -``` - -其中第二个参数是可选的。 - -你还可以使用 `catch` 方法,将逻辑写成: - -```js -todo - .save() - .then((todo) => { - console.log("成功更新 todo。"); - }) - .catch((error) => { - console.error(error); - }); -``` - -### 将 `Promise` 组织在一起 - -Promise 比较神奇,可以代替多层嵌套方式来解决发送异步请求代码的调用顺序问题。如果一个 `Promise` 的回调会返回一个 `Promise`,那么第二个 `then` 里的 callback 在第一个 `then` 的 callback 没有解决前是不会解决的,也就是所谓 **Promise Chain**。 - -```js -// 将内容按章节顺序添加到页面上 -const chapterIds = [ - "584e1c408e450a006c676162", // 第一章 - "584e1c43128fe10058b01cf5", // 第二章 - "581aff915bbb500059ca8d0b", // 第三章 -]; - -new AV.Query("Chapter") - .get(chapterIds[0]) - .then((chapterOne) => { - // 向页面添加内容 - addHtmlToPage(chapterOne.get("content")); - // 返回新的 Promise - return new AV.Query("Chapter").get(chapterIds[1]); - }) - .then((chapterTwo) => { - addHtmlToPage(chapterTwo.get("content")); - return new AV.Query("Chapter").get(chapterIds[2]); - }) - .then((chapterThree) => { - addHtmlToPage(chapterThree.get("content")); - // 完成 - }); -``` - -### 错误处理 - -如果任意一个在链中的 `Promise` 抛出一个异常的话,所有接下来可能成功的 callback 都会被跳过直到遇到一个处理错误的 callback。 - -通常来说,在正常情况的回调函数链的末尾,加一个错误处理的回调函数,是一种很常见的做法。 - -利用 `catch` 方法可以将上述代码改写为: - -```js -new AV.Query("Chapter") - .get(chapterIds[0]) - .then((chapterOne) => { - addHtmlToPage(chapterOne.get("content")); - // 强制失败 - throw new Error("出错啦"); - return new AV.Query("Chapter").get(chapterIds[1]); - }) - .then((chapterTwo) => { - // 这里的代码将被忽略 - addHtmlToPage(chapterTwo.get("content")); - return new AV.Query("Chapter").get(chapterIds[2]); - }) - .then((chapterThree) => { - // 这里的代码将被忽略 - addHtmlToPage(chapterThree.get("content")); - }) - .catch((error) => { - // 这个错误处理函数将被调用,错误信息是 '出错啦' - console.error(error.message); - }); -``` - -### `async` 和 `await` - -`async` 和 `await` 能让你以更接近同步代码的方式使用 Promise: - -```js -async function example() { - try { - const query = new AV.Query("Todo"); - query.equalTo("priority", 1); - const todos = await query.find(); - const todo = todos[0]; - todo.set("notes", "今天需要完成。"); - return await todo.save(); - } catch (error) { - console.error(error); - } -} -``` - -如果你想更深入地了解和学习 Promise,包括如何对并行的异步操作进行控制,我们推荐阅读 [JavaScript Promise 迷你书(中文版)](http://liubin.github.io/promises-book/)这本书。 - -## GeoPoint - -云服务允许你通过将 `AV.GeoPoint` 关联到 `AV.Object` 的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。 - -要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个 `AV.GeoPoint` 并将其纬度(`latitude`)设为 `39.9`,经度(`longitude`)设为 `116.4`: - -```js -const point = new AV.GeoPoint(39.9, 116.4); - -// 其他构建 AV.GeoPoint 的方式 -const point = new AV.GeoPoint([39.9, 116.4]); -const point = new AV.GeoPoint({ latitude: 39.9, longitude: 116.4 }); -``` - -现在可以将这个地理位置存储为一个对象的属性: - -```js -todo.set("location", point); -``` - -### 地理位置查询 - -给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 `AV.Query` 添加 `near` 条件。下面的代码查找 `location` 属性值离某一点最近的 `Todo` 对象: - -```js -const query = new AV.Query("Todo"); -const point = new AV.GeoPoint(39.9, 116.4); -query.near("location", point); - -// 限制为 10 条结果 -query.limit(10); -query.find().then((todos) => { - // todos 是包含满足条件的 Todo 对象的数组 -}); -``` - -像 `ascending` 和 `descending` 这样额外的排序条件会获得比默认的距离排序更高的优先级。 - -若要限制结果和给定地点之间的距离,可以参考 API 文档中的 `withinKilometers`、`withinMiles` 和 `withinRadians` 参数。 - -若要查询在某一矩形范围内的对象,可以用 `withinGeoBox`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```js -const query = new AV.Query("Todo"); -const southwest = new AV.GeoPoint(30, 115); -const northeast = new AV.GeoPoint(40, 118); -query.withinGeoBox("location", southwest, northeast); -``` - -### GeoPoint 的注意事项 - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -## 用户 - -请参阅[内建账户指南](/sdk/authentication/guide/)。 - -## 角色 - -随着用户量的增长,你可能会发现相比于为每一名用户单独设置权限,将预先设定好的权限直接分配给一部分用户是更好的选择。为了迎合这种需求,云服务支持基于角色的权限管理。请参阅[ACL 权限管理开发指南](/sdk/storage/guide/acl/)。 - -## 全文搜索 - -全文搜索是一个针对应用数据进行全局搜索的接口,它基于搜索引擎构建,提供更强大的搜索功能。要深入了解其用法和阅读示例代码,请阅读[全文搜索指南](/sdk/storage/guide/fulltext-search/)。 - -## WebView - -JS SDK 支持在各种 WebView 中使用,包括 PhoneGap、Cordova、微信 WebView 等。 - -### Android WebView - -如果是 Android WebView,在 Native 代码创建 WebView 的时候你需要打开几个选项,这些选项生成 WebView 的时候默认并不会被打开,需要配置: - -1. 因为我们 JS SDK 目前使用了 `window.localStorage`,所以你需要开启 WebView 的 `localStorage`: - - ```java - yourWebView.getSettings().setDomStorageEnabled(true); - ``` - -2. 如果你希望直接调试手机中的 WebView,也同样需要在生成 WebView 的时候设置远程调试,具体使用方式请参考 [Google 官方文档](https://developer.chrome.com/devtools/docs/remote-debugging)。 - - ```java - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - yourWebView.setWebContentsDebuggingEnabled(true); - } - ``` - - 注意:这种调试方式仅支持 Android 4.4 以上版本(含 4.4)。 - -3. 如果你是通过 WebView 来开发界面,Native 调用本地特性的 Hybrid 方式开发你的 App。比较推荐的开发方式是:通过 Chrome 的开发者工具开发界面部分,当界面部分完成,与 Native 再来做数据连调,这种时候才需要用 Remote debugger 方式在手机上直接调试 WebView。这样做会大大节省你开发调试的时间,不然如果界面都通过 Remote debugger 方式开发,可能效率较低。 - -4. 为了防止通过 JavaScript 反射调用 Java 代码访问 Android 文件系统的安全漏洞,在 Android 4.2 以后的系统中,WebView 中间只能访问通过 [`@JavascriptInterface`](http://developer.android.com/reference/android/webkit/JavascriptInterface.html) 标记过的方法。如果你的目标用户覆盖 4.2 以上的机型,请注意加上这个标记,以避免出现 `Uncaught TypeError`。 diff --git a/leancloud/docs/sdk/storage/js-guide/setup-js.mdx b/leancloud/docs/sdk/storage/js-guide/setup-js.mdx deleted file mode 100644 index ea4da166d..000000000 --- a/leancloud/docs/sdk/storage/js-guide/setup-js.mdx +++ /dev/null @@ -1,1431 +0,0 @@ ---- -title: 数据存储 JavaScript SDK 配置指南 -sidebar_label: JavaScript SDK 配置 -slug: /sdk/storage/guide/setup-js/ -sidebar_position: 1 ---- - -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import DomainBinding from "../../_partials/setup-domain.mdx"; -import AppConfig from "../_partials/app-config.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { CardGrid, Card } from "/src/docComponents/doc"; - -## 获取 SDK - -本指南按照应用的适用平台来介绍各自的安装与集成方式。 - - - - - - - - - - - - - - - - - -### Web - -适用于运行在浏览器、WebView 或其他应用内 HTML 平台上的应用。 - -#### 安装与引用 SDK - -##### npm - -如果你的 Web 应用使用了 webpack 等前端打包工具,我们推荐使用包管理工具 npm 安装 SDK: - - - - -``` -$ npm install leancloud-storage --save -``` - - - - -``` -$ npm install leancloud-storage --save -``` - - - - -``` -$ npm install leancloud-realtime --save -``` - - - - -``` -$ npm install leancloud-realtime leancloud-realtime-plugin-typed-messages leancloud-storage --save -``` - - - - -如果因为网络原因无法通过官方的 npm 站点下载,推荐通过 taobao 镜像来下载(在 `npm install` 后添加 `--registry=https://registry.npm.taobao.org` 参数)。 - -安装完成后,在代码中通过 `require` 获得 SDK 的引用: - - - - -```js -const AV = require("leancloud-storage"); -const { Query, User } = AV; -``` - - - - -```js -const AV = require("leancloud-storage/live-query"); -const { Query, User } = AV; -``` - - - - -```js -const { Realtime, TextMessage } = require("leancloud-realtime"); -``` - -为了保证兼容性,SDK 一直以来分发的都是 ECMAScript 5 版本的代码,并打包了所有需要的 Polyfills(比如 Promise)。 -即时通讯 SDK v5.0.0-rc.2 起同时提供以最新版本 ECMAScript 为编译目标的版本,该版本拥有更小的体积与更好的运行时优化,适用于只需要兼容最新版本浏览器的使用场景。 -如果应用使用了 `@babel/preset-env` 或类似方案,也可以在转译时 include 最新 ECMAScript 版本的 SDK,由应用来决定要兼容的目标运行环境。 -需要注意最新版本 ECMAScript 每年都会变,而该版本的目标即是提供与最新标准对齐的代码,因此由于引入了新版本 ECMASCript 特性导致不再支持某些非最新版本的运行环境将不被视为破坏兼容性的改动。 -当前 ECMAScript 的版本为 2020。 - -如果想要试用这一功能,可以通过以下方式引入即时通讯 SDK: - -```js -const { Realtime } = require("leancloud-realtime/es-latest"); -``` - - - - -```js -const AV = require("leancloud-storage"); -const IM = require("leancloud-realtime"); - -const initPlugin = require("leancloud-realtime-plugin-typed-messages"); - -const { Realtime, TextMessage } = IM; -const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM); -``` - -富媒体消息插件包含的消息类型可以参考 [富媒体消息插件 API 文档](https://leancloud.github.io/js-realtime-sdk/plugins/typed-messages/docs/module-leancloud-realtime-plugin-typed-messages.html)。 - -为了保证兼容性,SDK 一直以来分发的都是 ECMAScript 5 版本的代码,并打包了所有需要的 Polyfills(比如 Promise)。 -即时通讯 SDK v5.0.0-rc.2 起同时提供以最新版本 ECMAScript 为编译目标的版本,该版本拥有更小的体积与更好的运行时优化,适用于只需要兼容最新版本浏览器的使用场景。 -如果应用使用了 `@babel/preset-env` 或类似方案,也可以在转译时 include 最新 ECMAScript 版本的 SDK,由应用来决定要兼容的目标运行环境。 -需要注意最新版本 ECMAScript 每年都会变,而该版本的目标即是提供与最新标准对齐的代码,因此由于引入了新版本 ECMASCript 特性导致不再支持某些非最新版本的运行环境将不被视为破坏兼容性的改动。 -当前 ECMAScript 的版本为 2020。 - -如果想要试用这一功能,可以通过以下方式引入即时通讯 SDK: - -```js -const { Realtime } = require("leancloud-realtime/es-latest"); -``` - - - - -##### CDN - -:::caution - -以下 CDN 资源为第三方服务商提供的免费服务,无可用性保证,**不推荐在生产环境中使用**。 - -::: - -你也可以直接在页面中通过 `script` 标签引入我们的 SDK: - - - - - - {``} - - - - - - - {``} - - - - - - - {``} - - - - - - - {` - -`} - - - - - -通过这种方式引入的 SDK 可以通过全局变量 `AV` 获得引用: - - - - -```js -const { Query, User } = AV; -``` - - - - -```js -const { Query, User } = AV; -``` - - - - -```js -const { Realtime, TextMessage } = AV; -``` - -如果想要试用以最新版本 ECMAScript 为编译目标的即时通讯 SDK 版本(参见 [npm](#npm) 章节的说明),可以这样加载文件: - - - {``} - - - - - -```js -const { Realtime, TextMessage, TypedMessagesPlugin, ImageMessage } = AV; -``` - - - - -### Node.js - -JavaScript SDK 也可以运行在 Node.js 运行环境中。如果希望在云引擎中访问我们的存储服务,请参照 [云引擎 Node.js 运行环境](/sdk/engine/deploy/nodejs/),使用模板项目中提供的 `leanengine` 包接入存储服务。 - -注意,云引擎内部访问 API 是通过内网,所以不需要也不应该配置 API 自定义域名。 -模板项目和[云引擎 SDK 使用指南](/sdk/engine/functions/sdk/)中的示例代码均未配置 API 自定义域名, -请勿设置 serverURL,以免变成公网访问,影响性能。 -在使用命令行工具(`lean up`)本地调试云引擎托管项目时,虽然是公网访问,但命令行工具会自动设置相应的环境变量,供 SDK 访问 API,所以也不需要设置 serverURL。 - -#### 安装与引用 SDK - -Node.js 中 SDK 的安装与引用也是通过包管理工具 npm,请参考 [npm](#npm)。 - -### 微信 / QQ 小程序 - -:::note - -QQ 小程序兼容微信小程序的 API,因此两者使用同一个 SDK,安装与使用方法也是一样的。不过 QQ 小程序使用的 Adapters 与微信小程序不同,请前往 [QQ 小程序 Adapters 下载页](https://unpkg.com/browse/@leancloud/platform-adapters-qqapp/lib/) 下载。 - -::: - -#### 手动导入文件 - - - - -前往 [存储 SDK 下载页](https://releases.leanapp.cn/#/leancloud/javascript-sdk/releases),下载最新版本的 `av-core-min.js`,移动到 `libs` 目录。 - - - - -前往 [存储 SDK 下载页](https://releases.leanapp.cn/#/leancloud/javascript-sdk/releases),下载最新版本的 `av-live-query-core-min.js`,移动到 `libs` 目录。 - - - - -前往 [即时通讯 SDK 下载页](https://releases.leanapp.cn/#/leancloud/js-realtime-sdk/releases),下载最新版本的 `im.min.js`,移动到 `libs` 目录。 - - - - -前往 [存储 SDK 下载页](https://releases.leanapp.cn/#/leancloud/javascript-sdk/releases),下载最新版本的 `av-core-min.js`,移动到 `libs` 目录,并将文件重命名为 `leancloud-storage.js`。 - -前往 [即时通讯 SDK 下载页](https://releases.leanapp.cn/#/leancloud/js-realtime-sdk/releases),下载最新版本的 `im.min.js`,移动到 `libs` 目录,并将文件重命名为 `leancloud-realtime.js`。 - -下载 [`typed-messages.min.js`](https://code.bdstatic.com/npm/leancloud-realtime-plugin-typed-messages@4.0.2/dist/typed-messages.min.js),移动到 `libs` 目录。 - - - - -前往 [微信小程序 Adapters 下载页](https://unpkg.com/browse/@leancloud/platform-adapters-weapp/lib/),下载最新版本的 `index.js`,移动到 `libs` 目录,并将文件重命名为 `leancloud-adapters-weapp.js`。 - -在 `app.js` 中引用 SDK,并设置 Adapters : - -:::note - -在其他文件中引用 SDK 时请将路径替换成对应的相对路径,Adapters 仅需在 app.js 中设置。 - -::: - - - - -```js -const AV = require("./libs/av-core-min.js"); -const adapters = require("./libs/leancloud-adapters-weapp.js"); - -AV.setAdapters(adapters); -``` - - - - -```js -const AV = require("./libs/av-live-query-core-min.js"); -const adapters = require("./libs/leancloud-adapters-weapp.js"); - -AV.setAdapters(adapters); -``` - - - - -```js -const { Realtime, TextMessage, setAdapters } = require("./libs/im.min.js"); -const adapters = require("./libs/leancloud-adapters-weapp.js"); - -setAdapters(adapters); -``` - - - - -```js -// 需要保证依次加载三个文件 -const AV = require("./libs/leancloud-storage.js"); -const IM = require("./libs/leancloud-realtime.js"); -const initPlugin = require("./libs/typed-messages.min.js"); -const adapters = require("./libs/leancloud-adapters-weapp.js"); - -const { Realtime, TextMessage } = IM; -const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM); - -AV.setAdapters(adapters); // 为存储 SDK 设置 adapters -IM.setAdapters(adapters); // 为即时通讯 SDK 设置 adapters -``` - - - - -#### WePY - -如果使用 [WePY](https://wepyjs.gitee.io/wepy-docs/) 来开发小程序,可以直接通过 npm 安装和引用 SDK,具体操作步骤请参考 [npm](#npm)。 - -#### mpvue - -如果使用 [mpvue](http://mpvue.com/) 来开发小程序,可以直接通过 npm 安装和引用 SDK,具体操作步骤请参考 [npm](#npm)。 - -#### Taro - -如果使用 [Taro](https://taro.jd.com/) 来开发小程序,通过 npm 安装 SDK 后,需要从指定路径引入小程序 SDK: - -```js -import AV from "leancloud-storage/dist/av-weapp.js"; -``` - -如果使用 TypeScript 开发,可以手动把 `leancloud-storage/storage.d.ts` 复制到 `leancloud-storage/dist/av-live-query-weapp.d.ts`。 - -#### 小程序插件 - -小程序插件引入 SDK 的方法与微信小程序一致。 - -### 微信 / QQ 小游戏 - -小游戏手动导入 SDK 的步骤与小程序一致,请参考 [微信 / QQ 小程序 · 手动导入文件](#手动导入文件)。 - -如果使用游戏引擎提供的开发工具开发小游戏,请参照对应的游戏引擎章节。 - -### 支付宝小程序 - -支付宝小程序通过 npm 安装与引用 SDK,同时由独立的 Adapters 库(`@leancloud/platform-adapters-alipay`)提供支持。 - -安装: - - - - -``` -$ npm install leancloud-storage @leancloud/platform-adapters-alipay -``` - - - - -``` -$ npm install leancloud-storage @leancloud/platform-adapters-alipay -``` - - - - -``` -$ npm install leancloud-realtime @leancloud/platform-adapters-alipay -``` - - - - -``` -$ npm install leancloud-realtime leancloud-realtime-plugin-typed-messages leancloud-storage @leancloud/platform-adapters-alipay -``` - - - - -获得引用: - - - - -```js -const AV = require("leancloud-storage/core"); -const adapters = require("@leancloud/platform-adapters-alipay"); - -AV.setAdapters(adapters); -``` - - - - -```js -const AV = require("leancloud-storage/live-query-core"); -const adapters = require("@leancloud/platform-adapters-alipay"); - -AV.setAdapters(adapters); -``` - - - - -```js -const { Realtime, setAdapters } = require("leancloud-realtime/im"); -const adapters = require("@leancloud/platform-adapters-alipay"); - -setAdapters(adapters); -``` - - - - -```js -const AV = require("leancloud-storage/core"); -const IM = require("leancloud-realtime/im"); -const initPlugin = require("leancloud-realtime-plugin-typed-messages"); -const adapters = require("@leancloud/platform-adapters-alipay"); - -const { Realtime, TextMessage } = IM; -const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM); - -AV.setAdapters(adapters); // 为存储 SDK 设置 adapters -IM.setAdapters(adapters); // 为即时通讯 SDK 设置 adapters -``` - - - - -### 百度小程序 - -百度小程序通过 npm 安装与引用 SDK,同时由独立的 Adapters 库(`@leancloud/platform-adapters-baidu`)提供支持。 - -安装: - - - - -``` -$ npm install leancloud-storage @leancloud/platform-adapters-baidu -``` - - - - -``` -$ npm install leancloud-storage @leancloud/platform-adapters-baidu -``` - - - - -``` -$ npm install leancloud-realtime @leancloud/platform-adapters-baidu -``` - - - - -``` -$ npm install leancloud-realtime leancloud-realtime-plugin-typed-messages leancloud-storage @leancloud/platform-adapters-baidu -``` - - - - -获得引用: - - - - -```js -const AV = require("leancloud-storage/core"); -const adapters = require("@leancloud/platform-adapters-baidu"); - -AV.setAdapters(adapters); -``` - - - - -```js -const AV = require("leancloud-storage/live-query-core"); -const adapters = require("@leancloud/platform-adapters-baidu"); - -AV.setAdapters(adapters); -``` - - - - -```js -const { Realtime, setAdapters } = require("leancloud-realtime/im"); -const adapters = require("@leancloud/platform-adapters-baidu"); - -setAdapters(adapters); -``` - - - - -```js -const AV = require("leancloud-storage/core"); -const IM = require("leancloud-realtime/im"); -const initPlugin = require("leancloud-realtime-plugin-typed-messages"); -const adapters = require("@leancloud/platform-adapters-baidu"); - -const { Realtime, TextMessage } = IM; -const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM); - -AV.setAdapters(adapters); // 为存储 SDK 设置 adapters -IM.setAdapters(adapters); // 为即时通讯 SDK 设置 adapters -``` - - - - -### 字节跳动小程序 - - - - -前往 [存储 SDK 下载页](https://releases.leanapp.cn/#/leancloud/javascript-sdk/releases),下载最新版本的 `av-core-min.js`,移动到 `libs` 目录。 - - - - -前往 [存储 SDK 下载页](https://releases.leanapp.cn/#/leancloud/javascript-sdk/releases),下载最新版本的 `av-live-query-core-min.js`,移动到 `libs` 目录。 - - - - -前往 [即时通讯 SDK 下载页](https://releases.leanapp.cn/#/leancloud/js-realtime-sdk/releases),下载最新版本的 `im.min.js`,移动到 `libs` 目录。 - - - - -前往 [存储 SDK 下载页](https://releases.leanapp.cn/#/leancloud/javascript-sdk/releases),下载最新版本的 `av-core-min.js`,移动到 `libs` 目录,并将文件重命名为 `leancloud-storage.js`。 - -前往 [即时通讯 SDK 下载页](https://releases.leanapp.cn/#/leancloud/js-realtime-sdk/releases),下载最新版本的 `im.min.js`,移动到 `libs` 目录,并将文件重命名为 `leancloud-realtime.js`。 - -下载 [`typed-messages.min.js`](https://code.bdstatic.com/npm/leancloud-realtime-plugin-typed-messages@4.0.2/dist/typed-messages.min.js),移动到 `libs` 目录。 - - - - -前往 [字节跳动小程序 Adapters 下载页](https://code.bdstatic.com/npm/@leancloud/platform-adapters-toutiao@1.4.1/lib/index.js),下载最新版本的 `index.js`,移动到 `libs` 目录,并将文件重命名为 `leancloud-adapters-toutiao.js`。 - -在 `app.js` 中引用 SDK,并设置 Adapters : - -:::note - -在其他文件中引用 SDK 时请将路径替换成对应的相对路径,Adapters 仅需在 app.js 中设置。 - -::: - - - - -```js -const AV = require("./libs/av-core-min.js"); -const adapters = require("./libs/leancloud-adapters-toutiao.js"); - -AV.setAdapters(adapters); -``` - - - - -```js -const AV = require("./libs/av-live-query-core-min.js"); -const adapters = require("./libs/leancloud-adapters-toutiao.js"); - -AV.setAdapters(adapters); -``` - - - - -```js -const { Realtime, TextMessage, setAdapters } = require("./libs/im.min.js"); -const adapters = require("./libs/leancloud-adapters-toutiao.js"); - -setAdapters(adapters); -``` - - - - -```js -// 需要保证依次加载三个文件 -const AV = require("./libs/leancloud-storage.js"); -const IM = require("./libs/leancloud-realtime.js"); -const initPlugin = require("./libs/typed-messages.min.js"); -const adapters = require("./libs/leancloud-adapters-toutiao.js"); - -const { Realtime, TextMessage } = IM; -const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM); - -AV.setAdapters(adapters); // 为存储 SDK 设置 adapters -IM.setAdapters(adapters); // 为即时通讯 SDK 设置 adapters -``` - - - - -### CocosCreator - -CocosCreator 支持直接通过 npm 安装与引用 SDK,具体操作步骤请参考 [npm](#npm)。 - -:::note - -CocosCreator 项目默认没有 `package.json` 文件,可以在安装 SDK 前通过 `npm init -y` 命令创建。 - -::: - -如果你的 CocosCreator 项目需要发布为微信或 QQ 小游戏,需要在构建发布到小游戏之前修改 SDK 的引用路径: - - - - -```diff -// SDK 应用路径变更为 -- const AV = require('leancloud-storage'); -+ const AV = require('leancloud-storage/dist/av-weapp-min.js'); -``` - - - - -```diff -// SDK 应用路径变更为 -- const AV = require('leancloud-storage/live-query'); -+ const AV = require('leancloud-storage/dist/av-live-query-weapp-min.js'); -``` - - - - -```diff -// SDK 应用路径变更为 -- const { Realtime } = require('leancloud-realtime'); -+ const { Realtime } = require('leancloud-realtime/dist/im-weapp.js'); -``` - - - - -```diff -// 暂不支持 -``` - - - - -请注意,此改动会导致其他平台的产出(包括浏览器与模拟器的预览功能)不能正常工作,因此应该只在构建发布到小游戏之前临时修改,并在发布之后修改回来。 - -在改动之后,CocosCreator 的控制台可能会出现 load script error,但不影响构建发布小游戏,并且构建产出在小游戏开发工具中运行也不会有异常。 - -### LayaAir - -LayaAir 支持直接通过 npm 安装与引用 SDK,具体操作步骤请参考 [npm](#npm)。 - -:::note - -LayaAir 项目默认没有 `package.json` 文件,可以在安装 SDK 前通过 `npm init -y` 命令创建。 - -::: - -如果你的 LayaAir 项目需要发布为微信或 QQ 小游戏,需要在构建发布到小游戏之前修改 SDK 的引用路径,具体替换方法请参考 [CocosCreator](#cocoscreator) 章节。 - -同样,此改动会导致其他平台的产出(包括浏览器与模拟器的预览功能)不能正常工作,因此应该只在构建发布到小游戏之前临时修改,并在发布之后修改回来。 - -### 快应用 - -快应用通过 npm 安装与引用 SDK,同时由独立的 Adapters 库(`@leancloud/platform-adapters-quickapp`)提供支持。 - -安装: - - - - -``` -$ npm install leancloud-storage @leancloud/platform-adapters-quickapp --save -``` - - - - -``` -$ npm install leancloud-storage @leancloud/platform-adapters-quickapp --save -``` - - - - -``` -$ npm install leancloud-realtime @leancloud/platform-adapters-quickapp --save -``` - - - - -``` -$ npm install leancloud-realtime leancloud-realtime-plugin-typed-messages leancloud-storage @leancloud/platform-adapters-quickapp --save -``` - - - - -获得引用: - - - - -```js -const AV = require("leancloud-storage/core"); -const adapters = require("@leancloud/platform-adapters-quickapp"); -AV.setAdapters(adapters); -``` - - - - -```js -const AV = require("leancloud-storage/live-query-core"); -const adapters = require("@leancloud/platform-adapters-quickapp"); -AV.setAdapters(adapters); -``` - - - - -```js -const { Realtime, setAdapters } = require("leancloud-realtime/im"); -const adapters = require("@leancloud/platform-adapters-quickapp"); -setAdapters(adapters); -``` - - - - -```js -const AV = require("leancloud-storage/core"); -const IM = require("leancloud-realtime/im"); -const initPlugin = require("leancloud-realtime-plugin-typed-messages"); - -const { Realtime, TextMessage } = IM; -const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM); - -const adapters = require("@leancloud/platform-adapters-quickapp"); -AV.setAdapters(adapters); -IM.setAdapters(adapters); -``` - - - - -### React Native - -React Native 通过 npm 安装与引用 SDK,同时由独立的 Adapters 库(`@leancloud/platform-adapters-react-native`)提供支持。 - -安装: - - - - -``` -# Step 1: Install -$ yarn add leancloud-storage @leancloud/platform-adapters-react-native @react-native-community/async-storage@1 - -# Step 2: Link -# For React Native 0.60+ -$ npx pod-install -# For React Native <= 0.59 -# npx react-native link @react-native-community/async-storage -# For Expo (SDK >= 38) 无需 link -``` - - - - -``` -# Step 1: Install -$ yarn add leancloud-storage @leancloud/platform-adapters-react-native @react-native-community/async-storage@1 - -# Step 2: Link -# For React Native 0.60+ -$ npx pod-install -# For React Native <= 0.59 -# npx react-native link @react-native-community/async-storage -# For Expo (SDK >= 38) 无需 link -``` - - - - -``` -$ yarn add leancloud-realtime @leancloud/platform-adapters-react-native -``` - - - - -``` -# Step 1: Install -$ yarn add leancloud-realtime leancloud-realtime-plugin-typed-messages leancloud-storage @leancloud/platform-adapters-react-native @react-native-community/async-storage@1 - -# Step 2: Link -# For React Native 0.60+ -$ npx pod-install -# For React Native <= 0.59 -# npx react-native link @react-native-community/async-storage -# For Expo (SDK >= 38) 无需 link -``` - - - - -获得引用: - - - - -```js -import AV from "leancloud-storage/core"; -import * as adapters from "@leancloud/platform-adapters-react-native"; -AV.setAdapters(adapters); -``` - - - - -```js -import AV from "leancloud-storage/live-query-core"; -import * as adapters from "@leancloud/platform-adapters-react-native"; -AV.setAdapters(adapters); -``` - - - - -```js -import { Realtime, setAdapters } from "leancloud-realtime/im"; -import * as adapters from "@leancloud/platform-adapters-react-native"; -setAdapters(adapters); -``` - - - - -```js -import AV from "leancloud-storage/core"; -import IM from "leancloud-realtime/im"; -import initPlugin from "leancloud-realtime-plugin-typed-messages"; -import * as adapters from "@leancloud/platform-adapters-react-native"; - -const { Realtime, TextMessage } = IM; -const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM); - -AV.setAdapters(adapters); -IM.setAdapters(adapters); -``` - - - - -### Electron - -Electron 使用包管理工具 npm 管理依赖,你可以通过以下命令安装 SDK: - - - - -``` -$ npm install leancloud-storage --save -``` - - - - -``` -$ npm install leancloud-storage --save -``` - - - - -``` -$ npm install leancloud-realtime --save -``` - - - - -``` -$ npm install leancloud-realtime leancloud-realtime-plugin-typed-messages leancloud-storage --save -``` - - - - -#### 作为浏览器脚本引入 - -在 index.html 中可以通过 `script` 标签引入 SDK: - - - - -```html - -``` - - - - -```html - -``` - - - - -```html - -``` - - - - -```html - - - -``` - - - - -#### 作为 Node.js 模块引入 - -我们推荐使用 `script` 标签引入 SDK,该方式能满足绝大部分的需求。但是如果有以下的需求,SDK 也支持通过 `require('leancloud-storage')` 方法作为 Node.js 模块引入。 - -- 需要在 main process 中使用 SDK -- 需要使用 Node.js 的 `Buffer` 或 `Stream` 构造 `AV.File` - -:::note - -通过 Node.js `require` 方法引入的 SDK 与通过浏览器 `script` 标签引入的 SDK 是两个不同的 SDK,需要各自分别初始化,并且不能混用。 - -::: - -### 其他平台 - -SDK 提供了平台无关的的版本以支持其他平台。所有平台相关的 API 被抽象成了可配置的 Adapters,在目标平台引入 SDK 后还需要配置目标平台的 Adapters。假设在 npm 上存在某平台(xyz)的 adapters package(`platform-adapters-xyz`),需要通过以下方式配置 SDK: - - - - -```js -const AV = require("leancloud-storage/core"); -const adapters = require("platform-adapters-xyz"); -AV.setAdapters(adapters); -``` - - - - -```js -const AV = require("leancloud-storage/live-query-core"); -const adapters = require("platform-adapters-xyz"); -AV.setAdapters(adapters); -``` - - - - -```js -const { Realtime, setAdapters } = require("leancloud-realtime/im"); -const adapters = require("platform-adapters-xyz"); -setAdapters(adapters); -``` - - - - -```js -const AV = require("leancloud-storage/core"); -const IM = require("leancloud-realtime/im"); -const initPlugin = require("leancloud-realtime-plugin-typed-messages"); -const adapters = require("platform-adapters-xyz"); - -const { Realtime, TextMessage } = IM; -const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM); - -AV.setAdapters(adapters); -IM.setAdapters(adapters); -``` - - - - -对于不支持使用 npm 管理依赖的运行环境,SDK 同时也提供了预编译好的 UMD 类型文件: - - - - - - {`https://code.bdstatic.com/npm/leancloud-storage@${sdkVersions.leancloud.js.storage}/dist/av-core-min.js`} - - - - - - - {`https://code.bdstatic.com/npm/leancloud-storage@${sdkVersions.leancloud.js.storage}/dist/av-live-query-core-min.js`} - - - - - - - {`https://code.bdstatic.com/npm/leancloud-realtime@${sdkVersions.leancloud.js.realtime}/dist/im.min.js`} - - - - - -``` -// 不支持该方式 -``` - - - - -开发者可以自行实现目标平台的 Adapters 来适配该平台。Adapters 的接口定义可以在 package [`@leancloud/adapter-types`](https://unpkg.com/browse/@leancloud/adapter-types/types.d.ts) 中找到。你也可以通过关键字 [`platform-adapters`](https://www.npmjs.com/search?q=keywords:platform-adapters) 查找社区中其他开发者贡献的 Adapters。 - -## 初始化 - -无论是通过 npm 安装还是直接通过 CDN 加载,初始化的方法都是一样的。 - - - - - - -```js -AV.init({ - appId: "your-client-id", - appKey: "your-client-token", - serverURL: "https://your_server_url", -}); -``` - - - - -```js -AV.init({ - appId: "your-client-id", - appKey: "your-client-token", - serverURL: "https://your_server_url", -}); -``` - - - - -```js -const realtime = new Realtime({ - appId: "your-client-id", - appKey: "your-client-token", - server: "https://your_server_url", -}); -``` - - - - -```js -const realtime = new Realtime({ - appId: "your-client-id", - appKey: "your-client-token", - server: "https://your_server_url", - // 初始化即时通讯服务时需要指定富媒体消息插件 - plugins: [TypedMessagesPlugin], -}); -// 需要同时初始化存储服务 -AV.init({ - appId: "your-client-id", - appKey: "your-client-token", - serverURL: "https://your_server_url", -}); -``` - - - - - - - - - - - -```js -AV.init({ - appId: "your-app-id", - appKey: "your-app-key", - serverURL: "https://your_server_url", -}); -``` - - - - -```js -AV.init({ - appId: "your-app-id", - appKey: "your-app-key", - serverURL: "https://your_server_url", -}); -``` - - - - -```js -const realtime = new Realtime({ - appId: "your-app-id", - appKey: "your-app-key", - server: "https://your_server_url", -}); -``` - - - - -```js -const realtime = new Realtime({ - appId: "your-app-id", - appKey: "your-app-key", - server: "https://your_server_url", - // 初始化即时通讯服务时需要指定富媒体消息插件 - plugins: [TypedMessagesPlugin], -}); -// 需要同时初始化存储服务 -AV.init({ - appId: "your-app-id", - appKey: "your-app-key", - serverURL: "https://your_server_url", -}); -``` - - - - - - -### 应用凭证 - - - -## 域名 - - - -## 开启调试日志 - -在应用开发阶段,你可以选择开启 SDK 的调试日志(debug log)来方便追踪问题。调试日志开启后,SDK 会把网络请求、错误消息等信息输出到 IDE 的日志窗口,或是浏览器 Console 或是云引擎日志(如果在云引擎下运行 SDK)。 - -如果是在 Node.js 中运行,可以通过在启动应用时设置环境变量来打印调试日志(下面假设启动应用的命令是 `npm start`): - - - - -``` -DEBUG=leancloud* npm start -``` - - - - -``` -DEBUG=leancloud*,LC* npm start -``` - - - - -``` -DEBUG=LC* npm start -``` - - - - -``` -DEBUG=LC* npm start -``` - - - - -如果是在浏览器中运行,可以通过设置 `localStorage` 来让日志打印到浏览器控制台: - - - - -``` -localStorage.setItem('debug', 'leancloud*'); -``` - - - - -``` -localStorage.setItem('debug', 'leancloud*,LC*'); -``` - - - - -``` -localStorage.setItem('debug', 'LC*'); -``` - - - - -``` -localStorage.setItem('debug', 'LC*'); -``` - - - - -除了在 Node.js 中使用环境变量,在浏览器中使用 localStorage 启用调试模式外,新版本的 SDK 还支持在代码中启用、停用调试模式。 - - - - -```js -// 需要 SDK 版本 >= v3.14.0 -const AV = require("leancloud-storage"); -AV.debug.enable(); // 启用 -AV.debug.disable(); // 停用 -``` - - - - -```js -// 需要 SDK 版本 >= v3.14.0 -const AV = require("leancloud-storage"); -AV.debug.enable(); // 启用 -AV.debug.disable(); // 停用 -``` - - - - -```js -// 需要 SDK 版本 >= v5.0.0-alpha.1 -const { debug } = require("leancloud-realtime"); -debug.enable(); // 启用 -debug.disable(); // 停用 -``` - - - - -```js -// 需要 SDK 版本 >= v5.0.0-alpha.1 -const { debug } = require("leancloud-realtime"); -debug.enable(); // 启用 -debug.disable(); // 停用 -``` - - - - -:::caution -在应用发布之前,请关闭调试日志,以免暴露敏感数据。 -::: - -## 验证 - -首先,确认本地网络环境是可以访问云端服务器的,可以执行以下命令: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` 为绑定的 API 自定义域名。 - -如果当前网络正常会返回当前时间: - -```json -{ "__type": "Date", "iso": "2020-10-12T06:46:56.000Z" } -``` - -然后在项目中编写如下测试代码: - -```js -const TestObject = AV.Object.extend("TestObject"); -const testObject = new TestObject(); -testObject.set("words", "Hello world!"); -testObject.save().then((testObject) => { - console.log("保存成功。"); -}); -``` - -保存后运行程序。 - -然后打开 ** > 结构化数据 > `TestObject`**,如果看到数据表中出现一行「words」列的值为「Hello world!」的数据,说明 SDK 已经正确地执行了上述代码,配置完毕。 - -如果控制台没有发现对应的数据,请参考 [问题排查](#问题排查)。 - -## 问题排查 - -SDK 安装指南基于当前最新版本的 SDK 编写,所以排查问题前,请先检查下安装的 SDK 是不是最新版本。 - -### `401 Unauthorized` - -如果 SDK 抛出 `401` 异常或者查看本地网络访问日志存在: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -则可认定为 App ID 或者 `App Key` 输入有误,或者是不匹配,很多开发者同时注册了多个应用,导致拷贝粘贴的时候,用 A 应用的 App ID 匹配 B 应用的 `App Key`,这样就会出现服务端鉴权失败的错误。 - -### 客户端无法访问网络 - -客户端尤其是手机端,应用在访问网络的时候需要申请一定的权限。 diff --git a/leancloud/docs/sdk/storage/objc-guide/_category_.json b/leancloud/docs/sdk/storage/objc-guide/_category_.json deleted file mode 100644 index 219e26ae3..000000000 --- a/leancloud/docs/sdk/storage/objc-guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Objective-C", - "collapsed": true, - "position": 4 -} diff --git a/leancloud/docs/sdk/storage/objc-guide/objc.mdx b/leancloud/docs/sdk/storage/objc-guide/objc.mdx deleted file mode 100644 index d425f2c9d..000000000 --- a/leancloud/docs/sdk/storage/objc-guide/objc.mdx +++ /dev/null @@ -1,1541 +0,0 @@ ---- -title: 数据存储开发指南 · Objective-C -sidebar_label: Objective-C 开发指南 -slug: /sdk/storage/guide/objc/ -sidebar_position: 6 ---- - -import Path from "/src/docComponents/path"; -import { Conditional } from "/src/docComponents/conditional"; - - -数据存储是云服务提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端: - -```objc -// 构建对象 -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; - -// 为属性赋值 -[todo setObject:@"工程师周会" forKey:@"title"]; -[todo setObject:@"周二两点,全体成员" forKey:@"content"]; - -// 将对象保存到云端 -[todo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 成功保存之后,执行其他逻辑 - NSLog(@"保存成功。objectId:%@", todo.objectId); - } else { - // 异常处理 - } -}]; -``` - -我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。 - -## SDK 安装与初始化 - -请阅读[数据存储 Objective-C SDK 配置指南](/sdk/storage/guide/setup-objc/)。 - -## 对象 - -### `LCObject` - -`LCObject` 是云服务对复杂对象的封装,每个 `LCObject` 包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个 `LCObject` 上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。 - -比如说,一个保存着单个 Todo 的 `LCObject` 可能包含如下数据: - -```json -title: "给小林发邮件确认会议时间", -isComplete: false, -priority: 2, -tags: ["工作", "销售"] -``` - -### 数据类型 - -`LCObject` 支持的数据类型包括 `String`、`Number`、`Boolean`、`Object`、`Array`、`Date` 等等。你可以通过嵌套的方式在 `Object` 或 `Array` 里面存储更加结构化的数据。 - -`LCObject` 还支持两种特殊的数据类型 `Pointer` 和 `File`,可以分别用来存储指向其他 `LCObject` 的指针以及二进制数据。 - -`LCObject` 同时支持 `GeoPoint`,可以用来存储地理位置信息。参见 [GeoPoint](#geopoint)。 - -以下是一些示例: - -```objc -// 基本类型 -NSNumber *boolean = @(YES); -NSNumber *number = [NSNumber numberWithInt:2018]; -NSString *string = [NSString stringWithFormat:@"%@ 流行音乐榜单", number]; -NSDate *date = [NSDate date]; -NSData *data = [@"Hello world!" dataUsingEncoding:NSUTF8StringEncoding]; -NSArray *array = [NSArray arrayWithObjects: string, number, nil]; -NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: number, @"number", string, @"string", nil]; - -// 构建对象 -LCObject *testObject = [LCObject objectWithClassName:@"TestObject"]; -[testObject setObject:boolean forKey:@"testBoolean"]; -[testObject setObject:number forKey:@"testInteger"]; -[testObject setObject:string forKey:@"testString"]; -[testObject setObject:date forKey:@"testDate"]; -[testObject setObject:data forKey:@"testData"]; -[testObject setObject:array forKey:@"testArray"]; -[testObject setObject:dictionary forKey:@"testDictionary"]; -[testObject saveInBackground]; -``` - -我们不推荐通过 `NSData` 在 `LCObject` 里面存储图片、文档等大型二进制数据。每个 `LCObject` 的大小不应超过 **128 KB**。如需存储大型文件,可创建 `LCFile` 实例并将其关联到 `LCObject` 的某个属性上。参见 [文件](#文件)。 - -注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。 - -** > 结构化数据** 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。 - -若想了解云服务是如何保护应用数据的,请阅读[数据和安全](/sdk/storage/guide/security/)。 - -### 构建对象 - -下面的代码构建了一个 class 为 `Todo` 的 `LCObject`: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; - -// 等同于 -LCObject *todo = [[LCObject alloc] initWithClassName:@"Todo"]; -``` - -在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。 - -### 保存对象 - -下面的代码将一个 class 为 `Todo` 的对象存入云端: - -```objc -// 构建对象 -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; - -// 为属性赋值 -[todo setObject:@"马拉松报名" forKey:@"title"]; -[todo setObject:@2 forKey:@"priority"]; - -// 将对象保存到云端 -[todo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 成功保存之后,执行其他逻辑 - NSLog(@"保存成功。objectId:%@", todo.objectId); - } else { - // 异常处理 - } -}]; -``` - -为了确认对象已经保存成功,我们可以到 ** > 结构化数据 > `Todo`** 里面看一下,应该会有一行新的数据产生。点一下这个数据的 `objectId`,应该能看到类似这样的内容: - -```json -{ - "title": "马拉松报名", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -注意,无需在 ** > 结构化数据** 里面创建新的 `Todo` class 即可运行前面的代码。如果 class 不存在,它将自动创建。 - -以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定: - -| 内置属性 | 类型 | 描述 | -| ----------- | ---------- | -------------------------------------------------------------- | -| `objectId` | `NSString` | 该对象唯一的 ID 标识。 | -| `ACL` | `LCACL` | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 | -| `createdAt` | `NSDate` | 该对象被创建的时间。 | -| `updatedAt` | `NSDate` | 该对象最后一次被修改的时间。 | - -这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的 `LCObject` 不存在这些属性。 - -属性名(**keys**)只能包含字母、数字和下划线。自定义属性不得以双下划线(`__`)开头或与任何系统保留字段和内置属性(`ACL`、`className`、`createdAt`、`objectId` 和 `updatedAt`)重名,无论大小写。 - -属性值(**values**)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 [数据类型](#数据类型)。 - -我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 `CustomData`。属性,采用小驼峰法,如 `imageUrl`。 - -### 获取对象 - -对于已经保存到云端的 `LCObject`,可以通过它的 `objectId` 将其取回: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query getObjectInBackgroundWithId:@"582570f38ac247004f39c24b" block:^(LCObject *todo, NSError *error) { - // todo 就是 objectId 为 582570f38ac247004f39c24b 的 Todo 实例 - NSString *title = todo[@"title"]; - int priority = [[todo objectForKey:@"priority"] intValue]; - - // 获取内置属性 - NSString *objectId = todo.objectId; - NSDate *updatedAt = todo.updatedAt; - NSDate *createdAt = todo.createdAt; -}]; -``` - -如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 `nil`。 - -#### 同步对象 - -当云端数据发生更改时,你可以调用 `fetchInBackgroundWithBlock` 方法来刷新对象,使之与云端数据同步: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -[todo fetchInBackgroundWithBlock:^(LCObject *todo, NSError *error) { - // todo 已刷新 -}]; -``` - -刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,**`fetchInBackgroundWithBlock` 操作会丢弃这些修改**。为避免这种情况,你可以在刷新时指定 **需要刷新的属性**,这样只有指定的属性会被刷新(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`),其他属性不受影响。 - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -LCObjectFetchOption *option = [LCObjectFetchOption new]; -option.selectKeys = @[@"priority", @"location"]; -[todo fetchInBackgroundWithOption:option block:^(LCObject * _Nullable object, NSError * _Nullable error) { - // 只有 priority 和 location 会被获取和刷新 -}]; -``` - -### 更新对象 - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `saveInBackground` 方法。例如: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -[todo setObject:@"这周周会改到周三下午三点。" forKey:@"content"]; -[todo saveInBackground]; -``` - -云服务会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。 - -#### 有条件更新对象 - -通过传入 `query` 选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回 `305` 错误。 - -例如,用户的账户表 `Account` 有一个余额字段 `balance`,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求: - -```objc -LCObject *account = [LCObject objectWithClassName:@"Account" objectId:@"5745557f71cfe40068c6abe0"]; -// 对 balance 原子减少 100 -NSInteger amount = -100; -[account incrementKey:@"balance" byAmount:@(amount)]; -// 设置条件 -LCQuery *query = [[LCQuery alloc] init]; -[query whereKey:@"balance" greaterThanOrEqualTo:@(-amount)]; -LCSaveOption *option = [[LCSaveOption alloc] init]; -option.query = query; -// 操作结束后,返回最新数据。 -// 如果是新对象,则所有属性都会被返回, -// 否则只有更新的属性会被返回。 -option.fetchWhenSave = YES; -[account saveInBackgroundWithOption:option block:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"当前余额为:%@", account[@"balance"]); - } else if (error.code == 305) { - NSLog(@"余额不足,操作失败!"); - } -}]; -``` - -**`query` 选项只对已存在的对象有效**,不适用于尚未存入云端的对象。 - -`query` 选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过 `LCQuery` 查询 `LCObject` 再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。 - -#### 更新计数器 - -设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 **原子操作** 来增加或减少一个属性内保存的数字: - -```objc -[post incrementKey:@"likes" byAmount:@1]; -``` - -可以指定需要增加或减少的值。若未指定,则默认使用 `1`。 - -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,需要原子增减的字段建议使用整数以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 - -#### 更新数组 - -更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据: - -- `addObject:forKey:` 将指定对象附加到数组末尾。 -- `addObjectsFromArray:forKey:` 将指定对象数组附加到数组末尾。 -- `addUniqueObject:forKey:` 将指定对象附加到数组末尾,确保对象唯一。 -- `addUniqueObjectsFromArray:forKey:` 将指定对象数组附加到数组末尾,确保对象唯一。 -- `removeObject:forKey:` 从数组字段中删除指定对象的所有实例。 -- `removeObjectsInArray:forKey:` 从数组对象中删除指定数组中的所有对象。 - -例如,`Todo` 用一个 `alarms` 属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性: - -```objc --(NSDate*) getDateWithDateString:(NSString*) dateString{ - NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init]; - [dateFormat setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; - NSDate *date = [dateFormat dateFromString:dateString]; - return date; -} - -NSDate *alarm1 = [self getDateWithDateString:@"2018-04-30 07:10:00"]; -NSDate *alarm2 = [self getDateWithDateString:@"2018-04-30 07:20:00"]; -NSDate *alarm3 = [self getDateWithDateString:@"2018-04-30 07:30:00"]; - -NSArray *alarms = [NSArray arrayWithObjects:alarm1, alarm2, alarm3, nil]; - -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; -[todo addUniqueObjectsFromArray:alarms forKey:@"alarms"]; -[todo saveInBackground]; -``` - -### 删除对象 - -下面的代码从云端删除一个 `Todo` 对象: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -[todo deleteInBackground]; -``` - -注意,删除对象是一个较为敏感的操作,我们建议你阅读[ACL 权限管理开发指南](/sdk/storage/guide/acl/)来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。 - -### 批量操作 - -```objc -// 批量构建和更新 -+ (BOOL)saveAll:(NSArray *)objects error:(NSError **)error; -+ (void)saveAllInBackground:(NSArray *)objects - block:(LCBooleanResultBlock)block; - -// 批量删除 -+ (BOOL)deleteAll:(NSArray *)objects error:(NSError **)error; -+ (void)deleteAllInBackground:(NSArray *)objects - block:(LCBooleanResultBlock)block; - -// 批量同步 -+ (BOOL)fetchAll:(NSArray *)objects error:(NSError **)error; -+ (void)fetchAllInBackground:(NSArray *)objects - block:(LCArrayResultBlock)block; -``` - -下面的代码将所有 `Todo` 的 `isComplete` 设为 `true`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query findObjectsInBackgroundWithBlock:^(NSArray *todos, NSError *error) { - // 获取需要更新的 todo - for (LCObject *todo in todos) { - // 更新属性值 - todo[@"isComplete"] = @(YES); - } - // 批量更新 - [LCObject saveAllInBackground:todos]; -}]; -``` - -虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。 - -### 后台运行 - -细心的开发者已经发现,在所有的示例代码中几乎都是用了异步来访问云端,形如 `xxxxInBackground` 的用法都是提供给开发者在主线程调用用以实现后台运行的方法,因此开发者在主线程可以放心地调用这种命名方式的函数。 - -### 离线存储对象 - -大多数保存功能可以立刻执行,并通知应用「保存完毕」。不过若不需要知道保存完成的时间,则可使用 `saveEventually` 来代替。 - -它的优点在于:如果用户目前尚未接入网络,`saveEventually` 会缓存设备中的数据,并在网络连接恢复后上传。如果应用在网络恢复之前就被关闭了,那么当它下一次打开时,SDK 会再次尝试保存操作。 - -所有 `saveEventually`(或 `deleteEventually`)的相关调用,将按照调用的顺序依次执行。因此,多次对某一对象使用 `saveEventually` 是安全的。 - -### 数据模型 - -对象之间可以产生关联。拿一个博客应用来说,一个 `Post` 对象可以与许多个 `Comment` 对象产生关联。云服务支持三种关系:一对一、一对多、多对多。 - -#### 一对一、一对多关系 - -一对一、一对多关系可以通过将 `LCObject` 保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个 `Comment` 指向一个 `Post`。 - -下面的代码会创建一个含有单个 `Comment` 的 `Post`: - -```objc -// 创建 post -LCObject *post = [[LCObject alloc] initWithClassName:@"Post"]; -[post setObject:@"饿了……" forKey:@"title"]; -[post setObject:@"中午去哪吃呢?" forKey:@"content"]; - -// 创建 comment -LCObject *comment = [[LCObject alloc] initWithClassName:@"Comment"]; -[comment setObject:@"当然是肯德基啦!" forKey:@"content"]; - -// 将 post 设为 comment 的一个属性值 -[comment setObject:post forKey:@"parent"]; - -// 保存 comment 会同时保存 post -[comment saveInBackground]; -``` - -云端存储时,会将被指向的对象用 `Pointer` 的形式存起来。你也可以用 `objectId` 来指向一个对象: - -```objc -LCObject *post = [LCObject objectWithClassName:@"Post" objectId:@"57328ca079bc44005c2472d0"]; -[comment setObject:post forKey:@"post"]; -``` - -请参阅 [关系查询](#关系查询) 来了解如何获取关联的对象。 - -#### 多对多关系 - -想要建立多对多关系,最简单的办法就是使用 **数组**。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 **中间表** 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。 - -我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。 - -### 序列化和反序列化 - -在实际的开发中,把 `LCObject` 当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此 `LCObject` 也提供了序列化和反序列化的方法。 - -序列化: - -```objc -LCObject *todo = [[LCObject alloc] initWithClassName:@"Todo"]; // 构建对象 -[todo setObject:@"马拉松报名" forKey:@"title"]; // 设置名称 -[todo setObject:@2 forKey:@"priority"]; // 设置优先级 -[todo setObject:[LCUser currentUser] forKey:@"owner"]; // 这里就是一个 Pointer 类型,指向当前登录的用户 - -NSMutableDictionary *serializedJSONDictionary = [todo dictionaryForObject]; // 获取序列化后的字典 -``` - -反序列化: - -```objc -// 由 NSMutableDictionary 转化一个 LCObject -LCObject *todo = [LCObject objectWithDictionary:serializedJSONDictionary]; -``` - -## 查询 - -我们已经了解到如何从云端获取单个 `LCObject`,但你可能还会有一次性获取多个符合特定条件的 `LCObject` 的需求,这时候就需要用到 `LCQuery` 了。 - -### 基础查询 - -执行一次基础查询通常包括这些步骤: - -1. 构建 `LCQuery`; -2. 向其添加查询条件; -3. 执行查询并获取包含满足条件的对象的数组。 - -下面的代码获取所有 `lastName` 为 `Smith` 的 `Student`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Student"]; -[query whereKey:@"lastName" equalTo:@"Smith"]; -[query findObjectsInBackgroundWithBlock:^(NSArray *students, NSError *error) { - // students 是包含满足条件的 Student 对象的数组 -}]; -``` - -### 查询条件 - -可以给 `LCObject` 添加不同的条件来改变获取到的结果。 - -下面的代码查询所有 `firstName` 不为 `Jack` 的对象: - -```objc -[query whereKey:@"firstName" notEqualTo:@"Jack"]; -``` - -对于能够排序的属性(比如数字、字符串),可以进行比较查询: - -```objc -// 限制 age < 18 -[query whereKey:@"age" lessThan:@18]; - -// 限制 age <= 18 -[query whereKey:@"age" lessThanOrEqualTo:@18]; - -// 限制 age > 18 -[query whereKey:@"age" greaterThan:@18]; - -// 限制 age >= 18 -[query whereKey:@"age" greaterThanOrEqualTo:@18]; -``` - -可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是 `AND` 的关系: - -```objc -[query whereKey:@"firstName" equalTo:@"Jack"]; -[query whereKey:@"age" greaterThan:@18]; -``` - -可以通过指定 `limit` 限制返回结果的数量(默认为 `100`): - -```objc -// 最多获取 10 条结果 -query.limit = 10; -``` - -由于性能原因,`limit` 最大只能设为 `1000`。即使将其设为大于 `1000` 的数,云端也只会返回 1,000 条结果。 - -如果只需要一条结果,可以直接用 `getFirstObject`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"priority" equalTo:@2]; -[query getFirstObjectInBackgroundWithBlock:^(LCObject *todo, NSError *error) { - // todo 是第一个满足条件的 Todo 对象 -}]; -``` - -可以通过设置 `skip` 来跳过一定数量的结果: - -```objc -// 跳过前 20 条结果 -query.skip = 20; -``` - -把 `skip` 和 `limit` 结合起来,就能实现翻页功能: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"priority" equalTo:@2]; -query.limit = 10; -query.skip = 20; -``` - -需要注意的是,`skip` 的值越高,查询所需的时间就越长。作为替代方案,可以通过设置 `createdAt` 或 `updatedAt` 的范围来实现更高效的翻页,因为它们都自带索引。 -同理,也可以通过设置自增字段的范围来实现翻页。 - -对于能够排序的属性,可以指定结果的排序规则: - -```objc -// 按 createdAt 升序排列 -[query orderByAscending:@"createdAt"]; - -// 按 createdAt 降序排列 -[query orderByDescending:@"createdAt"]; -``` - -还可以为同一个查询添加多个排序规则: - -```objc -[query addAscendingOrder:@"priority"]; -[query addDescendingOrder:@"createdAt"]; -``` - -下面的代码可用于查找包含或不包含某一属性的对象: - -```objc -// 查找包含 "images" 的对象 -[query whereKeyExists:@"images"]; - -// 查找不包含 "images" 的对象 -[query whereKeyDoesNotExist:@"images"]; -``` - -可以通过 `selectKeys` 指定需要返回的属性。下面的代码只获取每个对象的 `title` 和 `content`(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`): - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query selectKeys:@[@"title", @"content"]]; -[query getFirstObjectInBackgroundWithBlock:^(LCObject *todo, NSError *error) { - NSString *title = todo[@"title"]; // √ - NSString *content = todo[@"content"]; // √ - NSString *notes = todo[@"notes"]; // 会报错 -}]; -``` - -`selectKeys` 支持点号(`author.firstName`),详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)。 -另外,字段名前添加减号前缀表示反向选择,例如 `-author` 表示不返回 `author` 字段。 -反向选择同样适用于内置字段,比如 `-objectId`,也可以和点号组合使用,比如 `-pubUser.createdAt`。 - -对于未获取的属性,可以通过对结果中的对象进行 `fetchInBackgroundWithBlock` 操作来获取。参见 [同步对象](#同步对象)。 - -### 字符串查询 - -可以用 `hasPrefix` 来查找某一属性值以特定字符串开头的对象。和 SQL 中的 `LIKE` 一样,你可以利用索引带来的优势: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -// 相当于 SQL 中的 title LIKE 'lunch%' -[query whereKey:@"title" hasPrefix:@"lunch"]; -``` - -可以用 `containsString` 来查找某一属性值包含特定字符串的对象: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -// 相当于 SQL 中的 title LIKE '%lunch%' -[query whereKey:@"title" containsString:@"lunch"]; -``` - -和 `hasPrefix` 不同,`containsString` 无法利用索引,因此不建议用于大型数据集。 - -注意 `hasPrefix` 和 `containsString` 都是 **区分大小写** 的,所以上述查询会忽略 `Lunch`、`LUNCH` 等字符串。 - -如果想查找某一属性值不包含特定字符串的对象,可以使用 `matchesRegex` 进行基于正则表达式的查询: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -// "title" 不包含 "ticket"(不区分大小写) -[query whereKey:@"title" matchesRegex:@"^((?!ticket).)*$", modifiers:"i"]; -``` - -不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class, -因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以使用全文搜索服务。 - -使用查询时如果遇到性能问题,可参阅 [查询性能优化](#查询性能优化)。 - -### 数组查询 - -下面的代码查找所有数组属性 `tags` 包含 `工作` 的对象: - -```objc -[query whereKey:@"tags" equalTo:@"工作"]; -``` - -下面的代码查询数组属性长度为 3(正好包含 3 个标签)的对象: - -```objc -[query whereKey:@"tags" sizeEqualTo:3]; -``` - -下面的代码查找所有数组属性 `tags` **同时包含** `工作`、`销售` 和 `会议` 的对象: - -```objc -[query whereKey:@"tags" containsAllObjectsInArray:[NSArray arrayWithObjects:@"工作", @"销售", @"会议", nil]]; -``` - -如需获取某一属性值包含一列值中任意一个值的对象,可以直接用 `containedIn` 而无需执行多次查询。下面的代码构建的查询会查找所有 `priority` 为 `1` **或** `2` 的 todo 对象: - -```objc -// 单个查询 -LCQuery *priorityOneOrTwo = [LCQuery queryWithClassName:@"Todo"]; -[priorityOneOrTwo whereKey:@"priority" containedIn:[NSArray arrayWithObjects:@1, @2, nil]]; -// 这样就可以了 :) - -// --------------- -// vs. -// --------------- - -// 多个查询 -LCQuery *priorityOne = [LCQuery queryWithClassName:@"Todo"]; -[priorityOne whereKey:@"priority" equalTo:@1]; - -LCQuery *priorityTwo = [LCQuery queryWithClassName:@"Todo"]; -[priorityTwo whereKey:@"priority" equalTo:@2]; - -LCQuery *priorityOneOrTwo = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:priorityOne, priorityTwo, nil]]; -// 好像有些繁琐 :( -``` - -反过来,还可以用 `notContainedIn` 来获取某一属性值不包含一列值中任何一个的对象。 - -### 关系查询 - -查询关联数据有很多种方式,常见的一种是查询某一属性值为特定 `LCObject` 的对象,这时可以像其他查询一样直接用 `equalTo`。比如说,如果每一条博客评论 `Comment` 都有一个 `post` 属性用来存放原文 `Post`,则可以用下面的方法获取所有与某一 `Post` 相关联的评论: - -```objc -LCObject *post = [LCObject objectWithClassName:@"Post" objectId:@"57328ca079bc44005c2472d0"]; -LCQuery *query = [LCQuery queryWithClassName:@"Comment"]; -[query whereKey:@"post" equalTo:post]; -[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) { - // comments 包含与 post 相关联的评论 -}]; -``` - -如需获取某一属性值为另一查询结果中任一 `LCObject` 的对象,可以用 `matchesQuery`。下面的代码构建的查询可以找到所有包含图片的博客文章的评论: - -```objc -LCQuery *innerQuery = [LCQuery queryWithClassName:@"Post"]; -[innerQuery whereKeyExists:@"images"]; - -LCQuery *query = [LCQuery queryWithClassName:@"Comment"]; -[query whereKey:@"post" matchesQuery:innerQuery]; -``` - -如需获取某一属性值不是另一查询结果中任一 `LCObject` 的对象,则使用 `doesNotMatchQuery`。 - -有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用 `includeKey`。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Comment"]; - -// 获取最新发布的 -[query orderByDescending:@"createdAt"]; - -// 只获取 10 条 -query.limit = 10; - -// 同时包含博客文章 -[query includeKey:@"post"]; - -[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) { - // comments 包含最新发布的 10 条评论,包含各自对应的博客文章 - for (LCObject *comment in comments) { - // 该操作无需网络连接 - LCObject *post = comment[@"post"]; - } -}]; -``` - -可以用 dot 符号(`.`)来获取多级关系,例如 `post.author`,详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)的《在查询对象时使用点号》一节。 - -可以在同一查询上应用多次 `includeKey` 以包含多个属性。通过这种方法获取到的对象同样接受 `getFirstObject` 等 `LCQuery` 辅助方法。 - -通过 `includeKey` 进行多级查询的方式不适用于数组属性内部的 `LCObject`,只能包含到数组本身。 - -#### 关系查询的注意事项 - -云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌/子查询(和普通查询一样,`limit` 默认为 `100`,最大 `1000`),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出 `limit`,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有 `limit` 数量以内的结果会被填入主查询。 - -我们建议采用以下方案进行改进: - -- 确保子查询的结果在 100 条以下,如果在 100 至 1,000 条之间的话请将子查询的 `limit` 设为 `1000`。 -- 将需要查询的字段冗余到主查询所在的表上。 -- 进行多次查询,每次在子查询上设置不同的 `skip` 值来遍历所有记录(注意 `skip` 的值较大时可能会引发性能问题,因此不是很推荐)。 - -### 统计总数量 - -如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用 `countObjectsInBackgroundWithBlock` 来代替 `findObjectsInBackgroundWithBlock`。比如说,查询有多少个已完成的 todo: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"isComplete" equalTo:@(YES)]; -[query countObjectsInBackgroundWithBlock:^(NSInteger count, NSError *error) { - NSLog(@"%ld 个 todo 已完成。", count); -}]; -``` - -### 组合查询 - -组合查询就是把诸多查询条件用一定逻辑合并到一起(`OR` 或 `AND`)再交给云端去查询。 - -组合查询不支持在子查询中包含 `GeoPoint` 或其他非过滤性的限制(例如 `near`、`withinGeoBox`、`limit`、`skip`、`ascending`、`descending`、`include`)。 - -#### OR 查询 - -OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于 `3` 或者已经完成了的 todo: - -```objc -LCQuery *priorityQuery = [LCQuery queryWithClassName:@"Todo"]; -[priorityQuery whereKey:@"priority" greaterThanOrEqualTo:@3]; - -LCQuery *isCompleteQuery = [LCQuery queryWithClassName:@"Todo"]; -[isCompleteQuery whereKey:@"isComplete" equalTo:@(YES)]; - -LCQuery *query = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:priorityQuery, isCompleteQuery, nil]]; -``` - -使用 OR 查询时,子查询中不能包含 `GeoPoint` 相关的查询。 - -#### AND 查询 - -使用 AND 查询的效果等同于往 `LCQuery` 添加多个条件。下面的代码构建的查询会查找创建时间在 `2016-11-13` 和 `2016-12-02` 之间的 todo: - -```objc -NSDate *(^dateFromString)(NSString *string) = ^(NSString *string) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd"]; - return [dateFormatter dateFromString:string]; -}; - -LCQuery *startDateQuery = [LCQuery queryWithClassName:@"Todo"]; -[startDateQuery whereKey:@"createdAt" greaterThanOrEqualTo:dateFromString(@"2016-11-13")]; - -LCQuery *endDateQuery = [LCQuery queryWithClassName:@"Todo"]; -[endDateQuery whereKey:@"createdAt" lessThan:dateFromString(@"2016-12-03")]; - -LCQuery *query = [LCQuery andQueryWithSubqueries:[NSArray arrayWithObjects:startDateQuery, endDateQuery, nil]]; -``` - -单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询: - -```objc -NSDate *(^dateFromString)(NSString *string) = ^(NSString *string) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd"]; - return [dateFormatter dateFromString:string]; -}; - -LCQuery *createdAtQuery = [LCQuery queryWithClassName:@"Todo"]; -[createdAtQuery whereKey:@"createdAt" greaterThanOrEqualTo:dateFromString(@"2018-04-30")]; -[createdAtQuery whereKey:@"createdAt" lessThan:dateFromString(@"2018-05-01")]; - -LCQuery *locationQuery = [LCQuery queryWithClassName:@"Todo"]; -[locationQuery whereKeyDoesNotExist:@"location"]; - -LCQuery *priority2Query = [LCQuery queryWithClassName:@"Todo"]; -[priority2Query whereKey:@"priority" equalTo:@2]; - -LCQuery *priority3Query = [LCQuery queryWithClassName:@"Todo"]; -[priority3Query whereKey:@"priority" equalTo:@3]; - -LCQuery *priorityQuery = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:priority2Query, priority3Query, nil]]; -LCQuery *timeLocationQuery = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:locationQuery, createdAtQuery, nil]]; -LCQuery *query = [LCQuery andQueryWithSubqueries:[NSArray arrayWithObjects:priorityQuery, timeLocationQuery, nil]]; -``` - -### 缓存查询 - -缓存一些查询的结果到磁盘上,这可以让你在离线的时候,或者应用刚启动,网络请求还没有足够时间完成的时候可以展现一些数据给用户。当缓存占用了太多空间的时候,SDK 会自动清空缓存。 - -默认情况下的查询不会使用缓存,除非你调用接口明确设置启用。例如,尝试从网络请求,如果网络不可用则从缓存数据中获取,可以这样设置: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Post"]; -query.cachePolicy = kLCCachePolicyNetworkElseCache; - -// 设置缓存有效期 -query.maxCacheAge = 24*3600; - -[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { - if (!error) { - // 成功找到结果,先找网络再访问磁盘 - } else { - // 无法访问网络,本次查询结果未做缓存 - } -}]; -``` - -#### 缓存策略 - -为了满足多变的需求,SDK 默认提供了以下几种缓存策略: - -| 策略枚举 | 含义及解释 | -| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `kLCCachePolicyIgnoreCache` | **(默认缓存策略)**查询行为不从缓存加载,也不会将结果保存到缓存中。 | -| `kLCCachePolicyCacheOnly` | 查询行为忽略网络状况,只从缓存加载。如果没有缓存结果,该策略会产生 `LCError`。 | -| `kLCCachePolicyCacheElseNetwork` | 查询行为首先尝试从缓存加载,若加载失败,则通过网络加载结果。如果缓存和网络获取行为均为失败,则产生 `LCError`。注意查询条件默认会从当前时间从新往旧查询,因此这种情况下第一次查询总需要访问网络。 | -| `kLCCachePolicyNetworkElseCache` | 查询行为先尝试从网络加载,若加载失败,则从缓存加载结果。如果缓存和网络获取行为均为失败,则产生 `LCError`。 | -| `kLCCachePolicyCacheThenNetwork` | 查询先从缓存加载,然后从网络加载。在这种情况下,回调函数会被调用两次,第一次是缓存中的结果,然后是从网络获取的结果。因为它会在不同的时间返回两个结果,所以该策略不能与 `findObjects` 同时使用。 | - -#### 缓存相关的操作 - -- 检查是否存在缓存查询结果: - - ```objc - BOOL isInCache = [query hasCachedResult]; - ``` - -- 删除某一查询的任何缓存结果(删除缓存只影响持久化缓存(磁盘缓存),不影响内存缓存,下同): - - ```objc - [query clearCachedResult]; - ``` - -- 删除查询的所有缓存结果: - - ```objc - [LCQuery clearAllCachedResults]; - ``` - -- 设定缓存结果的最长时限: - - ```objc - query.maxCacheAge = 60 * 60 * 24; // 一天的总秒数 - ``` - -查询缓存也适用于 `LCQuery` 的辅助方法,包括 `getFirstObject` 和 `getObjectInBackground`。 - -### 查询性能优化 - -影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。 - -- 不等于和不包含查询(无法使用索引) -- 通配符在前面的字符串查询(无法使用索引) -- 有条件的 `count`(需要扫描所有数据) -- `skip` 跳过较多的行数(相当于需要先查出被跳过的那些行) -- 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引) -- 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据) - - - -## LiveQuery - -LiveQuery 衍生于 [`LCQuery`](#查询),并为其带来了更强大的功能。它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。 - -设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用 `LCQuery` 并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。 - -想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的 `LCQuery`。订阅成功后,一旦有符合 `LCQuery` 的 `LCObject` 发生变化,云端就会主动、实时地将信息通知到客户端。 - -LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。 - -### 启用 LiveQuery - -进入 ** > 服务设置**,在 **安全设置** 里面勾选 **启用 LiveQuery** 即可。 - -如果没有集成 `Realtime` 模块,需先集成 `Realtime` 模块,pod 添加示例如下: - -```ruby -pod 'LeanCloudObjc/Realtime' -``` - -可以在 [SDK 安装与初始化](#sdk-安装与初始化) 中找到完整设置方法。 - -之后在相关头文件中导入 `Realtime` 模块,示例如下: - -```objc -#import -``` - -### Demo - -下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果: - - - -使用我们的「LeanTodo」微信小程序和网页应用,可以实际体验以上视频所演示的效果,步骤如下: - -1. 微信扫码,添加小程序「LeanTodo」; - - ![LeanTodo mini program](/img/leantodo-weapp-qr.jpg) - -2. 进入小程序,点击首页左下角 **设置** > **账户设置**,输入便于记忆的用户名和密码; - -3. 使用浏览器访问 ,输入刚刚在小程序中更新好的账户信息,点击 **Login**; - -4. 随意添加更改数据,查看两端的同步状态。 - -注意按以上顺序操作。在网页应用中使用 **Signup** 注册的账户无法与小程序创建的账户相关联,所以如果颠倒以上操作顺序,则无法观测到数据同步效果。 - -[LiveQuery 公开课](http://www.bilibili.com/video/av11291992/) 涵盖了许多开发者关心的问题和解答。 - -### 构建订阅 - -首先创建一个普通的 `LCQuery` 对象,添加查询条件(如有),然后进行订阅操作: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -self.liveQuery = [[LCLiveQuery alloc] initWithQuery:query]; -self.liveQuery.delegate = self; -[self.liveQuery subscribeWithCallback:^(BOOL succeeded, NSError * _Nonnull error) { - // 订阅成功 -}]; -``` - -LiveQuery 不支持内嵌查询,也不支持返回指定属性。 - -订阅成功后,就可以接收到和 `LCObject` 相关的更新了。假如在另一个客户端上创建了一个 `Todo` 对象,对象的 `title` 设为 `更新作品集`,那么下面的代码可以获取到这个新的 `Todo`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -self.liveQuery = [[LCLiveQuery alloc] initWithQuery:query]; -self.liveQuery.delegate = self; -[self.liveQuery subscribeWithCallback:^(BOOL succeeded, NSError * _Nonnull error) { - // 订阅成功 -}]; -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidCreate:(id)object { - if (liveQuery == self.liveQuery) { - NSLog(@"%@", object[@"title"]); // 更新作品集 - } -} -``` - -此时如果有人把 `Todo` 的 `content` 改为 `把我最近画的插画放上去`,那么下面的代码可以获取到本次更新: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidUpdate:(id)updatedTodo updatedKeys:(NSArray *)updatedKeys { - NSLog(@"%@", updatedTodo[@"content"]); // 把我最近画的插画放上去 -} -``` - -### 事件处理 - -订阅成功后,可以选择监听如下几种数据变化: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` 事件 - -当有新的满足 `LCQuery` 查询条件的 `LCObject` 被创建时,`create` 事件会被触发。下面的 `object` 就是新建的 `LCObject`: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidCreate:(id)object { - if (liveQuery == self.liveQuery) { - NSLog(@"对象被创建。"); - } -} -``` - -#### `update` 事件 - -当有满足 `LCQuery` 查询条件的 `LCObject` 被更新时,`update` 事件会被触发。下面的 `object` 就是有更新的 `LCObject`: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidUpdate:(id)object updatedKeys:(NSArray *)updatedKeys { - if (liveQuery == self.liveQuery) { - NSLog(@"对象被更新。"); - } -} -``` - -#### `enter` 事件 - -当一个已存在的、原本不符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后符合查询条件,`enter` 事件会被触发。下面的 `object` 就是进入 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidEnter:(id)object updatedKeys:(nonnull NSArray *)updatedKeys { - if (liveQuery == self.liveQuery) { - NSLog(@"对象进入。"); - } -} -``` - -注意区分 `create` 和 `enter` 的不同行为。如果一个对象已经存在,在更新之前不符合查询条件,而在更新之后符合查询条件,那么 `enter` 事件会被触发。如果一个对象原本不存在,后来被构建了出来,那么 `create` 事件会被触发。 - -#### `leave` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后不符合查询条件,`leave` 事件会被触发。下面的 `object` 就是离开 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidLeave:(id)object updatedKeys:(nonnull NSArray *)updatedKeys { - if (liveQuery == self.liveQuery) { - NSLog(@"对象离开。"); - } -} -``` - -#### `delete` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 被删除,`delete` 事件会被触发。下面的 `object` 就是被删除的 `LCObject` 的 `objectId`: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidDelete:(id)object { - if (liveQuery == self.liveQuery) { - NSLog(@"对象被删除。"); - } -} -``` - -### 取消订阅 - -如果不再需要接收有关 `LCQuery` 的更新,可以取消订阅。 - -```objc -[liveQuery unsubscribeWithCallback:^(BOOL succeeded, NSError * _Nonnull error) { - if (succeeded) { - // 成功取消订阅 - } else { - // 错误处理 - } -}]; -``` - -### 断开连接 - -断开连接有几种情况: - -1. 网络异常或者网络切换,非预期性断开。 -2. 退出应用、关机或者打开飞行模式等,用户在应用外的操作导致断开。 - -如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。 - -而另外一种极端情况——**当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下**,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。 - -### LiveQuery 的注意事项 - -因为 LiveQuery 的实时性,很多用户会陷入一个误区,试着用 LiveQuery 来实现一个简单的聊天功能。 -我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,后期维护的难度非常大(聊天记录、对话维护之类的代码会很混乱),并且云服务已经提供了即时通讯的服务。 -LiveQuery 的核心还是提供一个针对查询的推拉结合的用法,脱离设计初衷容易造成前端的模块混乱。 - - - -## 文件 - -有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用 `LCObject` 保存,此时文件对象 `LCFile` 便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。 - -### 构建文件 - -可以通过字符串构建文件: - -```objc -NSData *data = [@"LeanCloud" dataUsingEncoding:NSUTF8StringEncoding]; -// resume.txt 是文件名 -LCFile *file = [LCFile fileWithData:data name:@"resume.txt"]; -``` - -除此之外,还可以通过 URL 构建文件: - -```objc -LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png"]]; -``` - -通过 URL 构建文件时,SDK 并不会将原本的文件转储到云端,而是会将文件的物理地址存储为字符串,这样也就不会产生任何文件上传流量。使用其他方式构建的文件会被保存在云端。 - -与前面提到的方式相比,一个更常见的文件构建方式是从本地路径上传。 - -```objc -NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); -NSString *documentsDirectory = [paths objectAtIndex:0]; -NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:@"avatar.jpg"]; -NSError *error; -LCFile *file = [LCFile fileWithLocalPath:imagePath error:&error]; -``` - -这里上传的文件名字叫做 `avatar.jpg`。需要注意: - -- 每个文件会被分配到一个独一无二的 `objectId`,所以在一个应用内是允许多个文件重名的。 -- 文件必须有扩展名才能被云端正确地识别出类型。比如说要用 `LCFile` 保存一个 PNG 格式的图像,那么扩展名应为 `.png`。 -- 如果文件没有扩展名,且没有手动指定类型,那么云服务将默认使用 `application/octet-stream`。 - -### 保存文件 - -将文件保存到云端后,便可获得一个永久指向该文件的 URL: - -```objc -[file uploadWithCompletionHandler:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"文件保存完成。URL:%@", file.url); - } else { - // 保存失败,可能是文件无法被读取,或者上传过程中出现问题 - } -}]; -``` - -文件上传后,可以在 ** > 文件** 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的 `objectId` 和 URL。 - -已经保存到云端的文件可以关联到 `LCObject`: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; -[todo setObject:@"买蛋糕" forKey:@"title"]; -// attachments 是一个 Array 属性 -[todo addObject:file forKey:@"attachments"]; -[todo saveInBackground]; -``` - -也可以通过构建 `LCQuery` 进行[查询](#查询): - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"_File"]; -``` - -需要注意的是,内部文件(上传到文件服务的文件)的 `url` 字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 -因此,通过 `url` 字段查询文件仅适用于外部文件(直接保存外部 URL 到文件服务创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。 - -注意,如果文件被保存到了 `LCObject` 的一个数组属性中,那么在查询 `LCObject` 时如果需要包含文件,则要用到 `LCQuery` 的 `includeKey` 方法。比如说,在获取所有标题为 `买蛋糕` 的 todo 的同时获取附件中的文件: - -```objc -// 获取同一标题且包含附件的 todo -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"title" equalTo:@"买蛋糕"]; -[query whereKeyExists:@"attachments"]; - -// 同时获取附件中的文件 -[query includeKey:@"attachments"]; - -[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable todos, NSError * _Nullable error) { - for (LCObject *todo in todos) { - // 获取每个 todo 的 attachments 数组 - } -}]; -``` - -### 上传进度监听 - -上传过程中可以实时向用户展示进度: - -```objc -[file uploadWithProgress:^(NSInteger percent) { - // percent 是一个 0 到 100 之间的整数,表示上传进度 -} completionHandler:^(BOOL succeeded, NSError *error) { - // 保存后的操作 -}]; -``` - -### 文件元数据 - -上传文件时,可以用 `metaData` 添加额外的属性。文件一旦保存,`metaData` 便不可再修改。 - -```objc -// 设置元数据 -[file.metaData setObject:@"LeanCloud" forKey:@"author"]; -[file uploadWithCompletionHandler:^(BOOL succeeded, NSError *error) { - // 获取全部元数据 - NSDictionary *metadata = file.metaData; - // 获取 author 属性 - NSString *author = metadata[@"author"]; - // 获取文件名 - NSString *fileName = file.name; - // 获取大小(不适用于通过 base64 编码的字符串或者 URL 保存的文件) - NSUInteger *size = file.size; -}]; -``` - -### 图像缩略图 - -成功保存图像后,除了可以获取指向该文件的 URL 外,还可以获取图像的缩略图 URL,并且可以指定缩略图的宽度和高度: - -```objc -LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"文件-url"]]; -[file getThumbnail:YES width:100 height:100 withBlock:^(UIImage *image, NSError *error) { - // 其他逻辑 -}]; -``` - -图片最大不超过 **20 MB** 才可以获取缩略图。 - -国际版不支持图片缩略图。 - -### 下载文件 - -客户端 SDK 接口可以下载文件并把它缓存起来,只要文件的 URL 不变,那么一次下载成功之后,就不会再重复下载,目的是为了减少客户端的流量。 - -```objc -[file downloadWithProgress:^(NSInteger number) { - // 下载的进度数据,number 介于 0 和 100 -} completionHandler:^(NSURL * _Nullable filePath, NSError * _Nullable error) { - // filePath 是文件下载到本地的地址 -}]; -``` - -`filePath` 是一个相对路径,文件存储在缓存目录(使用缓存功能)或系统临时目录(不使用缓存功能)中。 - -请注意代码中 `下载进度` 数据的读取。 - -### 清除缓存 - -LCFile 也提供了清除缓存的方法: - -```objc -// 清除当前文件缓存 -- (void)clearPersistentCache; - -// 类方法,清除所有缓存 -+ (BOOL)clearAllPersistentCache; -``` - -### 删除文件 - -下面的代码从云端删除一个文件: - -```objc -LCFile *file = [LCFile getFileWithObjectId:@"552e0a27e4b0643b709e891e"]; -[file deleteInBackground]; -``` - -默认情况下,文件的删除权限是关闭的。 -如需删除文件,一般建议在服务端使用 Master Key 调用 REST API 删除。 -取决于产品的具体需求,也可以考虑在 ** > 文件 > 权限** 向特定用户或特定角色开启删除权限。 - -### 文件审核 - -当前**文件审核**功能支持检测**图片**文件。 - -你可以在 **数据存储 > 文件 > 文件审核** 标签下勾选「自动审核新上传图片」,还可以批量审核指定时间范围内的图片,图片审核结果将在 **文件管理** 标签页展示。 - -如果你需要人工二次审核,可以点击每一行记录,在文件详情中选择「通过」或「封禁」。 - -### iOS 9 适配 - -从 iOS 9 开始,Apple 默认屏蔽 HTTP 访问,只支持 HTTPS 访问。云服务除了 `LCFile` 的 `getData` 之外的 API 都支持通过 HTTPS 访问。 - -如果你仍然需要 HTTP 访问,你可以 **为项目配置访问策略来允许 HTTP 访问**,从而解决这个问题。方法如下: - -选择项目的 `Info.plist`,右击选择 **Opened As** > **Source Code**,在 **plist** > **dict** 节点中加入以下文本: - -```xml -NSAppTransportSecurity - - NSExceptionDomains - - clouddn.com - - NSIncludesSubdomains - - NSTemporaryExceptionAllowsInsecureHTTPLoads - - - - -``` - -或者在 **Target** 的 **Info** 标签中修改配置: - -![在「NSAppTransportSecurity > NSExceptionDomains > clouddn.com」下面分别添加「NSTemporaryExceptionAllowsInsecureHTTPLoads」和「NSIncludesSubdomains」两个 Boolean 字段并将它们的值设为 YES。](/img/ios_qiniu_http.png) - -你也可以根据项目需要,允许所有的 HTTP 访问,更多可参考 [iOS 9 适配系列教程](https://github.com/ChenYilong/iOS9AdaptationTips)。 - -## GeoPoint - -云服务允许你通过将 `LCGeoPoint` 关联到 `LCObject` 的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。 - -要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个 `LCGeoPoint` 并将其纬度(`latitude`)设为 `39.9`,经度(`longitude`)设为 `116.4`: - -```objc -LCGeoPoint *point = [LCGeoPoint geoPointWithLatitude:39.9 longitude:116.4]; -``` - -现在可以将这个地理位置存储为一个对象的属性: - -```objc -[todo setObject:point forKey:@"location"]; -``` - -### 地理位置查询 - -给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 `LCQuery` 添加 `nearGeoPoint` 条件。下面的代码查找 `location` 属性值离某一点最近的 `Todo` 对象: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -LCGeoPoint *point = [LCGeoPoint geoPointWithLatitude:39.9 longitude:116.4]; -[query whereKey:@"location" nearGeoPoint:point]; - -// 限制为 10 条结果 -query.limit = 10; -[query findObjectsInBackgroundWithBlock:^(NSArray *todos, NSError *error) { - // todos 是包含满足条件的 Todo 对象的数组 -}]; -``` - -像 `orderByAscending` 和 `orderByDescending` 这样额外的排序条件会获得比默认的距离排序更高的优先级。 - -若要限制结果和给定地点之间的距离,可以参考 API 文档中的 `withinKilometers`、`withinMiles` 和 `withinRadians` 参数。 - -若要查询在某一矩形范围内的对象,可以用 `withinGeoBoxFromSouthwest` 和 `toNortheast`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -LCGeoPoint *southwest = [LCGeoPoint geoPointWithLatitude:30 longitude:115]; -LCGeoPoint *northeast = [LCGeoPoint geoPointWithLatitude:40 longitude:118]; -[query whereKey:@"location" withinGeoBoxFromSouthwest:southwest toNortheast:northeast]; -``` - -### GeoPoint 的注意事项 - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -iOS 8.0 之后,使用定位服务之前,需要调用 `[locationManager requestWhenInUseAuthorization]` 或 `[locationManager requestAlwaysAuthorization]` 来获取用户的「使用期授权」或「永久授权」,而这两个请求授权需要在 `info.plist` 里面对应添加 `NSLocationWhenInUseUsageDescription` 或 `NSLocationAlwaysUsageDescription` 的键值对,值为开启定位服务原因的描述。SDK 内部默认使用的是「使用期授权」。 - -## 用户 - -请参阅[内建账户指南](/sdk/authentication/guide/)。 - -## 角色 - -随着用户量的增长,你可能会发现相比于为每一名用户单独设置权限,将预先设定好的权限直接分配给一部分用户是更好的选择。为了迎合这种需求,云服务支持基于角色的权限管理。请参阅[ACL 权限管理开发指南](/sdk/storage/guide/acl/)。 - -## 子类化 - -子类化推荐给进阶的开发者在进行代码重构的时候做参考。你可以用 `LCObject` 访问到所有的数据,用 `objectForKey:` 获取任意字段。在成熟的代码中,子类化有很多优势,包括降低代码量,具有更好的扩展性,和支持自动补全。 - -子类化是可选的,请对照下面的例子来加深理解: - -```objc -LCObject *student = [LCObject objectWithClassName:@"Student"]; -[student setObject:@"小明" forKey:@"name"]; -[student saveInBackground]; -``` - -可改写成: - -```objc -Student *student = [Student object]; -student.name = @"小明"; -[student saveInBackground]; -``` - -这样代码看起来是不是更简洁呢? - -### 子类化的实现 - -要实现子类化,需要下面几个步骤: - -1. 导入 `LCObject+Subclass.h`; -2. 继承 `LCObject` 并实现 `LCSubclassing` 协议; -3. 实现类方法 `parseClassName`,返回的字符串是原先要传给 `initWithClassName:` 的参数,这样后续就不必再进行类名引用了。如果不实现,默认返回的是类的名字。**请注意:`LCUser` 子类化后必须返回 `_User`**; -4. 在实例化子类之前调用 `[YourClass registerSubclass]`(**在应用当前生命周期中,只需要调用一次**。可在子类的 `+load` 方法或者 `UIApplication` 的 `-application:didFinishLaunchingWithOptions:` 方法里面调用)。 - -下面是实现 `Student` 子类化的例子: - -```objc -// Student.h -@interface Student : LCObject - -@property(nonatomic,copy) NSString *name; - -@end - - -// Student.m -#import "Student.h" - -@implementation Student - -@dynamic name; - -+ (NSString *)parseClassName { - return @"Student"; -} - -@end - - -// AppDelegate.m -#import "Student.h" - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { -[Student registerSubclass]; -[LCApplication setApplicationId:{{appid}} - clientKey:{{appkey}} - serverURLString:"https://please-replace-with-your-customized.domain.com"]; -} -``` - -### 属性 - -为 `LCObject` 的子类添加自定义的属性和方法,可以更好地将这个类的逻辑封装起来。用 `LCSubclassing` 可以把所有的相关逻辑放在一起,这样不必再使用不同的类来区分业务逻辑和存储转换逻辑了。 - -`LCObject` 支持动态 synthesizer,就像 `NSManagedObject` 一样。先正常声明一个属性,只是在 `.m` 文件中把 `@synthesize` 变成 `@dynamic`。 - -请看下面的例子是怎么添加一个「年龄」属性: - -```objc -// Student.h -@interface Student : LCObject - -@property int age; - -@end - - -// Student.m -#import "Student.h" - -@implementation Student - -@dynamic age; -``` - -这样就可以通过 `student.age = 19` 这样的方式来读写 `age` 字段了,当然也可以写成: - -```objc -[student setAge:19] -``` - -**注意:属性名称保持首字母小写**(错误:`student.Age`;正确:`student.age`)。 - -`NSNumber` 类型的属性可用 `NSNumber` 或者是它的原始数据类型(`int`、`long` 等)来实现。例如,`[student objectForKey:@"age"]` 返回的是 `NSNumber` 类型,而实际被设为 `int` 类型。 - -你可以根据自己的需求来选择使用哪种类型。原始类型更为易用,而 `NSNumber` 支持 `nil` 值,这可以让结果更清晰易懂。 - -注意:`LCRelation` 同样可以作为子类化的一个属性来使用,比如: - -```objc -@interface Student : LCUser -@property(retain) LCRelation *friends; -// ... -@end - -@implementation Student -@dynamic friends; -// ... -``` - -另外,值为 `Pointer` 的实例可用 `LCObject*` 来表示。例如,如果 `Student` 中 `bestFriend` 代表一个指向另一个 `Student` 的键,由于 `Student` 是一个 `LCObject`,因此在表示这个键的值的时候,可以用一个 `LCObject*` 来代替: - -```objc -@interface Student : LCUser -@property(nonatomic, strong) LCObject *bestFriend; - … -@end - -@implementation Student -@dynamic bestFriend; - … -``` - -提示:当需要更新的时候,最后都要记得加上 `[student save]` 或者对应的后台存储函数进行更新,才会同步至服务器。 - -如果要使用更复杂的逻辑而不是简单的属性访问,可以这样实现: - -```objc - @dynamic iconFile; - - - (UIImageView *)iconView { - UIImageView *view = [[UIImageView alloc] initWithImage:kPlaceholderImage]; - view.image = [UIImage imageNamed:self.iconFile]; - return [view autorelease]; - } - -``` - -### 针对 LCUser 子类化的特别说明 - -假如现在已经有一个基于 `LCUser` 的子类,如上面提到的 `Student`: - -```objc -@interface Student : LCUser -@property NSString *displayName; -@end - - -@implementation Student -@dynamic displayName; -+ (NSString *)parseClassName { - return @"_User"; -} -@end -``` - -登录时需要调用 `Student` 的登录方法才能通过 `currentUser` 得到这个子类: - -```objc -[Student logInWithUsernameInBackground:@"USER_NAME" password:@"PASSWORD" block:^(LCUser *user, NSError *error) { - Student *student = [Student currentUser]; - student.displayName = @"YOUR_DISPLAY_NAME"; - }]; -``` - -同样需要调用 `[Student registerSubclass];`,确保在其他地方得到的对象是 `Student`,而非 `LCUser`。 - -### 初始化子类 - -创建一个子类实例,要使用 `object` 类方法。要创建并关联到已有的对象,请使用 `objectWithObjectId:` 类方法。 - -### 子类查询 - -使用类方法 `query` 可以得到这个子类的查询对象。 - -例如,查询年龄小于 21 岁的学生: - -```objc - LCQuery *query = [Student query]; - [query whereKey:@"age" lessThanOrEqualTo:@(21)]; - [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { - if (!error) { - Student *stu1 = [objects objectAtIndex:0]; - // … - } - }]; -``` - -## 全文搜索 - -全文搜索是一个针对应用数据进行全局搜索的接口,它基于搜索引擎构建,提供更强大的搜索功能。要深入了解其用法和阅读示例代码,请阅读[全文搜索指南](/sdk/storage/guide/fulltext-search/)。 diff --git a/leancloud/docs/sdk/storage/objc-guide/setup-objc.mdx b/leancloud/docs/sdk/storage/objc-guide/setup-objc.mdx deleted file mode 100644 index c03eff87d..000000000 --- a/leancloud/docs/sdk/storage/objc-guide/setup-objc.mdx +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: 数据存储 Objective-C SDK 配置指南 -sidebar_label: Objective-C SDK 配置 -slug: /sdk/storage/guide/setup-objc/ -sidebar_position: 5 ---- - -import DomainBinding from "../../_partials/setup-domain.mdx"; -import AppConfig from "../_partials/app-config.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -## 获取 SDK - -获取 SDK 有多种方式,较为推荐的方式是通过包依赖管理工具下载最新版本。 - -### 包依赖管理工具安装 - -通过 [CocoaPods](https://cocoapods.org) 安装可以最大化地简化安装过程。 - -首先,确保开发环境中已经安装了最新版 `pod`。如果没有,请参考官网的 [INSTALL](https://cocoapods.org) 文档。 - -接着,在项目根目录下通过命令行工具执行下列命令生成 `Podfile` 文件: - -```sh -$ pod init -``` - -参考 [GET STARTED](https://cocoapods.org) 文档,在 `Podfile` 文件中的 `target` 里添加以下 pod 依赖: - -```ruby -pod 'LeanCloudObjc' # 集成所有服务模块 -``` - -`LeanCloudObjc` 包含多个 Subspecs。如果只需要部分功能,可以按需选择: - -```ruby -pod 'LeanCloudObjc/Foundation' # 数据存储、短信、推送、云引擎等基础服务模块 -pod 'LeanCloudObjc/Realtime' -``` - -最后,在项目根目录下执行下列任意命令,集成最新的 SDK: - -```sh -$ pod update -``` - -或者 - -```sh -$ pod install --repo-update -``` - -集成 SDK 成功后,使用项目根目录下 **`<项目名称>.xcworkspace`** 来打开项目。 - -### 手动安装 - -在 [SDK 下载页面](https://releases.leanapp.cn/#/leancloud/objc-sdk/releases) 下载最新版的源码。 - -将 `AVOS`/`AVOS.xcodeproj` 项目文件拖入示例项目,作为 subproject: - -![「AVOS.xcodeproj」会出现在项目根目录下。](/img/quick_start/ios/subproject.png) - -接着为示例项目连接依赖库,在 **xcodeproj > target > general > frameworks** 添加如下内容: - -![「LeanCloudObjc.framework」](/img/quick_start/ios/link-binary.png) - -这样就集成完毕了。 - -## 初始化 - - - -:::info -[TapSDK 初始化](/sdk/start/quickstart/#初始化)时,会自动执行这一节的初始化方法。 - -如果已经参考 **快速开始** 文档完成了 TapSDK 初始化,则**不需要**参考这里的初始化。 -::: - - - -打开 `AppDelegate` 文件,导入基础模块头文件: - -```objc -#import -``` - -然后在 `application:didFinishLaunchingWithOptions:` 方法中设置 `App ID`,`App Key` 以及服务器地址: - - - -```objc -[LCApplication setApplicationId:@"your-client-id" - clientKey:@"your-client-token" - serverURLString:@"https://your_server_url"]; -``` - - - - - -```objc -[LCApplication setApplicationId:@"your-app-id" - clientKey:@"your-app-key" - serverURLString:@"https://your_server_url"]; -``` - - - -在使用 SDK 的 API 时,请确保进行了 Application 的 ID、Key 以及 Server URL 的初始化。 - -### 应用凭证 - - - -## 域名 - - - -## 开启调试日志 - -在应用开发阶段,你可以选择开启 SDK 的调试日志(debug log)来方便追踪问题。调试日志开启后,SDK 会把网络请求、错误消息等信息输出到 IDE 的日志窗口,或是浏览器 Console 或是云引擎日志(如果在云引擎下运行 SDK)。 - -```objc -// 在 Application 初始化代码执行之前执行 -[LCApplication setAllLogsEnabled:true]; -``` - -详细调试流程可以参考 [Objective-C SDK 调试指南][objc-debug-guide]。 - -[objc-debug-guide]: https://forum.leancloud.cn/t/leancloud-sdk-objective-c-sdk/21851 - -:::caution -在应用发布之前,请关闭调试日志,以免暴露敏感数据。 -::: - -## 验证 - -首先,确认本地网络环境是可以访问云端服务器的,可以执行以下命令: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` 为绑定的 API 自定义域名。 - -如果当前网络正常会返回当前时间: - -```json -{ "__type": "Date", "iso": "2020-10-12T06:46:56.000Z" } -``` - -下面来试着向云端保存一条数据,将下面的代码拷贝到 `viewDidLoad` 方法或其他在应用运行时会被调用的方法中: - -```objc -LCObject *testObject = [LCObject objectWithClassName:@"TestObject"]; -[testObject setObject:@"Hello world!" forKey:@"words"]; -[testObject save]; -``` - -然后,点击 `Run` 运行调试,真机和虚拟机均可。 - -然后打开 ** > 结构化数据 > `TestObject`**,如果看到数据表中出现一行「words」列的值为「Hello world!」的数据,说明 SDK 已经正确地执行了上述代码,配置完毕。 - -如果控制台没有发现对应的数据,请参考 [问题排查](#问题排查)。 - -## 问题排查 - -SDK 安装指南基于当前最新版本的 SDK 编写,所以排查问题前,请先检查下安装的 SDK 是不是最新版本。 - -### `401 Unauthorized` - -如果 SDK 抛出 `401` 异常或者查看本地网络访问日志存在: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -则可认定为 `App ID` 或者 `App Key` 输入有误,或者是不匹配,很多开发者同时注册了多个应用,导致拷贝粘贴的时候,用 A 应用的 App ID 匹配 B 应用的 `App Key`,这样就会出现服务端鉴权失败的错误。 - -### 客户端无法访问网络 - -客户端尤其是手机端,应用在访问网络的时候需要申请一定的权限。 diff --git a/leancloud/docs/sdk/storage/rest.mdx b/leancloud/docs/sdk/storage/rest.mdx deleted file mode 100644 index 3bedec7ee..000000000 --- a/leancloud/docs/sdk/storage/rest.mdx +++ /dev/null @@ -1,1976 +0,0 @@ ---- -title: 存储 REST API -sidebar_label: REST API -slug: /sdk/storage/guide/rest/ -sidebar_position: 15 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -REST API 可以让你用任何支持发送 HTTP 请求的设备来与云服务进行交互,你可以使用 REST API 做很多事情,比如: - -- 使用任何编程语言操作云端数据。 -- 如果你不再需要使用云服务,你可以导出你所有的数据。 -- 一个追求最少化依赖库的应用可以不引入 SDK,直接访问 REST API 获取云服务上的数据。 -- 你可以批量新增大量数据,供应用之后读取。 -- 你可以下载最近的数据用于离线分析和归档备份。 - -## API 版本 - -当前的 API 版本是 `1.1`。 - -### 在线测试 - -为了方便测试 REST API,文档给出了 curl 命令示例,示例针对类 unix 平台(macOS、Linux 等)编写,直接粘贴至 Windows 平台 cmd.exe 很可能无法工作。 -例如,curl 命令示例中的 shell 换行符(`\`)在 cmd.exe 中是目录分隔符。 -Windows 平台建议使用 [Postman](https://www.postman.com/) 等客户端测试。 - -
    - -点击展开 Postman 示例 - -Postman 可直接导入 curl 命令。 - -![Postman 中点击 Import 按钮,在「Raw Text」标签中粘贴 curl 命令](/img/postman-import-curl.png) - -Postman 还支持自动生成多种语言(库)调用 REST API 的代码。 - -![Postman 中点击 Code,在展开的面板中选择语言(库)](/img/postman-generate-code.png) - -
    - -### Base URL - -REST API 请求的 Base URL(下文 curl 示例中用 `{{host}}` 表示)即应用绑定的 API 自定义域名,可以在 **云服务控制台 > 设置 > 域名绑定****开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置** 绑定、查看。 - - - -详见文档关于[域名](/sdk/domain/guide/)的说明。 - - - -### 对象 - -| URL | HTTP | 功能 | -| ----------------------------------------------- | ------ | ---------------------- | -| /1.1/classes/<className> | POST | 创建对象 | -| /1.1/classes/<className>/<objectId> | GET | 获取对象 | -| /1.1/classes/<className>/<objectId> | PUT | 更新对象 | -| /1.1/classes/<className> | GET | 查询对象 | -| /1.1/classes/<className>/<objectId> | DELETE | 删除对象 | -| /1.1/scan/classes/<className> | GET | 按照特定顺序遍历 Class | - -### 角色 - -| URL | HTTP | 功能 | -| --------------------------- | ------ | -------- | -| /1.1/roles | POST | 创建角色 | -| /1.1/roles/<objectId> | GET | 获取角色 | -| /1.1/roles/<objectId> | PUT | 更新角色 | -| /1.1/roles | GET | 查询角色 | -| /1.1/roles/<objectId> | DELETE | 删除角色 | - -### 数据 Schema - -| URL | HTTP | 功能 | -| ------------------------------ | ---- | ---------------------------- | -| /1.1/schemas | GET | 获取应用所有 Class 的 Schema | -| /1.1/schemas/<className> | GET | 获取应用指定 Class 的 Schema | - -### 其他 API - -| URL | HTTP | 功能 | -| -------------------------- | ---- | -------------------------- | -| /1.1/date | GET | 获得服务端当前时间 | -| /1.1/exportData | POST | 请求导出应用数据 | -| /1.1/exportData/<id> | GET | 获取导出数据任务状态和结果 | - -### 应用凭证 - - - -### 请求格式 - -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP header 的 `Content-Type` 需要设置为 `application/json`。 - -用户验证通过 HTTP header 来进行,`X-LC-Id` 标明正在运行的是哪个应用(应用的 `App ID`,对应到开发者中心游戏的 `Client ID`),`X-LC-Key` 用来授权鉴定 endpoint: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "更新一篇博客的内容"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -`X-LC-Key` 通常情况下是应用的 `App Key`(对应到开发者中心游戏的 `Client Token`),有些情况(需要超级权限的操作)下是应用的 `Master Key`(对应到开发者中心游戏的 `Server Secret`)。 -当 `X-LC-Key` 值为 `Master Key` 时,需要在其后添加 `,master` 后缀以示区分,例如: - -``` -X-LC-Key: {{masterkey}},master -``` - -对于 JavaScript 使用,云服务支持跨域资源共享,所以你可以将这些 header 同 `XMLHttpRequest` 一同使用。 - -REST API 通讯支持 `gzip` 和 `brotli` 压缩,客户端可以通过指定相应的 `Accept-Encoding` HTTP 头开启压缩。 - -#### 更安全的鉴权方式 - -我们还支持一种新的 API 鉴权方式,即在 HTTP header 中使用 `X-LC-Sign` 来代替 `X-LC-Key`,以降低 `App Key` 的泄露风险。例如: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" \ - -H "Content-Type: application/json" \ - -d '{"content": "在 HTTP header 中使用 X-LC-Sign 来更新一篇博客的内容"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -`X-LC-Sign` 的值是由 `sign,timestamp[,master]` 组成的字符串: - -| 取值 | 约束 | 描述 | -| --------- | ---- | ------------------------------------------------------------------------------------------------ | -| sign | 必须 | 将 timestamp 加上 `App Key` 或 `Master Key` 组成的字符串,再对它做 MD5 签名后的结果。 | -| timestamp | 必须 | 客户端产生本次请求的 unix 时间戳(UTC),精确到**毫秒**。 | -| master | 可选 | 字符串 `"master"`,当使用 master key 签名请求的时候,必须加上这个后缀明确说明是使用 master key。 | - -注意,`sign` 中的 MD5 签名的 hex 值中的字母请使用小写。如果使用大写字母,会导致签名校验失败。 - -举例来说,假设应用的信息如下: - - - - - - - - - - - - - - - - - - - - - - - - -
    App Id - FFnN2hso42Wego3pWq4X5qlu -
    App Key - UtOCzqb67d3sN12Kts4URwy8 -
    Master Key - DyJegPlemooo4X1tg94gQkw1 -
    请求时间2016-01-17 15:15:43.466 GMT+08:00
    timestamp - 1453014943466 -
    - -**使用 `App Key` 来计算 sign**: - -``` -md5( timestamp + App Key ) -= md5(1453014943466UtOCzqb67d3sN12Kts4URwy8) -= d5bcbb897e19b2f6633c716dfdfaf9be -``` - -```sh - -H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" \ -``` - -**使用 `Master Key` 来计算 sign**: - -``` -md5( timestamp + Master Key ) -= md5(1453014943466DyJegPlemooo4X1tg94gQkw1) -= e074720658078c898aa0d4b1b82bdf4b -``` - -```sh - -H "X-LC-Sign: e074720658078c898aa0d4b1b82bdf4b,1453014943466,master" \ -``` - -(最后加上 `,master` 来告诉服务器这个签名是使用 master key 生成的。) - -**使用 master key 将绕过所有权限校验,应该确保只在可控环境中使用,比如自行开发的管理平台,并且要完全避免泄露。** -以上两种计算 sign 的方法可以根据实际情况来选择一种使用。 - -#### 指定 hook 函数调用环境 - -请求可能触发云引擎的 hook 函数,可以通过设置 HTTP 头 `X-LC-Prod` 来区分调用的环境。 - -- `X-LC-Prod: 0` 表示调用预备环境 -- `X-LC-Prod: 1` 表示调用生产环境 - -默认(未指定 `X-LC-Prod` 头)调用生产环境的 hook 函数。 - -### 响应格式 - -对于所有的请求,响应格式都是一个 JSON 对象。 - -一个请求是否成功是由 HTTP 状态码标明的。一个 2XX 的状态码表示成功,而一个 4XX 表示请求失败。当一个请求失败时响应的主体仍然是一个 JSON 对象,但是总是会包含 `code` 和 `error` 这两个字段,你可以用它们来进行调试。举个例子,如果尝试用非法的属性名来保存一个对象会得到如下信息: - -```json -{ - "code": 105, - "error": "Invalid key name. Keys are case-sensitive and 'a-zA-Z0-9_' are the only valid characters. The column is: 'invalid?'." -} -``` - -## 对象 - -### 对象格式 - -数据存储服务是建立在 LCObject(对象)基础上的,每个 LCObject 包含若干属性值对(key-value,也称「键值对」),属性的值是与 JSON 格式兼容的数据。 -通过 REST API 保存对象需要将对象的数据通过 JSON 来编码。这个数据是无模式化的(schema free),这意味着你不需要提前标注每个对象上有哪些 key,你只需要随意设置键值对就可以,后端会保存它。 - -举个例子,假如我们要实现一个类似于微博的社交 App,主要有三类数据:账户、帖子、评论,一条微博帖子可能包含下面几个属性: - -```json -{ - "content": "每个 Java 程序员必备的 8 个开发工具", - "pubUser": "官方客服", - "pubTimestamp": 1435541999 -} -``` - -Key(属性名)必须是字母、数字、下划线组成的字符串,Value(属性值)可以是任何可以 JSON 编码的数据。 - -每个对象都有一个类名,你可以通过类名来区分不同的数据。例如,我们可以把微博的帖子对象称之为 Post。我们建议将类和属性名分别按照 `NameYourClassesLikeThis` 和 `nameYourKeysLikeThis` 这样的惯例来命名,即区分第一个字母的大小写,这样可以提高代码的可读性和可维护性。 - -当你从云端获取对象时,一些字段会被自动加上,如 `createdAt`、`updatedAt` 和 `objectId`。这些字段的名字是保留的,值也不允许修改。我们上面设置的对象在获取时应该是下面的样子: - -```json -{ - "content": "每个 Java 程序员必备的 8 个开发工具", - "pubUser": "官方客服", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -`createdAt` 和 `updatedAt` 都是 UTC 时间戳,以 ISO 8601 标准和毫秒级精度储存:`YYYY-MM-DDTHH:MM:SS.MMMZ`。`objectId` 是一个字符串,在类中可以唯一标识一个实例。 - -在 REST API 中,class 级的操作都是通过一个带类名的资源路径(URL)来标识的。例如,如果类名是 `Post`,那么 class 的 URL 就是: - -``` -https://{{host}}/1.1/classes/Post -``` - -对于**用户账户**这种对象,有一个特殊的 URL: - -``` -https://{{host}}/1.1/users -``` - -针对于一个特定的对象的操作可以通过组织一个 URL 来做。例如,对 `Post` 中的一个 `objectId` 为 `558e20cbe4b060308e3eb36c` 的对象的操作应使用如下 URL: - -``` -https://{{host}}/1.1/classes/Post/558e20cbe4b060308e3eb36c -``` - -### 创建对象 - -创建一个新的对象,应该向 class 的 URL 发送一个 **POST** 请求,其中应该包含对象本身。 -例如,要创建如上所说的对象: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "每个 Java 程序员必备的 8 个开发工具","pubUser": "官方客服","pubTimestamp": 1435541999}' \ - https://{{host}}/1.1/classes/Post -``` - -当创建成功时,HTTP 的返回是 **201 Created**,而 header 中的 Location 表示新的 object 的 URL: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/classes/Post/558e20cbe4b060308e3eb36c -``` - -响应的主体是一个 JSON 对象,包含新的对象的 objectId 和 createdAt 时间戳。 - -```json -{ - "createdAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -如果希望返回新创建的对象的完整信息,可以在 URL 里加上 `fetchWhenSave` 选项,并且设置为 true: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "每个 Java 程序员必备的 8 个开发工具","pubUser": "官方客服","pubTimestamp": 1435541999}' \ - https://{{host}}/1.1/classes/Post?fetchWhenSave=true -``` - -:::info -Class 名称只能包含字母、数字、下划线。 -**每个应用最多可以创建 500 个 class,每个 class 最多包含 300 个字段,**但每个 class 中的记录数量没有限制。 -::: - -### 获取对象 - -当你创建了一个对象时,你可以通过发送一个 GET 请求到返回的 header 的 Location 以获取它的内容。例如,为了得到我们上面创建的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/classes/Post/558e20cbe4b060308e3eb36c -``` - -返回的主体是一个 JSON 对象包含所有用户提供的 field 加上 createdAt、updatedAt 和 objectId 字段: - -```json -{ - "content": "每个 Java 程序员必备的 8 个开发工具", - "pubUser": "官方客服", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -当获取的对象有指向其子对象的指针时,你可以加入 `include` 选项来获取这些子对象。假设微博记录中有一个字段 `author` 来指向发布者的账户信息,按上面的例子,可以这样来连带获取发布者完整信息: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'include=author' \ - https://{{host}}/1.1/classes/Post/ -``` - -`include` 支持点号,例如,假定发布者有一个字段 `department` 指向发布者所属的部门,那么可以使用 `include=author.department` 一并获取部门信息。 - -类不存在时,返回 404 Not Found 错误: - -```json -{ - "code": 101, - "error": "Class or object doesn't exists." -} -``` - -objectId 不存在时,返回一个空对象(HTTP 状态码为 200 OK): - -```json -{} -``` - -某些特殊的系统内置类(类名以下划线开头),objectId 不存在时不一定返回空对象。 -例如,查询 `_User` 时,objectId 不存在会返回 400 Bad Request 错误: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/classes/_User/ -``` - -返回: - -```json -{ - "code": 211, - "error": "Could not find user." -} -``` - -顺带提一下,获取用户推荐使用 `GET /users/`,而不是直接查询 `_User` 类。 -参见后文[获取用户](/sdk/authentication/rest/#获取用户)一节。 - -### 更新对象 - -为了更改一个对象已经有的数据,你可以发送一个 PUT 请求到对象相应的 URL 上,任何你未指定的 key 都不会更改,所以你可以只更新对象数据的一个子集。例如,我们来更改我们对象的一个 content 字段: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "每个 JavaScript 程序员必备的 8 个开发工具:http://buzzorange.com/techorange/2015/03/03/9-javascript-ide-editor/"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -返回的 JSON 对象只会包含一个 updatedAt 字段,表明更新发生的时间: - -```json -{ - "updatedAt": "2015-06-30T18:02:52.248Z" -} -``` - -`fetchWhenSave` 选项对更新对象也同样有效。 -但和创建对象不同,用于更新对象时仅返回更新的字段,而非全部字段。 - -#### 计数器 - -比如一条微博,我们需要记录有多少人喜欢或者转发了它,但可能很多次喜欢都是同时发生的,如果每个客户端都直接把读到的计数值更改之后再写回去,那么极容易引发冲突和覆盖,导致最终结果不准。 -云服务提供了对数字类型字段进行原子增加或者减少的功能,稳妥地实现对计数器类型数据的更新: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"upvotes":{"__op":"Increment","amount":1}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -这样就将对象的 `upvotes` 属性值(被用户点赞的次数)加上 1,其中 `amount` 为递增的数字大小,如果为负数,则为递减。 - -除了 Increment,我们也提供了 Decrement 用于递减,等价于 Increment 一个负数。 - -:::info -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,**需要原子增减的字段建议使用整数**以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 -::: - -#### 位运算 - -如果数据表的某一列是整型,可以使用位运算操作符该列进行原子的位运算: - -- BitAnd 与运算 -- BitOr 或运算 -- BitXor 异或运算 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"flags":{"__op":"BitOr","value": 0x0000000000000004}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -#### 数组 - -有 3 种原子性操作可用于存储和更改数组类型的字段: - -- **Add**:在一个数组字段的后面添加一些指定的对象(包装在一个数组内) -- **AddUnique**:只会在数组内原本没有这个对象的情形下才会添加入数组,插入的位置不定。 -- **Remove**:从一个数组内移除所有的指定的对象 - -每种操作都有一个 key `objects`,其值为被添加或删除的对象列表。例如为每条微博增加一个「标签」属性 tags,然后往里面加入一些值: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"tags":{"__op":"AddUnique","objects":["Frontend","JavaScript"]}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -#### 有条件更新对象 - -假设从某个账户对象 Account 的余额中扣除一定金额,但是要求余额要大于等于被扣除的金额才允许操作,那么就需要通过 `where` 参数为更新操作加上限定条件 `balance >= amount`: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"balance":{"__op":"Decrement","amount": 30}}' \ - "https://{{host}}/1.1/classes/Account/558e20cbe4b060308e3eb36c?where=%7B%22balance%22%3A%7B%22%24gte%22%3A%2030%7D%7D" -``` - -URL 中 where 参数的值是 `%7B%22balance%22%3A%7B%22%24gte%22%3A%2030%7D%7D`,其实这是 `{"balance":{"$gte": 30}}` 被 URL 编码后的结果。 - -如果条件不满足,更新将失败,同时返回错误码 `305`: - -```json -{ - "code": 305, - "error": "No effect on updating/deleting a document." -} -``` - -**特别强调:where 一定要作为 URL 的 Query Parameters 传入。** - -#### \_\_op 操作汇总 - -使用 `__op("操作名称", {JSON 参数})` 函数可以完成原子性操作,确保数据的一致性。 - -| 操作 | 说明 | 示例 | -| -------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- | -| Delete | 删除对象的一个属性 | `__op('Delete', {'delete': true})` | -| Add | 在数组末尾添加对象 | `__op('Add',{'objects':['Apple','Google']})` | -| AddUnique | 在数组末尾添加不会重复的对象,插入位置不定 | `__op('AddUnique', {'objects':['Apple','Google']})` | -| Remove | 从数组中删除对象 | `__op('Remove',{'objects':['Apple','Google']})` | -| AddRelation | 添加一个关系 | `__op('AddRelation', {'objects':[pointer('_User','558e20cbe4b060308e3eb36c')]})` | -| RemoveRelation | 删除一个关系 | `__op('RemoveRelation', {'objects':[pointer('_User','558e20cbe4b060308e3eb36c')]})` | -| Increment | 递增 | `__op('Increment', {'amount': 50})` | -| Decrement | 递减 | `__op('Decrement', {'amount': 50})` | -| BitAnd | 与运算 | `__op('BitAnd', {'value': 0x0000000000000004})` | -| BitOr | 或运算 | `__op('BitOr', {'value': 0x0000000000000004})` | -| BitXor | 异或运算 | `__op('BitXor', {'value': 0x0000000000000004})` | - -### 删除对象 - -要在云端删除一个对象,可以发送一个 DELETE 请求到指定的对象的 URL,比如: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/classes/Post/ -``` - -还可以使用 Delete 操作删除一个对象的一个字段(注意此时 **HTTP Method 是 PUT**): - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"downvotes":{"__op":"Delete"}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -#### 有条件删除对象 - -为请求增加 `where` 参数即可以按指定的条件来删除对象。例如删除点击量 clicks 为 0 的帖子: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - "https://{{host}}/1.1/classes/Post/?where=%7B%22clicks%22%3A%200%7D" -``` - -URL 中 where 参数的值是 `%7B%22clicks%22%3A%200%7D`,其实这是 `{"clicks": 0}` 被 URL 编码后的结果。 - -如果条件不满足,删除将失败,同时返回错误码 `305`: - -```json -{ - "code": 305, - "error": "No effect on updating/deleting a document." -} -``` - -**特别强调:where 一定要作为 URL 的 Query Parameters 传入。** - -### 遍历 Class - -因为更新和删除都是基于单个对象的,都要求提供 objectId,但是有时候用户需要高效地遍历一个 Class,做一些批量的更新或者删除的操作。 - -通常情况下,如果 **Class 的数量规模不大**,使用查询加上 `skip` 和 `limit` 分页配合排序 `order` 就可以遍历所有数据。 - -但是当 **Class 数量规模比较大**的时候, `skip` 的效率就非常低了(这跟 MySQL 等关系数据库的原因一样,深度翻页比较慢)。 -为了避免性能问题,这种情况下可以通过指定 `createdAt` 或 `updatedAt` 的范围来实现翻页。 -我们还额外提供了 `scan` 接口,可以按照特定字段排序来高效地遍历一张表,相比指定 `createdAt` 或 `updatedAt` 范围来翻页的写法也更加直截了当。 - -默认情况下,按 `objectId` 升序,同时支持设置 `limit` 限定每一批次的返回数量,**默认 limit 为 100,最大可设置为 1000**: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - https://{{host}}/1.1/scan/classes/Article -``` - -`scan` 强制要求使用 `MasterKey`。 - -返回: - -```json -{ - "results": [ - { - "tags": ["clojure", "\u7b97\u6cd5"], - "createdAt": "2016-07-07T08:54:13.250Z", - "updatedAt": "2016-07-07T08:54:50.268Z", - "title": "clojure persistent vector", - "objectId": "577e18b50a2b580057469a5e" - } - //... - ], - "cursor": "pQRhIrac3AEpLzCA" -} -``` - -其中 `results` 对应的就是返回的对象列表,而 `cursor` 表示本次遍历当前位置的「指针」,当 `cursor` 为 null 的时候,表示已经遍历完成,如果不为 null,请继续传入 `cursor` 到 `scan` 接口就可以从上次到达的位置继续往后查找: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'cursor=pQRhIrac3AEpLzCA' \ - https://{{host}}/1.1/scan/classes/Article -``` - -每次返回的 `cursor` 的有效期是 10 分钟。 - -遍历还支持过滤条件,加入 where 参数: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'where={"score": 100}' \ - https://{{host}}/1.1/scan/classes/Article -``` - -默认情况下系统按 `objectId` 升序排序,增加 `scan_key` 参数可以使用其他字段来排序: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'scan_key=score' \ - https://{{host}}/1.1/scan/classes/Article -``` - -scan_key 也支持倒序,前面加个减号即可,例如 `-score`。 - -**自定义的 scan_key 需要满足严格单调递增的条件,并且 scan_key 不可作为 where 查询条件存在。** - -scan 不支持 include 参数,**调用 scan 时传入 include 参数是未定义行为**。 -如果遍历 Class 时需要使用 include 参数,请使用普通的查询,并通过指定 `createdAt` 或 `updatedAt` 的范围来实现翻页。 - -### 批量操作 - -为了减少网络交互的次数太多带来的时间浪费,你可以在一个请求中对多个对象进行 create、update、delete 操作。 - -在一个批次中每一个操作都有相应的方法、路径和主体,这些参数可以代替你通常会使用的 HTTP 方法。这些操作会以发送过去的顺序来执行,比如我们要一次发布一系列的微博: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "requests": [ - { - "method": "POST", - "path": "/1.1/classes/Post", - "body": { - "content": "2021 年 5 月 1 日至 2021 年 5 月 5 日放假五天,5 月 8 日调休正常上班。", - "pubUser": "官方客服" - } - }, - { - "method": "POST", - "path": "/1.1/classes/Post", - "body": { - "content": "我们将于 2021 年 2 月 10 日至 2021 年 2 月 17 日放假八天,2 月 18 日恢复正常工作,放假期间,运维团队仍将在线值班,以应对可能的突发情况,保障服务稳定。", - "pubUser": "官方客服" - } - } - ] - }' \ - https://{{host}}/1.1/batch -``` - -我们对每一批次中所包含的操作数量(requests 数组中的元素个数)暂不设限,但考虑到**云端对每次请求的 body 内容大小有 20 MB 的限制,因此建议将每一批次的操作数量控制在 100 以内**。 - -批量操作的响应 body 会是一个列表,列表的元素数量和顺序与给定的操作请求是一致的。每一个在列表中的元素都有一个字段是 success 或者 error。 - -```json -[ - { - "error": { - "code": 1, - "error": "Could not find object by id '558e20cbe4b060308e3eb36c' for class 'Post'." - } - }, - { - "success": { - "updatedAt": "2017-02-22T06:35:29.419Z", - "objectId": "58ad2e850ce463006b217888" - } - } -] -``` - -:::info -需要注意,即使一个 batch 请求返回的响应码为 200,这仅代表服务端已收到并处理了这个请求,但并不说明该 -batch 中的所有操作都成功完成,只有当返回 body 的列表中**不存在 error 元素**,开发者才可以认为所有操作都已成功完成。 -::: - -在 batch 操作中 update 和 delete 同样是有效的: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "requests": [ - { - "method": "PUT", - "path": "/1.1/classes/Post/55a39634e4b0ed48f0c1845b", - "body": { - "upvotes": 2 - } - }, - { - "method": "DELETE", - "path": "/1.1/classes/Post/55a39634e4b0ed48f0c1845c" - } - ] - }' \ - https://{{host}}/1.1/batch -``` - -批量操作还有一个冷门用途,代替 URL 过长的 GET(比如使用 containedIn 等方法构造查询)和 DELETE (比如批量删除)请求,以绕过服务端和某些客户端对 URL 长度的限制。 - -### 数据类型 - -到现在为止我们只使用了可以被标准 JSON 编码的值,客户端 SDK 同样支持日期、二进制数据和关系型数据。在 REST API 中,这些值都被编码了,同时有一个 `__type` 字段(注意:**前缀是两个下划线**)来标示出它们的类型,所以如果你采用正确的编码的话就可以读或者写这些字段。 - -**Date** 类型包含了一个 iso 字段,其值是一个 UTC 时间戳,以 ISO 8601 格式和毫秒级的精度来存储的时间值,格式为:`YYYY-MM-DDTHH:MM:SS.MMMZ`: - -```json -{ - "__type": "Date", - "iso": "2015-06-21T18:02:52.249Z" -} -``` - -Date 和内置的 createdAt 字段和 updatedAt 字段相结合的时候特别有用,举个例子:为了找到在一个特殊时间发布的微博,只需要将 Date 编码后放在使用了比较条件的查询里面: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"createdAt":{"$gte":{"__type":"Date","iso":"2015-06-21T18:02:52.249Z"}}}' \ - https://{{host}}/1.1/classes/Post -``` - -注意,由于 createdAt 和 updatedAt 属于保留字段,因此通过 REST API 请求这两个字段时,将直接返回 UTC 时间戳字符串。 - -**Byte** 类型包含了一个 base64 字段,这个字段是一些二进制数据编码过的 base64 字符串。base64 是 MIME 使用的标准,不包含空白符: - -```json -{ - "__type": "Bytes", - "base64": "5b6I5aSa55So5oi36KGo56S65b6I5Zac5qyi5oiR5Lus55qE5paH5qGj6aOO5qC877yM5oiR5Lus5bey5bCGIExlYW5DbG91ZCDmiYDmnInmlofmoaPnmoQgTWFya2Rvd24g5qC85byP55qE5rqQ56CB5byA5pS+5Ye65p2l44CC" -} -``` - -**Pointer** 类型是用来设定 LCObject 作为另一个对象的值时使用的,它包含了 className 和 objectId 两个属性值,用来提取目标对象: - -```json -{ - "__type": "Pointer", - "className": "Post", - "objectId": "55a39634e4b0ed48f0c1845c" -} -``` - -指向用户对象的 Pointer 的 className 为 `_User`,前面加一个下划线表示开发者不能定义的类名,而且所指的类是内置的。 -类似地,指向角色和 Installation 的 Pointer 的 className 为 `_Role` 和 `_Installation`。 - -然而,关联文件(可以看成指向文件的 Pointer)采用不同于 Pointer 的编码形式: - -```json -{ - "id": "543cbaede4b07db196f50f3c", - "__type": "File" -} -``` - -**GeoPoint** 包含地理位置的经纬度: - -```json -{ - "__type": "GeoPoint", - "latitude": 39.9, - "longitude": 116.4 -} -``` - -当更多的数据类型被加入的时候,它们都会采用 hashmap 加上一个 `__type` 字段的形式,所以你不应该使用 `__type` 作为你自己的 JSON 对象的 key。 - -## 查询 - -### 基础查询 - -通过发送一个 GET 请求到类的 URL 上,不需要任何 URL 参数,你就可以一次获取多个对象。下面就是简单地获取所有微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/classes/Post -``` - -返回的值就是一个 JSON 对象包含了 results 字段,它的值就是对象的列表: - -```json -{ - "results": [ - { - "content": "2021 年 5 月 1 日至 2021 年 5 月 5 日放假五天,5 月 8 日调休正常上班。", - "pubUser": "官方客服", - "upvotes": 2, - "createdAt": "2015-06-29T03:43:35.931Z", - "objectId": "55a39634e4b0ed48f0c1845b" - }, - { - "content": "我们将于 2021 年 2 月 10 日至 2021 年 2 月 17 日放假八天,2 月 18 日恢复正常工作,放假期间,运维团队仍将在线值班,以应对可能的突发情况,保障服务稳定。", - "pubUser": "官方客服", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" - } - ] -} -``` - -:::info -* 控制台对 `createdAt` 和 `updatedAt` 的展示做了优化,它们会依据用户操作系统时区而显示为本地时间; -* 客户端 SDK 获取到这些时间后也会将其转换为本地时间; -* 通过 REST API 获取到的则是原始的 UTC 时间,开发者可能需要根据情况做相应的时区转换。 -::: - -### 查询约束 - -通过 `where` 参数的形式可以对查询对象做出约束。 - -`where` 参数的值应该是 JSON 编码过的。就是说,如果你查看真正被发出的 URL 请求,它应该是先被 JSON 编码过,然后又被 URL 编码过。最简单的使用 `where` 参数的方式就是包含应有的 key 和 value。例如,如果我们想要看到「官方客服」发布的所有微博,我们应该这样构造查询: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"pubUser":"官方客服"}' \ - https://{{host}}/1.1/classes/Post -``` - -除了完全匹配一个给定的值以外,`where` 也支持比较的方式,而且它还支持对 key 的一些 hash 操作,比如包含。`where` 参数支持如下选项: - -| Key | Operation | -| ------------- | ------------------------------------- | -| `$ne` | 不等于 | -| `$lt` | 小于 | -| `$lte` | 小于等于 | -| `$gt` | 大于 | -| `$gte` | 大于等于 | -| `$regex` | 正则表达式;`$options` 指定全局修饰符 | -| `$in` | 包含任意一个数组值 | -| `$nin` | 不包含任意一个数组值 | -| `$all` | 包括所有的数组值 | -| `$exists` | 指定 Key 有值 | -| `$select` | 匹配另一个查询的返回值 | -| `$dontSelect` | 排除另一个查询的返回值 | - -例如获取在 **2015-06-29** 当天发布的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"createdAt":{"$gte":{"__type":"Date","iso":"2015-06-29T00:00:00.000Z"},"$lt":{"__type":"Date","iso":"2015-06-30T00:00:00.000Z"}}}' \ - https://{{host}}/1.1/classes/Post -``` - -求点赞次数少于 10 次,且该次数还是奇数的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"upvotes":{"$in":[1,3,5,7,9]}}' \ - https://{{host}}/1.1/classes/Post -``` - -获取不是「官方客服」发布的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"pubUser":{"$nin":["官方客服"]}}' \ - https://{{host}}/1.1/classes/Post -``` - -获取有人喜欢的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"upvotes":{"$exists":true}}' \ - https://{{host}}/1.1/classes/Post -``` - -获取没有被人喜欢过的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"upvotes":{"$exists":false}}' \ - https://{{host}}/1.1/classes/Post -``` - -微博有用户互相关注的功能,如果我们用 `_Followee`(用户关注的人) 和 `_Follower`(用户的粉丝) 这两个类来存储用户之间的关注关系,我们可以创建一个查询来找到某个用户关注的人发布的微博(`Post` 表中有一个字段 `author` 指向发布者): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={ - "author": { - "$select": { - "query": { - "className":"_Followee", - "where": { - "user":{ - "__type": "Pointer", - "className": "_User", - "objectId": "55a39634e4b0ed48f0c1845c" - } - } - }, - "key":"followee" - } - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -`order` 参数指定一个字段的排序方式,前面加一个负号表示逆序。返回 Post 记录并按发布时间升序排列: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=createdAt' \ - https://{{host}}/1.1/classes/Post -``` - -降序排列: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=-createdAt' \ - https://{{host}}/1.1/classes/Post -``` - -对多个字段进行排序,要使用逗号分隔的列表。将 Post 以 createdAt 升序和 pubUser 降序进行排序: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=createdAt,-pubUser' \ - https://{{host}}/1.1/classes/Post -``` - -你可以用 `limit` 和 `skip` 来做分页。`limit` 的默认值是 100,任何 1 到 1000 之间的值都是可选的,在 1 到 1000 范围之外的都强制转成默认的 100。比如为了获取排序在 401 到 600 之间的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'limit=200' \ - --data-urlencode 'skip=400' \ - https://{{host}}/1.1/classes/Post -``` - -你可以通过传入 `keys` 参数和一个逗号分隔列表限定返回的字段。为了返回对象只包含 `pubUser` 和 `content` 字段(还有特殊的内置字段比如 objectId、createdAt 和 updatedAt): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'keys=pubUser,content' \ - https://{{host}}/1.1/classes/Post -``` - -`keys` 的参数中可以使用点号进一步限定只返回字段的部分属性,例如,只返回发布者的姓氏:`keys=pubUser.familyName`。 - -`keys` 还支持反向选择,也就是不返回某些字段,字段名前面加个减号即可,比如我不想查询返回 `author`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'keys=-author' \ - https://{{host}}/1.1/classes/Post -``` - -反向选择同样适用于内置字段,比如 `keys=-createdAt,-updatedAt,-objectId`。 -另外反向选择同样可以和点号组合使用,例如 `keys=-pubUser.createdAt,-pubUser.updatedAt`。 - -### 返回 ACL 字段 - -默认情况下不会返回 ACL 字段。 -** > 服务设置 > 查询设置** 中勾选 **查询时返回值包括 ACL**,且指定了 `returnACL=true` 时返回结果中才会包含 ACL 字段。 - -所有以上这些参数都可以组合使用。 - -### 正则查询 - -获取标题以大写「WTO」开头的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"title":{"$regex":"^WTO.*","$options":"i"}}' \ - https://{{host}}/1.1/classes/Post -``` - -我们使用以下数据来演示如何使用 `$options` 匹配 **title** 字段值: - -``` -{ "_id" : 100, "title" : "Single line description." }, -{ "_id" : 101, "title" : "First line\nSecond line" }, -{ "_id" : 102, "title" : "Many spaces before line" }, -{ "_id" : 103, "title" : "Multiple\nline description" }, -{ "_id" : 104, "title" : "abc123" } -``` - -| 参数 | 说明 | 示例 | -| ---- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `i` | **忽略大小写** | `{"$regex":"single", "$options":"i"}` 将匹配

    `{ "_id" : 100, "title" : "Single line description." }`
    | -| `m` | **多行匹配**
    比如文本中包含了换行符 `\n` | `{"$regex":"^S", "$options":"m"}`(以大写字母 S 开头)将匹配

    `{ "_id" : 100, "title" : "Single line description." },`
    `{ "_id" : 101, "title" : "First line\nSecond line" }`
    | -| `x` | **忽略空白字符**
    包括空格、tab、`\n`、`#` 注释等,
    但对 vertical tab(ASCII 码为 11)无效。 | `{"$regex":"abc #category code\n123 #item number", "$options":"x"}`(# 后面为注释)将匹配

    `{ "_id" : 104, "title" : "abc123" }`
    | -| `s` | **允许 `.` 匹配任意字符和换行** | `{"$regex":"m.*line", "$options":"si"}` 将匹配

    `{ "_id" : 102, "title" : "Many spaces before     line" },`
    `{ "_id" : 103, "title" : "Multiple\nline description" }`
    | - -以上参数可以组合使用,如 `"$options":"sixm"`。 - -### 数组查询 - -如果 key 的值是数组类型,查找 key 值中有 2 的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":2}' \ - https://{{host}}/1.1/classes/TestObject -``` - -查找 key 值中有 2 或 3 或 4 的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":{"$in":[2,3,4]}}' \ - https://{{host}}/1.1/classes/TestObject -``` - -使用 `$all` 操作符来找到 key 值中**同时**有 2 和 3 和 4 的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":{"$all":[2,3,4]}}' \ - https://{{host}}/1.1/classes/TestObject -``` - -使用 `$size` 操作符来查找 key 值数组长度为 3 的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":{"$size": 3}}' \ - https://{{host}}/1.1/classes/TestObject -``` - -### 关系查询 - -有几种方式来查询对象之间的关系数据。如果你想获取对象,而这个对象的一个字段对应了另一个对象,你可以用一个 `where` 查询,自己构造一个 Pointer,和其他数据类型一样。例如,每条微博都会有很多人评论,我们可以让每一个 Comment 将它对应的 Post 对象保存到 post 字段上,这样你可以取得一条微博下所有 Comment: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"post":{"__type":"Pointer","className":"Post","objectId":"558e20cbe4b060308e3eb36c"}}' \ - https://{{host}}/1.1/classes/Comment -``` - -如果你想获取对象,这个对象的一个字段指向的对象需要另一个查询来指定,你可以使用 `$inQuery` 操作符。注意 `limit` 的默认值是 100 且最大值是 1000,这个限制同样适用于内部的查询,所以对于较大的数据集你可能需要细心地构建查询来获得期望的结果。 - -如上面的例子,假设每条微博还有一个 `image` 的字段,用来存储配图,你可以这样列出带图片的微博的评论数据: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"post":{"$inQuery":{"where":{"image":{"$exists":true}},"className":"Post"}}}' \ - https://{{host}}/1.1/classes/Comment -``` - -有时候,你可能需要在一个查询之中返回多种类型,你可以通过传入字段到 `include` 参数中。比如,我们想获得最近的 10 篇评论,而你想同时得到它们关联的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=-createdAt' \ - --data-urlencode 'limit=10' \ - --data-urlencode 'include=post' \ - https://{{host}}/1.1/classes/Comment -``` - -在返回结果中,`post` 字段会被展开为一个完整的对象,但 `__type` 仍将保持为 `Pointer`。 -例如,不传入 `include` 参数的情况下,返回结果中包含的指向 Post 的 Pointer 只包含 `__type`、`className`、`objectId`: - -```json -{ - "__type": "Pointer", - "className": "Post", - "objectId": "51e3a359e4b015ead4d95ddc" -} -``` - -传入 `include=post` 后,可以看到 pointer 被展开为: - -```json -{ - "__type": "Pointer", - "className": "Post", - "objectId": "51e3a359e4b015ead4d95ddc", - "createdAt": "2015-06-29T09:31:20.371Z", - "updatedAt": "2015-06-29T09:31:20.371Z", - "desc": "Post 的其他字段也会一同被包含进来。" -} -``` - -你可以同样做多层的 `include`,这时要使用点号。如果你要 include 一个 Comment 对应的 Post 对应的 `author`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=-createdAt' \ - --data-urlencode 'limit=10' \ - --data-urlencode 'include=post.author' \ - https://{{host}}/1.1/classes/Comment -``` - -如果你要构建一个查询,这个查询要 include 多个类,此时用逗号分隔列表即可。 - -### 地理查询 - -之前我们简要介绍过 GeoPoint。 - -假如在发布微博的时候,我们也支持用户加上当时的位置信息(新增一个 `location` 字段),如果想看看指定的地点附近发生的事情,可以通过 GeoPoint 数据类型加上在查询中使用 `$nearSphere` 做到。获取离当前用户最近的 10 条微博应该看起来像下面这个样子: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'where={ - "location": { - "$nearSphere": { - "__type": "GeoPoint", - "latitude": 39.9, - "longitude": 116.4 - } - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -这会按照距离纬度 39.9、经度 116.4(当前用户所在位置)的远近排序返回一系列结果,第一个就是最近的对象。(注意:**如果指定了 order 参数的话,它会覆盖按距离排序。**) - -为了限定搜索的最大距离,需要加入 `$maxDistanceInMiles` 和 `$maxDistanceInKilometers` 或者 `$maxDistanceInRadians` 参数来限定。比如要找的半径在 10 英里内的话: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={ - "location": { - "$nearSphere": { - "__type": "GeoPoint", - "latitude": 39.9, - "longitude": 116.4 - }, - "$maxDistanceInMiles": 10.0 - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -同样做查询寻找在一个特定的范围里面的对象也是可以的,为了找到在一个矩形的区域里的对象,按下面的格式加入一个约束 `{"$within": {"$box": [southwestGeoPoint, northeastGeoPoint]}}`。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={ - "location": { - "$within": { - "$box": [ - { - "__type": "GeoPoint", - "latitude": 39.97, - "longitude": 116.33 - }, - { - "__type": "GeoPoint", - "latitude": 39.99, - "longitude": 116.37 - } - ] - } - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -### 文件查询 - -查询文件和查询一般对象基本一致。 -例如,以下命令可以获取所有文件(和查询一般对象一样,默认最多返回 100 条结果): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/classes/files -``` - -需要注意的是,内部文件(上传到文件服务的文件)的 `url` 字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 -因此,通过 `url` 字段查询文件仅适用于外部文件(直接保存外部 URL 到文件服务创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。 - -同理,调用 `scan` 接口遍历文件时,返回结果中的内部文件也不包含 `url` 字段,只包含 `key` 字段。 - -### 对象计数 - -如果你在使用 `limit`,或者如果返回的结果很多,你可能想要知道到底有多少对象应该返回,而不用把它们全部获得以后再计数,此时你可以使用 `count` 参数。举个例子,如果你仅仅是关心某个用户发布了多少条微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"pubUser":"官方客服"}' \ - --data-urlencode 'count=1' \ - --data-urlencode 'limit=0' \ - https://{{host}}/1.1/classes/Post -``` - -因为这个 request 请求了 `count` 而且把 `limit` 设为了 0,返回的值里面只有计数,没有 `results`: - -```json -{ - "results": [], - "count": 7 -} -``` - -如果有一个非 0 的 `limit` 的话,则既会返回 `results` 也会返回 `count`。 - -### 复合查询 - -`$or` 操作符用于查询**符合任意一种条件**的对象,它的值为一个 JSON 数组。例如,查询企业账号和个人账号的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"$or":[{"pubUserCertificate":{"$gt":2}},{"pubUserCertificate":{"$lt":3}}]}' \ - https://{{host}}/1.1/classes/Post -``` - -任何在查询上的其他约束都会对返回的对象生效,所以你可以用 `$or` 对其他的查询添加约束。 - -`$and` 操作符用于查询**符合全部条件**的对象,它的值为一个 JSON 数组。例如查找存在 price 字段且 price != 199 的对象: - -``` -where={"$and":[{"price": {"$ne":199}},{"price":{"$exists":true}}]} -``` - -不过,查询条件表达式列表隐含 `$and` 操作符,所以上面的查询条件也可以改写为: - -``` -where=[{"price": {"$ne":199}},{"price":{"$exists":true}}] -``` - -实际上,由于这两个查询条件都是针对同一个字段(`price`)的查询,还可以进一步简化为: - -``` -where={"price": {"$ne":199, "$exists":true}} -``` - -不过,如果查询条件包含不止一个 or 查询,那就必须使用 `$and` 了: - -``` -where={"$and":[{"$or":[{"pubUserCertificate":{"$gt":2}},{"pubUserCertificate":{"$lt":3}}]},{"$or":[{"pubUser":"官方客服"},{"pubUser":"工程师团队"}]}]} -``` - -注意,**在组合查询的子查询中不支持使用 limit、skip、order、include 等非过滤型的约束。** - -## 用户 - -参考 [内建账户 REST API](/sdk/authentication/rest/) 文档。 - -## 角色 - -当你的应用的规模和用户基数成长时,你可能发现你需要比 ACL 模型(针对每个用户)更加粗粒度的访问控制你的数据的方法。为了适应这种需求,云服务支持一种基于角色的权限控制方式。角色系统提供一种逻辑方法让你通过权限的方式来访问你的数据,角色是一种有名称的对象,包含了用户和其他角色。任何授予一个角色的权限隐含着授予它包含着的其他的角色相应的权限。 - -例如,你的应用中可能有一些类似于「主持人」的角色可以修改和删除其他用户创建的新的内容,你可能还有一些「管理员」有着与「主持人」相同的权限,但是还可以修改应用的其他全局性设置。通过给予用户这些角色,你可以保证新的用户可以做主持人或者管理员,不需要手动地授予每个资源的权限给各个用户。 - -我们提供一个特殊的角色(Role)类来表示这些用户组,为了设置权限用。角色有一些和其他对象不太一样的特殊字段。 - -| 字段 | 说明 | -| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | 角色的名字,这个值是必须的,而且只允许被设置一次,只要这个角色被创建了的话。角色的名字必须由字母、数字、下划线这些字符构成。这个名字可以用来标明角色而不需要它的 objectId。 | -| users | 一个指向一系列用户的关系,这些用户会继承角色的权限。 | -| roles | 一个指向一系列子角色的关系,这些子关系会继承父角色所有的权限。 | - -通常来说,为了保持这些角色安全,你的移动应用不应该为角色的创建和管理负责。作为替代,角色应该是通过一个不同的网页上的界面来管理,或者手工被管理员所管理。 - -### 创建角色 - -创建一个新的角色与其他的对象不同的是 name 字段是必须的。角色必须指定一个 ACL,这个 ACL 必须尽量的约束严格一些,这样可以防止错误的用户修改角色。 - -创建一个新角色,发送一个 POST 请求到 roles 根路径: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Manager", - "ACL": { - "*": { - "read": true - } - } - }' \ - https://{{host}}/1.1/roles -``` - -和创建对象类似,创建成功时,HTTP 返回 `201 Created`,`Location` header 包含了新的对象的 URL: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/roles/55a483f0e4b05001a774b837 -``` - -返回值是一个 JSON 对象: - -```json -{ - "createdAt": "2015-07-14T03:34:41.074Z", - "objectId": "55a48351e4b05001a774a89f" -} -``` - -你可以通过加入已有的对象到 roles 和 users 关系中来创建一个有子角色和用户的角色: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "CLevel", - "ACL": { - "*": { - "read": true - } - }, - "roles": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_Role", - "objectId": "55a48351e4b05001a774a89f" - } - ] - }, - "users": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_User", - "objectId": "55a47496e4b05001a7732c5f" - } - ] - } - }' \ - https://{{host}}/1.1/roles -``` - -你也许注意到了,上面的代码里出现了一个新操作符 `AddRelation`。 -因为一些性能上的考量,Relation 的实现比较复杂。 -不过我们可以简单地把它看成 Pointer 数组。 -一般推荐使用中间表而不是 Relation。 -但由于一些历史原因,角色中还是用到了 Relation 这一概念。 - -### 获取角色 - -类似获取对象,你可以通过发送一个 GET 请求到 Location header 中返回的 URL 来获取这个对象,比如我们想要获取上面创建的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/roles/55a483f0e4b05001a774b837 -``` - -响应的 body 是一个 JSON 对象包含角色的所有字段: - -```json -{ - "name": "CLevel", - "createdAt": "2015-07-14T03:37:20.992Z", - "updatedAt": "2015-07-14T03:37:20.994Z", - "objectId": "55a483f0e4b05001a774b837", - "users": { - "__type": "Relation", - "className": "_User" - }, - "roles": { - "__type": "Relation", - "className": "_Role" - } -} -``` - -### 更新角色 - -更新一个角色通常可以像更新其他对象一样使用,但是 name 字段是不可以更改的。加入和删除 users 和 roles 可以通过使用 `AddRelation` 和 `RemoveRelation` 操作来进行。 - -举例来说,我们对 Manager 角色加入 1 个用户: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "users": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_User", - "objectId": "55a4800fe4b05001a7745c41" - } - ] - } - }' \ - https://{{host}}/1.1/roles/55a48351e4b05001a774a89f -``` - -相似的,我们可以删除一个 Manager 的子角色: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "roles": { - "__op": "RemoveRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_Role", - "objectId": "55a483f0e4b05001a774b837" - } - ] - } - }' \ - https://{{host}}/1.1/roles/55a48351e4b05001a774a89f -``` - -### 查询角色 - -查询用户具有哪些角色: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"users": {"__type": "Pointer", "className": "_User", "objectId": "5e03100ed4b56c008e4a91dc"}}' \ - https://{{host}}/1.1/roles -``` - -查询角色包含哪些用户(不计子角色中的用户): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode '"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"5f3dea7b7a53400006b13886"},"key":"users"}' \ - https://{{host}}/1.1/users -``` - -可以像查询普通对象一样,根据角色的属性进行查询。 - -### 删除角色 - -想要删除一个角色,只需要发送 DELETE 请求到它的 URL 就可以了。 - -我们需要传入 X-LC-Session 来通过一个有权限的用户账号来访问这个角色对象,例如: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/roles/55a483f0e4b05001a774b837 -``` - -### 角色和 ACL - -当你通过 REST API 访问云服务的时候,访问和 SDK 一样可能被 ACL 所限制。你仍然可以通过 REST API 来读和修改 ACL,只用通过访问「ACL」键就可以了。 - -除了用户级的权限设置以外,你可以通过设置角色级的权限来限制对云端对象的访问。取代了指定一个 objectId 带一个权限的方式,你可以设定一个角色的权限为它的名字在前面加上 `role:` 前缀作为 key。你可以同时使用用户级的权限和角色级的权限来提供精细的用户访问控制。 - -比如,为了限制一个对象可以被在 Staff 里的任何人读到,而且可以被它的创建者和任何有 Manager 角色的人所修改,你应该向下面这样设置 ACL: - -```json -{ - "55a4800fe4b05001a7745c41": { - "write": true - }, - "role:Staff": { - "read": true - }, - "role:Manager": { - "write": true - } -} -``` - -如果创建的用户和 Manager 本身就是 Staff 的子角色和用户,那么它们都会继承授予 Staff 的权限。 -所以我们上面没有显式地为创建者和 Manager 授予「读」权限。 - -就像我们之前提到的一样,一个角色可以包含另一个,可以为 2 个角色建立一个「父子」关系。 -这个关系的结果就是任何被授予父角色的权限隐含地被授予子角色。 - -这样的关系类型通常在用户管理的内容类的 app 上比较常见,比如论坛。有一些少数的用户是「管理员」,有最高级的权限来调整程序的设置、创建新的论坛、设定全局的消息等等。 - -另一类用户是「版主」,他们有责任保证用户生成的内容是合适的。任何有管理员权限的人都应该有版主的权利。为了建立这个关系,你应该把「Administrators」的角色设置为「Moderators」的子角色,具体来说就是把 Administrators 这个角色加入 Moderators 对象的 roles 关系之中: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "roles": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_Role", - "objectId": "" - } - ] - } - }' \ - https://{{host}}/1.1/roles/ -``` - -## 文件 - -### 创建文件 - -REST API 不支持文件上传,请使用 SDK 或命令行工具上传并创建文件。 - -如果已有 URL,可以使用以下命令创建文件(在文件服务中新增一条数据): - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"url": "https://example.com/foo.jpg", "name": "foo.jpg", "mime_type": "image/jpeg"}' \ - https://{{host}}/1.1/files -``` - -响应和返回值请参考前文[创建对象](#创建对象)一节。 - -### 关联文件到对象 - -一个文件被保存到文件服务后,你可以关联该文件到某个 LCObject 对象上: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "tom", - "picture": { - "id": "543cbaede4b07db196f50f3c", - "__type": "File" - } - }' \ - https://{{host}}/1.1/classes/Staff -``` - -其中 `id` 就是文件对象的 objectId。 - -### 删除文件 - -知道文件对象 ObjectId 的情况下,可以通过 DELETE 删除文件: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/files/543cbaede4b07db196f50f3c -``` - -### 文件审核 - -你可以审核单个文件: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/files/:id/censor -``` - -其中 `id` 就是文件对象的 objectId。 - -## 数据 Schema - -为了方便开发者使用、自行研发一些代码生成工具或者内部使用的管理平台。我们提供了获取数据 Class Schema 的开放 API,基于安全考虑,强制要求使用 `Master Key` 才可以访问。 - -查询一个应用下面所有 Class 的 Schema: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/schemas -``` - -返回的 JSON 数据包含了每个 Class 对应的 Schema: - -```json -{ - "_User": { - "username": { "type": "String" }, - "password": { "type": "String" }, - "objectId": { "type": "String" }, - "emailVerified": { "type": "Boolean" }, - "email": { "type": "String" }, - "createdAt": { "type": "Date" }, - "updatedAt": { "type": "Date" }, - "authData": { "type": "Object" } - } - // 其他 class -} -``` - -也可以单独获取某个 Class 的 Schema: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/schemas/_User -``` - -## 数据导出 API - -你可以通过请求 `/exportData` 来导出应用数据: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{}' \ - https://{{host}}/1.1/exportData -``` - -`exportData` 要求使用 master key 来授权。 - -你还可以指定导出数据的起始时间(`updatedAt`): - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_date":"2015-09-20", "to_date":"2015-09-25"}' \ - https://{{host}}/1.1/exportData -``` - -还可以指定具体的 class 列表,使用逗号隔开: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"classes":"_User,GameScore,Post"}' \ - https://{{host}}/1.1/exportData -``` - -增加 `only-schema` 选项就可以只导出 schema: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"only-schema":"true"}' \ - https://{{host}}/1.1/exportData -``` - -导出的 Schema 文件同样可以使用数据导入功能来导入到其他应用。 - -默认导出的结果将发送到应用的创建者邮箱,你也可以指定接收邮箱: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"email":"username@exmaple.com"}' \ - https://{{host}}/1.1/exportData -``` - -调用结果将返回本次任务的 id 和状态: - -```json -{ - "status": "running", - "id": "1wugzx81LvS5R4RHsuaeMPKlJqFMFyLwYDNcx6LvCc6MEzQ2", - "app_id": "{{appid}}" -} -``` - -除了被动等待邮件之外,你还可以主动使用 id 去查询导出任务状态: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/exportData/1wugzx81LvS5R4RHsuaeMPKlJqFMFyLwYDNcx6LvCc6MEzQ2 -``` - -如果导出完成,将返回导出结果的下载链接: - -```json -{ - "status": "done", - "download_url": "https://download.leancloud.cn/export/example.tar.gz", - "id": "1wugzx81LvS5R4RHsuaeMPKlJqFMFyLwYDNcx6LvCc6MEzQ2", - "app_id": "{{appid}}" -} -``` - -如果任务还没有完成, `status` 仍然将为 `running` 状态,**请间隔一段时间后再尝试查询。** - -**导出应用数据中不包括即时通讯聊天记录。** 如需导出这些记录,请调用相应的 REST API 接口获取。 - -## 其他 API - -### 服务器时间 - -获取服务端当前日期时间可以通过 `/date` API: - -```sh -curl -i -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/date -``` - -返回 UTC 日期: - -```json -{ - "iso": "2015-08-27T07:38:33.643Z", - "__type": "Date" -} -``` - -## 浏览器跨域和特殊方法解决方案 - -注:直接使用 RESTful API 遇到跨域问题,请遵守 HTML5 CORS 标准即可。以下方法非推荐方式,而是内部兼容方法。 - -对于跨域操作,我们定义了如下的 text/plain 数据格式来支持用 POST 的方法实现 GET、PUT、DELETE 的操作。 - -### GET - -```sh - curl -i -X POST \ - -H "Content-Type: text/plain" \ - -d \ - '{"_method":"GET", - "_ApplicationId":"{{appid}}", - "_ApplicationKey":"{{appkey}}"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -对应的输出: - -``` -HTTP/1.1 200 OK -Server: nginx -Date: Thu, 04 Dec 2014 06:34:34 GMT -Content-Type: application/json;charset=utf-8 -Content-Length: 174 -Connection: keep-alive -Last-Modified: Thu, 04 Dec 2014 06:34:08.498 GMT -Cache-Control: no-cache,no-store -Pragma: no-cache -Strict-Transport-Security: max-age=31536000 -{ - "content": "每个 Java 程序员必备的 8 个开发工具", - "pubUser": "官方客服", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -### PUT - -```sh -curl -i -X POST \ - -H "Content-Type: text/plain" \ - -d \ - '{"_method":"PUT", - "_ApplicationId":"{{appid}}", - "_ApplicationKey":"{{appkey}}", - "upvotes":99}' \ - https://{{host}}/1.1/classes/Post/ -``` - -对应的输出: - -``` -HTTP/1.1 200 OK -Server: nginx -Date: Thu, 04 Dec 2014 06:40:38 GMT -Content-Type: application/json;charset=utf-8 -Content-Length: 78 -Connection: keep-alive -Cache-Control: no-cache,no-store -Pragma: no-cache -Strict-Transport-Security: max-age=31536000 - -{"updatedAt":"2015-07-13T06:40:38.310Z","objectId":"558e20cbe4b060308e3eb36c"} -``` - -### DELETE - -```sh -curl -i -X POST \ - -H "Content-Type: text/plain" \ - -d \ - '{"_method": "DELETE", - "_ApplicationId":"{{appid}}", - "_ApplicationKey":"{{appkey}}"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -对应的输出是: - -```sh -HTTP/1.1 200 OK -Server: nginx -Date: Thu, 04 Dec 2014 06:15:10 GMT -Content-Type: application/json;charset=utf-8 -Content-Length: 2 -Connection: keep-alive -Cache-Control: no-cache,no-store -Pragma: no-cache -Strict-Transport-Security: max-age=31536000 - -{} -``` - -总之,就是利用 POST 传递的参数,把 \_method、\_ApplicationId 以及 \_ApplicationKey 传递给服务端,服务端会自动把这些请求翻译成指定的方法,这样可以使得 Unity3D 以及 JavaScript 等平台(或者语言)可以绕开浏览器跨域或者方法限制。 diff --git a/leancloud/docs/sdk/storage/security.mdx b/leancloud/docs/sdk/storage/security.mdx deleted file mode 100644 index 7468a6afd..000000000 --- a/leancloud/docs/sdk/storage/security.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: 数据和安全 -slug: /sdk/storage/guide/security/ -sidebar_position: 16 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -几乎每一位使用云服务的开发者都会问,如何能够保证自己应用数据的安全?对安全的关注说明你也是位对产品负责、对用户负责、对自己负责、做事态度认真的开发者,这也正是我们所信守的价值观。 - -云服务所有的 API 请求都通过 SSL 加密传输,保证传输过程中的数据安全性和可靠性。 -云端对客户端发过来的每一个请求,都进行了身份鉴别(Authentication)和访问授权(Authorization)的严格检查。 - -## 身份鉴别(Authentication) - -访问云服务 API 需要提供应用的 ClientID AppIDClientToken AppKey(用于在客户端发起请求),或是 ClientID AppID 和 MasterKey(用于在云引擎、开发者自己的服务器等受信任的环境发起请求)。 -Android SDK 还额外支持仅使用 ClientID AppID 访问云服务 API. - -使用 MasterKey 访问云服务 API 会跳过所有的访问权限控制,所以请确保 MasterKey 不会泄露。 - -在客户端使用 ClientToken AppKey 访问云服务 API 时: - -1. 客户端访问 API 通过 HTTPS 加密通讯。 -2. 通过 SDK 访问 API 时,HTTP Header 中不直接传输 ClientToken AppKey,而是传输客户端根据 ClientToken AppKey 和请求发起时间生成的签名字符串,供云端校验。开发者不通过 SDK,直接请求 REST API 时,也可以使用[这种更安全的鉴权方式](/sdk/storage/guide/rest#更安全的鉴权方式)。 - -通过 HTTPS 加密通讯,不仅保护了数据安全,而且能避免 ClientToken AppKey 被第三方窃取,但无法阻止别有用心的人通过在客户端使用抓包工具获取 ClientToken AppKey。 -通过签名字符串可以避免在网络传输过程中泄露 ClientToken AppKey,但是无法阻止别有用心的人通过反编译等手段获取代码中初始化 SDK 时设定的 ClientToken AppKey。 -Android SDK 提供仅使用 ClientID AppID 的初始化方式,SDK 通过闭源的 native library 根据开发者配置的应用签名证书自动对请求进行签名。 -这在一定程度上避免了暴露应用核心配置信息,也显著增加了破解的难度(反编译 native library 后仍需破解加密算法),但仍无法保证绝对的数据安全。 -因此,**凡是需要在客户端访问的应用,都需要设定 ACL 等权限设置,以保障应用数据的安全性**。 - -## 访问授权(Authorization) - -云服务支持在 Class(表)、字段(列)、对象三个不同的层次设置访问权限。 - -### Class 权限 - -Class 权限指整个 Class(整张表)的读写权限,需要在控制台进行设置。 - -在创建 Class 对话框可以设置 Class 的访问权限: - -![创建 Class 对话框](/img/security/class-permissions.png) - -- `add_fields` - 给 Class 增加新的字段,也就是说,保存对象时,如果对应的字段(列)不存在,是否允许自动创建新的字段。如果已经在控制台创建好 Class 的所有字段,最好对任意用户都关闭此权限,防止脏数据写入。 -- `create` - 在 Class 表中插入一个新对象。对于需要登录用户或者拥有指定授权的用户才能创建内容的场景,可以考虑根据情况设置此权限。 -- `delete` - 删除既有对象。 -- `update` - 修改既有对象。 -- `find` - 通过指定条件查询对象。关闭此权限时,无法查询对象,只能通过 objectId 获取对象(如果没有同时关闭 `get` 权限),一定程度上可以防范别有用心的人批量抓取该 Class 下的所有对象。 -- `get` - 通过 objectId 获取单个对象。 - -对于每一种权限,又可以开放给不同用户: - -- 所有用户 - 相当于 public 权限。这里的「用户」泛指「客户端」,而不是云服务内建账户系统的用户。也就是说,无论请求是否携带 sessionToken,携带的 sessionToken 是否有效,都可以进行这一操作。 -- 登录用户 - 只有使用云服务内建账户系统(`_User` 表)并且进行了登录的客户端(携带了有效 sessionToken 的请求),才可以执行这一操作。 -- 指定用户 - 指定用户才可以执行这一操作。支持通过用户名、用户的 objectId、角色名、角色的 objectId 指定用户。如果留空,那么意味着所有用户都没有权限。 - -譬如我们有一个匿名发帖的应用,所有人都可以发表帖子,但是只有经过管理员审核后的帖子才能被展示出来。我们可以有两张表,第一张表用来存放审核前的帖子,这张表的 create 权限就可以开放给所有用户;第二张表用来存放审核后的帖子,这张表的 create 权限就只开放给管理员。 - -另外,你也可以修改现有 Class 的访问权限。 -进入** > 结构化数据**,选择一个 Class,再点击**权限**标签页。 - -### 字段权限 - -在控制台还可以设置每个字段的权限。 -访问 ** > 结构化数据**,选择一个 Class,点击相应字段的下拉菜单,选择 **编辑**: - -1. 勾选 **只读** 后,客户端无法更新这一字段。 -2. 勾选 **客户端不可见** 后,客户端发起查询或获取对象的时候,返回的结果将不包含这一字段。比如,对于一个匿名发帖的应用,你仍然希望发帖的时候记录下真实的作者,但不希望将此信息返回给客户端。 - -### 对象权限 - -每个对象都有一个特殊的 ACL(Access Control List)属性。 -ACL 允许开发者设定某个具体对象的权限,是最精细的权限控制方式。 - -比如: - -- 对于私有数据,read 和 write 都可以限制为对象创建者所有(其读写权限都设置成创建者自身,并且不开放「所有用户」的读、写权限)。 -- 一个信息公告板的帖子,作者和属于「版主」角色的成员可拥有修改权限,普通访客则只允许浏览(给「所有用户」开放「读」权限,给「作者」和「版主角色」开放「写」权限)。 -- 应用内全局的每日头条,对所有用户是只读的,只有管理员可以修改它(给「所有用户」开放「读」权限,给「管理员角色」开放「写」权限)。 -- 用户共享一篇文章给另一个用户,可以将读和写的访问许可限制到关联的这两个用户,其他人一概不可读写(对「所有用户」关闭「读、写」权限,只对两个用户开放权限)。 - -详细信息请参考[ACL 权限管理指南](/sdk/storage/guide/acl/)。 - -## 安全设置 - - - -**云服务控制台 > 设置 > 安全中心** 可以设置或查看服务开关。服务开关可以用来开启或者关闭当前应用所使用的服务,从根本上防止由于 ClientID AppIDClientToken AppKey 泄露而可能会引发的服务资源被盗取的问题。 - -另外,有些子服务的开关在具体服务的设置页面,比如: - - - -** > 服务设置 > 安全设置** 中可以设置是否开启 LiveQuery。 - -出于安全考虑,有些应用只在服务端调用接口向用户推送通知。对于这些应用,可以访问 ** > 设置** 勾选 **禁止从客户端进行消息推送**。勾选后客户端无法推送消息,只能通过 MasterKey 调用 REST API 发送或在控制台发送(** > 在线发送**)。 - - - -同理,有些应用只在服务端调用接口向用户发送短信。对于这些应用,可以访问 **云服务控制台 > 短信 > 设置**,不勾选 **启用通用的短信验证码服务(开放 requestSmsCode 和 verifySmsCode 接口)**。这种情况下,应用仍然能在服务端通过 MasterKey 调用 `requestSmsCode` 发送短信。另外,与用户相关的短信接口与此选项无关。不勾选的情况下,客户端仍能调用用户相关的短信接口。 - - - -** > 服务设置 > 安全设置** 下可以设置 **禁止客户端创建 Class**。有些应用的开发者习惯预先规划好应用需要用到的 Class(表),并事先在控制台创建相应的 Class。对于这些开发者,推荐始终勾选这一选项。有些应用的开发者习惯直接着手开发应用的原型,在开发过程中逐渐完善数据结构。对于这些开发者,我们推荐在开发测试阶段不勾选这一选项,在应用上线前勾选这一选项。 - -** > 服务设置 > 查询设置** 下可以设置 **查询 include 引入的 Pointer 类型数据时校验 ACL 权限**。我们建议所有应用都勾选这一选项(这一选项默认处于勾选状态),以保证数据安全。 - -** > 设置** 页面下有一些用户相关的安全选项,可以要求修改密码时提供旧密码,密码修改后强制重新登录,验证第三方登录的 `Access Token` 是否有效。 - -** > 文件 > 设置** 可以限制用户上传的文件类型。 - - - -#### Web 安全域名 - -「Web 安全域名」可以限制请求来源,防止其他人通过外网其他地址盗用你的服务器资源。 - -「Web 安全域名」域名配置策略与浏览器域安全策略一致,要求域名协议、域和端口号都严格一致,不支持子域和通配符。所以如果你要配置一个域名,要写清楚协议、域和端口,缺少一个都可能导致访问被禁止(在使用默认端口的情况下,也可以省略端口号,比如 `https://example.com` 和 `https://example.com:443` 是等效的)。 - -云函数不受 Web 安全域名限制。另外,为方便开发调试,localhost 总是会被放行。 - -但是要注意,Web 安全域名所能达到的目的是防御恶意部署,而不是防御伪造脏数据(恶意用户通过绑定 host 的方式还是有可能访问到应用的数据),所以要想对数据进行更多细粒度的控制,需要配合 ACL 来使用。 - -#### 操作日志 - -**云服务控制台 > 设置 > 操作日志** 会显示应用创建者及所有协作者的重要操作记录,比如删除数据操作的历史、操作用户名、操作 IP 及操作时间等,这个日志的目的是为了遇到问题更好地定位故障缘由,排查可能的恶意操作,防止应用数据被错误地改动。 - -#### 自动备份 - -应用每日自动备份,云服务会保留最近 7 天的备份。商用版应用可以在 **云服务控制台 > 数据存储 > 导入导出 > 备份恢复** 恢复最近 7 天的数据(还可以指定需要恢复的 Class 或 objectId)。所有应用都可以在 **云服务控制台 > 数据存储 > 导入导出 > 备份导出** 下载备份(可以指定需要下载的备份日期、Class)。 - -注意: - -- 开发版不支持数据恢复。 -- 已删除的文件无法恢复。 -- 恢复操作只会插入数据,如果有被变更过的数据需要恢复,请先把目标数据自行备份并删除后再提交任务。 -- 如需恢复删除的 Class,需要在控制台手动创建一个同名的空 Class,并手动添加相应列。 - -此外,开发者还可以使用 [数据导出功能](/sdk/start/dashboard/#导出数据) 将应用数据备份到本地,该功能商用版、开发版均可用。 - - diff --git a/leancloud/docs/sdk/storage/unreal-guide/_category_.json b/leancloud/docs/sdk/storage/unreal-guide/_category_.json deleted file mode 100644 index 4243e021f..000000000 --- a/leancloud/docs/sdk/storage/unreal-guide/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "Unreal", - "collapsed": true, - "position": 7 - -} diff --git a/leancloud/docs/sdk/storage/unreal-guide/setup-unreal.mdx b/leancloud/docs/sdk/storage/unreal-guide/setup-unreal.mdx deleted file mode 100644 index 0f1a783a9..000000000 --- a/leancloud/docs/sdk/storage/unreal-guide/setup-unreal.mdx +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: 数据存储 Unreal SDK 配置指南 -sidebar_label: Unreal SDK 配置 -slug: /sdk/storage/guide/setup-Unreal/ -sidebar_position: 5 ---- - -import DomainBinding from "../../_partials/setup-domain.mdx"; -import AppConfig from "../_partials/app-config.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -## 安装 SDK - -### 下载 SDK - -可以从 [GitHub](https://github.com/leancloud/unreal-sdk/releases) 获取 SDK 。 - -### 导入插件 - -从下载的 SDK 中将 LeanCloud 插件拷贝到自己项目的插件目录中,如果下载的是项目工程,那么 LeanCloud 插件路径如下:`LeanCloudProject/Plugins/LeanCloud` - -### 给 Module 添加依赖 - -在使用 LeanCloud 功能的 Module 中添加依赖,找到项目当前 Module 的配置文件,即 `Module.build.cs` 文件。可以在该文件中的 `PublicDependencyModuleNames` 或者 `PrivateDependencyModuleNames` 数组中添加 `LeanCloud` 的依赖 -![](/img/quick_start/unreal/ModuleDependencyLeanCloud.jpg) - - -这样 SDK 就集成完毕了。 - -## 初始化 - -初始化的方式有两种:[配置初始化](#配置初始化) 和 [代码初始化](#代码初始化) - -### 配置初始化 - -你可以通过编辑器 Project Setting 中的 LeanCloud 的配置来设置: -![](/img/quick_start/unreal/ProjectSettings.png) -![](/img/quick_start/unreal/LeanCloudSetting.png) - -在上面打码的地方填上相关的配置。 - -### 代码初始化 - -导入头文件: -```cpp -#include "LCApplication.h" -``` - -初始化代码: -```cpp -FLCApplicationSettings Settings = FLCApplicationSettings(); -Settings.AppId = "your-client-id"; -Settings.AppKey = "your-client-token"; -Settings.ServerUrl = "https://your_server_url"; -FLCApplication::Register(Settings); -``` - -### 初始化多个应用 - -Unreal 支持多个应用的初始化: -- 在 [配置初始化](#配置初始化) 中,可以点 `+` 号,然后填写多个应用的配置。 -![](/img/quick_start/unreal/MutiLeanCloudSetting.png) -- 在 [代码初始化](#代码初始化) 中,多次调用`FLCApplication::Register`方法来初始化就行。 - -初始化的应用对象(Application)会当成单例保存起来,多次初始化相同配置只会保存一份,我们通过如下代码来获取应用对象(Application): -```cpp -TSharedPtr AppPtr = FLCApplication::Get("your-client-id"); -``` - -我们会把第一个注册的配置设为默认配置,你也可以通过对象 `FLCApplication::DefaultPtr` 来设置或获取默认配置。 - -因为支持初始化多个应用,所以后续在 `FLCObject`、`FLCQuery`等类中,需要指定应用的配置,如果不指定,那么会使用默认的配置。 - - -### 应用凭证 - - - -## 域名 - - - -## 开启日志 - -在应用开发者阶段,你可以选择开启 SDK 的调试日志(debug log)来方便追踪问题,调试日志开启后,SDK 会网络请求、错误消息等输出到 IDE 的日志窗口,或是浏览器 Console。 - -```cpp -// 最好在 Application 初始化代码执行之前执行 -FLCApplication::SetLogDelegate(FLeanCloudLogDelegate::CreateLambda([](ELCLogLevel LogLevel, const FString& LogMsg) { -switch (LogLevel) { -case ELCLogLevel::Error: - UE_LOG(LogTemp, Error, TEXT("%s"), *LogMsg); - break; -case ELCLogLevel::Warning: - UE_LOG(LogTemp, Warning, TEXT("%s"), *LogMsg); - break; -case ELCLogLevel::Debug: - UE_LOG(LogTemp, Display, TEXT("%s"), *LogMsg); - break; -case ELCLogLevel::Verbose: - UE_LOG(LogTemp, Display, TEXT("%s"), *LogMsg); - break; -default: ; -} -``` - -:::caution -在应用发布之前,请调高打印日志的级别,以免暴露敏感数据。 -::: - -## 验证 - -首先,确认本地网络环境是可以访问云端服务器的,可以执行以下命令: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` 为绑定的 API 自定义域名。 - -如果当前网络正常会返回当前时间: - -```json -{ "__type": "Date", "iso": "2020-10-12T06:46:56.000Z" } -``` - - -## 问题排查 - -SDK 安装指南基于当前最新版本的 SDK 编写,所以排查问题前,请先检查下安装的 SDK 是不是最新版本。 - -### `401 Unauthorized` - -如果 SDK 抛出 `401` 异常或者查看本地网络访问日志存在: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -则可认定为 `App ID` 或者 `App Key` 输入有误,或者是不匹配,很多开发者同时注册了多个应用,导致拷贝粘贴的时候,用 A 应用的 App ID 匹配 B 应用的 `App Key`,这样就会出现服务端鉴权失败的错误。 - -### 客户端无法访问网络 - -客户端尤其是手机端,应用在访问网络的时候需要申请一定的权限。 diff --git a/leancloud/docs/sdk/storage/unreal-guide/unreal.mdx b/leancloud/docs/sdk/storage/unreal-guide/unreal.mdx deleted file mode 100644 index 42f323709..000000000 --- a/leancloud/docs/sdk/storage/unreal-guide/unreal.mdx +++ /dev/null @@ -1,858 +0,0 @@ ---- -title: 数据存储开发指南 · Unreal -sidebar_label: Unreal 开发指南 -slug: /sdk/storage/guide/unreal/ -sidebar_position: 6 ---- - -import Path from "/src/docComponents/path"; - - -数据存储是云服务提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端: - -```cpp -// 构建对象 -TSharedPtr ToDo = MakeShared("Todo"); - -// 为属性赋值 -ToDo->Set("title", TEXT("工程师周会")); -ToDo->Set("content", TEXT("工程师周会")); - -// 将对象保存到云端 -ToDo->Save(FLeanCloudBoolResultDelegate::CreateLambda([=](bool bIsSuccess, const FLCError& Error) { - if (bIsSuccess) { - // 成功保存之后,执行其他逻辑 - UE_LOG(LogTemp, Error, TEXT("%s"), *ToDo->GetObjectId()); - } else { - // 异常处理 - } -})); -``` - -我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。 - -## SDK 安装与初始化 - -请阅读[数据存储 Unreal SDK 配置指南](/sdk/storage/guide/setup-unreal/)。 - -## 对象 - -### `FLCObject` - -`FLCObject` 是云服务对复杂对象的封装,每个 `FLCObject` 包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个 `FLCObject` 上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。 - -比如说,一个保存着单个 Todo 的 `FLCObject` 可能包含如下数据: - -```json -title: "给小林发邮件确认会议时间", -isComplete: false, -priority: 2, -tags: ["工作", "销售"] -``` - -### 数据类型 - -`FLCObject` 支持的数据类型包括 `FString`、`int`、`Double`、`Boolean`、`Object`、`Array`、`Date` 等等。你可以通过嵌套的方式在 `Object` 或 `Array` 里面存储更加结构化的数据。 - -`FLCObject` 还支持两种特殊的数据类型 `Pointer` 和 `File`,可以分别用来存储指向其他 `FLCObject` 的指针以及二进制数据。 - -`FLCObject` 同时支持 `GeoPoint`,可以用来存储地理位置信息。参见 [GeoPoint](#geopoint)。 - -以下是一些示例: - -```cpp -// 基本类型 -FString String = TEXT("工程师周会"); -int Interger = 10; -double Double = 20.0; -bool Boolean = true; -FDateTime Time = FDateTime::Now(); -TArray Data = {8, 16}; -TLCMap Map; -Map.Add("Key", "Value"); -TLCArray Array = {1, "HAHA", true}; -TSharedPtr Object = MakeShared("Todo"); - -// 构建对象 -TSharedPtr TestObject = MakeShared("TestObject"); -TestObject->Set("testString", String); -TestObject->Set("testInt", Interger); -TestObject->Set("testDouble", Double); -TestObject->Set("testBoolean", Boolean); -TestObject->Set("testTime", Time); -TestObject->Set("testMap", Map); -TestObject->Set("testArray", Array); -TestObject->Set("testObject", Object); -TestObject->Save(); -``` - -我们不推荐通过 `NSData` 在 `FLCObject` 里面存储图片、文档等大型二进制数据。每个 `FLCObject` 的大小不应超过 **128 KB**。如需存储大型文件,可创建 `FLCFile`(暂不支持) 实例并将其关联到 `FLCObject` 的某个属性上。参见 [文件](#文件)。 - -注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。 - -** > 结构化数据** 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。 - -若想了解云服务是如何保护应用数据的,请阅读[数据和安全](/sdk/storage/guide/security/)。 - -### 构建对象 - -下面的代码构建了一个 class 为 `Todo` 的 `FLCObject`: - -```cpp -TSharedPtr Object = MakeShared("Todo"); -``` - -在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。 - -### 保存对象 - -下面的代码将一个 class 为 `Todo` 的对象存入云端: - -```cpp -// 构建对象 -TSharedPtr ToDo = MakeShared("Todo"); - -// 为属性赋值 -ToDo->Set("title", TEXT("马拉松报名")); -ToDo->Set("priority", 2); - -// 将对象保存到云端 -ToDo->Save(FLeanCloudBoolResultDelegate::CreateLambda([=](bool bIsSuccess, const FLCError& Error) { - if (bIsSuccess) { - // 成功保存之后,执行其他逻辑 - UE_LOG(LogTemp, Error, TEXT("%s"), *ToDo->GetObjectId()); - } else { - // 异常处理 - } -})); -``` - -为了确认对象已经保存成功,我们可以到 ** > 结构化数据 > `Todo`** 里面看一下,应该会有一行新的数据产生。点一下这个数据的 `objectId`,应该能看到类似这样的内容: - -```json -{ - "title": "马拉松报名", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -注意,无需在 ** > 结构化数据** 里面创建新的 `Todo` class 即可运行前面的代码。如果 class 不存在,它将自动创建。 - -以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定: - -| 内置属性 | 类型 | 描述 | -| ----------- | ---------- | -------------------------------------------------------------- | -| `objectId` | `NSString` | 该对象唯一的 ID 标识。 | -| `ACL` | `LCACL` | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 | -| `createdAt` | `NSDate` | 该对象被创建的时间。 | -| `updatedAt` | `NSDate` | 该对象最后一次被修改的时间。 | - -这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的 `FLCObject` 不存在这些属性。 - -属性名(**keys**)只能包含字母、数字和下划线。自定义属性不得以双下划线(`__`)开头或与任何系统保留字段和内置属性(`ACL`、`className`、`createdAt`、`objectId` 和 `updatedAt`)重名,无论大小写。 - -属性值(**values**)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 [数据类型](#数据类型)。 - -我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 `CustomData`。属性,采用小驼峰法,如 `imageUrl`。 - -### 获取对象 - -对于已经保存到云端的 `FLCObject`,可以通过它的 `objectId` 将其取回: - -```cpp -FLCQuery Query = FLCQuery("Todo"); -Query.Get("582570f38ac247004f39c24b", FLeanCloudQueryObjectDelegate::CreateLambda([](TSharedPtr ObjectPtr, const FLCError& Error) { - if (ObjectPtr.IsValid()) { - FString Title = ObjectPtr->Get("title").AsString(); - int Priority = ObjectPtr->Get("priority").AsInteger(); - - // 获取内置属性 - FString ObjectID = ObjectPtr->GetObjectId(); - FDateTime UpdateAt = ObjectPtr->GetUpdatedAt(); - FDateTime CreatedAt = ObjectPtr->GetCreatedAt(); - } -})); -``` - -如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回默认值或者空。 - -#### 同步对象 - -当云端数据发生更改时,你可以调用 `FLCObject::Fetch` 方法来刷新对象,使之与云端数据同步: - -```cpp -TSharedPtr ToDo = MakeShared("Todo", "5745557f71cfe40068c6abe0"); -ToDo->Fetch(FLeanCloudBoolResultDelegate::CreateLambda([=](bool bIsSuccess, const FLCError& Error) { - if (bIsSuccess) { - // todo 已刷新 - } -})); -``` - -刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,**`FLCObject::Fetch` 操作会丢弃这些修改**。为避免这种情况,你可以在刷新时指定 **需要刷新的属性**,这样只有指定的属性会被刷新(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`),其他属性不受影响。 - -```cpp -TSharedPtr ToDo = MakeShared("Todo", "5745557f71cfe40068c6abe0"); -ToDo->Fetch({"priority", "location"}, FLeanCloudBoolResultDelegate::CreateLambda([=](bool bIsSuccess, const FLCError& Error) { - if (bIsSuccess) { - // 只有 priority 和 location 会被获取和刷新 - } -})); -``` - -### 更新对象 - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `FLCObject::Save` 方法。例如: - -```cpp -TSharedPtr ToDo = MakeShared("Todo", "5745557f71cfe40068c6abe0"); -ToDo->Set("content", TEXT("这周周会改到周三下午三点。")); -ToDo->Save(); -``` - -云服务会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。 - -#### 有条件更新对象 - -通过传入 `query` 选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回 `305` 错误。 - -例如,用户的账户表 `Account` 有一个余额字段 `balance`,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求: - -```cpp -TSharedPtr Account = MakeShared("Account", "5745557f71cfe40068c6abe0"); -// 对 balance 原子减少 100 -int amount = -100; -Account->Increase(FString("balance"), amount); -// 设置条件 -FLCQuery Query = FLCQuery(); -Query.WhereGreaterThanOrEqualTo("balance", -amount); -FLCSaveOption Option; -Option.SetMatchQuery(Query); -// 操作结束后,返回最新数据。 -// 如果是新对象,则所有属性都会被返回, -// 否则只有更新的属性会被返回。 -Option.SetFetchWhenSave(true); -Account->Save(FLeanCloudBoolResultDelegate::CreateLambda([=](bool bIsSuccess, const FLCError& Error) { - if (bIsSuccess) { - UE_LOG(LogTemp, Display, TEXT("当前余额为:%s"), *Account->Get("balance").AsString()); - } else if (Error.Code == 305) { - UE_LOG(LogTemp, Error, TEXT("余额不足,操作失败!")); - } -})); -``` - -**`query` 选项只对已存在的对象有效**,不适用于尚未存入云端的对象。 - -`query` 选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过 `FLCQuery` 查询 `FLCObject` 再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。 - -#### 更新计数器 - -设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 **原子操作** 来增加或减少一个属性内保存的数字: - -```cpp -Post->Increase(FString("likes"), 1); -``` - -可以指定需要增加或减少的值。若未指定,则默认使用 `1`。 - -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,需要原子增减的字段建议使用整数以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 - -#### 更新数组 - -更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据: - -```cpp -/** - * @brief 将指定对象或数组附加到数组末尾 - * @param Key 数组所在的Key - * @param Value 要添加的对象 - * @param bIsUnique 确保对象唯一 - */ -void Add(const FString& Key, const FLCValue& Value, bool bIsUnique = false); - -/** - * @brief 从数组字段中删除指定对象或数组的所有实例 - * @param Key 数组所在的Key - * @param Value 要删除的对象 - */ -void Remove(const FString& Key, const FLCValue& Value); -``` - -例如,`Todo` 用一个 `alarms` 属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性: - -```cpp -FDateTime alarm1; -FDateTime alarm2; -FDateTime alarm3; -FDateTime::ParseIso8601(TEXT("2023-06-07T12:10:29.907Z"), alarm1); -FDateTime::ParseIso8601(TEXT("2023-06-07T12:20:29.907Z"), alarm2); -FDateTime::ParseIso8601(TEXT("2023-06-07T12:30:29.907Z"), alarm3); -TSharedPtr ToDo = MakeShared("Todo"); -ToDo->Add("alarms", {alarm1, alarm2, alarm3}); -ToDo->Save(); -``` - -### 删除对象 - -下面的代码从云端删除一个 `Todo` 对象: - -```cpp -TSharedPtr ToDo = MakeShared("Todo", "582570f38ac247004f39c24b"); -ToDo->Delete(); -``` - -注意,删除对象是一个较为敏感的操作,我们建议你阅读[ACL 权限管理开发指南](/sdk/storage/guide/acl/)来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。 - -### 批量操作 - -```cpp -// 批量构建和更新 -static void Save(const TArray>& Objects, FLeanCloudBoolResultDelegate CallBack = nullptr); -static void Save(const TArray>& Objects, const FLCSaveOption& Option, FLeanCloudBoolResultDelegate CallBack = nullptr); - -// 批量删除 -static void Delete(const TArray>& Objects, FLeanCloudBoolResultDelegate CallBack = nullptr); - -// 批量同步 -static void Fetch(const TArray>& Objects, FLeanCloudBoolResultDelegate CallBack = nullptr); -static void Fetch(const TArray>& Objects, const TArray& Keys, FLeanCloudBoolResultDelegate CallBack = nullptr); -``` - -下面的代码将所有 `Todo` 的 `isComplete` 设为 `true`: - -```cpp -FLCQuery Query = FLCQuery("Todo"); -Query.Find(FLeanCloudQueryObjectsDelegate::CreateLambda([=](TArray> ObjectPtrs, - const FLCError& _Error) { - // 获取需要更新的 todo - for (auto FlcObject : ObjectPtrs) { - // 更新属性值 - FlcObject->Set("isComplete", true); - } - // 批量更新 - FLCObject::Save(ObjectPtrs); -})); -``` - -虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。 - -### 后台运行 - -细心的开发者已经发现,在所有的示例代码中都是用了异步来访问云端,在 Game 线程调用用以实现后台运行的方法,因此开发者在主线程可以放心地调用函数。 - - -### 数据模型 - -对象之间可以产生关联。拿一个博客应用来说,一个 `Post` 对象可以与许多个 `Comment` 对象产生关联。云服务支持三种关系:一对一、一对多、多对多。 - -#### 一对一、一对多关系 - -一对一、一对多关系可以通过将 `FLCObject` 保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个 `Comment` 指向一个 `Post`。 - -下面的代码会创建一个含有单个 `Comment` 的 `Post`: - -```cpp -// 创建 post -TSharedPtr Post = MakeShared("Post"); -Post->Set("title", TEXT("饿了……")); -Post->Set("content", TEXT("中午去哪吃呢?")); - -// 创建 comment -TSharedPtr Comment = MakeShared("Comment"); -Comment->Set("content", TEXT("当然是肯德基啦!")); - -// 将 post 设为 comment 的一个属性值 -Comment->Set("parent", Post); - -// 保存 comment 会同时保存 post -Comment->Save(); -``` - -云端存储时,会将被指向的对象用 `Pointer` 的形式存起来。你也可以用 `objectId` 来指向一个对象: - -```cpp -TSharedPtr Post = MakeShared("Post", "57328ca079bc44005c2472d0"); -Comment->Set("post", Post); -``` - -请参阅 [关系查询](#关系查询) 来了解如何获取关联的对象。 - -#### 多对多关系 - -想要建立多对多关系,最简单的办法就是使用 **数组**。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 **中间表** 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。 - -我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。 - -### 序列化和反序列化 - -在实际的开发中,把 `FLCObject` 当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此 `FLCObject` 也提供了序列化和反序列化的方法。 - -序列化: - -```cpp -TSharedPtr ToDo = MakeShared("Todo"); // 构建对象 -ToDo->Set("title", TEXT("马拉松报名")); // 设置名称 -ToDo->Set("priority", 2); // 设置优先级 -ToDo->Set("owner", StaticCastSharedPtr(FLCUser::GetCurrentUser())); // 这里就是一个 Pointer 类型,指向当前登录的用户 - -// 获取序列化 -TArray Data; -FMemoryWriter Writer(Data); -Writer << *ToDo.Get(); -``` - -反序列化: - -```cpp -// 由 TArray 转化一个 FLCObject -TArray Data; -TSharedPtr ToDo = MakeShared("Todo"); -FMemoryReader Reader(Data); -Reader << *ToDo.Get(); -``` - -## 查询 - -我们已经了解到如何从云端获取单个 `FLCObject`,但你可能还会有一次性获取多个符合特定条件的 `FLCObject` 的需求,这时候就需要用到 `FLCQuery` 了。 - -### 基础查询 - -执行一次基础查询通常包括这些步骤: - -1. 构建 `FLCQuery`; -2. 向其添加查询条件; -3. 执行查询并获取包含满足条件的对象的数组。 - -下面的代码获取所有 `lastName` 为 `Smith` 的 `Student`: - -```cpp -FLCQuery Query = FLCQuery("Student"); -Query.WhereEqualTo("lastName", "Smith"); -Query.Find(FLeanCloudQueryObjectsDelegate::CreateLambda([](TArray> Students, - const FLCError& Error) { - // students 是包含满足条件的 Student 对象的数组 -})); -``` - -### 查询条件 - -可以给 `FLCObject` 添加不同的条件来改变获取到的结果。 - -下面的代码查询所有 `firstName` 不为 `Jack` 的对象: - -```cpp -Query.WhereNotEqualTo("firstName", "Jack"); -``` - -对于能够排序的属性(比如数字、字符串),可以进行比较查询: - -```cpp -// 限制 age < 18 -Query.WhereLessThan("age", 18); - -// 限制 age <= 18 -Query.WhereLessThanOrEqualTo("age", 18); - -// 限制 age > 18 -Query.WhereGreaterThan("age", 18); - -// 限制 age >= 18 -Query.WhereGreaterThanOrEqualTo("age", 18); -``` - -可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是 `AND` 的关系: - -```cpp -Query.WhereEqualTo("firstName", "Jack"); -Query.WhereGreaterThan("age", 18); -``` - -可以通过指定 `limit` 限制返回结果的数量(默认为 `100`): - -```cpp -// 最多获取 10 条结果 -Query.Limit = 100; -``` - -由于性能原因,`limit` 最大只能设为 `1000`。即使将其设为大于 `1000` 的数,云端也只会返回 1,000 条结果。 - -如果只需要一条结果,可以直接用 `getFirstObject`: - -```cpp -FLCQuery Query = FLCQuery("Todo"); -Query.WhereEqualTo("priority", 2); -Query.GetFirst(FLeanCloudQueryObjectDelegate::CreateLambda([](TSharedPtr ObjectPtr, const FLCError& Error) { - // todo 是第一个满足条件的 Todo 对象 -})); -``` - -可以通过设置 `skip` 来跳过一定数量的结果: - -```cpp -// 跳过前 20 条结果 -Query.Skip = 20; -``` - -把 `skip` 和 `limit` 结合起来,就能实现翻页功能: - -```cpp -FLCQuery Query = FLCQuery("Todo"); -Query.WhereEqualTo("priority", 2); -Query.Limit = 10; -Query.Skip = 20; -``` - -需要注意的是,`skip` 的值越高,查询所需的时间就越长。作为替代方案,可以通过设置 `createdAt` 或 `updatedAt` 的范围来实现更高效的翻页,因为它们都自带索引。 -同理,也可以通过设置自增字段的范围来实现翻页。 - -对于能够排序的属性,可以指定结果的排序规则: - -```cpp -// 按 createdAt 升序排列 -Query.WhereOrderByAscending("createdAt"); - -// 按 createdAt 降序排列 -Query.WhereOrderByDescending("createdAt"); -``` - -还可以为同一个查询添加多个排序规则: - -```cpp -Query.WhereOrderByAscending("priority"); -Query.WhereOrderByDescending("createdAt"); -``` - -下面的代码可用于查找包含或不包含某一属性的对象: - -```cpp -// 查找包含 "images" 的对象 -Query.WhereKeyExisted("images"); - -// 查找不包含 "images" 的对象 -Query.WhereKeyNotExisted("images"); -``` - -可以通过 `selectKeys` 指定需要返回的属性。下面的代码只获取每个对象的 `title` 和 `content`(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`): - -```cpp -FLCQuery Query = FLCQuery("Todo"); -Query.SetSelectKeys({"title", "content"}); -Query.GetFirst(FLeanCloudQueryObjectDelegate::CreateLambda([](TSharedPtr ObjectPtr, const FLCError& Error) { - if (ObjectPtr.IsValid()) { - FString Title = ObjectPtr->Get("title").AsString(); // √ - FString Content = ObjectPtr->Get("content").AsString(); // √ - FString Notes = ObjectPtr->Get("notes").AsString(); // 为空 - } -})); -``` - -`selectKeys` 支持点号(`author.firstName`),详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)。 -另外,字段名前添加减号前缀表示反向选择,例如 `-author` 表示不返回 `author` 字段。 -反向选择同样适用于内置字段,比如 `-objectId`,也可以和点号组合使用,比如 `-pubUser.createdAt`。 - -对于未获取的属性,可以通过对结果中的对象进行 `FLCObject::Fetch` 操作来获取。参见 [同步对象](#同步对象)。 - -### 字符串查询 - -可以用 `WherePrefixedBy` 来查找某一属性值以特定字符串开头的对象。和 SQL 中的 `LIKE` 一样,你可以利用索引带来的优势: - -```cpp -FLCQuery Query = FLCQuery("Todo"); -// 相当于 SQL 中的 title LIKE 'lunch%' -Query.WherePrefixedBy("title", "lunch"); -``` - -可以用 `WhereMatchedSubstring` 来查找某一属性值包含特定字符串的对象: - -```cpp -FLCQuery Query = FLCQuery("Todo"); -// 相当于 SQL 中的 title LIKE '%lunch%' -Query.WhereMatchedSubstring("title", "lunch"); -``` - -和 `WherePrefixedBy` 不同,`WhereMatchedSubstring` 无法利用索引,因此不建议用于大型数据集。 - -注意 `WherePrefixedBy` 和 `WhereMatchedSubstring` 都是 **区分大小写** 的,所以上述查询会忽略 `Lunch`、`LUNCH` 等字符串。 - -如果想查找某一属性值不包含特定字符串的对象,可以使用 `WhereMatchedRegularExpression` 进行基于正则表达式的查询: - -```cpp -FLCQuery Query = FLCQuery("Todo"); -// "title" 不包含 "ticket"(不区分大小写) -Query.WhereMatchedRegularExpression("title", "^((?!ticket).)*$", "i"); -``` - -不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class, -因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以使用全文搜索服务。 - -使用查询时如果遇到性能问题,可参阅 [查询性能优化](#查询性能优化)。 - -### 数组查询 - -下面的代码查找所有数组属性 `tags` 包含 `工作` 的对象: - -```cpp -Query.WhereEqualTo("tags", TEXT("工作")); -``` - -下面的代码查询数组属性长度为 3(正好包含 3 个标签)的对象: - -```cpp -Query.WhereEqualToSize("tags", 3); -``` - -下面的代码查找所有数组属性 `tags` **同时包含** `工作`、`销售` 和 `会议` 的对象: - -```cpp -TLCArray Array = {TEXT("工作"), TEXT("销售"), TEXT("会议")}; -Query.WhereContainedAllIn("tags", Array); -``` - -如需获取某一属性值包含一列值中任意一个值的对象,可以直接用 `WhereContainedIn` 而无需执行多次查询。下面的代码构建的查询会查找所有 `priority` 为 `1` **或** `2` 的 todo 对象: - -```cpp -// 单个查询 -FLCQuery PriorityOneOrTwo = FLCQuery("Todo"); -PriorityOneOrTwo.WhereContainedIn("priority", {1, 2}); -// 这样就可以了 :) - -// --------------- -// vs. -// --------------- - -// 多个查询 -FLCQuery PriorityOne = FLCQuery("Todo"); -PriorityOne.WhereEqualTo("priority", 1); - -FLCQuery PriorityTwo = FLCQuery("Todo"); -PriorityOne.WhereEqualTo("priority", 2); - -FLCQuery PriorityOneOrTwo = PriorityOne.Or(PriorityTwo); -// 好像有些繁琐 :( -``` - -反过来,还可以用 `WhereNotContainedIn` 来获取某一属性值不包含一列值中任何一个的对象。 - -### 关系查询 - -查询关联数据有很多种方式,常见的一种是查询某一属性值为特定 `FLCObject` 的对象,这时可以像其他查询一样直接用 `WhereEqualTo`。比如说,如果每一条博客评论 `Comment` 都有一个 `post` 属性用来存放原文 `Post`,则可以用下面的方法获取所有与某一 `Post` 相关联的评论: - -```cpp -TSharedPtr Post = MakeShared("Post", "57328ca079bc44005c2472d0"); -FLCQuery Query = FLCQuery("Comment"); -Query.WhereEqualTo("post", Query); -Query.Find(FLeanCloudQueryObjectsDelegate::CreateLambda([](TArray> ObjectPtrs, - const FLCError& Error) { - // comments 包含与 post 相关联的评论 -})); -``` - -如需获取某一属性值为另一查询结果中任一 `FLCObject` 的对象,可以用 `matchesQuery`。下面的代码构建的查询可以找到所有包含图片的博客文章的评论: - -```cpp -FLCQuery InnerQuery = FLCQuery("Post"); -InnerQuery.WhereKeyExisted("images"); - -FLCQuery Query = FLCQuery("Comment"); -Query.WhereMatchesQuery("post", InnerQuery); -``` - -如需获取某一属性值不是另一查询结果中任一 `FLCObject` 的对象,则使用 `WhereDoesNotMatchQuery`。 - -有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用 `includeKey`。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章: - -```cpp -FLCQuery Query = FLCQuery("Comment"); - -// 获取最新发布的 -Query.WhereOrderByDescending("createdAt"); - -// 只获取 10 条 -Query.Limit = 10; - -// 同时包含博客文章 -Query.WhereKeyIncluded("post"); - -Query.Find(FLeanCloudQueryObjectsDelegate::CreateLambda([](TArray> Comments, - const FLCError& Error) { - // comments 包含最新发布的 10 条评论,包含各自对应的博客文章 - for (auto Comment : Comments) { - // 该操作无需网络连接 - TSharedPtr Post = Comment->Get("post").AsObject(); - } -})); -``` - -可以用 dot 符号(`.`)来获取多级关系,例如 `post.author`,详见[《点号使用指南》](/sdk/storage/guide/dot-notation/)的《在查询对象时使用点号》一节。 - -可以在同一查询上应用多次 `WhereKeyIncluded` 以包含多个属性。通过这种方法获取到的对象同样接受 `GetFirst` 等 `FLCQuery` 辅助方法。 - -通过 `WhereKeyIncluded` 进行多级查询的方式不适用于数组属性内部的 `FLCObject`,只能包含到数组本身。 - -#### 关系查询的注意事项 - -云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌/子查询(和普通查询一样,`limit` 默认为 `100`,最大 `1000`),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出 `limit`,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有 `limit` 数量以内的结果会被填入主查询。 - -我们建议采用以下方案进行改进: - -- 确保子查询的结果在 100 条以下,如果在 100 至 1,000 条之间的话请将子查询的 `limit` 设为 `1000`。 -- 将需要查询的字段冗余到主查询所在的表上。 -- 进行多次查询,每次在子查询上设置不同的 `skip` 值来遍历所有记录(注意 `skip` 的值较大时可能会引发性能问题,因此不是很推荐)。 - -### 统计总数量 - -如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用 `FLCQuery::Count` 来代替 `FLCQuery::Find`。比如说,查询有多少个已完成的 todo: - -```cpp -FLCQuery Query = FLCQuery("Todo"); -Query.WhereEqualTo("isComplete", true); -Query.Count(FLeanCloudQueryCountDelegate::CreateLambda([](int Count, const FLCError& Error) { - UE_LOG(LogTemp, Display, TEXT("%d 个 todo 已完成"), Count); -})); -``` - -### 组合查询 - -组合查询就是把诸多查询条件用一定逻辑合并到一起(`OR` 或 `AND`)再交给云端去查询。 - -组合查询不支持在子查询中包含 `GeoPoint` 或其他非过滤性的限制(例如 `near`、`withinGeoBox`、`limit`、`skip`、`ascending`、`descending`、`include`)。 - -#### OR 查询 - -OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于 `3` 或者已经完成了的 todo: - -```cpp -FLCQuery priorityQuery = FLCQuery("Todo"); -priorityQuery.WhereGreaterThanOrEqualTo("priority", 3); - -FLCQuery isCompleteQuery = FLCQuery("Todo"); -isCompleteQuery.WhereEqualTo("isComplete", true); - -FLCQuery Query = FLCQuery::Or({priorityQuery, isCompleteQuery}); -``` - -使用 OR 查询时,子查询中不能包含 `GeoPoint` 相关的查询。 - -#### AND 查询 - -使用 AND 查询的效果等同于往 `FLCQuery` 添加多个条件。下面的代码构建的查询会查找创建时间在 `2016-11-13` 和 `2016-12-02` 之间的 todo: - -```cpp -FDateTime alarm1; -FDateTime alarm2; -FDateTime::ParseIso8601(TEXT("2016-11-13T00:00:00.000Z"), alarm1); -FDateTime::ParseIso8601(TEXT("2016-12-02T00:00:00.000Z"), alarm2); - -FLCQuery StartDateQuery = FLCQuery("Todo"); -StartDateQuery.WhereGreaterThanOrEqualTo("createdAt", alarm1); - -FLCQuery EndDateQuery = FLCQuery("Todo"); -EndDateQuery.WhereLessThan("createdAt", alarm2); - -FLCQuery Query = FLCQuery::And({StartDateQuery, EndDateQuery}); -``` - -单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询: - -```cpp -FDateTime alarm1; -FDateTime alarm2; -FDateTime::ParseIso8601(TEXT("2016-11-13T00:00:00.000Z"), alarm1); -FDateTime::ParseIso8601(TEXT("2016-12-02T00:00:00.000Z"), alarm2); -FLCQuery CreatedAtQuery = FLCQuery("Todo"); -CreatedAtQuery.WhereGreaterThanOrEqualTo("createdAt", alarm1); -CreatedAtQuery.WhereLessThan("createdAt", alarm2); - -FLCQuery LocationQuery = FLCQuery("Todo"); -LocationQuery.WhereKeyNotExisted("location"); - -FLCQuery Priority2Query = FLCQuery("Todo"); -Priority2Query.WhereEqualTo("priority", 2); - -FLCQuery Priority3Query = FLCQuery("Todo"); -Priority3Query.WhereEqualTo("priority", 3); - -FLCQuery PriorityQuery = FLCQuery::Or({Priority2Query, Priority3Query}); -FLCQuery TimeLocationQuery = FLCQuery::Or({LocationQuery, CreatedAtQuery}); -FLCQuery Query = FLCQuery::And({PriorityQuery, TimeLocationQuery}); -``` - - -### 查询性能优化 - -影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。 - -- 不等于和不包含查询(无法使用索引) -- 通配符在前面的字符串查询(无法使用索引) -- 有条件的 `count`(需要扫描所有数据) -- `skip` 跳过较多的行数(相当于需要先查出被跳过的那些行) -- 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引) -- 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据) - -## GeoPoint - -云服务允许你通过将 `FLCGeoPoint` 关联到 `FLCObject` 的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。 - -要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个 `FLCGeoPoint` 并将其纬度(`latitude`)设为 `39.9`,经度(`longitude`)设为 `116.4`: - -```cpp -FLCGeoPoint Point = FLCGeoPoint(39.9, 116.4); -``` - -现在可以将这个地理位置存储为一个对象的属性: - -```cpp -ToDo->Set("location", Point); -``` - -### 地理位置查询 - -给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 `FLCQuery` 添加 `nearGeoPoint` 条件。下面的代码查找 `location` 属性值离某一点最近的 `Todo` 对象: - -```cpp -FLCQuery Query = FLCQuery("Todo"); -FLCGeoPoint Point = FLCGeoPoint(39.9, 116.4); -Query.WhereNear("location", Point); - -// 限制为 10 条结果 -Query.Limit = 10; -Query.Find(FLeanCloudQueryObjectsDelegate::CreateLambda([](TArray> ObjectPtrs, - const FLCError& Error) { - // todos 是包含满足条件的 Todo 对象的数组 -})); -``` - -像 `WhereOrderByAscending` 和 `WhereOrderByDescending` 这样额外的排序条件会获得比默认的距离排序更高的优先级。 - -若要限制结果和给定地点之间的距离,可以参考 API 文档中的 `WhereWithinKilometers`、`WhereWithinMiles` 和 `WhereWithinRadians` 参数。 - -若要查询在某一矩形范围内的对象,可以用 `WhereWithinGeoBox`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```cpp -FLCQuery Query = FLCQuery("Todo"); -FLCGeoPoint Southwest = FLCGeoPoint(30, 115); -FLCGeoPoint Northeast = FLCGeoPoint(40, 118); -Query.WhereWithinGeoBox("location", Southwest, Northeast); -``` - -### GeoPoint 的注意事项 - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -iOS 8.0 之后,使用定位服务之前,需要调用 `[locationManager requestWhenInUseAuthorization]` 或 `[locationManager requestAlwaysAuthorization]` 来获取用户的「使用期授权」或「永久授权」,而这两个请求授权需要在 `info.plist` 里面对应添加 `NSLocationWhenInUseUsageDescription` 或 `NSLocationAlwaysUsageDescription` 的键值对,值为开启定位服务原因的描述。SDK 内部默认使用的是「使用期授权」。 - diff --git a/leancloud/i18n/en/code.json b/leancloud/i18n/en/code.json deleted file mode 100644 index 2ff523469..000000000 --- a/leancloud/i18n/en/code.json +++ /dev/null @@ -1,416 +0,0 @@ -{ - "theme.navbar.mobileLanguageDropdown.label": { - "message": "Languages", - "description": "The label for the mobile language switcher dropdown" - }, - "theme.ErrorPageContent.title": { - "message": "This page crashed.", - "description": "The title of the fallback page when the page crashed" - }, - "theme.ErrorPageContent.tryAgain": { - "message": "Try again", - "description": "The label of the button to try again when the page crashed" - }, - "theme.NotFound.title": { - "message": "Page Not Found", - "description": "The title of the 404 page" - }, - "theme.NotFound.p1": { - "message": "We could not find what you were looking for.", - "description": "The first paragraph of the 404 page" - }, - "theme.NotFound.p2": { - "message": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.", - "description": "The 2nd paragraph of the 404 page" - }, - "theme.admonition.note": { - "message": "note", - "description": "The default label used for the Note admonition (:::note)" - }, - "theme.admonition.tip": { - "message": "tip", - "description": "The default label used for the Tip admonition (:::tip)" - }, - "theme.admonition.danger": { - "message": "danger", - "description": "The default label used for the Danger admonition (:::danger)" - }, - "theme.admonition.info": { - "message": "info", - "description": "The default label used for the Info admonition (:::info)" - }, - "theme.admonition.caution": { - "message": "caution", - "description": "The default label used for the Caution admonition (:::caution)" - }, - "theme.BackToTopButton.buttonAriaLabel": { - "message": "Scroll back to top", - "description": "The ARIA label for the back to top button" - }, - "theme.blog.archive.title": { - "message": "Archive", - "description": "The page & hero title of the blog archive page" - }, - "theme.blog.archive.description": { - "message": "Archive", - "description": "The page & hero description of the blog archive page" - }, - "theme.blog.paginator.navAriaLabel": { - "message": "Blog list page navigation", - "description": "The ARIA label for the blog pagination" - }, - "theme.blog.paginator.newerEntries": { - "message": "Newer Entries", - "description": "The label used to navigate to the newer blog posts page (previous page)" - }, - "theme.blog.paginator.olderEntries": { - "message": "Older Entries", - "description": "The label used to navigate to the older blog posts page (next page)" - }, - "theme.blog.post.plurals": { - "message": "One post|{count} posts", - "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" - }, - "theme.blog.tagTitle": { - "message": "{nPosts} tagged with \"{tagName}\"", - "description": "The title of the page for a blog tag" - }, - "theme.tags.tagsPageLink": { - "message": "View All Tags", - "description": "The label of the link targeting the tag list page" - }, - "theme.colorToggle.ariaLabel": { - "message": "Switch between dark and light mode (currently {mode})", - "description": "The ARIA label for the navbar color mode toggle" - }, - "theme.colorToggle.ariaLabel.mode.dark": { - "message": "dark mode", - "description": "The name for the dark color mode" - }, - "theme.colorToggle.ariaLabel.mode.light": { - "message": "light mode", - "description": "The name for the light color mode" - }, - "theme.blog.post.paginator.navAriaLabel": { - "message": "Blog post page navigation", - "description": "The ARIA label for the blog posts pagination" - }, - "theme.blog.post.paginator.newerPost": { - "message": "Newer Post", - "description": "The blog post button label to navigate to the newer/previous post" - }, - "theme.blog.post.paginator.olderPost": { - "message": "Older Post", - "description": "The blog post button label to navigate to the older/next post" - }, - "theme.docs.breadcrumbs.home": { - "message": "Home page", - "description": "The ARIA label for the home page in the breadcrumbs" - }, - "theme.docs.breadcrumbs.navAriaLabel": { - "message": "Breadcrumbs", - "description": "The ARIA label for the breadcrumbs" - }, - "theme.docs.DocCard.categoryDescription": { - "message": "{count} items", - "description": "The default description for a category card in the generated index about how many items this category includes" - }, - "theme.docs.tagDocListPageTitle.nDocsTagged": { - "message": "One doc tagged|{count} docs tagged", - "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" - }, - "theme.docs.tagDocListPageTitle": { - "message": "{nDocsTagged} with \"{tagName}\"", - "description": "The title of the page for a docs tag" - }, - "theme.docs.versionBadge.label": { - "message": "Version: {versionLabel}" - }, - "theme.docs.versions.unreleasedVersionLabel": { - "message": "This is unreleased documentation for {siteTitle} {versionLabel} version.", - "description": "The label used to tell the user that he's browsing an unreleased doc version" - }, - "theme.docs.versions.unmaintainedVersionLabel": { - "message": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.", - "description": "The label used to tell the user that he's browsing an unmaintained doc version" - }, - "theme.docs.versions.latestVersionSuggestionLabel": { - "message": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).", - "description": "The label used to tell the user to check the latest version" - }, - "theme.docs.versions.latestVersionLinkLabel": { - "message": "latest version", - "description": "The label used for the latest version suggestion link label" - }, - "theme.common.editThisPage": { - "message": "Edit this page", - "description": "The link label to edit the current page" - }, - "theme.docs.paginator.navAriaLabel": { - "message": "Docs pages navigation", - "description": "The ARIA label for the docs pagination" - }, - "theme.docs.paginator.previous": { - "message": "Previous", - "description": "The label used to navigate to the previous doc" - }, - "theme.docs.paginator.next": { - "message": "Next", - "description": "The label used to navigate to the next doc" - }, - "theme.common.headingLinkTitle": { - "message": "Direct link to heading", - "description": "Title for link to heading" - }, - "theme.lastUpdated.atDate": { - "message": " on {date}", - "description": "The words used to describe on which date a page has been last updated" - }, - "theme.lastUpdated.byUser": { - "message": " by {user}", - "description": "The words used to describe by who the page has been last updated" - }, - "theme.lastUpdated.lastUpdatedAtBy": { - "message": "Last updated{atDate}{byUser}", - "description": "The sentence used to display when a page has been last updated, and by who" - }, - "theme.navbar.mobileVersionsDropdown.label": { - "message": "Versions", - "description": "The label for the navbar versions dropdown on mobile view" - }, - "theme.common.skipToMainContent": { - "message": "Skip to main content", - "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" - }, - "theme.tags.tagsListLabel": { - "message": "Tags:", - "description": "The label alongside a tag list" - }, - "theme.AnnouncementBar.closeButtonAriaLabel": { - "message": "Close", - "description": "The ARIA label for close button of announcement bar" - }, - "theme.blog.sidebar.navAriaLabel": { - "message": "Blog recent posts navigation", - "description": "The ARIA label for recent posts in the blog sidebar" - }, - "theme.CodeBlock.copied": { - "message": "Copied", - "description": "The copied button label on code blocks" - }, - "theme.CodeBlock.copyButtonAriaLabel": { - "message": "Copy code to clipboard", - "description": "The ARIA label for copy code blocks button" - }, - "theme.CodeBlock.copy": { - "message": "Copy", - "description": "The copy button label on code blocks" - }, - "theme.CodeBlock.wordWrapToggle": { - "message": "Toggle word wrap", - "description": "The title attribute for toggle word wrapping button of code block lines" - }, - "theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": { - "message": "Toggle the collapsible sidebar category '{label}'", - "description": "The ARIA label to toggle the collapsible sidebar category" - }, - "theme.TOCCollapsible.toggleButtonLabel": { - "message": "On this page", - "description": "The label used by the button on the collapsible TOC component" - }, - "theme.blog.post.readMore": { - "message": "Read More", - "description": "The label used in blog post item excerpts to link to full blog posts" - }, - "theme.blog.post.readMoreLabel": { - "message": "Read more about {title}", - "description": "The ARIA label for the link to full blog posts from excerpts" - }, - "theme.docs.sidebar.collapseButtonTitle": { - "message": "Collapse sidebar", - "description": "The title attribute for collapse button of doc sidebar" - }, - "theme.docs.sidebar.collapseButtonAriaLabel": { - "message": "Collapse sidebar", - "description": "The title attribute for collapse button of doc sidebar" - }, - "theme.blog.post.readingTime.plurals": { - "message": "One min read|{readingTime} min read", - "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" - }, - "theme.docs.sidebar.expandButtonTitle": { - "message": "Expand sidebar", - "description": "The ARIA label and title attribute for expand button of doc sidebar" - }, - "theme.docs.sidebar.expandButtonAriaLabel": { - "message": "Expand sidebar", - "description": "The ARIA label and title attribute for expand button of doc sidebar" - }, - "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": { - "message": "← Back to main menu", - "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)" - }, - "theme.tags.tagsPageTitle": { - "message": "Tags", - "description": "The title of the tag list page" - }, - - "tds-home-开发者文档": { - "message": "Developer Documentation", - "description": "from HomePage Title" - }, - "tds-home-入门指南": { - "message": "Get Started", - "description": "from HomePage Main Button" - }, - "tds-footer-易玩(上海)网络科技有限公司": { - "message": "Yiwan (Shanghai) Network Technology Co., Ltd.", - "description": "from Footer" - }, - "tds-footer-公司地址:上海市静安区灵石路 718 号 B1 北楼": { - "message": "Address: B1 North Building, 718 Ling Shi Road, Jing An District, Shanghai", - "description": "from Footer" - }, - "tds-footer-注册地址:上海市闵行区紫星路 588 号 2 幢 2122 室": { - "message": "Registered Address: Room 2122, Building 2, Zi Xing Road, Min Hang District, Shanghai", - "description": "from Footer" - }, - "tds-header-开发者中心": { - "message": "Developer services", - "description": "from Header" - }, - "tds-home-游戏商店": { - "message": "Store Settings", - "description": "from Homepage" - }, - "tds-home-有关如何上架游戏、开放测试和参与平台活动": { - "message": "Publish your game, set up open tests, and participate in platform events", - "description": "from Homepage" - }, - "tds-home-游戏服务": { - "message": "SDK Features", - "description": "from Homepage" - }, - "tds-home-TDS 为游戏开发提供的全套 SDK 服务": { - "message": "Full set of SDK services for game development by TDS", - "description": "from Homepage" - }, - "tds-home-社区运营指南": { - "message": "Community Building", - "description": "from Homepage" - }, - "tds-home-TapTap 为开发者提供的社区新手攻略": { - "message": "A starting guide for fresh developers from TapTap", - "description": "from Homepage" - }, - "tds-home-资源下载": { - "message": "Downloads", - "description": "from Homepage" - }, - "tds-home-TapTap 相关品牌元素及开发工具包下载": { - "message": "Download TapTap related assets and development toolkits", - "description": "from Homepage" - }, - "tds-home-link-设计资源": { - "message": "Design Resources", - "description": "from Homepage" - }, - "tds-home-平台功能申请": { - "message": "Featured Applications", - "description": "from Homepage" - }, - "tds-home-TapTap 平台功能申请": { - "message": "How to appear on TapTap Home", - "description": "from Homepage" - }, - "tds-home-开发者运营手册": { - "message": "Developer Operations Manual", - "description": "from Homepage" - }, - "tds-home-link-查看更多": { - "message": "Learn More", - "description": "from Homepage" - }, - - "tds-home-域名": { - "message": "Domains", - "description": "from Homepage" - }, - "tds-home-内建账户": { - "message": "Authentication", - "description": "from Homepage" - }, - "tds-home-数据存储": { - "message": "Data Storage", - "description": "from Homepage" - }, - "tds-home-云引擎": { - "message": "LeanEngine", - "description": "from Homepage" - }, - - "tds-home-即时通讯": { - "message": "Instant Messaging", - "description": "from Homepage" - }, - - "tds-home-推送通知": { - "message": "Push Notification", - "description": "from Homepage" - }, - - "tds-home-短信": { - "message": "SMS", - "description": "from Homepage" - }, - - "tds-footer-推广": { - "message": "TapAD", - "description": "from Footer Left Link 1" - }, - "tds-footer-工作": { - "message": "Career", - "description": "from Footer Left Link 2" - }, - "tds-footer-认证": { - "message": "Verification", - "description": "from Footer Left Link 3" - }, - "tds-footer-服务协议": { - "message": "Terms of Service", - "description": "from Footer Left Link 4" - }, - "tds-footer-隐私政策": { - "message": "Privacy Policy", - "description": "from Footer Left Link 5" - }, - "tds-footer-侵权投诉": { - "message": "Report Infringement", - "description": "from Footer Left Link 6" - }, - "tds-footer-来-Discord-和我们交流": { - "message": "Join the conversation on Discord", - "description": "from Footer" - }, - - "tds.search.search": { "message": "Search" }, - "tds.search.clearQuery": { "message": "Clear the query" }, - "tds.search.cancel": { "message": "Cancel" }, - "tds.search.recent": { "message": "Recent" }, - "tds.search.removeItem": { "message": "Remove this item" }, - "tds.search.noHistory": { "message": "No recent searches" }, - "tds.search.noResults": { "message": "No results" }, - - "exchange.currency": { - "message": "Currency" - }, - "exchange.currencyType": { - "message": "Currency type (currency_type)" - }, - "exchange.currentRate": { - "message": "Real time exchange rate: (Standard Currency: Dollar USD)" - }, - "exchange.commonCurrent": { - "message": "Common currency" - } -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current.json deleted file mode 100644 index d05e6f4a6..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "sidebar.store.link.关于 TapTap": { - "message": "About TapTap", - "description": "The label for category 关于 TapTap in sidebar store" - }, - "sidebar.store.link.侵权投诉": { - "message": "Report Infringement", - "description": "The label for category 侵权投诉 in sidebar store" - }, - "sidebar.store.link.推广投放": { - "message": "TapAD", - "description": "The label for category 推广投放 in sidebar store" - }, - - "sidebar.sdk.category.开发指南": { - "message": "Guides", - "description": "The label for category 开发指南 in sidebar sdk" - }, - "sidebar.sdk.category.最佳实践": { - "message": "Best Practices", - "description": "The label for category 最佳实践 in sidebar sdk" - }, - - "sidebar.sdk.category.入门指南": { - "message": "Getting Started", - "description": "The label for category 入门指南 in sidebar sdk" - }, - "sidebar.sdk.category.TapTap 登录": { - "message": "TapTap Login", - "description": "The label for category TapTap 登录 in sidebar sdk" - }, - "sidebar.sdk.category.内嵌动态": { - "message": "Embedded Moments", - "description": "The label for category 内嵌动态 in sidebar sdk" - }, - "sidebar.sdk.category.内建账户": { - "message": "Authentication", - "description": "The label for category 内建账户 in sidebar sdk" - }, - "sidebar.sdk.category.游戏好友": { - "message": "Friends", - "description": "The label for category 游戏好友 in sidebar sdk" - }, - "sidebar.sdk.category.成就系统": { - "message": "Achievements", - "description": "The label for category 成就系统 in sidebar sdk" - }, - "sidebar.sdk.category.公告系统": { - "message": "Billboard", - "description": "The label for category 公告系统 in sidebar sdk" - }, - "sidebar.sdk.category.排行榜": { - "message": "Leaderboard", - "description": "The label for category 排行榜 in sidebar sdk" - }, - "sidebar.sdk.category.云存档": { - "message": "Cloud Save", - "description": "The label for category 云存档 in sidebar sdk" - }, - "sidebar.sdk.category.礼包系统": { - "message": "Gift Packages", - "description": "The label for category 礼包系统 in sidebar sdk" - }, - "sidebar.sdk.category.防沉迷": { - "message": "Anti-addiction", - "description": "The label for category 防沉迷 in sidebar sdk" - }, - "sidebar.sdk.category.付费下载和正版验证": { - "message": "Paid Games & Copyright Verification", - "description": "The label for category 付费下载和正版验证 in sidebar sdk" - }, - "sidebar.sdk.category.应用安全": { - "message": "Application Security", - "description": "The label for category 应用安全 in sidebar sdk" - }, - "sidebar.sdk.category.TapTap Connect(悬浮窗)": { - "message": "TapTap Connect (Floating Window) ", - "description": "The label for category TapTap Connect(悬浮窗) in sidebar sdk" - }, - "sidebar.sdk.category.唤起更新": { - "message": "Updates", - "description": "The label for category 唤起更新 in sidebar sdk" - }, - "sidebar.sdk.category.数据存储": { - "message": "Data Storage", - "description": "The label for category 数据存储 in sidebar sdk" - }, - "sidebar.sdk.category.全文搜索": { - "message": "Full-Text Search", - "description": "The label for category 全文搜索 in sidebar sdk" - }, - "sidebar.sdk.category.云引擎": { - "message": "Cloud Engine", - "description": "The label for category 云引擎 in sidebar sdk" - }, - "sidebar.sdk.category.部署应用": { - "message": "Deploying Apps", - "description": "The label for category 部署应用 in sidebar sdk" - }, - "sidebar.sdk.category.数据库": { - "message": "Databases", - "description": "The label for category 数据库 in sidebar sdk" - }, - "sidebar.sdk.category.深入了解": { - "message": "Deep Dive", - "description": "The label for category 深入了解 in sidebar sdk" - }, - "sidebar.sdk.category.实时语音": { - "message": "RTC", - "description": "The label for category 实时语音 in sidebar sdk" - }, - "sidebar.sdk.category.即时通讯": { - "message": "Instant Messaging", - "description": "The label for category 即时通讯 in sidebar sdk" - }, - "sidebar.sdk.category.TapTap Connect(悬浮窗)": { - "message": "TapTap Connect (Floating Window) ", - "description": "The label for category TapTap Connect(悬浮窗) in sidebar sdk" - }, - "sidebar.sdk.category.推送通知": { - "message": "Push Notification", - "description": "The label for category 推送通知 in sidebar sdk" - }, - - "sidebar.design.link.品牌素材": { - "message": "Brand Resources", - "description": "The label for link 品牌素材 in sidebar design" - }, - "sidebar.sdk.category.数据分析": { - "message": "TapDB", - "description": "The label for category 数据分析 in sidebar sdk" - }, - "sidebar.sdk.category.功能指南": { - "message": "Feature Guide", - "description": "The label for category 功能指南 in sidebar design" - }, - "sidebar.sdk.category.常见问题": { - "message": "FAQ", - "description": "The label for category 常见问题 in sidebar sdk" - }, - "sidebar.sdk.category.功能更新": { - "message": "Changelog", - "description": "The label for category 功能更新 in sidebar design" - }, - "sidebar.sdk.category.自定义事件": { - "message": "Custom Event", - "description": "The label for category 自定义事件 in sidebar design" - }, - "sidebar.sdk.category.其他": { - "message": "Other", - "description": "The label for category 其他 in sidebar design" - }, - "sidebar.sdk.category.多人在线对战": { - "message": "Multiplayer", - "description": "The label for category 多人在线对战 in sidebar design" - }, - "sidebar.sdk.category.快速入门": { - "message": "Quick Start", - "description": "The label for category 快速入门 in sidebar design" - }, - "sidebar.sdk.category.客服": { - "message": "TapSupport", - "description": "The label for category 客服 in sidebar sdk" - } -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/android-package-visibility.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/android-package-visibility.mdx deleted file mode 100644 index 62ba95767..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/android-package-visibility.mdx +++ /dev/null @@ -1,23 +0,0 @@ -Android 11 (API level 30) has ramped up its privacy protection policies resulting in a series of changes and restrictions, one of the key changes being [Package Visibility](https://developer.android.com/about/versions/11/privacy/package-visibility), which prevents third-party apps from launching the TapTap app. This has affected related TapTap services from functioning properly, including but not limited to accessing TapTap for updates and purchase verification. - -**Solution 1** - -Compile the game with `targetSdkVersion` set to 29 (setting this to 30 or above will lead to the problem). - -**Solution 2** - -1. Change gradle build tools to 4.1.0+: - - ```java - classpath 'com.android.tools.build:gradle:4.1.0' - ``` - -2. Add the following lines to AndroidManifest.xml: - - ```xml - - - - - - ``` diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/languages.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/languages.mdx deleted file mode 100644 index a63adc26d..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/languages.mdx +++ /dev/null @@ -1,112 +0,0 @@ -import MultiLang from "/src/docComponents/MultiLang"; - - - -<> - -```cs -TapCommon.SetLanguage(TapLanguage.AUTO); -``` - -The following languages are supported: - -```cs -namespace TapTap.Common -{ - public enum TapLanguage - { - AUTO = 0, // Auto - ZH_HANS = 1, // Simplified Chinese - EN = 2, // English - ZH_HANT = 3, // Traditional Chinese - JA = 4, // Japanese - KO = 5, // Korean - TH = 6, // Thai - ID = 7, // Indonesian - } -} -``` - - - -<> - -```java -int auto = 0; -TapBootstrap.setPreferredLanguage(auto); -``` - -The following languages are supported: - -``` -0 Auto -1 Simplified Chinese -2 English -3 Traditional Chinese -4 Japanese -5 Korean -6 Thai -7 Indonesian -``` - - - -<> - -```objc -[TapBootstrap setPreferredLanguage:TapLanguageType_Auto]; -``` - -The following languages are supported: - -```objc -typedef NS_ENUM (NSInteger, TapLanguageType) { - TapLanguageType_Auto = 0, // Auto - TapLanguageType_zh_Hans, // Simplified Chinese - TapLanguageType_en, // English - TapLanguageType_zh_Hant, // Traditional Chinese - TapLanguageType_ja, // Japanese - TapLanguageType_ko, // Korean - TapLanguageType_th, // Thai - TapLanguageType_id, // Indonesian -}; -``` - - - -<> - -```cpp -TapUECommon::SetLanguage(ELanguageType::AUTO); -``` - -The following languages are supported: - -```cpp -UENUM(BlueprintType) -enum class ELanguageType : uint8 -{ - AUTO = 0, // Auto - ZH, // Simplified Chinese - EN, // English - ZHTW, // Traditional Chinese - JA, // Japanese - KO, // Korean - TH, // Thai - ID, // Indonesian - DE, // German - ES, // spanish - FR, // French - PT, // Portuguese - RU, // Russian - TR, // Turkish - VI, // Vietnamese -}; -``` - - - - - -When *Auto* is selected, the SDK will set the language based on the system language. If the system language is not among the supported languages listed above, the SDK will set the language according to the region configured when the SDK is initialized. -If the region is China mainland, the language will be Simplified Chinese. Otherwise, the language will be English. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/setup-domain.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/setup-domain.mdx deleted file mode 100644 index bafc6c32d..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/setup-domain.mdx +++ /dev/null @@ -1,62 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - - - - - -To use TDS cloud services, you must set `server_url` with the API domain when initializing the SDK. You can obtain a **shared domain** provided by TDS by going to **Developer Center > Your Game > Game Services > Configuration > Domain**. - - - - - -To use TDS cloud services, you must bind a custom API domain to isolate your game’s gateway from other developers’. This will help your app avoid being affected by other apps in case of a DDoS attack. - -If you plan to use the file service provided by the Data Storage service, which includes the storage of the files carried by multimedia messages sent through the Instant Messaging service (such as images, audios, and videos), you must bind a **file access domain**. - -### Binding an API Domain - -To bind an API domain, you must first have **a domain that already got an ICP license**. - -
    -See how to bind an API domain - -Assuming your domain is `example.com`, the following steps show how you can bind it to your app as an API domain: - -![domain guide](https://capacity-files.lcfile.com/RonCpipde80meo5BL8fxrjHTee39Wit6/domain-guide.png) - -- Go to **Developer Center > Your Game > Game Services > Configuration > Domain**, then tap **Add domain**. API domains do not support the direct binding of bare domains. You must add a custom name before the primary domain. In other words, you must create a subdomain, such as `api.example.com`. -- When the console displays **Verifying ICP**, please wait patiently. -- If the domain does not have an ICP license, you will see **Failed**. -- If the domain passes the verification, you will see **Configure DNS** below the domain. -- Now proceed to the domain service provider’s console, open the domain’s DNS settings, and add an A record (this can direct the domain to an IP address). Copy the custom domain you entered in the Developer Center and the A record displayed under **Recommended DNS Configuration** into the corresponding fields. -- It will take some time for your DNS records to take effect and for our server to apply for certificates for your domain (if you enabled automatic certificate management), so please wait patiently. Once the record is in effect, the console will display **Bound**. - -
    - -When initializing the SDK, please set `server_url` to the custom domain (e.g. `https://api.example.com`). The domain shown here is just an example and you should replace `api.example.com` with your own domain. Please make sure to include `https://` in the value of `server_url`. - -It will take some time for you to finish setting up a custom domain. Therefore, TDS provides you with **shared domains** for testing. However, the availability of these domains cannot be guaranteed and the domains may be prone to DDoS attacks. Before a game goes online, be sure to confirm that the API access domain used for the game is your own domain. Do not use shared domains in a production environment. - -### Binding File Access URL - -Go to **Developer Center > Your Game > Game Services > Cloud Services > Data Storage > Files > Settings > File access domain** to bind file domains. The process is the same as that for binding custom API domains, except that: - -1. API domains use A records while file domains use CNAME records. File domains also do not support the binding of bare domains. For example, if your primary domain is `example.com`, you can bind `files.example.com` as a file domain. -2. Once the binding is complete, you must go to **Files > Settings > File access URL** and click “Edit” to switch to your custom domain. - -:::info - -Each subdomain may only be bound to one game. Additionally, custom API domains and file domains cannot share the same subdomain. If you have already bound a subdomain with a game, you will see “This domain is already bound to an application” when you try to bind it with another game. When this occurs, you may try a different subdomain under the same primary domain to proceed with the binding. - -::: - -
    - -
    - - - -See [Binding Your Domains](/sdk/domain/guide/). - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/tap-login-profile.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/tap-login-profile.mdx deleted file mode 100644 index 5ad969a96..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/tap-login-profile.mdx +++ /dev/null @@ -1,35 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -`Profile` will contain the following info: - - - -| Parameter | Description | -| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | The player's nickname on TapTap. | -| `avatar` | The URL of the player's profile picture on TapTap. | -| `openid` | A **unique identifier** generated from the user's profile and the game's information. Each player has a unique `openid` in each game. | -| `unionid` | A unique identifier generated from the user's profile and the vendor's information. A player has the same `unionid` for all the games made by the same vendor, but different ones for different vendors. | -| `email` | The email used by the user to register TapTap | -| `emailVerified` | Whether the email used by the user to register TapTap is verified | - - - - - -| Parameter | Description | -| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | The player's nickname on TapTap. | -| `avatar` | The URL of the player's profile picture on TapTap. | -| `openid` | A **unique identifier** generated from the user's profile and the game's information. Each player has a unique `openid` in each game. | -| `unionid` | A unique identifier generated from the user's profile and the vendor's information. A player has the same `unionid` for all the games made by the same vendor, but different ones for different vendors. | - - - -`openid` and `unionid` are Base64-encoded strings (with padding) containing characters from `A-Za-z0-9+/=`. - -:::info -Since `unionid` is strongly associated with the vendor, if your game gets transferred to a different vendor, the `unionid` of the users will get changed. - -If your game depends on `unionid`, our technical support staff will have a discussion with you regarding how the data should be handled so the game will still work properly after being transferred. -::: diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/unity-sdk-installation.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/unity-sdk-installation.mdx deleted file mode 100644 index c8582b0f7..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/_partials/unity-sdk-installation.mdx +++ /dev/null @@ -1,54 +0,0 @@ -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; - -The SDK can be imported either **through Unity Package Manager** or manually**. Please choose according to your project needs. - -#### Method 1: Use Unity Package Manager - -##### NPMJS Installation - -As of version 3.25.0, TapSDK supports NPMJS installation, with the advantage that only the version number needs to be configured and nested dependencies are supported. - -Add the following dependencies to your project's `Packages/manifest.json` file: - - - {`"dependencies":{ - ${props.npmDeps.map(dep => `"${dep}":"${sdkVersions.taptap.unity}",`).join('\n ')} -}`} - - -However, it should be noted that `scopedRegistries` must be declared under the same level of dependencies in `Packages/manifest.json`: - - -{`"scopedRegistries": [ - { - "name": "NPMJS", - "url": "https://registry.npmjs.org/", - "scopes": ["com.tapsdk", "com.taptap", "com.leancloud"] - } - ]`} - - -##### GitHub Installation - -Add the following dependencies to your project's `Packages/manifest.json` file: - - - {`"dependencies":{ - ${props.githubDeps.map(dep => `"${dep.package}":"${dep.url}",`).join('\n ')} -}`} - - -Select **Window > Package Manager** in the top menu of Unity to see the packages already installed in your project. - -#### Method 2: Import Manually - -1. Find the download addresses of **TapSDK Unity** and **LeanCloud C# SDK** on the [download page](/tap-download) , and download `TapSDK-UnityPackage.zip` and `LeanCloud-SDK-Realtime-Unity.zip` respectively. - -2. In the Unity project, go to **Assets > Import Packages > Custom Packages**, and from the unzipped `TapSDK-UnityPackage.zip`, select the TapSDK packages that you want to use in the game, and import them: - -
      - {props.unitypackageModules.map(module =>
    • {module.name} {module.desc}。
    • )} -
    - -3. The decompresses `LeanCloud-SDK-Realtime-Unity.zip` is in the Plugins folder, drag and drop it to Unity. \ No newline at end of file diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/_category_.json deleted file mode 100644 index 5da22988f..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "内建账户", - "collapsed": true, - "position": 5 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/faq.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/faq.mdx deleted file mode 100644 index 8110d8eea..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/faq.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: 内建账户常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -### 应用内用户的密码需要加密吗 - -不需要加密密码,我们的服务端已使用随机生成的 salt,自动对密码做了加密。 如果用户忘记了密码,可以调用 `requestResetPassword` 方法(具体查看 SDK 的 AVUser 用法),向用户注册的邮箱发送邮件,用户以此可自行重设密码。 在整个过程中,密码都不会有明文保存的问题,密码也不会在客户端保存,只是会保存 sessionToken 来标示用户的登录状态。 - -### sessionToken 在什么情况下会失效? - -如果在控制台的存储的设置中勾选了「密码修改后,强制客户端重新登录」,则用户修改密码后, sessionToken 会变更,需要重新登录。如果没有勾选这个选项,Token 就不会改变。当新建应用时,这个选项默认是被勾上的。 - -### 在 PC 端用手机号登录,在小程序上用微信登录,如何绑定到同一个账号上? - -从逻辑上,在 PC 端登录的账号,与在小程序中用微信登录的账号,他们没有任何可以联系在一起的地方。如果都是独立创建了两个账号,只能在业务层面进行绑定(也就是将一个账号的所有关联对象全都迁移到另一个账号,然后删除原账号)。 - -如果可以在业务上加一些限制,则可以避免上面这种「创建了两个独立的账号」的情况。比如,如果手机号是账号必须设置的信息,那么我们可以在以手机号作为关联项。具体的步骤如下,首先是 `loginWithWeapp` 并带上 `failOnNotExist` 参数,这样如果该微信关联的用户已经存在则照常登录,如果没有则会失败,此时跳转到使用手机号登录/注册的页面,让用户通过手机号登录或注册,成功之后再通过 `associateWithWeapp` 接口关联当前微信账号。 - -### 不通过短信验证能否强制修改 _User 表 mobilePhoneVerified 字段,使其设置为 true? - -可以通过云引擎使用 [master key](/sdk/engine/functions/sdk/#使用超级权限) 来修改 `mobilePhoneVerified` 的值。因为云引擎运行在可信的服务器端环境中,所以可以全局开启超级权限(Master Key),这样云端会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据,当然这种方式也只允许调用一些仅供 Master Key 使用的 API。 diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/guide.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/guide.mdx deleted file mode 100644 index 865d680d3..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/guide.mdx +++ /dev/null @@ -1,3852 +0,0 @@ ---- -title: 内建账户指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; - -用户系统几乎是每款应用都要加入的功能,我们为此专门提供了一个 `LCUser` 类来方便应用使用各项用户管理的功能。 - -`LCUser` 是 `LCObject` 的子类,这意味着任何 `LCObject` 提供的方法也适用于 `LCUser`,唯一的区别就是 `LCUser` 提供一些额外的用户管理相关的功能。 - -## 用户的属性 - -`LCUser` 相比一个普通的 `LCObject` 多出了以下属性: - -- `username`:用户的用户名。 -- `password`:用户的密码。 -- `email`:用户的电子邮箱。 -- `emailVerified`:用户的电子邮箱是否已验证。 -- `mobilePhoneNumber`:用户的手机号。 -- `mobilePhoneVerified`:用户的手机号是否已验证。 - -在接下来对用户功能的介绍中我们会逐一了解到这些属性。 - -## 注册 - -用户第一次打开应用的时候,可以让用户注册一个账户。下面的代码展示了一个典型的使用用户名和密码注册的流程: - - - -<> - -```cs -// 创建实例 -LCUser user = new LCUser(); - -// 等同于 user["username"] = "Tom"; -user.Username = "Tom"; -user.Password = "cat!@#123"; - -// 可选 -user.Email = "tom@xd.com"; -user.Mobile = "+8619201680101"; - -// 设置其他属性的方法跟 LCObject 一样 -user["gender"] = "secret"; -await user.SignUp(); -``` - -新建 `LCUser` 的操作应使用 `SignUp` 而不是 `Save`,但以后的更新操作就可以用 `Save` 了。 - - - -<> - -```java -// 创建实例 -LCUser user = new LCUser(); - -// 等同于 user.put("username", "Tom") -user.setUsername("Tom"); -user.setPassword("cat!@#123"); - -// 可选 -user.setEmail("tom@xd.com"); -user.setMobilePhoneNumber("+8619201680101"); - -// 设置其他属性的方法跟 LCObject 一样 -user.put("gender", "secret"); - -user.signUpInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 注册成功 - System.out.println("注册成功。objectId:" + user.getObjectId()); - } - public void onError(Throwable throwable) { - // 注册失败(通常是因为用户名已被使用) - } - public void onComplete() {} -}); -``` - -新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 - - - -<> - -```objc -// 创建实例 -LCUser *user = [LCUser user]; - -// 等同于 [user setObject:@"Tom" forKey:@"username"] -user.username = @"Tom"; -user.password = @"cat!@#123"; - -// 可选 -user.email = @"tom@xd.com"; -user.mobilePhoneNumber = @"+8619201680101"; - -// 设置其他属性的方法跟 LCObject 一样 -[user setObject:@"secret" forKey:@"gender"]; - -[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 注册成功 - NSLog(@"注册成功。objectId:%@", user.objectId); - } else { - // 注册失败(通常是因为用户名已被使用) - } -}]; -``` - -新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 - - - -<> - -```swift -do { - // 创建实例 - let user = LCUser() - - // 等同于 user.set("username", value: "Tom") - user.username = LCString("Tom") - user.password = LCString("cat!@#123") - - // 可选 - user.email = LCString("tom@xd.com") - user.mobilePhoneNumber = LCString("+8619201680101") - - // 设置其他属性的方法跟 LCObject 一样 - try user.set("gender", value: "secret") - - _ = user.signUp { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -新建 `LCUser` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -<> - -```dart -// 创建实例 -LCUser user = LCUser(); - -// 等同于 user['username'] = 'Tom'; -user.username = 'Tom'; -user.password = 'cat!@#123'; - -// 可选 -user.email = 'tom@xd.com'; -user.mobile = '+8619201680101'; - -// 设置其他属性的方法跟 LCObject 一样 -user['gender'] = 'secret'; -await user.signUp(); -``` - -新建 `LCUser` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -<> - -```js -// 创建实例 -const user = new AV.User(); - -// 等同于 user.set('username', 'Tom') -user.setUsername("Tom"); -user.setPassword("cat!@#123"); - -// 可选 -user.setEmail("tom@xd.com"); -user.setMobilePhoneNumber("+8619201680101"); - -// 设置其他属性的方法跟 AV.Object 一样 -user.set("gender", "secret"); - -user.signUp().then( - (user) => { - // 注册成功 - console.log(`注册成功。objectId:${user.id}`); - }, - (error) => { - // 注册失败(通常是因为用户名已被使用) - } -); -``` - -新建 `AV.User` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -<> - -```python -# 创建实例 -user = leancloud.User() - -# 等同于 user.set('username', 'Tom') -user.set_username('Tom') -user.set_password('cat!@#123') - -# 可选 -user.set_email('tom@xd.com') -user.set_mobile_phone_number('+8619201680101') - -# 设置其他属性的方法跟 leancloud.Object 一样 -user.set('gender', 'secret') - -user.sign_up() -``` - -新建 `leancloud.User` 的操作应使用 `sign_up` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -<> - -```php -// 创建实例 -$user = new User(); - -// 等同于 $user->set("username", "Tom") -$user->setUsername("Tom"); -$user->setPassword("cat!@#123"); - -// 可选 -$user->setEmail("tom@xd.com"); -$user->setMobilePhoneNumber("+8619201680101"); - -// 设置其他属性的方法跟 LeanObject 一样 -$user->set("gender", "secret"); - -$user->signUp(); -``` - -新建 `User` 的操作应使用 `signUp` 而不是 `save`,但以后的更新操作就可以用 `save` 了。 - - - -```go -// 注册用户 -user, err := client.Users.SignUp("Tom", "cat!@#123") -if err != nil { - panic(err) -} - -// 设置其他属性 -if err := client.Users.ID(user.ID).Set("email", "tom@xd.com", leancloud.UseUser(user)); err != nil { - panic(err) -} -``` - - - -如果收到 `202` 错误码,意味着已经存在使用同一 `username` 的账号,此时应提示用户换一个用户名。除此之外,每个用户的 `email` 和 `mobilePhoneNumber` 也需要保持唯一性,否则会收到 `203` 或 `214` 错误。可以考虑在注册时把用户的 `username` 设为与 `email` 相同,这样用户可以直接 [用邮箱重置密码](#重置密码)。 - -采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码(云端对密码的长度、复杂度不作限制),并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调 **在客户端,应用切勿再次对密码加密,这会导致 [重置密码](#重置密码) 等功能失效**。 - -### 手机号注册 - -对于移动应用来说,允许用户以手机号注册是个很常见的需求。实现该功能大致分两步,第一步是让用户提供手机号,点击「获取验证码」按钮后,该号码会收到一个六位数的验证码: - - - -```cs -await LCSMSClient.RequestSMSCode("+8619201680101"); -``` - -```java -LCSMSOption option = new LCSMSOption(); -option.setSignatureName("sign_name"); // 设置短信签名名称 -LCSMS.requestSMSCodeInBackground("+8619201680101", option).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - } - @Override - public void onNext(LCNull avNull) { - Log.d("TAG","Result: succeed to request SMSCode."); - } - @Override - public void onError(Throwable throwable) { - Log.d("TAG","Result: failed to request SMSCode. cause:" + throwable.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -LCShortMessageRequestOptions *options = [[LCShortMessageRequestOptions alloc] init]; -options.templateName = @"template_name"; // 控制台配置好的模板名称 -options.signatureName = @"sign_name"; // 控制台配置好的短信签名名称 -[LCSMS requestShortMessageForPhoneNumber:@"+8619201680101" options:options callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - /* 请求成功 */ - } else { - /* 请求失败 */ - } -}]; -``` - -```swift -// templateName 是短信模版名称,signatureName 是短信签名名称。可以在控制台 > 短信 > 设置中查看。 -_ = LCSMSClient.requestShortMessage(mobilePhoneNumber: "+8619201680101", templateName: "template_name", signatureName: "sign_name") { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCSMSClient.requestSMSCode('+8619201680101'); -``` - -```js -AV.Cloud.requestSmsCode("+8619201680101"); -``` - -```python -leancloud.cloud.request_sms_code('+8619201680101') -``` - -```php -SMS::requestSmsCode("+8619201680101"); -``` - -```go -// 暂不支持 -``` - - - -用户填入验证码后,用下面的方法完成注册: - - - -```cs -await LCUser.SignUpOrLoginByMobilePhone("+8619201680101", "123456"); -``` - -```java -LCUser.signUpOrLoginByMobilePhoneInBackground("+8619201680101", "123456").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 注册成功 - System.out.println("注册成功。objectId:" + user.getObjectId()); - } - public void onError(Throwable throwable) { - // 验证码不正确 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser signUpOrLoginWithMobilePhoneNumberInBackground:@"+8619201680101" smsCode:@"123456" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 注册成功 - NSLog(@"注册成功。objectId:%@", user.objectId); - } else { - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.signUpOrLogIn(mobilePhoneNumber: "+8619201680101", verificationCode: "123456", completion: { (result) in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -}) -``` - -```dart - await LCUser.signUpOrLoginByMobilePhone('+8619201680101', '123456'); -``` - -```js -AV.User.signUpOrlogInWithMobilePhone("+8619201680101", "123456").then( - (user) => { - // 注册成功 - console.log(`注册成功。objectId:${user.id}`); - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -user = leancloud.User.signup_or_login_with_mobile_phone('+8619201680101', '123456') -``` - -```php -User::signUpOrLoginByMobilePhone("+8619201680101", "123456"); -``` - -```go -user, err := client.Users.SignUpByMobilePhone("+8619201680101", "123456") -if err != nil { - panic(err) -} -``` - - - -`username` 将与 `mobilePhoneNumber` 相同,`password` 会由云端随机生成。如果希望让用户指定密码,可以在客户端让用户填写手机号和密码,然后按照上一小节使用用户名和密码注册的流程,将用户填写的手机号作为 `username` 和 `mobilePhoneNumber` 的值同时提交。同时根据业务需求,在**云服务控制台 > 内建账户 > 设置**勾选**未验证手机号码的用户,禁止登录**、**已验证手机号码的用户,允许以短信验证码登录**。 - -### 手机号格式 - -云端接受的手机号以 `+` 和国家代码开头,后面紧跟着剩余的部分。手机号中不应含有任何划线、空格等非数字字符。例如,`+15559463664` 是一个合法的美国或加拿大手机号(`1` 是国家代码),`+8619201680101` 是一个合法的中国手机号(`86` 是国家代码)。 - -请参阅官网的[价格](https://www.leancloud.cn/pricing/)页面以了解支持的国家和地区。 - -## 登录 - -下面的代码用用户名和密码登录一个账户: - - - -```cs -try { - // 登录成功 - LCUser user = await LCUser.Login("Tom", "cat!@#123"); -} catch (LCException e) { - // 登录失败(可能是密码错误) - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` - -```objc -[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 登录失败(可能是密码错误) - } -}]; -``` - -```swift -_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - // 登录成功 - LCUser user = await LCUser.login('Tom', 'cat!@#123'); -} on LCException catch (e) { - // 登录失败(可能是密码错误) - print('${e.code} : ${e.message}'); -} -``` - -```js -AV.User.logIn("Tom", "cat!@#123").then( - (user) => { - // 登录成功 - }, - (error) => { - // 登录失败(可能是密码错误) - } -); -``` - -```python -user = leancloud.User() -user.login(username='Tom', password='cat!@#123') -``` - -```php -User::logIn("Tom", "cat!@#123"); -``` - -```go -user, err := client.Users.LogIn("Tom", "cat!@#123") -if err != nil { - panic(err) -} -``` - - - -### 邮箱登录 - -下面的代码用邮箱和密码登录一个账户: - - - -```cs -try { - // 登录成功 - LCUser user = await LCUser.LoginByEmail("tom@xd.com", "cat!@#123"); -} catch (LCException e) { - // 登录失败(可能是密码错误) - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.loginByEmail("tom@xd.com", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` - -```objc -[LCUser loginWithEmail:@"tom@xd.com" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 登录失败(可能是密码错误) - } -}]; -``` - -```swift -_ = LCUser.logIn(email: "tom@xd.com", password: "cat!@#123") { result in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - // 登录成功 - LCUser user = await LCUser.loginByEmail('tom@xd.com', 'cat!@#123'); -} on LCException catch (e) { - // 登录失败(可能是密码错误) - print('${e.code} : ${e.message}'); -} -``` - -```js -AV.User.loginWithEmail("tom@xd.com", "cat!@#123").then( - (user) => { - // 登录成功 - }, - (error) => { - // 登录失败(可能是密码错误) - } -); -``` - -```python -user = leancloud.User() -user.login(email='tom@xd.com', password='cat!@#123') -``` - -```php -User::logInWithEmail("tom@xd.com", "cat!@#123"); -``` - -```go -user, err := client.LoginByEmail("tom@xd.com", "cat!@#123") -if err != nil { - panic(err) -} - -fmt.Println(user) -``` - - - -### 手机号登录 - -如果应用允许用户以手机号注册,那么也可以让用户以手机号配合密码或短信验证码登录。下面的代码用手机号和密码登录一个账户: - - - -```cs -try { - // 登录成功 - LCUser user = await LCUser.LoginByMobilePhoneNumber("+8619201680101", "cat!@#123"); -} catch (LCException e) { - // 登录失败(可能是密码错误) - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.loginByMobilePhoneNumber("+8619201680101", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` - -```objc -[LCUser logInWithMobilePhoneNumberInBackground:@"+8619201680101" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 登录失败(可能是密码错误) - } -}]; -``` - -```swift -_ = LCUser.logIn(mobilePhoneNumber: "+8619201680101", password: "cat!@#123") { result in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - // 登录成功 - LCUser user = await LCUser.loginByMobilePhoneNumber('+8619201680101', 'cat!@#123'); -} on LCException catch (e) { - // 登录失败(可能是密码错误) - print('${e.code} : ${e.message}'); -} -``` - -```js -AV.User.logInWithMobilePhone("+8619201680101", "cat!@#123").then( - (user) => { - // 登录成功 - }, - (error) => { - // 登录失败(可能是密码错误) - } -); -``` - -```python -user = leancloud.User.login_with_mobile_phone('+8619201680101', 'cat!@#123') -``` - -```php -User::logInWithMobilePhoneNumber("+8619201680101", "cat!@#123"); -``` - -```go -user, err := client.LogInByMobilePhoneNumber("+8619201680101", "cat!@#123") -if err != nil { - panic(err) -} - -fmt.Println(user) -``` - - - -默认情况下,云服务允许所有关联了手机号的用户直接以手机号登录,无论手机号是否 [通过验证](#验证手机号)。为了让应用更加安全,你可以选择只允许验证过手机号的用户通过手机号登录。可以在 **控制台 > 内建账户 > 设置** 里面开启该功能。 - -除此之外,还可以让用户通过短信验证码登录,适用于用户忘记密码且不愿重置密码的情况。和 [通过手机号注册](#手机号注册) 的步骤类似,首先让用户填写与账户关联的手机号码,然后在用户点击「获取验证码」后调用下面的方法: - - - -```cs -await LCUser.RequestLoginSMSCode("+8619201680101"); -``` - -```java -LCUser.requestLoginSmsCodeInBackground("+8619201680101").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestLoginSmsCode:@"+8619201680101"]; -``` - -```swift -_ = LCUser.requestLoginVerificationCode(mobilePhoneNumber: "+8619201680101") { result in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestLoginSMSCode('+8619201680101'); -``` - -```js -AV.User.requestLoginSmsCode("+8619201680101"); -``` - -```python -leancloud.User.request_login_sms_code('+8619201680101') -``` - -```php -SMS::requestSmsCode("+8619201680101"); -``` - -```go -if err := client.Users.RequestLoginSMSCode("+8619201680101"); err != nil { - panic(err) -} -``` - - - -用户填写收到的验证码后,用下面的方法完成登录: - - - -```cs -try { - // 登录成功 - await LCUser.SignUpOrLoginByMobilePhone("+8619201680101", "123456"); -} catch (LCException e) { - // 验证码不正确 - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.signUpOrLoginByMobilePhoneInBackground("+8619201680101", "123456").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 验证码不正确 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser logInWithMobilePhoneNumberInBackground:@"+8619201680101" smsCode:@"123456" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.logIn(mobilePhoneNumber: "+8619201680101", verificationCode: "123456") { result in - switch result { - case .success(object: let user): - print(user) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - // 登录成功 - await LCUser.signUpOrLoginByMobilePhone('+8619201680101', '123456'); -} on LCException catch (e) { - // 验证码不正确 - print('${e.code} : ${e.message}'); -} -``` - -```js -AV.User.logInWithMobilePhoneSmsCode("+8619201680101", "123456").then( - (user) => { - // 登录成功 - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -user = leancloud.User.signup_or_login_with_mobile_phone('+8619201680101', '123456') -``` - -```php -User::signUpOrLoginByMobilePhone("+8619201680101", "123456"); -``` - -```go -user, err := client.Users.LogInByMobilePhoneNumber("+8619201680101", "123456") -if err != nil { - panic(err) -} -``` - - - -### 测试手机号和固定验证码 - -在开发过程中,可能会因测试目的而需要频繁地用手机号注册登录,然而运营商的发送频率限制往往会导致测试过程耗费较多的时间。 - -为了解决这个问题,可以在 **控制台 > 短信 > 设置** 里面设置一个测试手机号,而云端会为该号码生成一个固定验证码。以后进行登录操作时,只要使用的是这个号码,云端就会直接放行,无需经过运营商网络。 - -测试手机号还可用于将 iOS 应用提交到 App Store 进行审核的场景,因为审核人员可能因没有有效的手机号码而无法登录应用来进行评估审核。如果不提供一个测试手机号,应用有可能被拒绝。 - -可参阅 [短信 SMS 服务使用指南](/sdk/sms/guide/) 来了解更多有关短信发送和接收的限制。 - -### 单设备登录 - -某些场景下需要确保用户的账户在同一时间只在一台设备上登录,也就是说当用户在一台设备上登录后,其他设备上的会话全部失效。可以按照以下方案来实现: - -1. 新建一个专门用于记录用户登录信息和当前设备信息的 class。 -2. 每当用户在新设备上登录时,将该 class 中该用户对应的设备更新为该设备。 -3. 在另一台设备上打开客户端时,检查该设备是否与云端保存的一致。若不一致,则将用户 [登出](#当前用户)。 - -### 账户锁定 - -输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 `{ "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." }`,开发者可在客户端进行必要提示。 - -锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 - -## 验证邮箱 - -可以通过要求用户在登录或使用特定功能之前验证邮箱的方式防止恶意注册。默认情况下,当用户注册或变更邮箱后,`emailVerified` 会被设为 `false`。在应用的 **云服务控制台 > 内建账户 > 设置** 中,可以开启 **启用邮箱验证功能** 选项,这样当用户注册或变更邮箱时,会收到一封含有验证链接的邮件。在同一设置页面还可找到阻止未验证邮箱的用户登录的选项。 - -如果用户忘记点击链接并且在未来某一时刻需要进行验证,可以用下面的代码发送一封新的邮件: - - - -```cs -await LCUser.RequestEmailVerify("tom@xd.com"); -``` - -```java -LCUser.requestEmailVerifyInBackground("tom@xd.com").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestEmailVerify:@"tom@xd.com"]; -``` - -```swift -_ = LCUser.requestVerificationMail(email: "tom@xd.com") { result in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestEmailVerify('tom@xd.com'); -``` - -```js -AV.User.requestEmailVerify("tom@xd.com"); -``` - -```python -leancloud.User.request_email_verify('tom@xd.com') -``` - -```php -User::requestEmailVerify("tom@xd.com"); -``` - -```go -if err := client.Users.RequestEmailVerify("tom@xd.com"); err != nil { - panic(err) -} -``` - - - -用户点击邮件内的链接后,`emailVerified` 会变为 `true`。如果用户的 `email` 属性为空,则该属性永远不会为 `true`。 - -## 验证手机号 - -和 [验证邮箱](#验证邮箱) 类似,应用还可以要求用户在登录或使用特定功能之前验证手机号。默认情况下,当用户注册或变更手机号后,`mobilePhoneVerified` 会被设为 `false`。在应用的 **控制台 > 内建账户 > 设置** 中,可以开启阻止未验证手机号的用户登录的选项。 - -可以用下面的代码发送一条新的验证码(如果相应用户的 `mobilePhoneVerified` 已经为 `true`,那么验证短信不会发送): - - - -```cs -await LCUser.RequestMobilePhoneVerify("+8619201680101"); -``` - -```java -LCUser.requestMobilePhoneVerifyInBackground("+8619201680101").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestMobilePhoneVerify:@"+8619201680101" withBlock:^(BOOL succeeded, NSError * _Nullable error) { - if(succeeded){ - // 请求成功 - }else{ - // 请求失败 - } -}]; -``` - -```swift -_ = LCUser.requestVerificationCode(mobilePhoneNumber: "+8619201680101") { result in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestMobilePhoneVerify('+8619201680101'); -``` - -```js -AV.User.requestMobilePhoneVerify("+8619201680101"); -``` - -```python -leancloud.User.request_mobile_phone_verify('+8619201680101') -``` - -```php -User::requestMobilePhoneVerify("+8619201680101"); -``` - -```go -if err := client.Users.RequestMobilePhoneVerify("+8619201680101"); err != nil { - panic(err) -} -``` - - - -用户填写验证码后,调用下面的方法来完成验证。`mobilePhoneVerified` 将变为 `true`: - - - -```cs -await LCUser.VerifyMobilePhone("+8619201680101", "123456"); -``` - -```java -LCUser.verifyMobilePhoneInBackground("123456").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // mobilePhoneVerified 将变为 true - } - public void onError(Throwable throwable) { - // 验证码不正确 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser verifyMobilePhone:@"123456" withBlock:^(BOOL succeeded, NSError * _Nullable error) { - if(succeeded){ - // mobilePhoneVerified 将变为 true - }else{ - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.verifyMobilePhoneNumber(mobilePhoneNumber: "+8619201680101", verificationCode: "123456") { result in - switch result { - case .success: - // mobilePhoneVerified 将变为 true - break - case .failure(error: let error): - // 验证码不正确 - print(error) - } -} -``` - -```dart -await LCUser.verifyMobilePhone('+8619201680101','123456'); -``` - -```js -AV.User.verifyMobilePhone("123456").then( - () => { - // mobilePhoneVerified 将变为 true - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -leancloud.User.verify_mobile_phone_number('123456') -``` - -```php -User::verifyMobilePhone("123456"); -``` - -```go -// 暂不支持 -``` - - - -### 绑定、修改手机号之前先验证 - -除了在用户绑定、修改手机号**之后**进行验证,云服务也支持在用户绑定或修改手机号**之前**先通过短信验证。也就是说,绑定手机号或修改手机号时先请求发送验证码(用户需处于登录状态),再凭短信验证码完成绑定或修改操作。 - - - -```cs -await LCUser.RequestSMSCodeForUpdatingPhoneNumber("+8619201680101"); - -await LCUser.VerifyCodeForUpdatingPhoneNumber("+8619201680101", "123456"); -// 更新本地数据 -LCUser currentUser = await LCUser.GetCurrent(); -user.Mobile = "+8619201680101"; -``` - -```java -LCUser.requestSMSCodeForUpdatingPhoneNumberInBackground("+8619201680101",null).subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - } - @Override - public void onNext(@NonNull LCNull lcNull) { - // 成功调用 - } - @Override - public void onError(@NonNull Throwable e) { - // 调用出错 - } - @Override - public void onComplete() { - } -}); - -LCUser.verifySMSCodeForUpdatingPhoneNumberInBackground("123456", "+8619201680101").subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - } - @Override - public void onNext(@NonNull LCNull lcNull) { - // 更新本地数据 - LCUser currentUser = LCUser.getCurrentUser(); - currentUser.setMobilePhoneNumber("+8619201680101"); - } - @Override - public void onError(@NonNull Throwable e) { - // 验证码不正确 - } - @Override - public void onComplete() { - } -}); -``` - -```objc -[LCUser requestVerificationCodeForUpdatingPhoneNumber:@"+8619201680101" withBlock:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // 请求成功 - } else { - // 请求失败 - } -}]; - -[LCUser verifyCodeToUpdatePhoneNumber:@"+8619201680101" code:@"123456" withBlock:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // mobilePhoneNumber 变为 +8619201680101 - // mobilePhoneVerified 变为 true - } else { - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.requestVerificationCode(forUpdatingMobilePhoneNumber: "+8619201680101") { result in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} - -_ = LCUser.verifyVerificationCode("123456", toUpdateMobilePhoneNumber:"+8619201680101") { result in - switch result { - case .success: - // mobilePhoneNumber 变为 +8619201680101 - // mobilePhoneVerified 变为 true - break - case .failure(error: let error): - // 验证码不正确 - print(error) - } -} -``` - -```dart -await LCUser.requestSMSCodeForUpdatingPhoneNumber('+8619201680101'); - -await LCUser.verifyCodeForUpdatingPhoneNumber('+8619201680101', '123456'); -// 更新本地数据 -LCUser currentUser = await LCUser.getCurrent(); -user.mobile = '+8619201680101'; -``` - -```js -AV.User.requestChangePhoneNumber("+8619201680101"); - -AV.User.changePhoneNumber("+8619201680101", "123456").then( - () => { - // 更新本地数据 - const currentUser = AV.User.current(); - currentUser.setMobilePhoneNumber("+8619201680101"); - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -User.request_change_phone_number("+8619201680101") - -User.change_phone_number("123456", "+8619201680101") -# 更新本地数据 -current_user = leancloud.User.get_current() -current_user.set_mobile_phone_number("+8619201680101") -``` - -```php -User::requestChangePhoneNumber("+8619201680101"); - -User::changePhoneNumber("123456", "+8619201680101"); -// 更新本地数据 -$currentUser = User::getCurrentUser(); -$user->setMobilePhoneNumber("+8619201680101"); -``` - -```go -if err := client.Users.requestChangePhoneNumber("+8619201680101"); err != nil { - panic(err) -} - -if err := client.Users.ChangePhoneNumber("123456", "+8619201680101"); err != nil { - panic(err) -} -``` - - - -## 当前用户 - -用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```java -LCUser currentUser = LCUser.getCurrentUser(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```objc -LCUser *currentUser = [LCUser currentUser]; -if (currentUser != nil) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```swift -if let user = LCApplication.default.currentUser { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```js -const currentUser = AV.User.current(); -if (currentUser) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```python -current_user = leancloud.User.get_current() -if current_user is not None: - # 跳到首页 - pass -else: - # 显示注册或登录页面 - pass -``` - -```php -$currentUser = User::getCurrentUser(); -if ($currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -```go -// 暂不支持 -``` - - - -会话信息会长期有效,直到用户主动登出: - - - -```cs -await LCUser.Logout(); - -// currentUser 变为 null -LCUser currentUser = await LCUser.GetCurrent(); -``` - -```java -LCUser.logOut(); - -// currentUser 变为 null -LCUser currentUser = LCUser.getCurrentUser(); -``` - -```objc -[LCUser logOut]; - -// currentUser 变为 nil -LCUser *currentUser = [LCUser currentUser]; -``` - -```swift -LCUser.logOut() - -// currentUser 变为 nil -let currentUser = LCApplication.default.currentUser -``` - -```dart -await LCUser.logout(); - -// currentUser 变为 null -LCUser currentUser = await LCUser.getCurrent(); -``` - -```js -AV.User.logOut(); - -// currentUser 变为 null -const currentUser = AV.User.current(); -``` - -```python -user.logout() - -current_user = leancloud.User.get_current() # None -``` - -```php -User::logOut(); - -// currentUser 变为 null -$currentUser = User::getCurrentUser(); -``` - -```go -// 暂不支持 -``` - - - -## 设置当前用户 - -用户登录后,云端会返回一个 **session token** 给客户端,它会由 SDK 缓存起来并用于日后同一用户的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个用户发起的请求了。 - -以下是一些应用可能需要用到 session token 的场景: - -- 应用根据以前缓存的 session token 登录。 -- 应用内的某个 WebView 需要知道当前登录的用户。 -- 在服务端登录后,返回 session token 给客户端,客户端根据返回的 session token 登录。 - -下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效): - - - -```cs -await LCUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf"); -``` - -```java -LCUser.becomeWithSessionTokenInBackground("anmlwi96s381m6ca7o7266pzf").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 修改 currentUser - LCUser.changeCurrentUser(user, true); - } - public void onError(Throwable throwable) { - // session token 无效 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser becomeWithSessionTokenInBackground:@"anmlwi96s381m6ca7o7266pzf" block:^(LCUser * _Nullable user, NSError * _Nullable error) { - if (user != nil) { - // 登录成功 - } else { - // session token 无效 - } -}]; -``` - -```swift -_ = LCUser.logIn(sessionToken: "anmlwi96s381m6ca7o7266pzf") { (result) in - switch result { - case .success(object: let user): - // 登录成功 - print(user) - case .failure(error: let error): - // session token 无效 - print(error) - } -} -``` - -```dart -await LCUser.becomeWithSessionToken('anmlwi96s381m6ca7o7266pzf'); -``` - -```js -AV.User.become("anmlwi96s381m6ca7o7266pzf").then( - (user) => { - // 登录成功 - }, - (error) => { - // session token 无效 - } -); -``` - -```python -user = leancloud.User.become('anmlwi96s381m6ca7o7266pzf') -``` - -```php -User::become("anmlwi96s381m6ca7o7266pzf"); -``` - -```go -user, err := client.Users.Become("anmlwi96s381m6ca7o7266pzf") -if err != nil { - panic(err) -} -``` - - - -请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。 - -如果在 **控制台 > 内建账户 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 `403 (Forbidden)` 错误。 - -下面的代码检查 session token 是否有效: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -bool isAuthenticated = await currentUser.IsAuthenticated(); -if (isAuthenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - -```java -boolean authenticated = LCUser.getCurrentUser().isAuthenticated(); -if (authenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - -```objc -LCUser *currentUser = [LCUser currentUser]; -NSString *token = currentUser.sessionToken; -[currentUser isAuthenticatedWithSessionToken:token callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // session token 有效 - } else { - // session token 无效 - } -}]; -``` - -```swift -if let sessionToken = LCApplication.default.currentUser?.sessionToken?.value { - _ = LCUser.logIn(sessionToken: sessionToken) { (result) in - if result.isSuccess { - // session token 有效 - } else { - // session token 无效 - } - } -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -bool isAuthenticated = await currentUser.isAuthenticated(); -if (isAuthenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - -```js -const currentUser = AV.User.current(); -currentUser.isAuthenticated().then((authenticated) => { - if (authenticated) { - // session token 有效 - } else { - // session token 无效 - } -}); -``` - -```python -authenticated = leancloud.User.get_current().is_authenticated() -if authenticated: - # session token 有效 - pass -else: - # session token 无效 - pass -``` - -```php -$authenticated = User::isAuthenticated(); -if ($authenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - -```go -// 暂不支持 -``` - - - -## 重置密码 - -我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了多种重置密码的方法。 - -邮箱重置密码的流程如下: - -1. 用户输入注册的电子邮箱,请求重置密码; -2. 云端向该邮箱发送一封包含重置密码的特殊链接的电子邮件; -3. 用户点击重置密码链接后,一个特殊的页面会打开,让他们输入新密码; -4. 用户的密码已被重置为新输入的密码。 - -首先让用户填写注册账户时使用的邮箱,然后调用下面的方法: - - - -```cs -await LCUser.RequestPasswordReset("tom@xd.com"); -``` - -```java -LCUser.requestPasswordResetInBackground("tom@xd.com").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestPasswordResetForEmailInBackground:@"tom@xd.com"]; -``` - -```swift -_ = LCUser.requestPasswordReset(email: "tom@xd.com") { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestPasswordReset('tom@xd.com'); -``` - -```js -AV.User.requestPasswordReset("tom@xd.com"); -``` - -```python -leancloud.User.request_password_reset('tom@xd.com') -``` - -```php -User::requestPasswordReset("tom@xd.com"); -``` - -```go -if err := client.Users.RequestPasswordReset("tom@xd.com"); err != nil { - panic(err) -} -``` - - - -上面的代码会查询是否有用户的 `email` 属性与前面提供的邮箱匹配。如果有的话,则向该邮箱发送一封密码重置邮件。之前提到过,应用可以让 `username` 与 `email` 保持一致,也可以单独收集用户的邮箱并将其存为 `email`。 - -密码重置邮件的内容可在应用的 **云服务控制台 > 内建账户 > 邮件模版** 中自定义。更多关于自定义邮件模板和验证链接的内容,请参考《自定义邮件验证和重设密码页面》。 - -除此之外,还可以用手机号重置密码: - -1. 用户输入注册的手机号,请求重置密码; -2. 云端向该号码发送一条包含验证码的短信; -3. 用户输入验证码和新密码。 - -下面的代码向用户发送含有验证码的短信: - - - -```cs -await LCUser.RequestPasswordRestBySmsCode("+8619201680101"); -``` - -```java -LCUser.requestPasswordResetBySmsCodeInBackground("+8619201680101").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser requestPasswordResetWithPhoneNumber:@"+8619201680101"]; -``` - -```swift -_ = LCUser.requestPasswordReset(mobilePhoneNumber: "+8619201680101") { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -```dart -await LCUser.requestPasswordRestBySmsCode('+8619201680101'); -``` - -```js -AV.User.requestPasswordResetBySmsCode("+8619201680101"); -``` - -```python -leancloud.User.request_password_reset_by_sms_code('+8619201680101') -``` - -```php -User::requestPasswordResetBySmsCode("+8619201680101"); -``` - -```go -if err := client.Users.RequestPasswordResetBySmsCode("+8619201680101"); err != nil { - panic(err) -} -``` - - - -上面的代码会查询是否有用户的 `mobilePhoneNumber` 属性与前面提供的手机号匹配。如果有的话,则向该号码发送验证码短信。 - -可以在 **云服务控制台 > 内建账户 > 设置** 中设置只有在 `mobilePhoneVerified` 为 `true` 的情况下才能用手机号重置密码。 - -用户输入验证码和新密码后,用下面的代码完成密码重置: - - - -```cs -await LCUser.ResetPasswordBySmsCode("+8619201680101", "123456", "cat!@#123"); -``` - -```java -LCUser.resetPasswordBySmsCodeInBackground("123456", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 密码重置成功 - } - public void onError(Throwable throwable) { - // 验证码不正确 - } - public void onComplete() {} -}); -``` - -```objc -[LCUser resetPasswordWithSmsCode:@"123456" newPassword:@"cat!@#123" block:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 密码重置成功 - } else { - // 验证码不正确 - } -}]; -``` - -```swift -_ = LCUser.resetPassword(mobilePhoneNumber: "+8619201680101", verificationCode: "123456", newPassword: "cat!@#123") { result in - switch result { - case .success: - // 密码重置成功 - break - case .failure(error: let error): - // 验证码不正确 - print(error) - } -} -``` - -```dart -await LCUser.resetPasswordBySmsCode('+8619201680101', '123456', 'cat!@#123'); -``` - -```js -AV.User.resetPasswordBySmsCode("123456", "cat!@#123").then( - () => { - // 密码重置成功 - }, - (error) => { - // 验证码不正确 - } -); -``` - -```python -leancloud.User.reset_password_by_sms_code('123456', 'cat!@#123') -``` - -```php -User::resetPasswordBySmsCode("123456", "cat!@#123"); -``` - -```go -if err := client.Users.ResetPasswordBySmsCode("+8619201680101", "123456", "cat!@#123"); err != nil { - panic(err) -} -``` - - - -## 用户的查询 - -使用下面的代码来查询用户: - - - -```cs -LCQuery userQuery = LCUser.GetQuery(); -``` - -```java -LCQuery userQuery = LCUser.getQuery(); -``` - -```objc -LCQuery *userQuery = [LCUser query]; -``` - -```swift -let userQuery = LCQuery(className: "_User") -``` - -```dart -LCQuery userQuery = LCUser.getQuery(); -``` - -```js -const userQuery = new AV.Query("_User"); -``` - -```python -user_query = leancloud.Query('_leancloud.User') -``` - -```php -$userQuery = new Query("_User"); -``` - -```go -userQuery := client.Users.NewUserQuery() -``` - - - -为了安全起见,**新创建的应用的 `_User` 表默认关闭了 `find` 权限**,这样每位用户登录后只能查询到自己在 `_User` 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 `find` 查询权限。除此之外,还可以在 [云引擎](/sdk/engine/overview) 里封装用户查询相关的方法。 - -可以参见 [用户对象的安全](#用户对象的安全) 来了解 `_User` 表的一些限制,还可以阅读[《数据和安全》](/sdk/storage/guide/security)来了解更多 class 级权限设置的方法。 - -## 关联用户对象 - -关联用户的方法和对象是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书: - - - -```cs -LCObject book = new LCObject("Book"); -LCUser author = await LCUser.GetCurrent(); -book["title"] = "我的第五本书"; -book["author"] = author; -await book.Save(); - -LCQuery query = new LCQuery("Book"); -query.WhereEqualTo("author", author); -// books 是包含同一作者所有 Book 对象的数组 -ReadOnlyCollection books = await query.Find(); -``` - -```java -LCObject book = new LCObject("Book"); -LCUser author = LCUser.getCurrentUser(); -book.put("title", "我的第五本书"); -book.put("author", author); -book.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject book) { - // 获取所有该作者写的书 - LCQuery query = new LCQuery<>("Book"); - query.whereEqualTo("author", author); - query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List books) { - // books 是包含同一作者所有 Book 对象的数组 - } - public void onError(Throwable throwable) {} - public void onComplete() {} - }); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -```objc -LCObject *book = [LCObject objectWithClassName:@"Book"]; -LCUser *author = [LCUser currentUser]; -[book setObject:@"我的第五本书" forKey:@"title"]; -[book setObject:author forKey:@"author"]; -[book saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - // 获取所有该作者写的书 - LCQuery *query = [LCQuery queryWithClassName:@"Book"]; - [query whereKey:@"author" equalTo:author]; - [query findObjectsInBackgroundWithBlock:^(NSArray *books, NSError *error) { - // books 是包含同一作者所有 Book 对象的数组 - }]; -}]; -``` - -```swift -do { - guard let author = LCApplication.default.currentUser else { - return - } - let book = LCObject(className: "Book") - try book.set("title", value: "我的第五本书") - try book.set("author", value: author) - _ = book.save { result in - switch result { - case .success: - // 获取所有该作者写的书 - let query = LCQuery(className: "Book") - query.whereKey("author", .equalTo(author)) - _ = query.find { result in - switch result { - case .success(objects: let books): - // books 是包含同一作者所有 Book 对象的数组 - break - case .failure(error: let error): - print(error) - } - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` - -```dart -LCObject book = LCObject('Book'); -LCUser author = await LCUser.getCurrent(); -book['title'] = '我的第五本书'; -book['author'] = author; -await book.save(); - -LCQuery query = LCQuery('Book'); -query.whereEqualTo('author', author); -// books 是包含同一作者所有 Book 对象的数组 -List books = await query.find(); -``` - -```js -const Book = AV.Object.extend("Book"); -const book = new Book(); -const author = AV.User.current(); -book.set("title", "我的第五本书"); -book.set("author", author); -book.save().then((book) => { - // 获取所有该作者写的书 - const query = new AV.Query("Book"); - query.equalTo("author", author); - query.find().then((books) => { - // books 是包含同一作者所有 Book 对象的数组 - }); -}); -``` - -```python -Book = leancloud.Object.extend('Book') -book = Book() -author = leancloud.User.get_current() -book.set('title', '我的第五本书') -book.set('author', author) -book.save() - -# 获取所有该作者写的书 -query = Book.query -query.equal_to('author', author) -book_list = query.find() -``` - -```php -$book = new LeanObject("Book"); -$author = User::getCurrentUser(); -$book->set("title", "我的第五本书"); -$book->set("author", $author); -$book->save(); - -// 获取所有该作者写的书 -$query = new Query("Book"); -$query->equalTo("author", $author); -$books = $query->find(); -``` - -```go -// 暂不支持 -``` - - - -## 用户对象的安全 - -用户对象自带安全保障,只有通过经过鉴权的方法获取到的用户对象才能进行更新或删除操作,保证每个用户只能修改自己的数据。 - -这样设计是因为用户对象中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。 - -下面的代码展现了这种安全措施: - - - -```cs -try { - LCUser user = await LCUser.Login("Tom", "cat!@#123"); - // 试图修改用户名 - user["username"] = "Jerry"; - // 密码已被加密,这样做会获取到空字符串 - string password = user["password"]; - // 可以执行,因为用户已鉴权 - await user.Save(); - - // 绕过鉴权直接获取用户 - LCQuery userQuery = LCUser.GetQuery(); - LCUser unauthenticatedUser = await userQuery.Get(user.ObjectId); - unauthenticatedUser["username"] = "Toodle"; - - // 会出错,因为用户未鉴权 - unauthenticatedUser.Save(); -} catch (LCException e) { - print($"{e.code} : {e.message}"); -} -``` - -```java -LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 试图修改用户名 - user.put("username", "Jerry"); - // 密码已被加密,这样做会获取到空字符串 - String password = user.getString("password"); - // 可以执行,因为用户已鉴权 - user.save(); - - // 绕过鉴权直接获取用户 - LCQuery query = new LCQuery<>("_User"); - query.getInBackground(user.getObjectId()).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser unauthenticatedUser) { - unauthenticatedUser.put("username", "Toodle"); - // 会出错,因为用户未鉴权 - unauthenticatedUser.save(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} - }); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -```objc -[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 试图修改用户名 - [user setObject:@"Jerry" forKey:@"username")]; - // 密码已被加密,这样做会获取到空字符串 - NSString *password = user[@"password"]; - // 保存更改 - [user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 可以执行,因为用户已鉴权 - - // 绕过鉴权直接获取用户 - LCQuery *query = [LCQuery queryWithClassName:@"_User"]; - [query getObjectInBackgroundWithId:user.objectId block:^(LCObject *unauthenticatedUser, NSError *error) { - [unauthenticatedUser setObject:@"Toodle" forKey:@"username"]; - [unauthenticatedUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 无法执行,因为用户未鉴权 - } else { - // 操作失败 - } - }]; - }]; - } else { - // 错误处理 - } - }]; - } else { - // 错误处理 - } -}]; -``` - -```swift -_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in - switch result { - case .success(object: let user): - // 试图修改用户名 - try! user.set("username", "Jerry") - // 密码已被加密,这样做会获取到空字符串 - let password = user.get("password") - // 可以执行,因为用户已鉴权 - user.save() - - // 绕过鉴权直接获取用户 - let query = LCQuery(className: "_User") - _ = query.get(user.objectId) { result in - switch result { - case .success(object: let unauthenticatedUser): - try! unauthenticatedUser.set("username", "Toodle") - _ = unauthenticatedUser.save { result in - switch result { - .success: - // 无法执行,因为用户未鉴权 - .failure: - // 操作失败 - } - } - case .failure(error: let error): - print(error) - } - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - LCUser user = await LCUser.login('Tom', 'cat!@#123'); - // 试图修改用户名 - user['username'] = 'Jerry'; - // 密码已被加密,这样做会获取到空字符串 - String password = user['password']; - // 可以执行,因为用户已鉴权 - await user.save(); - - // 绕过鉴权直接获取用户 - LCQuery userQuery = LCQuery('_User'); - LCUser unauthenticatedUser = await userQuery.get(user.objectId); - unauthenticatedUser['username'] = 'Toodle'; - - // 会出错,因为用户未鉴权 - unauthenticatedUser.save(); -} on LCException catch (e) { - print('${e.code} : ${e.message}'); -} -``` - -```js -const user = AV.User.logIn("Tom", "cat!@#123").then((user) => { - // 试图修改用户名 - user.set("username", "Jerry"); - // 密码已被加密,这样做会获取到空字符串 - const password = user.get("password"); - // 保存更改 - user.save().then((user) => { - // 可以执行,因为用户已鉴权 - - // 绕过鉴权直接获取用户 - const query = new AV.Query("_User"); - query.get(user.objectId).then((unauthenticatedUser) => { - unauthenticatedUser.set("username", "Toodle"); - unauthenticatedUser.save().then( - (unauthenticatedUser) => {}, - (error) => { - // 会出错,因为用户未鉴权 - } - ); - }); - }); -}); -``` - -```python -leancloud.User.login('Tom', 'cat!@#123') -current_user = leancloud.User.get_current() - -# 试图修改用户名 -current_user.set('username', 'Jerry') -# 密码已被加密,这样做会获取到空字符串 -password = current_user.get('password') -# 可以执行,因为用户已鉴权 -current_user.save() - -# 绕过鉴权直接获取用户 -query = leancloud.Query('_User') -unauthenticated_user = query.get(current_user.id) -unauthenticated_user.set('username', 'Toodle') -# 会出错,因为用户未鉴权 -unauthenticated_user.save() -``` - -```php -User::logIn("Tom", "cat!@#123"); -$currentUser = User::getCurrentUser(); - -// 试图修改用户名 -$currentUser->set("username", "Jerry"); -// 密码已被加密,这样做会获取到空字符串 -$password = $currentUser->get("password"); -// 可以执行,因为用户已鉴权 -$currentUser->save(); - -// 绕过鉴权直接获取用户 -$query = new Query("_User"); -$unauthenticatedUser = $query->get($currentUser->getObjectId()) -$unauthenticatedUser->set("username", "Toodle"); -// 会出错,因为用户未鉴权 -$unauthenticatedUser->save() -``` - -```go -user, err := client.Users.LogIn("Tom", "cat!@#123") -if err != nil { - panic(err) -} - -// 试图修改用户名,未鉴权将失败 -if err := client.User(user).Set("username", "Jerry"); err != nil { - panic(err) -} - -// 密码已被加密,这样做会获取到空字符串 -password := user.String("password") - -// 可以执行,因为用户已鉴权 -if err := client.User(user).Set("username", "Jerry", leancloud.UseUser(user)); err != nil { - panic(err) -} - -// 绕过鉴权直接获取用户 -unauthenticatedUser := User{} -if err := client.Users.NewUserQuery().EqualTo("objectId", user.ID).First(&unauthenticatedUser); err != nil { - panic(err) -} - -// 会出错,因为用户未鉴权 -if err := client.User(unauthenticatedUser).Set("username", "Toodle"); err != nil { - panic(err) -} -``` - - - -通过调用 [当前用户](#当前用户) 相关方法获取的用户总是经过鉴权的。 - -要查看一个用户对象是否经过鉴权,可以调用如下方法。通过经过鉴权的方法获取到的用户对象无需进行该检查。 - - - -```cs -IsAuthenticated -``` - -```java -isAuthenticated -``` - -```objc -isAuthenticatedWithSessionToken -``` - -```swift -// 暂不支持 -``` - -```dart -isAuthenticated -``` - -```js -isAuthenticated; -``` - -```python -is_authenticated -``` - -```php -isAuthenticated -``` - -```go -// 暂不支持 -``` - - - -注意,用户的密码只能在注册的时候进行设置,日后如需修改,只能通过 [重置密码](#重置密码) 的方式进行。密码不会被缓存在本地。如果尝试直接获取已登录用户的密码,会得到 `null`。 - -## 其他对象的安全 - -对于给定的一个对象,可以指定哪些用户有权限读取或修改它。为实现该功能,每个对象都有一个由 `ACL` 对象组成的访问控制表。请参阅[ACL 权限管理开发指南](/sdk/storage/guide/acl/)。 - -## 第三方账户登录 - -云服务支持应用层直接使用第三方社交平台(例如微信、微博、QQ 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。 - -例如以下的代码展示了终端用户使用微信登录的处理流程: - - - -```cs -Dictionary thirdPartyData = new Dictionary { - // 必须 - { "openid", "OPENID" }, - { "access_token", "ACCESS_TOKEN" }, - { "expires_in", 7200 }, - - // 可选 - { "refresh_token", "REFRESH_TOKEN" }, - { "scope", "SCOPE" } -}; -LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin"); -``` - -```java -Map thirdPartyData = new HashMap(); -// 必须 -thirdPartyData.put("expires_in", 7200); -thirdPartyData.put("openid", "OPENID"); -thirdPartyData.put("access_token", "ACCESS_TOKEN"); -// 可选 -thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); -thirdPartyData.put("scope", "SCOPE"); -LCUser.loginWithAuthData(thirdPartyData, "weixin").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) { - } - public void onNext(LCUser user) { - System.out.println("成功登录"); - } - public void onError(Throwable throwable) { - System.out.println("尝试使用第三方账号登录,发生错误。"); - } - public void onComplete() { - } -}); -``` - -```objc -NSDictionary *thirdPartyData = @{ - // 必须 - @"openid":@"OPENID", - @"access_token":@"ACCESS_TOKEN", - @"expires_in":@7200, - - // 可选 - @"refresh_token":@"REFRESH_TOKEN", - @"scope":@"SCOPE", - }; -LCUser *user = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = LeanCloudSocialPlatformWeiXin; -[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let thirdPartyData: [String: Any] = [ - // 必须 - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - - // 可选 - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" -] -let user = LCUser() -user.logIn(authData: thirdPartyData, platform: .weixin) { (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -var thirdPartyData = { - // 必须 - 'openid': 'OPENID', - 'access_token': 'ACCESS_TOKEN', - 'expires_in': 7200, - - // 可选 - 'refresh_token': 'REFRESH_TOKEN', - 'scope': 'SCOPE' -}; -LCUser currentUser = await LCUser.loginWithAuthData(thirdPartyData, 'weixin'); -``` - -```js -const thirdPartyData = { - // 必须 - openid: "OPENID", - access_token: "ACCESS_TOKEN", - expires_in: 7200, - - // 可选 - refresh_token: "REFRESH_TOKEN", - scope: "SCOPE", -}; -AV.User.loginWithAuthData(thirdPartyData, "weixin").then( - (user) => { - // 登录成功 - }, - (error) => { - // 登录失败 - } -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -`loginWithAuthData` 系列方法需要两个参数来唯一确定一个账户: - -- 第三方平台的名字,就是前例中的 `weixin`,该名字由应用层自己决定。 -- 第三方平台的授权信息,就是前例中的 `thirdPartyData`(一般包括 `uid`、`access_token`、`expires_in` 等信息,与具体的第三方平台有关)。 - -云端会使用第三方平台的鉴权信息来查询是否已经存在与之关联的账户。如果存在的话,则返回 `200 OK` 状态码,同时附上用户的信息(包括 [`sessionToken`](#设置当前用户))。如果第三方平台的信息没有和任何账户关联,客户端会收到 `201 Created` 状态码,意味着新账户被创建,同时附上用户的 `objectId`、`createdAt`、`sessionToken` 和一个自动生成的 `username`,例如: - -```json -{ - "username": "k9mjnl7zq9mjbc7expspsxlls", - "objectId": "5b029266fb4ffe005d6c7c2e", - "createdAt": "2018-05-21T09:33:26.406Z", - "updatedAt": "2018-05-21T09:33:26.575Z", - "sessionToken": "…", - // authData 通常不会返回,继续阅读以了解其中原因 - "authData": { - "weixin": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" - } - } - // … -} -``` - -这时候我们会看到 `_User` 表中出现了一条新的账户记录,账户中有一个名为 `authData` 的列,保存了第三方平台的授权信息。出于安全考虑,`authData` 不会被返回给客户端,除非它属于当前用户。 - -开发者需要自己完成第三方平台的鉴权流程(一般通过 OAuth 1.0 或 2.0),以获取鉴权信息,继而到云端来登录。 - -### Sign in with Apple - -如果你需要开发 [Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api),云服务可以帮你校验 `identityToken`,并获取 Apple 的 `access_token`。Apple Sign In 的 `authData` 结构如下: - -```json -{ - "lc_apple": { - "uid": "从 Apple 获取到的 User Identifier", - "identity_token": "从 Apple 获取到的 identityToken", - "code": "从 Apple 获取到的 Authorization Code" - } -} -``` - -`authData` 中的 key 的作用: - -- **`lc_apple`**:只有 platform 为 `lc_apple` 时,云服务才会执行 `identity_token` 和 `code` 的逻辑。 -- **`uid`**:必填。云服务通过 `uid` 判断是否存在用户。 -- **`identity_token`**:可选。`authData` 中有 `identity_token` 时云端会自动校验 `identity_token` 的有效性。开发者需要在 **云服务控制台 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 -- **`code`**:可选。`authData` 中有 `code` 时云端会自动用该 `code` 向 Apple 换取 `access_token` 和 `refresh_token`。开发者需要在 **云服务控制台 > 内建账户 > 设置 > 第三方集成** 中填写 Apple 的相关信息。 - -#### 获取 Client ID - -Client ID 用于校验 `identity_token` 及获取 `access_token`,指的是 Apple 应用的 identifier,也就是 `AppID` 或 `serviceID`。对于原生应用来说,指的是 Xcode 中的 Bundle Identifier,例如 `com.mytest.app`。详情请参考 [Apple 的文档](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)。 - -#### 获取 Private Key 及 Private Key ID - -Private Key 用于获取 `access_token`。登录 Apple 开发者平台,在左侧的「Certificates, Identifiers & Profiles」中选择「Keys」,添加一个用于 Apple Sign In 的 Private Key,下载 `.p8` 文件,同时在下载 Key 的页面获得 Private Key ID。详情请参考 [Apple 的文档](https://help.apple.com/developer-account/#/dev77c875b7e)。 - -将 Key ID 填写到控制台,将下载下来的 Private Key 文件上传到控制台。控制台只能上传 Private Key 文件,无法查看及下载其内容。 - -#### 获取 Team ID - -Team ID 用于获取 `access_token`。登录 Apple 开发者平台,在右上角或 Membership 页面即可看到自己所属开发团队的 Team ID。注意选择 Bundle ID 对应的 Team。 - -#### 使用 Apple Sign In 登录云服务 - -在控制台填写完成所有信息后,使用以下代码登录。 - - - -```cs -Dictionary appleAuthData = new Dictionary { - // 必须 - { "uid", "USER IDENTIFIER" }, - - // 可选 - { "identity_token", "IDENTITY TOKEN" }, - { "code", "AUTHORIZATION CODE" } -}; -LCUser currentUser = await LCUser.LoginWithAuthData(appleAuthData, "lc_apple"); -``` - -```java -// 不支持 -``` - -```objc -NSDictionary *appleAuthData = @{ - // 必须 - @"uid":@"USER IDENTIFIER", - // 可选 - @"identity_token":@"IDENTITY TOKEN", - @"code":@"AUTHORIZATION CODE", - }; -LCUser *user = [LCUser user]; -[user loginWithAuthData:appleAuthData platformId:"lc_apple" options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let appleData: [String: Any] = [ - // 必须 - "uid": "USER IDENTIFIER", - // 可选 - "identity_token": "IDENTITY TOKEN", - "code": "AUTHORIZATION CODE" -] -let user = LCUser() -user.logIn(authData: appleData, platform: .apple) { (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } -} - -``` - -```dart -var appleData = { - // 必须 - "uid": "USER IDENTIFIER", - // 可选 - "identity_token": "IDENTITY TOKEN", - "code": "AUTHORIZATION CODE" -}; -LCUser currentUser = await LCUser.loginWithAuthData(appleData, 'lc_apple'); -``` - -```js -// 不支持 -``` - -```python -# 不支持 -``` - -```php -// 不支持 -``` - -```go -// 不支持 -``` - - - -### 鉴权数据的保存 - -每个用户的 `authData` 是一个以平台名为键名,鉴权信息为键值的 JSON 对象。 - -一个关联了微信账户的用户应该会有下列对象作为 `authData`: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - } -} -``` - -而一个关联了微博账户的用户,则会有如下的 `authData`: - -```json -{ - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx" - } -} -``` - -我们允许一个账户绑定多个第三方平台的鉴权数据,这样如果某个用户同时关联了微信和微博账户,则其 `authData` 可能会是这样的: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - }, - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx" - } -} -``` - -理解 `authData` 的数据结构至关重要。一个终端用户通过如下的鉴权信息来登录的时候, - -```json -"platform": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" -} -``` - -云端首先会查找账户系统,看看是否存在 `authData.platform.openid` 等于 `OPENID` 的账户,如果存在,则返回现有账户,如果不存在那么就创建一个新账户,同时将上面的鉴权信息写入新账户的 `authData` 属性中,并将新账户的数据当成结果返回。 - -云端会自动为每个用户的 `authData..` 创建唯一索引,从而避免重复数据。 -`` 在微信等部分云服务内建支持的第三方平台上为 `openid` 字段,在其他第三方平台(包括部分云服务专门支持的第三方平台和所有云服务没有专门支持的第三方平台)上为 `uid` 字段。 - -### 自动验证第三方平台授权信息 - -为了确保账户数据的有效性,云端还支持对部分平台的 Access Token 的有效性进行自动验证,以防止伪造账户数据。如果有效性验证不通过,云端会返回 `invalid authData` 错误,关联不会被建立。对于云端无法识别的服务,开发者需要自己去验证 Access Token 的有效性。 -比如,注册、登录时分别通过云引擎的 `beforeSave` hook、`beforeUpdate` hook 来验证 Access Token 有效性。 - -如果希望使用这一功能,则在开始使用前,需要在 **云服务控制台 > 内建账户 > 设置** 配置相应平台的 **应用 ID** 和 **应用 Secret Key**。 - -如果不希望云端自动验证 Access Token,可以在 **云服务控制台 > 内建账户 > 设置** 里面取消勾选 **第三方登录时,验证用户 AccessToken 合法性**。 - -配置平台账号的目的在于创建用户对象时,云端会使用相关信息去校验请求参数 `thirdPartyData` 的合法性,确保用户对象实际对应着一个合法真实的用户,确保平台安全性。 - -### 绑定第三方账户 - -如果用户已经登录,也可以在当前账户上绑定或解绑更多第三方平台信息。 - -绑定成功后,新的第三方账户信息会被添加到用户对象的 `authData` 字段里。 - -例如,下面的代码可以关联微信账户: - - - -```cs -await currentUser.AssociateAuthData(weixinData, "weixin"); -``` - -```java -user.associateWithAuthData(weixinData, "weixin").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("绑定成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("绑定失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -[user associateWithAuthData:weixinData platformId:LeanCloudSocialPlatformWeiXin options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"成功"); - } else{ - NSLog(@"失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -currentUser.associate(authData: weixinData, platform: .weixin) { (result) in - switch result { - case .success: - // 关联成功 - case .failure(error: let error): - // 关联失败 - } -} -``` - -```dart -await currentUser.associateAuthData(weixinData, 'weixin'); -``` - -```js -user - .associateWithAuthData(weixinData, "weixin") - .then(function (user) { - // 成功绑定 - }) - .catch(function (error) { - console.error("error: ", error); - }); -``` - -```python -user.link_with("weixin", weixin_data) -``` - -```php -$user->linkWith("weixin", $weixinData); -``` - -```go -// 暂不支持 -``` - - - -为节省篇幅,上面的代码示例中没有给出具体的平台授权信息,相关内容请参考上面的[「第三方账户登录」](#第三方账户登录)一节。 - -### 解除与第三方账户的关联 - -类似地,可以解绑第三方账户。 - -例如,下面的代码可以解除用户和微信账户的关联: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -await currentUser.DisassociateWithAuthData("weixin"); -``` - -```java -LCUser user = LCUser.currentUser(); -user.dissociateWithAuthData("weixin").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("解绑成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("解绑失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -[user disassociateWithPlatform:LeanCloudSocialPlatformWeiXin callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"成功"); - } else{ - NSLog(@"失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -currentUser.disassociate(authData: .weixin) { (result) in - switch result { - case .success: - // 解除关联成功 - case .failure(error: let error): - // 解除关联失败 - } -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -await currentUser.disassociateWithAuthData('weixin'); -``` - -```js -user.dissociateAuthData("weixin").then( - (s) => { - // 解除关联成功 - }, - (error) => { - // 解除关联失败 - } -); -``` - -```python -user.unlink_from("weixin") -``` - -```php -$user->unlinkWith("weixin"); -``` - -```go -// 暂不支持 -``` - - - -
    - -扩展:第三方登录时补充完整的用户信息 - -有些产品,新用户在使用第三方账号授权拿到相关信息后,仍然需要补充设置用户名、手机号、密码等重要信息后,才被允许登录成功。 - -这时要使用 `loginWithauthData` 登录接口的 `failOnNotExist` 参数并将其设置为 `true`。服务端会判断是否已存在能匹配上的 `authData`,如果不存在则会返回 `211` 错误码和 `Could not find user` 报错信息。开发者根据这个 `211` 错误码,跳转到要求输入用户名、密码、手机号等信息的页面,实例化一个用户对象,保存上述补充数据,再次调用 `loginWithauthData` 接口进行登录,并 **不再传入 `failOnNotExist` 参数**。示例代码如下: - - - -```cs -try { - Dictionary thirdPartyData = new Dictionary { - // 必须 - { "openid", "OPENID" }, - { "access_token", "ACCESS_TOKEN" }, - { "expires_in", 7200 }, - - // 可选 - { "refresh_token", "REFRESH_TOKEN" }, - { "scope", "SCOPE" } - }; - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.FailOnNotExist = true; - LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); -} catch (LCException e) { - if (e.code == 211) { - // 不存在 authData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 - } -} - -// 跳转到输入用户名、密码、手机号等业务页面之后 -Dictionary thirdPartyData = new Dictionary { - { "expires_in", 7200 }, - { "openid", "OPENID" }, - { "access_token", "ACCESS_TOKEN" } -}; -try { - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.FailOnNotExist = true; - LCUser user = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); - user.Username = "Tom"; - user.Mobile = "+8618200008888"; - await user.Save(); -} catch (LCException e) { - //其他报错信息 -} -``` - -```java -Map thirdPartyData = new HashMap(); -thirdPartyData.put("expires_in", 7200); -thirdPartyData.put("openid", "OPENID"); -thirdPartyData.put("access_token", "ACCESS_TOKEN"); -thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); -thirdPartyData.put("scope", "SCOPE"); -Boolean failOnNotExist = true; -LCUser user = new LCUser(); -user.loginWithAuthData(thirdPartyData, "weixin", failOnNotExist).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("存在匹配的用户,登录成功"); - } - @Override - public void onError(Throwable e) { - LCException avException = new LCException(e); - int code = avException.getCode(); - if (code == 211){ - // 跳转到输入用户名、密码、手机号等业务页面 - } else { - System.out.println("发生错误:" + e.getMessage()); - } - } - @Override - public void onComplete() { - } -}); - -// 跳转到输入用户名、密码、手机号等业务页面之后 -LCUser user = new LCUser(); -user.setUsername("Tom"); -user.setMobilePhoneNumber("+8618200008888"); -Boolean failOnNotExist = false; -user.loginWithAuthData(thirdPartyData, "weixin", failOnNotExist).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("登录成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("登录失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -NSDictionary *thirdPartyData = @{ - @"access_token":@"ACCESS_TOKEN", - @"expires_in":@7200, - @"refresh_token":@"REFRESH_TOKEN", - @"openid":@"OPENID", - @"scope":@"SCOPE", - }; -LCUser *user = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = LeanCloudSocialPlatformWeiXin; -option.failOnNotExist = true; -[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // 你的逻辑 - } else if ([error.domain isEqualToString:kLeanCloudErrorDomain] && error.code == 211) { - // 不存在 thirdPartyData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 - } -}]; - -// 跳转到输入用户名、密码、手机号等业务页面之后 -LCUser *user = [LCUser user]; -user.username = @"Tom"; -user.mobilePhoneNumber = @"+8618200008888"; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = LeanCloudSocialPlatformWeiXin; -[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let thirdPartyData: [String: Any] = [ - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "openid": "OPENID", - "scope": "SCOPE" -] -let user = LCUser() -user.logIn(authData: thirdPartyData, platform: .weixin, options: [.failOnNotExist]) { (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - if error.code == 211 { - // 不存在绑定了当前 authData 的 User 的实例 - // 跳转到输入用户名、密码、手机号等业务页面 - let user = LCUser() - user.username = "Tom" - user.password = "cat!@#123" - user.mobilePhoneNumber = "+8618200008888" - user.logIn(authData: thirdPartyData, platform: .weixin, completion: { (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } - }) - } - } -} -``` - -```dart -try { - Map thirdPartyData = { - // 必须 - 'openid': 'OPENID', - 'access_token': 'ACCESS_TOKEN', - 'expires_in': 7200, - - // 可选 - 'refresh_token': 'REFRESH_TOKEN', - 'scope': 'SCOPE' - }; - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.failOnNotExist = true; - LCUser currentUser = await LCUser.loginWithAuthData(thirdPartyData, 'weixin', option: option); -} on LCException catch (e) { - if (e.code == 211) { - // 不存在 authData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 - } -} - -// 跳转到输入用户名、密码、手机号等业务页面之后 -Map thirdPartyData = { - 'expires_in': 7200, - 'openid': 'OPENID', - 'access_token': 'ACCESS_TOKEN' -}; -try { - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.failOnNotExist = true; - LCUser user = await LCUser.loginWithAuthData(thirdPartyData, 'weixin', option: option); - user.username = 'Tome'; - user.mobile = '+8618200008888'; - await user.save(); -} on LCException catch (e) { - //其他报错信息 -} -``` - -```js -const thirdPartyData = { - access_token: "ACCESS_TOKEN", - expires_in: 7200, - refresh_token: "REFRESH_TOKEN", - openid: "OPENID", - scope: "SCOPE", -}; -AV.User.loginWithAuthData(thirdPartyData, "weixin", { - failOnNotExist: true, -}).then( - (s) => { - // 登录成功 - }, - (error) => { - // 登录失败 - // 检查 error.code == 211,跳转到用户名、手机号等资料的输入页面 - } -); - -const user = new AV.User(); -// 设置用户名 -user.setUsername("Tom"); -// 设置密码 -user.setMobilePhoneNumber("+8618200008888"); -user.setPassword("cat!@#123"); -// 设置邮箱 -user.setEmail("tom@leancloud.rocks"); -user.loginWithAuthData(thirdPartyData, "weixin").then( - (loggedInUser) => { - console.log(loggedInUser); - }, - (error) => {} -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -
    - -
    - -扩展:接入 UnionID 体系,打通不同子产品的账号系统 - -随着第三方平台的账户体系变得日渐复杂,它们的第三方鉴权信息出现了一些较大的变化。下面我们以最典型的微信开放平台为例来进行说明。 - -当一个用户在移动应用内登录微信账号时,会被分配一个 OpenID;在微信小程序内登录账号时,又会被分配另一个不同的 OpenID。这样的架构会导致的问题是,使用同一个微信号的用户,也无法在微信开放平台下的移动应用和小程序之间互通。 - -微信官方为了解决这个问题,引入了 `UnionID` 的体系,以下为其官方说明: - -> 通过获取用户基本信息接口,开发者可通过 OpenID 来获取用户基本信息,而如果开发者拥有多个公众号,可使用以下办法通过 UnionID 机制来在多公众号之间进行用户帐号互通。只要是同一个微信开放平台帐号下的公众号,用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID 是相同的。 - -其他平台,如 QQ 和微博,与微信的设计也基本一致。 - -云服务支持 `UnionID` 体系。你只需要给 `loginWithauthData` 和 `associateWithauthData` 接口传入更多的第三方鉴权信息,即可完成新 UnionID 体系的集成。新增加的第三方鉴权登录选项包括: - -- unionId,指第三方平台返回的 UnionId 字符串。 -- unionId platform,指 unionId 对应的 platform 字符串,由应用层自己指定,[后面](#该如何指定-unionidplatform)会详述。 -- asMainAccount,指示是否把当前平台的鉴权信息作为主账号来使用。如果作为主账号,那么就由当前用户唯一占有该 unionId,以后其他平台使用同样的 unionId 登录的话,会绑定到当前的用户记录上来;否则,当前应用的鉴权信息会被绑定到其他账号上去。 - -下面让我们通过一个例子来说明如何使用这些参数完成 UnionID 登录。 - -假设云服务在微信开放平台上有两个应用,一个是「云服务通讯」,一个是「云服务技术支持」,这两个应用在接入第三方鉴权的时候,分别使用了 `wxleanoffice` 和 `wxleansupport` 作为 platform 来进行登录。现在我们开启 UnionID 的用户体系,希望同一个微信用户在这两个应用中都能对应到同一个账户系统(`_User` 表中的同一条记录),同时我们决定将 `wxleanoffice` 平台作为主账号平台。 - -假设对于用户 A,微信给 ta 为云服务分配的 UnionId 为 `unionid4a`,而对两个应用的授权信息分别为: - -```json -"wxleanoffice": { - "access_token": "officetoken", - "openid": "officeopenid", - "expires_in": 1384686496 -}, -"wxleansupport": { - "openid": "supportopenid", - "access_token": "supporttoken", - "expires_in": 1384686496 -} -``` - -现在,用户 A 在「云服务通讯」中通过微信登录,其调用请求为: - - - -```cs -Dictionary thirdPartyData = new Dictionary { - // 必须 - { "uid", "officeopenid" }, - { "access_token", "officetoken" }, - { "expires_in", 1384686496 }, - { "unionId", "unionid4a" }, // 新增属性 - - // 可选 - { "refresh_token", "..." }, - { "scope", "SCOPE" } -}; -LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); -option.AsMainAccount = true; -option.UnionIdPlatform = "weixin"; -LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( - thirdPartyData, "wxleanoffice", "unionid4a", - option: option); -``` - -```java -Map thirdPartyData = new HashMap(); -thirdPartyData.put("expires_in", 1384686496); -thirdPartyData.put("uid", "officeopenid"); -thirdPartyData.put("access_token", "officetoken"); -thirdPartyData.put("scope", "SCOPE"); -LCUser.loginWithAuthData(LCUser.class, thirdPartyData, "wxleanoffice", - "unionid4a", "weixin", true) // 新增参数,分别表示 uniondId,unionIdPlatform,asMainAccount - // 对于 unionIdPlatform,这里使用「weixin」来指代微信平台。 - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("登录成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("登录失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -NSDictionary *thirdPartyData = @{ - @"access_token":@"officetoken", - @"expires_in":@1384686496, - @"uid":@"officeopenid", - @"scope":@"SCOPE", - @"unionid":@"unionid4a" // 新增属性 - }; -LCUser *currentuser = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = @"weixin"; // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 -option.unionId = thirdPartyData[@"unionid"]; -option.isMainAccount = true; -[currentuser loginWithAuthData:thirdPartyData platformId:@"wxleanoffice" options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let thirdPartyData: [String: Any] = [ - "access_token": "officetoken", - "expires_in": 1384686496, - "uid": "officeopenid", - "scope": "SCOPE", - "unionid": "unionid4a" // 新增属性 -] -let user = LCUser() -user.logIn( - authData: thirdPartyData, - platform: .custom("wxleanoffice"), - unionID: thirdPartyData["unionid"] as? String, - unionIDPlatform: .weixin, // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 - options: [.mainAccount]) -{ (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -Map thirdPartyData = { - // 必须 - 'uid': 'officeopenid', - 'access_token': 'officetoken', - 'expires_in': 1384686496, - 'unionId': 'unionid4a', // 新增属性 - - // 可选 - 'refresh_token': '...', - 'scope': 'SCOPE' -}; -LCUserAuthDataLoginOption option = LCUserAuthDataLoginOption(); -option.asMainAccount = true; -option.unionIdPlatform = 'weixin'; -LCUser currentUser = await LCUser.loginWithAuthDataAndUnionId( - thirdPartyData, 'wxleanoffice', 'unionid4a', - option: option); -``` - -```js -const thirdPartyData = { - access_token: "officetoken", - expires_in: 1384686496, - uid: "officeopenid", - scope: "SCOPE", -}; - -AV.User.loginWithAuthDataAndUnionId( - thirdPartyData, - "wxleanoffice", - "unionid4a", // 新增参数 - { - unionIdPlatform: "weixin", // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 - asMainAccount: true, - } -).then( - (user) => { - // 绑定成功 - }, - (error) => { - // 绑定失败 - } -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 `authData..uid` 的唯一索引,具体可以参考《数据存储 REST API 使用详解》的[《连接用户账户和第三方平台》](/sdk/authentication/rest/#连接用户账户和第三方平台)一节。 - -如果用户 A 是第一次在「云服务通讯」中通过微信登录,那么 `_User` 表中会增加一个新用户(假设其 `objectId` 为 `ThisIsUserA`),其 `authData` 的结果如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - - // 新增键值对 - "_weixin_unionid": { - "uid": "unionid4a" - } -} -``` - -可以看到,与之前的第三方登录 API 相比,这里由于登录时指定了 `asMainAccount` 为 `true`,所以 `authData` 的第一级子目录中增加了 `_weixin_unionid` 的键值对,这里的 `weixin` 就是我们指定的 `unionIdPlatform` 的值。`_weixin_unionid` 这个增加的键值对非常重要,以后我们判断是否存在同样 UnionID 的账户就是依靠它来查找的,而是否增加这个键值对,则是由登录时指定的 `asMainAccount` 的值决定的: - -- 当 `asMainAccount` 为 `true` 时,云端会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,当前账号就会作为这一个 UnionID 对应的主账号被唯一确定。 -- 当 `asMainAccount` 为 `false` 时,云端不会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,此时如果通过提供的 UnionID 可以找到主账号,则会将当前的鉴权信息合并进主账号的 `authData` 属性里,同时返回主账号对应的 `_User` 表记录;如果通过提供的 UnionID 找不到主账号,则会根据平台的 `openid` 去查找账户,找到匹配的账户就返回匹配的,找不到就新建一个账户,此时的处理逻辑与不使用 UnionID 时的逻辑完全一致。 - -接下来,用户 A 继续在「云服务技术支持」中进行微信登录,其登录逻辑为: - - - -```cs -Dictionary thirdPartyData = new Dictionary { - // 必须 - { "uid", "supportopenid" }, - { "access_token", "supporttoken" }, - { "expires_in", 1384686496 }, - { "unionId", "unionid4a" }, - - // 可选 - { "refresh_token", "..." }, - { "scope", "SCOPE" } -}; -LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); -option.AsMainAccount = false; -option.UnionIdPlatform = "weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( - thirdPartyData, "wxleansupport", "unionid4a", - option: option); -``` - -```java -Map thirdPartyData = new HashMap(); -thirdPartyData.put("expires_in", 1384686496); -thirdPartyData.put("uid", "supportopenid"); -thirdPartyData.put("access_token", "supporttoken"); -thirdPartyData.put("scope", "SCOPE"); -LCUser.loginWithAuthData(LCUser.class, thirdPartyData, "wxleansupport", "unionid4a", - "weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 - false).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser user) { - System.out.println("登录成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("登录失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - -```objc -NSDictionary *thirdPartyData = @{ - @"access_token":@"supporttoken", - @"expires_in":@1384686496, - @"uid":@"supportopenid", - @"scope":@"SCOPE", - @"unionid":@"unionid4a" - }; -LCUser *currentuser = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = @"weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -option.unionId = thirdPartyData[@"unionid"]; -option.isMainAccount = false; -[currentuser loginWithAuthData:thirdPartyData platformId:@"wxleansupport" options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -```swift -let thirdPartyData: [String: Any] = [ - "access_token": "supporttoken", - "expires_in": 1384686496, - "uid": "supportopenid", - "scope": "SCOPE", - "unionid": "unionid4a" -] -let user = LCUser() -user.logIn( - authData: thirdPartyData, - platform: .custom("wxleansupport"), - unionID: thirdPartyData["unionid"] as? String, - unionIDPlatform: .weixin, // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 - options: [.mainAccount]) -{ (result) in - switch result { - case .success: - assert(user.objectId != nil) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -Map thirdPartyData = { - // 必须 - 'uid': 'supportopenid', - 'access_token': 'supporttoken', - 'expires_in': 1384686496, - 'unionId': 'unionid4a', - - // 可选 - 'refresh_token': '...', - 'scope': 'SCOPE' -}; -LCUserAuthDataLoginOption option = LCUserAuthDataLoginOption(); -option.asMainAccount = false; -option.unionIdPlatform = 'weixin'; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -LCUser currentUser = await LCUser.loginWithAuthDataAndUnionId( - thirdPartyData, 'wxleansupport', 'unionid4a', - option: option); -``` - -```js -const thirdPartyData = { - access_token: "supporttoken", - expires_in: 1384686496, - uid: "supportopenid", - scope: "SCOPE", -}; - -AV.User.loginWithAuthDataAndUnionId( - thirdPartyData, - "wxleansupport", - "unionid4a", - { - unionIdPlatform: "weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 - asMainAccount: false, - } -).then( - (user) => { - // 绑定成功 - }, - (error) => { - // 绑定失败 - } -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -与「云服务通讯」中的登录过程相比,在「云服务技术支持」这个应用中,我们在登录时只是将 `asMainAccount` 设为了 `false`。 这时我们看到,本次登录得到的还是 `objectId` 为 `ThisIsUserA` 的 `_User` 表记录(同一个账户),同时该账户的 `authData` 属性中发生了变化,多了 `wxleansupport` 的数据,如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - }, - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -在新的登录方式中,当一个用户以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录得到新的用户对象后,接下来这个用户以「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 `wxleansupport` 的新用户数据加入到已有账户的 `authData` 里了,不会再创建新的账户。 - -这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个用户对象上,实现互通。 - -### 为 UnionID 建立索引 - -云端会为 UnionID 自动建立索引,不过因为自动创建基于请求的抽样统计,可能会滞后。 -因此,我们推荐自行创建相关索引,特别是用户量(`_User` 表记录数)很大的应用,更需要预先创建索引,否则用户使用 UnionID 账号登录时可能超时失败。 -以上面的微信 UnionID 为例,建议在控制台预先创建下列唯一索引(允许缺失值): - -- `authData.wxleanoffice.uid` -- `authData.wxleansupport.uid` -- `authData._weixin_unionid.uid` - -### 该如何指定 unionIdPlatform - -从上面的例子可以看出,使用 UnionID 登录的时候,需要指定 `unionIdPlatform` 的主要目的,就是为了便于查找已经存在的唯一主账号。云端会在主账号对应账户的 `authData` 属性中增加一个 `_{unionIdPlatform}_unionid` 键值对来标记唯一性,终端用户在其他应用中登录的时候,云端会根据参数中提供的 `uniondId` + `unionIdPlatform` 的组合,在 `_User` 表中进行查找,这样来确定唯一的既存主账号。 - -本来 `unionIdPlatform` 的取值,应该是开发者可以自行决定的,但是 JavaScript SDK 基于易用性的目的,在 `loginWithAuthDataAndUnionId` 之外,还额外提供了两个接口: - -- `AV.User.loginWithQQAppWithUnionId`,这里默认使用 `qq` 作为 `unionIdPlatform`。 -- `AV.User.loginWithWeappWithUnionId`,这里默认使用 `weixin` 作为 `unionIdPlatform`。 - -从我们的统计来看,这两个接口已经被很多开发者接受,在大量线上产品中产生着实际数据。所以为了避免数据在不同平台(例如 Android 和 iOS 应用)间发生冲突,建议大家统一按照 JavaScript SDK 的默认值来设置 `unionIdPlatform`,即: - -- 微信平台的多个应用,统一使用 `weixin` 作为 `unionIdPlatform`; -- QQ 平台的多个应用,统一使用 `qq` 作为 `unionIdPlatform`; -- 微博平台的多个应用,统一使用 `weibo` 作为 `unionIdPlatform`; -- 除此之外的其他平台,开发者可以自行定义 `unionIdPlatform` 的名字,只要自己保证多个应用间统一即可。 - -### 主副应用不同登录顺序出现的不同结果 - -上面的流程是用户先登录了「云服务通讯」这个主应用,然后再登录「云服务技术支持」这个副应用,所以账号都被通过 UnionID 有效关联起来了。可能有人会想到另外一个问题,如果用户 B 先登录副应用,后登录主应用,这时候会发生什么事情呢? - -用户 B 首先登录副应用的时候,提供了「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,并且指定「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `false`」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个用户对象,该账户 `authData` 结果为: - -```json -{ - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,以及「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 `true`」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个用户对象,该账户 `authData` 结果为: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - } -} -``` - -还有更复杂的情况。如果某公司的产品之前就接入了微信登录,产生了很多存量用户,并且分散在不同的子产品中,这时候怎么办?我们接下来专门讨论此时的解决方案。 - -### 存量账户如何通过 UnionID 实现关联 - -还是以我们的两个子产品「云服务通讯」(后续以「产品 1」指代)和「云服务技术支持为例」(后续以「产品 2」指代)为例,在接入 UnionID 之前,我们就接入了之前版本的微信平台登录,这时候账户系统内可能存在多种账户: - -- 只使用产品 1 的微信用户 A -- 只使用产品 2 的微信用户 B -- 同时使用两个产品的微信用户 C - -此时的存量账户表如下所示: - -| `objectId` | 微信用户 | `authData.{platform}` | `authData._{platform}_unionid` | -| ---------- | -------- | --------------------- | ------------------------------ | -| 1 | UserA | openid1(对应产品 1) | N/A | -| 2 | UserB | openid2(对应产品 2) | N/A | -| 3 | UserC | openid3(对应产品 1) | N/A | -| 4 | UserC | openid4(对应产品 2) | N/A | - -现在我们对两个子产品进行升级,接入 UnionID 体系。这时因为已经有同一个微信用户在不同子产品中创建了不同的账户(例如 `objectId` 为 3 和 4 的账户),我们需要确定以哪个平台的账号为主。比如决定使用「云服务通讯」上生成的账号为主账号,则在该应用程序更新版本时,使用 `asMainAccount=true` 参数。这个应用带着 UnionID 登录匹配或创建的账号将作为主账号,之后所有这个 UnionID 的登录都会匹配到这个账号。请注意这时 `_User` 表里会剩下一些用户数据,也就是没有被选为主账号的、其他平台的同一个用户的旧账号数据(例如 `objectId` 为 2 和 4 的账户)。这部分数据会继续服务于已经发布的但仍然使用 OpenID 登录的旧版应用。 - -接下来我们看一下,如果以产品 1 的账户作为「主账户」,按照前述的方式同时提供 openid/unionid 完成登录,则最后达到的结果是: - -1. 使用老版本的用户,不管在哪个产品里面,都可以和往常一样通过 openid 登录到正确的账户; -2. 使用产品 1 的新版本的老用户,通过 openid/unionid 组合,还是绑定到原来的账户。例如 UserC 在产品 1 中通过 openid3/unionId3 还是会绑定到 objectId=3 的账户(会增加 uniondId 记录);而 UserC 在产品 2 的新版本中,通过 openid4/unionId3 的组合则会绑定到 objectId=3 的账户,而不再是原来的 objectId=4 的账户。 -3. 使用产品 1 的新版本的新用户,通过 openid/unionid 组合,会创建新的账户;之后该用户再使用产品 2 的新版本,也会绑定到刚才创建的新账户上。 - -以上面的三个用户为例,他们分别升级到两个产品的最新版,且最终都会启用两个产品,则账户表的最终结果如下: - -| `objectId` | 微信用户 | `authData.{platform}` | `authData._{platform}_unionid` | -| ---------- | -------- | ------------------------------------------- | ------------------------------ | -| 1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A | -| 2 | UserB | openid2(对应产品 2) | N/A | -| 3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C | -| 4 | UserC | openid4(对应产品 2) | N/A | -| 5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B | - -之后有新的用户 D,分别在两个产品的新版本中登录,则账户表中会增加一条新的 objectId=6 的记录,结果如下: - -| `objectId` | 微信用户 | `authData.{platform}` | `authData._{platform}_unionid` | -| ---------- | -------- | ------------------------------------------- | ------------------------------ | -| 1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A | -| 2 | UserB | openid2(对应产品 2) | N/A | -| 3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C | -| 4 | UserC | openid4(对应产品 2) | N/A | -| 5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B | -| 6 | UserD | openid7(对应产品 1)/openid8(对应产品 2) | unionId_user_D | - -如果之后我们增加了新的子产品 3,这些用户在子产品 3 中也进行微信登录的话,那么四个用户还是会继续绑定到 objectId 为 1/3/5/6 的主账户。此时账户表的结果会变为: - -| `objectId` | 微信用户 | `authData.{platform}` | `authData._{platform}_unionid` | -| ---------- | -------- | ------------------------------------------------------------------ | ------------------------------ | -| 1 | UserA | openid1(对应产品 1)/openid6(对应产品 2)/openid9(对应产品 3) | unionId_user_A | -| 2 | UserB | openid2(对应产品 2) | N/A | -| 3 | UserC | openid3(对应产品 1)/openid4(对应产品 2)/openid10(对应产品 3) | unionId_user_C | -| 4 | UserC | openid4(对应产品 2) | N/A | -| 5 | UserB | openid5(对应产品 1)/openid2(对应产品 2)/openid11(对应产品 3) | unionId_user_B | -| 6 | UserD | openid7(对应产品 1)/openid8(对应产品 2)/openid12(对应产品 3) | unionId_user_D | - -
    - -## 匿名用户 - -将数据与用户关联需要首先创建一个用户,但有时你不希望强制用户在一开始就进行注册。使用匿名用户,可以让应用不提供注册步骤也能创建用户。下面的代码创建一个新的匿名用户: - - - -```cs -await LCUser.LoginAnonymously(); -``` - -```java -LCUser.logInAnonymously().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // user 是新的匿名用户 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -```objc -[LCUser loginAnonymouslyWithCallback:^(LCUser *user, NSError *error) { - // user 是新的匿名用户 -}]; -``` - -```swift -// 暂不支持 -``` - -```dart -await LCUser.loginAnonymously(); -``` - -```js -AV.User.loginAnonymously().then((user) => { - // user 是新的匿名用户 -}); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -可以像给普通用户设置属性那样给匿名用户设置 `username`、`password`、`email` 等属性,还可以通过走正常的注册流程来将匿名用户转化为普通用户。匿名用户能够: - -- [使用用户名和密码注册](#注册) -- [关联第三方平台](#第三方账户登录),比如微信 - -下面的代码为一名匿名用户设置用户名和密码: - - - -```cs -LCUser currentUser = await LCUser.LoginAnonymously(); -currentUser.Username = "Tom"; -currentUser.Password = "cat!@#123"; - -await currentUser.SignUp(); -``` - -```java -// currentUser 是个匿名用户 -LCUser currentUser = LCUser.getCurrentUser(); - -currentUser.setUsername("Tom"); -currentUser.setPassword("cat!@#123"); - -currentUser.signUpInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // currentUser 已经转化为普通用户 - } - public void onError(Throwable throwable) { - // 注册失败(通常是因为用户名已被使用) - } - public void onComplete() {} -}); -``` - -```objc -// currentUser 是个匿名用户 -LCUser *currentUser = [LCUser currentUser]; - -user.username = @"Tom"; -user.password = @"cat!@#123"; - -[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // currentUser 已经转化为普通用户 - } else { - // 注册失败(通常是因为用户名已被使用) - } -}]; -``` - -```swift -// 暂不支持 -``` - -```dart -LCUser currentUser = await LCUser.loginAnonymously(); -currentUser.username = 'Tom'; -currentUser.password = 'cat!@#123'; - -await currentUser.signUp(); -``` - -```js -// currentUser 是个匿名用户 -const currentUser = AV.User.current(); - -user.setUsername("Tom"); -user.setPassword("cat!@#123"); - -user.signUp().then( - (user) => { - // currentUser 已经转化为普通用户 - }, - (error) => { - // 注册失败(通常是因为用户名已被使用) - } -); -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -下面的代码检查当前用户是否为匿名用户: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -if (currentUser.IsAnonymous) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```java -LCUser currentUser = LCUser.getCurrentUser(); -if (currentUser.isAnonymous()) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```objc -LCUser *currentUser = [LCUser currentUser]; -if (currentUser.isAnonymous) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```swift -// 暂不支持 -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -if (currentUser.isAnonymous) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```js -const currentUser = AV.User.current(); -if (currentUser.isAnonymous()) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -```python -# 暂不支持 -``` - -```php -// 暂不支持 -``` - -```go -// 暂不支持 -``` - - - -如果匿名用户未能在登出前转化为普通用户,那么该用户将无法再次登录同一账户,且之前产生的数据也无法被取回。 diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/rest.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/rest.mdx deleted file mode 100644 index f8005f582..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/authentication/rest.mdx +++ /dev/null @@ -1,759 +0,0 @@ ---- -title: 内建账户 REST API -sidebar_label: REST API -sidebar_position: 3 ---- - -:::tip -请参考 数据存储 REST API 文档中关于 [Base URL](sdk/storage/guide/rest/#base-url)、[请求格式](/sdk/storage/guide/rest/#请求格式)、[响应格式](/sdk/storage/guide/rest/#响应格式)的说明。 -::: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    URLHTTP功能
    /1.1/usersPOST用户注册
    用户连接
    /1.1/usersByMobilePhonePOST使用手机号码注册或登录
    /1.1/loginPOST用户登录
    /1.1/users/<objectId>GET获取用户
    /1.1/users/meGET根据 sessionToken 获取用户信息
    /1.1/users/<objectId>/refreshSessionTokenPUT重置用户 sessionToken。
    /1.1/users/<objectId>/updatePasswordPUT更新密码,要求输入旧密码。
    /1.1/users/<objectId>PUT更新用户
    用户连接
    验证Email
    /1.1/usersGET查询用户
    /1.1/users/<objectId>DELETE删除用户
    /1.1/requestPasswordResetPOST请求密码重设
    /1.1/requestEmailVerifyPOST请求验证用户邮箱
    /1.1/requestMobilePhoneVerifyPOST请求发送用户手机号码验证短信
    /1.1/verifyMobilePhone/<code>POST使用"验证码"验证用户手机号码
    /1.1/requestChangePhoneNumberPOST请求发送手机短信验证码以绑定或更新手机号。
    /1.1/changePhoneNumberPOST验证手机短信验证码并绑定或更新手机号。
    /1.1/requestLoginSmsCodePOST请求发送手机号码登录短信。
    /1.1/requestPasswordResetBySmsCodePOST请求发送手机短信验证码重置用户密码。
    /1.1/resetPasswordBySmsCode/<code>PUT验证手机短信验证码并重置密码。
    - -不仅在移动应用上,还在其他系统中,很多应用都有一个统一的登录流程。通过 REST API 访问用户的账户让你可以简单实现这一功能。 - -通常来说,**用户**(类名 `_User`)这个类的功能与其他的对象是相同的,比如都没有限制模式(Schema free)。User 对象和其他对象不同的是一个用户必须有用户名(username)和密码(password),密码会被自动地加密和存储。 -**username 和 email 这两个字段必须是没有重复的(大小写敏感)。** - -## 注册 - -注册一个新用户与创建一个新的普通对象之间的不同点在于 username 和 password 字段都是必需的。password 字段会以和其他的字段不一样的方式处理,它在储存时会被加密而且永远不会被返回给任何来自客户端的请求。 - -你可以让云服务自动验证邮件地址,做法是进入 **云服务控制台 > 内建账户 > 设置**,勾选 **启用邮箱验证功能**。 - -这项设置启用了的话,所有填写了 email 的用户在注册时都会产生一个 email 验证地址,并发回到用户邮箱,用户打开邮箱点击了验证链接之后,用户表里 `emailVerified` 属性值会被设为 true。你可以在 `emailVerified` 字段上查看用户的 email 是否已经通过验证。 - -你还可以在 **云服务控制台 > 内建账户 > 设置**,勾选**未验证邮箱的用户,禁止登录**。 - -为了注册一个新的用户,需要向 user 路径发送一个 POST 请求,你可以加入一个新的字段,例如,创建一个新的用户有一个电话号码: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"username":"tom","password":"f32@ds*@&dsa","phone":"18612340000"}' \ - https://API_BASE_URL/1.1/users -``` - -当创建成功时,HTTP返回为 201 Created,Location 头包含了新用户的 URL: - -```sh -Status: 201 Created -Location: https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f -``` - -返回的主体是一个 JSON 对象,包含 objectId、createdAt 时间戳表示创建对象时间,sessionToken 可以被用来认证这名用户随后的请求: - -``` -{ - "sessionToken":"qmdj8pdidnmyzp0c7yqil91oc", - "createdAt":"2015-07-14T02:31:50.100Z", - "objectId":"55a47496e4b05001a7732c5f" -} -``` - -## 登录 - -在你允许用户注册之后,在以后你需要让他们用自己的用户名和密码登录。为了做到这一点,发送一个 POST 请求到 `/1.1/login`,加上 username 和 password 作为 body。 - -```sh -curl -X POST \ --H "Content-Type: application/json" \ --H "X-LC-Id: {{appid}}" \ --H "X-LC-Key: {{appkey}}" \ --d '{"username":"tom","password":"f32@ds*@&dsa"}' \ -https://API_BASE_URL/1.1/login -``` - -用户也可以通过邮箱地址和密码登录,只需将 body 中的 username 换成 email: - -```json -{"email":"tom@example.com","password":"f32@ds*@&dsa"} -``` - -类似地,将 `username` 换成 `mobilePhoneNumber` 可以通过手机号和密码登录: - -```json -{"mobilePhoneNumber":"+86186xxxxxxxx","password":"f32@ds*@&dsa"} -``` - -返回的主体是一个 JSON 对象包括所有除了 password 以外的自定义字段。它同样包含了 createdAt、updateAt、objectId 和 sessionToken 字段。 - -```json -{ - "sessionToken":"qmdj8pdidnmyzp0c7yqil91oc", - "updatedAt":"2015-07-14T02:31:50.100Z", - "phone":"18612340000", - "objectId":"55a47496e4b05001a7732c5f", - "username":"tom", - "createdAt":"2015-07-14T02:31:50.100Z", - "emailVerified":false, - "mobilePhoneVerified":false -} -``` - -可以将 sessionToken 理解为用户的登录凭证,每个用户的 sessionToken 在同一个应用内都是唯一的, 类似于 Cookie 的概念。 - -正常情况下,用户的 sessionToken 是固定不变的,但在以下情况下会发生改变: - -* 客户端调用了忘记密码功能,重设了密码。 -* 开发者在 **云服务控制台 > 内建账户 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么在修改密码后 sessionToken 也将强制更换。 -* 调用 [`refreshSessionToken`](#重置登录_sessionToken) 主动重置。 - -在 sessionToken 变化后,已有的登录如果调用到用户相关权限受限的 API,将返回 403 权限错误。 - -## 重置登录 sessionToken - -可以主动重置用户的 sessionToken: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://API_BASE_URL/1.1/users/57e3bcca67f35600577c3063/refreshSessionToken -``` - -调用这个 API 要求传入登录返回的 `X-LC-Session` 作为认证,或者使用 Master Key。 - -重置成功将返回新的 sessionToken 及用户信息: - -```json -{ - "sessionToken":"5frlikqlwzx1nh3wzsdtfr4q7", - "updatedAt":"2016-10-20T03:10:57.926Z", - "objectId":"57e3bcca67f35600577c3063", - "username":"tom", - "createdAt":"2016-09-22T11:13:14.842Z", - "emailVerified":false, - "mobilePhoneVerified":false -} -``` - -### 账户锁定 - -输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 `{"code":219,"error":"登录失败次数超过限制,请稍候再试,或者通过忘记密码重设密码。"}`,开发者可在客户端进行必要提示。 - -锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 - -## 使用手机号码注册或登录 - -请参考 [短信服务 REST API 详解 · 使用手机号码注册或登录](/sdk/sms/rest/#使用手机号码注册或登录)。 - -## 验证 Email - -设置 email 验证是 app 设置中的一个选项,通过这个标识,应用层可以对提供真实 email 的用户更好的功能或者体验。Email 验证会在 User 对象中加入 `emailVerified` 字段,当一个用户的 email 被新设置或者修改过的话,`emailVerified` 会被重置为 false。云服务会往用户填写的邮箱发送一个验证链接,用户点击这个链接可以让 `emailVerified` 被设置为 true。 - -emailVerified 字段有 3 种状态可以参考: - -1. **true**:用户已经点击了发送到邮箱的验证地址,邮箱被验证为真实有效。云端保证在新创建用户的时候 emailVerified 一定为 false。 -2. **false**:User 对象最后一次被更新的时候,用户并没有确认过他的 email 地址。如果你看到 emailVerified 为 false 的话,你可以考虑刷新 User 对象或者再次请求验证用户邮箱。 -3. **null**:User对象在 email 验证没有打开的时候就已经创建了,或者 User 没有 email。 - -邮件模板和验证链接可以在**云服务控制台 > 内建账户 > 邮件模板**定制。 - -## 请求验证 Email - -发送给用户的邮箱验证邮件在一周内失效,你可以通过调用 `/1.1/requestEmailVerify` 来强制重新发送: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"email":"tom@example.com"}' \ - https://API_BASE_URL/1.1/requestEmailVerify -``` - -## 请求密码重设 - -在用户将 email 与他们的账户关联起来之后,你可以通过邮件来重设密码。操作方法为,发送一个 POST 请求到 `/1.1/requestPasswordReset`,同时在 request 的 body 部分带上 email 字段。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"email":"tom@example.com"}' \ - https://API_BASE_URL/1.1/requestPasswordReset -``` - -如果成功的话,将返回状态码 `200 OK`。 - -邮件模板和验证链接可以在**云服务控制台 > 内建账户 > 邮件模板**定制。 - -## 手机号码验证 - -请参考 [短信服务 REST API 详解 - 用户账户与手机号码验证](/sdk/sms/rest/#用户账户与手机号码验证)。 - -## 获取用户 - -和[获取对象](#获取对象)类似,你可以发送一个 GET 请求到 URL 以获取用户的账户信息。比如,为了获取上面创建的用户: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f -``` - -你还可以通过 sessionToken 获取用户信息。 -这种方法最常见的使用场景是用户成功注册或登录后,本地保存服务器返回的 sessionToken,后续使用 sessionToken 获取当前用户信息: - -``` -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://API_BASE_URL/1.1/users/me -``` - -返回的 JSON 数据与 [`/login`](#登录) 登录请求所返回的相同。 - -用户不存在时返回 400 Bad Request 错误: - -```json -{ - "code": 211, - "error": "Could not find user." -} -``` - -## 更新用户 - -在通常的情况下,没有人会允许别人来改动他们自己的数据。为了做好权限认证,确保只有用户自己可以修改个人数据,在更新用户信息的时候,必须在 HTTP 头部加入一个 `X-LC-Session` 项来请求更新,这个 session token 在注册和登录时会返回。 - -为了改动一个用户已经有的数据,需要对这个用户的 URL 发送一个 PUT 请求。任何你没有指定的 key 都会保持不动,所以你可以只改动用户数据中的一部分。 - -比如,如果我们想对 「tom」 的手机号码做出一些改动: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{"phone":"18600001234"}' \ - https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f -``` - -返回的 body 是一个 JSON 对象,只有一个 `updatedAt` 字段表明更新发生的时间. - -```json -{ - "updatedAt": "2015-07-14T02:35:50.100Z" -} -``` - -username 也可以改动,但是新的 username 不能和既有数据重复。 - -很多开发者会希望让用户输入一次旧密码做一次认证,旧密码正确才可以修改为新密码,因此我们提供了一个单独的 API `PUT /1.1/users/:objectId/updatePassword` 来安全地更新密码: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{"old_password":"the_old_password", "new_password":"the_new_password"}' \ - https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f/updatePassword -``` - -* **old_password**:用户的老密码 -* **new_password**:用户的新密码 - -注意:仍然需要传入 X-LC-Session,也就是登录用户才可以修改自己的密码。 - -## 查询 - -**请注意,`_User` 表的查询权限默认是关闭的。** -你可以在云服务控制台的 class 权限设置处打开,但出于安全考虑,一般情况下不建议开启这项权限。 -在 `_User` 表查询权限关闭的情况下,可以在服务端等受信任的环境使用 master key 进行查询。 - -你可以一次获取多个用户,只要向用户的根 URL 发送一个 GET 请求。没有任何 URL 参数的话,可以简单地列出所有用户: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://API_BASE_URL/1.1/users -``` - -返回的值是一个 JSON 对象包括一个 `results` 字段,值是包含了所有对象的一个 JSON 数组。 - -```json -{ - "results":[ - { - "updatedAt":"2015-07-14T02:31:50.100Z", - "phone":"18612340000", - "objectId":"55a47496e4b05001a7732c5f", - "username":"tom", - "createdAt":"2015-07-14T02:31:50.100Z", - "emailVerified":false, - "mobilePhoneVerified":false - } - ] -} -``` - -所有的对普通对象的查询选项都适用于对用户对象的查询。 - -## 删除用户 - -想要删除一个用户,可以向它的 URL 上发送一个 DELETE 请求。同样的,你必须提供一个 X-LC-Session 在 HTTP 头上以便认证。例如: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f -``` - -## 连接用户账户和第三方平台 - -你可以连接你的用户到其他服务,比如新浪微博和腾讯微博,这样就允许你的用户直接用他们现有的账号 ID 来注册或登录你的应用。在进行注册和登录时,需要附带上 `authData` 字段来提供你希望连接的服务的授权信息。一旦关联了某个服务,`authData` 将被存储到你的用户信息里。 - -`authData` 是一个普通的 JSON 对象,它所要求的 key 根据 service 不同而不同,具体要求见下面。每种情况下,你都需要自己负责完成整个授权过程(一般是通过 OAuth 协议,1.0 或者 2.0) 来获取授权信息,提供给连接 API。 - -[新浪微博](http://weibo.com/) 的 authData 内容: - -```json -{ - "weibo": { - "uid": "123456789", - "access_token": "2.00vs3XtCI5FevCff4981adb5jj1lXE", - "expiration_in": "36000" - } -} -``` - - - -[微信](http://open.weixin.qq.com/) 的 authData 内容: - -```json -{ - "weixin": { - "openid": "0395BA18A5CD6255E5BA185E7BEBA242", - "access_token": "12345678-SaMpLeTuo3m2avZxh5cjJmIrAfx4ZYyamdofM7IjU", - "expires_in": 1382686496 - } -} -``` - -[Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api) 的 authData 内容: - -```json -{ - "lc_apple": { - "uid": "从 Apple 获取到的 User Identifier", - "identity_token": "从苹果获取到的 identity Token", - "code": "从苹果获取到的 Authorization Code" - } -} -``` - -其他任意第三方平台: - -```json -{ - "第三方平台名称,例如facebook": { - "uid": "在第三方平台上的唯一用户 ID 字符串", - "access_token": "在第三方平台的 access token", - // ……其他可选属性 - } -} -``` - -云端会自动为 `authData.第三方平台名称.uid` 创建唯一索引,以确保一个第三方账号只绑定到一个应用内用户上。 - -注意: - -- 其他第三方平台不支持校验 access token。 -- 其他第三方平台不支持后面提到的 UnionID 登录功能,因此也不用设置相应的 `unionid`、`platform`、`main_account` 字段。 -- 其他第三方平台请使用 `uid` 字段储存第三方平台的唯一用户 ID 字符串,不要使用 `openid`。 - -和第三方登录相似的一个概念是匿名登录。 -匿名用户(Anonymous user)的 authData 内容如下: - -```json -{ - "anonymous": { - "id": "random UUID with lowercase hexadecimal digits" - } -} -``` - -### 注册和登录 - -使用一个连接服务来注册用户并登录,同样使用 POST 请求 users,只是需要提供 `authData` 字段。例如,使用 QQ 账户注册或者登录用户: - - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "authData": { - "qq": { - "openid": "0395BA18A5CD6255E5BA185E7BEBA242", - "access_token": "12345678-SaMpLeTuo3m2avZxh5cjJmIrAfx4ZYyamdofM7IjU", - "expires_in": 1382686496 - } - } - }' \ - https://API_BASE_URL/1.1/users -``` - -云端会检查是否已经有一个用户连接了这个 `authData` 服务。如果已经有用户存在并连接了同一个 `authData`,那么返回 200 OK 和详细信息(包括用户的 `sessionToken`): - -```sh -Status: 200 OK -Location: https://API_BASE_URL/1.1/users/75a4800fe4b05001a7745c41 -``` - -应答的 body 类似: - -```json -{ - "username": "Tom", - "createdAt": "2015-06-28T23:49:36.353Z", - "updatedAt": "2015-06-28T23:49:36.353Z", - "objectId": "75a4800fe4b05001a7745c41", - "sessionToken": "anythingstringforsessiontoken", - "authData": { - "qq": { - "openid": "0395BA18A5CD6255E5BA185E7BEBA242", - "access_token": "12345678-SaMpLeTuo3m2avZxh5cjJmIrAfx4ZYyamdofM7IjU", - "expires_in": 1382686496 - } - } -} -``` - -如果用户还没有连接到这个账号,则你会收到 201 Created 的应答状态码,标识新的用户已经被创建: - -```sh -Status: 201 Created -Location: https://API_BASE_URL/1.1/users/55a4800fe4b05001a7745c41 -``` - -应答内容包括 objectId、createdAt、sessionToken 以及一个自动生成的随机 username,例如: - -```json -{ - "username":"ec9m07bo32cko6soqtvn6bko5", - "sessionToken":"tfrvbzmdf609nu9204v5f0tuj", - "createdAt":"2015-07-14T03:20:47.733Z", - "objectId":"55a4800fe4b05001a7745c41" -} -``` - -云端会自动验证部分平台 access token 的有效性。 -详见数据存储开发指南《自动验证第三方平台授权信息》章节的说明。 - -### UnionID 注册和登录 - -微信与新浪微博都有 UnionID 登录机制,使用 UnionID 注册登录,可以使得不同微信公众号或小程序之间共享用户。 - -微信的 authData 内容: - -```json - "authData": { - "access_token" : "access_token", - "expires_in" : 7200, - "openid" : "openid", - "refresh_token" : "refresh_token", - "scope" : "snsapi_userinfo", - "unionid" : "ox7NLs-e-32ZyHg2URi_F2iPEI2U" -} -``` - -使用 UnionID 注册登录,需要提供带有 `unionid` 参数的 `authData`。另外需要配合传递 `platform` 和 `main_account` 这两个字段。 - -* `platform`:unionId 对应的注册平台,可由应用自行指定,微信、QQ、微博平台建议设为《数据存储开发指南·该如何指定 unionIdPlatform》中推荐的值。 -* `main_account`: `main_account` 为 true 时把当前平台的鉴权信息作为主账号。 - -在服务端进行存储的时候会根据 `platform` 来命名新增的平台,如传入 `"platform" = "weixin"` 时,返回数据中会增加 `_weixin_unionid` 字段存储 `{"uid":"xxxxx"}`。 - -``` -"_weixin_unionid": { - "uid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U" -} -``` - -完整的第三方注册登录请求如下,以使用微信 UnionId 为例: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "authData": { - "wxleanoffice": { - "openid": "oTY851cqL0gk3DqW3xINqG1Q4PTc", - "access_token": "12_b6mz7ujXbTY4vpbqCRaKVa_y0Ij3N9grCeVtM8VJT8KFd4qnQ9lXtBsZVxG6x9c9Nay_oNgvbKK7KYKbn8R2P7uEgA0EhsXMHmxkx-xU-Tk", - "expires_in": 7200, - "refresh_token": "12_71UYUnqHDuIfekimsJsYjBDfY67ilo30fDqrYkqlwZtxNgcBhMmQgDVhT6mJWkRg0mngvX9kXeCGP8kmBWdvUtc5ngRiN5LDTWAau4du838", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account":"true" - }, - "wxleansupport": { - "openid": "ZTY873cOa0gk5aOW5OaaOa3Q6PTc", - "access_token": "34_b6mO7OjXbTY6O-bOaRaaVa_O0aj5a9gOaeVOa8VaT8aad6OnQ9lXO-OZVOa6O9c9aaO_ZagObaa7aYabn8R4P7Oagn0ahOXaamOkO-OU-Tk", - "expires_in": 7200, - "refresh_token": "8-_78UYUnOaaOafekimOaOYj-afY67ilZ40faOOYkOlOZOOagc-hamQgaVhT6maWkRg0mngOX9kXeaaP8km-WdOUOc4ngRia4aaTWnaO4dO848", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account":"false" - }, - }}' \ - https://API_BASE_URL/1.1/users -``` - -我们看到,在上面的例子中,`wxleanoffice` 和 `wxleansupport` 的 `unionid` 是一样的,`platform` 均指定为 `weixin`,`wxleanoffice` 是主账号,`main_account` 为 `true`。 - -应答内容包括 objectId、createdAt、sessionToken、authData 以及一个自动生成的随机 username,应答的 body 类似: - -```json -{ - "sessionToken": "v53f0q4oecbrjojn530w89s5f", - "updatedAt": "2018-08-16T08:03:44.203Z", - "objectId": "5b752fe0a22b9d003137e16d", - "username": "vp7szn9ytuaylgtnw14qnjx2u", - "createdAt": "2018-08-16T08:03:44.203Z", - "emailVerified": false, - "authData": { - "wxleanoffice": { - "openid": "oTY851cqL0gk3DqW3xINqG1Q4PTc", - "access_token": "12_b6mz7ujXbTY4vpbqCRaKVa_y0Ij3N9grCeVtM8VJT8KFd4qnQ9lXtBsZVxG6x9c9Nay_oNgvbKK7KYKbn8R2P7uEgA0EhsXMHmxkx-xU-Tk", - "expires_in": 7200, - "refresh_token": "12_71UYUnqHDuIfekimsJsYjBDfY67ilo30fDqrYkqlwZtxNgcBhMmQgDVhT6mJWkRg0mngvX9kXeCGP8kmBWdvUtc5ngRiN5LDTWAau4du838", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account": "true" - }, - "wxleansupport": { - "openid": "ZTY873cOa0gk5aOW5OaaOa3Q6PTc", - "access_token": "34_b6mO7OjXbTY6O-bOaRaaVa_O0aj5a9gOaeVOa8VaT8aad6OnQ9lXO-OZVOa6O9c9aaO_ZagObaa7aYabn8R4P7Oagn0ahOXaamOkO-OU-Tk", - "expires_in": 7200, - "refresh_token": "8-_78UYUnOaaOafekimOaOYj-afY67ilZ40faOOYkOlOZOOagc-hamQgaVhT6maWkRg0mngOX9kXeaaP8km-WdOUOc4ngRia4aaTWnaO4dO848", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account":"false" - }, - "_wxleanoffice_unionid": { - "uid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U" - } - }, - "mobilePhoneVerified": false -} -``` - -可参见《数据存储开发指南》的《扩展:接入_UnionID_体系,打通不同子产品的账号系统》一节的详细说明。 - -### 连接 - -连接一个现有的用户到新浪微博或者微信,可以向 user endpoint 发送一个附带 `authData` 字段的 PUT 请求来实现。例如,连接一个用户到微信账号发起的请求类似这样: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{ - "authData": { - "weixin": { - "uid": "123456789", - "access_token": "2.00vs3XtCI5FevCff4981adb5jj1lXE", - "expiration_in": "36000" - ... - } - } - }' \ - https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f -``` - -完成连接后,你可以使用匹配的 `authData` 来认证他们。 - -### 断开连接 - -断开一个现有用户到某个服务,可以通过删除 `authData` 中对应的平台来做到。 -例如,已经绑定过微信的用户 `authData` 数据格式如下,平台名称为 `weixin`: - -```json -{ - "username": "3td7p1nucap1i1p53m1zibwgx", - "authData": { - "weixin": { - "openid": "oTY851cqL0gk3DqW3xINqG1Q4PTc", - "scope": "snsapi_userinfo", - "refresh_token": "refresh_token", - "platform": "weixin", - "unionid": "unionid, - "access_token": "access_token", - "expires_in": 7200 - } - }, -} -``` - -取消微信关联通过删除 `authData.weixin` 来实现: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: 6fehqhr2t2na5mv1aq2om7jgz" \ - -H "Content-Type: application/json" \ - -d '{"authData.weixin":{"__op":"Delete"}}' \ - https://API_BASE_URL/1.1/users/5b7e53a767f356005fb374f6 -``` - -其返回值类似于: - -```json -{ - "updatedAt":"2018-08-23T06:32:47.633Z", - "objectId":"5b7e53a767f356005fb374f6" -} -``` - -## 安全 - -当你用 REST API key 来访问云服务时,访问可能被 ACL 所限制,就像 iOS 和 Android SDK 上所做的一样。你仍然可以通过 REST API 来读和修改,只需要通过 `ACL` 的 key 来访问一个对象。 - -ACL 按 JSON 对象格式来表示,JSON 对象的 key 是 objectId 或者一个特别的 key(`*`,表示公共访问权限)。ACL 的值是权限对象,这个 JSON 对象的 key 即是权限名,而这些 key 的值总是 true。 - -举个例子,如果你想让一个 id 为 55a47496e4b05001a7732c5f 的用户有读和写一个对象的权限,而且这个对象应该可以被公共读取,符合的 ACL 应该是: - -```json -{ - "55a47496e4b05001a7732c5f": { - "read": true, - "write": true - }, - "*": { - "read": true - } -} -``` - -这样,只有在 `X-LC-Session` HTTP 头中携带了用户 55a47496e4b05001a7732c5f 的有效 sessionToken 的情况下,对该对象的写请求才不会因为权限不足而报错。 -sessionToken 的值会在用户登录成功时返回。 diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_category_.json deleted file mode 100644 index b52074471..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "云引擎", - "collapsed": true, - "position": 16 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/building-advanced.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/building-advanced.mdx deleted file mode 100644 index ba8af0016..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/building-advanced.mdx +++ /dev/null @@ -1,28 +0,0 @@ -You can override the default behavior by specifying startup commands (`run`), dependency installation commands (`install`), and build commands (`build`) in `leanengine.yaml`: - -```yaml title='leanengine.yaml' -run: echo 'run another command' -install: - - {use: 'default'} - - echo 'install additional dependencies here' -build: - - echo 'overwrite default build command here' -``` - -

    - See Reference: leanengine.yaml for more information. - {props.children ? ' Below are a few examples:' : ''} -

    - -<>{props.children} - -### System Dependencies - -You can specify the system dependencies provided by the server-side environment using `leanengine.yaml`: - -```yaml title='leanengine.yaml' -systemDependencies: - - imagemagick -``` - -See [Reference: leanengine.yaml](/sdk/engine/deep-dive/leanengine-yaml#systemdependencies-系统级依赖) for a complete list of supported system dependencies. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/building-build-logs.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/building-build-logs.mdx deleted file mode 100644 index dec5921c2..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/building-build-logs.mdx +++ /dev/null @@ -1,3 +0,0 @@ -By default, the logs generated during the build process won’t be printed to the console. If the build process fails, the logs from the last completed step will be printed to the console. - -To print the complete build log for debugging, check _Print build logs_ if you are deploying from the dashboard or add `--options 'printBuildLogs=true'` if you are deploying with the CLI. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-custom-domain.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-custom-domain.mdx deleted file mode 100644 index 5ae87eb98..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-custom-domain.mdx +++ /dev/null @@ -1,17 +0,0 @@ -import { HAS_SUB_DOMAIN } from "/src/constants/env.ts"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -Projects deployed to Cloud Engine can only be accessed with domains configured. You can bind domains by going to ** > Manage deployment > Your group > Settings > Domains**. - - - -If you bind a domain that starts with `stg-` (e.g., `stg-api.example.com`), it will be assigned to the staging environment automatically. - - - - - -We provide shared domains for apps that are still under testing. You can assign your app a shared domain with a prefix of your choice. - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-environments.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-environments.mdx deleted file mode 100644 index 41152aa07..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-environments.mdx +++ /dev/null @@ -1,23 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -The following environment variables are available for your application to use: - -| Variable name | Description | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `LEANCLOUD_APP_ID` | The `App ID` of the current application. | -| `LEANCLOUD_APP_KEY` | The `App Key` of the current application. | -| `LEANCLOUD_APP_MASTER_KEY` | The `Master Key` of the current application. | -| `LEANCLOUD_APP_ENV` | The environment your application is running in. If you are running your application on your local computer, the value will be non-existent or `development` (if you are starting your application with the CLI). It will be `stage` for the staging environment and `production` for the production environment. | -| `LEANCLOUD_APP_PORT` | The port opened up for your application. Your application has to listen on this port in order for users to access your service. | -| `LEANCLOUD_API_SERVER` | The address used to access the Data Storage service. Please use this value if your application needs to access the Data Storage service or other cloud services with the REST API. | -| `LEANCLOUD_APP_GROUP` | The group the instance is located at. | -| `LEANCLOUD_REGION` | The region the application is running in. It will be `CN` for Mainland China and `US` for the United States. | -| `LEANCLOUD_VERSION_TAG` | The version number of the deployment. | - - - -The environment variables starting with `LC_` (like `LC_APP_ID`) used by the older version of Cloud Engine have already been deprecated. Those environment variables will still be present for a while to ensure compatibility but will eventually get removed. If you are still using them in your application, please replace them with those starting with `LEANCLOUD_`. - - - -You can also set up custom environment variables on the dashboard to store configurations. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-filesystem.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-filesystem.mdx deleted file mode 100644 index e970e6ada..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-filesystem.mdx +++ /dev/null @@ -1,7 +0,0 @@ -Your application can create temporary files under `/home/leanengine` and `/tmp`. The size limit for all the files created by your application is 1 GB. - -:::caution -Each time you trigger a new deployment for your application, Cloud Engine will create a new container for it. Even though you don’t trigger deployments, Cloud Engine will still perform occasional maintenance operations. This means that your application **should not treat the file system provided by Cloud Engine as permanent storage**. -::: - -If the files created by your application bear relatively larger sizes, we recommend that your application always cleans them up once it finishes using them. Creating more files when there are already more than 1 GB files existing might lead to the `Disk quota exceeded` error. You can trigger a deployment to quickly clean up all the temporary files. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-health-check.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-health-check.mdx deleted file mode 100644 index b7bb3ad7b..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-health-check.mdx +++ /dev/null @@ -1,27 +0,0 @@ -import Path from "/src/docComponents/path"; - -Cloud Engine is primarily optimized for web applications. Your app is expected to provide HTTP services through the port specified by the environment variable named `LEANCLOUD_APP_PORT`. Keep in mind that the app should listen on `0.0.0.0` (all interfaces) instead of `127.0.0.1` which is the default host of many frameworks. - -While your app is being deployed, Cloud Engine will check your app every second to see if it has been successfully started. If your app has not been started within the time limit (30 seconds by default), the deployment will be canceled. After your app has been deployed, Cloud Engine will run health checks for your app regularly and automatically restart it if the check fails. - -The way the health check works is that Cloud Engine will send an HTTP request to the homepage (`/`) of your app. If it gets an HTTP 2xx response, your app will pass the health check. - -
    -Health check and the Cloud Engine SDK - -Cloud Engine will also check `/__engine/1/ping` which is handled by the SDK. If the SDK is integrated correctly, Cloud Engine will not check the homepage (`/`) anymore. - -If ** > Manage deployment > Your group > Settings > Cloud functions mode** is set to _Enable_, or if `functionsMode` in `leanengine.yaml` is set to `strict`, Cloud Engine will check if the SDK is integrated correctly. If not, it will consider your app to have failed to start. - -
    - -
    -Customizing startup timeout (startupTimeout) - -The default timeout for your app to start is 30 seconds. You can change it to any value between 15 and 120 seconds with `leanengine.yaml`: - -```yaml title='leanengine.yaml' -startupTimeout: 60 -``` - -
    diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-internet-address.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-internet-address.mdx deleted file mode 100644 index 779fc7aed..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-internet-address.mdx +++ /dev/null @@ -1,16 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -Some third-party platforms (like Weixin Open Platform) may require that you provide an IP address whitelist. You can obtain the inbound and outbound IP addresses used by Cloud Engine on ** > Manage deployment > Your group > Settings > Inbound IP and outbound IP**. - - - -:::info -For Cloud Engine applications deployed within Mainland China, CDN will be enabled by default. Depending on the provider we use, the inbound IP addresses might be changed frequently. To get a fixed inbound IP address for your application, consider enabling dedicated IP. -::: - - - -We will do our best to minimize the frequency of changing the inbound and outbound IP addresses, but there remains the possibility for them to get changed. If you encounter any problems with IP addresses, the first thing you can do is look at the IP addresses displayed on the dashboard and see if they have been changed. - -To get a fixed inbound IP address for your application, consider enabling dedicated IP. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-load-balancer.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-load-balancer.mdx deleted file mode 100644 index b50d299ef..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-load-balancer.mdx +++ /dev/null @@ -1,154 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; -import { HAS_ENGINE_CDN_DOMAIN } from "/src/constants/env.ts"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; - -All HTTP and HTTPS requests sent to Cloud Engine will go through a load balancer that deals with chores including HTTPS encryption, HTTPS redirection, and response compression. You won’t have to implement features for handling these tasks yourself for the programs hosted on Cloud Engine. Meanwhile, the load balancer brings the following restrictions that your program cannot bypass: - -- Paths starting with `/.well-known/acme-challenge/` are used by Cloud Engine to automatically renew certificates. Requests sent to these paths won’t be forwarded to your program. -- The size of a request header (URL and headers) should be within 64K and each line of the request header should be within 8K. -- The size of a request (for uploading files) should be within 100M. -- The timeout for connecting or waiting for a response is 60 seconds. - - - -#### Getting the Client IP Address - -Cloud Engine’s load balancer includes the following information depicting the original request in the HTTP header: - -- `X-Real-IP`: The original IP address. -- `X-Forwarded-Proto`: The original protocol (`http` or `https`). -- `Forwarded`: Information about the proxy, defined by RFC 7239. It contains the IP address and the protocol. - - - -:::caution -If CDN is enabled, the information included in the HTTP headers above will be those of the CDN rather than the original request. -::: - -With CDN enabled, the following HTTP headers will also be present: - -- `X-Forwarded-For`: IP addresses separated by a comma. The first one would be the IP address of the original request. - -:::caution -It’s possible that the information included in the HTTP headers above gets counterfeited. Cloud Engine won’t be able to guarantee its authenticity. -::: - - - - - - -In Express: - -```js -app.get("/", function (req, res) { - console.log(req.headers["x-real-ip"]); - res.send(req.headers["x-real-ip"]); -}); -``` - - - - -Python (Flask): - -```python -from flask import Flask -from flask import request - -app = Flask(__name__) - -@app.route('/') -def index(): - print(request.headers['x-real-ip']) - return 'ok' -``` - -Python (Django): - -```python -def index(request): - print(request.META['HTTP_X_REAL_IP']) - return render(request, 'index.html', {}) -``` - - - - -```php -$app->get('/', function($req, $res) { - error_log($_SERVER['HTTP_X_REAL_IP']); - return $res; -}); -``` - - - - -```java -EngineRequestContext.getRemoteAddress(); -``` - - - - -Go (Echo): - -```go -func fetchRealIP(c echo.Context) error { - realIP = c.RealIP() - //... -} -``` - - - - - - -:::info -For Cloud Engine applications deployed within Mainland China, CDN will be enabled by default. To ensure that your application always gets the accurate IP addresses of original requests, consider enabling dedicated IP. You can learn more about the differences between CDN and dedicated IP on [Binding Your Domains § Cloud Engine Domains](/sdk/domain/guide/#cloud-engine-domains). -::: - - - - -#### HTTPS Redirect - -When you bind a custom Cloud Engine domain, you can enable _Force HTTPS_ to have the load balancer redirect HTTP requests to HTTPS while keeping the paths. - - - -:::caution -_Force HTTPS_ won’t work properly if CDN is enabled. You’ll still need to [implement redirect in your project’s code](/sdk/engine/functions/sdk/#how-to-redirect-http-requests-to-https-with-the-sdk). -::: - - - - - -#### CDN Caching - -If you resolve your custom domain to the CDN (including the shared domain provided by Cloud Engine), the CDN will cache the responses for the requests it has received. There are some default rules for caching followed by the CDN. - -The CDN will cache the response if: - -- The response header contains `Last-Modified` (this indicates that the resource requested is static; for HTML files, they will be cached for at most 60 seconds). - -The CDN will not cache the response if: - -- There is an error with the response (not 2xx). -- The request is not idempotent (like a `POST` request). -- The response header doesn’t contain `Last-Modified` (this often indicates that the resource requested is dynamic). - -The age of the cache for a given file will depend on the file type and the value of the `Last-Modified` header. The less frequently the file gets modified, the longer the file gets cached. You can override the default behavior by configuring `Cache-Control` and the CDN will try its best to follow your configurations. For example: - -- You can use `Cache-Control: no-cache` to prevent the response from being cached. -- You can use `Cache-Control: max-age=3600` to specify the age of the cache (here we set it to be 1 hour). - -:::info -To prevent your application from being affected by the caching mechanism, consider enabling dedicated IP. You can learn more about the differences between CDN and dedicated IP on [Binding Your Domains § Cloud Engine Domains](/sdk/domain/guide/#cloud-engine-domains). -::: - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-logs.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-logs.mdx deleted file mode 100644 index 2b45c47c5..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-logs.mdx +++ /dev/null @@ -1,52 +0,0 @@ -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; - -:::note -See [Cloud Engine Platform Features § Viewing Logs](/sdk/engine/platform/#viewing-logs) for more information on how to view logs and access logs on the dashboard. -::: - -Cloud Engine will collect the logs your application has printed to standard output (stdout) and standard error (stderr): - - - - -```js -console.log("hello"); // stdout -console.error("some error"); // stderr -``` - - - - -```python -import sys - -print('hello') # stdout -print('some error', file=sys.stderr) # stderr -``` - -
    -Example for printing logs with Python 2 - -```python -import sys - -print 'hello' # stdout -print >> sys.stderr, 'some error' # stderr -``` - -
    - -
    - - -```php -error_log("some error"); -``` - - -
    - -:::note -Each line of the logs can contain a maximum of 4096 characters. A maximum of 600 lines of logs can be collected every minute. The logs generated by your application that exceed these limits will be discarded. -::: diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-timezone.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-timezone.mdx deleted file mode 100644 index aeeb41bc6..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/cloud-timezone.mdx +++ /dev/null @@ -1,13 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - - - -The server side uses Beijing Time (UTC+8). - - - - - -The timezone used on the server side is UTC+0. - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/functions-introduction.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/functions-introduction.mdx deleted file mode 100644 index d5402c9d6..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/functions-introduction.mdx +++ /dev/null @@ -1,13 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -Cloud Functions lets you run backend code on the cloud in response to various types of events. It is supported by our client-side SDKs and can automatically serialize objects that have the data types provided by our [Data Storage](/sdk/storage/features)[Data Storage](/sdk/storage/overview/) service. - -The following scenarios can be easily implemented with the help of Cloud Functions and Hooks: - -- Manage complex logics in one place without implementing them with different languages for each platform. -- Adjust certain logics of your project on the server side without updating clients. -- Retrieve and update data regardless of the ACL or class permissions. -- Trigger custom logics or perform additional permission checks when objects are created, updated, or deleted, or when users are logged in or verified. -- Run scheduled tasks to fulfill requirements like closing unpaid orders every hour, deleting outdated data every midnight, etc. - -You can write Cloud Functions with any of the languages (runtime environments) supported by Cloud Engine, including Node.js, Python, Java, PHP, .NET, and Go. Our dashboard provides an online editor for you to write Cloud Functions in Node.js. If you prefer using another language, please create a project based on our demo project and deploy it to Cloud Engine. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/leandb-cli-access.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/leandb-cli-access.mdx deleted file mode 100644 index 289b6813f..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/leandb-cli-access.mdx +++ /dev/null @@ -1,43 +0,0 @@ -import { Command } from "/src/docComponents/engine"; -import { CLI_BINARY } from "/src/constants/env.ts"; -import CodeBlock from "@theme/CodeBlock"; - -You can open an interactive shell connected to LeanDB using , a command provided by the [CLI](/sdk/engine/cli): - - - {`$ ${CLI_BINARY} db shell mysqldb -Welcome to the MySQL monitor. -Your MySQL connection id is 3450564 -Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement. -mysql> use test -Database changed -mysql> insert into users set name = 'leancloud'; -Query OK, 1 row affected (0.04 sec) -mysql> select * from users; -+------+-----------+ -| id | name | -+------+-----------+ -| 1 | zhenzhen | -| 2 | leancloud | -+------+-----------+ -2 rows in set (0.06 sec)`} - - -With , you can export LeanDB to a local port and have local programs or GUI clients connect to the database: - - - {`$ ${CLI_BINARY} db proxy myredis -[INFO] Now, you can connect myredis via [redis-cli -h 127.0.0.1 -a hsdt9wIAuKcTZmpg -p 5678]`} - - -As long as you keep the terminal open, you’ll be able to access LeanDB from the port 5678. You can use a GUI client to browse and interact with LeanDB. While running your project with , you can also have your program connected to LeanDB using this feature. You can set the environment variable (from the output of ): - -```shell -export REDIS_URL_myredis=redis://default:hsdt9wIAuKcTZmpg@127.0.0.1:5678 -``` - -:::note - -You should only use for developing and debugging locally. Don’t use it for the production environment, as the connection might be interrupted occasionally. - -::: diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/leandb-create-instance.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/leandb-create-instance.mdx deleted file mode 100644 index 342e681f5..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/leandb-create-instance.mdx +++ /dev/null @@ -1,51 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -:::caution - -<> - Your account will start to be billed as soon as you create a{" "} - {props.instanceName || "LeanDB"} instance. Please make sure there’s enough - balance in your account. - - -::: - -You will see the following options when you click **Create instance**: - -<>{props.children} - -You’ll see the price of the current instance once you choose a specification. - -
    -More about the billing of {props.instanceName || 'LeanDB'} - -

    - {props.instanceName || "LeanDB"} instances are charged every day. If you use - an instance for less than one day, you will be charged for one day’s usage. - You will be billed for your usage of the previous day every single day. You - will be charged for your {props.instanceName || "LeanDB"} instances based on - the specification you chose. Even though you don’t use the service after you - create an instance, you will still be billed. You will see the prices of - different specifications when you create your {props.instanceName || "LeanDB"}{" "} - instance. You can also view the prices on the pricing page - of the current region. You can view - your bill history on - the Consumption details page on the dashboard - - - the Transactions page on the Developer Center - - . -

    - -:::danger - -<> - If your account maintains a negative balance for more than a month, all the{" "} - {props.instanceName || "LeanDB"} instances in your account, including their - data, will be deleted. - - -::: - -
    diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/nodejs-setup-dependencies.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/nodejs-setup-dependencies.mdx deleted file mode 100644 index ec00b75bf..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/nodejs-setup-dependencies.mdx +++ /dev/null @@ -1,21 +0,0 @@ -Cloud Engine will automatically install dependencies according to the `package.json` in your project: - -```json title='package.json' -{ - "dependencies": { - "leancloud-storage": "^3.11.0", - "leanengine": "^3.3.2" - }, - "devDependencies": { - "nodemon": "^1.18.7" - } -} -``` - -While installing dependencies, Cloud Engine will trigger the [life cycle scripts](https://docs.npmjs.com/cli/v8/using-npm/scripts#life-cycle-scripts) of npm, such as `postinstall` and `prepare`. - -Since Cloud Engine installs dependencies on the server side, the CLI won’t upload `node_modules` by default. If you’re deploying with Git, you should include `node_modules` in `.gitignore` so it won’t be tracked by Git. - -:::note -CLI will upload `.yarn` directory, if you don't want to enable [Zero-installs](https://yarnpkg.com/features/caching#zero-installs) when Yarn 2+ [PnP\(Plug'n'Play\)](https://yarnpkg.com/features/pnp) is used, add `.yarn/cache` to your `.gitignore` and `.leanignore` -::: diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/nodejs-setup-package-manager.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/nodejs-setup-package-manager.mdx deleted file mode 100644 index 1be3d27e1..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/nodejs-setup-package-manager.mdx +++ /dev/null @@ -1,57 +0,0 @@ -Cloud Engine now supports these package managers: - -- [npm](https://docs.npmjs.com/cli/v10/commands/npm) -- [pnpm](https://pnpm.io) -- [Yarn 1](https://classic.yarnpkg.com) -- [Yarn 2+](https://yarnpkg.com) - -Cloud Engine will automatically detect the package manager based on these conditions: - -| Package Manager | Condition | Version | -| --------------- | ------------------------------------------------------------------------------- | ---------------------------- | -| pnpm | A valid `pnpm-lock.yaml` exists | | -| | `lockfileVersion: '6.0'` or higher | 8 | -| | `lockfileVersion: 5.3` or higher | 7 | -| | Otherwise | 6 | -| Yarn 1 | `yarn.lock` exists | 1 | -| Yarn 2+ | Not supported by default, enable via [Corepack](#experimental-corepack-support) | 2+ | -| npm | Otherwise | npm with the Node.js is used | - -### Experimental Corepack support - -**Due to Corepack is still experimental, Cloud Engine can't make sure the support for Corepack is stable, use at your own risk** - -To enable Corepack, setting the `ENABLE_EXPERIMENTAL_COREPACK` environment variable of the group to any non-empty string. - -Cloud Engine will automatically detect package manager based on `packageManager` field in `package.json`, then use Corepack to install and enable the package manager. This is the only way to use Yarn 2+ for now. - -Assume we have a `package.json` shown below: - -```json title="package.json" -{ - "name": "example", - "packageManager": "yarn@4.0.2" -} -``` - -Now Cloud Engine will call `corepack prepare --activate` and detect the package manager is Yarn 2+. - -Reference:[Corepack](https://nodejs.org/api/corepack.html) - -### Default commands - -The default commands may vary depending on the package manager, e.g. if pnpm is used, `npm ci` will be `pnpm install --frozen-lockfile`. - -Cloud Engine will skip installing the devDependencies only when `installDevDependencies` is not `true` and the build script is empty (which either not specified in `leanengine.yaml` or `scripts.build` in `package.json` not exists). - -| Pharse | Package Mamager | Condition | Command | -| ------- | --------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| install | npm | Node.js version is higher then 10 and `package-lock.json` or `npm-shrinkwrap.json` exists | `npm ci` | -| | | | `npm install` or `npm install --omit=dev` | -| | pnpm | | `pnpm install --frozen-lockfile` or `pnpm install --frozen-lockfile --prod` | -| | Yarn 1 | | `yarn install` or `yarn install --production` | -| | Yarn 2+ | | `yarn install` | - -:::note -Be adviced, Yarn 1 will use the resolved URL in `yarn.lock` without respecting your registry configuration, use proper registry before deploy to Cloud Engine, or build time might increase. -::: diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/nodejs-setup-runtime.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/nodejs-setup-runtime.mdx deleted file mode 100644 index b2718095e..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/nodejs-setup-runtime.mdx +++ /dev/null @@ -1,26 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -You can specify the version of Node.js by setting `engines.node` in `package.json`: - -```json title='package.json' -{ - "engines": { - "node": "16.x" - } -} -``` - -You can set it to `*` to stick to the latest (current) version. - -:::note - -For newly created apps, if -If you don’t specify a Node.js version, -the latest stable (LTS) version will be used. - - {" "} - For apps created before 9/2/2021, Node.js `0.12` will be used by default to ensure - compatibility. - - -::: diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/platform-introduction.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/platform-introduction.mdx deleted file mode 100644 index b1337555a..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/platform-introduction.mdx +++ /dev/null @@ -1 +0,0 @@ -Cloud Engine is a platform that lets you host backends for your applications. If you have web apps or backend programs built with Node.js, Python, Java, PHP, .NET, Go, or C++, you can deploy them to Cloud Engine and it will automatically build runnable _versions_ from the source code and run them in independent containers. Cloud Engine provides capabilities including log viewing, monitoring, load balancing, zero downtime deployment, and autoscaling that you can use out of the box. Additional features provided by Cloud Engine include scheduled tasks, domain and certificate management, and hosted database management systems including Redis, MySQL, MongoDB, and Elasticsearch. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/platform-runtimes.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/platform-runtimes.mdx deleted file mode 100644 index f25ef7833..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/platform-runtimes.mdx +++ /dev/null @@ -1,8 +0,0 @@ -- [Frontend Runtime Environment](/sdk/engine/deploy/webapp) -- [Node.js Runtime Environment](/sdk/engine/deploy/nodejs) -- [Python Runtime Environment](/sdk/engine/deploy/python) -- [Java Runtime Environment](/sdk/engine/deploy/java) -- [PHP Runtime Environment](/sdk/engine/deploy/php) -- [.NET Runtime Environment](/sdk/engine/deploy/dotnet) -- [Go Runtime Environment](/sdk/engine/deploy/go) -- [C++ Runtime Environment](/sdk/engine/deploy/cpp) diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/quick-start-deploy.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/quick-start-deploy.mdx deleted file mode 100644 index 7131b7710..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/quick-start-deploy.mdx +++ /dev/null @@ -1,6 +0,0 @@ -import { CLI_BINARY } from "/src/constants/env.ts"; -import CodeBlock from "@theme/CodeBlock"; - -Run the following command to deploy your project to the production environment: - -{`${CLI_BINARY} deploy --prod`} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/quick-start-new.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/quick-start-new.mdx deleted file mode 100644 index 74024c2cd..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/_partials/quick-start-new.mdx +++ /dev/null @@ -1,89 +0,0 @@ -import { CLI_BINARY } from "/src/constants/env.ts"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import { Conditional } from "/src/docComponents/conditional"; -import { Command } from "/src/docComponents/engine"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; - - - -Please first install the latest CLI by following [CLI Guide § Installation](/sdk/engine/cli/#installation), then log in to your account by following [CLI Guide § Logging In](/sdk/engine/cli/#logging-in). - - - -If you haven’t created an app on the dashboard, please do it first. Once you have your app created, create a new project by running : - - - {`$ ${CLI_BINARY} new ${props.appName || "my-engine-app"} -[?] Please select an app template: - 1) Node.js - Express - 2) Node.js - Koa - 3) Python - Flask - 4) Python - Django - 5) Java - Servlet - 6) Java - Spring Boot - 7) PHP - Slim - 8) .NET Core - 9) Go - Echo - 10) React Web App (via create-react-app) - 11) Vue Web App (via @vue/cli) - => 1 -[?] Please select an app: - 1) ${props.appName || "my-engine-app"} - => 1 -[INFO] Downloading templates 7.71 KiB / 7.71 KiB [==================] 100.00% 0s -[INFO] Creating project... -[INFO] Created Node.js - Express project in \`${ - props.appName || "my-engine-app" - }\` -[INFO] Lean how to use Express at https://expressjs.com`} - - -

    - will create a directory with the name you provided. - We can now run cd {props.appName || "my-engine-app"} and install - the dependencies: -

    - - - - -```sh -npm install -``` - - - - -```sh -pip install -Ur requirements.txt -``` - - - - -```sh -composer install -``` - - - - -```sh -mvn package -``` - - - - -Install the .NET SDK with the version specified in global.json. - - - - -```sh -go mod tidy -``` - - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/cli.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/cli.mdx deleted file mode 100644 index 97eb36013..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/cli.mdx +++ /dev/null @@ -1,484 +0,0 @@ ---- -title: CLI Guide -sidebar_label: CLI -sidebar_position: 8 ---- - -import CodeBlock from "@theme/CodeBlock"; -import LeandbCliAccess from "./_partials/leandb-cli-access.mdx"; -import QuickStartNew from "./_partials/quick-start-new.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import { Command } from "/src/docComponents/engine"; -import { CLI_BINARY, BRAND } from "/src/constants/env.ts"; -import Path from "/src/docComponents/path"; - -The Cloud Engine CLI () is a tool for you to deploy your Cloud Engine projects and manage your applications. - -## Install the CLI - -### macOS - -You can install the CLI with [Homebrew](https://brew.sh/): - -```sh -brew update && brew install lean-cli -``` - -
    -Having trouble using Homebrew? - -If you’re having a poor connection to Homebrew’s service, you can [set up environment variables like `http_proxy` to make your connection faster](https://docs.brew.sh/Manpage#using-homebrew-behind-a-proxy). You can also use a mirror for Homebrew (like [TUNA](https://mirror.tuna.tsinghua.edu.cn/help/homebrew/)). - -If you prefer not to use Homebrew, you can download the binary file named (for Apple Silicon) or (for Intel) from [GitHub releases], rename the file to , and move the file to a directory listed within `$PATH`. Make sure to make the file executable by running . - -
    - -### Windows - -You can download the 32-bit or 64-bit **msi** file from [GitHub releases] and run the file to install the CLI. After you install the CLI, you’ll be able to use it by running in Command Prompt or PowerShell. - -You can also download the **exe** file, rename it to , and add its path to the **PATH** environment variable ([instructions](https://www.java.com/en/download/help/path.html)). You will then be able to run under any directory using Command Prompt or PowerShell. Besides adding the file’s path to PATH, you can also choose to move the file to a directory already listed within PATH, like `C:\Windows\System32`. - -### Linux - -For Debian-based distributions, you can download the deb file from [GitHub releases]. - -For other distributions, you can download the pre-compiled binary file (like ) from [GitHub releases], rename the file to , and move the file to a directory listed within `$PATH`. Make sure to make the file executable by running . - -[github releases]: https://releases.leanapp.cn/#/leancloud/lean-cli/releases - -### Upgrade to the Latest Version - -If you have already installed the CLI, you can upgrade it to the latest version by reinstalling the CLI with the latest file. - -## Usage - -After installing the CLI, you can run on the terminal to view the help information: - -
    -View the output of - - - -``` -NAME: - lean - Command line tool to manage and deploy LeanEngine apps - -USAGE: - lean [global options] command [command options] [arguments...] - -VERSION: - 1.0.0 - -COMMANDS: - login Log in to LeanCloud - switch Change the associated LeanEngine app - info Show information about the associated user and app - up Start a development instance locally with debug console - new Create a new LeanEngine project from official examples - deploy Deploy the project to LeanEngine - publish Publish the version of staging to production - db Access to to LeanDB instances - file Manage files ('_File' class in Data Storage) - logs Show LeanEngine logs - debug Start the debug console without running the project - env Print custom environment variables on LeanEngine (secret variables not included) - cql Enter CQL interactive shell (warn: CQL is deprecated) - help Show usages of all subcommands - -GLOBAL OPTIONS: - --version, -v print the version -``` - - - - -``` -NAME: - tds - Command line tool to manage and deploy Cloud Engine apps - -USAGE: - tds [global options] command [command options] [arguments...] - -VERSION: - 1.0.0 - -COMMANDS: - login Log in to TapTap Developer Services - switch Change the associated Cloud Engine app - info Show information about the associated user and app - up Start a development instance locally with debug console - new Create a new Cloud Engine project from official examples - deploy Deploy the project to Cloud Engine - publish Publish the version of staging to production - db Access to to Database instances - file Manage files ('_File' class in Data Storage) - logs Show Cloud Engine logs - debug Start the debug console without running the project - env Print custom environment variables on Cloud Engine (secret variables not included) - cql Enter CQL interactive shell (warn: CQL is deprecated) - help Show usages of all subcommands - -GLOBAL OPTIONS: - --version, -v print the version -``` - - - -
    - -To view the version of the CLI, use the `--version` option: - - - {`$ ${CLI_BINARY} --version -${CLI_BINARY} version 1.0.0`} - - -The following subcommands are available in the CLI: - -| Command | Purpose | -| --------- | ----------------------------------------------------------------------------------------------------------- | -| `login` | Log in to an account | -| `switch` | Change the application and group linked to the project | -| `info` | View the application and group linked to the project | -| `up` | Run and debug the project locally | -| `new` | Create a new project from a template | -| `deploy` | Deploy the project to Cloud Engine | -| `publish` | Publish the version in the staging environment to the production environment | -| `db` | Connect to LeanCache or LeanDB | -| `file` | Upload files to the Data Storage service (uploaded files can be found on ** > Files**) | -| `logs` | View Cloud Engine logs | -| `debug` | Open the Cloud Function Debug Console without running the project | -| `env` | View or edit the environment variables of the current project | - -You can learn more about each subcommand by running . For example: - -
    -View the output of - - - {`NAME: - ${CLI_BINARY} deploy - Deploy the project to ${ - BRAND === "leancloud" ? "LeanEngine" : "Cloud Engine" - }\n -USAGE: - tds deploy [command options] (--prod | --staging) [--no-cache --build-logs --overwrite-functions]\n -OPTIONS: - --prod Deploy to production environment - --staging Deploy to staging environment - --build-logs Print build logs - -g Deploy from git repo - --war Deploy .war file for Java project. The first .war file in target/ is used by default - --no-cache Disable buliding cache - --overwrite-functions Overwrite cloud functions with the same name in other groups - --leanignore value Rule file for ignored files in deployment (default: ".leanignore") - --message value, -m value Comment for this version, only applicable when deploying from local files - --keep-deploy-file - --revision value, -r value Git revision or branch. Only applicable when deploying from Git (default: "master") - --options --options build-root=app Send additional deploy options to server, in urlencode format(like --options build-root=app) - --direct Upload project's tarball to remote directly`} - - -
    - -## Log In - -The first thing you may want to do after installing the CLI is to log in to your account. - - - -Please go to the Developer Center, click “Create Game”, and then follow the instructions to enter your game’s information. After this, go to your game’s **Game Services > Cloud Services > Cloud Engine > Turn on now > Deploy projects > Deploy using CLI** and follow the instructions to log in to your account. - - - -To switch to a different account, run again. - -## Initialize a Project - -After logging in, you can initialize a project with . This will also link the project to an existing application. - - - -## Link to an Application and Group - -Most of the functions provided by the CLI require the project to be linked to an application. You can use to link a project to an application: - -{`${CLI_BINARY} switch`} - -If your application contains multiple groups, you’ll need to pick a group as well. - -To link the project to a different application, run again. - -You can also run to quickly switch to a different application. - -You can view the application linked to the current project with . - -## Run and Debug Locally - -Run the following command under the root directory of your project to run and debug the project locally: - -{`${CLI_BINARY} up`} - -Besides starting your project, the CLI will also start a Cloud Function Debug Console. - -- Visit for the homepage of a web application. -- Visit for the Cloud Function Debug Console (you can debug hooks as well). - -To change the port for starting the project, run . - -The CLI won’t take care of auto-restart or hot-reload for your project, though some of our demo projects (like the Node.js demo project) come with the auto-restart feature. - -Besides starting your project with the CLI, you can also start them **natively** using commands like `node server.js` or `python wsgi.py`. This helps if you want to integrate the development process of your Cloud Engine project into your own development process or the IDE you’re using. If you created your project with the CLI, the project would depend on certain environment variables. You will need to configure these environment variables on your own. - -You can run to get the environment variables mentioned above. Once you’ve configured them on the terminal, you won’t have to start your project with the CLI anymore. If your shell is compatible with `sh`, you can also run to have all the environment variables configured automatically. - -When starting your project, you can add custom options to the startup command by adding `--` after . All the options added after `--` will be passed to the actual startup command. For example, if you want to enable the inspector by adding `--inspect` when starting a Node.js project, you can run . - -You can also start your project with any startup command by using the `--cmd` flag, like . - -If you ever need to start your project with an IDE or debug the Cloud Functions of a project located on a virtual machine or a remote computer, you can run the Cloud Function Debug Console without running the project itself: - - - {`$ ${CLI_BINARY} debug --remote=http://remote-url-or-ip-address:remote-port --app-id=xxxxxx`} - - -For more information about Cloud Engine, see [Cloud Engine Overview](/sdk/engine/overview/). - -## Deploy - -Starting from v1.0, requires that you provide an argument specifying the environment you would like to deploy your project to. If you don’t provide the argument, the CLI will ask for the environment interactively: - -| Command | Behavior under trial mode | Behavior under standard mode | -| -------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------- | -| | Deploy to the production environment | Deploy to the production environment | -| | Not supported | Deploy to the staging environment | -| | Not supported | Publish the version in the staging environment to the production environment | - -### Deploy From Local Project - -Once you’ve finished developing and testing your project, you can deploy it to Cloud Engine: - -{`${CLI_BINARY} deploy --prod`} - -This command will deploy the project to the production environment and override the previous version deployed from a local project, Git, or the online editor. - -You will see the progress during the deployment: - - - {`$ ${CLI_BINARY} deploy --prod -[INFO] lean (v1.0.0) running on darwin/arm64 -[INFO] Current app: my-engine-app (xxxxxxxxxxxxxxxxxxxxxxxx), group: web, region: cn-n1 -[INFO] Deploying new verison to production -[INFO] Node.js runtime detected -[INFO] Uploading file 6.40 KiB / 6.40 KiB [=========================] 100.00% 0s -[REMOTE] 开始构建 20220328-114036 -[REMOTE] 正在下载应用代码 ... -[REMOTE] 正在解压缩应用代码 ... -[REMOTE] 运行环境: nodejs -[REMOTE] 正在下载和安装依赖项 ... -[REMOTE] 存储镜像到仓库(0B)... -[REMOTE] 镜像构建完成:20220328-114036 -[REMOTE] 开始部署 20181207-115634 到 web1 -[REMOTE] 正在创建新实例 ... -[REMOTE] 正在启动新实例 ... -[REMOTE] [Python] 使用 Python 3.7.1, Python SDK 2.1.8 -[REMOTE] 实例启动成功:{"version": "2.1.8", "runtime": "cpython-3.7.1"} -[REMOTE] 云函数和 Hook 信息已更新 -[REMOTE] 部署完成:1 个实例部署成功`} - - -The default message for the deployment is “Built from lean-cli”, which will be displayed on ** > Manage deployment > Your group > Logs**. You can customize the message with `-m`: - - - {`${CLI_BINARY} deploy --prod -m 'fix #42'`} - - -After deploying, you’ll need to bind a Cloud Engine domain for your application, then you’ll be able to test your project with curl or visit your site via a browser. - -
    -Ignore files with .leanignore - -If your project contains certain files that need to be skipped when your deploy your project (like temporary files or those used by source code management tools), you can add them to `.leanignore`. - -`.leanignore` shares a similar format with `.gitignore` (the syntax for `.leanignore` is actually a subset of that for `.gitignore`), with every single line as a file or directory to be ignored. If your project does not contain a `.leanignore`, the CLI will automatically create one [according to the language used for the project][defaultignorepatterns]. Make sure to check if the content in the file meets the requirements of your project. - -[defaultignorepatterns]: https://github.com/leancloud/lean-cli/blob/master/runtimes/ignorefiles.go#L13 - -
    - -### Staging Environment - -If you’ve upgraded your application to the standard mode, a staging environment will be included in your application. The staging environment shares almost the same environment with the production environment and they both have access to the same data from the Data Storage services. You can bind a separate domain to the staging environment for testing purposes. While developing your project, you can deploy the changes to the staging environment first, and only publish the changes to the production environment if everything goes well in the staging environment. - -To deploy to the staging environment: - -{`${CLI_BINARY} deploy --staging`} - -To publish the version in the staging environment to the production environment: - - - {`$ ${CLI_BINARY} publish -[INFO] Current CLI tool version: 0.21.0 -[INFO] Retrieving app info ... -[INFO] Deploying AwesomeApp(xxxxxx) to region: cn group: web production -[REMOTE] 开始部署 20181207-115634 到 web1 -[REMOTE] 正在创建新实例 ... -[REMOTE] 正在启动新实例 ... -[REMOTE] 实例启动成功:{"version": "2.1.8", "runtime": "cpython-3.7.1"} -[REMOTE] 正在更新云函数信息 ... -[REMOTE] 部署完成:1 个实例部署成功`} - - - guarantees that the exact same version in the staging -environment (including dependencies and the build output) will be deployed to the -production environment. - -### Deploy From Git Repository - -If your project is hosted on a Git platform (like [GitHub](https://github.com/)) and you have already configured the Git repository and deploy key on the dashboard, you can run the following command to have your project deployed with the source code in the repository: - -{`${CLI_BINARY} deploy --prod -g`} - -- `-g` means to deploy from the Git repository. Make sure it is already set up on the dashboard. -- The command will use the latest commit on the **master** branch by default. You can specify the commit or branch by adding `-r `. -- See [Cloud Engine Platform Features § Deploying With Git](/sdk/engine/platform/#deploying-with-git) for instructions on how to set up Git repository and deploy key. - -## View Logs - -You can view the latest logs of your application with `logs`: - - - {`$ ${CLI_BINARY} logs - 2019-11-20 17:17:12 Deploying 20191120-171431 to web1 - 2019-11-20 17:17:12 Creating new instance ... - 2019-11-20 17:17:22 Starting new instance ... -web1 2019-11-20 17:17:22 -web1 2019-11-20 17:17:22 > node-js-getting-started@1.0.0 start /home/leanengine/app -web1 2019-11-20 17:17:22 > node server.js -web1 2019-11-20 17:17:22 -web1 2019-11-20 17:17:23 Node app is running on port: 3000 - 2019-11-20 17:17:23 Instance started: {"runtime":"nodejs-v12.13.1","version":"3.4.0"} - 2019-11-20 17:17:23 Cloud functions and hooks metadata updated - 2019-11-20 17:17:23 Deploy finished: 1 instances deployed`} - - -The command will return 30 entries by default, with the latest ones on the bottom. - -You can use the `-f` option to have logs fetched continuously (similar to using `tail -f`): - -{`${CLI_BINARY} logs -f`} - -Newer logs will be automatically printed to the bottom of the screen. - -
    -Advanced usage of - -You can specify the number of entries returned by the CLI using the `-l` option. For example, to return the latest 100 entries: - -{`${CLI_BINARY} logs -l 100`} - -If you want to fetch the logs generated within a certain period of time, you can provide a `--from` and `--to` parameter: - - - {`${CLI_BINARY} logs --from=2017-07-01 --to=2017-07-07`} - - -Use `--from` without `--to` if you want to fetch the logs from a certain day to the current time: - -{`${CLI_BINARY} logs --from=2017-07-01`} - -If you prefer viewing logs with a different tool on your computer, you can export the logs into a file in the format of JSON: - - - {`${CLI_BINARY} logs --from=2017-07-01 --to=2017-07-07 --format=json > leanengine.logs`} - - -Both `--from` and `--to` use the local time zone (the time zone of the machine in which you run ). - -
    - -## Connect to LeanDB - - - -## Troubleshooting - -### What should I do if the deployment failed? - -There could be different reasons that could cause a deployment to fail. Please read the error message to find the specific reason. -If you’re not using the latest version of the CLI, you can try to upgrade the CLI to the latest version and try again. - -### I’m seeing `Error: listen EADDRINUSE :::3000` when starting my project locally. - -`listen EADDRINUSE :::3000` means the default port used by your program, 3000, is already occupied by a different program. You can find and kill the program using the port 3000 by following the instructions below: - -- [For macOS, you can use `lsof` and `kill`](https://stackoverflow.com/questions/3855127/find-and-kill-process-locking-port-3000-on-mac) -- [For Linux, you can use `fuser`](https://stackoverflow.com/questions/11583562/how-to-kill-a-process-running-on-particular-port-in-linux) -- [For Windows, you can use `netstat` and `taskkill`](https://stackoverflow.com/questions/6204003/kill-a-process-by-looking-up-the-port-being-used-by-it-from-a-bat) - -You can also change the default port used by the CLI: - -{`${CLI_BINARY} -p 3002`} - -### How to upload files with the CLI? - - - {`$ ${CLI_BINARY} file upload public/index.html -Uploads /Users/dennis/programming/avos/new_app/public/index.html successfully at: http://ac-7104en0u.qiniudn.com/f9e13e69-10a2-1742-5e5a-8e71de75b9fc.html`} - - -You will get a URL for each file successfully uploaded. That’s the message shown after `successfully at:` in the example above. - -To upload all the files under `images`: - -{`${CLI_BINARY} file upload images/`} - -### How to deploy the same project to multiple applications? - -You can switch to a different application with and deploy the project with . can be used non-interactively with arguments provided: - - - {`${CLI_BINARY} switch --region --group -${CLI_BINARY} deploy --prod`} - - -In the command above, `` is the region of the application, which can be set to `cn-n1` (China North), `cn-e1` (China East), or `us-w1` (International). -`--prod` means to deploy to the production environment. To deploy to the staging environment, use . -With the two commands mentioned above, you can write CI scripts to quickly deploy your project to multiple applications. - -### Can I extend the features provided by the CLI? - -If you have certain operations that need to be performed frequently (like checking the total number of records in `_User`), you can define your own commands. - -Simply create an executable file with its name starting with `lean-` (like `lean-usercount`) and put it into a directory included in `PATH` or `.leancloud/bin` under the project directory. After this, you will be able to run the file with . Compared to running `lean-usercount`, this method gives the program in the file the ability to access the environment variables related to each application. - -For example, you can put the file below into a directory included in `PATH` (like `/usr/local/bin`): - -```python -#! /bin/env python - -import sys - -import leancloud - -app_id = os.environ['LEANCLOUD_APP_ID'] -master_key = os.environ['LEANCLOUD_APP_MASTER_KEY'] - -leancloud.init(app_id, master_key=master_key) -print(leancloud.User.query.count()) -``` - -Grant the file the permission to be executed by running `$ chmod +x /usr/local/bin/lean-usercount`, and then run under the project directory. Now you will see the total number of records in `_User`. - -### What has been changed in the 1.0 version of the CLI? - -In March 2022, we launched the 1.0 version of the CLI. Below are the breaking changes: - -- When using , you have to specify the environment you want to deploy your project to (`--prod` or `--staging`). The older version of the CLI will deploy your project to the production environment if you’re using the trial mode and to the staging environment if you’re using the standard mode. -- won’t fetch the server-side environment variables by default. - You can add the `--fetch-env` option to have those environment variables fetched. -- Deleted . We recommend that you use the most advanced for accessing LeanCache and LeanDB. - -### I’ve installed the older version of the CLI with `npm`. How do I upgrade to the latest version? - -If you’ve installed the older version of the CLI with `npm`, please uninstall the CLI with `npm uninstall -g avoscloud-code leancloud-cli` to avoid any conflicts. You can also follow Homebrew’s instructions to run `brew link --overwrite lean-cli` to override the previous `lean` command. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/_category_.json deleted file mode 100644 index d740043e2..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "数据库", - "collapsed": true, - "position": 6 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/es.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/es.mdx deleted file mode 100644 index 2d10a6cc8..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/es.mdx +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: LeanDB Elasticsearch Guide -sidebar_label: LeanDB Elasticsearch -sidebar_position: 4 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import LeandbCliAccess from "../_partials/leandb-cli-access.mdx"; -import LeandbCreateInstance from "../_partials/leandb-create-instance.mdx"; -import TabItem from "@theme/TabItem"; -import Path from "/src/docComponents/path"; - -LeanDB Elasticsearch is the hosted database provided by Cloud Engine. You can have your project connect to the database using Elasticsearch libraries or HTTP APIs and have access to all the functions provided by Elasticsearch. See [Cloud Engine Overview](/sdk/engine/overview) to learn about the other hosted database services provided by Cloud Engine. - -Elasticsearch has the following features: - -- **High availability**: With clusters containing multiple nodes, errors on a single node won’t affect the availability of the service. -- **Resizing online**: Adjust the size of your instance at any time. -- **Multiple instances**: Get larger storage and better performance as you need. - -## Creating and Managing Instances - -You can create and manage LeanDB Elasticsearch instances on ** > Database > Elasticsearch**. - -### Creating Instances - - - -- **Specification** You can choose from `512M`, `1GB`, `2GB`, `4GB`, and `8GB`. - -Each specification has its limit on the space. Upgrade to a higher specification to allow for more space. - - - -### Elasticsearch Version - -LeanDB only provides Elasticsearch 7.9 at this time. - -### Resizing Online - -At this time, you can’t resize LeanDB Elasticsearch instances on your own. Please reach out to us if you need to have your instances resized. - -### Sharing Instances - -You can use the “Manage sharing” function to share your LeanDB instances with other applications. When you share an instance with another application, the instance will appear in this application. The relevant environment variables will also be available in this application’s Cloud Engine instances. - -## Accessing From Cloud Engine - -When you deploy a project to the Cloud Engine instances under an application, some environment variables containing information for connecting to Elasticsearch will be injected, including: - -- `ELASTICSEARCH_URL_` - -Here `` is the name you provided when creating your LeanDB instance. If the name of your LeanDB instance is `MYES`, there will be an environment variable named `ELASTICSEARCH_URL_MYES`. - -The environment variable has a format like `http://username:password@host:port`, containing everything you need to connect to the instance, including credentials. - - - - -To connect to Elasticsearch from Node.js: - -```javascript -const { Client } = require("@elastic/elasticsearch"); -const client = new Client({ - node: process.env.ELASTICSEARCH_URL_MYES, -}); - -// promise API -const result = await client.search({ - index: "my-index", - body: { - query: { - match: { hello: "world" }, - }, - }, -}); - -// callback API -client.search( - { - index: "my-index", - body: { - query: { - match: { hello: "world" }, - }, - }, - }, - (err, result) => { - if (err) console.log(err); - } -); -``` - -- Make sure to install the dependencies used in the code above with `npm install @elastic/elasticsearch` -- See [Elasticsearch Node.js client’s docs](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html) for more information - - - - -### Custom Dictionary - -You can upload a custom dictionary on the dashboard. -The dictionary should be encoded with UTF-8 with every line containing a word. The file should be no larger than 10 MB. For example: - -``` -Object-oriented programming -Functional programming -Higher-order function -Responsive web design -``` - -Save the content into a text file with its name being something like `dict.txt`. Once you upload the file, it should be effective in 2 minutes. You can test if the dictionary is working with the [analyze API](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/test-analyzer.html). Keep in mind that when using the analyze API, an index has to be specified, so the request looks like `curl -X POST "localhost:9200/my-index/_analyze?pretty"`. - -## Managing Data - -Besides accessing LeanDB with your code from Cloud Engine, you can also use the following ways to manage, debug, or perform operations on your instances. - -### Connecting With the CLI - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/mongo.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/mongo.mdx deleted file mode 100644 index 2c42837ca..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/mongo.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: LeanDB MongoDB Guide -sidebar_label: LeanDB MongoDB -sidebar_position: 3 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import LeandbCliAccess from "../_partials/leandb-cli-access.mdx"; -import LeandbCreateInstance from "../_partials/leandb-create-instance.mdx"; -import TabItem from "@theme/TabItem"; -import Path from "/src/docComponents/path"; - -LeanDB MongoDB is the hosted database provided by Cloud Engine. You can have your project connect to the database using MongoDB libraries and have access to all the functions provided by MongoDB. See [Cloud Engine Overview](/sdk/engine/overview) to learn about the other hosted database services provided by Cloud Engine. - -## Creating and Managing Instances - -You can create and manage LeanDB MongoDB instances on ** > Database > MongoDB**. - -### Creating Instances - - - -- **Specification** You can choose from `512M`, `1GB`, `2GB`, `4GB`, and `8GB`. - -Each specification has its limit on the number of connections and the space. Upgrade to a higher specification to allow for more connections and space. - - - -### MongoDB Version - -LeanDB only provides MongoDB 4.0 at this time. - -### Resizing Online - -At this time, you can’t resize LeanDB MongoDB instances on your own. Please reach out to us if you need to have your instances resized. - -### Sharing Instances - -You can use the “Manage sharing” function to share your LeanDB instances with other applications. When you share an instance with another application, the instance will appear in this application. The relevant environment variables will also be available in this application’s Cloud Engine instances. - -## Accessing From Cloud Engine - -When you deploy a project to the Cloud Engine instances under an application, some environment variables containing information for connecting to MongoDB will be injected, including: - -- `MONGODB_URL_` - -Here `` is the name you provided when creating your LeanDB instance. If the name of your LeanDB instance is `MYDB`, there will be an environment variable named `MONGODB_URL_MYDB`. - - - - -To connect to MongoDB from Node.js (assuming the instance name is `MYDB`): - -```js title='app.js' -const { MongoClient } = require("mongodb"); - -const mongoClient = new MongoClient(process.env["MONGODB_URL_MYDB"], { - useUnifiedTopology: true, - poolSize: 10, -}); - -mongoClient - .connect() - .then(() => { - console.log("Connected to MongoDB"); - }) - .catch((err) => { - console.eror("Connect to MongoDB failed", err.message); - }); - -app.get("/", (req, res) => { - const cats = mongoClient.collection("cats"); - - res.json(cats.find({}, { limit: 10 })); -}); -``` - -- Make sure to install the dependencies used in the code above with `npm install mongodb` -- See [MongoDB Node Driver’s docs](https://www.mongodb.com/docs/drivers/node/current/) for more information - - - - -To connect to MongoDB from .NET (assuming the instance name is `MYDB`): - -```cs -string url = Environment.GetEnvironmentVariable("MONGODB_URL_MYDB"); -MongoClient client = new MongoClient(url); -IMongoCollection collection = client.GetDatabase("leancloud") - .GetCollection("hello"); -FilterDefinition filter = Builders.Filter.Empty; -Console.WriteLine(collection.Find(filter).ToList().ToJson()); -``` - - - - -## Managing Data - -Besides accessing LeanDB with your code from Cloud Engine, you can also use the following ways to manage, debug, or perform operations on your instances. - -### Connecting With the CLI - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/mysql.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/mysql.mdx deleted file mode 100644 index 3f102920e..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/mysql.mdx +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: LeanDB MySQL Guide -sidebar_label: LeanDB MySQL -sidebar_position: 2 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import LeandbCliAccess from "../_partials/leandb-cli-access.mdx"; -import LeandbCreateInstance from "../_partials/leandb-create-instance.mdx"; -import TabItem from "@theme/TabItem"; -import Path from "/src/docComponents/path"; - -LeanDB MySQL is the hosted database provided by Cloud Engine. You can have your project connect to the database using MySQL libraries and have access to all the functions provided by MySQL. See [Cloud Engine Overview](/sdk/engine/overview) to learn about the other hosted database services provided by Cloud Engine. - -## Creating and Managing Instances - -You can create and manage LeanDB MySQL instances on ** > Database > MySQL**. - -### Creating Instances - - - -- **Name** Used to access this instance from Cloud Engine. Should be unique in the application. -- **Memory** You can choose from `0.5GB`, `1GB`, `2GB`, and `4GB`. -- **Storage** You can choose from `20G`, `100G`, and `500G`. - - - -### MySQL Version - -LeanDB only provides MySQL 5.6 at this time. - -### Resizing Online - -At this time, you can’t resize LeanDB MySQL instances on your own. Please reach out to us if you need to have your instances resized. - -### Sharing Instances - -You can use the “Manage sharing” function to share your LeanDB instances with other applications. When you share an instance with another application, the instance will appear in this application. The relevant environment variables will also be available in this application’s Cloud Engine instances. - -## Accessing From Cloud Engine - -When you deploy a project to the Cloud Engine instances under an application, some environment variables containing information for connecting to MySQL will be injected, including: - -- `MYSQL_HOST_` -- `MYSQL_PORT_` -- `MYSQL_ADMIN_USER_` -- `MYSQL_ADMIN_PASSWORD_` - -Here `` is the name you provided when creating your LeanDB instance. If the name of your LeanDB instance is `MYRDB`, there will be an environment variable named `MYSQL_HOST_MYRDB` (together with the other three). - - - - -To connect to MySQL from Node.js: - -```javascript -const mysql = require("mysql"); -const Promise = require("bluebird"); - -const mysqlPool = Promise.promisifyAll( - mysql.createPool({ - host: process.env["MYSQL_HOST_MYRDB"], - port: process.env["MYSQL_PORT_MYRDB"], - user: process.env["MYSQL_ADMIN_USER_MYRDB"], - password: process.env["MYSQL_ADMIN_PASSWORD_MYRDB"], - database: "test", - connectionLimit: 10, - }) -); - -mysqlPool - .queryAsync("SELECT 1 + 1 AS solution") - .then((rows) => { - console.log("The solution is", rows[0].solution); - }) - .catch((err) => { - console.error(err); - }); -``` - -- Make sure to install the dependencies needed by running `npm install --save mysql bluebird` -- See [mysqljs/mysql](https://github.com/mysqljs/mysql) for more information - - - - -To connect to MySQL from Python: - -```python -import os -import mysql.connector - -result = '' - -host = os.environ['MYSQL_HOST_MYRDB'] -port = os.environ['MYSQL_PORT_MYRDB'] -user = os.environ['MYSQL_ADMIN_USER_MYRDB'] -password = os.environ['MYSQL_ADMIN_PASSWORD_MYRDB'] -try: - cnx = mysql.connector.connect( - user=user, password=password, database='test', host=host, port=port) -except mysql.connector.Error as err: - if err.errno == errorcode.ER_ACCESS_DENIED_ERROR: - print("username or password error") - elif err.errno == errorcode.ER_BAD_DB_ERROR: - print("Database does not exist") - else: - print(err) - else: - cursor = cnx.cursor() - cursor.execute('SELECT 1 + 1 AS solution') - for row in cursor: - result = "The solution is {}".format(row[0]) - - cursor.close() - cnx.close() -``` - -- We’re using MySQL’s official Python driver; make sure to list the dependency in `requirements.txt` like this: `mysql-connector-python>=8.0.16,<9.0.0` -- See [MySQL Connector/Python](https://dev.mysql.com/doc/connector-python/en/) for more information. - - - - -To connect to MySQL from PHP: - -```php -try { - $mysqlHost = getenv('MYSQL_HOST_MYRDB'); - $mysqlPort = getenv('MYSQL_PORT_MYRDB'); - $pdo = new PDO("mysql:host=$mysqlHost:$mysqlPort;dbname=test", getenv('MYSQL_ADMIN_USER_MYRDB'), getenv('MYSQL_ADMIN_PASSWORD_MYRDB')); - - foreach($pdo->query('SELECT 1 + 1 AS solution') as $row) { - print "The solution is {$row['solution']}"; - } -} catch (PDOException $e) { - print $e->getMessage(); -} -``` - -- See [PDO’s docs](https://www.php.net/manual/zh/class.pdo.php) for more information - - - - -To connect to MySQL from Java: - -```java -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.Statement; -import java.sql.ResultSet; -import java.sql.SQLException; - -String host = System.getenv("MYSQL_HOST_MYRDB"); -String port = System.getenv("MYSQL_PORT_MYRDB"); -String user = System.getenv("MYSQL_ADMIN_USER_MYRDB"); -String password = System.getenv("MYSQL_ADMIN_PASSWORD_MYRDB"); -try { - Class.forName("com.mysql.jdbc.Driver").newInstance(); -} catch (Exception ex) { - // Handle error -} -try { - Connection connection = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port + "/test?" + - "user=" + user + "&password=" + password); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT 1 + 1 AS solution"); - resultSet.first(); - System.out.format("The solution is %d", resultSet.getInt("solution")); -} catch (SQLException ex) { - // Handle error -} -``` - -- Make sure to add the dependency for MySQL connector to `pom.xml`: - -```xml - - mysql - mysql-connector-java - 8.0.16 - -``` - -- See [MySQL Connector/J](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-usagenotes-connect-drivermanager.html) for more information - - - - -To connect to MySQL from .NET: - -```cs -string host = Environment.GetEnvironmentVariable("MYSQL_HOST_mysql"); -string port = Environment.GetEnvironmentVariable("MYSQL_PORT_mysql"); -string uid = Environment.GetEnvironmentVariable("MYSQL_ADMIN_USER_mysql"); -string password = Environment.GetEnvironmentVariable("MYSQL_ADMIN_PASSWORD_mysql"); -string connectionString = $"server={host};port={port};uid={uid};pwd={password};database=leancloud"; -MySqlConnection conn = new MySqlConnection(connectionString); -conn.Open(); -string sql = "SELECT * FROM hello"; -MySqlCommand command = new MySqlCommand(sql, conn); -MySqlDataReader reader = command.ExecuteReader(); -StringBuilder sb = new StringBuilder(); -while (reader.Read()) { - sb.AppendLine($"{reader[0]} -- {reader[1]}"); -} -Console.WriteLine(sb.ToString()); -``` - -- See [MySQL Connector .NET](https://dev.mysql.com/doc/connector-net/en/) for more information - - - - -## Managing Data - -Besides accessing LeanDB with your code from Cloud Engine, you can also use the following ways to manage, debug, or perform operations on your instances. - -### Admin Panel - -To make it easy for you to develop and debug, we provide a web interface for you to manage your MySQL instance. You can access the web interface by clicking “Admin panel” on the dashboard. - -You can run SQL to query and update your data, create and manage databases, and create and manage indices. - -### Connecting With the CLI - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/redis.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/redis.mdx deleted file mode 100644 index 925bab686..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/database/redis.mdx +++ /dev/null @@ -1,285 +0,0 @@ ---- -title: LeanCache Guide -sidebar_label: LeanCache -sidebar_position: 1 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import LeandbCliAccess from "../_partials/leandb-cli-access.mdx"; -import LeandbCreateInstance from "../_partials/leandb-create-instance.mdx"; -import TabItem from "@theme/TabItem"; -import Path from "/src/docComponents/path"; - -LeanCache is the hosted Redis provided by Cloud Engine. You can have your project connect to the database using Redis libraries and have access to all the functions provided by Redis. See [Cloud Engine Overview](/sdk/engine/overview) to learn about the other hosted database services provided by Cloud Engine. - -Based on [Redis](https://redis.io/), LeanCache provides in-memory key-value storage with high performance and high availability, allowing you to use it as cache or persistent storage for your application. You can upgrade the space of your storage at any time without interrupting the service. - -
    -LeanCache’s use cases - -- 某些数据量少,但是读写比例很高,比如某些应用的菜单可以通过后台调整,所有用户会频繁读取该信息。 -- 需要同步锁或者队列处理,比如秒杀、抢红包等场景。 -- 多个云引擎节点的协同和通信。 - -恰当使用 LeanCache 不仅可以极大地提高应用的服务性能,还能 **降低成本**,因为某些高频率的查询不需要走存储服务(存储服务按调用次数收费)。你可以在 [leanengine-nodejs-demos](https://github.com/leancloud/leanengine-nodejs-demos) 中找到一些有关 LeanCache 的示例: - -- [associated-data](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/associated-data.js) 缓存关联数据 -- [leaderboard](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/leaderboard.js) 实现排行榜 -- [limited-stock-rush](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/limited-stock-rush.js) 实现秒杀抢购 -- [redlock](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/redlock.js) 实现分布式锁 - -
    - -
    -More about LeanCache’s high availability - -每个 LeanCache 实例使用 Redis Master-Slave 主从热备,其下的多个观察节点每隔 1 秒钟观察一次主节点的状态。如果「主节点」最后一次有效响应在 5 秒之前,则该观察节点认为主节点失效。如果超过总数一半的观察节点发现主节点失效,则自动将「从节点」切换为主节点,并会有新的从节点启动重新组成主从热备。这个过程对应用完全透明,不需要修改连接字符串或者重启,整个切换过程应用只有几秒钟会出现访问中断。 - -与此同时,从节点还会以 [AOF 方式](http://www.redis.cn/topics/persistence.html) 将数据持久化存储到可靠的中央文件中,每秒刷新一次。如果很不巧主从节点同时失效,则马上会有新的 Redis 节点启动,并从 AOF 文件恢复,完成后即可再次提供服务,并且会有新的从节点与之构成主从热备。 - -当一个实例中的主节点失效,而最新的数据没有同步到对应的从节点时,主从切换会造成这部分数据丢失。当主、从节点同时失效,未同步到从节点和从节点未刷新到磁盘 AOF 文件中的数据将会丢失。 - -
    - -## Creating and Managing Instances - -You can create and manage LeanCache instances on ** > Database > LeanCache (Redis)**. - -### Creating Instances - - - -- **Name** Used to access this instance from Cloud Engine. Should be unique in the application. -- **Cache eviction strategy** The strategy for deleting data when the memory gets full. Defaults to `volatile-lru`. See [Cache Eviction Strategy](#cache-eviction-strategy) for more information. -- **Instance capacity** You can choose from `128M`, `256M`, `512M`, `1G`, `2G`, `4G`, and `8G`. - - - -### Cache Eviction Strategy - -See Redis’s [official docs](https://redis.io/docs/manual/eviction/#eviction-policies) for more information. Below is a quick summary: - -- `noeviction` Don’t delete any data and return an error when the memory is full. -- `allkeys-lru` Delete the least recently used key first. -- `volatile-lru` Delete the least recently used key that has an expiration date first. -- `allkeys-random` Delete a random key. -- `volatile-random` Delete a random key that has an expiration date. -- `volatile-ttl` Delete the oldest key that has an expiration date. - -### Redis Version - -LeanCache only provides Redis 6 at this time. - -### Resizing Online - -You can resize the maximum memory capacity of a LeanCache instance online. The operation may take a while and LeanCache may stop responding for a few seconds. If your application has a high traffic volume, this may impact your Cloud Engine instances (like increasing their memory usage). You can consider resizing your LeanCache instances when there is a low traffic volume. - -:::caution -Make sure the size of data is smaller than the size you’re changing to, or the operation might cause data loss. -::: - -### Sharing Instances - -You can use the “Manage sharing” function to share your LeanCache instances with other applications. When you share an instance with another application, the instance will appear in this application. The relevant environment variables will also be available in this application’s Cloud Engine instances. - -## Accessing From Cloud Engine - -When you deploy a project to the Cloud Engine instances under an application, some environment variables containing information for connecting to Redis will be injected, including: - -- `REDIS_URL_` - -Here `` is the name you provided when creating your LeanDB instance. If the name of your LeanDB instance is `MYRDB`, there will be an environment variable named `REDIS_URL_MYRDB`. - - - - -Add dependencies to your project: - -```json -"dependencies": { - "ioredis": "^4.9.0" -} -``` - -Get the Redis connection with the code below: (assuming the instance name is `MYCACHE`) - -```js -const Redis = require("ioredis"); - -const client = new Redis(process.env["REDIS_URL_MYCACHE"]); -client.on("error", function (err) { - return console.error("redis err: ", err); -}); -``` - - - - -Add dependencies to the `requirements.txt` of your project: - -``` -Flask>=0.10.1 -leancloud-sdk>=1.0.9 -... -redis -``` - -Get the Redis connection with the code below: (assuming the instance name is `MYCACHE`) - -```python -import os -import redis - -r = redis.from_url(os.environ.get("REDIS_URL_MYCACHE")) -``` - - - - -Add a dependency for Redis, like `predis`; - -```sh -composer require 'predis/predis:1.1.*' -``` - -Get the address for the Redis connection and create a connection with the code below: (assuming the instance name is `MYCACHE`) - -```php -use Predis; -$redis = new Predis\Client(getenv("REDIS_URL_MYCACHE")); -$redis->ping(); -``` - - - - -Add a dependency for Redis client in `pom.xml`: - -```xml - - redis.clients - jedis - 3.2.0 - -``` - -Import the dependency: - -```java -import redis.clients.jedis.Jedis; -``` - -Get the address for the Redis connection and create a Redis client instance. (assuming the instance name is `MYCACHE`) - -```java -String redisUrl = System.getenv("REDIS_URL_MYCACHE"); -Jedis jedis = new Jedis(redisUrl); -jedis.set("foo", "bar"); -String value = jedis.get("foo"); -jedis.close(); -``` - -Consider using a connection pool when there is high traffic: - -```java -public class RedisHelper { - private final JedisPool jedisPool; - public RedisHelper() { - // Create a connection pool first when creating the application; use the default configuration - jedisPool = new JedisPool(System.getenv("REDIS_URL_jedis_128m")); - } - - // Get a jedis connection from the pool - // Make sure to return the connection by calling the `close` method of the jedis object returned: `jedis.close()` - public Jedis getJedis() { - Jedis jedis = jedisPool.getResource(); - return jedis; - } - - public void closePool() { - // Close the pool when closing the application - jedisPool.close(); - } -} -``` - - - - -We will use the method mentioned in [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/). - -Assuming there’s a LeanCache instance named `dev`, the code below will show how you can have your application connect to this instance, store data, and read data: - -```cs -string host = Environment.GetEnvironmentVariable("REDIS_HOST_dev"); -string port = Environment.GetEnvironmentVariable("REDIS_PORT_dev"); -string user = Environment.GetEnvironmentVariable("REDIS_USER_dev"); -string password = Environment.GetEnvironmentVariable("REDIS_PASSWORD_dev"); -ConfigurationOptions config = new ConfigurationOptions { - EndPoints = { - { host, int.Parse(port) } - }, - User = user, - Password = password -}; -ConnectionMultiplexer conn = ConnectionMultiplexer.Connect(config); -IDatabase db = conn.GetDatabase(); -db.StringSet("foo", "bar"); -var bar = db.StringGet("foo"); -``` - -See [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/) for more information about the usage of `ConnectionMultiplexer`. This is a recommended library for .NET Core. - - - - -## Managing Data - -Besides accessing LeanDB with your code from Cloud Engine, you can also use the following ways to manage, debug, or perform operations on your instances. - -### Connecting With the CLI - - - -## FAQ - -### How to locally debug a project that depends on LeanCache? - -You can connect to LeanCache [using the CLI](#connecting-with-the-cli) or install Redis locally: - -- For Mac, install Redis by running `brew install redis`, then start the service using `redis-server` -- For Debian/Ubuntu, run `apt-get install redis-server` -- For CentOS/RHEL, run `yum install redis` -- There’s no official support for Windows yet, but you can download the installer for [Microsoft’s distribution](https://github.com/microsoftarchive/redis/releases). - -By default, environment variables relevant to LeanCache are not available when you run your project locally. The local Redis server’s address will be used. - -```js -// process.env['REDIS_URL_'] will be `undefined` when running locally; the default address, 127.0.0.1:6379, will be connected -const client = new Redis(process.env["REDIS_URL_MYCACHE"]); // Assuming the instance name is `MYCACHE` -``` - -If you see an error like `redis err: Error: Redis connection to 127.0.0.1:6379 failed - connect ECONNREFUSED 127.0.0.1:6379`, please check if the value in the `REDIS_URL_` in the example above is replaced correctly. Please also take a look at the examples under [Accessing From Cloud Engine (Node.js runtime environment)](#accessing-from-cloud-engine). - -See [Redis’s docs](https://redis.io/docs/) for more information about how to use Redis. - -### Compared to a self-built HashTable, what’s the benefit of LeanCache? - -与自己在程序的全局作用域中维护一个 HashTable 相比,使用 LeanCache 的优势在于: - -- **多实例之间的数据共享**:云引擎支持多实例运行,自行维护的 HashTable 数据无法[跨实例共享](#管理共享)。 -- **数据持久化存储**:在程序重启或重新部署后数据不会丢失,Redis 会帮你完成数据持久化的工作。LeanCache 还会为你的 Redis 做热备,具有非常高的可靠性。 -- **原子操作和性能**:Redis 提供了常见的数据结构和大量原子操作,其文档中列出了每个操作符的时间复杂度,而自行实现的 HashTable 的性能则很大程度依赖于具体语言的实现。 - -### I’m seeing the error `Redis connection gone from end event` - -LeanCache 或者任何网络程序都有可能出现连接闪断的问题,可能是因为网络波动,或是服务器负载、容量调整等等。这时只需要重建连接即可使用。而 Redis Client 一般都有断开重连的机制,未连接期间指令会保存到队列,待连接成功后再发送队列中的指令([Redis client library](https://www.npmjs.com/package/redis) 便是如此实现)。所以如果这个错误偶尔发生,一般不会有什么问题;同时建议在应用中 [增加 Redis 的 on error 事件处理](#在云引擎中使用)。 - -如果这个错误**频繁出现**,那么很可能 LeanCache 节点处于非受控状态,请提交工单联系技术支持进行处理。 - -### Using Multiple Instances - -有些时候,你可能希望在一个应用里创建多个 LeanCache 实例: - -- **需要存储的数据大于 8 GB**:目前我们提供的实例最大容量为 8 GB。如果有大于此容量的数据,建议你创建多个实例,然后根据功能来划分,比如一个用来做持久化,另一个用来做缓存。 -- **需要更高的性能**:如果单实例的性能已经成为应用的瓶颈,你可以创建多个实例,然后在云引擎中同时连接,并自己决定 key 的分片策略,使请求分散到不同的实例来获得更高的性能。 diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/dedicated-IP.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/dedicated-IP.mdx deleted file mode 100644 index 8f2b50eae..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/dedicated-IP.mdx +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: 云引擎独立 IP -sidebar_label: 独立 IP -sidebar_position: 9 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -请参考 [域名绑定指南的云引擎域名章节](/sdk/domain/guide/#云引擎域名) 了解云引擎独立 IP 的优势。 - -前往 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置****云服务控制台 > 账号设置 > 独立 IP** 可以查看、管理当前账号使用的云引擎独立 IP。 - -申请独立 IP 后,在新增域名绑定时,请按提示到自有域名的域名服务商处添加 A 记录,指向独立 IP。 已经在开发者中心绑定的自定义域名,只需在域名服务商处修改解析记录,将 CNAME 记录替换为 A 记录即可切换。 - -解绑 IP 前,请确保所有指向该 IP 的自定义域名均已修改解析记录,将相应 A 记录替换回 CNAME 记录。 由于 DNS 在整个互联网生效需要较长时间,修改解析记录后,请**至少等待 48 小时再解绑 IP**,以免影响服务。 - -独立 IP 是账户下所有应用共享的。如果你希望进一步隔离各应用的入口,也可以购买更多独立 IP,你需要自行规划哪些应用使用哪些 IP。 - -每一个独立 IP 默认提供了 2 Gbps 的防护带宽,可以防护小规模的攻击,你也无需为此承担额外的费用。 如果攻击超出了默认的防护容量, IP 会被禁用,你可以购买一个新的 IP 进行更换。 或者你也可以将新的 IP 作为源站 IP 接入第三方清洗服务,请通过工单联系我们,我们会提供必要的支持。 - -独立 IP 的费用为人民币 50 元 / 个 / 月,购买每个 IP 时会扣除一个月的费用(50 元),从下个自然月起,每月 1 日按 50 元 / 个的标准进行扣费。 - -独立 IP 与账户绑定,不会随应用的转移而发生变化。为了避免影响服务,转移的应用如果不改变自定义域名的解析配置,那么还可以继续使用原来的独立 IP。 - -如果你有多个独立 IP,在通过修改域名解析切换独立 IP 前,可能希望验证通过新 IP 可以成功访问服务。 这种场景下可以使用 curl 的 `--resolve` 参数指定域名解析到特定的 IP(效果类似修改 `/etc/hosts` 文件): - -```sh -curl --resolve 'engine.example.com:443:YOUR-ENGINE-IP' https://engine.example.com/ -``` diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deep-dive/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deep-dive/_category_.json deleted file mode 100644 index 67ca88829..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deep-dive/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "深入了解", - "collapsed": true, - "position": 7 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deep-dive/index.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deep-dive/index.mdx deleted file mode 100644 index 25484a222..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deep-dive/index.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Deep Dive Into Cloud Engine ---- - -import Mermaid from "/src/docComponents/Mermaid"; - -:::note -This article is intended to introduce the technical specifications of Cloud Engine to experienced developers. For instructions on how to get started with Cloud Engine, see [Getting Started With Cloud Engine](/sdk/engine/deploy/getting-started). -::: - -## What kinds of applications does Cloud Engine work well with? - -Cloud Engine provides container-based process-level runtime environments for your application. When using Cloud Engine, you can focus on your application’s process without caring about the environment provided by the operating system. - -With Cloud Engine, your application’s process can focus on the business logic of your project. Your application can interact with Cloud Engine through standardized interfaces: - -- You can host your project’s code with Git and have Cloud Engine fetch the code from the corresponding repository -- You can describe the environment required by your application with a dependency list (like `package.json`) and have Cloud Engine automatically prepare the environment for your application -- You can manage environment variables with Cloud Engine and have your application load environment variables from Cloud Engine -- Your application is expected to contain no data or states and is expected to access data through network requests; Cloud Engine provides hosted databases and cache services for your application -- Your application can be built on Cloud Engine; Cloud Engine provides customizable build mechanisms for your application -- Your application can run in multiple processes -- Your application should be complete and runnable without containing a host environment; your application is expected to provide HTTP services through the network -- Your application can simply print logs to the standard output and Cloud Engine will collect these logs for you to review - -:::note -The design of Cloud Engine is greatly inspired by [The Twelve-Factor App](https://12factor.net/). You can learn more about the methodologies for building modernized, migratable, extensible, and maintainable backend services from its website. -::: - -## Build - -Cloud Engine can be used to build projects in different languages (runtime environments). When you deploy a project to Cloud Engine, Cloud Engine will be able to tell the type of the project according to the project’s code. For example, if the project contains `package.json`, it will be considered a Node.js project. - -Once you upload your project’s code to Cloud Engine with Git or the CLI, Cloud Engine will start a _build process_ during which the dependencies of your project will be installed (`npm install`), executable files will be created (`go build`), and your custom commands will be executed (can be configured in [leanengine.yaml](/sdk/engine/deep-dive/leanengine-yaml)). - -|install| +Source["Full code\\n(package.json + *.ts)"] - +Source -->|build| Version["Version\\n(bundled.js)"] - Version -->|run| Instances1([Instance\\nnode ./bundled.js]) - Version -->|run| Instances2([Instance\\nnode ./bundled.js]) -`} -/> - -A _version_ will be created by the end of the build process. It contains the source code of your project, the dependencies downloaded, and the executable files generated (for certain runtime environments). Each version has an ID like `20210913-150821`, which is unique in the application. - -### Build Cache - -Cloud Engine uses layered cache to accelerate the build process. This means that if the operation needed for a step is not changed, the operation will not be executed and the result (cache) from the previous execution will be used. If the operation is changed, all the steps since this step will be executed again. For example, if the dependency list (like `package.json`) is not changed, Cloud Engine will not reinstall dependencies for your project but will use the result from the previous installation, which reduces the time spent on the build process. - -:::caution -The caching mechanism for the build process doesn’t guarantee that the cache will be used if the dependency list of your project doesn’t change. -::: - -:::caution -If you specify a scope for the versions of the dependencies of your application and you use the same dependency list when you deploy your application for a second time, the dependencies installed for the previous version will be reused even if there are new versions released for the dependencies (and these versions are still within the scope). -To force Cloud Engine to install the latest versions of the dependencies within the scope, you can disable the cache with the `--no-cache` option. -::: - -### Resource Limits - -The **build process** is provided with following resources: - -- **2 CPU cores**: CPU utilization is up to 200% -- **30 minutes of CPU time**: the build command will be terminated when used too much CPU time. -- **4 GB memory**: if memory consumption reaches the limit, you may see errors like "Out of memory" or "memory allocation error". In this case, try to change your build command to use less memory, like limiting the number of compiler processes running concurrently. - -## Instances and Deployment - -A version can be deployed to _instances_. Each instance corresponds to a container (process) on the server, which provides the computing power for your application and allows your application to provide services to users. - -### Zero Downtime Deployment - -If you’re using the standard version of Cloud Engine, when you deploy your project, restart your instances, or when we’re migrating instances, new containers will be started first. With new containers running normally, each old container will still keep running for at least 30 seconds to fulfill the ongoing requests. With this mechanism, when you deploy new versions or restart your instances, there probably won’t be requests that would fail, nor will the ability of your application to handle requests get impaired. - -Your application can listen to the `SIGTERM` signal to perform cleanups before it exits. By default, your application has 10 seconds to perform cleanups once it receives a `SIGTERM` signal. - -### Hibernation - -A trial instance (including the free one in the production environment or the staging environment) will hibernate if it receives no requests within a while. While hibernating, the instance will restart if it receives a request. It may take 10 to 20 seconds for an instance to start from hibernation. If a trial instance has run for more than 18 hours within the past 24 hours, it will be forced to hibernate and won’t restart even if there are requests coming in. - -### Automatic Instance Migration - -Sometimes the system will automatically trigger instance migrations even if you don’t start a deployment or restart your instances. When this happens, the zero-downtime-deployment mechanism will also be used. You may see logs saying that instances have been restarted, but there’s no need to worry about it. - -## Cloud Functions - -Cloud Functions lets you run backend code on the cloud in response to various types of events. The core functionality of Cloud Functions is provided by the Cloud Engine SDK, which shares the same process and HTTP port as your own project. - -Hooks are implemented in a similar manner as Cloud Functions. The difference is that hooks use a special set of names and can only be invoked by the Data Storage service or the Instant Messaging service through our private network. The Cloud Engine SDK will check the IP address of the request sender to ensure that it comes from our private network. - -The Cloud Functions and hooks in the different groups of the same application share the same namespace. This means that different Cloud Functions can be provided by different groups and built with different languages. When you deploy your application to Cloud Engine, Cloud Engine will communicate with the SDK to get a list of Cloud Functions and know which Cloud Functions belong to which groups. Our load balancer will then be able to redirect requests to the correct groups. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deep-dive/leanengine-yaml.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deep-dive/leanengine-yaml.mdx deleted file mode 100644 index c60baea00..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deep-dive/leanengine-yaml.mdx +++ /dev/null @@ -1,127 +0,0 @@ ---- -title: 'Reference: leanengine.yaml' -sidebar_label: 'leanengine.yaml' ---- - -import { Conditional } from "/src/docComponents/conditional"; - -`leanengine.yaml` contains configurations for the server-side runtime environment. It uses YAML as syntax and should be placed in the root directory of the project. - -## Overriding Runtime Environment With `runtime` - -You can override the environment that Cloud Engine automatically detects. You can use the following values: - -- `cpp` -- `dotnet` -- `go` -- `java` -- `nodejs` -- `php` -- `python` -- `static` (Frontend) - -## Overriding Command for Execution With `run` - -```yaml -run: $(npm bin)/serve -c static.json -l ${LEANCLOUD_APP_PORT} -``` - -You can use shell syntax here for purposes like accessing environment variables. - -## Overriding Command for Installing Dependencies With `install` - -You can override the default command for installing dependencies (like `npm install`) or run additional commands before or after installing dependencies. You can specify multiple commands with an array, use shell syntax, and access environment variables. - -Most runtimes have their default commands for installing dependencies. You can refer to the default command with `use: default`: - -```yaml -install: - - use: default - - npm run install-additional -``` - -When installing dependencies, only the dependency lists (like `package.json`) will be loaded into the build directory. To load additional files, use `require`: - -```yaml -install: - - require: - - frontend/package.json - - frontend/package-lock.json - - cd frontend && npm ci -``` - -## Overriding Command for Building With `build` - -```yaml -build: NODE_ENV=production $(npm bin)/webpack -``` - -As with `install`, you can import the default command with `use: default`. You can specify multiple pieces of commands with an array. You can also use shell syntax and access environment variables. - -When building your project, all the files in your project will be loaded into the build directory. - -## Specifying System Dependencies With `systemDependencies` - -To install additional system dependencies during deployment: - -```yaml -systemDependencies: - - imagemagick -``` - -- `ffmpeg` A library for processing audio and videos. -- `imagemagick` A library for processing images. -- `fonts-wqy` 文泉驿点阵宋体 and 文泉驿微米黑. Often used with `chrome-headless` to display Chinese. -- `fonts-noto` Source Han Sans (comes with a large size). Often used with `chrome-headless` to display Chinese. -- ~~`phantomjs` A headless browser based on WebKit~~ (discontinued). -- `chrome-headless` Headless Chrome. It comes with a large size and will significantly increase the time needed for deployment. It also consumes a lot of CPU and RAM resources. If you need to use `puppeteer`, please provide `{executablePath: '/usr/bin/google-chrome', args: ['--no-sandbox', '--disable-setuid-sandbox']}` to `puppeteer.launch`. -- `node-canvas` The system dependency required for installing [node-canvas](https://github.com/Automattic/node-canvas) (you still need to install `node-canvas` yourself). -- `python-talib` The system dependency required for installing [TA-Lib](https://pypi.org/project/TA-Lib/) (you still need to install `TA-Lib` yourself). - -:::caution -Adding system dependencies will significantly increase the time needed for deployment. Please avoid adding dependencies that your project doesn’t need. -::: - -## `buildRoot` 构建根目录 - -指定代码包或仓库中的一个子目录进行构建,相当于只上传了这一个子目录的代码。 - -## `exposeEnvironmentsOnBuild` 在构建阶段使用环境变量 - -默认情况下,应用在运行阶段才能够读取到内置环境变量和自定义环境变量,设置该选项可以在安装依赖或编译阶段读取到这些环境变量。 - -```yaml -exposeEnvironmentsOnBuild: true -``` - -云引擎运行环境默认提供的环境变量(以及 Node.js 环境变量 NODE_ENV)无法被自定义环境变量覆盖。 - -## `startupTimeout` 启动超时 - -```yaml -startupTimeout: 60 -``` - -配置程序启动的超时时间,可设置 15 - 120 的值(秒) - -## `functionsMode` 云函数功能开关 - -控制项目是否使用云函数(Hook)特性。 - -```yaml -functionsMode: strict -``` - -设置为 `strict` 表示需要使用云函数特性,如获取云函数信息失败则中断部署;设置为 `disabled` 表示不开启云函数相关功能。 - -## ~~Node.js `installDevDependencies` 安装开发依赖~~ - -:::caution -已废弃,请使用 `package-lock.json`。 -::: - -```yaml -installDevDependencies: true -``` - -安装 `package.json` 中 `devDependencies` 部分的依赖。 diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/_category_.json deleted file mode 100644 index 41ef17570..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "部署应用", - "collapsed": true, - "position": 3 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/cpp.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/cpp.mdx deleted file mode 100644 index fae0e9548..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/cpp.mdx +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: C++ Runtime Environment -sidebar_label: C++ -sidebar_position: 10 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; - -:::info -This article serves as a thorough introduction to Cloud Engine’s C++ runtime environment. To quickly get started with Cloud Engine, see [Getting Started With Cloud Engine](/sdk/engine/deploy/getting-started). -::: - -Cloud Engine can be used to build projects using Bazel or Makefile (CMake). - -The C++ runtime environment provides GCC 9.4 as a compiler. - -## Bazel Projects - -If `WORKSPACE` exists in the project root, Cloud Engine will build your project with `bazel build -c opt //:all` and run your project with `bazel run -c opt //:all`. - -## Makefile (CMake) Projects - -If `Makefile` exists in the project root, Cloud Engine will build your project with `make`. - -If `CMakeLists.txt` exists in the project root, Cloud Engine will first create a Makefile with `cmake .`. - -Cloud Engine doesn’t have a default command for running Makefile projects. You need to specify it in `leanengine.yaml`: - -```yaml title='leanengine.yaml' -run: ./myapp -``` - -## Uploading Compiled Programs - -You can also compile your programs on your own computer and upload them to Cloud Engine. We recommend that you compile your programs using static linking on Ubuntu 20.04. - -Make sure you specify the command to run your program in `leanengine.yaml`: - -```yaml title='leanengine.yaml' -runtime: cpp -run: ./myapp -``` - -:::info -There may be changes to the Cloud Engine build environment in the future. For example, the Linux distribution used by Cloud Engine might be updated. When this happens, you may need to adjust compilation options to ensure that your programs compile without errors. Versions that have already been built on Cloud Engine will not have their build environments changed, so you can continue to use them or revert back to them at any time. -::: - -## Customize Build Process - - - -### Build Logs - - - -## Health Check - - - -## Cloud Environment - -### Custom Domains - - - -### Load Balancer and CDN - - - -### Environment Variables - - - -### Logs - - - -### Timezone - - - -### File System - - - -### IP Addresses - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/dotnet.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/dotnet.mdx deleted file mode 100644 index 7b938dbbc..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/dotnet.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: .NET Runtime Environment -sidebar_label: .NET -sidebar_position: 8 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; - -:::info -This article serves as a thorough introduction to Cloud Engine’s .NET runtime environment. To quickly get started with Cloud Engine, see [Getting Started With Cloud Engine](/sdk/engine/deploy/getting-started). -::: - -A .NET project has to have an `app.sln` under its root directory for Cloud Engine to correctly identify it as a .NET project. The structure of a .NET project often looks like this: - -``` -├── web -| ├── StartUp.cs -| ├── Program.cs -| ├── web.csproj -| └── wwwroot -| ├── css -| ├── lib -| └── js -├── app.sln -└── global.json -``` - -If you are looking to start a new project, we recommend that you use our [.NET sample project](https://github.com/leancloud/dotnet-core-getting-started) as a boilerplate. - -## Startup Command - -Once the build process is complete, Cloud Engine will start your application with `dotnet release/web.dll`. - -## .NET Version - -At this time, Cloud Engine only provides .NET 3.1.100 for running your project. - -## Install Dependencies and Build - -Cloud Engine will install dependencies with `dotnet restore app.sln` and build your project with `dotnet publish -o release -c Release`. - -## Customize Build Process - - - -### Build Logs - - - -## Health Check - - - -## Cloud Environment - -### Custom Domains - - - -### Load Balancer and CDN - - - -### Environment Variables - - - -### Logs - - - -### Timezone - - - -### File System - - - -### IP Addresses - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/getting-started.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/getting-started.mdx deleted file mode 100644 index 96d0fc554..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/getting-started.mdx +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: Getting Started With Cloud Engine -sidebar_label: Getting Started -sidebar_position: 1 ---- - -import QuickStartNew from "../_partials/quick-start-new.mdx"; -import QuickStartDeploy from "../_partials/quick-start-deploy.mdx"; -import PlatformIntroduction from "../_partials/platform-introduction.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import PlatformRuntimes from "../_partials/platform-runtimes.mdx"; -import { CLI_BINARY } from "/src/constants/env.ts"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; - -:::info -If you are planning to use Cloud Functions and Hooks rather than deploy a general-purpose backend program, see [Getting Started With Cloud Functions and Hooks](/sdk/engine/functions/getting-started). - -To deploy a frontend app, see [Frontend Runtime Environment § Getting Started](/sdk/engine/deploy/webapp/#getting-started). -::: - - - -## Create a Project - -The easiest way to start a new project is to use our boilerplate. - - - - -## Binding a Project - -

    - To associate an existing project to a Cloud Engine application, you can use the {CLI_BINARY} switch: -

    - - - {`$ ${CLI_BINARY} switch -[?] Please select an app: - 1) my-engine-app - => 1 -Switching to my-engine-app (group: web)`} - - - -## Run and Debug Locally - -You can define routes with the web framework for the language of your choice so the program can handle requests sent to the paths specified. Feel free to look into the examples within the boilerplate: - - - - -```javascript title='app.js' -app.get("/", function (req, res) { - res.render("index", { currentTime: new Date() }); -}); -``` - - - - -```python title='app.py' -@app.route('/') -def index(): - return render_template('index.html') -``` - - - - -```php title='src/app.php' -$app->get('/', function (Request $request, Response $response) { - return $this->view->render($response, "index.phtml", array( - "currentTime" => new \DateTime(), - )); -}); -``` - - - - -```java title='src/main/webapp/WEB-INF/web.xml' - - index.html - -``` - - - - -```cs title='web/Startup.cs' -app.UseEndpoints(endpoints => { - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); -}) -``` - - - - -```go title='main.go' -e.GET("/", routes.Index) -``` - -```go title='routes/index.go' -func Index(c echo.Context) error { - return c.Render(http.StatusOK, "index", time.Now().String()) -} -``` - - - - -With all the dependencies correctly installed, run the project locally by running the CLI under the project’s root directory: - -{`$ ${CLI_BINARY} up`} - -See [Cloud Engine CLI Guide](/sdk/engine/cli/) for more information on the CLI as well as debugging your project locally. - -## Deploy to Cloud Engine - - - - - -For example, if you have bound the domain `web.example.com`, you will be able to access your app on `https://web.example.com` (production environment). - -## More - -Continue reading [Cloud Engine Platform Features](/sdk/engine/platform) to learn about the additional features provided by Cloud Engine. You may also skip to the dedicated pages for specific runtime environments: - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/go.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/go.mdx deleted file mode 100644 index 2b94056ef..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/go.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Go Runtime Environment -sidebar_label: Go -sidebar_position: 9 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; - -:::info -This article serves as a thorough introduction to Cloud Engine’s Go runtime environment. To quickly get started with Cloud Engine, see [Getting Started With Cloud Engine](/sdk/engine/deploy/getting-started). -::: - -At this time, Cloud Engine can only manage dependencies with Go Modules. - -A Go project has to have a `go.mod` under its root directory for Cloud Engine to correctly identify it as a Go project. - -## Commands for Building and Running - -By default, Cloud Engine builds your project with `go build -o main` and runs the compiled executable (`main`). You can customize the commands for building and running your project with `leanengine.yaml`: - -```yaml title='leanengine.yaml' -build: go build -o myapp -run: ./myapp -``` - -## Configure Go Version - -Cloud Engine will use the Go version specified in `go.mod`: - -```plain title='go.mod' -go 1.14 -``` - -:::note -If you don’t specify a Go version, the latest stable version will be used. -::: - -## Install Dependencies (`go.mod`) - -Cloud Engine will automatically install the dependencies listed in `go.mod` and `go.sum`. - -## Customize Build Process - - - -### Build Logs - - - -## Health Check - - - -## Cloud Environment - -### Custom Domains - - - -### Load Balancer and CDN - - - -### Environment Variables - - - -### Logs - - - -### Timezone - - - -### File System - - - -### IP Addresses - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/java.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/java.mdx deleted file mode 100644 index d94b3f0b5..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/java.mdx +++ /dev/null @@ -1,236 +0,0 @@ ---- -title: Java Runtime Environment -sidebar_label: Java -sidebar_position: 6 ---- - -import { CLI_BINARY } from "/src/constants/env.ts"; -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; -import CodeBlock from "@theme/CodeBlock"; -import { Conditional } from "/src/docComponents/conditional"; - -:::info -This article serves as a thorough introduction to Cloud Engine’s Java runtime environment. To quickly get started with Cloud Engine, see [Getting Started With Cloud Engine](/sdk/engine/deploy/getting-started). -::: - -You can deploy WAR and JAR projects built with Maven to Cloud Engine, or upload WAR files directly to Cloud Engine. - -If you are looking to start a new project, we recommend that you use one of our sample projects as a boilerplate: - -- [Servlet sample project](https://github.com/leancloud/servlet-getting-started) -- [Spring Boot sample project](https://github.com/leancloud/spring-boot-getting-started) - -:::caution -Running a Java program usually costs a large amount of memory. If you are using a trial instance with 256M of memory, Cloud Engine might encounter an OOM error when starting your Java program, causing your deployment to fail. Even if Cloud Engine is able to start your program, the program might frequently get restarted due to low memory. - -We recommend that you pick an instance with at least 512 MB of memory. If your project is built with Spring Boot, a minimum of 1024 MB of memory is recommended. You can adjust the memory size at any time even if you've already deployed your project. See [Cloud Engine Platform Features § Adjusting Quota and Number of Instances](/sdk/engine/platform/#adjusting-quota-and-number-of-instances) for more information on how to adjust the memory size of your instances. -::: - -## Startup Command - -Once the build is finished, Cloud Engine will look for a `.war` or `.jar` file under `target`: - -- If it finds a `.war` file, it will place the file into a Servlet container (Jetty 9.x). -- If it finds a `.jar` file, it will run the file with `java -jar`. - -### Configure JVM Parameters - -When Cloud Engine runs a Java application, it will give `-Xmx` a value equivalent to 70% of the size of the instance. The remaining 30% will be reserved for off-heap memory and other consumptions. You may customize the value of `-Xmx` if your application has special needs (like using a large amount of off-heap memory). For example, if your instance has a memory of 2 GB, you can add a _custom environment variable_ on Cloud Engine’s _Settings_ page with `JAVA_OPTS` as its name and `-Xmx1500m` as its value. This will limit the size of the JVM heap to 1.5 GB and leave 500 MB for PermGen, off-heap memory, and other stuff. **Keep in mind that if the value of `-Xmx` is too small, a large amount of CPU might be wasted on repetitive garbage collection tasks.** - -## Configure Java Version - -To specify the Java version you want to use, create a file named `system.properties` under the root directory of your project: - -```plain title='system.properties' -java.runtime.version=11 -``` - -At this time, Cloud Engine supports AdoptOpenJDK `8`, `11`, `12`, `13`, and `14`. - -:::note - -For newly created apps, if -If you don’t specify a Java version, the -latest stable (LTS) version will be used. - - {" "} - For apps created before 9/2/2021, Java `8` will be used by default to ensure compatibility. - -::: - -## Upload WAR File - -If you’ve already built the WAR file on your local computer (with commands like `mvn package`), you can add the `--war` option when you deploy your project with the CLI. This tells the CLI to upload the WAR file rather than the source code. - -{`${CLI_BINARY} deploy --war`} - -In this case, Cloud Engine won’t install dependencies or build your project on the server side. Instead, it will run the WAR file by placing it into a Servlet container. - -## Install Dependencies and Build - -If you’ve uploaded the source code, Cloud Engine will install the dependencies listed in `pom.xml` with Maven and build your project by running `mvn package`. - -## Customize Build Process - - - -#### Upload Jar Without Building on the Server - -```yaml title='leanengine.yaml' -runtime: java -install: [] -build: [] -run: java -jar your-package.jar -``` - -Here we have specified that we are using the Java Runtime Environment. We have skipped the dependency installation and build steps by leaving them empty. We also specified the run command (run `your-package.jar` in the root directory). - - - -### Build Logs - - - -## Health Check - - - -## Cloud Environment - -### Custom Domains - - - -### Load Balancer and CDN - - - -### Environment Variables - - - -### Logs - - - -### Timezone - - - -### File System - - - -### IP Addresses - - - -## Troubleshooting - -### Can I run my Cloud Engine Java project locally without using the CLI? - -Once you have configured the environment variables required for running your project on Cloud Engine, you can run your Java project locally with certain commands or using an IDE without using the CLI. - -To run a Jetty or JAR project from the command line, first configure the environment variables: - -```sh -eval "$(lean env)" -``` - -Notice that `lean env` outputs the commands used to configure the environment variables required for the current application. You can use `eval` to execute these commands. -If you’re using Windows, you need to manually configure the environment variables returned by `lean env`. - -If you have a Jetty project, run the following command: - -``` -mvn jetty:run -``` - -If you have a JAR project, package it with Maven and run the following command: - -```sh -mvn package -java -jar target/{zipped jar file} -``` - -To run the application with Eclipse: - -Please first make sure that you have installed the Maven plugin with Eclipse, then import the project into Eclipse as a **Maven Project**. - -After this, right-click on the project under the **Package Explorer** view: - -- For a Jetty project, select **Run As** > **Maven build…** and set **Goals** under the **Main** tab to be `jetty:run`. -- For a JAR project, select **Run As** > **Run Configurations…**, select `Application`, and set `Main class:` (`cn.leancloud.demo.todo.Application` for the sample project). - -Then add the following environment variables under the **Environment** tab: - -| Name | Value | -| -------------------------- | --------------- | -| `LEANCLOUD_APP_ENV` | `development` | -| `LEANCLOUD_APP_ID` | `{{appid}}` | -| `LEANCLOUD_APP_KEY` | `{{appkey}}` | -| `LEANCLOUD_APP_MASTER_KEY` | `{{masterkey}}` | -| `LEANCLOUD_APP_PORT` | `3000` | - -Once you’ve finished the configuration above, you will be able to run your project by hitting _run_. - -### If my project depends on private libraries, can I use them on Cloud Engine? - -You can follow the instructions below to use private libraries on Cloud Engine: - -1. Create a directory named `libs` under the root directory of your project, then copy all the jar files your project depends on into it. -2. Create a file named `leanengine.yaml` under the root directory of your project, then customize the `install` stage (see the example below). -3. Edit the dependencies in `pom.xml` and add `includeSystemScope` into `spring-boot-maven-plugin` in the same file (see the example below). - -Your project structure will look like this: - -``` -{root} -|---libs -| |- yourdependency.jar etc. -|---leanengine.yaml -\---pom.xml -``` - -`leanengine.yaml` will look like this: - -```yaml -install: - - require: - - libs - - { use: "default" } -``` - -To add dependencies into `pom.xml`: - -```xml - - com.sample - sample - 1.0 - system - ${project.basedir}/libs/yourdependency.jar - -``` - -Edit `spring-boot-maven-plugin` in `pom.xml` so it looks like this: - -```xml - - org.springframework.boot - spring-boot-maven-plugin - - true - true - - -``` diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/nodejs.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/nodejs.mdx deleted file mode 100644 index f4516b59a..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/nodejs.mdx +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: Node.js Runtime Environment -sidebar_label: Node.js -sidebar_position: 4 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import NodejsSetupRuntime from "../_partials/nodejs-setup-runtime.mdx"; -import NodejsSetupPackageManager from "../_partials/nodejs-setup-package-manager.mdx"; -import NodejsSetupDependencies from "../_partials/nodejs-setup-dependencies.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; - -:::info -This article serves as a thorough introduction to Cloud Engine’s Node.js runtime environment. To quickly get started with Cloud Engine, see [Getting Started With Cloud Engine](/sdk/engine/deploy/getting-started). -::: - -A Node.js project has to have a [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json) under its root directory for Cloud Engine to correctly identify it as a Node.js project. This file also helps Cloud Engine understand the project’s requirements for the environment: - -```json title='package.json' -{ - "name": "node-js-getting-started", - "scripts": { - "start": "node server.js", - "dev": "nodemon server.js -- " - }, - "dependencies": { - "leancloud-storage": "^3.11.0", - "leanengine": "^3.3.2" - }, - "devDependencies": { - "nodemon": "^1.18.7" - }, - "engines": { - "node": "16.x" - } -} -``` - -If you are looking to start a new project, we recommend that you use our [Node.js sample project](https://github.com/leancloud/node-js-getting-started) as a boilerplate. - -## Startup Command - -By default, Cloud Engine starts your project with `npm start`. You can change the entry point of your program or add additional options to `node` by editing `scripts.start` in `package.json`. - -```json title='package.json' -{ - "scripts": { - "start": "node server.js" - } -} -``` - -:::note -When debugging locally by running `lean up` with the [CLI](/sdk/engine/cli), the CLI will start your project with `npm run dev`. -::: - -## Configure Node.js Version - - - -## Configure Package Manager - - - -## Install Dependencies (`package.json`) - - - -## Customize Build Process - - - -#### Install Dependencies for Sub-Projects - -```yaml title='leanengine.yaml' -install: - - use: default - - require: - - ./frontend/package.json - - ./frontend/package-lock.json - - cd frontend && npm ci -build: - - npm run build - - cd frontend && run build -``` - -Here we keep the default behavior and install dependencies and run build commands for the `frontend` directory. - - - -### Build Logs - - - -## Health Check - - - -## Cloud Environment - -### Custom Domains - - - -### Load Balancer and CDN - - - -### Environment Variables - - - -### Logs - - - -### Timezone - - - -### File System - - - -### IP Addresses - - - -## Troubleshooting - -### Why are the dependencies listed under `devDependencies` not installed? - -When you deploy your project, Cloud Engine will install dependencies (including those listed under `devDependencies`) for your project with `npm ci`. -However, if the Node.js version you specified is below 10 or lockfile not exists, Cloud Engine will use `npm install --production` instead. This means that the dependencies listed under `devDependencies` will **not** be installed, see [Default Commands](/sdk/engine/deploy/nodejs#Default+Commands). -To have these dependencies installed as well, you can specify `installDevDependencies: true` in `leanengine.yaml`. - -### What should I do if I see `npm ERR! peer dep missing`? - -If you see an error like this when deploying your project: - -``` -npm ERR! peer dep missing: graphql@^0.10.0 || ^0.11.0, required by express-graphql@0.6.11 -``` - -This means that some of the peer dependencies are not successfully installed. The reason is that if the version of Node.js you specified is below 10, Cloud Engine will only install the dependencies listed under `dependencies`. Please make sure the required dependencies of the dependencies listed under `dependencies` are also listed under `dependencies`, not `devDependencies`. - -You can reproduce the problem on your local computer by deleting `node_modules` and installing dependencies with `npm install --production`. - -You may also consider upgrading Node.js to 10 or higher to solve the problem. - -#### Timeout Settings - -If a request triggers a function that doesn’t give a response due to an error, the server’s RAM will still be occupied for this request. To prevent the RAM from being occupied in this case and to have the client receive an error without delay, there needs to be a timeout set up on the server so that when a request doesn’t receive a response in a while, the server will return an HTTP error code to the client. - -The default timeout for the custom routes defined with Express is 15 seconds. You can edit this value in `app.js`: - -```js -// Set the default timeout -app.use(timeout("15s")); -``` diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/php.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/php.mdx deleted file mode 100644 index b37f7f7a8..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/php.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: PHP Runtime Environment -sidebar_label: PHP -sidebar_position: 7 ---- - -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; -import { Conditional } from "/src/docComponents/conditional"; - -:::info -This article serves as a thorough introduction to Cloud Engine’s PHP runtime environment. To quickly get started with Cloud Engine, see [Getting Started With Cloud Engine](/sdk/engine/deploy/getting-started). -::: - -A PHP project has to have a `composer.json` and `public/index.php` under its root directory for Cloud Engine to correctly identify it as a PHP project. - -If you are looking to start a new project, we recommend that you use our [PHP sample project](https://github.com/leancloud/slim-getting-started) as a boilerplate. - -## How It Works - -Cloud Engine runs your application with Nginx and PHP-FPM. The `public` directory will be mapped as the document root of your website. The `.php` files in this directory will be handled by PHP-FPM while other static files will be handled by Nginx. If a user requests a path that does not exist, the request will be handled by `public/index.php`, which can serve as the entry point for most frameworks. - -Cloud Engine assigns a PHP-FPM worker for every 64 MB of memory. To customize the number of workers, you can add a _custom environment variable_ on Cloud Engine’s _Settings_ page with `PHP_WORKERS` as its name and the number of workers you want as its value. Keep in mind that if you set the number too low, there might not be enough workers for handling new requests; if you set the number too high, there might not be enough memory for handling requests. - -## Configure PHP Version - -You can specify the PHP version you want to use in `composer.json`: - -```json -"require": { - "php": "8.0" -} -``` - -At this time, Cloud Engine supports PHP `5.6`, `7.0`, `7.1`, `7.2`, `7.3`, `7.4`, and `8.0`. - -:::note - -For newly created apps, if -If you don’t specify a PHP version, the -latest stable version will be used. - - {" "} - For apps created before 9/2/2021, PHP `5.6` will be used by default to ensure compatibility. - - -::: - -## Install Dependencies (`composer.json`) - -Cloud Engine will automatically install the dependencies listed in `composer.json`. At this time, the version of Composer used by Cloud Engine is 1.x. - -## PHP Extensions - -Regardless of what PHP version you are using, the following extensions will be enabled by default: `fpm`, `curl`, `mysql`, `zip`, `xml`, `mbstring`, `gd`, `soap`, and `sqlite3`. - -For PHP 7.0 and above, `mongodb` will be enabled by default. - -Starting from PHP 7.2, the `mcrypt` extension is not provided by default anymore. To use this extension in your project, you can add `ext-mcrypt: *` into `require` in `composer.json`. Adding `mcrypt` will increase the time needed for deployment. If your project doesn’t need this extension, you don’t have to add it. - -## Customize Build Process - - - -### Build Logs - - - -## Health Check - - - -## Cloud Environment - -### Custom Domains - - - -### Load Balancer and CDN - - - -### Environment Variables - - - -### Logs - - - -### Timezone - - - -### File System - - - -### IP Addresses - - - -## Troubleshooting - -### Can I specify local Composer repositories with the `path` type? - -Because Cloud Engine will copy `composer.json` and `composer.lock` to dedicated directories for installing dependencies, local Composer repositories with the `path` type are not supported. -If your project uses local Composer repositories with the `path` type, we recommend that you change them to the `vcs` type. - -### When I deploy my project, it fails to fetch files from files.phpcomposer.com. - -phpcomposer.com has already stopped its service, so if the `composer.lock` in your PHP project contains URLs under this domain, you won’t be able to install dependencies for your project. -There are two solutions: - -1. Remove `composer.lock` before you deploy your project so that Cloud Engine will install dependencies according to `composer.json`. -2. After you have correctly configured the repository address on your local computer, run `composer update --lock` to update the download links in `composer.lock`. This won’t affect the versions specified for the dependencies. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/python.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/python.mdx deleted file mode 100644 index 1f92667db..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/python.mdx +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: Python Runtime Environment -sidebar_label: Python -sidebar_position: 5 ---- - -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import CloudLogs from "../_partials/cloud-logs.mdx"; -import CloudFilesystem from "../_partials/cloud-filesystem.mdx"; -import CloudHealthCheck from "../_partials/cloud-health-check.mdx"; -import CloudEnvironments from "../_partials/cloud-environments.mdx"; -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import CloudInternetAddress from "../_partials/cloud-internet-address.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import { Conditional } from "/src/docComponents/conditional"; - -:::info -This article serves as a thorough introduction to Cloud Engine’s Python runtime environment. To quickly get started with Cloud Engine, see [Getting Started With Cloud Engine](/sdk/engine/deploy/getting-started). -::: - -A Python project has to have a `wsgi.py` and `requirements.txt` under its root directory for Cloud Engine to correctly identify it as a Python project. - -By default, Cloud Engine runs Python projects with WSGI. It will load the module `wsgi.py` and call the global variable `application` in it as a WSGI function. Therefore, please ensure that `wsgi.py` contains a global variable/function/class named `application` that complies with the [WSGI specification](https://peps.python.org/pep-0333/): - -```python title='app.py' -from flask import Flask -app = Flask(__name__) -@app.route('/') -def index(): - return "hi" -``` - -```python title='wsgi.py' -from app import app -application = app -``` - -Most popular Python-based web frameworks come with support for WSGI, including [Flask](https://flask.palletsprojects.com/en/), [Django](https://www.djangoproject.com), and [Tornado](https://www.tornadoweb.org/en/stable/). We provide the following Flask- and Django-based boilerplates for you to reference and start your project with: - -- [Flask](https://github.com/leancloud/python-getting-started) -- [Django](https://github.com/leancloud/django-getting-started) - -## Run Without WSGI - -You can run Python programs on Cloud Engine without using WSGI, or you can host your own WSGI server as well. To do so, create a file named `leanengine.yaml` and add the following configuration: - -```yaml title='leanengine.yaml' -run: python app.py -``` - -Make sure that your app listens on the port specified by the environment variable `LEANCLOUD_APP_PORT` to provide HTTP services. - -## Configure Python Version - -Cloud Engine is compatible with [pyenv](https://github.com/pyenv/pyenv)’s `.python-version`. You can place this file under the root directory of your project to specify the Python version you want to use: - -```plain title='.python-version' -3.10 -``` - -Cloud Engine will install Python with the version specified in this file. - -At this time, Cloud Engine only supports CPython. It doesn’t support other Python implementations including PyPy, Jython, and IronPython. - -:::note - -For newly created apps, if -If you don’t specify a Python version, -the latest stable version will be used. - - {" "} - For apps created before 9/2/2021, Python `2.7` will be used by default to ensure - compatibility. - -::: - -:::note -If you are using `pyenv` on your local computer, `pyenv` will follow this file to run your project with the specified Python version. We recommend that you use `pyenv` on your local computer so that the environment on your local computer will be the same as that on the server. See [pyenv’s GitHub repository](https://github.com/pyenv/pyenv) for more information on how to install `pyenv`. -::: - -## Install Dependencies (`requirements.txt`) - -Cloud Engine will install the packages specified in [requirements.txt](https://pip.pypa.io/en/stable/user_guide/#requirements-files) with `pip`: - -```plain title='requirements.txt' -leancloud>=2.9.1,<3.0.0 -Flask>=1.0.0 -``` - -:::note -We recommend that you specify the major version of your packages using a format like `leancloud>=2.9.1,<3.0.0`. This prevents incompatible changes introduced by major version bumps from causing problems during the deployments of your application. -::: - -## Customize Build Process - - - -#### Start Projects With uWSGI - -```yaml title='leanengine.yaml' -run: uwsgi --gevent 5000 --http :3000 --wsgi-file wsgi.py --master --process=${LEANCLOUD_AVAILABLE_CPUS} --disable-log -``` - - - -### Build Logs - - - -### System Dependencies - - - -## Health Check - - - -## Cloud Environment - -### Custom Domains - - - -### Load Balancer and CDN - - - -### Environment Variables - - - -### Logs - - - -### Timezone - - - -### File System - - - -### IP Addresses - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/webapp.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/webapp.mdx deleted file mode 100644 index b10a84508..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/deploy/webapp.mdx +++ /dev/null @@ -1,157 +0,0 @@ ---- -title: Frontend Runtime Environment -sidebar_label: Frontend -sidebar_position: 3 ---- - -import CloudTimezone from "../_partials/cloud-timezone.mdx"; -import NodejsSetupRuntime from "../_partials/nodejs-setup-runtime.mdx"; -import NodejsSetupPackageManager from "../_partials/nodejs-setup-package-manager.mdx"; -import BuildingAdvanced from "../_partials/building-advanced.mdx"; -import BuildingBuildLogs from "../_partials/building-build-logs.mdx"; -import NodejsSetupDependencies from "../_partials/nodejs-setup-dependencies.mdx"; -import CloudLoadBalancer from "../_partials/cloud-load-balancer.mdx"; -import CloudCustomDomain from "../_partials/cloud-custom-domain.mdx"; -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -import QuickStartDeploy from "../_partials/quick-start-deploy.mdx"; - -You can deploy frontend apps (like websites) to Cloud Engine. For apps built with frameworks like React and Vue, their build processes can be run on Cloud Engine so you don’t have to clutter your Git repositories with production builds or set up additional CI environments. With Cloud Engine, you can easily bind custom domains to your app, set up auto-renewal for SSL certificates, and enable HTTPS redirects. These features help you reduce the time spent on deploying and maintaining your app. - -:::info -This article serves as an introduction to Cloud Engine’s frontend runtime environment. For features provided by the Cloud Engine platform, see [Cloud Engine Platform Features](/sdk/engine/platform). -::: - -If your project has a file named `static.json` or `index.html` under its root directory, Cloud Engine will identify it as a frontend project. When you deploy your project, Cloud Engine will build it with the Node.js runtime environment and automatically start an HTTP server with [serve](https://www.npmjs.com/package/serve). - -## Getting Started - -Most frontend scaffolds can be deployed to Cloud Engine with little to no configurations. Consider using them if you plan to create a new project. - - - - -[create-react-app](https://create-react-app.dev/) provides out-of-the-box toolchains that automatically set up build tools for your React project so you can focus on development: - -```sh -npx create-react-app react-for-engine --use-npm -``` - -Then navigate to the project’s directory (`react-for-engine` in the example above) and create a configuration file named `static.json` that rewrites non-existing URLs to `index.html`. This allows your single-page application to use its own frontend router (like `react-router`): - -```json title='static.json' -{ - "public": "build", - "rewrites": [{ "source": "**", "destination": "/index.html" }] -} -``` - -Then create a file named `leanengine.yaml` and specify the build command in it: - -```yaml title='leanengine.yaml' -build: npm run build -``` - - - - -You can create a Vue project with [Vue CLI](https://cli.vuejs.org/): - -```sh -npm install -g @vue/cli -vue create vue-for-engine -``` - -Then navigate to the project’s directory (`vue-for-engine` in the example above) and create a configuration file named `static.json` that rewrites non-existing URLs to `index.html`. This allows your single-page application to use its own frontend router (like `vue-router`): - -```json title='static.json' -{ - "public": "dist", - "rewrites": [{ "source": "**", "destination": "/index.html" }] -} -``` - -Then create a file named `leanengine.yaml` and specify the build command in it: - -```yaml title='leanengine.yaml' -build: npm run build -``` - - - - -You can create a Next.js project with [create-next-app](https://nextjs.org/docs/api-reference/create-next-app): - -``` -npx create-next-app next-for-engine --use-npm -``` - -Then navigate to the project’s directory (`next-for-engine` in the example above), create a file named `leanengine.yaml`, and specify the build command in it: - -```yaml title='leanengine.yaml' -build: npm run build -``` - -:::info - -In order for Next.js [API Routes](https://nextjs.org/docs/api-routes/introduction) to work, Cloud Engine will run the project in a [Node.js runtime environment](/sdk/engine/deploy/nodejs/) by starting a [Next.js production server](https://nextjs.org/docs/api-reference/cli#production) with `npm start`. The section below regarding configuring serve won’t be applicable to Next.js projects. - -::: - - - - -The build process can be run on Cloud Engine so you don’t have to include a production build in your Git repository or set up additional CI environments. - -### Deploy to Cloud Engine - - - -## Configure Node.js Version - - - -## Config Package Manager - - - -## Install Dependencies (`package.json`) - - - -## Configure serve - -To customize the behavior of serve, create a file named `static.json` under the root directory of your project. - -```json title='static.json' -{ - "public": "build", // Start the website from the `build` directory rather than the root directory - "rewrites": [ - { "source": "**", "destination": "/index.html" } // Redirect all non-existing URLs to `index.html` (applicable to most single-page applications) - ] -} -``` - -See [serve-handler · Options](https://github.com/vercel/serve-handler#options) for more options and usages of serve. - -## Customize Build Process - - - -### Build Logs - - - -## Cloud Environment - -### Custom Domains - - - -### Load Balancer and CDN - - - -### Timezone - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/faq.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/faq.mdx deleted file mode 100644 index a2fecd2b3..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/faq.mdx +++ /dev/null @@ -1,262 +0,0 @@ ---- -title: Cloud Engine FAQ -sidebar_label: FAQ -sidebar_position: 10 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -## Cloud Engine Features - -### What are the languages supported by Cloud Engine? - -The runtime environments supported by Cloud Engine include Node.js, Python, Java, PHP, .NET, and Go. Frontend projects based on Node.js can also be hosted on Cloud Engine. See [Cloud Engine Overview](/sdk/engine/overview/) for more information. - -You can let us know if you wish other runtime environments to be supported. - -### Can I host static websites on Cloud Engine? - -Yes. See [Frontend Runtime Environment](/sdk/engine/deploy/webapp/) for more information. - -### Does Cloud Engine support HTTPS? - -Yes. When binding a custom domain, you can choose to either upload your own SSL certificate or have Cloud Engine automatically manage certificates for your project. - -You can also enable _Force HTTPS_ to have HTTP requests automatically redirected to HTTPS. - -### Would my application still be able to provide services while I’m deploying a new version of my project? - -Yes. When you deploy a new version of your project, Cloud Engine will first start new instances running the new version. After the new version passes the health check, Cloud Engine will update the router to redirect incoming requests to the new instances and then shut down the old ones. With this mechanism, your application can keep providing services while you deploy a new version. - -### What’s the relationship between Cloud Engine and Cloud Functions? - -Cloud Engine is a platform for hosting backend services. It can be used to host all kinds of backend programs and it won’t interfere with the internal logic of the programs. - -On top of this, you can choose to integrate the Cloud Engine SDK into your program, which gives your program access to functions like Cloud Functions and hooks. Cloud Functions is designed to work perfectly with our [Data Storage](/sdk/storage/features)[Data Storage](/sdk/storage/overview/) service. If you’re already using the Data Storage service, you’ll find it easy to get started with Cloud Functions. - -You can still use the other features besides Cloud Functions without integrating the Cloud Engine SDK into your program. In addition to the features mentioned above, Cloud Engine also provides hosted [Redis](/sdk/engine/database/redis/), [MongoDB](/sdk/engine/database/mongo/), and [Elasticsearch](/sdk/engine/database/es/) for you to store the data of your application. - -## Cloud Functions - -### What are some restrictions with Cloud Functions? - -Cloud Functions is a **relatively constrained** function that allows you to define server-side logics for your application. It is **highly integrated** into our SDK. Cloud Functions is designed as an **RPC**-like mechanism. When invoking a Cloud Function, you can only pass in arguments and receive a result from the function. You can’t customize timeouts, HTTP methods, or URLs for it, nor can you read and set the header. - -To use the semantic functions of HTTP freely or to use the standard RESTful API provided by third-party frameworks, you can choose to have your application handle HTTP requests without using Cloud Functions. - -### Can I use Cloud Functions with multiple groups? - -Cloud Engine will automatically redirect requests for invoking Cloud Functions to the correct groups. This means that you can use Cloud Functions with multiple groups. - -### I’ve finished deploying my project, but Cloud Functions and hooks still don’t work. - -In order to support Cloud Functions and hooks, Cloud Engine’s management program will interact with the Cloud Engine SDK through `/1.1/functions/_ops/metadatas`. Please make sure to have the SDK take care of this URL. -By default, Cloud Engine will try to get the metadata of Cloud Functions and hooks by accessing `/1.1/functions/_ops/metadatas`. If Cloud Engine fails to access this URL, your project’s Cloud Functions and hooks will not work, but the deployment won’t be interrupted. -To interrupt the deployment if Cloud Engine fails to access the metadata, set `functionsMode` to `strict` in `leanengine.yaml`. -If your project doesn’t need Cloud Functions or hooks, you can choose: - -- Not to specify `functionsMode` in `leanengine.yaml`. At the same time, let `/1.1/functions/_ops/metadatas` **return an HTTP status code of `404`**, which indicates that Cloud Functions and hooks are not used by the project. -- Or, to set `functionsMode` to `disabled` in `leanengine.yaml`. Keep in mind that if you do this, hooks will not take effect even though you define them in your program. Invocations to Cloud Functions (including the remote ones triggered by the SDK and those triggered through the REST API) may fail for being redirected to the wrong groups. - -### The deployment is interrupted, saying that there’s a Cloud Function with the same name. - -You can use Cloud Functions with multiple groups. -However, if the code you’re deploying contains a Cloud Function that shares the same name as one that already exists in a different group, the deployment will be interrupted by default, and you’ll see an error message. This helps you avoid accidentally redefining a Cloud Functions. -We recommend that you remove the Cloud Functions you don’t need from your project since it would be hard for you to comprehend and maintain duplicate Cloud Functions. -Though you can choose to specify the `--overwrite-functions` option to have the Cloud Functions in the code you’re deploying override those in the other groups. - -### What could be the possible reason for hooks not being triggered as expected? - -Please first make sure that you’ve understood when each hook gets triggered: - -- `beforeSave`: Before an object gets saved -- `afterSave`: After an object has been saved -- `beforeUpdate`: Before an object gets updated -- `afterUpdate`: After an object has been updated -- `beforeDelete`: Before an object gets deleted -- `afterDelete`: After an object has been deleted -- `onVerified`: After a user has verified their email or phone number -- `onLogin`: When a user logs in (this doesn’t include `become(sessionToken)`) - -If you’re debugging locally, the hooks in the staging environment will be triggered. If your application doesn’t have a staging environment, no hooks will be triggered. - -You can check if a hook has been triggered using the following method: - -Print a log at the beginning of the function defined for a hook, then trigger the hook. Now you can check the logs on the dashboard to see if the log shows up. If it doesn’t show up, possible reasons include: - -- Your code is not deployed to the correct application -- Your code is not (successfully) deployed to the production environment -- You entered an incorrect class name for the hook - -If the log shows up, you can check if there are error messages on the dashboard. For `before` hooks, please ensure that they return within 15 seconds, or a timeout will be caused. - -For `after` hooks, the timeout is 3 seconds. If your trial instance has already hibernated, it might not be able to receive `after` hooks due to its long startup time. We recommend that you upgrade to a standard instance to prevent hibernation. - -### Can I have my program in a Cloud Function retrieve data from the \_User table without logging in? - -Yes. By skipping permission checks with the `masterKey`, you can have your program in a Cloud Function retrieve data from the \_User table without logging in. - -Since Cloud Engine runs within the trusted server-side environment, you can enable `Master Key` globally to skip permission checks enforced by ACL and class permission settings. See [Cloud Engine SDK Guide § Using MasterKey](/sdk/engine/functions/sdk/#using-masterkey) for more information. - -## Deployment - -### When I deploy my project for a second time, there’s a huge discrepancy between the mirror size of this deployment and that of the previous deployment. Why? - -Cloud Engine uses a caching mechanism to accelerate the build process of deployments. The mirror size displayed for a deployment shows the size of the data generated for this deployment. You can deduce if a deployment has made use of the cache according to this size. This size doesn’t necessarily indicate the size of the entire project. - -### What should I do if a deployment is stuck in the stage of downloading and installing dependencies? - -During this stage, Cloud Engine would be using the package manager of the language of your choice (`npm`, `pip`, `composer`, or `maven`) to install dependencies for your project. We have a caching mechanism that helps accelerate this process, but the cache might not work due to various reasons (like if the dependency list is modified). When this happens, it might take us longer to finish installing the dependencies, so please wait patiently. If you have specified system dependencies with `leanengine.yaml`, these dependencies will also be installed during this stage, so please don’t add unused dependencies to this file. - -For Node.js projects, please check if you have specified a slow mirror in `package-lock.json` or `yarn.lock`. - -### I’m deploying my project to multiple instances. Do I need to redeploy my project if the deployment fails on some of the instances? - -When you have multiple instances under any of the environments (staging environment or production environment), Cloud Engine will deploy your project to all of the instances under this environment. If the deployment fails on some of the instances, Cloud Engine will try to redeploy your project automatically in a few minutes. You don’t have to manually redeploy your project. - -### I’m seeing “Deploying” on the dashboard even after the deployment has finished. Why? - -When the dashboard says “Deploying”, it could mean that maintenance is going on. This may be triggered by starting hibernated instances or server errors and doesn’t necessarily mean that you’ve triggered a deployment. - -### What is a health check? - -Cloud Engine checks the statuses of all the instances every few minutes (through HTTP requests; see the _Health Check_ section in the respective runtime environment’s docs for more information). -If any of the instances cannot provide a valid response, Cloud Engine will trigger a redeployment and print the following log on the console: - -> 健康检查失败:web1 检测到 Error connect ECONNREFUSED 10.19.30.220:51797 - -This could happen once or twice a week and that would be totally fine. If it happens frequently, chances are that your program is not having enough resources for it to respond correctly. There could be other reasons causing this problem as well. - -## Restrictions and Pricing - -### What are the restrictions imposed by Cloud Engine? - -A request cannot be larger than 100 MB (including uploading files to Cloud Engine). - -A request has to get a response within 60 seconds. - -A WebSocket connection will be stopped if no data is transmitted within the past 60 seconds. - -### What’s the maximum number of instances an application can have? - -Each application can have at most 12 instances. Please reach out to us if you need more instances for your application. - -### How will I be billed? - -If your application makes requests to the Data Storage API, those requests will be billed according to the Data Storage service’s pricing. - -Standard instances, LeanDB instances, back-to-origin traffic, and CDN traffic will all lead to charges. See the pricing page for more information. - -### How will the traffic be billed? - -Each instance has a free quota of 1 GB every day. For the price of the exceeding part, see the pricing page of the current region. - -The traffic of all the instances under an application will be summed up. If the total number of standard instances under all the groups of an application is `n`, the free quota every day will be `max(n, 1)` GB. - -**Cloud Engine is not designed to distribute large files**. Please use the file service if you have this type of requirement. - -You can see the traffic of your instances on ** > Manage deployment > Your group > Statistics > Traffic**. - -### If I’ve changed the quota or number of instances on a day, how will I be billed for this day? - -You will be billed for **the maximum number of instances you have had for the day** on the next day. For example, if you have done the following activities during a day: - -- Your application has 4 standard-512 instances; -- You changed the quota to standard-1024; -- You changed the number of instances to 2. - -You will be billed for the price of standard-1024 multiplied by 4 instances. - -### Will hooks lead to API calls? Will `afterUpdate` lead to an API call? - -`afterUpdate` is executed within Cloud Engine, so `afterUpdate` won’t lead to API calls. If `afterUpdate` leads to API requests, the API requests will be billed. - -## Best Practices - -### How do I upload files to Cloud Engine? - -You can use the interface for uploading files provided by the SDK, -though we generally recommend that you upload files with client SDKs without having the files go through Cloud Engine. This helps you reduce the unnecessary traffic generated. - -## I get the error "failed to upload file to qiniu bcz current file has not data stream" when uploading files in Cloud Engine. - -LCFile internally uses a local cache (which requires a local directory) to hold the binary data before uploading it to the file storage service. Inside the Cloud Engine it is not possible to create such a cache directory or file due to insufficient permissions, resulting in errors like `failed to upload file to qiniu bcz current file has not data stream`. -We do not recommend using file services in the Cloud Engine for several reasons: - -1. The permission issue mentioned above will occur. This makes it impossible to create cache directories and temporary files in the Cloud Engine. -2. All file data goes to the server first, and then the API is called to push it to the file storage provider, which leads to low efficiency and high network cost for the user (you will need to pay for the excessive traffic usage with Cloud Engine). - -It is recommended to use the SDK to upload the file, and then give the result (e.g. the object id or url of the file) to the server (the code for Cloud Engine). Uploading on the client side will use the file storage provider's CDN (servers close to the user), which makes the user experience faster and saves the traffic costs of Cloud Engine. - -### Should scheduled tasks be executed in the staging environment or the production environment? - -The free trial instances provided by the staging environment may hibernate automatically, which could impact the execution of the scheduled tasks. Therefore, we recommend that you test scheduled tasks in the staging environment and run them in the production environment. -If your scheduled tasks take up a lot of CPU and memory and you don’t want them to interfere with the production environment, consider buying standard instances in the staging environment and running your scheduled tasks in the staging environment. - -### How do I tell if I’m using the staging environment or the production environment? - -By default, there’s only a production environment provided by Cloud Engine. The domain of it would be web.example.com. There will only be a trial instance in the production environment for you to run your program. - -When you upgrade the trial instance in the production environment to a standard instance, you will get a staging environment with its domain being stg-web.example.com. Both two environments have access to the same data in the Data Storage service. You can test your code with the staging environment by deploying your updated code to this environment and only publish your code to the production environment after testing your code. If you want an environment that has access to a different set of data in the Data Storage service, we recommend that you create a separate application. - -Keep in mind that the stg-web.example.com domain needs to be bound on the dashboard on your own. - -### How can I access the website hosted in the staging environment? - -Please bind a domain starting with `stg-` on the dashboard. Domains starting with `stg-` (like stg-web.example.com) will be automatically bound to the staging environment. - -### Can I bind a bare domain to Cloud Engine? - - - -Yes. You can add an A record pointing to the dedicated IP address. - - - - - -To bind bare domains, we recommend that you choose a domain registry that supports ANAME or CNAME flattening. - - - -## Other Problems - -### I’m seeing the _Application not found_ error. - -There could be different reasons that lead to this error: - -- You’re accessing the incorrect environment. There might not be a staging environment in your application, but you’re trying to access this environment. -- You’re entering the wrong domain for Cloud Engine. -- You’re using the trial version and your instances have gone hibernated. You can upgrade to the standard version to prevent your instances from hibernating. - -### What can I do if the response time of my instances increases? - -There could be different reasons that cause the response time of your instances to increase. -We recommend that you scrutinize your code and the usage of CPU and memory to spot the bottleneck, then consider if you need to increase the quota or the number of instances. -It might be helpful to download the access logs so you can see which APIs or Cloud Functions are responding slowly. - -### What if I can’t access the files hosted on my project? - -Please first check if the capitalizations of the letters in the file names are correct. The file system used by Cloud Engine is case-sensitive, while Windows and macOS are often configured to be case-insensitive. - -### Will Cloud Engine retry any requests? - -For idempotent requests (GET and PUT), Cloud Engine will retry them when there’s an error or timeout with HTTP. To avoid retries from happening, use the correct verb (like POST) for your requests. - - - - - - -### Where is the `AccessToken` for the cloud engine - -Deployed using ** CLI > Generate ** new access token -Like: - -![](https://capacity-files.lcfile.com/x87lNfXuP7FFArbDu6HzCRLHrLgeKHRd/Frame%205.png) -![](https://capacity-files.lcfile.com/2b28rUUa3OdUvMp2O6QetnmCEbGYWe4X/Frame%203%20%281%29.png) - - - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/_category_.json deleted file mode 100644 index 3f294f069..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "云函数和 Hook", - "collapsed": true, - "position": 5 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/cloud-queue.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/cloud-queue.mdx deleted file mode 100644 index 3b4b44117..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/cloud-queue.mdx +++ /dev/null @@ -1,208 +0,0 @@ ---- -title: Cloud Queue Guide -sidebar_label: Cloud Queue -sidebar_position: 4 ---- - -Cloud Queue allows you to invoke Cloud Functions outside of Cloud Engine’s context. Based on the existing implementation of Cloud Functions, Cloud Queue makes it possible for you to retry failed function calls, skip duplicate function calls, look up function outputs, delay function calls, and have your Cloud Functions triggered routinely. Queued tasks can be reliably stored in the Cloud Queue of your application and won’t be lost even though the Cloud Engine instances in your application get restarted due to deployments, overloading, and crashing. When these situations happen, the tasks in the queue will be executed once the instances get back to their normal state. - -At this time, Cloud Queue is still an experimental feature for you to try for free. It will become a paid feature once it’s officially launched. Since Cloud Queue is designed to handle bursty traffic, you will be billed based on “the number of tasks queued per hour” and “the peak number of the remaining tasks in the queue”. - -Cloud Queue runs outside of Cloud Engine and accepts tasks through HTTP requests. It also invokes Cloud Functions hosted on Cloud Engine with HTTP requests so the tasks can be executed. All these features are encapsulated in our SDK. For now, the interface to enqueue a task, `Cloud.enqueue`, can only be invoked with the `masterKey` within Cloud Engine (it might be opened up to clients in the future). The interface for checking results, `Cloud.getTaskInfo`, can be invoked by clients with `uniqueId`. - -## Features and Use Cases - -Cloud Queue offers the following features: - -- **Retry failed tasks** If a task fails to run, Cloud Queue will try to run the task again. You can configure how many times a task should be retried (`attempts`) as well as the interval between every two attempts (`backoff`). When Cloud Queue retries a task, the `uniqueId` of the task won’t change. -- **Skip duplicate tasks** You can set a `uniqueId` for each task (if the `uniqueId` is not provided, a random one will be generated). While the task is in the queue, Cloud Queue won’t accept another task with the same `uniqueId`. -- **Look up function outputs** A task will remain in the queue for a while after it has been completed (set by `keepResult`). You can look up the result of the task with its `uniqueId`. -- **Delay tasks** You can delay a task by setting `delay`. -- **Have Cloud Functions triggered routinely** For now, scheduled tasks is a subfunction of Cloud Queue. You can create and manage any number of scheduled tasks on the dashboard. You can use [CRON expressions](#cron-expressions) or set intervals in seconds. -- **Concurrency control** Cloud Queue will store the incoming tasks in a queue and run one of them each time. This prevents Cloud Engine from being overloaded. We will introduce a smarter concurrency controlling algorithm in the future. -- **Priority settings** You can set a `priority` for certain tasks. Cloud Queue will keep running the task with the highest priority in the queue. - -You can find demos showing how to use Cloud Engine on [leanengine-nodejs-demos](https://github.com/leancloud/leanengine-nodejs-demos). - -- [queue-delay-retry](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/queue-delay-retry.js) Delay and retry Cloud Functions -- [queue-result-query](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/queue-result-query.js) Look up results -- [crawler](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/crawler.js) A crawler that fetches all the pages under a site (shows the ability to skip duplicate tasks and concurrency control) - -## Basic Usage - -```javascript -const { Cloud } = require("leanengine"); - -// The Cloud Function being invoked -Cloud.define("closeOrder", async function ({ params }) { - try { - const order = await new Query("Order").get(params.id); - // The returned value can be used to loop up results in the future; it will be logged as well - return await order.save({ status: "closed" }); - } catch (err) { - // Throw an error to make the task fail (errors from JavaScript and dependencies will also make the task fail) - throw new Cloud.Error(`Some error happened: ${err.message}`); - } -}); - -// Add a task; `enqueue` will return a `uniqueId` once the task is added to the queue (the task is not run yet) -// You can use `Cloud.enqueue` in any part of your project, including Cloud Functions and custom routes of your website -const { uniqueId } = await Cloud.enqueue("closeOrder", { id: 1234 }); - -// Get the result of the task; you can send the `uniqueId` to the client to have the client look up the result -// Only tasks in the queue can be looked up; you can change how long a completed task should stay in the queue with `keepResult` -console.log(await Cloud.getTaskInfo(uniqueId)); - -// Increasing `attempts` and decreasing `backoff` -Cloud.enqueue("closeOrder", { id: 1234 }, { attempts: 10, backoff: 10000 }); - -// Add a delayed task; `closeOrder` will run in 1 minute -Cloud.enqueue("closeOrder", { id: 1234 }, { delay: 600000 }); - -// Specify `uniqueId`; if a task with the same `uniqueId` already exists, an error will be thrown -// If a task is not in the queue anymore, its `uniqueId` will not be compared; you can change how long a completed task should stay in the queue with `keepResult` -Cloud.enqueue("closeOrder", { id: 1234 }, { uniqueId: "1234" }); -``` - -To make a Cloud Function only able to be invoked by Cloud Queue (rather than a client), add the [`internal` option](https://github.com/leancloud/leanengine-node-sdk/blob/master/API.md#avclouddefine) to `Cloud.define`. Cloud Functions defined in this way can only be invoked by Cloud Queue and other code using `masterKey`. - -## API - -Cloud Queue is supported by Node.js SDK 3.4 and above: - -``` -Cloud.enqueue(functionName, params, options?): Promise<{uniqueId: string}> -Cloud.getTaskInfo(uniqueId): Promise -``` - -Usage: - -```javascript -const { Cloud } = require("leanengine"); - -// Add a task; `enqueue` will return a `uniqueId` once the task is added to the queue (the task is not run yet) -const { uniqueId } = await Cloud.enqueue("sendMail", { userId: 1234 }); - -// Delay a task -Cloud.enqueue("closeOrder", { id: 1234 }, { delay: 600000 }); - -// Get the result -console.log(await Cloud.getTaskInfo(uniqueId)); -``` - -`options` contains the following properties: - -- `attempts?: number`: Maximum number of retries; defaults to `1` -- `backoff?: number`: Interval for each retry (in milliseconds); defaults to `60000` (1 minute) -- `delay?: number`: How long to delay (in milliseconds) -- `deliveryMode?: string`: What to do when timeout happens; could be `atLeastOnce` (run at least once but could run multiple times); `atMostOnce` (run at most once and won’t retry); defaults to `atLeastOnce` -- `keepResult?: number`: How long the task should stay in the queue once it’s done (in milliseconds); defaults to `300000` (5 minutes) -- `priority?: number`: Priority; defaults to the current timestamp; change it to a smaller number to have the task run earlier -- `timeout?: number`: Timeout in milliseconds; defaults to `15000`; should be no more than `15000` -- `uniqueId?: string`: The unique ID of the task; should be no more than 32 characters; defaults to a random UUID - -`TaskInfo` contains the following properties: - -- `uniqueId: string`: The unique ID of the task -- `status: string`: The status of the task; could be `queued` (waiting or running), `success` (completed), or `failed` - -The `TaskInfo` of a completed task will have: - -- `finishedAt?: string` The time the task was completed -- `statusCode?: number` The HTTP status code of the Cloud Function’s response -- `result?: object` The response from the Cloud Function - -The `TaskInfo` of a failed task will have: - -- `error?: string` Error message -- `retryAt?: string` The time the task will be retried for the next time - -## Performance and Reliability - -The interface for enqueueing (`Cloud.enqueue`) is designed to handle bursty traffic (like above 1000 QPS). Cloud Queue will store all the tasks received and run one of them each time. This helps reduce the burden of your Cloud Engine instances. - -Cloud Queue runs outside of Cloud Engine instances, meaning that even though your Cloud Engine instances encounter errors or are restarted, the Cloud Queue won’t be affected. Meanwhile, tasks are executed by the Cloud Functions within Cloud Engine instances, meaning that the environment for running tasks is the same as that for running Cloud Functions. - -## The Queue - -If the concurrency limit is reached, new tasks will be placed into a queue. Whenever there’s a task finished, a task with the highest priority (lowest `priority`) from the queue will be executed. - -The default value of `priority` is the timestamp the task is enqueued. For example, the timestamp of `2019-05-20T17:32:07.166+08:00` is `1558344727166`. This means that if you don’t customize the `priority`, tasks will be executed according to the time they’re added to the queue. You can change the `priority` to a smaller number to have a task run earlier, or a larger number for a task to run later. - -## CRON Expressions - -The basic syntax of a CRON expression is: - -``` -second minute hour day-of-month month day-of-week -``` - -| Position | Field | Constraints | Range | Special Characters Accepted | -| -------- | ------------ | ----------- | -------------------- | --------------------------- | -| 1 | Second | Required | 0–59 | `, - * /` | -| 2 | Minute | Required | 0–59 | `, - * /` | -| 3 | Hour | Required | 0–23 (0 is midnight) | `, - * /` | -| 4 | Day of month | Required | 1–31 | `, - * ? /` | -| 5 | Month | Required | 1–12、JAN–DEC | `, - * /` | -| 6 | Day of week | Required | 1–7、SUN–SAT | `, - ? /` | - -Special characters can be used in the following ways: - -| Character | Meaning | Usage | -| --------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `*` | All values | All the values a field can have. For example, to run a task every minute, set **minutes** to `*`. | -| `?` | Unspecified value | Can be used on at most one of the two fields that accept this value. For example, to run a task on the 10th of every month regardless of what day it is, set **day-of-month** to `10` and **day-of-week** to `?`. | -| `-` | Scope | For example, setting **hours** to `10-12` means 10am, 11am, and 12pm. | -| `,` | Splitting multiple values | For example, setting **day-of-week** to `MON,WED,FRI` means Monday, Wednesday, and Friday. | -| `/` | Interval | For example, setting **seconds** to `*/15` means every 15 seconds starting from the 0th, which are the 0th, 15th, 30th, and 45th seconds. | - -Fields are concatenated with spaces. Values like `JAN`–`DEC` and `SUN`–`SAT` are case-insensitive (`MON` is the same as `mon`). - -To illustrate: - -| Expression | Explanation | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------- | -| `0 */5 * * * ?` | Run a task every 5 minutes. | -| `10 */5 * * * ?` | Run a task every 5 minutes and the time to run it is always the 10th second of a minute (like 10:00:10, 10:05:10, etc.). | -| `0 30 10-13 ? * WED,FRI` | Run a task at 10:30am, 11:30am, 12:30am, and 1:30pm every Wednesday and Friday. | -| `0 */30 8-9 5,20 * ?` | Run a task every 30 minutes between 8am and 10am (8:00am, 8:30am, 9:00am, and 9:30am) on the 5th and 20th of every month. | - -The time zone followed by CRON expressions is `UTC+8` for China and `UTC+0` for other regions. - -## Restrictions - -The following restrictions are applied while we’re testing Cloud Queue: - -- The QPS for enqueueing for each application is 100 -- At most 10000 tasks can be enqueued every day for each application -- An application can have at most 1000 tasks in the queue - -You may contact us if you need to have the restrictions adjusted. - -## FAQ - -### What’s your plan for the future? - -这里列出的是后续可能会实施的一些计划,如果你非常需要其中某个功能,请通过工单或论坛联系我们,让我们知道。 - -- **允许客户端调用** 我们有计划为用户提供一个「允许客户端调用云队列」的选项,开启这个选项后客户端将会可以进行入队操作(`Cloud.enqueue`),但客户端不能指定 `options` 中的任何选项,只能传递参数。 -- **超时时间** 因为之前我们的云函数一直将超时时间限制在 15 秒,所以目前云队列受限于云函数,超时也是 15 秒。我们正在调整相关的基础设施,计划在后续让从云队列调用云函数的超时时间最长可以达到 5 分钟。 -- **并发限制** 目前所有应用的并发限制都是 1,我们有计划实现一种自动控制并发的机制:在任务较多的情况下,并发会逐渐增加,直到负载体现在实例的 CPU 或内存压力上。 -- **使用单独的分组运行任务** 我们有计划为用户提供一个「在独立分组中运行定时任务」的选项,开启这个选项后会自动创建一个特殊分组,所有定时任务都在这个分组中运行。 -- **定时任务的可编程接口** 我们有计划为定时任务提供可编程接口,但优先级较低。 -- **云队列的日志** 目前云队列会将执行日志直接打印到云引擎的应用日志中,后续我们准备把云队列的日志显示在一个单独的 Tab 中。 -- **在其他 SDK 中使用云函数** 后续我们会在 Python SDK、Java SDK、PHP SDK 中添加云队列的支持。 - -### What situations will be affected by the error handling policy (deliveryMode)? - -异常处理策略(deliveryMode)用于在以下几种不确定任务是否已经执行或是否应该执行的情况下,来决定是否重试: - -- 云队列已将请求发到云函数,但云函数未在超时时间内给出成功或失败的响应。 -- 因云队列本身的故障导致失去对正在执行的任务的追踪。 -- 因云队列本身的故障导致定时任务没有在指定的时间触发。 - -如果选择「放弃(atMostOnce)」在出现上述情况时任务可能不会执行;如果选择「重试(atLeastOnce)」在发生上述情况时任务可能会执行多次。 - -### Can I use Cloud Queue when debugging my project locally? - -你可以在本地调试时远程调用云队列相关的 API,但云队列只会在线上的云引擎中运行指定的云函数。 diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/getting-started.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/getting-started.mdx deleted file mode 100644 index 0e291101d..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/getting-started.mdx +++ /dev/null @@ -1,137 +0,0 @@ ---- -title: Getting Started With Cloud Functions and Hooks -sidebar_label: Getting Started -sidebar_position: 1 ---- - -import QuickStartNew from "../_partials/quick-start-new.mdx"; -import QuickStartDeploy from "../_partials/quick-start-deploy.mdx"; -import FunctionsIntroduction from "../_partials/functions-introduction.mdx"; -import PlatformRuntimes from "../_partials/platform-runtimes.mdx"; -import { CLI_BINARY } from "/src/constants/env.ts"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; - - - -:::info -If you only need to write simple Cloud Functions and Hooks with Node.js, see [Cloud Functions and Hooks Guide § Writing Cloud Functions Online](/sdk/engine/functions/guides/#writing-cloud-functions-online). -::: - -## Create a Project - - - -## Write a Cloud Function - -You can find the following Cloud Function example in the demo project of the respective runtime environment: - - - - -```js title='cloud.js' -AV.Cloud.define("hello", function (request) { - return "Hello world!"; -}); -``` - - - - -```python title='cloud.py' -@engine.define -def hello(**params): - if 'name' in params: - return 'Hello, {}!'.format(params['name']) - else: - return 'Hello, LeanCloud!' -``` - - - - -```php title='src/cloud.php' -Cloud::define("sayHello", function($params, $user) { - return "hello {$params['name']}"; -}); -``` - - - - -```java title='src/main/java/cn/leancloud/demo/todo/Cloud.java' -@EngineFunction("hello") -public static String hello(@EngineFunctionParam("name") String name) { - if (name == null) { - return "What is your name?"; - } - - return String.format("Hello %s!", name); -} -``` - - - - -```cs title='web/HelloSample.cs' -[EngineFunction("Hello")] -public static string Hello([EngineFunctionParameter("text")]string text) -{ - return $"Hello, {text}"; -} -``` - - - - -```go title='functions/hello.go' -func init() { - leancloud.Engine.Define("hello", hello) -} - -func hello(req *leancloud.FunctionRequest) (interface{}, error) { - return map[string]string{ - "hello": "world", - }, nil -} -``` - - - - -The way you write a Hook is similar to that for a Cloud Function. We will cover more about the usage of Cloud Functions and Hooks later. - -## Run and Debug Locally - -

    - You can run and debug your project locally by running{" "} - {CLI_BINARY} up. The CLI will automatically inject your app’s - environment variables into the project so that the Cloud Functions in your - project can have access to the data stored within the Data Storage service. -

    - - - {`$ ${CLI_BINARY} up -[INFO] The project is running at: http://localhost:3000 -[INFO] Cloud function debug console (if available) is accessible at: http://localhost:3001`} - - -

    - {CLI_BINARY} up will also start a console for debugging Cloud - Functions on port 3001 ( - http://localhost:3001). The console allows - you to debug Cloud Functions and Hooks by providing dummy data. -

    - -![Cloud Function Debug Console](/img/cloud-engine/engine-cli-debug-console.png) - -## Deploy to Cloud Engine - - - -## Next Steps - -Read [Cloud Functions and Hooks Guide](/sdk/engine/functions/guides) to learn how you can get the most out of Cloud Functions. If you’re interested in more advanced ways of using the Cloud Engine SDK, you can jump to [Cloud Engine SDK Guide](/sdk/engine/functions/sdk). If you haven’t read it yet, make sure to check out [Cloud Engine Platform Features](/sdk/engine/platform) where you can explore the diverse features provided by Cloud Engine. To learn more about the specific runtime environments provided by Cloud Engine, see the pages below: - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/guides.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/guides.mdx deleted file mode 100644 index 204ea1b25..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/guides.mdx +++ /dev/null @@ -1,2242 +0,0 @@ ---- -title: Cloud Functions and Hooks Guide -sidebar_label: Guide -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import FunctionsIntroduction from "../_partials/functions-introduction.mdx"; -import Mermaid from "/src/docComponents/Mermaid"; -import TabItem from "@theme/TabItem"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -:::info -This article focuses on a special use case of Cloud Engine that involves Cloud Functions and Hooks. To deploy general-purpose backend applications or learn more about the features provided by Cloud Engine, see [Cloud Engine Platform Features](/sdk/engine/platform). -::: - - - -## Cloud Functions - -Now let’s look at a more complex example. Assuming that we have an app for users to leave their reviews for movies. A `Review` object would look like this: - -```json -{ - "movie": "A Quiet Place", - "stars": 5, - "comment": "I almost forgot breathing when watching this movie" -} -``` - -Here `stars` is a number between 1 and 5 that represents the score given by the reviewer. Let’s see how we can define a Cloud Function that helps us calculate the average score of a movie. - -Cloud Functions accept requests in the form of JSON objects, which allows us to pass a movie’s name into a function when we invoke the function. Within the Cloud Function, we can use the Data Storage SDK to retrieve all the scores given to a movie. Now we have everything we need to implement our `averageStars` function: - - - - -```js -AV.Cloud.define("averageStars", function (request) { - var query = new AV.Query("Review"); - query.equalTo("movie", request.params.movie); - return query.find().then(function (results) { - var sum = 0; - for (var i = 0; i < results.length; i++) { - sum += results[i].get("stars"); - } - return sum / results.length; - }); -}); -``` - -`AV.Cloud.define` accepts an optional parameter, `options` (placed between the function name and the function), which contains the following properties: - -- `fetchUser?: boolean`: Whether to automatically fetch the user logged in on the client side. Defaults to `true`. When set to `false`, `Request` will not contain the `currentUser` property. -- `internal?: boolean`: Whether to only allow the function to be invoked within Cloud Engine (with `AV.Cloud.run` without enabling `remote`) or with the `Master Key` (by providing `useMasterKey` when calling `AV.Cloud.run`). When set to `true`, the function cannot be invoked directly by the client. Defaults to `false`. - -For example, if we don’t want to allow clients to invoke the function defined above and we don’t care about the user logged in on the client side, the function above can be rewritten in the following way: - -```js -AV.Cloud.define( - "averageStars", - { fetchUser: false, internal: true }, - function (request) { - // Same as above - } -); -``` - - - - -```python -@engine.define -def averageStars(movie, **params): - reviews = leancloud.Query(Review).equal_to('movie', movie).find() - result = sum(x.get('stars') for x in reviews) - return result -``` - -The Python function’s name will be used as the default name of the Cloud Function, which you can use to invoke the function with the client SDK. To give the Cloud Function a different name, attach the name after `engine.define`: - -```python -@engine.define('averageStars') -def my_custom_average_start(movie, **params): - pass -``` - - - - -```java -@EngineFunction("averageStars") -public static float getAverageStars(@EngineFunctionParam("movie") String movie) throws LCException { - LCQuery query = new LCQuery("Review"); - query.whereEqualTo("movie", movie); - List reviews = query.find(); - int sum = 0; - if (reviews == null && reviews.isEmpty()) { - return 0; - } - for (LCObject review : reviews) { - sum += review.getInt("star"); - } - return sum / reviews.size(); -} -``` - - - - -```php -use \LeanCloud\Engine\Cloud; -use \LeanCloud\Query; -use \LeanCloud\CloudException; - -Cloud::define("averageStars", function($params, $user) { - $query = new Query("Review"); - $query->equalTo("movie", $params["movie"]); - try { - $reviews = $query->find(); - } catch (CloudException $ex) { - // Failed to query; output the error to the log - error_log($ex->getMessage()); - return 0; - } - $sum = 0; - forEach($reviews as $review) { - $sum += $review->get("stars"); - } - if (count($reviews) > 0) { - return $sum / count($reviews); - } else { - return 0; - } -}); -``` - - - - -```cs -[LCEngineFunction("averageStars")] -public static float AverageStars([LCEngineFunctionParam("movie")] string movie) { - if (movie == "A Quiet Place") { - return 3.8f; - } - return 0; -} -``` - - - - -```go -type Review struct { - leancloud.Object - Movie string `json:"movie"` - Stars int `json:"stars"` - Comment string `json:"comment"` -} - -leancloud.Engine.Define("averageStars", func(req *leancloud.FunctionRequest) (interface{}, error) { - reviews := make([]Review, 10) // Reserve some space - if err := client.Class("Review").NewQuery().EqualTo("movie", req.Params["movie"].(string)).Find(&reviews); err != nil { - return nil, err - } - - sum := 0 - for _, v := range reviews { - sum += v.Stars - } - - return sum / len(reviews), nil -}) -``` - - - - -### Parameters and Return Values - - - - -`Request` will be passed into the Cloud Function as a parameter. It contains the following properties: - -- `params: object`: The parameters sent from the client. When the Cloud Function is invoked with `rpc`, this could be an `AV.Object`. -- `currentUser?: AV.User`: The user logged in on the client (according to the `X-LC-Session` header sent from the client). -- `sessionToken?: string`: The `sessionToken` sent from the client (from the `X-LC-Session` header). -- `meta: object`: More information about the client. For now, it only contains a `remoteAddress` property, which contains the IP address of the client. - -If the Cloud Function returns a Promise, the resolved value of the Promise will be used as the response. When the Promise gives an error, the error will be used as the response instead. Error objects constructed with `AV.Cloud.Error` are considered client errors and will not be printed to the standard output. For other errors, call stacks will be printed to the standard output to help you debug your program. - -We recommend that you build your program with Promise chains, which makes it easy for you to organize asynchronous tasks and handle errors. **Please be sure to chain the Promises and return the chain within the Cloud Function.** - -
    -Details about the early versions of the Node.js SDK - -For Node.js SDK 2.0 and earlier, Cloud Function accepts two parameters: `request` and `response`. We will continue supporting this usage until the next major version, though it’s encouraged that you adopt Promise-style Cloud Functions for your project as soon as possible. - -
    - -
    - - -The arguments provided when invoking the Cloud Function will be passed directly into the Cloud Function. The parameters of your Cloud Function will mirror those of the Python function defined for it. If you plan to provide different sets of arguments to your Cloud Function for different cases, make sure to update your Python function to treat additional arguments as keyword arguments, or your program may cause Python errors. - -```python -@engine.define -def my_cloud_func(foo, bar, baz, **params): - pass -``` - -Besides accessing the arguments passed into the function, your program can obtain additional information about the client by accessing the `engine.current` object. This object contains the following parameters: - -- `engine.current.user: leancloud.User`: The user logged in on the client (according to the `X-LC-Session` header sent from the client). -- `engine.current.session_token: str`: The `sessionToken` sent from the client (from the `X-LC-Session` header). -- `engine.current.meta: dict`: More information about the client. For now, it only contains a `remote_address` property, which contains the IP address of the client. - - - - -Each Cloud Function has the following parameters: - -- `$params: array`: The arguments sent from the client. -- `$user: User`: The user logged in on the client (according to the `X-LC-Session` header sent from the client). -- `$meta: array`: More information about the client. For now, it only contains a `$meta['remoteAddress']` property, which contains the IP address of the client. - - - - -Your program can access the following data from within the Cloud Function: - -- `@EngineFunctionParam`: The arguments sent from the client. -- `EngineRequestContext`: More information about the client. You can get the sessionToken of the user logged in on the client from `EngineRequestContext.getSessionToken()` (according to the `X-LC-Session` header sent from the client) and the IP address of the client from `EngineRequestContext.getRemoteAddress()`. - - - - -Your program can access the following data from within the Cloud Function: - -- `LCEngineFunctionParam`: The arguments sent from the client. -- `LCEngineRequestContext`: More information about the client. You can get the sessionToken of the user logged in on the client from `LCEngineRequestContext.SessionToken` (according to the `X-LC-Session` header sent from the client) and the IP address of the client from `LCEngineRequestContext.RemoteAddress`. - - - - -`leancloud.FunctionRequest` will be passed into the Cloud Function as an argument. It contains the following properties: - -- `Params` contains the arguments sent from the client. -- `CurrentUser` contains the user logged in on the client (according to the `X-LC-Session` header sent from the client). When defining your Cloud Function with `Define`, you can provide `WithoutFetchUser()` as an optional argument to prevent the Cloud Function from obtaining the logged in user form the client. -- `SessionToken` contains the `sessionToken` sent from the client (from the `X-LC-Session` header). When defining your Cloud Function with `Define`, you can provide `WithoutFetchUser()` as an optional argument to prevent the Cloud Function from obtaining the `sessionToken` from the client. -- `Meta` contains more information about the client. For now, it only contains a `remoteAddress` property, which contains the IP address of the client. - - -
    - -### Invoking Cloud Functions With Client SDKs - -You can invoke Cloud Functions with client SDKs: - - - -```cs -try { - Dictionary response = await LCCloud.Run("averageStars", parameters: new Dictionary { - { "movie", "A Quiet Place" } - }); - // Handle result -} catch (LCException e) { - // Handle error -} -``` - -<> - -```java -// Construct a dictionary containing the arguments to be passed to the server -Map dicParameters = new HashMap(); -dicParameters.put("movie", "A Quiet Place"); - -// Invoke the Cloud Function named averageStars with arguments -LCCloud.callFunctionInBackground("averageStars", dicParameters).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - - } - - @Override - public void onNext(Object object) { - // succeed. - } - - @Override - public void onError(Throwable throwable) { - // failed. - } - - @Override - public void onComplete() { - - } -}); -``` - -Similar to how you use `LCQuery`, the Java SDK provides a `callFunctionWithCacheInBackground` method that allows your program to cache the results fetched from the server. You can specify the `CachePolicy` as well as the maximum age of the cache. - - - -```objc -// Construct a dictionary containing the arguments to be passed to the server -NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"A Quiet Place" - forKey:@"movie"]; - -// Invoke the Cloud Function named averageStars with arguments -[LCCloud callFunctionInBackground:@"averageStars" - withParameters:dicParameters - block:^(id object, NSError *error) { - if(error == nil){ - // Handle result - } else { - // Handle error - } -}]; -``` - -```swift -LCEngine.run("averageStars", parameters: ["movie": "A Quiet Place"]) { (result) in - switch result { - case .success(value: let resultValue): - print(resultValue) - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - Map response = await LCCloud.run('averageStars', params: { 'movie': 'A Quiet Place' }); - // Handle result -} on LCException catch (e) { - // Handle error -} -``` - -```js -var paramsJson = { - movie: "A Quiet Place", -}; -AV.Cloud.run("averageStars", paramsJson).then( - function (data) { - // Handle result - }, - function (err) { - // Handle error - } -); -``` - -```python -from leancloud import cloud - -cloud.run('averageStars', movie='A Quiet Place') -``` - -```php -use \LeanCloud\Engine\Cloud; -$params = array( - "movie" => "A Quiet Place" -); -Cloud::run("averageStars", $params); -``` - -```go -// ... -averageStars, err := leancloud.Run("averageStars", map[string]string{"movie": "A Quiet Place"}) -if err != nil { - panic(err) -} -// ... -``` - - - -When running a Cloud Function, the arguments and responses will be treated as JSON objects. To pass LCObjects through requests and responses, you can invoke the Cloud Function with RPC, which makes the SDK serialize and deserialize LCObjects. This allows your program within the Cloud Function and on the client side to have access to the LCObjects directly: - - - -```cs -try { - LCObject response = await LCCloud.RPC("averageStars", parameters: new Dictionary { - { "movie", "A Quiet Place" } - }); - // Handle result -} catch (LCException e) { - // Handle error -} -``` - -<> - -```java -// Construct arguments -Map dicParameters = new HashMap<>(); -dicParameters.put("movie", "A Quiet Place"); - -LCCloud.callRPCInBackground("averageStars", dicParameters).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - - } - - @Override - public void onNext(LCObject avObject) { - // succeed. - } - - @Override - public void onError(Throwable throwable) { - // failed - } - - @Override - public void onComplete() { - - } -}); -``` - -Similar to how you use `LCQuery`, the Java SDK provides a `callRPCWithCacheInBackground` method that allows your program to cache the results fetched from the server. You can specify the `CachePolicy` as well as the maximum age of the cache. - - - -```objc -NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"A Quiet Place" - forKey:@"movie"]; - -[LCCloud rpcFunctionInBackground:@"averageStars" - withParameters:parameters - block:^(id object, NSError *error) { - if(error == nil){ - // Handle result - } - else { - // Handle error - } -}]; -``` - -```swift -LCEngine.call("averageStars", parameters: ["movie": "A Quiet Place"]) { (result) in - switch result { - case .success(object: let object): - if let object = object { - print(object) - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - LCObject response = await LCCloud.rpc('averageStars', params: { 'movie': 'A Quiet Place' }); - // Handle result -} on LCException catch (e) { - // Handle error -} -``` - -```js -var paramsJson = { - movie: "A Quiet Place", -}; - -AV.Cloud.rpc("averageStars", paramsJson).then( - function (object) { - // Handle result - }, - function (error) { - // Handle error - } -); -``` - -```python -from leancloud import cloud - -cloud.rpc('averageStars', movie='A Quiet Place') -``` - -```php -// Not supported yet -``` - -```go -// ... -averageStars := 0 -if err := leancloud.RPC("averageStars", Review{Movie: "A Quiet Place"}, &averageStars); err != nil { - panic(err) -} -// .. -``` - - - -When using RPC, the SDK will process the objects in the following forms that exist in the requests and responses: - -- Single LCObject -- A HashMap containing LCObjects -- An array containing LCObjects - -Everything else in the requests and responses will remain in their original forms. - -### Invoking Other Cloud Functions Within a Cloud Function - - - - -When invoking a Cloud Function under the Node.js runtime environment, a local invocation will be triggered by default, which means that the invocation won’t make an HTTP request like what the client SDK would do. - -```js -AV.Cloud.run("averageStars", { - movie: "A Quiet Place", -}).then( - function (data) { - // Succeeded; data is the response - }, - function (error) { - // Handle error - } -); -``` - -To force the invocation to make an HTTP request, provide the `remote: true` option. This is useful when you run the Node.js SDK in a different group or outside Cloud Engine: - -```js -AV.Cloud.run("averageStars", { movie: "A Quiet Place" }, { remote: true }).then( - function (data) { - // Succeeded - }, - function (error) { - // Handle error - } -); -``` - -Here `remote` constitutes a part of the `options` object, which is an optional parameter of `AV.Cloud.run`. `options` contains the following parameters: - -- `remote?: boolean`: The `remote` option used in the example above. Defaults to `false`. -- `user?: AV.User`: The user used to invoke the Cloud Function. Often used when `remote` is `false`. -- `sessionToken?: string`: The `sessionToken` used to invoke the Cloud Function. Often used when `remote` is `true`. -- `req?: http.ClientRequest | express.Request`: Used to provide properties like `remoteAddress` for the Cloud Function being invoked. - - - - -When invoking a Cloud Function under the Python runtime environment, a remote invocation will be triggered by default. -The code below will make an HTTP request to invoke a Cloud Function on Cloud Engine. - -```python -from leancloud import cloud - -cloud.run('averageStars', movie='A Quiet Place') -``` - -To invoke a local Cloud Function (i.e., a Cloud Function that exists in the current process) or to save an HTTP request when invoking a Cloud Function from within Cloud Engine, use `leancloud.cloud.run.local` instead of `leanengine.cloud.run`. This will let your program invoke the Cloud Function in the current process without making an HTTP request. - - - - -Local invocations of Cloud Functions are not supported by the Java SDK. -To reuse the same set of code, we recommend that you put them into basic Java functions and invoke them from different Cloud Functions. - - - - -When you invoke a Cloud Function from Cloud Engine, a local invocation will be made instead of a remote invocation that relies on an HTTP request. - -```php -try { - $result = Cloud::run("averageStars", array("movie" => "A Quiet Place")); -} catch (\Exception $ex) { - // Cloud Function error -} -``` - -To make an invocation using an HTTP request, use the `runRemote` method: - -```php -try { - $token = User::getCurrentSessionToken(); // Invoke the Cloud Function with a specific `sessionToken`; optional - $result = Cloud::runRemote("averageStars", array("movie" => "A Quiet Place"), $token); -} catch (\Exception $ex) { - // Cloud Function error -} -``` - - - - -Local invocations of Cloud Functions are not supported by the .NET SDK. -To reuse the same set of code, we recommend that you put them into basic functions and invoke them from different Cloud Functions. - - - - -To trigger a local invocation, use `Engine.Run`: - -```go -averageStars, err := leancloud.Engine.Run("averageStars", Review{Movie: "A Quiet Place"}) -if err != nil { - panic(err) -} -``` - -To make an invocation using an HTTP request, use `Client.Run`. - -`Run` has the following optional parameters: - -- `WithSessionToken(token)` can be used to provide a `sessionToken` for the current invocation -- `WithUser(user)` can be used to provide a user for the current invocation - - - - -### Error Codes for Cloud Functions - -You can define custom error codes according to [HTTP status codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes). - - - - -```js -AV.Cloud.define("customErrorCode", function (request) { - throw new AV.Cloud.Error("Custom error message.", { code: 123 }); -}); -``` - - - - -```python -from leancloud import LeanEngineError - -@engine.define -def custom_error_code(**params): - raise LeanEngineError(123, 'Custom error message.') -``` - - - - -```java -@EngineFunction() -public static void customErrorCode() throws Exception { - throw new LCException(123, "Custom error message."); -} -``` - - - - -```php -Cloud::define("customErrorCode", function($params, $user) { - throw new FunctionError("Custom error message.", 123); -}); -``` - - - - -```cs -[LCEngineFunction("throwLCException")] -public static void ThrowLCException() { - throw new LCException(123, "Custom error message."); -} -``` - - - - -```go -leancloud.Engine.Define("customErrorCode", func(req *leancloud.FunctionRequest) (interface{}, error) { - return nil, leancloud.CloudError{123, "Custom error message."} -}) -``` - - - - -The response gotten by the client would look like `{ "code": 123, "error": "Custom error message." }`. - -### Timeouts for Cloud Functions - -The default timeout for invoking a Cloud Function is 15 seconds. If the timeout is exceeded, the client will get a response with the HTTP status code `503` and the body being `The request timed out on the server`. -Keep in mind that even though the client has already gotten a response, the Cloud Function might still be running, although its response will not be received by the client anymore (the error message `Can't set headers after they are sent` will be printed to the log). -In certain cases, the client will receive the `524` or `141` error instead of the `503` error. - -#### Avoiding Timeouts - -To prevent Cloud Functions and scheduled tasks from causing timeouts, we recommend that you convert time-consuming tasks in your code to queued tasks that can be executed asynchronously. - -For example, you can: - -1. Create a table in the Data Storage service with a column named `status`; -2. When a task is received, create an object in the table with `status` being `PROCESSING`, then respond to the client with the object’s `id`. -3. When a task is completed, update the `status` of the corresponding object to `COMPLETED` or `FAILED`; -4. You can look up the status of a task with its `id` on the dashboard at any time. - -The method introduced above might not apply to `before` hooks. -Although you can ensure a `before` hook to complete within the timeout using the method above, -its ability to abort an action when an error occurs will not work anymore. -If you have a `before` hook that keeps exceeding the timeout, consider changing it to an `after` hook. -For example, if a `beforeSave` hook needs to invoke a time-consuming API to tell if a user’s comment is spam, -you can change it to an `afterSave` hook that invokes the API after the comment has been saved and deletes the comment if it is spam. - -## Hooks for Data Storage - -Hooks are a special type of Cloud Functions that **get triggered automatically by the system** when certain events take place. Keep in mind that: - -- Importing data on the dashboard will not trigger any hooks. -- Be careful not to form [infinite loops](#avoiding-infinite-loops) with your hooks. -- Hooks don’t work with the `_Installation` table. -- Hooks only work with the classes that belong to the current application, which don’t include those bound to the current application. - -For `before` hooks, if an error is returned by the function, the original operation will be aborted. This means that you can reject an operation by having the function throw an error. This does not apply to `after` hooks since the operation would’ve been completed when the function starts running. - -D{object} -D-->E(new) -E-->|beforeSave|H{error?} -H-->N(No) -N-->B[create new object on the cloud] -B -->|afterSave|C((done)) -H-->Y(Yes) -Y-->Z((interrupted)) -D-->F(existing) -F-->|beforeUpdate|I{error?} -I-->Y -I-->V(No) -V-->G[update existing object on the cloud] -G-->|afterUpdate| C -`} -/> - -|beforeDelete|H{error?} -H-->Y(Yes) -Y-->Z((interrupted)) -H-->N(No) -N-->B[delete object on the cloud] -B -->|afterDelete|C((done)) -`} -/> - -Our SDK will run authentication to ensure that the hook requests received by it are the legitimate ones sent from the Data Storage service. If the authentication fails, you may see a message saying `Hook key check failed`. If you see this message when debugging your project locally, please ensure that you have started your project with the CLI. - -### BeforeSave - -This hook can be used to perform operations like cleanups and verifications before a new object gets created. For example, if we need to truncate each comment to 140 characters: - - - - -```js -AV.Cloud.beforeSave("Review", function (request) { - var comment = request.object.get("comment"); - if (comment) { - if (comment.length > 140) { - // Truncate the comment and add '…' - request.object.set("comment", comment.substring(0, 140) + "…"); - } - } else { - // Don’t save the object and return an error - throw new AV.Cloud.Error("No comment provided!"); - } -}); -``` - -In the example above, `request.object` is the `AV.Object` we are performing our operation on. `request` has another property besides `object`: - -- `currentUser?: AV.User`: The user who triggered the operation. - -The `request` parameter (and its properties) are available in other hooks as well. - - - - -```python -@engine.before_save('Review') # `Review` is the name of the class that the hook will be applied to -def before_review_save(review): - comment = review.get('comment') - if not comment: - raise leancloud.LeanEngineError(message='No comment provided!') - if len(comment) > 140: - review.comment.set('comment', comment[:140] + '…') -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.beforeSave) -public static LCObject reviewBeforeSaveHook(LCObject review) throws Exception { - if (StringUtil.isEmpty(review.getString("comment"))) { - throw new Exception("No comment provided!"); - } else if (review.getString("comment").length() > 140) { - review.put("comment", review.getString("comment").substring(0, 140) + "…"); - } - return review; -} -``` - - - - -```php -Cloud::beforeSave("Review", function($review, $user) { - $comment = $review->get("comment"); - if ($comment) { - if (strlen($comment) > 140) { - // Truncate the comment and add '…' - $review->set("comment", substr($comment, 0, 140) . "…"); - } - } else { - // Don’t save the object and return an error - throw new FunctionError("No comment provided!", 101); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeSave)] -public static LCObject ReviewBeforeSave(LCObject review) { - if (string.IsNullOrEmpty(review["comment"])) { - throw new Exception("No comment provided!"); - } - string comment = review["comment"] as string; - if (comment.Length > 140) { - review["comment"] = string.Format($"{comment.Substring(0, 140)}..."); - } - return review; -} -``` - - - - -```go -leancloud.Engine.BeforeSave("Review", func(req *ClassHookRequest) (interface{}, error) { - review := new(Review) - if err := req.Object.Clone(review); err != nil { - return nil, err - } - - if len(review.Comment) > 140 { - review.Comment = review.Comment[:140] - } - - return review, nil -}) -``` - - - - -### AfterSave - -This hook can be used to perform operations after a new object has been created. For example, if we need to update the total number of comments after a comment has been created: - - - - -```js -AV.Cloud.afterSave("Comment", function (request) { - var query = new AV.Query("Post"); - return query.get(request.object.get("post").id).then(function (post) { - post.increment("comments"); - return post.save(); - }); -}); -``` - - - - -```python -import leancloud - -@engine.after_save('Comment') # `Comment` is the name of the class that the hook will be applied to -def after_comment_save(comment): - post = leancloud.Query('Post').get(comment.id) - post.increment('commentCount') - try: - post.save() - except leancloud.LeanCloudError: - raise leancloud.LeanEngineError(message='An error occurred while trying to save the post.') -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.afterSave) -public static void reviewAfterSaveHook(LCObject review) throws Exception { - LCObject post = review.getLCObject("post"); - post.fetch(); - post.increment("comments"); - post.save(); -} -``` - - - - -```php -Cloud::afterSave("Comment", function($comment, $user) { - $query = new Query("Post"); - $post = $query->get($comment->get("post")->getObjectId()); - $post->increment("commentCount"); - try { - $post->save(); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred while trying to save the post: " . $ex->getMessage()); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.AfterSave)] -public static async Task ReviewAfterSave(LCObject review) { - LCObject post = review["post"] as LCObject; - await post.Fetch(); - post.Increment("comments", 1); - await post.Save(); -} -``` - - - - -```go -leancloud.Engine.AfterSave("Review", func(req *ClassHookRequest) error { - review := new(Review) - if err := req.Object.Clone(review); err != nil { - return err - } - - if err := client.Object(review.Post).Update(map[string]interface{}{ - "comment": leancloud.OpIncrement(1), - }); err != nil { - return leancloud.CloudError{Code: 500, Message: err.Error()} - } - - return nil -}) -``` - - - - -In the following example, we are adding a `from` property to each new user: - - - - -```js -AV.Cloud.afterSave("_User", function (request) { - console.log(request.object); - request.object.set("from", "LeanCloud"); - return request.object.save().then(function (user) { - console.log("Success!"); - }); -}); -``` - -Although we don’t care about the return value of an `after` hook, we still recommend that you have your function return a Promise. This ensures that you can see error messages and call stacks in the standard output when unexpected errors occur. - - - - -```python -@engine.after_save('_User') -def after_user_save(user): - print(user) - user.set('from', 'LeanCloud') - try: - user.save() - except LeanCloudError, e: - print('Error: ', e) -``` - - - - -```java -@EngineHook(className = "_User", type = EngineHookType.afterSave) -public static void userAfterSaveHook(LCUser user) throws Exception { - user.put("from", "LeanCloud"); - user.save(); -} -``` - - - - -```php -Cloud::afterSave("_User", function($userObj, $currentUser) { - $userObj->set("from", "LeanCloud"); - try { - $userObj->save(); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred while trying to save the user: " . $ex->getMessage()); - } -}); -``` - - - - -```cs -[LCEngineClassHook("_User", LCEngineObjectHookType.AfterSave)] -public static async Task UserAfterSave(LCObject user) { - user["from"] = "LeanCloud"; - await user.Save(); -} -``` - - - - -```go -leancloud.Engine.AfterSave("_User", func(req *ClassHookRequest) error{ - if req.User != nil { - if err := client.User(req.User).Set("from", "LeanCloud"); err != nil { - return err - } - } - return nil -}) -``` - - - - -### BeforeUpdate - -This hook can be used to perform operations before an existing object gets updated. You will be able to know which fields have been updated and reject the operation if necessary: - - - - -```js -AV.Cloud.beforeUpdate("Review", function (request) { - // If `comment` has been updated, check its length - if (request.object.updatedKeys.indexOf("comment") != -1) { - if (request.object.get("comment").length > 140) { - // Reject the update if the comment is too long - throw new AV.Cloud.Error( - "The comment should be no longer than 140 characters." - ); - } - } -}); -``` - - - - -```python -@engine.before_update('Review') -def before_hook_object_update(obj): - # If `comment` has been updated, check its length - if 'comment' in obj.updated_keys and len(obj.get('comment')) > 140: - # Reject the update if the comment is too long - raise leancloud.LeanEngineError(message='The comment should be no longer than 140 characters.') -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.beforeUpdate) -public static LCObject reviewBeforeUpdateHook(LCObject review) throws Exception { - List updateKeys = EngineRequestContext.getUpdateKeys(); - for (String key : updateKeys) { - // If `comment` has been updated, check its length - if ("comment".equals(key) && review.getString("comment").length()>140) { - throw new Exception("The comment should be no longer than 140 characters."); - } - } - return review; -} -``` - - - - -```php -Cloud::beforeUpdate("Review", function($review, $user) { - // If `comment` has been updated, check its length - if (in_array("comment", $review->updatedKeys) && - strlen($review->get("comment")) > 140) { - throw new FunctionError("The comment should be no longer than 140 characters."); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeUpdate)] -public static LCObject ReviewBeforeUpdate(LCObject review) { - ReadOnlyCollection updatedKeys = review.GetUpdatedKeys(); - if (updatedKeys.Contains("comment")) { - string comment = review["comment"] as string; - if (comment.Length > 140) { - throw new Exception("The comment should be no longer than 140 characters."); - } - } - return review; -} -``` - - - - -```go -leancloud.Engine.BeforeUpdate("Review", func(req *ClassHookRequest) (interface{}, error) { - updatedKeys = req.UpdatedKeys() - for _, v := range updatedKeys { - if v == "comment" { - comment, ok := req.Object.Raw()["comment"].(string) - if !ok { - return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"} - } - - if len(comment) > 140 { - return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"} - } - } - } - - return nil, nil -}) -``` - - - - -Modifications done directly to the object passed in will not be saved. To reject the update, you can have the function return an error. - -The object passed in is a temporary object not saved to the database yet. The object might not be the same as the one saved to the database in the end because there might be atomic operations like self-increments, array operations, and adding or updating relations that will happen later. - -### AfterUpdate - -This hook might lead to infinite loops if you use it improperly, causing additional API calls and even extra charges. Please make sure to read [Avoiding Infinite Loops](#avoiding-infinite-loops) carefully. - -This hook can be used to perform operations after an existing object has been updated. Similar to BeforeUpdate, you will be able to know which fields have been updated. - - - - -```js -AV.Cloud.afterUpdate("Review", function (request) { - if (request.object.updatedKeys.indexOf("comment") != -1) { - if (request.object.get("comment").length < 5) { - console.log(review.ObjectId + " looks like spam: " + comment); - } - } -}); -``` - - - - -```python -@engine.after_update('Review') -def after_review_update(article): - if 'comment' in obj.updated_keys and len(obj.get('comment')) < 5: - print(review.ObjectId + " looks like spam: " + comment) -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.afterUpdate) -public static void reviewAfterUpdateHook(LCObject review) throws Exception { - List updateKeys = EngineRequestContext.getUpdateKeys(); - for (String key : updateKeys) { - if ("comment".equals(key) && review.getString("comment").length()<5) { - LOGGER.d(review.ObjectId + " looks like spam: " + comment); - } - } -} -``` - - - - -```php -Cloud::afterUpdate("Review", function($review, $user) { - if (in_array("comment", $review->updatedKeys) && - strlen($review->get("comment")) < 5) { - error_log(review.ObjectId . " looks like spam: " . comment); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.AfterUpdate)] -public static void ReviewAfterUpdate(LCObject review) { - ReadOnlyCollection updatedKeys = review.GetUpdatedKeys(); - if (updatedKeys.Contains("comment")) { - string comment = review["comment"] as string; - if (comment.Length < 5) { - Console.WriteLine($"{review.ObjectId} looks like spam: {comment}"); - } - } -} -``` - - - - -```go -leancloud.Engine.AfterUpdate("Review", func(req *ClassHookRequest) error { - updatedKeys := req.UpdatedKeys() - for _, v := range updatedKeys { - if v == "comment" { - comment, ok := req.Object.Raw()["comment"].(string) - if !ok { - return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"} - } - - if len(comment) < 5 { - fmt.Println(req.Object.ID, " looks like spam: ", comment)) - } - } - } - - return nil -}) -``` - - - - -### BeforeDelete - -This hook can be used to perform operations before an object gets deleted. For example, to check if an `Album` contains any `Photo`s before it gets deleted: - - - - -```js -AV.Cloud.beforeDelete("Album", function (request) { - // See if any `Photo` belongs to this album - var query = new AV.Query("Photo"); - var album = AV.Object.createWithoutData("Album", request.object.id); - query.equalTo("album", album); - return query.count().then( - function (count) { - if (count > 0) { - // The `delete` operation will be aborted - throw new AV.Cloud.Error( - "Cannot delete an album if it still has photos in it." - ); - } - }, - function (error) { - throw new AV.Cloud.Error( - "Error " + - error.code + - " occurred when finding photos: " + - error.message - ); - } - ); -}); -``` - - - - -```python -import leancloud - -@engine.before_delete('Album') # `Album` is the name of the class that the hook will be applied to -def before_album_delete(album): - query = leancloud.Query('Photo') - query.equal_to('album', album) - try: - matched_count = query.count() - except leancloud.LeanCloudError: - raise engine.LeanEngineError(message='An error occurred with LeanEngine.') - if count > 0: - # The `delete` operation will be aborted - raise engine.LeanEngineError(message='Cannot delete an album if it still has photos in it.') -``` - - - - -```java -@EngineHook(className = "Album", type = EngineHookType.beforeDelete) -public static LCObject albumBeforeDeleteHook(LCObject album) throws Exception { - LCQuery query = new LCQuery("Photo"); - query.whereEqualTo("album", album); - int count = query.count(); - if (count > 0) { - // The `delete` operation will be aborted - throw new Exception("Cannot delete an album if it still has photos in it."); - } else { - return album; - } -} -``` - - - - -```php -Cloud::beforeDelete("Album", function($album, $user) { - $query = new Query("Photo"); - $query->equalTo("album", $album); - try { - $count = $query->count(); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred when getting photo count: {$ex->getMessage()}"); - } - if ($count > 0) { - // The `delete` operation will be aborted - throw new FunctionError("Cannot delete an album if it still has photos in it."); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Album", LCEngineObjectHookType.BeforeDelete)] -public static async Task AlbumBeforeDelete(LCObject album) { - LCQuery query = new LCQuery("Photo"); - query.WhereEqualTo("album", album); - int count = await query.Count(); - if (count > 0) { - throw new Exception("Cannot delete an album if it still has photos in it."); - } - return album; -} -``` - - - - -```go -leancloud.Engine.BeforeDelete("Album", func(req *ClassHookRequest) (interface{}, error) { - photo := new(Photo) - if err := req.Object.Clone(photo); err != nil { - return nil, err - } - - count, err := client.Class("Photo").NewQuery().EqualTo("album", photo.Album).Count() - if err != nil { - return nil, err - } - - if count > 0 { - return nil, leancloud.CloudError{Code: 500, Message: "Cannot delete an album if it still has photos in it."} - } - - fmt.Println("Deleted.") - - return nil, nil -}) -``` - - - - -### AfterDelete - -This hook can be used to perform operations like decrementing counters and removing associated objects after an object has been deleted. For example, to delete the photos in an album after the album has been deleted: - - - - -```js -AV.Cloud.afterDelete("Album", function (request) { - var query = new AV.Query("Photo"); - var album = AV.Object.createWithoutData("Album", request.object.id); - query.equalTo("album", album); - return query - .find() - .then(function (posts) { - return AV.Object.destroyAll(posts); - }) - .catch(function (error) { - console.error( - "Error " + - error.code + - " occurred when finding photos: " + - error.message - ); - }); -}); -``` - - - - -```python -import leancloud - -@engine.after_delete('Album') # `Album` is the name of the class that the hook will be applied to -def after_album_delete(album): - query = leancloud.Query('Photo') - query.equal_to('album', album) - try: - query.destroy_all() - except leancloud.LeanCloudError: - raise leancloud.LeanEngineError(message='An error occurred with LeanEngine.') -``` - - - - -```java -@EngineHook(className = "Album", type = EngineHookType.afterDelete) -public static void albumAfterDeleteHook(LCObject album) throws Exception { - LCQuery query = new LCQuery("Photo"); - query.whereEqualTo("album", album); - List result = query.find(); - if (result != null && !result.isEmpty()) { - LCObject.deleteAll(result); - } -} -``` - - - - -```php -Cloud::afterDelete("Album", function($album, $user) { - $query = new Query("Photo"); - $query->equalTo("album", $album); - try { - $photos = $query->find(); - LeanObject::destroyAll($photos); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred when getting photo count: {$ex->getMessage()}"); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Album", LCEngineObjectHookType.AfterDelete)] -public static async Task AlbumAfterDelete(LCObject album) { - LCQuery query = new LCQuery("Photo"); - query.WhereEqualTo("album", album); - ReadOnlyCollection result = await query.Find(); - if (result != null && result.Count > 0) { - await LCObject.DeleteAll(result); - } -} -``` - - - - -```go -leancloud.Engine.AfterDelete("Album", func(req *ClassHookRequest) error { - photo := new(Photo) - if err := req.Object.Clone(photo); err != nil { - return nil, err - } - - count, err := client.Class("Photo").NewQuery().EqualTo("album", photo.Album).Count() - if err != nil { - return nil, err - } - - if count > 0 { - return nil, leancloud.CloudError{Code: 500, Message: "An error occurred with LeanEngine."} - } - - fmt.Println("Deleted.") - - return nil, nil -}) -``` - - - - -### OnVerified - -This hook can be used to perform operations after a user has verified their email or phone number. For example: - - - - -```js -AV.Cloud.onVerified("sms", function (request) { - console.log("User " + request.object + " is verified by SMS."); -}); -``` - -The `object` in the example above can be replaced by `currentUser` since the user who triggered the operation is also the one that we want to perform operations on. -This also applies to the `onLogin` hook, which we’ll introduce later. - - - - -```python -@engine.on_verified('sms') -def on_sms_verified(user): - print(user) -``` - - - - -```java -@EngineHook(className = "_User", type = EngineHookType.onVerifiedSMS) -public static void userOnVerifiedHook(LCUser user) throws Exception { - LOGGER.d("User " + user.getObjectId() + " has verified their phone number."); -} - -@EngineHook(className = "_User", type = EngineHookType.onVerifiedEmail) -public static void userOnVerifiedHook(LCUser user) throws Exception { - LOGGER.d("User " + user.getObjectId() + " has verified their email."); -} -``` - - - - -```php -Cloud::onVerifed("sms", function($user, $meta) { - error_log("User {$user->getUsername()} is verified by SMS."); -}); -``` - - - - -```cs -[LCEngineUserHook(LCEngineUserHookType.OnSMSVerified)] -public static void OnVerifiedSMS(LCUser user) { - Console.WriteLine($"User {user.ObjectId} has verified their phone number."); -} - -[LCEngineUserHook(LCEngineUserHookType.OnEmailVerified)] -public static void OnVerifiedEmail(LCUser user) { - Console.WriteLine($"User {user.ObjectId} has verified their email."); -} -``` - - - - -```go -leancloud.Engine.OnVerified("sms", func(req *ClassHookRequest) error { - fmt.Println("User ", req.User.ID, " has verified their phone number.") -}) - -leancloud.Engine.OnVerified("email", func(req *ClassHookRequest) error { - fmt.Println("User ", req.User.ID, " has verified their email.") -}) -``` - - - - -Fields like `emailVerified` should not be updated here since the system will update them automatically. - -This hook is an `after` hook. - -### OnLogin - -This hook can be used to perform operations before a user gets logged in. For example, to prevent users on the blocklist from logging in: - - - - -```js -AV.Cloud.onLogin(function (request) { - // The user has not logged in yet, so the user data is in `request.object` - console.log("User " + request.object + " is trying to log in."); - if (request.object.get("username") === "noLogin") { - // If the program throws an error, the user won’t be able to log in (the client will receive a 401 error) - throw new AV.Cloud.Error("Forbidden"); - } -}); -``` - - - - -```python -@engine.on_login -def on_login(user): - print(user) - if user.get('username') == 'noLogin': - # If the program throws a `LeanEngineError`, the user won’t be able to log in (the client will receive a 401 error) - raise LeanEngineError('Forbidden') - # If the program doesn’t throw an error, the user will be able to log in -``` - - - - -```java -@EngineHook(className = "_User", type = EngineHookType.onLogin) -public static LCUser userOnLoginHook(LCUser user) throws Exception { - if ("noLogin".equals(user.getUsername())) { - throw new Exception("Forbidden"); - } else { - return user; - } -} -``` - - - - -```php -Cloud::onLogin(function($user) { - error_log("User {$user->getUsername()} is trying to log in."); - if ($user->get("blocked")) { - // If the program throws an error, the user won’t be able to log in (the client will receive a 401 error) - throw new FunctionError("Forbidden"); - } - // If the program doesn’t throw an error, the user will be able to log in -}); -``` - - - - -```cs -[LCEngineUserHook(LCEngineUserHookType.OnLogin)] -public static LCUser OnLogin(LCUser user) { - if (user.Username == "noLogin") { - throw new Exception("Forbidden"); - } - return user; -} -``` - - - - -```go -leancloud.Engine.OnLogin(func(req *ClassHookRequest) error { - fmt.Println("User ", req.User.ID, " has logged in.") -}) -``` - - - - -This hook is a `before` hook. - -### OnAuthData - -This hook gets triggered when a user’s `authData` gets updated. You can perform verifications and modifications to users’ `authData` with this hook. For example: - - - - -```js -AV.Cloud.onAuthData(function (request) { - let authData = request.authData; - console.log(authData); - - if (authData.weixin.code === "12345") { - authData.weixin.accessToken = "45678"; - } else { - // Verification failed; an error will be thrown and the user won’t be able to log in - throw new AV.Cloud.Error("invalid code"); - } - // Verification succeeded; the verified or modified `authData` will be returned and the user will be able to log in - return authData; -}); -``` - - - - -```python -@engine.on_auth_data -def on_auth_data(auth_data): - if auth_data['weixin']['code'] == '12345': - # Verification succeeded; the verified or modified `auth_data` will be returned and the user will be able to log in - auth_data['weixin']['code'] = '45678' - return auth_data - else: - # Verification failed; an error will be thrown and the user won’t be able to log in - raise LeanEngineError('invalid code') -``` - - - - -```cs -[LCEngineUserHook(LCEngineUserHookType.OnAuthData)] -public static Dictionary OnAuthData(Dictionary authData) { - if (authData.TryGetValue("fake_platform", out object tokenObj)) { - if (tokenObj is Dictionary token) { - // Emulating the verification process - if (token["openid"] as string == "123" && token["access_token"] as string == "haha") { - LCLogger.Debug("Auth data Verified OK."); - } else { - throw new Exception("Invalid auth data."); - } - } else { - throw new Exception("Invalid auth data"); - } - } - return authData; -``` - - - - -This hook is a `before` hook. - -### Avoiding Infinite Loops - -You might be curious about why saving `post` in `AfterUpdate` won’t trigger the same hook again. -This is because Cloud Engine has processed the object being passed in to prevent infinite loops. - -However, this mechanism won’t work when one of the following situations happens: - -- Calling `fetch` on the object being passed in. -- Reconstructing the object being passed in. - -In these cases, you might want to call the method for disabling hooks yourself: - - - - -```js -// Directly editing and saving an object won’t trigger the `afterUpdate` hook -request.object.set("foo", "bar"); -request.object.save().then(function (obj) { - // Your code -}); - -// If `fetch` has been called on the object, call `disableAfterHook` on the new object to prevent the object from triggering the hook -request.object - .fetch() - .then(function (obj) { - obj.disableAfterHook(); - obj.set("foo", "bar"); - return obj.save(); - }) - .then(function (obj) { - // Your code - }); - -// If the object has been reconstructed, call `disableAfterHook` on the new object to prevent the object from triggering the hook -var obj = AV.Object.createWithoutData("Post", request.object.id); -obj.disableAfterHook(); -obj.set("foo", "bar"); -obj.save().then(function (obj) { - // Your code -}); -``` - - - - -```python -@engine.after_update('Post') -def after_post_update(post): - # Directly editing and saving an object won’t trigger the `after_update` hook - post.set('foo', 'bar') - post.save() - - # If `fetch` has been called on the object, call `disable_after_hook` on the new object to prevent the object from triggering the hook -request.object - post.fetch() - post.disable_after_hook() - post.set('foo', 'bar') - - # If the object has been reconstructed, call `disable_after_hook` on the new object to prevent the object from triggering the hook - post = leancloud.Object.extend('Post').create_without_data(post.id) - post.disable_after_hook() - post.save() -``` - - - - -```java -@EngineHook(className="Post", type = EngineHookType.afterUpdate) -public static void afterUpdatePost(LCObject post) throws LCException { - // Directly editing and saving an object won’t trigger the `afterUpdate` hook - post.put("foo", "bar"); - post.save(); - - // If `fetch` has been called on the object, call `disableAfterHook` on the new object to prevent the object from triggering the hook - post.fetch(); - post.disableAfterHook(); - post.put("foo", "bar"); - - // If the object has been reconstructed, call `disableAfterHook` on the new object to prevent the object from triggering the hook - post = LCObject.createWithoutData("Post", post.getObjectId()); - post.disableAfterHook(); - post.save(); -} -``` - - - - -```php -Cloud::afterUpdate("Post", function($post, $user) { - // Directly editing and saving an object won’t trigger the `afterUpdate` hook - $post->set('foo', 'bar'); - $post->save(); - - // If `fetch` has been called on the object, call `disableAfterHook` on the new object to prevent the object from triggering the hook - $post->fetch(); - $post->disableAfterHook(); - $post->set('foo', 'bar'); - $post->save(); - - // If the object has been reconstructed, call `disableAfterHook` on the new object to prevent the object from triggering the hook - $post = LeanObject::create("Post", $post->getObjectId()); - $post->disableAfterHook(); - $post->save(); -}); -``` - - - - -```cs -// Directly editing and saving an object won’t trigger the `afterUpdate` hook -post["foo"] = "bar"; -await post.Save(); - -// If `fetch` has been called on the object, call `DisableAfterHook` on the new object to prevent the object from triggering the hook -await post.Fetch(); -post.DisableAfterHook(); -post["foo"] = "bar"; - -// If the object has been reconstructed, call `DisableAfterHook` on the new object to prevent the object from triggering the hook -post = LCObject.CreateWithoutData("Post", post.ObjectId); -post.DisableAfterHook(); -await post.Save(); -``` - - - - -### Error Codes for Hooks - -Use the following way to define error codes for hooks like `BeforeSave`: - - - - -```js -AV.Cloud.beforeSave("Review", function (request) { - // Convert the object to a string with JSON.stringify() - throw new AV.Cloud.Error( - JSON.stringify({ - code: 123, - message: "An error occurred.", - }) - ); -}); -``` - - - - -```python -@engine.before_save('Review') # `Review` is the name of the class that the hook will be applied to -def before_review_save(review): - comment = review.get('comment') - if not comment: - raise leancloud.LeanEngineError( - code=123, - message='An error occurred.' - ) -``` - - - - -```java -@EngineHook(className = "Review", type = EngineHookType.beforeSave) -public static LCObject reviewBeforeSaveHook(LCObject review) throws Exception { - throw new LCException(123, "An error occurred."); -} -``` - - - - -```php -Cloud::beforeSave("Review", function($review, $user) { - $comment = $review->get("comment"); - if (!$comment) { - throw new FunctionError(json_encode(array( - "code" => 123, - "message" => "An error occurred.", - ))); - } -}); -``` - - - - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeDelete)] -public static void ReviewBeforeDelete(LCObject review) { - throw new LCException(123, "An error occurred."); -} -``` - - - - -```go -leancloud.Engine.BeforeSave("Review", func(req *ClassHookRequest) (interface{}, error) { - return nil, leancloud.CloudError{Code: 123, Message: "An error occurred."} -}) -``` - - - - -The response gotten by the client would look like `Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." }`. The client can then **slice the string** to get the error message. - -### Timeouts for Hooks - -The timeout for `before` hooks is 10 seconds while that for other hooks is 3 seconds. If a hook is triggered by another Cloud Function (like when a `BeforeSave` or `AfterSave` hook gets triggered when a new object gets created), its timeout will be cut down to the remaining timeout of the triggering Cloud Function. - -For example, if a `BeforeSave` hook is triggered by a Cloud Function that has already been executed for 13 seconds, the hook will only have 2 seconds left. See [Timeouts for Cloud Functions](#timeouts-for-cloud-functions) for more information. - -## Hooks for Instant Messaging - -See [Hooks and System Conversations](/sdk/im/guide/systemconv/)[Hooks and System Conversations](https://docs.leancloud.app/en/sdk/im/guide/systemconv/) for more information. - -## Writing Cloud Functions Online - -You can write Cloud Functions online using the dashboard instead of creating and deploying a project. Keep in mind that: - -- Deploying the Cloud Functions you write online will override the project deployed with Git or the CLI. -- You can only write Cloud Functions and hooks online. You can’t upload static web pages or write dynamic routes with the web interface. -- You can only use the JavaScript SDK together with some built-in Node.js modules (listed in the table below). You can’t import other modules as dependencies. - -**Path** - -![](https://capacity-files.lcfile.com/dmLYApCx0fTIx0FCsrJbs4PnXwPGkiA2/Frame%202%20%281%29.png) - - -![Writing Cloud Functions online](https://capacity-files.lcfile.com/0M7S7C8siMMPwDfK44LwwIq2Nq5c0l3S/engine-snippets-list-en.png) - -On ** > Manage deployment > Your group > Deploy > Edit online**, you can: - -- **Create function**: When creating a Cloud Function, you can specify its type, name, code, and comments. Click _Create_ to save the Cloud Function. Types of Cloud Functions include _Function_ (basic Cloud Function), _Hook_, and _Global_ (shared logic used by multiple Cloud Functions). -- **Deploy**: You can select the environment to deploy your Cloud Functions to and click _Deploy_ to proceed. -- **Preview**: This will combine all the Cloud Functions into a single code snippet. You can verify the code in your Cloud Functions or override the file named `cloud.js` in a project created from the demo project with the code shown here. -- **Maintain Cloud Functions**: You can edit existing Cloud Functions, view histories, and delete Cloud Functions. - -After you edit your Cloud Functions, make sure to click **Deploy** to have the edits take effect. - -The online editor only supports Node.js at this time. The latest version, `v3`, uses Node.js 8.x and the Node.js SDK 3.x. With this version selected, you’ll need to write your functions using Promise. Packages provided by default include async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, and xml2js. - -
    -More about the SDK versions used by the online editor - -| Version | Node.js SDK | JS SDK | Node.js | Notes | Dependencies available | -| ------- | ----------- | ------ | ------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| v0 | 0.x | 0.x | 0.12 | Not recommended anymore | moment, request, underscore | -| v1 | 1.x | 1.x | 4 | | async, bluebird, co, ejs, handlebars, joi, lodash, marked, moment, q, request, superagent, underscore | -| v2 | 2.x | 2.x | 6 | Please write your functions using Promise | async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js | -| v3 | 3.x | 3.x | 8 | Please write your functions using Promise | async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js | - -**Upgrading from v0 to v1:** - -- Upgraded the JS SDK to [1.0](https://github.com/leancloud/javascript-sdk/releases/tag/v1.0.0). -- Requires you to obtain the user from `request.currentUser` instead of `AV.User.current`. -- Requires you to manually provide the `user` object when calling `AV.Cloud.run`. - -**Upgrading from v1 to v2:** - -- Upgraded the JS SDK to [2.0](https://github.com/leancloud/javascript-sdk/releases/tag/v2.0.0) (Promise must be used instead of callback). -- Removed `AV.Cloud.httpRequest`. -- **Requires** you to return a Promise from each Cloud Function and throw an `AV.Cloud.Error` for each error. - -**Upgrading from v2 to v3:** - -- Upgraded the JS SDK to [3.0](https://github.com/leancloud/javascript-sdk/releases/tag/v3.0.0) (includes changes to the behavior of `AV.Object.toJSON`). - -
    - -
    -Difference between writing Cloud Functions online and deploying a project - -When you write Cloud Functions online, the way our system deals with your functions is to join them together, generate a Cloud Engine project, and deploy this project. - -You can consider writing Cloud Functions online to be the same as deploying a project: they both form a complete project in the end. - -When you define functions online, you can quickly generate a Cloud Engine project without using our SDK or Git. - -You may also choose to create and deploy a project written based on our SDK. - -The two options are mutually exclusive: when you deploy with one option, the deployment using the other option will be overridden. - -
    - -
    -Migrating from writing Cloud Functions online to deploying a project - -1. Install the CLI according to the [CLI Guide](/sdk/engine/cli/) and create a new project by running `lean new`. Select `Node.js > Express` as the template (this is our demo project for Node.js). -2. Navigate to ** > Manage deployment > Your group > Deploy > Edit online** and click **Preview**. Copy the code shown here and replace the code in `cloud.js` with it. -3. Run `lean up`. Now you can test your Cloud Functions and hooks on . Once you’re done, run `lean deploy` to deploy your code to Cloud Engine. If you have standard instances, make sure to run `lean publish` as well. -4. After deploying your project, watch the dashboard to see if there are any errors. - -If you have been using the Node.js SDK 0.x, you’ll have to update your code to avoid compatibility issues. -For example, `AV.User.current()` should be changed to `request.currentUser`. - -
    - -## Viewing and Running Cloud Functions - -** > Manage deployment > Your group > Deploy** shows all the Cloud Functions and hooks defined within each group, including their names, groups, and QPM (requests per minute). You can click **Run** to run a Cloud Function from the dashboard. - -Cloud Functions from all the groups in your application will be displayed here regardless of the way they’re deployed (writing Cloud Functions online or deploying a project). - -![List of Cloud Functions](https://capacity-files.lcfile.com/Pjd2Wh7BLtaaQ1wtc9CxDwPMKJNlIRQa/engine-functions-list-en.png) - -## Production Environment vs. Staging Environment - -Your app comes with a production environment and a staging environment. When triggering a Cloud Function from within a Cloud Engine instance using the SDK, no matter explicitly or implicitly (by triggering hooks), the SDK will trigger the function defined in the same environment as the instance. For example, with `beforeDelete` defined, if an object is deleted with the SDK in the staging environment, the `beforeDelete` hook in the staging environment will be triggered. - -When triggering Cloud Functions outside of a Cloud Engine instance using the SDK, no matter explicitly or implicitly, `X-LC-Prod` will be set to `1` by default, which means that the Cloud Functions in the production environment will be triggered. For historical reasons, there are some differences among the SDKs: - -- For Node.js, PHP, Java, and C# SDKs, the production environment will be used by default. -- For the Python SDK, when debugging locally with lean-cli, the staging environment will be used if it exists. Otherwise, the production environment will be used. -- For Java example projects, [java-war-getting-started] and [spring-boot-getting-started], when debugging locally with lean-cli, the staging environment will be used if it exists. Otherwise, the production environment will be used (same as the Python SDK). - -[java-war-getting-started]: https://github.com/leancloud/java-war-getting-started/ -[spring-boot-getting-started]: https://github.com/leancloud/spring-boot-getting-started/ - -You can specify the environment being used with the SDK: - - - -```cs -LCCloud.IsProduction = true; // production (default) -LCCloud.IsProduction = false; // staging -``` - -```java -LCCloud.setProductionMode(true); // production -LCCloud.setProductionMode(false); // staging -``` - -```objc -[LCCloud setProductionMode:YES]; // production (default) -[LCCloud setProductionMode:NO]; // staging -``` - -```swift -// production by default - -// staging -do { - let environment: LCApplication.Environment = [.cloudEngineDevelopment] - let configuration = LCApplication.Configuration(environment: environment) - try LCApplication.default.set( - id: {{appid}}, - key: {{appkey}}, - serverURL: "https://please-replace-with-your-customized.domain.com", - configuration: configuration) -} catch { - print(error) -} -``` - -```dart -LCCloud.setProduction(true); // production (default) -LCCloud.setProduction(false); // staging -``` - -```js -AV.setProduction(true); // production (default) -AV.setProduction(false); // staging -``` - -```python -leancloud.use_production(True) # production (default) -leancloud.use_production(False) # staging -# Needs to be called before `leancloud.init` -``` - -```php -Client::useProduction(true); // production (default) -Client::useProduction(false); // staging -``` - -```go -// Not supported yet (will always use the production environment) -``` - - - -If you’re using the trial mode, there will only be a production environment and you won’t be able to switch to the staging environment. - -## Scheduled Tasks - -You can set up scheduled tasks to run your Cloud Functions periodically. For example, you can have your app clean up temporary data every night, send push notifications to users every Monday, etc. The time set for a scheduled task can be accurate to a **second**. - -The timeout applied to ordinary Cloud Functions also applies to scheduled tasks. See [Avoiding Timeouts](#avoiding-timeouts) for more information. - -If a scheduled task triggers more than 30 `400` (Bad Request) or `502` (Bad Gateway) errors within 24 hours, the task will be disabled and you will get an email regarding the issue. The error `timerAction short-circuited and no fallback available` will also be printed to the log. - -After deploying your program to Cloud Engine, go to ** > Manage deployment > Your group > Scheduled tasks** and click **Create scheduled task** to create a scheduled task for a Cloud Function. For example, if we have a function named `logTimer`: - - - - -```js -AV.Cloud.define("logTimer", function (request) { - console.log("This log is printed by logTimer."); -}); -``` - - - - -```python -@engine.define -def logTimer(movie, **params): - print('This log is printed by logTimer.') -``` - - - - -```java -@EngineFunction("logTimer") -public static float logTimer throws Exception { - LogUtil.avlog.d("This log is printed by logTimer."); -} -``` - - - - -```php -Cloud::define("logTimer", function($params, $user) { - error_log("This log is printed by logTimer."); -}); -``` - - - - -```cs -[LCEngineFunction("logTimer")] -public static void LogTimer() { - Console.WriteLine("This log is printed by logTimer."); -} -``` - - - - -```go -leancloud.Engine.Define("logTimer", func(req *FunctionRequest) (interface{}, error) { - fmt.Println("This log is printed by logTimer.") - return nil, nil -}) -``` - - - - -![List of scheduled tasks](https://capacity-files.lcfile.com/X9r5bB8ztvAaAIk3TeqQbQXyKyX4zwvI/engine-cronjobs-list-en.png) - -You can specify the times a scheduled task gets triggered using one of the following expressions: - -- CRON expression -- Interval in seconds - -Take the CRON expression as an example. To print logs at 8am every Monday, create a scheduled task for the function `logTimer` using a **CRON expression** and enter `0 0 8 ? * MON` for it. - -See [Cloud Queue Guide § CRON Expressions](/sdk/engine/functions/cloud-queue/#cron-expressions) for more information about CRON expressions. - -When creating a scheduled task, you can optionally fill in the following two fields: - -- Params: The arguments passed to the Cloud Function as a JSON object. -- Error-handling policy: Whether to retry or cancel the task when it fails due to a Cloud Function timeout. See [Cloud Queue Guide § Error-Handling Policy](/sdk/engine/functions/cloud-queue/#what-situations-will-be-affected-by-the-error-handling-policy-deliverymode) for more information. - -**Last execution** is the time and result of the last execution. This information will only be retained for 5 minutes. Under the details of the execution: - -- `status`: The status of the task; could be `success` or `failed` -- `uniqueId`: A unique ID for the task -- `finishedAt`: The exact time when the task finished (for succeeded tasks only) -- `statusCode`: The HTTP status returned by the Cloud Function (for succeeded tasks only) -- `result`: The response body returned by the Cloud Function (for succeeded tasks only) -- `error`: Error message (for failed tasks only) -- `retryAt`: The time the task will be rerun (for failed tasks only) - -You can view the logs of all the scheduled tasks on ** > Manage deployment > Your group > Logs**. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/rest-api.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/rest-api.mdx deleted file mode 100644 index b8f79a923..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/rest-api.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Cloud Engine REST API -sidebar_label: REST API -sidebar_position: 5 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -:::info -This article serves as a thorough introduction to Cloud Engine’s REST API. To learn about Cloud Functions and hooks, see [Cloud Functions and Hooks Guide](/sdk/engine/functions/guides). -::: - -Cloud Engine offers a REST API for users to access Cloud Functions. When you invoke Cloud Functions using our SDKs, the SDKs use the same API for accessing our service. - -We recommend that you use [Postman](https://www.postman.com/) for testing the REST API. - -## Base URL - -The base URL of the REST API is the API domain of your application (represented with `{{host}}`). You can bind and view API domains on the dashboard. See [Domain](/sdk/storage/guide/setup-dotnet/#domain)[Binding Your Domains](/sdk/domain/guide/) for more information. - -See [this article](/sdk/storage/guide/rest/#request-format)[this article](https://docs.leancloud.app/en/sdk/storage/guide/rest/#request-format) for more information about the format of the request. - -## Overview - -| URL | HTTP | Function | -| ----------------------------------- | ---- | ---------------------------------------------------------------------- | -| /1.1/functions/<functionName> | POST | Invoke a Cloud Function | -| /1.1/call/<functionName> | POST | Invoke a Cloud Function with `LCObject`s as the argument or the result | - -## Staging Environment vs. Production Environment - -When invoking Cloud Functions from the client with the REST API, you can set the HTTP header `X-LC-Prod` to specify the environment. - -- `X-LC-Prod: 0` means to use the staging environment -- `X-LC-Prod: 1` means to use the production environment - -When invoking Cloud Functions with the SDK, the SDK will set the HTTP header `X-LC-Prod` according to the current environment. See [Cloud Functions and Hooks Guide § Production Environment vs. Staging Environment](/sdk/engine/functions/guides/#production-environment-vs-staging-environment) for more information. - -## Cloud Functions - -You can invoke a Cloud Function by accessing `POST /functions/:name`. Both the argument and the result are in JSON. -For example, to get the score of a movie with the movie’s name: - -```sh -curl -X POST -H "Content-Type: application/json; charset=utf-8" \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -d '{"movie":"A Quiet Place"}' \ -https://{{host}}/1.1/functions/averageStars -``` - -Response: - -```json -{ - "result": { - "movie": "A Quiet Place", - "stars": "2.5" - } -} -``` - -If a user needs to be used to invoke the Cloud Function, provide the `sessionToken` of the user through `X-LC-Session`: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{}' \ - https://{{host}}/1.1/functions/hello -``` - -If you need to use `LCObject`s as the argument or the result of a Cloud Function, you can invoke the function with RPC by accessing `POST /1.1/call/:name`: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"__type": "Object", "className": "Post", "pubUser": "LeanCloud Staff"}' \ - https://{{host}}/1.1/call/addPost -``` - -Response: - -```json -{ - "result": { - "__type": "Object", - "className": "Post", - "pubUser": "Staff" - } -} -``` - -When invoking a Cloud Function with RPC, you can provide or receive an object containing multiple `LCObject`s. -For example, if a Cloud Function returns an array containing a number and a `Todo` object, the result of the RPC invocation would be: - -```json -{ - "result": [ - 1, - { - "title": "R&D Weekly Meeting", - "createdAt": { - "__type": "Date", - "iso": "2019-04-28T08:34:12.932Z" - }, - "updatedAt": { - "__type": "Date", - "iso": "2019-04-28T08:34:12.932Z" - }, - "objectId": "5cc5658443e78cb53fe7b731", - "__type": "Object", - "className": "Todo" - } - ] -} -``` - -When invoking a Cloud Function with RPC using the SDK, the SDK will automatically deserialize everything. - -If a timeout occurs, the client will get a response with its HTTP status code being 503, 524, or 141. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/sdk.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/sdk.mdx deleted file mode 100644 index d8c16a2a7..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/functions/sdk.mdx +++ /dev/null @@ -1,734 +0,0 @@ ---- -title: Cloud Engine SDK Guide -sidebar_label: Cloud Engine SDK -sidebar_position: 3 ---- - -import EngineRuntimes from "/src/docComponents/MultiLang/engine"; -import TabItem from "@theme/TabItem"; -import { Conditional } from "/src/docComponents/conditional"; - -:::info -This article serves as a thorough introduction to the Cloud Engine SDK. To learn about the usage of Cloud Functions and hooks, see [Cloud Functions and Hooks Guide](/sdk/engine/functions/guides). -::: - -The Cloud Engine SDKs for most of the runtime environments are based on the [Data Storage](/sdk/storage/features)[Data Storage](/sdk/storage/overview/) SDK, offering additional features that support Cloud Functions and hooks. With the Cloud Engine SDK, you can easily build backend apps that can be deployed to Cloud Engine. - -## Integrating the Cloud Engine SDK - -If you’ve created your project based on one of our demo projects, you already have the Cloud Engine SDK integrated. To integrate the Cloud Engine SDK into other projects, follow these steps: - - - - -```sh -npm install leanengine leancloud-storage -``` - -Then mount the Cloud Engine middleware to Express: - -```js title='app.js' -var express = require("express"); -var AV = require("leanengine"); - -AV.init({ - appId: process.env.LEANCLOUD_APP_ID, - appKey: process.env.LEANCLOUD_APP_KEY, - masterKey: process.env.LEANCLOUD_APP_MASTER_KEY, -}); - -var app = express(); -app.use(AV.express()); -app.listen(process.env.LEANCLOUD_APP_PORT); -``` - -`AV.express` accepts an optional parameter, `options`, that contains the following optional properties: - -- `onError`: A global error handling function, which will be called when a Cloud Function (including hook) throws an error. You can have your program send error reports using this function. -- `ignoreInvalidSessionToken`: If set to `true`, the invalid `sessionToken`s (from the `X-LC-session` header) sent from clients will be ignored. If set to `false`, a `401` error, `{"code": 211, "error": "Verify sessionToken failed, maybe login expired: ..."}`, will be thrown in this case. - -
    -If you’re using Koa - -```js title='app.js' -var koa = require("koa"); -var AV = require("leanengine"); - -AV.init({ - appId: process.env.LEANCLOUD_APP_ID, - appKey: process.env.LEANCLOUD_APP_KEY, - masterKey: process.env.LEANCLOUD_APP_MASTER_KEY, -}); - -var app = koa(); -app.use(AV.koa()); -app.listen(process.env.LEANCLOUD_APP_PORT); -``` - -
    - -
    -Differences among the different versions of the Node.js SDK - -The Node.js SDK has the following versions: - -- `0.x`: This is the initial version of the Node.js SDK. It’s not very compatible with Node.js 4.x and above. -- `1.x`: The global `currentUser` has been deprecated in this version. The JavaScript SDK has also been upgraded to 1.x. This version provides support for Koa and Node.js 4.x and above. -- `2.x`: This version starts supporting Cloud Functions and hooks written with Promise. Some deprecated features (like `AV.Cloud.httpRequest`) have been removed. Callbacks with the Backbone style are not supported anymore. -- `3.x`: This is the **recommended** version. The JavaScript SDK has become a peer dependency in this version and you can customize the version of the JavaScript SDK. The JavaScript SDK has been upgraded to 3.x. - -
    - -You can find the source code of the Node.js SDK on [GitHub](https://github.com/leancloud/leanengine-node-sdk). - -
    - - -Add `leancloud` to `requirements.txt`: - -``` -leancloud>=2.9.4,<3.0.0 -``` - -When running and debugging your project locally, you can run the following command under your project’s directory to install the dependencies. - -```sh -pip install -r requirements.txt -``` - -Now you can import the SDK into your code. Since `wsgi.py` is the first file being executed, we recommend that you initialize the Python SDK in this file: - -```python -import os -import leancloud - -APP_ID = os.environ['LEANCLOUD_APP_ID'] -APP_KEY = os.environ['LEANCLOUD_APP_KEY'] -MASTER_KEY = os.environ['LEANCLOUD_APP_MASTER_KEY'] - -leancloud.init(APP_ID, app_key=APP_KEY, master_key=MASTER_KEY) - -leancloud.use_master_key(True) -``` - -By default, the permission for the SDK to use the `masterKey` is enabled, which means that permission restrictions like ACL will be skipped. See [Using MasterKey](#using-masterkey) for more information. - -
    -Differences between leancloud-sdk and leancloud on PyPI - -`leancloud-sdk` is the older version of the Python SDK, which is not maintained anymore. Please use `leancloud`. - -
    - -You can find the source code of the Python SDK on [GitHub](https://github.com/leancloud/python-sdk). - -
    - - -Add the following configurations to `pom.xml` to add the Java SDK: - -```xml - - - cn.leancloud - engine-core - 8.2.1 - - -``` - -Initialize the SDK in your code: - -```java -import cn.leancloud.LCCloud; -import cn.leancloud.LCObject; -import cn.leancloud.core.GeneralRequestSignature; -import cn.leancloud.LeanEngine; - -String appId = System.getenv("LEANCLOUD_APP_ID"); -String appKey = System.getenv("LEANCLOUD_APP_KEY"); -String appMasterKey = System.getenv("LEANCLOUD_APP_MASTER_KEY"); -String hookKey = System.getenv("LEANCLOUD_APP_HOOK_KEY"); - -LeanEngine.initialize(appId, appKey, appMasterKey); - -GeneralRequestSignature.setMasterKey(appMasterKey); -``` - -The permission for the SDK to use the `masterKey` is enabled by the code above, which means that permission restrictions like ACL will be skipped. See [Using MasterKey](#using-masterkey) for more information. - -You can find the source code of the Java SDK on [GitHub](https://github.com/leancloud/java-unified-sdk). - - - - -Install the dependency:: - -```sh -composer require leancloud/leancloud-sdk -``` - -Initialize the SDK: - -```php -use \LeanCloud\Client; - -Client::initialize( - getenv("LEANCLOUD_APP_ID"), - getenv("LEANCLOUD_APP_KEY"), - getenv("LEANCLOUD_APP_MASTER_KEY") -); - -Client::useMasterKey(true); -``` - -The permission for the SDK to use the `masterKey` is enabled by the code above, which means that permission restrictions like ACL will be skipped. See [Using MasterKey](#using-masterkey) for more information. - -You can find the source code of the PHP SDK on [GitHub](https://github.com/leancloud/php-sdk). - - - - -Add the dependency:: - -```sh -dotnet add package LeanCloud.Storage -``` - -Initialize the SDK: - -```cs -LCEngine.Initialize(services); -``` - -You can find the source code of the .NET SDK on [GitHub](https://github.com/leancloud/csharp-sdk). - - - - -Add the dependency: - -```go -import "github.com/leancloud/go-sdk/leancloud" -``` - -Initialize the SDK: - -```go -client := leancloud.NewEnvClient() -leancloud.Engine.Init(client) -``` - -The Go SDK offers an interface in the form of the HTTP function in the standard library for any framework to access it. Take **echo** as an example: - -```go -// ./adapters/echo.go -//... -func Echo(e *echo.Echo) { - e.Any("/1/*", echo.WrapHandler(leancloud.Engine.Handler()), setResponseContentType) - e.Any("/1.1/*", echo.WrapHandler(leancloud.Engine.Handler()), setResponseContentType) - e.Any("/__engine/*", echo.WrapHandler(leancloud.Engine.Handler()), setResponseContentType) -} - -func setResponseContentType(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - c.Response().Header().Set("Content-Type", "application/json; charset=UTF-8") - return next(c) - } -} -``` - -The **Echo** function accepts an instance of echo and binds the interfaces from the Go SDK that provide features relevant to Cloud Engine to routes starting with `/1/`, `/1.1/`, and `/__engine/`. This allows Cloud Engine’s basic features to work as expected. - -Most Go-based web frameworks provide functions for you to convert the HTTP handler from the standard library to specialized handlers. As long as you can integrate the two components mentioned above into the framework you’re using, you will be able to integrate the Cloud Engine SDK into your project. - -You can find the source code of the Go SDK on [GitHub](https://github.com/leancloud/go-sdk). - - -
    - -## Using the Data Storage Service - -Once you have integrated the SDK, you will be able to access the [Data Storage](/sdk/storage/features)[Data Storage](/sdk/storage/overview/) service from Cloud Engine to store data. See the articles for Data Storage SDKs for more information. - -Functions related to the Data Storage service can be accessed from Cloud Functions and hooks as well as other places in your project (like the web framework you’re using). - -## Using MasterKey - -Since Cloud Engine runs in a trusted environment on the server side, when your program accesses the data from the Data Storage service, it can skip permission checks enforced by ACL and class permissions with the Master Key of your application. Some interfaces that are available to administrators, which require the Master Key, can also be accessed from Cloud Engine. - -This means that you can enable the use of Master Key globally in your program to have the permission checks enforced by ACL and class permissions skipped. Certain APIs requiring the `Master Key` can also be accessed from Cloud Engine. - - - - -To enable Master Key globally: - -```js title='sever.js' -AV.Cloud.useMasterKey(); -``` - -If this line of code is not added to your program, the Master Key won’t be used by default. This means that data protected by ACL can’t be accessed by your program. You’ll have to specify a `sessionToken` for each operation to have the operation executed with a user’s permission: - -```js -const post = new Post(); -post.save( - { author: user } - // Or use `request.sessionToken` (enable `Cloud.CookieSession` for web hosting) - // { - // sessionToken: user.getSessionToken() - // } -); -``` - -You can also enable `Master Key` for a single operation to skip permission checks for it: - -```js -post.destroy({ useMasterKey: true }); -``` - -With Master Key enabled globally, you can use `useMasterKey: false` to disable Master Key for a single operation. - - - - -```python -# Often in wsgi.py -leancloud.use_master_key(True) -``` - - - - -```java -// Often in src/…/AppInitListener.java -RequestSignImplementation.setMasterKey(appMasterKey); -``` - - - - -```php -// Often in src/app.php -Client::useMasterKey(true); -``` - - - - -```cs -LCApplication.UseMasterKey = true; -``` - - - - -To enable `Master Key` for an operation like `Create`, `Set`, or `Update`, append `UseMasterKey()` as an optional parameter of the operation. - - - - -If you’re unsure whether you should enable Master Key globally, see the tips below: - -- If your project contains a lot of operations that need special permissions or work with global data that doesn’t belong to users, we would recommend that you enable `Master Key` globally. Your program needs to check permissions for the requests sent from users carefully. -- If your project contains a lot of operations that are relevant to users’ data and need to follow ACL, we would recommend that you disable `Master Key`. Your program can pass the user’s `sessionToken` into each operation when needed. - -## Managing User Statuses - -:::caution -Since Cloud Engine’s runtime environments run on multiple hosts and processes, storing sessions in memory (e.g., with [express-session](https://github.com/expressjs/session)’s default storage, `MemoryStore`, or PHP’s built-in `$_SESSION`) could lead to unexpected behavior of your program. -::: - -### Using HTTP Header - -If the pages of your application are mostly rendered by the browser, we recommend that you log users in on the frontend with the SDK and obtain the session token with the SDK’s interface, then send the session token to the backend using HTTP header. - - - - -The example below logs a user in, gets the session token with `user.getSessionToken(), and sends it to the backend: - -```js -AV.User.login(user, pass).then((user) => { - return fetch("/profile", { - headers: { - "X-LC-Session": user.getSessionToken(), - }, - }); -}); -``` - -Here is the Node.js code on the backend: - -```js -app.get("/profile", function (req, res) { - AV.User.become(req.headers["x-lc-session"]) - .then((user) => { - res.send(user); - }) - .catch((err) => { - res.send({ error: err.message }); - }); -}); - -app.post("/todos", function (req, res) { - var todo = new Todo(); - todo - .save(req.body, { sessionToken: req.headers["x-lc-session"] }) - .then(() => { - res.send(todo); - }) - .catch((err) => { - res.send({ error: err.message }); - }); -}); -``` - - - - -### CookieSession - -If the pages in your application are mostly rendered on the server side, you can use the cookie session component provided by some of our SDKs, which will store the session token from the Data Storage service as a cookie. This makes it easier for the server side to manage user statuses. - -:::danger -If you decide to use this method, make sure you have taken measures to [prevent CSRF attacks](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html). - -CSRF tokens are often used to prevent CSRF attacks. The way it works is that the server will send a random string (CSRF token) to the client (can be passed through cookies) and the client has to include this token in the header or the body of each request that may have side effects. The server will need to check if the CSRF token is correct. -::: - - - - -If the pages in your application are mostly rendered on the server side (like with EJS or Pug) and the frontend part doesn’t need to perform data operations with the JavaScript SDK, you can use the `AV.Cloud.CookieSession` middleware to maintain user statuses with cookies: - -```js -// Express -app.use( - AV.Cloud.CookieSession({ - secret: "my secret", - maxAge: 3600000, - fetchUser: true, - }) -); -// Koa -app.use( - AV.Cloud.CookieSession({ - framework: "koa", - secret: "my secret", - maxAge: 3600000, - fetchUser: true, - }) -); -``` - -You need to pass in a `secret` to sign the cookie (required). The middleware will keep the user status of `AV.User` with the cookie. The next time the user opens your app, the SDK will automatically check if the user is logged in. If the user is logged in, you can obtain the current user with `req.currentUser`. - -`AV.Cloud.CookieSession` accepts the following options: - -- **fetchUser**: Whether to automatically `fetch` the `AV.User` object of the current user. Defaults to `false`. When set to `true`, each HTTP request will invoke an API call to `fetch` the user. When set to `false`, only the `id` if the `req.currentUser` object (i.e., the `objectId` in the `_User` table) can be accessed by default. You’ll have to manually `fetch` the user when needed. -- **name**: The name of the cookie. Defaults to `avos.sess`. -- **maxAge**: The max age of the cookie in milliseconds. - -You won’t be able to get a user’s data using `AV.User.current()` in the Node.js SDK. You will have to: - -- Get the user data with `request.currentUser` -- Pass the user object explicitly to other functions - -
    -An example of a site that allows users to log in - -```js -app.post("/login", function (req, res) { - AV.User.logIn(req.body.username, req.body.password).then( - function (user) { - res.saveCurrentUser(user); // save cookie - res.redirect("/profile"); - }, - function (error) { - res.redirect("/login"); - } - ); -}); - -app.get("/profile", function (req, res) { - if (req.currentUser) { - res.send(req.currentUser); - } else { - res.redirect("/login"); - } -}); - -app.get("/logout", function (req, res) { - req.currentUser.logOut(); - res.clearCurrentUser(); // clear cookie - res.redirect("/profile"); -}); -``` - -
    - -
    -Browsers’ restrictions on accessing cookies under different origins (SameSite) - -Starting from Chrome 80, the default value of `SameSite` has been set to `Lax`. If the frontend part of your application is not deployed to Cloud Engine and your app needs to send POST requests with cookies, you’ll need to set `SameSite` to `none`. - -`AV.Cloud.CookieSession` will pass all the parameters to the browser’s `cookies.set()`, so you can pass in `sameSite`: - -```js -AV.Cloud.CookieSession({ sameSite: "none" }); -``` - -Keep in mind that: - -- `SameSite` has to be sent together with the `Secure` attribute. Please make sure the client of your app accesses Cloud Engine through HTTPS. -- Please only set `SameSite` to `none` when necessary, or the risk for your application to suffer from CSRF attacks may increase. - -
    - -
    - - -The Python SDK offers a WSGI middleware called `leancloud.engine.CookieSessionMiddleware`, which maintains the user status of `leancloud.User` with cookie. To use this middleware, find the following line in `wsgi.py`: - -```python -application = engine -``` - -Then replace it with the following line: - -```python -application = leancloud.engine.CookieSessionMiddleware(engine, secret=YOUR_APP_SECRET) -``` - -You need to pass in a `secret` to sign the cookie (required). The middleware will keep the user status of `AV.User` with the cookie. The next time the user opens your app, the SDK will automatically check if the user is logged in. If the user is logged in, you can obtain the current user with `leancloud.User.get_current()`. - -When being initialized, `leancloud.engine.CookieSessionMiddleware` accepts the following optional options: - -- **name**: The key of the cookie storing the session token. Defaults to `leancloud:session`. -- **excluded_paths**: Specifies the URL paths that should not take care of the session token (like those for static files). Accepts a `list` as its value. -- **fetch_user**: Whether to fetch the user from the Data Storage service. If set to `False`, the user data obtained from `leancloud.User.get_current()` will contain nothing except `session_token`. You’ll have to manually fetch the user data with `fetch()`. If set to `True`, `fetch()` will be automatically called on the user object. Defaults to `False`. -- **expires**: The expirtaion data of the cookie (see [Werkzeug Document](https://werkzeug.palletsprojects.com/en/2.1.x/http/#werkzeug.http.dump_cookie)). -- **max_age**: The max age of the cookie in seconds (see [Werkzeug Document](https://werkzeug.palletsprojects.com/en/2.1.x/http/#werkzeug.http.dump_cookie)). - - - - -Cloud Engine offers a module called `LeanCloud\Storage\CookieStorage`, which is used to maintain the user status of `User` objects with cookies. To use it, add the following code to `app.php`: - -```php -use \LeanCloud\Storage\CookieStorage; -Client::setStorage(new CookieStorage(60 * 60 * 24, "/")); -``` - -`CookieStorage` accepts a max age in seconds and a path as the scope of the cookie. The default max age is 7 days. - -You can get the current user with `User::getCurrentUser()`. The code below shows how you can implement a website that allows users to log in: - -```php -$app->get('/login', function($req, $res) { - // login page -}); - -$app->post('/login', function($req, $res) { - $params = $req->getQueryParams(); - try { - User::logIn($params["username"], $params["password"]); - return $res->withRedirect('/profile'); - } catch (Exception $ex) { - return $res->withRedirect('/login'); - } -}); - -$app->get('/profile', function($req, $res) { - $user = User::getCurrentUser(); - if ($user) { - return $res->getBody()->write($user->getUsername()); - } else { - return $res->withRedirect('/login'); - } -}); - -$app->get('/logout', function($req, $res) { - User::logOut(); - return $res->redirect("/"); -}); -``` - -A simple log-in page could look like this: - -```html - - - -
    - - - - - -
    - - -``` - -`CookieStorage` can be used to store other properties as well: - -```php -$cookieStorage = Client::getStorage(); -$cookieStorage->set("key", "val"); -``` - -
    -
    - -## FAQ - -### How to redirect to HTTPS using the SDK? - -We recommend that you enable _Force HTTPS_ when binding custom domains instead of using the middleware from the SDK for redirecting. - - - -:::info -However, _Force HTTPS_ only works with dedicated IP addresses at this time. -If you’re using CDN, you’ll have to implement the redirect within the code of your project. -::: - - - -
    -View how to redirect to HTTPS using the SDK (not recommended) - -Some SDKs provide a middleware for redirecting to HTTPS. With this middleware enabled, visitors to your site will be redirected to HTTPS. - - - - -Node.js (Express): - -```js -app.enable("trust proxy"); -app.use(AV.Cloud.HttpsRedirect()); -``` - -Node.js (Koa): - -```js -app.proxy = true; -app.use(AV.Cloud.HttpsRedirect({ framework: "koa" })); -``` - - - - -```python -import leancloud - -application = get_your_wsgi_func() - -application = leancloud.HttpsRedirectMiddleware(application) -``` - - - - -```php -SlimEngine::enableHttpsRedirect(); -$app->add(new SlimEngine()); -``` - - - - -```java -LeanEngine.setHttpsRedirectEnabled(true); -``` - - - - -```cs -app.UseHttpsRedirection(); -``` - - - -
    - -### How to print the HTTP requests made by the SDK? - - - - -You can set the environment variable `DEBUG=leancloud:request` to have the HTTP requests made by the SDK printed. When debugging locally, you can start your project like this: - -```sh -env DEBUG=leancloud:request lean up -``` - -When the SDK sends a request to the server, you will see a log like this: - -```sh -leancloud:request request(0) +0ms GET https://{{host}}/1.1/classes/Todo?&where=%7B%7D&order=-createdAt { where: '{}', order: '-createdAt' } -leancloud:request response(0) +220ms 200 {"results":[{"content":"1","createdAt":"2016-08-09T06:18:13.028Z","updatedAt":"2016-08-09T06:18:13.028Z","objectId":"57a975a55bbb5000643fb690"}]} -``` - -We don’t recommend that you enable these logs on the server side, or you will see excessive logs on the dashboard. You can use `DEBUG=leancloud:request:error` to only have errors printed. - - - - -### Why is the data in a Pointer field only partially sent to the client? - - - - -> You can upgrade the JavaScript SDK and the Node.js SDK to 3.0 and above to solve the problem. - -When a Cloud Function sends a response to the client, it will call the `AV.Object#toJSON` method to serialize the result to a JSON object. In the early versions of the SDK, `AV.Object#toJSON` will only return the metadata of the Pointer for Pointer fields and won’t include the other fields. We have redesigned the serialization-related logic in [JavaScript SDK 3.0](https://github.com/leancloud/javascript-sdk/releases/tag/v3.0.0). **You can upgrade the JavaScript SDK and the Node.js SDK to 3.0 and above to solve the problem**. - -If you can’t upgrade the SDK used by your project to the newer version, you can bypass the problem in the following way: - -```javascript -AV.Cloud.define("querySomething", function (req, res) { - var query = new AV.Query("Something"); - // `user` is a `Pointer` field in the `Something` table - query.include("user"); - query - .find() - .then(function (results) { - // Serialize manually - results.forEach(function (result) { - result.set( - "user", - result.get("user") ? result.get("user").toJSON() : null - ); - }); - // Return the result to the client - res.success(results); - }) - .catch(res.error); -}); -``` - - - - -The Python SDK will only return the metadata of Pointer. You will have to manually perform a query and serialize the result (see the code for Node.js). - - - - -### When invoking a Cloud Function with RPC, why do I unexpectedly get an empty object as the result? - - - - -For a Cloud Function defined with the Node.js SDK, if the function returns a value other than an `AVObject` (like a string or number), the RPC invocation will get an empty object (`{}`). -Similarly, if an array containing members other than `AVObject`s gets returned, those members will be serialized to `{}`. -This problem will be fixed in the next major version of the Node.js SDK (4.0). -To bypass the problem, you can return an object (`{}`) containing the response. - - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/overview.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/overview.mdx deleted file mode 100644 index 69b8e17a0..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/overview.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Cloud Engine Overview -sidebar_label: Overview -sidebar_position: 1 ---- - -import PlatformIntroduction from "./_partials/platform-introduction.mdx"; -import { Conditional } from "/src/docComponents/conditional"; - - - -## Deploying Apps - -:::tip -Ready to deploy your first app? Check out [this guide](/sdk/engine/deploy/getting-started/) to learn how to get your app running on Cloud Engine in seconds. From there you can explore the different runtime environments supported by Cloud Engine. -::: - -| Runtime environment | Supported versions | Supported package managers | Documentation | Demo projects | -| ------------------- | ------------------ | -------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Frontend | Node.js >= 0.12 | NPM / Yarn | [Frontend Runtime Environment](/sdk/engine/deploy/webapp/) | [Frontend Runtime Environment § Getting Started](/sdk/engine/deploy/webapp/#getting-started) | -| Node.js | >= 0.12 | NPM / Yarn | [Node.js Runtime Environment](/sdk/engine/deploy/nodejs/) | [node-js-getting-started](https://github.com/leancloud/node-js-getting-started/) (Express) | -| Python | >= 2.7 | pip | [Python Runtime Environment](/sdk/engine/deploy/python/) | [python-getting-started](https://github.com/leancloud/python-getting-started) (Flask) | -| Java | 8, 11–15 | Maven | [Java Runtime Environment](/sdk/engine/deploy/java/) | [servlet-getting-started](https://github.com/leancloud/servlet-getting-started)
    [spring-boot-getting-started](https://github.com/leancloud/spring-boot-getting-started) | -| PHP | 5.6, 7.0–8.0 | Composer (v1) | [PHP Runtime Environment](/sdk/engine/deploy/php/) | [slim-getting-started](https://github.com/leancloud/slim-getting-started) | -| .NET | 3.1 | dotnet | [.NET Runtime Environment](/sdk/engine/deploy/dotnet/) | [dotnet-core-getting-started](https://github.com/leancloud/dotnet-core-getting-started) | -| Go | >= 1.10 | go mod | [Go Runtime Environment](/sdk/engine/deploy/go/) | [golang-getting-started](https://github.com/leancloud/golang-getting-started) (Echo) | -| C++ | GCC 9.4 | Bazel | [C++ Runtime Environment](/sdk/engine/deploy/cpp/) | [cpp-socket](https://github.com/leancloud/leanengine-unit-test/tree/cpp-socket-bazel) (Bazel) | - -## Cloud Functions and Hooks - -Cloud Functions lets you run backend code on the cloud in response to various types of events. It automatically serializes objects that have the data types provided by our [Data Storage](/sdk/storage/features)[Data Storage](/sdk/storage/overview/) service and is supported by our client-side SDKs. Hooks allows you to trigger custom logics or perform additional permission checks when there are objects created, updated, or deleted in the Data Storage service, users logged in or verified, or messages sent, conversations created, or clients logged in or logged out in the Instant Messaging service. - -:::tip -You can start using Cloud Functions with little experience in traditional backend development. Check out [this guide](/sdk/engine/functions/getting-started/) to learn how to write your first Cloud Function. -::: - -Cloud Functions comes with features like [Scheduled Tasks](/sdk/engine/functions/guides/#scheduled-tasks) and [Cloud Queue](/sdk/engine/functions/cloud-queue/) that make it convenient for you to manage your Cloud Functions in a more complex manner. You can have your Cloud Functions triggered routinely, retry failed function calls, skip duplicate function calls, look up function outputs, and delay function calls. - -## LeanDB - -Cloud Engine hosts a collection of popular database management systems that you can use as alternatives to the [Data Storage](/sdk/storage/features)[Data Storage](/sdk/storage/overview/) service: - -| DBMS | Clusters | Cluster availability | Documentation | -| ------------- | ---------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------ | -| Redis | Master/slave (1M/1S) | High availability by default with automatic failover | [LeanCache Guide](/sdk/engine/database/redis/) | -| MongoDB | Replica set (1P/1S/1A) | High availability by default with automatic failover | [LeanDB MongoDB Guide](/sdk/engine/database/mongo/) | -| MySQL | Master/slave (1M/1S) | High availability by default with automatic failover | [LeanDB MySQL Guide](/sdk/engine/database/mysql/) | -| Elasticsearch | One or three nodes | High availability by default with automatic failover when using three nodes | [LeanDB Elasticsearch Guide](/sdk/engine/database/es/) | - -## More - - - -- With the **CLI**, you can easily deploy and debug projects that use Cloud Functions. See [CLI Guide](/sdk/engine/cli/) for more information. -- You can use Cloud Functions with its **REST API** besides the Data Storage SDK. See [Cloud Engine REST API Guide](/sdk/engine/functions/rest-api/) for more information. -- If you have a **dedicated IP**, you can bind it to your Cloud Engine instances. See [Dedicated IP for Cloud Engine](/sdk/engine/dedicated-IP) for more information. -- For those curious about the stuff behind the scenes, [Deep Dive Into Cloud Engine](/sdk/engine/deep-dive/) lists a few technical details about Cloud Engine. - - - - - -- With the **CLI**, you can easily deploy and debug projects that use Cloud Functions. See [CLI Guide](/sdk/engine/cli/) for more information. -- You can use Cloud Functions with its **REST API** besides the Data Storage SDK. See [Cloud Engine REST API Guide](/sdk/engine/functions/rest-api/) for more information. -- For those curious about the stuff behind the scenes, [Deep Dive Into Cloud Engine](/sdk/engine/deep-dive/) lists a few technical details about Cloud Engine. - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/platform.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/platform.mdx deleted file mode 100644 index 8df19be9e..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/engine/platform.mdx +++ /dev/null @@ -1,261 +0,0 @@ ---- -title: Cloud Engine Platform Features -sidebar_label: Platform Features -sidebar_position: 2 ---- - -import CloudCustomDomain from "./_partials/cloud-custom-domain.mdx"; -import PlatformIntroduction from "./_partials/platform-introduction.mdx"; -import PlatformRuntimes from "./_partials/platform-runtimes.mdx"; -import { CLI_BINARY } from "/src/constants/env.ts"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - - - -:::info - -This article will guide you through the features provided by the Cloud Engine platform. For details about specific runtime environments, check out the following pages: - - - -::: - -## What Can Be Deployed to Cloud Engine - -Cloud Engine offers runtime environments on the process level. This means that you no longer need to think about OS environments when developing your application. Instead, you only need to take care of the processes created by your application (like `node` processes or executables compiled from Go projects), leaving you more time to work on the business logic of your application. - -Cloud Engine automatically prepares runtime environments for the applications deployed to it as long as these applications follow conventional project structures. For example, if you plan to deploy a Node.js application, the project has to have a `package.json` file. The [Overview](/sdk/engine/overview) page contains links to the dedicated pages for specific runtime environments, from which you can learn more about this. - -Cloud Engine barely interferes in the processes of your application. You are free to use your preferred frameworks and libraries in your project and organize files and directories in your way. Cloud Engine’s load balancer forwards all the HTTP requests sent to the domains bound to your application. It’s up to you to design the HTTP API’s paths as well as requests’ and responses’ formats under the web framework used by your application. - -Cloud Engine is primarily optimized for hosting stateless HTTP services. Although your application has access to the disk, there’s no guarantee that the disk will keep the files created by your application persistently. For long-term data storage, use the [Data Storage](/sdk/storage/features)[Data Storage](/sdk/storage/overview/) service or LeanDB’s hosted [Redis](/sdk/engine/database/redis/), [MongoDB](/sdk/engine/database/mongo/), and [Elasticsearch](/sdk/engine/database/es/). - - -

    - To associate an existing project to a Cloud Engine application, run{" "} - {CLI_BINARY} switch: -

    - - - {`$ ${CLI_BINARY} switch -[?] Please select an app: - 1) my-engine-app - => 1 -Switching to my-engine-app (group: web)`} - - -## Viewing Logs - -### Logs - -You can view deploy logs and program output on ** > Manage deployment > Your group > Logs**. Logs can be filtered by environment (staging/production), type (standard output / standard error), instance, and time. - -![Cloud Engine logs](https://capacity-files.lcfile.com/B28L3vYhjdCaavUkLF5TDkHJgFQvxBEo/engine-logs-en.png) - -You can export logs to your local computer with the CLI. See [CLI Guide § Viewing Logs](/sdk/engine/cli/#viewing-logs) for more information. - -## Viewing Metrics - -** > Manage deployment > Your group > Statistics** shows the usage data of all the instances under the current application. You can tell if the instances’ consumption of resources is approaching or exceeding the limit. - -![Cloud Engine statistics](https://capacity-files.lcfile.com/WzAblUPPgp8Upr9P1EPkgSN3pHFAYwWJ/engine-metrics-en.png) - -#### Requests per Minute - -This graph shows the number of requests handled by Cloud Engine per minute during a given time. You can filter the data by request type (web hosting / Cloud Functions) and HTTP status code using the dropdown menu on the top-left corner of the page. - -#### Response Time - -If the line in this graph goes up, it indicates that the instances’ consumption of CPU resources is reaching or exceeding the limit. Again, you can filter the data by request type (web hosting / Cloud Functions) and HTTP status code. - -#### CPU - -This graph shows the actual CPU usage of the application during a given time. If the usage approaches or reaches the limit, the response time of the application might be prolonged. - -#### RAM - -This graph shows the actual RAM usage of the application during a given time. If the usage reaches the limit, the processes of your application (like Node.js processes or Python processes) will be restarted due to OOM. While restarting, the instance will become unavailable and your application won’t be able to handle requests. If the graph frequently goes up to the limit and then goes down, it means that the processes have been restarted due to OOM. - -If **Show details for each instance** is checked, a separate line will be displayed for each instance in the graph for CPU and RAM. Otherwise, the data for all the instances will be combined into a single line. - -You can change the time period for the graphs on the top-right corner of the page. Options include Today, Yesterday, and Last 7 days. - -## Deploying and Publishing - -### Staging Environments vs Production Environments - -With the standard mode of Cloud Engine, you get a production environment and a staging environment for each group. The production environment is designed to take in and handle requests from actual users of your application. The staging environment provides almost the same environment as the production environment for you to test your application. They both have access to the same data from the Data Storage service, though a separate domain can be assigned to each environment. When invoking Cloud Functions or updating objects that may trigger hooks with TDSLeanCloud’s client-side SDKs, you can specify the environment to which the requests should be sent (`X-LC-Prod`). - -While working on your application, you can first deploy your changes to the staging environment, test the changes with the data from the Data Storage service, and publish the changes to the production environment only if everything goes well. If you wish to have a testing environment with a separate data source, consider creating another application. - -The trial mode doesn’t come with a staging environment. - -### Deploying With CLI - -Run the following command under the root directory of your project to deploy your project: - -{`${CLI_BINARY} deploy`} - -The CLI will display an interactive interface that asks you if you want to deploy your project to the production environment or the staging environment. To deploy the project directly to the production environment, add `--prod` to the end of the command. For more information about the usage of the CLI, see [CLI Guide § Deploying a Local Project](/sdk/engine/cli/#deploying-a-local-project). - -### Deploying With Git - -You can also deploy your project from a Git repository either hosted on platforms like [GitHub](https://github.com/) and [Gitee](https://gitee.com/) or self-hosted with services like [GitLab](https://about.gitlab.com/install/). You can configure the address of your project’s Git repository on ** > Manage deployment > Your group > Deploy > Deploy from Git**. - -If you have a private repository, you can provide the address with the SSH protocol (like `git@github.com:leancloud/node-js-getting-started.git`). Make sure to set up a deploy key for Cloud Engine with the service hosting the repository. If you have a public repository, we recommend that you provide the address with the HTTPS protocol. - -Once you have configured the address of your Git repository, you’ll be able to deploy your project to Cloud Engine by hitting “Deploy”. Cloud Engine will use the code on the `master` branch by default, but you can also specify a version by entering a branch name, tag name, or commit hash. - -To trigger automatic deployment each time a commit gets pushed to a specific branch of the Git repository, generate a deploy token on ** > Manage deployment > Your group > Settings > Auto deploy** and use it with the webhook URL shown on the page. Cloud Engine will deploy the code on the specified branch to the specified environment each time the webhook URL receives a POST request. See the guidance on the console for more information on how to set up automatic deployment with GitHub Actions. - -### Deployment History - -You can view the past deployments of an environment on ** > Manage deployment > Your group > Deploy** by expanding the dropdown menu on an environment and opening “Versions”. -Each deployment has its description (based on information like Git commit message), version number, and timestamp displayed on the page. -The deployments are ordered chronologically with the current deployment on the top. -You can roll back to a previous deployment by hitting “Deploy to”. -Besides viewing deployments by the environment, you can also view all deployments chronologically under “Timeline” (with the latest on the top). - -In addition to the deployment history, the page also shows the deployment status (_hibernating_, _deploying_, or _running_) of each environment. -You can **restart** an environment (redeploy the current version) or **clear the deployment** (remove the deployment and cancel the Cloud Functions and hooks) with the buttons on the top-right corner of the page. -For the staging environment, you can hit **Deploy to production** to publish the latest deployment on the staging environment to the production environment. -If the deployment status of an environment is _deploying_, the page will show the details of the deployment process as well as a **Cancel deployment** button. You can hit this button to cancel the deployment. - -### Groups - -You can create multiple _groups_ under the same application for hosting different backend programs that have access to the same data from the Data Storage service. Each group can be bound to a different custom domain, making the following scenarios possible: - -- Create different projects for the client-side interface and the admin console. Assign different domains to them. -- Deploy non-essential systems separately from the main system so the main system could keep working if problems occur to any non-essential system. -- Use different server-side languages for Cloud Functions and the website. For example, you can use Node.js for Cloud Functions and PHP for the website. - -Each group comes with a separate staging environment for you to test your code, as well as a separate domain for users to access it. It also has its own environment variables and repository configurations. When deploying your code, you get to choose the group you want to deploy to. - -You can deploy Cloud Functions, hooks, and scheduled tasks to any group. One thing to keep in mind is that when you deploy your code to a group, if the code contains a Cloud Function that has the same name as one in a different group, the deployment will be aborted. To force the deployment to continue, enable [`--overwrite-functions`](#deployment-options). - -Each application has a default group named `web`. You can create additional groups on ** > Manage deployment**. A new group doesn’t contain any instances and cannot respond to users’ requests until you [adjust the quota and number of instances](#adjusting-quota-and-number-of-instances). - -### Deployment Options - -You can add the following options when deploying your project: - -#### `--no-cache` - -If you encounter problems with installing dependencies, you can try adding this option to disable the [caching mechanism](/sdk/engine/deep-dive/#build-process) used to accelerate the build process. - -#### `--options 'printBuildLogs=true'` - -If you encounter problems with the build process, you can try adding this option to have the build log printed to the console. - -#### `--overwrite-functions` - -When your project contains Cloud Functions and hooks with duplicate names among different groups, you can add this option to force the Cloud Functions and hooks in your current deployment to take effect. You can use this option to move Cloud Functions and hooks across different groups. - -## Managing Resources and Capacities - -### Trial Mode - -Each application comes with a free trial instance with 0.5 CPU and 256 MB RAM. You can use this instance for learning and trying out Cloud Engine. - -**A trial instance hibernates** if it doesn’t receive any requests in a while and restarts once it receives a request. It might take a few seconds for an instance to restart. A trial instance can run for a maximum of 18 hours per day. - -
    -More about hibernation - -- A trial instance will hibernate if it receives no requests in 30 minutes. -- While hibernating, the instance will restart if it receives a request. The first visitor might find that they’ll have to wait for 5 to 30 seconds before they can get a response. -- If a trial instance has run for more than 18 hours within the past 24 hours, it will be forced to hibernate and won’t restart even if there are requests coming in. The requests will get the 503 error. You can view the errors received by requests on ** > Manage deployment > Your group > Statistics**. - -
    - -### Standard Mode - - - -The billing of Cloud Engine doesn’t get affected by the plan (Developer/Business) you are using. Upgrading to or downgrading from the Business Plan won’t affect the charges incurred by your Cloud Engine instances. - - - -:::tip -If you’re hosting business projects and already-launched products on Cloud Engine, we recommend that you upgrade to the standard mode and use at least two instances to enhance the availability of your application. -::: - -A standard instance won’t hibernate as a trial instance does. It also comes with a staging environment for you to test your code. If you purchase two or more instances, you can also utilize load balancing and automatic failover to maximize the availability of your application. - -The standard mode comes with the following features: - -#### Load Balancing (High Availability) - -Cloud Engine’s gateway will evenly distribute the requests from users to all the instances in your application. You can enhance your application’s capability of handling requests by simply increasing the number of instances in your application. - -When there are two or more instances in the same group’s same environment, automatic failover becomes possible. When one of the instances stops working, Cloud Engine’s gateway will forward requests to other working instances until the corrupted instance is restored. - -#### Staging Environment - -Each group with the standard mode comes with a free staging environment. It has almost the same runtime environment as the production environment. Before you launch your code, you can deploy it to the staging environment and test it with the environment and data on the server side. - -The staging environment shares the same quota with the production environment and hibernates like a trial instance as well. - -#### Zero Downtime Deployment - -When you’re deploying a new version of your project, or when maintenance operations are going on, the system will have both the older and newer versions of your project run for a while and gradually shut down the older version. This ensures that there will be no downtime in your application. - -#### Multiple Groups - -With multiple groups, you can deploy different backend programs that have access to the same data from the Data Storage service. Each group can also have its own domain. See [Groups](#groups) for more information. - -### Adjusting Quota and Number of Instances - -The following quotas are available for standard instances. The difference mainly lies in the size of RAM: - -| Quota | RAM | CPU | -| ------------- | ------- | ------ | -| standard-512 | 512 MB | 1 Core | -| standard-1024 | 1024 MB | 1 Core | -| standard-2048 | 2048 MB | 1 Core | -| standard-4096 | 4096 MB | 1 Core | - -You can adjust the quota and number of instances on ** > Manage deployment > Your group > Deploy**. - -:::tip -We recommend that you pick a quota according to the maximum RAM needed by your program and later adjust the number of instances to cater to the growth of the number of requests. For business projects and already-launched products, we recommend that you use at least two instances to enhance the availability of your application. -::: - -To ensure your instances’ consumption of resources doesn’t exceed the limit, we recommend that you [check the metrics](#viewing-metrics) regularly: - -- If the average **RAM** usage during a day goes beyond **70%** of the available resources (717 MB for a standard-1024 instance), you should consider increasing the quota. -- If the average **CPU** usage during a day goes beyond **30%** of the available resources (30% CPU for a standard-1024 instance), you should consider increasing the number of instances. - -
    -Multiple instances - -Your program will be running on multiple instances concurrently if you have created multiple instances in the same group’s same environment. This also happens when deployment or maintenance operations (like restarting instances) are going on. - -Each instance has its own RAM and file system. This means that if a global variable or a file is created in one of the instances, it won’t be accessible from other instances. We recommend that you test your program thoroughly when you first switch from using a single instance to using multiple instances. - -To share data among multiple instances with low latency, consider using LeanCache. - -
    - -### Billing - -The amount charged for your usage will be based on the multiplication of the selected quota and the number of instances. For each given day, the amount for the maximum number of instances that existed on this day will be charged on the next day. You can view the prices of the available quotas on the pricing page of the current region. The billing history can be found under _Consumption details_ on the dashboard_Transactions_ on the Developer Center. - -If you want to stop being charged, you can downgrade the quota of all the groups in your application to the _trial mode_. This will remove all the standard instances and only leave a free trial instance in one group. The final bill will be produced on the next day and you won’t be charged after that. - -## Adjusting Settings - -### Environment Variables - -To inject configurations into your application, you can set up custom environment variables on ** > Manage deployment > Your group > Settings > Custom environment variables**. Your updates to the custom environment variables will take effect upon the next deployment. - -You can check **Secret** to prevent an environment variable from being displayed on the page. This reduces the chance for sensitive information like keys and passwords to be accidentally leaked. - -The default environment variables provided by Cloud Engine runtime environments cannot be overwritten by your custom environment variables. - -### Binding Custom Domains - - diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/_category_.json deleted file mode 100644 index 0b22b23d7..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "数据存储", - "collapsed": true, - "position": 15 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/_partials/app-config.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/_partials/app-config.mdx deleted file mode 100644 index b3585a024..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/_partials/app-config.mdx +++ /dev/null @@ -1,23 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - - - -You can view the credentials of your app by going to **Developer Center > Your game > Game Services > Configuration**: - -- **Client ID**, also called `App ID`, will be used when you initialize the SDK. -- **Client Token**, also called `App Key`, will be used when you initialize the SDK on the client side. -- **Server Secret**, also called `Master Key`, will be used when you interact with admin interfaces on **trusted environments** including your own server and Cloud Engine. When you use it, all the permission checks will be skipped. Make sure to **keep it private and never use it in the code for the client**. - - - - - -You can view the credentials of your app by going to **Dashboard > Settings > App keys**: - -- **App ID** will be used when you initialize the SDK. -- **App Key** will be used when you initialize the SDK on the client side. -- **Master Key** will be used when you interact with admin interfaces on **trusted environments** including your own server and LeanEngine. When you use it, all the permission checks will be skipped. Make sure to **keep it private and never use it in the code for the client**. - - - -See [Domain](#domain) for setting up the domain. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/acl.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/acl.mdx deleted file mode 100644 index d5d79b0bf..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/acl.mdx +++ /dev/null @@ -1,938 +0,0 @@ ---- -title: ACL Guide -sidebar_label: ACL -slug: /sdk/storage/guide/acl/ -sidebar_position: 12 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -:::info -ACL stands for Access Control List. It defines the access permissions for an object. -::: - -The ACL mechanism used by TDSLeanCloud grants the permission to perform each operation to specific users or roles, allowing only them to perform the operation. - -For example: - -```json -{ - "*":{ - "read":true, - "write":false - }, - "role:admin":{ - "read":true, - "write":true - }, - "58113fbda0bb9f0061ddc869":{ - "read":true, - "write":true - } -} -``` - -- Everyone can read, but not everyone can write (`*` means everyone). -- Users with the admin role (including its sub-roles) can read and write. -- The user with the ID `58113fbda0bb9f0061ddc869` can read and write. - -We use the built-in `_User` table to maintain the [account system](/sdk/authentication/guide/) and the `_Role` table to maintain **roles**. -Roles can contain users as well as other roles, meaning that roles can have hierarchies. Granting a permission to a role also grants the permission to the roles that belong to this role. - -For each request sent to the cloud from the client, the cloud authenticates the user and performs checks on ACL. You can have the data in your app protected with the help of ACL. - -## Default ACL - -Below is the default ACL value for all classes. It allows everyone to read and write: - -```json -{ - "*":{ - "read":true, - "write":true - } -} -``` - -When creating a new class on the dashboard, you can configure its default ACL in the dialog box: - -![create class dialog box](/img/security/class-acl.png) - -Here you can specify which users you want to open read and write permissions for: - -- See [Data Security](/sdk/storage/guide/security/#class) for more information about "All users" and "Designated users". -- "Object creator (owner)" refers to the user who creates the data. In other words, it is the user that the `X-LC-Session` HTTP header used when creating the object corresponds to. - -In addition to specifying which users are allowed to read and write, the dialog also provides some shortcuts for **common settings**: - -- **Restrict write**: The creator can read and write; other users can read but not write. -- **Restrict read**: The creator can read and write; other users cannot read or write. -- **Restrict all**: The creator can read but not write; other users cannot read or write. -- **No restrictions**: Everyone can read and write. - -For an existing class, you can **update the default ACL and access permissions**. -Go to ** > Data**, select a class, and click the **Permission** tab. -However, changing the default ACL only affects new objects; the ACL values of existing objects remain unchanged. - -In addition to setting the default ACL, you can also **set the ACL for individual objects** in the console. -However, it is not practical to manually set ACLs for each object in the console, as it is too tedious. -So we typically set the **default ACL** in the console to ensure that all newly created objects have the appropriate initial ACL values. -To set more complex and granular ACLs for individual objects, set their `ACL` fields by code. - -For example, the default ACL for a `Post` class might look like this - -- read: all users -- write: creator (owner) - -This means that all users can view posts, and users can only edit or delete their own posts. - -Here, the data creator is the user corresponding to the session token carried in the HTTP header of the request that created the object, in our case the author of the post. - -## ACL with Users - -Let's continue with the example above. Suppose you want to also allow a coauthor to modify a post: - - - -```cs -try { - LCQuery userQuery = LCUser.GetQuery(); - LCUser otherUser = await userQuery.Get("55f1572460b2ce30e8b7afde"); - // Create a post object - LCObject post = LCObject("Post"); - post["title"] = "This is my second post."; - post["content"] = "I started watching soccer and basketball."; - - //Create a new ACL instance - LCACL acl = new LCACL(); - acl.PublicReadAccess = true; // Set read access to public so that anyone can read - LCUser currentUser = await LCUser.GetCurrent(); - acl.SetUserWriteAccess(currentUser, true); // Set write access to the current user so that only the current user can update this post - acl.SetUserWriteAccess(otherUser, true); - post.ACL = acl; - - await post.Save(); -} catch (LCException e) { - print($"{e.Code} : {e.Message}"); -} -``` - -```java -LCQuery query = LCUser.getQuery(); -query.getInBackground("55f1572460b2ce30e8b7afde").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser anotherUser) { - // Create a post object - LCObject post= new LCObject("Post"); - post.put("title","This is my second post."); - post.put("content","I started watching soccer and basketball."); - - //Create a new ACL instance - LCACL acl = new LCACL(); - acl.setPublicReadAccess(true);// Set read access to public so that anyone can read - acl.setWriteAccess(LCUser.getCurrentUser(), true);//Set write access to the current user - acl.setWriteAccess(anotherUser, true); - - // Assign the ACL instance to the post object - post.setACL(acl); - - //Save the object to the cloud - post.saveInBackground(); - } - @Override - public void onError(Throwable e) { - System.out.println("errorMessage:" + e.getMessage()); - } - @Override - public void onComplete() { - } -} -``` - -```objc -LCQuery *query = [LCUser query]; -[query getObjectInBackgroundWithId:@"55f1572460b2ce30e8b7afde" block:^(LCObject * _Nullable object, NSError * _Nullable error) { - if (error == nil) { - // Create a post object - LCObject *post = [LCObject objectWithClassName:@"Post"]; - [post setObject:@"This is my second post." forKey:@"title"]; - [post setObject:@"I started watching soccer and basketball." forKey:@"content"]; - - //Create a new ACL instance - LCACL *acl = [LCACL ACL]; - [acl setPublicReadAccess:YES];// Set read access to public so that anyone can read - [acl setWriteAccess:YES forUser:[LCUser currentUser]];// Set write access to the current user - [acl setWriteAccess:YES forUser:otherUser]; - - post.ACL = acl;// Assign the ACL instance to the post object - - [post save]; - } else { - NSLog(@"error"); - } -}]; -``` - -```swift -let query = LCQuery(className: LCUser.objectClassName()) - -_ = query.get("55f1572460b2ce30e8b7afde") { result in - switch result { - case .success(object: let object): - do { - let post = LCObject(className: "Post") - - try post.set("title", value: "This is my second post.") - try post.set("content", value: "I started watching soccer and basketball.") - - let acl = LCACL() - - acl.setAccess([.read], allowed: true) - if let currentUserID = LCApplication.default.currentUser?.objectId?.value { - acl.setAccess([.write], allowed: true, forUserID: currentUserID) - } - if let anotherUserID = (object as? LCUser)?.objectId?.value { - acl.setAccess([.write], allowed: true, forUserID: anotherUserID) - } - - post.ACL = acl - - assert(post.save().isSuccess) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } -} -``` - -```dart -try { - LCQuery userQuery = LCUser.getQuery(); - LCUser otherUser = await userQuery.get('55f1572460b2ce30e8b7afde'); - // Create a post object - LCObject post = LCObject("Post"); - post['title'] = 'This is my second post.'; - post['content'] = 'I started watching soccer and basketball.'; - - //Create a new ACL instance - LCACL acl = LCACL(); - acl.setPublicReadAccess(true); // Set read access to public so that anyone can read - LCUser currentUser = await LCUser.getCurrent(); - acl.setUserWriteAccess(currentUser, true); // Set write access to the current user so that only the current user can update this post - acl.setUserWriteAccess(otherUser, true); - post.acl = acl; //Assign the ACL instance to the post object - - await post.save(); -} on LCException catch (e) { - print('${e.code} : ${e.message}'); -} -``` - -```js - // Create a query for User - var query = new AV.Query(AV.User); - query.get('55f1572460b2ce30e8b7afde').then(function(otherUser) { - var post = new AV.Object('Post'); - post.set('title', 'This is my second post.'); - post.set('content','I started watching soccer and basketball.'); - - // Create a new ACL instance - var acl = new AV.ACL(); - acl.setPublicReadAccess(true); - acl.setWriteAccess(AV.User.current(), true); - acl.setWriteAccess(otherUser, true); - - // Assign the ACL instance to the post object - post.setACL(acl); - - // Save the object to the cloud - return post.save(); - }).then(function() { - // Saved - }).catch(function(error) { - // Error - console.log(error); - }); -``` - -```python -import leancloud - -# Log in a user -user = leancloud.User() -user.login('my_user_name', 'my_password') - -# Create a Post object -Post = leancloud.Object.extend('Post') -post = Post() -post.set('title', 'This is my second post.') -post.set('content', 'I started watching soccer and basketball.') - -# Create a leancloud.ACL instance -acl = leancloud.ACL() -acl.set_public_read_access(True) -# Set write permission for the currently logged in user -acl.set_write_access(leancloud.User.get_current().id, True) -# Set write permission for the user with a given objectId -acl.set_write_access('55f1572460b2ce30e8b7afde', True) -post.set_acl(acl) -post.save() -``` - -```php -// Log in a user -User::logIn("Tom", "cat!@#123"); - -// Create a post -$post = new LeanObject("Post"); -$post->set("title", "This is my second post."); -$post->set("content", "I started watching soccer and basketball."); - -// Create a new ACL instance -$acl = new ACL(); -// Set read access to public so that anyone can read -$acl->setPublicReadAccess(true); -// Set write access to the current user so that only the current user can update this post -$acl->setWriteAccess(User::getCurrentUser(), true); -$otherUser = LeanObject::create("_User", "55f1572460b2ce30e8b7afde"); -$acl->setWriteAccess($otherUser, true); -$post->setACL($acl); -$post->save(); -``` - - - -Go back to the dashboard after running the above code and you will see that the ACL of the post will be: - -```json -{ - "*":{ - "read":true - }, - "55b9df0400b0f6d7efaa8801":{ - "write":true - }, - "55f1572460b2ce30e8b7afde":{ - "write":true - } -} -``` - -As you can see from the result, the post now allows the two users with `objectId` of `55b9df0400b0f6d7efaa8801` and `55f1572460b2ce30e8b7afde` to update. They have the write permission `"write": ture`. - -:::tip Notice -In order to avoid long and unfocused examples, the following examples do not give the complete code that can be executed directly, but only the key parts. -::: - -Assuming the forum has an administrator who can edit and delete all posts, we can add the appropriate permissions to each post in a similar way: - - - -```cs -acl.SetUserWriteAccess(anAdministrator, true); -``` - -```java -acl.setWriteAccess(anAdministrator, true); -``` - -```objc -[acl setWriteAccess:YES forUser:anAdministrator]; -``` - -```swift -acl.setAccess([.write], allowed: true, forUserID: anAdministratorID) -``` - -```dart -acl.setUserWriteAccess(anAdministrator, true); -``` - -```js -acl.setWriteAccess(anAdministrator, true); -``` - -```python -acl.set_write_access(an_administrator_id, True) -``` - -```php -$acl->setReadAccess($anAdministrator, true); -``` - - - -However, new administrators may join and old ones may leave in the future, so it is too inflexible to manage permissions based on users only, and any personnel changes require bulk data revisions (modification of ACL fields). - -Therefore, we need to introduce the concept of roles. - -## ACL with Roles - -A role is a group of users, and roles can be nested. -In other words, the member of a role is either a user or another role. - -Each role has an immutable and unique name, consisting of alphanumerics and underscores. - -Continuing the example above, let's see how to grant write permission to the administrator role (assuming there is an existing role named `admin`): - - - -```cs -LCRole admin = LCRole.CreateWithoutData("_Role", "55fc0eb700b039e44440016c"); -acl.SetRoleReadAccess(admin, true); -``` - -```java -acl.setRoleWriteAccess("admin", true); -``` - -```objc -LCRole *admin = [LCRole objectWithClassName:@"_Role" objectId:@"55fc0eb700b039e44440016c"]; -[acl setWriteAccess:YES forRole:admin]; -``` - -```swift -acl.setAccess([.write], allowed: true, forRoleName:"admin") -``` - -```dart -LCRole admin = LCRole.createWithoutData('_Role', '55fc0eb700b039e44440016c'); -acl.setRoleWriteAccess(admin, true); -``` - -```js -acl.setRoleWriteAccess('admin', true); -``` - -```python -admin = leancloud.Role('admin') -acl.set_role_write_access(admin, True) -``` - -```php -$acl->setRoleWriteAccess("admin", true); -``` - - - -### Role Creation - -Let's see how to create a role. - -Note that since a `Role` itself is also an `Object`, it has its own ACL control, and it should have tighter permission control. -So usually when we create a role, we explicitly set the ACL for that role. -If you don't specify it, then the SDK will set the ACL of the role to be readable by all and unwritable by all by default. -In other words, without explicitly specifying the ACL, the SDK's default setting makes it impossible to modify the role on the client side once it is created, and future operations such as adding members must be done on the console or on the server side using masterKey. -For testing purposes, the following sample code temporarily assigns write access to the user who created the role (the current user). **Please set the appropriate ACL according to your specific needs in the actual project**. - - - -```cs -try { - // The ACL of the role - LCACL acl = new LCACL(); - acl.PublicReadAccess = true; - LCUser currentUser = await LCUser.GetCurrent(); - acl.SetUserWriteAccess(currentUser, true); - - LCRole admin = LCRole.Create(name, acl); - await admin.Save(); -} catch (LCException e) { - print($"{e.Code} : {e.Message}"); -} -``` - -```java -// The ACL of the role -LCACL roleACL = new LCACL(); -roleACL.setPublicReadAccess(true); -roleACL.setWriteAccess(LCUser.getCurrentUser(),true); - -LCRole admin = new LCRole("admin", roleACL); -admin.saveInBackground().blockingSubscribe(); -``` - -```objc -// The ACL of the role -LCACL *roleACL = [LCACL ACL]; -[roleACL setPublicReadAccess:YES]; -[roleACL setWriteAccess:YES forUser:[LCUser currentUser]]; - -LCRole *admin = [LCRole roleWithName:@"admin" acl:roleACL]; -[admin save]; -``` - -```swift -do { - // The ACL of the role - let roleACL = LCACL() - roleACL.setAccess([.read], allowed: true) - if let currentUserID = LCApplication.default.currentUser?.objectId?.value { - roleACL.setAccess([.write], allowed: true, forUserID: currentUserID) - } - - let admin = LCRole(name: "admin") - admin.ACL = roleACL - assert(admin.save().isSuccess) -} catch { - print(error) -} -``` - -```dart -try { - // The ACL of the role - LCACL acl = LCACL(); - acl.setPublicReadAccess(true); - LCUser currentUser = await LCUser.getCurrent(); - acl.setUserWriteAccess(currentUser, true); - - LCRole admin = LCRole.create('admin', acl); - await admin.save(); -} on LCException catch (e) { - print('${e.code} : ${e.message}'); -} -``` - -```js -// The ACL of the role -const roleAcl = new AV.ACL(); -roleAcl.setPublicReadAccess(true); -roleAcl.setWriteAccess(AV.User.current(), true); - -const admin = new AV.Role('admin', roleAcl); -await admin.save(); -``` - -```python -# The ACL of the role -role_acl = leancloud.ACL() -role_acl.set_public_read_access(True) -current_user_id = leancloud.User.get_current().id -role_acl.set_write_access(current_user_id) - -admin = leancloud.Role('admin', role_acl) -admin.save() -``` - -```php -// The ACL of the role -$roleACL = new ACL(); -$roleACL->setPublicReadAccess(true); -$roleACL->setWriteAccess(User::getCurrentUser(), true); - -$admin = new Role(); -$admin->setName("admin"); -$admin->setACL($roleACL) -$admin->save(); -``` - - - -Now the `admin` role is empty. Let's **add** the current user to this role: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -admin.AddRelation("users", currentUser); -``` - -```java -admin.getUsers().add(LCUser.getCurrentUser()); -``` - -```objc -[[admin users] addObject: [LCUser currentUser]]; -``` - -```swift -if let currentUser = LCApplication.default.currentUser { - if let users = admin.users { - try? users.insert(currentUser) - } else { - let users = admin.relationForKey("users") - if (try? users.insert(currentUser)) != nil { - admin.users = users - } - } -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -admin.addRelation('users', currentUser); -``` - -```js -admin.getUsers().add(AV.User.current()); -``` - -```python -admin.get_users().add(leancloud.User.get_current()) -``` - -```php -$admin->getUsers().add(User::getCurrentUser()); -``` - - - -To **remove** a user from the role: - - - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -admin.RemoveRelation("users", currentUser); -``` - -```java -admin.getUsers().remove(LCUser.getCurrentUser()); -``` - -```objc -[[admin users] removeObject:[LCUser currentUser]]; -``` - -```swift -if let currentUser = LCApplication.default.currentUser { - try? admin.users?.remove(currentUser) -} -``` - -```dart -LCUser currentUser = await LCUser.getCurrent(); -admin.removeRelation('users', currentUser); -``` - -```js -admin.getUsers().remove(AV.User.current()); -``` - -```python -admin.get_users().remove(leancloud.User.get_current()) -``` - -```php -$admin->getUsers().remove(User::getCurrentUser()); -``` - - - -As mentioned earlier, the member of a role can be another role. -Suppose there are two roles, an `admin` and a `moderator', and we want `admin` to be a child of `moderator`, since the administrator also has all the privileges of the moderator. - - - -```cs -moderator.AddRelation("roles", admin); -``` - -```java -moderator.getRoles().add(admin); -``` - -```objc -[[moderator roles] addObject:admin]; -``` - -```swift -if let subroles = moderator.roles { - try? subroles.insert(admin) -} else { - let subroles = moderator.relationForKey("roles") - if (try? subroles.insert(currentUser)) != nil { - admin.roles = subroles - } -} -``` - -```dart -moderator.addRelation('roles', admin); -``` - -```js -moderator.getRoles().add(admin); -``` - -```python -moderator.get_roles().add(admin) -``` - -```php -$moderator->getRoles().add(admin); -``` - - - -Occasionally you may want to remove subroles. For example, we later changed our minds and decided that admins should focus on global administrative tasks, and that editing, deleting, and the like should be the responsibility of moderators. - - - -```cs -moderator.RemoveRelation("roles", admin); -``` - -```java -moderator.getRoles().remove(admin); -``` - -```objc -[[moderator roles] removeObject:admin]; -``` - -```swift -try? moderator.roles?.remove(admin) -``` - -```dart -moderator.removeRelation('roles', admin); -``` - -```js -moderator.getRoles().remove(admin); -``` - -```python -moderator.get_roles().remove(admin) -``` - -```php -$moderator->getRoles().remove(admin); -``` - - - -### Role Query - -Query which roles a user has: - - - -```cs -try { - LCUser currentUser = await LCUser.GetCurrent(); - LCQuery roleQuery = LCRole.GetQuery(); - roleQuery.WhereEqualTo("users", currentUser); - ReadOnlyCollection roles = await roleQuery.Find(); -} catch (LCException e) { - print($"{e.Code} : {e.Message}"); -} -``` - -```java -LCUser user = LCUser.getCurrentUser(); -user.getRolesInBackground().subscribe(new Observer>() { - @Override public void onSubscribe(Disposable d) {} - @Override public void onNext(List avRoles) { - // avRoles is the result - } - @Override public void onError(Throwable e) {} - @Override public void onComplete() {} -}); -``` - -```objc -LCUser *user = [LCUser currentUser]; -[user getRolesInBackgroundWithBlock:^(NSArray * _Nullable avRoles, NSError * _Nullable error) { - // avRoles is the result -}]; -``` - -```swift -if let user = LCApplication.default.currentUser { - let roleQuery = LCQuery(className: LCRole.objectClassName()) - roleQuery.whereKey("users", .equalTo(user)) - _ = roleQuery.find { result in - switch result { - case .success(objects: let roles): - print(roles) - case .failure(error: let error): - print(error) - } - } -} -``` - -```dart -try { - LCUser currentUser = await LCUser.getCurrent(); - LCQuery roleQuery = LCRole.getQuery(); - roleQuery.whereEqualTo('users', currentUser); - List roles = await roleQuery.find(); -} on LCException catch (e) { - print('${e.code} : ${e.message}'); -} -``` - -```js -const roles = await AV.User.current().getRoles(); -``` - -```python -roles = leancloud.User.get_current().get_roles() -``` - -```php -$roles = User::getCurrentUser().getRoles(); -``` - - - -To query the users included in a role (only the code to build the query is given here): - - - -```cs -LCQuery userQuery = moderator.Users.Query; -``` - -```java -LCQuery userQuery = moderator.getUsers().getQuery(); -``` - -```objc -LCUser *userQuery = [[moderator users] query]; -``` - -```swift -let *userQuery = moderator.users?.query -``` - -```dart -LCQuery userQuery = moderator.users.query(); -``` - -```js -const userQuery = moderator.getUsers().query(); -``` - -```python -user_query = moderator.get_users().query -``` - -```php -$userQuery = $moderator->getUsers().getQuery(); -``` - - - -Of course, the above code does not take into account the users contained in the subroles. -If you want to query all of the users contained in a role (both directly and indirectly), you need to recursively look up all of the role's subroles (including subroles of subroles, and so on), and then query all of the users contained in those roles. -For brevity, we won't include the full code here, just the code to build the subrole query: - - - -```cs -LCQuery subroleQuery = moderator.Roles.Query; -``` - -```java -LCQuery subroleQuery = moderator.getRoles().getQuery(); -``` - -```objc -LCRole *subroleQuery = [[moderator roles] query]; -``` - -```swift -let *subRoleQuery = moderator.roles?.query -``` - -```dart -LCQuery subroleQuery = moderator.roles.query(); -``` - -```js -const subroleQuery = moderator.getRoles().query(); -``` - -```python -subrole_query = moderator.get_roles().query -``` - -```php -$subRoleQuery = $moderator->getRoles().getQuery(); -``` - - - -Because roles inherit from structured stored objects, you can also perform various queries based on other properties of the role, just as you can with general object queries. - -## Special Rules - -Because user-related information is sensitive, the `_User` table ignores ACL settings and no user can change the properties of other users. For example, if the currently logged in user is A, and A wants to change the username, password, or other custom properties of user B via a query, it won't work, even if user B has write access to A in their ACL. - -LiveQuery is designed to be used on the client side, and the client side should not use MasterKey, otherwise there will be big security risks. -Therefore, the MasterKey is ignored when subscribing to events with LiveQuery. In other words, LiveQuery should not use the MasterKey when subscribing to events, and even if it does, it won't skip permission checks like ACLs. - -## Retrieving ACL Value - -When retrieving data, the SDK does not return the ACL value of the object by default. If you want to get the ACL value when fetching an object, the following two conditions must be met: - -1. Go to ** > Settings > Queries** and select "Include ACL with objects being queried". -2. The client specifies that the ACL should be returned when the object is retrieved. - - -The code is as follows: - - - -```cs -LCQuery query = new LCQuery("Todo"); -query.IncludeACL = true; -``` - -```java -LCQuery query = new LCQuery<>("Todo"); -query.includeACL(true); -``` - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -query.includeACL = YES; -``` - -```swift -let query = LCQuery(className: "Todo") -query.includeACL = true -``` - -```dart -LCQuery query = LCQuery('Todo'); -query.includeACL(true); -``` - -```js -var query = new AV.Query('Todo'); -query.includeACL(true); -``` - -```python -query = leancloud.Object.extend('Todo').query.include_acl(True) -``` - -```php -// Not yet supported -``` - - - -## Best Practice - -If the application's permission control needs are relatively simple, we recommend setting [class permissions](/sdk/storage/guide/security/#class), [field permissions](/sdk/storage/guide/security/#field), and [default ACL](#default-acl) correctly, and then setting individual ACLs that require fine-grained permission control through client-side code. - -For applications with complex permission control requirements, we recommend setting class permissions, field permissions, and default ACLs in the console, and then unifying the ACL-related logic in the Cloud Engine. -On the one hand, this eliminates the need to constantly update and maintain client-side code with very similar logic across iOS, Android, Web, and so on. -On the other hand, in addition to handling ACLs, the Cloud Engine can also control permissions through hooks based on more complex conditions, such as not allowing posts that exceed a certain number of words. -See [在云引擎中使用 ACL](/sdk/storage/guide/engine-acl/) for more details. - -For classes that store very sensitive data and have high security requirements, you may also want to consider turning off write and even read permissions in the [class permissions](/sdk/storage/guide/security/#class) and route all client requests through the Cloud Engine, giving you the same security guarantee as building your own backend. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/_category_.json deleted file mode 100644 index 4e75adc71..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "最佳实践", - "collapsed": true, - "position": 18 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/engine-acl.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/engine-acl.mdx deleted file mode 100644 index 99134a88c..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/best-practice/engine-acl.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: Using ACL in Cloud Engine -slug: /sdk/storage/guide/engine-acl/ -sidebar_position: 3 ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -**[Cloud Engine](https://developer.taptap.cn/docs/en/sdk/engine/overview/)** offers a way for you to define logic on the cloud to perform certain actions when certain events happen. When you need to print logs, verify permissions, or enforce ACL settings on data operations initiated by clients, this could be very helpful. See **[Cloud Functions and Hooks](/sdk/engine/functions/getting-started/)** for more information. - -## Requirements - -We have mentioned the requirements above to help you better understand the following requirement description: - -Imagine that you are building an application for iOS, Android, and web (JavaScript), and you need to implement a function that adds permission settings to all the objects created. Traditionally, you will need to write the same function in different languages for each platform. But now you can write the same function only once and put it on the cloud, which makes the development process way easier. - -## Use Cases - -Let's start with a simple example: - -We want the administrator to have read and write access to every post made by a user from a client, be it iOS or Android. - -To get started, we need to write our Cloud Engine hook function (see [BeforeSave](/sdk/engine/functions/guides/#beforesave) for an introduction to Cloud Engine hook functions): - - - - - -```js -AV.Cloud.beforeSave('Post', (request) => { - const post = request.object; - if (post) { - var acl = new AV.ACL(); - acl.setPublicReadAccess(true); - // Assuming a role named `admin` exists - acl.setRoleWriteAccess('admin', true); - post.setACL(acl); - } else { - throw new AV.Cloud.Error('Invalid Post object.'); - } -}); -``` - - - - - -```python -@engine.before_save('Post') -def before_post_save(post): - acl = leancloud.ACL() - acl.set_public_read_access(True) - # Assuming a role named `admin` exists - admin = leancloud.Role('admin') - acl.set_role_write_access(admin, True) - post.set_acl(acl) -``` - - - - - -```php -Cloud::beforeSave("Post", function($post, $user) { - $acl = new ACL(); - $acl->setPublicReadAccess(true); - // Assuming a role named `admin` exists - $acl->setRoleWriteAccess("admin", true); - $post->setACL($acl); -}); -``` - - - - - -```java -@EngineHook(className = "Post", type = EngineHookType.beforeSave) -public static AVObject postBeforeSaveHook(AVObject post) throws Exception { - AVACL acl = new AVACL(); - acl.setPublicReadAccess(true); - // Assuming a role named `admin` exists - acl.setRoleWriteAccess("admin", true); - post.setACL(acl); - return post; -} -``` - - - - - -After [deploying the code to the cloud](/sdk/engine/functions/getting-started/#deploy-to-cloud-engine), every post created on the client side from now on will automatically have the following ACL: - -```json -{"*":{"read":true},"role:admin":{"write":true}} -``` diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/datalake.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/datalake.mdx deleted file mode 100644 index 6200788c6..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/datalake.mdx +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: Data Lake Guide -sidebar_label: Data Lake -sidebar_position: 14 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -Based on ClickHouse, a modern database, we have built a new generation of data lake. Tightly integrated with cloud services, it supports automatic entry of stored data and log table data, providing powerful data support for statistical analysis of business operations. - -The new data lake offers the following unique features: - -1. Column-based storage that supports high compression rates, which can significantly reduce storage costs; -1. Column-based traversal and vector operations that support efficient queries, which can reduce complex queries that would otherwise take minutes to seconds; -1. Intuitive data views that support the storage of intermediate results, which, together with join queries, can support highly efficient and complex second-order queries; -1. Richer functionality to support more complex SQL queries. See the [Common Use Cases](#common-use-cases) section below; -1. Seamless connection to log tables that can support real-time input and query from a variety of data sources, allowing for more flexible integration of external data sources and more complete data analysis capabilities for the business. - -## Data Entry - -Before querying the data, we need to store the data first. At present, we mainly support data input from classes in the Data Storage service. Classes are divided into two categories: log tables and basic classes. - -### Log Tables - -Log tables are a special class of data storage designed to meet the business needs of storing events, logs, and other "immutable" data that cannot be changed after it is written. Once collected, this data can be used for business auditing and operational analysis, and for developers to track events. Because this type of data is appended and not modified, we are able to provide greater concurrent write throughput. - -#### Enabling and Accessing Log Tables - -On the "Data Storage" page of the dashboard, click "Create class" and check the "log table" option to create a log table. For example, we can create a log table named `EventLog`. - -At the SDK level, creating a log table object is the same as creating a normal object: - -```java -// for Android -AVObject event = new AVObject("EventLog"); -event.put("eventType", "buttonClick"); -event.put("eventName", "orderSubmit"); -event.put("eventDate", new Date()); -event.saveInBackground(); -``` - -Logs are submitted and stored directly in the data lake and are available in real time. - -### Syncing With Basic Classes - -The process of synchronizing basic class objects to the data lake is a bit more complicated because of the support for updates. - -On the dashboard, go to "Data Storage" - "Data lake", click "Create Class Sync", and select the fields that need to be queried and analyzed in the data lake, then you can start synchronizing the class with the data lake. The synchronization starts immediately. Depending on the size of the data, the synchronization may take a long time, so please be patient. - -For classes with synchronization enabled, we will synchronize the previous day's updated data in the early morning of the next day. Therefore, the updated objects will not be visible until the next day. We will continue to tweak this process to achieve more real-time synchronization. - -### Data Type Conversion - -The field types in the data lake are significantly different from those in the Data Dtorage service, and we perform data conversions during the data entry process. Among the things that need special attention are that - -1. The data lake does not support the `null` type, and missing fields are stored as zero values. The zero value of a string type is an empty string, and the zero value of a numeric field is the number 0. -1. The `Boolean` type is converted to `UInt8`. 0 means false and 1 means true. With the above zero-value feature, the default value for a missing field is 0, which means false. -1. Nested objects are not supported. They are stored as JSON strings and must be extracted using the JSON function. -1. For array types, the elements must be of the same type. They are converted to `Array(String)` during data entry, and you must use the type conversion function to convert them back to their original type. - -See the following table for specific type conversion behavior: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Data Storage TypeData Lake TypeDescriptionExample
    ArrayArray(String)The element type is stored as a string, and you need to use the type to convert the elements to the original type after extracting the elementstoInt64(xs[1])
    BooleanUInt80 means false, 1 means trueemailVerified = 1
    BytesN/ANot supported
    DateDateTime
    File - *.className String
    - *.objectId String -
    Is expanded to two fields: className and objectId
    GeoPointPointExperimental support
    NumberFloat64
    ObjectStringStored as JSON string; you must extract the fields using the appropriate functionsvisitParamExtractInt64(object, 'id')
    Pointer - *.className String
    - *.objectId String -
    Is expanded to two fields: className and objectId
    - -See [ClickHouse Data Types](https://clickhouse.com/docs/en/sql-reference/data-types/) for all the types supported by the data lake, and [Type Conversion Functions](https://clickhouse.com/docs/en/sql-reference/functions/type-conversion-functions/) for how to convert data between different types. - -## Query Syntax - -The data lake only supports `select` queries. The syntax looks like this - -```sql -SELECT column1, column2, expr ... -FROM table | (subquery) -[INNER|LEFT|RIGHT|FULL|CROSS] JOIN (subquery)|table (ON ) -[WHERE expr] -[GROUP BY expr_list] -[HAVING expr] -[ORDER BY expr_list] -[LIMIT [skip, ]n] -[UNION ...] -``` - -Note that strings, dates, and certain other values must be enclosed in **single quotes**. For special characters in the field or table name, you can use backquotes to enclose them in quotation marks. For example - -```sql -WHERE order_status = 'delivered' - AND `from` = 'example.com' -``` - -For more query syntax, please refer to the [ClickHouse documentation](https://clickhouse.com/docs/en/sql-reference/statements/select/). - -## Common Use Cases - -#### Extracting JSON Fields Using visitParamExtract* - -During data synchronization, multiple layers of nested objects are imported as JSON strings. To extract the fields from a JSON string, it is recommended to use `visitParamExtract*`. For example - -```sql -SELECT visitParamExtractString('{"abc":"\\u263a"}', 'abc') = '☺'; -``` - -If the nesting is complex and there are overlapping fields, you can use `JSONExtract*`. For example - -```sql -SELECT JSONExtractFloat('{"a": "hello", "b": [-100, 200.0, 300]}', 'b', 2) = 200.0 -``` - -More JSON extraction functions can be found in the [official documentation](https://clickhouse.com/docs/en/sql-reference/functions/json-functions/#visitparamextractuintparams-name). - -#### Time Zone Conversion With toTimeZone - -By default, dates are displayed in the server-side timezone. If this is not expected, you can use `toTimeZone` to convert the date to a specific timezone. - -```sql -toTimeZone(createdAt, 'Asia/Shanghai') -``` - -#### Using toYYYYMMDD for Statistics by Date - -When analyzing statistics by date, you can use `toYYYYMMDD` to stringify the date and use "group by". You also need to be aware of time zone handling, for example - -```sql -GROUP BY toYYYYMMDD(toTimeZone(createdAt, 'Asia/Shanghai')) -``` - -#### Use argMax to Extract the Most Recent Version - -When you encounter duplicate data, you can use argMax to extract the last piece of data as the "latest version" of the data. For example, we can extract the latest status of a particular order - -```sql -SELECT - orderId, - argMax(status, updatedAt) AS status -FROM my_class -GROUP BY orderId -``` - - -## Other Restrictions - -* For security reasons, the `sessionToken`, `password`, `salt`, and `authData` fields of the `_User` table and the `ACL` fields of all tables do not support synchronization. -* For performance reasons, large array fields such as `m` and `mu` of the `_Conversation` table do not support synchronization at this time. -* To better isolate the impact between applications, we impose quota limits on slow queries from the same application. For example, we only allow a maximum of 3 slow queries to run simultaneously. If a query takes more than 5 seconds, it is a slow query. - -## REST API - -Coming soon. - -## Pricing - -The data lake feature is only available to applications with the Business Planis available to all games and is currently in beta preview, so there is no charge for now. When we officially release the feature, we will charge for both "storage" and "compute resources". diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/dotnet-guide/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/dotnet-guide/_category_.json deleted file mode 100644 index 61bdfc91e..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/dotnet-guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": ".NET", - "collapsed": true, - "position": 2 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/dotnet-guide/dotnet.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/dotnet-guide/dotnet.mdx deleted file mode 100644 index 13e289fb8..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/dotnet-guide/dotnet.mdx +++ /dev/null @@ -1,1060 +0,0 @@ ---- -title: Data Storage Guide for .NET -sidebar_label: .NET Guide -slug: /sdk/storage/guide/dotnet/ -sidebar_position: 2 ---- - -import Path from "/src/docComponents/path"; - -With the Data Storage service, you can have your app persist data on the cloud and query them at any time. The code below shows how you can create an object and store it into the cloud: - -```cs -// Create an object -LCObject todo = new LCObject("Todo"); -// Set values of fields -todo["title"] = "R&D Weekly Meeting"; -todo["content"] = "All team members, Tue 2pm"; -// Save the object to the cloud -await todo.Save(); -``` - -The SDK designed for each language interacts with the same REST API via HTTPS, offering fully functional interfaces for you to manipulate the data in the cloud. - -## Installing SDK - -See [Installing .NET SDK](/sdk/storage/guide/setup-dotnet/). - -## Objects - -### `LCObject` - -The objects on the cloud are built around `LCObject`. Each `LCObject` contains key-value pairs of JSON-compatible data. This data is schema-free, which means that you don't need to specify ahead of time what keys exist on each `LCObject`. Simply set whatever key-value pairs you want, and our backend will store them. - -For example, the `LCObject` storing a simple todo item may contain the following data: - -```json -title: "Email Linda to Confirm Appointment", -isComplete: false, -priority: 2, -tags: ["work", "sales"] -``` - -### Data Types - -`LCObject` supports a wide range of data types to be used for each field, including common ones like `String`, `Number`, `Boolean`, `Object`, `Array`, and `Date`. You can nest objects in JSON format to store more structured data within a single `Object` or `Array` field. - -Special data types supported by `LCObject` include `Pointer` and `File`, which are used to store a reference to another `LCObject` and binary data respectively. - -`LCObject` also supports `GeoPoint`, a special data type you can use to store location-based data. See [GeoPoints](#geopoints) for more details. - -Some examples: - -```cs -// Basic types -int numberValue = 2018; -bool boolValue = true; -string stringValue = "hello, world"; -DateTime now = DateTime.Now; -List intList = new List { 1, 2, 3 }; -Dictionary dict = new Dictionary { - { "year", 1780 }, - { "first", "partridge" }, - { "second", "turtledoves" }, - { "fifth", "golden rings" } -}; - -// Create an object -LCObject object = new LCObject("Hello"); -object["numberValue"] = numberValue; -object["boolValue"] = boolValue; -object["stringValue"] = stringValue; -object["time"] = now; -object["intList"] = intList; -object["dictValue"] = dict; -``` - -We do not recommend storing large pieces of binary data like images or documents with `LCObject` using `byte[]`. The size of each `LCObject` should not exceed **128 KB**. We recommend using `LCFile` for storing images, documents, and other types of files. To do so, create `LCFile` objects and assign them to fields of `LCObject`. See [Files](#files) for details. - -Keep in mind that our backend stores dates in UTC format and the SDK will convert them to local times upon retrieval. - -The date values displayed on ** > Data** are also converted to match your operating system's time zone. The only exception is that when you retrieve these date values through our REST API, they will remain in UTC format. You can manually convert them using appropriate time zones when necessary. - -To learn about how you can protect the data stored on the cloud, see [Data Security](/sdk/storage/guide/security/). - -### Creating Objects - -The code below creates a new instance of `LCObject` with class `Todo`: - -```cs -LCObject object = new LCObject("Todo"); -``` - -The constructor takes a class name as a parameter so that the cloud knows the class you are using to create the object. A class is comparable to a table in a relational database. A class name starts with a letter and can only contain numbers, letters, and underscores. - -### Saving Objects - -The following code saves a new object with class `Todo` to the cloud: - -```cs -// Create an object -LCObject todo = new LCObject("Todo"); -// Set values of fields -todo["title"] = "Sign up for Marathon"; -todo["priority"] = 2; -// Save the object to the cloud -await todo.Save(); -``` - -To make sure the object is successfully saved, take a look at ** > Data > `Todo`** in your app. You should see a new entry of data with something like this when you click on its `objectId`: - -```json -{ - "title": "Sign up for Marathon", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -You don't have to create or set up a new class called `Todo` in ** > Data** before running the code above. If the class doesn't exist, it will be automatically created. - -Several built-in fields are provided by default which you don't need to specify in your code: - -| Built-in Field | Type | Description | -| -------------- | ---------- | ---------------------------------------------------------------------------------------------- | -| `objectId` | `string` | A unique identifier for each saved object. | -| `ACL` | `LCACL` | Access Control List, a special object defining the read and write permissions of other people. | -| `createdAt` | `DateTime` | The time the object was created. | -| `updatedAt` | `DateTime` | The time the object was last modified. | - -Each of these fields is filled in by the cloud automatically and doesn't exist on the local `LCObject` until a save operation has been completed. - -Field names, or **keys**, can only contain letters, numbers, and underscores. A custom key can neither start with double underscores `__`, nor be identical to any system reserved words or built-in field names (`ACL`, `className`, `createdAt`, `objectId`, and `updatedAt`) regardless of letter cases. - -**Values** can be strings, numbers, booleans, or even arrays and dictionaries — anything that can be JSON-encoded. See [Data Types](#data-types) for more information. - -We recommend that you adopt CamelCase naming convention to `NameYourClassesLikeThis` and `nameYourKeysLikeThis`, which keeps your code more readable. - -### Retrieving Objects - -If an `LCObject` is already in the cloud, you can retrieve it using its `objectId` with the following code: - -```cs -LCQuery query = new LCQuery("Todo"); -LCObject todo = await query.Get("582570f38ac247004f39c24b"); -// todo is the instance of the Todo object with objectId 582570f38ac247004f39c24b -string title = todo["title"] as string; -int priority = (int)(todo["priority"]); - -// Get special properties -string objectId = todo.ObjectId; -DateTime updatedAt = todo.UpdatedAt; -DateTime createdAt = todo.CreatedAt; -``` - -If you try to access a field or property that doesn't exist, the SDK will not raise an error. Instead, it will return `null`. - -#### Refreshing Objects - -If you need to refresh a local object with the latest version of it in the cloud, call the `Fetch` method on it: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -await todo.Fetch(); -// todo is refreshed -``` - -Keep in mind that **any unsaved changes made to the object prior to calling `Fetch` will be discarded**. To avoid this, you have the option to provide **a list of keys** when calling the method so that only the fields being specified are retrieved and refreshed (including special built-in fields such as `objectId`, `createdAt`, and `updatedAt`). Changes made to other fields will remain intact. - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -await todo.Fetch(includes: new string[] { "priority", "location" }); -// Only priority and location will be retrieved and refreshed -``` - -### Updating Objects - -To update an existing object, assign the new data to each field and call the `Save` method. For example: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -todo["content"] = "Weekly meeting has been rescheduled to Wed 3pm for this week."; -await todo.Save(); -``` - -The cloud automatically figures out which data has changed and only the fields with changes will be sent to the cloud. The fields you didn't update will remain intact. - -#### Updating Data Conditionally - -By passing in a `query` option when saving, you can specify conditions on the save operation so that the object can be updated atomically only when those conditions are met. If no object matches the conditions, the cloud will return error `305` to indicate that there was no update taking place. - -For example, in the class `Account` there is a field called `balance`, and there are multiple incoming requests that want to modify this field. Since an account cannot have a negative balance, we can only allow a request to update the balance when the amount requested is lower than or equal to the balance: - -```cs -try { - LCObject account = LCObject.CreateWithoutData("Account", "5745557f71cfe40068c6abe0"); - // Atomically decrease balance by 100 - int amount = -100; - account.Increment("balance", amount); - // Add the condition - LCQuery query = new LCQuery("Account"); - query.WhereGreaterThanOrEqualTo("balance", -amount); - // Return the latest data in the cloud upon completion. - // All the fields will be returned if the object is new, - // otherwise only fields with changes will be returned. - await account.Save(fetchWhenSave: true, query: query); - print($"Balance: {account["balance"]}"); -} catch (LCException e) { - if (e.code == 305) { - print("Insufficient balance. Operation failed!"); - } -} -``` - -**The `query` option only works for existing objects.** In other words, it does not affect objects that haven't been saved to the cloud yet. - -The benefit of using the `query` option instead of combining `LCQuery` and `LCObject` shows up when you have multiple clients trying to update the same field at the same time. The latter way is more cumbersome and may lead to potential inconsistencies. - -#### Updating Counters - -Take Twitter as an example, we need to keep track of how many Likes and Retweets a tweet has gained so far. Since a Like or Retweet action can be triggered simultaneously by multiple clients, saving objects with updated values directly can lead to inaccurate results. To make sure that the total number is stored correctly, you can **atomically** increase (or decrease) the value of a number field: - -```cs -post.Increment("likes", 1); -``` - -You can specify the amount of increment (or decrement) by providing an additional argument. If the argument is not provided, `1` is used by default. - -#### Updating Arrays - -There are several operations that can be used to atomically update an array associated with a given key: - -- `Add(key, value)` appends the given object to the end of an array. -- `AddAll(key, values)` appends the given array of objects to the end of an array. -- `AddUnique(key, value)` appends the given object to the end of an array ensuring that the object only appears once within the array. -- `AddAllUnique(key, values)` appends the given array of objects to the end of an array ensuring that each object only appears once within the array. -- `Remove(key, value)` removes all instances of the given object from an array. -- `RemoveAll(key, values)` removes all instances of the given array of objects from an array. - -For example, `Todo` has a field named `alarms` for keeping track of the times at which a user wants to be alerted. The following code adds the times to the alarms field: - -```cs -DateTime alarm1 = DateTime.Parse("2018-04-30 07:10:00Z"); -DateTime alarm2 = DateTime.Parse("2018-04-30 07:20:00Z"); -DateTime alarm3 = DateTime.Parse("2018-04-30 07:30:00Z"); - -LCObject todo = new LCObject("Todo"); -todo.AddAllUnique("alarms", new object[] { alarm1, alarm2, alarm3 }); -await todo.Save(); -``` - -### Deleting Objects - -The following code deletes a `Todo` object from the cloud: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -await todo.Delete(); -``` - -You can delete a given field of an object with the `unset` method: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); - -// priority will be deleted -todo.Unset("priority"); - -// Save the object -await todo.Save(); -``` - -Removing data from the cloud should always be dealt with great caution as it may lead to non-recoverable data loss. We strongly advise that you read [ACL Guide](/sdk/storage/guide/acl/) to understand the risks thoroughly. You should also consider implementing class-level, object-level, and field-level permissions for your classes in the cloud to guard against unauthorized data operations. - -### Batch Processing - -```cs -// Batch create and update -LCObject.SaveAll(); - -// Batch delete -LCObject.DeleteAll(); - -// Batch fetch -LCObject.FetchAll(); -``` - -The following code sets `isComplete` of all `Todo` objects to be `true`: - -```cs -LCQuery query = new LCQuery("Todo"); -ReadOnlyCollection results = await query.Find(); -// Get a collection of todos to work on -foreach (LCObject todo in results) { - // Update value - todo["isComplete"] = true; -} -await LCObject.SaveAll(results); -``` - -Although each function call sends multiple operations in one single network request, saving operations and fetching operations are billed as separate API calls for each object in the collection, while deleting operations are billed as a single API call. - -### Data Models - -Objects may have relationships with other objects. For example, in a blogging application, a `Post` object may have relationships with many `Comment` objects. The Data Storage service supports three kinds of relationships, including one-to-one, one-to-many, and many-to-many. - -#### One-to-One and One-to-Many Relationships - -One-to-one and one-to-many relationships are modeled by saving `LCObject` as a value in the other object. For example, each `Comment` in a blogging app might correspond to one `Post`. - -The following code creates a new `Post` with a single `Comment`: - -```cs -// Create a post -LCObject post = new LCObject("Post"); -post["title"] = "I am starving!"; -post["content"] = "Hmmm, where should I go for lunch?"; - -// Create a comment -LCObject comment = new LCObject("Comment"); -comment["content"] = "How about KFC?"; - -// Add the post as a property of the comment -comment["parent"] = post; - -// This will save both post and comment -await comment.Save(); -``` - -Internally, the backend will store the referred-to object with the `Pointer` type in just one place in order to maintain consistency. You can also link objects using their `objectId`s like this: - -```cs -LCObject post = LCObject.CreateWithoutData("Post", "57328ca079bc44005c2472d0"); -comment["post"] = post; -``` - -See [Relational Queries](#relational-queries) for instructions on how to query relational data. - -#### Many-to-Many Relationships - -The easiest way to model many-to-many relationships is to use **arrays**. In most cases, using arrays helps you reduce the number of queries you need to make and leads to better performance. However, if additional properties need to be attached to the relationships between two classes, using **join tables** would be a better choice. Keep in mind that the additional properties are used to describe the relationships between classes rather than any single class. - -We recommend you to use join tables if the total number of objects of any class exceeds 100. - -### Serialization and Deserialization - -If you need to pass an `LCObject` to a method as an argument, you may want to first serialize the object to avoid certain problems. You can use the following ways to serialize and deserialize `LCObject`s. - -Serialization: - -```cs -LCObject object = new LCObject("Todo"); -object["title"] = "Sign up for Marathon"; -object["priority"] = 2; -object["owner"] = await LCUser.GetCurrent(); -string serializedString = object.ToString(); -``` - -Deserialization: - -```cs -LCObject newObject = LCObject.ParseObject(json); -await newObject.Save(); -``` - -## Queries - -We've already seen how you can retrieve a single object from the cloud with `LCObject`, but it doesn't seem to be powerful enough when you need to retrieve multiple objects that match certain conditions at once. In such a situation, `LCQuery` would be a more efficient tool you can use. - -### Basic Queries - -The general steps of performing a basic query include: - -1. Creating `LCQuery`. -2. Putting conditions on it. -3. Retrieving an array of objects matching the conditions. - -The code below retrieves all `Student` objects whose `lastName` is `Smith`: - -```cs -LCQuery query = new LCQuery("Student"); -query.WhereEqualTo("lastName", "Smith"); -// students is an array of Student objects satisfying conditions -ReadOnlyCollection students = await query.Find(); -``` - -### Query Constraints - -There are several ways to put constraints on the objects found by `LCObject`. - -The code below filters out objects with `Jack` as `firstName`: - -```cs -query.WhereNotEqualTo("firstName", "Jack"); -``` - -For sortable types like numbers and strings, you can use comparisons in queries: - -```cs -// Restricts to age < 18 -query.WhereLessThan("age", 18); - -// Restricts to age <= 18 -query.WhereLessThanOrEqualTo("age", 18); - -// Restricts to age > 18 -query.WhereGreaterThan("age", 18); - -// Restricts to age >= 18 -query.WhereGreaterThanOrEqualTo("age", 18); -``` - -You can apply multiple constraints to a single query, and objects will only be in the results if they match all of the constraints. In other words, it's like concatenating constraints with `AND`: - -```cs -query.WhereEqualTo("firstName", "Jack"); -query.WhereGreaterThan("age", 18); -``` - -You can limit the number of results by setting `limit` (defaults to `100`): - -```cs -// Get at most 10 results -query.Limit(10); -``` - -For performance reasons, the maximum value allowed for `limit` is `1000`, meaning that the cloud would only return 1,000 results even if it is set to be greater than `1000`. - -If you need exactly one result, you may use `First` for convenience: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("priority", 2); -// todo is the first Todo object satisfying conditions -LCObject todo = await query.First(); -``` - -You can skip a certain number of results by setting `skip`: - -```cs -query.Skip(20); -``` - -You can implement pagination in your app by using `skip` together with `limit`: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("priority", 2); -query.Limit(10); -query.Skip(20); -``` - -Keep in mind that the higher the `skip` goes, the slower the query will run. You may consider using `createdAt` or `updatedAt` (which are indexed) to set range boundaries for large datasets to make queries more efficient. -You may also use the last value returned from an auto-increment field along with `limit` for pagination. - -For sortable types, you can control the order in which results are returned: - -```cs -// Sorts the results in ascending order by the createdAt property -query.OrderByAscending("createdAt"); - -// Sorts the results in descending order by the createdAt property -query.OrderByDescending("createdAt"); -``` - -You can even attach multiple sorting rules to a single query: - -```cs -query.AddAscendingOrder("priority"); -query.AddDescendingOrder("createdAt"); -``` - -You can restrict the fields returned by providing a list of keys with `Select`. The code below retrieves todos with only the `title` and `content` fields (and also special built-in fields including `objectId`, `createdAt`, and `updatedAt`): - -```cs -LCQuery query = new LCQuery("Todo"); -query.Select("title"); -query.Select("content"); -LCObject todo = await query.First(); - -string title = todo["title"] as string; // √ -string content = todo["content"] as string; // √ -string notes = todo["notes"] as string; // null -``` - -You can add a minus prefix to the attribute name for inverted selection. -For example, if you do not care about the post author, use `-author`. -The inverted selection also applies to preserved attributes and can be used with dot notations, e.g., `-pubUser.createdAt`. - -The unselected fields can be fetched later with `Fetch`. See [Refreshing Objects](#refreshing-objects). - -### Queries on String Values - -Use `WhereStartsWith` to restrict to string values that start with a particular string. Similar to a `LIKE` operator in SQL, it is indexed so it is efficient for large datasets: - -```cs -LCQuery query = new LCQuery("Todo"); -// SQL equivalent: title LIKE 'lunch%' -query.WhereStartsWith("title", "lunch"); -``` - -Use `WhereContains` to restrict to string values that contain a particular string: - -```cs -LCQuery query = new LCQuery("Todo"); -// SQL equivalent: title LIKE '%lunch%' -query.WhereContains("title", "lunch"); -``` - -Unlike `WhereStartsWith`, `WhereContains` can't take advantage of indexes, so it is not encouraged to be used for large datasets. - -Please note that both `WhereStartsWith` and `WhereContains` perform **case-sensitive** matching, so the examples above will not look for string values containing `Lunch`, `LUNCH`, etc. - -If you are looking for string values that do not contain a particular string, use `WhereMatches` with regular expressions: - -```cs -LCQuery query = new LCQuery("Todo"); -// "title" without "ticket" (case-insensitive) -query.WhereMatches("title", "^((?!ticket).)*\$", modifiers: "i"); -``` - -However, performing queries with regular expressions as constraints can be very expensive, especially for classes with over 100,000 records. The reason behind this is that queries like this can't take advantage of indexes and will lead to exhaustive scanning of the whole dataset to find the matching objects. We recommend that you take a look at our In-App Searching feature, a full-text search solution we provide to improve your app's searching ability and user experience. - -If you are facing performance issues with queries, please refer to [Optimizing Performance](#optimizing-performance) for possible workarounds and best practices. - -### Queries on Array Values - -The code below looks for all the objects with `work` as an element of its array field `tags`: - -```cs -query.WhereEqualTo("tags", "work"); -``` - -To look for objects whose array field `tags` contains three elements: - -```cs -query.WhereSizeEqualTo("tags", 3); -``` - -You can also look for objects whose array field `tags` contains `work`, `sales`, **and** `appointment`: - -```cs -query.WhereContainsAll("tags", new string[] { "work", "sales", "appointment" }); -``` - -To retrieve objects whose field matches any one of the values in a given list, you can use `WhereContainedIn` instead of performing multiple queries. The code below constructs a query that retrieves todo items with `priority` to be `1` **or** `2`: - -```cs -// Single query -LCQuery priorityOneOrTwo = new LCQuery("Todo"); -priorityOneOrTwo.WhereContainedIn("priority", new string[] { 1, 2 }); -// Mission completed :) - -// --------------- -// vs. -// --------------- - -// Multiple queries -LCQuery priorityOne = new LCQuery("Todo"); -priorityOne.WhereEqualTo("priority", 1); - -LCQuery priorityTwo = new LCQuery("Todo"); -priorityTwo.WhereEqualTo("priority", 2); - -LCQuery priorityOneOrTwo = LCQuery.Or(new LCQuery[] { priorityOne, priorityTwo }); -ReadOnlyCollection results = await priorityOneOrTwo.Find(); -// Kind of verbose :( -``` - -Conversely, you can use `WhereNotContainedIn` if you want to retrieve objects that do not match any of the values in a list. - -### Relational Queries - -There are several ways to perform queries for relational data. To retrieve objects whose given field matches a particular `LCObject`, you can use `WhereEqualTo` just like how you use it for other data types. For example, if each `Comment` has a `Post` object in its `post` field, you can fetch all the comments for a particular `Post` with the following code: - -```cs -LCObject post = LCObject.CreateWithoutData("Post", "57328ca079bc44005c2472d0"); -LCQuery query = new LCQuery("Comment"); -query.WhereEqualTo("post", post); -// comments contains the comments for the post -ReadOnlyCollection comments = await query.Find(); -``` - -To retrieve objects whose given field contains an `LCObject` that matches a different query, you can use `WhereMatchesQuery`. The code below constructs a query that looks for all the comments for posts with images: - -```cs -LCQuery innerQuery = new LCQuery("Post"); -innerQuery.WhereExists("image"); - -LCQuery query = new LCQuery("Comment"); -query.WhereMatchesQuery("post", innerQuery); -``` - -To retrieve objects whose given field does not contain an `LCObject` that matches a different query, use `WhereDoesNotMatchQuery` instead. - -Sometimes you may need to look for related objects from different classes without extra queries. In such situations, you can use `Include` on the same query. The following code retrieves the last 10 comments together with the posts related to them: - -```cs -LCQuery query = new LCQuery("Comment"); - -// Retrieve the most recent ones -query.OrderByDescending("createdAt"); - -// Only retrieve the last 10 -query.Limit(10); - -// Include the related post together with each comment -query.Include("post"); - -// comments contains the last 10 comments including the post associated with each -ReadOnlyCollection comments = await query.Find(); -foreach (LCObject comment in comments) { -// This does not require a network access - LCObject post = comment["post"] as LCObject; -} -``` - -#### Caveats about Inner Queries - -The Data Storage service is not built on relational databases, which makes it impossible to join tables while querying. For the relational queries mentioned above, what we would do is to perform an inner query first (with `100` as the default `limit` and `1000` as the maximum) and then insert the result from this query into the outer query. If the number of records matching the inner query exceeds the `limit` and the outer query contains other constraints, the amount of the records returned in the end could be zero or less than your expectation since only the records within the `limit` would be inserted into the outer query. - -The following actions can be taken to solve the problem: - -- Make sure the number of records in the result of the inner query is no more than 100. If it is between 100 and 1,000, set `1000` as the `limit` of the inner query. -- Create redundancy for the fields being queried by the inner query on the table for the outer query. -- Repeat the same query with different `skip` values until all the records are gone through (performance issue could occur if the value of `skip` gets too big). - -### Counting Objects - -If you just need to count how many objects match a query but do not need to retrieve the actual objects, use `Count` instead of `Find`. For example, to count how many todos have been completed: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("isComplete", true); -int count = await query.Count(); -print($"{count} todos completed."); -``` - -### Compound Queries - -Compound queries can be used if complex query conditions need to be specified. A compound query is a logical combination (`OR` or `AND`) of subqueries. - -Note that we do not support `GeoPoint` or non-filtering constraints (e.g. `near`, `withinGeoBox`, `limit`, `skip`, `ascending`, `descending`, `include`) in the subqueries of a compound query. - -#### OR-ed Query Constraints - -An object will be returned as long as it fulfills any one of the subqueries. The code below constructs a query that looks for all the todos that either have priorities higher than or equal to `3`, or are already completed: - -```cs -LCQuery priorityQuery = new LCQuery("Todo"); -priorityQuery.WhereGreaterThanOrEqualTo("priority", 3); - -LCQuery isCompleteQuery = new LCQuery("Todo"); -isCompleteQuery.WhereEqualTo("isComplete", true); - -LCQuery priorityOneOrTwo = LCQuery.Or(new LCQuery[] { priorityQuery, isCompleteQuery }); -ReadOnlyCollection results = await priorityOneOrTwo.Find(); -``` - -Queries regarding `GeoPoint` cannot be present among OR-ed queries. - -#### AND-ed Query Constraints - -The effect of using AND-ed query is the same as adding constraints to `LCQuery`. The code below constructs a query that looks for all the todos that are created between `2016-11-13` and `2016-12-02`: - -```cs -LCQuery startDateQuery = new LCQuery("Todo"); -startDateQuery.WhereGreaterThanOrEqualTo("createdAt", DateTime.Parse("2016-11-13 00:00:00Z")); - -LCQuery endDateQuery = new LCQuery("Todo"); -endDateQuery.WhereLessThan("createdAt", DateTime.Parse("2016-12-03 00:00:00Z")); - -LCQuery query = LCQuery.And(new LCQuery[] { startDateQuery, endDateQuery }); -ReadOnlyCollection results = await query.Find(); -``` - -While using an AND-ed query by itself doesn't bring anything new compared to a basic query, to combine two or more OR-ed queries, you have to use AND-ed queries: - -```cs -LCQuery createdAtQuery = new LCQuery("Todo"); -createdAtQuery.WhereGreaterThanOrEqualTo("createdAt", DateTime.Parse("2018-04-30 00:00:00Z")); -createdAtQuery.WhereLessThan("createdAt", DateTime.Parse("2018-05-01 00:00:00Z")); - -LCQuery locationQuery = new LCQuery("Todo"); -locationQuery.WhereDoesNotExist("location"); - -LCQuery priority2Query = new LCQuery("Todo"); -priority2Query.WhereEqualTo("priority", 2); - -LCQuery priority3Query = new LCQuery("Todo"); -priority3Query.WhereEqualTo("priority", 3); - -LCQuery priorityQuery = LCQuery.Or(new LCQuery[] { priority2Query, priority3Query }); -LCQuery timeLocationQuery = LCQuery.Or(new LCQuery[] { locationQuery, createdAtQuery }); -LCQuery query = LCQuery.And(new LCQuery[] { priorityQuery, timeLocationQuery }); -``` - -### Optimizing Performance - -There are several factors that could lead to potential performance issues when you conduct a query, especially when more than 100,000 records are returned at a time. We are listing some common ones here so you can design your apps accordingly to avoid them: - -- Querying with "not equal to" or "not include" (index will not work) -- Querying on strings with a wildcard at the beginning of the pattern (index will not work) -- Using `count` with conditions (all the entries will be gone through) -- Using `skip` for a large number of entries (all the entries that need to be skipped will be gone through) -- Sorting without an index (querying and sorting cannot share a composite index unless the conditions used on them are both covered by the same one) -- Querying without an index (the conditions used on the query cannot share a composite index unless all of them are covered by the same one; additional time will be consumed if excessive data falls under the uncovered conditions) - -## LiveQuery - -LiveQuery is, as its name implies, derived from [`LCQuery`](#queries) but has enhanced capability. It allows you to automatically synchronize data changes from one client to other clients without writing complex code, making it suitable for apps that need real-time data. - -Suppose you are building an app that allows multiple users to edit the same file at the same time. `LCQuery` would not be an ideal tool since it is based on a pull model and you cannot know when to query from the cloud to get the updates. - -To solve this problem, we introduced LiveQuery. This tool allows you to subscribe to the `LCQuery`s you are interested in. Once subscribed, the cloud will notify clients by generating event messages whenever `LCObject`s that match the `LCQuery` are created or updated, in real-time. - -Behind the scenes, we use WebSocket connections to have clients and the cloud communicate with each other and maintain the subscription status of clients. In most cases, it isn't necessary to deal with the WebSocket connections directly, so we developed a simple API to help you focus on your business logic rather than technical implementations. - -### Initializing LiveQuery - -To use LiveQuery in your app, go to ** > Settings** and check the **Enable LiveQuery** option under the **Security** section. - -### Demo - -We’ve made a demo app called “LeanTodo” which shows the functionality of LiveQuery. If you’d like to try it: - -1. Go to , enter a username and a password, and then hit “Signup”. -2. Open the same URL on a different device, enter the same credentials, and hit “Login”. -3. Create, edit, or delete some items on one device and watch what happens on the other one. - -### Creating a Subscription - -To make a query **live**, create an `LCQuery` object, put conditions on it if there are any, and then subscribe to events: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("isComplete", true); -await query.Subscribe(); -// Subscribed -``` - -You can't use subqueries or restrict fields being returned when using LiveQuery. - -Now you will be able to receive updates related to `LCObject`. If a `Todo` object is created by another client with `Update Portfolio` as `title`, the following code can get the new `Todo` for you: - -```cs -LCQuery query = new LCQuery("Todo"); -LCLiveQuery liveQuery = await query.Subscribe(); -liveQuery.OnCreate = (obj) => { - print(obj["title"]); // Update Portfolio -}; -``` - -If someone updates this `Todo` by changing its `content` to `Add my recent paintings`, the following code can get the updated version for you: - -```cs -liveQuery.OnUpdate = (updatedTodo, updatedKeys) => { - print(updatedTodo["content"]); // Add my recent paintings -}; -``` - -### Event Handling - -The following types of data changes can be monitored once a subscription is set up: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` Event - -A `create` event will be triggered when a new `LCObject` is created and fulfills the `LCQuery` you subscribed to. The `obj` is the new `LCObject` being created: - -```cs -liveQuery.OnCreate = (obj) => { - print("Object created."); -}; -``` - -#### `update` Event - -An `update` event will be triggered when an existing `LCObject` fulfilling the `LCQuery` you subscribed to is updated. The `obj` is the `LCObject` being updated: - -```cs -liveQuery.OnUpdate = (obj, updatedKeys) => { - print("Object updated."); -}; -``` - -#### `enter` Event - -An `enter` event will be triggered when an existing `LCObject`'s old value does not fulfill the `LCQuery` you subscribed to but its new value does. The `obj` is the `LCObject` entering the `LCQuery` and its content is the latest value of it: - -```cs -liveQuery.OnEnter = (obj, updatedKeys) => { - print("Object entered."); -}; -``` - -There is a difference between a `create` event and an `enter` event. If an object already exists and later matches the query's conditions, an `enter` event will be triggered. If an object didn't exist already and is later created, a `create` event will be triggered. - -#### `leave` Event - -A `leave` event will be triggered when an existing `LCObject`'s old value fulfills the `LCQuery` you subscribed to but its new value does not. The `obj` is the `LCObject` leaving the `LCQuery` and its content is the latest value of it: - -```cs -liveQuery.OnLeave = (obj, updatedKeys) => { - print("Object left."); -}; -``` - -#### `delete` Event - -A `delete` event will be triggered when an existing `LCObject` fulfilling the `LCQuery` you subscribed to is deleted. The `objId` is the `objectId` of the `LCObject` being deleted: - -```cs -liveQuery.OnDelete = (objId) => { - print("Object deleted."); -}; -``` - -### Unsubscribing - -You can cancel a subscription to stop receiving events regarding `LCQuery`. After that, you won't get any events from the subscription. - -```cs -await liveQuery.Unsubscribe(); -// Successfully unsubscribed -``` - -### Losing Connections - -There are different scenarios regarding losing connections: - -1. The connection to the Internet is lost unexpectedly. -2. The user performs certain operations outside of the app, like switching the app to the background, turning off the phone, or turning on the flight mode. - -For the scenarios above, you don't need to do any extra work. As long as the user switches back to the app, the SDK will automatically re-establish the connection. - -There is another scenario when **the user completely kills the app or closes the web page**. In this case, the SDK cannot automatically re-establish the connection. You will have to create subscriptions again by yourself. - -### Caveats about LiveQuery - -Given the real-time feature of LiveQuery, developers may find it tempting to use it for instant messaging. As LiveQuery is neither designed nor optimized for completing such tasks, we discourage such use of this tool, let alone there will be an additional cost for saving message history and rising challenges of code maintenance. We recommend using our Instant Messaging service for this scenario. - -## Files - -`LCFile` allows you to store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `LCObject`. The most common use case is storing images, but you can also use it for documents, videos, music, and any other binary data. - -### Creating Files - -You can create a file from a string: - -```cs -LCFile file = new LCFile("resume.txt", UTF8.GetBytes("LeanCloud")); -``` - -You can also create a file from a URL: - -```cs -LCFile file = new LCFile("logo.png", new Uri("https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png")); -``` - -When creating files from URLs, the SDK will not upload the actual files into the cloud but will store the addresses of the files as strings. This will not lead to actual traffic for uploading files, as opposed to creating files in other ways by doing which the files will be actually stored into the cloud. - -The cloud will auto-detect the type of the file you are uploading based on the file extension, but you can also specify the `Content-Type` (commonly referred to as MIME type): - -```cs -LCFile file = new LCFile("resume.txt", UTF8.GetBytes("LeanCloud")); -file.MimeType = "application/json"; -``` - -But the most common method for creating files is to upload them from local paths: - -```cs -LCFile file = new LCFile("avatar.jpg", "./avatar.jpg"); -``` - -The file we uploaded here is named `avatar.jpg`. There are a couple of things to note here: - -- Each file uploaded will get its unique `objectId`, so it is allowed for multiple files to share the same name. -- A correct extension needs to be assigned to each file which the cloud will use to infer the type of a file. For example, if you are storing a PNG image with `LCFile`, use `.png` as its extension. -- If the file doesn't have an extension and the content type is not specified, the file will get the default type `application/octet-stream`. - -### Saving Files - -By saving a file, you store it into the cloud and get a permanent URL pointing to it: - -```cs -await file.Save(); -print(file.Url); -``` - -A file successfully uploaded can be found in ** > Files** and cannot be modified later. If you need to change the file, you have to upload the modified file again and a new `objectId` and URL will be generated. - -You can associate a file with `LCObject` after it has been saved: - -```cs -LCObject todo = new LCObject("Todo"); -todo["title"] = "Buy Cakes"; -// The type of attachments is LCFile[] -todo.Add("attachments", file); -await todo.Save(); -``` - -You can also construct an `LCQuery` to query files: - -```cs -LCQuery query = LCFile.GetQuery(); -``` - -Note that the `url` field of internal files (files uploaded to the file service) is dynamically generated by the cloud, which will switch custom domain names automatically. -Therefore, querying files by the `url` field is only applicable to external files (files created by saving the external URL directly to the `_File` table). -Query internal files by the `key` field (path in URL) instead. - -On a related note, if the files are referenced in an array field of `LCObject` and you want to get them within the same query for `LCObject`, you need to use the `Include` method with `LCQuery`. For example, if you are retrieving all the todos with the same title `Buy Cakes` and you want to retrieve their related attachments at the same time: - -```cs -// Get all todos with the same title and contain attachments -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("title", "Buy Cakes"); -query.WhereExists("attachments"); - -// Include attachments with each todo -query.Include("attachments"); -ReadOnlyCollection todos = await query.Find(); -foreach (LCObject todo in todos) { - // Get the attachments array for each todo - List attachments = todo["attachments"] as List; -} -``` - -### Upload Progress - -You can monitor the upload progress and display that to the user: - -```cs -await file.Save((count, total) => { - print($"{count}/{total}"); - if (count == total) { - print("done"); - } -}); -``` - -### File Metadata - -When uploading a file, you can attach additional properties to it with `metaData`. A file's `metaData` cannot be updated once the file is stored to the cloud. - -```cs -file.AddMetaData("size", 1024); -file.AddMetaData("width", 128); -file.AddMetaData("height", 256); -file.MimeType = "image/jpg"; -await file.Save(); -``` - -### Image Thumbnails - -After saving an image, you can get the URL of a thumbnail of the image beside that of the image itself. You can even specify the width and height of the thumbnail: - -```cs -// Get the URL of a 100px x 200px thumbnail -string url = file.GetThumbnailUrl(100, 200); -``` - -You can only get thumbnails for images smaller than **20 MB**. - -### Deleting Files - -The code below deletes a file from the cloud: - -```cs -LCFile file = LCObject.CreateWithoutData("_File", "552e0a27e4b0643b709e891e"); -await file.Delete(); -``` - -By default, a file is not allowed to be deleted. -We recommend you delete files by accessing our REST API with the Master Key. -You can also allow certain users and roles to delete files by going to ** > Files > Permission**. - -### File Censorship - -The **censorship** feature allows you to censor **image** files stored on the cloud. - -You can **Enable automatic content censor for subsequent uploaded pictures** by going to **Data Storage > Files > Censorship**. You can also batch-censor all the images uploaded during a specific time scope. You can view the results of the censorship under the **Files** tab. - -You can manually **Pass** or **Block** images even if they have gone through automatic censorship. - -## GeoPoints - -You can associate real-world latitude and longitude coordinates with an object by adding an `LCGeoPoint` to the `LCObject`. By doing so, queries on the proximity of an object to a given point can be performed, allowing you to implement functions like looking for users or places nearby easily. - -To associate a point with an object, you need to create the point first. The code below creates an `LCGeoPoint` with `39.9` as `latitude` and `116.4` as `longitude`: - -```cs -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -``` - -Now you can store the point into an object as a regular field: - -```cs -todo["location"] = point; -``` - -### Geo Queries - -With a number of existing objects with spatial coordinates, you can find out which of them are closest to a given point, or are contained within a particular area. This can be done by adding another restriction to `LCQuery` using `WhereNear`. The code below returns a list of `Todo` objects with `location` closest to a given point: - -```cs -LCQuery query = new LCQuery("Todo"); -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -query.WhereNear("location", point); - -// Limit to 10 results -query.Limit(10); -// todos is an array of Todo objects satisfying conditions -ReadOnlyCollection todos = await query.Find(); -``` - -Additional sorting conditions like `OrderByAscending` and `OrderByDescending` will gain higher priorities than the default order by distance. - -To have the results limited within a certain distance, check out our API docs. - -You can also query for the set of objects that are contained within a rectangular bounding box with `WhereWithinGeoBox`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```cs -LCQuery query = new LCQuery("Todo"); -LCGeoPoint southwest = new LCGeoPoint(30, 115); -LCGeoPoint northeast = new LCGeoPoint(40, 118); -query.WhereWithinGeoBox("location", southwest, northeast); -``` - -### Caveats about GeoPoints - -Points should not exceed the extreme ends of the ranges. Latitude should be between `-90.0` and `90.0`. Longitude should be between `-180.0` and `180.0`. Attempting to set latitude or longitude out of bounds will cause an error. -Also, each `LCObject` can only have one field for `LCGeoPoint`. - -## Users - -See [TDS Authentication Guide](/sdk/authentication/guide/). - -## Roles - -As your app grows in scope and user base, you may find yourself needing more coarse-grained control over access to pieces of your data than user-linked ACLs can provide. To address this requirement, we support a form of role-based access control. Check the detailed [ACL Guide](/sdk/storage/guide/acl/) to learn how to set it up for your objects. - -## Full-Text Search - -Full-Text Search offers a better way to search through the information contained within your app. It's built with search engine capabilities that you can easily tap into your app. Effective and useful searching functionality in your app is crucial for helping users find what they need. For more details, see [Full-Text Search Guide](/sdk/storage/guide/fulltext-search/). diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/dotnet-guide/setup-dotnet.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/dotnet-guide/setup-dotnet.mdx deleted file mode 100644 index afeecfffd..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/dotnet-guide/setup-dotnet.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: Installing .NET SDK for Data Storage and Instant Messaging -sidebar_label: Installing .NET SDK -slug: /sdk/storage/guide/setup-dotnet/ -sidebar_position: 1 ---- - -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import DomainBinding from "../../_partials/setup-domain.mdx"; -import AppConfig from "../_partials/app-config.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -:::info -The .NET SDK is based on .NET Standard 2.0. It supports the following frameworks: - -- Unity 2018.1+ -- .NET Core 2.0+ -- .NET Framework 4.6.1+ -- Mono 5.4+ - -See [.NET Standard](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) for more frameworks supported. - -::: - -## Installing SDK - -First of all, download the latest SDK from our [GitHub Releases](https://github.com/leancloud/csharp-sdk/releases). - -The table below shows the modules included in the SDK as well as their dependencies: - -| Name | Description | -| ------------------------ | --------------------------------------------------------------------------------------------------------------- | -| `LeanCloud-SDK-Storage` | For the Data Storage service. | -| `LeanCloud-SDK-Realtime` | For Instant Messaging and LiveQuery. Depends on the Data Storage service. | -| `LeanCloud-SDK-Engine` | For Cloud Engine. Depends on the Data Storage service and works on the server side environment of Cloud Engine. | - -You can download only what you need to reduce the size of your app. - -### For Unity Projects - -- Direct import: Download LeanCloud-SDK-XXX-Unity.zip, unzip it as a `Plugins` folder, and drag and drop the folder into Unity. - -- UPM: Add the following dependencies into `Packages/manifest.json` in your project: - - {`"dependencies": { -"com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-${sdkVersions.leancloud.csharp}", -"com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-${sdkVersions.leancloud.csharp}" -}`} - - -Note: Only Unity 2018+ is supported (Unity Api Compatibility Level being .NET Standard 2.0). - -### For Other Projects - -For .NET Core and other projects supporting .NET Standard 2.0, please download LeanCloud-SDK-XXX-Standard.zip, unzip it, and set dependencies accordingly (XXX means the cloud services you are using, like Storage, Realtime (which includes LiveQuery), and Engine). - -## Initializing Your Project - -Import the modules: - -```cs -// The basics -using LeanCloud; -// Data Storage -using LeanCloud.Storage; -// Instant Messaging (optional) -using LeanCloud.Realtime; -// LiveQuery (optional) -using LeanCloud.LiveQuery; -``` - -Before using the service, initialize the SDK with the following code: - - - -```cs -LCApplication.Initialize("your-client-id", "your-client-token", "https://your_server_url"); -``` - - - - - -```cs -LCApplication.Initialize("your-app-id", "your-app-key", "https://your_server_url"); -``` - - - -### Credentials - - - -## Domain - - - -## Enabling Debug Logs - -You can easily trace the problems in your project by turning debug logs on during the development phase. Once enabled, details of every request made by the SDK along with errors will be output to your IDE, your browser console, or your Cloud Engine instances’ logs. - - - - -```cs -LCLogger.LogDelegate = (LCLogLevel level, string info) => { - switch (level) { - case LCLogLevel.Debug: - Debug.Log($"[DEBUG] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Warn: - Debug.Log($"[WARNING] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Error: - Debug.Log($"[ERROR] {DateTime.Now} {info}\n"); - break; - default: - Debug.Log(info); - break; - } -} -``` - - - - - -```cs -LCLogger.LogDelegate = (LCLogLevel level, string info) => { - switch (level) { - case LCLogLevel.Debug: - TestContext.Out.WriteLine($"[DEBUG] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Warn: - TestContext.Out.WriteLine($"[WARNING] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Error: - TestContext.Out.WriteLine($"[ERROR] {DateTime.Now} {info}\n"); - break; - default: - TestContext.Out.WriteLine(info); - break; - } -} -``` - - - - -:::caution -Make sure debug logs are turned off before your app is published. Failure to do so may lead to the exposure of sensitive data. -::: - -## Verifying - -First of all, make sure you are able to connect to the server from your computer. You can test it by running the following command: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` is the custom API domain. - -If everything goes well, it will return the current date: - -```json -{ "__type": "Date", "iso": "2020-10-12T06:46:56.000Z" } -``` - -Now add the following code to your project: - -```cs -LCObject testObject = new LCObject("TestObject"); -testObject["words"] = "Hello world!"; -await testObject.Save(); -``` - -Save and run your program. - -Then go to ** > Data > `TestObject`**. If you see a row with its `words` column being `Hello world!`, it means that you have correctly installed the SDK. - -See [Debugging](#Debugging) if you’re not seeing the content. - -## Debugging - -This guide is written for the latest version of our SDK. If you encounter any errors, please first make sure you have the latest version installed. - -### `401 Unauthorized` - -If you get a `401` error or see the following content in network logs: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -It means that the App ID or App Key might be incorrect or don’t match. If you have multiple apps, you might have used the App ID of one app with the App Key of another one, which will lead to such an error. - -### The Client Cannot Access the Internet - -Make sure you have granted the required permissions to your mobile app. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/faq.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/faq.mdx deleted file mode 100644 index 17caf324b..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/faq.mdx +++ /dev/null @@ -1,324 +0,0 @@ ---- -title: Data Storage FAQ -sidebar_label: FAQ -slug: /sdk/storage/faq/ -sidebar_position: 18 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -## API - -### Is there any limit to the number of API calls? - -With Developer Edition [Standard Edition](https://developer.taptap.cn/product-intro/price), there is a free quota of 30,000 API read and write requests per day. - -Push service is free to use, and does not take up the free quota. The call frequency of the push message interface is limited. For details, please refer to: [Push message interface limitations](/sdk/push/guide/rest/#push-message-interface) documentation. - - - -By default, the maximum number of worker threads that can be used by the Business Edition application at one time is 60, which means that at most 60 data requests can be processed at the same time. **We will adjust this value based on application performance and operation and maintenance needs**. If you need to increase this limit, please [submit a work order](https://www.leanticket.cn) to request it. - - - -### Calculation of the number of API calls - -For data storage, each `create` and `update` of an object counts as 1 request, e.g. 1 call to `object.saveInBackground` counts as 1 API request. In the case of API call failure, if it is rejected by the cloud due to **application flow control overrun** (error code 429), it **will not** be counted as 1 request; if it is due to other reasons, for example, **insufficient privileges** (error code 430), it will still be counted as 1 request. - -#### One request - -- `create` -- `save` -- `fetch` -- `find` -- `delete` -- `deleteAll` - -A call to `fetch` or `find` that returns 100 associated objects via `include` counts as 1 API request. A call to `find` or `deleteAll` to find or delete 500 records counts as 1 API request. - -#### Multiple requests - -- `saveAll` -- `fetchAll` - -Calling `saveAll` or `fetchAll` once to save or fetch 100 objects in an array counts as 100 API requests. - - - -For [Social Stream](https://docs.leancloud.cn/sdk/other/openview/status_system/), `create` and `update` are billed according to the number of objects in Status and Follower/Followee. - - - -A query is billed by the number of requests, regardless of the size of the result. The `query.count` counts as 1 API request. Collection fetch is also billed by request count. - -### How to get API access logs - - - -Go to ** > API Access Logs**, turn on the logging service, and refresh the page later to see the logs generated from the time it was turned on to the current time. - - - - - -The Developer Center backend does not support viewing API access logs at this time. - - - -### How to encode parameters for calling REST APIs in other languages - -The REST API documentation uses curl as an example, where `--data-urlencode` indicates that the parameters are to be URL encoded. - -In the case of a GET request, the URL encoded parameters are concatenated with `&` and placed after the question mark in the URL. For example, `https://API_BASE_URL/1.1/login?username=xxxx&password=xxxxx`. - -## Queries - -### How to implement case-insensitive queries - -Direct support is not currently provided, a regular expression query approach can be used, see [StackOverflow - MongoDB: Is it possible to make a case-insensitive query](http://stackoverflow.com/questions/1863399/mongodb-is-it-possible-to-make-a-case-insensitive-query). - -Use the `matchesRegex` method provided by the AVQuery object of each platform's SDK (Android SDK uses the `whereMatches` method). - -### What is the maximum number of results a query can return? - -- A query can return up to 1000 pieces of data. -- If more than one query result is returned, the total size will not exceed 6 MB (for compatibility with old clients, if it exceeds 6 MB, no error will be reported and truncated results will be returned); if one query result is returned, the size will not exceed 16 MB. - -### The query result can only return 1000 pieces of data at most, what should I do when I need more than 1000 pieces of data? - -You can continue to obtain new results from the previous breakpoint by changing the query conditions each time, for example: -- The first query, 1000 pieces of data whose createdAt time is after 2015-12-01 00:00:00 (the createdAt value of the last piece is x); -- The second query, createdAt is 1000 pieces of data after x (createdAt of the last piece is y); -- The third query, createdAt 1000 data pieces of data after y (the last piece is z); - -And so on. - -### Can paging be accomplished by specifying a range of objectId's? - -The current algorithm for automatically generating `objectId` by the datastore service is time-based, so paging can indeed be achieved by specifying the`objectId` range. -However, from the perspective of code readability and rigor (e.g., you can specify different formats of `objectId` when importing data), it is recommended to implement paging by specifying the `createdAt` range. - -### How to speed up queries when the amount of data is increasing? - -As with the use of traditional databases, query optimization is achieved primarily by **indexing**. An index is like a table of contents in a dictionary, which can help you look up words more quickly in a huge amount of text. - -Principle: When the amount of data is small, no index is built. When there is more, please remember that after building an index, you still need to update the index when writing, in exchange for less query time. Therefore, in general, if you write less and read more, you can build more indexes; if you write more and read less, you can build less indexes. - -Indexes can be created in **** by selecting the corresponding Class and going to "Performance and Indexing" self-service, see [console guide](/sdk/start/dashboard/#build-indexes-for-a-certain-class-data) for more details. index). - -### Does LeanCloud query support functions like `Sum`, `Group By`, `Distinct`? - -No. 'Group By] queries often involve traversing a large number of objects, and data storage is not suitable for such scenarios. -For this reason we recommend using the "[data warehouse](/sdk/storage/datalake/)" feature, which supports large and efficient data traversal, tailored for data analysis as a scenario. - -### Why query results for default values are incorrect? - -This is the limitation of default value. MongoDB itself does not support default values. The default value we provide is only the enhancement of the application level. If the old data does not have a corresponding key, it will not be corrected after setting the default value, but the presentation layer will be optimized after querying. Accordingly, after changing the default value, these keys will also be displayed as the new default value. There are two solutions: - -1. Update the old data, query the records where the key does not exist (whereDoesNotExist), and then update them back. -2. Add or query to the query condition, or key does not exist (whereDoesNotExist). - -### How do I query for non-null values? - -The specific semantics of a null value depends on the type of the field and the actual situation of the item. Taking a string field as an example, a null value might mean: - -1. the field does not exist -2. the field is `null' -3. the field is the empty string `""`. - -Accordingly, the query for the field being non-null is `where={"some_field": {"$exists": true, "$nin": ["", null]}}`. - -Determining a rigorous specification during the data model design phase of a project can help simplify query conditions and avoid errors due to inadvertent omission of conditions. -For example, if the design ensures that a string field is empty by simply not setting the field (it doesn't exist) and never setting it to `""` and `null` (setting a field to `null` is strongly discouraged, the SDK doesn't provide a way to set a field to `null`), then simply querying for `{"$exists": true}` would be sufficient. - -### Geolocation query error - -If the error message is similar to `can't find any special indices: 2d (needs index), 2dsphere (needs index), for field name`, it means that the field used for the query doesn't have a 2D index, you can find **Indexes** management in the **Other** menu of the Class management. You can find **Index** Management in the **Other** menu of Class Management, click to enter, find the field name, select and create "2dsphere" index type. - -![image](/img/storage/geopoint_faq.png) - -### How to improve query efficiency of flag bits? - -When there are many Boolean types of data in the data table, you can consider using binary storage to improve the query efficiency. For example, if you need to store multiple states such as whether push is enabled, whether you are muted,whether you are a member, etc., you can represent it like this: - -`111`: Turn on push, mute, and be a member - -`101`: Turn on push, not muted, and be a member - -Stored as an integer field in LeanCloud, the method to manipulate this field can be found in the interface documentation for bitwise operations: [REST API - Bitwise Operations](/sdk/storage/guide/rest/#bitwise-operations). - -### When searching for keywords in the app, the result is data from two months ago. What should I do if the latesxt data cannot be queried? - -In this case, you can try to rebuild the index in ** > Full text search** , and there will be a reminder by email after the index is created. - -Generally speaking, when a user uploads a new dictionary, or deletes data in a batch, it is necessary to perform a "rebuild index" operation. You can also try to rebuild the index when you find inconsistencies between searches and stored data. - -### What should I do if a single object in a table is too large, resulting in slower requests? - -If the data of a single object is too big, you need to optimize the code at the business level, for example, data is a big data field: - -- When querying, use AVQuery.Select to select the required field (don't select data field). -- Fetch the data field to the client only at the time of GET object. -- Or consider saving the data in the data field as a "file" and associating it in the table through Pointer. -- If the data is a huge object and you only need to get one or a few attributes, you can consider specifying the attribute(s) with a dot in select, see [Use dot in querying objects](/sdk/storage/guide/dot-notation/#use-dot-in-querying-objects). - -## File storage - -### Is File Storage CDN-accelerated? - -The file storage service of China node comes with CDN acceleration, but it does not include overseas CDN acceleration. - -Commercial application can send a work order to request to enable overseas CDN acceleration (after enabling, the cost of http/https traffic for accessing files from overseas will be adjusted to RMB 0.40/GB and RMB 0.60/GB respectively). - -There is no ready-made CDN acceleration service in the international version, so users need to configure it by themselves. - -Take CloudFront acceleration service as an example, the configuration process is as follows: - -- Read the official guide [Getting Started with CloudFront](http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.html). -- Create an AWS account to start using and paying for the CloudFront service. -- Public access to S3 (read permission) is already configured, and you can skip the [section on S3 configuration](http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/) in the guide. GettingStarted.html#GettingStartedUploadContent). -- The **Origin Domain Name** in the CloudFront configuration should be obtained from the URL of the AVFile, all others can be left as default. - -### Is there a size limit for file storage? - -No, there is not. - -Except for uploading files through the JavaScript SDK in the browser, or uploading files directly through our website, which has a size limit of 10 MB, there are no restrictions on other SDKs. The JavaScript SDK also has no size limit in the Node.js environment. - -### Can I store images as thumbnails, etc.? - -Yes. By default, our `AVFile` class provides a way to get thumbnails, see the developer's guide for each SDK. If you want to do it yourself, you can get the `URL` property of `AVFile`. - -Use [Seven Bulls Image Processing API](https://developer.qiniu.com/dora/manual/1279/basic-processing-images-imageview2) to perform processing, such as adding watermarks, cropping, and so on. - -### Does file storage support access control? - -Since any user who knows the file URL can access the file, you need to set appropriate Class permissions and ACLs for the `_File` Class and the Class that references the file to restrict unauthorized users from getting to the file URL. - -If you want to have more granular control over file access, such as rechecking whether you have access to a file after a certain period of time, it is recommended that you use a third-party file storage provider that supports access rights control, and then save an external URL directly in the LeanCloud File Service. -Note that the `_File` table is immutable, which means that a URL that has been written cannot be modified. -However, usually file storage providers that support access control implement access rights by passing an additional token parameter (which is usually time-sensitive) after an existing URL, such as `https://file.example.com/some-url`, for example `https://file.example.com/some-url?token=xxxxxx`. -So when a client needs to access a file, it can get the `url` attribute from `LCFile` and then add this `token` parameter to form the final URL to download the file. -To get the `token`, you need to deploy a simple API service that generates the `token` for accessing the `token` on the cloud engine or your own server, and the client accesses the API service to get the `token`. - -### How do I modify an existing file in the File Store? - -The `_File` class of the file store is immutable data. Once written, the contents of the file and related metadata cannot be modified, but can only be deleted and re-uploaded or rebuilt via URL. -If you are using an external file (constructed via URL) and need to modify the file or metadata frequently, you can consider the following solutions: - -- Implement a Class to store information related to external file, such as `ExternalFile`. Set a Pointer field in the object that references the file to point to `ExternalFile`. Note that in this case, when querying the object containing the file, if you want to get the file information at the same time, you need to include the corresponding field (if you use the built-in `_File` Class, it will be included automatically, no additional specification is required). -- In the object that needs to reference the file, you can directly use the string type to save the URL. In this case, file-related functions shoule be implemented by yourself. If you want to save meta information about the file, you need to design it separately. If you want to save the meta information about the file, you need to design it differently, for example, by saving it on other fields of the object, or by changing the type of the corresponding field from string to object and saving both the URL and the meta information. Note that such a design will result in the application's file information being scattered among the classes that need to use the file, and it will be troublesome to batch query and batch process the entire application's files. - -## SDK - -### The Size of the iOS Project After Packaging - -Create a new blank project and use CocoaPod to install AVOSCloud and AVOSCloudIM modules. At this time, the project size exceeds 80 MB. Will the size shrink after packaging? Approximately how big will it be? - -The datastore iOS SDK binary contains 5 CPU slices, including i386, armv7, arm64, etc. Non-ARM symbols and symbols not involved in the connection are stripped out during the release process. Therefore, the final application size will not increase by more than 10 MB, so please feel free to use it. - -### Why does the Java SDK return null when using the getDate("createdAt") method to read the creation time of an AVObject object? - -Use the `getCreatedAt` method of `AVObject`; for `updatedAt` use `getUpdatedAt`. -Both methods return the Date type. - -If you want to return a string, you can use `getUpdatedAtString()` and `getCreatedAtString()`. - -### Why does the installationId always change every time an Android device starts up? How can I stop it from changing? - -There are two possible reasons for this: - -- The SDK version is too old, and the logic for generating the installationId has been modified during version changes. Please update to the latest version. -- Caused by code obfuscation, please add [SDK obfuscation exclusion](/sdk/storage/guide/setup-java/#android-code-obfuscation) to the proguard file. - -### How to do Android code obfuscation - -Refer to the [Android Code Obfuscation](/sdk/storage/guide/setup-java/#android-code-obfuscation) documentation. - -### What is the reason for the error 'already has one request sending' in Java SDK? - -The `com.avos.avoscloud.AVException: already has one request sending` error in the logs indicates that two asynchronous `save` operations have been performed on the same `AVObject` instance object at the same time. To prevent data mismatch, the datastore SDK limits such concurrent writes to the same data, so this exception is thrown. - -You need to inspect the code and print logs and breakpoints to find out which `save` line triggered it. - -### Using the data storage function of the Android SDK, the app was detected by the developer to have a self-start issue when it was uploaded. - -Solution: Upgrade the Android SDK to v8.2.19 or above. - -The reason for the self-start issue is that LiveQuery needs a long link in the background so that it can receive event notifications from the cloud in time. This long link is placed in a background Service with auto-reconnect capability - it will monitor the network and start this service when the network is active. This logic may be judged as "self-starting" by some platforms. - -Older versions of the SDK sometimes threw exceptions because they didn't adapt to the newer versions of Android. The 8.2.19 version of the SDK has adapted to the newer versions of Android, but it doesn't completely prohibit this auto-reconnect logic, so it may still be misjudged. If you have already used the new SDK but still encounter this problem, you can complain to the vendor. - -### Does JavaScript SDK have a synchronization API? - -The JavaScript SDK does not provide a synchronization API due to platform specificity (running in a single-threaded browser or Node.js environment). All APIs that require network interaction need to be called in the form of callbacks. We provide [Promise mode](/sdk/storage/guide/js/#promise) to minimize the problem of over-nested callbacks. - -### JavaScript SDK uses Master Key in AV.init, but the AJAX requests sent returns 206 - -Currently the JavaScript SDK does not send a Master Key when working in the browser (not Node) because we discourage the use of Master Keys in the browser, which represent the highest level of access to the data and should only be used in backend applications. - -If your application is indeed internal (with security measures in place so that it cannot be accessed from the outside), you can add the following code after `AV.init` to make the JavaScript SDK send the Master Key: - -```js -AV.Cloud.useMasterKey(true); -``` - -### JavaScript SDK will expose App Key and App Id, how to ensure security? - -First of all, please read [**Security overview**](/sdk/storage/guide/security/) to understand the complete security system of data storage service. It is mentioned that you can use a "secure domain name", and in the absence of a domain name, you can use [ACL](/sdk/storage/guide/acl/). - -Theoretically all clients are untrusted, so security needs to be designed on the server side. If you need advanced security, you can use the [ACL](/sdk/storage/guide/acl/) management. If you need more advanced customization, you can use the [cloud engine](/sdk/engine/overview/). - -## Other - -### How to prevent DDoS attacks, or high-frequency CC attacks? - - - -Whether it is a DDoS attack or a high-frequency CC attack, it is necessary to set up a high-defense resource in front of the independent IP requested by the service. -The specific steps are as follows: - -1. Bind a domain name that has been filed in the console. To use data storage and instant messaging services, you need to bind the API domain name, and to use cloud engine services, you need to bind the domain name of the cloud engine. when the SDK is initialized, specify the Server URL as the address of the API domain name bound to the console. To bind a domain name, go to Console > Application > Settings > Domain Binding. -2. Set the DNS resolution to a separate IP (set the IP address to an A record) when binding the domain name. Independent IP can be viewed at Console Personal Center > Account Settings > Independent IP, API and Cloud Engine have different IPs, one API independent IP is free for Commercial Edition users, and the second API independent IP is charged from the moment you purchase it, there is no free IP for Cloud Engine, each IP is charged at $50 per month. 3. Purchase high security resources from a third party. -3. Purchase high defense resources from a third party and use the newly purchased IPs above to round-trip to the source. - -Since many network attacks are destined to specific IPs, if the standalone IP is already publicly available with the service, you can purchase another standalone IP to be the source IP of the high defense resource to avoid the high defense resource failing due to the unavailability of the source IP. - - - - - -See [TDS cloud service with high defense](/sdk/start/faq/#tds-cloud-service-with-high-defense) for details. - - - -### Saving data gives an error ‘The key is too long’, what is the problem? - -You can check whether the field you want to save or update is indexed or not. The length limit of indexed field cannot exceed 1 KB. - -### How to address data consistency or transaction needs? - -The data storage service does not provide complete transaction function at present, but it provides some features to ensure data consistency, which can solve most of the consistency needs: - - - -- Updates to multiple fields are done atomically in a single save operation on a single object. -- Numeric fields can be updated atomically using [increment](/sdk/storage/guide/js/#update-counter) (atomic counter). -- [Unique Index](https://docs.leancloud.cn/sdk/start/dashboard/#indexing-a-class-of-data) ensures that there is only one object with the same value on a field. -- [Conditional update object](/sdk/storage/guide/js/#conditional-update-object) allows you to perform an update operation only when a certain query condition is met; on top of this feature, you can implement your own more complex [two-phase commit](http://www.howardliu.cn/translation-perform-two-phase-commits-in-mongodb/). -- You can also implement custom [exclusion locks](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/) on the cloud engine with the help of [LeanCache](/sdk/engine/database/redis/). functions/redlock.js). - - - - - -- Updates to multiple fields are done atomically in a single save operation on a single object. -- Numeric fields can be updated atomically using [increment](/sdk/storage/guide/js/#update-counter) (atomic counter). -- [Unique-index](/sdk/start/dashboard/#indexing-a-class-of-data) ensures that there is only one object with the same value on a field. -- [Conditional update object](/sdk/storage/guide/js/#conditionally-update-object) can be updated only when a certain query condition is met; based on this feature, you can implement a more complex [two-phase commit](http://www.howardliu.cn/translation-perform-two-phase-commits-in-mongodb/). -- You can also implement custom [exclusion locks](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/) on the cloud engine with the help of [LeanCache](/sdk/engine/database/redis/). functions/redlock.js). - - - -We've also recorded a [public video](https://www.bilibili.com/video/av12823801/) on this topic, which includes a detailed introduction to the above features and practical tutorials for solving common scenarios (including implementing a two-stage submission). \ No newline at end of file diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/fulltext-search/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/fulltext-search/_category_.json deleted file mode 100644 index 2f9a2a89c..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/fulltext-search/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "全文搜索", - "collapsed": true, - "position": 5 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/fulltext-search/fulltext-search-guide.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/fulltext-search/fulltext-search-guide.mdx deleted file mode 100644 index 9b8402d64..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/fulltext-search/fulltext-search-guide.mdx +++ /dev/null @@ -1,240 +0,0 @@ ---- -title: Full-Text Search Guide -slug: /sdk/storage/guide/fulltext-search/ -sidebar_position: 1 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -Full-text search is a common feature used by a lot of modern applications. -You might already know that it is possible to implement full-text search by using regular expressions in queries, but this approach will not work efficiently when there are massive objects or fields in a class. To solve the problem, we provide a dedicated full-text search service that you can use in your app. - -## Enabling Search for Classes - -Before using full-text search, you must first enable it for at least one class. For each class you enable the service on, an index will be created for it, and you will be able to use our search component or use the [API](#search-api) to search for the objects in it. - -**The ACLs of the objects in a class still take effect when you use full-text search.** - -You can enable search for a class by going to ** > Full-text Search** and clicking on **Enable search for class**: - -- **Class**: The class you want to enable search on. You can enable search on at most 5 classes for apps with Developer Plans and 10 classes for apps with Business Plans. -- **Enabled columns**: The fields you want to enable search on. By default, `objectId`, `createdAt`, and `updatedAt` are always enabled. Besides them, you can enable at most 5 custom fields for apps with Developer Plans and 10 custom fields for apps with Business Plans. - -**If the cloud has not received any full-text search API requests within two weeks after you enabled search for a class, the search service for the class will be disabled.** - -## Search API - -We offer a [REST API](/sdk/storage/guide/fulltext-search-rest/) with all the interfaces you need to use the service, -and our SDKs encapsulate those interfaces for you to use. - -Assuming you have [enabled full-text search](#enabling-search-for-classes) for a class named `GameScore`, you can search for objects in this class with keywords: - - - -```cs -LCSearchQuery query = new LCSearchQuery("GameScore"); -query.QueryString("dennis") - .OrderByDescending("score") - .Limit(10); -LCSearchResponse response = await query.Find(); -// The number of documents matching the condition -Debug.Log(response.Hits); -// The documents matching the condition -foreach (GameScore score in response.Results) { - -} -// Mark the result of this query so you can use the sid to paginate -Debug.Log(response.Sid); -``` - -```java -LCSearchQuery searchQuery = new LCSearchQuery("dennis"); -searchQuery.setClassName("GameScore"); -searchQuery.setLimit(10); -searchQuery.orderByAscending("score"); // Sort in ascending order of score -searchQuery.findInBackground().subscribe(new Observer>() { - @Override - public void onSubscribe(Disposable disposable) {} - - @Override - public void onNext(List results) { - for (LCObject o:results) { - System.out.println(o); - } - testSucceed = true; - latch.countDown(); - } - - @Override - public void onError(Throwable throwable) { - throwable.printStackTrace(); - testSucceed = true; - latch.countDown(); - } - - @Override - public void onComplete() {} -}); -``` - -```objc -LCSearchQuery *searchQuery = [LCSearchQuery searchWithQueryString:@"test-query"]; -searchQuery.className = @"GameScore"; -searchQuery.highlights = @"field1,field2"; -searchQuery.limit = 10; -searchQuery.cachePolicy = kLCCachePolicyCacheElseNetwork; -searchQuery.maxCacheAge = 60; -searchQuery.fields = @[@"field1", @"field2"]; -[searchQuery findInBackground:^(NSArray *objects, NSError *error) { - for (LCObject *object in objects) { - NSString *appUrl = [object objectForKey:@"_app_url"]; - NSString *deeplink = [object objectForKey:@"_deeplink"]; - NSString *highlight = [object objectForKey:@"_highlight"]; - // other fields - // code is here - } -}]; -``` - -```dart -LCSearchQuery query = new LCSearchQuery('GameScore'); -query.queryString('dennis'); -query.orderByDescending('score'); -query.limit(10); -LCSearchResponse response = await query.find(); -// The number of documents matching the condition -print(response.hits); -// The documents matching the condition -for (GameScore score in response.results) { - -} -// Mark the result of this query so you can use the sid to paginate -print(response.sid); -``` - -```js -const query = new AV.SearchQuery("GameScore"); -query.queryString("dennis"); -// Highlight all the matched `dennis` string in the `player` field; provide an array to match multiple fields -query.highlights("player"); -query.highlights("player"); -query - .find() - .then(function (results) { - console.log("Find " + query.hits() + " docs."); - // Output: Find 4 docs. - // Print the first matched result with highlight - console.log(results[0].get("_highlight").player); - // Output: [ 'dennis ZX' ] - }) - .catch(function (err) { - // Handle err - }); -``` - - - -For the syntax of the query string, see [Syntax for `q`](/sdk/storage/guide/fulltext-search-rest#syntax-for-q). - -Since there is a `limit` for each query, you may not get all the records that match the query conditions at once. -You can call `hits()` on `SearchQuery` to get the number of records that match the conditions, -and then call `find()` multiple times on `SearchQuery` to get the remaining records. - -If you cannot preserve the same `query` object used for querying across different requests, you can use `sid` for pagination instead. Each query is uniquely marked with the `_sid` property on `SearchQuery`. -You can reconstruct a `query` object by calling `sid()` on `SearchQuery` to continue the pagination. -Each `sid` is valid for 5 minutes. - -You can use `SearchSortBuilder` to build complex rules for sorting. For example, assuming `scores` is an array of scores, if you need to sort results in descending order of the average score, and put those who do not have scores to the last: - - - -```cs -LCSearchSortBuilder sortBuilder = new LCSearchSortBuilder(); -sortBuilder.OrderByAscending("balance", "avg", "last"); -searchQuery.SortBy(sortBuilder); -``` - -```java -LCSearchSortBuilder builder = LCSearchSortBuilder.newBuilder(); -builder.orderByDescending("scores","avg","last"); -searchQuery.setSortBuilder(builder); -``` - -```objc -LCSearchSortBuilder *builder = [LCSearchSortBuilder newBuilder]; -[builder orderByDescending:@"scores" withMode:@"max" andMissing:@"last"]; -searchQuery.sortBuilder = builder; -``` - -```dart -LCSearchSortBuilder sortBuilder = new LCSearchSortBuilder(); -sortBuilder.orderByAscending('scores', mode: 'avg', missing: 'last'); -searchQuery.sortBy(sortBuilder); -``` - -```js -searchQuery.sortBy( - new AV.SearchSortBuilder().descending("scores", "avg", "last") -); -``` - - - -To learn more about the APIs you can use, check out the API docs of our SDKs: - - - -<> - -- [LCSearchQuery](https://leancloud.github.io/csharp-sdk/html/classLeanCloud_1_1Storage_1_1LCSearchQuery.html) -- [LCSearchSortBuilder](https://leancloud.github.io/csharp-sdk/html/classLeanCloud_1_1Storage_1_1LCSearchSortBuilder.html) -- [LCSearchResponse](https://leancloud.github.io/csharp-sdk/html/classLeanCloud_1_1Storage_1_1LCSearchResponse.html) - - -<> - -- [LCSearchQuery](https://leancloud.github.io/java-unified-sdk) -- [LCSearchSortBuilder](https://leancloud.github.io/java-unified-sdk) - - -<> - -- [LCSearchQuery](https://leancloud.github.io/objc-sdk/Classes/LCSearchQuery.html) -- [LCSearchSortBuilder](https://leancloud.github.io/objc-sdk/Classes/LCSearchSortBuilder.html) - - -<> - -- [LCSearchQuery](https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/LCSearchQuery-class.html) -- [LCSearchSortBuilder](https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/LCSearchSortBuilder-class.html) -- [LCSearchResponse](https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/LCSearchResponse-class.html) - - -<> - -- [AV.SearchQuery](https://leancloud.github.io/javascript-sdk/docs/AV.SearchQuery.html) -- [AV.SearchSortBuilder](https://leancloud.github.io/javascript-sdk/docs/AV.SearchSortBuilder.html) - - - - - -## Custom Word List - -By default, all `String` fields will be automatically analyzed through [mmseg](https://github.com/medcl/elasticsearch-analysis-mmseg). You can upload your custom word list as a file by going to ** > Full-text Search > Custom word list**. - -The file should be UTF-8-encoded with each word taking up a line. The maximum size of the file is 512 K. Here is an example: - -``` -Object-oriented programming -Functional programming -Higher-order function -Responsive design -``` - -The word list will take effect in 3 minutes since you upload it. You can check if your word list is in effect with the [`analyze` API](/sdk/storage/guide/fulltext-search-rest#analyzing-strings) (requires Master Key). - -Your custom word list will only be applied to **new and updated documents/records**. To apply it to your existing documents, go to **Data Storage** > **Full-text Search** and click on **Rebuild index**. -You should rebuild the index when you update or delete your custom word list, too. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/fulltext-search/fulltext-search-rest.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/fulltext-search/fulltext-search-rest.mdx deleted file mode 100644 index 87a70300e..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/fulltext-search/fulltext-search-rest.mdx +++ /dev/null @@ -1,340 +0,0 @@ ---- -title: Full-Text Search REST API -slug: /sdk/storage/guide/fulltext-search-rest/ -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -The Full-Text Search service offers the following REST API interfaces: - -| URL | HTTP | Function | -| ------------------- | ---- | ----------------------- | -| /1.1/search/select | GET | Search by conditions | -| /1.1/search/mlt | GET | moreLikeThis query | -| /1.1/search/analyze | GET | Analyze a piece of text | - -Before using the REST API, make sure you have enabled search for the classes you want to search on. -Please also see [Data Storage REST API](/sdk/storage/guide/rest/)[Data Storage REST API](https://docs.leancloud.app/rest_api.html) for more information about API Base URL, request format, and response format, as well as the _[Custom Word List](/sdk/storage/guide/fulltext-search/#custom-word-list)_ section in _Full-Text Search Guide_. - -## Search by Conditions - -Use `GET /1.1/search/select` to conduct a full-text search with conditions: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/select?q=dennis&limit=200&clazz=GameScore&order=-score" -``` - -The response looks like this: - -```json -{ - "hits": 1, - "results": [ - { - "_app_url": "http://stg.pass.com//1/go/com.leancloud/classes/GameScore/51e3a334e4b0b3eb44adbe1a", - "_deeplink": "com.leancloud.appSearchTest://leancloud/classes/GameScore/51e3a334e4b0b3eb44adbe1a", - "_highlight": null, - "updatedAt": "2011-08-20T02:06:57.931Z", - "playerName": "Sean Plott", - "objectId": "51e3a334e4b0b3eb44adbe1a", - "createdAt": "2011-08-20T02:06:57.931Z", - "cheatMode": false, - "score": 1337 - } - ], - "sid": "cXVlcnlUaGVuRmV0Y2g7Mzs0NDpWX0NFUmFjY1JtMnpaRDFrNUlBcTNnOzQzOlZfQ0VSYWNjUm0yelpEMWs1SUFxM2c7NDU6Vl9DRVJhY2NSbTJ6WkQxazVJQXEzZzswOw==" -} -``` - -The following query parameters are available: - -| Parameter | Required | Description | -| ------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | -| `q` | Required | [Elasticsearch query string] | -| `skip` | Optional | The number of results skipped. Defaults to 0. | -| `limit` | Optional | The number of returned objects. The default value is 100 and the maximum value is 1000. | -| `sid` | Optional | Elasticsearch [scroll id]. Returned by a previous search. Used for pagination. | -| `fields` | Optional | Comma-separated field names. | -| `highlights` | Optional | Highlighted keywords. It can be a comma-separated string or wildcard `*`. | -| `clazz` | Optional | Class name. If not specified, all search-enabled classes will be searched. | -| `include` | Optional | Include objects referenced by Pointers. Example: `user,comment`. | -| `order` | Optional | Order by. Prefix `-` for descending. Example: `-score,createdAt`. | -| `sort` | Optional | Refer to [Elasticsearch sort] documentation and the [GeoPoint Queries](#geopoint-queries) section below for details. | - -[elasticsearch query string]: https://www.elastic.co/guide/en/elasticsearch/reference/6.5/query-dsl-query-string-query.html#query-string-syntax -[scroll id]: https://www.elastic.co/guide/en/elasticsearch/reference/6.5/search-request-scroll.html -[elasticsearch sort]: https://www.elastic.co/guide/en/elasticsearch/reference/6.5/search-request-sort.html - -The response contains the following fields: - -- `results`: The documents matching the search criteria. -- `hits`: The number of documents matching the search criteria. -- `sid`: Used to mark the current search result. You can provide it to the next search to implement pagination. - -`results` is a list of objects with each of them containing the fields you enabled for search. There are three special fields: - -- `_app_url`: The URL for the results on your website. -- `_deeplink`: The URL for opening the app. -- `_highlight`: The search result with highlighted keywords enclosed with `em` tags. If the `highlights` parameter is not provided in the request, this field will be `null`. - -`sid` is used to mark the current search result. You can get the next 200 results by providing it to the next search: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/select?q=dennis&limit=200&clazz=GameScore&order=-score&sid=cXVlcnlUaGVuRmV0Y2g7Mzs0NDpWX0NFUmFjY1JtMnpaRDFrNUlBcTNnOzQzOlZfQ0VSYWNjUm0yelpEMWs1SUFxM2c7NDU6Vl9DRVJhY2NSbTJ6WkQxazVJQXEzZzswOw" -``` - -…until all results have been returned. - -### Syntax for `q` - -The `q` parameter follows the [query string syntax](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-query-string-query.html#query-string-syntax) of Elasticsearch. - -If you are already familiar with the query string syntax of Elasticsearch, you can skip this section and jump right into the [GeoPoint Queries](#geopoint-queries) section. - -Reserved characters for querying include `+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /`. Please make sure to use URL escape codes for them. - -#### Basic Syntax - -- Search for a single keyword, like `coke`. -- Search for **multiple keywords**, like `coke no ice`. Keywords should be split with a space. The returned results will be sorted by relevance. For other sorting rules, see [`order`](#search-by-conditions) and [`sort`](#complex-sorting). -- Search for a **phrase**, like `"lady gaga"`. Make sure the phrase is double-quoted. -- Search by a **field**, like `nickname:Passenger`. -- Search by a field with a phrase, like `nickname:"lady gaga"`. Make sure the phrase is double-quoted. -- **Compound query** with `AND` or `OR`, like `nickname:(Passenger OR Holes)`. -- Assuming `book` is an `Object`, you can search by **nested fields**, like `book.name:clojure OR book.content:clojure` and `book.*:clojure`. -- Search for objects without a `title`: `_missing_:title`. -- Search for objects with a `title` field that is not `null`: `_exists_:title`. - -**If you are querying on fields, please make sure they are enabled for search.** - -#### Wildcards and Regular Expressions - -`qu?ck bro*` is an example query string with wildcard characters. `?` means a single character and `*` means any number of characters (including zero). - -Wildcards are a simple form of regular expressions. You can use regular expressions in queries as well: - -``` -name:/joh?n(ath[oa]n)/ -``` - -See [Regexp query](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-regexp-query.html#regexp-syntax) for more details. - -#### Fuzziness - -Search for terms similar to the given one with `~`, like `quikc~`. The [Damerau–Levenshtein algorithm](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance) is used to find all terms with a maximum of two changes. - -The example query above allows you to find terms like `quick`, `qukic`, and `qukci`. - -#### Ranges - -``` -// 1 to 5: -count:[1 TO 5] - -// In 2012 -date:[2012-01-01 TO 2012-12-31] - -// Before 2012 -{* TO 2012-01-01} -``` - -Use `[]` for inclusive ranges and `{}` for exclusive ranges. - -You can also use comparison operators: - -``` -age:>10 -age:>=10 -age:<10 -age:<=10 -``` - -#### Grouping - -You can group queries with parentheses: - -``` -(quick OR brown) AND fox -``` - -#### Notes for Special Fields - -- The type of `objectId` is `string`, which means you can query them like this: `objectId: 558e20cbe4b060308e3eb36c`. However, this is unnecessary because you could simply use the Data Storage SDK for such a type of query. -- `createdAt` and `updatedAt` are mapped as `date`s, like `createdAt:["2015-07-30T00:00:00.000Z" TO "2015-08-15T00:00:00.000Z"]` and `updatedAt: [2012-01-01 TO 2012-12-31]`. -- For `Date` fields except `createdAt` and `updatedAt`, add `.iso` after the field name: `birthday.iso: [2012-01-01 TO 2012-12-31]`. -- For `Pointer` fields, you can query them with `field.objectId`, like `player.objectId: 558e20cbe4b060308e3eb36c and player.className: Player`. Each pointer only has these two properties. No other properties will be included in full-text search. -- `Relation` fields are not supported yet. -- For `File` fields, you can query them by `url` or `id`, like `avatar.url: "https://developer.taptap.io/docs/sdk/storage/guide/fulltext-search-rest/"`. You cannot query files by the content in them. - -### Complex Sorting - -Assuming you want to sort results by an array field named `scores`. To sort results in descending order of the average score, and put those who do not have scores to the last: - -```sh ---data-urlencode 'sort=[{"scores":{"order":"desc","mode":"avg","missing":"_last"}}]' -``` - -The value of `sort` can be an array in JSON with each element being a JSON object: - -```json -{ "scores": { "order": "desc", "mode": "avg", "missing": "_last" } } -``` - -With the field used for sorting as the key, the field can have the following fields: - -- `order`: `asc` for ascending order and `desc` for descending order. -- `mode`: If the field has multiple values or is an array, you can choose to sort objects by the field’s minimum value `min`, maximum value `max`, sum `sum`, or average `avg`. -- `missing`: Specify where to place the objects that have this field missing. You can set it to be `_last` or `_first`, or give it a default value. - -To sort results by multiple fields: - -```json -[ - { - "scores": { "order": "desc", "mode": "avg", "missing": "_last" } - }, - { - "updatedAt": { "order": "asc" } - } -] -``` - -### GeoPoint Queries - -If a field of a class has `GeoPoint` as its type, you can sort the results from this class by their distances to a given point. For example, to find players closest to `[39.9, 116.4]`, you can set `sort` to be: - -```json -{ - "_geo_distance": { - "location": [39.9, 116.4], - "order": "asc", - "unit": "km", - "mode": "min" - } -} -``` - -`order` and `mode` have the same meanings as those described in the last section. Use `unit` to specify the unit of distances: `km` for kilometer, `m` for meter, and `cm` for centimeter. - -## moreLikeThis Queries - -Beside `/1.1/search/select`, we also provide the `/1.1/search/mlt` interface for you to query similar documents. You can use it to implement recommendations and so on. - -Assuming we have a class named `Post` for storing blog articles and we want to use its `tags` field to give users recommendations: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/mlt?like=clojure&clazz=Post&fields=tags" -``` - -Here we set `like` to be `clojure` and `fields` to be `tags`, which means to search for `Post` objects with `tags` similar to `clojure`. The response will look like this: - -```json -{ - "results": [ - { - "tags": ["clojure", "data structure and algorithm"], - "updatedAt": "2016-07-07T08:54:50.268Z", - "_deeplink": "cn.leancloud.qfo17qmvr8w2y6g5gtk5zitcqg7fyv4l612qiqxv8uqyo61n://leancloud/classes/Article/577e18b50a2b580057469a5e", - "_app_url": "https://leancloud.cn/1/go/cn.leancloud.qfo17qmvr8w2y6g5gtk5zitcqg7fyv4l612qiqxv8uqyo61n/classes/Article/577e18b50a2b580057469a5e", - "objectId": "577e18b50a2b580057469a5e", - "_highlight": null, - "createdAt": "2016-07-07T08:54:13.250Z", - "className": "Article", - "title": "clojure persistent vector" - } - // … - ], - "sid": null -} -``` - -You can also use the `likeObjectIds` parameter instead of `like` to search for similar objects: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/mlt?likeObjectIds=577e18b50a2b580057469a5e&clazz=Post&fields=tags" -``` - -The code above searches for objects similar to the one with `577e18b50a2b580057469a5e` as its `objectId`. - -Below are all the query parameters available: - -| Parameter | Required | Description | -| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `clazz` | Required | Class name. | -| `like` | Optional | Keywords. **You need to specify either this parameter or the `likeObjectIds` parameter.** | -| `likeObjectIds` | Optional | Comma-separated objectId list. **You need to specify either this parameter or the `like` parameter.** | -| `min_term_freq` | Optional | The minimum term frequency below which the terms will be ignored from the input document. Defaults to 2. | -| `min_doc_freq` | Optional | The minimum document frequency below which the terms will be ignored from the input document. Defaults to 5. | -| `max_doc_freq` | Optional | The maximum document frequency above which the terms will be ignored from the input document. This could be useful to ignore highly frequent words such as stop words. Defaults to 0. | -| `skip` | Optional | The number of skipped results. Defaults to 0. | -| `limit` | Optional | The number of returned objects. The default value is 100 and the maximum value is 1000. | -| `fields` | Optional | Comma-separated column list. Defaults to `_all`. | -| `include` | Optional | Include objects referenced by Pointers. Example: `user,comment`. | - -You can also refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-mlt-query.html) for more information. - -## Analyzing Strings - -The Full-Text Search service will automatically analyze all `String` fields. -If the outcome doesn’t fit your expectation, you can test how a string will be analyzed with the `analyze` API (requires the Master Key). -You can also use this API to check if a custom word list has taken effect. - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - "https://{{host}}/1.1/search/analyze?clazz=GameScore&text=Responsive+design" -``` - -Provide `clazz` and `text` as parameters. Here `text` is the text being tested. The result looks like this: - -```json -{ - "tokens": [ - { - "token": "Responsive", - "start_offset": 0, - "end_offset": 10, - "type": "word", - "position": 0 - }, - { - "token": "design", - "start_offset": 11, - "end_offset": 17, - "type": "word", - "position": 1 - } - ] -} -``` - -We can see that the term `Responsive design` is divided into two words. -If the analyzer recognizes it as a whole word, the result will be: - -```json -{ - "tokens": [ - { - "token": "Responsive design", - "start_offset": 0, - "end_offset": 17, - "type": "word", - "position": 0 - } - ] -} -``` diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/_category_.json deleted file mode 100644 index 6c29c0a34..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Java", - "collapsed": true, - "position": 3 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/java.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/java.mdx deleted file mode 100644 index 0c832efdb..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/java.mdx +++ /dev/null @@ -1,1389 +0,0 @@ ---- -title: Data Storage Guide for Java -sidebar_label: Java Guide -slug: /sdk/storage/guide/java/ -sidebar_position: 4 ---- - -import Path from "/src/docComponents/path"; - -With the Data Storage service, you can have your app persist data on the cloud and query them at any time. The code below shows how you can create an object and store it into the cloud: - -```java -// Create an object -LCObject todo = new LCObject("Todo"); - -// Set values of fields -todo.put("title", "R&D Weekly Meeting"); -todo.put("content", "All team members, Tue 2pm"); - -// Save the object to the cloud -todo.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // Execute any logic that should take place after the object is saved - System.out.println("Object saved. objectId: " + todo.getObjectId()); - } - public void onError(Throwable throwable) { - // Execute any logic that should take place if the save fails - } - public void onComplete() {} -}); -``` - -The SDK designed for each language interacts with the same REST API via HTTPS, offering fully functional interfaces for you to manipulate the data in the cloud. - -## Installing SDK - -See [Installing Java SDK](/sdk/storage/guide/setup-java/). - -## Objects - -### `LCObject` - -The objects on the cloud are built around `LCObject`. Each `LCObject` contains key-value pairs of JSON-compatible data. This data is schema-free, which means that you don't need to specify ahead of time what keys exist on each `LCObject`. Simply set whatever key-value pairs you want, and our backend will store them. - -For example, the `LCObject` storing a simple todo item may contain the following data: - -```json -title: "Email Linda to Confirm Appointment", -isComplete: false, -priority: 2, -tags: ["work", "sales"] -``` - -### Data Types - -`LCObject` supports a wide range of data types to be used for each field, including common ones like `String`, `Number`, `Boolean`, `Object`, `Array`, and `Date`. You can nest objects in JSON format to store more structured data within a single `Object` or `Array` field. - -Special data types supported by `LCObject` include `Pointer` and `File`, which are used to store a reference to another `LCObject` and binary data respectively. - -`LCObject` also supports `GeoPoint`, a special data type you can use to store location-based data. See [GeoPoints](#geopoints) for more details. - -Some examples: - -```java -// Basic types -boolean bool = true; -int number = 2018; -String string = number + " Top Hits"; -Date date = new Date(); -byte[] data = "Hello world!".getBytes(); -ArrayList arrayList = new ArrayList<>(); -arrayList.add(number); -arrayList.add(string); -HashMap hashMap = new HashMap<>(); -hashMap.put("number", number); -hashMap.put("string", string); - -// Create an object -LCObject testObject = new LCObject("TestObject"); -testObject.put("testBoolean", bool); -testObject.put("testInteger", number); -testObject.put("testDate", date); -testObject.put("testData", data); -testObject.put("testArrayList", arrayList); -testObject.put("testHashMap", hashMap); -testObject.save(); -``` - -We do not recommend storing large pieces of binary data like images or documents with `LCObject` using `byte[]`. The size of each `LCObject` should not exceed **128 KB**. We recommend using `LCFile` for storing images, documents, and other types of files. To do so, create `LCFile` objects and assign them to fields of `LCObject`. See [Files](#files) for details. - -Keep in mind that our backend stores dates in UTC format and the SDK will convert them to local times upon retrieval. - -The date values displayed on ** > Data** are also converted to match your operating system's time zone. The only exception is that when you retrieve these date values through our REST API, they will remain in UTC format. You can manually convert them using appropriate time zones when necessary. - -To learn about how you can protect the data stored on the cloud, see [Data Security](/sdk/storage/guide/security/). - -### Creating Objects - -The code below creates a new instance of `LCObject` with class `Todo`: - -```java -LCObject todo = new LCObject("Todo"); -``` - -The constructor takes a class name as a parameter so that the cloud knows the class you are using to create the object. A class is comparable to a table in a relational database. A class name starts with a letter and can only contain numbers, letters, and underscores. - -### Saving Objects - -The following code saves a new object with class `Todo` to the cloud: - -```java -// Create an object -LCObject todo = new LCObject("Todo"); - -// Set values of fields -todo.put("title", "Sign up for Marathon"); -todo.put("priority", 2); - -// Save the object to the cloud -todo.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // Execute any logic that should take place after the object is saved - System.out.println("Object saved. objectId: " + todo.getObjectId()); - } - public void onError(Throwable throwable) { - // Execute any logic that should take place if the save fails - } - public void onComplete() {} -}); -``` - -To make sure the object is successfully saved, take a look at ** > Data > `Todo`** in your app. You should see a new entry of data with something like this when you click on its `objectId`: - -```json -{ - "title": "Sign up for Marathon", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -You don't have to create or set up a new class called `Todo` in ** > Data** before running the code above. If the class doesn't exist, it will be automatically created. - -Several built-in fields are provided by default which you don't need to specify in your code: - -| Built-in Field | Type | Description | -| -------------- | -------- | ---------------------------------------------------------------------------------------------- | -| `objectId` | `String` | A unique identifier for each saved object. | -| `ACL` | `LCACL` | Access Control List, a special object defining the read and write permissions of other people. | -| `createdAt` | `Date` | The time the object was created. | -| `updatedAt` | `Date` | The time the object was last modified. | - -Each of these fields is filled in by the cloud automatically and doesn't exist on the local `LCObject` until a save operation has been completed. - -Field names, or **keys**, can only contain letters, numbers, and underscores. A custom key can neither start with double underscores `__`, nor be identical to any system reserved words or built-in field names (`ACL`, `className`, `createdAt`, `objectId`, and `updatedAt`) regardless of letter cases. - -**Values** can be strings, numbers, booleans, or even arrays and dictionaries — anything that can be JSON-encoded. See [Data Types](#data-types) for more information. - -We recommend that you adopt CamelCase naming convention to `NameYourClassesLikeThis` and `nameYourKeysLikeThis`, which keeps your code more readable. - -### Retrieving Objects - -If an `LCObject` is already in the cloud, you can retrieve it using its `objectId` with the following code: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.getInBackground("582570f38ac247004f39c24b").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // todo is the instance of the Todo object with objectId 582570f38ac247004f39c24b - String title = todo.getString("title"); - int priority = todo.getInt("priority"); - - // Get special properties - String objectId = todo.getObjectId(); - Date updatedAt = todo.getUpdatedAt(); - Date createdAt = todo.getCreatedAt(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -If you try to access a field or property that doesn't exist, the SDK will not raise an error. Instead, it will return `null`. - -#### Refreshing Objects - -If you need to refresh a local object with the latest version of it in the cloud, call the `fetchInBackground` method on it: - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.fetchInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // todo is refreshed - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -Keep in mind that **any unsaved changes made to the object prior to calling `fetchInBackground` will be discarded**. To avoid this, you have the option to provide **a list of keys** when calling the method so that only the fields being specified are retrieved and refreshed (including special built-in fields such as `objectId`, `createdAt`, and `updatedAt`). Changes made to other fields will remain intact. - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -String keys = "priority, location"; -todo.fetchInBackground(keys).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // Only priority and location will be retrieved and refreshed - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### Updating Objects - -To update an existing object, assign the new data to each field and call the `saveInBackground` method. For example: - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.put("content", "Weekly meeting has been rescheduled to Wed 3pm for this week."); -todo.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject savedTodo) { - System.out.println("Saved."); - } - public void onError(Throwable throwable) { - System.out.println("Failed to save."); - } - public void onComplete() {} -});; -``` - -The cloud automatically figures out which data has changed and only the fields with changes will be sent to the cloud. The fields you didn't update will remain intact. - -#### Updating Data Conditionally - -By passing in a `query` option when saving, you can specify conditions on the save operation so that the object can be updated atomically only when those conditions are met. If no object matches the conditions, the cloud will return error `305` to indicate that there was no update taking place. - -For example, in the class `Account` there is a field called `balance`, and there are multiple incoming requests that want to modify this field. Since an account cannot have a negative balance, we can only allow a request to update the balance when the amount requested is lower than or equal to the balance: - -```java -LCObject account = LCObject.createWithoutData("Account", "5745557f71cfe40068c6abe0"); -// Atomically decrease balance by 100 -final int amount = -100; -account.increment("balance", amount); -// Add the condition -LCSaveOption option = new LCSaveOption(); -option.query(new LCQuery<>("Account").whereGreaterThanOrEqualTo("balance", -amount)); -// Return the latest data in the cloud upon completion. -// All the fields will be returned if the object is new, -// otherwise only fields with changes will be returned. -option.setFetchWhenSave(true); -account.saveInBackground(option).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject account) { - System.out.println("Balance: " + account.get("balance")); - } - public void onError(Throwable throwable) { - System.out.println("Insufficient balance. Operation failed!"); - } - public void onComplete() {} -}); -``` - -**The `query` option only works for existing objects.** In other words, it does not affect objects that haven't been saved to the cloud yet. - -The benefit of using the `query` option instead of combining `LCQuery` and `LCObject` shows up when you have multiple clients trying to update the same field at the same time. The latter way is more cumbersome and may lead to potential inconsistencies. - -#### Updating Counters - -Take Twitter as an example, we need to keep track of how many Likes and Retweets a tweet has gained so far. Since a Like or Retweet action can be triggered simultaneously by multiple clients, saving objects with updated values directly can lead to inaccurate results. To make sure that the total number is stored correctly, you can **atomically** increase (or decrease) the value of a number field: - -```java -post.increment("likes", 1); -``` - -You can specify the amount of increment (or decrement) by providing an additional argument. If the argument is not provided, `1` is used by default. - -#### Updating Arrays - -There are several operations that can be used to atomically update an array associated with a given key: - -- `add()` appends the given object to the end of an array. -- `addUnique()` adds the given object into an array only if it is not in it. The object will be inserted at a random position. -- `removeAll()` removes all instances of the given object from an array. - -For example, `Todo` has a field named `alarms` for keeping track of the times at which a user wants to be alerted. The following code adds the times to the alarms field: - -```java -Date getDateWithDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date date = dateFormat.parse(dateString); - return date; -} - -Date alarm1 = getDateWithDateString("2018-04-30 07:10:00"); -Date alarm2 = getDateWithDateString("2018-04-30 07:20:00"); -Date alarm3 = getDateWithDateString("2018-04-30 07:30:00"); - -LCObject todo = new LCObject("Todo"); -todo.addAllUnique("alarms", Arrays.asList(alarm1, alarm2, alarm3)); -todo.save(); -``` - -### Deleting Objects - -The following code deletes a `Todo` object from the cloud: - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.deleteInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) {} - - @Override - public void onNext(LCNull response) { - // succeed to delete a todo. - } - - @Override - public void onError(@NonNull Throwable e) { - System.out.println("failed to delete a todo: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); -``` - -Removing data from the cloud should always be dealt with great caution as it may lead to non-recoverable data loss. We strongly advise that you read [ACL Guide](/sdk/storage/guide/acl/) to understand the risks thoroughly. You should also consider implementing class-level, object-level, and field-level permissions for your classes in the cloud to guard against unauthorized data operations. - -### Batch Processing - -```java -// Batch create and update -saveAll() -saveAllInBackground() - -// Batch delete -deleteAll() -deleteAllInBackground() - -// Batch fetch -fetchAll() -fetchAllInBackground() -``` - -The following code sets `isComplete` of all `Todo` objects to be `true`: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List todos) { - // Get a collection of todos to work on - for (LCObject todo : todos) { - // Update value - todo.put("isComplete", true); - } - // Save all at once - LCObject.saveAll(todos); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -Although each function call sends multiple operations in one single network request, saving operations and fetching operations are billed as separate API calls for each object in the collection, while deleting operations are billed as a single API call. - -### Running in the Background - -You may have noticed from the examples above that we have been accessing the cloud asynchronously in our code. The methods with names like `xxxxInBackground` are provided for you to implement asynchronous calls so that your main thread will not be blocked. - -### Storing Data Locally - -Most of the operations for saving objects can be executed immediately and the program will be notified once the operation is done. However, if the program does not need to know when the operation is done, you can use `saveEventually` instead. - -The benefit of this function is that if the device is offline, `saveEventually` will cache the data locally and send them to the server once the device gets online again. If the app is closed before the device gets online, the SDK will try to send the data to the server when the app is opened again. - -It is safe to call `saveEventually` (or `deleteEventually`) multiple times since all the operations will be performed in the order they are initiated. - -### Data Models - -Objects may have relationships with other objects. For example, in a blogging application, a `Post` object may have relationships with many `Comment` objects. The Data Storage service supports three kinds of relationships, including one-to-one, one-to-many, and many-to-many. - -#### One-to-One and One-to-Many Relationships - -One-to-one and one-to-many relationships are modeled by saving `LCObject` as a value in the other object. For example, each `Comment` in a blogging app might correspond to one `Post`. - -The following code creates a new `Post` with a single `Comment`: - -```java -// Create a post -LCObject post = new LCObject("Post"); -post.put("title", "I am starving!"); -post.put("content", "Hmmm, where should I go for lunch?"); - -// Create a comment -LCObject comment = new LCObject("Comment"); -comment.put("content", "How about KFC?"); - -// Add the post as a property of the comment -comment.put("parent", post); - -// This will save both post and comment -comment.save(); -``` - -Internally, the backend will store the referred-to object with the `Pointer` type in just one place in order to maintain consistency. You can also link objects using their `objectId`s like this: - -```java -LCObject post = LCObject.createWithoutData("Post", "57328ca079bc44005c2472d0"); -comment.put("post", post); -``` - -See [Relational Queries](#relational-queries) for instructions on how to query relational data. - -#### Many-to-Many Relationships - -The easiest way to model many-to-many relationships is to use **arrays**. In most cases, using arrays helps you reduce the number of queries you need to make and leads to better performance. However, if additional properties need to be attached to the relationships between two classes, using **join tables** would be a better choice. Keep in mind that the additional properties are used to describe the relationships between classes rather than any single class. - -We recommend you to use join tables if the total number of objects of any class exceeds 100. - -### Serialization and Deserialization - -If you need to pass an `LCObject` to a method as an argument, you may want to first serialize the object to avoid certain problems. You can use the following ways to serialize and deserialize `LCObject`s. - -Serialization: - -```java -LCObject todo = new LCObject("Todo"); // Create object -todo.put("title", "Sign up for Marathon"); // Set title -todo.put("priority", 2); // Set priority -todo.put("owner", LCUser.getCurrentUser()); // A Pointer pointing to the current user -String serializedString = todo.toString(); -``` - -Deserialization: - -```java -LCObject deserializedObject = LCObject.parseLCObject(serializedString); -deserializedObject.save(); // Save to the cloud -``` - -## Queries - -We've already seen how you can retrieve a single object from the cloud with `LCObject`, but it doesn't seem to be powerful enough when you need to retrieve multiple objects that match certain conditions at once. In such a situation, `LCQuery` would be a more efficient tool you can use. - -### Basic Queries - -The general steps of performing a basic query include: - -1. Creating `LCQuery`. -2. Putting conditions on it. -3. Retrieving an array of objects matching the conditions. - -The code below retrieves all `Student` objects whose `lastName` is `Smith`: - -```java -LCQuery query = new LCQuery<>("Student"); -query.whereEqualTo("lastName", "Smith"); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List students) { - // students is an array of Student objects satisfying conditions - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### Query Constraints - -There are several ways to put constraints on the objects found by `LCObject`. - -The code below filters out objects with `Jack` as `firstName`: - -```java -query.whereNotEqualTo("firstName", "Jack"); -``` - -For sortable types like numbers and strings, you can use comparisons in queries: - -```java -// Restricts to age < 18 -query.whereLessThan("age", 18); - -// Restricts to age <= 18 -query.whereLessThanOrEqualTo("age", 18); - -// Restricts to age > 18 -query.whereGreaterThan("age", 18); - -// Restricts to age >= 18 -query.whereGreaterThanOrEqualTo("age", 18); -``` - -You can apply multiple constraints to a single query, and objects will only be in the results if they match all of the constraints. In other words, it's like concatenating constraints with `AND`: - -```java -query.whereEqualTo("firstName", "Jack"); -query.whereGreaterThan("age", 18); -``` - -You can limit the number of results by setting `limit` (defaults to `100`): - -```java -// Get at most 10 results -query.limit(10); -``` - -For performance reasons, the maximum value allowed for `limit` is `1000`, meaning that the cloud would only return 1,000 results even if it is set to be greater than `1000`. - -If you need exactly one result, you may use `getFirstInBackground` for convenience: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("priority", 2); -query.getFirstInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // todo is the first Todo object satisfying conditions - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -You can skip a certain number of results by setting `skip`: - -```java -// Skip the first 20 results -query.skip(20); -``` - -You can implement pagination in your app by using `skip` together with `limit`: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("priority", 2); -query.limit(10); -query.skip(20); -``` - -Keep in mind that the higher the `skip` goes, the slower the query will run. You may consider using `createdAt` or `updatedAt` (which are indexed) to set range boundaries for large datasets to make queries more efficient. -You may also use the last value returned from an auto-increment field along with `limit` for pagination. - -For sortable types, you can control the order in which results are returned: - -```java -// Sorts the results in ascending order by the createdAt property -query.orderByAscending("createdAt"); - -// Sorts the results in descending order by the createdAt property -query.orderByDescending("createdAt"); -``` - -You can even attach multiple sorting rules to a single query: - -```java -query.addAscendingOrder("priority"); -query.addDescendingOrder("createdAt"); -``` - -To retrieve objects that have or do not have particular fields: - -```java -// Finds objects that have the "images" field -query.whereExists("images"); - -// Finds objects that don't have the 'images' field -query.whereDoesNotExist("images"); -``` - -You can restrict the fields returned by providing a list of keys with `selectKeys`. The code below retrieves todos with only the `title` and `content` fields (and also special built-in fields including `objectId`, `createdAt`, and `updatedAt`): - -```java -LCQuery query = new LCQuery<>("Todo"); -query.selectKeys(Arrays.asList("title", "content")); -query.getFirstInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - String title = todo.getString("title"); // √ - String content = todo.getString("content"); // √ - String notes = todo.getString("notes"); // An error will occur - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -You can add a minus prefix to the attribute name for inverted selection. -For example, if you do not care about the post author, use `-author`. -The inverted selection also applies to preserved attributes and can be used with dot notations, e.g., `-pubUser.createdAt`. - -The unselected fields can be fetched later with `fetchInBackground`. See [Refreshing Objects](#refreshing-objects). - -### Queries on String Values - -Use `whereStartsWith` to restrict to string values that start with a particular string. Similar to a `LIKE` operator in SQL, it is indexed so it is efficient for large datasets: - -```java -LCQuery query = new LCQuery<>("Todo"); -// SQL equivalent: title LIKE 'lunch%' -query.whereStartsWith("title", "lunch"); -``` - -Use `whereContains` to restrict to string values that contain a particular string: - -```java -LCQuery query = new LCQuery<>("Todo"); -// SQL equivalent: title LIKE '%lunch%' -query.whereContains("title", "lunch"); -``` - -Unlike `whereStartsWith`, `whereContains` can't take advantage of indexes, so it is not encouraged to be used for large datasets. - -Please note that both `whereStartsWith` and `whereContains` perform **case-sensitive** matching, so the examples above will not look for string values containing `Lunch`, `LUNCH`, etc. - -If you are looking for string values that do not contain a particular string, use `whereMatches` with regular expressions: - -```java -LCQuery query = new LCQuery<>("Todo"); -// "title" without "ticket" (case-insensitive) -query.whereMatches("title", "^((?!ticket).)*$", "i"); -``` - -However, performing queries with regular expressions as constraints can be very expensive, especially for classes with over 100,000 records. The reason behind this is that queries like this can't take advantage of indexes and will lead to exhaustive scanning of the whole dataset to find the matching objects. We recommend that you take a look at our In-App Searching feature, a full-text search solution we provide to improve your app's searching ability and user experience. - -If you are facing performance issues with queries, please refer to [Optimizing Performance](#optimizing-performance) for possible workarounds and best practices. - -### Queries on Array Values - -The code below looks for all the objects with `work` as an element of its array field `tags`: - -```java -query.whereEqualTo("tags", "work"); -``` - -To look for objects whose array field `tags` contains three elements: - -```java -query.whereSizeEqual("tags", 3); -``` - -You can also look for objects whose array field `tags` contains `work`, `sales`, **and** `appointment`: - -```java -query.whereContainsAll("tags", Arrays.asList("work", "sales", "appointment")); -``` - -To retrieve objects whose field matches any one of the values in a given list, you can use `whereContainedIn` instead of performing multiple queries. The code below constructs a query that retrieves todo items with `priority` to be `1` **or** `2`: - -```java -// Single query -LCQuery priorityOneOrTwo = new LCQuery<>("Todo"); -priorityOneOrTwo.whereContainedIn("priority", Arrays.asList(1, 2)); -// Mission completed :) - -// --------------- -// vs. -// --------------- - -// Multiple queries -final LCQuery priorityOne = new LCQuery<>("Todo"); -priorityOne.whereEqualTo("priority", 1); - -final LCQuery priorityTwo = new LCQuery<>("Todo"); -priorityTwo.whereEqualTo("priority", 2); - -LCQuery priorityOneOrTwo = LCQuery.or(Arrays.asList(priorityOne, priorityTwo)); -// Kind of verbose :( -``` - -Conversely, you can use `whereNotContainedIn` if you want to retrieve objects that do not match any of the values in a list. - -### Relational Queries - -There are several ways to perform queries for relational data. To retrieve objects whose given field matches a particular `LCObject`, you can use `whereEqualTo` just like how you use it for other data types. For example, if each `Comment` has a `Post` object in its `post` field, you can fetch all the comments for a particular `Post` with the following code: - -```java -LCObject post = LCObject.createWithoutData("Post", "57328ca079bc44005c2472d0"); -LCQuery query = new LCQuery<>("Comment"); -query.whereEqualTo("post", post); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List comments) { - // comments contains the comments for the post - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -To retrieve objects whose given field contains an `LCObject` that matches a different query, you can use `whereMatchesQuery`. The code below constructs a query that looks for all the comments for posts with images: - -```java -LCQuery innerQuery = new LCQuery<>("Post"); -innerQuery.whereExists("image"); - -LCQuery query = new LCQuery<>("Comment"); -query.whereMatchesQuery("post", innerQuery); -``` - -To retrieve objects whose given field does not contain an `LCObject` that matches a different query, use `whereDoesNotMatchQuery` instead. - -Sometimes you may need to look for related objects from different classes without extra queries. In such situations, you can use `include` on the same query. The following code retrieves the last 10 comments together with the posts related to them: - -```java -LCQuery query = new LCQuery<>("Comment"); - -// Retrieve the most recent ones -query.orderByDescending("createdAt"); - -// Only retrieve the last 10 -query.limit(10); - -// Include the related post together with each comment -query.include("post"); - -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List comments) { - // comments contains the last 10 comments including the post associated with each - for (LCObject comment : comments) { - // This does not require a network access - LCObject post = comment.getLCObject("post"); - } - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -#### Caveats about Inner Queries - -The Data Storage service is not built on relational databases, which makes it impossible to join tables while querying. For the relational queries mentioned above, what we would do is to perform an inner query first (with `100` as the default `limit` and `1000` as the maximum) and then insert the result from this query into the outer query. If the number of records matching the inner query exceeds the `limit` and the outer query contains other constraints, the amount of the records returned in the end could be zero or less than your expectation since only the records within the `limit` would be inserted into the outer query. - -The following actions can be taken to solve the problem: - -- Make sure the number of records in the result of the inner query is no more than 100. If it is between 100 and 1,000, set `1000` as the `limit` of the inner query. -- Create redundancy for the fields being queried by the inner query on the table for the outer query. -- Repeat the same query with different `skip` values until all the records are gone through (performance issue could occur if the value of `skip` gets too big). - -### Counting Objects - -If you just need to count how many objects match a query but do not need to retrieve the actual objects, use `countInBackground` instead of `findInBackground`. For example, to count how many todos have been completed: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("isComplete", true); -query.countInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(Integer count) { - System.out.println(count + " todos completed."); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### Compound Queries - -Compound queries can be used if complex query conditions need to be specified. A compound query is a logical combination (`OR` or `AND`) of subqueries. - -Note that we do not support `GeoPoint` or non-filtering constraints (e.g. `near`, `withinGeoBox`, `limit`, `skip`, `ascending`, `descending`, `include`) in the subqueries of a compound query. - -#### OR-ed Query Constraints - -An object will be returned as long as it fulfills any one of the subqueries. The code below constructs a query that looks for all the todos that either have priorities higher than or equal to `3`, or are already completed: - -```java -final LCQuery priorityQuery = new LCQuery<>("Todo"); -priorityQuery.whereGreaterThanOrEqualTo("priority", 3); - -final LCQuery isCompleteQuery = new LCQuery<>("Todo"); -isCompleteQuery.whereEqualTo("isComplete", true); - -LCQuery query = LCQuery.or(Arrays.asList(priorityQuery, isCompleteQuery)); -``` - -Queries regarding `GeoPoint` cannot be present among OR-ed queries. - -#### AND-ed Query Constraints - -The effect of using AND-ed query is the same as adding constraints to `LCQuery`. The code below constructs a query that looks for all the todos that are created between `2016-11-13` and `2016-12-02`: - -```java -Date getDateWithDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - Date date = dateFormat.parse(dateString); - return date; -} - -final LCQuery startDateQuery = new LCQuery<>("Todo"); -startDateQuery.whereGreaterThanOrEqualTo("createdAt", getDateWithDateString("2016-11-13")); - -final LCQuery endDateQuery = new LCQuery<>("Todo"); -endDateQuery.whereLessThan("createdAt", getDateWithDateString("2016-12-03")); - -LCQuery query = LCQuery.and(Arrays.asList(startDateQuery, endDateQuery)); -``` - -While using an AND-ed query by itself doesn't bring anything new compared to a basic query, to combine two or more OR-ed queries, you have to use AND-ed queries: - -```java -Date getDateWithDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - Date date = dateFormat.parse(dateString); - return date; -} - -final LCQuery createdAtQuery = new LCQuery<>("Todo"); -createdAtQuery.whereGreaterThanOrEqualTo("createdAt", getDateWithDateString("2018-04-30")); -createdAtQuery.whereLessThan("createdAt", getDateWithDateString("2018-05-01")); - -final LCQuery locationQuery = new LCQuery<>("Todo"); -locationQuery.whereDoesNotExist("location"); - -final LCQuery priority2Query = new LCQuery<>("Todo"); -priority2Query.whereEqualTo("priority", 2); - -final LCQuery priority3Query = new LCQuery<>("Todo"); -priority3Query.whereEqualTo("priority", 3); - -LCQuery priorityQuery = LCQuery.or(Arrays.asList(priority2Query, priority3Query)); -LCQuery timeLocationQuery = LCQuery.or(Arrays.asList(locationQuery, createdAtQuery)); -LCQuery query = LCQuery.and(Arrays.asList(priorityQuery, timeLocationQuery)); -``` - -### Optimizing Performance - -There are several factors that could lead to potential performance issues when you conduct a query, especially when more than 100,000 records are returned at a time. We are listing some common ones here so you can design your apps accordingly to avoid them: - -- Querying with "not equal to" or "not include" (index will not work) -- Querying on strings with a wildcard at the beginning of the pattern (index will not work) -- Using `count` with conditions (all the entries will be gone through) -- Using `skip` for a large number of entries (all the entries that need to be skipped will be gone through) -- Sorting without an index (querying and sorting cannot share a composite index unless the conditions used on them are both covered by the same one) -- Querying without an index (the conditions used on the query cannot share a composite index unless all of them are covered by the same one; additional time will be consumed if excessive data falls under the uncovered conditions) - -## LiveQuery - -LiveQuery is, as its name implies, derived from [`LCQuery`](#queries) but has enhanced capability. It allows you to automatically synchronize data changes from one client to other clients without writing complex code, making it suitable for apps that need real-time data. - -Suppose you are building an app that allows multiple users to edit the same file at the same time. `LCQuery` would not be an ideal tool since it is based on a pull model and you cannot know when to query from the cloud to get the updates. - -To solve this problem, we introduced LiveQuery. This tool allows you to subscribe to the `LCQuery`s you are interested in. Once subscribed, the cloud will notify clients by generating event messages whenever `LCObject`s that match the `LCQuery` are created or updated, in real-time. - -Behind the scenes, we use WebSocket connections to have clients and the cloud communicate with each other and maintain the subscription status of clients. In most cases, it isn't necessary to deal with the WebSocket connections directly, so we developed a simple API to help you focus on your business logic rather than technical implementations. - -### Initializing LiveQuery - -To use LiveQuery in your app, go to ** > Settings** and check the **Enable LiveQuery** option under the **Security** section. Make sure the module for instant messaging is added to `AndroidManifest.xml`: - -```xml - - - - - - - - -``` - -See [Installing SDK](#installing-sdk) for more details. - -### Demo - -We’ve made a demo app called “LeanTodo” which shows the functionality of LiveQuery. If you’d like to try it: - -1. Go to , enter a username and a password, and then hit “Signup”. -2. Open the same URL on a different device, enter the same credentials, and hit “Login”. -3. Create, edit, or delete some items on one device and watch what happens on the other one. - -### Creating a Subscription - -To make a query **live**, create an `LCQuery` object, put conditions on it if there are any, and then subscribe to events: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("isComplete", true); -LCLiveQuery liveQuery = LCLiveQuery.initWithQuery(query); -liveQuery.subscribeInBackground(new LCLiveQuerySubscribeCallback() { - @Override - public void done(LCException e) { - if (e == null) { - // Subscribed - } - } -}); -``` - -You can't use subqueries or restrict fields being returned when using LiveQuery. - -Now you will be able to receive updates related to `LCObject`. If a `Todo` object is created by another client with `Update Portfolio` as `title`, the following code can get the new `Todo` for you: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("isComplete", true); -LCLiveQuery liveQuery = LCLiveQuery.initWithQuery(query); -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectCreated(LCObject newTodo) { - System.out.println(newTodo.getString("title")); // Update Portfolio - } -}); -liveQuery.subscribeInBackground(new LCLiveQuerySubscribeCallback() { - @Override - public void done(LCException e) { - if (e == null) { - // Subscribed - } - } -}); -``` - -If someone updates this `Todo` by changing its `content` to `Add my recent paintings`, the following code can get the updated version for you: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectUpdated(LCObject updatedTodo, List updatedKeys) { - System.out.println(updatedTodo.getString("content")); // Add my recent paintings - } -}); -``` - -### Event Handling - -The following types of data changes can be monitored once a subscription is set up: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` Event - -A `create` event will be triggered when a new `LCObject` is created and fulfills the `LCQuery` you subscribed to. The `object` is the new `LCObject` being created: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectCreated(LCObject object) { - System.out.println("Object created."); - } -}); -``` - -#### `update` Event - -An `update` event will be triggered when an existing `LCObject` fulfilling the `LCQuery` you subscribed to is updated. The `object` is the `LCObject` being updated: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectUpdated(LCObject object, List updatedKeys) { - System.out.println("Object updated."); - } -}); -``` - -#### `enter` Event - -An `enter` event will be triggered when an existing `LCObject`'s old value does not fulfill the `LCQuery` you subscribed to but its new value does. The `object` is the `LCObject` entering the `LCQuery` and its content is the latest value of it: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectEnter(LCObject object, List updatedKeys) { - System.out.println("Object entered."); - } -}); -``` - -There is a difference between a `create` event and an `enter` event. If an object already exists and later matches the query's conditions, an `enter` event will be triggered. If an object didn't exist already and is later created, a `create` event will be triggered. - -#### `leave` Event - -A `leave` event will be triggered when an existing `LCObject`'s old value fulfills the `LCQuery` you subscribed to but its new value does not. The `object` is the `LCObject` leaving the `LCQuery` and its content is the latest value of it: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectLeave(LCObject object, List updatedKeys) { - System.out.println("Object left."); - } -}); -``` - -#### `delete` Event - -A `delete` event will be triggered when an existing `LCObject` fulfilling the `LCQuery` you subscribed to is deleted. The `object` is the `objectId` of the `LCObject` being deleted: - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectDeleted(String object) { - System.out.println("Object deleted."); - } -}); -``` - -### Unsubscribing - -You can cancel a subscription to stop receiving events regarding `LCQuery`. After that, you won't get any events from the subscription. - -```java -liveQuery.unsubscribeInBackground(new LCLiveQuerySubscribeCallback() { - @Override - public void done(LCException e) { - if (e == null) { - // Successfully unsubscribed - } - } -}); -``` - -### Losing Connections - -There are different scenarios regarding losing connections: - -1. The connection to the Internet is lost unexpectedly. -2. The user performs certain operations outside of the app, like switching the app to the background, turning off the phone, or turning on the flight mode. - -For the scenarios above, you don't need to do any extra work. As long as the user switches back to the app, the SDK will automatically re-establish the connection. - -There is another scenario when **the user completely kills the app or closes the web page**. In this case, the SDK cannot automatically re-establish the connection. You will have to create subscriptions again by yourself. - -### Monitoring Network Status Changes - -Your app can get notified when a LiveQuery connection is established, closed, or getting an error with the `setConnectionHandler` method: - -```java -LCLiveQuery.setConnectionHandler(new LCLiveQueryConnectionHandler() { - @Override - public void onConnectionOpen() { - System.out.println("============ LiveQuery Connection opened ============"); - } - - @Override - public void onConnectionClose() { - System.out.println("============ LiveQuery Connection closed ============"); - } - - @Override - public void onConnectionError(int code, String reason) { - System.out.println("============ LiveQuery Connection error. code:" + code - + ", reason:" + reason + " ============"); - } -}); -``` - -### Caveats about LiveQuery - -Given the real-time feature of LiveQuery, developers may find it tempting to use it for instant messaging. As LiveQuery is neither designed nor optimized for completing such tasks, we discourage such use of this tool, let alone there will be an additional cost for saving message history and rising challenges of code maintenance. We recommend using our Instant Messaging service for this scenario. - -Applications that use LiveQuery without using Instant Messaging and other push services -can invoke the static method `startIfRequired` of `PushService` to create WebSocket connections when initializing the SDK: - -```java -PushService.startIfRequired(android.content.Context context); -``` - -## Files - -`LCFile` allows you to store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `LCObject`. The most common use case is storing images, but you can also use it for documents, videos, music, and any other binary data. - -### Creating Files - -You can create a file from a string: - -```java -// resume.txt is the file name -LCFile file = new LCFile("resume.txt", "LeanCloud".getBytes()); -``` - -You can also create a file from a URL: - -```java -LCFile file = new LCFile( - "logo.png", - "https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png", - new HashMap() -); -``` - -When creating files from URLs, the SDK will not upload the actual files into the cloud but will store the addresses of the files as strings. This will not lead to actual traffic for uploading files, as opposed to creating files in other ways by doing which the files will be actually stored into the cloud. - -The cloud will auto-detect the type of the file you are uploading based on the file extension, but you can also specify the `Content-Type` (commonly referred to as MIME type): - -```java -Map meta = new HashMap(); -meta.put("mime_type", "application/json"); -LCFile file = new LCFile("resume.txt", "LeanCloud".getBytes(), meta); -``` - -But the most common method for creating files is to upload them from local paths: - -```java -LCFile file = LCFile.withAbsoluteLocalPath("avatar.jpg", "/tmp/avatar.jpg"); -``` - -The file we uploaded here is named `avatar.jpg`. There are a couple of things to note here: - -- Each file uploaded will get its unique `objectId`, so it is allowed for multiple files to share the same name. -- A correct extension needs to be assigned to each file which the cloud will use to infer the type of a file. For example, if you are storing a PNG image with `LCFile`, use `.png` as its extension. -- If the file doesn't have an extension and the content type is not specified, the file will get the default type `application/octet-stream`. - -### Saving Files - -By saving a file, you store it into the cloud and get a permanent URL pointing to it: - -```java -file.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCFile file) { - System.out.println("File saved. URL: " + file.getUrl()); - } - public void onError(Throwable throwable) { - // The file either could not be read or could not be saved to the cloud - } - public void onComplete() {} -}); -``` - -A file successfully uploaded can be found in ** > Files** and cannot be modified later. If you need to change the file, you have to upload the modified file again and a new `objectId` and URL will be generated. - -You can associate a file with `LCObject` after it has been saved: - -```java -LCObject todo = new LCObject("Todo"); -todo.put("title", "Buy Cakes"); -// The type of attachments is Array -todo.add("attachments", file); -todo.save(); -``` - -You can also construct an `LCQuery` to query files: - -```java -LCQuery query = new LCQuery<>("_File"); -``` - -Note that the `url` field of internal files (files uploaded to the file service) is dynamically generated by the cloud, which will switch custom domain names automatically. -Therefore, querying files by the `url` field is only applicable to external files (files created by saving the external URL directly to the `_File` table). -Query internal files by the `key` field (path in URL) instead. - -On a related note, if the files are referenced in an array field of `LCObject` and you want to get them within the same query for `LCObject`, you need to use the `include` method with `LCQuery`. For example, if you are retrieving all the todos with the same title `Buy Cakes` and you want to retrieve their related attachments at the same time: - -```java -// Get all todos with the same title and contain attachments -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("title", "Buy Cakes"); -query.whereExists("attachments"); - -// Include attachments with each todo -query.include("attachments"); - -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List todos) { - for (LCObject todo : todos) { - // Get the attachments array for each todo - } - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### Upload Progress - -You can monitor the upload progress and display that to the user: - -```java -file.saveInBackground(new ProgressCallback() { - @Override - public void done(Integer percent) { - System.out.println("Progress: " + percent + "%"); - } -}); -``` - -### File Metadata - -When uploading a file, you can attach additional properties to it with `metaData`. A file's `metaData` cannot be updated once the file is stored to the cloud. - -```java -// Set metadata -file.addMetaData("author", "LeanCloud"); -file.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCFile file) { - // Get author - String author = (String) file.getMetaData("author"); - // Get file name - String fileName = file.getName(); - // Get size (not available for files created from base64-encoded strings or URLs) - int size = file.getSize(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### Image Thumbnails - -After saving an image, you can get the URL of a thumbnail of the image beside that of the image itself. You can even specify the width and height of the thumbnail: - -```java -LCFile file = new LCFile("test.jpg", "file-url", new HashMap()); -file.getThumbnailUrl(true, 100, 100); -``` - -You can only get thumbnails for images smaller than **20 MB**. - -### Downloading Files - -Call `getDataInBackground` or `getDataStreamInBackground` on an `LCFile` to download the file: - -```java -file.getDataInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) {} - - @Override - public void onNext(byte[] bytes) { - Log.d("LCFile", "file data length: " + bytes.length); - } - - @Override - public void onError(Throwable e) { - Log.d("LCFile", "failed to get data. cause: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); - -file.getDataStreamInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) {} - - @Override - public void onNext(InputStream inputStream) { - try { - byte[] buffer = new byte[102400]; - int read = inputStream.read(buffer); - Log.d("LCFile", "file data length: " + read); - inputStream.close(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - @Override - public void onError(Throwable e) { - Log.d("LCFile", "failed to get data. cause: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); -``` - -You can also get the `url` of the `LCFile` and use the standard library or a third-party library to download the file. - -### Deleting Files - -The code below deletes a file from the cloud: - -```java -LCObject file = LCObject.createWithoutData("_File", "552e0a27e4b0643b709e891e"); -file.deleteInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) {} - - @Override - public void onNext(LCNull response) { - // succeed to delete the file - } - - @Override - public void onError(@NonNull Throwable e) { - System.out.println("failed to delete the file: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); -``` - -By default, a file is not allowed to be deleted. -We recommend you delete files by accessing our REST API with the Master Key. -You can also allow certain users and roles to delete files by going to ** > Files > Permission**. - -### File Censorship - -The **censorship** feature allows you to censor **image** files stored on the cloud. - -You can **Enable automatic content censor for subsequent uploaded pictures** by going to **Data Storage > Files > Censorship**. You can also batch-censor all the images uploaded during a specific time scope. You can view the results of the censorship under the **Files** tab. - -You can manually **Pass** or **Block** images even if they have gone through automatic censorship. - -## GeoPoints - -You can associate real-world latitude and longitude coordinates with an object by adding an `LCGeoPoint` to the `LCObject`. By doing so, queries on the proximity of an object to a given point can be performed, allowing you to implement functions like looking for users or places nearby easily. - -To associate a point with an object, you need to create the point first. The code below creates an `LCGeoPoint` with `39.9` as `latitude` and `116.4` as `longitude`: - -```java -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -``` - -Now you can store the point into an object as a regular field: - -```java -todo.put("location", point); -``` - -### Geo Queries - -With a number of existing objects with spatial coordinates, you can find out which of them are closest to a given point, or are contained within a particular area. This can be done by adding another restriction to `LCQuery` using `whereNear`. The code below returns a list of `Todo` objects with `location` closest to a given point: - -```java -LCQuery query = new LCQuery<>("Todo"); -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -query.whereNear("location", point); - -// Limit to 10 results -query.limit(10); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List todos) { - // todos is an array of Todo objects satisfying conditions - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -Additional sorting conditions like `orderByAscending` and `orderByDescending` will gain higher priorities than the default order by distance. - -To have the results limited within a certain distance, check out `whereWithinKilometers`, `whereWithinMiles`, and `whereWithinRadians` in our API docs. - -You can also query for the set of objects that are contained within a rectangular bounding box with `whereWithinGeoBox`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```java -LCQuery query = new LCQuery<>("Todo"); -LCGeoPoint southwest = new LCGeoPoint(30, 115); -LCGeoPoint northeast = new LCGeoPoint(40, 118); -query.whereWithinGeoBox("location", southwest, northeast); -``` - -### Caveats about GeoPoints - -Points should not exceed the extreme ends of the ranges. Latitude should be between `-90.0` and `90.0`. Longitude should be between `-180.0` and `180.0`. Attempting to set latitude or longitude out of bounds will cause an error. -Also, each `LCObject` can only have one field for `LCGeoPoint`. - -## Users - -See [TDS Authentication Guide](/sdk/authentication/guide/). - -## Roles - -As your app grows in scope and user base, you may find yourself needing more coarse-grained control over access to pieces of your data than user-linked ACLs can provide. To address this requirement, we support a form of role-based access control. Check the detailed [ACL Guide](/sdk/storage/guide/acl/) to learn how to set it up for your objects. - -## Full-Text Search - -Full-Text Search offers a better way to search through the information contained within your app. It's built with search engine capabilities that you can easily tap into your app. Effective and useful searching functionality in your app is crucial for helping users find what they need. For more details, see [Full-Text Search Guide](/sdk/storage/guide/fulltext-search/). diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/setup-java.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/setup-java.mdx deleted file mode 100644 index c41668e8f..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/java-guide/setup-java.mdx +++ /dev/null @@ -1,483 +0,0 @@ ---- -title: Installing Java SDK for Data Storage and Instant Messaging -sidebar_label: Installing Java SDK -slug: /sdk/storage/guide/setup-java/ -sidebar_position: 3 ---- - -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import DomainBinding from "../../_partials/setup-domain.mdx"; -import AppConfig from "../_partials/app-config.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -## The Relationships Between the SDK and Different Platforms - -The following content shows the libraries included in the Java SDK as well as their relationships with different platforms: - -### Basic Libraries (Can Be Used in Pure Java Environment) - -- `storage-core`: Provides features for the Data Storage service, which include: - - Data Storage (LCObject) - - TDS Authentication (LCUser) - - Queries (LCQuery) - - File Storage (LCFile) - - Friends (LCFriendship; not available in the current version yet) - - Moments (LCStatus; not available in the current version yet) - - SMS (LCSMS) - - And more -- `realtime-core`: Partially depending on the `storage-core` library, provides features like LiveQuery and Instant Messaging, which include: - - LiveQuery - - LCIMClient - - LCIMConversation and different types of conversations - - LCIMMessage and multimedia messages - - And more - -### Libraries for Android Only - -- `storage-android`: The `storage-core` library customized for Android. Offers exactly the same interfaces as `storage-core`. -- `realtime-android`: The `realtime-core` library customized for Android. Contains interfaces for push notifications for Android. -- `mixpush-android`: The mixpush library supporting the official push services of Huawei, Xiaomi, Meizu, vivo, and OPPO. -- `leancloud-fcm`: An encapsulation of Firebase Cloud Messaging for Push Notification services. - -### Dependencies - -The Java SDK contains the following modules: - -| Directory | Module name | Platform | Dependency | -| ------------------------------ | ----------------------------------------------------------------------------------- | -------- | ------------------------------ | -| ./core | storage-core (for Data Storage) | Java | None | -| ./realtime | realtime-core (for LiveQuery and Instant Messaging) | Java | storage-core | -| ./android-sdk/storage-android | storage-android (Data Storage for Android) | Android | storage-core | -| ./android-sdk/realtime-android | realtime-android (for Android push notifications, LiveQuery, and Instant Messaging) | Android | storage-android, realtime-core | -| ./android-sdk/mixpush-android | Android mixpush | Android | realtime-android | -| ./android-sdk/leancloud-fcm | Firebase Cloud Messaging library | Android | realtime-android | - -## Installing SDK - -There are several ways for you to install our SDK and the most convenient one is to use a package manager. - -We have published all the libraries to Maven. You can use any package manager to install the SDK. - -### Data Storage - - - - -Use the following packages if you are building an Android app: - - -{`implementation 'cn.leancloud:storage-android:${sdkVersions.leancloud.java}' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'`} - - - - - - - {` - cn.leancloud - storage-core - ${sdkVersions.leancloud.java} -`} - - - - - - - - {``} - - - - - - - - {`libraryDependencies += "cn.leancloud" %% "storage-core" % "${sdkVersions.leancloud.java}"`} - - - - - - - - {`implementation 'cn.leancloud:storage-core:${sdkVersions.leancloud.java}'`} - - - - - - -### Instant Messaging and Push Notification - - - - -Use the following packages if you are building an Android app: - - - {`implementation 'cn.leancloud:realtime-android:${sdkVersions.leancloud.java}' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'`} - - -To use mixpush: - - - {`implementation 'cn.leancloud:mixpush-android:${sdkVersions.leancloud.java}' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'`} - - - - - - - - {` - cn.leancloud - realtime-core - ${sdkVersions.leancloud.java} -`} - - - - - - - - {``} - - - - - - - - {`libraryDependencies += "cn.leancloud" %% "realtime-core" % "${sdkVersions.leancloud.java}"`} - - - - - - - - {`implementation 'cn.leancloud:realtime-core:${sdkVersions.leancloud.java}'`} - - - - - - -
    -Special notes about Maven - -We noticed that sometimes the cache policy of the CDN provided by Maven may not work properly, which makes it unable for certain versions (or certain formats of a version) of our library to be downloaded. If this happens to you, you can try to specify a Sonatype repository in your configuration. - -To do so, update pom.xml for Maven: - -```xml - - - oss-sonatype - oss-sonatype - https://oss.sonatype.org/content/groups/public/ - - -``` - -Update build.gradle for Gradle: - -```groovy -buildscript { - repositories { - google() - jcenter() - // Add the following configuration - maven { - url "https://oss.sonatype.org/content/groups/public/" - } - } -} - -allprojects { - repositories { - google() - jcenter() - // Add the following configuration - maven { - url "https://oss.sonatype.org/content/groups/public/" - } - } -} -``` - -
    - -### Installing Manually - -Run the following command to download and install the Java SDK: - -```sh -$ git clone https://github.com/leancloud/java-unified-sdk.git -$ cd java-unified-sdk/ -$ mvn clean install -``` - -Download and install the Android SDK: - -```sh -$ cd java-unified-sdk/ -$ cd android-sdk/ -$ gradle clean assemble -``` - -## Initializing Your Project - -### Credentials - - - -### Initializing for Android - -If you are working on an Android project, add the following lines to the `onCreate` method of the `Application` class: - - - -```java -import cn.leancloud.LeanCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // Do not call the initialize method of cn.leancloud.core.LeanCloud, or you will see errors like NetworkOnMainThread. - LeanCloud.initialize(this, "your-client-id", "your-client-token", "https://your_server_url"); - } -} -``` - - - - - -```java -import cn.leancloud.LeanCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // Do not call the initialize method of cn.leancloud.core.LeanCloud, or you will see errors like NetworkOnMainThread. - LeanCloud.initialize(this, "your-app-id", "your-app-key", "https://your_server_url"); - } -} -``` - - - -Then specify the permissions needed by the SDK and declare the `MyLeanCloudApp` class in `AndroidManifest.xml`: - -```xml - - - - - - - - - - - - - - - - - - - -``` - -Note: If you’re using Instant Messaging without Push Notification, you can set the `LCIMOptions#disableAutoLogin4Push` when initializing the SDK so that it will take less time for the users to log in: - -```java -// Call after LeanCloud.initialize to disable the automatic login request for Push Notification -LCIMOptions.getGlobalOptions().setDisableAutoLogin4Push(true); -``` - -#### A Safer Way to Initialize the SDK for the Client - -For Android developers, starting from version 6.1.0 of our SDK, you can initialize the SDK with a safer method besides using the appId and the appKey. With this method, you can initialize the SDK with the appId only: - -```java -import cn.leancloud.LeanCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // Provide `this`, App ID, and the custom API domain as arguments - LeanCloud.initializeSecurely(this, "{{appid}}", "https://your_server_url"); - } -} -``` - -### Initializing for Java - -If you are working on a Java project, add the following lines to the beginning of your code; - - - -```java -import cn.leancloud.core.LeanCloud; - -LeanCloud.initialize("your-client-id", "your-client-token", "https://your_server_url"); -``` - - - - - -```java -import cn.leancloud.core.LeanCloud; - -LeanCloud.initialize("your-app-id", "your-app-key", "https://your_server_url"); -``` - - - -Keep in mind that if you are using the SDK within Cloud Engine, you do not need to set the `serverUrl`. -If you are initializing your project with our boilerplate, you will see that the `serverUrl` is not provided. -Please do not call the `setServer` method as it will make the APIs go through the internet rather than the intranet, which increases the time each request needs to take. - -The `realtime-core` library can be used in Java apps as well, but the way it is used is different than that for Android apps. You will have to explicitly establish a persistent connection (which is handled automatically by PushService in Android). The following code shows how you can establish a persistent connection: - -```java -LCConnectionManager.getInstance().startConnection(new LCCallback() { - @Override - protected void internalDone0(Object o, LCException e) { - if (e == null) { - System.out.println("WebSocket connection established"); - } else { - System.out.println("Failed to establish WebSocket connection: " + e.getMessage()); - } - } -}); -``` - -The messaging functions can be used with the persistent connection established. - -## Domain - - - -## Enabling Debug Logs - -You can easily trace the problems in your project by turning debug logs on during the development phase. Once enabled, details of every request made by the SDK along with errors will be output to your IDE, your browser console, or your Cloud Engine instances’ logs. - -```java -// execute before initializing SDK -LeanCloud.setLogLevel(LCLogger.Level.DEBUG); -``` - -:::caution -Make sure debug logs are turned off before your app is published. Failure to do so may lead to the exposure of sensitive data. -::: - -## Verifying - -First of all, make sure you are able to connect to the server from your computer. You can test it by running the following command: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` is the custom API domain. - -If everything goes well, it will return the current date: - -```json -{ "__type": "Date", "iso": "2020-10-12T06:46:56.000Z" } -``` - -Now add the following code to your project: - -```java -LCObject testObject = new LCObject("TestObject"); -testObject.put("words", "Hello world!"); -testObject.saveInBackground().blockingSubscribe(); -``` - -Save and run your program. - -Then go to ** > Data > `TestObject`**. If you see a row with its `words` column being `Hello world!`, it means that you have correctly installed the SDK. - -See [Debugging](#Debugging) if you’re not seeing the content. - -## Debugging - -This guide is written for the latest version of our SDK. If you encounter any errors, please first make sure you have the latest version installed. - -### `401 Unauthorized` - -If you get a `401` error or see the following content in network logs: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -It means that the App ID or App Key might be incorrect or don’t match. If you have multiple apps, you might have used the App ID of one app with the App Key of another one, which will lead to such an error. - -### The Client Cannot Access the Internet - -Make sure you have granted the required permissions to your mobile app. - -## Android Code Obfuscation - -To make sure the SDK still works after you obfuscate your code, certain classes and third-party libraries should not be obfuscated: - -``` -# proguard.cfg --keepattributes Signature --dontwarn com.jcraft.jzlib.** --keep class com.jcraft.jzlib.** { *;} --dontwarn sun.misc.** --keep class sun.misc.** { *;} --dontwarn retrofit2.** --keep class retrofit2.** { *;} --dontwarn io.reactivex.** --keep class io.reactivex.** { *;} --dontwarn sun.security.** --keep class sun.security.** { *; } --dontwarn com.google.** --keep class com.google.** { *;} --dontwarn cn.leancloud.** --keep class cn.leancloud.** { *;} --keep public class android.net.http.SslError --keep public class android.webkit.WebViewClient --dontwarn android.webkit.WebView --dontwarn android.net.http.SslError --dontwarn android.webkit.WebViewClient --dontwarn android.support.** --dontwarn org.apache.** --keep class org.apache.** { *;} --dontwarn okhttp3.** --keep class okhttp3.** { *;} --keep interface okhttp3.** { *; } --dontwarn okio.** --keep class okio.** { *;} --keepattributes *Annotation* -``` diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/objc-guide/_category_.json b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/objc-guide/_category_.json deleted file mode 100644 index 219e26ae3..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/objc-guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Objective-C", - "collapsed": true, - "position": 4 -} diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/objc-guide/objc.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/objc-guide/objc.mdx deleted file mode 100644 index ec6d0a441..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/objc-guide/objc.mdx +++ /dev/null @@ -1,1276 +0,0 @@ ---- -title: Data Storage Guide for Objective-C -sidebar_label: Objective-C Guide -slug: /sdk/storage/guide/objc/ -sidebar_position: 6 ---- - -import Path from "/src/docComponents/path"; - -With the Data Storage service, you can have your app persist data on the cloud and query them at any time. The code below shows how you can create an object and store it into the cloud: - -```objc -// Create an object -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; - -// Set values of fields -[todo setObject:@"R&D Weekly Meeting" forKey:@"title"]; -[todo setObject:@"All team members, Tue 2pm" forKey:@"content"]; - -// Save the object to the cloud -[todo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // Execute any logic that should take place after the object is saved - NSLog(@"Object saved. objectId: %@", todo.objectId); - } else { - // Execute any logic that should take place if the save fails - } -}]; -``` - -The SDK designed for each language interacts with the same REST API via HTTPS, offering fully functional interfaces for you to manipulate the data in the cloud. - -## Installing SDK - -See [Installing Objective-C SDK](/sdk/storage/guide/setup-objc/). - -## Objects - -### `LCObject` - -The objects on the cloud are built around `LCObject`. Each `LCObject` contains key-value pairs of JSON-compatible data. This data is schema-free, which means that you don't need to specify ahead of time what keys exist on each `LCObject`. Simply set whatever key-value pairs you want, and our backend will store them. - -For example, the `LCObject` storing a simple todo item may contain the following data: - -```json -title: "Email Linda to Confirm Appointment", -isComplete: false, -priority: 2, -tags: ["work", "sales"] -``` - -### Data Types - -`LCObject` supports a wide range of data types to be used for each field, including common ones like `String`, `Number`, `Boolean`, `Object`, `Array`, and `Date`. You can nest objects in JSON format to store more structured data within a single `Object` or `Array` field. - -Special data types supported by `LCObject` include `Pointer` and `File`, which are used to store a reference to another `LCObject` and binary data respectively. - -`LCObject` also supports `GeoPoint`, a special data type you can use to store location-based data. See [GeoPoints](#geopoints) for more details. - -Some examples: - -```objc -// Basic types -NSNumber *boolean = @(YES); -NSNumber *number = [NSNumber numberWithInt:2018]; -NSString *string = [NSString stringWithFormat:@"%@ Top Hits", number]; -NSDate *date = [NSDate date]; -NSData *data = [@"Hello world!" dataUsingEncoding:NSUTF8StringEncoding]; -NSArray *array = [NSArray arrayWithObjects: string, number, nil]; -NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: number, @"number", string, @"string", nil]; - -// Create an object -LCObject *testObject = [LCObject objectWithClassName:@"TestObject"]; -[testObject setObject:boolean forKey:@"testBoolean"]; -[testObject setObject:number forKey:@"testInteger"]; -[testObject setObject:string forKey:@"testString"]; -[testObject setObject:date forKey:@"testDate"]; -[testObject setObject:data forKey:@"testData"]; -[testObject setObject:array forKey:@"testArray"]; -[testObject setObject:dictionary forKey:@"testDictionary"]; -[testObject saveInBackground]; -``` - -We do not recommend storing large pieces of binary data like images or documents with `LCObject` using `NSData`. The size of each `LCObject` should not exceed **128 KB**. We recommend using `LCFile` for storing images, documents, and other types of files. To do so, create `LCFile` objects and assign them to fields of `LCObject`. See [Files](#files) for details. - -Keep in mind that our backend stores dates in UTC format and the SDK will convert them to local times upon retrieval. - -The date values displayed on ** > Data** are also converted to match your operating system's time zone. The only exception is that when you retrieve these date values through our REST API, they will remain in UTC format. You can manually convert them using appropriate time zones when necessary. - -To learn about how you can protect the data stored on the cloud, see [Data Security](/sdk/storage/guide/security/). - -### Creating Objects - -The code below creates a new instance of `LCObject` with class `Todo`: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; - -// You can also use this equivalent way -LCObject *todo = [[LCObject alloc] initWithClassName:@"Todo"]; -``` - -The constructor takes a class name as a parameter so that the cloud knows the class you are using to create the object. A class is comparable to a table in a relational database. A class name starts with a letter and can only contain numbers, letters, and underscores. - -### Saving Objects - -The following code saves a new object with class `Todo` to the cloud: - -```objc -// Create an object -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; - -// Set values of fields -[todo setObject:@"Sign up for Marathon" forKey:@"title"]; -[todo setObject:@2 forKey:@"priority"]; - -// Save the object to the cloud -[todo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // Execute any logic that should take place after the object is saved - NSLog(@"Object saved. objectId: %@", todo.objectId); - } else { - // Execute any logic that should take place if the save fails - } -}]; -``` - -To make sure the object is successfully saved, take a look at ** > Data > `Todo`** in your app. You should see a new entry of data with something like this when you click on its `objectId`: - -```json -{ - "title": "Sign up for Marathon", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -You don't have to create or set up a new class called `Todo` in ** > Data** before running the code above. If the class doesn't exist, it will be automatically created. - -Several built-in fields are provided by default which you don't need to specify in your code: - -| Built-in Field | Type | Description | -| -------------- | ---------- | ---------------------------------------------------------------------------------------------- | -| `objectId` | `NSString` | A unique identifier for each saved object. | -| `ACL` | `LCACL` | Access Control List, a special object defining the read and write permissions of other people. | -| `createdAt` | `NSDate` | The time the object was created. | -| `updatedAt` | `NSDate` | The time the object was last modified. | - -Each of these fields is filled in by the cloud automatically and doesn't exist on the local `LCObject` until a save operation has been completed. - -Field names, or **keys**, can only contain letters, numbers, and underscores. A custom key can neither start with double underscores `__`, nor be identical to any system reserved words or built-in field names (`ACL`, `className`, `createdAt`, `objectId`, and `updatedAt`) regardless of letter cases. - -**Values** can be strings, numbers, booleans, or even arrays and dictionaries — anything that can be JSON-encoded. See [Data Types](#data-types) for more information. - -We recommend that you adopt CamelCase naming convention to `NameYourClassesLikeThis` and `nameYourKeysLikeThis`, which keeps your code more readable. - -### Retrieving Objects - -If an `LCObject` is already in the cloud, you can retrieve it using its `objectId` with the following code: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query getObjectInBackgroundWithId:@"582570f38ac247004f39c24b" block:^(LCObject *todo, NSError *error) { - // todo is the instance of the Todo object with objectId 582570f38ac247004f39c24b - NSString *title = todo[@"title"]; - int priority = [[todo objectForKey:@"priority"] intValue]; - - // Get special properties - NSString *objectId = todo.objectId; - NSDate *updatedAt = todo.updatedAt; - NSDate *createdAt = todo.createdAt; -}]; -``` - -If you try to access a field or property that doesn't exist, the SDK will not raise an error. Instead, it will return `nil`. - -#### Refreshing Objects - -If you need to refresh a local object with the latest version of it in the cloud, call the `fetchInBackgroundWithBlock` method on it: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -LCObjectFetchOption *option = [LCObjectFetchOption new]; -option.selectKeys = @[@"priority", @"location"]; -[todo fetchInBackgroundWithOption:option block:^(LCObject * _Nullable object, NSError * _Nullable error) { - // Only priority and location will be retrieved and refreshed -}]; -``` - -### Updating Objects - -To update an existing object, assign the new data to each field and call the `saveInBackground` method. For example: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -[todo setObject:@"Weekly meeting has been rescheduled to Wed 3pm for this week." forKey:@"content"]; -[todo saveInBackground]; -``` - -The cloud automatically figures out which data has changed and only the fields with changes will be sent to the cloud. The fields you didn't update will remain intact. - -#### Updating Data Conditionally - -By passing in a `query` option when saving, you can specify conditions on the save operation so that the object can be updated atomically only when those conditions are met. If no object matches the conditions, the cloud will return error `305` to indicate that there was no update taking place. - -For example, in the class `Account` there is a field called `balance`, and there are multiple incoming requests that want to modify this field. Since an account cannot have a negative balance, we can only allow a request to update the balance when the amount requested is lower than or equal to the balance: - -```objc -LCObject *account = [LCObject objectWithClassName:@"Account" objectId:@"5745557f71cfe40068c6abe0"]; -// Atomically decrease balance by 100 -NSInteger amount = -100; -[account incrementKey:@"balance" byAmount:@(amount)]; -// Add the condition -LCQuery *query = [[LCQuery alloc] init]; -[query whereKey:@"balance" greaterThanOrEqualTo:@(-amount)]; -LCSaveOption *option = [[LCSaveOption alloc] init]; -option.query = query; -// Return the latest data in the cloud upon completion. -// All the fields will be returned if the object is new, -// otherwise only fields with changes will be returned. -option.fetchWhenSave = YES; -[account saveInBackgroundWithOption:option block:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"Balance: %@", account[@"balance"]); - } else if (error.code == 305) { - NSLog(@"Insufficient balance. Operation failed!"); - } -}]; -``` - -**The `query` option only works for existing objects.** In other words, it does not affect objects that haven't been saved to the cloud yet. - -The benefit of using the `query` option instead of combining `LCQuery` and `LCObject` shows up when you have multiple clients trying to update the same field at the same time. The latter way is more cumbersome and may lead to potential inconsistencies. - -#### Updating Counters - -Take Twitter as an example, we need to keep track of how many Likes and Retweets a tweet has gained so far. Since a Like or Retweet action can be triggered simultaneously by multiple clients, saving objects with updated values directly can lead to inaccurate results. To make sure that the total number is stored correctly, you can **atomically** increase (or decrease) the value of a number field: - -```objc -[post incrementKey:@"likes" byAmount:@1]; -``` - -You can specify the amount of increment (or decrement) by providing an additional argument. If the argument is not provided, `1` is used by default. - -#### Updating Arrays - -There are several operations that can be used to atomically update an array associated with a given key: - -- `addObject:forKey:` appends the given object to the end of an array. -- `addObjectsFromArray:forKey:` appends the given array of objects to the end of an array. -- `addUniqueObject:forKey:` appends the given object to the end of an array ensuring that the object only appears once within the array. -- `addUniqueObjectsFromArray:forKey:` appends the given array of objects to the end of an array ensuring that each object only appears once within the array. -- `removeObject:forKey:` removes all instances of the given object from an array. -- `removeObjectsInArray:forKey:` removes all instances of the given array of objects from an array. - -For example, `Todo` has a field named `alarms` for keeping track of the times at which a user wants to be alerted. The following code adds the times to the alarms field: - -```objc --(NSDate*) getDateWithDateString:(NSString*) dateString{ - NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init]; - [dateFormat setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; - NSDate *date = [dateFormat dateFromString:dateString]; - return date; -} - -NSDate *alarm1 = [self getDateWithDateString:@"2018-04-30 07:10:00"]; -NSDate *alarm2 = [self getDateWithDateString:@"2018-04-30 07:20:00"]; -NSDate *alarm3 = [self getDateWithDateString:@"2018-04-30 07:30:00"]; - -NSArray *alarms = [NSArray arrayWithObjects:alarm1, alarm2, alarm3, nil]; - -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; -[todo addUniqueObjectsFromArray:alarms forKey:@"alarms"]; -[todo saveInBackground]; -``` - -### Deleting Objects - -The following code deletes a `Todo` object from the cloud: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -[todo deleteInBackground]; -``` - -Removing data from the cloud should always be dealt with great caution as it may lead to non-recoverable data loss. We strongly advise that you read [ACL Guide](/sdk/storage/guide/acl/) to understand the risks thoroughly. You should also consider implementing class-level, object-level, and field-level permissions for your classes in the cloud to guard against unauthorized data operations. - -### Batch Processing - -```objc -// Batch create and update -+ (BOOL)saveAll:(NSArray *)objects error:(NSError **)error; -+ (void)saveAllInBackground:(NSArray *)objects - block:(LCBooleanResultBlock)block; - -// Batch delete -+ (BOOL)deleteAll:(NSArray *)objects error:(NSError **)error; -+ (void)deleteAllInBackground:(NSArray *)objects - block:(LCBooleanResultBlock)block; - -// Batch fetch -+ (BOOL)fetchAll:(NSArray *)objects error:(NSError **)error; -+ (void)fetchAllInBackground:(NSArray *)objects - block:(LCArrayResultBlock)block; -``` - -The following code sets `isComplete` of all `Todo` objects to be `true`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query findObjectsInBackgroundWithBlock:^(NSArray *todos, NSError *error) { - // Get a collection of todos to work on - for (LCObject *todo in todos) { - // Update value - todo[@"isComplete"] = @(YES); - } - // Save all at once - [LCObject saveAllInBackground:todos]; -}]; -``` - -Although each function call sends multiple operations in one single network request, saving operations and fetching operations are billed as separate API calls for each object in the collection, while deleting operations are billed as a single API call. - -### Running in the Background - -You may have noticed from the examples above that we have been accessing the cloud asynchronously in our code. The methods with names like `xxxxInBackground` are provided for you to implement asynchronous calls so that your main thread will not be blocked. - -### Storing Data Locally - -Most of the operations for saving objects can be executed immediately and the program will be notified once the operation is done. However, if the program does not need to know when the operation is done, you can use `saveEventually` instead. - -The benefit of this function is that if the device is offline, `saveEventually` will cache the data locally and send them to the server once the device gets online again. If the app is closed before the device gets online, the SDK will try to send the data to the server when the app is opened again. - -It is safe to call `saveEventually` (or `deleteEventually`) multiple times since all the operations will be performed in the order they are initiated. - -### Data Models - -Objects may have relationships with other objects. For example, in a blogging application, a `Post` object may have relationships with many `Comment` objects. The Data Storage service supports three kinds of relationships, including one-to-one, one-to-many, and many-to-many. - -#### One-to-One and One-to-Many Relationships - -One-to-one and one-to-many relationships are modeled by saving `LCObject` as a value in the other object. For example, each `Comment` in a blogging app might correspond to one `Post`. - -The following code creates a new `Post` with a single `Comment`: - -```objc -// Create a post -LCObject *post = [[LCObject alloc] initWithClassName:@"Post"]; -[post setObject:@"I am starving!" forKey:@"title"]; -[post setObject:@"Hmmm, where should I go for lunch?" forKey:@"content"]; - -// Create a comment -LCObject *comment = [[LCObject alloc] initWithClassName:@"Comment"]; -[comment setObject:@"How about KFC?" forKey:@"content"]; - -// Add the post as a property of the comment -[comment setObject:post forKey:@"parent"]; - -// This will save both post and comment -[comment saveInBackground]; -``` - -Internally, the backend will store the referred-to object with the `Pointer` type in just one place in order to maintain consistency. You can also link objects using their `objectId`s like this: - -```objc -LCObject *post = [LCObject objectWithClassName:@"Post" objectId:@"57328ca079bc44005c2472d0"]; -[comment setObject:post forKey:@"post"]; -``` - -See [Relational Queries](#relational-queries) for instructions on how to query relational data. - -#### Many-to-Many Relationships - -The easiest way to model many-to-many relationships is to use **arrays**. In most cases, using arrays helps you reduce the number of queries you need to make and leads to better performance. However, if additional properties need to be attached to the relationships between two classes, using **join tables** would be a better choice. Keep in mind that the additional properties are used to describe the relationships between classes rather than any single class. - -We recommend you to use join tables if the total number of objects of any class exceeds 100. - -### Serialization and Deserialization - -If you need to pass an `LCObject` to a method as an argument, you may want to first serialize the object to avoid certain problems. You can use the following ways to serialize and deserialize `LCObject`s. - -Serialization: - -```objc -LCObject *todo = [[LCObject alloc] initWithClassName:@"Todo"]; // Create object -[todo setObject:@"Sign up for Marathon" forKey:@"title"]; // Set title -[todo setObject:@2 forKey:@"priority"]; // Set priority -[todo setObject:[LCUser currentUser] forKey:@"owner"]; // A Pointer pointing to the current user - -NSMutableDictionary *serializedJSONDictionary = [todo dictionaryForObject]; // Get serialized object as a dictionary -``` - -Deserialization: - -```objc -// Convert NSMutableDictionary to LCObject -LCObject *todo = [LCObject objectWithDictionary:serializedJSONDictionary]; -``` - -## Queries - -We've already seen how you can retrieve a single object from the cloud with `LCObject`, but it doesn't seem to be powerful enough when you need to retrieve multiple objects that match certain conditions at once. In such a situation, `LCQuery` would be a more efficient tool you can use. - -### Basic Queries - -The general steps of performing a basic query include: - -1. Creating `LCQuery`. -2. Putting conditions on it. -3. Retrieving an array of objects matching the conditions. - -The code below retrieves all `Student` objects whose `lastName` is `Smith`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Student"]; -[query whereKey:@"lastName" equalTo:@"Smith"]; -[query findObjectsInBackgroundWithBlock:^(NSArray *students, NSError *error) { - // students is an array of Student objects satisfying conditions -}]; -``` - -### Query Constraints - -There are several ways to put constraints on the objects found by `LCObject`. - -The code below filters out objects with `Jack` as `firstName`: - -```objc -[query whereKey:@"firstName" notEqualTo:@"Jack"]; -``` - -For sortable types like numbers and strings, you can use comparisons in queries: - -```objc -// Restricts to age < 18 -[query whereKey:@"age" lessThan:@18]; - -// Restricts to age <= 18 -[query whereKey:@"age" lessThanOrEqualTo:@18]; - -// Restricts to age > 18 -[query whereKey:@"age" greaterThan:@18]; - -// Restricts to age >= 18 -[query whereKey:@"age" greaterThanOrEqualTo:@18]; -``` - -You can apply multiple constraints to a single query, and objects will only be in the results if they match all of the constraints. In other words, it's like concatenating constraints with `AND`: - -```objc -[query whereKey:@"firstName" equalTo:@"Jack"]; -[query whereKey:@"age" greaterThan:@18]; -``` - -You can limit the number of results by setting `limit` (defaults to `100`): - -```objc -// Get at most 10 results -query.limit = 10; -``` - -For performance reasons, the maximum value allowed for `limit` is `1000`, meaning that the cloud would only return 1,000 results even if it is set to be greater than `1000`. - -If you need exactly one result, you may use `getFirstObject` for convenience: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"priority" equalTo:@2]; -[query getFirstObjectInBackgroundWithBlock:^(LCObject *todo, NSError *error) { - // todo is the first Todo object satisfying conditions -}]; -``` - -You can skip a certain number of results by setting `skip`: - -```objc -// Skip the first 20 results -query.skip = 20; -``` - -You can implement pagination in your app by using `skip` together with `limit`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"priority" equalTo:@2]; -query.limit = 10; -query.skip = 20; -``` - -Keep in mind that the higher the `skip` goes, the slower the query will run. You may consider using `createdAt` or `updatedAt` (which are indexed) to set range boundaries for large datasets to make queries more efficient. -You may also use the last value returned from an auto-increment field along with `limit` for pagination. - -For sortable types, you can control the order in which results are returned: - -```objc -// Sorts the results in ascending order by the createdAt property -[query orderByAscending:@"createdAt"]; - -// Sorts the results in descending order by the createdAt property -[query orderByDescending:@"createdAt"]; -``` - -You can even attach multiple sorting rules to a single query: - -```objc -[query addAscendingOrder:@"priority"]; -[query addDescendingOrder:@"createdAt"]; -``` - -To retrieve objects that have or do not have particular fields: - -```objc -// Finds objects that have the "images" field -[query whereKeyExists:@"images"]; - -// Finds objects that don't have the "images" field -[query whereKeyDoesNotExist:@"images"]; -``` - -You can restrict the fields returned by providing a list of keys with `selectKeys`. The code below retrieves todos with only the `title` and `content` fields (and also special built-in fields including `objectId`, `createdAt`, and `updatedAt`): - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query selectKeys:@[@"title", @"content"]]; -[query getFirstObjectInBackgroundWithBlock:^(LCObject *todo, NSError *error) { - NSString *title = todo[@"title"]; // √ - NSString *content = todo[@"content"]; // √ - NSString *notes = todo[@"notes"]; // An error will occur -}]; -``` - -You can add a minus prefix to the attribute name for inverted selection. -For example, if you do not care about the post author, use `-author`. -The inverted selection also applies to preserved attributes and can be used with dot notations, e.g., `-pubUser.createdAt`. - -The unselected fields can be fetched later with `fetchInBackgroundWithBlock`. See [Refreshing Objects](#refreshing-objects). - -### Queries on String Values - -Use `hasPrefix` to restrict to string values that start with a particular string. Similar to a `LIKE` operator in SQL, it is indexed so it is efficient for large datasets: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -// SQL equivalent: title LIKE 'lunch%' -[query whereKey:@"title" hasPrefix:@"lunch"]; -``` - -Use `containsString` to restrict to string values that contain a particular string: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -// SQL equivalent: title LIKE '%lunch%' -[query whereKey:@"title" containsString:@"lunch"]; -``` - -Unlike `hasPrefix`, `containsString` can't take advantage of indexes, so it is not encouraged to be used for large datasets. - -Please note that both `hasPrefix` and `containsString` perform **case-sensitive** matching, so the examples above will not look for string values containing `Lunch`, `LUNCH`, etc. - -If you are looking for string values that do not contain a particular string, use `matchesRegex` with regular expressions: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -// "title" without "ticket" (case-insensitive) -[query whereKey:@"title" matchesRegex:@"^((?!ticket).)*$", modifiers:"i"]; -``` - -However, performing queries with regular expressions as constraints can be very expensive, especially for classes with over 100,000 records. The reason behind this is that queries like this can't take advantage of indexes and will lead to exhaustive scanning of the whole dataset to find the matching objects. We recommend that you take a look at our In-App Searching feature, a full-text search solution we provide to improve your app's searching ability and user experience. - -If you are facing performance issues with queries, please refer to [Optimizing Performance](#optimizing-performance) for possible workarounds and best practices. - -### Queries on Array Values - -The code below looks for all the objects with `work` as an element of its array field `tags`: - -```objc -[query whereKey:@"tags" equalTo:@"work"]; -``` - -To look for objects whose array field `tags` contains three elements: - -```objc -[query whereKey:@"tags" sizeEqualTo:3]; -``` - -You can also look for objects whose array field `tags` contains `work`, `sales`, **and** `appointment`: - -```objc -[query whereKey:@"tags" containsAllObjectsInArray:[NSArray arrayWithObjects:@"work", @"sales", @"appointment", nil]]; -``` - -To retrieve objects whose field matches any one of the values in a given list, you can use `containedIn` instead of performing multiple queries. The code below constructs a query that retrieves todo items with `priority` to be `1` **or** `2`: - -```objc -// Single query -LCQuery *priorityOneOrTwo = [LCQuery queryWithClassName:@"Todo"]; -[priorityOneOrTwo whereKey:@"priority" containedIn:[NSArray arrayWithObjects:@1, @2, nil]]; -// Mission completed :) - -// --------------- -// vs. -// --------------- - -// Multiple queries -LCQuery *priorityOne = [LCQuery queryWithClassName:@"Todo"]; -[priorityOne whereKey:@"priority" equalTo:@1]; - -LCQuery *priorityTwo = [LCQuery queryWithClassName:@"Todo"]; -[priorityTwo whereKey:@"priority" equalTo:@2]; - -LCQuery *priorityOneOrTwo = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:priorityOne, priorityTwo, nil]]; -// Kind of verbose :( -``` - -Conversely, you can use `notContainedIn` if you want to retrieve objects that do not match any of the values in a list. - -### Relational Queries - -There are several ways to perform queries for relational data. To retrieve objects whose given field matches a particular `LCObject`, you can use `equalTo` just like how you use it for other data types. For example, if each `Comment` has a `Post` object in its `post` field, you can fetch all the comments for a particular `Post` with the following code: - -```objc -LCObject *post = [LCObject objectWithClassName:@"Post" objectId:@"57328ca079bc44005c2472d0"]; -LCQuery *query = [LCQuery queryWithClassName:@"Comment"]; -[query whereKey:@"post" equalTo:post]; -[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) { - // comments contains the comments for the post -}]; -``` - -To retrieve objects whose given field contains an `LCObject` that matches a different query, you can use `matchesQuery`. The code below constructs a query that looks for all the comments for posts with images: - -```objc -LCQuery *innerQuery = [LCQuery queryWithClassName:@"Post"]; -[innerQuery whereKeyExists:@"images"]; - -LCQuery *query = [LCQuery queryWithClassName:@"Comment"]; -[query whereKey:@"post" matchesQuery:innerQuery]; -``` - -To retrieve objects whose given field does not contain an `LCObject` that matches a different query, use `doesNotMatchQuery` instead. - -Sometimes you may need to look for related objects from different classes without extra queries. In such situations, you can use `includeKey` on the same query. The following code retrieves the last 10 comments together with the posts related to them: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Comment"]; - -// Retrieve the most recent ones -[query orderByDescending:@"createdAt"]; - -// Only retrieve the last 10 -query.limit = 10; - -// Include the related post together with each comment -[query includeKey:@"post"]; - -[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) { - // comments contains the last 10 comments including the post associated with each - for (LCObject *comment in comments) { - // This does not require a network access - LCObject *post = comment[@"post"]; - } -}]; -``` - -#### Caveats about Inner Queries - -The Data Storage service is not built on relational databases, which makes it impossible to join tables while querying. For the relational queries mentioned above, what we would do is to perform an inner query first (with `100` as the default `limit` and `1000` as the maximum) and then insert the result from this query into the outer query. If the number of records matching the inner query exceeds the `limit` and the outer query contains other constraints, the amount of the records returned in the end could be zero or less than your expectation since only the records within the `limit` would be inserted into the outer query. - -The following actions can be taken to solve the problem: - -- Make sure the number of records in the result of the inner query is no more than 100. If it is between 100 and 1,000, set `1000` as the `limit` of the inner query. -- Create redundancy for the fields being queried by the inner query on the table for the outer query. -- Repeat the same query with different `skip` values until all the records are gone through (performance issue could occur if the value of `skip` gets too big). - -### Counting Objects - -If you just need to count how many objects match a query but do not need to retrieve the actual objects, use `countObjectsInBackgroundWithBlock` instead of `findObjectsInBackgroundWithBlock`. For example, to count how many todos have been completed: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"isComplete" equalTo:@(YES)]; -[query countObjectsInBackgroundWithBlock:^(NSInteger count, NSError *error) { - NSLog(@"%ld todos completed.", count); -}]; -``` - -### Compound Queries - -Compound queries can be used if complex query conditions need to be specified. A compound query is a logical combination (`OR` or `AND`) of subqueries. - -Note that we do not support `GeoPoint` or non-filtering constraints (e.g. `near`, `withinGeoBox`, `limit`, `skip`, `ascending`, `descending`, `include`) in the subqueries of a compound query. - -#### OR-ed Query Constraints - -An object will be returned as long as it fulfills any one of the subqueries. The code below constructs a query that looks for all the todos that either have priorities higher than or equal to `3`, or are already completed: - -```objc -LCQuery *priorityQuery = [LCQuery queryWithClassName:@"Todo"]; -[priorityQuery whereKey:@"priority" greaterThanOrEqualTo:@3]; - -LCQuery *isCompleteQuery = [LCQuery queryWithClassName:@"Todo"]; -[isCompleteQuery whereKey:@"isComplete" equalTo:@(YES)]; - -LCQuery *query = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:priorityQuery, isCompleteQuery, nil]]; -``` - -Queries regarding `GeoPoint` cannot be present among OR-ed queries. - -#### AND-ed Query Constraints - -The effect of using AND-ed query is the same as adding constraints to `LCQuery`. The code below constructs a query that looks for all the todos that are created between `2016-11-13` and `2016-12-02`: - -```objc -NSDate *(^dateFromString)(NSString *string) = ^(NSString *string) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd"]; - return [dateFormatter dateFromString:string]; -}; - -LCQuery *startDateQuery = [LCQuery queryWithClassName:@"Todo"]; -[startDateQuery whereKey:@"createdAt" greaterThanOrEqualTo:dateFromString(@"2016-11-13")]; - -LCQuery *endDateQuery = [LCQuery queryWithClassName:@"Todo"]; -[endDateQuery whereKey:@"createdAt" lessThan:dateFromString(@"2016-12-03")]; - -LCQuery *query = [LCQuery andQueryWithSubqueries:[NSArray arrayWithObjects:startDateQuery, endDateQuery, nil]]; -``` - -While using an AND-ed query by itself doesn't bring anything new compared to a basic query, to combine two or more OR-ed queries, you have to use AND-ed queries: - -```objc -NSDate *(^dateFromString)(NSString *string) = ^(NSString *string) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd"]; - return [dateFormatter dateFromString:string]; -}; - -LCQuery *createdAtQuery = [LCQuery queryWithClassName:@"Todo"]; -[createdAtQuery whereKey:@"createdAt" greaterThanOrEqualTo:dateFromString(@"2018-04-30")]; -[createdAtQuery whereKey:@"createdAt" lessThan:dateFromString(@"2018-05-01")]; - -LCQuery *locationQuery = [LCQuery queryWithClassName:@"Todo"]; -[locationQuery whereKeyDoesNotExist:@"location"]; - -LCQuery *priority2Query = [LCQuery queryWithClassName:@"Todo"]; -[priority2Query whereKey:@"priority" equalTo:@2]; - -LCQuery *priority3Query = [LCQuery queryWithClassName:@"Todo"]; -[priority3Query whereKey:@"priority" equalTo:@3]; - -LCQuery *priorityQuery = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:priority2Query, priority3Query, nil]]; -LCQuery *timeLocationQuery = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:locationQuery, createdAtQuery, nil]]; -LCQuery *query = [LCQuery andQueryWithSubqueries:[NSArray arrayWithObjects:priorityQuery, timeLocationQuery, nil]]; -``` - -### Caching - -You can cache the results of some queries on the device so the app could still display some data to the user even if the device is offline or the user just opened the app and queries for fetching the latest data have not been made yet. The SDK will automatically clear the cache if it is taking up too much space. - -Caching is not enabled by default. You need to specify that you want to enable the cache when you make a query. The following example shows how you can make a request to query some data but use the cache on the device if the device is offline: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Post"]; -query.cachePolicy = kLCCachePolicyNetworkElseCache; - -// Set cache age -query.maxCacheAge = 24*3600; - -[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { - if (!error) { - // Got the results from the server, or the cache if the device is offline - } else { - // The device is offline and there is not cache found - } -}]; -``` - -#### Cache Policy - -The following cache policies are provided to fulfill different needs: - -| Name | Description | -| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `kLCCachePolicyIgnoreCache` | **(Default)** The query will not load the results from the cache or save the results into the cache. | -| `kLCCachePolicyCacheOnly` | Only load the results from the cache. If the results do not exist in the cache, an `LCError` will occur. | -| `kLCCachePolicyCacheElseNetwork` | Try to load the results from the cache. If the results do not exist, fetch the results from the server. If the SDK fails to connect to the server, an `LCError` will occur. Note that if you are running a query for the first time, the query will always try to load the results from the server. | -| `kLCCachePolicyNetworkElseCache` | Try to fetch the results from the server. If the SDK fails to connect to the server, load the results from the cache. If the results do not exist in the cache, an `LCError` will occur. | -| `kLCCachePolicyCacheThenNetwork` | Load the results from the cache and then load the results from the server. With this policy, the callback function will be called twice with the first time getting the results from the cache and the second time getting the results from the server. Since the callback function will get two different results, this policy should not be used with `findObjects`. | - -#### Cache-Related Operations - -- Check if the results exist in the cache: - - ```objc - BOOL isInCache = [query hasCachedResult]; - ``` - -- Delete the cache for a query (only delete the cache from the non-volatile storage (the disk) and not the volatile storage (the memory); same for the next operation): - - ```objc - [query clearCachedResult]; - ``` - -- Delete the cache for all queries: - - ```objc - [LCQuery clearAllCachedResults]; - ``` - -- Set the maximum age for the cache: - - ```objc - query.maxCacheAge = 60 * 60 * 24; // The number of seconds in a day - ``` - -You can use cache when calling `getFirstObject` and `getObjectInBackground` too. - -### Optimizing Performance - -There are several factors that could lead to potential performance issues when you conduct a query, especially when more than 100,000 records are returned at a time. We are listing some common ones here so you can design your apps accordingly to avoid them: - -- Querying with "not equal to" or "not include" (index will not work) -- Querying on strings with a wildcard at the beginning of the pattern (index will not work) -- Using `count` with conditions (all the entries will be gone through) -- Using `skip` for a large number of entries (all the entries that need to be skipped will be gone through) -- Sorting without an index (querying and sorting cannot share a composite index unless the conditions used on them are both covered by the same one) -- Querying without an index (the conditions used on the query cannot share a composite index unless all of them are covered by the same one; additional time will be consumed if excessive data falls under the uncovered conditions) - -## LiveQuery - -LiveQuery is, as its name implies, derived from [`LCQuery`](#queries) but has enhanced capability. It allows you to automatically synchronize data changes from one client to other clients without writing complex code, making it suitable for apps that need real-time data. - -Suppose you are building an app that allows multiple users to edit the same file at the same time. `LCQuery` would not be an ideal tool since it is based on a pull model and you cannot know when to query from the cloud to get the updates. - -To solve this problem, we introduced LiveQuery. This tool allows you to subscribe to the `LCQuery`s you are interested in. Once subscribed, the cloud will notify clients by generating event messages whenever `LCObject`s that match the `LCQuery` are created or updated, in real-time. - -Behind the scenes, we use WebSocket connections to have clients and the cloud communicate with each other and maintain the subscription status of clients. In most cases, it isn't necessary to deal with the WebSocket connections directly, so we developed a simple API to help you focus on your business logic rather than technical implementations. - -### Initializing LiveQuery - -To use LiveQuery in your app, go to ** > Settings** and check the **Enable LiveQuery** option under the **Security** section. - -If you have not integrated the `Realtime` module, make sure to integrate it first. To add the pod for it: - -```ruby -pod 'LeanCloudObjc/Realtime' -``` - -See [Installing SDK](#installing-sdk) for more details. - -### Demo - -We’ve made a demo app called “LeanTodo” which shows the functionality of LiveQuery. If you’d like to try it: - -1. Go to , enter a username and a password, and then hit “Signup”. -2. Open the same URL on a different device, enter the same credentials, and hit “Login”. -3. Create, edit, or delete some items on one device and watch what happens on the other one. - -### Creating a Subscription - -To make a query **live**, create an `LCQuery` object, put conditions on it if there are any, and then subscribe to events: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -self.liveQuery = [[LCLiveQuery alloc] initWithQuery:query]; -self.liveQuery.delegate = self; -[self.liveQuery subscribeWithCallback:^(BOOL succeeded, NSError * _Nonnull error) { - // Subscribed -}]; -``` - -You can't use subqueries or restrict fields being returned when using LiveQuery. - -Now you will be able to receive updates related to `LCObject`. If a `Todo` object is created by another client with `Update Portfolio` as `title`, the following code can get the new `Todo` for you: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -self.liveQuery = [[LCLiveQuery alloc] initWithQuery:query]; -self.liveQuery.delegate = self; -[self.liveQuery subscribeWithCallback:^(BOOL succeeded, NSError * _Nonnull error) { - // Subscribed -}]; -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidCreate:(id)object { - if (liveQuery == self.liveQuery) { - NSLog(@"%@", object[@"title"]); // Update Portfolio - } -} -``` - -If someone updates this `Todo` by changing its `content` to `Add my recent paintings`, the following code can get the updated version for you: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidUpdate:(id)updatedTodo updatedKeys:(NSArray *)updatedKeys { - NSLog(@"%@", updatedTodo[@"content"]); // Add my recent paintings -} -``` - -### Event Handling - -The following types of data changes can be monitored once a subscription is set up: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` Event - -A `create` event will be triggered when a new `LCObject` is created and fulfills the `LCQuery` you subscribed to. The `object` is the new `LCObject` being created: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidCreate:(id)object { - if (liveQuery == self.liveQuery) { - NSLog(@"Object created."); - } -} -``` - -#### `update` Event - -An `update` event will be triggered when an existing `LCObject` fulfilling the `LCQuery` you subscribed to is updated. The `object` is the `LCObject` being updated: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidUpdate:(id)object updatedKeys:(NSArray *)updatedKeys { - if (liveQuery == self.liveQuery) { - NSLog(@"Object updated."); - } -} -``` - -#### `enter` Event - -An `enter` event will be triggered when an existing `LCObject`'s old value does not fulfill the `LCQuery` you subscribed to but its new value does. The `object` is the `LCObject` entering the `LCQuery` and its content is the latest value of it: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidEnter:(id)object updatedKeys:(nonnull NSArray *)updatedKeys { - if (liveQuery == self.liveQuery) { - NSLog(@"Object entered."); - } -} -``` - -There is a difference between a `create` event and an `enter` event. If an object already exists and later matches the query's conditions, an `enter` event will be triggered. If an object didn't exist already and is later created, a `create` event will be triggered. - -#### `leave` Event - -A `leave` event will be triggered when an existing `LCObject`'s old value fulfills the `LCQuery` you subscribed to but its new value does not. The `object` is the `LCObject` leaving the `LCQuery` and its content is the latest value of it: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidLeave:(id)object updatedKeys:(nonnull NSArray *)updatedKeys { - if (liveQuery == self.liveQuery) { - NSLog(@"Object left."); - } -} -``` - -#### `delete` Event - -A `delete` event will be triggered when an existing `LCObject` fulfilling the `LCQuery` you subscribed to is deleted. The `object` is the `objectId` of the `LCObject` being deleted: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidDelete:(id)object { - if (liveQuery == self.liveQuery) { - NSLog(@"Object deleted."); - } -} -``` - -### Unsubscribing - -You can cancel a subscription to stop receiving events regarding `LCQuery`. After that, you won't get any events from the subscription. - -```objc -[liveQuery unsubscribeWithCallback:^(BOOL succeeded, NSError * _Nonnull error) { - if (succeeded) { - // Successfully unsubscribed - } else { - // Error handling - } -}]; -``` - -### Losing Connections - -There are different scenarios regarding losing connections: - -1. The connection to the Internet is lost unexpectedly. -2. The user performs certain operations outside of the app, like switching the app to the background, turning off the phone, or turning on the flight mode. - -For the scenarios above, you don't need to do any extra work. As long as the user switches back to the app, the SDK will automatically re-establish the connection. - -There is another scenario when **the user completely kills the app or closes the web page**. In this case, the SDK cannot automatically re-establish the connection. You will have to create subscriptions again by yourself. - -### Caveats about LiveQuery - -Given the real-time feature of LiveQuery, developers may find it tempting to use it for instant messaging. As LiveQuery is neither designed nor optimized for completing such tasks, we discourage such use of this tool, let alone there will be an additional cost for saving message history and rising challenges of code maintenance. We recommend using our Instant Messaging service for this scenario. - -## Files - -`LCFile` allows you to store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `LCObject`. The most common use case is storing images, but you can also use it for documents, videos, music, and any other binary data. - -### Creating Files - -You can create a file from a string: - -```objc -NSData *data = [@"LeanCloud" dataUsingEncoding:NSUTF8StringEncoding]; -// resume.txt is the file name -LCFile *file = [LCFile fileWithData:data name:@"resume.txt"]; -``` - -You can also create a file from a URL: - -```objc -LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png"]]; -``` - -When creating files from URLs, the SDK will not upload the actual files into the cloud but will store the addresses of the files as strings. This will not lead to actual traffic for uploading files, as opposed to creating files in other ways by doing which the files will be actually stored into the cloud. - -But the most common method for creating files is to upload them from local paths: - -```objc -NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); -NSString *documentsDirectory = [paths objectAtIndex:0]; -NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:@"avatar.jpg"]; -NSError *error; -LCFile *file = [LCFile fileWithLocalPath:imagePath error:&error]; -``` - -The file we uploaded here is named `avatar.jpg`. There are a couple of things to note here: - -- Each file uploaded will get its unique `objectId`, so it is allowed for multiple files to share the same name. -- A correct extension needs to be assigned to each file which the cloud will use to infer the type of a file. For example, if you are storing a PNG image with `LCFile`, use `.png` as its extension. -- If the file doesn't have an extension and the content type is not specified, the file will get the default type `application/octet-stream`. - -### Saving Files - -By saving a file, you store it into the cloud and get a permanent URL pointing to it: - -```objc -[file uploadWithCompletionHandler:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"File saved. URL: %@", file.url); - } else { - // The file either could not be read or could not be saved to the cloud - } -}]; -``` - -A file successfully uploaded can be found in ** > Files** and cannot be modified later. If you need to change the file, you have to upload the modified file again and a new `objectId` and URL will be generated. - -You can associate a file with `LCObject` after it has been saved: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; -[todo setObject:@"Buy Cakes" forKey:@"title"]; -// The type of attachments is Array -[todo addObject:file forKey:@"attachments"]; -[todo saveInBackground]; -``` - -You can also construct an `LCQuery` to query files: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"_File"]; -``` - -Note that the `url` field of internal files (files uploaded to the file service) is dynamically generated by the cloud, which will switch custom domain names automatically. -Therefore, querying files by the `url` field is only applicable to external files (files created by saving the external URL directly to the `_File` table). -Query internal files by the `key` field (path in URL) instead. - -On a related note, if the files are referenced in an array field of `LCObject` and you want to get them within the same query for `LCObject`, you need to use the `includeKey` method with `LCQuery`. For example, if you are retrieving all the todos with the same title `Buy Cakes` and you want to retrieve their related attachments at the same time: - -```objc -// Get all todos with the same title and contain attachments -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"title" equalTo:@"Buy Cakes"]; -[query whereKeyExists:@"attachments"]; - -// Include attachments with each todo -[query includeKey:@"attachments"]; - -[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable todos, NSError * _Nullable error) { - for (LCObject *todo in todos) { - // Get the attachments array for each todo - } -}]; -``` - -### Upload Progress - -You can monitor the upload progress and display that to the user: - -```objc -[file uploadWithProgress:^(NSInteger percent) { - // percent is an integer between 0 and 100, indicating the progress -} completionHandler:^(BOOL succeeded, NSError *error) { - // Things to do after saving -}]; -``` - -### File Metadata - -When uploading a file, you can attach additional properties to it with `metaData`. A file's `metaData` cannot be updated once the file is stored to the cloud. - -```objc -// Set metadata -[file.metaData setObject:@"LeanCloud" forKey:@"author"]; -[file uploadWithCompletionHandler:^(BOOL succeeded, NSError *error) { - // Get all metadata - NSDictionary *metadata = file.metaData; - // Get author - NSString *author = metadata[@"author"]; - // Get file name - NSString *fileName = file.name; - // Get size (not available for files created from base64-encoded strings or URLs) - NSUInteger *size = file.size; -}]; -``` - -### Image Thumbnails - -After saving an image, you can get the URL of a thumbnail of the image beside that of the image itself. You can even specify the width and height of the thumbnail: - -```objc -LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"file-url"]]; -[file getThumbnail:YES width:100 height:100 withBlock:^(UIImage *image, NSError *error) { - // Other things to do -}]; -``` - -You can only get thumbnails for images smaller than **20 MB**. - -### Downloading Files - -You can download files with the SDK and cache them locally. As long as the URL of a file does not change, the file will not be downloaded for a second time。 - -```objc -[file downloadWithProgress:^(NSInteger number) { - // number is an integer between 0 and 100, indicating the progress -} completionHandler:^(NSURL * _Nullable filePath, NSError * _Nullable error) { - // filePath is the path of the downloaded file -}]; -``` - -`filePath` is a relative path. The file will be either in the cache directory (if cache is enabled) or the temp directory (if cache is disabled). - -### Clearing Cache - -You can clear the cache for files at any time: - -```objc -// Clear the current file from the cache -- (void)clearPersistentCache; - -// Clear all files from the cache -+ (BOOL)clearAllPersistentCache; -``` - -### Deleting Files - -The code below deletes a file from the cloud: - -```objc -LCFile *file = [LCFile getFileWithObjectId:@"552e0a27e4b0643b709e891e"]; -[file deleteInBackground]; -``` - -By default, a file is not allowed to be deleted. -We recommend you delete files by accessing our REST API with the Master Key. -You can also allow certain users and roles to delete files by going to ** > Files > Permission**. - -### File Censorship - -The **censorship** feature allows you to censor **image** files stored on the cloud. - -You can **Enable automatic content censor for subsequent uploaded pictures** by going to **Data Storage > Files > Censorship**. You can also batch-censor all the images uploaded during a specific time scope. You can view the results of the censorship under the **Files** tab. - -You can manually **Pass** or **Block** images even if they have gone through automatic censorship. - -### HTTP Support for iOS 9 and Up - -Starting iOS 9, Apple requires HTTPS connections for iOS apps and denies HTTP connections by default. All the APIs support HTTPS except for the `getData` method of `LCFile`. - -If your app still needs to make HTTP requests, such as when accessing files in the Instant Messaging service that still reference insecure domains, you should add those insecure domains to your project's `Info.plist`: - -Right-click on `Info.plist`, choose **Opened As** > **Source Code**, append the following text to the node **plist** > **dict**: - -```xml -NSAppTransportSecurity - - NSExceptionDomains - - clouddn.com - - NSIncludesSubdomains - - NSTemporaryExceptionAllowsInsecureHTTPLoads - - - - -``` - -## GeoPoints - -You can associate real-world latitude and longitude coordinates with an object by adding an `LCGeoPoint` to the `LCObject`. By doing so, queries on the proximity of an object to a given point can be performed, allowing you to implement functions like looking for users or places nearby easily. - -To associate a point with an object, you need to create the point first. The code below creates an `LCGeoPoint` with `39.9` as `latitude` and `116.4` as `longitude`: - -```objc -LCGeoPoint *point = [LCGeoPoint geoPointWithLatitude:39.9 longitude:116.4]; -``` - -Now you can store the point into an object as a regular field: - -```objc -[todo setObject:point forKey:@"location"]; -``` - -### Geo Queries - -With a number of existing objects with spatial coordinates, you can find out which of them are closest to a given point, or are contained within a particular area. This can be done by adding another restriction to `LCQuery` using `nearGeoPoint`. The code below returns a list of `Todo` objects with `location` closest to a given point: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -LCGeoPoint *point = [LCGeoPoint geoPointWithLatitude:39.9 longitude:116.4]; -[query whereKey:@"location" nearGeoPoint:point]; - -// Limit to 10 results -query.limit = 10; -[query findObjectsInBackgroundWithBlock:^(NSArray *todos, NSError *error) { - // todos is an array of Todo objects satisfying conditions -}]; -``` - -Additional sorting conditions like `orderByAscending` and `orderByDescending` will gain higher priorities than the default order by distance. - -To have the results limited within a certain distance, check out `withinKilometers`, `withinMiles`, and `withinRadians` in our API docs. - -You can also query for the set of objects that are contained within a rectangular bounding box with `withinGeoBoxFromSouthwest` and `toNortheast`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -LCGeoPoint *southwest = [LCGeoPoint geoPointWithLatitude:30 longitude:115]; -LCGeoPoint *northeast = [LCGeoPoint geoPointWithLatitude:40 longitude:118]; -[query whereKey:@"location" withinGeoBoxFromSouthwest:southwest toNortheast:northeast]; -``` - -### Caveats about GeoPoints - -Points should not exceed the extreme ends of the ranges. Latitude should be between `-90.0` and `90.0`. Longitude should be between `-180.0` and `180.0`. Attempting to set latitude or longitude out of bounds will cause an error. -Also, each `LCObject` can only have one field for `LCGeoPoint`. - -## Users - -See [TDS Authentication Guide](/sdk/authentication/guide/). - -## Roles - -As your app grows in scope and user base, you may find yourself needing more coarse-grained control over access to pieces of your data than user-linked ACLs can provide. To address this requirement, we support a form of role-based access control. Check the detailed [ACL Guide](/sdk/storage/guide/acl/) to learn how to set it up for your objects. - -## Full-Text Search - -Full-Text Search offers a better way to search through the information contained within your app. It's built with search engine capabilities that you can easily tap into your app. Effective and useful searching functionality in your app is crucial for helping users find what they need. For more details, see [Full-Text Search Guide](/sdk/storage/guide/fulltext-search/). diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/objc-guide/setup-objc.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/objc-guide/setup-objc.mdx deleted file mode 100644 index ee5cb7586..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/objc-guide/setup-objc.mdx +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: Installing Objective-C SDK for Data Storage and Instant Messaging -sidebar_label: Installing Objective-C SDK -slug: /sdk/storage/guide/setup-objc/ -sidebar_position: 5 ---- - -import DomainBinding from "../../_partials/setup-domain.mdx"; -import AppConfig from "../_partials/app-config.mdx"; -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -## Installing SDK - -There are several ways for you to install our SDK and the most convenient one is to use a package manager. - -### Installing with Package Manager - -The easiest way to integrate our SDK into your project is to use [CocoaPods](https://cocoapods.org). - -Make sure you already have the latest version of `pod` on your computer. If not, please check out the [INSTALL](https://cocoapods.org) section on CocoaPods’ website. - -Then, run the following command under the root directory of your project to generate the `Podfile`: - -```sh -$ pod init -``` - -Following the [GET STARTED](https://cocoapods.org) section on CocoaPods’ website, add the following pod dependency into the `target` of the `Podfile`: - -```ruby -pod 'LeanCloudObjc' # A module for all the services -``` - -`LeanCloudObjc` contains multiple Subspecs. You can choose to only add the ones you need: - -```ruby -pod 'LeanCloudObjc/Foundation' # Basic services including Data Storage, SMS, Push Notification, and Cloud Engine -pod 'LeanCloudObjc/Realtime' # Instant Messaging and LiveQuery -``` - -The last step is to run the following command to integrate the latest SDK: - -```sh -$ pod update -``` - -You can also run: - -```sh -$ pod install --repo-update -``` - -After installing the SDK, you can open your project with `.xcworkspace` under the root directory of your project. - -### Installing Manually - -First of all, [download](https://releases.leanapp.cn/#/leancloud/objc-sdk/releases) the latest source code of the SDK. - -Then drag and drop the `AVOS`/`AVOS.xcodeproj` project file into the project as a subproject: - -![AVOS.xcodeproj will appear under the root directory of the project.](/img/quick_start/ios/subproject.png) - -Now connect the dependencies by going to **xcodeproj > target > general > frameworks** and adding the following content: - -![LeanCloudObjc.framework](/img/quick_start/ios/link-binary.png) - -That’s it. You are now ready to use our SDK in your project. - -## Initializing Your Project - -Import the basic modules into `AppDelegate`: - -```objc -#import -``` - -Then configure the `App ID`, `App Key`, and the server URL under the `application:didFinishLaunchingWithOptions:` method: - - - -```objc -[LCApplication setApplicationId:@"your-client-id" - clientKey:@"your-client-token" - serverURLString:@"https://your_server_url"]; -``` - - - - - -```objc -[LCApplication setApplicationId:@"your-app-id" - clientKey:@"your-app-key" - serverURLString:@"https://your_server_url"]; -``` - - - -Before using the APIs provided by the SDK, make sure you have initialized your application with the App ID, App Key, and server URL. - -### Credentials - - - -## Domain - - - -## Enabling Debug Logs - -You can easily trace the problems in your project by turning debug logs on during the development phase. Once enabled, details of every request made by the SDK along with errors will be output to your IDE, your browser console, or your Cloud Engine instances’ logs. - -```objc -// Run before initializing the Application -[LCApplication setAllLogsEnabled:true]; -``` - -:::caution -Make sure debug logs are turned off before your app is published. Failure to do so may lead to the exposure of sensitive data. -::: - -## Verifying - -First of all, make sure you are able to connect to the server from your computer. You can test it by running the following command: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` is the custom API domain. - -If everything goes well, it will return the current date: - -```json -{ "__type": "Date", "iso": "2020-10-12T06:46:56.000Z" } -``` - -Now let’s try saving an object to the cloud. Add the following code into the `viewDidLoad` method or any other method that will be called when the app starts: - -```objc -LCObject *testObject = [LCObject objectWithClassName:@"TestObject"]; -[testObject setObject:@"Hello world!" forKey:@"words"]; -[testObject save]; -``` - -Hit `Run` to start debugging. This can be done in either a host or a virtual machine. - -Then go to ** > Data > `TestObject`**. If you see a row with its `words` column being `Hello world!`, it means that you have correctly installed the SDK. - -See [Debugging](#Debugging) if you’re not seeing the content. - -## Debugging - -This guide is written for the latest version of our SDK. If you encounter any errors, please first make sure you have the latest version installed. - -### `401 Unauthorized` - -If you get a `401` error or see the following content in network logs: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -It means that the App ID or App Key might be incorrect or don’t match. If you have multiple apps, you might have used the App ID of one app with the App Key of another one, which will lead to such an error. - -### The Client Cannot Access the Internet - -Make sure you have granted the required permissions to your mobile app. diff --git a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/security.mdx b/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/security.mdx deleted file mode 100644 index 93df0d09d..000000000 --- a/leancloud/i18n/en/docusaurus-plugin-content-docs/current/sdk/storage/security.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: Data Security -slug: /sdk/storage/guide/security/ -sidebar_position: 10 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import Path from "/src/docComponents/path"; - -As a secure serverless platform you can count on, TDS takes data security seriously. -All API requests sent to TDS are encrypted by SSL and strictly verified for authentication and authorization. - -## Authentication - -On the client side, accessing the API requires the AppID and the AppKey of the application. -Requests from trusted environments such as Cloud Engine or your own servers can use the AppID and the MasterKey instead. -The Android SDK supports an additional authentication mechanism using the AppID only. - -Using MasterKey to access the API skips all access controls, so make sure your MasterKey is never leaked. - -When accessing the API from the client side, the following security measures are applied: - -1. All connections between the client side and the server side are HTTPS-encrypted. -2. When accessing the API through an SDK, the SDK will not include the AppKey in the HTTP header, but a signature string generated by the client based on the AppKey and the request initiation time. You can also use [this approach](/sdk/storage/guide/rest#x-lc-sign)[this approach](https://docs.leancloud.app/en/sdk/storage/guide/rest/) when sending requests to our REST API directly without SDKs. - -HTTPS-encrypted communications can avoid leaking data and the AppKey to attackers in the middle. -However, it does not prevent an attacker from intercepting the transmission at the client side. -Verification signature string can avoid leaking AppKey during transmission, -but does not prevent an attacker from acquiring the AppKey via reverse-compiling the application package. -The Android SDK supports authentication with the AppID only. -A closed source native library shipped with the Android SDK will sign the requests automatically based on the application certificate provided by you. -This avoids exposing the AppKey of the application and significantly increases the difficulties of reverse engineering. -However, to fully ensure data security, you still need to utilize access control settings to restrict data access. - -## Authorization - -You can configure access permissions at three different levels: - -- Class (table) -- Field (column) -- Object (record) - -### Class - -You can configure access permissions for the entire class/table on the dashboard. - -When creating a new class on the dashboard, you can configure its access permissions in the dialog box. - -![Create class dialog box](/img/io/security/class-permissions.png) - -- `add_fields`: When saving an object, if it contains non-existent fields, this option determines whether they can be created automatically. If you have created all the fields needed on the dashboard, you’d better disallow anyone to add fields. -- `create`: This option determines whether a new object can be saved. For example, you can only allow registered users or specific users to create a new object. -- `delete`: This option determines whether deleting an object is allowed. -- `update`: This option determines whether updating an existing object is allowed. -- `find`: This option determines whether querying objects according to specific conditions is allowed. If you disallow this, you can only retrieve an object with its objectId (assuming the `get` permission is allowed). -- `get`: This option determines whether retrieving an object with its objectId is allowed. - -You can assign each permission to different users: - -- All users: Here “users” refer to clients, not users in the built-in user system of TDS Authentication. In other words, all requests, including those without a session token or with an invalid session token, are permitted. -- Logged-in users: Only requests with a valid session token are permitted. -- Designated users: Only requests with a session token belonging to certain users are permitted. You can specify these users by filling in their usernames, the `objectId`s of them, the names of the roles they have, or the `objectId`s of these roles. If you don’t provide any users or roles, no one will get the permission. - -Here is an example: if we have an app that allows users to make posts anonymously, but only those reviewed by the admin can be publicly displayed, we can create two tables for the purpose. The first table stores all the unreviewed posts with its `create` permission given to all users. The second table stores reviewed posts with its `create` permission given to admins only. - -After a class has been created, you can still modify its permission settings. -To do so, simply go to ** > Data**, select the class, and click the **Permission** tab. - -### Field - -You can also configure the access permissions of each field. -To do so, go to ** > Data**, select a class, click the dropdown arrow of a field, and select **Edit**. - -1. **Read only**: This option determines whether the client side is allowed to update this field. -2. **Hide from clients**: This option determines whether this field is included in the result returned to the client side. For example, a pseudo-anonymous application may still store the author of each post, but it is invisible to the client side. - -### Object - -Every object has a special **ACL** field. -ACL, a.k.a. _Access Control List_, allows for specifying access permission of a certain object. -This is the finest level of access permission control. - -Please refer to [The ACL Guide](/sdk/storage/guide/acl/) for details. - -## Security Settings - - - -You can configure security settings on **Dashboard > Settings > Security**. By turning off the services you don’t use, you can prevent the resources in your app from being abused by those who have stolen your AppId and AppKey. - -You can find the switches of some sub-services on the corresponding settings pages. For example: - - - -You can turn on/off LiveQuery by going to ** > Settings > Security**. - -For security reasons, you man only want your app to send push notifications to users from the server side. There is an option under ** > Settings** called **Prevent clients from sending push notifications**. When this option is enabled, you can only send push notifications by accessing the REST API with the MasterKey or by going to ** > Send notifications**. - - - -**Dashboard > SMS > Settings > Enable SMS verification code**: When this option is disabled, you can only invoke `requestSmsCode` with MasterKey. Be aware that user-related short messages are not controlled by this option. When this option is disabled, the client-side can still invoke SMS interfaces related to users. - - - -You can disallow clients to create classes by going to ** > Settings > Security** and enabling **Disallow clients to create classes**. If you prefer to design the data structure of your application beforehand, it is recommended that you tick this option and create all the classes needed on the dashboard. If you prefer to dive in directly and refine the data structure during the development of your application’s prototype, it is recommended that you do not tick this option during the initial development stage, and tick this option before deploying your application to production. - -There is an option under ** > Settings > Queries** called **Check ACL when querying included Pointer data**. This option is enabled by default. It is recommended that all applications keep this option enabled. - -There are some user-related security settings under ** > Settings**. Here you can choose whether to require a user to provide their old password when setting a new password, whether to log out a user when their password is updated, and whether to validate the access token when a user logs in with a third-party account. - -You can specify the types of files users can upload by going to ** > Files > Settings**. - - - -#### Web Secure Domains - -Web secure domains can be used to restrict request origins, preventing others from abusing the resources of your servers. - -The web secure domains should be configured following the domain security policies of the browsers. Subdomains need to be listed explicitly and wildcards are not supported. Protocols and port numbers must match exactly (you can omit the port number if the default one is used; for example, `https://example.com` is equivalent to `https://example.com:443`). - -Note that Cloud Functions are not restricted by web secure domains. Also, for the convenience of debugging, requests from localhost are always permitted. - -Web secure domains setting is meant to prevent attackers from deploying client-side code to abuse the resources of your application. It cannot prevent attackers from forging data since attackers can still access your application by manipulating their host configuration file. - -#### Operation Logs - -**Dashboard > Settings > Logs** displays important operations by the creator and collaborators of the application, like data deletions (including the operator, IP, and time). You can use these logs to audit operations. - -#### Automatic Backup and Restore - -The data of applications with Business Plans will be backed up daily and retained for 7 days. You can restore the data on **Dashboard > Data Storage > Import/export > Data recovery**, where you can refine the scope of restoration to classes or objects specified. This feature is only available to apps with Business Plans. - -Notice that: - -- You cannot restore data for applications with Developer Plans. -- Files cannot be restored once they are deleted. -- The restoration process will only restore the objects that have been deleted. To restore the objects that have been modified, please make a backup of the existing objects, delete them from the cloud, and then start the restoration process. -- To restore a deleted class, please first create an empty class with the same name and add the corresponding columns to it. - -You can also [export your application data yourself](https://docs.leancloud.app/en/sdk/storage/guide/rest/) and make a copy of them to your servers (or upload them to third-party services). This feature is available to all applications. - - diff --git a/leancloud/i18n/en/docusaurus-theme-classic/navbar.json b/leancloud/i18n/en/docusaurus-theme-classic/navbar.json deleted file mode 100644 index d72d1bd67..000000000 --- a/leancloud/i18n/en/docusaurus-theme-classic/navbar.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "title": { - "message": "TapSDK Doc", - "description": "The title in the navbar" - }, - "item.label.文档首页": { - "message": "Home", - "description": "Navbar item with label 文档首页" - }, - "item.label.游戏商店": { - "message": "Store Settings", - "description": "Navbar item with label 游戏商店" - }, - "item.label.游戏服务": { - "message": "SDK Features", - "description": "Navbar item with label 游戏服务" - }, - "item.label.下载": { - "message": "Download", - "description": "Navbar item with label 下载" - }, - "item.label.设计资源": { - "message": "Design Resources", - "description": "Navbar item with label 设计资源" - }, - "item.label.SDK 工具包": { - "message": "SDK Toolkits", - "description": "Navbar item with label SDK 工具包" - }, - "item.label.云课堂": { - "message": "Learn", - "description": "Navbar item with label 设计资源" - }, - "item.label.资源": { - "message": "Downloads", - "description": "Navbar item with label 下载" - }, - "item.label.API 文档": { - "message": "API", - "description": "Navbar item with label 下载" - } -} diff --git a/leancloud/i18n/zh-Hans/code.json b/leancloud/i18n/zh-Hans/code.json deleted file mode 100644 index 02294aa48..000000000 --- a/leancloud/i18n/zh-Hans/code.json +++ /dev/null @@ -1,313 +0,0 @@ -{ - "exchange.currency": { - "message": "币种" - }, - "exchange.currencyType": { - "message": "货币类型(currency_type)" - }, - "exchange.currentRate": { - "message": "实时汇率(本位币:美元 USD)" - }, - "exchange.commonCurrent": { - "message": "常用货币" - }, - "tds-footer-来-Discord-和我们交流": { - "message": "来 Discord 和我们交流", - "description": "from Footer" - }, - "tds-footer-易玩(上海)网络科技有限公司": { - "message": "易玩(上海)网络科技有限公司", - "description": "from Footer" - }, - "tds-footer-公司地址:上海市静安区灵石路 718 号 B1 北楼": { - "message": "公司地址:上海市静安区灵石路 718 号 B1 北楼", - "description": "from Footer" - }, - "tds-footer-注册地址:上海市闵行区紫星路 588 号 2 幢 2122 室": { - "message": "注册地址:上海市闵行区紫星路 588 号 2 幢 2122 室", - "description": "from Footer" - }, - "tds-header-开发者中心": { - "message": "开发者服务", - "description": "from Header" - }, - "tds.search.search": { - "message": "搜索文档" - }, - "tds-home-入门指南": { - "message": "入门指南" - }, - "tds.search.recent": { - "message": "最近看过" - }, - "tds.search.removeItem": { - "message": "删除该项目" - }, - "tds.search.clearQuery": { - "message": "清除搜索词" - }, - "tds.search.cancel": { - "message": "取消" - }, - "tds.search.noHistory": { - "message": "无搜索记录" - }, - "tds.search.noResults": { - "message": "找不到匹配结果" - }, - "theme.ErrorPageContent.title": { - "message": "页面已崩溃。", - "description": "The title of the fallback page when the page crashed" - }, - "theme.ErrorPageContent.tryAgain": { - "message": "重试", - "description": "The label of the button to try again when the page crashed" - }, - "theme.NotFound.title": { - "message": "找不到页面", - "description": "The title of the 404 page" - }, - "theme.NotFound.p1": { - "message": "我们找不到您要找的页面。", - "description": "The first paragraph of the 404 page" - }, - "theme.NotFound.p2": { - "message": "请联系原始链接来源网站的所有者,并告知他们链接已损坏。", - "description": "The 2nd paragraph of the 404 page" - }, - "theme.admonition.note": { - "message": "备注", - "description": "The default label used for the Note admonition (:::note)" - }, - "theme.admonition.tip": { - "message": "提示", - "description": "The default label used for the Tip admonition (:::tip)" - }, - "theme.admonition.danger": { - "message": "危险", - "description": "The default label used for the Danger admonition (:::danger)" - }, - "theme.admonition.info": { - "message": "信息", - "description": "The default label used for the Info admonition (:::info)" - }, - "theme.admonition.caution": { - "message": "警告", - "description": "The default label used for the Caution admonition (:::caution)" - }, - "theme.BackToTopButton.buttonAriaLabel": { - "message": "回到顶部", - "description": "The ARIA label for the back to top button" - }, - "theme.blog.archive.title": { - "message": "历史博文", - "description": "The page & hero title of the blog archive page" - }, - "theme.blog.archive.description": { - "message": "历史博文", - "description": "The page & hero description of the blog archive page" - }, - "theme.blog.paginator.navAriaLabel": { - "message": "博文列表分页导航", - "description": "The ARIA label for the blog pagination" - }, - "theme.blog.paginator.newerEntries": { - "message": "较新的博文", - "description": "The label used to navigate to the newer blog posts page (previous page)" - }, - "theme.blog.paginator.olderEntries": { - "message": "较旧的博文", - "description": "The label used to navigate to the older blog posts page (next page)" - }, - "theme.blog.post.paginator.navAriaLabel": { - "message": "博文分页导航", - "description": "The ARIA label for the blog posts pagination" - }, - "theme.blog.post.paginator.newerPost": { - "message": "较新一篇", - "description": "The blog post button label to navigate to the newer/previous post" - }, - "theme.blog.post.paginator.olderPost": { - "message": "较旧一篇", - "description": "The blog post button label to navigate to the older/next post" - }, - "theme.blog.post.plurals": { - "message": "{count} 篇博文", - "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" - }, - "theme.blog.tagTitle": { - "message": "{nPosts} 含有标签「{tagName}」", - "description": "The title of the page for a blog tag" - }, - "theme.tags.tagsPageLink": { - "message": "查看所有标签", - "description": "The label of the link targeting the tag list page" - }, - "theme.colorToggle.ariaLabel": { - "message": "切换浅色/暗黑模式(当前为{mode})", - "description": "The ARIA label for the navbar color mode toggle" - }, - "theme.colorToggle.ariaLabel.mode.dark": { - "message": "暗黑模式", - "description": "The name for the dark color mode" - }, - "theme.colorToggle.ariaLabel.mode.light": { - "message": "浅色模式", - "description": "The name for the light color mode" - }, - "theme.docs.breadcrumbs.home": { - "message": "主页面", - "description": "The ARIA label for the home page in the breadcrumbs" - }, - "theme.docs.breadcrumbs.navAriaLabel": { - "message": "页面路径", - "description": "The ARIA label for the breadcrumbs" - }, - "theme.docs.DocCard.categoryDescription": { - "message": "{count} 个项目", - "description": "The default description for a category card in the generated index about how many items this category includes" - }, - "theme.docs.paginator.navAriaLabel": { - "message": "文档分页导航", - "description": "The ARIA label for the docs pagination" - }, - "theme.docs.paginator.previous": { - "message": "上一页", - "description": "The label used to navigate to the previous doc" - }, - "theme.docs.paginator.next": { - "message": "下一页", - "description": "The label used to navigate to the next doc" - }, - "theme.docs.tagDocListPageTitle.nDocsTagged": { - "message": "{count} 篇文档带有标签", - "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" - }, - "theme.docs.tagDocListPageTitle": { - "message": "{nDocsTagged}「{tagName}」", - "description": "The title of the page for a docs tag" - }, - "theme.docs.versionBadge.label": { - "message": "版本:{versionLabel}" - }, - "theme.docs.versions.unreleasedVersionLabel": { - "message": "此为 {siteTitle} {versionLabel} 版尚未发行的文档。", - "description": "The label used to tell the user that he's browsing an unreleased doc version" - }, - "theme.docs.versions.unmaintainedVersionLabel": { - "message": "此为 {siteTitle} {versionLabel} 版的文档,为了更好的服务开发者,2023 年 12 月 01 日起不再支持新的开发者接入该版接口。", - "description": "The label used to tell the user that he's browsing an unmaintained doc version" - }, - "theme.docs.versions.latestVersionSuggestionLabel": { - "message": "线上正式文档请参阅 {latestVersionLink} ({versionLabel})。", - "description": "The label used to tell the user to check the latest version" - }, - "theme.docs.versions.latestVersionLinkLabel": { - "message": "最新版本", - "description": "The label used for the latest version suggestion link label" - }, - "theme.common.editThisPage": { - "message": "编辑此页", - "description": "The link label to edit the current page" - }, - "theme.common.headingLinkTitle": { - "message": "标题的直接链接", - "description": "Title for link to heading" - }, - "theme.lastUpdated.atDate": { - "message": "于 {date} ", - "description": "The words used to describe on which date a page has been last updated" - }, - "theme.lastUpdated.byUser": { - "message": "由 {user} ", - "description": "The words used to describe by who the page has been last updated" - }, - "theme.lastUpdated.lastUpdatedAtBy": { - "message": "最后{byUser}{atDate}更新", - "description": "The sentence used to display when a page has been last updated, and by who" - }, - "theme.navbar.mobileVersionsDropdown.label": { - "message": "选择版本", - "description": "The label for the navbar versions dropdown on mobile view" - }, - "theme.common.skipToMainContent": { - "message": "跳到主要内容", - "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" - }, - "theme.tags.tagsListLabel": { - "message": "标签:", - "description": "The label alongside a tag list" - }, - "theme.blog.sidebar.navAriaLabel": { - "message": "最近博文导航", - "description": "The ARIA label for recent posts in the blog sidebar" - }, - "theme.CodeBlock.copied": { - "message": "复制成功", - "description": "The copied button label on code blocks" - }, - "theme.CodeBlock.copyButtonAriaLabel": { - "message": "复制代码到剪贴板", - "description": "The ARIA label for copy code blocks button" - }, - "theme.CodeBlock.copy": { - "message": "复制", - "description": "The copy button label on code blocks" - }, - "theme.AnnouncementBar.closeButtonAriaLabel": { - "message": "关闭", - "description": "The ARIA label for close button of announcement bar" - }, - "theme.CodeBlock.wordWrapToggle": { - "message": "切换自动换行", - "description": "The title attribute for toggle word wrapping button of code block lines" - }, - "theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": { - "message": "打开/收起侧边栏菜单「{label}」", - "description": "The ARIA label to toggle the collapsible sidebar category" - }, - "theme.navbar.mobileLanguageDropdown.label": { - "message": "选择语言", - "description": "The label for the mobile language switcher dropdown" - }, - "theme.TOCCollapsible.toggleButtonLabel": { - "message": "本页总览", - "description": "The label used by the button on the collapsible TOC component" - }, - "theme.blog.post.readMore": { - "message": "阅读更多", - "description": "The label used in blog post item excerpts to link to full blog posts" - }, - "theme.blog.post.readMoreLabel": { - "message": "阅读 {title} 的全文", - "description": "The ARIA label for the link to full blog posts from excerpts" - }, - "theme.blog.post.readingTime.plurals": { - "message": "阅读需 {readingTime} 分钟", - "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" - }, - "theme.docs.sidebar.collapseButtonTitle": { - "message": "收起侧边栏", - "description": "The title attribute for collapse button of doc sidebar" - }, - "theme.docs.sidebar.collapseButtonAriaLabel": { - "message": "收起侧边栏", - "description": "The title attribute for collapse button of doc sidebar" - }, - "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": { - "message": "← 回到主菜单", - "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)" - }, - "theme.docs.sidebar.expandButtonTitle": { - "message": "展开侧边栏", - "description": "The ARIA label and title attribute for expand button of doc sidebar" - }, - "theme.docs.sidebar.expandButtonAriaLabel": { - "message": "展开侧边栏", - "description": "The ARIA label and title attribute for expand button of doc sidebar" - }, - "theme.tags.tagsPageTitle": { - "message": "标签", - "description": "The title of the tag list page" - } -} diff --git a/sidebars.js b/sidebars.js index 9f6f00b4d..9c9daf379 100644 --- a/sidebars.js +++ b/sidebars.js @@ -2,45 +2,12 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - store: [ - { - type: "link", - label: "关于 TapTap", - href: "https://www.taptap.cn/about-us/", - }, - { - type: "autogenerated", - dirName: "store", - }, - { - type: "link", - label: "侵权投诉", - href: "https://www.taptap.cn/doc/27", - }, - ], sdk: [ { type: "autogenerated", dirName: "sdk", }, ], - design: [ - { - type: "autogenerated", - dirName: "design", - }, - { - type: "link", - label: "品牌素材", - href: "https://www.taptap.cn/about-us/brand-resources", - }, - ], - community: [ - { - type: "autogenerated", - dirName: "community", - }, - ], }; module.exports = sidebars; diff --git a/src/constants/env.ts b/src/constants/env.ts index 6a6039b51..ef24d4d96 100644 --- a/src/constants/env.ts +++ b/src/constants/env.ts @@ -1,4 +1,4 @@ -export const BRAND: string = "tds"; +export const BRAND: string = "leancloud"; export const REGION: string = "cn"; // Cloud Engine diff --git a/src/styles/override.scss b/src/styles/override.scss index b3743d9bb..a682fb1f3 100644 --- a/src/styles/override.scss +++ b/src/styles/override.scss @@ -4,19 +4,19 @@ :root { --ifm-code-font-size: 95%; --ifm-color-emphasis-300: var(--tap-grey2); - --ifm-color-primary: #06c4b0; - --ifm-color-primary-dark: #13b1b9; - --ifm-color-primary-darker: #12a7af; - --ifm-color-primary-darkest: #0f8a90; - --ifm-color-primary-light: #17d9e3; - --ifm-color-primary-lighter: #1ddee8; - --ifm-color-primary-lightest: #3ce3eb; + --ifm-color-primary: #2c97e8; + --ifm-color-primary-dark: #188ae0; + --ifm-color-primary-darker: #1782d4; + --ifm-color-primary-darkest: #136bae; + --ifm-color-primary-light: #45a3eb; + --ifm-color-primary-lighter: #51a9ec; + --ifm-color-primary-lightest: #77bcf0; --ifm-dropdown-hover-background-color: var(--tap-grey0); --ifm-dropdown-link-color: var(--tap-grey6); --ifm-global-shadow-md: var(--tap-box-shadow-3); --ifm-hr-border-color: var(--tap-grey2); --ifm-leading: 1em; - --ifm-menu-color-background-active: #EDFCFB; + --ifm-menu-color-background-active: #eef6fc; --ifm-menu-color-background-hover: #fafafa; --ifm-menu-link-padding-horizontal: 19px; --ifm-menu-link-padding-vertical: 7px; diff --git a/versioned_docs/version-v2/sdk/01-start/01-overview.mdx b/versioned_docs/version-v2/sdk/01-start/01-overview.mdx deleted file mode 100644 index 9455f3677..000000000 --- a/versioned_docs/version-v2/sdk/01-start/01-overview.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -id: overview -title: 概览 -sidebar_label: 概览 -slug: /sdk ---- - - -TapTap 开发者服务(TapTap Developer Services,简称 TDS 服务) 旨在帮助开发者降低游戏研发、运营维护等阶段投入的精力和成本,TDS 整合了各项服务,让开发者能够聚焦在游戏核心乐趣的创造上,创作更优秀的游戏,进而促进游戏行业生态的良性循环,最终让开发者与玩家双双受益。 - -TDS 提供以下服务,开发者可以通过在游戏中集成 TapSDK 来开启使用: - -- **[TapTap 登录](/v2/sdk/taptap-login/guide/start)**:提供 TapTap 登录方式,玩家可以通过 TapTap 授权快速开始游戏。 - -- **[TapDB 数据服务](/v2/sdk/tapdb/guide)**:通过简单的接入就可以获得丰富实用的数据看板和广告追踪能力,让数据分析和广告投放变得轻松易操作;同时 TapDB 也可以用于分析人群画像,帮助开发者更好地理解用户。 - -- **[内嵌动态](/v2/sdk/embedded-moments/guide)**:玩家可以在游戏内访问 TapTap 的社区论坛(官方公告、游戏攻略、问题反馈、热门话题等),同时也可以看到 TapTap 好友的游戏动态,并参与其他玩家、官方和大神之间的互动。 - -- **[正版验证](/v2/sdk/copyright-verification/guide)**:帮助开发者验证玩家设备中的付费游戏是否通过 TapTap 商店付费购买下载,有效地控制了未付费玩家从其他途径获得游戏的场景。 - -- **[DLC](/v2/sdk/dlc/guide)**:为开发者提供一个在游戏内销售商品的渠道,其间玩家无需离开游戏即可完成购买流程。 - -- **[唤起更新](/v2/sdk/update/guide)**:当游戏 apk 有更新时,支持玩家从游戏内直接跳转至 TapTap 进行游戏 apk 更新。 - -- **[数据存储](/v2/sdk/storage/guide/setup-dotnet)**:数据存储服务能够高效存取海量级 JSON 对象、二进制文件、地理位置等数据。其内置的行级 ACL 权限控制,以及通用的用户及角色管理体系,可以帮助你快速实现安全而灵活的数据访问。 - -- **[云引擎](/v2/sdk/engine/guide/overview)**:基于 Docker 的容器云计算平台,既可以被简单地用来托管静态网站,又可以接受任意程序语言的定制开发来动态处理外来请求,满足业务定制化需求。 - - - -使用对应的服务请先完成[开发者注册](https://developer.taptap.cn),之后登录开发者中心开启「游戏服务」。 \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/01-start/03-get-ready.mdx b/versioned_docs/version-v2/sdk/01-start/03-get-ready.mdx deleted file mode 100644 index 10bb21357..000000000 --- a/versioned_docs/version-v2/sdk/01-start/03-get-ready.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -id: get-ready -title: 准备工作 -sidebar_label: 准备工作 ---- - - -为了能使用 TapTap 开发者服务(TapTap Developer Services,简称 TDS 服务),你需要完成前期的配置工作。 - -## 创建应用 - -在使用 TDS 服务之前,你需要创建一个应用,来完成对接前的准备工作。创建应用请参考[商店指南](/store/release/store-creategame)。 - -## 开启应用配置 - -在 「TapTap 开发者中心」 - 「游戏服务」 - 「应用配置」,点击「开启」,获得当前应用的基本信息。 - -### 基本信息 - -`Client ID` 是一个应用实体包在 TapTap 开发者中心的唯一身份标识,TapTap 通过 `Client ID` 来鉴别应用的身份。每个应用仅能拥有一个 `Client ID`,如同一个应用区分测试服与正式服,需要创建两个不同的应用,分别开启应用配置。 - -### 适用地区 - -一个 client 仅能对应一个地区。这是由于在 TapTap 的账号系统内,将中国大陆用户与全球用户做了隔离区分,互不相通。 - -![](https://capacity-files.lcfile.com/nnQKxgJJzgErOlIOcxnbIHt8Vc1RmGYe/tap_get_ready.png) - -## 隐私声明 - -集成账号服务的功能,需要先签订[《TapTap 平台开发者协议》](/store/store-devagreement)。使用 TDS 服务,视为你同意前述所有协议,且你将基于这些协议承担相应的法律责任与义务。 diff --git a/versioned_docs/version-v2/sdk/01-start/04-quickstart.mdx b/versioned_docs/version-v2/sdk/01-start/04-quickstart.mdx deleted file mode 100644 index 573d4043f..000000000 --- a/versioned_docs/version-v2/sdk/01-start/04-quickstart.mdx +++ /dev/null @@ -1,570 +0,0 @@ ---- -id: quickstart -title: TapSDK 快速开始 -sidebar_label: 快速开始 ---- -import MultiLang from '/src/docComponents/MultiLang'; - -本文介绍如何快速接入 TapSDK 并实现 [TapTap 登录](/v2/sdk/taptap-login/guide/start)功能。 - -:::note -[下载](/tap-download) 页面提供了 Unity、Android、iOS 示例项目,可供参考。 -::: - -## 创建应用 -请登录 [TapTap 开发者中心](https://developer.taptap.cn/) 注册为开发者并创建应用。 - -## 下载 TapTap 应用 -点击下载 [TapTap 应用](https://www.taptap.cn/mobile) - -## 环境要求 - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - -<> - -- Android 5.0(API level 21)或更高版本 - - -<> - -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - - -## 项目配置 - - -<> - -SDK 可以通过 Unity Package Manager 导入或手动导入,请根据项目需要选择。 - -#### 使用 Unity Package Manager - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - -```json -"dependencies":{ -// 登录 -"com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#2.1.8", -"com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#2.1.8", -"com.taptap.tds.bootstrap":"https://github.com/TapTap/TapBootstrap-Unity.git#2.1.8", -} -``` - -:::tip -如果是手动下载 unitypackage 进行 SDK 导入,需要将 `Assets/TapTap/Common/Plugins/iOS/TapTap.Common.dll` 设置为只支持 iOS -::: - -[点击](https://github.com/TapTap) 参考 SDK 最新版本号。 - -#### 手动导入 - -1. [点击下载 TapSDK-UnityPackage.zip](/tap-download),然后将该 SDK 解压到方便的位置。 - -2. 在 Unity 项目中依次转到 **Assets > Import Packages > Custom Packages**。 - -3. 从解压缩中的 TapSDK 中,选择希望在应用中使用的 TapSDK 包导入。 - - - `TapTap_TapBootstrap.unitypackage` 必选,TapSDK 启动器 - - `TapTap_TapCommon.unitypackag` 必选,TapSDK 基础库 - - `TapTap_TapLogin.unitypackage` 必选,TapTap 登录 - - -导入 SDK 后还需进行 Android、iOS 平台的相关配置。 - -#### Android 配置 - -1. **File > Build Settings** 添加 Android 配置文件。 - - ![](https://img.tapimg.com/market/images/b00843d16d7a2ae2974ae69e3a7cae55.png) - -2. 编辑 `Assets/Plugins/Android/AndroidManifest.xml` 文件,在 Application Tag 下添加以下代码。 - - ```xml - - ``` - -#### iOS 配置 - -在 `Assets/Plugins/iOS/Resource` 目录下创建 `TDS-Info.plist` 文件,复制以下代码并且替换其中的 `ClientId` 和授权文案: - -```xml - - - - - taptap - - client_id - ClientId - - - NSPhotoLibraryUsageDescription - 说明为何应用需要此项权限 - NSCameraUsageDescription - 说明为何应用需要此项权限 - NSMicrophoneUsageDescription - 说明为何应用需要此项权限 - - NSUserTrackingUsageDescription - 说明为何应用需要此项权限 - - -``` - - -<> - -1. [点击下载 TapSDK_Android](/tap-download),将 SDK 包导入到项目 `project/app/libs` 目录下。 - -2. 打开项目的 `project/app/build.gradle` 文件,添加 gradle 配置如下: - - ```java - repositories{ - flatDir { - dirs 'libs' - } - } - - dependencies { - ... - implementation (name:'TapBootstrap_2.1.8', ext:'aar') // 必选:TapSDK 启动器 - implementation (name:'TapCommon_2.1.8', ext:'aar') // 必选:TapSDK 基础库 - implementation (name:'TapLogin_2.1.8', ext:'aar') // 必选:TapTap 登录 - - } - ``` - -3. 在 `AndroidManifest.xml` 添加网络权限: - - ```java - - ``` - -4. 旧版 Android 额外配置 - - 如果 `targetSdkVersion < 29`,还需要添加如下配置: - - - manifest 节点添加 `xmlns:tools="http://schemas.android.com/tools"` - - application 节点添加 `tools:remove="android:requestLegacyExternalStorage"` - - -<> - -#### 导入 SDK - -1. 在 Xcode 选择工程,到 **Build Setting > Other Linker Flags** 添加 `-ObjC` 和 `-Wl -ld_classic`。 - -2. 直接拖拽 [下载的 TapSDK_iOS](/tap-download) 到项目目录即可。 - -3. 视需要导入下载的资源文件: - - - 必选:TapTap 启动器、基础库、登录 - - ``` - TapBootstrapSDK.framework - TapCommonResource.bundle - TapLoginResource.bundle - TapCommonSDK.framework - TapLoginSDK.framework - ``` - - -4. 请仔细核对下面依赖库是否都添加成功: - - ``` - // 必选 - WebKit.framework - Security.framework - SystemConfiguration.framework - CoreTelephony.framework - SystemConfiguration.framework - libc++.tbd - - // TapTap 内嵌动态 - AVFoundation.framework - CoreTelephony.framework - MobileCoreServices.framework - Photos.framework - SystemConfiguration.framework - WebKit.framework - - // 数据分析 - AppTrackingTransparency.framework - AdSupport.framework - CoreMotion.framework - Security.framework - SystemConfiguration.framework - libresolv.tbd - libsqlite3.0.tbd - libz.tbd - ``` - -#### 配置权限 - -**TapTap 内嵌动态功能需要相册、相机、麦克风访问权限,数据分析功能需要 IDFA 权限。** - -因此,如果游戏加入了 TapTap 内嵌动态功能或数据分析功能,那么需要在 `info.plist` 添加如下配置(请替换授权文案): - -```xml - -NSPhotoLibraryUsageDescription -说明为何应用需要此项权限 - -NSCameraUsageDescription -说明为何应用需要此项权限 - -NSMicrophoneUsageDescription -说明为何应用需要此项权限 - -NSUserTrackingUsageDescription -说明为何应用需要此项权限 -``` - -#### 配置跳转 TapTap 应用 - -用户无 TapTap 应用时,默认会通过 WebView 登录。 - -1. 打开 `info.plist`,添加如下配置(请替换 `clientID` 为你在控制台获取的 `Client ID`): - - ![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` - -2. 配置 openUrl: - - a) 如果项目中有 `SceneDelegate.m`,请先删除,然后请添加如下代码到 `AppDelegate.m` 文件: - - ```objectivec - - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - return [TapBootstrap handleOpenURL:url]; - } - - - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [TapBootstrap handleOpenURL:url]; - } - ``` - - b) 删除 `info.plist` 里面的 Application Scene Manifest - ![](/img/tap_ios_appmanifest.png) - - c) 删除 AppDelegate.m 文件中的两个管理 Scenedelegate 生命周期代理方法 - - ```objectivec - - #pragma mark - UISceneSession lifecycle - - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { - - return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; - } - - - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { - - } - - ``` - - d) 在 `AppDelegate.h` 中添加 `UIWindow` - - ```objectivec - @property (strong, nonatomic) UIWindow *window; - ``` - - - - - - -## 初始化 - -初始化 TapSDK 时需传入 `Client ID`、区域等应用配置信息。 - - - -```cs -TapConfig tapConfig = new TapConfig.Builder() - .ClientID("clientId")// 必须 - .ClientSecret("client_secret")// 必须,开发者中心对应 Client Token - .RegionType(RegionType.CN)// 非必须,默认 CN - .ConfigBuilder(); - -TapBootstrap.Init(tapConfig); -``` - -```java -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(getApplicationContext()) - .withRegionType(TapRegionType.CN) // TapRegionType.CN: 国内 TapRegionType.IO: 国外 - .withClientId("clientId") - .withClientSecret("clientSecret")// 开发者中心对应 Client Token - .build(); -TapBootstrap.init(MainActivity.this, tapConfig); -``` - -```objectivec -// 初始化 SDK -TapConfig *config = TapConfig.new; -config.clientId = @"clientId"; -config.clientSecret=@"clientSecret";// 开发者中心对应 Client Token - -config.region = TapSDKRegionTypeCN; -[TapBootstrap initWithConfig:config]; -``` - - - -## 登录回调 - -注册登录回调,以接收登录结果。 - - - -```cs -TapBootstrap.RegisterLoginResultListener(new MyLoginCallback()); -public class MyLoginCallback : ITapLoginResultListener { - public void OnLoginSuccess(AccessToken accessToken) - { - Debug.Log("登录成功: " + accessToken.ToJSON()); - } - - public void OnLoginError(TapError error) - { - Debug.Log("登录失败: " + error.errorDescription); - } - - public void OnLoginCancel() - { - Debug.Log("登录取消"); - } -} -``` - -```java -TapBootstrap.registerLoginResultListener(new TapLoginResultListener() { - @Override - public void loginSuccess(AccessToken accessToken) { - Log.d(TAG, "onLoginSuccess: " + accessToken.toJSON()); - } - - @Override - public void loginFail(TapError tapError) { - Log.d(TAG, "onLoginError: " + tapError.toJSON()); - } - - @Override - public void loginCancel() { - Log.d(TAG, "onLoginCancel"); - } -}); -``` - -```objectivec -// 注册登录回调 -[TapBootstrap registerLoginResultDelegate:self]; - -// 实现回调方法 -// 登录成功回调 -// @param token token 对象 -- (void)onLoginSuccess:(AccessToken *)token{ - NSLog (@"onLoginSuccess"); -} - -// 登录取消 -- (void)onLoginCancel{ - NSLog (@"onLoginCancel"); -} - -// 登录失败 -// @param error 失败原因 -- (void)onLoginError:(NSError *)error{ - NSLog (@"onLoginError error"); -} -``` - - - -## AccessToken - -上面代码示例中的 `AccessToken` 用于用户鉴权,过期时间为 90 天(过期后 SDK 会自动清除本地缓存),可以传到游戏服务端去获取用户信息,参见 [获取用户信息](/v2/sdk/taptap-login/guide/userinfo#流程)。 - -`AccessToken` 示例: - -```json -{ - "access_token":"accessToken", - "kid":"kid", - "macAlgorithm":"macAlgorithm", - "tokenType":"tokenType", - "macKey":"macKey", - "expireIn" :7776000 -} -``` - -### 参数说明 - -参数 | 描述 -| ------ | ------ | -access_token | 用户登录后的凭证 -kid | 当前实际返回的 kid 和 accessToken 值相等,建议使用 accessToken -macAlgorithm | 固定为 `hmac-sha-1` -tokenType | 固定为 `mac` -macKey | mac 密钥 -expireIn | 过期时间 - - -## TapTap 登录 - -在尝试登录用户前先检查登录状态。 - -### 检查登录状态 - -尝试获取当前用户的 `Access Token`,如 `Access Token` 为空则用户未登录。 - - - -```cs -TapBootstrap.GetAccessToken((accessToken, error) => { - if (accessToken == null) - { - Debug.Log("当前未登录"); - } - else - { - Debug.Log("已登录"); - } -}); -``` - -```java -if (TapBootstrap.getCurrentToken() == null) { - // 未登录 -} else { - // 已登录 -} -``` - -```objectivec -AccessToken *accessToken = [TapBootstrap getCurrentToken]; -if (accessToken == nil) { - // 未登录 -} else { - // 已登录 -} -``` - - - -### 登录 - - - -```cs -LoginType loginType = LoginType.TAPTAP; -TapBootstrap.Login(loginType, new string[] { "public_profile" }); -``` - -```java -/** - * @param activity 当前 Activity - * @param @param LoginType.TAPTAP - */ -TapBootstrap.login(MainActivity.this, LoginType.TAPTAP, "public_profile"); -``` - -```objectivec -TapBootstrapLoginType loginType = TapBootstrapLoginTypeTapTap; -[TapBootstrap login:(loginType) permissions:@[@"public_profile"]]; -``` - - - -### 登出 - -:::caution -当用户退出登录的时候请务必调用此方法执行退出功能, 避免用户信息错乱。 -::: - - - -```cs -TapBootstrap.Logout(); -``` - -```java -TapBootstrap.logout(); -``` - -```objectivec -[TapBootstrap logout]; -``` - - - -## 打包 - - -<> - -### 打包 APK - -1. 配置 package name 和签名文件: - - ![](https://capacity-files.lcfile.com/qooIRbr5qtLrnhsP0hWjOSnBYW12eNg6/tap_unity_android_build.png) - -2. 检查 **Player Settings > Other Settings > Target APILevel** 版本,当 `Target APILever < 29` 时,需要配置 manifest,在 application 节点添加 - - ``` - tools:remove="android:requestLegacyExternalStorage" - ``` - -### 导出 Xcode 工程 - -需要配置 icon 和 `BundleID` - -![](https://capacity-files.lcfile.com/Nke4QO6zdEz5mRd2Kwd8R9ydyP8QYaJy/tap_ios_build.png) - - -<> - -按通常的 Android APK 打包流程操作即可。 - - -<> - -按通常的 iOS 应用打包流程操作即可。 - - - diff --git a/versioned_docs/version-v2/sdk/01-start/05-test-accounts.mdx b/versioned_docs/version-v2/sdk/01-start/05-test-accounts.mdx deleted file mode 100644 index e2809fac2..000000000 --- a/versioned_docs/version-v2/sdk/01-start/05-test-accounts.mdx +++ /dev/null @@ -1,19 +0,0 @@ ---- -id: test-accounts -title: 测试用户管理 -sidebar_label: 测试用户管理 ---- - - -测试用户管理功能是用于 TapTap 登录、正版验证等相关服务的测试用户名单管理,例如在商店通过审核上架前,测试用户可提前通过「TapTap 登录」进入游戏测试相关游戏服务。商店发布后,可修改「TapTap 登录 - 应用状态」对全部用户开放登录。 - -**注意事项:** - -* **请确保操作人员的角色配置了【测试用户管理】的权限,默认包含该权限的角色有「主管理员」、「游戏管理员」、「发行」 和 「开发」。** -* **测试用户管理需要先开启应用配置,请确保应用配置先行开启后再添加用户。** - -1、登录 [TapTap 开发者中心](https://developer.taptap.cn),在所属游戏页面点击 「测试用户管理」 - -2、选择 「测试用户管理」,进入页面,点击【 + 添加用户】按钮 - -3、填写用户 ID 或昵称搜索用户,选择用户并提交,成功将用户添加进名单 diff --git a/versioned_docs/version-v2/sdk/01-start/07-agreement.mdx b/versioned_docs/version-v2/sdk/01-start/07-agreement.mdx deleted file mode 100644 index 5dc172710..000000000 --- a/versioned_docs/version-v2/sdk/01-start/07-agreement.mdx +++ /dev/null @@ -1,267 +0,0 @@ ---- -id: agreement -title: TapSDK 隐私政策 -sidebar_label: TapSDK 隐私政策 ---- - - -本隐私政策旨在向开发者及其终端用户说明我们收集个人信息的类型及我们如何处理和保护个人信息。在注册、接入、使用 TapSDK 产品和/或服务前,请开发者务必仔细阅读本隐私政策,在确认充分了解并同意后再接入并使用,如果开发者不同意本隐私政策,应立即停止接入及使用 TapSDK 产品和/或服务。 - - - -本隐私政策不适用于接入并使用 TapSDK 产品和/或服务的开发者在其 App 中处理由其所控制的终端用户个人信息的行为,也不适用于展示在、链接到或再封装我们的产品和/或服务的那些适用第三方隐私政策、并由第三方提供的服务。我们建议终端用户在认真阅读、充分了解并同意 App/ 相关第三方的隐私政策后,再使用相应的产品/服务。 - - - -为了便于开发者及终端用户阅读及理解,我们对关键术语进行了定义,请参见本隐私政策“附录:关键术语定义”。 - - - -**特别说明:** - -如果开发者在其 App 中接入并使用 TapSDK 产品和/或服务,请开发者知悉并承诺: - -(1)开发者已经遵守并将持续遵守适用的法律、法规、政策和监管要求收集、使用和处理终端用户的个人信息,保护个人信息安全。 - -(2)开发者已经告知终端用户在其 App 中接入并使用 TapSDK 产品和/或服务的情况,以及 TapSDK 对终端用户必要个人信息的收集、使用和保护规则(即本隐私政策),并且已经获得终端用户对于 TapSDK 收集、使用、处理其个人信息充分、必要且明确的授权同意(包括获取了儿童监护人对提供终端用户是儿童的个人信息的授权同意)。 - -(3)开发者已经向终端用户提供了易于操作的用户权利实现机制(包括但不限于访问、更正、删除其个人信息,撤销或更改其授权同意范围,注销其个人账号等)。 - - - -## **一、我们如何收集和使用开发者和/或终端用户的个人信息** - -### **(一)我们收集的个人信息** - -我们会根据开发者选择不同的服务收集不同的信息: - -1、当开发者选择 TapSDK 提供动态服务时,为了便于终端用户拍摄照片、视频和上传、分享图片、视频信息,我们将在经过终端用户同意后获取本地存储权限。 - -2、当开发者选择 TapSDK 提供数据分析服务时,为了更精确地辅助开发者进行移动游戏数据分析,我们将在经过终端用户同意后获取 IP 地址、应用标识符、独立设备标识符、iOS 广告标识符(IDFA)、安卓广告主标识符、网卡(MAC)地址、国际移动设备识别码(IMEI)、设备型号、终端制造厂商、终端设备操作系统版本、时区和网络状态(WiFi 等)。其中终端用户数据还可能包括:终端用户在开发者产品中的标识符、地理位置、用户触发的事件、错误和页面浏览量等。此外,通过统计分析工具发送的 HTTP/HTTPS 请求中还可能包含终端用户的 IP 地址、设备类型、地区等信息。 - -3、当开发者选择 TapSDK 提供的部分 TapTap 平台服务时,或终端用户直接与 TapTap 平台交互的场景,还应遵守 TapTap 平台隐私政策的相关约定。 - -4、我们会按照在本隐私政策中载明收集的个人信息用途收集使用开发者和/或终端用户的个人信息,如果我们要将收集个人信息用于本声隐私政策未载明的其它服务/用途,我们会以合理的方式向开发者和/或终端用户告知。 - -5、TapSDK 可能收集的个人信息/权限取决于开发者对 TapSDK 具体功能/服务的选择。如果在部分 App 中不涵盖某些服务内容或未提供特定功能,则本隐私政策中涉及到上述服务/功能及相关个人信息的内容将不适用。 - - - - - -### **(二)个人信息的使用规则** - -1、我们会根据本隐私政策和/或与开发者的协议约定,并仅为实现 TapSDK 产品和/或服务功能对所收集的个人信息进行处理。若需要将收集的个人信息用于其他目的,我们会以合理方式告知开发者,并在开发者获得终端用户同意后进行使用。 - -2、在收集开发者和/或终端用户的个人信息后,我们将通过技术手段对个人信息进行去标识化或匿名化处理。 - -3、我们向开发者提供的 TapSDK 产品和/或服务停止运营后,或者我们被合理通知开发者和/或终端用户撤回个人信息的授权后,或者我们被合理通知开发者和/或终端用户注销账户或主动删除个人信息后,我们将会于合理的时间内销毁、匿名化或去标识化处理从开发者和/或终端用户处接收的所有个人信息,除非法律另有规定。 - - - -### **(三)事先征得授权同意的例外** - -1、与国家安全、国防安全有关的; - -2、与公共安全、公共卫生、重大公共利益有关的; - -3、与犯罪侦查、起诉、审判和判决执行等有关的; - -4、出于维护个人信息主体或其他个人的生命、财产等重大合法权益但又很难得到开发者和/或终端用户本人同意的; - -5、所获取的个人信息是开发者和/或终端用户自行向社会公众公开的; - -6、从合法公开披露的信息中获取个人信息的,如合法的新闻报道、政府信息公开等渠道; - -7、根据开发者和/或终端用户的要求签订合同所必需的; - -8、用于维护所提供的产品或服务的安全稳定运行所必需的,例如发现、处置产品或服务的故障; - -9、为合法的新闻报道所必需的; - -10、学术研究机构基于公共利益开展统计或学术研究所必要,且对外提供学术研究或描述的结果时,对结果中所包含的个人信息进行去标识化处理的; - -11、法律法规规定的其他情形。 - - - -## **二、我们如何使用 Cookie 等同类技术** - -1、为确保网站正常运转,我们会在终端用户的计算机或移动设备上存储名为 Cookie 的小数据文件。Cookie 通常包含标识符、站点名称以及一些号码和字符。Cookie 主要的功能是便于终端用户使用网站产品和服务,以及帮助网站统计独立访客数量等。运用 Cookie 技术,我们能够为终端用户提供更加周到的个性化服务,并允许终端用户设定属于其特定的服务选项,并可根据自己的偏好管理或删除 Cookie。 - -2、当终端用户使用 TapSDK 的产品和/或服务时,我们会向终端用户的设备发送 Cookie。当终端用户与我们提供给合作伙伴的产品和/或服务进行交互时,我们允许 Cookie(或者其他匿名标识符)发送给我们的服务器。我们不会将 Cookie 用于本隐私政策所述目的之外的任何用途。 - - - -## **三、我们如何共享、转让、公开披露开发者和/或终端用户的个人信息** - -### **(一)共享** - -1、我们会与我们的关联公司共享必要的个人信息,且受本隐私政策中所提及目的的约束。 - -2、我们可能会向合作伙伴及第三方共享开发者和/或终端用户的必要的个人信息,以保障为开发者和/或终端用户提供的产品和/或服务顺利完成。我们的合作伙伴无权将共享的个人信息用于任何其他用途。 - -3、对我们与之共享个人信息的公司、组织和个人,我们会对其数据安全环境进行调查,并与其签署严格的保密协议,要求他们按照与我们同等的标准来处理个人信息。 - - - -### **(二)转让** - -我们不会将开发者和/或终端用户的个人信息转让给任何公司、组织和个人,但以下情况除外: - -1、 事先获得开发者和/或终端用户的明确授权或同意; - -2、满足法律法规、法律程序的要求或强制性的政府要求或司法裁定; - -3、在涉及合并、收购、资产转让或类似的交易时,如涉及到个人信息转让,我们会要求新的持有开发者和/或终端用户个人信息的公司、组织继续受本隐私声明的约束,否则,我们将要求该公司、组织重新向开发者和/或终端用户征求授权同意。 - - - -### **(三)公开披露** - -我们不会公开披露开发者和/或终端用户的个人信息,但以下情况除外: - -1、获得开发者和/或终端用户的明确同意; - -2、在法律、法律程序、诉讼或政府主管部门强制要求的情况下。 - - - -### **(四)共享、转让、公开披露个人信息时事先征得授权同意的例外** - -在以下情形中,共享、转让、公开披露个人信息无需事先征得个人信息主体的授权同意: - -1、 与国家安全、国防安全直接相关的; - -2、 与犯罪侦查、起诉、审判和判决执行等直接相关的; - -3、 与个人信息控制者履行法律法规规定的义务相关的; - -4、 个人信息主体自行向社会公开的个人信息; - -5、 从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道。 - - - -## **四、我们如何存储开发者和/或终端用户的个人信息** - -### **(一)存储期限** - -开发者和/或终端用户在使用 TapSDK 产品及服务期间,我们将持续存储开发者和/或终端用户的个人信息。如果开发者和/或终端用户注销帐户或主动删除上述信息,我们将在不违反相关法律法规规定的期限内或个人信息主体另行授权同意的期限内,存储开发者和/或终端用户的个人信息。 - - - -### **(二)存储地域** - -我们会按照中国法律法规的规定,将中国境内获取的开发者和/或终端用户的个人信息存储于中国境内。在不违反当地法律法规的前提下,将当地获取的开发者和/或终端用户个人信息也存储于中国境内。 - - - -## **五、我们如何保护开发者和/或终端用户的个人信息安全** - -### **(一)安全保护措施** - -1、我们会以业界成熟的安全标准和规范收集、使用、存储和传输用户信息,并使用符合业界标准的技术保护措施保护开发者和/或终端用户提供的个人信息及个人敏感信息(包括但不限于防火墙、加密、去标识化或匿名化处理、数据脱敏加密、访问控制措施等),防止数据遭到未经授权访问、公开披露、使用、修改、毁损、丢失或泄露。 - -2、我们会对安全管理负责人和关键安全岗位的人员进行安全背景审查,还会对处理个人信息的员工进行身份认证及权限控制,并与接触个人信息的员工签署保密协议,明确岗位职责及行为准则,确保只有授权人员才可访问个人信息。 - -3、我们成立了保障个人信息安全的专责团队,并建立了专门的信息安全管理制度和内部安全事件处置机制,以防发生个人信息泄露、损毁、丢失等安全事件。 - - - -### **(二)安全事件处置机制** - -1、我们会制定个人信息安全事件紧急预案,也会定期组织工作团队成员进行安全应急预案演练,防止此类安全事件发生。 - -2、若发生个人信息泄露、损毁、丢失等安全事件,我们会及时启动紧急预案,采取相应处理措施,阻止安全事件扩大,并按照规定向有关主管部门报告。 - -3、在个人信息安全事件发生后,我们会及时通知开发者和/或终端用户安全事件的基本情况、我们已采取或将要采取的处理措施,以及我们对个人信息主体的应对建议。 - - - -## **六、如何管理个人信息** - -可通过以下方式访问及管理个人信息: - -1、 查询、更正和补充个人信息 - -开发者如需审阅、更正或补充存在我们这里的信息,请访问我们的网站并登录账号进行操作。如某项权利的行使无法在账号页面操作,您可以通过客服系统或本政策中的联系方式与我们联系,由我们协助您进行相应操作。 - - - -2、账号注销与删除个人信息 - -如开发者不希望继续使用我们的产品,可以通过工单系统(具体路径为登录开发者后台账号,点击我的客服,创建问题进行反馈)向我们提交注销账号的申请,我们通常会在 15 个工作日内回应您的申请需求,但为了保障账号安全,我们将需要开发者提交充分有效的身份信息以便我们识别身份、核实并处理请求。账号注销后,我们将不再提供服务,如开发者希望我们删除其个人信息,也可以向我们提交删除个人信息的申请。 - - - -## **七、我们如何处理未成年人的个人信息** - -我们非常重视未成年人的个人信息保护工作。 - - - -请开发者确保是 18 周岁(含)以上人士,同时请开发者理解并知悉: - -1、如果 App 是针对 18 周岁以下的未成年用户并/或为 18 周岁以下的未成年用户设计和开发的,请开发者务必确保终端用户(未成年人)的监护人已经阅读并同意了 App 的隐私政策,并且授权同意提供未成年人的个人信息给我们以实现 App 在隐私政策中所述的相关功能。 - -2、如果 App 是针对 14 周岁以下的儿童用户并/或为 14 周岁以下的儿童用户设计和开发的,请开发者务必确保终端用户(儿童)的监护人已经阅读并同意了 App 的隐私政策以及[《TapTap 儿童个人信息保护规则》](https://www.taptap.cn/children-privacy),并且授权同意提供儿童个人信息给我们以实现 App 在隐私政策中所述的相关功能。 - - - -我们只会在受到法律允许、父母或监护人同意或者保护未成年人所必要的情况下收集、使用、公开披露未成年的个人信息。如果我们在不知情或错误的情况下收集了未成年人的个人信息,我们将及时进行删除,除非法律要求我们保留此类资料。如果我们发现在未事先获得可证实的父母同意的情况下收集了未成年人的个人信息,将会采取措施尽快删除相关信息。 - - - -## **八、隐私政策的修订** - -为给开发者提供更好的服务以及随着 TapSDK 产品和/或服务的不断发展与变化,我们可能会适时对本声明进行修订。 - - - -当本隐私政策发生重大变更时,我们会在开发者中心页面向开发者及终端用户进行公示,供开发者及终端用户随时查看;或以发送电子邮件或站内信的形式,向开发者说明本隐私政策的具体变更内容,并说明生效日期。 - - - -如开发者不同意接受修订后的本隐私政策,请停止接入和使用我们的产品和/或服务;如终端用户不同意接受本隐私政策,可以停止使用 TapSDK 的产品和/或服务,开发者应向终端用户提供相应实现机制,若终端用户继续使用即表示其同意接受修订后的本隐私政策的约束。 - - - -## **九、如何联系我们** - -如开发者和/或终端用户对本隐私政策或与个人信息相关事宜有任何疑问,可以通过以下方式与我们取得联系: - -(i)发送邮件至:[law@taptap.com](mailto:law@taptap.com); - -(ii)邮寄信件至:中国上海市静安区万荣路 700 号 A3 202 TapTap 法务部(收) 邮编:200072。 - -我们将及时验证请求人身份并尽快审核所涉问题,并在 15 天内予以回复。 - - - - - - -## **附录:关键术语定义** - -1、 **我们**:指在相关区域有权运营 TapSDK 的对应企业实体。 - -2、 **TapSDK**:是一款为移动应用开发者(以下简称“开发者”)提供移动游戏数据分析服务的软件开发工具包。 - -3、 **TapSDK 产品和/或服务**:指我们及关联公司开发的 SDK 产品和/或服务。 - -4、 **开发者**:指注册、接入、使用 TapSDK 产品和/或服务的开发者客户。 - -5、 **终端用户**:指使用嵌入 TapSDK 产品和/或服务的 App 终端用户。 - -6、 **个人信息**:指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息。个人信息包括姓名、出生日期、身份证件号码、个人生物识别信息、住址、通信通讯联系方式、通信记录和内容、帐号密码、财产信息、征信信息、行踪轨迹、住宿信息、健康生理信息、交易信息等。 - -7、 **个人敏感信息**:指一旦泄露、非法提供或滥用可能危害人身和财产安全,极易导致个人名誉、身心健康受到损害或歧视性待遇等的个人信息。个人敏感信息包括身份证件号码、个人生物识别信息、银行账号、通信记录和内容、财产信息、征信信息、行踪轨迹、住宿信息、健康生理信息、交易信息、14 岁以下(含)未成年人的个人信息等。 - -8、 **用户画像**:指通过收集、汇聚、分析个人信息,对某特定自然人个人特征,如其职业、经济、健康、教育、个人喜好、信用、行为等方面做出分析或预测,形成其个人特征模型的过程。直接使用特定自然人的个人信息,形成该自然人的特征模型,称为直接用户画像。使用来源于特定自然人以外的个人信息,如其所在群体的数据,形成该自然人的特征模型,称为间接用户画像。 - -9、 **去标识化**: 指通过对个人信息的技术处理,使其在不借助额外信息的情况下,无法识别个人信息主体的过程。 - -10、**匿名化**:指通过对个人信息的技术处理,使得个人信息主体无法被识别,且处理后的信息不能被复原的过程。个人信息经匿名化处理后所得的信息不属于个人信息。 - -11、**中国或中国境内**:指中华人民共和国大陆地区,不包含香港特别行政区、澳门特别行政区和台湾地区。 \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/01-start/_category_.json b/versioned_docs/version-v2/sdk/01-start/_category_.json deleted file mode 100644 index dadbec88d..000000000 --- a/versioned_docs/version-v2/sdk/01-start/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "入门指南", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/02-taptap-login/01-features.mdx b/versioned_docs/version-v2/sdk/02-taptap-login/01-features.mdx deleted file mode 100644 index 56bc49ccf..000000000 --- a/versioned_docs/version-v2/sdk/02-taptap-login/01-features.mdx +++ /dev/null @@ -1,83 +0,0 @@ ---- -id: features -title: 功能介绍 -sidebar_label: 功能介绍 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -为了访问 TapTap Deverloper Services(以下简称 TDS)的相关服务功能,你的用户需要拥有一个 TapTap 账号。如果用户未使用 TapTap 账号,你的应用在调用 TDS 服务 API 时可能会遇到错误。本文档介绍了如何在你的应用中实现 TapTap 登录体验。 - -## 业务介绍 - -TapTap 账号服务,基于标准的 OAuth 2.0 协议构建的授权登录系统,为开发者提供了简单、安全、快速的账号登录授权功能,为用户免去输入账号密码的繁琐步骤,一键通过 TapTap 账号授权,即刻使用你的应用。 - -在取得用户授权之后,开发者可以通过接口调用的方式获得 TapTap 用户的相关公开信息,包括用户昵称、头像、性别等信息,可用于提高应用内的用户体验设计。 - - - -## 前期工作 - -请确认已经在 TapTap 开发者中心 - 应用配置完成了开启操作。 - -### 配置签名证书 - -为了更高的安全性,TapTap 登录服务需要校验你的游戏。你需要提交游戏的 package name(Android 包名)、Bundle id(iOS 包名)以及 Android 签名。 - -:::tip - -1、Android 的包名请使用符合 Android 规范的命名方式。参考文档:[Android 开发者 - 设置应用 ID](https://developer.android.com/studio/build/application-id) - -2、Android 的签名为 Keystore 文件中的 MD5 字符串(32 位),填入时请去除特殊符号 - -3、iOS 的 Bundle ID 请使用符合苹果规范的命名方式。参考文档:[Property List Key - CFBundle 标识符](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier) - -::: - -## 实现交互式登录  - -检查当前如果没有用户登录状态时,需要为用户提供一个可视化点击交互的登录界面。TapTap 审核团队会在应用上架 TapTap 商店时审核你的登录界面,请务必参照[《登录按钮设计规范》](/design)进行绘制。 - -### 单个登录方式 - -当应用中仅提供 TapTap 一种登录方式时,建议在开始游戏的主界面,绘制一个可交互的登录按钮。按钮的范围大小、按钮上的文案使用,均不能误导、不能阻碍用户的正常顺畅点击。 - -登录按钮的设计样式,在[《登录按钮设计规范》](/design)允许的范围内,可适当添加与游戏气质相符的风格元素。此外,TDS 也为你准备了不同场景下 TapTap 登录按钮的设计图标,帮助你快速实现登录流程,点击 [《TapTap 登录按钮设计图标》](/tap-download)下载资源。 - - - - - -### 多种登录方式 - -如果游戏还有其他登录方式同时存在时,为用户提供合理布局的登录界面,尽可能的从外观明显区分每种登录方式的不同,让用户可以快速找到目标。 - - -       - -## 实现静默登录 - -静默登录可以帮助用户节省登录的流程,通常用于用户下一次启动游戏时,仍有登录状态的场景。 - -当用户启动游戏时,你可以尝试获取当前用户的 `Access Token`  来检查用户是否已经当前设备上登录过。则可以尝试在不显示登录按钮或界面的情况下帮用户完成登录过程。 - - - -## 登录授权 - -移动应用的 TapTap 账号服务,需要与 TapTap 移动端客配合使用。TapSDK 会根据用户设备中,TapTap 客户端的安装情况,来自动选择使用合适的登录流程。 - -[点击此处](https://www.taptap.cn/mobile)下载 TapTap 移动客户端 - -### 唤起 TapTap 客户端授权登录 - -当用户单击 TapTap 登录按钮时,TapSDK 检测到用户设备中已经安装了 TapTap 客户端,会自动唤起设备中的 TapTap 客户端,并识别客户端中的登录信息,进行授权登录。 - - - - -### 打开 WebView 授权登录 - -当用户单击 TapTap 登录按钮时,TapSDK 检测到用户设备中未安装 TapTap 客户端,则会打开 WebView 进行登录流程。 - - diff --git a/versioned_docs/version-v2/sdk/02-taptap-login/02-guide/01-start.mdx b/versioned_docs/version-v2/sdk/02-taptap-login/02-guide/01-start.mdx deleted file mode 100644 index 109fccf4c..000000000 --- a/versioned_docs/version-v2/sdk/02-taptap-login/02-guide/01-start.mdx +++ /dev/null @@ -1,193 +0,0 @@ ---- -id: start -title: 接入 TapTap 登录 -sidebar_label: 功能接入 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - -[快速开始](/v2/sdk/start/quickstart)中简单介绍了[如何在游戏中加入 Tap 登录](/v2/sdk/start/quickstart#taptap-登录),这里详细介绍 TapSDK 的[登录功能](/v2/sdk/taptap-login/features)。 - -## 检查登录状态 - -尝试获取当前用户的 `Access Token`,如 `Access Token` 为空则用户未登录。 -调用登录方法前先检查登录状态,可以避免重复登录。 - - - -```cs -TapBootstrap.GetAccessToken((accessToken, error) => { - if (accessToken == null) - { - Debug.Log("当前未登录"); - } - else - { - Debug.Log("已登录"); - } -}); -``` - -```java -if (TapBootstrap.getCurrentToken() == null) { - // 未登录 -} else { - // 已登录 -} -``` - -```objectivec -AccessToken *accessToken = [TapBootstrap getCurrentToken]; -if (accessToken == nil) { - // 未登录 -} else { - // 已登录 -} -``` - - - -## 登录资格校验 - -:::tip -该功能仅用于需要上线「篝火测试服」的游戏,对有登录白名单的用户进行资格校验,防止测试阶段开发包外传被利用 -::: - -请在登录成功的回调里调用相关 API 进行校验,[点击](https://www.taptap.cn/campfire)了解篝火计划 - - - -```cs - public void OnLoginSuccess(AccessToken accessToken) - { - Debug.Log("登录成功:" + accessToken.ToJSON()); - TapBootstrap.GetTestQualification((valid, error) => { - if (valid) - { - Debug.Log("该用户已拥有测试资格"); - } - else - { - Debug.Log("不具备测试资格,游戏层面进行拦截"); - } - }); - } -``` - -```java -TapBootstrap.getTestQualification(new Callback() { - @Override - public void onSuccess(Boolean aBoolean) { - // 该玩家已拥有测试资格 - } - - @Override - public void onFail(TapError tapError) { - // 该玩家不具备测试资格, 游戏层面进行拦截 - } -}); -``` - -```objectivec -- (void)onLoginSuccess:(AccessToken *)token{ - NSLog (@"onLoginSuccess"); - [TapBootstrap getTestQualification:^(BOOL isQualified, NSError *_Nullable error) { - if (error) { - // 网络异常或游戏未开启篝火测试 - } else { - if (isQualified) { - // 有篝火测试资格 - } - } - }]; -} -``` - - - -** Error 信息为网络错误,或者该游戏未开通篝火测试服 ** - -## 获取用户信息 - -获取当前登录用户的 ID、昵称、头像等基本信息。 - - - -```cs -TapBootstrap.GetUser((user, error) => { - Debug.Log(user.ToJSON()); -}); -``` - -```java -TapBootstrap.getUser(new Callback() { - @Override - public void onSuccess(TapUser tapUser) { - - } - - @Override - public void onFail(TapError tapError) { - - } -}); -``` - -```objectivec -[TapBootstrap getUser:^(TapUser * _Nullable userInfo, NSError * _Nullable error) { - if (error) { - NSLog(@"获取用户信息失败 %@", error); - } else { - NSLog(@"获取用户信息成功 %@", userInfo); - } -}]; -``` - - - - -## 登录 - -执行登录操作,优先跳转 TapTap APP 登录,当没有 TapTap APP 时,会打开内置 WebView 登录。 -另外,请仔细阅读[登录按钮设计规范](/design)。 - - - - -```cs -LoginType loginType = LoginType.TAPTAP; -TapBootstrap.Login(loginType, new string[] { "public_profile" }); -``` - -```java -TapBootstrap.login(MainActivity.this, LoginType.TAPTAP, "public_profile"); -``` - -```objectivec -TapBootstrapLoginType loginType = TapBootstrapLoginTypeTapTap; -[TapBootstrap login:(loginType) permissions:@[@"public_profile"]]; -``` - - - -上述代码示例中,登录类型固定为 TapTap 登录,权限固定为 `public_profile`。 - -## 登出 - -退出登录,清除用户登录缓存。 - - - -```cs -TapBootstrap.Logout(); -``` - -```java -TapBootstrap.logout(); -``` - -```objectivec -[TapBootstrap logout]; -``` - - diff --git a/versioned_docs/version-v2/sdk/02-taptap-login/02-guide/02-user-info.mdx b/versioned_docs/version-v2/sdk/02-taptap-login/02-guide/02-user-info.mdx deleted file mode 100644 index 11874249b..000000000 --- a/versioned_docs/version-v2/sdk/02-taptap-login/02-guide/02-user-info.mdx +++ /dev/null @@ -1,368 +0,0 @@ ---- -id: userinfo -title: 获取登录信息 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; - -## 概述 - -OpenAPI 采用统一的 Mac Token 头部签算来传递用户身份。 - -接入客户端 SDK 后,经过用户的授权流程,会获得这个用户在当前应用中的 Mac Token。Mac Token 长期有效,只有在用户更新自己账号相关安全信息、注销对当前应用的授权时才会失效。开发者应当将 Mac Token 妥善保管于自己的服务器上,作为后续与 TapTap 服务端通讯的标示。(Mac Token 算法细节见文档中的 [MAC Token 算法](#mac-token-算法) 部分) - -以下接口,均提供为国内示例,海外用户请参考[海外 API 说明](#海外-api-说明)。 - -## 流程 -1. 移动端用 SDK 的 TapTap 登录,可以通过 `GetAccessToken` 获取 AccessToken,里面包含 - - ```java - public String kid; - public String access_token; - public String token_type; - public String mac_key; - public String mac_algorithm; - public String expire_in; - private String json = null; - ``` - -2. 再把移动端获取的参数发到游戏务服务器,服务端签算 mac token。 -3. 请求 `https://tds-tapsdk.cn.tapapis.com/api/v1/user/info` , header 携带 mac token - -注意:当前实际返回的 kid 和 access_token 值相等,建议使用 access_token - -## API - -### 获取当前账户详细信息 - -> GET https://tds-tapsdk.cn.tapapis.com/api/v1/user/info?client_id=xxx
    Authorization mac token - - -#### 请求参数 - -| 字段 | 类型 | 说明 | -| --------- | ------ | ------ | -| client_id | string | 该应用的 `Client ID`,应与约定相同 | - -#### 响应参数 - -字段 | 类型 | 说明 ---------------- | ------------- | ------------ -user_id | string | tds id,用户唯一标识 -name | string | 用户名 -avatar | string | 用户头像图片 -gender   | int  | UNKNOWN = 0;
    MALE = 1;
    FEMALE = 2 -is_guest   | bool  | 是否是游客,暂时弃用 - - -#### 请求示例 -替换其中的 `MAC id` 和 `Client ID` 为自己签算的 mac token 和控制台的 `Client ID` - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://tds-tapsdk.cn.tapapis.com/api/v1/user/info?client_id=" -``` - - - - - -## 其他 - -### MAC Token 算法 - -MAC Token 包含以下字段: - -| 字段 | 类型 | 说明 | -| ------------- | ------ | ------------------------------- | -| kid | string | mac_key id, The key identifier. | -| access_token | string | 该字段暂无作用 | -| token_type | string | Token 类型,如 mac | -| mac_key | string | mac 密钥 | -| mac_algorithm | string | mac 计算的算法名称 hmac-sha-1 | - -使用 Mac Token 签算一个接口: - -### 脚本请求示例 -可用此脚本验证直接替换参数,用来验证自己服务端签算的 mac token 是否正确 -CLIENT_ID 替换为控制台获取的 `Client ID`,ACCESS_TOKEN 和 MAC_KEY 为客户端登录成功后的 `access_token`、`mac_key` -``` -#!/usr/bin/env bash - -# 客户端 ID -CLIENT_ID="请替换为控制台的 `Client ID`" -# SDK 获取的 access_token -ACCESS_TOKEN="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" -# SDK 获取的 mac_key -MAC_KEY="mSUQNYUGRBPXyRyW" - -# 随机数,正式上线请替换 -NONCE="abcdef" -# 当前时间戳 -TS=$(date +%s) - -# 请求方法 -METHOD="GET" -# 请求地址 (带 query string) -REQUEST_URI="/api/v1/user/info?client_id=${CLIENT_ID}" -# 请求域名 -REQUEST_HOST="tds-tapsdk.cn.tapapis.com" - -MAC=$(printf "%s\n%s\n%s\n%s\n%s\n443\n\n" "${TS}" "${NONCE}" "${METHOD}" "${REQUEST_URI}" "${REQUEST_HOST}" | openssl dgst -binary -sha1 -hmac ${MAC_KEY} | base64) - -AUTHORIZATION=$(printf 'MAC id="%s",ts="%s",nonce="%s",mac="%s"' "${ACCESS_TOKEN}" "${TS}" "${NONCE}" "${MAC}") - -curl -s -H"Authorization:${AUTHORIZATION}" "https://tds-tapsdk.cn.tapapis.com/api/v1/user/info?client_id=${CLIENT_ID}" -``` - -### nodejs 请求示例 - -```javascript -const urllib = require('urllib'); -var format = require('string-format'); -const utils = require('./utils'); -/** -TapSDK 登录后信息获取 -**/ -var kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"; -var mac_key = "mSUQNYUGRBPXyRyW"; -var nonce = "adssd"; -var client_id = "0RiAlMny7jiz086FaU"; - - -var ts = Math.ceil(Date.now() / 1000); -var ext = ""; -var signArray = [ts, nonce, 'GET', '/api/v1/user/info?client_id=' + client_id, 'tds-tapsdk.cn.tapapis.com', 443, ext]; - -var mac = utils.hmacSha1(signArray.join("\n")+"\n", mac_key); -var auth = format('MAC id={id},ts={ts},nonce={nonce},mac={mac}', { - id: '\"'+kid+'\"', - ts: '\"'+ts+'\"', - nonce: '\"'+nonce+'\"', - mac: '\"'+mac+'\"' -}); - -var headers = { - Authorization: auth -} - -var reqData = { - method: "GET", - headers: headers -} - -urllib.request("https://tds-tapsdk.cn.tapapis.com/api/v1/user/info?client_id=" + client_id, reqData, - (err, data, response) => { - if(!err){ - console.log("返回数据:" + data.toString()); - } - }); - -``` - -```javascript -//utils -var crypto = require('crypto'); - -exports.base64ToUrlSafe = function (v) { - return v.replace(/\//g, '_').replace(/\+/g, '-'); -}; - -exports.urlsafeBase64Encode = function (jsonFlags) { - var encoded = Buffer.from(jsonFlags).toString('base64'); - return exports.base64ToUrlSafe(encoded); -}; - -exports.hmacSha1 = function (encodedFlags, secretKey) { - var hmac = crypto.createHmac('sha1', secretKey); - hmac.update(encodedFlags); - return hmac.digest('base64'); -}; -``` - -### java 请求示例 - -```java -package com.taptap; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.*; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -public class Authorization { - public static void main(String[] args) throws IOException { - String client_id = "0RiAlMny7jiz086FaU"; - String kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"; // kid - String mac_key = "mSUQNYUGRBPXyRyW"; // mac_key - String method = "GET"; - String request_url = "https://tds-tapsdk.cn.tapapis.com/api/v1/user/info?client_id=" + client_id; // - String authorization = getAuthorization(request_url, method, kid, mac_key); - System.out.println(authorization); - URL url = new URL(request_url); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - // Http - conn.setRequestProperty("Authorization", authorization); - conn.setRequestMethod("GET"); - BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String line; - StringBuilder result = new StringBuilder(); - while ((line = rd.readLine()) != null) { - result.append(line); - } - rd.close(); - System.out.println(result.toString()); - } - /** - * @param request_url - * @param method "GET" or "POST" - * @param key_id key id by OAuth 2.0 - * @param mac_key mac key by OAuth 2.0 - * @return authorization string - */ - public static String getAuthorization(String request_url, String method, String key_id, String - mac_key) { - try { - URL url = new URL(request_url); - String time = String.format(Locale.US, "%010d", System.currentTimeMillis() / 1000); - String randomStr = getRandomString(5); - String host = url.getHost(); - String uri = request_url.substring(request_url.lastIndexOf(host) + host.length()); - String port = "80"; - if (request_url.startsWith("https")) { - port = "443"; - } - String other = ""; - String sign = sign(mergeSign(time, randomStr, method, uri, host, port, other), mac_key); - return "MAC " + getAuthorizationParam("id", key_id) + "," + getAuthorizationParam("ts", time) - + "," + getAuthorizationParam("nonce", randomStr) + "," + getAuthorizationParam("mac", - sign); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - return null; - } - private static String getRandomString(int length) { //length - String base = "abcdefghijklmnopqrstuvwxyz0123456789"; - Random random = new Random(); - StringBuffer sb = new StringBuffer(); - for (int i = 0; i < length; i++) { - int number = random.nextInt(base.length()); - sb.append(base.charAt(number)); - } - return sb.toString(); - } - private static String mergeSign(String time, String randomCode, String httpType, String uri, - String domain, String port, String other) { - if (time.isEmpty() || randomCode.isEmpty() || httpType.isEmpty() || domain.isEmpty() || port.isEmpty()) - { - return null; - } - String prefix = - time + "\n" + randomCode + "\n" + httpType + "\n" + uri + "\n" + domain + "\n" + port - + "\n"; - if (other.isEmpty()) { - prefix += "\n"; - } else { - prefix += (other + "\n"); - } - return prefix; - } - private static String sign(String signatureBaseString, String key) { - try { - SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(signingKey); - byte[] text = signatureBaseString.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = mac.doFinal(text); - signatureBytes = Base64.getEncoder().encode(signatureBytes); - return new String(signatureBytes, StandardCharsets.UTF_8); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new IllegalStateException(e); - } - } - private static String getAuthorizationParam(String key, String value) { - if (key.isEmpty() || value.isEmpty()) { - return null; - } - return key + "=" + "\"" + value + "\""; - } -} - -``` - - - -### 通用接口错误信息 - -**统一格式** - -| 字段 | 类型 | 说明 | -| ----------------- | ------ | ---------------------------------------------------- | -| code | int | 预留字段,用于以后追踪问题 | -| error | string | 错误码,代码逻辑判断时使用 | -| error_description | string | 错误描述信息,开发的时候用来帮助理解和解决发生的错误 | - - -**错误响应** - -| 错误码 | 详细描述 | -| -------------------------| ------------------------------------------------------------ | -| invalid_request | 请求缺少某个必需参数,包含一个不支持的参数或参数值,或者格式不正确 | -| invalid_time | MAC Token 算法中,ts 时间不合法,**应请求服务器时间重新构造** | -| invalid_client | client_id 参数无效 | -| access_denied | 授权服务器拒绝请求 **这个状态出现在拿着 token 请求用户资源时,如出现,客户端应退出本地的用户登录信息,引导用户重新登录** | -| forbidden | 用户没有对当前动作的权限,**引导重新身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交** | -| not_found | 请求失败,请求所希望得到的资源未被在服务器上发现。**在参数相同的情况下,不应该重复请求** | -| server_error | 服务器出现异常情况 **可稍等后重新尝试请求,但需有尝试上限,建议最多 3 次,如一直失败,则中断并告知用户** | - -### 海外 API 说明 -当移动端初始化为海外时 -```cs -TapConfig tapConfig = new TapConfig("your-client-id", false); // true 表示国内,false 表示国外 -TapBootstrap.Init(tapConfig); -``` -登录即为海外,服务端文档以上流程不变,变更海外域名即可 - -#### 海外域名: -##### main domain - - host: tds-tapsdk0.intl.tapapis.com - - host: tds-tapsdk1.intl.tapapis.com - - host: tds-tapsdk2.intl.tapapis.com - -##### backup domain - - host: tds-tapsdk-b0.intl.tapapis.com - - host: tds-tapsdk-b1.intl.tapapis.com diff --git a/versioned_docs/version-v2/sdk/02-taptap-login/02-guide/_category_.json b/versioned_docs/version-v2/sdk/02-taptap-login/02-guide/_category_.json deleted file mode 100644 index 586e68f44..000000000 --- a/versioned_docs/version-v2/sdk/02-taptap-login/02-guide/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/02-taptap-login/03-best-practice.mdx b/versioned_docs/version-v2/sdk/02-taptap-login/03-best-practice.mdx deleted file mode 100644 index 3d6e36870..000000000 --- a/versioned_docs/version-v2/sdk/02-taptap-login/03-best-practice.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: best-practice -title: 最佳实践 -sidebar_label: 最佳实践 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -## 设计简短的登录流程 - -玩家在登录时,操作的步骤越少,路径越短,则转化率越高。建议使用相对简短的引导,让玩家能快速进入游戏。如图所示: - - - -## 优化玩家账号体验 - -### 绘制易识别易操作的登录界面 - -为用户提供一个 TapTap 登录 Button,Button 的最佳显示方式为:依照提供的 [UI 规范](/design),完整展示 TapTap 品牌名称与 LOGO;可参考 [功能介绍](/v2/sdk/taptap-login/guide/start) 中单个登录与多个登录的展现方式 - - - -### 向玩家提供切换账号功能 - -- 当玩家需要更换账号时,建议为玩家提供切换账号的功能。玩家切换账号时,务必使用 logout 接口,以保证登录账号与其他游戏服务(动态)账号保持一致; -- 当玩家已经完成了登出的操作,为玩家自动显示出登录的界面,让玩家可以使用另一个账号登录 - - -### 在游戏内使用玩家的公开信息 - -玩家在游戏内创建角色时,可以直接使用玩家在 TapTap 上的公开信息,包括头像、昵称,帮助玩家自动完成填写流程。 - - - -### 提供账号绑定功能 - -为了避免用户账号丢失,请开发者在服务端记录 Tap ID 与 游戏 ID 、游戏 ID 与区服的匹配关系;当用户清除本地游戏数据后,使用相同的 TapTap ID 登录,依然能够载入上次登录的游戏进度; - -为了更好的用户体验,在您的游戏中添加账号绑定功能,为用户提供多种登录方式; - -- 其他登录方式的最佳绑定流程为: - - 使用非 TapTap 账号登录游戏的,在内嵌动态中登录 TapTap 账号后,将获得的 tds id 与游戏账号进行绑定; - - 在用户中心,提供“绑定 TapTap 账号”的功能; -- 游客账号的最佳绑定流程为:在用户中心,提供“绑定 TapTap 账号”的功能 - - -## 妥善处理敏感信息,提高客户端安全性 - -1、AppId / AppKey 不要存储在 apk 里,这样很容易被反编译; - -2、提交给 TapTap 平台的游戏包名必须是唯一的,且游戏上线后不要轻易修改,以免影响线上用户的体验; \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/02-taptap-login/_category_.json b/versioned_docs/version-v2/sdk/02-taptap-login/_category_.json deleted file mode 100644 index 5db689242..000000000 --- a/versioned_docs/version-v2/sdk/02-taptap-login/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "TapTap 登录", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/03-tapdb/01-features.mdx b/versioned_docs/version-v2/sdk/03-tapdb/01-features.mdx deleted file mode 100644 index 1465136b2..000000000 --- a/versioned_docs/version-v2/sdk/03-tapdb/01-features.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -id: features -title: TapDB 功能介绍 -sidebar_label: 功能介绍 ---- - -TapDB 是一套专注于解决游戏项目数据需求的分析工具,致力于帮开发者实现低成本、高效率的接入与查询体验。 - -## 初衷 - -同为游戏开发者,我们深知做游戏的不易。思考如何打磨游戏产品就足够令人头疼了,选择一套适合自己的数据工具并正确地用起来可能会耗费相当的精力且效果一般。于是我们相信提供一套低门槛又足够好用、精确的工具一定是有高价值的。 - -在做游戏的这些年,我们积累了一些自己认可的经验和方法,希望可以通过 TapDB 这个产品将这些经验和能力共享给大家。我们坚信 TapTap 和良好的行业生态具有共生性:帮助开发者创造优质的游戏就是在帮助 TapTap 成长。 - -## 实用功能 - -TapDB 服务提供的主要功能有: - -### 基础 BI - -非常实用、无门槛上手的四大数据报表,沉淀着我们数十年的游戏从业经验。 - -### 衍生事件 - -为了开发者的使用体验,我们通过一系列衍生事件将基础 BI 的查询时间缩短至 1/10 不到,确保你能快速访问所需要的数据。 - -### 广告投放跟踪 - -对接了全球主流广告投放系统(如巨量引擎、AMS、AppsFlyer 等),轻松完成广告投放追踪、回调以及数据分析。 - -### 权限角色 - -给每个使用者配备独立账号并单独控制权限,确保数据安全。 - -### 自定义事件分析 - -- 自由定制你所想要的事件,关注那些你最关心的行为。 - -- 多维透视,快速拆解数据,助你更高效地找到问题根源。 - -- 日志级查询能力,用户做了什么了如指掌。 - -## 优势和特色 - -- 低门槛接入:接入基础事件非常简单,而这已足够让你获得非常完善的分析和广告投放能力。 - -- 完全无延迟:上报后可以立刻查到数据,时间就是生命。 - -- 保持更新的游戏分析框架:我们会将最新的经验和方法持续地分享给你。 - -- 结合 TapTap 生态的数据,为开发者提供全链路的游戏数据分析能力(敬请期待)。 - -- 免费使用。 - diff --git a/versioned_docs/version-v2/sdk/03-tapdb/02-guide.mdx b/versioned_docs/version-v2/sdk/03-tapdb/02-guide.mdx deleted file mode 100644 index 949e4f288..000000000 --- a/versioned_docs/version-v2/sdk/03-tapdb/02-guide.mdx +++ /dev/null @@ -1,638 +0,0 @@ ---- -id: guide -title: 开发指南 -sidebar_label: 开发指南 ---- - - -import MultiLang from '/src/docComponents/MultiLang'; - -## 介绍 - -TapSDK 提供了一套可供游戏开发者收集账号数据的 API。 -系统会收集账号数据并进行分析,最终形成数据报表,帮助游戏开发者分析账号行为并优化游戏。 - -## SDK 获取 - -请先[下载](/tap-download) SDK,并添加相关依赖 . -如果只需要单独使用 TapDB,可以只依赖 `common+tapdb` - - - -```cs -"dependencies":{ -// 登录 -"com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#2.1.8", -"com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#2.1.8", -"com.taptap.tds.bootstrap":"https://github.com/TapTap/TapBootstrap-Unity.git#2.1.8", -// 数据分析 -"com.taptap.tds.tapdb": "https://github.com/TapTap/TapDB-Unity.git#2.1.8", -} -``` - -```java -repositories{ - flatDir { - dirs 'libs' - } -} - -dependencies { -... - implementation (name:'TapBootstrap_2.1.8', ext:'aar') // 必选: TapSDK 启动器 - implementation (name:'TapCommon_2.1.8', ext:'aar') // 必选:TapSDK 基础库 - implementation (name:'TapLogin_2.1.8', ext:'aar') // 必选:TapTap 登录 - implementation (name:'TapDB_2.1.8', ext:'aar') // 数据统计 -} -``` - -```objectivec -// 登录 -TapBootstrapSDK.framework -TapCommonResource.bundle -TapLoginResource.bundle -TapCommonSDK.framework -TapLoginSDK.framework -//TapDB -TapDB.framework -``` - - - -## 初始化 SDK - -:::info -以下两种初始化方式结合使用场景任选其一即可。 -::: - -### TapSDK 初始化 - -在 TapSDK 初始化时,同步初始化 TapDB。 - - - -```cs -TapConfig tapConfig = new TapConfig.Builder() - .ClientID("clientId")// 必须 - .ClientSecret("client_secret")// 必须 - .RegionType(RegionType.CN)// 非必须,默认 CN - .TapDBConfig(true, "gameChannel", "gameVersion", true) // TapDB 会根据 TapConfig 的配置进行自动初始化 - .ConfigBuilder(); - -TapBootstrap.Init(tapConfig); -``` - -```java -TapDBConfig tapDBConfig = new TapDBConfig(); - tapDBConfig.setEnable(true); - tapDBConfig.setChannel("gameChannel"); - -TapConfig tapConfig = new TapConfig.Builder() - .withAppContext(getApplicationContext()) - .withRegionType(TapRegionType.CN) // TapRegionType.CN: 国内 TapRegionType.IO: 国外 - .withClientId("clientId") - .withClientSecret("clientSecret") - .withTapDBConfig(tapDBConfig) - .build(); -TapBootstrap.init(MainActivity.this, tapConfig); -``` - -```objectivec - // 初始化 SDK - TapConfig *config = TapConfig.new; - config.clientId = @"clientId"; - config.clientSecret=@"clientSecret"; - - TapDBConfig * dbConfig = [[TapDBConfig alloc]init]; - dbConfig.enable = YES; - dbConfig.channel=@"taptap"; - dbConfig.gameVersion=@"1.0.0"; - dbConfig.advertiserIDCollectionEnabled=YES; - config.dbConfig = dbConfig; - - config.region = TapSDKRegionTypeCN; - [TapBootstrap initWithConfig:config]; -``` - - - -### TapDB 单独使用 - -在单独使用 TapDB 功能时(即不接登录功能时,不导入 TapBootstrap 包时),可以通过以下方式初始化 TapDB。 - - - -```cs -TapDB.Init("clientId", "taptap", "gameVersion", true); -``` - -```java -TapDB.init(getApplicationContext(), "clientId", "taptap", "gameVersion", true); -``` - -```objectivec -[TapDB onStartWithClientId:@"clientid" channel:@"taptap" version:@"gameVersion" isCN:YES]; -``` - - - -上述代码示例中,`clientId` 可以在控制台获取,`taptap` 为分包渠道(游戏安装包渠道),`gameVersion` 为游戏版本号,最后一个参数表示区域,`true` 表示中国大陆,`false` 表示国际。 -分包渠道和游戏版本号的长度不大于 256,可以为 `null`。 -分包渠道为 `null` 时,就无法根据渠道筛选收集到的数据了。 -游戏版本号为 `null` 时,TapSDK 会自动获取游戏安装包的版本。 - -## 设置获取 IDFA - -针对 `iOS14.5+`,可以设置是否获取 IDFA。 -默认不获取 IDFA,如果设置获取 IDFA,还需要在应用层额外配置相关弹窗权限。 - - - -```cs -TapDB.AdvertiserIDCollectionEnabled(true); -``` - -```java -// Android 平台不适用 -``` - -```objectivec -[TapDB setAdvertiserIDCollectionEnabled:YES]; -``` - - - -## 设置账号 - -调用该 API 记录一个账号,当账号登录时调用。 - - - -```cs -TapDB.SetUser("userId"); -``` - -```java -TapDB.setUser("userId"); -``` - - -```objectivec -[TapDB setUser:@"userId"]; -``` - - - -`userId` 是代表账号的唯一字符串,字符串长度不大于 256,只能包含数字、大小写字母、下划线(`_`)、短横(`-`)。 -开发者需要保证不同账号的 `userId` 均不相同。 - -## 账号昵称 - -游戏设置账号昵称时调用。 - - - -```cs -TapDB.SetName("Tarara"); -``` - -```java -TapDB.setName("Tarara"); -``` - -```objectivec -[TapDB setName:@"Tarara"]; -``` - - - -参数的类型为字符串,不可为空,长度不大于 256。 - -## 账号等级 - -设置账号等级,通常在账号升级时调用。 - - - -```cs -TapDB.SetLevel(5); -``` - -```java -TapDB.setLevel(5); -``` - - -```objectivec -[TapDB setLevel:5]; -``` - -等级参数为整型,不可为空,大于等于 0。 - - - -## 账号所在服务器 - -通常在账号登录或切换服务器时调用。 - - - -```cs -TapDB.SetServer("1 区"); -``` - -```java -TapDB.setServer("1 区"); -``` - -```objectivec -[TapDB setServer:@"1 区"]; -``` - - - -服务器参数为非空字符串,长度不大于 256。 - - -## 充值 - -充值成功时调用。 -可以选择通过客户端 SDK 推送和通过服务端推送。 -建议优先选择服务端推送方式,以保证数据的准确性。 - -### 客户端推送 - - - -```cs -TapDB.OnCharge("0xueiEns", "轩辕剑", "100", "CNY", "wechat", "{\"on_sell\":true}"); -``` - -```java -JSONObject info = new JSONObject(); -info.put("on_sell": true); -TapDB.onCharge("0xueiEns", "轩辕剑", "100", "CNY", "wechat", info); -``` - -```objectivec -[TapDB onChargeSuccess:@"0xueiEns" product:@"轩辕剑" amount:100 currencyType:@"CNY" payment:@"wechat", properties:@{@"on_sell":YES}]; -``` - - - -上述代码示例中,`0xueiEns` 是订单 ID,`轩辕剑` 是商品名,`100` 是金额,`CNY` 是货币名称,`wechat` 是充值渠道,最后一个参数传入自定义字段,示例中传入了一个 `on_sell` 字段,表示这个商品是否正在促销。 -订单 ID、商品名、充值渠道是长度大于 0、不大于 256 的字符串,可以为 `null`。 -传递订单 ID 方便排重,防止重复计算。 -金额是大于 0、小于等于 100000000000 的整数,单位为分(主币价值的百分之一),不可为 `null`。 -货币名称由三位字母组成,遵循 ISO 4217 标准,为 `null` 时表示使用默认值 `CNY`。 - -### 服务端推送 - -由于客户端推送可能会不太准确,因此推荐通过服务端推送。 - -请求地址:`https://e.tapdb.net/event` - -POST 数据(实际发送请求时需去除注释、空格、换行符并 urlencode): - -```json -{ - "module": "GameAnalysis", // 固定值 - "ip": "172.16.254.1", // 可选,充值账号的 IP - "name": "charge", // 固定值 - // 必需,需要替换成 client id - "client_id": "CLIENTID", - // 必需,账号 ID。 - // 必须和 SDK 的 setUser 接口传递的账号 ID 相一致, - // 并且对应账号已经通过 SDK 接口进行过推送。 - "identify": "userId", - "properties": { // 详见上节「客户端推送」的说明 - "order_id": "0xueiEns", // 可选,订单 ID - "amount": 100, // 必需,金额 - "currency_type": "CNY", // 可选,货币名称 - "product": "轩辕剑", // 可选,商品名 - "payment": "wechat" // 可选,充值渠道 - "on_sell": true // 自定义字段 - } -} -``` - -返回的 HTTP Code 为 200 时发送成功,否则发送失败。 - -## 自定义事件 - -需要发送自定义事件时调用,自定义事件的 eventName 和 properties 属性都必须在元数据管理预先配置,才可以使用 SDK 进行发送 - -用户可以通过调用 trackEvent 方法上传需要跟踪的自定义事件。eventName 为自定义事件的事件名,需要保证以 '#' 开头,取值规则请参考自定义属性登记页面。properties 为自定义事件所包含的自定义属性(以 Key : Value 的形式保存),其中 Key 代表了自定义属性的属性名,Value 代表了该属性的值。这里需要注意的是 Key 的命名规则同 eventName 一致,也需要保证以 '#' 开头。目前所支持的 Value 类型为 String, Number, Boolean。String 类型支持最大长度为 256。Number 类型取值区间为 [-9E15, 9E15]。以战斗事件为例: - - - -```cs -TapDB.TrackEvent("eventName", "{\"weapon\":\"axe\"}"); -``` - -```java -JSONObject properties = new JSONObject(); -properties.put("#weapon", "axe"); -properties.put("#level", 10); -properties.put("#map", "atrium"); -TapDB.trackEvent("#battle", properties); -``` - -```objectivec - NSDictionary* dic = @{@"aaa":@"xxx",@"bbb":@"yyy"}; -[TapDB trackEvent:@"testEvent2" properties:dic]; -``` - - - - -## 设置通用事件属性 - -对于某些重要的属性需要在每个上传的事件中出现,用户可以将这些属性设置为全局通用的自定义属性,包括静态通用属性和动态通用属性,静态通用属性为固定值,动态通用属性每次获取的值由用户所设置的计算逻辑产生。这些通用属性在注册之后,会被附带在 TapDB 上传的事件中。这里需要注意 trackEvent 中传入的属性优先级 > 动态通用属性优先级 > 静态通用属性优先级,也就是说动态通用属性会覆盖同名的静态通用属性。trackEvent 中的属性会覆盖同名的动态通用属性和静态通用属性。 - -### 添加静态通用属性 - -例如,添加来源渠道: - - - -```cs -string properties = "{\"channel\":\"TapDB\"}"; -TapDB.RegisterStaticProperties(properties); -``` - -```java -JSONObject commonProperties = new JSONObject(); -commonProperties.put("channel", "TapDB"); -TapDB.registerStaticProperties(properties); -``` - -```objectivec -[TapDB registerStaticProperties:@{@"channel":@"TapDB"}]; -``` - - - -### 删除静态通用属性 - -删除单个已添加的事件属性: - - - -```cs -TapDB.UnregisterStaticProperty("channel"); -``` - -```java -TapDB.unregisterStaticProperty("channel"); -``` - -```objectivec -[TapDB unregisterStaticProperty:@"channel"]; -``` - - - -删除所有事件属性: - - - -```cs -TapDB.ClearStaticProperties(); -``` - -```java -TapDB.clearStaticProperties(); -``` - -```objectivec -[TapDB clearStaticProperties]; -``` - - - -### 添加动态通用属性 - -如果需要添加的通用属性的值在不同的上传事件中具有动态的赋值逻辑,那么可以调用 registerDynamicProperties 方法,注册相应的取值逻辑。以用户事件调用当前等级为例: - - - -```cs -public class TapDBDynamicPropertiesImpl : IDynamicProperties -{ - public Dictionary GetDynamicProperties() - { - Dictionary dic = new Dictionary(); - dic["#currentLevel"] = level; - return dic; - } -} -TapDB.RegisterDynamicProperties(new TapDBDynamicPropertiesImpl()); -``` - -```java - -TapDB.registerDynamicProperties( - () -> { - JSONObject properties = new JSONObject(); - // getCurrentLevel 在这里仅作为案例,表示用户任何的自有逻辑实现 - long level = getCurrentLevel(); - properties.put("#currentLevel", level); - return properties; - } -); -``` - -```objectivec -[TapDB registerDynamicProperties:^NSDictionary *_Nonnull { - return @{ - @"#currentLevel": level - }; - }]; -``` - - - - - - -## 事件主体操作 - -TapDB 目前支持两个事件主体:设备,账号。相应支持主体属性的操作为初始化,更新和累加。累加操作只支持数值类型。需要注意的是,传入的自定义属性需要同预登记属性名保持一致。 - -### 初始化 - -初始化操作用于初始化属性。 -已初始化的属性,后续的初始化操作会被忽略。 -以上报首次活跃服务器为例: - - - -```cs -string properties = "{\"firstActiveServer\":\"server1\"}"; -TapDB.DeviceInitialize(properties); - -string properties = "{\"firstActiveServer\":\"server2\"}"; -TapDB.DeviceInitialize(properties); -``` - -```java -JSONObject properties = new JSONObject(); -properties.put("firstActiveServer", "server1"); -TapDB.deviceInitialize(properties); - -properties.put("firstActiveServer", "server2"); -TapDB.deviceInitialize(properties); -``` - -```objectivec -[TapDB deviceInitialize:@{@"firstActiveServer":@"server1"}]; - -[TapDB deviceInitialize:@{@"firstActiveServer":@"server2"}]; -``` - - - -运行上述代码后,设备表的 `firstActiveServer` 字段值仍为 `server1`。 - -### 更新 - -更新操作用于更新属性。 -该操作会覆盖原属性值。 -以上报当前点数为例: - - - -```cs -string properties = "{\"currentPoints\":10}"; -TapDB.DeviceUpdate(properties); - -properties = "{\"currentPoints\":42}"; -TapDB.DeviceUpdate(properties); -``` - -```java -JSONObject properties = new JSONObject(); -properties.put("currentPoints", 10); -TapDB.deviceUpdate(properties); - -properties.put("currentPoints", 42); -TapDB.deviceUpdate(properties); -``` - -```objectivec -[TapDB deviceUpdate:@{@"currentPoints":@10}]; - -[TapDB deviceUpdate:@{@"currentPoints":@42}]; -``` - - - -运行上述代码后,设备表的 `currentPoints` 字段值为 `42`。 - -### 累加 - -累加操作用于增减属性,目前只支持数字属性。 -该操作会在原属性值基础上累加数值,原属性不存在时,原属性值计为 0. -以上报总点数为例: - - - -```cs -string properties = "{\"totalPoints\":10}"; -TapDB.DeviceAdd(properties); - -properties = "{\"totalPoints\":-2}"; -TapDB.DeviceAdd(properties); -``` - -```java -JSONObject properties = new JSONObject(); -properties.put("totalPoints", 10); -TapDB.deviceAdd(properties); - -properties.put("totalPoints", -2); -TapDB.deviceAdd(properties); -``` - -```objectivec -[TapDB deviceAdd:@{@"totalPoints":@10}]; - -[TapDB deviceAdd:@{@"totalPoints":@(-2)}]; -``` - - -运行上述代码后,设备表的 `totalPoints` 字段值为 `8`。 - -上述代码示例中,属性值为整数。 -累加操作也支持浮点数,不过浮点数相加有精度问题,开发者还需留意。 - -初始化、更新、累加操作同样适用于账号主体: - - - -```cs -TapDB.UserInitialize(properties); -TapDB.UserUpdate(properties); -TapDB.UserAdd(properties); -``` - -```java -TapDB.userInitialize(properties); -TapDB.userUpdate(properties); -TapDB.userAdd(properties); -``` - -```objectivec -[TapDB userInitialize:@{@"firstActiveServer":@"server1"}]; -[TapDB userUpdate:@{@"currentPoints":@10}]; -[TapDB userAdd:@{@"totalPoints":@10}]; -``` - - -## 服务端在线人数推送 - -游戏服务端自行统计在线人数,每隔 5 分钟向 `https://se.tapdb.net/tapdb/online` 发送 POST 请求,请求内容为 json 格式的数据,包含以下信息: - -参数名 | 参数类型 | 参数说明 -| ------ | ------ | ------ | -client_id | string | 游戏的 client id -onlines | array | 多条在线数据(最多 100 条) - -其中 `onlines` 数组元素的结构为: - -参数名 | 参数类型 | 参数说明 -| ------ | ------ | ------ | -server | string | 服务器。自然时间 5 分钟内,TapDB 对同一服务器仅接受一次数据。 -online | int | 在线人数 -timestamp | long | 当前统计数据的时间戳(秒)。TapDB 会按照自然时间 5 分钟对齐数据。 - -示例: - -```json -{ - "client_id":"gkjasd13bbsa1sdk", - "onlines":[{ - "server":"s1", - "online":123, - "timestamp":1489739590 - },{ - "server":"s2", - "online":188, - "timestamp":1489739560 - }] -} -``` - -返回的 HTTP Code 为 200 时发送成功,否则发送失败。 - -注意,因为请求内容为 json 格式数据,所以发送请求时别忘了加上 `Content-Type: application/json` 的 HTTP 头。 - -## 收集设备指纹 - -### OAID - -当需要采集设备指纹时,需要引入 OAID. -TapSDK 支持的 OAID 版本为 `1.0.5-1.0.25`。 - -当游戏集成的其他 SDK 引入了 OAID,TapSDK 里面无需引入,可以直接使用。 \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/03-tapdb/_category_.json b/versioned_docs/version-v2/sdk/03-tapdb/_category_.json deleted file mode 100644 index be4419bc7..000000000 --- a/versioned_docs/version-v2/sdk/03-tapdb/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "数据分析", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/04-embedded-moments/01-features.mdx b/versioned_docs/version-v2/sdk/04-embedded-moments/01-features.mdx deleted file mode 100644 index 0a5ce147b..000000000 --- a/versioned_docs/version-v2/sdk/04-embedded-moments/01-features.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -id: features -title: 功能介绍 -sidebar_label: 功能介绍 ---- -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; -import useBaseUrl from '@docusaurus/useBaseUrl'; - -:::info -如需配置[主题](#内嵌动态主题配置),可参考[动态设计规范](/design/design-moment) -::: - -内嵌动态能让玩家在游戏内访问 TapTap 社区,同时也可以查看好友的游戏动态,参与其他玩家、官方和大神之间的互动。 - - - - -## Web 层面功能介绍 -### 全新首页 -玩家在新版首页信息流可以浏览更多推荐内容,游戏官方可以通过推荐位和运营位更好地进行内容运营。 - -![](/img/moment-1.3.1.png) - -提示: - -> 未配置推荐位和运营位时,页面不做展示 - -> 推荐位配置后台位于论坛管理中心,运营位配置位于开发者中心 - - - -### 关注流和社区论坛板块 -玩家进入动态可以浏览的内容 - -![](/img/moment-1.3.2.png) - -提示: - -> 关注:已登录 TapTap 的用户,默认显示 用户在 TapTap 上关注的好友发布的动态 和 官方动态。 - -> 全部:未登录 TapTap 的用户,默认显示全部动态,互动时触发登录。 - -> 其他板块:和 TapTap 社区保持一致。 - - - -### 动态中的发布功能 -玩家可以在内嵌动态中发布图文动态,视频动态并进行转发 - - -![](/img/moment-1.3.3.png) - - -### 动态内互动 -玩家可以直接关注其他用户,也可以点赞、评论和转发动态。玩家之间的互动会触发通知,有助于玩家之间更好地建立联系和沉淀关系。 - -![](/img/moment-1.3.4.png) - -## SDK 层面功能介绍 -### 场景化入口 -游戏内绘制场景化入口按钮,游戏运营可以在内嵌动态后台实时更改玩家点击按钮后的落地页,帮助玩家更好地消费游戏与社区内容 - - -![](/img/moment-1.3.5.png) -![](/img/moment-1.3.6.png) - - - - -提示: - -> 开启场景化入口能力需要权限,相关事宜请联系 TapTap 技术对接 - - - -### 关注流新发布内容的小红点 - -![](/img/moment-1.3.7.png) - -提示: - -> 小红点对提升使用率至关重要,建议将入口放在显眼的位置。 - -> 玩家关注的用户发布了内容将触发消息通知,消息刷新间隔为 1 分钟一次。获取到通知消息后,可以在用户界面上用小红点提示用户有新消息。 - -> 玩家打开动态后游戏需要清除小红点。 - - - -### 一键发布 API -游戏内任意场景可触发,一键发布到 TapTap 社区(一键发布目前只支持图文动态)。 - - -![](/img/moment-1.3.12.png) - - -### 关闭动态的引导弹窗 -游戏厂商可以自定义引导弹窗文案和触发时机 - -![](/img/moment-1.3.8.png) - - - -## 后台功能介绍 -### 数据回收和查看 -在开发者中心 - 内嵌动态后台,点击右上角可直接跳转论坛管理中心,查看数据。 - -![](/img/moment-1.3.9.png) -提示: - -> 前往论坛管理中心,请确认账号是否有相应查看权限。 - - -### 内嵌动态主题配置 - -为了更好地配合游戏方做好内容运营,使内嵌动态更贴合游戏画风,降低玩家的割裂感,内嵌动态支持配置动态主题。游戏运营人员可以在开发者中心的内嵌动态后台自定义皮肤主题,以及内嵌动态的框体配色和背景图等。图片需要人工审核,一般会在 2 个工作日内审核完成。 - - - - -### 运营位和推荐位配置 -运营位配置位于开发者中心 - 内嵌动态后台,推荐位配置位于论坛管理中心。 - - diff --git a/versioned_docs/version-v2/sdk/04-embedded-moments/02-guide.mdx b/versioned_docs/version-v2/sdk/04-embedded-moments/02-guide.mdx deleted file mode 100644 index b7779c31c..000000000 --- a/versioned_docs/version-v2/sdk/04-embedded-moments/02-guide.mdx +++ /dev/null @@ -1,318 +0,0 @@ ---- -id: guide -title: 开发指南 -sidebar_label: 开发指南 ---- - - -import MultiLang from '/src/docComponents/MultiLang'; - -本文介绍如何在游戏中加入 [TapTap 动态](/v2/sdk/embedded-moments/features)。使用内嵌动态功能需依赖 TapTap 登录。 - -## SDK 获取 - -请先[下载](/tap-download) SDK,并添加相关依赖 - - - -```cs -"dependencies":{ -// 登录 -"com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#2.1.8", -"com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#2.1.8", -"com.taptap.tds.bootstrap":"https://github.com/TapTap/TapBootstrap-Unity.git#2.1.8", -// 动态 -"com.taptap.tds.moment":"https://github.com/TapTap/TapMoment-Unity.git#2.1.8", -} -``` - -```java -repositories{ - flatDir { - dirs 'libs' - } -} - -dependencies { -... - implementation (name:'TapBootstrap_2.1.8', ext:'aar') // 必选:TapSDK 启动器 - implementation (name:'TapCommon_2.1.8', ext:'aar') // 必选:TapSDK 基础库 - implementation (name:'TapLogin_2.1.8', ext:'aar') // 必选:TapTap 登录 - implementation (name:'TapMoment_2.1.8', ext:'aar') // TapTap 内嵌动态 -} -``` - -```objectivec -// 基础库 -TapBootstrapSDK.framework -TapCommonResource.bundle -TapLoginResource.bundle -TapCommonSDK.framework -TapLoginSDK.framework -// 内嵌动态 -TapMomentResource.bundle -TapMomentSDK.framework -``` - - - -## 设置回调 - -设置回调以获取动态的状态变化。 - - - -```cs -TapMoment.SetCallback((code, msg) => { - Debug.Log(code + "---" + msg); -}); -``` - -```java -TapMoment.setCallback(new TapMoment.TapMomentCallback() { - @Override - public void onCallback(int code, String msg) { - - } -}); -``` - -```objectivec -@interface ViewController () -@end - -[TapMoment setDelegate:self]; -- (void)onMomentCallbackWithCode:(NSInteger)code msg:(NSString *)msg -{ - NSLog (@"msg:%@, code:%i", msg, code); -} -``` - - - -回调方法中 code 表示事件类型,现支持的回调类型如下: - -回调 | 回调值 | 说明 | - ----------- | --- | -------- | -CALLBACK_CODE_PUBLISH_SUCCESS | 10000 | 动态发布成功 | -CALLBACK_CODE_PUBLISH_FAIL | 10100 | 动态发布失败 | -CALLBACK_CODE_PUBLISH_CANCEL | 10200 | 关闭动态发布页面 | -CALLBACK_CODE_GET_NOTICE_SUCCESS | 20000 | 获取新消息成功 | -CALLBACK_CODE_GET_NOTICE_FAIL | 20100 | 获取新消息失败 | -CALLBACK_CODE_MOMENT_APPEAR | 30000 | 动态页面打开 | -CALLBACK_CODE_MOMENT_DISAPPEAR | 30100 | 动态页面关闭 | -CALLBACK_CODE_CLOSE_CANCEL | 50000 | 取消关闭所有动态界面(弹框点击取消按钮) | -CALLBACK_CODE_CLOSE_CONFIRM | 50100 | 确认关闭所有动态界面(弹框点击确认按钮) | -CALLBACK_CODE_LOGIN_SUCCESS | 60000 | 动态页面内登录成功 | -CALLBACK_CODE_SCENE_EVENT | 70000 | 场景化入口回调 | - -## 获取新消息 - -定时调用获取消息通知的接口,有新信息时可以在 TapTap 动态入口显示小红点,提醒玩家查看新动态。 - - - -```cs -TapMoment.FetchNotification(); -``` - -```java -TapMoment.fetchNotification(); -``` - -```objectivec -[TapMoment fetchNotification]; -``` - - - -获取消息通知的结果会在本文刚开始设置的回调中返回,`code` 为 `CALLBACK_CODE_GET_NOTICE_SUCCESS`(`20000`)表示获取成功,`CALLBACK_CODE_GET_NOTICE_FAIL`(`20100`)表示获取失败。 -获取成功时,`msg` 为新消息数量,`0` 表示没有新消息。 - -:::tip -为了方便玩家查看好友动态、游戏公告等,我们建议将 TapTap 动态入口放在显眼的位置,**每分钟调用 1 次**获取消息通知的接口。 - -获取消息通知时,如果没有新消息(`msg` 为 `0`),那么游戏需要清除界面上的小红点。 -同样,打开 TapTap 动态页面后,游戏也需要清除界面上的小红点。 -::: -## 显示动态页面 - -在游戏中显示 TapTap 动态页面,在这个页面,玩家不仅可以查看动态,还能发布新动态。 - - - -```cs -TapMoment.Open(TapSDK.Orientation.ORIENTATION_LANDSCAPE); -``` - -```java -TapMoment.open(TapMoment.ORIENTATION_PORTRAIT); -``` - -```objectivec -TapMomentConfig *mConfig = TapMomentConfig.new; -mConfig.orientation = TapMomentOrientationDefault; -[TapMoment open:mConfig]; -``` - - - - -:::note -打开动态页面时,请先屏蔽游戏自身的声音,以免干扰动态内的视频声音。 - -如需要动态能支持横竖屏随设备自动旋转,需要游戏自身能支持横竖屏。 - -如前所述,打开动态页面后别忘了清除动态页面入口处的小红点。 -::: - -动态页面的背景图可以配置,[点击查看图解](https://capacity-files.lcfile.com/Gboeocmn2zmPlN0779P1RHI4vj0AivGH/tap_moment_bg.png)。 -背景图需要人工审核后才能生效,请预留充足的时间。 - - -## 场景化入口 - -打开动态页面跳转到指定的页面 - -:::info -该功能当前处于测试阶段,如需开通,请联系商务 -::: - - - -```cs -var sceneDic = new Dictionary() { { TapMomentConstants.TapMomentPageShortCutKey, sceneId } }; -TapMoment.DirectlyOpen(Orientation.ORIENTATION_DEFAULT, TapMomentConstants.TapMomentPageShortCut, sceneDic); -``` - -```java -Map extras = new HashMap<>(); -// 注意:这里的 key 是固定的,"scene_id"; 第二个参数:开发者后台开启场景化入口并配置相关项后可以得到 -extras.put("scene_id", "xxxx"); -// 注意:第二个参数固定为 "tap://moment/scene/" -TapMoment.directlyOpen(TapMoment.ORIENTATION_DEFAULT,"tap://moment/scene/", extras); -``` - -```objectivec -TapMomentConfig *mConfig = TapMomentConfig.new; -mConfig.orientation = TapMomentOrientationDefault; -[TapMoment directlyOpen:mConfig page:TapMomentPageShortcut extras:@{ TapMomentPageShortcutKey: @"sceneid" }]; -``` - - - -#### 参数说明 -参数 | 说明 | - ----------- | -------- | -orientation | 打开方向 | -page | 固定为 TapMomentConstants.TapMomentPageShortCut | -Dictionary | 其中 TapMomentConstants.TapMomentPageShortCutKey 固定,第二个参数为要需要跳转的页面 id | - -### 场景化入口回调格式说明 - -**SDK 回调结构** - -字段名 | 值类型 |required | 说明 | - ----------- | -------- |-------- |-------- | -sceneId | 字符串 | 是 | 场景化入口 ID | -eventType | 字符串 | 是 | 枚举的事件类型,如 VIEW,FORWARD,VOTE 等 | -eventPayload | 字符串 | 是 | 根据类型自定义的 JSON 字符串 | -timestamp | 整数 | 是 | unix 时间戳,ms | - -**事件类型** - -eventType | eventPayload (未序列化) | 说明 - ----------- | -------- | -------- | - READY | {} | 已成功落地,将在 dom 挂载时触发(获取数据之前) | -REPOST | {} | 转发 | -VOTE | { isCancel: boolean } | 点赞(含是否取消),仅帖子本身 | -FOLLOW | { isCancel: boolean } | 关注(含是否取消),仅帖子本身 | -COMMENT | {} | 评论,仅帖子本身 | - -## 关闭动态页面 - -玩家可以在动态页面退出。 -但在特定场景下,游戏可能需要主动关闭动态页面。 - -比如,玩家排位等待结束,准备进入对局时提示玩家关闭动态页面,玩家确认后关闭。 - - - -```cs -TapMoment.Close("提示", "匹配成功,进入游戏"); -``` - -```java -TapMoment.closeWithConfirmWindow("提示", "匹配成功,进入游戏"); -``` - -```objectivec -[TapMoment closeWithTitle:@"提示" content:@"匹配成功,进入游戏" showConfirm:YES]; -``` - - - -用户的选择会通过回调返回: - -- `CALLBACK_CODE_CLOSE_CANCEL`(50000),表示玩家点了「取消」,选择不关闭动态页面。 -- `CALLBACK_CODE_CLOSE_CONFIRM`(50100),表示玩家点了「确认」,选择关闭动态页面。 - -如果需要直接关闭动态窗口,不弹出二次确认框: - - - -```cs -TapMoment.Close(); -``` - -```java -TapMoment.close(); -``` - -```objectivec -[TapMoment close]; -``` - - - -## 一键发布 - -:::info -这是可选功能,请根据项目需求决定是否在游戏中加入这一功能。 -::: - -我们推荐游戏让玩家直接在动态页面发布新动态。 -不过,SDK 也提供了发布图文动态的 API,以支持「一键发动态」等需求。 -图文动态包括单张或多张图片及相应的文字内容。 - - - -```cs -string content = "我是描述"; -string[] images = {"imgpath01","imgpath02","imgpath03"}; -TapMoment.Publish(Orientation.ORIENTATION_LANDSCAPE, images, content); -``` - -```java -int orientation = TapMoment.ORIENTATION_PORTRAIT; -String content = "描述"; -String[] imagePaths = new String[]{"content://hello.jpg", "/sdcard/world.jpg"}; -TapMoment.publish(orientation, imagePaths, content); -``` - -```objectivec -TapMomentConfig * tconfig = TapMomentConfig.new; -tconfig.orientation = TapMomentOrientationDefault; - -TapMomentImageData *postData = TapMomentImageData.new; -postData.images = @[@"file://..."]; -postData.content = @"我是图片描述"; -[TapMoment publish:tconfig content:(postData)]; -``` - - - -:::info -玩家在动态页面可以发布图文动态和视频动态。 -「一键发布」只支持发布图文动态。 -::: diff --git a/versioned_docs/version-v2/sdk/04-embedded-moments/_category_.json b/versioned_docs/version-v2/sdk/04-embedded-moments/_category_.json deleted file mode 100644 index 0fe30d1e3..000000000 --- a/versioned_docs/version-v2/sdk/04-embedded-moments/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "内嵌动态", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/05-copyright-verification/01-features.mdx b/versioned_docs/version-v2/sdk/05-copyright-verification/01-features.mdx deleted file mode 100644 index cb1ae32a8..000000000 --- a/versioned_docs/version-v2/sdk/05-copyright-verification/01-features.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: features -title: 功能介绍 -sidebar_label: 功能介绍 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -## 业务介绍 -TapTap 的正版验证服务适用于付费下载的游戏,当玩家在使用付费游戏时,校验玩家是否已经成功购买付费游戏。 - -## 开启正版验证 - -请确认在 TapTap 开发者中心的游戏服务中,已经完成了申请开启正版验证服务的操作。 - - - -## 售卖设置 - -TapTap 的正版验证服需与付费游戏的售卖设置配合使用。TapTap 为开发者和玩家提供游戏付费下载的服务,并支持多种支付渠道。开发者可在 [TapTap 开发者中心](https://developer.taptap.cn/)的售卖设置中,开启付费下载的服务,并设定游戏价格。如游戏有折扣活动,可以开启折扣功能并设定折扣时间与折扣价格。 - - - -## 集成正版验证 SDK - -游戏集成正版验证 SDK 后,当用户启动游戏时,SDK 会查询校验其购买结果。如已购买,则可正常进入游戏。如查询到未购买,则会提示用户进行购买。可参考[“开发流程”](/v2/sdk/copyright-verification/guide)进行集成。 diff --git a/versioned_docs/version-v2/sdk/05-copyright-verification/02-guide.mdx b/versioned_docs/version-v2/sdk/05-copyright-verification/02-guide.mdx deleted file mode 100644 index d4468d4ef..000000000 --- a/versioned_docs/version-v2/sdk/05-copyright-verification/02-guide.mdx +++ /dev/null @@ -1,129 +0,0 @@ ---- -id: guide -title: 开发指南 -sidebar_label: 开发指南 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - -import {Gray,Blue, Red, Black} from '/src/docComponents/doc'; - - -## 正版验证 - -为了游戏售卖服务的功能,避免 APK 流出导致盗版横行;当游戏准备在 TapTap 开放售卖时,可以接入此功能。 - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - -```json -"dependencies":{ -// 公共库 -"com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#{version}", -// 付费购买 -"com.taptap.tds.dlc": "https://github.com/TapTap/TapLicense-Unity.git#{version}", -} -``` - -### 设置授权回调 - - - -```cs -// 需要引入 license 库 -using TapTap.License; - -// 默认情况下 SDK 会弹出不可由玩家手动取消的弹窗来避免未授权玩家进入游戏,如果需要回调来触发流程,请添加如下代码 -TapLicense.SetLicencesCallback(ITapLicenseCallback callback); - -public interface ITapLicenseCallback -{ - // 授权成功回调 - void OnLicenseSuccess(); -} - -``` - -```java -// 默认情况下 SDK 会弹出不可由玩家手动取消的弹窗来避免未授权玩家进入游戏,如果需要回调来触发流程,请添加如下代码 -TapLicenseHelper.setLicenseCallback(new TapLicenseCallback() { - @Override - public void onLicenseSuccess() { - // 授权成功回调 - } -}); -``` - - -### 检查付费授权 - - - -```cs -TapLicense.Check(); -``` - -```java -TapLicenseHelper.check(Activity activity); -``` - - -## 测试 - -为了保证上线后,游戏对于用户是否购买的判断能够正常生效,**请务必按照以下说明完成自测。** - -### 上传 APK - -上传需要测试的 APK 至开发者中心,并通过审核。 - -### 配置 SDK - -前往开发者中心 >> 选择SDK 控制台 >> 选择购买激活 SDK >> 选择相应的游戏的配置 >> 填写测试用户的 TapTap ID 。 - -或者,前往开发者中心 >> 选择已经开放售卖的游戏 >> 选择购买激活 SDK 设置 >> 填写测试用户的 TapTap ID 。 - -### 开始测试 - -在 TapTap 客户端使用已填写的测试用户账号登录。 - -## 正式开始售卖 - -### 完善应用信息 - -前往开发者中心,按照[物料要求](/store/release/store-material)填写应用信息,并审核通过。 - -### 设置售卖价格 - -前往开发者中心 >> 售卖设置 ,开启售卖开关,设置游戏售卖金额,提交审核,并同步对接的 TapTap 运营相关信息。 - -### 正式上线 - -所有流程都确保顺利后,游戏可正式上线。 - ---- - -## 常见问题 - -### 关于 Android 11 无法拉起 TapTap 客户端的解决方案 - -Android 11 加强了隐私保护策略,引入了大量变更和限制,其中一个重要变更 — [软件包可见性](https://developer.android.com/about/versions/11/privacy/package-visibility) ,将会导致第三方应用无法拉起 TapTap 客户端,从而影响 TapTap 相关功能的正常使用 ,包括但不限于更新唤起 TapTap 、购买验证等功能。 -特别需要注意的是,Android 11 的该变更只会影响到升级 ` targetSdkVersion=30 ` 的应用,未升级的应用暂不受影响。 - -**方案一:** - -编译时将 ` targetSdkVersion` 改为 29(目前设置成 30 会触发该问题) - -**方案二:** - -1. 将 gradle build tools 改为 4.1.0+ -```java -classpath 'com.android.tools.build:gradle:4.1.0' -``` - -2. 在 AndroidManifest.xml 里添加如下内容 -```xml - - - - - -``` diff --git a/versioned_docs/version-v2/sdk/05-copyright-verification/_category_.json b/versioned_docs/version-v2/sdk/05-copyright-verification/_category_.json deleted file mode 100644 index 271ff994e..000000000 --- a/versioned_docs/version-v2/sdk/05-copyright-verification/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "正版验证", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/06-dlc/01-features.mdx b/versioned_docs/version-v2/sdk/06-dlc/01-features.mdx deleted file mode 100644 index d08562e49..000000000 --- a/versioned_docs/version-v2/sdk/06-dlc/01-features.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -id: features -title: 功能介绍 -sidebar_label: 功能介绍 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - - -TapTap 开发者服务,支持付费的可下载内容(DLC),让玩家不离开游戏便能浏览、购买、拥有新内容。你需要在游戏内向玩家显示 DLC 的示意内容与购买按钮,所销售的商品内容由游戏决定,你也可以随意决定游戏内商品的数量,可以单独出售商品,也可以捆绑销售。每个 DLC 内容都拥有一个唯一的身份标识(商品 ID)。DLC SDK 会向玩家显示购买环节的操作。 - -:::note -目前,DLC 服务仅提供给付费下载的游戏。 -::: - -## 开通与配置 - -如需要使用 DLC 服务,请先联系对接的 TapTap 商务或运营进行开通。 - -## 更新 DLC - -DLC 通常在玩家购买成功后则立即拥有。如玩家在购买过程中,取消付款或付款失败,你均会收到通知。详情请参照开发流程进行接入。 - -DLC 与游戏包体为绑定发布,如要新增 DLC 商品,你必须将新增的 DLC 加入到游戏包体中,并在 TapTap 商店里更新你的 APK。当玩家更新 APK 后,会在游戏内看到新增的商品。 - -## 售卖与退款 - -当游戏正式上线后,DLC 便开始售卖。为了维护玩家的体验,TapTap 给玩家提供了可以退款的功能。你可以在 [TapTap 开发者中心](https://developer.taptap.cn/) 的概览中,查看 DLC 的售卖情况,包括订单数量、售卖金额、退款订单与金额等数据。 - - diff --git a/versioned_docs/version-v2/sdk/06-dlc/02-guide.mdx b/versioned_docs/version-v2/sdk/06-dlc/02-guide.mdx deleted file mode 100644 index 609727f58..000000000 --- a/versioned_docs/version-v2/sdk/06-dlc/02-guide.mdx +++ /dev/null @@ -1,134 +0,0 @@ ---- -id: guide -title: 开发指南 -sidebar_label: 开发指南 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - -import {Gray,Blue, Red, Black} from '/src/docComponents/doc'; - -## DLC 查询和购买 - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - -```json -"dependencies":{ -// 公共库 -"com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#{version}", -// 付费购买 -"com.taptap.tds.dlc": "https://github.com/TapTap/TapLicense-Unity.git#{version}", -} -``` -## DLC 查询和购买 - -### DLC 回调设置 - - - -```cs -public class MyTapDLCCallback:ITapDlcCallback -{ - public void OnQueryCallBack(TapLicenseQueryCode code, Dictionary queryList) - { - - } - - public void OnOrderCallBack(string sku, TapLicensePurchasedCode status) - { - - } -} - -TapLicense.SetDLCCallback(new MyTapDLCCallback()); -``` - -```java -TapLicenseHelper.setDLCCallback(new DLCManager.InventoryCallback() { - @Override - public boolean onQueryCallBack(int i, HashMap hashMap) { - return false; - } - - @Override - public void onOrderCallBack(String s, int i) { - - } -}); -``` - - - -### DLC 查询 - - - -```cs -TapLicense.QueryDLC(string[] skuIds); -``` - -```java -TapLicenseHelper.queryDLC(Activity activity, String[] skuIds); -``` - - -### DLC 购买 - - - -```cs -TapLicense.PurchaseDLC(string skuId); -``` - -```java -TapLicenseHelper.queryDLC(Activity activity, String skuIds); -``` - - -### 参数说明 -#### TapLicenseQueryCode -回调 | 回调值 | 说明 | ------------ | --- | -------- | -QUERY_RESULT_OK | 0 | 查询成功 | -QUERY_RESULT_NOT_INSTALL_TAPTAP | 1 | 检查测试机未安装 TapTap 客户端 | -QUERY_RESULT_ERR | 2 | 查询失败 | -ERROR_CODE_UNDEFINED | 80000 | 未知错误 | - - -#### skuId: -内购商品 id,需要联系 TapTap 运营同学进行配置; - - -## 测试 - -为了保证上线后,游戏对于用户是否购买的判断能够正常生效,**请务必按照以下说明完成自测。** - -### 上传 APK - -上传需要测试的 APK 至开发者中心,并通过审核。 - -### 配置 SDK - -前往开发者中心 >> 选择SDK 控制台 >> 选择购买激活 SDK >> 选择相应的游戏的配置 >> 填写测试用户的 TapTap ID 。 - -或者,前往开发者中心 >> 选择已经开放售卖的游戏 >> 选择购买激活 SDK 设置 >> 填写测试用户的 TapTap ID 。 - -### 开始测试 - -在 TapTap 客户端使用已填写的测试用户账号登录。 - -## 正式开始售卖 - -### 完善应用信息 - -前往开发者中心,按照[物料要求](/store/release/store-material)填写应用信息,并审核通过。 - -### 设置售卖价格 - -前往开发者中心 >> 售卖设置 ,开启售卖开关,设置游戏售卖金额,提交审核,并同步对接的 TapTap 运营相关信息。 - -### 正式上线 - -所有流程都确保顺利后,游戏可正式上线。 - ---- \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/06-dlc/_category_.json b/versioned_docs/version-v2/sdk/06-dlc/_category_.json deleted file mode 100644 index 8a1bfe0d6..000000000 --- a/versioned_docs/version-v2/sdk/06-dlc/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "DLC", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/07-update/01-guide.mdx b/versioned_docs/version-v2/sdk/07-update/01-guide.mdx deleted file mode 100644 index 668fbbb49..000000000 --- a/versioned_docs/version-v2/sdk/07-update/01-guide.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -id: guide -title: 开发指南 -sidebar_label: 开发指南 ---- - -TapTap 开发者服务为游戏和玩家提供唤起 TapTap 客户端进行游戏更新的功能。当游戏发布了新版本,且需要玩家进行更新才能体验新版本时,在游戏内绘制一个界面告知玩家,需要进行新版本更新,并且提供一个更新的按钮。玩家点击后,会跳转到 TapTap 客户端内的游戏详情页面,进行更新。 - -## Unity - -### 检查 TapTap 是否安装 - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - -```json -"dependencies":{ -// 公共库,{version} 为具体版本号 -"com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#{version}", -} -``` - -```cs -TapCommon.IsTapTapInstalled(installed => -{ - if (installed) { - Debug.Log("TapTap 已经安装"); - } -}); - -``` - - - - -### 唤起 TapTap 检查更新 - -```cs -TapCommon.UpdateGameInTapTap("appid", callSuccess => -{ - if (callSuccess) { - Debug.Log("TapTap 唤起成功"); - } -}); -``` -appid: 游戏在 TapTap 商店的唯一身份标识 -例如:https://www.taptap.cn/app/187168 ,其中 "187168" 是 appid - - -### 打开游戏评论区 - -```cs -TapCommon.OpenReviewInTapTap(appId, openSuccess => -{ - if (openSuccess) { - Debug.Log("打开游戏评论区成功"); - } -}); - -``` - -## Android - -### 检查 TapTap 是否安装 - -接口在 TapGameUtil 里面,`import com.tds.common.utils.TapGameUtil;` - -```java -if(TapGameUtil.isTapTapInstalled(this)){ - Log.d(TAG, "已经安装 TapTap 客户端"); -} -``` - - -### 唤起 TapTap 检查更新 - -```java -if(TapGameUtil.updateGameInTapTap(this,"appid")){ - Log.d(TAG, "唤起 TapTap 客户端成功"); -} -``` -appid: 游戏在 TapTap 商店的唯一身份标识 -例如:https://www.taptap.cn/app/187168 ,其中 "187168" 是 appid - - -### 打开游戏评论区 - -```java -if(TapGameUtil.openReviewInTapTap(this,"appid")){ - Log.d(TAG, "打开评论区成功"); -} - -``` - -## 常见问题 - -### 关于 Android 11 无法拉起 TapTap 客户端的解决方案 - -Android 11 加强了隐私保护策略,引入了大量变更和限制,其中一个重要变更 —— [软件包可见性](https://developer.android.com/about/versions/11/privacy/package-visibility) ,将会导致第三方应用无法拉起 TapTap 客户端,从而影响 TapTap 相关功能的正常使用 ,包括但不限于更新唤起 TapTap 、购买验证等功能。 -特别需要注意的是,Android 11 的该变更只会影响到升级 `targetSdkVersion=30` 的应用,未升级的应用暂不受影响。 - -**方案一:** - -编译时将 `targetSdkVersion` 改为 29(目前 `=30` 会触发该问题) - -**方案二:** - -1. 将 gradle build tools 改为 4.1.0+ -```java -classpath 'com.android.tools.build:gradle:4.1.0' -``` - -2. 在 AndroidManifest.xml 里添加如下内容 -```xml - - - - - -``` \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/07-update/_category_.json b/versioned_docs/version-v2/sdk/07-update/_category_.json deleted file mode 100644 index c14a5b7be..000000000 --- a/versioned_docs/version-v2/sdk/07-update/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "唤起更新", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/08-storage/01-features.mdx b/versioned_docs/version-v2/sdk/08-storage/01-features.mdx deleted file mode 100644 index 23ae2970d..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/01-features.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: features -title: 数据存储功能介绍 -sidebar_label: 功能介绍 ---- - - -数据存储服务能够高效存取海量级 JSON 对象、二进制文件、地理位置等数据。其内置的行级 ACL 权限控制,以及通用的用户及角色管理体系,可以帮助你快速实现安全而灵活的数据访问。 - -## 初衷 - -大部分的产品都是数据驱动的,它们有一个最大的特点,就是对后端的需求在模式上其实是比较统一的: - -- 前端负责数据展现和用户交互处理,与后端的 app server 通过网络来交换需要的数据; - -- app server 负责业务逻辑处理,生成核心数据存储到 data server,或者聚合 data server 查询到的数据返回给客户端; - -- data server 负责核心数据的存储和备份。 - -这样的模式适合互联网上绝大部分产品,虽然数据结构有差异、业务逻辑不一样,但是前后端交互的主体——「数据」,抽象来看是一致的,后端的架构(譬如 LAMP)也是大同小异的,而且同样的系统在一遍一遍地被重复开发,极大浪费了我们宝贵的技术资源。 - -## 实用功能 - -开发者无需担心数据规模的大小和访问流量的多少,可以将我们的数据存储服务看成是一个面向对象的海量数据库来使用。 - -### 结构化数据存储 - -存储任意类型的 JSON 对象,支持对象之间的关联映射,同时提供完整的增删改查操作接口。 - -### 文件存储 - -存储图片,文档,音视频等二进制文件,自动提供弹性空间和多副本的冗余备份策略,默认支持多个 CDN 加速节点。 - -### ACL 权限控制 - -支持在整表、单行、单列三个维度上,进行读写分离的权限控制。严密的 ACL 机制,确保数据安全。 - -### 内建账户系统 - -支持用户通过邮件或手机进行注册和登录,并提供密码重置等实用功能。 - -### 多端实时同步 - -数据可在云端与多个客户端之间实时同步,支持跨设备进行实时互动协作。 - -### 大数据分析处理 - -提供通用的 SQL 查询接口,可实时并行处理数据,专为数据挖掘、OLAP 以及商业智能而建。 - -## 优势和特色 - -- 多副本分布保存,提供 99.999% 的数据可靠性和超高并发访问能力。完善的备份机制确保数据万无一失。 - -- 扩展功能组件,支持朋友圈、动态消息等常见社区功能,让业务开发更简单便捷。 - -- 每天支撑超过 10 亿次请求,峰值压力堪比电商秒杀。自动应对流量高峰,服务稳定可靠。 - diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/01-setup-dotnet.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/01-setup-dotnet.mdx deleted file mode 100644 index 269bd0465..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/01-setup-dotnet.mdx +++ /dev/null @@ -1,176 +0,0 @@ ---- -id: setup-dotnet -title: 数据存储、即时通讯 .NET SDK 配置指南 -sidebar_label: .NET SDK 配置 ---- - - - -我们于 2020 年 12 月推出了基于 .NET Standard 2.0 接口标准实现的[新版 .NET SDK](https://github.com/leancloud/csharp-sdk)。旧版 .NET SDK(类名以 `AV` 开头的) 已不再更新,欢迎旧版 SDK 的用户尽快切换到[新版 .NET SDK](https://github.com/leancloud/csharp-sdk),具体迁移方法详见 [.NET SDK 迁移指南]。 - -[.NET SDK 迁移指南]: https://github.com/leancloud/csharp-sdk/wiki/.Net-Standard-SDK-%E8%BF%81%E7%A7%BB%E6%8C%87%E5%8D%97 - -新版 .NET SDK 基于 .NET Standard 2.0 接口标准实现,支持框架如下: - -- Unity 2018.1+ -- .NET Core 2.0+ -- .NET Framework 4.6.1+ -- Mono 5.4+ - -更多支持框架可参考:https://docs.microsoft.com/en-us/dotnet/standard/net-standard - -通过 GitHub 仓库 [Releases](https://github.com/leancloud/csharp-sdk/releases) 下载最新版本 SDK。 - -### 安装 - -#### Unity 项目 - -- 直接导入:请下载 LeanCloud-SDK-XXX-Unity.zip,解压后为 Plugins 文件夹,拖拽至 Unity 即可。 - -- UPM:请在项目的 Packages/manifest.json 中添加依赖项 - - ```json - "dependencies": { - "com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-0.7.5", - "com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-0.7.5" - } - ``` - -注意:仅支持 Unity 2018+,即 Unity Api Compatibility Level 支持 .NET Standard 2.0 的版本。 - -#### 非 Unity 项目 - -.NET Core 或其他支持 .NET Standard 2.0 的项目请下载 LeanCloud-SDK-XXX-Standard.zip,解压后设置依赖即可。 -(XXX 指云服务,包括存储 Storage,即时通讯(含 LiveQuery) Realtime,云引擎 Engine) - -### 模块及依赖关系 - -名称 | 模块描述 ---|--- -`LeanCloud-SDK-Storage`| 存储服务。 -`LeanCloud-SDK-Realtime`| 即时通信、LiveQuery 服务,依赖于存储服务。 -`LeanCloud-SDK-Engine`| 云引擎服务,依赖于存储,适用于云引擎服务端环境。 - -如只需使用某种服务,可下载最小依赖包,减小程序体积。 - -## 快速开始 - -### 绑定域名 - -你需要绑定 API 自定义域名,以便和其他厂商的应用隔离入口,避免其他应用受到 DDoS 攻击时相互牵连。 -如果使用了文件服务,也需要绑定文件自定义域名。 - -进入 **开发者中心 > 你的游戏 > 游戏服务 > 技术服务 > 数据存储 > 服务设置 > 自定义域名** 点击「绑定新域名」按钮,根据控制台提示完成绑定步骤。 -注意,DNS 解析记录和证书申请(如果选择了自动管理 SSL 证书)都需要一定时间,请耐心等待。 - -绑定成功后,初始化 SDK 时,请传入绑定的自定义域名(`https://please-replace-with-your-customized.domain.com`)。 - -如果你使用了文件服务(包括即时通讯的多媒体消息(图像、音频、视频等)),同样需要前往 **开发者中心 > 你的游戏 > 游戏服务 > 技术服务 > 数据存储 > 文件 > 设置 > 文件访问域名** 绑定域名,步骤和 API 自定义域名基本相同,但有两点不一样: - -1. API 域名解析使用 A 记录,文件域名解析使用 CNAME 记录,也因此文件域名不支持绑定裸域名(例如 `example.com`),需要绑定子域名(例如 `files.example.com`)。 -2. 绑定成功后,还需在 **文件 > 设置 > 文件访问地址** 点击「修改」按钮进行切换。 - -### 应用凭证 - -在 **开发者中心 > 你的游戏 > 游戏服务 > 基本信息** 可以查看应用凭证: - -- **Client ID**,又称 `App ID`,在 SDK 初始化时用到。联系技术支持时,提供 `Client ID` 可以方便我们更快定位到你的应用。 -- **Client Token**,又称 ``App Key``,在 SDK 初始化时用到。 -- **Server Secret**,又称 `Master Key`,用于在自有服务器、云引擎等**受信任环境**调用管理接口,具备跳过一切权限验证的超级权限。所以**一定注意保密,千万不要在客户端代码中使用该凭证**。 - -### 导入模块 - -```cs -// 导入基础模块 -using LeanCloud; -// 导入存储模块 -using LeanCloud.Storage; -// 如有需要,导入即时通讯模块 -using LeanCloud.Realtime; -``` - -### 初始化 SDK - -在使用服务前,先调用如下代码: - -```cs -LCApplication.Initialize("your-client-id", "your-client-token", "https://please-replace-with-your-customized.domain.com"); -``` - -## 开启调试日志 - -在应用开发阶段,你可以选择开启 SDK 的调试日志(debug log)来方便追踪问题。调试日志开启后,SDK 会把网络请求、错误消息等信息输出到 IDE 的日志窗口,或是浏览器 Console 或是云引擎日志(如果在云引擎下运行 SDK)。 - -```cs -LCLogger.LogDelegate = (LCLogLevel level, string info) => { - switch (level) { - case LCLogLevel.Debug: - TestContext.Out.WriteLine($"[DEBUG] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Warn: - TestContext.Out.WriteLine($"[WARNING] {DateTime.Now} {info}\n"); - break; - case LCLogLevel.Error: - TestContext.Out.WriteLine($"[ERROR] {DateTime.Now} {info}\n"); - break; - default: - TestContext.Out.WriteLine(info); - break; - } -} -``` - -Unity 平台可重定向到 Debug. - -在应用发布之前,请关闭调试日志,以免暴露敏感数据。 - -## 验证 - -首先,确认本地网络环境是可以访问云端服务器的,可以执行以下命令: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` 为绑定的 API 自定义域名。 - -如果当前网路正常会返回当前时间: - -```json -{"__type":"Date","iso":"2020-10-12T06:46:56.000Z"} -``` - -然后在项目中编写如下测试代码: - -```cs -LCObject testObject = new LCObject("TestObject"); -testObject["words"] = "Hello world!"; -await testObject.Save(); -``` - -保存后运行程序。 - -然后打开 **云服务控制台 > 数据存储 > 结构化数据 > `TestObject`**,如果看到数据表中出现一行「words」列的值为「Hello world!」的数据,说明 SDK 已经正确地执行了上述代码,配置完毕。 - -如果控制台没有发现对应的数据,请参考 [问题排查](#问题排查)。 - -## 问题排查 - -SDK 安装指南基于当前最新版本的 SDK 编写,所以排查问题前,请先检查下安装的 SDK 是不是最新版本。 - -### `401 Unauthorized` - -如果 SDK 抛出 `401` 异常或者查看本地网络访问日志存在: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -则可认定为 App ID 或者 `App Key` 输入有误,或者是不匹配,很多开发者同时注册了多个应用,导致拷贝粘贴的时候,用 A 应用的 App ID 匹配 B 应用的 `App Key`,这样就会出现服务端鉴权失败的错误。 - -### 客户端无法访问网络 - -客户端尤其是手机端,应用在访问网络的时候需要申请一定的权限。 \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/02-dotnet.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/02-dotnet.mdx deleted file mode 100644 index 14eba7e17..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/02-dotnet.mdx +++ /dev/null @@ -1,1895 +0,0 @@ ---- -id: dotnet -title: 数据存储指南 · .NET -sidebar_label: .NET 指南 ---- - - - -数据存储是云服务提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端: - - -```cs -// 构建对象 -LCObject todo = new LCObject("Todo"); -// 为属性赋值 -todo["title"] = "工程师周会"; -todo["content"] = "周二两点,全体成员"; -// 将对象保存到云端 -await todo.Save(); -``` - -我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。 - -## SDK 安装与初始化 - -请阅读 [数据存储、即时通讯 .NET SDK 配置指南](/v2/sdk/storage/guide/setup-dotnet)。 - -> C#/.NET/Unity SDK 老版本迁移 -> -> 如果你还在在使用我们老版本的 C#/.NET/Unity SDK(版本号为 `YYYYMMDD.N` 格式,类名以 `AV` 开头),请参考 [.NET SDK 迁移指南] 迁移到新版 SDK。 - -[.NET SDK 迁移指南]: https://github.com/leancloud/csharp-sdk/wiki/.Net-Standard-SDK-%E8%BF%81%E7%A7%BB%E6%8C%87%E5%8D%97 - -## 对象 - -### `LCObject` - -`LCObject` 是云服务对复杂对象的封装,每个 `LCObject` 包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个 `LCObject` 上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。 - -比如说,一个保存着单个 Todo 的 `LCObject` 可能包含如下数据: - -```json -title: "给小林发邮件确认会议时间", -isComplete: false, -priority: 2, -tags: ["工作", "销售"] -``` - -### 数据类型 - -`LCObject` 支持的数据类型包括 `String`、`Number`、`Boolean`、`Object`、`Array`、`Date` 等等。你可以通过嵌套的方式在 `Object` 或 `Array` 里面存储更加结构化的数据。 - -`LCObject` 还支持两种特殊的数据类型 `Pointer` 和 `File`,可以分别用来存储指向其他 `LCObject` 的指针以及二进制数据。 - -`LCObject` 同时支持 `GeoPoint`,可以用来存储地理位置信息。参见 [GeoPoint](#geopoint)。 - -以下是一些示例: - - -```cs -// 基本类型 -int numberValue = 2018; -bool boolValue = true; -string stringValue = "hello, world"; -DateTime now = DateTime.Now; -List intList = new List { 1, 2, 3 }; -Dictionary dict = new Dictionary { - { "year", 1780 }, - { "first", "partridge" }, - { "second", "turtledoves" }, - { "fifth", "golden rings" } -}; - -// 构建对象 -LCObject object = new LCObject("Hello"); -object["numberValue"] = numberValue; -object["boolValue"] = boolValue; -object["stringValue"] = stringValue; -object["time"] = now; -object["intList"] = intList; -object["dictValue"] = dict; -``` - -我们不推荐通过 `byte[]` 在 `LCObject` 里面存储图片、文档等大型二进制数据。每个 `LCObject` 的大小不应超过 **128 KB**。如需存储大型文件,可创建 `LCFile` 实例并将其关联到 `LCObject` 的某个属性上。参见 [文件](#文件)。 - -注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。 - -**云服务控制台 > 数据存储 > 结构化数据** 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。 - -若想了解云服务是如何保护应用数据的,请阅读[数据和安全](/v2/sdk/storage/guide/security)。 - -### 构建对象 - -下面的代码构建了一个 class 为 `Todo` 的 `LCObject`: - -```cs -LCObject object = new LCObject("Todo"); -``` - -在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。 - -### 保存对象 - -下面的代码将一个 class 为 `Todo` 的对象存入云端: - - -```cs -// 构建对象 -LCObject todo = new LCObject("Todo"); -// 为属性赋值 -todo["title"] = "马拉松报名"; -todo["priority"] = 2; -// 将对象保存到云端 -await todo.Save(); -``` - -为了确认对象已经保存成功,我们可以到 **云服务控制台 > 数据存储 > 结构化数据 > `Todo`** 里面看一下,应该会有一行新的数据产生。点一下这个数据的 `objectId`,应该能看到类似这样的内容: - -```json -{ - "title": "马拉松报名", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -注意,无需在 **云服务控制台 > 数据存储 > 结构化数据** 里面创建新的 `Todo` class 即可运行前面的代码。如果 class 不存在,它将自动创建。 - -以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定: - -内置属性 | 类型 | 描述 ---- | --- | --- -`objectId` | string | 该对象唯一的 ID 标识。 -`ACL` | LCACL | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 -`createdAt` | DateTime | 该对象被创建的时间。 -`updatedAt` | DateTime | 该对象最后一次被修改的时间。 - -这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的 `LCObject` 不存在这些属性。 - -属性名(**keys**)只能包含字母、数字和下划线。自定义属性不得以双下划线(`__`)开头或与任何系统保留字段和内置属性(`ACL`、`className`、`createdAt`、`objectId` 和 `updatedAt`)重名,无论大小写。 - -属性值(**values**)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 [数据类型](#数据类型)。 - -我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 `CustomData`。属性,采用小驼峰法,如 `imageUrl`。 - -### 获取对象 - -对于已经保存到云端的 `LCObject`,可以通过它的 `objectId` 将其取回: - -```cs -LCQuery query = new LCQuery("Todo"); -LCObject todo = await query.Get("582570f38ac247004f39c24b"); -// todo 就是 ObjectId 为 582570f38ac247004f39c24b 的 Todo 实例 -string title = todo["title"] as string; -int priority = (int)(todo["priority"]); - -// 获取内置属性 -string objectId = todo.ObjectId; -DateTime updatedAt = todo.UpdatedAt; -DateTime createdAt = todo.CreatedAt; -``` - -对象拿到之后,可以通过 `get` 方法来获取各个属性的值。注意 `objectId`、`updatedAt` 和 `createdAt` 这三个内置属性不能通过 `get` 获取或通过 `set` 修改,只能由云端自动进行填充。尚未保存的 `LCObject` 不存在这些属性。 - -如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 null。 -#### 同步对象 - -当云端数据发生更改时,你可以调用 `Fetch` 方法来刷新对象,使之与云端数据同步: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -await todo.Fetch(); -// todo 已刷新 -``` - -刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,**`Fetch` 操作会丢弃这些修改**。为避免这种情况,你可以在刷新时指定 **需要刷新的属性**,这样只有指定的属性会被刷新(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`),其他属性不受影响。 - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -await todo.Fetch(includes: new string[] { "priority", "location" }); -// 只有 priority 和 location 会被获取和刷新 -``` -### 更新对象 - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `Save` 方法。例如: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -todo["content"] = "这周周会改到周三下午三点。"; -await todo.Save(); -``` - -云服务会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。 - -#### 有条件更新对象 - -通过传入 `query` 选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回 `305` 错误。 - -例如,用户的账户表 `Account` 有一个余额字段 `balance`,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求: - -```cs -try { - LCObject account = LCObject.CreateWithoutData("Account", "5745557f71cfe40068c6abe0"); - // 对 balance 原子减少 100 - int amount = -100; - account.Increment("balance", amount); - // 设置条件 - LCQuery query = new LCQuery("Account"); - query.WhereGreaterThanOrEqualTo("balance", -amount); - // 操作结束后,返回最新数据。 - // 如果是新对象,则所有属性都会被返回, - // 否则只有更新的属性会被返回。 - await account.Save(fetchWhenSave: true, query: query); - print($"当前余额为:{account["balance"]}"); -} catch (LCException e) { - if (e.code == 305) { - print("余额不足,操作失败!"); - } -} -``` - -**`query` 选项只对已存在的对象有效**,不适用于尚未存入云端的对象。 - -`query` 选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过 `LCQuery` 查询 `LCObject` 再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。 - -#### 更新计数器 - -设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 **原子操作** 来增加或减少一个属性内保存的数字: - -```cs -post.Increment("likes", 1); -``` - -可以指定需要增加或减少的值。若未指定,则默认使用 `1`。 - -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,需要原子增减的字段建议使用整数以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 - -#### 更新数组 - -更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据: - -- `Add(key, value)` 将指定对象附加到数组末尾。 -- `AddAll(key, values)` 将一组对象附加到数组末尾。 -- `AddUnique(key, value)` 将指定对象附加到数组末尾,确保对象唯一。 -- `AddAllUnique(key, values)` 将指定对象数组附加到数组末尾,确保对象唯一。 -- `Remove(key, value)` 从数组字段中删除指定对象的所有实例。 -- `RemoveAll(key, values)` 从数组字段中删除指定的对象数组。 - - -例如,`Todo` 用一个 `alarms` 属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性: - -```cs -DateTime alarm1 = DateTime.Parse("2018-04-30 07:10:00Z"); -DateTime alarm2 = DateTime.Parse("2018-04-30 07:20:00Z"); -DateTime alarm3 = DateTime.Parse("2018-04-30 07:30:00Z"); - -LCObject todo = new LCObject("Todo"); -todo.AddAllUnique("alarms", new object[] { alarm1, alarm2, alarm3 }); -await todo.Save(); -``` - -### 删除对象 - -下面的代码从云端删除一个 `Todo` 对象: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); -await todo.Delete(); -``` - -如果只需删除对象的一个属性,可以用 `Unset`: - -```cs -LCObject todo = LCObject.CreateWithoutData("Todo", "582570f38ac247004f39c24b"); - -// priority 属性会被删除 -todo.Unset("priority"); - -// 保存对象 -await todo.Save(); -``` - -注意,删除对象是一个较为敏感的操作,我们建议你阅读[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html) 来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。 - -### 批量操作 - -```cs -// 批量构建和更新 -LCObject.SaveAll(); - -// 批量删除 -LCObject.DeleteAll(); - -// 批量同步 -LCObject.FetchAll(); -``` - -下面的代码将所有 `Todo` 的 `isComplete` 设为 `true`: - -```cs -LCQuery query = new LCQuery("Todo"); -ReadOnlyCollection results = await query.Find(); -// 获取需要更新的 todo -foreach (LCObject todo in results) { - // 更新属性值 - todo["isComplete"] = true; -} -await LCObject.SaveAll(results); -``` - -虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。 -### 后台运行 - -细心的开发者已经发现,在所有的示例代码中几乎都是用了异步来访问云端,形如的用法都是提供给开发者在主线程调用用以实现后台运行的方法,因此开发者在主线程可以放心地调用这种命名方式的函数。 - -### 数据模型 - -对象之间可以产生关联。拿一个博客应用来说,一个 `Post` 对象可以与许多个 `Comment` 对象产生关联。云服务支持三种关系:一对一、一对多、多对多。 - -#### 一对一、一对多关系 - -一对一、一对多关系可以通过将 `LCObject` 保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个 `Comment` 指向一个 `Post`。 - -下面的代码会创建一个含有单个 `Comment` 的 `Post`: - -```cs -// 创建 post -LCObject post = new LCObject("Post"); -post["title"] = "饿了……"; -post["content"] = "中午去哪吃呢 ?"; - -// 创建 comment -LCObject comment = new LCObject("Comment"); -comment["content"] = "当然是肯德基啦 !"; - -// 将 post 设为 comment 的一个属性值 -comment["parent"] = post; - -// 保存 comment 会同时保存 post -await comment.Save(); -``` - -云端存储时,会将被指向的对象用 `Pointer` 的形式存起来。你也可以用 `objectId` 来指向一个对象: - -```cs -LCObject post = LCObject.CreateWithoutData("Post", "57328ca079bc44005c2472d0"); -comment["post"] = post; -``` - -请参阅 [关系查询](#关系查询) 来了解如何获取关联的对象。 - -#### 多对多关系 - -想要建立多对多关系,最简单的办法就是使用 **数组**。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 **中间表** 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。 - -我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。 - -### 序列化和反序列化 - -在实际的开发中,把 `LCObject` 当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此 `LCObject` 也提供了序列化和反序列化的方法。 - -序列化: - -```cs -LCObject object = new LCObject("Hello"); -object["title"] = "马拉松报名"; -object["priority"] = 2; -object["owner"] = await LCUser.GetCurrent(); -string serializedString = object.ToString(); -``` - -反序列化: - -```cs -LCObject newObject = LCObject.ParseObject(json); -await newObject.Save(); -``` - -## 查询 - -我们已经了解到如何从云端获取单个 `LCObject`,但你可能还会有一次性获取多个符合特定条件的 `LCObject` 的需求,这时候就需要用到 `LCQuery` 了。 - -### 基础查询 - -执行一次基础查询通常包括这些步骤: - -1. 构建 `LCQuery`; -2. 向其添加查询条件; -3. 执行查询并获取包含满足条件的对象的数组。 - -下面的代码获取所有 `lastName` 为 `Smith` 的 `Student`: - -```cs -LCQuery query = new LCQuery("Student"); -query.WhereEqualTo("lastName", "Smith"); -// students 是包含满足条件的 Student 对象的数组 -ReadOnlyCollection students = await query.Find(); -``` -### 查询条件 - -可以给 `LCObject` 添加不同的条件来改变获取到的结果。 - -下面的代码查询所有 `firstName` 不为 `Jack` 的对象: - - -```cs -query.WhereNotEqualTo("firstName", "Jack"); -``` - -对于能够排序的属性(比如数字、字符串),可以进行比较查询: - - -```cs -// 限制 age < 18 -query.WhereLessThan("age", 18); - -// 限制 age <= 18 -query.WhereLessThanOrEqualTo("age", 18); - -// 限制 age > 18 -query.WhereGreaterThan("age", 18); - -// 限制 age >= 18 -query.WhereGreaterThanOrEqualTo("age", 18); -``` - -可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是 `AND` 的关系: - - -```cs -query.WhereEqualTo("firstName", "Jack"); -query.WhereGreaterThan("age", 18); -``` - - -可以通过指定 `limit` 限制返回结果的数量(默认为 `100`): - - -```cs -// 最多获取 10 条结果 -query.Limit(10); -``` - - -由于性能原因,`limit` 最大只能设为 `1000`。即使将其设为大于 `1000` 的数,云端也只会返回 1,000 条结果。 - -如果只需要一条结果,可以直接用 `First`: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("priority", 2); -// todo 是第一个满足条件的 Todo 对象 -LCObject todo = await query.First(); -``` - -可以通过设置 `skip` 来跳过一定数量的结果: - -```cs -query.Skip(20); -``` - -把 `skip` 和 `limit` 结合起来,就能实现翻页功能: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("priority", 2); -query.Limit(10); -query.Skip(20); -``` - -需要注意的是,`skip` 的值越高,查询所需的时间就越长。作为替代方案,可以通过设置 `createdAt` 或 `updatedAt` 的范围来实现更高效的翻页,因为它们都自带索引。 -同理,也可以通过设置自增字段的范围来实现翻页。 - -对于能够排序的属性,可以指定结果的排序规则: - -```cs -// 按 createdAt 升序排列 -query.OrderByAscending("createdAt"); - -// 按 createdAt 降序排列 -query.OrderByDescending("createdAt"); -``` - -还可以为同一个查询添加多个排序规则: - -```cs -query.AddAscendingOrder("priority"); -query.AddDescendingOrder("createdAt"); -``` - -下面的代码可用于查找包含或不包含某一属性的对象: - - -可以通过 `Select` 指定需要返回的属性。下面的代码只获取每个对象的 `title` 和 `content`(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`): - - -```cs -LCQuery query = new LCQuery("Todo"); -query.Select("title"); -query.Select("content"); -LCObject todo = await query.First(); - -string title = todo["title"] as string; // √ -string content = todo["content"] as string; // √ -string notes = todo["notes"] as string; // null -``` - -`Select` 支持点号(`author.firstName`),详见[《点号使用指南》](https://leancloud.cn/docs/dot-notation.html)。 - -另外,字段名前添加减号前缀表示反向选择,例如 `-author` 表示不返回 `author` 字段。 -反向选择同样适用于内置字段,比如 `-objectId`,也可以和点号组合使用,比如 `-pubUser.createdAt`。 - -对于未获取的属性,可以通过对结果中的对象进行 `Fetch` 操作来获取。参见 [同步对象](#同步对象)。 -### 字符串查询 - -可以用 `WhereStartsWith` 来查找某一属性值以特定字符串开头的对象。和 SQL 中的 `LIKE` 一样,你可以利用索引带来的优势: - -```cs -LCQuery query = new LCQuery("Todo"); -// 相当于 SQL 中的 title LIKE 'lunch%' -query.WhereStartsWith("title", "lunch"); -``` - -可以用 `WhereContains` 来查找某一属性值包含特定字符串的对象: - -```cs -LCQuery query = new LCQuery("Todo"); -// 相当于 SQL 中的 title LIKE '%lunch%' -query.WhereContains("title", "lunch"); -``` - -和 `WhereStartsWith` 不同,`WhereContains` 无法利用索引,因此不建议用于大型数据集。 - -注意 `WhereStartsWith` 和 `WhereContains` 都是 **区分大小写** 的,所以上述查询会忽略 `Lunch`、`LUNCH` 等字符串。 - -如果想查找某一属性值不包含特定字符串的对象,可以使用 `WhereMatches` 进行基于正则表达式的查询: - -```cs -LCQuery query = new LCQuery("Todo"); -// 'title' 不包含 "ticket"(不区分大小写) -query.WhereMatches("title", "^((?!ticket).)*\$", modifiers: "i"); -``` - -不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class,因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以使用全文搜索服务。 - -使用查询时如果遇到性能问题,可参阅 [查询性能优化](#查询性能优化)。 - -### 数组查询 - -下面的代码查找所有数组属性 `tags` 包含 ` 工作 ` 的对象: - -```cs -query.WhereEqualTo("tags", "工作"); -``` - - -下面的代码查询数组属性长度为 3 (正好包含 3 个标签)的对象: - - -```cs -query.WhereSizeEqualTo("tags", 3); -``` - - -下面的代码查找所有数组属性 `tags` **同时包含** ` 工作 `、` 销售 ` 和 ` 会议 ` 的对象: - - -```cs -query.WhereContainsAll("tags", new string[] { "工作", "销售", "会议" }); -``` - - -如需获取某一属性值包含一列值中任意一个值的对象,可以直接用 `WhereContainedIn` 而无需执行多次查询。下面的代码构建的查询会查找所有 `priority` 为 `1` **或** `2` 的 todo 对象: - - -```cs -// 单个查询 -LCQuery priorityOneOrTwo = new LCQuery("Todo"); -priorityOneOrTwo.WhereContainedIn("priority", new string[] { 1, 2 }); -// 这样就可以了 :) - -// --------------- -// vs. -// --------------- - -// 多个查询 -LCQuery priorityOne = new LCQuery("Todo"); -priorityOne.WhereEqualTo("priority", 1); - -LCQuery priorityTwo = new LCQuery("Todo"); -priorityTwo.WhereEqualTo("priority", 2); - -LCQuery priorityOneOrTwo = LCQuery.Or(new LCQuery[] { priorityOne, priorityTwo }); -ReadOnlyCollection results = await priorityOneOrTwo.Find(); -// 好像有些繁琐 :( -``` - - -反过来,还可以用 `WhereNotContainedIn` 来获取某一属性值不包含一列值中任何一个的对象。 - -### 关系查询 - -查询关联数据有很多种方式,常见的一种是查询某一属性值为特定 `LCObject` 的对象,这时可以像其他查询一样直接用 `WhereEqualTo`。比如说,如果每一条博客评论 `Comment` 都有一个 `post` 属性用来存放原文 `Post`,则可以用下面的方法获取所有与某一 `Post` 相关联的评论: - -```cs -LCObject post = LCObject.CreateWithoutData("Post", "57328ca079bc44005c2472d0"); -LCQuery query = new LCQuery("Comment"); -query.WhereEqualTo("post", post); -// comments 包含与 post 相关联的评论 -ReadOnlyCollection comments = await query.Find(); -``` - -如需获取某一属性值为另一查询结果中任一 `LCObject` 的对象,可以用 `WhereMatchesQuery`。下面的代码构建的查询可以找到所有包含图片的博客文章的评论: - -```cs -LCQuery innerQuery = new LCQuery("Post"); -innerQuery.WhereExists("image"); - -LCQuery query = new LCQuery("Comment"); -query.WhereMatchesQuery("post", innerQuery); -``` - -如需获取某一属性值不是另一查询结果中任一 `LCObject` 的对象,则使用 `WhereDoesNotMatchQuery`。 - -有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用 `Include`。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章: - -```cs -LCQuery query = new LCQuery("Comment"); - -// 获取最新发布的 -query.OrderByDescending("createdAt"); - -// 只获取 10 条 -query.Limit(10); - -// 同时包含博客文章 -query.Include("post"); - -// comments 包含最新发布的 10 条评论,包含各自对应的博客文章 -ReadOnlyCollection comments = await query.Find(); -foreach (LCObject comment in comments) { -// 该操作无需网络连接 - LCObject post = comment["post"] as LCObject; -} -``` - - -可以用 dot 符号(`.`)来获取多级关系,例如 `post.author`,详见[《点号使用指南》](https://leancloud.cn/docs/dot-notation.html)的《在查询对象时使用点号》一节。 - -可以在同一查询上应用多次 `Include` 以包含多个属性。 - -通过 `Include` 进行多级查询的方式不适用于数组属性内部的 `LCObject`,只能包含到数组本身。 - -#### 关系查询的注意事项 - -云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌 / 子查询(和普通查询一样,`limit` 默认为 `100`,最大 `1000`),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出 `limit`,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有 `limit` 数量以内的结果会被填入主查询。 - -我们建议采用以下方案进行改进: - -- 确保子查询的结果在 100 条以下,如果在 100 至 1,000 条之间的话请将子查询的 `limit` 设为 `1000`。 -- 将需要查询的字段冗余到主查询所在的表上。 -- 进行多次查询,每次在子查询上设置不同的 `skip` 值来遍历所有记录(注意 `skip` 的值较大时可能会引发性能问题,因此不是很推荐)。 - -### 统计总数量 - -如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用 `Count` 来代替 `Find`。比如说,查询有多少个已完成的 todo: - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("isComplete", true); -int count = await query.Count(); -print($"{count} 个 todo 已完成"); -``` - -### 组合查询 - -组合查询就是把诸多查询条件用一定逻辑合并到一起(`OR` 或 `AND`)再交给云端去查询。 - -组合查询不支持在子查询中包含 `GeoPoint` 或其他非过滤性的限制(例如 `near`、`withinGeoBox`、`limit`、`skip`、`ascending`、`descending`、`include`)。 - -#### OR 查询 - -OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于 `3` 或者已经完成了的 todo: - - -```cs -LCQuery priorityQuery = new LCQuery("Todo"); -priorityQuery.WhereGreaterThanOrEqualTo("priority", 3); - -LCQuery isCompleteQuery = new LCQuery("Todo"); -isCompleteQuery.WhereEqualTo("isComplete", true); - -LCQuery priorityOneOrTwo = LCQuery.Or(new LCQuery[] { priorityQuery, isCompleteQuery }); -ReadOnlyCollection results = await priorityOneOrTwo.Find(); -``` - -使用 OR 查询时,子查询中不能包含 `GeoPoint` 相关的查询。 - -#### AND 查询 - -使用 AND 查询的效果等同于往 `LCQuery` 添加多个条件。下面的代码构建的查询会查找创建时间在 `2016-11-13` 和 `2016-12-02` 之间的 todo: - -```cs -LCQuery startDateQuery = new LCQuery("Todo"); -startDateQuery.WhereGreaterThanOrEqualTo("createdAt", DateTime.Parse("2016-11-13 00:00:00Z")); - -LCQuery endDateQuery = new LCQuery("Todo"); -endDateQuery.WhereLessThan("createdAt", DateTime.Parse("2016-12-03 00:00:00Z")); - -LCQuery query = LCQuery.And(new LCQuery[] { startDateQuery, endDateQuery }); -ReadOnlyCollection results = await query.Find(); -``` - -单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询: - - -```cs -LCQuery createdAtQuery = new LCQuery("Todo"); -createdAtQuery.WhereGreaterThanOrEqualTo("createdAt", DateTime.Parse("2018-04-30 00:00:00Z")); -createdAtQuery.WhereLessThan("createdAt", DateTime.Parse("2018-05-01 00:00:00Z")); - -LCQuery locationQuery = new LCQuery("Todo"); -locationQuery.WhereDoesNotExist("location"); - -LCQuery priority2Query = new LCQuery("Todo"); -priority2Query.WhereEqualTo("priority", 2); - -LCQuery priority3Query = new LCQuery("Todo"); -priority3Query.WhereEqualTo("priority", 3); - -LCQuery priorityQuery = LCQuery.Or(new LCQuery[] { priority2Query, priority3Query }); -LCQuery timeLocationQuery = LCQuery.Or(new LCQuery[] { locationQuery, createdAtQuery }); -LCQuery query = LCQuery.And(new LCQuery[] { priorityQuery, timeLocationQuery }); -``` - -### 查询性能优化 - -影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。 - -- 不等于和不包含查询(无法使用索引) -- 通配符在前面的字符串查询(无法使用索引) -- 有条件的 `count`(需要扫描所有数据) -- `skip` 跳过较多的行数(相当于需要先查出被跳过的那些行) -- 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引) -- 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据) - -## LiveQuery - -LiveQuery 衍生于 [`LCQuery`](#查询),并为其带来了更强大的功能。它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。 - -设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用 `LCQuery` 并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。 - -想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的 `LCQuery`。订阅成功后,一旦有符合 `LCQuery` 的 `LCObject` 发生变化,云端就会主动、实时地将信息通知到客户端。 - -LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。 - -### 启用 LiveQuery - -进入 **云服务控制台 > 数据存储 > 设置**,在 **安全设置** 里面勾选 **启用 LiveQuery** - -```cs -using LeanCloud.LiveQuery; -``` - -### Demo - -下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果: - - - -使用我们的「LeanTodo」微信小程序和网页应用,可以实际体验以上视频所演示的效果,步骤如下: - -1. 微信扫码,添加小程序「LeanTodo」; - - ![LeanTodo mini program](/img/leantodo-weapp-qr.jpg) - -2. 进入小程序,点击首页左下角 **设置** > **账户设置**,输入便于记忆的用户名和密码; - -3. 使用浏览器访问 ,输入刚刚在小程序中更新好的账户信息,点击 **Login**; - -4. 随意添加更改数据,查看两端的同步状态。 - -注意按以上顺序操作。在网页应用中使用 **Signup** 注册的账户无法与小程序创建的账户相关联,所以如果颠倒以上操作顺序,则无法观测到数据同步效果。 - -[LiveQuery 公开课](http://www.bilibili.com/video/av11291992/) 涵盖了许多开发者关心的问题和解答。 - -### 构建订阅 - -首先创建一个普通的 `LCQuery` 对象,添加查询条件(如有),然后进行订阅操作: - - -```cs -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("isComplete", true); -await query.Subscribe(); -// 订阅成功 -``` - - -LiveQuery 不支持内嵌查询,也不支持返回指定属性。 - -订阅成功后,就可以接收到和 `LCObject` 相关的更新了。假如在另一个客户端上创建了一个 `Todo` 对象,对象的 `title` 设为 ` 更新作品集 `,那么下面的代码可以获取到这个新的 `Todo`: - -```cs -LCQuery query = new LCQuery("Todo"); -LCLiveQuery liveQuery = await query.Subscribe(); -liveQuery.OnCreate = (obj) => { - print(obj["title"]); // 更新作品集 -}; -``` - - -此时如果有人把 `Todo` 的 `content` 改为 ` 把我最近画的插画放上去 `,那么下面的代码可以获取到本次更新: - -```cs -liveQuery.OnUpdate = (updatedTodo, updatedKeys) => { - print(updatedTodo["content"]); // 把我最近画的插画放上去 -}; -``` - -### 事件处理 - -订阅成功后,可以选择监听如下几种数据变化: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` 事件 - -当有新的满足 `LCQuery` 查询条件的 `LCObject` 被创建时,`create` 事件会被触发。下面的 `obj` 就是新建的 `LCObject`: - -```cs -liveQuery.OnCreate = (obj) => { - print("对象被创建。"); -}; -``` - -#### `update` 事件 - -当有满足 `LCQuery` 查询条件的 `LCObject` 被更新时,`update` 事件会被触发。下面的 `obj` 就是有更新的 `LCObject`: - -```cs -liveQuery.OnUpdate = (obj, updatedKeys) => { - print("对象被更新。"); -}; -``` - -#### `enter` 事件 - -当一个已存在的、原本不符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后符合查询条件,`enter` 事件会被触发。下面的 `obj` 就是进入 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```cs -liveQuery.OnEnter = (obj, updatedKeys) => { - print("对象进入。"); -}; -``` - -注意区分 `create` 和 `enter` 的不同行为。如果一个对象已经存在,在更新之前不符合查询条件,而在更新之后符合查询条件,那么 `enter` 事件会被触发。如果一个对象原本不存在,后来被构建了出来,那么 `create` 事件会被触发。 - -#### `leave` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后不符合查询条件,`leave` 事件会被触发。下面的 `obj` 就是离开 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```cs -liveQuery.OnLeave = (obj, updatedKeys) => { - print("对象离开。"); -}; -``` - -#### `delete` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 被删除,`delete` 事件会被触发。下面的 `objId` 就是被删除的 `LCObject` 的 `objectId`: - -```cs -liveQuery.OnDelete = (objId) => { - print("对象被删除。"); -}; -``` - -### 取消订阅 - -如果不再需要接收有关 `LCQuery` 的更新,可以取消订阅。 - -```cs -await liveQuery.Unsubscribe(); -// 成功取消订阅 -``` - -### 断开连接 - -断开连接有几种情况: - -1. 网络异常或者网络切换,非预期性断开。 -2. 退出应用、关机或者打开飞行模式等,用户在应用外的操作导致断开。 - -如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。 - -而另外一种极端情况——**当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下**,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。 - -### LiveQuery 的注意事项 - -因为 LiveQuery 的实时性,很多用户会陷入一个误区,试着用 LiveQuery 来实现一个简单的聊天功能。 -我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,后期维护的难度非常大(聊天记录、对话维护之类的代码会很混乱),并且云服务已经提供了即时通讯的服务。 -LiveQuery 的核心还是提供一个针对查询的推拉结合的用法,脱离设计初衷容易造成前端的模块混乱。 - -## 文件 - -有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用 `LCObject` 保存,此时文件对象 `LCFile` 便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。 - -### 构建文件 - -可以通过字符串构建文件: -```cs -LCFile file = new LCFile("resume.txt", UTF8.GetBytes("LeanCloud")); -``` - -除此之外,还可以通过 URL 构建文件: - -```cs -LCFile file = new LCFile("logo.png", new Uri("https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png")); -``` - -通过 URL 构建文件时,SDK 并不会将原本的文件转储到云端,而是会将文件的物理地址存储为字符串,这样也就不会产生任何文件上传流量。使用其他方式构建的文件会被保存在云端。 - -云端会根据文件扩展名自动检测文件类型。如果需要的话,也可以手动指定 `Content-Type`(一般称为 MIME 类型): - -```cs -LCFile file = new LCFile("resume.txt", UTF8.GetBytes("LeanCloud")); -file.MimeType = "application/json"; -``` - -与前面提到的方式相比,一个更常见的文件构建方式是从本地路径上传。 - -```cs -LCFile file = new LCFile("avatar.jpg", "./avatar.jpg"); -``` - -这里上传的文件名字叫做 `avatar.jpg`。需要注意: - -- 每个文件会被分配到一个独一无二的 `objectId`,所以在一个应用内是允许多个文件重名的。 -- 文件必须有扩展名才能被云端正确地识别出类型。比如说要用 `LCFile` 保存一个 PNG 格式的图像,那么扩展名应为 `.png`。 -- 如果文件没有扩展名,且没有手动指定类型,那么云服务将默认使用 `application/octet-stream`。 - -### 保存文件 - -将文件保存到云端后,便可获得一个永久指向该文件的 URL: - -```cs -await file.Save(); -print(file.Url); -``` - -文件上传后,可以在 `_File` class 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的 `objectId` 和 URL。 - - -已经保存到云端的文件可以关联到 `LCObject`: - -```cs -LCObject todo = new LCObject("Todo"); -todo["title"] = "买蛋糕"; -// attachments 是一个 File 类型 -todo.Add("attachments", file); -await todo.Save(); -``` - -也可以通过构建 `LCQuery` 进行[查询](#查询): - -```cs -LCQuery query = LCFile.GetQuery(); -``` - -需要注意的是,内部文件(上传到文件服务的文件)的 `url` 字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 -因此,通过 url 字段查询文件仅适用于外部文件(直接保存外部 URL 到 `_File` 表创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。 - -注意,如果文件被保存到了 `LCObject` 的一个数组属性中,那么在查询 `LCObject` 时如果需要包含文件,则要用到 `LCQuery` 的 `Include` 方法。比如说,在获取所有标题为 ` 买蛋糕 ` 的 todo 的同时获取附件中的文件: - -```cs -// 获取同一标题且包含附件的 todo -LCQuery query = new LCQuery("Todo"); -query.WhereEqualTo("title", "买蛋糕"); -query.WhereExists("attachments"); - -// 同时获取附件中的文件 -query.Include("attachments"); -ReadOnlyCollection todos = await query.Find(); -foreach (LCObject todo in todos) { - // 获取每个 todo 的 attachments 数组 - List attachments = todo["attachments"] as List; -} -``` - -### 上传进度监听 - -上传过程中可以实时向用户展示进度: - -```cs -await file.Save((count, total) => { - print($"{count}/{total}"); - if (count == total) { - print("done"); - } -}); -``` - -### 文件元数据 - -上传文件时,可以用 `metaData` 添加额外的属性。文件一旦保存,`metaData` 便不可再修改。 - -```cs -file.AddMetaData("size", 1024); -file.AddMetaData("width", 128); -file.AddMetaData("height", 256); -file.MimeType = "image/jpg"; -await file.Save(); -``` - -### 图像缩略图 - -成功保存图像后,除了可以获取指向该文件的 URL 外,还可以获取图像的缩略图 URL,并且可以指定缩略图的宽度和高度: - -```cs -// 获得宽度为 100 像素,高度为 200 像素的缩略图 url -string url = file.GetThumbnailUrl(100, 200); -``` - -图片最大不超过 **20 MB** 才可以获取缩略图。 - -国际版不支持图片缩略图。 - -### 删除文件 - -下面的代码从云端删除一个文件: - -```cs -LCFile file = LCObject.CreateWithoutData("_File", "552e0a27e4b0643b709e891e"); -await file.Delete(); -``` - -默认情况下,文件的删除权限是关闭的,需要进入 **云服务控制台 > 数据存储 > 结构化数据 > `_File`**,选择 **其他** > **权限设置** > **`delete`** 来开启。 - -## GeoPoint - -云服务允许你通过将 `LCGeoPoint` 关联到 `LCObject` 的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。 - -要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个 `LCGeoPoint` 并将其纬度(`latitude`)设为 `39.9`,经度(`longitude`)设为 `116.4`: - -```cs -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -``` - -现在可以将这个地理位置存储为一个对象的属性: - -```cs -todo["location"] = point; -``` - -### 地理位置查询 - -给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 `LCQuery` 添加 `WhereNear` 条件。下面的代码查找 `location` 属性值离某一点最近的 `Todo` 对象: - -```cs -LCQuery query = new LCQuery("Todo"); -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -query.WhereNear("location", point); - -// 限制为 10 条结果 -query.Limit(10); -// todos 是包含满足条件的 Todo 对象的数组 -ReadOnlyCollection todos = await query.Find(); -``` - -像 `OrderByAscending` 和 `OrderByDescending` 这样额外的排序条件会获得比默认的距离排序更高的优先级。 - -若要限制结果和给定地点之间的距离,可以参考 API 文档中的 参数。 - -若要查询在某一矩形范围内的对象,可以用 `WhereWithinGeoBox`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```cs -LCQuery query = new LCQuery("Todo"); -LCGeoPoint southwest = new LCGeoPoint(30, 115); -LCGeoPoint northeast = new LCGeoPoint(40, 118); -query.WhereWithinGeoBox("location", southwest, northeast); -``` - -### GeoPoint 的注意事项 - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -## 用户 - -用户系统几乎是每款应用都要加入的功能,我们为此专门提供了一个 `LCUser` 类来方便应用使用各项用户管理的功能。 - -`LCUser` 是 `LCObject` 的子类,这意味着任何 `LCObject` 提供的方法也适用于 `LCUser`,唯一的区别就是 `LCUser` 提供一些额外的用户管理相关的功能。每个应用都有一个专门的 `_User` class 用于存放所有的 `LCUser`。 - -### 用户的属性 - -`LCUser` 相比一个普通的 `LCObject` 多出了以下属性: - -- `username`:用户的用户名。 -- `password`:用户的密码。 -- `email`:用户的电子邮箱。 -- `emailVerified`:用户的电子邮箱是否已验证。 -- `mobilePhoneNumber`:用户的手机号。 -- `mobilePhoneVerified`:用户的手机号是否已验证。 - -在接下来对用户功能的介绍中我们会逐一了解到这些属性。 - -### 注册 - -用户第一次打开应用的时候,可以让用户注册一个账户。下面的代码展示了一个典型的使用用户名和密码注册的流程: - - -```cs -// 创建实例 -LCUser user = new LCUser(); - -// 等同于 user["username"] = "Tom"; -user.Username = "Tom"; -user.Password = "cat!@#123"; - -// 可选 -user.Email = "tom@leancloud.rocks"; -user.Mobile = "+8618200008888"; - -// 设置其他属性的方法跟 LCObject 一样 -user["gender"] = "secret"; -await user.SignUp(); -``` - - -新建 `LCUser` 的操作应使用 `SignUp` 而不是 `Save`,但以后的更新操作就可以用 `Save` 了。 - -如果收到 `202` 错误码,意味着 `_User` 表里已经存在使用同一 `username` 的账号,此时应提示用户换一个用户名。除此之外,每个用户的 `email` 和 `mobilePhoneNumber` 也需要保持唯一性,否则会收到 `203` 或 `214` 错误。 -可以考虑在注册时把用户的 `username` 设为与 `email` 相同,这样用户可以直接 [用邮箱重置密码](#重置密码)。 - -采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码(云端对密码的长度、复杂度不作限制),并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调 **在客户端,应用切勿再次对密码加密,这会导致 [重置密码](#重置密码) 等功能失效**。 - - -### 登录 - -下面的代码用用户名和密码登录一个账户: - -```cs -try { - // 登录成功 - LCUser user = await LCUser.Login("Tom", "cat!@#123"); -} catch (LCException e) { - // 登录失败(可能是密码错误) - print($"{e.code} : {e.message}"); -} -``` - -#### 邮箱登录 - -下面的代码用邮箱和密码登录一个账户: - -```cs -try { - // 登录成功 - LCUser user = await LCUser.LoginByEmail("tom@leancloud.rocks", "cat!@#123"); -} catch (LCException e) { - // 登录失败(可能是密码错误) - print($"{e.code} : {e.message}"); -} -``` - - -#### 单设备登录 - -某些场景下需要确保用户的账户在同一时间只在一台设备上登录,也就是说当用户在一台设备上登录后,其他设备上的会话全部失效。可以按照以下方案来实现: - -1. 新建一个专门用于记录用户登录信息和当前设备信息的 class。 -2. 每当用户在新设备上登录时,将该 class 中该用户对应的设备更新为该设备。 -3. 在另一台设备上打开客户端时,检查该设备是否与云端保存的一致。若不一致,则将用户 [登出](#当前用户)。 - -#### 账户锁定 - -输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 `{ "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." }`,开发者可在客户端进行必要提示。 - -锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 - -### 验证邮箱 - -可以通过要求用户在登录或使用特定功能之前验证邮箱的方式防止恶意注册。默认情况下,当用户注册或变更邮箱后,`emailVerified` 会被设为 `false`。在应用的 **控制台 > 数据存储 > 用户 > 设置** 中,可以开启 **启用邮箱验证功能** 选项,这样当用户注册或变更邮箱时,会收到一封含有验证链接的邮件。在同一设置页面还可找到阻止未验证邮箱的用户登录的选项。 - -如果用户忘记点击链接并且在未来某一时刻需要进行验证,可以用下面的代码发送一封新的邮件: - -```cs -await LCUser.RequestEmailVerify("tom@leancloud.rocks"); -``` - -用户点击邮件内的链接后,`emailVerified` 会变为 `true`。如果用户的 `email` 属性为空,则该属性永远不会为 `true`。 - -### 当前用户 - -用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户: - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -会话信息会长期有效,直到用户主动登出: - -```cs -await LCUser.Logout(); - -// currentUser 变为 null -LCUser currentUser = await LCUser.GetCurrent(); -``` - -### 设置当前用户 - -用户登录后,云端会返回一个 **session token** 给客户端,它会由 SDK 缓存起来并用于日后同一 `LCUser` 的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个 `LCUser` 发起的请求了。 - -以下是一些应用可能需要用到 session token 的场景: - -- 应用根据以前缓存的 session token 登录(可以通过 `SessionToken` 属性获取到当前用户的 session token,在服务端等受信任的环境下,可以通过 `Master Key` 读取任意用户的 `sessionToken` 字段以获取 session token)。 -- 应用内的某个 WebView 需要知道当前登录的用户。 -- 在服务端登录后,返回 session token 给客户端,客户端根据返回的 session token 登录。 - -下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效): - -```cs -await LCUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf"); -``` - -请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。 - -如果在 **控制台 > 数据存储 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 `403 (Forbidden)` 错误。 - -下面的代码检查 session token 是否有效: - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -bool isAuthenticated = await currentUser.IsAuthenticated(); -if (isAuthenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - -### 重置密码 - -我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了多种重置密码的方法。 - -邮箱重置密码的流程如下: - -1. 用户输入注册的电子邮箱,请求重置密码; -2. 云端向该邮箱发送一封包含重置密码的特殊链接的电子邮件; -3. 用户点击重置密码链接后,一个特殊的页面会打开,让他们输入新密码; -4. 用户的密码已被重置为新输入的密码。 - -首先让用户填写注册账户时使用的邮箱,然后调用下面的方法: - -```cs -await LCUser.RequestPasswordReset("tom@leancloud.rocks"); -``` - -上面的代码会查询 `_User` 表中是否有对象的 `email` 属性与前面提供的邮箱匹配。如果有的话,则向该邮箱发送一封密码重置邮件。之前提到过,应用可以让 `username` 与 `email` 保持一致,也可以单独收集用户的邮箱并将其存为 `email`。 - -密码重置邮件的内容可在应用的 **云服务控制台 > 数据存储 > 用户 > 邮件模版** 中自定义。更多关于自定义邮件模板和验证链接的内容,请参考[《自定义邮件验证和重设密码页面》](https://leancloud.cn/docs/custom-reset-verify-page.html)。 - - -### 用户的查询 - -可以直接构建一个针对 `_User` 的 `LCQuery` 来查询用户: - -```cs -LCQuery userQuery = LCUser.GetQuery(); -``` - -为了安全起见,**新创建的应用的 `_User` 表默认关闭了 `find` 权限**,这样每位用户登录后只能查询到自己在 `_User` 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 `find` 查询权限。除此之外,还可以在云引擎里封装用户查询相关的方法。 - -可以参见 [用户对象的安全](#用户对象的安全) 来了解 `_User` 表的一些限制,还可以阅读[数据和安全](/v2/sdk/storage/guide/security)来了解更多 class 级权限设置的方法。 - -### 关联用户对象 - -关联 `LCUser` 的方法和 `LCObject` 是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书: - -```cs -LCObject book = new LCObject("Book"); -LCUser author = await LCUser.GetCurrent(); -book["title"] = "我的第五本书"; -book["author"] = author; -await book.Save(); - -LCQuery query = new LCQuery("Book"); -query.WhereEqualTo("author", author); -// books 是包含同一作者所有 Book 对象的数组 -ReadOnlyCollection books = await query.Find(); -``` - -### 用户对象的安全 - -`LCUser` 类自带安全保障,只有通过 `Login` 或者 `SignUp` 这种经过鉴权的方法获取到的 `LCUser` 才能进行保存或删除相关的操作,保证每个用户只能修改自己的数据。 - -这样设计是因为 `LCUser` 中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。 - -下面的代码展现了这种安全措施: - -```cs -try { - LCUser user = await LCUser.Login("Tom", "cat!@#123"); - // 试图修改用户名 - user["username"] = "Jerry"; - // 密码已被加密,这样做会获取到空字符串 - string password = user["password"]; - // 可以执行,因为用户已鉴权 - await user.Save(); - - // 绕过鉴权直接获取用户 - LCQuery userQuery = LCUser.GetQuery(); - LCUser unauthenticatedUser = await userQuery.Get(user.ObjectId); - unauthenticatedUser["username"] = "Toodle"; - - // 会出错,因为用户未鉴权 - unauthenticatedUser.Save(); -} catch (LCException e) { - print($"{e.code} : {e.message}"); -} -``` - -通过 `LCUser.GetCurrent()` 获取的 `LCUser` 总是经过鉴权的。 - -要查看一个 `LCUser` 是否经过鉴权,可以调用 `IsAuthenticated` 方法。通过经过鉴权的方法获取到的 `LCUser` 无需进行该检查。 - -注意,用户的密码只能在注册的时候进行设置,日后如需修改,只能通过 [重置密码](#重置密码) 的方式进行。密码不会被缓存在本地。如果尝试直接获取已登录用户的密码,会得到 `null`。 - -### 其他对象的安全 - -对于给定的一个对象,可以指定哪些用户有权限读取或修改它。为实现该功能,每个对象都有一个由 `ACL` 对象组成的访问控制表。请参阅[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html)。 - -### 第三方账户登录 - -云服务支持应用层直接使用第三方社交平台(例如微信、微博、QQ 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。 - -例如以下的代码展示了终端用户使用微信登录的处理流程: - -```cs -Dictionary thirdPartyData = new Dictionary { - // 必须 - { "openid", "OPENID" }, - { "access_token", "ACCESS_TOKEN" }, - { "expires_in", 7200 }, - - // 可选 - { "refresh_token", "REFRESH_TOKEN" }, - { "scope", "SCOPE" } -}; -LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin"); -``` - -`LCUser#loginWithAuthData` 系列方法需要两个参数来唯一确定一个账户: - -- 第三方平台的名字,就是前例中的 `weixin`,该名字由应用层自己决定。 -- 第三方平台的授权信息,就是前例中的 `thirdPartyData`(一般包括 `uid`、`token`、`expires` 等信息,与具体的第三方平台有关)。 - -云端会使用第三方平台的鉴权信息来查询是否已经存在与之关联的账户。如果存在的话,则返回 `200 OK` 状态码,同时附上用户的信息(包括 [`sessionToken`](#设置当前用户))。如果第三方平台的信息没有和任何账户关联,客户端会收到 `201 Created` 状态码,意味着新账户被创建,同时附上用户的 `objectId`、`createdAt`、`sessionToken` 和一个自动生成的 `username`,例如: - -```json -{ - "username": "k9mjnl7zq9mjbc7expspsxlls", - "objectId": "5b029266fb4ffe005d6c7c2e", - "createdAt": "2018-05-21T09:33:26.406Z", - "updatedAt": "2018-05-21T09:33:26.575Z", - "sessionToken": "…", - // authData 通常不会返回,继续阅读以了解其中原因 - "authData": { - "weixin": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" - } - } - // … -} -``` - -这时候我们会看到 `_User` 表中出现了一条新的账户记录,账户中有一个名为 `authData` 的列,保存了第三方平台的授权信息。出于安全考虑,`authData` 不会被返回给客户端,除非它属于当前用户。 - -开发者需要自己完成第三方平台的鉴权流程(一般通过 OAuth 1.0 或 2.0),以获取鉴权信息,继而到云端来登录。 - -#### Signin With Apple -如果你需要开发 [Sigin With Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api),云服务可以帮你校验 `identityToken`,并获取 Apple 的 `access_token`。Apple Sign In 的 `authData` 结构如下: - -``` -{ - "lc_apple": { - "uid" : "从 Apple 获取到的 User Identifier", - "identity_token" : "从苹果获取到的 identityToken" - "code" : "从苹果获取到的 Authorization Code" - } -} -``` -`authData` 中的 key 的作用: - -* **`lc_apple`**:只有 platform 为 `lc_apple` 时,云服务才会执行 `identity_token` 和 `code` 的逻辑。 -* **`uid`**:必填。云服务通过 `uid` 判断是否存在用户。 -* **`identity_token`**:可选。`authData` 中有 `identity_token` 时云端会自动校验 `identity_token` 的有效性。开发者需要在云服务控制台「存储」-「用户」-「设置」-「第三方集成」中填写 Apple 的相关信息。 -* **`code`**:可选。`authData` 中有 `code` 时云端会自动用该 `code` 向 Apple 换取 `access_token` 和 `refresh_token`。开发者需要在云服务控制台「存储」-「用户」-「设置」-「第三方集成」中填写 Apple 的相关信息。 - -##### 获取 Client ID - -Client ID 用于校验 `identity_token` 及获取 `access_token`,指的是 Apple 应用的 identifier,也就是 `AppID` 或 `serviceID`。对于原生应用来说,指的是 Xcode 中的 Bundle Identifier,例如 `com.mytest.app`。详情请参考 [Apple 的文档](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)。 - -##### 获取 Private Key 及 Private Key ID - -Private Key 用于获取 `access_token`。登录 Apple 开发者平台,在左侧的 「Certificates, Identifiers & Profiles」 中选择 「Keys」,添加一个用于 Apple Sign In 的 Private Key,下载 XXXXX.p8 文件,同时在下载 Key 的页面获得 Private Key ID。详情请参考[ Apple 的文档](https://help.apple.com/developer-account/#/dev77c875b7e)。 - -将 Key ID 填写到控制台,将下载下来的 Private Key 文件上传到控制台。控制台只能上传 Private Key 文件,无法查看及下载其内容。 - -##### 获取 Team ID - -Team ID 用于获取 `access_token`。登录 Apple 开发者平台,在右上角或 Membership 页面即可看到自己所属开发团队的 Team ID。注意选择 Bundle ID 对应的 Team。 - -##### 使用 Apple Sign In 登录云服务 - -在控制台填写完成所有信息后,使用以下代码登录。 - -#### 鉴权数据的保存 - -`_User` class 中的 `authData` 是一个以平台名为键名,鉴权信息为键值的 JSON 对象。 - -一个关联了微信账户的用户应该会有下列对象作为 `authData`: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - } -} -``` - -而一个关联了微博账户的用户,则会有如下的 `authData`: - -```json -{ - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx", - } -} -``` - -我们允许一个账户绑定多个第三方平台的鉴权数据,这样如果某个用户同时关联了微信和微博账户,则其 `authData` 可能会是这样的: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - } - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx", - } -} -``` - -理解 `authData` 的数据结构至关重要。一个终端用户通过如下的鉴权信息来登录的时候, - -```json -"weixin": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" -} -``` - -云端首先会查找账户系统(_User 表),看看是否存在 authData.weixin.openid = “ OPENID ” 的账户,如果存在,则返回现有账户,如果不存在那么就创建一个新账户,同时将上面的鉴权信息写入新账户的 `authData` 属性中,并将新账户的数据当成结果返回。 - -云端会自动为 `_User` class 中每个用户的 `authData..` 创建唯一索引,从而避免重复数据。 -`` 在微信等部分云服务内建支持的第三方平台上为 `openid` 字段,在其他第三方平台(包括部分云服务专门支持的第三方平台和所有云服务没有专门支持的第三方平台)上为 `uid` 字段。 - -#### 自动验证第三方平台授权信息 - -为了确保账户数据的有效性,云端还支持对部分平台的 `Access Token` 的有效性进行自动验证,以防止伪造账户数据。如果有效性验证不通过,云端会返回 `invalid authData` 错误,关联不会被建立。对于云端无法识别的服务,开发者需要自己去验证 `Access Token` 的有效性。 -比如,注册、登录时分别通过云引擎的 `beforeSave hook`、`beforeUpdate hook` 来验证 `Access Token` 有效性。 - -如果希望使用这一功能,则在开始使用前,需要在 **云服务控制台 > 数据存储 > 用户 > 设置** 配置相应平台的 **应用 ID** 和 **应用 Secret Key**。 - -如果不希望云端自动验证 `Access Token`,可以在 **云服务控制台 > 数据存储 > 设置** 里面取消勾选 **第三方登录时,验证用户 AccessToken 合法性**。 - -配置平台账号的目的在于创建 `LCUser` 时,云端会使用相关信息去校验请求参数 `thirdPartyData` 的合法性,确保 `LCUser` 实际对应着一个合法真实的用户,确保平台安全性。 - -#### 绑定第三方账户 - -用户已经有了 LCUser 并登录成功后,可以绑定新的第三方账号信息。 -绑定成功后,新的第三方账户信息会被添加到 LCUser 的 authData 字段里。 - -例如,下面的代码可以关联微信账户: - -```cs -await currentUser.AssociateAuthData(weixinData, "weixin"); -``` - -为节省篇幅,上面的代码示例中没有给出具体的微信平台授权信息,相关内容请参考上面的[「第三方账户登录」](#第三方账户登录)一节。 - -#### 解除与第三方账户的关联 - -类似地,可以解绑第三方账户。 - -例如,下面的代码可以解除用户和微信账户的关联: - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -await currentUser.DisassociateWithAuthData("weixin"); -``` - -#### 扩展:第三方登录时补充完整的用户信息 - -有些产品,新用户在使用第三方账号授权拿到相关信息后,仍然需要补充设置用户名、手机号、密码等重要信息后,才被允许登录成功。 - -这时要使用 `loginWithauthData` 登录接口的 `failOnNotExist` 参数并将其设置为 `true`。服务端会判断是否已存在能匹配上的 `authData`,如果不存在则会返回 `211` 错误码和 `Could not find user` 报错信息。开发者根据这个 `211` 错误码,跳转到要求输入用户名、密码、手机号等信息的页面,实例化一个 `LCUser` 对象,保存上述补充数据,再次调用 `loginWithauthData` 接口进行登录,并 **不再传入 `failOnNotExist` 参数**。示例代码如下: - -```cs -try { - Dictionary thirdPartyData = new Dictionary { - // 必须 - { "openid", "OPENID" }, - { "access_token", "ACCESS_TOKEN" }, - { "expires_in", 7200 }, - - // 可选 - { "refresh_token", "REFRESH_TOKEN" }, - { "scope", "SCOPE" } - }; - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.FailOnNotExist = true; - LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); -} catch (LCException e) { - if (e.code == 211) { - // 不存在 authData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 - } -} - -// 跳转到输入用户名、密码、手机号等业务页面之后 -Dictionary thirdPartyData = new Dictionary { - { "expires_in", 7200 }, - { "openid", "OPENID" }, - { "access_token", "ACCESS_TOKEN" } -}; -try { - LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); - option.FailOnNotExist = true; - LCUser user = await LCUser.LoginWithAuthData(thirdPartyData, "weixin", option: option); - user.Username = "Tom"; - user.Mobile = "+8618200008888"; - await user.Save(); -} catch (LCException e) { - // 其他报错信息 -} -``` - -#### 扩展:接入 UnionID 体系,打通不同子产品的账号系统 - -随着第三方平台的账户体系变得日渐复杂,它们的第三方鉴权信息出现了一些较大的变化。下面我们以最典型的微信开放平台为例来进行说明。 - -当一个用户在移动应用内登录微信账号时,会被分配一个 OpenID;在微信小程序内登录账号时,又会被分配另一个不同的 OpenID。这样的架构会导致的问题是,使用同一个微信号的用户,也无法在微信开发平台下的移动应用和小程序之间互通。 - -微信官方为了解决这个问题,引入了 `UnionID` 的体系,以下为其官方说明: - -> 通过获取用户基本信息接口,开发者可通过 OpenID 来获取用户基本信息,而如果开发者拥有多个公众号,可使用以下办法通过 UnionID 机制来在多公众号之间进行用户帐号互通。只要是同一个微信开放平台帐号下的公众号,用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID 是相同的。 - -其他平台,如 QQ 和微博,与微信的设计也基本一致。 - -云服务支持 `UnionID` 体系。你只需要给 `loginWithauthData` 和 `associateWithauthData` 接口传入更多的第三方鉴权信息,即可完成新 UnionID 体系的集成。新增加的第三方鉴权登录选项包括: - -- unionId,指第三方平台返回的 UnionId 字符串。 -- unionId platform,指 unionId 对应的 platform 字符串,由应用层自己指定,[后面](#该如何指定 -unionIdPlatform)会详述。 -- asMainAccount,指示是否把当前平台的鉴权信息作为主账号来使用。如果作为主账号,那么就由当前用户唯一占有该 unionId,以后其他平台使用同样的 unionId 登录的话,会绑定到当前的用户记录上来;否则,当前应用的鉴权信息会被绑定到其他账号上去。 - -下面让我们通过一个例子来说明如何使用这些参数完成 UnionID 登录。 - -假设云服务在微信开放平台上有两个应用,一个是「云服务通讯」,一个是「云服务技术支持」,这两个应用在接入第三方鉴权的时候,分别使用了 `wxleanoffice` 和 `wxleansupport` 作为 platform 来进行登录。现在我们开启 UnionID 的用户体系,希望同一个微信用户在这两个应用中都能对应到同一个账户系统(_User 表中的同一条记录),同时我们决定将 `wxleanoffice` 平台作为主账号平台。 - -假设对于用户 A,微信给 ta 为 云服务分配的 UnionId 为 `unionid4a`,而对两个应用的授权信息分别为: - -```json -"wxleanoffice": { - "access_token": "officetoken", - "openid": "officeopenid", - "expires_in": 1384686496 -}, -"wxleansupport": { - "openid": "supportopenid", - "access_token": "supporttoken", - "expires_in": 1384686496 -} -``` - -现在,用户 A 在「云服务通讯」中通过微信登录,其调用请求为: - -```cs -Dictionary thirdPartyData = new Dictionary { - // 必须 - { "uid", "officeopenid" }, - { "access_token", "officetoken" }, - { "expires_in", 1384686496 }, - { "unionId", "unionid4a" }, // 新增属性 - - // 可选 - { "refresh_token", "..." }, - { "scope", "SCOPE" } -}; -LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); -option.AsMainAccount = true; -option.UnionIdPlatform = "weixin"; -LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( - thirdPartyData, "wxleanoffice", "unionid4a", - option: option); -``` - -> 注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 `authData..uid` 的唯一索引,具体可以参考[数据存储 REST API 使用详解](/v2/sdk/storage/guide/rest)的《连接用户账户和第三方平台》一节。 - -如果用户 A 是第一次在「云服务通讯」中通过微信登录,那么 _User 表中会增加一个新用户(假设其 objectId 为 `ThisIsUserA`),其 `authData` 的结果如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { // 新增键值对 - "uid": "unionid4a" - } -} -``` - -可以看到,与之前的第三方登录 API 相比,这里由于登录时指定了 `asMainAccount` 为 true,所以 authData 的第一级子目录中增加了 `_weixin_unionid` 的键值对,这里的 `weixin` 就是我们指定的 `unionIdPlatform` 的值。`_weixin_unionid` 这个增加的键值对非常重要,以后我们判断是否存在同样 UnionID 的账户就是依靠它来查找的,而是否增加这个键值对,则是由登录时指定的 `asMainAccount` 的值决定的: - -- 当 `asMainAccount` 为 true 时,云端会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,当前账号就会作为这一个 UnionID 对应的主账号被唯一确定。 -- 当 `asMainAccount` 为 false 时,云端不会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,此时如果通过提供的 UnionID 可以找到主账号,则会将当前的鉴权信息合并进主账号的 `authData` 属性里,同时返回主账号对应的 _User 表记录;如果通过提供的 UnionID 找不到主账号,则会根据平台的 `openid` 去查找账户,找到匹配的账户就返回匹配的,找不到就新建一个账户,此时的处理逻辑与不使用 UnionID 时的逻辑完全一致。 - -接下来,用户 A 继续在「云服务技术支持」中进行微信登录,其登录逻辑为: - -```cs -Dictionary thirdPartyData = new Dictionary { - // 必须 - { "uid", "supportopenid" }, - { "access_token", "supporttoken" }, - { "expires_in", 1384686496 }, - { "unionId", "unionid4a" }, - - // 可选 - { "refresh_token", "..." }, - { "scope", "SCOPE" } -}; -LCUserAuthDataLoginOption option = new LCUserAuthDataLoginOption(); -option.AsMainAccount = false; -option.UnionIdPlatform = "weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -LCUser currentUser = await LCUser.LoginWithAuthDataAndUnionId( - thirdPartyData, "wxleansupport", "unionid4a", - option: option); -``` - -与「云服务通讯」中的登录过程相比,在「云服务技术支持」这个应用中,我们在登录时只是将 `asMainAccount` 设为了 false。 这时我们看到,本次登录得到的还是 objectId 为 `ThisIsUserA` 的 _User 表记录(同一个账户),同时该账户的 `authData` 属性中发生了变化,多了 `wxleansupport` 的数据,如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - }, - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -在新的登录方式中,当一个用户以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录得到新的 `LCUser` 后,接下来这个用户以「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 `wxleansupport` 的新用户数据加入到已有账户的 `authData` 里了,不会再创建新的账户。 - -这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个 `LCUser` 上,实现互通。 - -##### 为 UnionID 建立索引 - -云端会为 UnionID 自动建立索引,不过因为自动创建基于请求的抽样统计,可能会滞后。 -因此,我们推荐自行创建相关索引,特别是用户量(`_User` 表记录数)很大的应用,更需要预先创建索引,否则用户使用 UnionID 账号登录时可能超时失败。 -以上面的微信 UnionID 为例,建议在控制台预先创建下列唯一索引(允许缺失值): - -- `authData.wxleanoffice.uid` -- `authData.wxleansupport.uid` -- `authData._weixin_unionid.uid` - -##### 该如何指定 unionIdPlatform - -从上面的例子可以看出,使用 UnionID 登录的时候,需要指定 `unionIdPlatform` 的主要目的,就是为了便于查找已经存在的唯一主账号。云端会在主账号对应账户的 `authData` 属性中增加一个 `_{unionIdPlatform}_unionid` 键值对来标记唯一性,终端用户在其他应用中登录的时候,云端会根据参数中提供的 `uniondId` + `unionIdPlatform` 的组合,在 `_User` 表中进行查找,这样来确定唯一的既存主账号。 - -本来 `unionIdPlatform` 的取值,应该是开发者可以自行决定的,但是 Javascript SDK 基于易用性的目的,在 `loginWithAuthDataAndUnionId` 之外,还额外提供了两个接口: - -- `AV.User.loginWithQQAppWithUnionId`,这里默认使用 `qq` 作为 `unionIdPlatform`。 -- `AV.User.loginWithWeappWithUnionId`,这里默认使用 `weixin` 作为 `unionIdPlatform`。 - -从我们的统计来看,这两个接口已经被很多开发者接受,在大量线上产品中产生着实际数据。所以为了避免数据在不同平台(例如 Android 和 iOS 应用)间发生冲突,建议大家统一按照 Javascript SDK 的默认值来设置 `unionIdPlatform`,即: - -- 微信平台的多个应用,统一使用 `weixin` 作为 `unionIdPlatform`; -- QQ 平台的多个应用,统一使用 `qq` 作为 `unionIdPlatform`; -- 微博平台的多个应用,统一使用 `weibo` 作为 `unionIdPlatform`; -- 除此之外的其他平台,开发者可以自行定义 `unionIdPlatform` 的名字,只要自己保证多个应用间统一即可。 - -##### 主副应用不同登录顺序出现的不同结果 - -上面的流程是用户先登录了「云服务通讯」这个主应用,然后再登录「云服务技术支持」这个副应用,所以账号都被通过 UnionID 有效关联起来了。可能有人会想到另外一个问题,如果用户 B 先登录副应用,后登录主应用,这时候会发生什么事情呢? - -用户 B 首先登录副应用的时候,提供了「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,并且指定「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 false」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个 `LCUser` 对象,该账户 `authData` 结果为: - -```json -{ - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,以及「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 true」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个 `LCUser` 对象,该账户 `authData` 结果为: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - } -} -``` - -还有更复杂的情况。如果某公司的产品之前就接入了微信登录,产生了很多存量用户,并且分散在不同的子产品中,这时候怎么办?我们接下来专门讨论此时的解决方案。 - -##### 存量账户如何通过 UnionID 实现关联 - -还是以我们的两个子产品「云服务通讯」(后续以「产品 1」指代)和「云服务技术支持为例」(后续以「产品 2」指代),在接入 UnionID 之前,我们就接入了之前版本的微信平台登录,这时候账户系统内可能存在多种账户: - -- 只使用产品 1 的微信用户 A -- 只使用产品 2 的微信用户 B -- 同时使用两个产品的微信用户 C - -此时的存量账户表如下所示: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1) | N/A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1) | N/A -4 | UserC | openid4(对应产品 2) | N/A - -现在我们对两个子产品进行升级,接入 UnionID 体系。这时因为已经有同一个微信用户在不同子产品中创建了不同的账户(例如 objectId 为 3 和 4 的账户),我们需要确定以哪个平台的账号为主。比如决定使用「云服务通讯」上生成的账号为主账号,则在该应用程序更新版本时,使用 `asMainAccount=true` 参数。这个应用带着 UnionID 登录匹配或创建的账号将作为主账号,之后所有这个 UnionID 的登录都会匹配到这个账号。请注意这时 `_User` 表里会剩下一些用户数据,也就是没有被选为主账号的、其他平台的同一个用户的旧账号数据(例如 objectId 为 2 和 4 的账户)。这部分数据会继续服务于已经发布的但仍然使用 OpenID 登录的旧版应用。 - -接下来我们看一下,如果以产品 1 的账户作为「主账户」,按照前述的方式同时提供 openid/unionid 完成登录,则最后达到的结果是: - -1. 使用老版本的用户,不管在哪个产品里面,都可以和往常一样通过 openid 登录到正确的账户; -2. 使用产品 1 的新版本的老用户,通过 openid/unionid 组合,还是绑定到原来的账户。例如 UserC 在产品 1 中通过 openid3/unionId3 还是会绑定到 objectId=3 的账户(会增加 uniondId 记录);而 UserC 在产品 2 的新版本中,通过 openid4/unionId3 的组合则会绑定到 objectId=3 的账户,而不再是原来的 objectId=4 的账户。 -3. 使用产品 1 的新版本的新用户,通过 openid/unionid 组合,会创建新的账户;之后该用户再使用产品 2 的新版本,也会绑定到刚才创建的新账户上。 - -以上面的三个用户为例,他们分别升级到两个产品的最新版,且最终都会启用两个产品,则账户表的最终结果如下: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C -4 | UserC | openid4(对应产品 2) | N/A -5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B - -之后有新的用户 D,分别在两个产品的新版本中登录,则账户表中会增加一条新的 objectId=6 的记录,结果如下: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C -4 | UserC | openid4(对应产品 2) | N/A -5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B -6 | UserD | openid7(对应产品 1)/openid8(对应产品 2) | unionId_user_D - -如果之后我们增加了新的子产品 3,这些用户在子产品 3 中也进行微信登录的话,那么四个用户还是会继续绑定到 objectId 为 1/3/5/6 的主账户。此时账户表的结果会变为: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1)/openid6(对应产品 2)/openid9(对应产品 3) | unionId_user_A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1)/openid4(对应产品 2)/openid10(对应产品 3) | unionId_user_C -4 | UserC | openid4(对应产品 2) | N/A -5 | UserB | openid5(对应产品 1)/openid2(对应产品 2)/openid11(对应产品 3) | unionId_user_B -6 | UserD | openid7(对应产品 1)/openid8(对应产品 2)/openid12(对应产品 3) | unionId_user_D - -### 匿名用户 - -将数据与用户关联需要首先创建一个用户,但有时你不希望强制用户在一开始就进行注册。使用匿名用户,可以让应用不提供注册步骤也能创建用户。下面的代码创建一个新的匿名用户: - -```cs -await LCUser.LoginAnonymously(); -``` - -可以像给普通用户设置属性那样给匿名用户设置 `username`、`password`、`email` 等属性,还可以通过走正常的注册流程来将匿名用户转化为普通用户。匿名用户能够: - -- [使用用户名和密码注册](#注册) -- [关联第三方平台](#第三方账户登录),比如微信 - -下面的代码为一名匿名用户设置用户名和密码: - -```cs -LCUser currentUser = await LCUser.LoginAnonymously(); -currentUser.Username = "Tom"; -currentUser.Password = "cat!@#123"; - -await currentUser.SignUp(); -``` - -下面的代码检查当前用户是否为匿名用户: - -```cs -LCUser currentUser = await LCUser.GetCurrent(); -if (currentUser.IsAnonymous) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - -如果匿名用户未能在登出前转化为普通用户,那么该用户将无法再次登录同一账户,且之前产生的数据也无法被取回。 - -## 角色 - -随着用户量的增长,你可能会发现相比于为每一名用户单独设置权限,将预先设定好的权限直接分配给一部分用户是更好的选择。为了迎合这种需求,云服务支持基于角色的权限管理。请参阅[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html)。 - -## 全文搜索 - -全文搜索是一个针对应用数据进行全局搜索的接口,它基于搜索引擎构建,提供更强大的搜索功能。要深入了解其用法和阅读示例代码,请阅读[全文搜索指南](/v2/sdk/storage/guide/fulltext-search)。 diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/03-setup-java.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/03-setup-java.mdx deleted file mode 100644 index 8d4fac10b..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/03-setup-java.mdx +++ /dev/null @@ -1,437 +0,0 @@ ---- -id: setup-java -title: 数据存储、即时通讯 Java SDK 配置 -sidebar_label: Java SDK 配置 ---- - - - -## SDK 维护期说明 - -我们于 2018 年 9 月推出了新的 [Java Unified SDK](https://leancloudblog.com/java-unified-sdk-kai-fang-ce-shi-tong-zhi/),兼容纯 Java、云引擎和 Android 等多个平台,老的 Android SDK(版本号低于 `5.0.0`,`groupId` 为 `cn.leancloud.android` 的 libraries)已于 2019 年 9 月底停止维护。 - -Java Unified SDK 根据底层依赖的 JSON 解析库的不同,有两个不同分支: - -- 6.x 分支依赖 [fastjson](https://github.com/alibaba/fastjson) 来进行 JSON 解析; -- 7.0 以后版本使用 [Gson](https://github.com/google/gson) 来进行 JSON 解析(最新版本); - -两个版本的对外接口完全一致,但是考虑到平台兼容性和稳定性,我们推荐大家使用带 Gson 库的版本来进行开发。 - -从 2021 年 6 月开始,我们推出了 8.0 版本,与 7.x 版本相比,主要的变化是改变了公开类的前缀(`AV` -> `LC`),同时也删除了一些长期处于 `deprecated` 状态的接口,它将是我们今后会长期维护的版本。欢迎老 SDK 的用户尽快切换到新的 Java Unified SDK,具体迁移方法见 [Java SDK 的 README](https://github.com/leancloud/java-unified-sdk#migration-to-8x)。 - -请开发者注意,各个版本 SDK 的维护期如下: - -1. 6.0 以前版本已经停止维护。 -2. 原 7.x 版本从现在开始进入维护期(只改 bug,不增加新功能),并计划于 2022 年 6 月停止维护。 -3. 8.x 会是我们今后长期维护版本。 - -> 下面的流程都是基于 8.x 版本 Java Unified SDK 书写。 - -## 平台与 SDK 对应关系 -Java SDK 主要包含以下几个 library,其层次结构以及平台对应关系如下: - -### 基础包(可以在纯 Java 环境下调用) -- storage-core:包含所有数据存储的功能,如 - - 结构化数据(LCObject) - - 内建账户系统(LCUser) - - 查询(LCQuery) - - 文件存储(LCFile) - - 社交关系(LCFriendship,当前版本暂不提供) - - 朋友圈(LCStatus,当前版本暂不提供) - - 短信(LCSMS) - - 等等 -- realtime-core:部分依赖 storage-core library,实现了 LiveQuery 以及即时通讯功能,如: - - LiveQuery - - LCIMClient - - LCIMConversation 以及多种场景对话 - - LCIMMessage 以及多种子类化的多媒体消息 - - 等等 - -### Android 特有的包 -- storage-android:是 storage-core 在 Android 平台的定制化实现,接口与 storage-core 完全相同。 -- realtime-android:是 realtime-core 在 Android 平台的定制化实现。 -- leancloud-fcm:是 Firebase Cloud Messaging 的封装 library,供美国节点的 app 使用推送服务。 - - -### 模块依赖关系 -Java SDK 一共包含如下几个模块: - -目录 | 模块名 | 适用平台 | 依赖关系 ----|---|---|--- -./core | storage-core,存储核心 library | java | 无,它是 LeanCloud 最核心的 library -./realtime | realtime-core,LiveQuery 与实时通讯核心 library | java | storage-core -./android-sdk/storage-android | storage-android,Android 存储 library | Android | storage-core -./android-sdk/realtime-android | realtime-android,LiveQuery、即时通讯 library | Android | storage-android, realtime-core - -## 获取 SDK - -获取 SDK 有多种方式,较为推荐的方式是通过包依赖管理工具下载最新版本。 - -我们已经将所有的 library 发布到了 maven 中心仓库,开发者可以用以下任意包管理工具来安装 SDK。 - -#### 使用存储功能 - -Maven: - -```xml - - cn.leancloud - storage-core - 8.0.1 - -``` - -Ivy: - -```xml - -``` - -SBT: - -```scala -libraryDependencies += "cn.leancloud" %% "storage-core" % "8.0.1" -``` - -Gradle: - -```groovy -implementation 'cn.leancloud:storage-core:8.0.1' -``` - -如果是 Android 项目,则换成以下这些包: - -```groovy -implementation 'cn.leancloud:storage-android:8.0.1' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' -``` - -#### 使用即时通讯服务 - -Maven: - -```xml - - cn.leancloud - realtime-core - 8.0.1 - -``` - -Ivy: - -```xml - -``` - -SBT: - -```scala -libraryDependencies += "cn.leancloud" %% "realtime-core" % "8.0.1" -``` - -Gradle: - -```groovy -implementation 'cn.leancloud:realtime-android:8.0.1' -implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' -``` - -#### 对 maven 源的特别说明 - -我们发现有时候 Maven 源的 CDN 缓存同步策略出现问题,可能会导致我们某个版本或者该版本下某种格式的 library 文件无法下载,这时候你可以在配置文件中显式增加一个 Sonatype 的源,就可以解决找不到文件的问题。 - -Maven pom.xml 的修改示例如下: - -```xml - - - oss-sonatype - oss-sonatype - https://oss.sonatype.org/content/groups/public/ - - -``` - -Gradle build.gradle 的修改示例如下: - -```groovy -buildscript { - repositories { - google() - jcenter() - // 增加下面的配置 - maven { - url "https://oss.sonatype.org/content/groups/public/" - } - } -} - -allprojects { - repositories { - google() - jcenter() - // 增加下面的配置 - maven { - url "https://oss.sonatype.org/content/groups/public/" - } - } -} -``` - -### 手动安装 - -#### 从源码编译 - -可以执行以下命令获取 Java SDK 并安装: - -```sh -$ git clone https://github.com/leancloud/java-unified-sdk.git -$ cd java-unified-sdk/ -$ mvn clean install -``` - -获取和安装 Android SDK: - -```sh -$ cd java-unified-sdk/ -$ cd android-sdk/ -$ gradle clean assemble -``` - -## 快速开始 - -### 绑定域名 - -你需要绑定 API 自定义域名,以便和其他厂商的应用隔离入口,避免其他应用受到 DDoS 攻击时相互牵连。 -如果使用了文件服务,也需要绑定文件自定义域名。 - -进入 **开发者中心 > 你的游戏 > 游戏服务 > 技术服务 > 数据存储 > 服务设置 > 自定义域名** 点击「绑定新域名」按钮,根据控制台提示完成绑定步骤。 -注意,DNS 解析记录和证书申请(如果选择了自动管理 SSL 证书)都需要一定时间,请耐心等待。 - -绑定成功后,初始化 SDK 时,请传入绑定的自定义域名(`https://please-replace-with-your-customized.domain.com`)。 - -如果你使用了文件服务(包括即时通讯的多媒体消息(图像、音频、视频等)),同样需要前往 **开发者中心 > 你的游戏 > 游戏服务 > 技术服务 > 数据存储 > 文件 > 设置 > 文件访问域名** 绑定域名,步骤和 API 自定义域名基本相同,但有两点不一样: - -1. API 域名解析使用 A 记录,文件域名解析使用 CNAME 记录,也因此文件域名不支持绑定裸域名(例如 `example.com`),需要绑定子域名(例如 `files.example.com`)。 -2. 绑定成功后,还需在 **文件 > 设置 > 文件访问地址** 点击「修改」按钮进行切换。 - -### 应用凭证 - -在 **开发者中心 > 你的游戏 > 游戏服务 > 基本信息** 可以查看应用凭证: - -- **Client ID**,又称 `App ID`,在 SDK 初始化时用到。联系技术支持时,提供 Client ID 可以方便我们更快定位到你的应用。 -- **Client Token**,又称 `App Key`,在 SDK 初始化时用到。 -- **Server Secret**,又称 `Master Key`,用于在自有服务器、云引擎等**受信任环境**调用管理接口 ,具备跳过一切权限验证的超级权限。所以**一定注意保密,千万不要在客户端代码中使用该凭证**。 - -### Android 平台初始化 - -如果是一个 Android 项目,则向 `Application` 类的 `onCreate` 方法添加: - -```java -import cn.leancloud.LeanCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // 提供 this、App ID、App Key、Server Host 作为参数 - // 注意这里千万不要调用 cn.leancloud.core.LeanCloud 的 initialize 方法,否则会出现 NetworkOnMainThread 等错误。 - LeanCloud.initialize(this, "your-client-id", "your-client-token", "https://please-replace-with-your-customized.domain.com"); - } -} -``` - - -然后指定 SDK 需要的权限并在 `AndroidManifest.xml` 里面声明 `MyLeanCloudApp` 类: - -```xml - - - - -``` - -#### 更安全的客户端初始化方法 - -对 Android 开发者来说,从 6.1.0 版本开始,除了支持通过 appId + appKey 完成初始化,我们还提供一种更加安全的使用方式,支持仅仅通过 appId 来初始化应用,例如: - -```java -import cn.leancloud.LeanCloud; - -public class MyLeanCloudApp extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // 提供 this、App ID、绑定的自定义 API 域名作为参数 - LeanCloud.initializeSecurely(this, "{{appid}}", "https://please-replace-with-your-customized.domain.com"); - } -} -``` - -这时候程序初始化不再需要 appKey,避免了核心配置信息在客户端泄漏可能带来的潜在风险。具体的集成方法可参看文档[《Android SDK 更安全的接入和初始化方式》](https://leancloud.cn/docs/sdk_setup_android_securely.html)。 - -### Java 平台初始化代码 - -如果是一个普通 Java 项目,则在代码开头添加: - -```java -import cn.leancloud.core.LeanCloud; - -LeanCloud.initialize("your-client-id", "your-client-token", "https://please-replace-with-your-customized.domain.com"); -``` - -注意,云引擎内部访问 API 是通过内网,所以不需要也不应该配置 API 自定义域名(`serverUrl`)。 -模板项目和[云引擎网站托管指南](/v2/sdk/engine/guide/webhosting)中的示例代码均未配置 API 自定义域名, -请勿调用 `setServer`,否则会变成公网访问,影响性能。 - -realtime-core library 也支持在纯 Java Application 中使用,但是与 Android 的调用方式有细微差异,Java Application 中需要开发者显式建立与云端的长链接(Android 平台是通过 PushService 自动建立的)。建立长链接的方法如下: - -```java -LCConnectionManager.getInstance().startConnection(new LCCallback() { - @Override - protected void internalDone0(Object o, LCException e) { - if (e == null) { - System.out.println("成功建立 WebSocket 链接"); - } else { - System.out.println("建立 WebSocket 链接失败:" + e.getMessage()); - } - } -}); -``` - -只有长链接成功建立之后,后续的聊天请求才能开始。 - -## 开启调试日志 - -在应用开发阶段,你可以选择开启 SDK 的调试日志(debug log)来方便追踪问题。调试日志开启后,SDK 会把网络请求、错误消息等信息输出到 IDE 的日志窗口,或是浏览器 Console 或是云引擎日志(如果在云引擎下运行 SDK)。 - -```java -// 在初始化之前调用 -LeanCloud.setLogLevel(LCLogger.Level.DEBUG); -``` - -详细调试流程可以参考[Android SDK 调试指南][android-debug-guide]。 - -[android-debug-guide]: https://forum.leancloud.cn/t/leancloud-sdk-android-sdk/21829 - -注意,在应用发布之前,请关闭调试日志,以免暴露敏感数据。 - -## 验证 - -首先,确认本地网络环境是可以访问云端服务器的,可以执行以下命令: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` 为绑定的 API 自定义域名。 - -如果当前网路正常会返回当前时间: - -```json -{"__type":"Date","iso":"2020-10-12T06:46:56.000Z"} -``` - -然后在项目中编写如下测试代码: - -```java -LCObject testObject = new LCObject("TestObject"); -testObject.put("words", "Hello world!"); -testObject.saveInBackground().blockingSubscribe(); -``` - -保存后运行程序。 - -然后打开 **云服务控制台 > 数据存储 > 结构化数据 > `TestObject`**,如果看到数据表中出现一行「words」列的值为「Hello world!」的数据,说明 SDK 已经正确地执行了上述代码,配置完毕。 - -如果控制台没有发现对应的数据,请参考 [问题排查](#问题排查)。 - -## 问题排查 - -SDK 安装指南基于当前最新版本的 SDK 编写,所以排查问题前,请先检查下安装的 SDK 是不是最新版本。 - -### `401 Unauthorized` - -如果 SDK 抛出 `401` 异常或者查看本地网络访问日志存在: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -则可认定为 `App ID` 或者 `App Key` 输入有误,或者是不匹配,很多开发者同时注册了多个应用,导致拷贝粘贴的时候,用 A 应用的 `App ID` 匹配 B 应用的 `App Key`,这样就会出现服务端鉴权失败的错误。 - -### 客户端无法访问网络 - -客户端尤其是手机端,应用在访问网络的时候需要申请一定的权限。 - -## Android 代码混淆 - -为了保证 SDK 在代码混淆后能正常运作,需要保证部分类和第三方库不被混淆,参考下列配置: - -``` -# proguard.cfg - --keepattributes Signature --dontwarn com.jcraft.jzlib.** --keep class com.jcraft.jzlib.** { *;} - --dontwarn sun.misc.** --keep class sun.misc.** { *;} - --dontwarn com.alibaba.fastjson.** --keep class com.alibaba.fastjson.** { *;} - --dontwarn org.ligboy.retrofit2.** --keep class org.ligboy.retrofit2.** { *;} - --dontwarn io.reactivex.rxjava2.** --keep class io.reactivex.rxjava2.** { *;} - --dontwarn sun.security.** --keep class sun.security.** { *; } - --dontwarn com.google.** --keep class com.google.** { *;} - --dontwarn com.avos.** --keep class com.avos.** { *;} - --dontwarn cn.leancloud.** --keep class cn.leancloud.** { *;} - --keep public class android.net.http.SslError --keep public class android.webkit.WebViewClient - --dontwarn android.webkit.WebView --dontwarn android.net.http.SslError --dontwarn android.webkit.WebViewClient - --dontwarn android.support.** - --dontwarn org.apache.** --keep class org.apache.** { *;} - --dontwarn org.jivesoftware.smack.** --keep class org.jivesoftware.smack.** { *;} - --dontwarn com.loopj.** --keep class com.loopj.** { *;} - --dontwarn com.squareup.okhttp.** --keep class com.squareup.okhttp.** { *;} --keep interface com.squareup.okhttp.** { *; } - --dontwarn okio.** - --dontwarn org.xbill.** --keep class org.xbill.** { *;} - --keepattributes *Annotation* - -``` diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/04-java.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/04-java.mdx deleted file mode 100644 index 898238dfa..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/04-java.mdx +++ /dev/null @@ -1,2321 +0,0 @@ ---- -id: java -title: 数据存储开发指南 · Java -sidebar_label: Java 指南 ---- - - - -数据存储是云服务提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端: - -```java -// 构建对象 -LCObject todo = new LCObject("Todo"); - -// 为属性赋值 -todo.put("title", "工程师周会"); -todo.put("content", "周二两点,全体成员"); - -// 将对象保存到云端 -todo.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // 成功保存之后,执行其他逻辑 - System.out.println("保存成功。objectId:" + todo.getObjectId()); - } - public void onError(Throwable throwable) { - // 异常处理 - } - public void onComplete() {} -}); -``` - -我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。 - -## SDK 安装与初始化 - -请阅读[数据存储、即时通讯 Java SDK 配置指南](/v2/sdk/storage/guide/setup-java)。 - -> Android SDK 老版本迁移 -> -> 如果你还在在使用我们老版本 Android SDK(所有版本号低于 `5.0.0`,`groupId` 为 `cn.leancloud.android` 的 libraries),要迁移到新版的 Unified SDK,具体迁移方法见 [Java SDK 的 README](https://github.com/leancloud/java-unified-sdk#migration-to-8x)。 - -## 对象 - -### `LCObject` - -`LCObject` 是云服务对复杂对象的封装,每个 `LCObject` 包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个 `LCObject` 上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。 - -比如说,一个保存着单个 Todo 的 `LCObject` 可能包含如下数据: - -```json -title: "给小林发邮件确认会议时间", -isComplete: false, -priority: 2, -tags: ["工作", "销售"] -``` - - -### 数据类型 - -`LCObject` 支持的数据类型包括 `String`、`Number`、`Boolean`、`Object`、`Array`、`Date` 等等。你可以通过嵌套的方式在 `Object` 或 `Array` 里面存储更加结构化的数据。 - -`LCObject` 还支持两种特殊的数据类型 `Pointer` 和 `File`,可以分别用来存储指向其他 `LCObject` 的指针以及二进制数据。 - -`LCObject` 同时支持 `GeoPoint`,可以用来存储地理位置信息。参见 [GeoPoint](#geopoint)。 - -以下是一些示例: - - -```java -// 基本类型 -boolean bool = true; -int number = 2018; -String string = number + " 流行音乐榜单"; -Date date = new Date(); -byte[] data = "Hello world!".getBytes(); -ArrayList arrayList = new ArrayList<>(); -arrayList.add(number); -arrayList.add(string); -HashMap hashMap = new HashMap<>(); -hashMap.put("number", number); -hashMap.put("string", string); - -// 构建对象 -LCObject testObject = new LCObject("TestObject"); -testObject.put("testBoolean", bool); -testObject.put("testInteger", number); -testObject.put("testDate", date); -testObject.put("testData", data); -testObject.put("testArrayList", arrayList); -testObject.put("testHashMap", hashMap); -testObject.save(); -``` - - -我们不推荐通过 `byte[]` 在 `LCObject` 里面存储图片、文档等大型二进制数据。每个 `LCObject` 的大小不应超过 **128 KB**。如需存储大型文件,可创建 `LCFile` 实例并将其关联到 `LCObject` 的某个属性上。参见 [文件](#文件)。 - -注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。 - -**云服务控制台 > 数据存储 > 结构化数据** 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。 - -若想了解云服务是如何保护应用数据的,请阅读[数据和安全](/v2/sdk/storage/guide/security)。 - -### 构建对象 - -下面的代码构建了一个 class 为 `Todo` 的 `LCObject`: - -```java -LCObject todo = new LCObject("Todo"); -``` - - -在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。 - -### 保存对象 - -下面的代码将一个 class 为 `Todo` 的对象存入云端: - - -```java -// 构建对象 -LCObject todo = new LCObject("Todo"); - -// 为属性赋值 -todo.put("title", "马拉松报名"); -todo.put("priority", 2); - -// 将对象保存到云端 -todo.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // 成功保存之后,执行其他逻辑 - System.out.println("保存成功。objectId:" + todo.getObjectId()); - } - public void onError(Throwable throwable) { - // 异常处理 - } - public void onComplete() {} -}); -``` - - -为了确认对象已经保存成功,我们可以到 **云服务控制台 > 数据存储 > 结构化数据 > `Todo`** 里面看一下,应该会有一行新的数据产生。点一下这个数据的 `objectId`,应该能看到类似这样的内容: - -```json -{ - "title": "马拉松报名", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -注意,无需在 **云服务控制台 > 数据存储 > 结构化数据** 里面创建新的 `Todo` class 即可运行前面的代码。如果 class 不存在,它将自动创建。 - -以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定: - -内置属性 | 类型 | 描述 ---- | --- | --- -`objectId` | `String` | 该对象唯一的 ID 标识。 -`ACL` | `LCACL` | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 -`createdAt` | `Date` | 该对象被创建的时间。 -`updatedAt` | `Date` | 该对象最后一次被修改的时间。 - - -这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的 `LCObject` 不存在这些属性。 - -属性名(**keys**)只能包含字母、数字和下划线。自定义属性不得以双下划线(`__`)开头或与任何系统保留字段和内置属性(`ACL`、`className`、`createdAt`、`objectId` 和 `updatedAt`)重名,无论大小写。 - -属性值(**values**)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 [数据类型](#数据类型)。 - -我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 `CustomData`。属性,采用小驼峰法,如 `imageUrl`。 - -### 获取对象 - -对于已经保存到云端的 `LCObject`,可以通过它的 `objectId` 将其取回: - - -```java -LCQuery query = new LCQuery<>("Todo"); -query.getInBackground("582570f38ac247004f39c24b").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // todo 就是 objectId 为 582570f38ac247004f39c24b 的 Todo 实例 - String title = todo.getString("title"); - int priority = todo.getInt("priority"); - - // 获取内置属性 - String objectId = todo.getObjectId(); - Date updatedAt = todo.getUpdatedAt(); - Date createdAt = todo.getCreatedAt(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -对象拿到之后,可以通过 `get` 方法来获取各个属性的值。注意 `objectId`、`updatedAt` 和 `createdAt` 这三个内置属性不能通过 `get` 获取或通过 `set` 修改,只能由云端自动进行填充。尚未保存的 `LCObject` 不存在这些属性。 - -如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 `null`。 - -#### 同步对象 - -当云端数据发生更改时,你可以调用 `fetchInBackground` 方法来刷新对象,使之与云端数据同步: - - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.fetchInBackground().subscribe(new Observable() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // todo 已刷新 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,**`fetchInBackground` 操作会丢弃这些修改**。为避免这种情况,你可以在刷新时指定 **需要刷新的属性**,这样只有指定的属性会被刷新(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`),其他属性不受影响。 - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -String keys = "priority, location"; -todo.fetchInBackground(keys).subscribe(new Observable() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // 只有 priority 和 location 会被获取和刷新 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -如果想要确保只刷新已经保存到云端的对象,那么可以用 `fetchIfNeededInBackground` 替换 `fetchInBackground`。 -调用 `fetchIfNeededInBackground` 方法时,如果是本地构造尚未保存的对象,那么不会访问云端,onNext 方法中传入的是本地对象。 - -### 更新对象 - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `saveInBackground` 方法。例如: - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `save` 方法。例如: - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.put("content", "这周周会改到周三下午三点。"); -todo.save(); -``` - -云服务会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。 - -#### 有条件更新对象 - -通过传入 `query` 选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回 `305` 错误。 - -例如,用户的账户表 `Account` 有一个余额字段 `balance`,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求: - -```java -LCObject account = LCObject.createWithoutData("Account", "5745557f71cfe40068c6abe0"); -// 对 balance 原子减少 100 -final int amount = -100; -account.increment("balance", amount); -// 设置条件 -LCSaveOption option = new LCSaveOption(); -option.query(new LCQuery<>("Account").whereGreaterThanOrEqualTo("balance", -amount)); -// 操作结束后,返回最新数据。 -// 如果是新对象,则所有属性都会被返回, -// 否则只有更新的属性会被返回。 -option.setFetchWhenSave(true); -account.saveInBackground(option).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject account) { - System.out.println("当前余额为:" + account.get("balance")); - } - public void onError(Throwable throwable) { - System.out.println("余额不足,操作失败!"); - } - public void onComplete() {} -}); -``` - - -**`query` 选项只对已存在的对象有效**,不适用于尚未存入云端的对象。 - -`query` 选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过 `LCQuery` 查询 `LCObject` 再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。 - -#### 更新计数器 - -设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 **原子操作** 来增加或减少一个属性内保存的数字: - - -```java -post.increment("likes", 1); -``` - -可以指定需要增加或减少的值。若未指定,则默认使用 `1`。 - -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,需要原子增减的字段建议使用整数以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 - -#### 更新数组 - -更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据: - - -- `add()` 将指定对象附加到数组末尾。 -- `addUnique()` 如果数组中不包含指定对象,则将该对象加入数组。对象的插入位置是随机的。 -- `removeAll()` 从数组字段中删除指定对象的所有实例。 - -例如,`Todo` 用一个 `alarms` 属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性: - - -```java -Date getDateWithDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date date = dateFormat.parse(dateString); - return date; -} - -Date alarm1 = getDateWithDateString("2018-04-30 07:10:00"); -Date alarm2 = getDateWithDateString("2018-04-30 07:20:00"); -Date alarm3 = getDateWithDateString("2018-04-30 07:30:00"); - -LCObject todo = new LCObject("Todo"); -todo.addAllUnique("alarms", Arrays.asList(alarm1, alarm2, alarm3)); -todo.save(); -``` - -### 删除对象 - -下面的代码从云端删除一个 `Todo` 对象: - -```java -LCObject todo = LCObject.createWithoutData("Todo", "582570f38ac247004f39c24b"); -todo.delete(); -``` - -注意,删除对象是一个较为敏感的操作,我们建议你阅读[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html)来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。 - -### 批量操作 - - -```java -// 批量构建和更新 -saveAll() -saveAllInBackground() - -// 批量删除 -deleteAll() -deleteAllInBackground() - -// 批量同步 -fetchAll() -fetchAllInBackground() -``` - - -下面的代码将所有 `Todo` 的 `isComplete` 设为 `true`: - - -```java -LCQuery query = new LCQuery<>("Todo"); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List todos) { - // 获取需要更新的 todo - for (LCObject todo : todos) { - // 更新属性值 - todo.put("isComplete", true); - } - // 批量更新 - LCObject.saveAll(todos); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。 -### 后台运行 - -细心的开发者已经发现,在所有的示例代码中几乎都是用了异步来访问云端,形如 `xxxxInBackground` 的用法都是提供给开发者在主线程调用用以实现后台运行的方法,因此开发者在主线程可以放心地调用这种命名方式的函数。 -### 离线存储对象 - -大多数保存功能可以立刻执行,并通知应用「保存完毕」。不过若不需要知道保存完成的时间,则可使用 `saveEventually` 来代替。 - -它的优点在于:如果用户目前尚未接入网络,`saveEventually` 会缓存设备中的数据,并在网络连接恢复后上传。如果应用在网络恢复之前就被关闭了,那么当它下一次打开时,SDK 会再次尝试保存操作。 - -所有 `saveEventually`(或 `deleteEventually`)的相关调用,将按照调用的顺序依次执行。因此,多次对某一对象使用 `saveEventually` 是安全的。 - -### 数据模型 - -对象之间可以产生关联。拿一个博客应用来说,一个 `Post` 对象可以与许多个 `Comment` 对象产生关联。云服务支持三种关系:一对一、一对多、多对多。 - -#### 一对一、一对多关系 - -一对一、一对多关系可以通过将 `LCObject` 保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个 `Comment` 指向一个 `Post`。 - -下面的代码会创建一个含有单个 `Comment` 的 `Post`: - - -```java -// 创建 post -LCObject post = new LCObject("Post"); -post.put("title", "饿了……"); -post.put("content", "中午去哪吃呢?"); - -// 创建 comment -LCObject comment = new LCObject("Comment"); -comment.put("content", "当然是肯德基啦!"); - -// 将 post 设为 comment 的一个属性值 -comment.put("parent", post); - -// 保存 comment 会同时保存 post -comment.save(); -``` - - -云端存储时,会将被指向的对象用 `Pointer` 的形式存起来。你也可以用 `objectId` 来指向一个对象: - - -```java -LCObject post = LCObject.createWithoutData("Post", "57328ca079bc44005c2472d0"); -comment.put("post", post); -``` - -请参阅 [关系查询](#关系查询) 来了解如何获取关联的对象。 - -#### 多对多关系 - -想要建立多对多关系,最简单的办法就是使用 **数组**。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 **中间表** 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。 - -我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。 - -### 序列化和反序列化 - -在实际的开发中,把 `LCObject` 当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此 `LCObject` 也提供了序列化和反序列化的方法。 - -序列化: - - -```java -LCObject todo = new LCObject("Todo"); // 构建对象 -todo.put("title", "马拉松报名"); // 设置名称 -todo.put("priority", 2); // 设置优先级 -todo.put("owner", LCUser.getCurrentUser()); // 这里就是一个 Pointer 类型,指向当前登录的用户 -String serializedString = todo.toString(); -``` - -反序列化: - - -```java -LCObject deserializedObject = LCObject.parseLCObject(serializedString); -deserializedObject.save(); // 保存到服务端 -``` - -## 查询 - -我们已经了解到如何从云端获取单个 `LCObject`,但你可能还会有一次性获取多个符合特定条件的 `LCObject` 的需求,这时候就需要用到 `LCQuery` 了。 - -### 基础查询 - -执行一次基础查询通常包括这些步骤: - -1. 构建 `LCQuery`; -2. 向其添加查询条件; -3. 执行查询并获取包含满足条件的对象的数组。 - -下面的代码获取所有 `lastName` 为 `Smith` 的 `Student`: - -```java -LCQuery query = new LCQuery<>("Student"); -query.whereEqualTo("lastName", "Smith"); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List students) { - // students 是包含满足条件的 Student 对象的数组 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### 查询条件 - -可以给 `LCObject` 添加不同的条件来改变获取到的结果。 - -下面的代码查询所有 `firstName` 不为 `Jack` 的对象: - -```java -query.whereNotEqualTo("firstName", "Jack"); -``` - -对于能够排序的属性(比如数字、字符串),可以进行比较查询: - -```java -// 限制 age < 18 -query.whereLessThan("age", 18); - -// 限制 age <= 18 -query.whereLessThanOrEqualTo("age", 18); - -// 限制 age > 18 -query.whereGreaterThan("age", 18); - -// 限制 age >= 18 -query.whereGreaterThanOrEqualTo("age", 18); -``` - -可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是 `AND` 的关系: - -```java -query.whereEqualTo("firstName", "Jack"); -query.whereGreaterThan("age", 18); -``` - -可以通过指定 `limit` 限制返回结果的数量(默认为 `100`): - -```java -// 最多获取 10 条结果 -query.limit(10); -``` - -由于性能原因,`limit` 最大只能设为 `1000`。即使将其设为大于 `1000` 的数,云端也只会返回 1,000 条结果。 - -如果只需要一条结果,可以直接用 ``: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("priority", 2); -query.getFirstInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - // todo 是第一个满足条件的 Todo 对象 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -可以通过设置 `skip` 来跳过一定数量的结果: - -```java -// 跳过前 20 条结果 -query.skip(20); -``` - -把 `skip` 和 `limit` 结合起来,就能实现翻页功能: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("priority", 2); -query.limit(10); -query.skip(20); -``` - -需要注意的是,`skip` 的值越高,查询所需的时间就越长。作为替代方案,可以通过设置 `createdAt` 或 `updatedAt` 的范围来实现更高效的翻页,因为它们都自带索引。 -同理,也可以通过设置自增字段的范围来实现翻页。 - -对于能够排序的属性,可以指定结果的排序规则: - -```java -// 按 createdAt 升序排列 -query.orderByAscending("createdAt"); - -// 按 createdAt 降序排列 -query.orderByDescending("createdAt"); -``` - -还可以为同一个查询添加多个排序规则: - -```java -query.addAscendingOrder("priority"); -query.addDescendingOrder("createdAt"); -``` - -下面的代码可用于查找包含或不包含某一属性的对象: - -```java -// 查找包含 "images" 的对象 -query.whereExists("images"); - -// 查找不包含 "images" 的对象 -query.whereDoesNotExist("images"); -``` - -可以通过 `selectKeys` 指定需要返回的属性。下面的代码只获取每个对象的 `title` 和 `content`(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`): - -```java -LCQuery query = new LCQuery<>("Todo"); -query.selectKeys(Arrays.asList("title", "content")); -query.getFirstInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject todo) { - String title = todo.getString("title"); // √ - String content = todo.getString("content"); // √ - String notes = todo.getString("notes"); // 会报错 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -`selectKeys` -支持点号(`author.firstName`),详见[《点号使用指南》](https://leancloud.cn/docs/dot-notation.html)。 -另外,字段名前添加减号前缀表示反向选择,例如 `-author` 表示不返回 `author` 字段。 -反向选择同样适用于内置字段,比如 `-objectId`,也可以和点号组合使用,比如 `-pubUser.createdAt`。 - -对于未获取的属性,可以通过对结果中的对象进行 `fetchInBackground` 操作来获取。参见 [同步对象](#同步对象)。 -### 字符串查询 - -可以用 `whereStartsWith` 来查找某一属性值以特定字符串开头的对象。和 SQL 中的 `LIKE` 一样,你可以利用索引带来的优势: - -```java -LCQuery query = new LCQuery<>("Todo"); -// 相当于 SQL 中的 title LIKE 'lunch%' -query.whereStartsWith("title", "lunch"); -``` - - -可以用 `whereContains` 来查找某一属性值包含特定字符串的对象: - -```java -LCQuery query = new LCQuery<>("Todo"); -// 相当于 SQL 中的 title LIKE '%lunch%' -query.whereContains("title", "lunch"); -``` - - -和 `whereStartsWith` 不同,`whereContains` 无法利用索引,因此不建议用于大型数据集。 - -注意 `whereStartsWith` 和 `whereContains` 都是 **区分大小写** 的,所以上述查询会忽略 `Lunch`、`LUNCH` 等字符串。 - -如果想查找某一属性值不包含特定字符串的对象,可以使用 `whereMatches` 进行基于正则表达式的查询: - -```java -LCQuery query = new LCQuery<>("Todo"); -// "title" 不包含 "ticket"(不区分大小写) -query.whereMatches("title", "^((?!ticket).)*$", "i"); -``` - -不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class, -因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以使用全文搜索服务。 - -使用查询时如果遇到性能问题,可参阅 [查询性能优化](#查询性能优化)。 - -### 数组查询 - -下面的代码查找所有数组属性 `tags` 包含 `工作` 的对象: - -```java -query.whereEqualTo("tags", "工作"); -``` - -下面的代码查询数组属性长度为 3 (正好包含 3 个标签)的对象: - -```java -query.whereSizeEqual("tags", 3); -``` - -下面的代码查找所有数组属性 `tags` **同时包含** `工作`、`销售` 和 `会议` 的对象: - -```java -query.whereContainsAll("tags", Arrays.asList("工作", "销售", "会议")); -``` - -如需获取某一属性值包含一列值中任意一个值的对象,可以直接用 `whereContainedIn` 而无需执行多次查询。下面的代码构建的查询会查找所有 `priority` 为 `1` **或** `2` 的 todo 对象: - -```java -// 单个查询 -LCQuery priorityOneOrTwo = new LCQuery<>("Todo"); -priorityOneOrTwo.whereContainedIn("priority", Arrays.asList(1, 2)); -// 这样就可以了 :) - -// --------------- -// vs. -// --------------- - -// 多个查询 -final LCQuery priorityOne = new LCQuery<>("Todo"); -priorityOne.whereEqualTo("priority", 1); - -final LCQuery priorityTwo = new LCQuery<>("Todo"); -priorityTwo.whereEqualTo("priority", 2); - -LCQuery priorityOneOrTwo = LCQuery.or(Arrays.asList(priorityOne, priorityTwo)); -// 好像有些繁琐 :( -``` - -反过来,还可以用 `whereNotContainedIn` 来获取某一属性值不包含一列值中任何一个的对象。 - -### 关系查询 - -查询关联数据有很多种方式,常见的一种是查询某一属性值为特定 `LCObject` 的对象,这时可以像其他查询一样直接用 `whereEqualTo`。比如说,如果每一条博客评论 `Comment` 都有一个 `post` 属性用来存放原文 `Post`,则可以用下面的方法获取所有与某一 `Post` 相关联的评论: - -```java -LCObject post = LCObject.createWithoutData("Post", "57328ca079bc44005c2472d0"); -LCQuery query = new LCQuery<>("Comment"); -query.whereEqualTo("post", post); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List comments) { - // comments 包含与 post 相关联的评论 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -如需获取某一属性值为另一查询结果中任一 `LCObject` 的对象,可以用 `whereMatchesQuery`。下面的代码构建的查询可以找到所有包含图片的博客文章的评论: - -```java -LCQuery innerQuery = new LCQuery<>("Post"); -innerQuery.whereExists("image"); - -LCQuery query = new LCQuery<>("Comment"); -query.whereMatchesQuery("post", innerQuery); -``` - -如需获取某一属性值不是另一查询结果中任一 `LCObject` 的对象,则使用 `whereDoesNotMatchQuery`。 - -有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用 `include`。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章: - -```java -LCQuery query = new LCQuery<>("Comment"); - -// 获取最新发布的 -query.orderByDescending("createdAt"); - -// 只获取 10 条 -query.limit(10); - -// 同时包含博客文章 -query.include("post"); - -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List comments) { - // comments 包含最新发布的 10 条评论,包含各自对应的博客文章 - for (LCObject comment : comments) { - // 该操作无需网络连接 - LCObject post = comment.getLCObject("post"); - } - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -可以用 dot 符号(`.`)来获取多级关系,例如 `post.author`,详见[《点号使用指南》](https://leancloud.cn/docs/dot-notation.html)的《在查询对象时使用点号》一节。 - -可以在同一查询上应用多次 `include` 以包含多个属性。通过这种方法获取到的对象同样接受 `getFirst` 等 `LCQuery` 辅助方法。 - -通过 `include` 进行多级查询的方式不适用于数组属性内部的 `LCObject`,只能包含到数组本身。 - -#### 关系查询的注意事项 - -云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌/子查询(和普通查询一样,`limit` 默认为 `100`,最大 `1000`),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出 `limit`,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有 `limit` 数量以内的结果会被填入主查询。 - -我们建议采用以下方案进行改进: - -- 确保子查询的结果在 100 条以下,如果在 100 至 1,000 条之间的话请将子查询的 `limit` 设为 `1000`。 -- 将需要查询的字段冗余到主查询所在的表上。 -- 进行多次查询,每次在子查询上设置不同的 `skip` 值来遍历所有记录(注意 `skip` 的值较大时可能会引发性能问题,因此不是很推荐)。 - -### 统计总数量 - -如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用 `count` 来代替 `findInBackground`。比如说,查询有多少个已完成的 todo: - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("isComplete", true); -query.countInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(Integer count) { - System.out.println(count + " 个 todo 已完成。"); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### 组合查询 - -组合查询就是把诸多查询条件用一定逻辑合并到一起(`OR` 或 `AND`)再交给云端去查询。 - -组合查询不支持在子查询中包含 `GeoPoint` 或其他非过滤性的限制(例如 `near`、`withinGeoBox`、`limit`、`skip`、`ascending`、`descending`、`include`)。 - -#### OR 查询 - -OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于 `3` 或者已经完成了的 todo: - -```java -final LCQuery priorityQuery = new LCQuery<>("Todo"); -priorityQuery.whereGreaterThanOrEqualTo("priority", 3); - -final LCQuery isCompleteQuery = new LCQuery<>("Todo"); -isCompleteQuery.whereEqualTo("isComplete", true); - -LCQuery query = LCQuery.or(Arrays.asList(priorityQuery, isCompleteQuery)); -``` - -使用 OR 查询时,子查询中不能包含 `GeoPoint` 相关的查询。 - -#### AND 查询 - -使用 AND 查询的效果等同于往 `LCQuery` 添加多个条件。下面的代码构建的查询会查找创建时间在 `2016-11-13` 和 `2016-12-02` 之间的 todo: - -```java -Date getDateWithDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - Date date = dateFormat.parse(dateString); - return date; -} - -final LCQuery startDateQuery = new LCQuery<>("Todo"); -startDateQuery.whereGreaterThanOrEqualTo("createdAt", getDateWithDateString("2016-11-13")); - -final LCQuery endDateQuery = new LCQuery<>("Todo"); -endDateQuery.whereLessThan("createdAt", getDateWithDateString("2016-12-03")); - -LCQuery query = LCQuery.and(Arrays.asList(startDateQuery, endDateQuery)); -``` - - -单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询: - - -```java -Date getDateWithDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - Date date = dateFormat.parse(dateString); - return date; -} - -final LCQuery createdAtQuery = new LCQuery<>("Todo"); -createdAtQuery.whereGreaterThanOrEqualTo("createdAt", getDateWithDateString("2018-04-30")); -createdAtQuery.whereLessThan("createdAt", getDateWithDateString("2018-05-01")); - -final LCQuery locationQuery = new LCQuery<>("Todo"); -locationQuery.whereDoesNotExist("location"); - -final LCQuery priority2Query = new LCQuery<>("Todo"); -priority2Query.whereEqualTo("priority", 2); - -final LCQuery priority3Query = new LCQuery<>("Todo"); -priority3Query.whereEqualTo("priority", 3); - -LCQuery priorityQuery = LCQuery.or(Arrays.asList(priority2Query, priority3Query)); -LCQuery timeLocationQuery = LCQuery.or(Arrays.asList(locationQuery, createdAtQuery)); -LCQuery query = LCQuery.and(Arrays.asList(priorityQuery, timeLocationQuery)); -``` - - -### 查询性能优化 - -影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。 - -- 不等于和不包含查询(无法使用索引) -- 通配符在前面的字符串查询(无法使用索引) -- 有条件的 `count`(需要扫描所有数据) -- `skip` 跳过较多的行数(相当于需要先查出被跳过的那些行) -- 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引) -- 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据) - -## LiveQuery - -LiveQuery 衍生于 [`LCQuery`](#查询),并为其带来了更强大的功能。它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。 - -设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用 `LCQuery` 并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。 - -想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的 `LCQuery`。订阅成功后,一旦有符合 `LCQuery` 的 `LCObject` 发生变化,云端就会主动、实时地将信息通知到客户端。 - -LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。 - -### 启用 LiveQuery - -进入 **控制台 > 存储 > 设置**,在 **其他** 里面勾选 **启用 LiveQuery**即可。确保即时通信模块已被添加到 `AndroidManifest.xml`: - - -```xml - - - - - - - - -``` - -可以在 [SDK 安装与初始化](#SDK-安装与初始化) 中找到完整设置方法。 - -### Demo - -下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果: - - - -使用我们的「LeanTodo」微信小程序和网页应用,可以实际体验以上视频所演示的效果,步骤如下: - -1. 微信扫码,添加小程序「LeanTodo」; - - ![LeanTodo mini program](/img/leantodo-weapp-qr.jpg) - -2. 进入小程序,点击首页左下角 **设置** > **账户设置**,输入便于记忆的用户名和密码; - -3. 使用浏览器访问 ,输入刚刚在小程序中更新好的账户信息,点击 **Login**; - -4. 随意添加更改数据,查看两端的同步状态。 - -注意按以上顺序操作。在网页应用中使用 **Signup** 注册的账户无法与小程序创建的账户相关联,所以如果颠倒以上操作顺序,则无法观测到数据同步效果。 - -[LiveQuery 公开课](http://www.bilibili.com/video/av11291992/) 涵盖了许多开发者关心的问题和解答。 - -### 构建订阅 - -首先创建一个普通的 `LCQuery` 对象,添加查询条件(如有),然后进行订阅操作: - - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("isComplete", true); -LCLiveQuery liveQuery = LCLiveQuery.initWithQuery(query); -liveQuery.subscribeInBackground(new LCLiveQuerySubscribeCallback() { - @Override - public void done(LCException e) { - if (e == null) { - // 订阅成功 - } - } -}); -``` - -LiveQuery 不支持内嵌查询,也不支持返回指定属性。 - -订阅成功后,就可以接收到和 `LCObject` 相关的更新了。假如在另一个客户端上创建了一个 `Todo` 对象,对象的 `title` 设为 `更新作品集`,那么下面的代码可以获取到这个新的 `Todo`: - - -```java -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("isComplete", true); -LCLiveQuery liveQuery = LCLiveQuery.initWithQuery(query); -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectCreated(LCObject newTodo) { - System.out.println(newTodo.getString("title")); // 更新作品集 - } -}); -liveQuery.subscribeInBackground(new LCLiveQuerySubscribeCallback() { - @Override - public void done(LCException e) { - if (e == null) { - // 订阅成功 - } - } -}); -``` - -此时如果有人把 `Todo` 的 `content` 改为 `把我最近画的插画放上去`,那么下面的代码可以获取到本次更新: - - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectUpdated(LCObject updatedTodo, List updatedKeys) { - System.out.println(updatedTodo.getString("content")); // 把我最近画的插画放上去 - } -}); -``` - -### 事件处理 - -订阅成功后,可以选择监听如下几种数据变化: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` 事件 - -当有新的满足 `LCQuery` 查询条件的 `LCObject` 被创建时,`create` 事件会被触发。下面的 `object` 就是新建的 `LCObject`: - - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectCreated(LCObject object) { - System.out.println("对象被创建。"); - } -}); -``` - -#### `update` 事件 - -当有满足 `LCQuery` 查询条件的 `LCObject` 被更新时,`update` 事件会被触发。下面的 `object` 就是有更新的 `LCObject`: - - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectUpdated(LCObject object, List updatedKeys) { - System.out.println("对象被更新。"); - } -}); -``` - -#### `enter` 事件 - -当一个已存在的、原本不符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后符合查询条件,`enter` 事件会被触发。下面的 `object` 就是进入 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectEnter(LCObject object, List updatedKeys) { - System.out.println("对象进入。"); - } -}); -``` - -注意区分 `create` 和 `enter` 的不同行为。如果一个对象已经存在,在更新之前不符合查询条件,而在更新之后符合查询条件,那么 `enter` 事件会被触发。如果一个对象原本不存在,后来被构建了出来,那么 `create` 事件会被触发。 - -#### `leave` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后不符合查询条件,`leave` 事件会被触发。下面的 `object` 就是离开 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectLeave(LCObject object, List updatedKeys) { - System.out.println("对象离开。"); - } -}); -``` - -#### `delete` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 被删除,`delete` 事件会被触发。下面的 `object` 就是被删除的 `LCObject` 的 `objectId`: - - -```java -liveQuery.setEventHandler(new LCLiveQueryEventHandler() { - @Override - public void onObjectDeleted(String object) { - System.out.println("对象被删除。"); - } -}); -``` - - -### 取消订阅 - -如果不再需要接收有关 `LCQuery` 的更新,可以取消订阅。 - - -```java -liveQuery.unsubscribeInBackground(new LCLiveQuerySubscribeCallback() { - @Override - public void done(LCException e) { - if (e == null) { - // 成功取消订阅 - } - } -}); -``` - -### 断开连接 - -断开连接有几种情况: - -1. 网络异常或者网络切换,非预期性断开。 -2. 退出应用、关机或者打开飞行模式等,用户在应用外的操作导致断开。 - -如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。 - -而另外一种极端情况——**当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下**,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。 - -### LiveQuery 的注意事项 - -因为 LiveQuery 的实时性,很多用户会陷入一个误区,试着用 LiveQuery 来实现一个简单的聊天功能。 -我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,后期维护的难度非常大(聊天记录、对话维护之类的代码会很混乱),并且云服务已经提供了即时通讯的服务。 -LiveQuery 的核心还是提供一个针对查询的推拉结合的用法,脱离设计初衷容易造成前端的模块混乱。 - - -如果你的应用只使用 LiveQuery (不使用即时通讯和其他推送服务),那么可以在初始化 SDK 时使用 `PushService` 的静态方法 `startIfRequired` 来创建 WebSocket 连接: - -```java -PushService.startIfRequired(android.content.Context context); -``` - -## 文件 - -有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用 `LCObject` 保存,此时文件对象 `LCFile` 便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。 - -### 构建文件 - - -可以通过字符串构建文件: - -```java -// resume.txt 是文件名 -LCFile file = new LCFile("resume.txt", "LeanCloud".getBytes()); -``` - -除此之外,还可以通过 URL 构建文件: - -```java -LCFile file = new LCFile( - "logo.png", - "https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png", - new HashMap() -); -``` - -通过 URL 构建文件时,SDK 并不会将原本的文件转储到云端,而是会将文件的物理地址存储为字符串,这样也就不会产生任何文件上传流量。使用其他方式构建的文件会被保存在云端。 - -云端会根据文件扩展名自动检测文件类型。如果需要的话,也可以手动指定 `Content-Type`(一般称为 MIME 类型): - - -```java -Map meta = new HashMap(); -meta.put("mime_type", "application/json"); -LCFile file = new LCFile("resume.txt", "LeanCloud".getBytes(), meta); -``` - -与前面提到的方式相比,一个更常见的文件构建方式是从本地路径上传。 - - -```java -LCFile file = LCFile.withAbsoluteLocalPath("avatar.jpg", "/tmp/avatar.jpg"); -``` - -这里上传的文件名字叫做 `avatar.jpg`。需要注意: - -- 每个文件会被分配到一个独一无二的 `objectId`,所以在一个应用内是允许多个文件重名的。 -- 文件必须有扩展名才能被云端正确地识别出类型。比如说要用 `LCFile` 保存一个 PNG 格式的图像,那么扩展名应为 `.png`。 -- 如果文件没有扩展名,且没有手动指定类型,那么云服务将默认使用 `application/octet-stream`。 - -### 保存文件 - -将文件保存到云端后,便可获得一个永久指向该文件的 URL: - -```java -file.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCFile file) { - System.out.println("文件保存完成。URL: " + file.getUrl()); - } - public void onError(Throwable throwable) { - // 保存失败,可能是文件无法被读取,或者上传过程中出现问题 - } - public void onComplete() {} -}); -``` - -文件上传后,可以在 `_File` class 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的 `objectId` 和 URL。 - - - -```java -file.saveInBackground(true).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCFile file) { - System.out.println("文件保存完成。objectId:" + file.getObjectId()); - } - public void onError(Throwable throwable) { - // 保存失败,可能是文件无法被读取,或者上传过程中出现问题 - } - public void onComplete() {} -}); -``` - -已经保存到云端的文件可以关联到 `LCObject`: - -```java -LCObject todo = new LCObject("Todo"); -todo.put("title", "买蛋糕"); -// attachments 是一个 Array 属性 -todo.add("attachments", file); -todo.save(); -``` - -也可以通过构建 `LCQuery` 进行[查询](#查询): - -```java -LCQuery query = new LCQuery<>("_File"); -``` - - -需要注意的是,内部文件(上传到文件服务的文件)的 `url` 字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 -因此,通过 url 字段查询文件仅适用于外部文件(直接保存外部 URL 到 `_File` 表创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。 - -注意,如果文件被保存到了 `LCObject` 的一个数组属性中,那么在查询 `LCObject` 时如果需要包含文件,则要用到 `LCQuery` 的 `include` 方法。比如说,在获取所有标题为 `买蛋糕` 的 todo 的同时获取附件中的文件: - -```java -// 获取同一标题且包含附件的 todo -LCQuery query = new LCQuery<>("Todo"); -query.whereEqualTo("title", "买蛋糕"); -query.whereExists("attachments"); - -// 同时获取附件中的文件 -query.include("attachments"); - -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List todos) { - for (LCObject todo : todos) { - // 获取每个 todo 的 attachments 数组 - // {# TODO #} - } - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - - -### 上传进度监听 - -上传过程中可以实时向用户展示进度: - -```java -file.saveInBackground(new ProgressCallback() { - @Override - public void done(Integer percent) { - // percent 是一个 0 到 100 之间的整数,表示上传进度 - } -}); -``` - -### 文件元数据 - -上传文件时,可以用 `metaData` 添加额外的属性。文件一旦保存,`metaData` 便不可再修改。 - -```java -// 设置元数据 -file.addMetaData("author", "LeanCloud"); -file.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCFile file) { - // 获取 author 属性 - String author = (String) file.getMetaData("author"); - // 获取文件名 - String fileName = file.getName(); - // 获取大小(不适用于通过 base64 编码的字符串或者 URL 保存的文件) - int size = file.getSize(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### 图像缩略图 - -成功保存图像后,除了可以获取指向该文件的 URL 外,还可以获取图像的缩略图 URL,并且可以指定缩略图的宽度和高度: - - -```java -LCFile file = new LCFile("test.jpg", "文件-url", new HashMap()); -file.getThumbnailUrl(true, 100, 100); -``` - - -图片最大不超过 **20 MB** 才可以获取缩略图。 - -国际版不支持图片缩略图。 - -### 下载文件 - -可以调用 LCFile 的 `getDataInBackground` 或 `getDataStreamInBackground` 方法下载文件: - -```java -file.getDataInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) {} - - @Override - public void onNext(byte[] bytes) { - Log.d("file data length: " + bytes.length); - } - - @Override - public void onError(Throwable e) { - Log.d("failed to get data. cause: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); - -file.getDataStreamInBackground().subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) {} - - @Override - public void onNext(InputStream inputStream) { - try { - byte[] buffer = new byte[102400]; - int read = inputStream.read(buffer); - Log.d("file data length: " + read); - inputStream.close(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - @Override - public void onError(Throwable e) { - Log.d("failed to get data. cause: " + e.getMessage()); - } - - @Override - public void onComplete() {} -}); -``` - -除了使用 SDK 提供的方法外,也可以获取 LCFile 的 url 后调用标准库和第三方库下载文件。 - - -### 删除文件 - -下面的代码从云端删除一个文件: - -```java -LCObject file = LCObject.createWithoutData("_File", "552e0a27e4b0643b709e891e"); -file.delete() -``` - -默认情况下,文件的删除权限是关闭的,需要进入 **云服务控制台 > 数据存储 > 结构化数据 > `_File`**,选择 **其他** > **权限设置** > **`delete`** 来开启。 - -## GeoPoint - -云服务允许你通过将 `LCGeoPoint` 关联到 `LCObject` 的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。 - -要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个 `LCGeoPoint` 并将其纬度(`latitude`)设为 `39.9`,经度(`longitude`)设为 `116.4`: - -```java -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -``` - - -现在可以将这个地理位置存储为一个对象的属性: - -```java -todo.put("location", point); -``` - -### 地理位置查询 - -给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 `LCQuery` 添加 `whereNear` 条件。下面的代码查找 `location` 属性值离某一点最近的 `Todo` 对象: - -```java -LCQuery query = new LCQuery<>("Todo"); -LCGeoPoint point = new LCGeoPoint(39.9, 116.4); -query.whereNear("location", point); - -// 限制为 10 条结果 -query.limit(10); -query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List todos) { - // todos 是包含满足条件的 Todo 对象的数组 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -像 `orderByAscending` 和 `orderByDescending` 这样额外的排序条件会获得比默认的距离排序更高的优先级。 - -若要限制结果和给定地点之间的距离,可以参考 API 文档中的 `whereWithinKilometers`、`whereWithinMiles` 和 `whereWithinRadians` 参数。 - -若要查询在某一矩形范围内的对象,可以用 `whereWithinGeoBox`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```java -LCQuery query = new LCQuery<>("Todo"); -LCGeoPoint southwest = new LCGeoPoint(30, 115); -LCGeoPoint northeast = new LCGeoPoint(40, 118); -query.whereWithinGeoBox("location", southwest, northeast); -``` - -### GeoPoint 的注意事项 - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -## 用户 - -用户系统几乎是每款应用都要加入的功能,我们为此专门提供了一个 `LCUser` 类来方便应用使用各项用户管理的功能。 - -`LCUser` 是 `LCObject` 的子类,这意味着任何 `LCObject` 提供的方法也适用于 `LCUser`,唯一的区别就是 `LCUser` 提供一些额外的用户管理相关的功能。每个应用都有一个专门的 `_User` class 用于存放所有的 `LCUser`。 - -### 用户的属性 - -`LCUser` 相比一个普通的 `LCObject` 多出了以下属性: - -- `username`:用户的用户名。 -- `password`:用户的密码。 -- `email`:用户的电子邮箱。 -- `emailVerified`:用户的电子邮箱是否已验证。 -- `mobilePhoneNumber`:用户的手机号。 -- `mobilePhoneVerified`:用户的手机号是否已验证。 - -在接下来对用户功能的介绍中我们会逐一了解到这些属性。 - -### 注册 - -用户第一次打开应用的时候,可以让用户注册一个账户。下面的代码展示了一个典型的使用用户名和密码注册的流程: - -```java -// 创建实例 -LCUser user = new LCUser(); - -// 等同于 user.put("username", "Tom") -user.setUsername("Tom"); -user.setPassword("cat!@#123"); - -// 可选 -user.setEmail("tom@leancloud.rocks"); -user.setMobilePhoneNumber("+8618200008888"); - -// 设置其他属性的方法跟 LCObject 一样 -user.put("gender", "secret"); - -user.signUpInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 注册成功 - System.out.println("注册成功。objectId:" + user.getObjectId()); - } - public void onError(Throwable throwable) { - // 注册失败(通常是因为用户名已被使用) - } - public void onComplete() {} -}); -``` - -新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 - -如果收到 `202` 错误码,意味着 `_User` 表里已经存在使用同一 `username` 的账号,此时应提示用户换一个用户名。除此之外,每个用户的 `email` 和 `mobilePhoneNumber` 也需要保持唯一性,否则会收到 `203` 或 `214` 错误。 -可以考虑在注册时把用户的 `username` 设为与 `email` 相同,这样用户可以直接 [用邮箱重置密码](#重置密码)。 - -采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码(云端对密码的长度、复杂度不作限制),并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调 **在客户端,应用切勿再次对密码加密,这会导致 [重置密码](#重置密码) 等功能失效**。 - -### 登录 - -下面的代码用用户名和密码登录一个账户: - -```java -LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` - -#### 邮箱登录 - -下面的代码用邮箱和密码登录一个账户: - -```java -LCUser.loginByEmail("tom@leancloud.rocks", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功 - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` - -#### 单设备登录 - -某些场景下需要确保用户的账户在同一时间只在一台设备上登录,也就是说当用户在一台设备上登录后,其他设备上的会话全部失效。可以按照以下方案来实现: - -1. 新建一个专门用于记录用户登录信息和当前设备信息的 class。 -2. 每当用户在新设备上登录时,将该 class 中该用户对应的设备更新为该设备。 -3. 在另一台设备上打开客户端时,检查该设备是否与云端保存的一致。若不一致,则将用户 [登出](#当前用户)。 - -#### 账户锁定 - -输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 `{ "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." }`,开发者可在客户端进行必要提示。 - -锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 - -### 验证邮箱 - -可以通过要求用户在登录或使用特定功能之前验证邮箱的方式防止恶意注册。默认情况下,当用户注册或变更邮箱后,`emailVerified` 会被设为 `false`。在应用的 **云服务控制台 > 数据存储 > 用户 > 设置** 中,可以开启 **启用邮箱验证功能** 选项,这样当用户注册或变更邮箱时,会收到一封含有验证链接的邮件。在同一设置页面还可找到阻止未验证邮箱的用户登录的选项。 - -如果用户忘记点击链接并且在未来某一时刻需要进行验证,可以用下面的代码发送一封新的邮件: - -```java -LCUser.requestEmailVerifyInBackground("tom@leancloud.rocks").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -用户点击邮件内的链接后,`emailVerified` 会变为 `true`。如果用户的 `email` 属性为空,则该属性永远不会为 `true`。 - -### 当前用户 - -用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户: - - -```java -LCUser currentUser = LCUser.getCurrentUser(); -if (currentUser != null) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - - -会话信息会长期有效,直到用户主动登出: - - -```java -LCUser.logOut(); - -// currentUser 变为 null -LCUser currentUser = LCUser.getCurrentUser(); -``` - - -### 设置当前用户 - -用户登录后,云端会返回一个 **session token** 给客户端,它会由 SDK 缓存起来并用于日后同一 `LCUser` 的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个 `LCUser` 发起的请求了。 - -以下是一些应用可能需要用到 session token 的场景: - -- 应用根据以前缓存的 session token 登录(可以用 `LCUser.getCurrentUser().getSessionToken()` 获取到当前用户的 session token,在服务端等受信任的环境下,可以通过 `Master Key` 读取任意用户的 `sessionToken` 字段以获取 session token)。 -- 应用内的某个 WebView 需要知道当前登录的用户。 -- 在服务端登录后,返回 session token 给客户端,客户端根据返回的 session token 登录。 - -下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效): - - -```java -LCUser.becomeWithSessionTokenInBackground("anmlwi96s381m6ca7o7266pzf").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 修改 currentUser - LCUser.changeCurrentUser(user, true); - } - public void onError(Throwable throwable) { - // session token 无效 - } - public void onComplete() {} -}); -``` - - -请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。 - -如果在 **控制台 > 存储 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 `403 (Forbidden)` 错误。 - -下面的代码检查 session token 是否有效: - - -```java -boolean authenticated = LCUser.getCurrentUser().isAuthenticated(); -if (authenticated) { - // session token 有效 -} else { - // session token 无效 -} -``` - - -### 重置密码 - -我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了多种重置密码的方法。 - -邮箱重置密码的流程如下: - -1. 用户输入注册的电子邮箱,请求重置密码; -2. 云端向该邮箱发送一封包含重置密码的特殊链接的电子邮件; -3. 用户点击重置密码链接后,一个特殊的页面会打开,让他们输入新密码; -4. 用户的密码已被重置为新输入的密码。 - -首先让用户填写注册账户时使用的邮箱,然后调用下面的方法: - -```java -LCUser.requestPasswordResetInBackground("tom@leancloud.rocks").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCNull null) { - // 成功调用 - } - public void onError(Throwable throwable) { - // 调用出错 - } - public void onComplete() {} -}); -``` - -上面的代码会查询 `_User` 表中是否有对象的 `email` 属性与前面提供的邮箱匹配。如果有的话,则向该邮箱发送一封密码重置邮件。之前提到过,应用可以让 `username` 与 `email` 保持一致,也可以单独收集用户的邮箱并将其存为 `email`。 - -密码重置邮件的内容可在应用的 **云服务云服务控制台 > 数据存储 > 用户 > 邮件模版** 中自定义。更多关于自定义邮件模板和验证链接的内容,请参考[《自定义邮件验证和重设密码页面》](https://leancloud.cn/docs/custom-reset-verify-page.html)。 - - -### 用户的查询 - -可以直接构建一个针对 `_User` 的 `LCQuery` 来查询用户: - -```java -LCQuery userQuery = LCUser.getQuery(); -``` - - -为了安全起见,**新创建的应用的 `_User` 表默认关闭了 `find` 权限**,这样每位用户登录后只能查询到自己在 `_User` 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 `find` 查询权限。除此之外,还可以在云引擎里封装用户查询相关的方法。 - -可以参见 [用户对象的安全](#用户对象的安全) 来了解 `_User` 表的一些限制,还可以阅读[数据和安全](/v2/sdk/storage/guide/security) 来了解更多 class 级权限设置的方法。 - -### 关联用户对象 - -关联 `LCUser` 的方法和 `LCObject` 是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书: - -```java -LCObject book = new LCObject("Book"); -LCUser author = LCUser.getCurrentUser(); -book.put("title", "我的第五本书"); -book.put("author", author); -book.saveInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCObject book) { - // 获取所有该作者写的书 - LCQuery query = new LCQuery<>("Book"); - query.whereEqualTo("author", author); - query.findInBackground().subscribe(new Observer>() { - public void onSubscribe(Disposable disposable) {} - public void onNext(List books) { - // books 是包含同一作者所有 Book 对象的数组 - } - public void onError(Throwable throwable) {} - public void onComplete() {} - }); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -### 用户对象的安全 - -`LCUser` 类自带安全保障,只有通过 `logIn` 或者 `signUpInBackground` 这种经过鉴权的方法获取到的 `LCUser` 才能进行保存或删除相关的操作,保证每个用户只能修改自己的数据。 - -这样设计是因为 `LCUser` 中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。 - -下面的代码展现了这种安全措施: - -```java -LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 试图修改用户名 - user.put("username", "Jerry"); - // 密码已被加密,这样做会获取到空字符串 - String password = user.getString("password"); - // 可以执行,因为用户已鉴权 - user.save(); - - // 绕过鉴权直接获取用户 - LCQuery query = new LCQuery<>("_User"); - query.getInBackground(user.getObjectId()).subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser unauthenticatedUser) { - unauthenticatedUser.put("username", "Toodle"); - // 会出错,因为用户未鉴权 - unauthenticatedUser.save(); - } - public void onError(Throwable throwable) {} - public void onComplete() {} - }); - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - -通过 `LCUser.getCurrentUser()` 获取的 `LCUser` 总是经过鉴权的。 - -要查看一个 `LCUser` 是否经过鉴权,可以调用 `isAuthenticated` 方法。通过经过鉴权的方法获取到的 `LCUser` 无需进行该检查。 - -注意,用户的密码只能在注册的时候进行设置,日后如需修改,只能通过 [重置密码](#重置密码) 的方式进行。密码不会被缓存在本地。如果尝试直接获取已登录用户的密码,会得到 `null`。 - -### 其他对象的安全 - -对于给定的一个对象,可以指定哪些用户有权限读取或修改它。为实现该功能,每个对象都有一个由 `LCACL` 对象组成的访问控制表。请参阅[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html)。 - -### 第三方账户登录 - -云服务支持应用层直接使用第三方社交平台(例如微信、微博、QQ 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。 - -例如以下的代码展示了终端用户使用微信登录的处理流程: - -```java -Map thirdPartyData = new HashMap(); -// 必须 -thirdPartyData.put("expires_in", 7200); -thirdPartyData.put("openid", "OPENID"); -thirdPartyData.put("access_token", "ACCESS_TOKEN"); -// 可选 -thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); -thirdPartyData.put("scope", "SCOPE"); -LCUser.loginWithAuthData(thirdPartyData, "weixin").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) { - } - public void onNext(LCUser avUser) { - System.out.println("成功登录"); - } - public void onError(Throwable throwable) { - System.out.println("尝试使用第三方账号登录,发生错误。"); - } - public void onComplete() { - } -}); -``` - -`LCUser#loginWithAuthData` 系列方法需要两个参数来唯一确定一个账户: - -- 第三方平台的名字,就是前例中的 `weixin`,该名字由应用层自己决定。 -- 第三方平台的授权信息,就是前例中的 `thirdPartyData`(一般包括 `uid`、`token`、`expires` 等信息,与具体的第三方平台有关)。 - -云端会使用第三方平台的鉴权信息来查询是否已经存在与之关联的账户。如果存在的话,则返回 `200 OK` 状态码,同时附上用户的信息(包括 [`sessionToken`](#设置当前用户))。如果第三方平台的信息没有和任何账户关联,客户端会收到 `201 Created` 状态码,意味着新账户被创建,同时附上用户的 `objectId`、`createdAt`、`sessionToken` 和一个自动生成的 `username`,例如: - -```json -{ - "username": "k9mjnl7zq9mjbc7expspsxlls", - "objectId": "5b029266fb4ffe005d6c7c2e", - "createdAt": "2018-05-21T09:33:26.406Z", - "updatedAt": "2018-05-21T09:33:26.575Z", - "sessionToken": "…", - // authData 通常不会返回,继续阅读以了解其中原因 - "authData": { - "weixin": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" - } - } - // … -} -``` - -这时候我们会看到 `_User` 表中出现了一条新的账户记录,账户中有一个名为 `authData` 的列,保存了第三方平台的授权信息。出于安全考虑,`authData` 不会被返回给客户端,除非它属于当前用户。 - -开发者需要自己完成第三方平台的鉴权流程(一般通过 OAuth 1.0 或 2.0),以获取鉴权信息,继而到云端来登录。 - - -#### 鉴权数据的保存 - -`_User` class 中的 `authData` 是一个以平台名为键名,鉴权信息为键值的 JSON 对象。 - -一个关联了微信账户的用户应该会有下列对象作为 `authData`: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - } -} -``` - -而一个关联了微博账户的用户,则会有如下的 `authData`: - -```json -{ - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx", - } -} -``` - -我们允许一个账户绑定多个第三方平台的鉴权数据,这样如果某个用户同时关联了微信和微博账户,则其 `authData` 可能会是这样的: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - } - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx", - } -} -``` - -理解 `authData` 的数据结构至关重要。一个终端用户通过如下的鉴权信息来登录的时候, - -```json -"weixin": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" -} -``` - -云端首先会查找账户系统(_User 表),看看是否存在 `authData.weixin.openid = "OPENID"` 的账户,如果存在,则返回现有账户,如果不存在那么就创建一个新账户,同时将上面的鉴权信息写入新账户的 `authData` 属性中,并将新账户的数据当成结果返回。 - -云端会自动为 `_User` class 中每个用户的 `authData..` 创建唯一索引,从而避免重复数据。 -`` 在微信等部分云服务内建支持的第三方平台上为 `openid` 字段,在其他第三方平台(包括部分云服务专门支持的第三方平台和所有云服务没有专门支持的第三方平台)上为 `uid` 字段。 - -#### 自动验证第三方平台授权信息 - -为了确保账户数据的有效性,云端还支持对部分平台的 `Access Token` 的有效性进行自动验证,以防止伪造账户数据。如果有效性验证不通过,云端会返回 `invalid authData` 错误,关联不会被建立。对于云端无法识别的服务,开发者需要自己去验证 `Access Token` 的有效性。 -比如,注册、登录时分别通过云引擎的 `beforeSave hook`、`beforeUpdate hook` 来验证 `Access Token` 有效性。 - -如果希望使用这一功能,则在开始使用前,需要在 **控制台 > 存储 > 用户 > 设置** 配置相应平台的 **应用 ID** 和 **应用 Secret Key**。 - -如果不希望云端自动验证 `Access Token`,可以在 **控制台 > 存储 > 设置** 里面取消勾选 **第三方登录时,验证用户 AccessToken 合法性**。 - -配置平台账号的目的在于创建 `LCUser` 时,云端会使用相关信息去校验请求参数 `thirdPartyData` 的合法性,确保 `LCUser` 实际对应着一个合法真实的用户,确保平台安全性。 - -#### 绑定第三方账户 - -用户已经有了 LCUser 并登录成功后,可以绑定新的第三方账号信息。 -绑定成功后,新的第三方账户信息会被添加到 LCUser 的 authData 字段里。 - -例如,下面的代码可以关联微信账户: - -```java -avUser.associateWithAuthData(weixinData, "weixin").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser avUser) { - System.out.println("绑定成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("绑定失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - - -为节省篇幅,上面的代码示例中没有给出具体的微信平台授权信息,相关内容请参考上面的[「第三方账户登录」](#第三方账户登录)一节。 - -#### 解除与第三方账户的关联 - -类似地,可以解绑第三方账户。 - -例如,下面的代码可以解除用户和微信账户的关联: - -```java -LCUser avUser = LCUser.currentUser(); -avUser.dissociateWithAuthData("weixin").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser avUser) { - System.out.println("解绑成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("解绑失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - - -#### 扩展:第三方登录时补充完整的用户信息 - -有些产品,新用户在使用第三方账号授权拿到相关信息后,仍然需要补充设置用户名、手机号、密码等重要信息后,才被允许登录成功。 - -这时要使用 `loginWithauthData` 登录接口的 `failOnNotExist` 参数并将其设置为 `true`。服务端会判断是否已存在能匹配上的 `authData`,如果不存在则会返回 `211` 错误码和 `Could not find user` 报错信息。开发者根据这个 `211` 错误码,跳转到要求输入用户名、密码、手机号等信息的页面,实例化一个 `LCUser` 对象,保存上述补充数据,再次调用 `loginWithauthData` 接口进行登录,并 **不再传入 `failOnNotExist` 参数**。示例代码如下: - -```java -Map thirdPartyData = new HashMap(); -thirdPartyData.put("expires_in", 7200); -thirdPartyData.put("openid", "OPENID"); -thirdPartyData.put("access_token", "ACCESS_TOKEN"); -thirdPartyData.put("refresh_token", "REFRESH_TOKEN"); -thirdPartyData.put("scope", "SCOPE"); -Boolean failOnNotExist = true; -LCUser user = new LCUser(); -user.loginWithAuthData(thirdPartyData, "weixin", failOnNotExist).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser avUser) { - System.out.println("存在匹配的用户,登录成功"); - } - @Override - public void onError(Throwable e) { - LCException avException = new LCException(e); - int code = avException.getCode(); - if (code == 211){ - // 跳转到输入用户名、密码、手机号等业务页面 - } else { - System.out.println("发生错误:" + e.getMessage()); - } - } - @Override - public void onComplete() { - } -}); - -// 跳转到输入用户名、密码、手机号等业务页面之后 -LCUser avUser = new LCUser(); -avUser.setUsername("Tom"); -avUser.setMobilePhoneNumber("+8618200008888"); -avUser.loginWithAuthData(thirdPartyData, "weixin").subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser avUser) { - System.out.println("登录成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("登录失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - - -#### 扩展:接入 UnionID 体系,打通不同子产品的账号系统 - -随着第三方平台的账户体系变得日渐复杂,它们的第三方鉴权信息出现了一些较大的变化。下面我们以最典型的微信开放平台为例来进行说明。 - -当一个用户在移动应用内登录微信账号时,会被分配一个 OpenID;在微信小程序内登录账号时,又会被分配另一个不同的 OpenID。这样的架构会导致的问题是,使用同一个微信号的用户,也无法在微信开发平台下的移动应用和小程序之间互通。 - -微信官方为了解决这个问题,引入了 `UnionID` 的体系,以下为其官方说明: - -> 通过获取用户基本信息接口,开发者可通过 OpenID 来获取用户基本信息,而如果开发者拥有多个公众号,可使用以下办法通过 UnionID 机制来在多公众号之间进行用户帐号互通。只要是同一个微信开放平台帐号下的公众号,用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID 是相同的。 - -其他平台,如 QQ 和微博,与微信的设计也基本一致。 - -云服务支持 `UnionID` 体系。你只需要给 `loginWithauthData` 和 `associateWithauthData` 接口传入更多的第三方鉴权信息,即可完成新 UnionID 体系的集成。新增加的第三方鉴权登录选项包括: - -- unionId,指第三方平台返回的 UnionId 字符串。 -- unionId platform,指 unionId 对应的 platform 字符串,由应用层自己指定,[后面](#该如何指定-unionIdPlatform)会详述。 -- asMainAccount,指示是否把当前平台的鉴权信息作为主账号来使用。如果作为主账号,那么就由当前用户唯一占有该 unionId,以后其他平台使用同样的 unionId 登录的话,会绑定到当前的用户记录上来;否则,当前应用的鉴权信息会被绑定到其他账号上去。 - -下面让我们通过一个例子来说明如何使用这些参数完成 UnionID 登录。 - -假设云服务在微信开放平台上有两个应用,一个是「云服务通讯」,一个是「云服务技术支持」,这两个应用在接入第三方鉴权的时候,分别使用了 `wxleanoffice` 和 `wxleansupport` 作为 platform 来进行登录。现在我们开启 UnionID 的用户体系,希望同一个微信用户在这两个应用中都能对应到同一个账户系统(_User 表中的同一条记录),同时我们决定将 `wxleanoffice` 平台作为主账号平台。 - -假设对于用户 A,微信给 ta 为 云服务分配的 UnionId 为 `unionid4a`,而对两个应用的授权信息分别为: - -```json -"wxleanoffice": { - "access_token": "officetoken", - "openid": "officeopenid", - "expires_in": 1384686496 -}, -"wxleansupport": { - "openid": "supportopenid", - "access_token": "supporttoken", - "expires_in": 1384686496 -} -``` - -现在,用户 A 在「云服务通讯」中通过微信登录,其调用请求为: - -```java -Map thirdPartyData = new HashMap(); -thirdPartyData.put("expires_in", 1384686496); -thirdPartyData.put("uid", "officeopenid"); -thirdPartyData.put("access_token", "officetoken"); -thirdPartyData.put("scope", "SCOPE"); -LCUser.loginWithAuthData(thirdPartyData, "wxleanoffice", - "unionid4a", "weixin", true) // 新增参数,分别表示 uniondId,unionIdPlatform,asMainAccount - // 对于 unionIdPlatform,这里使用「weixin」来指代微信平台。 - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser avUser) { - System.out.println("登录成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("登录失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - - -> 注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 `authData..uid` 的唯一索引,具体可以参考[数据存储 REST API 使用详解](/v2/sdk/storage/guide/rest)的《连接用户账户和第三方平台》。 - -如果用户 A 是第一次在「云服务通讯」中通过微信登录,那么 _User 表中会增加一个新用户(假设其 objectId 为 `ThisIsUserA`),其 `authData` 的结果如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { // 新增键值对 - "uid": "unionid4a" - } -} -``` - -可以看到,与之前的第三方登录 API 相比,这里由于登录时指定了 `asMainAccount` 为 true,所以 authData 的第一级子目录中增加了 `_weixin_unionid` 的键值对,这里的 `weixin` 就是我们指定的 `unionIdPlatform` 的值。`_weixin_unionid` 这个增加的键值对非常重要,以后我们判断是否存在同样 UnionID 的账户就是依靠它来查找的,而是否增加这个键值对,则是由登录时指定的 `asMainAccount` 的值决定的: - -- 当 `asMainAccount` 为 true 时,云端会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,当前账号就会作为这一个 UnionID 对应的主账号被唯一确定。 -- 当 `asMainAccount` 为 false 时,云端不会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,此时如果通过提供的 UnionID 可以找到主账号,则会将当前的鉴权信息合并进主账号的 `authData` 属性里,同时返回主账号对应的 _User 表记录;如果通过提供的 UnionID 找不到主账号,则会根据平台的 `openid` 去查找账户,找到匹配的账户就返回匹配的,找不到就新建一个账户,此时的处理逻辑与不使用 UnionID 时的逻辑完全一致。 - - -接下来,用户 A 继续在「云服务技术支持」中进行微信登录,其登录逻辑为: - -```java -Map thirdPartyData = new HashMap(); -thirdPartyData.put("expires_in", 1384686496); -thirdPartyData.put("uid", "supportopenid"); -thirdPartyData.put("access_token", "supporttoken"); -thirdPartyData.put("scope", "SCOPE"); -LCUser.loginWithAuthData(thirdPartyData, "wxleansupport", "unionid4a", - "weixin", // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 - false).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - @Override - public void onNext(LCUser avUser) { - System.out.println("登录成功"); - } - @Override - public void onError(Throwable e) { - System.out.println("登录失败:" + e.getMessage()); - } - @Override - public void onComplete() { - } -}); -``` - - -与「云服务通讯」中的登录过程相比,在「云服务技术支持」这个应用中,我们在登录时只是将 `asMainAccount` 设为了 false。 这时我们看到,本次登录得到的还是 objectId 为 `ThisIsUserA` 的 _User 表记录(同一个账户),同时该账户的 `authData` 属性中发生了变化,多了 `wxleansupport` 的数据,如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - }, - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -在新的登录方式中,当一个用户以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录得到新的 `LCUser` 后,接下来这个用户以「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 `wxleansupport` 的新用户数据加入到已有账户的 `authData` 里了,不会再创建新的账户。 - -这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个 `LCUser` 上,实现互通。 - -##### 为 UnionID 建立索引 - -云端会为 UnionID 自动建立索引,不过因为自动创建基于请求的抽样统计,可能会滞后。 -因此,我们推荐自行创建相关索引,特别是用户量(`_User` 表记录数)很大的应用,更需要预先创建索引,否则用户使用 UnionID 账号登录时可能超时失败。 -以上面的微信 UnionID 为例,建议在控制台预先创建下列唯一索引(允许缺失值): - -- `authData.wxleanoffice.uid` -- `authData.wxleansupport.uid` -- `authData._weixin_unionid.uid` - -##### 该如何指定 unionIdPlatform - -从上面的例子可以看出,使用 UnionID 登录的时候,需要指定 `unionIdPlatform` 的主要目的,就是为了便于查找已经存在的唯一主账号。云端会在主账号对应账户的 `authData` 属性中增加一个 `_{unionIdPlatform}_unionid` 键值对来标记唯一性,终端用户在其他应用中登录的时候,云端会根据参数中提供的 `uniondId` + `unionIdPlatform` 的组合,在 `_User` 表中进行查找,这样来确定唯一的既存主账号。 - -本来 `unionIdPlatform` 的取值,应该是开发者可以自行决定的,但是 Javascript SDK 基于易用性的目的,在 `loginWithAuthDataAndUnionId` 之外,还额外提供了两个接口: - -- `LC.User.loginWithQQAppWithUnionId`,这里默认使用 `qq` 作为 `unionIdPlatform`。 -- `LC.User.loginWithWeappWithUnionId`,这里默认使用 `weixin` 作为 `unionIdPlatform`。 - -从我们的统计来看,这两个接口已经被很多开发者接受,在大量线上产品中产生着实际数据。所以为了避免数据在不同平台(例如 Android 和 iOS 应用)间发生冲突,建议大家统一按照 Javascript SDK 的默认值来设置 `unionIdPlatform`,即: - -- 微信平台的多个应用,统一使用 `weixin` 作为 `unionIdPlatform`; -- QQ 平台的多个应用,统一使用 `qq` 作为 `unionIdPlatform`; -- 微博平台的多个应用,统一使用 `weibo` 作为 `unionIdPlatform`; -- 除此之外的其他平台,开发者可以自行定义 `unionIdPlatform` 的名字,只要自己保证多个应用间统一即可。 - - -##### 主副应用不同登录顺序出现的不同结果 - -上面的流程是用户先登录了「云服务通讯」这个主应用,然后再登录「云服务技术支持」这个副应用,所以账号都被通过 UnionID 有效关联起来了。可能有人会想到另外一个问题,如果用户 B 先登录副应用,后登录主应用,这时候会发生什么事情呢? - -用户 B 首先登录副应用的时候,提供了「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,并且指定「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 false」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个 `LCUser` 对象,该账户 `authData` 结果为: - -```json -{ - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,以及「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 true」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个 `LCUser` 对象,该账户 `authData` 结果为: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - } -} -``` - -还有更复杂的情况。如果某公司的产品之前就接入了微信登录,产生了很多存量用户,并且分散在不同的子产品中,这时候怎么办?我们接下来专门讨论此时的解决方案。 - -##### 存量账户如何通过 UnionID 实现关联 - -还是以我们的两个子产品「云服务通讯」(后续以「产品 1」指代)和「云服务技术支持为例」(后续以「产品 2」指代),在接入 UnionID 之前,我们就接入了之前版本的微信平台登录,这时候账户系统内可能存在多种账户: - -- 只使用产品 1 的微信用户 A -- 只使用产品 2 的微信用户 B -- 同时使用两个产品的微信用户 C - -此时的存量账户表如下所示: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1) | N/A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1) | N/A -4 | UserC | openid4(对应产品 2) | N/A - -现在我们对两个子产品进行升级,接入 UnionID 体系。这时因为已经有同一个微信用户在不同子产品中创建了不同的账户(例如 objectId 为 3 和 4 的账户),我们需要确定以哪个平台的账号为主。比如决定使用「云服务通讯」上生成的账号为主账号,则在该应用程序更新版本时,使用 `asMainAccount=true` 参数。这个应用带着 UnionID 登录匹配或创建的账号将作为主账号,之后所有这个 UnionID 的登录都会匹配到这个账号。请注意这时 `_User` 表里会剩下一些用户数据,也就是没有被选为主账号的、其他平台的同一个用户的旧账号数据(例如 objectId 为 2 和 4 的账户)。这部分数据会继续服务于已经发布的但仍然使用 OpenID 登录的旧版应用。 - -接下来我们看一下,如果以产品 1 的账户作为「主账户」,按照前述的方式同时提供 openid/unionid 完成登录,则最后达到的结果是: - -1. 使用老版本的用户,不管在哪个产品里面,都可以和往常一样通过 openid 登录到正确的账户; -2. 使用产品 1 的新版本的老用户,通过 openid/unionid 组合,还是绑定到原来的账户。例如 UserC 在产品 1 中通过 openid3/unionId3 还是会绑定到 objectId=3 的账户(会增加 uniondId 记录);而 UserC 在产品 2 的新版本中,通过 openid4/unionId3 的组合则会绑定到 objectId=3 的账户,而不再是原来的 objectId=4 的账户。 -3. 使用产品 1 的新版本的新用户,通过 openid/unionid 组合,会创建新的账户;之后该用户再使用产品 2 的新版本,也会绑定到刚才创建的新账户上。 - -以上面的三个用户为例,他们分别升级到两个产品的最新版,且最终都会启用两个产品,则账户表的最终结果如下: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C -4 | UserC | openid4(对应产品 2) | N/A -5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B - -之后有新的用户 D,分别在两个产品的新版本中登录,则账户表中会增加一条新的 objectId=6 的记录,结果如下: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C -4 | UserC | openid4(对应产品 2) | N/A -5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B -6 | UserD | openid7(对应产品 1)/openid8(对应产品 2) | unionId_user_D - -如果之后我们增加了新的子产品 3,这些用户在子产品 3 中也进行微信登录的话,那么四个用户还是会继续绑定到 objectId 为 1/3/5/6 的主账户。此时账户表的结果会变为: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1)/openid6(对应产品 2)/openid9(对应产品 3) | unionId_user_A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1)/openid4(对应产品 2)/openid10(对应产品 3) | unionId_user_C -4 | UserC | openid4(对应产品 2) | N/A -5 | UserB | openid5(对应产品 1)/openid2(对应产品 2)/openid11(对应产品 3) | unionId_user_B -6 | UserD | openid7(对应产品 1)/openid8(对应产品 2)/openid12(对应产品 3) | unionId_user_D - -### 匿名用户 - -将数据与用户关联需要首先创建一个用户,但有时你不希望强制用户在一开始就进行注册。使用匿名用户,可以让应用不提供注册步骤也能创建用户。下面的代码创建一个新的匿名用户: - -```java -LCUser.logInAnonymously().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // user 是新的匿名用户 - } - public void onError(Throwable throwable) {} - public void onComplete() {} -}); -``` - - -可以像给普通用户设置属性那样给匿名用户设置 `username`、`password`、`email` 等属性,还可以通过走正常的注册流程来将匿名用户转化为普通用户。匿名用户能够: - -- [使用用户名和密码注册](#注册) -- [关联第三方平台](#第三方账户登录),比如微信 - -下面的代码为一名匿名用户设置用户名和密码: - -```java -// currentUser 是个匿名用户 -LCUser currentUser = LCUser.getCurrentUser(); - -currentUser.setUsername("Tom"); -currentUser.setPassword("cat!@#123"); - -currentUser.signUpInBackground().subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // currentUser 已经转化为普通用户 - } - public void onError(Throwable throwable) { - // 注册失败(通常是因为用户名已被使用) - } - public void onComplete() {} -}); -``` - - -下面的代码检查当前用户是否为匿名用户: - -```java -LCUser currentUser = LCUser.getCurrentUser(); -if (currentUser.isAnonymous()) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - - -如果匿名用户未能在登出前转化为普通用户,那么该用户将无法再次登录同一账户,且之前产生的数据也无法被取回。 - - -## 角色 - -随着用户量的增长,你可能会发现相比于为每一名用户单独设置权限,将预先设定好的权限直接分配给一部分用户是更好的选择。为了迎合这种需求,云服务支持基于角色的权限管理。请参阅[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html)。 - - -## 全文搜索 - -全文搜索是一个针对应用数据进行全局搜索的接口,它基于搜索引擎构建,提供更强大的搜索功能。要深入了解其用法和阅读示例代码,请阅[全文搜索指南](/v2/sdk/storage/guide/fulltext-search)。 diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/05-setup-objc.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/05-setup-objc.mdx deleted file mode 100644 index c6b1f530e..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/05-setup-objc.mdx +++ /dev/null @@ -1,180 +0,0 @@ ---- -id: setup-objc -title: 数据存储、即时通讯 Objective-C SDK 配置 -sidebar_label: Objective-C SDK 配置 ---- - - - -## 获取 SDK - -获取 SDK 有多种方式,较为推荐的方式是通过包依赖管理工具下载最新版本。 - -### 包依赖管理工具安装 - -通过 [CocoaPods](https://cocoapods.org) 安装可以最大化地简化安装过程。 - -首先,确保开发环境中已经安装了最新版 `pod`。如果没有,请参考官网的 [INSTALL](https://cocoapods.org) 文档。 - -接着,在项目根目录下通过命令行工具执行下列命令生成 `Podfile` 文件: - -```sh -$ pod init -``` - -参考 [GET STARTED](https://cocoapods.org) 文档,在 `Podfile` 文件中的 `target` 里添加以下 pod 依赖: - -```ruby -pod 'LeanCloudObjc' # 集成所有服务模块 -``` - -`LeanCloudObjc` 包含多个 Subspecs。如果只需要部分功能,可以按需选择: - -```ruby -pod 'LeanCloudObjc/Foundation' # 数据存储、短信、云引擎等基础服务模块 -pod 'LeanCloudObjc/Realtime' # 即时通讯、LiveQuery 模块 -``` - -最后,在项目根目录下执行下列任意命令,集成最新的 SDK: - -```sh -$ pod update -``` - -或者 - -```sh -$ pod install --repo-update -``` - -集成 SDK 成功后,使用项目根目录下 **`<项目名称>.xcworkspace`** 来打开项目。 - -### 手动安装 - -#### 下载源码 - -在 [SDK 下载页面][download-sdk],下载最新版的源码。 - -[download-sdk]: https://releases.leanapp.cn/#/leancloud/objc-sdk/releases -#### 集成 SDK - -将 `AVOS`/`AVOS.xcodeproj` 项目文件拖入示例项目,作为 subproject: - -![「AVOS.xcodeproj」会出现在项目根目录下。](/img/quick_start/ios/subproject.png) - -接着为示例项目连接依赖库,在 **xcodeproj > target > general > frameworks** 添加如下内容: - -![「LeanCloudObjc.framework」](/img/quick_start/ios/link-binary.png) - -这样就集成完毕了。 - -## 快速开始 - -### 绑定域名 - -你需要绑定 API 自定义域名,以便和其他厂商的应用隔离入口,避免其他应用受到 DDoS 攻击时相互牵连。 -如果使用了文件服务,也需要绑定文件自定义域名。 - -进入 **开发者中心 > 你的游戏 > 游戏服务 > 技术服务 > 数据存储 > 服务设置 > 自定义域名** 点击「绑定新域名」按钮,根据控制台提示完成绑定步骤。 -注意,DNS 解析记录和证书申请(如果选择了自动管理 SSL 证书)都需要一定时间,请耐心等待。 - -绑定成功后,初始化 SDK 时,请传入绑定的自定义域名(`https://please-replace-with-your-customized.domain.com`)。 - -如果你使用了文件服务(包括即时通讯的多媒体消息(图像、音频、视频等)),同样需要前往 **开发者中心 > 你的游戏 > 游戏服务 > 技术服务 > 数据存储 > 文件 > 设置 > 文件访问域名** 绑定域名,步骤和 API 自定义域名基本相同,但有两点不一样: - -1. API 域名解析使用 A 记录,文件域名解析使用 CNAME 记录,也因此文件域名不支持绑定裸域名(例如 `example.com`),需要绑定子域名(例如 `files.example.com`)。 -2. 绑定成功后,还需在 **文件 > 设置 > 文件访问地址** 点击「修改」按钮进行切换。 - -### 应用凭证 - -在 **开发者中心 > 你的游戏 > 游戏服务 > 基本信息** 可以查看应用凭证: - -- **Client ID**,又称 `App ID`,在 SDK 初始化时用到。联系技术支持时,提供 `Client ID` 可以方便我们更快定位到你的应用。 -- **Client Token**,又称 `App Key`,在 SDK 初始化时用到。 -- **Server Secret**,又称 `Master Key`,用于在自有服务器、云引擎等**受信任环境**调用管理接口,具备跳过一切权限验证的超级权限。所以**一定注意保密,千万不要在客户端代码中使用该凭证**。 - -### 初始化 - -打开 `AppDelegate` 文件,导入基础模块头文件: - -```objc -#import -``` - - -然后在 `application:didFinishLaunchingWithOptions:` 方法中设置 `App ID`,`App Key` 以及服务器地址: - -```objc -[LCApplication setApplicationId:@"your-client-id" - clientKey:@"your-client-token" - serverURLString:@"https://please-replace-with-your-customized.domain.com"]; -``` - -在使用 SDK 的 API 时,请确保进行了 Application 的 ID、Key 以及 Server URL 的初始化。 - - -## 开启调试日志 - -在应用开发阶段,你可以选择开启 SDK 的调试日志(debug log)来方便追踪问题。调试日志开启后,SDK 会把网络请求、错误消息等信息输出到 IDE 的日志窗口,或是浏览器 Console 或是云引擎日志(如果在云引擎下运行 SDK)。 - -```objc -// 在 Application 初始化代码执行之前执行 -[LCApplication setAllLogsEnabled:true]; -``` - -详细调试流程可以参考 [Objective-C SDK 调试指南][objc-debug-guide]。 - -[objc-debug-guide]: https://forum.leancloud.cn/t/leancloud-sdk-objective-c-sdk/21851 - -注意,在应用发布之前,请关闭调试日志,以免暴露敏感数据。 - -## 验证 - -首先,确认本地网络环境是可以访问云端服务器的,可以执行以下命令: - -```sh -curl "https://{{host}}/1.1/date" -``` - -`{{host}}` 为绑定的 API 自定义域名。 - -如果当前网路正常会返回当前时间: - -```json -{"__type":"Date","iso":"2020-10-12T06:46:56.000Z"} -``` - -下面来试着向云端保存一条数据,将下面的代码拷贝到 `viewDidLoad` 方法或其他在应用运行时会被调用的方法中: - -```objc -LCObject *testObject = [LCObject objectWithClassName:@"TestObject"]; -[testObject setObject:@"Hello world!" forKey:@"words"]; -[testObject save]; -``` - -然后,点击 `Run` 运行调试,真机和虚拟机均可。 - -然后打开 **云服务控制台 > 数据存储 > 结构化数据 > `TestObject`**,如果看到数据表中出现一行「words」列的值为「Hello world!」的数据,说明 SDK 已经正确地执行了上述代码,配置完毕。 - -如果控制台没有发现对应的数据,请参考 [问题排查](#问题排查)。 - -## 问题排查 - -SDK 安装指南基于当前最新版本的 SDK 编写,所以排查问题前,请先检查下安装的 SDK 是不是最新版本。 - -### `401 Unauthorized` - -如果 SDK 抛出 `401` 异常或者查看本地网络访问日志存在: - -```json -{ - "code": 401, - "error": "Unauthorized." -} -``` - -则可认定为 `App ID` 或者 `App Key` 输入有误,或者是不匹配,很多开发者同时注册了多个应用,导致拷贝粘贴的时候,用 A 应用的 App ID 匹配 B 应用的 `App Key`,这样就会出现服务端鉴权失败的错误。 - -### 客户端无法访问网络 - -客户端尤其是手机端,应用在访问网络的时候需要申请一定的权限。 \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/06-objc.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/06-objc.mdx deleted file mode 100644 index 7797fc441..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/06-objc.mdx +++ /dev/null @@ -1,2422 +0,0 @@ ---- -id: objc -title: 数据存储开发指南 · Objective-C -sidebar_label: Objective-C 指南 ---- - - - -数据存储是云服务提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端: - -```objc -// 构建对象 -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; - -// 为属性赋值 -[todo setObject:@"工程师周会" forKey:@"title"]; -[todo setObject:@"周二两点,全体成员" forKey:@"content"]; - -// 将对象保存到云端 -[todo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 成功保存之后,执行其他逻辑 - NSLog(@"保存成功。objectId:%@", todo.objectId); - } else { - // 异常处理 - } -}]; -``` - -我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。 - -## SDK 安装与初始化 - -请阅读[数据存储、即时通讯 Objective C SDK 配置指南](/v2/sdk/storage/guide/setup-objc)。 - -## 对象 - -### `LCObject` - -`LCObject` 是云服务对复杂对象的封装,每个 `LCObject` 包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个 `LCObject` 上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。 - -比如说,一个保存着单个 Todo 的 `LCObject` 可能包含如下数据: - -```json -title: "给小林发邮件确认会议时间", -isComplete: false, -priority: 2, -tags: ["工作", "销售"] -``` - - -### 数据类型 - -`LCObject` 支持的数据类型包括 `String`、`Number`、`Boolean`、`Object`、`Array`、`Date` 等等。你可以通过嵌套的方式在 `Object` 或 `Array` 里面存储更加结构化的数据。 - -`LCObject` 还支持两种特殊的数据类型 `Pointer` 和 `File`,可以分别用来存储指向其他 `LCObject` 的指针以及二进制数据。 - -`LCObject` 同时支持 `GeoPoint`,可以用来存储地理位置信息。参见 [GeoPoint](#geopoint)。 - -以下是一些示例: - -```objc -// 基本类型 -NSNumber *boolean = @(YES); -NSNumber *number = [NSNumber numberWithInt:2018]; -NSString *string = [NSString stringWithFormat:@"%@ 流行音乐榜单", number]; -NSDate *date = [NSDate date]; -NSData *data = [@"Hello world!" dataUsingEncoding:NSUTF8StringEncoding]; -NSArray *array = [NSArray arrayWithObjects: string, number, nil]; -NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: number, @"number", string, @"string", nil]; - -// 构建对象 -LCObject *testObject = [LCObject objectWithClassName:@"TestObject"]; -[testObject setObject:boolean forKey:@"testBoolean"]; -[testObject setObject:number forKey:@"testInteger"]; -[testObject setObject:string forKey:@"testString"]; -[testObject setObject:date forKey:@"testDate"]; -[testObject setObject:data forKey:@"testData"]; -[testObject setObject:array forKey:@"testArray"]; -[testObject setObject:dictionary forKey:@"testDictionary"]; -[testObject saveInBackground]; -``` - -我们不推荐通过 `NSData` 在 `LCObject` 里面存储图片、文档等大型二进制数据。每个 `LCObject` 的大小不应超过 **128 KB**。如需存储大型文件,可创建 `LCFile` 实例并将其关联到 `LCObject` 的某个属性上。参见 [文件](#文件)。 - -注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。 - -**云服务控制台 > 数据存储 > 结构化数据** 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。 - -若想了解云服务是如何保护应用数据的,请阅读[数据和安全](/v2/sdk/storage/guide/security)。 - -### 构建对象 - -下面的代码构建了一个 class 为 `Todo` 的 `LCObject`: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; - -// 等同于 -LCObject *todo = [[LCObject alloc] initWithClassName:@"Todo"]; -``` - - -在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。 - -### 保存对象 - -下面的代码将一个 class 为 `Todo` 的对象存入云端: - -```objc -// 构建对象 -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; - -// 为属性赋值 -[todo setObject:@"马拉松报名" forKey:@"title"]; -[todo setObject:@2 forKey:@"priority"]; - -// 将对象保存到云端 -[todo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 成功保存之后,执行其他逻辑 - NSLog(@"保存成功。objectId:%@", todo.objectId); - } else { - // 异常处理 - } -}]; -``` - -为了确认对象已经保存成功,我们可以到 **云服务控制台 > 数据存储 > 结构化数据 > `Todo`** 里面看一下,应该会有一行新的数据产生。点一下这个数据的 `objectId`,应该能看到类似这样的内容: - -```json -{ - "title": "马拉松报名", - "priority": 2, - "ACL": { - "*": { - "read": true, - "write": true - } - }, - "objectId": "582570f38ac247004f39c24b", - "createdAt": "2017-11-11T07:19:15.549Z", - "updatedAt": "2017-11-11T07:19:15.549Z" -} -``` - -注意,无需在 **云服务控制台 > 数据存储 > 结构化数据** 里面创建新的 `Todo` class 即可运行前面的代码。如果 class 不存在,它将自动创建。 - -以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定: - -内置属性 | 类型 | 描述 ---- | --- | --- -`objectId` | `NSString` | 该对象唯一的 ID 标识。 -`ACL` | `LCACL` | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 -`createdAt` | `NSDate` | 该对象被创建的时间。 -`updatedAt` | `NSDate` | 该对象最后一次被修改的时间。 - - -这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的 `LCObject` 不存在这些属性。 - -属性名(**keys**)只能包含字母、数字和下划线。自定义属性不得以双下划线(`__`)开头或与任何系统保留字段和内置属性(`ACL`、`className`、`createdAt`、`objectId` 和 `updatedAt`)重名,无论大小写。 - -属性值(**values**)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 [数据类型](#数据类型)。 - -我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 `CustomData`。属性,采用小驼峰法,如 `imageUrl`。 - -### 获取对象 - -对于已经保存到云端的 `LCObject`,可以通过它的 `objectId` 将其取回: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query getObjectInBackgroundWithId:@"582570f38ac247004f39c24b" block:^(LCObject *todo, NSError *error) { - // todo 就是 objectId 为 582570f38ac247004f39c24b 的 Todo 实例 - NSString *title = todo[@"title"]; - int priority = [[todo objectForKey:@"priority"] intValue]; - - // 获取内置属性 - NSString *objectId = todo.objectId; - NSDate *updatedAt = todo.updatedAt; - NSDate *createdAt = todo.createdAt; -}]; -``` - - -对象拿到之后,可以通过 `get` 方法来获取各个属性的值。注意 `objectId`、`updatedAt` 和 `createdAt` 这三个内置属性不能通过 `get` 获取或通过 `set` 修改,只能由云端自动进行填充。尚未保存的 `LCObject` 不存在这些属性。 - -如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 `nil`。 - -#### 同步对象 - -当云端数据发生更改时,你可以调用 `fetchInBackgroundWithBlock` 方法来刷新对象,使之与云端数据同步: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -[todo fetchInBackgroundWithBlock:^(LCObject *todo, NSError *error) { - // todo 已刷新 -}]; -``` - - -刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,**`fetchInBackgroundWithBlock` 操作会丢弃这些修改**。为避免这种情况,你可以在刷新时指定 **需要刷新的属性**,这样只有指定的属性会被刷新(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`),其他属性不受影响。 - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -NSArray *keys = [NSArray arrayWithObjects:@"priority", @"location", nil]; -[todo fetchInBackgroundWithKeys:keys block:^(LCObject *todo, NSError *error) { - // 只有 priority 和 location 会被获取和刷新 -}]; -``` - -### 更新对象 - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `saveInBackground` 方法。例如: - -要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 `saveInBackground` 方法。例如: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -[todo setObject:@"这周周会改到周三下午三点。" forKey:@"content"]; -[todo saveInBackground]; -``` - -云服务会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。 - -#### 有条件更新对象 - -通过传入 `query` 选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回 `305` 错误。 - -例如,用户的账户表 `Account` 有一个余额字段 `balance`,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求: - -```objc -LCObject *account = [LCObject objectWithClassName:@"Account" objectId:@"5745557f71cfe40068c6abe0"]; -// 对 balance 原子减少 100 -NSInteger amount = -100; -[account incrementKey:@"balance" byAmount:@(amount)]; -// 设置条件 -LCQuery *query = [[LCQuery alloc] init]; -[query whereKey:@"balance" greaterThanOrEqualTo:@(-amount)]; -LCSaveOption *option = [[LCSaveOption alloc] init]; -option.query = query; -// 操作结束后,返回最新数据。 -// 如果是新对象,则所有属性都会被返回, -// 否则只有更新的属性会被返回。 -option.fetchWhenSave = YES; -[account saveInBackgroundWithOption:option block:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"当前余额为:%@", account[@"balance"]); - } else if (error.code == 305) { - NSLog(@"余额不足,操作失败!"); - } -}]; -``` - - -**`query` 选项只对已存在的对象有效**,不适用于尚未存入云端的对象。 - -`query` 选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过 `LCQuery` 查询 `LCObject` 再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。 - -#### 更新计数器 - -设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 **原子操作** 来增加或减少一个属性内保存的数字: - -```objc -[post incrementKey:@"likes" byAmount:@1]; -``` - - -可以指定需要增加或减少的值。若未指定,则默认使用 `1`。 - -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,需要原子增减的字段建议使用整数以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 - -#### 更新数组 - -更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据: - -- `addObject:forKey:` 将指定对象附加到数组末尾。 -- `addObjectsFromArray:forKey:` 将指定对象数组附加到数组末尾。 -- `addUniqueObject:forKey:` 将指定对象附加到数组末尾,确保对象唯一。 -- `addUniqueObjectsFromArray:forKey:` 将指定对象数组附加到数组末尾,确保对象唯一。 -- `removeObject:forKey:` 从数组字段中删除指定对象的所有实例。 -- `removeObjectsInArray:forKey:` 从数组字段中删除指定的对象数组。 - - -例如,`Todo` 用一个 `alarms` 属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性: - -```objc --(NSDate*) getDateWithDateString:(NSString*) dateString{ - NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init]; - [dateFormat setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; - NSDate *date = [dateFormat dateFromString:dateString]; - return date; -} - -NSDate *alarm1 = [self getDateWithDateString:@"2018-04-30 07:10:00"]; -NSDate *alarm2 = [self getDateWithDateString:@"2018-04-30 07:20:00"]; -NSDate *alarm3 = [self getDateWithDateString:@"2018-04-30 07:30:00"]; - -NSArray *alarms = [NSArray arrayWithObjects:alarm1, alarm2, alarm3, nil]; - -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; -[todo addUniqueObjectsFromArray:alarms forKey:@"alarms"]; -[todo saveInBackground]; -``` - -### 删除对象 - -下面的代码从云端删除一个 `Todo` 对象: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"]; -[todo deleteInBackground]; -``` - -注意,删除对象是一个较为敏感的操作,我们建议你阅读[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html)来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。 - -### 批量操作 - -```objc -// 批量构建和更新 -+ (BOOL)saveAll:(NSArray *)objects error:(NSError **)error; -+ (void)saveAllInBackground:(NSArray *)objects - block:(LCBooleanResultBlock)block; - -// 批量删除 -+ (BOOL)deleteAll:(NSArray *)objects error:(NSError **)error; -+ (void)deleteAllInBackground:(NSArray *)objects - block:(LCBooleanResultBlock)block; - -// 批量同步 -+ (BOOL)fetchAll:(NSArray *)objects error:(NSError **)error; -+ (void)fetchAllInBackground:(NSArray *)objects - block:(LCArrayResultBlock)block; -``` - -下面的代码将所有 `Todo` 的 `isComplete` 设为 `true`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query findObjectsInBackgroundWithBlock:^(NSArray *todos, NSError *error) { - // 获取需要更新的 todo - for (LCObject *todo in todos) { - // 更新属性值 - todo[@"isComplete"] = @(YES); - } - // 批量更新 - [LCObject saveAllInBackground:todos]; -}]; -``` - - -虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。 -### 后台运行 - -细心的开发者已经发现,在所有的示例代码中几乎都是用了异步来访问云端,形如 `xxxxInBackground` 的用法都是提供给开发者在主线程调用用以实现后台运行的方法,因此开发者在主线程可以放心地调用这种命名方式的函数。 - -### 离线存储对象 - -大多数保存功能可以立刻执行,并通知应用「保存完毕」。不过若不需要知道保存完成的时间,则可使用 `saveEventually` 来代替。 - -它的优点在于:如果用户目前尚未接入网络,`saveEventually` 会缓存设备中的数据,并在网络连接恢复后上传。如果应用在网络恢复之前就被关闭了,那么当它下一次打开时,SDK 会再次尝试保存操作。 - -所有 `saveEventually`(或 `deleteEventually`)的相关调用,将按照调用的顺序依次执行。因此,多次对某一对象使用 `saveEventually` 是安全的。 - -### 数据模型 - -对象之间可以产生关联。拿一个博客应用来说,一个 `Post` 对象可以与许多个 `Comment` 对象产生关联。云服务支持三种关系:一对一、一对多、多对多。 - -#### 一对一、一对多关系 - -一对一、一对多关系可以通过将 `LCObject` 保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个 `Comment` 指向一个 `Post`。 - -下面的代码会创建一个含有单个 `Comment` 的 `Post`: - -```objc -// 创建 post -LCObject *post = [[LCObject alloc] initWithClassName:@"Post"]; -[post setObject:@"饿了……" forKey:@"title"]; -[post setObject:@"中午去哪吃呢?" forKey:@"content"]; - -// 创建 comment -LCObject *comment = [[LCObject alloc] initWithClassName:@"Comment"]; -[comment setObject:@"当然是肯德基啦!" forKey:@"content"]; - -// 将 post 设为 comment 的一个属性值 -[comment setObject:post forKey:@"parent"]; - -// 保存 comment 会同时保存 post -[comment saveInBackground]; -``` - -云端存储时,会将被指向的对象用 `Pointer` 的形式存起来。你也可以用 `objectId` 来指向一个对象: - -```objc -LCObject *post = [LCObject objectWithClassName:@"Post" objectId:@"57328ca079bc44005c2472d0"]; -[comment setObject:post forKey:@"post"]; -``` - - -请参阅 [关系查询](#关系查询) 来了解如何获取关联的对象。 - -#### 多对多关系 - -想要建立多对多关系,最简单的办法就是使用 **数组**。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 **中间表** 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。 - -我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。 - -### 序列化和反序列化 - -在实际的开发中,把 `LCObject` 当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此 `LCObject` 也提供了序列化和反序列化的方法。 - -序列化: - -```objc -LCObject *todo = [[LCObject alloc] initWithClassName:@"Todo"]; // 构建对象 -[todo setObject:@"马拉松报名" forKey:@"title"]; // 设置名称 -[todo setObject:@2 forKey:@"priority"]; // 设置优先级 -[todo setObject:[LCUser currentUser] forKey:@"owner"]; // 这里就是一个 Pointer 类型,指向当前登录的用户 - -NSMutableDictionary *serializedJSONDictionary = [todo dictionaryForObject]; // 获取序列化后的字典 -NSError *err; -NSData *jsonData = [NSJSONSerialization dataWithJSONObject:serializedJSONDictionary options:0 error:&err]; // 获取 JSON 数据 -NSString *serializedString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; // 获取 JSON 字符串 -// serializedString 的内容是:{"title":"马拉松报名","className":"Todo","priority":2} -``` - - -反序列化: - -```objc -NSMutableDictionary *objectDictionary = [NSMutableDictionary dictionaryWithCapacity:10];// 声明一个 NSMutableDictionary -[objectDictionary setObject:@"马拉松报名" forKey:@"title"]; -[objectDictionary setObject:@2 forKey:@"priority"]; -[objectDictionary setObject:@"Todo" forKey:@"className"]; - -LCObject *todo = [LCObject objectWithDictionary:objectDictionary]; // 由 NSMutableDictionary 转化一个 LCObject - -[todo saveInBackground]; // 保存到云端 -``` - -## 查询 - -我们已经了解到如何从云端获取单个 `LCObject`,但你可能还会有一次性获取多个符合特定条件的 `LCObject` 的需求,这时候就需要用到 `LCQuery` 了。 - -### 基础查询 - -执行一次基础查询通常包括这些步骤: - -1. 构建 `LCQuery`; -2. 向其添加查询条件; -3. 执行查询并获取包含满足条件的对象的数组。 - -下面的代码获取所有 `lastName` 为 `Smith` 的 `Student`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Student"]; -[query whereKey:@"lastName" equalTo:@"Smith"]; -[query findObjectsInBackgroundWithBlock:^(NSArray *students, NSError *error) { - // students 是包含满足条件的 Student 对象的数组 -}]; -``` - -### 查询条件 - -可以给 `LCObject` 添加不同的条件来改变获取到的结果。 - -下面的代码查询所有 `firstName` 不为 `Jack` 的对象: - -```objc -[query whereKey:@"firstName" notEqualTo:@"Jack"]; -``` - -对于能够排序的属性(比如数字、字符串),可以进行比较查询: - -```objc -// 限制 age < 18 -[query whereKey:@"age" lessThan:@18]; - -// 限制 age <= 18 -[query whereKey:@"age" lessThanOrEqualTo:@18]; - -// 限制 age > 18 -[query whereKey:@"age" greaterThan:@18]; - -// 限制 age >= 18 -[query whereKey:@"age" greaterThanOrEqualTo:@18]; -``` - -可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是 `AND` 的关系: - -```objc -[query whereKey:@"firstName" equalTo:@"Jack"]; -[query whereKey:@"age" greaterThan:@18]; -``` - -可以通过指定 `limit` 限制返回结果的数量(默认为 `100`): - -```objc -// 最多获取 10 条结果 -query.limit = 10; -``` - -由于性能原因,`limit` 最大只能设为 `1000`。即使将其设为大于 `1000` 的数,云端也只会返回 1,000 条结果。 - -如果只需要一条结果,可以直接用 `getFirstObject`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"priority" equalTo:@2]; -[query getFirstObjectInBackgroundWithBlock:^(LCObject *todo, NSError *error) { - // todo 是第一个满足条件的 Todo 对象 -}]; -``` - -可以通过设置 `skip` 来跳过一定数量的结果: - -```objc -// 跳过前 20 条结果 -query.skip = 20; -``` - -把 `skip` 和 `limit` 结合起来,就能实现翻页功能: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"priority" equalTo:@2]; -query.limit = 10; -query.skip = 20; -``` - -需要注意的是,`skip` 的值越高,查询所需的时间就越长。作为替代方案,可以通过设置 `createdAt` 或 `updatedAt` 的范围来实现更高效的翻页,因为它们都自带索引。 -同理,也可以通过设置自增字段的范围来实现翻页。 - -对于能够排序的属性,可以指定结果的排序规则: - -```objc -// 按 createdAt 升序排列 -[query orderByAscending:@"createdAt"]; - -// 按 createdAt 降序排列 -[query orderByDescending:@"createdAt"]; -``` - -还可以为同一个查询添加多个排序规则: - -```objc -[query addAscendingOrder:@"priority"]; -[query addDescendingOrder:@"createdAt"]; -``` - -下面的代码可用于查找包含或不包含某一属性的对象: - - -可以通过 `selectKeys` 指定需要返回的属性。下面的代码只获取每个对象的 `title` 和 `content`(包括内置属性 `objectId`、`createdAt` 和 `updatedAt`): - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query selectKeys:@[@"title", @"content"]]; -[query getFirstObjectInBackgroundWithBlock:^(LCObject *todo, NSError *error) { - NSString *title = todo[@"title"]; // √ - NSString *content = todo[@"content"]; // √ - NSString *notes = todo[@"notes"]; // 会报错 -}]; -``` - -`selectKeys` -支持点号(`author.firstName`),详见[《点号使用指南》](https://leancloud.cn/docs/dot-notation.html)。 -另外,字段名前添加减号前缀表示反向选择,例如 `-author` 表示不返回 `author` 字段。 -反向选择同样适用于内置字段,比如 `-objectId`,也可以和点号组合使用,比如 `-pubUser.createdAt`。 - -对于未获取的属性,可以通过对结果中的对象进行 `fetchInBackgroundWithBlock` 操作来获取。参见 [同步对象](#同步对象)。 -### 字符串查询 - -可以用 `hasPrefix` 来查找某一属性值以特定字符串开头的对象。和 SQL 中的 `LIKE` 一样,你可以利用索引带来的优势: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -// 相当于 SQL 中的 title LIKE 'lunch%' -[query whereKey:@"title" hasPrefix:@"lunch"]; -``` - - -可以用 `containsString` 来查找某一属性值包含特定字符串的对象: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -// 相当于 SQL 中的 title LIKE '%lunch%' -[query whereKey:@"title" containsString:@"lunch"]; -``` - - -和 `hasPrefix` 不同,`containsString` 无法利用索引,因此不建议用于大型数据集。 - -注意 `hasPrefix` 和 `containsString` 都是 **区分大小写** 的,所以上述查询会忽略 `Lunch`、`LUNCH` 等字符串。 - -如果想查找某一属性值不包含特定字符串的对象,可以使用 `matchesRegex` 进行基于正则表达式的查询: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -// "title" 不包含 "ticket"(不区分大小写) -[query whereKey:@"title" matchesRegex:@"^((?!ticket).)*$", modifiers:"i"]; -``` - -不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class, -因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以使用全文搜索服务。 - -使用查询时如果遇到性能问题,可参阅 [查询性能优化](#查询性能优化)。 - -### 数组查询 - -下面的代码查找所有数组属性 `tags` 包含 ` 工作 ` 的对象: - -```objc -[query whereKey:@"tags" equalTo:@"工作"]; -``` - -下面的代码查询数组属性长度为 3 (正好包含 3 个标签)的对象: - -```objc -[query whereKey:@"tags" sizeEqualTo:3]; -``` - -下面的代码查找所有数组属性 `tags` **同时包含** `工作`、`销售` 和 `会议` 的对象: - -```objc -[query whereKey:@"tags" containsAllObjectsInArray:[NSArray arrayWithObjects:@"工作", @"销售", @"会议", nil]]; -``` - -如需获取某一属性值包含一列值中任意一个值的对象,可以直接用 `containedIn` 而无需执行多次查询。下面的代码构建的查询会查找所有 `priority` 为 `1` **或** `2` 的 todo 对象: - -```objc -// 单个查询 -LCQuery *priorityOneOrTwo = [LCQuery queryWithClassName:@"Todo"]; -[priorityOneOrTwo whereKey:@"priority" containedIn:[NSArray arrayWithObjects:@1, @2, nil]]; -// 这样就可以了 :) - -// --------------- -// vs. -// --------------- - -// 多个查询 -LCQuery *priorityOne = [LCQuery queryWithClassName:@"Todo"]; -[priorityOne whereKey:@"priority" equalTo:@1]; - -LCQuery *priorityTwo = [LCQuery queryWithClassName:@"Todo"]; -[priorityTwo whereKey:@"priority" equalTo:@2]; - -LCQuery *priorityOneOrTwo = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:priorityOne, priorityTwo, nil]]; -// 好像有些繁琐 :( -``` - -反过来,还可以用 `notContainedIn` 来获取某一属性值不包含一列值中任何一个的对象。 - -### 关系查询 - -查询关联数据有很多种方式,常见的一种是查询某一属性值为特定 `LCObject` 的对象,这时可以像其他查询一样直接用 `equalTo`。比如说,如果每一条博客评论 `Comment` 都有一个 `post` 属性用来存放原文 `Post`,则可以用下面的方法获取所有与某一 `Post` 相关联的评论: - -```objc -LCObject *post = [LCObject objectWithClassName:@"Post" objectId:@"57328ca079bc44005c2472d0"]; -LCQuery *query = [LCQuery queryWithClassName:@"Comment"]; -[query whereKey:@"post" equalTo:post]; -[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) { - // comments 包含与 post 相关联的评论 -}]; -``` - -如需获取某一属性值为另一查询结果中任一 `LCObject` 的对象,可以用 `matchesQuery`。下面的代码构建的查询可以找到所有包含图片的博客文章的评论: - -```objc -LCQuery *innerQuery = [LCQuery queryWithClassName:@"Post"]; -[innerQuery whereKeyExists:@"images"]; - -LCQuery *query = [LCQuery queryWithClassName:@"Comment"]; -[query whereKey:@"post" matchesQuery:innerQuery]; -``` - -如需获取某一属性值不是另一查询结果中任一 `LCObject` 的对象,则使用 `doesNotMatchQuery`。 - -有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用 `includeKey`。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Comment"]; - -// 获取最新发布的 -[query orderByDescending:@"createdAt"]; - -// 只获取 10 条 -query.limit = 10; - -// 同时包含博客文章 -[query includeKey:@"post"]; - -[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) { - // comments 包含最新发布的 10 条评论,包含各自对应的博客文章 - for (LCObject *comment in comments) { - // 该操作无需网络连接 - LCObject *post = comment[@"post"]; - } -}]; -``` - -可以用 dot 符号(`.`)来获取多级关系,例如 `post.author`,详见[《点号使用指南》](https://leancloud.cn/docs/dot-notation.html)的《在查询对象时使用点号》一节。 - -可以在同一查询上应用多次 `includeKey` 以包含多个属性。通过这种方法获取到的对象同样接受 `getFirstObject` 等 `LCQuery` 辅助方法。 - -通过 `includeKey` 进行多级查询的方式不适用于数组属性内部的 `LCObject`,只能包含到数组本身。 - -#### 关系查询的注意事项 - -云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌/子查询(和普通查询一样,`limit` 默认为 `100`,最大 `1000`),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出 `limit`,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有 `limit` 数量以内的结果会被填入主查询。 - -我们建议采用以下方案进行改进: - -- 确保子查询的结果在 100 条以下,如果在 100 至 1,000 条之间的话请将子查询的 `limit` 设为 `1000`。 -- 将需要查询的字段冗余到主查询所在的表上。 -- 进行多次查询,每次在子查询上设置不同的 `skip` 值来遍历所有记录(注意 `skip` 的值较大时可能会引发性能问题,因此不是很推荐)。 - -### 统计总数量 - -如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用 `countObjectsInBackgroundWithBlock` 来代替 `findObjectsInBackgroundWithBlock`。比如说,查询有多少个已完成的 todo: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"isComplete" equalTo:@(YES)]; -[query countObjectsInBackgroundWithBlock:^(NSInteger count, NSError *error) { - NSLog(@"%ld 个 todo 已完成。", count); -}]; -``` - -### 组合查询 - -组合查询就是把诸多查询条件用一定逻辑合并到一起(`OR` 或 `AND`)再交给云端去查询。 - -组合查询不支持在子查询中包含 `GeoPoint` 或其他非过滤性的限制(例如 `near`、`withinGeoBox`、`limit`、`skip`、`ascending`、`descending`、`include`)。 - -#### OR 查询 - -OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于 `3` 或者已经完成了的 todo: - -```objc -LCQuery *priorityQuery = [LCQuery queryWithClassName:@"Todo"]; -[priorityQuery whereKey:@"priority" greaterThanOrEqualTo:@3]; - -LCQuery *isCompleteQuery = [LCQuery queryWithClassName:@"Todo"]; -[isCompleteQuery whereKey:@"isComplete" equalTo:@(YES)]; - -LCQuery *query = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:priorityQuery, isCompleteQuery, nil]]; -``` - -使用 OR 查询时,子查询中不能包含 `GeoPoint` 相关的查询。 - -#### AND 查询 - -使用 AND 查询的效果等同于往 `LCQuery` 添加多个条件。下面的代码构建的查询会查找创建时间在 `2016-11-13` 和 `2016-12-02` 之间的 todo: - -```objc -NSDate *(^dateFromString)(NSString *string) = ^(NSString *string) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd"]; - return [dateFormatter dateFromString:string]; -}; - -LCQuery *startDateQuery = [LCQuery queryWithClassName:@"Todo"]; -[startDateQuery whereKey:@"createdAt" greaterThanOrEqualTo:dateFromString(@"2016-11-13")]; - -LCQuery *endDateQuery = [LCQuery queryWithClassName:@"Todo"]; -[endDateQuery whereKey:@"createdAt" lessThan:dateFromString(@"2016-12-03")]; - -LCQuery *query = [LCQuery andQueryWithSubqueries:[NSArray arrayWithObjects:startDateQuery, endDateQuery, nil]]; -``` - - -单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询: - - -```objc -NSDate *(^dateFromString)(NSString *string) = ^(NSString *string) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd"]; - return [dateFormatter dateFromString:string]; -}; - -LCQuery *createdAtQuery = [LCQuery queryWithClassName:@"Todo"]; -[createdAtQuery whereKey:@"createdAt" greaterThanOrEqualTo:dateFromString(@"2018-04-30")]; -[createdAtQuery whereKey:@"createdAt" lessThan:dateFromString(@"2018-05-01")]; - -LCQuery *locationQuery = [LCQuery queryWithClassName:@"Todo"]; -[locationQuery whereKeyDoesNotExist:@"location"]; - -LCQuery *priority2Query = [LCQuery queryWithClassName:@"Todo"]; -[priority2Query whereKey:@"priority" equalTo:@2]; - -LCQuery *priority3Query = [LCQuery queryWithClassName:@"Todo"]; -[priority3Query whereKey:@"priority" equalTo:@3]; - -LCQuery *priorityQuery = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:priority2Query, priority3Query, nil]]; -LCQuery *timeLocationQuery = [LCQuery orQueryWithSubqueries:[NSArray arrayWithObjects:locationQuery, createdAtQuery, nil]]; -LCQuery *query = [LCQuery andQueryWithSubqueries:[NSArray arrayWithObjects:priorityQuery, timeLocationQuery, nil]]; -``` - -### 缓存查询 - -缓存一些查询的结果到磁盘上,这可以让你在离线的时候,或者应用刚启动,网络请求还没有足够时间完成的时候可以展现一些数据给用户。当缓存占用了太多空间的时候,LeanStorage 会自动清空缓存。 - -默认情况下的查询不会使用缓存,除非你调用接口明确设置启用。例如,尝试从网络请求,如果网络不可用则从缓存数据中获取,可以这样设置: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Post"]; -query.cachePolicy = kLCCachePolicyNetworkElseCache; - -// 设置缓存有效期 -query.maxCacheAge = 24*3600; - -[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { - if (!error) { - // 成功找到结果,先找网络再访问磁盘 - } else { - // 无法访问网络,本次查询结果未做缓存 - } -}]; -``` - -#### 缓存策略 - -为了满足多变的需求,SDK 默认提供了以下几种缓存策略: - -策略枚举 | 含义及解释 ---- | --- -`kLCCachePolicyIgnoreCache`| **(默认缓存策略)**查询行为不从缓存加载,也不会将结果保存到缓存中。 -`kLCCachePolicyCacheOnly` | 查询行为忽略网络状况,只从缓存加载。如果没有缓存结果,该策略会产生 `LCError`。 -`kLCCachePolicyCacheElseNetwork` | 查询行为首先尝试从缓存加载,若加载失败,则通过网络加载结果。如果缓存和网络获取行为均为失败,则产生 `LCError`。注意查询条件默认会从当前时间从新往旧查询,因此这种情况下第一次查询总需要访问网络。 -`kLCCachePolicyNetworkElseCache` | 查询行为先尝试从网络加载,若加载失败,则从缓存加载结果。如果缓存和网络获取行为均为失败,则产生 `LCError`。 -`kLCCachePolicyCacheThenNetwork` | 查询先从缓存加载,然后从网络加载。在这种情况下,回调函数会被调用两次,第一次是缓存中的结果,然后是从网络获取的结果。因为它会在不同的时间返回两个结果,所以该策略不能与 `findObjects` 同时使用。 - -#### 缓存相关的操作 - -- 检查是否存在缓存查询结果: - - ```objc - BOOL isInCache = [query hasCachedResult]; - ``` - -- 删除某一查询的任何缓存结果:(删除缓存只影响持久化缓存(磁盘缓存),不影响内存缓存,下同) - - ```objc - [query clearCachedResult]; - ``` - -- 删除查询的所有缓存结果: - - ```objc - [LCQuery clearAllCachedResults]; - ``` - -- 设定缓存结果的最长时限: - - ```objc - query.maxCacheAge = 60 * 60 * 24; // 一天的总秒数 - ``` - -查询缓存也适用于 `LCQuery` 的辅助方法,包括 `getFirstObject` 和 `getObjectInBackground`。 - -### 查询性能优化 - -影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。 - -- 不等于和不包含查询(无法使用索引) -- 通配符在前面的字符串查询(无法使用索引) -- 有条件的 `count`(需要扫描所有数据) -- `skip` 跳过较多的行数(相当于需要先查出被跳过的那些行) -- 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引) -- 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据) - -## LiveQuery - -LiveQuery 衍生于 [`LCQuery`](#查询),并为其带来了更强大的功能。它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。 - -设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用 `LCQuery` 并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。 - -想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的 `LCQuery`。订阅成功后,一旦有符合 `LCQuery` 的 `LCObject` 发生变化,云端就会主动、实时地将信息通知到客户端。 - -LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。 - -### 启用 LiveQuery - -进入 **控制台 > 存储 > 设置**,在 **其他** 里面勾选 **启用 LiveQuery** - -如果没有集成 `Realtime` 模块,需先集成 `Realtime` 模块,pod 添加示例如下: - -```ruby -pod 'LeanCloudObjc/Realtime' -``` - -可以在 [SDK 安装与初始化](#SDK-安装与初始化) 中找到完整设置方法。 - -之后在相关头文件中导入 `Realtime` 模块,示例如下: - -```objc -#import -``` - - -### Demo - -下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果: - - - -使用我们的「LeanTodo」微信小程序和网页应用,可以实际体验以上视频所演示的效果,步骤如下: - -1. 微信扫码,添加小程序「LeanTodo」; - - ![LeanTodo mini program](/img/leantodo-weapp-qr.jpg) - -2. 进入小程序,点击首页左下角 **设置** > **账户设置**,输入便于记忆的用户名和密码; - -3. 使用浏览器访问 ,输入刚刚在小程序中更新好的账户信息,点击 **Login**; - -4. 随意添加更改数据,查看两端的同步状态。 - -注意按以上顺序操作。在网页应用中使用 **Signup** 注册的账户无法与小程序创建的账户相关联,所以如果颠倒以上操作顺序,则无法观测到数据同步效果。 - -[LiveQuery 公开课](http://www.bilibili.com/video/av11291992/) 涵盖了许多开发者关心的问题和解答。 - -### 构建订阅 - -首先创建一个普通的 `LCQuery` 对象,添加查询条件(如有),然后进行订阅操作: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -self.liveQuery = [[LCLiveQuery alloc] initWithQuery:query]; -self.liveQuery.delegate = self; -[self.liveQuery subscribeWithCallback:^(BOOL succeeded, NSError * _Nonnull error) { - // 订阅成功 -}]; -``` - - -LiveQuery 不支持内嵌查询,也不支持返回指定属性。 - -订阅成功后,就可以接收到和 `LCObject` 相关的更新了。假如在另一个客户端上创建了一个 `Todo` 对象,对象的 `title` 设为 `更新作品集`,那么下面的代码可以获取到这个新的 `Todo`: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -self.liveQuery = [[LCLiveQuery alloc] initWithQuery:query]; -self.liveQuery.delegate = self; -[self.liveQuery subscribeWithCallback:^(BOOL succeeded, NSError * _Nonnull error) { - // 订阅成功 -}]; -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidCreate:(id)object { - if (liveQuery == self.liveQuery) { - NSLog(@"%@", object[@"title"]); // 更新作品集 - } -} -``` - - -此时如果有人把 `Todo` 的 `content` 改为 `把我最近画的插画放上去`,那么下面的代码可以获取到本次更新: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidUpdate:(id)updatedTodo updatedKeys:(NSArray *)updatedKeys { - NSLog(@"%@", updatedTodo[@"content"]); // 把我最近画的插画放上去 -} -``` - - -### 事件处理 - -订阅成功后,可以选择监听如下几种数据变化: - -- `create` -- `update` -- `enter` -- `leave` -- `delete` - -#### `create` 事件 - -当有新的满足 `LCQuery` 查询条件的 `LCObject` 被创建时,`create` 事件会被触发。下面的 `object` 就是新建的 `LCObject`: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidCreate:(id)object { - if (liveQuery == self.liveQuery) { - NSLog(@"对象被创建。"); - } -} -``` - - -#### `update` 事件 - -当有满足 `LCQuery` 查询条件的 `LCObject` 被更新时,`update` 事件会被触发。下面的 `object` 就是有更新的 `LCObject`: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidUpdate:(id)object updatedKeys:(NSArray *)updatedKeys { - if (liveQuery == self.liveQuery) { - NSLog(@"对象被更新。"); - } -} -``` - - -#### `enter` 事件 - -当一个已存在的、原本不符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后符合查询条件,`enter` 事件会被触发。下面的 `object` 就是进入 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidEnter:(id)object updatedKeys:(nonnull NSArray *)updatedKeys { - if (liveQuery == self.liveQuery) { - NSLog(@"对象进入。"); - } -} -``` - - -注意区分 `create` 和 `enter` 的不同行为。如果一个对象已经存在,在更新之前不符合查询条件,而在更新之后符合查询条件,那么 `enter` 事件会被触发。如果一个对象原本不存在,后来被构建了出来,那么 `create` 事件会被触发。 - -#### `leave` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 发生更新,且更新后不符合查询条件,`leave` 事件会被触发。下面的 `object` 就是离开 `LCQuery` 的 `LCObject`,其内容为该对象最新的值: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidLeave:(id)object updatedKeys:(nonnull NSArray *)updatedKeys { - if (liveQuery == self.liveQuery) { - NSLog(@"对象离开。"); - } -} -``` - - -#### `delete` 事件 - -当一个已存在的、原本符合 `LCQuery` 查询条件的 `LCObject` 被删除,`delete` 事件会被触发。下面的 `object` 就是被删除的 `LCObject` 的 `objectId`: - -```objc -- (void)liveQuery:(LCLiveQuery *)liveQuery objectDidDelete:(id)object { - if (liveQuery == self.liveQuery) { - NSLog(@"对象被删除。"); - } -} -``` - -### 取消订阅 - -如果不再需要接收有关 `LCQuery` 的更新,可以取消订阅。 - -```objc -[liveQuery unsubscribeWithCallback:^(BOOL succeeded, NSError * _Nonnull error) { - if (succeeded) { - // 成功取消订阅 - } else { - // 错误处理 - } -}]; -``` - - -### 断开连接 - -断开连接有几种情况: - -1. 网络异常或者网络切换,非预期性断开。 -2. 退出应用、关机或者打开飞行模式等,用户在应用外的操作导致断开。 - -如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。 - -而另外一种极端情况——**当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下**,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。 - -### LiveQuery 的注意事项 - -因为 LiveQuery 的实时性,很多用户会陷入一个误区,试着用 LiveQuery 来实现一个简单的聊天功能。 -我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,后期维护的难度非常大(聊天记录、对话维护之类的代码会很混乱),并且云服务已经提供了即时通讯的服务。 -LiveQuery 的核心还是提供一个针对查询的推拉结合的用法,脱离设计初衷容易造成前端的模块混乱。 - -## 文件 - -有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用 `LCObject` 保存,此时文件对象 `LCFile` 便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。 - -### 构建文件 - - -可以通过字符串构建文件: - -```objc -NSData *data = [@"LeanCloud" dataUsingEncoding:NSUTF8StringEncoding]; -// resume.txt 是文件名 -LCFile *file = [LCFile fileWithData:data name:@"resume.txt"]; -``` - -除此之外,还可以通过 URL 构建文件: - -```objc -LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"https://leancloud.cn/assets/imgs/press/Logo%20-%20Blue%20Padding.a60eb2fa.png"]]; -``` - -通过 URL 构建文件时,SDK 并不会将原本的文件转储到云端,而是会将文件的物理地址存储为字符串,这样也就不会产生任何文件上传流量。使用其他方式构建的文件会被保存在云端。 - -云端会根据文件扩展名自动检测文件类型。如果需要的话,也可以手动指定 `Content-Type`(一般称为 MIME 类型): - -与前面提到的方式相比,一个更常见的文件构建方式是从本地路径上传。 - - -```objc -NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); -NSString *documentsDirectory = [paths objectAtIndex:0]; -NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:@"avatar.jpg"]; -NSError *error; -LCFile *file = [LCFile fileWithLocalPath:imagePath error:&error]; -``` - -这里上传的文件名字叫做 `avatar.jpg`。需要注意: - -- 每个文件会被分配到一个独一无二的 `objectId`,所以在一个应用内是允许多个文件重名的。 -- 文件必须有扩展名才能被云端正确地识别出类型。比如说要用 `LCFile` 保存一个 PNG 格式的图像,那么扩展名应为 `.png`。 -- 如果文件没有扩展名,且没有手动指定类型,那么云服务将默认使用 `application/octet-stream`。 - -### 保存文件 - -将文件保存到云端后,便可获得一个永久指向该文件的 URL: - -```objc -[file uploadWithCompletionHandler:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"文件保存完成。URL: %@", file.url); - } else { - // 保存失败,可能是文件无法被读取,或者上传过程中出现问题 - } -}]; -``` - -文件上传后,可以在 `_File` class 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的 `objectId` 和 URL。 - - -已经保存到云端的文件可以关联到 `LCObject`: - -```objc -LCObject *todo = [LCObject objectWithClassName:@"Todo"]; -[todo setObject:@"买蛋糕" forKey:@"title"]; -// attachments 是一个 Array 属性 -[todo addObject:file forKey:@"attachments"]; -[todo saveInBackground]; -``` - -也可以通过构建 `LCQuery` 进行[查询](#查询): - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"_File"]; -``` - - -需要注意的是,内部文件(上传到文件服务的文件)的 `url` 字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 -因此,通过 url 字段查询文件仅适用于外部文件(直接保存外部 URL 到 `_File` 表创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。 - -注意,如果文件被保存到了 `LCObject` 的一个数组属性中,那么在查询 `LCObject` 时如果需要包含文件,则要用到 `LCQuery` 的 `includeKey` 方法。比如说,在获取所有标题为 `买蛋糕` 的 todo 的同时获取附件中的文件: - -```objc -// 获取同一标题且包含附件的 todo -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -[query whereKey:@"title" equalTo:@"买蛋糕"]; -[query whereKeyExists:@"attachments"]; - -// 同时获取附件中的文件 -[query includeKey:@"attachments"]; - -[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable todos, NSError * _Nullable error) { - for (LCObject *todo in todos) { - // 获取每个 todo 的 attachments 数组 - // {# TODO #} - } -}]; -``` - - -### 上传进度监听 - -上传过程中可以实时向用户展示进度: - -```objc -[file uploadWithProgress:^(NSInteger percent) { - // percent 是一个 0 到 100 之间的整数,表示上传进度 -} completionHandler:^(BOOL succeeded, NSError *error) { - // 保存后的操作 -}]; -``` - -### 文件元数据 - -上传文件时,可以用 `metaData` 添加额外的属性。文件一旦保存,`metaData` 便不可再修改。 - -```objc -// 设置元数据 -[file.metaData setObject:@"LeanCloud" forKey:@"author"]; -[file uploadWithCompletionHandler:^(BOOL succeeded, NSError *error) { - // 获取全部元数据 - NSDictionary *metadata = file.metaData; - // 获取 author 属性 - NSString *author = metadata[@"author"]; - // 获取文件名 - NSString *fileName = file.name; - // 获取大小(不适用于通过 base64 编码的字符串或者 URL 保存的文件) - NSUInteger *size = file.size; -}]; -``` - -### 文件下载 - -客户端 SDK 接口可以下载文件并把它缓存起来,只要文件的 URL 不变,那么一次下载成功之后,就不会再重复下载,目的是为了减少客户端的流量。 - -```objc -[file downloadWithProgress:^(NSInteger number) { - // 下载的进度数据,number 介于 0 和 100 -} completionHandler:^(NSURL * _Nullable filePath, NSError * _Nullable error) { - // filePath 是文件下载到本地的地址 -}]; -``` - -`filePath` 是一个相对路径,文件存储在缓存目录(使用缓存功能)或系统临时目录(不使用缓存功能)中。 - -请注意代码中 `下载进度` 数据的读取。 - -### 图像缩略图 - -成功保存图像后,除了可以获取指向该文件的 URL 外,还可以获取图像的缩略图 URL,并且可以指定缩略图的宽度和高度: - -```objc -LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"文件-url"]]; -[file getThumbnail:YES width:100 height:100 withBlock:^(UIImage *image, NSError *error) { - // 其他逻辑 -}]; -``` - -图片最大不超过 **20 MB** 才可以获取缩略图。 - -国际版不支持图片缩略图。 - -### 清除缓存 - -LCFile 也提供了清除缓存的方法: - -```objc -// 清除当前文件缓存 -- (void)clearPersistentCache; - -// 类方法,清除所有缓存 -+ (BOOL)clearAllPersistentCache; -``` - - -### 删除文件 - -下面的代码从云端删除一个文件: - -```objc -LCFile *file = [LCFile getFileWithObjectId:@"552e0a27e4b0643b709e891e"]; -[file deleteInBackground]; -``` - -默认情况下,文件的删除权限是关闭的,需要进入 **云服务控制台 > 数据存储 > 结构化数据 > `_File`**,选择 **其他** > **权限设置** > **`delete`** 来开启。 - -#### iOS 9 适配 - -从 iOS 9 开始,Apple 默认屏蔽 HTTP 访问,只支持 HTTPS 访问。云服务除了 `LCFile` 的 `getData` 之外的 API 都支持通过 HTTPS 访问。 - -如果你仍然需要 HTTP 访问,例如即时通讯消息中仍然有使用 HTTP 域名的文件,你可以 **为项目配置访问策略来允许 HTTP 访问**,从而解决这个问题。方法如下: - -选择项目的 `Info.plist`,右击选择 **Opened As** > **Source Code**,在 **plist** > **dict** 节点中加入以下文本: - -```xml -NSAppTransportSecurity - - NSExceptionDomains - - clouddn.com - - NSIncludesSubdomains - - NSTemporaryExceptionAllowsInsecureHTTPLoads - - - - -``` - -或者在 **Target** 的 **Info** 标签中修改配置: - -![在「NSAppTransportSecurity > NSExceptionDomains > clouddn.com」下面分别添加「NSTemporaryExceptionAllowsInsecureHTTPLoads」和「NSIncludesSubdomains」两个 Boolean 字段并将它们的值设为 YES。](/img/ios_qiniu_http.png) - -你也可以根据项目需要,允许所有的 HTTP 访问,更多可参考 [iOS 9 适配系列教程](https://github.com/ChenYilong/iOS9AdaptationTips)。 - - -## GeoPoint - -云服务允许你通过将 `LCGeoPoint` 关联到 `LCObject` 的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。 - -要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个 `LCGeoPoint` 并将其纬度(`latitude`)设为 `39.9`,经度(`longitude`)设为 `116.4`: - -```objc -LCGeoPoint *point = [LCGeoPoint geoPointWithLatitude:39.9 longitude:116.4]; -``` - - -现在可以将这个地理位置存储为一个对象的属性: - -```objc -[todo setObject:point forKey:@"location"]; -``` - -### 地理位置查询 - -给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 `LCQuery` 添加 `nearGeoPoint` 条件。下面的代码查找 `location` 属性值离某一点最近的 `Todo` 对象: - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -LCGeoPoint *point = [LCGeoPoint geoPointWithLatitude:39.9 longitude:116.4]; -[query whereKey:@"location" nearGeoPoint:point]; - -// 限制为 10 条结果 -query.limit = 10; -[query findObjectsInBackgroundWithBlock:^(NSArray *todos, NSError *error) { - // todos 是包含满足条件的 Todo 对象的数组 -}]; -``` - -像 `orderByAscending` 和 `orderByDescending` 这样额外的排序条件会获得比默认的距离排序更高的优先级。 - -若要限制结果和给定地点之间的距离,可以参考 API 文档中的 `withinKilometers`、`withinMiles` 和 `withinRadians` 参数。 - -若要查询在某一矩形范围内的对象,可以用 `withinGeoBoxFromSouthwest` 和 `toNortheast`: - -![withinGeoBox](/img/geopoint-withingeobox.svg) - -```objc -LCQuery *query = [LCQuery queryWithClassName:@"Todo"]; -LCGeoPoint *southwest = [LCGeoPoint geoPointWithLatitude:30 longitude:115]; -LCGeoPoint *northeast = [LCGeoPoint geoPointWithLatitude:40 longitude:118]; -[query whereKey:@"location" withinGeoBoxFromSouthwest:southwest toNortheast:northeast]; -``` - -### GeoPoint 的注意事项 - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -iOS 8.0 之后,使用定位服务之前,需要调用 `[locationManager requestWhenInUseAuthorization]` 或 `[locationManager requestAlwaysAuthorization]` 来获取用户的「使用期授权」或「永久授权」,而这两个请求授权需要在 `info.plist` 里面对应添加 `NSLocationWhenInUseUsageDescription` 或 `NSLocationAlwaysUsageDescription` 的键值对,值为开启定位服务原因的描述。SDK 内部默认使用的是「使用期授权」。 - - -## 用户 - -用户系统几乎是每款应用都要加入的功能,我们为此专门提供了一个 `LCUser` 类来方便应用使用各项用户管理的功能。 - -`LCUser` 是 `LCObject` 的子类,这意味着任何 `LCObject` 提供的方法也适用于 `LCUser`,唯一的区别就是 `LCUser` 提供一些额外的用户管理相关的功能。每个应用都有一个专门的 `_User` class 用于存放所有的 `LCUser`。 - -### 用户的属性 - -`LCUser` 相比一个普通的 `LCObject` 多出了以下属性: - -- `username`:用户的用户名。 -- `password`:用户的密码。 -- `email`:用户的电子邮箱。 -- `emailVerified`:用户的电子邮箱是否已验证。 -- `mobilePhoneNumber`:用户的手机号。 -- `mobilePhoneVerified`:用户的手机号是否已验证。 - -在接下来对用户功能的介绍中我们会逐一了解到这些属性。 - -### 注册 - -用户第一次打开应用的时候,可以让用户注册一个账户。下面的代码展示了一个典型的使用用户名和密码注册的流程: - -```objc -// 创建实例 -LCUser *user = [LCUser user]; - -// 等同于 [user setObject:@"Tom" forKey:@"username"] -user.username = @"Tom"; -user.password = @"cat!@#123"; - -// 可选 -user.email = @"tom@leancloud.rocks"; -user.mobilePhoneNumber = @"+8618200008888"; - -// 设置其他属性的方法跟 LCObject 一样 -[user setObject:@"secret" forKey:@"gender"]; - -[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 注册成功 - NSLog(@"注册成功。objectId:%@", user.objectId); - } else { - // 注册失败(通常是因为用户名已被使用) - } -}]; -``` - -新建 `LCUser` 的操作应使用 `signUpInBackground` 而不是 `saveInBackground`,但以后的更新操作就可以用 `saveInBackground` 了。 - -如果收到 `202` 错误码,意味着 `_User` 表里已经存在使用同一 `username` 的账号,此时应提示用户换一个用户名。除此之外,每个用户的 `email` 和 `mobilePhoneNumber` 也需要保持唯一性,否则会收到 `203` 或 `214` 错误。 -可以考虑在注册时把用户的 `username` 设为与 `email` 相同,这样用户可以直接 [用邮箱重置密码](#重置密码)。 - -采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码(云端对密码的长度、复杂度不作限制),并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调 **在客户端,应用切勿再次对密码加密,这会导致 [重置密码](#重置密码) 等功能失效**。 - -### 登录 - -下面的代码用用户名和密码登录一个账户: - -```objc -[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 登录失败(可能是密码错误) - } -}]; -``` - -#### 邮箱登录 - -下面的代码用邮箱和密码登录一个账户: - -```objc -[LCUser loginWithEmail:@"tom@leancloud.rocks" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 登录成功 - } else { - // 登录失败(可能是密码错误) - } -}]; -``` - -#### 单设备登录 - -某些场景下需要确保用户的账户在同一时间只在一台设备上登录,也就是说当用户在一台设备上登录后,其他设备上的会话全部失效。可以按照以下方案来实现: - -1. 新建一个专门用于记录用户登录信息和当前设备信息的 class。 -2. 每当用户在新设备上登录时,将该 class 中该用户对应的设备更新为该设备。 -3. 在另一台设备上打开客户端时,检查该设备是否与云端保存的一致。若不一致,则将用户 [登出](#当前用户)。 - -#### 账户锁定 - -输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 `{ "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." }`,开发者可在客户端进行必要提示。 - -锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 - -### 验证邮箱 - -可以通过要求用户在登录或使用特定功能之前验证邮箱的方式防止恶意注册。默认情况下,当用户注册或变更邮箱后,`emailVerified` 会被设为 `false`。在应用的 **云服务控制台 > 数据存储 > 用户 > 设置** 中,可以开启 **启用邮箱验证功能** 选项,这样当用户注册或变更邮箱时,会收到一封含有验证链接的邮件。在同一设置页面还可找到阻止未验证邮箱的用户登录的选项。 - -如果用户忘记点击链接并且在未来某一时刻需要进行验证,可以用下面的代码发送一封新的邮件: - -```objc -[LCUser requestEmailVerify:@"tom@leancloud.rocks"]; -``` - -用户点击邮件内的链接后,`emailVerified` 会变为 `true`。如果用户的 `email` 属性为空,则该属性永远不会为 `true`。 - -### 当前用户 - -用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户: - -```objc -LCUser *currentUser = [LCUser currentUser]; -if (currentUser != nil) { - // 跳到首页 -} else { - // 显示注册或登录页面 -} -``` - -会话信息会长期有效,直到用户主动登出: - -```objc -[LCUser logOut]; - -// currentUser 变为 nil -LCUser *currentUser = [LCUser currentUser]; -``` - -### 设置当前用户 - -用户登录后,云端会返回一个 **session token** 给客户端,它会由 SDK 缓存起来并用于日后同一 `LCUser` 的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个 `LCUser` 发起的请求了。 - -以下是一些应用可能需要用到 session token 的场景: - -- 应用根据以前缓存的 session token 登录(可以用 `[LCUser currentUser].sessionToken` 获取到当前用户的 session token,在服务端等受信任的环境下,可以通过 `Master Key` 读取任意用户的 `sessionToken` 字段以获取 session token)。 -- 应用内的某个 WebView 需要知道当前登录的用户。 -- 在服务端登录后,返回 session token 给客户端,客户端根据返回的 session token 登录。 - -下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效): - -```objc -[LCUser becomeWithSessionTokenInBackground:@"anmlwi96s381m6ca7o7266pzf" block:^(LCUser * _Nullable user, NSError * _Nullable error) { - if (user != nil) { - // 登录成功 - } else { - // session token 无效 - } -}]; -``` - -请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。 - -如果在 **控制台 > 存储 > 设置** 中勾选了 **密码修改后,强制客户端重新登录**,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到 `403 (Forbidden)` 错误。 - -下面的代码检查 session token 是否有效: - -```objc -LCUser *currentUser = [LCUser currentUser]; -NSString *token = currentUser.sessionToken; -[currentUser isAuthenticatedWithSessionToken:token callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // session token 有效 - } else { - // session token 无效 - } -}]; -``` - -### 重置密码 - -我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了多种重置密码的方法。 - -邮箱重置密码的流程如下: - -1. 用户输入注册的电子邮箱,请求重置密码; -2. 云端向该邮箱发送一封包含重置密码的特殊链接的电子邮件; -3. 用户点击重置密码链接后,一个特殊的页面会打开,让他们输入新密码; -4. 用户的密码已被重置为新输入的密码。 - -首先让用户填写注册账户时使用的邮箱,然后调用下面的方法: - -```objc -[LCUser requestPasswordResetForEmailInBackground:@"tom@leancloud.rocks"]; -``` - -上面的代码会查询 `_User` 表中是否有对象的 `email` 属性与前面提供的邮箱匹配。如果有的话,则向该邮箱发送一封密码重置邮件。之前提到过,应用可以让 `username` 与 `email` 保持一致,也可以单独收集用户的邮箱并将其存为 `email`。 - -密码重置邮件的内容可在应用的 **云服务云服务控制台 > 数据存储 > 用户 > 邮件模版** 中自定义。更多关于自定义邮件模板和验证链接的内容,请参考[《自定义邮件验证和重设密码页面》](https://leancloud.cn/docs/custom-reset-verify-page.html)。 - - -### 用户的查询 - -可以直接构建一个针对 `_User` 的 `LCQuery` 来查询用户: - -```objc -LCQuery *userQuery = [LCUser query]; -``` - - -为了安全起见,**新创建的应用的 `_User` 表默认关闭了 `find` 权限**,这样每位用户登录后只能查询到自己在 `_User` 表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 `find` 查询权限。除此之外,还可以在云引擎里封装用户查询相关的方法。 - -可以参见 [用户对象的安全](#用户对象的安全) 来了解 `_User` 表的一些限制,还可以阅读[数据和安全](/v2/sdk/storage/guide/security)来了解更多 class 级权限设置的方法。 - -### 关联用户对象 - -关联 `LCUser` 的方法和 `LCObject` 是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书: - -```objc -LCObject *book = [LCObject objectWithClassName:@"Book"]; -LCUser *author = [LCUser currentUser]; -[book setObject:@"我的第五本书" forKey:@"title"]; -[book setObject:author forKey:@"author"]; -[book saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - // 获取所有该作者写的书 - LCQuery *query = [LCQuery queryWithClassName:@"Book"]; - [query whereKey:@"author" equalTo:author]; - [query findObjectsInBackgroundWithBlock:^(NSArray *books, NSError *error) { - // books 是包含同一作者所有 Book 对象的数组 - }]; -}]; -``` - -### 用户对象的安全 - -`LCUser` 类自带安全保障,只有通过 `logInWithUsername` 或者 `signUpInBackground` 这种经过鉴权的方法获取到的 `LCUser` 才能进行保存或删除相关的操作,保证每个用户只能修改自己的数据。 - -这样设计是因为 `LCUser` 中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。 - -下面的代码展现了这种安全措施: - -```objc -[LCUser logInWithUsernameInBackground:@"Tom" password:@"cat!@#123" block:^(LCUser *user, NSError *error) { - if (user != nil) { - // 试图修改用户名 - [user setObject:@"Jerry" forKey:@"username")]; - // 密码已被加密,这样做会获取到空字符串 - NSString *password = user[@"password"]; - // 保存更改 - [user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 可以执行,因为用户已鉴权 - - // 绕过鉴权直接获取用户 - LCQuery *query = [LCQuery queryWithClassName:@"_User"]; - [query getObjectInBackgroundWithId:user.objectId block:^(LCObject *unauthenticatedUser, NSError *error) { - [unauthenticatedUser setObject:@"Toodle" forKey:@"username"]; - [unauthenticatedUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 无法执行,因为用户未鉴权 - } else { - // 操作失败 - } - }]; - }]; - } else { - // 错误处理 - } - }]; - } else { - // 错误处理 - } -}]; -``` - -通过 `[LCUser currentUser]` 获取的 `LCUser` 总是经过鉴权的。 - -要查看一个 `LCUser` 是否经过鉴权,可以调用 `isAuthenticatedWithSessionToken` 方法。通过经过鉴权的方法获取到的 `LCUser` 无需进行该检查。 - -注意,用户的密码只能在注册的时候进行设置,日后如需修改,只能通过 [重置密码](#重置密码) 的方式进行。密码不会被缓存在本地。如果尝试直接获取已登录用户的密码,会得到 `null`。 - -### 其他对象的安全 - -对于给定的一个对象,可以指定哪些用户有权限读取或修改它。为实现该功能,每个对象都有一个由 `LCACL` 对象组成的访问控制表。请参阅[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html)。 - -### 第三方账户登录 - -云服务支持应用层直接使用第三方社交平台(例如微信、微博、QQ 等)的账户信息来创建自己的账户体系并完成登录,也允许将既有账户与第三方账户绑定起来,这样终端用户后续可以直接用第三方账户信息来便捷登录。 - -例如以下的代码展示了终端用户使用微信登录的处理流程: - -```objc -NSDictionary *thirdPartyData = @{ - // 必须 - @"openid":@"OPENID", - @"access_token":@"ACCESS_TOKEN", - @"expires_in":@7200, - - // 可选 - @"refresh_token":@"REFRESH_TOKEN", - @"scope":@"SCOPE", - }; -LCUser *user = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = LeanCloudSocialPlatformWeiXin; -[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - -`LCUser#loginWithAuthData` 系列方法需要两个参数来唯一确定一个账户: - -- 第三方平台的名字,就是前例中的 `weixin`,该名字由应用层自己决定。 -- 第三方平台的授权信息,就是前例中的 `thirdPartyData`(一般包括 `uid`、`token`、`expires` 等信息,与具体的第三方平台有关)。 - -云端会使用第三方平台的鉴权信息来查询是否已经存在与之关联的账户。如果存在的话,则返回 `200 OK` 状态码,同时附上用户的信息(包括 [`sessionToken`](#设置当前用户))。如果第三方平台的信息没有和任何账户关联,客户端会收到 `201 Created` 状态码,意味着新账户被创建,同时附上用户的 `objectId`、`createdAt`、`sessionToken` 和一个自动生成的 `username`,例如: - -```json -{ - "username": "k9mjnl7zq9mjbc7expspsxlls", - "objectId": "5b029266fb4ffe005d6c7c2e", - "createdAt": "2018-05-21T09:33:26.406Z", - "updatedAt": "2018-05-21T09:33:26.575Z", - "sessionToken": "…", - // authData 通常不会返回,继续阅读以了解其中原因 - "authData": { - "weixin": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" - } - } - // … -} -``` - -这时候我们会看到 `_User` 表中出现了一条新的账户记录,账户中有一个名为 `authData` 的列,保存了第三方平台的授权信息。出于安全考虑,`authData` 不会被返回给客户端,除非它属于当前用户。 - -开发者需要自己完成第三方平台的鉴权流程(一般通过 OAuth 1.0 或 2.0),以获取鉴权信息,继而到云端来登录。 - -#### Signin With Apple -如果你需要开发 [Sigin With Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api),云服务可以帮你校验 `identityToken`,并获取 Apple 的 `access_token`。Apple Sign In 的 `authData` 结构如下: - -``` -{ - "lc_apple": { - "uid" : "从 Apple 获取到的 User Identifier", - "identity_token" : "从苹果获取到的 identityToken" - "code" : "从苹果获取到的 Authorization Code" - } -} -``` -`authData` 中的 key 的作用: - -* **`lc_apple`**:只有 platform 为 `lc_apple` 时,云服务才会执行 `identity_token` 和 `code` 的逻辑。 -* **`uid`**:必填。云服务通过 `uid` 判断是否存在用户。 -* **`identity_token`**:可选。`authData` 中有 `identity_token` 时云端会自动校验 `identity_token` 的有效性。开发者需要在云服务控制台「存储」-「用户」-「设置」-「第三方集成」中填写 Apple 的相关信息。 -* **`code`**:可选。`authData` 中有 `code` 时云端会自动用该 `code` 向 Apple 换取 `access_token` 和 `refresh_token`。开发者需要在云服务控制台「存储」-「用户」-「设置」-「第三方集成」中填写 Apple 的相关信息。 - -##### 获取 Client ID - -`Client ID` 用于校验 `identity_token` 及获取 `access_token`,指的是 Apple 应用的 identifier,也就是 `AppID` 或 `serviceID`。对于原生应用来说,指的是 Xcode 中的 Bundle Identifier,例如 `com.mytest.app`。详情请参考 [Apple 的文档](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)。 - -##### 获取 Private Key 及 Private Key ID - -Private Key 用于获取 `access_token`。登录 Apple 开发者平台,在左侧的 「Certificates, Identifiers & Profiles」 中选择 「Keys」,添加一个用于 Apple Sign In 的 Private Key,下载 XXXXX.p8 文件,同时在下载 Key 的页面获得 Private Key ID。详情请参考[ Apple 的文档](https://help.apple.com/developer-account/#/dev77c875b7e)。 - -将 Key ID 填写到控制台,将下载下来的 Private Key 文件上传到控制台。控制台只能上传 Private Key 文件,无法查看及下载其内容。 - -##### 获取 Team ID - -Team ID 用于获取 `access_token`。登录 Apple 开发者平台,在右上角或 Membership 页面即可看到自己所属开发团队的 Team ID。注意选择 Bundle ID 对应的 Team。 - -##### 使用 Apple Sign In 登录云服务 - -在控制台填写完成所有信息后,使用以下代码登录 - -```objc -NSDictionary *appleAuthData = @{ - // 必须 - @"uid":@"USER IDENTIFIER", - // 可选 - @"identity_token":@"IDENTITY TOKEN", - @"code":@"AUTHORIZATION CODE", - }; -LCUser *user = [LCUser user]; -[user loginWithAuthData:appleAuthData platformId:"lc_apple" options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; - -``` - - -#### 鉴权数据的保存 - -`_User` class 中的 `authData` 是一个以平台名为键名,鉴权信息为键值的 JSON 对象。 - -一个关联了微信账户的用户应该会有下列对象作为 `authData`: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - } -} -``` - -而一个关联了微博账户的用户,则会有如下的 `authData`: - -```json -{ - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx", - } -} -``` - -我们允许一个账户绑定多个第三方平台的鉴权数据,这样如果某个用户同时关联了微信和微博账户,则其 `authData` 可能会是这样的: - -```json -{ - "weixin": { - "openid": "…", - "access_token": "…", - "expires_in": 7200, - "refresh_token": "…", - "scope": "…" - } - "weibo": { - "refresh_token": "2.0xxx", - "uid": "271XFEFEW273", - "expires_in": 115057, - "access_token": "2.00xxx", - } -} -``` - -理解 `authData` 的数据结构至关重要。一个终端用户通过如下的鉴权信息来登录的时候, - -```json -"weixin": { - "openid": "OPENID", - "access_token": "ACCESS_TOKEN", - "expires_in": 7200, - "refresh_token": "REFRESH_TOKEN", - "scope": "SCOPE" -} -``` - -云端首先会查找账户系统(_User 表),看看是否存在 `authData.weixin.openid = "OPENID"` 的账户,如果存在,则返回现有账户,如果不存在那么就创建一个新账户,同时将上面的鉴权信息写入新账户的 `authData` 属性中,并将新账户的数据当成结果返回。 - -云端会自动为 `_User` class 中每个用户的 `authData..` 创建唯一索引,从而避免重复数据。 -`` 在微信等部分云服务内建支持的第三方平台上为 `openid` 字段,在其他第三方平台(包括部分云服务专门支持的第三方平台和所有云服务没有专门支持的第三方平台)上为 `uid` 字段。 - -#### 自动验证第三方平台授权信息 - -为了确保账户数据的有效性,云端还支持对部分平台的 `Access Token` 的有效性进行自动验证,以防止伪造账户数据。如果有效性验证不通过,云端会返回 `invalid authData` 错误,关联不会被建立。对于云端无法识别的服务,开发者需要自己去验证 `Access Token` 的有效性。 -比如,注册、登录时分别通过云引擎的 `beforeSave hook`、`beforeUpdate hook` 来验证 `Access Token` 有效性。 - -如果希望使用这一功能,则在开始使用前,需要在 **控制台 > 存储 > 用户 > 设置** 配置相应平台的 **应用 ID** 和 **应用 Secret Key**。 - -如果不希望云端自动验证 `Access Token`,可以在 **控制台 > 存储 > 设置** 里面取消勾选 **第三方登录时,验证用户 AccessToken 合法性**。 - -配置平台账号的目的在于创建 `LCUser` 时,云端会使用相关信息去校验请求参数 `thirdPartyData` 的合法性,确保 `LCUser` 实际对应着一个合法真实的用户,确保平台安全性。 - -#### 绑定第三方账户 - -用户已经有了 LCUser 并登录成功后,可以绑定新的第三方账号信息。 -绑定成功后,新的第三方账户信息会被添加到 LCUser 的 authData 字段里。 - -例如,下面的代码可以关联微信账户: - -```objc -[user associateWithAuthData:weixinData platformId:LeanCloudSocialPlatformWeiXin options:nil callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"成功"); - } else{ - NSLog(@"失败:%@",error.localizedFailureReason); - } -}]; -``` - - -为节省篇幅,上面的代码示例中没有给出具体的微信平台授权信息,相关内容请参考上面的[「第三方账户登录」](#第三方账户登录)一节。 - -#### 解除与第三方账户的关联 - -类似地,可以解绑第三方账户。 - -例如,下面的代码可以解除用户和微信账户的关联: - -```objc -[user disassociateWithPlatform:LeanCloudSocialPlatformWeiXin callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"成功"); - } else{ - NSLog(@"失败:%@",error.localizedFailureReason); - } -}]; -``` - - -#### 扩展:第三方登录时补充完整的用户信息 - -有些产品,新用户在使用第三方账号授权拿到相关信息后,仍然需要补充设置用户名、手机号、密码等重要信息后,才被允许登录成功。 - -这时要使用 `loginWithauthData` 登录接口的 `failOnNotExist` 参数并将其设置为 `true`。服务端会判断是否已存在能匹配上的 `authData`,如果不存在则会返回 `211` 错误码和 `Could not find user` 报错信息。开发者根据这个 `211` 错误码,跳转到要求输入用户名、密码、手机号等信息的页面,实例化一个 `LCUser` 对象,保存上述补充数据,再次调用 `loginWithauthData` 接口进行登录,并 **不再传入 `failOnNotExist` 参数**。示例代码如下: - -```objc -NSDictionary *thirdPartyData = @{ - @"access_token":@"ACCESS_TOKEN", - @"expires_in":@7200, - @"refresh_token":@"REFRESH_TOKEN", - @"openid":@"OPENID", - @"scope":@"SCOPE", - }; -LCUser *user = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = LeanCloudSocialPlatformWeiXin; -option.failOnNotExist = true; -[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // 你的逻辑 - } else if ([error.domain isEqualToString:kLeanCloudErrorDomain] && error.code == 211) { - // 不存在 thirdPartyData 的 LCUser 的实例,跳转到输入用户名、密码、手机号等业务页面 - } -}]; - -// 跳转到输入用户名、密码、手机号等业务页面之后 -LCUser *user = [LCUser user]; -user.username = @"Tom"; -user.mobilePhoneNumber = @"+8618200008888"; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = LeanCloudSocialPlatformWeiXin; -[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - - -#### 扩展:接入 UnionID 体系,打通不同子产品的账号系统 - -随着第三方平台的账户体系变得日渐复杂,它们的第三方鉴权信息出现了一些较大的变化。下面我们以最典型的微信开放平台为例来进行说明。 - -当一个用户在移动应用内登录微信账号时,会被分配一个 OpenID;在微信小程序内登录账号时,又会被分配另一个不同的 OpenID。这样的架构会导致的问题是,使用同一个微信号的用户,也无法在微信开发平台下的移动应用和小程序之间互通。 - -微信官方为了解决这个问题,引入了 `UnionID` 的体系,以下为其官方说明: - -> 通过获取用户基本信息接口,开发者可通过 OpenID 来获取用户基本信息,而如果开发者拥有多个公众号,可使用以下办法通过 UnionID 机制来在多公众号之间进行用户帐号互通。只要是同一个微信开放平台帐号下的公众号,用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID 是相同的。 - -其他平台,如 QQ 和微博,与微信的设计也基本一致。 - -云服务支持 `UnionID` 体系。你只需要给 `loginWithauthData` 和 `associateWithauthData` 接口传入更多的第三方鉴权信息,即可完成新 UnionID 体系的集成。新增加的第三方鉴权登录选项包括: - -- unionId,指第三方平台返回的 UnionId 字符串。 -- unionId platform,指 unionId 对应的 platform 字符串,由应用层自己指定,[后面](#该如何指定-unionIdPlatform)会详述。 -- asMainAccount,指示是否把当前平台的鉴权信息作为主账号来使用。如果作为主账号,那么就由当前用户唯一占有该 unionId,以后其他平台使用同样的 unionId 登录的话,会绑定到当前的用户记录上来;否则,当前应用的鉴权信息会被绑定到其他账号上去。 - -下面让我们通过一个例子来说明如何使用这些参数完成 UnionID 登录。 - -假设云服务在微信开放平台上有两个应用,一个是「云服务通讯」,一个是「云服务技术支持」,这两个应用在接入第三方鉴权的时候,分别使用了 `wxleanoffice` 和 `wxleansupport` 作为 platform 来进行登录。现在我们开启 UnionID 的用户体系,希望同一个微信用户在这两个应用中都能对应到同一个账户系统(_User 表中的同一条记录),同时我们决定将 `wxleanoffice` 平台作为主账号平台。 - -假设对于用户 A,微信给 ta 为 云服务分配的 UnionId 为 `unionid4a`,而对两个应用的授权信息分别为: - -```json -"wxleanoffice": { - "access_token": "officetoken", - "openid": "officeopenid", - "expires_in": 1384686496 -}, -"wxleansupport": { - "openid": "supportopenid", - "access_token": "supporttoken", - "expires_in": 1384686496 -} -``` - -现在,用户 A 在「云服务通讯」中通过微信登录,其调用请求为: - -```objc -NSDictionary *thirdPartyData = @{ - @"access_token":@"officetoken", - @"expires_in":@1384686496, - @"uid":@"officeopenid", - @"scope":@"SCOPE", - @"unionid":@"unionid4a" // 新增属性 - }; -LCUser *currentuser = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = @"weixin"; // 这里指定 UnionIdPlatform,使用「weixin」来指代微信平台。 -option.unionId = thirdPartyData[@"unionid"]; -option.isMainAccount = true; -[currentuser loginWithAuthData:thirdPartyData platformId:@"wxleanoffice" options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - - -> 注意代码中将微信传回来的 openid 属性改为了 uid,这是因为云端要求对于自定义的 platform,只能使用 uid 这样的属性名,才能保证自动建立 `authData..uid` 的唯一索引,具体可以参考[数据存储 REST API 使用详解](/v2/sdk/storage/guide/rest)的《连接用户账户和第三方平台》。 - -如果用户 A 是第一次在「云服务通讯」中通过微信登录,那么 _User 表中会增加一个新用户(假设其 objectId 为 `ThisIsUserA`),其 `authData` 的结果如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { // 新增键值对 - "uid": "unionid4a" - } -} -``` - -可以看到,与之前的第三方登录 API 相比,这里由于登录时指定了 `asMainAccount` 为 true,所以 authData 的第一级子目录中增加了 `_weixin_unionid` 的键值对,这里的 `weixin` 就是我们指定的 `unionIdPlatform` 的值。`_weixin_unionid` 这个增加的键值对非常重要,以后我们判断是否存在同样 UnionID 的账户就是依靠它来查找的,而是否增加这个键值对,则是由登录时指定的 `asMainAccount` 的值决定的: - -- 当 `asMainAccount` 为 true 时,云端会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,当前账号就会作为这一个 UnionID 对应的主账号被唯一确定。 -- 当 `asMainAccount` 为 false 时,云端不会在 `authData` 下面增加名为 `_{unionIdPlatform}_unionid` 的键值对,此时如果通过提供的 UnionID 可以找到主账号,则会将当前的鉴权信息合并进主账号的 `authData` 属性里,同时返回主账号对应的 _User 表记录;如果通过提供的 UnionID 找不到主账号,则会根据平台的 `openid` 去查找账户,找到匹配的账户就返回匹配的,找不到就新建一个账户,此时的处理逻辑与不使用 UnionID 时的逻辑完全一致。 - - -接下来,用户 A 继续在「云服务技术支持」中进行微信登录,其登录逻辑为: - -```objc -NSDictionary *thirdPartyData = @{ - @"access_token":@"supporttoken", - @"expires_in":@1384686496, - @"uid":@"supportopenid", - @"scope":@"SCOPE", - @"unionid":@"unionid4a" - }; -LCUser *currentuser = [LCUser user]; -LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new]; -option.platform = @"weixin"; // 这里指定 unionIdPlatform,使用「weixin」来指代微信平台。 -option.unionId = thirdPartyData[@"unionid"]; -option.isMainAccount = false; -[currentuser loginWithAuthData:thirdPartyData platformId:@"wxleansupport" options:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"登录成功"); - }else{ - NSLog(@"登录失败:%@",error.localizedFailureReason); - } -}]; -``` - - -与「云服务通讯」中的登录过程相比,在「云服务技术支持」这个应用中,我们在登录时只是将 `asMainAccount` 设为了 false。 这时我们看到,本次登录得到的还是 objectId 为 `ThisIsUserA` 的 _User 表记录(同一个账户),同时该账户的 `authData` 属性中发生了变化,多了 `wxleansupport` 的数据,如下: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - }, - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -在新的登录方式中,当一个用户以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录得到新的 `LCUser` 后,接下来这个用户以「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息登录时,云端判定是同样的 UnionID,就直接把来自 `wxleansupport` 的新用户数据加入到已有账户的 `authData` 里了,不会再创建新的账户。 - -这样一来,云端通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个 `LCUser` 上,实现互通。 - -##### 为 UnionID 建立索引 - -云端会为 UnionID 自动建立索引,不过因为自动创建基于请求的抽样统计,可能会滞后。 -因此,我们推荐自行创建相关索引,特别是用户量(`_User` 表记录数)很大的应用,更需要预先创建索引,否则用户使用 UnionID 账号登录时可能超时失败。 -以上面的微信 UnionID 为例,建议在控制台预先创建下列唯一索引(允许缺失值): - -- `authData.wxleanoffice.uid` -- `authData.wxleansupport.uid` -- `authData._weixin_unionid.uid` - -##### 该如何指定 unionIdPlatform - -从上面的例子可以看出,使用 UnionID 登录的时候,需要指定 `unionIdPlatform` 的主要目的,就是为了便于查找已经存在的唯一主账号。云端会在主账号对应账户的 `authData` 属性中增加一个 `_{unionIdPlatform}_unionid` 键值对来标记唯一性,终端用户在其他应用中登录的时候,云端会根据参数中提供的 `uniondId` + `unionIdPlatform` 的组合,在 `_User` 表中进行查找,这样来确定唯一的既存主账号。 - -本来 `unionIdPlatform` 的取值,应该是开发者可以自行决定的,但是 Javascript SDK 基于易用性的目的,在 `loginWithAuthDataAndUnionId` 之外,还额外提供了两个接口: - -- `LC.User.loginWithQQAppWithUnionId`,这里默认使用 `qq` 作为 `unionIdPlatform`。 -- `LC.User.loginWithWeappWithUnionId`,这里默认使用 `weixin` 作为 `unionIdPlatform`。 - -从我们的统计来看,这两个接口已经被很多开发者接受,在大量线上产品中产生着实际数据。所以为了避免数据在不同平台(例如 Android 和 iOS 应用)间发生冲突,建议大家统一按照 Javascript SDK 的默认值来设置 `unionIdPlatform`,即: - -- 微信平台的多个应用,统一使用 `weixin` 作为 `unionIdPlatform`; -- QQ 平台的多个应用,统一使用 `qq` 作为 `unionIdPlatform`; -- 微博平台的多个应用,统一使用 `weibo` 作为 `unionIdPlatform`; -- 除此之外的其他平台,开发者可以自行定义 `unionIdPlatform` 的名字,只要自己保证多个应用间统一即可。 - - -##### 主副应用不同登录顺序出现的不同结果 - -上面的流程是用户先登录了「云服务通讯」这个主应用,然后再登录「云服务技术支持」这个副应用,所以账号都被通过 UnionID 有效关联起来了。可能有人会想到另外一个问题,如果用户 B 先登录副应用,后登录主应用,这时候会发生什么事情呢? - -用户 B 首先登录副应用的时候,提供了「平台名为 `wxleansupport`、uid 为 `supportopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,并且指定「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 false」(与上面的调用完全一致),此时云端由于找不到存在的 UnionID,会新建一个 `LCUser` 对象,该账户 `authData` 结果为: - -```json -{ - "wxleansupport": { - "platform": "weixin", - "uid": "supportopenid", - "expires_in": 1384686496, - "main_account": false, - "access_token": "supporttoken", - "unionid": "unionid4a" - } -} -``` - -用户 B 接着又使用了主应用,ta 再次通过微信登录,此时以「平台名为 `wxleanoffice`、uid 为 `officeopenid`、UnionID 为 `unionid4a`」的第三方鉴权信息,以及「UnionIDPlatform 为 `weixin`、`asMainAccount` 为 true」的参数进行登录,此时云端由于找不到存在的 UnionID,会再次新建一个 `LCUser` 对象,该账户 `authData` 结果为: - -```json -{ - "wxleanoffice": { - "platform": "weixin", - "uid": "officeopenid", - "expires_in": 1384686496, - "main_account": true, - "access_token": "officetoken", - "unionid": "unionid4a" - }, - "_weixin_unionid": { - "uid": "unionid4a" - } -} -``` - -还有更复杂的情况。如果某公司的产品之前就接入了微信登录,产生了很多存量用户,并且分散在不同的子产品中,这时候怎么办?我们接下来专门讨论此时的解决方案。 - -##### 存量账户如何通过 UnionID 实现关联 - -还是以我们的两个子产品「云服务通讯」(后续以「产品 1」指代)和「云服务技术支持为例」(后续以「产品 2」指代),在接入 UnionID 之前,我们就接入了之前版本的微信平台登录,这时候账户系统内可能存在多种账户: - -- 只使用产品 1 的微信用户 A -- 只使用产品 2 的微信用户 B -- 同时使用两个产品的微信用户 C - -此时的存量账户表如下所示: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1) | N/A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1) | N/A -4 | UserC | openid4(对应产品 2) | N/A - -现在我们对两个子产品进行升级,接入 UnionID 体系。这时因为已经有同一个微信用户在不同子产品中创建了不同的账户(例如 objectId 为 3 和 4 的账户),我们需要确定以哪个平台的账号为主。比如决定使用「云服务通讯」上生成的账号为主账号,则在该应用程序更新版本时,使用 `asMainAccount=true` 参数。这个应用带着 UnionID 登录匹配或创建的账号将作为主账号,之后所有这个 UnionID 的登录都会匹配到这个账号。请注意这时 `_User` 表里会剩下一些用户数据,也就是没有被选为主账号的、其他平台的同一个用户的旧账号数据(例如 objectId 为 2 和 4 的账户)。这部分数据会继续服务于已经发布的但仍然使用 OpenID 登录的旧版应用。 - -接下来我们看一下,如果以产品 1 的账户作为「主账户」,按照前述的方式同时提供 openid/unionid 完成登录,则最后达到的结果是: - -1. 使用老版本的用户,不管在哪个产品里面,都可以和往常一样通过 openid 登录到正确的账户; -2. 使用产品 1 的新版本的老用户,通过 openid/unionid 组合,还是绑定到原来的账户。例如 UserC 在产品 1 中通过 openid3/unionId3 还是会绑定到 objectId=3 的账户(会增加 uniondId 记录);而 UserC 在产品 2 的新版本中,通过 openid4/unionId3 的组合则会绑定到 objectId=3 的账户,而不再是原来的 objectId=4 的账户。 -3. 使用产品 1 的新版本的新用户,通过 openid/unionid 组合,会创建新的账户;之后该用户再使用产品 2 的新版本,也会绑定到刚才创建的新账户上。 - -以上面的三个用户为例,他们分别升级到两个产品的最新版,且最终都会启用两个产品,则账户表的最终结果如下: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C -4 | UserC | openid4(对应产品 2) | N/A -5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B - -之后有新的用户 D,分别在两个产品的新版本中登录,则账户表中会增加一条新的 objectId=6 的记录,结果如下: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1)/openid6(对应产品 2) | unionId_user_A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1)/openid4(对应产品 2) | unionId_user_C -4 | UserC | openid4(对应产品 2) | N/A -5 | UserB | openid5(对应产品 1)/openid2(对应产品 2) | unionId_user_B -6 | UserD | openid7(对应产品 1)/openid8(对应产品 2) | unionId_user_D - -如果之后我们增加了新的子产品 3,这些用户在子产品 3 中也进行微信登录的话,那么四个用户还是会继续绑定到 objectId 为 1/3/5/6 的主账户。此时账户表的结果会变为: - -objectId | 微信用户 | authData.{platform} | authData._{platform}_unionid ------- | ------ | ------ | ------ -1 | UserA | openid1(对应产品 1)/openid6(对应产品 2)/openid9(对应产品 3) | unionId_user_A -2 | UserB | openid2(对应产品 2) | N/A -3 | UserC | openid3(对应产品 1)/openid4(对应产品 2)/openid10(对应产品 3) | unionId_user_C -4 | UserC | openid4(对应产品 2) | N/A -5 | UserB | openid5(对应产品 1)/openid2(对应产品 2)/openid11(对应产品 3) | unionId_user_B -6 | UserD | openid7(对应产品 1)/openid8(对应产品 2)/openid12(对应产品 3) | unionId_user_D - -### 匿名用户 - -将数据与用户关联需要首先创建一个用户,但有时你不希望强制用户在一开始就进行注册。使用匿名用户,可以让应用不提供注册步骤也能创建用户。下面的代码创建一个新的匿名用户: - -```objc -[LCUser loginAnonymouslyWithCallback:^(LCUser *user, NSError *error) { - // user 是新的匿名用户 -}]; -``` - - -可以像给普通用户设置属性那样给匿名用户设置 `username`、`password`、`email` 等属性,还可以通过走正常的注册流程来将匿名用户转化为普通用户。匿名用户能够: - -- [使用用户名和密码注册](#注册) -- [关联第三方平台](#第三方账户登录),比如微信 - -下面的代码为一名匿名用户设置用户名和密码: - -```objc -// currentUser 是个匿名用户 -LCUser *currentUser = [LCUser currentUser]; - -user.username = @"Tom"; -user.password = @"cat!@#123"; - -[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // currentUser 已经转化为普通用户 - } else { - // 注册失败(通常是因为用户名已被使用) - } -}]; -``` - - -下面的代码检查当前用户是否为匿名用户: - -```objc -LCUser *currentUser = [LCUser currentUser]; -if (currentUser.isAnonymous) { - // currentUser 是匿名用户 -} else { - // currentUser 不是匿名用户 -} -``` - - -如果匿名用户未能在登出前转化为普通用户,那么该用户将无法再次登录同一账户,且之前产生的数据也无法被取回。 - - -## 角色 - -随着用户量的增长,你可能会发现相比于为每一名用户单独设置权限,将预先设定好的权限直接分配给一部分用户是更好的选择。为了迎合这种需求,云服务支持基于角色的权限管理。请参阅[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html)。 -## 子类化 - -子类化推荐给进阶的开发者在进行代码重构的时候做参考。你可以用 `LCObject` 访问到所有的数据,用 `objectForKey:` 获取任意字段。在成熟的代码中,子类化有很多优势,包括降低代码量,具有更好的扩展性,和支持自动补全。 - -子类化是可选的,请对照下面的例子来加深理解: - -```objc -LCObject *student = [LCObject objectWithClassName:@"Student"]; -[student setObject:@"小明" forKey:@"name"]; -[student saveInBackground]; -``` - -可改写成: - -```objc -Student *student = [Student object]; -student.name = @"小明"; -[student saveInBackground]; -``` - -这样代码看起来是不是更简洁呢? - -### 子类化的实现 - -要实现子类化,需要下面几个步骤: - -1. 导入 `LCObject+Subclass.h`; -2. 继承 `LCObject` 并实现 `LCSubclassing` 协议; -3. 实现类方法 `parseClassName`,返回的字符串是原先要传给 `initWithClassName:` 的参数,这样后续就不必再进行类名引用了。如果不实现,默认返回的是类的名字。**请注意:`LCUser` 子类化后必须返回 `_User`**; -4. 在实例化子类之前调用 `[YourClass registerSubclass]`(**在应用当前生命周期中,只需要调用一次**。可在子类的 `+load` 方法或者 `UIApplication` 的 `-application:didFinishLaunchingWithOptions:` 方法里面调用)。 - -下面是实现 `Student` 子类化的例子: - -```objc -// Student.h -@interface Student : LCObject - -@property(nonatomic,copy) NSString *name; - -@end - - -// Student.m -#import "Student.h" - -@implementation Student - -@dynamic name; - -+ (NSString *)parseClassName { - return @"Student"; -} - -@end - - -// AppDelegate.m -#import "Student.h" - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { -[Student registerSubclass]; -[LCApplication setApplicationId:{{appid}} - clientKey:{{appkey}} - serverURLString:"https://please-replace-with-your-customized.domain.com"]; -} -``` - -### 属性 - -为 `LCObject` 的子类添加自定义的属性和方法,可以更好地将这个类的逻辑封装起来。用 `LCSubclassing` 可以把所有的相关逻辑放在一起,这样不必再使用不同的类来区分业务逻辑和存储转换逻辑了。 - -`LCObject` 支持动态 synthesizer,就像 `NSManagedObject` 一样。先正常声明一个属性,只是在 `.m` 文件中把 `@synthesize` 变成 `@dynamic`。 - -请看下面的例子是怎么添加一个「年龄」属性: - -```objc -// Student.h -@interface Student : LCObject - -@property int age; - -@end - - -// Student.m -#import "Student.h" - -@implementation Student - -@dynamic age; -``` - -这样就可以通过 `student.age = 19` 这样的方式来读写 `age` 字段了,当然也可以写成: - -```objc -[student setAge:19] -``` - -**注意:属性名称保持首字母小写**(错误:`student.Age`;正确:`student.age`)。 - -`NSNumber` 类型的属性可用 `NSNumber` 或者是它的原始数据类型(`int`、`long` 等)来实现。例如,`[student objectForKey:@"age"]` 返回的是 `NSNumber` 类型,而实际被设为 `int` 类型。 - -你可以根据自己的需求来选择使用哪种类型。原始类型更为易用,而 `NSNumber` 支持 `nil` 值,这可以让结果更清晰易懂。 - -注意:`LCRelation` 同样可以作为子类化的一个属性来使用,比如: - -```objc -@interface Student : LCUser -@property(retain) LCRelation *friends; -// ... -@end - -@implementation Student -@dynamic friends; -// ... -``` - -另外,值为 `Pointer` 的实例可用 `LCObject*` 来表示。例如,如果 `Student` 中 `bestFriend` 代表一个指向另一个 `Student` 的键,由于 `Student` 是一个 `LCObject`,因此在表示这个键的值的时候,可以用一个 `LCObject*` 来代替: - -```objc -@interface Student : LCUser -@property(nonatomic, strong) LCObject *bestFriend; - … -@end - -@implementation Student -@dynamic bestFriend; - … -``` - -提示:当需要更新的时候,最后都要记得加上 `[student save]` 或者对应的后台存储函数进行更新,才会同步至服务器。 - -如果要使用更复杂的逻辑而不是简单的属性访问,可以这样实现: - -```objc - @dynamic iconFile; - - - (UIImageView *)iconView { - UIImageView *view = [[UIImageView alloc] initWithImage:kPlaceholderImage]; - view.image = [UIImage imageNamed:self.iconFile]; - return [view autorelease]; - } - -``` - -### 针对 LCUser 子类化的特别说明 - -假如现在已经有一个基于 `LCUser` 的子类,如上面提到的 `Student`: - -```objc -@interface Student : LCUser -@property NSString *displayName; -@end - - -@implementation Student -@dynamic displayName; -+ (NSString *)parseClassName { - return @"_User"; -} -@end -``` - -登录时需要调用 `Student` 的登录方法才能通过 `currentUser` 得到这个子类: - -```objc -[Student logInWithUsernameInBackground:@"USER_NAME" password:@"PASSWORD" block:^(LCUser *user, NSError *error) { - Student *student = [Student currentUser]; - student.displayName = @"YOUR_DISPLAY_NAME"; - }]; -``` - -同样需要调用 `[Student registerSubclass];`,确保在其他地方得到的对象是 `Student`,而非 `LCUser`。 - -### 初始化子类 - -创建一个子类实例,要使用 `object` 类方法。要创建并关联到已有的对象,请使用 `objectWithObjectId:` 类方法。 - -### 子类查询 - -使用类方法 `query` 可以得到这个子类的查询对象。 - -例如,查询年龄小于 21 岁的学生: - -```objc - LCQuery *query = [Student query]; - [query whereKey:@"age" lessThanOrEqualTo:@(21)]; - [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { - if (!error) { - Student *stu1 = [objects objectAtIndex:0]; - // … - } - }]; -``` - -## 全文搜索 - -全文搜索是一个针对应用数据进行全局搜索的接口,它基于搜索引擎构建,提供更强大的搜索功能。要深入了解其用法和阅读示例代码,请阅读[全文搜索指南](/v2/sdk/storage/guide/fulltext-search)。 - diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/07-rest.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/07-rest.mdx deleted file mode 100644 index 9fabab828..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/07-rest.mdx +++ /dev/null @@ -1,2720 +0,0 @@ ---- -id: rest -title: 存储 REST API -sidebar_label: 存储 REST API ---- - - - -REST API 可以让你用任何支持发送 HTTP 请求的设备来与云服务进行交互,你可以使用 REST API 做很多事情,比如: - -* 使用任何编程语言操作云端数据。 -* 如果你不再需要使用云服务,你可以导出你所有的数据。 -* 一个追求最少化依赖库的应用可以不引入 SDK,直接访问 REST API 获取云服务上的数据。 -* 你可以批量新增大量数据,供移动应用之后读取。 -* 你可以下载最近的数据用于离线分析和归档备份。 - -## API 版本 - -当前的 API 版本是 `1.1`。 - -### 在线测试 - -为了方便测试 REST API,文档给出了 curl 命令示例,示例针对类 unix 平台(macOS、Linux 等)编写,直接粘贴至 Windows 平台 cmd.exe 很可能无法工作。 -例如,curl 命令示例中的 shell 换行符(`\`)在 cmd.exe 中是目录分隔符。 -Windows 平台建议使用 Postman 等客户端测试。 -Postman 可直接导入 curl 命令。 - -[Postman]: https://www.getpostman.com/ - -![Postman 中点击 Import 按钮,在「Paste Raw Text」标签中粘贴 curl 命令](/img/postman-import-curl.png) - -Postman 还支持自动生成多种语言(库)调用 REST API 的代码。 - -![Postman 中点击 code,在弹出对话框中选择语言(库)](/img/postman-generate-code.png) - -### Base URL - -REST API 请求的 Base URL 可以在**云服务控制台 > 设置 > 应用 Keys > 服务器地址**查看。 - -### 对象 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    URLHTTP功能
    /1.1/classes/<className>POST创建对象
    /1.1/classes/<className>/<objectId>GET获取对象
    /1.1/classes/<className>/<objectId>PUT更新对象
    /1.1/classes/<className>GET查询对象
    /1.1/classes/<className>/<objectId>DELETE删除对象
    /1.1/scan/classes/<className>GET按照特定顺序遍历 Class
    - -### 用户 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    URLHTTP功能
    /1.1/usersPOST用户注册
    用户连接
    /1.1/usersByMobilePhonePOST使用手机号码注册或登录
    /1.1/loginPOST用户登录
    /1.1/users/<objectId>GET获取用户
    /1.1/users/meGET根据 sessionToken 获取用户信息
    /1.1/users/<objectId>/refreshSessionTokenPUT重置用户 sessionToken。
    /1.1/users/<objectId>/updatePasswordPUT更新密码,要求输入旧密码。
    /1.1/users/<objectId>PUT更新用户
    用户连接
    验证 Email
    /1.1/usersGET查询用户
    /1.1/users/<objectId>DELETE删除用户
    /1.1/requestPasswordResetPOST请求密码重设
    /1.1/requestEmailVerifyPOST请求验证用户邮箱
    - -### 角色 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    URLHTTP功能
    /1.1/rolesPOST创建角色
    /1.1/roles/<objectId>GET获取角色
    /1.1/roles/<objectId>PUT更新角色
    /1.1/rolesGET查询角色
    /1.1/roles/<objectId>DELETE删除角色
    - -### 数据 Schema - - - - - - - - - - - - - - - - - - - - - -
    URLHTTP功能
    /1.1/schemasGET获取应用的所有 Class 的 Schema
    /1.1/schemas/<className>GET获取应用指定的 Class 的 Schema
    - -### 其他 API - - - - - - - - - - - - - - - - - - - - - - - - - - -
    URLHTTP功能
    /1.1/dateGET获得服务端当前时间
    /1.1/exportDataPOST请求导出应用数据
    /1.1/exportData/<id>GET获取导出数据任务状态和结果
    - -### 请求格式 - -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP header 的 Content-Type 需要设置为 `application/json`。 - -用户验证通过 HTTP header 来进行,**X-LC-Id** 标明正在运行的是哪个应用(应用的 `App ID`), -**X-LC-Key** 用来授权鉴定 endpoint: - -``` -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "更新一篇博客的内容"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -**X-LC-Key** 通常情况下是应用的 `App Key`, -有些情况(需要超级权限的操作)下是应用的 `Master Key`。 -当 **X-LC-Key** 值为 `Master Key` 时,需要在其后添加 `,master` 后缀以示区分,例如: - -``` -X-LC-Key: {{masterkey}},master -``` - -对于 JavaScript 使用,云服务支持跨域资源共享,所以你可以将这些 header 同 XMLHttpRequest 一同使用。 - -REST API 通讯支持 `gzip` 和 `brotli` 压缩,客户端可以通过指定相应的 `Accept-Encoding` HTTP 头开启压缩。 - -#### 更安全的鉴权方式 - -我们还支持一种新的 API 鉴权方式,即在 HTTP header 中使用 **X-LC-Sign** 来代替 **X-LC-Key**,以降低 `App Key` 的泄露风险。例如: - -``` -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" \ - -H "Content-Type: application/json" \ - -d '{"content": "在 HTTP header 中使用 X-LC-Sign 来更新一篇博客的内容"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -**X-LC-Sign** 的值是由 `sign,timestamp[,master]` 组成的字符串: - -| 取值 | 约束 | 描述 | -| --------- | ---- | ---------------------------------------- | -| sign | 必须 | 将 timestamp 加上 `App Key` 或 `Master Key` 组成的字符串,再对它做 MD5 签名后的结果。 | -| timestamp | 必须 | 客户端产生本次请求的 unix 时间戳(UTC),精确到**毫秒**。 | -| master | 可选 | 字符串 `"master"`,当使用 master key 签名请求的时候,必须加上这个后缀明确说明是使用 master key。 | - -举例来说,假设应用的信息如下: - - - - - - - - - - - - - - - - - - - - - - - - -
    App IdFFnN2hso42Wego3pWq4X5qlu
    App KeyUtOCzqb67d3sN12Kts4URwy8
    Master KeyDyJegPlemooo4X1tg94gQkw1
    请求时间2016-01-17 15:15:43.466 GMT+08:00
    timestamp1453014943466
    - -**使用 `App Key` 来计算 sign**: - -``` -md5( timestamp + App Key ) -= md5(1453014943466UtOCzqb67d3sN12Kts4URwy8) -= d5bcbb897e19b2f6633c716dfdfaf9be -``` - -```sh - -H "X-LC-Sign: d5bcbb897e19b2f6633c716dfdfaf9be,1453014943466" \ -``` - -**使用 `Master Key` 来计算 sign**: - -``` -md5( timestamp + Master Key ) -= md5(1453014943466DyJegPlemooo4X1tg94gQkw1) -= e074720658078c898aa0d4b1b82bdf4b -``` - -```sh - -H "X-LC-Sign: e074720658078c898aa0d4b1b82bdf4b,1453014943466,master" \ -``` - -(最后加上 **master** 来告诉服务器这个签名是使用 master key 生成的。) - -**使用 master key 将绕过所有权限校验,应该确保只在可控环境中使用,比如自行开发的管理平台,并且要完全避免泄露。** -以上两种计算 sign 的方法可以根据实际情况来选择一种使用。 - -#### 指定 hook 函数调用环境 - -请求可能触发云引擎的 hook 函数,可以通过设置 HTTP 头 `X-LC-Prod` 来区分调用的环境。 - -* `X-LC-Prod: 0` 表示调用预备环境 -* `X-LC-Prod: 1` 表示调用生产环境 - -默认(未指定 `X-LC-Prod` 头)调用生产环境的 hook 函数。 - -### 响应格式 - -对于所有的请求,响应格式都是一个 JSON 对象。 - -一个请求是否成功是由 HTTP 状态码标明的。一个 2XX 的状态码表示成功,而一个 4XX 表示请求失败。当一个请求失败时响应的主体仍然是一个 JSON 对象,但是总是会包含 `code` 和 `error` 这两个字段,你可以用它们来进行调试。举个例子,如果尝试用非法的属性名来保存一个对象会得到如下信息: - -```json -{ - "code":105, - "error":"Invalid key name. Keys are case-sensitive and 'a-zA-Z0-9_' are the only valid characters. The column is: 'invalid?'."} -``` - -## 对象 - -### 对象格式 - -数据存储服务是建立在 LCObject(对象)基础上的,每个 LCObject 包含若干属性值对(key-value,也称「键值对」),属性的值是与 JSON 格式兼容的数据。 -通过 REST API 保存对象需要将对象的数据通过 JSON 来编码。这个数据是无模式化的(schema free),这意味着你不需要提前标注每个对象上有哪些 key,你只需要随意设置键值对就可以,后端会保存它。 - -举个例子,假如我们要实现一个类似于微博的社交 App,主要有三类数据:账户、帖子、评论,一条微博帖子可能包含下面几个属性: - -```json -{ - "content": "每个 Java 程序员必备的 8 个开发工具", - "pubUser": "官方客服", - "pubTimestamp": 1435541999 -} -``` - -Key(属性名)必须是字母和数字组成的字符串,Value(属性值)可以是任何可以 JSON 编码的数据。 - -每个对象都有一个类名,你可以通过类名来区分不同的数据。例如,我们可以把微博的帖子对象称之为 Post。我们建议将类和属性名分别按照 `NameYourClassesLikeThis` 和 `nameYourKeysLikeThis` 这样的惯例来命名,即区分第一个字母的大小写,这样可以提高代码的可读性和可维护性。 - -当你从云端获取对象时,一些字段会被自动加上,如 createdAt、updatedAt 和 objectId。这些字段的名字是保留的,值也不允许修改。我们上面设置的对象在获取时应该是下面的样子: - -```json -{ - "content": "每个 Java 程序员必备的 8 个开发工具", - "pubUser": "官方客服", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -createdAt 和 updatedAt 都是 UTC 时间戳,以 ISO 8601 标准和毫秒级精度储存:`YYYY-MM-DDTHH:MM:SS.MMMZ`。objectId 是一个字符串,在类中可以唯一标识一个实例。 -在 REST API 中,class 级的操作都是通过一个带类名的资源路径(URL)来标识的。例如,如果类名是 Post,那么 class 的 URL 就是: - -``` -https://{{host}}/1.1/classes/Post -``` - -对于**用户账户**这种对象,有一个特殊的 URL: - -``` -https://{{host}}/1.1/users -``` - -针对于一个特定的对象的操作可以通过组织一个 URL 来做。例如,对 Post 中的一个 objectId 为 `558e20cbe4b060308e3eb36c` 的对象的操作应使用如下 URL: - -``` -https://{{host}}/1.1/classes/Post/558e20cbe4b060308e3eb36c -``` - -### 创建对象 - -创建一个新的对象,应该向 class 的 URL 发送一个 **POST** 请求,其中应该包含对象本身。 -例如,要创建如上所说的对象: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "每个 Java 程序员必备的 8 个开发工具","pubUser": "官方客服","pubTimestamp": 1435541999}' \ - https://{{host}}/1.1/classes/Post -``` - -当创建成功时,HTTP 的返回是 **201 Created**,而 header 中的 Location 表示新的 object 的 URL: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/classes/Post/ -``` - -响应的主体是一个 JSON 对象,包含新的对象的 objectId 和 createdAt 时间戳。 - -```json -{ - "createdAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -如果希望返回新创建的对象的完整信息,可以在 URL 里加上 `fetchWhenSave` 选项,并且设置为 true: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "每个 Java 程序员必备的 8 个开发工具","pubUser": "官方客服","pubTimestamp": 1435541999}' \ - https://{{host}}/1.1/classes/Post?fetchWhenSave=true -``` - -**每个应用最多可以创建 500 个 class,每个 class 最多包含 300 个字段,**但每个 class 中的记录数量没有限制。 - -### 获取对象 - -当你创建了一个对象时,你可以通过发送一个 GET 请求到返回的 header 的 Location 以获取它的内容。例如,为了得到我们上面创建的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/classes/Post/ -``` - -返回的主体是一个 JSON 对象包含所有用户提供的 field 加上 createdAt、updatedAt 和 objectId 字段: - -```json -{ - "content": "每个 Java 程序员必备的 8 个开发工具", - "pubUser": "官方客服", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -当获取的对象有指向其子对象的指针时,你可以加入 `include` 选项来获取这些子对象。假设微博记录中有一个字段 `author` 来指向发布者的账户信息,按上面的例子,可以这样来连带获取发布者完整信息: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'include=author' \ - https://{{host}}/1.1/classes/Post/ -``` - -`include` 支持点号,例如,假定发布者有一个字段 `department` 指向发布者所属的部门,那么可以使用 `include=author.department` 一并获取部门信息。 - -类不存在时,返回 404 Not Found 错误: - -```json -{ - "code": 101, - "error": "Class or object doesn't exists." -} -``` - -objectId 不存在时,返回一个空对象(HTTP 状态码为 200 OK): - -```json -{} -``` - -某些特殊的系统内置类(类名以下划线开头),objectId 不存在时不一定返回空对象。 -例如,查询 `_User` 时,objectId 不存在会返回 400 Bad Request 错误: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/classes/_User/ -``` - -返回: - -```json -{ - "code": 211, - "error": "Could not find user." -} -``` - -顺带提一下,获取用户推荐使用 `GET /users/`,而不是直接查询 `_User` 类。 -参见后文[获取用户](#获取用户)一节。 - -### 更新对象 - -为了更改一个对象已经有的数据,你可以发送一个 PUT 请求到对象相应的 URL 上,任何你未指定的 key 都不会更改,所以你可以只更新对象数据的一个子集。例如,我们来更改我们对象的一个 content 字段: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"content": "每个 JavaScript 程序员必备的 8 个开发工具:http://buzzorange.com/techorange/2015/03/03/9-javascript-ide-editor/"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -返回的 JSON 对象只会包含一个 updatedAt 字段,表明更新发生的时间: - -```json -{ - "updatedAt": "2015-06-30T18:02:52.248Z" -} -``` - -fetchWhenSave 选项对更新对象也同样有效。 -但和创建对象不同,用于更新对象时仅返回更新的字段,而非全部字段。 - -#### 计数器 - -比如一条微博,我们需要记录有多少人喜欢或者转发了它,但可能很多次喜欢都是同时发生的,如果每个客户端都直接把读到的计数值更改之后再写回去,那么极容易引发冲突和覆盖,导致最终结果不准。 -云服务提供了对数字类型字段进行原子增加或者减少的功能,稳妥地实现对计数器类型数据的更新: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"upvotes":{"__op":"Increment","amount":1}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -这样就将对象的 **upvotes** 属性值(被用户点赞的次数)加上 1,其中 **amount** 为递增的数字大小,如果为负数,则为递减。 - -除了 Increment,我们也提供了 Decrement 用于递减,等价于 Increment 一个负数。 - -注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。 -因此,需要原子增减的字段建议使用整数以避免误差,例如 `3.14` 可以存储为 `314`,然后在客户端进行相应的转换。 -否则,以比较大小为条件查询对象的时候,需要特殊处理, -`< a` 需改查 `< a + e`,`> a` 需改查 `> a - e`,`== a` 需改查 `> a - e` 且 `< a + e`,其中 `e` 为误差范围,据所需精度取值,比如 `0.0001`。 - -#### 位运算 - -如果数据表的某一列是整型,可以使用位运算操作符该列进行原子的位运算: - -* BitAnd 与运算 -* BitOr 或运算 -* BitXor 异或运算 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"flags":{"__op":"BitOr","value": 0x0000000000000004}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -#### 数组 - -有 3 种原子性操作可用于存储和更改数组类型的字段: - -* **Add**:在一个数组字段的后面添加一些指定的对象(包装在一个数组内) -* **AddUnique**:只会在数组内原本没有这个对象的情形下才会添加入数组,插入的位置不定。 -* **Remove**:从一个数组内移除所有的指定的对象 - -每种操作都有一个 key `objects`,其值为被添加或删除的对象列表。例如为每条微博增加一个「标签」属性 tags,然后往里面加入一些值: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"tags":{"__op":"AddUnique","objects":["Frontend","JavaScript"]}}' \ - https://{{host}}/1.1/classes/Post/ -``` -#### 有条件更新对象 - -假设从某个账户对象 Account 的余额中扣除一定金额,但是要求余额要大于等于被扣除的金额才允许操作,那么就需要通过 `where` 参数为更新操作加上限定条件 `balance >= amount`: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"balance":{"__op":"Decrement","amount": 30}}' \ - "https://{{host}}/1.1/classes/Account/558e20cbe4b060308e3eb36c?where=%7B%22balance%22%3A%7B%22%24gte%22%3A%2030%7D%7D" -``` - -URL 中 where 参数的值是 `%7B%22balance%22%3A%7B%22%24gte%22%3A%2030%7D%7D`,其实这是 `{"balance":{"$gte": 30}}` 被 URL 编码后的结果。 - -如果条件不满足,更新将失败,同时返回错误码 `305`: - - -```json -{ - "code" : 305, - "error": "No effect on updating/deleting a document." -} -``` - -**特别强调:where 一定要作为 URL 的 Query Parameters 传入。** - -#### __op 操作汇总 - -使用 `__op("操作名称", {JSON 参数})` 函数可以完成原子性操作,确保数据的一致性。 - -操作 | 说明 | 示例 ----|---|--- -Delete | 删除对象的一个属性 | `__op('Delete', {'delete': true})` -Add | 在数组末尾添加对象 | `__op('Add',{'objects':['Apple','Google']})` -AddUnique | 在数组末尾添加不会重复的对象,插入位置不定。| `__op('AddUnique', {'objects':['Apple','Google']})` -Remove | 从数组中删除对象 | `__op('Remove',{'objects':['Apple','Google']})` -AddRelation | 添加一个关系 | `__op('AddRelation', {'objects':[pointer('_User','558e20cbe4b060308e3eb36c')]})` -RemoveRelation | 删除一个关系 | `__op('RemoveRelation', {'objects':[pointer('_User','558e20cbe4b060308e3eb36c')]})` -Increment | 递增 | `__op('Increment', {'amount': 50})` -Decrement | 递减 | `__op('Decrement', {'amount': 50})` -BitAnd | 与运算 | `__op('BitAnd', {'value': 0x0000000000000004})` -BitOr | 或运算 | `__op('BitOr', {'value': 0x0000000000000004})` -BitXor | 异或运算 | `__op('BitXor', {'value': 0x0000000000000004})` - -### 删除对象 - -要在云端删除一个对象,可以发送一个 DELETE 请求到指定的对象的 URL,比如: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/classes/Post/ -``` - -还可以使用 Delete 操作删除一个对象的一个字段(注意此时** HTTP Method 还是 PUT**): - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"downvotes":{"__op":"Delete"}}' \ - https://{{host}}/1.1/classes/Post/ -``` - -#### 有条件删除对象 - -为请求增加 `where` 参数即可以按指定的条件来删除对象。例如删除点击量 clicks 为 0 的帖子: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - "https://{{host}}/1.1/classes/Post/?where=%7B%22clicks%22%3A%200%7D" -``` - -URL 中 where 参数的值是 `%7B%22clicks%22%3A%200%7D`,其实这是 `{"clicks": 0}` 被 URL 编码后的结果。 - -如果条件不满足,删除将失败,同时返回错误码 `305`: - -```json -{ - "code" : 305, - "error": "No effect on updating/deleting a document." -} -``` - -**特别强调:where 一定要作为 URL 的 Query Parameters 传入。** - -### 遍历 Class - -因为更新和删除都是基于单个对象的,都要求提供 objectId,但是有时候用户需要高效地遍历一个 Class,做一些批量的更新或者删除的操作。 - -通常情况下,如果 Class 的数量规模不大,使用查询加上 `skip` 和 `limit` 分页配合排序 `order` 就可以遍历所有数据。但是当 Class 数量规模比较大的时候, `skip` 的效率就非常低了(这跟 MySQL 等关系数据库的原因一样,深度翻页比较慢),因此我们提供了 `scan` 协议,可以按照特定字段排序来高效地遍历一张表,默认这个字段是 `objectId` 升序,同时支持设置 `limit` 限定每一批次的返回数量,默认 limit 为 100,最大可设置为 1000: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - https://{{host}}/1.1/scan/classes/Article -``` - -`scan` 强制要求使用 master key。 - -返回: - -```json -{ - "results": - [ - { - "tags" : ["clojure","\u7b97\u6cd5"], - "createdAt": "2016-07-07T08:54:13.250Z", - "updatedAt": "2016-07-07T08:54:50.268Z", - "title" : "clojure persistent vector", - "objectId" : "577e18b50a2b580057469a5e" - }, - ... - ], - "cursor": "pQRhIrac3AEpLzCA"} -``` - -其中 `results` 对应的就是返回的对象列表,而 `cursor` 表示本次遍历当前位置的「指针」,当 `cursor` 为 null 的时候,表示已经遍历完成,如果不为 null,请继续传入 `cursor` 到 `scan` 接口就可以从上次到达的位置继续往后查找: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'cursor=pQRhIrac3AEpLzCA' \ - https://{{host}}/1.1/scan/classes/Article -``` - -每次返回的 `cursor` 的有效期是 10 分钟。 - -遍历还支持过滤条件,加入 where 参数: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'where={"score": 100}' \ - https://{{host}}/1.1/scan/classes/Article -``` - -默认情况下系统按 `objectId` 升序排序,增加 `scan_key` 参数可以使用其他字段来排序: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'scan_key=score' \ - https://{{host}}/1.1/scan/classes/Article -``` - -scan_key 也支持倒序,前面加个减号即可,例如 `-score`。 - -**自定义的 scan_key 需要满足严格单调递增的条件,并且 scan_key 不可作为 where 查询条件存在。** - -### 批量操作 - -为了减少网络交互的次数太多带来的时间浪费,你可以在一个请求中对多个对象进行 create、update、delete 操作。 - -在一个批次中每一个操作都有相应的方法、路径和主体,这些参数可以代替你通常会使用的 HTTP 方法。这些操作会以发送过去的顺序来执行,比如我们要一次发布一系列的微博: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "requests": [ - { - "method": "POST", - "path": "/1.1/classes/Post", - "body": { - "content": "2021 年 5 月 1 日至 2021 年 5 月 5 日放假五天,5 月 8 日调休正常上班。", - "pubUser": "官方客服" - } - }, - { - "method": "POST", - "path": "/1.1/classes/Post", - "body": { - "content": "我们将于 2021 年 2 月 10 日至 2021 年 2 月 17 日放假八天,2 月 18 日恢复正常工作,放假期间,运维团队仍将在线值班,以应对可能的突发情况,保障服务稳定。", - "pubUser": "官方客服" - } - } - ] - }' \ - https://{{host}}/1.1/batch -``` - -我们对每一批次中所包含的操作数量(requests 数组中的元素个数)暂不设限,但考虑到云端对每次请求的 body 内容大小有 20 MB 的限制,因此建议将每一批次的操作数量控制在 100 以内。 - -批量操作的响应 body 会是一个列表,列表的元素数量和顺序与给定的操作请求是一致的。每一个在列表中的元素都有一个字段是 success 或者 error。 - -``` -[ - { - "error": { - "code": 1, - "error": "Could not find object by id '558e20cbe4b060308e3eb36c' for class 'Post'." - } - }, - { - "success": { - "updatedAt": "2017-02-22T06:35:29.419Z", - "objectId": "58ad2e850ce463006b217888" - } - } -] -``` - -需要注意,即使一个 batch 请求返回的响应码为 200,这仅代表服务端已收到并处理了这个请求,但并不说明该 -batch 中的所有操作都成功完成,只有当返回 body 的列表中**不存在 error 元素**,开发者才可以认为所有操作都已成功完成。 - -在 batch 操作中 update 和 delete 同样是有效的: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "requests": [ - { - "method": "PUT", - "path": "/1.1/classes/Post/55a39634e4b0ed48f0c1845b", - "body": { - "upvotes": 2 - } - }, - { - "method": "DELETE", - "path": "/1.1/classes/Post/55a39634e4b0ed48f0c1845c" - } - ] - }' \ - https://{{host}}/1.1/batch -``` - -批量操作还有一个冷门用途,代替 URL 过长的 GET(比如使用 containedIn 等方法构造查询)和 DELETE (比如批量删除)请求,以绕过服务端和某些客户端对 URL 长度的限制。 - -### 数据类型 - -到现在为止我们只使用了可以被标准 JSON 编码的值,客户端 SDK 同样支持日期、二进制数据和关系型数据。在 REST API 中,这些值都被编码了,同时有一个 `__type` 字段(注意:**前缀是两个下划线**)来标示出它们的类型,所以如果你采用正确的编码的话就可以读或者写这些字段。 - -**Date** 类型包含了一个 iso 字段,其值是一个 UTC 时间戳,以 ISO 8601 格式和毫秒级的精度来存储的时间值,格式为:`YYYY-MM-DDTHH:MM:SS.MMMZ`: - -```json -{ - "__type": "Date", - "iso": "2015-06-21T18:02:52.249Z" -} -``` - -Date 和内置的 createdAt 字段和 updatedAt 字段相结合的时候特别有用,举个例子:为了找到在一个特殊时间发布的微博,只需要将 Date 编码后放在使用了比较条件的查询里面: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"createdAt":{"$gte":{"__type":"Date","iso":"2015-06-21T18:02:52.249Z"}}}' \ - https://{{host}}/1.1/classes/Post -``` - -注意,由于 createdAt 和 updatedAt 属于保留字段,因此通过 REST API 请求这两个字段时,将直接返回 UTC 时间戳字符串。 - -**Byte** 类型包含了一个 base64 字段,这个字段是一些二进制数据编码过的 base64 字符串。base64 是 MIME 使用的标准,不包含空白符: - -```json -{ - "__type": "Bytes", - "base64": "5b6I5aSa55So5oi36KGo56S65b6I5Zac5qyi5oiR5Lus55qE5paH5qGj6aOO5qC877yM5oiR5Lus5bey5bCGIExlYW5DbG91ZCDmiYDmnInmlofmoaPnmoQgTWFya2Rvd24g5qC85byP55qE5rqQ56CB5byA5pS+5Ye65p2l44CC" -} -``` - -**Pointer** 类型是用来设定 LCObject 作为另一个对象的值时使用的,它包含了 className 和 objectId 两个属性值,用来提取目标对象: - -```json -{ - "__type": "Pointer", - "className": "Post", - "objectId": "55a39634e4b0ed48f0c1845c" -} -``` - -指向用户对象的 Pointer 的 className 为 `_User`,前面加一个下划线表示开发者不能定义的类名,而且所指的类是内置的。 -类似地,指向角色和 Installation 的 Pointer 的 className 为 `_Role` 和 `_Installation`。 - -然而,关联文件(可以看成指向文件的 Pointer)采用不同于 Pointer 的编码形式: - -```json -{ - "id": "543cbaede4b07db196f50f3c", - "__type": "File" -} -``` - -**GeoPoint** 包含地理位置的经纬度: - -```json -{ - "__type": "GeoPoint", - "latitude": 39.9, - "longitude": 116.4 -} -``` - -当更多的数据类型被加入的时候,它们都会采用 hashmap 加上一个 `__type` 字段的形式,所以你不应该使用 `__type` 作为你自己的 JSON 对象的 key。 - -## 查询 - -### 基础查询 - -通过发送一个 GET 请求到类的 URL 上,不需要任何 URL 参数,你就可以一次获取多个对象。下面就是简单地获取所有微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/classes/Post -``` - -返回的值就是一个 JSON 对象包含了 results 字段,它的值就是对象的列表: - -```json -{ - "results": [ - { - "content": "2021 年 5 月 1 日至 2021 年 5 月 5 日放假五天,5 月 8 日调休正常上班。", - "pubUser": "官方客服", - "upvotes": 2, - "createdAt": "2015-06-29T03:43:35.931Z", - "objectId": "55a39634e4b0ed48f0c1845b" - }, - { - "content": "我们将于 2021 年 2 月 10 日至 2021 年 2 月 17 日放假八天,2 月 18 日恢复正常工作,放假期间,运维团队仍将在线值班,以应对可能的突发情况,保障服务稳定。", - "pubUser": "官方客服", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" - } - ] -} -``` - -控制台对 `createdAt` 和 `updatedAt` 的展示做了优化,它们会依据用户操作系统时区而显示为本地时间;客户端 SDK 获取到这些时间后也会将其转换为本地时间;而通过 REST API 获取到的则是原始的 UTC 时间,开发者可能需要根据情况做相应的时区转换。 - -### 查询约束 - -通过 `where` 参数的形式可以对查询对象做出约束。 - -`where` 参数的值应该是 JSON 编码过的。就是说,如果你查看真正被发出的 URL 请求,它应该是先被 JSON 编码过,然后又被 URL 编码过。最简单的使用 `where` 参数的方式就是包含应有的 key 和 value。例如,如果我们想要看到「官方客服」发布的所有微博,我们应该这样构造查询: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"pubUser":"官方客服"}' \ - https://{{host}}/1.1/classes/Post -``` - -除了完全匹配一个给定的值以外,`where` 也支持比较的方式,而且它还支持对 key 的一些 hash 操作,比如包含。`where` 参数支持如下选项: - -| Key | Operation | -| ------------- | ------------------------ | -| `$ne` | 不等于 | -| `$lt` | 小于 | -| `$lte` | 小于等于 | -| `$gt` | 大于 | -| `$gte` | 大于等于 | -| `$regex` | 正则表达式。`$options` 指定全局修饰符 | -| `$in` | 包含任意一个数组值 | -| `$nin` | 不包含任意一个数组值 | -| `$all` | 包括所有的数组值 | -| `$exists` | 指定 Key 有值 | -| `$select` | 匹配另一个查询的返回值 | -| `$dontSelect` | 排除另一个查询的返回值 | - -例如获取在 **2015-06-29** 当天发布的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"createdAt":{"$gte":{"__type":"Date","iso":"2015-06-29T00:00:00.000Z"},"$lt":{"__type":"Date","iso":"2015-06-30T00:00:00.000Z"}}}' \ - https://{{host}}/1.1/classes/Post -``` - -求点赞次数少于 10 次,且该次数还是奇数的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"upvotes":{"$in":[1,3,5,7,9]}}' \ - https://{{host}}/1.1/classes/Post -``` - -获取不是「官方客服」发布的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"pubUser":{"$nin":["官方客服"]}}' \ - https://{{host}}/1.1/classes/Post -``` - -获取有人喜欢的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"upvotes":{"$exists":true}}' \ - https://{{host}}/1.1/classes/Post -``` - -获取没有被人喜欢过的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"upvotes":{"$exists":false}}' \ - https://{{host}}/1.1/classes/Post -``` - -微博有用户互相关注的功能,如果我们用 `_Followee`(用户关注的人) 和 `_Follower`(用户的粉丝) 这两个类来存储用户之间的关注关系,我们可以创建一个查询来找到某个用户关注的人发布的微博(`Post` 表中有一个字段 `author` 指向发布者): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={ - "author": { - "$select": { - "query": { - "className":"_Followee", - "where": { - "user":{ - "__type": "Pointer", - "className": "_User", - "objectId": "55a39634e4b0ed48f0c1845c" - } - } - }, - "key":"followee" - } - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -`order` 参数指定一个字段的排序方式,前面加一个负号表示逆序。返回 Post 记录并按发布时间升序排列: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=createdAt' \ - https://{{host}}/1.1/classes/Post -``` - -降序排列: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=-createdAt' \ - https://{{host}}/1.1/classes/Post -``` - -对多个字段进行排序,要使用逗号分隔的列表。将 Post 以 createdAt 升序和 pubUser 降序进行排序: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=createdAt,-pubUser' \ - https://{{host}}/1.1/classes/Post -``` - -你可以用 `limit` 和 `skip` 来做分页。`limit` 的默认值是 100,任何 1 到 1000 之间的值都是可选的,在 1 到 1000 范围之外的都强制转成默认的 100。比如为了获取排序在 400 到 600 之间的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'limit=200' \ - --data-urlencode 'skip=400' \ - https://{{host}}/1.1/classes/Post -``` - -你可以限定返回的字段通过传入 `keys` 参数和一个逗号分隔列表。为了返回对象只包含 `pubUser` 和 `content` 字段(还有特殊的内置字段比如 objectId、createdAt 和 updatedAt): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'keys=pubUser,content' \ - https://{{host}}/1.1/classes/Post -``` - -`keys` 的参数中可以使用点号进一步限定只返回字段的部分属性,例如,只返回发布者的姓氏:`keys=pubUser.familyName`。 - -`keys` 还支持反向选择,也就是不返回某些字段,字段名前面加个减号即可,比如我不想查询返回 `author`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'keys=-author' \ - https://{{host}}/1.1/classes/Post -``` - -反向选择同样适用于内置字段,比如 `keys=-createdAt,-updatedAt,-objectId`。 -另外反向选择同样可以和点号组合使用,例如 `keys=-pubUser.createdAt,-pubUser.updatedAt`。 - -### 返回 ACL 字段 - -默认情况下不会返回 ACL 字段。 -**云服务控制台 > 存储 > 服务设置 > 查询设置** 中勾选 **查询时返回值包括 ACL**,且指定了 `returnACL=true` 时返回结果中才会包含 ACL 字段。 - -所有以上这些参数都可以组合使用。 - -### 正则查询 - -获取标题以大写「WTO」开头的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -G \ - --data-urlencode 'where={"title":{"$regex":"^WTO.*","$options":"i"}}' \ - https://{{host}}/1.1/classes/Post -``` - -我们使用以下数据来演示如何使用 `$options` 匹配 **title** 字段值: - -``` -{ "_id" : 100, "title" : "Single line description." }, -{ "_id" : 101, "title" : "First line\nSecond line" }, -{ "_id" : 102, "title" : "Many spaces before line" }, -{ "_id" : 103, "title" : "Multiple\nline description" }, -{ "_id" : 103, "title" : "abc123" } -``` - -参数 | 说明 | 示例 ----|---|---| -`i` | **忽略大小写** | `{"$regex":"single", "$options":"i"}` 将匹配

    { "_id" : 100, "title" : "Single line description." }
    -`m` | **多行匹配**
    比如文本中包含了换行符 `\n` | `{"$regex":"^S", "$options":"m"}`(以大写字母 S 开头)将匹配

    { "_id" : 100, "title" : "Single line description." },
    { "_id" : 101, "title" : "First line\nSecond line" }
    -`x` | **忽略空白字符**
    包括空格、tab、`\n`、`#` 注释等,
    但对 vertical tab(ASCII 码为 11)无效。 | `{"$regex":"abc #category code\n123 #item number", "$options":"x"}`(# 后面为注释)将匹配

    { "_id" : 103, "title" : "abc123" }
    -`s` | **允许 `.` 匹配任意字符和换行** | `{"$regex":"m.*line", "$options":"si"}` 将匹配

    { "_id" : 102, "title" : "Many spaces before     line" },
    { "_id" : 103, "title" : "Multiple\nline description" }
    - -以上参数可以组合使用,如 `"$options":"sixm"`。 - - -### 数组查询 - -如果 key 的值是数组类型,查找 key 值中有 2 的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":2}' \ - https://{{host}}/1.1/classes/TestObject -``` - -查找 key 值中有 2 或 3 或 4 的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":{"$in":[2,3,4]}}' \ - https://{{host}}/1.1/classes/TestObject -``` - -使用 `$all` 操作符来找到 key 值中**同时**有 2 和 3 和 4 的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":{"$all":[2,3,4]}}' \ - https://{{host}}/1.1/classes/TestObject -``` - -使用 `$size` 操作符来查找 key 值数组长度为 3 的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"arrayKey":{"$size": 3}}' \ - https://{{host}}/1.1/classes/TestObject -``` - -### 关系查询 - -有几种方式来查询对象之间的关系数据。如果你想获取对象,而这个对象的一个字段对应了另一个对象,你可以用一个 `where` 查询,自己构造一个 Pointer,和其他数据类型一样。例如,每条微博都会有很多人评论,我们可以让每一个 Comment 将它对应的 Post 对象保存到 post 字段上,这样你可以取得一条微博下所有 Comment: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"post":{"__type":"Pointer","className":"Post","objectId":"558e20cbe4b060308e3eb36c"}}' \ - https://{{host}}/1.1/classes/Comment -``` - -如果你想获取对象,这个对象的一个字段指向的对象需要另一个查询来指定,你可以使用 `$inQuery` 操作符。注意 `limit` 的默认值是 100 且最大值是 1000,这个限制同样适用于内部的查询,所以对于较大的数据集你可能需要细心地构建查询来获得期望的结果。 - -如上面的例子,假设每条微博还有一个 `image` 的字段,用来存储配图,你可以这样列出带图片的微博的评论数据: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"post":{"$inQuery":{"where":{"image":{"$exists":true}},"className":"Post"}}}' \ - https://{{host}}/1.1/classes/Comment -``` - -有时候,你可能需要在一个查询之中返回多种类型,你可以通过传入字段到 `include` 参数中。比如,我们想获得最近的 10 篇评论,而你想同时得到它们关联的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=-createdAt' \ - --data-urlencode 'limit=10' \ - --data-urlencode 'include=post' \ - https://{{host}}/1.1/classes/Comment -``` - -不是作为一个 Pointer 表示,`post` 字段现在已经被展开为一个完整的对象:`__type` 被设置为 Object 而 `className` 同样也被提供了。例如,一个指向 Post 的 Pointer 可能被展示为: - -```json -{ - "__type": "Pointer", - "className": "Post", - "objectId": "51e3a359e4b015ead4d95ddc" -} -``` - -当一个查询使用 `include` 参数来包含进去来取代 pointer 之后,可以看到 pointer 被展开为: - -```json -{ - "__type": "Object", - "className": "Post", - "objectId": "51e3a359e4b015ead4d95ddc", - "createdAt": "2015-06-29T09:31:20.371Z", - "updatedAt": "2015-06-29T09:31:20.371Z", - "desc": "Post 的其他字段也会一同被包含进来。" -} -``` - -你可以同样做多层的 `include`,这时要使用点号。如果你要 include 一个 Comment 对应的 Post 对应的 `author`: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'order=-createdAt' \ - --data-urlencode 'limit=10' \ - --data-urlencode 'include=post.author' \ - https://{{host}}/1.1/classes/Comment -``` - -如果你要构建一个查询,这个查询要 include 多个类,此时用逗号分隔列表即可。 - -### 地理查询 - -之前我们简要介绍过 GeoPoint。 - -假如在发布微博的时候,我们也支持用户加上当时的位置信息(新增一个 `location` 字段),如果想看看指定的地点附近发生的事情,可以通过 GeoPoint 数据类型加上在查询中使用 `$nearSphere` 做到。获取离当前用户最近的 10 条微博应该看起来像下面这个样子: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'limit=10' \ - --data-urlencode 'where={ - "location": { - "$nearSphere": { - "__type": "GeoPoint", - "latitude": 39.9, - "longitude": 116.4 - } - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -这会按照距离纬度 39.9、经度 116.4(当前用户所在位置)的远近排序返回一系列结果,第一个就是最近的对象。(注意:**如果指定了 order 参数的话,它会覆盖按距离排序。**) - -为了限定搜索的最大距离,需要加入 `$maxDistanceInMiles` 和 `$maxDistanceInKilometers` 或者 `$maxDistanceInRadians` 参数来限定。比如要找的半径在 10 英里内的话: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={ - "location": { - "$nearSphere": { - "__type": "GeoPoint", - "latitude": 39.9, - "longitude": 116.4 - }, - "$maxDistanceInMiles": 10.0 - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -同样做查询寻找在一个特定的范围里面的对象也是可以的,为了找到在一个矩形的区域里的对象,按下面的格式加入一个约束 `{"$within": {"$box": [southwestGeoPoint, northeastGeoPoint]}}`。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={ - "location": { - "$within": { - "$box": [ - { - "__type": "GeoPoint", - "latitude": 39.97, - "longitude": 116.33 - }, - { - "__type": "GeoPoint", - "latitude": 39.99, - "longitude": 116.37 - } - ] - } - } - }' \ - https://{{host}}/1.1/classes/Post -``` - -GeoPoint 的经纬度的类型是数字,且经度需在 -180.0 到 180.0 之间,纬度需在 -90.0 到 90.0 之间。 -另外,每个对象最多只能有一个类型为 GeoPoint 的属性。 - -### 文件查询 - -查询文件和查询一般对象基本一致。 -例如,以下命令可以获取所有文件(和查询一般对象一样,默认最多返回 100 条结果): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.1/classes/files -``` - -需要注意的是,内部文件(上传到文件服务的文件)的 `url` 字段是由云端动态生成的,其中涉及切换自定义域名的相关处理逻辑。 -因此,通过 url 字段查询文件仅适用于外部文件(直接保存外部 URL 到 `_File` 表创建的文件),内部文件请改用 key 字段(URL 中的路径)查询。 - -### 对象计数 - -如果你在使用 `limit`,或者如果返回的结果很多,你可能想要知道到底有多少对象应该返回,而不用把它们全部获得以后再计数,此时你可以使用 `count` 参数。举个例子,如果你仅仅是关心某个用户发布了多少条微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"pubUser":"官方客服"}' \ - --data-urlencode 'count=1' \ - --data-urlencode 'limit=0' \ - https://{{host}}/1.1/classes/Post -``` - -因为这个 request 请求了 `count` 而且把 `limit` 设为了 0,返回的值里面只有计数,没有 `results`: - -```json -{ - "results": [ - - ], - "count": 7 -} -``` - -如果有一个非 0 的 `limit` 的话,则既会返回 `results` 也会返回 `count`。 - -### 复合查询 - -`$or` 操作符用于查询**符合任意一种条件**的对象,它的值为一个 JSON 数组。例如,查询企业账号和个人账号的微博: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"$or":[{"pubUserCertificate":{"$gt":2}},{"pubUserCertificate":{"$lt":3}}]}' \ - https://{{host}}/1.1/classes/Post -``` - -任何在查询上的其他约束都会对返回的对象生效,所以你可以用 `$or` 对其他的查询添加约束。 - -`$and` 操作符用于查询**符合全部条件**的对象,它的值为一个 JSON 数组。例如查找存在 price 字段且 price != 199 的对象: - -``` -where={"$and":[{"price": {"$ne":199}},{"price":{"$exists":true}}]} -``` - -不过,查询条件表达式列表隐含 `$and` 操作符,所以上面的查询条件也可以改写为: - -``` -where=[{"price": {"$ne":199}},{"price":{"$exists":true}}] -``` - -实际上,由于这两个查询条件都是针对同一个字段(`price`)的查询,还可以进一步简化为: - -``` -where={"price": {"$ne":199, "$exists":true}} -``` - -不过,如果查询条件包含不止一个 or 查询,那就必须使用 `$and` 了: - -``` -where={"$and":[{"$or":[{"pubUserCertificate":{"$gt":2}},{"pubUserCertificate":{"$lt":3}}]},{"$or":[{"pubUser":"官方客服"},{"pubUser":"工程师团队"}]}]} -``` - -注意,**在组合查询的子查询中不支持使用 limit、skip、order、include 等非过滤型的约束。** - -## 用户 - -不仅在移动应用上,还在其他系统中,很多应用都有一个统一的登录流程。通过 REST API 访问用户的账户让你可以简单实现这一功能。 - -通常来说,**用户**(类名 `_User`)这个类的功能与其他的对象是相同的,比如都没有限制模式(Schema free)。User 对象和其他对象不同的是一个用户必须有用户名(username)和密码(password),密码会被自动地加密和存储。 -**username 和 email 这两个字段必须是没有重复的(大小写敏感)。** - -### 注册 - -注册一个新用户与创建一个新的普通对象之间的不同点在于 username 和 password 字段都是必需的。password 字段会以和其他的字段不一样的方式处理,它在储存时会被加密而且永远不会被返回给任何来自客户端的请求。 - -你可以让云服务自动验证邮件地址,做法是进入 **云服务控制台 > 存储 > 设置 > 用户账号**,勾选 **用户注册时,发送验证邮件**。 - -这项设置启用了的话,所有填写了 email 的用户在注册时都会产生一个 email 验证地址,并发回到用户邮箱,用户打开邮箱点击了验证链接之后,用户表里 `emailVerified` 属性值会被设为 true。你可以在 `emailVerified` 字段上查看用户的 email 是否已经通过验证。 - -你还可以在 **云服务控制台 > 存储 > 设置 > 用户账号**,勾选**未验证邮箱的用户,禁止登录**。 - -为了注册一个新的用户,需要向 user 路径发送一个 POST 请求,你可以加入一个新的字段,例如,创建一个新的用户有一个电话号码: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"username":"tom","password":"f32@ds*@&dsa","phone":"18612340000"}' \ - https://{{host}}/1.1/users -``` - -当创建成功时,HTTP 返回为 201 Created,Location 头包含了新用户的 URL: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -返回的主体是一个 JSON 对象,包含 objectId、createdAt 时间戳表示创建对象时间,sessionToken 可以被用来认证这名用户随后的请求: - -``` -{ - "sessionToken":"qmdj8pdidnmyzp0c7yqil91oc", - "createdAt":"2015-07-14T02:31:50.100Z", - "objectId":"55a47496e4b05001a7732c5f" -} -``` - -### 登录 - -在你允许用户注册之后,在以后你需要让他们用自己的用户名和密码登录。为了做到这一点,发送一个 POST 请求到 `/1.1/login`,加上 username 和 password 作为 body。 - -```sh -curl -X POST \ --H "Content-Type: application/json" \ --H "X-LC-Id: {{appid}}" \ --H "X-LC-Key: {{appkey}}" \ --d '{"username":"tom","password":"f32@ds*@&dsa"}' \ -https://{{host}}/1.1/login -``` - -用户也可以通过邮箱地址和密码登录,只需将 body 中的 username 换成 email: - -```json -{"email":"tom@example.com","password":"f32@ds*@&dsa"} -``` - -类似地,将 `username` 换成 `mobilePhoneNumber` 可以通过手机号和密码登录: - -```json -{"mobilePhoneNumber":"+86186xxxxxxxx","password":"f32@ds*@&dsa"} -``` - -返回的主体是一个 JSON 对象包括所有除了 password 以外的自定义字段。它同样包含了 createdAt、updateAt、objectId 和 sessionToken 字段。 - -```json -{ - "sessionToken":"qmdj8pdidnmyzp0c7yqil91oc", - "updatedAt":"2015-07-14T02:31:50.100Z", - "phone":"18612340000", - "objectId":"55a47496e4b05001a7732c5f", - "username":"tom", - "createdAt":"2015-07-14T02:31:50.100Z", - "emailVerified":false, - "mobilePhoneVerified":false -} -``` - -可以将 sessionToken 理解为用户的登录凭证,每个用户的 sessionToken 在同一个应用内都是唯一的, 类似于 Cookie 的概念。 - -正常情况下,用户的 sessionToken 是固定不变的,但在以下情况下会发生改变: - -* 客户端调用了忘记密码功能,重设了密码。 -* 开发者在 **云服务控制台 > 存储 > 设置 > 用户账号** 中勾选了 **密码修改后,强制客户端重新登录**,那么在修改密码后 sessionToken 也将强制更换。 -* 调用 [`refreshSessionToken`](#重置登录_sessionToken) 主动重置。 - -在 sessionToken 变化后,已有的登录如果调用到用户相关权限受限的 API,将返回 403 权限错误。 - -### 重置登录 sessionToken - -可以主动重置用户的 sessionToken: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/users/57e3bcca67f35600577c3063/refreshSessionToken -``` - -调用这个 API 要求传入登录返回的 `X-LC-Session` 作为认证,或者使用 `Master Key`。 - -重置成功将返回新的 sessionToken 及用户信息: - -```json -{ - "sessionToken":"5frlikqlwzx1nh3wzsdtfr4q7", - "updatedAt":"2016-10-20T03:10:57.926Z", - "objectId":"57e3bcca67f35600577c3063", - "username":"tom", - "createdAt":"2016-09-22T11:13:14.842Z", - "emailVerified":false, - "mobilePhoneVerified":false -} -``` - -#### 账户锁定 - -输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 `{"code":219,"error":"登录失败次数超过限制,请稍候再试,或者通过忘记密码重设密码。"}`,开发者可在客户端进行必要提示。 - -锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。 - -### 验证 Email - -设置 email 验证是 app 设置中的一个选项,通过这个标识,应用层可以对提供真实 email 的用户更好的功能或者体验。Email 验证会在 User 对象中加入 `emailVerified` 字段,当一个用户的 email 被新设置或者修改过的话,`emailVerified` 会被重置为 false。云服务会往用户填写的邮箱发送一个验证链接,用户点击这个链接可以让 `emailVerified` 被设置为 true。 - -emailVerified 字段有 3 种状态可以参考: - -1. **true**:用户已经点击了发送到邮箱的验证地址,邮箱被验证为真实有效。云端保证在新创建用户的时候 emailVerified 一定为 false。 -2. **false**:User 对象最后一次被更新的时候,用户并没有确认过他的 email 地址。如果你看到 emailVerified 为 false 的话,你可以考虑刷新 User 对象或者再次请求验证用户邮箱。 -3. **null**:User 对象在 email 验证没有打开的时候就已经创建了,或者 User 没有 email。 - -邮件模板和验证链接可以在**云服务控制台 > 数据存储 > 用户 > 邮件模板**定制。 - -### 请求验证 Email - -发送给用户的邮箱验证邮件在一周内失效,你可以通过调用 `/1.1/requestEmailVerify` 来强制重新发送: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"email":"tom@example.com"}' \ - https://{{host}}/1.1/requestEmailVerify -``` - -### 请求密码重设 - -在用户将 email 与他们的账户关联起来之后,你可以通过邮件来重设密码。操作方法为,发送一个 POST 请求到 `/1.1/requestPasswordReset`,同时在 request 的 body 部分带上 email 字段。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"email":"tom@example.com"}' \ - https://{{host}}/1.1/requestPasswordReset -``` - -如果成功的话,将返回状态码 `200 OK`。 - -邮件模板和验证链接可以在**云服务控制台 > 数据存储 > 用户 > 邮件模板**定制。 - -### 获取用户 - -和[获取对象](#获取对象)类似,你可以发送一个 GET 请求到 URL 以获取用户的账户信息。比如,为了获取上面创建的用户: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -你还可以通过 sessionToken 获取用户信息。 -这种方法最常见的使用场景是用户成功注册或登录后,本地保存服务器返回的 sessionToken,后续使用 sessionToken 获取当前用户信息: - -``` -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/users/me -``` - -返回的 JSON 数据与 [`/login`](#登录) 登录请求所返回的相同。 - -用户不存在时返回 400 Bad Request 错误: - -```json -{ - "code": 211, - "error": "Could not find user." -} -``` - -### 更新用户 - -在通常的情况下,没有人会允许别人来改动他们自己的数据。为了做好权限认证,确保只有用户自己可以修改个人数据,在更新用户信息的时候,必须在 HTTP 头部加入一个 `X-LC-Session` 项来请求更新,这个 session token 在注册和登录时会返回。 - -为了改动一个用户已经有的数据,需要对这个用户的 URL 发送一个 PUT 请求。任何你没有指定的 key 都会保持不动,所以你可以只改动用户数据中的一部分。 - -比如,如果我们想对 「tom」 的手机号码做出一些改动: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{"phone":"18600001234"}' \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -返回的 body 是一个 JSON 对象,只有一个 `updatedAt` 字段表明更新发生的时间。 - -```json -{ - "updatedAt": "2015-07-14T02:35:50.100Z" -} -``` - -username 也可以改动,但是新的 username 不能和既有数据重复。 - -很多开发者会希望让用户输入一次旧密码做一次认证,旧密码正确才可以修改为新密码,因此我们提供了一个单独的 API `PUT /1.1/users/:objectId/updatePassword` 来安全地更新密码: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{"old_password":"the_old_password", "new_password":"the_new_password"}' \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f/updatePassword -``` - -* **old_password**:用户的老密码 -* **new_password**:用户的新密码 - -注意:仍然需要传入 X-LC-Session,也就是登录用户才可以修改自己的密码。 - -### 查询 - -**请注意,`_User` 表的查询权限默认是关闭的。** -你可以在云服务控制台的 class 权限设置处打开,但出于安全考虑,一般情况下不建议开启这项权限。 -在 `_User` 表查询权限关闭的情况下,可以在服务端等受信任的环境使用 master key 进行查询。 - -你可以一次获取多个用户,只要向用户的根 URL 发送一个 GET 请求。没有任何 URL 参数的话,可以简单地列出所有用户: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/users -``` - -返回的值是一个 JSON 对象包括一个 `results` 字段,值是包含了所有对象的一个 JSON 数组。 - -```json -{ - "results":[ - { - "updatedAt":"2015-07-14T02:31:50.100Z", - "phone":"18612340000", - "objectId":"55a47496e4b05001a7732c5f", - "username":"tom", - "createdAt":"2015-07-14T02:31:50.100Z", - "emailVerified":false, - "mobilePhoneVerified":false - } - ] -} -``` - -所有的对普通对象的查询选项都适用于对用户对象的查询。 - -### 删除用户 - -想要删除一个用户,可以向它的 URL 上发送一个 DELETE 请求。同样的,你必须提供一个 X-LC-Session 在 HTTP 头上以便认证。例如: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -### 连接用户账户和第三方平台 - -你可以连接你的用户到其他服务,比如新浪微博和腾讯微博,这样就允许你的用户直接用他们现有的账号 ID 来注册或登录你的应用。在进行注册和登录时,需要附带上 `authData` 字段来提供你希望连接的服务的授权信息。一旦关联了某个服务,`authData` 将被存储到你的用户信息里。 - -`authData` 是一个普通的 JSON 对象,它所要求的 key 根据 service 不同而不同,具体要求见下面。每种情况下,你都需要自己负责完成整个授权过程(一般是通过 OAuth 协议,1.0 或者 2.0) 来获取授权信息,提供给连接 API。 - -[新浪微博](http://weibo.com/) 的 authData 内容: - -```json -{ - "weibo": { - "uid": "123456789", - "access_token": "2.00vs3XtCI5FevCff4981adb5jj1lXE", - "expiration_in": "36000" - } -} -``` - - -[微信](http://open.weixin.qq.com/) 的 authData 内容: - -```json -{ - "weixin": { - "openid": "0395BA18A5CD6255E5BA185E7BEBA242", - "access_token": "12345678-SaMpLeTuo3m2avZxh5cjJmIrAfx4ZYyamdofM7IjU", - "expires_in": 1382686496 - } -} -``` - -[Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api) 的 authData 内容: - -```json -{ - "lc_apple": { - "uid": "从 Apple 获取到的 User Identifier", - "identity_token": "从苹果获取到的 identity Token", - "code": "从苹果获取到的 Authorization Code" - } -} -``` - -其他任意第三方平台: - -```json -{ - "第三方平台名称,例如 facebook": { - "uid": "在第三方平台上的唯一用户 ID 字符串", - "access_token": "在第三方平台的 `Access Token`", - // ……其他可选属性 - } -} -``` - -云端会自动为 `authData.第三方平台名称.uid` 创建唯一索引,以确保一个第三方账号只绑定到一个应用内用户上。 - -注意: - -- 其他第三方平台不支持校验 `Access Token`。 -- 其他第三方平台不支持后面提到的 UnionID 登录功能,因此也不用设置相应的 `unionid`、`platform`、`main_account` 字段。 -- 其他第三方平台请使用 `uid` 字段储存第三方平台的唯一用户 ID 字符串,不要使用 `openid`。 - -和第三方登录相似的一个概念是匿名登录。 -匿名用户(Anonymous user)的 authData 内容如下: - -```json -{ - "anonymous": { - "id": "random UUID with lowercase hexadecimal digits" - } -} -``` - -#### 注册和登录 - -使用一个连接服务来注册用户并登录,同样使用 POST 请求 users,只是需要提供 `authData` 字段。例如,使用 QQ 账户注册或者登录用户: - - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "authData": { - "qq": { - "openid": "0395BA18A5CD6255E5BA185E7BEBA242", - "access_token": "12345678-SaMpLeTuo3m2avZxh5cjJmIrAfx4ZYyamdofM7IjU", - "expires_in": 1382686496 - } - } - }' \ - https://{{host}}/1.1/users -``` - -云端会检查是否已经有一个用户连接了这个 `authData` 服务。如果已经有用户存在并连接了同一个 `authData`,那么返回 200 OK 和详细信息(包括用户的 `sessionToken`): - -```sh -Status: 200 OK -Location: https://{{host}}/1.1/users/75a4800fe4b05001a7745c41 -``` - -应答的 body 类似: - -```json -{ - "username": "Tom", - "createdAt": "2015-06-28T23:49:36.353Z", - "updatedAt": "2015-06-28T23:49:36.353Z", - "objectId": "75a4800fe4b05001a7745c41", - "sessionToken": "anythingstringforsessiontoken", - "authData": { - "qq": { - "openid": "0395BA18A5CD6255E5BA185E7BEBA242", - "access_token": "12345678-SaMpLeTuo3m2avZxh5cjJmIrAfx4ZYyamdofM7IjU", - "expires_in": 1382686496 - } - } -} -``` - -如果用户还没有连接到这个账号,则你会收到 201 Created 的应答状态码,标识新的用户已经被创建: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/users/55a4800fe4b05001a7745c41 -``` - -应答内容包括 objectId、createdAt、sessionToken 以及一个自动生成的随机 username,例如: - -```json -{ - "username":"ec9m07bo32cko6soqtvn6bko5", - "sessionToken":"tfrvbzmdf609nu9204v5f0tuj", - "createdAt":"2015-07-14T03:20:47.733Z", - "objectId":"55a4800fe4b05001a7745c41" -} -``` - -云端会自动验证部分平台 `Access Token` 的有效性。 -详见[自动验证第三方平台授权信息](/v2/sdk/storage/guide/dotnet#自动验证第三方平台授权信息)。 - -#### UnionID 注册和登录 - -微信与新浪微博都有 UnionID 登录机制,使用 UnionID 注册登录,可以使得不同微信公众号或小程序之间共享用户。 - -微信的 authData 内容: - -```json - "authData": { - "access_token" : "access_token", - "expires_in" : 7200, - "openid" : "openid", - "refresh_token" : "refresh_token", - "scope" : "snsapi_userinfo", - "unionid" : "ox7NLs-e-32ZyHg2URi_F2iPEI2U" -} -``` - -使用 UnionID 注册登录,需要提供带有 `unionid` 参数的 `authData`。另外需要配合传递 `platform` 和 `main_account` 这两个字段。 - -* `platform`:unionId 对应的注册平台,可由应用自行指定,微信、QQ、微博平台建议设为[这些推荐的值](/v2/sdk/storage/guide/dotnet#该如何指定-unionidplatform)。 -* `main_account`: `main_account` 为 true 时把当前平台的鉴权信息作为主账号。 - -在服务端进行存储的时候会根据 `platform` 来命名新增的平台,如传入 `"platform" = "weixin"` 时,返回数据中会增加 `_weixin_unionid` 字段存储 `{"uid":"xxxxx"}`。 - -``` -"_weixin_unionid": { - "uid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U" -} -``` - -完整的第三方注册登录请求如下,以使用微信 UnionId 为例: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "authData": { - "wxleanoffice": { - "openid": "oTY851cqL0gk3DqW3xINqG1Q4PTc", - "access_token": "12_b6mz7ujXbTY4vpbqCRaKVa_y0Ij3N9grCeVtM8VJT8KFd4qnQ9lXtBsZVxG6x9c9Nay_oNgvbKK7KYKbn8R2P7uEgA0EhsXMHmxkx-xU-Tk", - "expires_in": 7200, - "refresh_token": "12_71UYUnqHDuIfekimsJsYjBDfY67ilo30fDqrYkqlwZtxNgcBhMmQgDVhT6mJWkRg0mngvX9kXeCGP8kmBWdvUtc5ngRiN5LDTWAau4du838", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account":"true" - }, - "wxleansupport": { - "openid": "ZTY873cOa0gk5aOW5OaaOa3Q6PTc", - "access_token": "34_b6mO7OjXbTY6O-bOaRaaVa_O0aj5a9gOaeVOa8VaT8aad6OnQ9lXO-OZVOa6O9c9aaO_ZagObaa7aYabn8R4P7Oagn0ahOXaamOkO-OU-Tk", - "expires_in": 7200, - "refresh_token": "8-_78UYUnOaaOafekimOaOYj-afY67ilZ40faOOYkOlOZOOagc-hamQgaVhT6maWkRg0mngOX9kXeaaP8km-WdOUOc4ngRia4aaTWnaO4dO848", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account":"false" - }, - }}' \ - https://{{host}}/1.1/users -``` - -我们看到,在上面的例子中,`wxleanoffice` 和 `wxleansupport` 的 `unionid` 是一样的,`platform` 均指定为 `weixin`,`wxleanoffice` 是主账号,`main_account` 为 `true`。 - -应答内容包括 objectId、createdAt、sessionToken、authData 以及一个自动生成的随机 username,应答的 body 类似: - -```json -{ - "sessionToken": "v53f0q4oecbrjojn530w89s5f", - "updatedAt": "2018-08-16T08:03:44.203Z", - "objectId": "5b752fe0a22b9d003137e16d", - "username": "vp7szn9ytuaylgtnw14qnjx2u", - "createdAt": "2018-08-16T08:03:44.203Z", - "emailVerified": false, - "authData": { - "wxleanoffice": { - "openid": "oTY851cqL0gk3DqW3xINqG1Q4PTc", - "access_token": "12_b6mz7ujXbTY4vpbqCRaKVa_y0Ij3N9grCeVtM8VJT8KFd4qnQ9lXtBsZVxG6x9c9Nay_oNgvbKK7KYKbn8R2P7uEgA0EhsXMHmxkx-xU-Tk", - "expires_in": 7200, - "refresh_token": "12_71UYUnqHDuIfekimsJsYjBDfY67ilo30fDqrYkqlwZtxNgcBhMmQgDVhT6mJWkRg0mngvX9kXeCGP8kmBWdvUtc5ngRiN5LDTWAau4du838", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account": "true" - }, - "wxleansupport": { - "openid": "ZTY873cOa0gk5aOW5OaaOa3Q6PTc", - "access_token": "34_b6mO7OjXbTY6O-bOaRaaVa_O0aj5a9gOaeVOa8VaT8aad6OnQ9lXO-OZVOa6O9c9aaO_ZagObaa7aYabn8R4P7Oagn0ahOXaamOkO-OU-Tk", - "expires_in": 7200, - "refresh_token": "8-_78UYUnOaaOafekimOaOYj-afY67ilZ40faOOYkOlOZOOagc-hamQgaVhT6maWkRg0mngOX9kXeaaP8km-WdOUOc4ngRia4aaTWnaO4dO848", - "scope": "snsapi_userinfo", - "unionid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U", - "platform": "weixin", - "main_account":"false" - }, - "_wxleanoffice_unionid": { - "uid": "ox7NLs-e-32ZyHg2URi_F2iPEI2U" - } - }, - "mobilePhoneVerified": false -} -``` - -参见[扩展:接入_UnionID_体系,打通不同子产品的账号系统](/v2/sdk/storage/guide/dotnet#扩展:接入-unionid-体系,打通不同子产品的账号系统)。 - -#### 连接 - -连接一个现有的用户到新浪微博或者微信,可以向 user endpoint 发送一个附带 `authData` 字段的 PUT 请求来实现。例如,连接一个用户到微信账号发起的请求类似这样: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{ - "authData": { - "weixin": { - "uid": "123456789", - "access_token": "2.00vs3XtCI5FevCff4981adb5jj1lXE", - "expiration_in": "36000" - ... - } - } - }' \ - https://{{host}}/1.1/users/55a47496e4b05001a7732c5f -``` - -完成连接后,你可以使用匹配的 `authData` 来认证他们。 - -#### 断开连接 - -断开一个现有用户到某个服务,可以通过删除 `authData` 中对应的平台来做到。 -例如,已经绑定过微信的用户 `authData` 数据格式如下,平台名称为 `weixin`: - -```json -{ - "username": "3td7p1nucap1i1p53m1zibwgx", - "authData": { - "weixin": { - "openid": "oTY851cqL0gk3DqW3xINqG1Q4PTc", - "scope": "snsapi_userinfo", - "refresh_token": "refresh_token", - "platform": "weixin", - "unionid": "unionid, - "access_token": "access_token", - "expires_in": 7200 - } - }, -} -``` - -取消微信关联通过删除 `authData.weixin` 来实现: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: 6fehqhr2t2na5mv1aq2om7jgz" \ - -H "Content-Type: application/json" \ - -d '{"authData.weixin":{"__op":"Delete"}}' \ - https://{{host}}/1.1/users/5b7e53a767f356005fb374f6 -``` - -其返回值类似于: - -```json -{ - "updatedAt":"2018-08-23T06:32:47.633Z", - "objectId":"5b7e53a767f356005fb374f6" -} -``` - -### 安全 - -当你用 REST API key 来访问云服务时,访问可能被 ACL 所限制,就像 iOS 和 Android SDK 上所做的一样。你仍然可以通过 REST API 来读和修改,只需要通过 `ACL` 的 key 来访问一个对象。 - -ACL 按 JSON 对象格式来表示,JSON 对象的 key 是 objectId 或者一个特别的 key(`*`,表示公共访问权限)。ACL 的值是权限对象,这个 JSON 对象的 key 即是权限名,而这些 key 的值总是 true。 - -举个例子,如果你想让一个 id 为 55a47496e4b05001a7732c5f 的用户有读和写一个对象的权限,而且这个对象应该可以被公共读取,符合的 ACL 应该是: - -```json -{ - "55a47496e4b05001a7732c5f": { - "read": true, - "write": true - }, - "*": { - "read": true - } -} -``` - -这样,只有在 `X-LC-Session` HTTP 头中携带了用户 55a47496e4b05001a7732c5f 的有效 sessionToken 的情况下,对该对象的写请求才不会因为权限不足而报错。 -sessionToken 的值会在用户登录成功时返回。 - -## 角色 - -当你的应用的规模和用户基数成长时,你可能发现你需要比 ACL 模型(针对每个用户)更加粗粒度的访问控制你的数据的方法。为了适应这种需求,云服务支持一种基于角色的权限控制方式。角色系统提供一种逻辑方法让你通过权限的方式来访问你的数据,角色是一种有名称的对象,包含了用户和其他角色。任何授予一个角色的权限隐含着授予它包含着的其他的角色相应的权限。 - -例如,你的应用中可能有一些类似于「主持人」的角色可以修改和删除其他用户创建的新的内容,你可能还有一些「管理员」有着与「主持人」相同的权限,但是还可以修改应用的其他全局性设置。通过给予用户这些角色,你可以保证新的用户可以做主持人或者管理员,不需要手动地授予每个资源的权限给各个用户。 - -我们提供一个特殊的角色(Role)类来表示这些用户组,为了设置权限用。角色有一些和其他对象不太一样的特殊字段。 - -| 字段 | 说明 | -| ----- | ---------------------------------------- | -| name | 角色的名字,这个值是必须的,而且只允许被设置一次,只要这个角色被创建了的话。角色的名字必须由字母、数字、下划线这些字符构成。这个名字可以用来标明角色而不需要它的 objectId。 | -| users | 一个指向一系列用户的关系,这些用户会继承角色的权限。 | -| roles | 一个指向一系列子角色的关系,这些子关系会继承父角色所有的权限。 | - -通常来说,为了保持这些角色安全,你的移动应用不应该为角色的创建和管理负责。作为替代,角色应该是通过一个不同的网页上的界面来管理,或者手工被管理员所管理。 - -### 创建角色 - -创建一个新的角色与其他的对象不同的是 name 字段是必须的。角色必须指定一个 ACL,这个 ACL 必须尽量的约束严格一些,这样可以防止错误的用户修改角色。 - -创建一个新角色,发送一个 POST 请求到 roles 根路径: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Manager", - "ACL": { - "*": { - "read": true - } - } - }' \ - https://{{host}}/1.1/roles -``` - -和创建对象类似,创建成功时,HTTP 返回 `201 Created`,`Location` header 包含了新的对象的 URL: - -```sh -Status: 201 Created -Location: https://{{host}}/1.1/roles/55a483f0e4b05001a774b837 -``` - -返回值是一个 JSON 对象: - -```json -{ - "createdAt":"2015-07-14T03:34:41.074Z", - "objectId":"55a48351e4b05001a774a89f" -} -``` - -你可以通过加入已有的对象到 roles 和 users 关系中来创建一个有子角色和用户的角色: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "CLevel", - "ACL": { - "*": { - "read": true - } - }, - "roles": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_Role", - "objectId": "55a48351e4b05001a774a89f" - } - ] - }, - "users": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_User", - "objectId": "55a47496e4b05001a7732c5f" - } - ] - } - }' \ - https://{{host}}/1.1/roles -``` - -你也许注意到了,上面的代码里出现了一个新操作符 `AddRelation`。 -因为一些性能上的考量,Relation 的实现比较复杂。 -不过我们可以简单地把它看成 Pointer 数组。 -一般推荐使用中间表而不是 Relation。 -但由于一些历史原因,角色中还是用到了 Relation 这一概念。 - -### 获取角色 - -类似获取对象,你可以通过发送一个 GET 请求到 Location header 中返回的 URL 来获取这个对象,比如我们想要获取上面创建的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/roles/55a483f0e4b05001a774b837 -``` - -响应的 body 是一个 JSON 对象包含角色的所有字段: - -```json -{ - "name":"CLevel", - "createdAt":"2015-07-14T03:37:20.992Z", - "updatedAt":"2015-07-14T03:37:20.994Z", - "objectId":"55a483f0e4b05001a774b837", - "users":{ - "__type":"Relation", - "className":"_User" - }, - "roles":{ - "__type":"Relation", - "className":"_Role" - } -} -``` - -### 更新角色 - -更新一个角色通常可以像更新其他对象一样使用,但是 name 字段是不可以更改的。加入和删除 users 和 roles 可以通过使用 `AddRelation` 和 `RemoveRelation` 操作来进行。 - -举例来说,我们对 Manager 角色加入 1 个用户: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "users": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_User", - "objectId": "55a4800fe4b05001a7745c41" - } - ] - } - }' \ - https://{{host}}/1.1/roles/55a48351e4b05001a774a89f -``` - -相似的,我们可以删除一个 Manager 的子角色: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "roles": { - "__op": "RemoveRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_Role", - "objectId": "55a483f0e4b05001a774b837" - } - ] - } - }' \ - https://{{host}}/1.1/roles/55a48351e4b05001a774a89f -``` - -### 查询角色 - -查询用户具有哪些角色: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'where={"users": {"__type": "Pointer", "className": "_User", "objectId": "5e03100ed4b56c008e4a91dc"}}' \ - https://{{host}}/1.1/roles -``` - -查询角色包含哪些用户(不计子角色中的用户): - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode '"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"5f3dea7b7a53400006b13886"},"key":"users"}' \ - https://{{host}}/1.1/users -``` - -可以像查询普通对象一样,根据角色的属性进行查询。 - -### 删除角色 - -想要删除一个角色,只需要发送 DELETE 请求到它的 URL 就可以了。 - -我们需要传入 X-LC-Session 来通过一个有权限的用户账号来访问这个角色对象,例如: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - https://{{host}}/1.1/roles/55a483f0e4b05001a774b837 -``` - -### 角色和 ACL - -当你通过 REST API 访问云服务的时候,访问和 SDK 一样可能被 ACL 所限制。你仍然可以通过 REST API 来读和修改 ACL,只用通过访问「ACL」键就可以了。 - -除了用户级的权限设置以外,你可以通过设置角色级的权限来限制对云端对象的访问。取代了指定一个 objectId 带一个权限的方式,你可以设定一个角色的权限为它的名字在前面加上 `role:` 前缀作为 key。你可以同时使用用户级的权限和角色级的权限来提供精细的用户访问控制。 - -比如,为了限制一个对象可以被在 Staff 里的任何人读到,而且可以被它的创建者和任何有 Manager 角色的人所修改,你应该向下面这样设置 ACL: - -```json -{ - "55a4800fe4b05001a7745c41": { - "write": true - }, - "role:Staff": { - "read": true - }, - "role:Manager": { - "write": true - } -} -``` - -如果创建的用户和 Manager 本身就是 Staff 的子角色和用户,那么它们都会继承授予 Staff 的权限。 -所以我们上面没有显式地为创建者和 Manager 授予「读」权限。 - -就像我们之前提到的一样,一个角色可以包含另一个,可以为 2 个角色建立一个「父子」关系。 -这个关系的结果就是任何被授予父角色的权限隐含地被授予子角色。 - -这样的关系类型通常在用户管理的内容类的 app 上比较常见,比如论坛。有一些少数的用户是「管理员」,有最高级的权限来调整程序的设置、创建新的论坛、设定全局的消息等等。 - -另一类用户是「版主」,他们有责任保证用户生成的内容是合适的。任何有管理员权限的人都应该有版主的权利。为了建立这个关系,你应该把「Administrators」的角色设置为「Moderators」 的子角色,具体来说就是把 Administrators 这个角色加入 Moderators 对象的 roles 关系之中: - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "roles": { - "__op": "AddRelation", - "objects": [ - { - "__type": "Pointer", - "className": "_Role", - "objectId": "" - } - ] - } - }' \ - https://{{host}}/1.1/roles/ -``` - -## 文件 - -### 创建文件 - -REST API 不支持文件上传,请使用 SDK 或命令行工具上传并创建文件。 - -如果已有 URL,可以使用以下命令创建文件(在 `_File` 表新增一条数据): - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"url": "https://example.com/foo.jpg", "name": "foo.jpg", "mime_type": "image/jpeg"}' \ - https://{{host}}/1.1/files -``` - -响应和返回值请参考前文[创建对象](#创建对象)一节。 - -### 关联文件到对象 - -一个文件被保存到 `_File` 表后,你可以关联该文件到某个 LCObject 对象上: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "tom", - "picture": { - "id": "543cbaede4b07db196f50f3c", - "__type": "File" - } - }' \ - https://{{host}}/1.1/classes/Staff -``` - -其中 `id` 就是文件对象的 objectId。 - -### 删除文件 - -知道文件对象 ObjectId 的情况下,可以通过 DELETE 删除文件: - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/files/543cbaede4b07db196f50f3c -``` -## 数据 Schema - -为了方便开发者使用、自行研发一些代码生成工具或者内部使用的管理平台。我们提供了获取数据 Class Schema 的开放 API,基于安全考虑,强制要求使用 `Master Key` 才可以访问。 - -查询一个应用下面所有 Class 的 Schema: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/schemas -``` - -返回的 JSON 数据包含了每个 Class 对应的 Schema: - -```json -{ - "_User":{ - "username" : {"type":"String"}, - "password" : {"type":"String"}, - "objectId" : {"type":"String"}, - "emailVerified": {"type":"Boolean"}, - "email" : {"type":"String"}, - "createdAt" : {"type":"Date"}, - "updatedAt" : {"type":"Date"}, - "authData" : {"type":"Object"} - } - // 其他 class -} -``` - -也可以单独获取某个 Class 的 Schema: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/schemas/_User -``` - -## 数据导出 API - -你可以通过请求 `/exportData` 来导出应用数据: - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{}' \ - https://{{host}}/1.1/exportData -``` - -`exportData` 要求使用 master key 来授权。 - -你还可以指定导出数据的起始时间(`updatedAt`): - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_date":"2015-09-20", "to_date":"2015-09-25"}' \ - https://{{host}}/1.1/exportData -``` - -还可以指定具体的 class 列表,使用逗号隔开: - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"classes":"_User,GameScore,Post"}' \ - https://{{host}}/1.1/exportData -``` - -增加 `only-schema` 选项就可以只导出 schema: - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"only-schema":"true"}' \ - https://{{host}}/1.1/exportData -``` - -导出的 Schema 文件同样可以使用数据导入功能来导入到其他应用。 - -默认导出的结果将发送到应用的创建者邮箱,你也可以指定接收邮箱: - -``` -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"email":"username@exmaple.com"}' \ - https://{{host}}/1.1/exportData -``` - -调用结果将返回本次任务的 id 和状态: - -```json -{ - "status":"running", - "id":"1wugzx81LvS5R4RHsuaeMPKlJqFMFyLwYDNcx6LvCc6MEzQ2", - "app_id":"{{appid}}" -} -``` - -除了被动等待邮件之外,你还可以主动使用 id 去查询导出任务状态: - -``` -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.1/exportData/1wugzx81LvS5R4RHsuaeMPKlJqFMFyLwYDNcx6LvCc6MEzQ2 -``` - -如果导出完成,将返回导出结果的下载链接: - -```json -{ - "status":"done", - "download_url": "https://download.leancloud.cn/export/example.tar.gz", - "id":"1wugzx81LvS5R4RHsuaeMPKlJqFMFyLwYDNcx6LvCc6MEzQ2", - "app_id":"{{appid}}" -} -``` - -如果任务还没有完成, `status` 仍然将为 `running` 状态,**请间隔一段时间后再尝试查询。** - -**导出应用数据中不包括即时通讯聊天记录。** 如需导出这些记录,请调用相应的 REST API 接口获取。 - -## 其他 API - -### 服务器时间 - -获取服务端当前日期时间可以通过 `/date` API: - -``` -curl -i -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - https://{{host}}/1.1/date -``` - -返回 UTC 日期: - -```json -{ - "iso": "2015-08-27T07:38:33.643Z", - "__type": "Date" -} -``` - -## 浏览器跨域和特殊方法解决方案 - -注:直接使用 RESTful API 遇到跨域问题,请遵守 HTML5 CORS 标准即可。以下方法非推荐方式,而是内部兼容方法。 - -对于跨域操作,我们定义了如下的 text/plain 数据格式来支持用 POST 的方法实现 GET、PUT、DELETE 的操作。 - -### GET - -``` - curl -i -X POST \ - -H "Content-Type: text/plain" \ - -d \ - '{"_method":"GET", - "_ApplicationId":"{{appid}}", - "_ApplicationKey":"{{appkey}}"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -对应的输出: - -``` -HTTP/1.1 200 OK -Server: nginx -Date: Thu, 04 Dec 2014 06:34:34 GMT -Content-Type: application/json;charset=utf-8 -Content-Length: 174 -Connection: keep-alive -Last-Modified: Thu, 04 Dec 2014 06:34:08.498 GMT -Cache-Control: no-cache,no-store -Pragma: no-cache -Strict-Transport-Security: max-age=31536000 -{ - "content": "每个 Java 程序员必备的 8 个开发工具", - "pubUser": "官方客服", - "pubTimestamp": 1435541999, - "createdAt": "2015-06-29T01:39:35.931Z", - "updatedAt": "2015-06-29T01:39:35.931Z", - "objectId": "558e20cbe4b060308e3eb36c" -} -``` - -### PUT - -``` -curl -i -X POST \ - -H "Content-Type: text/plain" \ - -d \ - '{"_method":"PUT", - "_ApplicationId":"{{appid}}", - "_ApplicationKey":"{{appkey}}", - "upvotes":99}' \ - https://{{host}}/1.1/classes/Post/ -``` - -对应的输出: - -``` -HTTP/1.1 200 OK -Server: nginx -Date: Thu, 04 Dec 2014 06:40:38 GMT -Content-Type: application/json;charset=utf-8 -Content-Length: 78 -Connection: keep-alive -Cache-Control: no-cache,no-store -Pragma: no-cache -Strict-Transport-Security: max-age=31536000 - -{"updatedAt":"2015-07-13T06:40:38.310Z","objectId":"558e20cbe4b060308e3eb36c"} -``` - -### DELETE - -``` -curl -i -X POST \ - -H "Content-Type: text/plain" \ - -d \ - '{"_method": "DELETE", - "_ApplicationId":"{{appid}}", - "_ApplicationKey":"{{appkey}}"}' \ - https://{{host}}/1.1/classes/Post/ -``` - -对应的输出是: - -``` -HTTP/1.1 200 OK -Server: nginx -Date: Thu, 04 Dec 2014 06:15:10 GMT -Content-Type: application/json;charset=utf-8 -Content-Length: 2 -Connection: keep-alive -Cache-Control: no-cache,no-store -Pragma: no-cache -Strict-Transport-Security: max-age=31536000 - -{} -``` - -总之,就是利用 POST 传递的参数,把 _method、_ApplicationId 以及 _ApplicationKey 传递给服务端,服务端会自动把这些请求翻译成指定的方法,这样可以使得 Unity3D 以及 JavaScript 等平台(或者语言)可以绕开浏览器跨域或者方法限制。 diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/08-fulltext-search.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/08-fulltext-search.mdx deleted file mode 100644 index ad921fe6c..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/08-fulltext-search.mdx +++ /dev/null @@ -1,229 +0,0 @@ ---- -id: fulltext-search -title: 全文搜索指南 -sidebar_label: 全文搜索指南 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - - - -在应用内使用全文搜索是一个很常见的需求。例如一个阅读类的应用,里面有很多有价值的文章,开发者会提供一个搜索框,让用户键入关键字后就能查找到应用内相关的文章,并按照相关度排序,就好像我们打开浏览器用 Google 搜索关键字一样。 -虽然使用正则查询也可以实现全文搜索功能,但数据量较大的时候正则查询会有性能问题,因此我们提供了专门的全文搜索功能。 - -## 为 Class 启用搜索 - -你需要选择至少一个 Class 为它开启全文搜索。开启后,该 Class 的数据会自动建立索引,并且可以调用我们的搜索组件或者 [API](#搜索_API) 搜索到内容。 - -**请注意,启用了搜索的 Class 数据,其搜索结果仍然遵循 ACL。如果你为 Class 里的 Object 设定了合理的 ACL,那么搜索结果也将遵循这些 ACL 值,保护你的数据安全。** - -你可以在**云服务控制台 > 数据存储 > 全文搜索**为 Class 启用搜索,点击「添加搜索 Class」: - -- **Class**:选择需要启用搜索的 Class。开发版应用最多允许 5 个 Class 启用全文搜索,商用版应用最多允许 10 个 Class 启用全文搜索。 -- **开放的列**:你可以选择将哪些字段加入搜索索引。默认情况下,`objectId`、`createdAt`、`updatedAt` 三个字段将无条件加入开放字段列表。除了这三个字段外,开发版应用每个 Class 最多允许索引 5 个字段,商用版应用每个 Class 最多允许索引 10 个字段。请仔细挑选要索引的字段。 - -**如果一个 Class 启用了全文搜索,但是超过两周没有任何搜索调用,我们将自动禁用该 Class 的搜索功能。** - -## 搜索 API - -我们提供了 [全文搜索的 REST API 接口](/v2/sdk/storage/guide/fulltext-search-rest)。 -SDK 封装了这一接口。 - -假设你对 GameScore 类[启用了全文搜索](#为_Class_启用搜索),你就可以尝试传入关键字来搜索: - - - -```cs -LCSearchQuery query = new LCSearchQuery("GameScore"); -query.QueryString("dennis") - .OrderByDescending("score") - .Limit(10); -LCSearchResponse response = await query.Find(); -// 符合查询条件的文档总数 -Debug.Log(response.Hits); -// 符合查询条件的结果文档 -foreach (GameScore score in response.Results) { - -} -// 标记本次查询结果,下次查询继续传入这个 sid 用于查找后续的数据,用来支持翻页查询 -Debug.Log(response.Sid); -``` -```java -LCSearchQuery searchQuery = new LCSearchQuery("dennis"); -searchQuery.setClassName("GameScore"); -searchQuery.setLimit(10); -searchQuery.orderByAscending("score"); // 根据 score 字段升序排序。 -searchQuery.findInBackground().subscribe(new Observer>() { - @Override - public void onSubscribe(Disposable disposable) {} - - @Override - public void onNext(List results) { - for (LCObject o:results) { - System.out.println(o); - } - testSucceed = true; - latch.countDown(); - } - - @Override - public void onError(Throwable throwable) { - throwable.printStackTrace(); - testSucceed = true; - latch.countDown(); - } - - @Override - public void onComplete() {} -}); -``` -```objc -LCSearchQuery *searchQuery = [LCSearchQuery searchWithQueryString:@"test-query"]; -searchQuery.className = @"GameScore"; -searchQuery.highlights = @"field1,field2"; -searchQuery.limit = 10; -searchQuery.cachePolicy = kLCCachePolicyCacheElseNetwork; -searchQuery.maxCacheAge = 60; -searchQuery.fields = @[@"field1", @"field2"]; -[searchQuery findInBackground:^(NSArray *objects, NSError *error) { - for (LCObject *object in objects) { - NSString *appUrl = [object objectForKey:@"_app_url"]; - NSString *deeplink = [object objectForKey:@"_deeplink"]; - NSString *highlight = [object objectForKey:@"_highlight"]; - // other fields - // code is here - } -}]; -``` -```dart -LCSearchQuery query = new LCSearchQuery('GameScore'); -query.queryString('dennis'); -query.orderByDescending('score'); -query.limit(10); -LCSearchResponse response = await query.find(); -// 符合查询条件的文档总数 -print(response.hits); -// 符合查询条件的结果文档 -for (GameScore score in response.results) { - -} -// 标记本次查询结果,下次查询继续传入这个 sid 用于查找后续的数据,用来支持翻页查询 -print(response.sid); -``` -```js -const query = new AV.SearchQuery('GameScore'); -query.queryString('dennis'); -// 高亮玩家字段中匹配到的 dennis 字符串,如要匹配多个字段,可传入一个数组 -query.highlights('player'); -query.highlights('player'); -query.find().then(function(results) { - console.log("Find " + query.hits() + " docs."); - // 打印输出:Find 4 docs. - // 打印带高亮的第一个匹配结果,剩余匹配结果的处理同理 - console.log(results[0].get('_highlight').player); - // 打印输出:[ 'dennis ZX' ] -}).catch(function(err){ - // 处理 err -}); -``` - - - - -有关查询语法,可以参考 [q 查询语法](/v2/sdk/storage/guide/fulltext-search-rest#q_查询语法)。 - -因为每次请求都有 limit 限制,所以一次请求可能并不能获取到所有满足条件的记录。 -`SearchQuery` 的 `hits()` 标示所有满足查询条件的记录数。 -你可以多次调用同一个 `SearchQuery` 的 `find()` 获取余下的记录。 - -如果在不同请求之间无法保存查询的 query 对象,可以利用 sid 做到翻页,一次查询是通过 `SearchQuery` 的 `_sid` 属性来标示的。 -你可以通过 `SearchQuery` 的 `sid()` 来重建查询 query 对象,继续翻页查询。 -sid 在 5 分钟内有效。 - -复杂排序可以使用 `SearchSortBuilder`,例如,假设 `scores` 是由分数组成的数组,现在需要根据分数的平均分倒序排序,并且没有分数的排在最后: - - - -```cs -LCSearchSortBuilder sortBuilder = new LCSearchSortBuilder(); -sortBuilder.OrderByAscending("balance", "avg", "last"); -searchQuery.SortBy(sortBuilder); -``` -```java -LCSearchSortBuilder builder = LCSearchSortBuilder.newBuilder(); -builder.orderByDescending("scores","avg","last"); -searchQuery.setSortBuilder(builder); -``` -```objc -LCSearchSortBuilder *builder = [LCSearchSortBuilder newBuilder]; -[builder orderByDescending:@"scores" withMode:@"max" andMissing:@"last"]; -searchQuery.sortBuilder = builder; -``` -```dart -LCSearchSortBuilder sortBuilder = new LCSearchSortBuilder(); -sortBuilder.orderByAscending('scores', mode: 'avg', missing: 'last'); -searchQuery.sortBy(sortBuilder); -``` -```js -searchQuery.sortBy(new AV.SearchSortBuilder().descending('scores', 'avg', 'last')); -``` - - - -更多 API 请参考 SDK API 文档: - - - -<> - -- [LCSearchQuery](https://leancloud.github.io/csharp-sdk/html/classLeanCloud_1_1Storage_1_1LCSearchQuery.html) -- [LCSearchSortBuilder](https://leancloud.github.io/csharp-sdk/html/classLeanCloud_1_1Storage_1_1LCSearchSortBuilder.html) -- [LCSearchResponse](https://leancloud.github.io/csharp-sdk/html/classLeanCloud_1_1Storage_1_1LCSearchResponse.html) - - -<> - -- [LCSearchQuery](https://leancloud.github.io/java-unified-sdk) -- [LCSearchSortBuilder](https://leancloud.github.io/java-unified-sdk) - - -<> - -- [LCSearchQuery](https://leancloud.github.io/objc-sdk/Classes/LCSearchQuery.html) -- [LCSearchSortBuilder](https://leancloud.github.io/objc-sdk/Classes/LCSearchSortBuilder.html) - - -<> - -- [LCSearchQuery](https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/LCSearchQuery-class.html) -- [LCSearchSortBuilder](https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/LCSearchSortBuilder-class.html) -- [LCSearchResponse](https://pub.dev/documentation/leancloud_storage/latest/leancloud_storage/LCSearchResponse-class.html) - - -<> - -- [AV.SearchQuery](https://leancloud.github.io/javascript-sdk/docs/AV.SearchQuery.html) -- [AV.SearchSortBuilder](https://leancloud.github.io/javascript-sdk/docs/AV.SearchSortBuilder.html) - - - - - - -## 自定义分词 - -默认情况下, String 类型的字段都将被自动执行分词处理,我们使用的分词组件是 [mmseg](https://github.com/medcl/elasticsearch-analysis-mmseg),词库来自搜狗。但是很多用户由于行业或者专业的特殊性,一般都有自定义词库的需求,因此我们提供了自定义词库的功能。应用创建者可以通过 **云服务控制台 > 数据存储 > 全文搜索 > 自定义词库** 上传词库文件。 - -词库文件要求为 UTF-8 编码,每个词单独一行,文件大小不能超过 512 K,例如: - -``` -面向对象编程 -函数式编程 -高阶函数 -响应式设计 -``` - -将其保存为文本文件,如 `words.txt`,上传即可。上传后,分词将于 3 分钟后生效。开发者可以通过 [`analyze` API](/v2/sdk/storage/guide/fulltext-search-rest#分词结果查询)(要求使用 master key)来测试。 - -自定义词库生效后,**仅对新添加或者更新的文档/记录才有效**,如果需要对原有的文档也生效的话,需要在 **存储** > **全文搜索** 点击「重建索引」按钮,重建原有索引。 -同样,如果更新了自定义词库(包括删除自定义词库),也需要重建索引。 diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/09-fulltext-search-rest.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/09-fulltext-search-rest.mdx deleted file mode 100644 index 881a5affe..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/09-fulltext-search-rest.mdx +++ /dev/null @@ -1,350 +0,0 @@ ---- -id: fulltext-search-rest -title: 全文搜索 REST API -sidebar_label: 全文搜索 REST API ---- - - - -全文搜索服务提供以下 REST API 接口: - -| URL | HTTP | 功能 | -| - | - | - | -| /1.1/search/select | GET | 条件查询 | -| /1.1/search/mlt | GET | moreLikeThis 相关性查询 | -| /1.1/search/analyze | GET | 分词结果查询 | - -在调用全文搜索的 REST API 接口前,需要首先为相应的 Class 启用搜索。 -另外也请参考[数据存储 REST API 使用详解](/v2/sdk/storage/guide/rest)中关于 API Base URL、请求格式、响应格式的说明,以及[全文搜索指南](/v2/sdk/storage/guide/fulltext-search)的《自定义分词》章节。 - -## 条件查询 - -`GET /1.1/search/select` REST API 接口提供全文搜索功能。 - - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/select?q=dennis&limit=200&clazz=GameScore&order=-score" -``` - -返回类似: - -``` json -{ -"hits": 1, -"results": [ - { - "_app_url": "http://stg.pass.com//1/go/com.leancloud/classes/GameScore/51e3a334e4b0b3eb44adbe1a", - "_deeplink": "com.leancloud.appSearchTest://leancloud/classes/GameScore/51e3a334e4b0b3eb44adbe1a", - "_highlight": null, - "updatedAt": "2011-08-20T02:06:57.931Z", - "playerName": "Sean Plott", - "objectId": "51e3a334e4b0b3eb44adbe1a", - "createdAt": "2011-08-20T02:06:57.931Z", - "cheatMode": false, - "score": 1337 - } -], -"sid": "cXVlcnlUaGVuRmV0Y2g7Mzs0NDpWX0NFUmFjY1JtMnpaRDFrNUlBcTNnOzQzOlZfQ0VSYWNjUm0yelpEMWs1SUFxM2c7NDU6Vl9DRVJhY2NSbTJ6WkQxazVJQXEzZzswOw==" -} -``` - -查询的参数支持: - -参数 | 约束 | 说明 ----|---|--- -`q`| 必须 | 查询文本,支持 elasticsearch 的 query string 语法。参见 [q 查询语法](#q- 查询语法)。 -`skip`| 可选 | 跳过的文档数目,默认为 0 -`limit`| 可选 | 返回集合大小,默认 100,最大 1000 -`sid`| 可选 | 之前查询结果中返回的 sid 值,用于分页,对应于 elasticsearch 中的 [scroll id]。 -`fields`| 可选 | 逗号隔开的字段列表,查询的字段列表 -`highlights`| 可选 | 高亮字段,可以是通配符 `*`,也可以是字段列表逗号隔开的字符串。 -`clazz`| 可选 | 类名,如果没有指定或者为空字符串,则搜索所有启用了全文搜索的 class。 -`include`| 可选 | 关联查询内联的 Pointer 字段列表,逗号隔开,形如 `user,comment` 的字符串。**仅支持 include Pointer 类型**。 -`order`| 可选 | 排序字段,形如 `-score,createdAt` 逗号隔开的字段,负号表示倒序,可以多个字段组合排序。 -`sort`| 可选 | 复杂排序字段,例如地理位置信息排序,见下文描述。 - -[scroll id]: https://www.elastic.co/guide/en/elasticsearch/reference/7.4/search-request-body.html#request-body-search-scroll - -返回结果属性介绍: - -- `results`:符合查询条件的结果文档。 -- `hits`:符合查询条件的文档总数 -- `sid`:标记本次查询结果,下次查询继续传入这个 sid 用于查找后续的数据,用来支持翻页查询。 - -返回结果 results 列表里是一个一个的对象,字段是你在全文搜索设置里启用的字段列表,并且有三个特殊字段: - -- `_app_url`:全文搜索结果在网站上的链接。 -- `_deeplink`:全文搜索的程序调用 URL,也就是 deeplink。 -- `_highlight`: 高亮的搜索结果内容,关键字用 `em` 标签括起来。如果搜索时未传入 `highlights` 参数,则该字段为 null。 - -最外层的 `sid` 用来标记本次查询结果,下次查询继续传入这个 sid 将翻页查找后 200 条数据: - -``` sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/select?q=dennis&limit=200&clazz=GameScore&order=-score&sid=cXVlcnlUaGVuRmV0Y2g7Mzs0NDpWX0NFUmFjY1JtMnpaRDFrNUlBcTNnOzQzOlZfQ0VSYWNjUm0yelpEMWs1SUFxM2c7NDU6Vl9DRVJhY2NSbTJ6WkQxazVJQXEzZzswOw" -``` - -直到返回结果为空。 - -### q 查询语法 - -q 参数遵循 elasticsearch 的 [query string 语法](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-query-string-query.html#query-string-syntax)。建议详细阅读这个文档。这里简单做个举例说明。 - -如果你非常熟悉 elasticsearch 的 query string 语法,那么可以跳至[地理位置信息查询](#地理位置信息查询)一节(地理位置查询是我们在 elasticsearch 上添加的扩展功能)。 - -查询的关键字保留字符包括: `+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /`,当出现这些字符的时候,请对这些保留字符做 URL Escape 转义。 - -#### 基础查询语法 - -- 查询某个关键字,例如 `可乐`。 -- 查询**多个关键字**,例如 `可口 可乐`,空格隔开,返回的结果默认按照文本相关性排序,其他排序方法请参考上文中的 [order](#搜索-API) 和下文中的 [sort](#复杂排序)。 -- 查询某个**短语**,例如 `"lady gaga"`,注意用双引号括起来,这样才能保证查询出来的相关对象里的相关内容的关键字也是按照 `lady gaga` 的顺序出现。 -- 根据**字段查询**,例如根据 nickname 字段查询:`nickname:逃跑计划`。 -- 根据字段查询,也可以是短语,记得加双引号在短语两侧: `nickname:"lady gaga"` -- **复合查询**,AND 或者 OR,例如 `nickname:(逃跑计划 OR 夜空中最亮的星)` -- 假设 book 字段是 object 类型,那么可以根据**内嵌字段**来查询,例如 `book.name:clojure OR book.content:clojure`,也可以用通配符简写为 `book.*:clojure`。 -- 查询没有 title 的对象: `_missing_:title`。 -- 查询有 title 字段并且不是 null 的对象:`_exists_:title`。 - -**上面举例根据字段查询,前提是这些字段在 class 的全文搜索设置里启用了索引。** - -#### 通配符和正则查询 - -`qu?ck bro*` 就是一个通配符查询,`?` 表示一个单个字符,而 `*` 表示 0 个或者多个字符。 - -通配符其实是正则的简化,可以使用正则查询: - -``` -name:/joh?n(ath[oa]n)/ -``` - -正则的语法参考 [正则语法](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-regexp-query.html#regexp-syntax)。 - -#### 模糊查询 - -根据文本距离相似度(Fuzziness)来查询。例如 `quikc~`,默认根据 [Damerau–Levenshtein 文本距离算法](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance)来查找最多两次变换的匹配项。 - -例如这个查询可以匹配 `quick`、`qukic`、`qukci` 等。 - -#### 范围查询 - -``` -// 数字 1 到 5: -count:[1 TO 5] - -// 2012 年内 -date:[2012-01-01 TO 2012-12-31] - -//2012 年之前 -{* TO 2012-01-01} -``` - -`[]` 表示闭区间,`{}` 表示开区间。 - -还可以采用比较运算符: - -``` -age:>10 -age:>=10 -age:<10 -age:<=10 -``` - -#### 查询分组 - -查询可以使用括号分组: - -``` -(quick OR brown) AND fox -``` - -#### 特殊类型字段说明 - -- objectId 在全文搜索的类型为 string,因此可以按照字符串查询: `objectId: 558e20cbe4b060308e3eb36c`,不过这个没有特别必要了,你可以直接走 SDK 查询,效率更好。 -- createdAt 和 updatedAt 映射为 date 类型,例如 `createdAt:["2015-07-30T00:00:00.000Z" TO "2015-08-15T00:00:00.000Z"]` 或者 `updatedAt: [2012-01-01 TO 2012-12-31]` -- 除了 createdAt 和 updatedAt 之外的 Date 字段类型,需要加上 `.iso` 后缀做查询: `birthday.iso: [2012-01-01 TO 2012-12-31]` -- Pointer 类型,可以采用 `字段名.objectId` 的方式来查询: `player.objectId: 558e20cbe4b060308e3eb36c and player.className: Player`,pointer 只有这两个属性,全文搜索不会 include 其他属性。 -- Relation 字段的查询不支持。 -- File 字段,可以根据 url 或者 id 来查询:`avatar.url: "https://leancloud.cn/docs/app_search_guide.html#搜索_API"`,无法根据文件内容做全文搜索。 - -### 复杂排序 - -假设你要排序的字段是一个数组,比如分数数组 `scores`,你想根据平均分来倒序排序,并且没有分数的排最后,那么可以传入: - -``` sh ---data-urlencode 'sort=[{"scores":{"order":"desc","mode":"avg","missing":"_last"}}]' -``` - -也就是 `sort` 可以是一个 JSON 数组,其中每个数组元素是一个 JSON 对象: - -``` json -{"scores":{"order":"desc","mode":"avg","missing":"_last"}} -``` - -排序的字段作为 key,字段可以设定下列选项: - -- `order`:`asc` 表示升序,`desc` 表示降序 -- `mode`:如果该字段是多值属性或者数组,那么可以选择按照最小值 `min`、最大值 `max`、总和 `sum` 或者平均值 `avg` 来排序。 -- `missing`:决定缺失该字段的文档排序在开始还是最后,可以选择 `_last` 或者 `_first`,或者指定一个默认值。 - -多个字段排序就类似: - -``` json -[ - { - "scores":{"order":"desc","mode":"avg","missing":"_last"} - }, - { - "updatedAt": {"order":"asc"} - } -] -``` - -### 地理位置信息查询 - -如果 class 里某个列是 `GeoPoint` 类型,那么可以根据这个字段的地理位置远近来排序,例如假设字段 `location` 保存的是 `GeoPoint` 类型,那么查询 `[39.9, 116.4]` 附近的玩家可以通过设定 sort 为: - -``` json -{ - "_geo_distance" : { - "location" : [39.9, 116.4], - "order" : "asc", - "unit" : "km", - "mode" : "min", - } -} -``` - -`order` 和 `mode` 含义跟上述复杂排序里的一致,`unit` 用来指定距离单位,例如 `km` 表示千米,`m` 表示米,`cm` 表示厘米等。 - -## moreLikeThis 相关性查询 - -除了 `/1.1/search/select` 之外,我们还提供了 `/1.1/search/mlt` API 接口,用于相似文档的查询,可以用来实现相关性推荐。 - -假设我们有一个 Class 叫 `Post` 是用来保存博客文章的,我们想基于它的标签字段 `tags` 做相关性推荐: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/mlt?like=clojure&clazz=Post&fields=tags" -``` - -我们设定了 `like` 参数为 `clojure`,查询的相关性匹配字段 `fields` 是 `tags`,也就是从 `Post` 里查找 `tags` 字段跟 `clojure` 这个文本相似的对象,返回类似: - -``` json -{ -"results": [ - { - "tags":[ - "clojure", - "数据结构与算法" - ], - "updatedAt":"2016-07-07T08:54:50.268Z", - "_deeplink":"cn.leancloud.qfo17qmvr8w2y6g5gtk5zitcqg7fyv4l612qiqxv8uqyo61n:\/\/leancloud\/classes\/Article\/577e18b50a2b580057469a5e", - "_app_url":"https:\/\/leancloud.cn\/1\/go\/cn.leancloud.qfo17qmvr8w2y6g5gtk5zitcqg7fyv4l612qiqxv8uqyo61n\/classes\/Article\/577e18b50a2b580057469a5e", - "objectId":"577e18b50a2b580057469a5e", - "_highlight":null, - "createdAt":"2016-07-07T08:54:13.250Z", - "className":"Article", - "title":"clojure persistent vector" - }, - // …… -], -"sid": null -} -``` - -除了可以通过指定 `like` 这样的相关性文本来指定查询相似的文档之外,还可以通过 likeObjectIds 指定一个对象的 objectId 列表,来查询相似的对象: - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - "https://{{host}}/1.1/search/mlt?likeObjectIds=577e18b50a2b580057469a5e&clazz=Post&fields=tags" -``` - -这次我们换成了查找和 `577e18b50a2b580057469a5e` 这个 objectId 指代的对象相似的对象。 - -更详细的查询参数说明: - -参数 | 约束 | 说明 ----|---|--- -`clazz`| 必须 | 类名 -`like`| 可选 |**和 `likeObjectIds` 参数二者必须提供其中之一**。代表相似的文本关键字。 -`likeObjectIds`| 可选 |**和 `like` 参数二者必须提供其中之一**。代表相似的对象 objectId 列表,用逗号隔开。 -`min_term_freq`| 可选 |**文档中一个词语至少出现次数,小于这个值的词将被忽略,默认是 2**,如果返回文档数目过少,可以尝试调低此值。 -`min_doc_freq`| 可选 |**词语至少出现的文档个数,少于这个值的词将被忽略,默认值为 5**,同样,如果返回文档数目过少,可以尝试调低此值。 -`max_doc_freq`| 可选 | 词语最多出现的文档个数,超过这个值的词将被忽略,防止一些无意义的热频词干扰结果,默认无限制。 -`skip`| 可选 | 跳过的文档数目,默认为 0 -`limit`| 可选 | 返回集合大小,默认 100,最大 1000 -`fields`| 可选 | 相似搜索匹配的字段列表,用逗号隔开,默认为所有索引字段 `_all` -`include`| 可选 | 关联查询内联的 Pointer 字段列表,逗号隔开,形如 `user,comment` 的字符串。**仅支持 include Pointer 类型**。 - -更多内容参考 [Elasticsearch 文档](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-mlt-query.html)。 - -## 分词结果查询 - -全文搜索会对 String 类型的字段自动进行分词处理。 -如果发现搜索结果不符合预期,推荐先通过 `analyze` API 检查分词结果(要求使用 master key)。 -`analyze` API 也用于验证自定义词库是否生效。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - "https://{{host}}/1.1/search/analyze?clazz=GameScore&text=响应式设计" -``` - -参数包括 `clazz` 和 `text`。`text` 就是测试的文本段,返回结果: - - -```json -{ - "tokens": [ - { - "token": "响应", - "start_offset": 0, - "end_offset": 2, - "type": "word", - "position": 0 - }, - { - "token": "式", - "start_offset": 2, - "end_offset": 3, - "type": "word", - "position": 1 - }, - { - "token": "设计", - "start_offset": 3, - "end_offset": 5, - "type": "word", - "position": 2 - } - ] -} -``` - -可以看到,分词系统将「响应式设计」分为了三个词。 -如果分词系统认为「响应式设计」是一个词(比如上传了包含「响应式设计」一词的自定义词库),那么返回结果会是: - -```json -{ - "tokens": [ - { - "token": "响应式设计", - "start_offset": 0, - "end_offset": 5, - "type": "word", - "position": 0 - } - ] -} -``` \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/10-security.mdx b/versioned_docs/version-v2/sdk/08-storage/02-guide/10-security.mdx deleted file mode 100644 index dac8aa050..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/10-security.mdx +++ /dev/null @@ -1,132 +0,0 @@ ---- -id: security -title: 数据和安全 ---- - - - -几乎每一位使用云服务的开发者都会问,如何能够保证自己应用数据的安全?对安全的关注说明你也是位对产品负责、对用户负责、对自己负责、做事态度认真的开发者,这也正是我们所信守的价值观。 - -云服务所有的 API 请求都通过 SSL 加密传输,保证传输过程中的数据安全性和可靠性。 -云端对客户端发过来的每一个请求,都进行了身份鉴别(Authentication)和访问授权(Authorization)的严格检查。 - -## 身份鉴别(Authentication) - -访问云服务 API 需要提供应用的 `AppID` 和 AppKey(用于在客户端发起请求),或是 `AppID` 和 MasterKey(用于在云引擎、开发者自己的服务器等受信任的环境发起请求)。 -Android SDK 还额外支持仅使用 `AppID` 访问云服务 API. - -使用 MasterKey 访问云服务 API 会跳过所有的访问权限控制,所以请确保 MasterKey 不会泄露。 -万一发生泄露,请立即前往**云服务控制台 > 设置 > 应用凭证 > Credentials**重置 MasterKey。 - -在客户端使用 AppKey 访问云服务 API 时: - -1. 客户端访问 API 通过 HTTPS 加密通讯。 -2. 通过 SDK 访问 API 时,HTTP Header 中不直接传输 AppKey,而是传输客户端根据 AppKey 和请求发起时间生成的签名字符串,供云端校验。开发者不通过 SDK,直接请求 REST API 时,也可以使用[这种更安全的鉴权方式](/v2/sdk/storage/guide/rest#更安全的鉴权方式)。 - -通过 HTTPS 加密通讯,不仅保护了数据安全,而且能避免 AppKey 被第三方窃取,但无法阻止别有用心的人通过在客户端使用抓包工具获取 AppKey。 -通过签名字符串可以避免在网络传输过程中泄露 AppKey,但是无法阻止别有用心的人通过反编译等手段获取代码中初始化 SDK 时设定的 AppKey。 -Android SDK 提供仅使用 `AppID` 的初始化方式,SDK 通过闭源的 native library 根据开发者配置的应用签名证书自动对请求进行签名。 -这在一定程度上避免了暴露应用核心配置信息,也显著增加了破解的难度(反编译 native library 后仍需破解加密算法),但仍无法保证绝对的数据安全。 -因此,**凡是需要在客户端访问的应用,都需要设定 ACL 等权限设置,以保障应用数据的安全性**。 - -## 访问授权(Authorization) - -云服务支持在 Class(表)、字段(列)、对象三个不同的层次设置访问权限。 - -### Class 权限 - -Class 权限指整个 Class(整张表)的读写权限,需要在控制台进行设置。 - -在创建 Class 对话框可以设置 Class 的访问权限: - -![创建 Class 对话框](/img/security/class-permissions.png) - -- `add_fields` - 给 Class 增加新的字段,也就是说,保存对象时,如果对应的字段(列)不存在,是否允许自动创建新的字段。如果已经在控制台创建好 Class 的所有字段,最好对任意用户都关闭此权限,防止脏数据写入。 -- `create` - 在 Class 表中插入一个新对象。对于需要登录用户或者拥有指定授权的用户才能创建内容的场景,可以考虑根据情况设置此权限。 -- `delete` - 删除既有对象。 -- `update` - 修改既有对象。 -- `find` - 通过指定条件查询对象。关闭此权限时,无法查询对象,只能通过 objectId 获取对象(如果没有同时关闭 `get` 权限),一定程度上可以防范别有用心的人批量抓取该 Class 下的所有对象。 -- `get` - 通过 objectId 获取单个对象。 - -对于每一种权限,又可以开放给不同用户: - -* 所有用户 - 相当于 public 权限。这里的「用户」泛指「客户端」,而不是云服务内建账户系统的用户。也就是说,无论请求是否携带 sessionToken,携带的 sessionToken 是否有效,都可以进行这一操作, -* 登录用户 - 只有使用云服务内建账户系统(`_User` 表)并且进行了登录的客户端(携带了有效 sessionToken 的请求),才可以执行这一操作。 -* 指定用户 - 指定用户才可以执行这一操作。支持通过用户名、用户的 objectId、角色名、角色的 objectId 指定用户。如果留空,那么意味着所有用户都没有权限。 - -譬如我们有一个匿名发帖的应用,所有人都可以发表帖子,但是只有经过管理员审核后的帖子才能被展示出来。我们可以有两张表,第一张表用来存放审核前的帖子,这张表的 create 权限就可以开放给所有用户;第二张表用来存放审核后的帖子,这张表的 create 权限就只开放给管理员。 - -另外,你也可以修改现有 Class 的访问权限。 -进入**云服务控制台 > 数据存储 > 结构化数据**,选择一个 Class,再点击**权限**标签页。 - -以 `_` 开头的 Class 有一些默认的基本设置。 -以用于即时通讯服务的 `_Conversation` Class 为例:由于即时通讯有封装好的 SDK 和专门的 REST API,客户端几乎没有直接操作相应的结构化数据存储 Class 的需要,所以这个 Class 默认对所有人关闭 Class 访问权限,直接对该 Class 进行的增删查改操作仅限通过控制台或 `Master Key` 进行。 -不过为了兼顾开发测试阶段的便利性以及老应用的兼容性,默认设置比较宽松。 -因此,开发者仍然需要根据项目的具体需求,设置这些 Class 的访问权限,才能保障安全。 - -### 字段权限 - -在控制台还可以设置每个字段的权限。 -访问 **云服务控制台 > 数据存储 > 结构化数据**,选择一个 Class,点击相应字段的下拉菜单,选择 **编辑**: - -1. 勾选 **只读** 后,客户端无法更新这一字段。 -2. 勾选 **客户端不可见** 后,客户端发起查询或获取对象的时候,返回的结果将不包含这一字段。比如,对于一个匿名发帖的应用,你仍然希望发帖的时候记录下真实的作者,但不希望将此信息返回给客户端。 - -### 对象权限 - -每个对象都有一个特殊的 ACL(Access Control List)属性。 -ACL 允许开发者设定某个具体对象的权限,是最精细的权限控制方式。 - -比如: - -* 对于私有数据,read 和 write 都可以限制为对象创建者所有(其读写权限都设置成创建者自身,并且不开放「所有用户」的读、写权限)。 -* 一个信息公告板的帖子,作者和属于「版主」角色的成员可拥有修改权限,普通访客则只允许 浏览(给「所有用户」开放「读」权限,给「作者」和「版主角色」开放「写」权限)。 -* 应用内全局的每日头条,对所有用户是只读的,只有管理员可以修改它(给「所有用户」开放「读」权限,给「管理员角色」开放「写」权限)。 -* 用户共享一篇文章给另一个用户,可以将读和写的访问许可限制到关联的这两个用户,其他人一概不可读写(对「所有用户」关闭「读、写」权限,只对两个用户开放权限)。 - -详细信息请参考[《ACL 权限管理指南》](https://leancloud.cn/docs/acl-guide.html)。 - -## 安全设置 - - -**云服务控制台 > 数据存储 > 服务设置 > 安全设置** 中可以设置是否开启 LiveQuery。 - -出于安全考虑,有些应用只在服务端调用接口向用户推送通知。 -对于这些应用,可以访问 **云服务控制台 > 推送 > 设置** 勾选 **禁止从客户端进行消息推送**。 -勾选后客户端无法推送消息,只能通过 MasterKey 调用 REST API 发送或在控制台发送(**云服务控制台 > 推送 > 在线发送**)。 - -同理,有些应用只在服务端调用接口向用户发送短信。 -对于这些应用,可以访问 **云服务控制台 > 短信 > 设置**,不勾选 **启用通用的短信验证码服务(开放 requestSmsCode 和 verifySmsCode 接口)**。 -这种情况下,应用仍然能在服务端通过 MasterKey 调用 `requestSmsCode` 发送短信。 -另外,与用户相关的短信接口与此选项无关。不勾选的情况下,客户端仍能调用用户相关的短信接口。 - - - - - -**云服务控制台 > 数据存储 > 服务设置 > 安全设置** 下可以设置 **禁止客户端创建 Class**。有些应用的开发者习惯预先规划好应用需要用到的 Class(表),并事先在控制台创建相应的 Class。对于这些开发者,推荐始终勾选这一选项。有些应用的开发者习惯直接着手开发应用的原型,在开发过程中逐渐完善数据结构。对于这些开发者,我们推荐在开发测试阶段不勾选这一选项,在应用上线前勾选这一选项。 - -**云服务控制台 > 数据存储 > 服务设置 > 查询设置** 下可以设置 **查询 include 引入的 Pointer 类型数据时校验 ACL 权限**。我们建议所有应用都勾选这一选项(这一选项默认处于勾选状态),以保证数据安全。 - -**云服务控制台 > 数据存储 > 用户 > 设置** 页面下有一些用户相关的安全选项,可以禁止未验证邮箱、手机的用户登录,要求修改密码时提供旧密码,密码修改后强制重新登录,验证第三方登录的 `Access Token` 是否有效。 - -**云服务控制台 > 数据存储 > 文件 > 设置** 可以限制用户上传的文件类型。 - -## 操作日志 - -**云服务控制台 > 设置 > 操作日志**会显示应用创建者及所有协作者的重要操作记录,比如删除数据操作的历史、操作用户名、操作 IP 及操作时间等,这个日志的目的是为了遇到问题更好地定位故障缘由,排查可能的恶意操作,防止应用数据被错误地改动。 - -## 自动备份 - -应用每日自动备份,云服务会保留最近 7 天的备份。 -商用版应用可以在 **云服务控制台 > 数据存储 > 导入导出 > 备份恢复** 恢复最近 7 天的数据(还可以指定需要恢复的 Class 或 objectId)。 -所有应用都可以在 **云服务控制台 > 数据存储 > 导入导出 > 备份导出** 下载备份(可以指定需要下载的备份日期、Class)。 - -注意: - -- 开发版不支持数据恢复。 -- 已删除的文件无法恢复。 -- 恢复操作只会插入数据,如果有被变更过的数据需要恢复,请先把目标数据自行备份并删除后再提交任务。 -- 如需恢复删除的 Class,需要在控制台手动创建一个同名的空 Class,并手动添加相应列。 - -此外,开发者还可以使用数据导出功能将应用数据备份到本地,该功能商用版、开发版均可用。 diff --git a/versioned_docs/version-v2/sdk/08-storage/02-guide/_category_.json b/versioned_docs/version-v2/sdk/08-storage/02-guide/_category_.json deleted file mode 100644 index 586e68f44..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/02-guide/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/08-storage/_category_.json b/versioned_docs/version-v2/sdk/08-storage/_category_.json deleted file mode 100644 index d74c6b574..000000000 --- a/versioned_docs/version-v2/sdk/08-storage/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "数据存储", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/09-engine/01-features.mdx b/versioned_docs/version-v2/sdk/09-engine/01-features.mdx deleted file mode 100644 index 7e2290eb5..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/01-features.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -id: features -title: 云引擎功能介绍 -sidebar_label: 功能介绍 ---- - - -云引擎是基于 Docker 的容器云计算平台。云引擎既可以被简单地用来托管静态网站,又可以接受任意程序语言的定制开发来动态处理外来请求,满足业务定制化需求。 - -## 实用功能 - -你只需要提供服务端的业务逻辑(网站或云函数等),而服务端的多实例负载均衡,不中断服务的平滑升级等都由云引擎提供支持。用最少代码,提供最大价值。 - -### Hook 函数 - -类似于数据库 trigger,可以在特定操作或事件(如数据更新、用户登录)发生前后,自动触发,完成额外的业务处理。 - -### 云函数 - -定义自己的 HTTP Endpoint,可以将复杂的客户端逻辑独立成一个公共的「云端函数」,让业务灵活可控。 - -### 定时任务 - -周期性执行云函数,比如定时清理过期数据、每周一向所有用户发送推送消息等等,让业务自动化运行。 - -### LeanCache - -基于分布式 Redis 集群技术,可承载高达 70,000 QPS 的峰值流量,轻松应对抢红包、秒杀购物等高并发场景。 - -### 实例分组管理 - -将云引擎实例按照用途分组,实现在访问同一数据源的情况下,部署多份云引擎代码,满足不同的业务需求。 - -### 灵活的部署方式 - -支持在线编写代码、通过命令行工具部署本地项目、以及部署远程 Git 源码等方式,让开发流程更简单。 - -## 优势和特色 - -- 基于 Docker 的容器云。项目代码运行在独立的 Docker 容器中,资源隔离,可以平滑部署、弹性扩容。 - -- 自动负载均衡。基于自动服务发现技术,多个云引擎实例之间实时进行负载均衡,在系统意外发生时会自动进行故障转移,提供超过 99.9% 的高可用性。 - -- 支持主流开发语言。支持 Node.js、Python、Java、PHP 等主流后端语言和运行时,并提供多种类型项目模版,10 分钟内即可完成项目发布。 - diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/00-overview.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/00-overview.mdx deleted file mode 100644 index 334ce68ce..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/00-overview.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: overview -title: 云引擎服务总览 -sidebar_label: 总览 ---- - - - -云引擎提供了 Node.js、Python、PHP、Java、.NET、Go 等多种环境来运行服务端程序。你只需要提供服务端的业务逻辑(网站或云函数等),而服务端的多实例负载均衡,不中断服务的平滑升级等都由云引擎提供支持。 - -下面为你了解云引擎提供一个索引: - -* [快速入门](/v2/sdk/engine/guide/quickstart):快速的了解如何创建一个云引擎项目,本地开发调试,以及如何部署到云端。 -* [运行方案](/v2/sdk/engine/guide/plan):了解云引擎对较大的商业应用如何提供多实例以及运维支持,以及对学习和测试应用的支持。 -* [网站托管](/v2/sdk/engine/guide/webhosting):使用你熟悉的语言开发一个 web 程序,提供静态和动态路由,开发一个 web 站点;或者为移动应用提供一个介绍页;或者开发一个管理员控制台。 -* [云函数](/v2/sdk/engine/guide/cloudfunction):可以将各平台客户端(Andorid、 iOS、浏览器等)的一些公共逻辑独立成一个公共的方法在云引擎中实现,各个 SDK 直接调用该方法,各平台客户端直接调用,并且云引擎更新相比客户端更新更加灵活容易;或者需要一些 Hook 函数,比如用户注册后执行一段业务逻辑;或设置一个定时任务在每天晚上清理数据等。 -* [命令行工具 CLI](/v2/sdk/engine/guide/cli):用来管理和部署云引擎项目的工具。它不仅可以部署、发布和回滚云引擎代码,对同一个云引擎项目做多应用管理,还能查看云引擎日志,批量将文件上传到 LeanCloud 云端。 -* [云引擎 REST API 指南](/v2/sdk/engine/guide/rest):直接通过 REST API 接口调用云函数。 -* [LeanCache 指南](/v2/sdk/engine/guide/redis):云引擎应用可以访问 LeanCache。LeanCache 使用 Redis 来提供高性能、高可用的 Key-Value 内存存储,可以在一些特殊场景(如秒杀、抢红包等)为应用提供很好的性能表现;或者对读写比例很高的数据做缓存,减少对存储服务的压力,提高应用性能表现。 -* LeanDB 指南:除了 Redis 外,在云引擎中可以根据需要来配置 [MySQL](/v2/sdk/engine/guide/mysql)、[MongoDB](/v2/sdk/engine/guide/mongo)、[Elasticsearch](/v2/sdk/engine/guide/es) 等不同数据库,相信可以让更多的现有(开源)方案直接跑在云引擎里面,给大家带来更多的便利。 -* [云队列(Cloud Queue)指南](/v2/sdk/engine/guide/cloudqueue):云队列实现了重试、去重、结果查询、延时任务、定时任务等功能,是对云函数功能的一个补充。尚未运行的任务会以一种可靠的方式暂存在云队列,即使你的云引擎因部署、过载、崩溃而重启,任务也不会丢失,云队列会等待你的云引擎实例恢复正常后继续运行它们。 -* [云引擎常见问题解答](/v2/sdk/engine/guide/faq):希望大多数关于云引擎的问题,都能在这里找到答案。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/01-quickstart.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/01-quickstart.mdx deleted file mode 100644 index bb19349b7..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/01-quickstart.mdx +++ /dev/null @@ -1,312 +0,0 @@ ---- -id: quickstart -title: 云引擎快速入门 -sidebar_label: 快速入门 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - - - -该文档帮助你快速的了解如何创建一个云引擎项目,本地开发调试,以及如何部署到云端。 - -## 创建项目 - -请根据《云引擎命令行工具使用指南》的[《安装命令行工具》](/v2/sdk/engine/guide/cli#安装命令行工具)一节安装最新版的命令行工具,并确保你已经在本地机器上可以成功运行命令行工具: - -```sh -tds help -``` - -如果一切正常,你应该看到命令行工具的帮助信息。 - -如果你尚未登录,请根据《云引擎命令行工具使用指南》的[《登录》](/v2/sdk/engine/guide/cli#登录)一节完成登录。 - -然后使用命令行工具创建项目: - -```sh -tds init -``` - -根据提示输入相关信息,即可基于模板项目创建你的云引擎项目。 - -## 本地运行 - -首先在当前项目的目录下安装必要的依赖,执行如下命令行: - - - -```javascript -npm install -``` -```python -pip install -Ur requirements.txt -``` -```php -composer install -``` -```java -mvn package -``` -```cs -// 需要安装 global.json 文件中指定的 .NET SDK 版本 -``` -```go -go mod tidy && go mod vendor -``` - - - -然后启动应用: - -```sh -tds up -``` - -## 访问站点 - -打开浏览器访问 会显示如下内容: - - - -```javascript -LeanEngine - -这是 LeanEngine 的示例应用 - -当前时间:Mon Feb 01 2016 18:23:36 GMT+0800 (CST) - -一个简单的「TODO 列表」示例 -``` -```python -LeanEngine - -这是 LeanEngine 的示例应用 - -一个简单的动态路由示例 - -一个简单的「TODO 列表」示例 -``` -```php -LeanEngine - -这是 LeanEngine 的示例应用 - -当前时间:2016-07-25T14:55:17+08:00 - -一个简单的「TODO 列表」示例 -``` -```java -LeanEngine - -这是 LeanEngine 的示例应用 - -一个简单的动态路由示例 - -一个简单的「TODO 列表」示例 -``` -```cs -Welcome - -Learn about building Web apps with ASP.NET Core. -``` -```go -LeanEngine - -This is a LeanEngine demo application. - -Current date: 2021-02-28 23:54:47.821183329 +0800 CST m=+1.093390203 - -A simple todo demo -``` - - - -访问页面的路由定义如下: - - - -```javascript -// ./app.js -// ... - -app.get('/', function(req, res) { - res.render('index', { currentTime: new Date() }); -}); - -// ... -``` -```python -# ./app.py -# ... - -@app.route('/') -def index(): - return render_template('index.html') - -# ... -``` -```php -// ./src/app.php -// ... - -$app->get('/', function (Request $request, Response $response) { - return $this->view->render($response, "index.phtml", array( - "currentTime" => new \DateTime(), - )); -}); - -// ... -``` -```java -// ./src/main/webapp/WEB-INF/web.xml -// ... - - - index.html - - -// ... -``` -```cs -// ./web/Startup.cs -// ... -app.UseEndpoints(endpoints => { - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); -}) -// ... -``` -```go -// ./main.go -// ... -e.GET("/", routes.Index) -// ... - -// ./routes/index.go -// ... -func Index(c echo.Context) error { - return c.Render(http.StatusOK, "index", time.Now().String()) -} -// ... -``` - - - -### 新建一个 Todo - -用浏览器打开 ,然后在输入框输入 「点个外卖」并点击 「新增」,可以看到 Todo 列表新增加了一行。 - -打开控制台选择对应的应用,可以看到在 Todo 表中会有一个新的记录,它的 `content` 列的值就是刚才输入的「点个外卖」。 - -详细的实现细节请阅读源代码,里面有完整的代码以及注释帮助开发者理解如何在 LeanEngine 上编写符合自己项目需求的代码。 - -注:.NET 模板项目暂未实现 Todo 功能演示。 - -### 新建一个云函数 - -云引擎的云函数可以实现一些更适合在服务端实现的功能,例如需要灵活调整逻辑、避免消耗客户端计算资源、需要更高权限来执行等情况。 - -例如,编写一个新建 Todo 的云函数: - - - -```js -// 在项目的 cloud.js 文件中新增一个云函数定义 -AV.Cloud.define('createTodo', async (request) => { - const Todo = AV.Object.extend('Todo'); - const todo = new Todo(); - todo.set('content', request.params.content); - return todo.save(); -}); -``` -```python -# 在项目的 cloud.py 文件中新增一个云函数定义 -@engine.define('createTodo') -def create_todo(content, **params): - import leancloud - todo = leancloud.Object.extend('Todo')() - todo.set('content', content) - return todo.save() -``` -```php -// 在项目的 src/cloud.php 文件中新增一个云函数定义 -use \LeanCloud\LeanObject; -Cloud::define("createTodo", function($params, $user) { - $todo = new LeanObject("Todo"); - $todo->set("content", $params["content"]); - $todo->save(); -}); -``` -```java -// 在项目的 src/main/java/cn/leancloud/demo/todo/Cloud.java 文件开头导入云函数定义中用到的类 -import cn.leancloud.AVException; -import cn.leancloud.AVObject; -// 在上述文件的 Cloud 类中新增一个方法 -@EngineFunction("createTodo") -public static void createTodo(@EngineFunctionParam("content") String content) - throws AVException { - AVObject todo = new AVObject("Todo"); - todo.put("content", content); - todo.save(); -} -``` -```cs -// 在项目的 web/App.cs 文件开头导入云函数定义中用到的类 -using System.Threading.Tasks; -using LeanCloud.Storage; -// 在上述文件的 App 类中新增一个方法 -[LCEngineFunction("createTodo")] -public static async Task createTodo([LCEngineFunctionParam("content")] string content) { - LCObject todo = new LCObject("Todo"); - todo["content"] = content; - await todo.Save(); -} -``` -```go -// 在云函数文件中导入 leancloud 包 -// 建议在项目的 functions 文件夹中定义云函数 -import "github.com/leancloud/go-sdk/leancloud" - -type Todo struct { - leancloud.Object - Content string `json:"content"` -} - -func init() { - leancloud.Define("createdTodo", createTodo) // 注册云函数 -} - -func createTodo(req *leancloud.FunctionRequest) (interface{}, error) { - todo := &Todo{ - Content: req.Params["content"].(string), - } - if err := client.Class("Todo").Create(todo); err != nil { - return nil, err - } - return nil, nil -} -``` - - - -还有一类特殊的云函数是由云端系统在特定事件发生时自动触发,这类云函数称为 Hook 函数。 -想要了解 Hook 函数的详情以及如何调用我们上面定义的 `createTodo` 云函数,请参考[云函数指南](/v2/sdk/engine/guide/cloudfunction)。 - -## 部署到云端 - -使用免费版的应用可以直接部署到生产环境: - -```sh -tds deploy -``` - -如果生产环境是标准实例,需要加上 `--prod 1` 参数,指定部署到生产环境: - -```sh -tds deploy --prod 1 -``` - -你可以在控制台绑定云引擎域名,绑定域名后,即可通过绑定域名访问你的应用。 -例如,假定你在控制台绑定了 `web.example.com` 这个域名,即可通过 `https://web.example.com` 访问你的应用(生产环境)。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/02-plan.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/02-plan.mdx deleted file mode 100644 index 93cc03aca..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/02-plan.mdx +++ /dev/null @@ -1,165 +0,0 @@ ---- -id: plan -title: 云引擎运行方案 -sidebar_label: 运行方案 ---- - - - -这篇文档主要介绍云引擎的付费方案、如何管理云引擎的资源、如何使用组管理功能。 - -云引擎的计费独立于开发版 / 商用版方案,开通或取消商用版不影响云引擎的实例和计费。 - -## 标准实例 - -对于商业项目和正式上线的产品,我们建议开发者购买标准实例。标准实例 24 小时运行随时待命,无请求时也不会休眠,并配有预备环境方便测试。如果购买了两个或更多的实例,还能进行负载均衡和故障切换,充分保障服务的可用性。 - -
    -
    预备环境
    -
    -

    云引擎会为标准实例赠送一个预备环境,它有着和生产环境几乎完全一样的运行环境。在正式上线前,开发者可以先将代码发布到预备环境,使用线上的环境和数据进行模拟测试。

    -
    -
    负载均衡
    -
    -

    云引擎的网关会将客户端的请求轮流分配给每个实例,随着业务请求量的增加,开发者可以简单地通过增加实例数量来提升处理能力。

    -
    -
    故障切换(高可用)
    -
    -

    在同一分组的同一环境中有两个或更多的实例时,便可实现故障切换。当其中一个实例出现故障无法工作时,云引擎的网关会自动将接下来的请求转发到其他可以正常工作的实例上,等待故障的实例恢复后复原。

    -

    -使用单个实例无法实现故障切换,当这个唯一的实例出现故障时,该实例会在几分钟之内被重新部署,在此期间该实例无法对客户端的请求做出响应。因此,我们建议对服务可用性要求较高的应用使用两个以上的云引擎实例。 -

    -
    -
    平滑部署
    -
    -

    在部署新版本或其他运维操作时,系统会让新旧版本的实例同时运行一段时间,再关闭旧版本的实例,让服务保持零中断。

    -
    -
    创建分组
    -
    -

    将实例分组可以实现在访问同一数据源的情况下,部署多份云引擎代码,满足不同的业务需求。每个组可以绑定独立域名。详见 [组管理](#组管理)。

    -
    -
    - -## 体验实例 - -云引擎对于每个应用都默认赠送一个 0.5 CPU / 256 MB 的体验实例,可以免费使用,供开发者学习和测试云引擎。 - -云引擎对于每个购买了标准实例的分组还会赠送一个包含相同规格体验实例的预备环境,可以用作正式上线前的测试。 - -体验实例在进行部署等管理操作时会暂停服务。 - -同时 **体验实例会执行休眠策略**,没有请求时会休眠,有请求时启动(首次启动可能需要十几秒的时间),每天最多运行 18 个小时,详见[云引擎 FAQ](/v2/sdk/engine/guide/faq)。 - -如果不希望预备环境的体验实例因为强制休眠而中断服务,或需要多个实例来完整模拟生产环境,可以在预备环境根据需要购买标准实例。 - -## 实例管理 - -我们为标准版实例提供了几种不同的规格,差别主要体现在可供使用的内存上: - -| 规格 | 内存 | CPU | -| ------------- | ------- | ------ | -| standard-512 | 512 MB | 1 Core | -| standard-1024 | 1024 MB | 1 Core | -| standard-2048 | 2048 MB | 1 Core | -| standard-4096 | 4096 MB | 1 Core | - -开发者可以根据自己的程序运行时所需要的最大内存来选择实例规格,然后通过调整实例数量来应对请求量的增加。 - -为了防止实例因为资源使用超限而受到影响,我们建议: - -- 一天内平均 **内存** 使用超过可用资源的 **70%**(例如对于 1 个 standard-1024 来说就是 717 MB)就建议提高实例规格。 -- 一天内平均 **CPU** 使用超过可用资源的 **30%**(例如对于 1 个 standard-1024 来说就是 30% CPU)就建议增加实例数量。 - -要了解实例资源的耗用程度,请阅读 [查看资源用量](#查看资源用量)。 - -### 修改实例规格(升级到标准版) - -在云引擎的资源页面,点击实例规格旁边的「更改」按钮,选择需要的规格。 - -在从免费版切换到标准版时,还需要选择要购买的标准实例数量。 - -### 修改实例数量 - -在云引擎的资源页面,点击预备环境或生产环境旁边的「更改」按钮,设置需要的数量。 - -### 退回免费版 - -如果不想继续付费,可以在所有分组中将实例规格修改为「免费版」,标准实例会被删除,系统会停止扣费,并在最后一个分组下赠送一个 0.5 CPU / 256 MB 的体验实例。 - -### 多实例运行 - -当一个分组中的一个环境里有多个实例时,我们称之为「多实例运行」。在部署或者运维操作(如重启)期间,也会有多个实例短暂地同时运行。 - -多个实例的内存和文件系统是独立的,这意味着在一个实例中写入全局变量或文件,其他实例无法读取,建议在首次切换到多实例运行时进行充分的测试。 - -如果需要在多个实例间低延迟地共享数据,可以使用 LeanCache。 - -### 查看资源用量 - -**云服务控制台 > 云引擎 > 云引擎分组 > 统计** 页面显示出当前应用下所有实例资源的使用情况,开发者由此可以判断实例资源是否即将或已经超限。 - -
    -
    每分钟请求数
    -
    -

    一段时间内云引擎每分钟处理的请求数。可以通过左上角的下拉菜单选择按不同请求类型(网站托管、云函数)和不同 HTTP 状态码分别查看。

    -
    -
    响应时间
    -
    -

    若曲线图升高,说明 CPU 使用量达到或超过限制,实例的 CPU 资源紧张。同样可以按不同请求类型(网站托管、云函数)和不同 HTTP 状态码分别查看。

    -
    -
    CPU
    -
    -

    曲线图展现出在一段时间内应用实际 CPU 的使用量。如果 CPU 接近或达到限制,应用表现为请求响应时间延长。

    -
    -
    内存
    -
    -

    曲线图展现出在一段时间内应用内存的实际使用量。如果内存使用量达到限制,则相关的业务进程(比如 Node.js 进程或者 Python 进程)会因为内存溢出(OOM)而重启,从而导致业务处理中断,并且该实例在启动期间服务不可用。如果内存曲线图频繁地接近限制然后忽然下降,就说明内存使用超限导致了进程重启。

    -
    -
    显示各实例详情(勾选框)
    -
    -

    勾选后会在 CPU、内存图表中为每个实例单独绘制一条线(不勾选默认显示所有实例合计的数值)。

    -
    -
    - -图表右上角可以切换时间范围:今天、昨天、过去 7 天。 - -## 组管理 - -开发者还可以使用云引擎的组管理功能,建立多个独立的云引擎分组,在访问同一数据源的情况下,部署多套不同的服务器端业务代码,并对每个分组绑定不同的自定义域名,实现更丰富的业务需求: - -* 将用户界面和管理后台拆分为不同的项目,使用不同的域名。 -* 单独部署主系统之外的边缘支持系统,以免边缘系统出现问题时影响主系统。 -* 使用不同的服务器端语言来编写云函数和网站,例如你可以使用 Node.js 编写云函数,而用 PHP 来实现网站。 - -每个分组都可以部署云函数、Hook 和定时任务。但如果当前部署代码中部分云函数与其他组的同名,默认情况会提示错误并中断部署,防止意外重复定义云函数。 - -每个分组都有独立的预备环境用于测试代码、独立的域名供外部访问,每个分组的环境变量、代码仓库等设置也是独立的,你可以单独对一个组部署代码。你可以按照 [实例管理](#实例管理) 一节中的介绍,在分组中创建和管理实例,如果组中没有实例就无法响应请求,如果组中有多个实例便可以提供负载均衡和高可用的能力。 - -### 创建和管理组 - -你可以在 **云服务控制台 > 云引擎** 中点击左上角的分组选择框,点击「组管理」,即可创建新的分组、删除分组、设置主要分组。 - -在选择一个分组之后,便可以在设置界面中修改分组的代码仓库、环境变量等信息,也可以在资源页面下调整规格和实例数量。 - -新建的分组不包含任何实例(也无法提供服务),开发者可以点击实例规格旁边的「更改」按钮,选择一个标准版的规格,再选择要在生产环境创建的实例数量,之后云引擎会自动赠送一个包含体验实例的预备环境。 - -如不再需要这个分组,在删除分组前需要先点击实例规格旁边的「更改」按钮,将规格修改为免费版,之后的实例会被删除(无法再提供服务)。 - -## 价格 - -### 实例价格 - -体验实例不收取费用。 - -标准实例按天扣费,费用为每一种规格的 **数量乘上对应的价格**,各规格的价格可以在当前节点的价格页面查看,具体扣费信息可在云服务控制台的消费明细中查看。 -[云引擎 FAQ](/v2/sdk/engine/guide/faq)中介绍了扣费的实现细节。 - -### 超限流量价格 - -每个云引擎实例每天有 1 G 免费额度,超出部分价格可以在当前节点的价格页面查看。 - -一个应用下的流量额度会合并计算,即每天的免费额度为 `max(n, 1)` GB,其中 `n` 为该应用所有云引擎分组下的标准实例总数。 - -**云引擎不适合分发大文件之类的场景**,有此需求的开发者可以使用文件服务。 - -在**云服务控制台 > 云引擎 > 云引擎分组 > 统计 > 流量**可以查看最近流量统计。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/03-webhosting.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/03-webhosting.mdx deleted file mode 100644 index 3f5d06d71..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/03-webhosting.mdx +++ /dev/null @@ -1,405 +0,0 @@ ---- -id: webhosting -title: 云引擎网站托管指南 -sidebar_label: 网站托管 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - - - -网站托管是云引擎的一个子模块,允许你用各种程序语言开发 Web 程序,提供云函数和 Hook,还可以提供静态文件的托管和自定义的路由、绑定你自己的域名。 -你可以用它为你的移动应用提供一个介绍和下载页、开发一个管理员控制台或完整的网站,或者运行一些必须在服务器端运行的自定义逻辑。 - -云引擎支持 Node.js、Python、PHP、Java、.NET、Go,你可以选择自己熟悉的技术栈进行开发。 - -## 从示例项目开始 - -我们为云引擎支持的各种语言准备了示例项目,建议从示例项目着手开始开发。 - -要理解如何从示例项目开始开发云引擎项目,本地调试,部署到云端,请先阅读[云引擎快速入门](/v2/sdk/engine/guide/quickstart)。 - -### 本地运行和调试 - -在确保所有的依赖都正确安装之后,就可以在项目根目录用我们的命令行工具来启动本地调试了: - -```sh -tds up -``` - -更多有关命令行工具和本地调试的内容请看[云引擎命令行工具使用指南](/v2/sdk/engine/guide/cli)。 - -### 部署到云端 - -在你的项目根目录运行: - -```sh -tds deploy -``` - -如果生产环境是标准实例,需要加上 `--prod 1` 参数,指定部署到生产环境: - -```sh -tds deploy --prod 1 -``` - -你可以在控制台绑定云引擎域名,绑定域名后,即可通过绑定域名访问你的应用。 -例如,假定你在控制台绑定了 `web.example.com` 这个域名,即可通过 `https://web.example.com` 访问你的应用(生产环境)。 -注意,DNS 可能需要等待几个小时后才能生效。 - -### 项目骨架 - - -<> - -以示例项目为例,在根目录我们看到有一个 `package.json` 文件,注意:**所有 Node.js 的项目必须包含 `package.json` 才会正确地被云引擎识别为 Node.js 项目**。 - -#### `package.json` - -Node.js 的 `package.json` 中可以指定 [很多选项](https://docs.npmjs.com/files/package.json),它通常看起来是这样: - -```json -{ - "name": "node-js-getting-started", - "scripts": { - "start": "node server.js" - }, - "engines": { - "node": "12.x" - }, - "dependencies": { - "express": "4.16.4", - "leanengine": "^3.3.2", - "leancloud-storage": "^3.11.0" - } -} -``` - -其中云引擎会尊重的选项包括: - -- `scripts.start` 启动项目时使用的命令;默认为 `node server.js`,如果你希望为 Node.js 附加启动选项(如 `--es_staging`)或使用其他的文件作为入口点,可以修改该选项。 -- `scripts.postinstall` 会在项目构建结束时运行一次;可以将构建命令(如 `gulp build`)写在这里。 -- `engines.node` 指定所需的 Node.js 版本;出于兼容性考虑默认版本仍为比较旧的 `0.12`,**因此建议大家自行指定一个更高的版本,建议使用 `12.x` 版本进行开发**,你也可以设置为 `*` 表示总是使用最新版本的 Node.js。 -- `dependencies` 项目所依赖的包;使用 Node.js 10 以上的版本时,云引擎会在部署时用 `npm ci` 为你安装这里列出的所有依赖。 -- `devDependencies` 项目开发时所依赖的包;使用 Node.js 10 以上的版本时,云引擎会安装这里的依赖。 - -建议你参考我们的 [项目模板](https://github.com/leancloud/node-js-getting-started/blob/master/package.json) 来编写自己的 `package.json`。 - -我们也对 `package-lock.json` 和 `yarn.lock` 提供了支持。 - - -<> - -参照示例项目,你的项目需要遵循一定格式才会被云引擎识别并运行。 - -使用 WSGI 规范来运行项目,项目根目录下必须有 `wsgi.py` 与 `requirements.txt` 文件,可选文件 `.python-version`、`runtime.txt`。云引擎运行时会首先加载 `wsgi.py` 这个模块,并将此模块的全局变量 `application` 作为 WSGI 函数进行调用。因此请保证 `wsgi.py` 文件中包含一个 `application` 的全局变量/函数/类,并且符合 WSGI 规范。 - -例如,一个[最简单的基于 Flask 框架的项目][lean-flask]只需两个文件(`requirements.txt` 除外,不考虑本地调试功能): - -```python -# app.py - -from flask import Flask -app = Flask(__name__) -@app.route('/') -def index(): - return "hi" - -# wsgi.py -from app import app -application = app -``` - -[lean-flask]: https://github.com/leancloud/lean-flask - -更多关于 **WSGI 函数** 的内容,请参考 [PEP333](https://www.python.org/dev/peps/pep-0333/)。 - -兼容 Python WSGI 规范的框架都可以在云引擎运行。目前比较流行的 Python Web 框架对此都有支持,比如 [Flask](http://flask.pocoo.org)、[Django](https://www.djangoproject.com)、[Tornado](http://www.tornadoweb.org)。我们提供了 Flask 和 Django 两个框架的示例项目作为参考,你也可以直接把它们当作一个应用项目的初始化模版: - -- [Flask](https://github.com/leancloud/python-getting-started) -- [Django](https://github.com/leancloud/django-getting-started) - -#### 添加第三方依赖模块 - -`requirements.txt` 中填写项目依赖的第三方模块,每行一个,如: - -``` -# 井号至行尾为注释 -leancloud>=2.9.1,<3.0.0 -Flask>=1.0.0 # 可以指定版本号/范围 -git+https://github.com/foo/bar.git@master#egg=bar # 可以使用 Git/SVN 等版本管理工具的远程地址 -``` - -详细格式请参考 [pip 19.0.1 Documentation > User Guide > Requirements Files](https://pip.pypa.io/en/stable/user_guide/#requirements-files)。 - -应用部署到云引擎之后,会自动按照 `requirements.txt` 中的内容进行依赖安装。在本地运行和调试项目的时候,可以在项目目录下使用如下命令安装依赖: - -```sh -pip install -r requirements.txt -``` - -另外当你部署应用的时候,建议将依赖的包的版本都按照 `foo==1.0.0` 这种格式来明确指定版本号(或版本的范围),防止因为依赖的模块升级且不再兼容老的 API 时,再次部署会导致应用运行失败。 - -#### 指定 Python 版本 - -你可以选择运行代码的 Python 版本,选择方法与 [`pyenv`](https://github.com/pyenv/pyenv) 相同,即在项目根目录的 `.python-version` 中写入需要的 Python 版本即可,比如 `3.6.1`。这样将代码部署到云引擎之后,系统会自动选择对应的 Python 版本。 - -如果在本地开发时已使用了 `pyenv`,`pyenv` 也会根据此文件来自动使用对应的 Python 运行项目。我们建议本地开发使用 `pyenv`,以保证本地环境与线上相同。`pyenv` 的安装方法请参考 [`pyenv` 的 GitHub 仓库](https://github.com/pyenv/pyenv)。 - - -<> - -你的项目需要遵循一定格式才会被云引擎识别并运行。 - -LeanEngine PHP 项目必须有 `$PROJECT_DIR/public/index.php` 文件,该文件为整个项目的启动文件。 - -云引擎默认提供 PHP 5.6 的运行环境,如需指定 PHP 版本,请在 `composer.json` 中添加: - -```json -"require": { - "php": "7.4.x" -} -``` - -LeanEngine PHP 不依赖第三方框架,你可以使用你最熟悉的框架进行开发,或者不使用任何框架。但是请保证通过执行 `public/index.php` 能够启动你的项目。 - -在项目中存在 `composer.lock` 文件时,云引擎会优先根据 composer.lock 安装依赖。 - - -<> - -云引擎目前支持 Java 8、11、12、13、14 运行环境和 war 包运行,你的项目需要遵循一定格式才会被云引擎识别并运行。 - -云引擎 Java 运行环境使用 Maven 进行构建,所以 LeanEngine Java 项目必须有 `$PROJECT_DIR/pom.xml` 文件,该文件为整个项目的配置文件。构建完成后云引擎会尝试到 `$PROJECT_DIR/target` 目录下寻找可以使用的包: - -- WAR:如果项目打包成 WAR 文件,则云引擎会将其放入 Servlet 容器(当前是 Jetty 9.x)来运行。 -- JAR:如果项目打包成 JAR 文件,则云引擎会通过 `java -jar .jar` 来运行。 - -我们建议使用示例项目做为起步,因为一些细节的开发环境的配置会让开发调试方便很多: - -- [`java-war-getting-started`](https://github.com/leancloud/java-war-getting-started):使用 Servlet,集成 LeanEngine Java SDK 的一个简单项目,打包成 WAR 文件。 -- [`spring-boot-getting-started`](https://github.com/leancloud/spring-boot-getting-started):使用 [Spring Boot](https://projects.spring.io/spring-boot/) 作为项目框架,集成 LeanEngine Java SDK 的一个简单的项目,打包成 JAR 文件。 - -Java 运行环境对内存的使用较多,所以建议: - -- 以 [示例项目](https://github.com/leancloud/java-war-getting-started) 起步的应用,建议使用 512 MB 或以上规格的实例。 -- 使用 [Spring Boot](https://projects.spring.io/spring-boot/) 的应用,建议使用 1 GB 或以上规格的实例。 -- 本地启动并模拟完成主要业务流程操作,待应用充分初始化后,根据 Java 进程内存占用量选择相应的实例规格,需要注意保留一定的余量用以应对请求高峰。 - -如果云引擎实例规格**选择不当**,可能造成应用启动时因为内存溢出(OOM)导致部署失败,或运行期内存溢出导致应用频繁重启。 - -Java 云引擎默认使用 Java 11 运行环境,如果希望使用其他版本的 Java,可以在项目根目录创建一个名为 `system.properties` 的文件,指定 `java.runtime.version`: - -``` -java.runtime.version=14 -``` - -#### 打包成 WAR 文件的项目 - -首先确认项目 `pom.xml` 中配置了 [Jetty plugin](https://www.eclipse.org/jetty/documentation/9.4.x/jetty-maven-plugin.html),并且 web server 的端口通过环境变量 `LEANCLOUD_APP_PORT` 获取,具体配置可以参考我们的 [示例代码](https://github.com/leancloud/java-war-getting-started/blob/master/pom.xml)。 - -然后使用 Maven 安装依赖并打包: - -```sh -mvn package -``` - -然后使用命令行工具本地启动应用: - -```sh -tds up -``` - -更多有关命令行工具和本地调试的内容请参考[云引擎命令行工具使用指南](/v2/sdk/engine/guide/cli)。 - -除了使用命令行工具本地启动应用外,还可以手动设置相应环境变量后,直接启动应用,详见[云引擎 FAQ](/v2/sdk/engine/guide/faq)。 - - -<> - -为了更为简洁的支持 .NET Core 的 Web 项目,云引擎要求您的源代码目录如下: - -``` -├── app.sln -├── web -| ├── StartUp.cs -| ├── Program.cs -|    ├── web.csproj -| └── wwwroot -| ├── css -| ├── lib -| └── js -└── global.json -``` - -示例项目 [`dotNET-getting-started`](https://github.com/leancloud/dotNET-getting-started) 是推荐的模板。 - -其中根目录必须拥有一个 `app.sln` 解决方案文件 和一个 `web/` 文件夹,这是必须的(这一硬性规定会将在未来取消,取消之后开发者自定义自己的项目结构)。 - - -<> - -你的 Go 程序需要使用 Go Modules 才能被云引擎识别。 - -云引擎默认提供 Go 1.14.0 的运行环境。 - -你可以使用你最熟悉的框架或不使用任何框架来开发 Go 程序,但需要将 Go SDK 中提供的云引擎中间件正确地接入你的程序中。 - - - - -## 使用数据存储服务 - -在云引擎中你可以使用云服务提供的数据存储作为应用的后端数据库,以及使用其他云服务提供的功能。 -SDK 可以让你更加方便地使用这些功能。 - -建议使用云引擎模板项目。 -模板项目已经集成了 SDK,也包含了初始化 SDK 的逻辑,可以直接使用。 - -建议在客户端(浏览器端、移动端)登录用户,调用 SDK 的接口获取 session token。 -对于需要后端以当前用户的身份完成的操作,客户端通过 HTTP Header 等方式将 session token 发送给后端。 - -参见[数据存储指南](/v2/sdk/storage/guide/dotnet)。 - -## 健康监测 - -你的应用在启动时,云引擎的管理程序会每秒去检查你的应用是否启动成功,如果超过启动时间限制仍未启动成功,即认为启动失败。 -在之后应用正常运行的过程中,也会有定期的「健康监测」,以确保你的应用正常运行,如果健康监测失败,云引擎管理程序会自动重启你的应用。 - -健康检查的 URL 包括你的应用首页(`/`)和 SDK 负责处理的 `/__engine/1/ping`,只要 **两者之一** 返回了 HTTP `200` 的响应,就视作成功。 -因此请确保你的应用使用了 SDK,或你的应用 **首页能够正常地返回 HTTP `200`** 响应。 - -## 部署与发布 - -### 命令行部署 - -在你的项目根目录运行: - -```sh -tds deploy -``` - -即可部署至预备环境。 -之后运行 `tds publish` 可以将预备环境的代码发布到生产环境。 -注意,免费版只有一个环境,`tds deploy` 会直接部署到生产环境。 - -使用命令行工具可以非常方便地部署、发布应用,查看应用状态,查看日志,甚至支持多应用部署。 -具体使用请参考《命令行工具指南》。 - -### 依赖缓存 - -云引擎实现了一个缓存机制来加快构建的速度,所谓构建就是指你的应用在云引擎上安装依赖的过程,在每次构建时,如果依赖没有新增或者删减,那么就直接使用上次安装的依赖,只将新的应用代码替换上去。 -例如 Node.js 项目连续两次部署,`package.json` 并没有修改,那么就会直接使用已经缓存的依赖。 - - -依赖缓存也会因为很多原因失效,因此不保证每次构建都可以利用上缓存。 -如果你遇到了与依赖安装有关的问题,可以在控制台部署时勾选「下载最新依赖」,或通过命令行工具部署时添加 `--no-cache` 选项。 - -```sh -tds deploy --no-cache -``` - -### Git 部署 - -除此之外,还可以使用 git 仓库部署。你需要将项目提交到一个 git 仓库,我们并不提供源码的版本管理功能,而是借助于 git 这个优秀的分布式版本管理工具。我们推荐你使用 [GitHub](https://github.com/)、[Coding](https://coding.net/) 或者 [码云](https://gitee.com/) 这样第三方的源码托管网站,也可以使用你自己搭建的 git 仓库(比如 [GitLab](https://about.gitlab.com/))。 - -你需要先在这些平台上创建一个项目(如果已有代码,请不需要选择「Initialize this repository with a README」),在网站的个人设置中填写本地机器的 SSH 公钥(以 GitHub 为例,在 **Settings** > **SSH and GPG keys** 中点击 **New SSH key**),然后在项目目录执行: - -```sh -git remote add origin git@github.com:/.git -git push -u origin master -``` - -然后到云引擎的设置界面填写你的 Git 仓库地址,如果是公开仓库建议填写 HTTPS 地址,例如 `https://github.com//.git`。 - -如果是私有仓库需要填写 SSH 地址(git@github.com:<username>/<repoName>.git),还需要你将云引擎分配给你的公钥填写到第三方托管平台的 Deploy keys 中,以 GitHub 为例,在项目的 **Settings** > **Deploy keys** 中点击 **Add deploy key**。 - -设置好之后,今后需要部署代码时就可以在云引擎的部署界面直接点击「部署」了,默认会部署 `master` 分支的代码,你也可以在部署时填写分支、标签或具体的 Commit。 -如果仓库使用了 submodule,云引擎也会自动拉取 submodule。 - -如果希望 push 到项目的 Git 仓库的特定分支后自动触发云引擎部署,可以在应用的 **云服务控制台 > 云引擎 > 部署 > git 部署 > 自动部署** 查看 deploy token 和 webhook 地址。 -控制台显示的 deploy token 可以用来构造 HTTP 请求触发部署。 -在控制台填写项目仓库的分支名称,并选择云引擎的运行环境后,控制台会生成相应的 webhook 地址。 -该地址收到任意 POST 请求后,会部署指定分支的代码到指定的运行环境。 - -例如,在 GitHub 代码仓库的 **Settings** > **Webhooks** > **Payload URL** 填写生成的 webhook 后(其他选项均使用默认值,不用修改),下次 push 到项目仓库的 **任意** 分支,云引擎会自动根据 **指定** 分支更新代码,重新部署。之所以 push 到任意分支都会触发重新部署,是因为 GitHub 的 webhook 触发事件设置粒度较粗,不能指定仅在 push 到特定分支时触发 webhook。另一方面,云引擎也没有适配具体的托管平台,不会根据 GitHub 提交的 POST 内容中的分支信息决定是否重新部署。 -不过,你可以使用 GitHub Action 更精细地控制部署时机,具体可以参考控制台显示的示例。 - -### 预备环境和生产环境 - -默认情况,云引擎只有一个「生产环境」。在生产环境中有一个「体验实例」来运行应用。 - -当生产环境的体验实例升级到「标准实例」后会有一个额外的「预备环境」。两个环境所访问的都是同样的数据,你可以用预备环境测试你的云引擎代码,每次修改先部署到预备环境,测试通过后再发布到生产环境;如果你希望有一个独立数据源的测试环境,建议单独创建一个应用。 - -在云引擎托管的网站需要绑定域名才能访问。 -以 `stg-` 开头的域名会自动绑定到预备环境。 - -如果访问云引擎遇到「Application not Found」的错误,通常是因为对应的环境还没有部署代码。例如应用可能没有预备环境,或应用尚未发布代码到生产环境。 - -有些时候你可能需要知道当前云引擎运行在什么环境(开发环境、预备环境或生产环境),从而做不同的处理。 -Node.js 项目可以通过检查环境变量 `NODE_ENV` 的值来判断:值为 `development` 意味着当前环境为「开发环境」,是由命令行工具启动的;值为 `production` 意味着当前环境为「生产环境」,是线上正式运行的环境;值为 `staging` 意味着当前环境为「预备环境」。 -其他语言的项目可以通过检查环境变量 `LEANCLOUD_APP_ENV` 的值来判断:值为 `development` 意味着当前环境为「开发环境」,是由命令行工具启动的;值为 `production` 意味着当前环境为「生产环境」,是线上正式运行的环境;值为 `stage` 意味着当前环境为「预备环境」。 - - - - -### 部署历史 - -在**云服务控制台 > 云引擎 > 云引擎分组 > 部署**页面可以分别查看预备环境和生产环境的历史部署。 -每个历史部署版本会显示简短描述(基于 git 提交日志等信息)、部署版本号、部署时间。 -历史部署按部署时间倒序排列,当前部署排在最前。 -点击**回滚至该版本**按钮,可以回滚至相应的部署版本。 -点击**查看更多历史部署**可以查看最近部署的 10 个版本。 -除了按预备环境和生产环境分别查看历史部署外,点击右上角的**查看部署时间线**则可以按照时间顺序(最新部署在前)查看部署到云引擎的各个版本。 - -除了查看部署历史外,这里还会显示预备环境或生产环境的部署状态(「休眠中」、「部署中」、「运行中」)。 -通过右上角的按钮还可以**重启**(重新部署当前版本)或**清除部署**(移除部署,注销相应的云函数和 Hook)。 -预备环境还可以点击右上角的**部署到生产环境**按钮将最近部署到预备环境的版本发布到生产环境。 -当部署状态为「部署中」时,控制台会显示部署进行中的一些信息,也会显示一个**取消部署**按钮,点击可以取消部署。 - -## 云端环境 - -### 环境变量 - -云引擎平台默认提供下列环境变量供应用使用: - -变量名 | 说明 ---- | --- -`LEANCLOUD_APP_ID` | 当前应用的 `App ID`。 -`LEANCLOUD_APP_KEY` | 当前应用的 `App Key`。 -`LEANCLOUD_APP_MASTER_KEY`| 当前应用的 `Master Key`。 -`LEANCLOUD_APP_ENV` | 当前的应用环境:开发环境没有该环境变量,或值为 `development`(通过命令行工具启动)。预备环境值为 `stage`。生产环境值为 `production`。 -`LEANCLOUD_APP_PORT` | 当前应用开放给外网的端口,只有监听此端口,用户才可以访问到你的服务。 -`LEANCLOUD_API_SERVER` | 访问存储服务时使用的地址。该值会因为所在数据中心等原因导致不一样,所以使用 REST API 请求存储服务或其他云服务时请使用此环境变量的值。 -`LEANCLOUD_APP_GROUP`| 云引擎实例所在的组。当使用云引擎组管理功能时,该值为组的名称。 -`LEANCLOUD_REGION` | 云引擎服务所在区域,值为 `CN` 或 `US`,分别表示国内版和国际版。 -`LEANCLOUD_VERSION_TAG` | 云引擎实例部署的版本号 - -旧版云引擎使用的以 `LC_` 开头的环境变量(如 `LC_APP_ID`)已经被弃用。为了保证代码兼容性,`LC_` 变量在一段时间内依然有效,但未来可能会完全失效。为了避免报错,建议使用 `LEANCLOUD_` 变量来替换。 - -你也可以在 **云服务控制台 > 云引擎 > 云引擎分组 > 设置 > 自定义环境变量** 页面中添加自定义的环境变量。其中名字必须是字母、数字、下划线且以字母开头,值必须是字符串,修改环境变量后会在下一次部署时生效。 - -按照一般的实践,可以将一些配置项存储在环境变量中,这样可以在不修改代码的情况下,修改环境变量并重新部署,来改变程序的行为;或者可以将一些第三方服务的 Secret Key 存储在环境变量中,避免这些密钥直接出现在代码中。 - -### 负载均衡和边缘节点 - -在云引擎上,用户的请求会先经过负载均衡组件,然后到达你的应用。 -负载均衡组件会处理 HTTPS 加密、对响应进行压缩等一般性工作,因此你不必在你的应用中添加 HTTPS 或 gzip 相关的功能。 - -### 文件系统 - -你可以向 `/home/leanengine` 或 `/tmp` 目录写入临时文件,最多不能超过 1 GB。 - -云引擎每次部署都会产生一个新的容器,即使不部署系统偶尔也会进行一些自动调度,这意味着你 **不能将本地文件系统当作持久的存储**,只能用作临时存储。 - -如果你写入的文件体积较大,建议在使用后自动删除他们,否则如果占用磁盘空间超过 1 GB,继续写入文件可能会收到类似 `Disk quota exceeded` 的错误,这种情况下你可以重新部署一下,这样文件就会被清空了。 - -### 日志 - -在 **云服务控制台 > 云引擎 > 云引擎分组 > 日志** 中可以查看云引擎的部署和运行日志,还可以通过环境(预备环境、生产环境)、类型(标准输出、标准错误)、实例、日期时间进行筛选。 - -你还可以通过命令行工具来导出最近七天的日志到本地文件,方便进行进一步的分析和统计。 - -应用的日志可以直接输出到「标准输出」或者「标准错误」,这些信息会分别对应日志的 `info` 和 `error` 级别。 - -云引擎的访问日志(Access Log)也可以在控制台导出(**云服务控制台 > 云引擎 > 访问日志**)。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/04-cloudfunction.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/04-cloudfunction.mdx deleted file mode 100644 index d368a753e..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/04-cloudfunction.mdx +++ /dev/null @@ -1,1856 +0,0 @@ ---- -id: cloudfunction -title: 云函数指南 -sidebar_label: 云函数 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import Mermaid from '/src/docComponents/Mermaid'; - - - -当你开发移动端应用时,可能会有下列需求: - -- 应用在多平台客户端(Android、iOS、浏览器等)中很多逻辑都是一样的,希望将这部分逻辑抽取出来只维护一份。 -- 有些逻辑希望能够较灵活的调整(比如某些个性化列表的排序规则),但又不希望频繁的更新和发布移动客户端。 -- 有些逻辑需要的数据量很大,或者运算成本高(比如某些统计汇总需求),不希望在移动客户端进行运算,因为这样会消耗大量的网络流量和手机运算能力。 -- 当应用执行特定操作时,由云端系统自动触发一段逻辑(称为 [Hook 函数](#Hook-函数)),例如用户注册后对该用户增加一些信息记录用于统计;或某业务数据发生变化后希望做一些别的业务操作。 -- 客户端提供能够越过 ACL 或表权限的限制,对数据进行操作。 -- 需要定时运行任务,比如每天凌晨清理垃圾注册账号等。 - -这时,你可以使用云引擎的云函数。 -云函数是一段部署在服务端的 JavaScript、Python、PHP、C#、Go 代码,可以很好地完成上述需求。 - -如果还不知道如何创建云引擎项目、本地调试并部署到云端,请阅读[云引擎快速入门](/v2/sdk/engine/guide/quickstart)。 - - -## 云函数 - -示例项目中文件定义了一个很简单的 `hello` 云函数。 -在云端进行计算的一个重要理由是,你不需要将大量的数据发送到设备上做计算,而是将这些计算放到服务端,并返回结果这一点点信息就好。 - -现在让我们看一个较复杂的例子来展示云函数的用途。 - -例如,你写了一个应用,让用户对电影评分,一个评分对象大概是这样: - -```json -{ - "movie": "夏洛特烦恼", - "stars": 5, - "comment": "夏洛一梦,笑成麻花" -} -``` - -`stars` 表示评分,1-5。如果你想查找《夏洛特烦恼》这部电影的平均分,你可以找出这部电影的所有评分,并在设备上根据这个查询结果计算平均分。但是这样一来,尽管你只是需要平均分这样一个数字,却不得不耗费大量的带宽来传输所有的评分。通过云引擎,我们可以简单地传入电影名称,然后返回电影的平均分。 - -云函数接收 JSON 格式的请求对象,我们可以用它来传入电影名称。 -各语言的 SDK 都在云引擎运行环境上有效,可以直接使用,所以我们可以使用它来查询所有的评分。 -结合在一起,实现 `averageStars` 函数的代码如下: - - -<> - -```js -AV.Cloud.define('averageStars', function (request) { - var query = new AV.Query('Review'); - query.equalTo('movie', request.params.movie); - return query.find().then(function (results) { - var sum = 0; - for (var i = 0; i < results.length; i++) { - sum += results[i].get('stars'); - } - return sum / results.length; - }); -}); -``` - - -<> - -```python -@engine.define -def averageStars(movie, **params): - reviews = leancloud.Query(Review).equal_to('movie', movie).find() - result = sum(x.get('stars') for x in reviews) - return result -``` - -客户端 SDK 调用时,云函数的名称默认为 Python 代码中函数的名称。有时需要设置云函数的名称与 Python 代码中的函数名称不相同,可以在 `engine.define` 后面指定云函数名称,比如: - -```python -@engine.define('averageStars') -def my_custom_average_start(movie, **params): - pass -``` - - -<> - -```php -use \LeanCloud\Engine\Cloud; -use \LeanCloud\Query; -use \LeanCloud\CloudException; - -Cloud::define("averageStars", function($params, $user) { - $query = new Query("Review"); - $query->equalTo("movie", $params["movie"]); - try { - $reviews = $query->find(); - } catch (CloudException $ex) { - // 查询失败,将错误输出到日志 - error_log($ex->getMessage()); - return 0; - } - $sum = 0; - forEach($reviews as $review) { - $sum += $review->get("stars"); - } - if (count($reviews) > 0) { - return $sum / count($reviews); - } else { - return 0; - } -}); -``` - - -<> - -```java -@EngineFunction("averageStars") -public static float getAverageStars(@EngineFunctionParam("movie") String movie) - throws LCException { - LCQuery query = new LCQuery("Review"); - query.whereEqualTo("movie", movie); - List reviews = query.find(); - int sum = 0; - if (reviews == null && reviews.isEmpty()) { - return 0; - } - for (LCObject review : reviews) { - sum += review.getInt("star"); - } - return sum / reviews.size(); -} -``` - - -<> - -```cs -[LCEngineFunction("averageStars")] -public static float AverageStars([LCEngineFunctionParam("movie")] string movie) { - if (movie == "夏洛特烦恼") { - return 3.8f; - } - return 0; -} -``` - -然后在程序启动的入口函数中添加如下代码来启用刚才编写的云函数: - -```cs -public class Startup { - ... - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) { - // 开启日志(可选) - LCLogger.LogDelegate = (level, log) => { - switch (level) { - case LCLogLevel.Debug: - Console.WriteLine($"[DEBUG] {log}"); - break; - case LCLogLevel.Warn: - Console.WriteLine($"[WARN] {log}"); - break; - case LCLogLevel.Error: - Console.WriteLine($"[ERROR] {log}"); - break; - default: - break; - } - }; - // 初始化 - LCEngine.Initialize(services); - - services.AddControllersWithViews(); - } - - ... -} -``` - - -<> - -```go -type Review struct { - leancloud.Object - Movie string `json:"movie"` - Stars int `json:"stars"` - Comment string `json:"comment"` -} - -leancloud.Define("averageStars", func(req *leancloud.FunctionRequest) (interface{}, error) { - reviews := make([]Review, 10) // 预留一小部分空间 - if err := client.Class("Review").NewQuery().EqualTo("movie", req.Params["movie"].(string)).Find(&reviews); err != nil { - return nil, err - } - - sum := 0 - for _, v := range reviews { - sum += v.Stars - } - - return sum / len(reviews), nil -}) -``` - - - - -### 参数和返回值 - - -<> - -`Request` 会作为参数传入到云函数中,`Request` 上的属性包括: - -- `params: object`:客户端发送的参数对象,当使用 `rpc` 调用时,也可能是 `AV.Object`。 -- `currentUser?: AV.User`:客户端所关联的用户(根据客户端发送的 `X-LC-Session` 头)。 -- `sessionToken?: string`:客户端发来的 `sessionToken`(`X-LC-Session` 头)。 -- `meta: object`:有关客户端的更多信息,目前只有一个 `remoteAddress` 属性表示客户端的 IP。 - -另外,`AV.Cloud.define` 还接受一个可选参数 `options`(位置在函数名称和调用函数之间)。 -这个 `options` 对象上的属性包括: - -- `fetchUser: boolean`:是否自动抓取客户端的用户信息,默认为真。设置为假时,`Request` 将不会有 `currentUser` 属性。 -- `internal: boolean`:是否只允许在云引擎内(使用 `AV.Cloud.run` 且未开启 `remote` 选项)或使用 `Master Key` (使用 `AV.Cloud.run` 时传入 `useMasterKey`)调用,不允许客户端直接调用。默认为假。 - -例如,假设我们不希望客户端直接调用上述函数,也不关心客户端用户信息,那么上述函数的定义可以改写为: - -```js -AV.Cloud.define('averageStars', { fetchUser: false, internal: true }, function (request) { - // 定义同上 -}); -``` - -如果云函数返回了一个 Promise,那么云函数会使用 Promise 成功结束后的结果作为成功响应;如果 Promise 中发生了错误,云函数会使用这个错误作为错误响应,对于使用 `AV.Cloud.Error` 构造的异常对象,我们认为是客户端错误,不会在标准输出打印消息,对于其他异常则会在标准输出打印调用栈,以便排查错误。 - -我们推荐大家使用链式的 Promise 写法来完成业务逻辑,这样会极大地方便异步任务的处理和异常处理,**请注意一定要将 Promise 串联起来并在云函数中 return** 以保证上述逻辑正确工作,推荐阅读 [JavaScript Promise 迷你书](http://liubin.org/promises-book/) 来深入地了解 Promise。 - -在 2.0 之前的早期版本中,云函数接受 `request` 和 `response` 两个参数,我们会继续兼容这种用法到下一个大版本,希望开发者尽快迁移到 Promise 风格的云函数上。之前版本的文档见[Node SDK v1 API 文档](https://github.com/leancloud/leanengine-node-sdk/blob/v1/API.md)。 - - -<> - -调用云函数时的参数会直接传递给云函数,因此直接声明这些参数即可。另外调用云函数时可能会根据不同情况传递不同的参数,这时如果定义云函数时没有声明这些参数,会触发 Python 异常,因此建议声明一个额外的关键字参数(关于关键字参数,请参考 [此篇文档](http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431752945034eb82ac80a3e64b9bb4929b16eeed1eb9000) 中「关键字参数」一节)来保存多余的参数。 - -```python -@engine.define -def my_cloud_func(foo, bar, baz, **params): - pass -``` - -除了调用云函数的参数之外,还可以通过 `engine.current` 对象,来获取到调用此云函数的客户端的其他信息。`engine.current` 对象上的属性包括: - -- `engine.current.user: leancloud.User`:客户端所关联的用户(根据客户端发送的 `X-LC-Session` 头)。 -- `engine.current.session_token: str`:客户端发来的 `sessionToken`(`X-LC-Session` 头)。 -- `engine.current.meta: dict`:有关客户端的更多信息,目前只有一个 `remote_address` 属性表示客户端的 IP。 - - -<> - -传递给云函数的参数依次为: - -- `$params: array`:客户端发送的参数。 -- `$user: User`:客户端所关联的用户(根据客户端发送的 `X-LC-Session` 头)。 -- `$meta: array`:有关客户端的更多信息,目前只有一个 `$meta['remoteAddress']` 属性表示客户端的 IP。 - - -<> - -云函数中可以获取的参数和上下文信息有: - -- `@EngineFunctionParam`:客户端发送的参数。 -- `EngineRequestContext` 有关客户端的更多信息,其中 `EngineRequestContext.getSessionToken()` 会返回客户端所关联用户的 sessionToken(根据客户端发送的 `X-LC-Session` 头),`EngineRequestContext.getRemoteAddress()` 会返回客户端的实际地址。 - - -<> - -云函数中可以获取的参数和上下文信息有: - -- `LCEngineFunctionParam`:客户端发送的参数。 -- `LCEngineRequestContext` 有关客户端的更多信息,其中 `LCEngineRequestContext.SessionToken` 会返回客户端所关联用户的 sessionToken(根据客户端发送的 `X-LC-Session` 头),`LCEngineRequestContext.RemoteAddress` 会返回客户端的实际地址。 - - -<> - -`leancloud.FunctionRequest` 会作为参数传入到云函数中,`leancloud.FunctionRequest` 上的属性包括: - -- `Params` 包含客户端发送的参数 -- `CurrentUser` 包含客户端所关联的用户(根据客户端发送的 `X-LC-Session` 头)。可以在 `Define` 定义云函数时,在最后传入可选参数 `WithoutFetchUser()` 禁止获取当前调用的用户。 -- `SessionToken` 包含客户端发来的 `sessionToken` 根据客户端发送的 `X-LC-Session` 头)。可以在 `Define` 定义云函数时,在最后传入可选参数 `WithoutFetchUser()` 禁止获取当前调用的 `sessionToken`。 -- `Meta` 包含有关客户端的更多信息,目前只有一个 `remoteAddress` 属性表示客户端的 IP。 - - - - -### SDK 调用云函数 - -LeanCloud 各个语言版本的 SDK 都提供了调用云函数的接口: - - - -```cs -try { - Dictionary response = await LCCloud.Run("averageStars", parameters: new Dictionary { - { "movie", "夏洛特烦恼" } - }); - // 处理结果 -} catch (LCException e) { - // 处理异常 -} -``` -```java -// 构建传递给服务端的参数字典 -Map dicParameters = new HashMap(); -dicParameters.put("movie", "夏洛特烦恼"); - -// 调用指定名称的云函数 averageStars,并且传递参数(默认不使用缓存) -LCCloud.callFunctionInBackground("averageStars", dicParameters).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - - } - - @Override - public void onNext(Object object) { - // succeed. - } - - @Override - public void onError(Throwable throwable) { - // failed. - } - - @Override - public void onComplete() { - - } -}); - -// Java SDK 还提供了一个支持缓存的版本,和 LCQuery 一样,开发者在请求的时候,可以指定 CachePolicy 以及缓存的 -// 最长期限,这样可以避免短时间一直直接请求云端服务器。 -// 下面的请求表示优先使用上次缓存的结果,缓存有效期为 30 秒(30000 毫秒)。 -LCCloud.callFunctionWithCacheInBackground("averageStars", dicParameters, LCQuery.CachePolicy.CACHE_ELSE_NETWORK, 30000) -.subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - - } - - @Override - public void onNext(Object object) { - // succeed. - } - - @Override - public void onError(Throwable throwable) { - // failed. - } - - @Override - public void onComplete() { - - } -}); -``` -```objc -// 构建传递给服务端的参数字典 -NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"夏洛特烦恼" - forKey:@"movie"]; - -// 调用指定名称的云函数 averageStars,并且传递参数 -[LCCloud callFunctionInBackground:@"averageStars" - withParameters:dicParameters - block:^(id object, NSError *error) { - if(error == nil){ - // 处理结果 - } else { - // 处理报错 - } -}]; -``` -```swift -LCEngine.run("averageStars", parameters: ["movie": "夏洛特烦恼"]) { (result) in - switch result { - case .success(value: let resultValue): - print(resultValue) - case .failure(error: let error): - print(error) - } -} -``` -```dart -try { - Map response = await LCCloud.run('averageStars', params: { 'movie': '夏洛特烦恼' }); - // 处理结果 -} on LCException catch (e) { - // 处理异常 -} -``` -```js -var paramsJson = { - movie: "夏洛特烦恼" -}; -AV.Cloud.run('averageStars', paramsJson).then(function (data) { - // 处理结果 -}, function (err) { - // 处理报错 -}); -``` -```python -from leancloud import cloud - -cloud.run('averageStars', movie='夏洛特烦恼') -``` -```php -use \LeanCloud\Engine\Cloud; -$params = array( - "movie" => "夏洛特烦恼" -); -Cloud::run("averageStars", $params); -``` -```go -// ... -averageStars, err := leancloud.Run("averageStars", map[string]string{"movie": "夏洛特烦恼"}) -if err != nil { - panic(err) -} -// ... -``` - - - -### RPC 调用云函数 - -在这种调用方式下,云引擎会自动为 HTTP Response Body 做序列化, -而 SDK 调用之后拿回的返回结果就是一个完整的 LCObject 或包含这样的对象的数据结构: - - - -```cs -try { - LCObject response = await LCCloud.RPC("averageStars", parameters: new Dictionary { - { "movie", "夏洛特烦恼" } - }); - // 处理结果 -} catch (LCException e) { - // 处理异常 -} -``` -```java -// 构建参数 -Map dicParameters = new HashMap<>(); -dicParameters.put("movie", "夏洛特烦恼"); - -LCCloud.callRPCInBackground("averageStars", dicParameters).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - - } - - @Override - public void onNext(LCObject avObject) { - // succeed. - } - - @Override - public void onError(Throwable throwable) { - // failed - } - - @Override - public void onComplete() { - - } -}); - -// Java SDK 还提供了一个支持缓存的版本,和 LCQuery 一样,开发者在请求的时候,可以指定 CachePolicy 以及缓存的 -// 最长期限,这样可以避免短时间一直直接请求云端服务器。 -// 下面的请求表示优先使用上次缓存的结果,缓存有效期为 30 秒(30000 毫秒)。 -LCCloud.callRPCWithCacheInBackground("averageStars", dicParameters, LCQuery.CachePolicy.CACHE_ELSE_NETWORK, 30000) -.subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) { - - } - - @Override - public void onNext(LCObject avObject) { - // succeed. - } - - @Override - public void onError(Throwable throwable) { - // failed - } - - @Override - public void onComplete() { - - } -}); -``` -```objc -NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"夏洛特烦恼" - forKey:@"movie"]; - -[LCCloud rpcFunctionInBackground:@"averageStars" - withParameters:parameters - block:^(id object, NSError *error) { - if(error == nil){ - // 处理结果 - } - else { - // 处理报错 - } -}]; -``` -```swift -LCEngine.call("averageStars", parameters: ["movie": "夏洛特烦恼"]) { (result) in - switch result { - case .success(object: let object): - if let object = object { - print(object) - } - case .failure(error: let error): - print(error) - } -} -``` -```dart -try { - LCObject response = await LCCloud.rpc('averageStars', params: { 'movie': '夏洛特烦恼' }); - // 处理结果 -} on LCException catch (e) { - // 处理异常 -} -``` -```js -var paramsJson = { - movie: "夏洛特烦恼" -}; - -AV.Cloud.rpc('averageStars', paramsJson).then(function (object) { - // 处理结果 -}, function (error) { - // 处理报错 -}); -``` -```python -from leancloud import cloud - -cloud.rpc('averageStars', movie='夏洛特烦恼') -``` -```php -// 暂不支持 -``` -```go -// ... -averageStars := 0 -if err := leancloud.RPC("averageStars", Review{Movie: "夏洛特烦恼"}, &averageStars); err != nil { - panic(err) -} -// .. -``` - - - -### 云函数错误响应码 - -可以根据 [HTTP status codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) 自定义错误响应码。 - - - -```js -AV.Cloud.define('customErrorCode', function (request) { - throw new AV.Cloud.Error('自定义错误信息。', { code: 123 }); -}); -``` -```python -from leancloud import LeanEngineError - -@engine.define -def custom_error_code(**params): - raise LeanEngineError(123, '自定义错误信息。') -``` -```php -Cloud::define("customErrorCode", function($params, $user) { - throw new FunctionError("自定义错误信息。", 123); -}); -``` -```java -@EngineFunction() -public static void customErrorCode() throws Exception { - throw new LCException(123, "自定义错误信息。"); -} -``` -```cs -[LCEngineFunction("throwLCException")] -public static void ThrowLCException() { - throw new LCException(123, "自定义错误信息。"); -} -``` -```go -leancloud.Define("customErrorCode", func(req *leancloud.FunctionRequest) (interface{}, error) { - return nil, leancloud.CloudError{123, "自定义错误信息。"} -}) -``` - - - -客户端收到的响应:`{ "code": 123, "error": "自定义错误信息。" }`。 - -### 云函数超时 - -云函数超时时间为 15 秒,如果超过阈值,客户端将收到 HTTP status code 为 `503` 的响应,body 为 `The request timed out on the server`。 -注意即使已经响应,此时云函数可能仍在执行,但执行完毕后的响应是无意义的(不会发给客户端,会在日志中打印一个 `Can't set headers after they are sent` 的异常)。 -除了 `503` 错误外,有些情况下客户端也可能收到其他报错,如 `524` 或 `141`。 - -#### 超时的处理方案 - -我们建议将代码中的任务转化为异步队列处理,以优化运行时间,避免云函数或定时任务发生超时。 - -例如: - -1. 在存储服务中创建一个队列表,包含 `status` 列; -2. 接到任务后,向队列表保存一条记录,`status` 值设置为 `处理中`,然后将请求结束掉,将队列对象的 `id` 发给客户端。 -3. 当业务处理完毕,根据处理结果更新刚才的队列对象状态,将 `status` 字段设置为 `完成` 或者 `失败`; -4. 在任何时候,在控制台通过队列 `id` 可以获取某个任务的执行结果,判断任务状态。 - -不过,对于 before 类 hook 函数,改为异步处理通常没有意义。 -虽然改为异步后能保证 before 类函数能够运行完成,不会因超时而报错。 -但是,只要 before 类 hook 函数不能及时抛出异常,就无法起到中断操作执行的作用。 -对于超时的 before 类 hook 函数,如果无法优化代码,压缩执行时间的话,那只能改为 after 类函数。 -例如,假设某个 beforeSave 函数需要调用耗时较久的第三方的自然语言处理接口判断用户提交的评论是否来自真实用户,导致超时, -那么可以改为 afterSave 函数,在保存评论后再调用第三方接口,如果判断是水军评论,那么再行删除。 - -## Hook 函数 - -Hook 函数本质上是云函数,但它有固定的名称,定义之后会 **由系统** 在特定事件或操作(如数据保存前、保存后,数据更新前、更新后等等)发生时 **自动触发**,而不是由开发者来控制其触发时机。需要注意: - -- 通过控制台进行数据导入时不会触发任何 hook 函数。 -- 使用 Hook 函数需要 [防止死循环调用](#防止死循环调用)。 -- `_Installation` 表暂不支持 Hook 函数。 -- Hook 函数只对当前应用的 Class 生效,对绑定后的目标 Class 无效。 - -对于 `before` 类的 Hook,如果返回了一个错误的话,这个操作就会被中断,因此你可以在这些 Hook 中主动抛出一个错误来拒绝掉某些操作。对于 `after` 类的 Hook,返回错误并不会影响操作的执行(因为其实操作已经执行完了)。 - -D{object} -D-->E(new) -E-->|beforeSave|H{error?} -H-->N(No) -N-->B[create new object on the cloud] -B -->|afterSave|C((done)) -H-->Y(Yes) -Y-->Z((interrupted)) -D-->F(existing) -F-->|beforeUpdate|I{error?} -I-->Y -I-->V(No) -V-->G[update existing object on the cloud] -G-->|afterUpdate| C -`} /> - - -|beforeDelete|H{error?} -H-->Y(Yes) -Y-->Z((interrupted)) -H-->N(No) -N-->B[delete object on the cloud] -B -->|afterDelete|C((done)) -`} /> - -为了认证 Hook 调用者的身份,我们的 SDK 内部会确认请求确实是从云引擎内网的云存储组件发出的,如果认证失败,可能会出现 `Hook key check failed` 的提示,如果在本地调试时出现这样的错误,请确保是通过命令行工具启动调试的。 - -### BeforeSave - -在创建新对象之前,可以对数据做一些清理或验证。例如,一条电影评论不能过长,否则界面上显示不开,需要将其截断至 140 个字符: - - -<> - -```js -AV.Cloud.beforeSave('Review', function (request) { - var comment = request.object.get('comment'); - if (comment) { - if (comment.length > 140) { - // 截断并添加 '…' - request.object.set('comment', comment.substring(0, 140) + '…'); - } - } else { - // 不保存数据,并返回错误 - throw new AV.Cloud.Error('No comment provided!'); - } -}); -``` - -上面的代码示例中,`request.object` 是被操作的 `AV.Object`。除了 `object` 之外,`request` 上还有一个属性: - -- `currentUser?: AV.User`:发起操作的用户。 - -类似地,其他 hook 的 `request` 参数上也包括 `object` 和 `currentUser` 这两个属性。 - - -<> - -```python -@engine.before_save('Review') # Review 为需要 hook 的 class 的名称 -def before_review_save(review): - comment = review.get('comment') - if not comment: - raise leancloud.LeanEngineError(message='No comment provided!') - if len(comment) > 140: - review.comment.set('comment', comment[:140] + '…') -``` - - -<> - -```php -Cloud::beforeSave("Review", function($review, $user) { - $comment = $review->get("comment"); - if ($comment) { - if (strlen($comment) > 140) { - // 截断并添加 '…' - $review->set("comment", substr($comment, 0, 140) . "…"); - } - } else { - // 不保存数据,并返回错误 - throw new FunctionError("No comment provided!", 101); - } -}); -``` - - -<> - -```java -@EngineHook(className = "Review", type = EngineHookType.beforeSave) -public static LCObject reviewBeforeSaveHook(LCObject review) throws Exception { - if (StringUtil.isEmpty(review.getString("comment"))) { - throw new Exception("No comment provided!"); - } else if (review.getString("comment").length() > 140) { - review.put("comment", review.getString("comment").substring(0, 140) + "…"); - } - return review; -} -``` - - -<> - -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeSave)] -public static LCObject ReviewBeforeSave(LCObject review) { - if (string.IsNullOrEmpty(review["comment"])) { - throw new Exception("No comment provided!"); - } - string comment = review["comment"] as string; - if (comment.Length > 140) { - review["comment"] = string.Format($"{comment.Substring(0, 140)}..."); - } - return review; -} -``` - - -<> - -```go -leancloud.BeforeSave("Review", func(req *ClassHookRequest) (interface{}, error) { - review := new(Review) - if err := req.Object.Clone(review); err != nil { - return nil, err - } - - if len(review.Comment) > 140 { - review.Comment = review.Comment[:140] - } - - return review, nil -}) -``` - - - - -### AfterSave - -在创建新对象后触发指定操作,比如当一条留言创建后再更新一下所属帖子的评论总数: - - - -```js -AV.Cloud.afterSave('Comment', function (request) { - var query = new AV.Query('Post'); - return query.get(request.object.get('post').id).then(function (post) { - post.increment('comments'); - return post.save(); - }); -}); -``` -```python -import leancloud - -@engine.after_save('Comment') # Comment 为需要 hook 的 class 的名称 -def after_comment_save(comment): - post = leancloud.Query('Post').get(comment.id) - post.increment('commentCount') - try: - post.save() - except leancloud.LeanCloudError: - raise leancloud.LeanEngineError(message='An error occurred while trying to save the post.') -``` -```php -Cloud::afterSave("Comment", function($comment, $user) { - $query = new Query("Post"); - $post = $query->get($comment->get("post")->getObjectId()); - $post->increment("commentCount"); - try { - $post->save(); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred while trying to save the post: " . $ex->getMessage()); - } -}); -``` -```java -@EngineHook(className = "Review", type = EngineHookType.afterSave) -public static void reviewAfterSaveHook(LCObject review) throws Exception { - LCObject post = review.getLCObject("post"); - post.fetch(); - post.increment("comments"); - post.save(); -} -``` -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.AfterSave)] -public static async Task ReviewAfterSave(LCObject review) { - LCObject post = review["post"] as LCObject; - await post.Fetch(); - post.Increment("comments", 1); - await post.Save(); -} -``` -```go -leancloud.AfterSave("Review", func(req *ClassHookRequest) error { - review := new(Review) - if err := req.Object.Clone(review); err != nil { - return err - } - - if err := client.Object(review.Post).Update(map[string]interface{}{ - "comment": leancloud.OpIncrement(1), - }); err != nil { - return leancloud.CloudError{Code: 500, Message: err.Error()} - } - - return nil -}) -``` - - - -再如,在用户注册成功之后,给用户增加一个新的属性 `from` 并保存: - - -<> - -```js -AV.Cloud.afterSave('_User', function (request) { - console.log(request.object); - request.object.set('from', 'LeanCloud'); - return request.object.save().then(function (user) { - console.log('Success!'); - }); -}); -``` - -虽然对于 `after` 类的 Hook 我们并不关心返回值,但我们仍建议你返回一个 Promise,这样如果发生了非预期的错误,会自动在标准输出中打印异常信息和调用栈。 - - -<> - -```python -@engine.after_save('_User') -def after_user_save(user): - print user - user.set('from', 'LeanCloud') - try: - user.save() - except LeanCloudError, e: - print 'Error: ', e -``` - - -<> - -```php -Cloud::afterSave("_User", function($userObj, $currentUser) { - $userObj->set("from", "LeanCloud"); - try { - $userObj->save(); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred while trying to save the user: " . $ex->getMessage()); - } -}); -``` - - -<> - -```java -@EngineHook(className = "_User", type = EngineHookType.afterSave) -public static void userAfterSaveHook(LCUser user) throws Exception { - user.put("from", "LeanCloud"); - user.save(); -} -``` - - -<> - -```cs -[LCEngineClassHook("_User", LCEngineObjectHookType.AfterSave)] -public static async Task UserAfterSave(LCObject user) { - user["from"] = "LeanCloud"; - await user.Save(); -} -``` - - -<> - -```go -leancloud.AfterSave("_User", func(req *ClassHookRequest) error{ - if req.User != nil { - if err := client.User(req.User).Set("from", "LeanCloud"); err != nil { - return err - } - } - return nil -}) -``` - - - - -### BeforeUpdate - -在更新已存在的对象前执行操作,这时你可以知道哪些字段已被修改,还可以在特定情况下拒绝本次修改: - - - -```js -AV.Cloud.beforeUpdate('Review', function (request) { - // 如果 comment 字段被修改了,检查该字段的长度 - if (request.object.updatedKeys.indexOf('comment') != -1) { - if (request.object.get('comment').length > 140) { - // 拒绝过长的修改 - throw new AV.Cloud.Error('comment 长度不得超过 140 字符。'); - } - } -}); -``` -```python -@engine.before_update('Review') -def before_hook_object_update(obj): - # 如果 comment 字段被修改了,检查该字段的长度 - if 'comment' in obj.updated_keys and len(obj.get('comment')) > 140: - # 拒绝过长的修改 - raise leancloud.LeanEngineError(message='comment 长度不得超过 140 字符。') -``` -```php -Cloud::beforeUpdate("Review", function($review, $user) { - // 如果 comment 字段被修改了,检查该字段的长度 - if (in_array("comment", $review->updatedKeys) && - strlen($review->get("comment")) > 140) { - throw new FunctionError("comment 长度不得超过 140 字符。"); - } -}); -``` -```java -@EngineHook(className = "Review", type = EngineHookType.beforeUpdate) -public static LCObject reviewBeforeUpdateHook(LCObject review) throws Exception { - List updateKeys = EngineRequestContext.getUpdateKeys(); - for (String key : updateKeys) { - // 如果 comment 字段被修改了,检查该字段的长度 - if ("comment".equals(key) && review.getString("comment").length()>140) { - throw new Exception("comment 长度不得超过 140 字符。"); - } - } - return review; -} -``` -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeUpdate)] -public static LCObject ReviewBeforeUpdate(LCObject review) { - ReadOnlyCollection updatedKeys = review.GetUpdatedKeys(); - if (updatedKeys.Contains("comment")) { - string comment = review["comment"] as string; - if (comment.Length > 140) { - throw new Exception("comment 长度不得超过 140 字符。"); - } - } - return review; -} -``` -```go -leancloud.BeforeUpdate("Review", func(req *ClassHookRequest) (interface{}, error) { - updatedKeys = req.UpdatedKeys() - for _, v := range updatedKeys { - if v == "comment" { - comment, ok := req.Object.Raw()["comment"].(string) - if !ok { - return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"} - } - - if len(comment) > 140 { - return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"} - } - } - } - - return nil, nil -}) -``` - - - -对传入对象直接进行的修改不会被保存。如需拒绝修改,可以让函数返回一个错误。 - -传入的对象是一个尚未保存到数据库的临时对象,并不保证与最终储存到数据库的对象完全相同,这是因为修改中可能包含自增、数组增改、关系增改等原子操作。 - -### AfterUpdate - - -本 Hook 使用不当可能会造成死循环,导致数据存储 API 的调用次数暴涨,甚至产生更多的费用。因此请仔细阅读 [防止死循环调用](#防止死循环调用) 部分,做出必要的调整和预防措施。 - - -在更新已存在的对象后执行特定的动作。和 BeforeUpdate 一样,你可以知道哪些字段已被修改。 - - - -```js -AV.Cloud.afterUpdate('Review', function(request) { - if (request.object.updatedKeys.indexOf('comment') != -1) { - if (request.object.get('comment').length < 5) { - console.log(review.ObjectId + " 看起来像灌水评论:" + comment) - } - } -}); -``` -```python -@engine.after_update('Review') -def after_review_update(article): - if 'comment' in obj.updated_keys and len(obj.get('comment')) < 5: - print(review.ObjectId + " 看起来像灌水评论:" + comment) -``` -```php -Cloud::afterUpdate("Review", function($review, $user) { - if (in_array("comment", $review->updatedKeys) && - strlen($review->get("comment")) < 5) { - error_log(review.ObjectId . " 看起来像灌水评论:" . comment); - } -}); -``` -```java -@EngineHook(className = "Review", type = EngineHookType.afterUpdate) -public static void reviewAfterUpdateHook(LCObject review) throws Exception { - List updateKeys = EngineRequestContext.getUpdateKeys(); - for (String key : updateKeys) { - if ("comment".equals(key) && review.getString("comment").length()<5) { - LOGGER.d(review.ObjectId + " 看起来像灌水评论:" + comment); - } - } -} -``` -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.AfterUpdate)] -public static void ReviewAfterUpdate(LCObject review) { - ReadOnlyCollection updatedKeys = review.GetUpdatedKeys(); - if (updatedKeys.Contains("comment")) { - string comment = review["comment"] as string; - if (comment.Length < 5) { - Console.WriteLine($"{review.ObjectId} 看起来像灌水评论:{comment}"); - } - } -} -``` -```go -leancloud.AfterUpdate("Review", func(req *ClassHookRequest) error { - updatedKeys := req.UpdatedKeys() - for _, v := range updatedKeys { - if v == "comment" { - comment, ok := req.Object.Raw()["comment"].(string) - if !ok { - return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"} - } - - if len(comment) < 5 { - fmt.Println(req.Object.ID, " 看起来像灌水评论:", comment)) - } - } - } - - return nil -}) -``` - - - -#### 防止死循环调用 - -你也许会好奇为什么可以在 AfterUpdate 中保存 `post` 而不会再次触发该 hook。 -这是因为云引擎对所有传入对象做了处理,以阻止死循环调用的产生。 - -不过请注意,以下情况还需要开发者自行处理: - -- 对传入对象进行 `fetch` 操作。 -- 重新构造传入的对象。 - -对于使用上述方式产生的对象,请根据需要自行调用禁用 hook 的接口: - - - -```js -// 直接修改并保存对象不会再次触发 afterUpdate Hook 函数 -request.object.set('foo', 'bar'); -request.object.save().then(function (obj) { - // 你的业务逻辑 -}); - -// 如果有 fetch 操作,则需要在新获得的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 -request.object.fetch().then(function (obj) { - obj.disableAfterHook(); - obj.set('foo', 'bar'); - return obj.save(); -}).then(function (obj) { - // 你的业务逻辑 -}); - -// 如果是其他方式构建对象,则需要在新构建的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 -var obj = AV.Object.createWithoutData('Post', request.object.id); -obj.disableAfterHook(); -obj.set('foo', 'bar'); -obj.save().then(function (obj) { - // 你的业务逻辑 -}); -``` -```python -@engine.after_update('Post') -def after_post_update(post): - # 直接修改并保存对象不会再次触发 after_update Hook 函数 - post.set('foo', 'bar') - post.save() - - # 如果有 fetch 操作,则需要在新获得的对象上调用 disable_after_hook 来确保不会再次触发 Hook 函数 - post.fetch() - post.disable_after_hook() - post.set('foo', 'bar') - - # 如果是其他方式构建对象,则需要在新构建的对象上调用 disable_after_hook 来确保不会再次触发 Hook 函数 - post = leancloud.Object.extend('Post').create_without_data(post.id) - post.disable_after_hook() - post.save() -``` -```php -Cloud::afterUpdate("Post", function($post, $user) { - // 直接修改并保存对象不会再次触发 afterUpdate Hook 函数 - $post->set('foo', 'bar'); - $post->save(); - - // 如果有 fetch 操作,则需要在新获得的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 - $post->fetch(); - $post->disableAfterHook(); - $post->set('foo', 'bar'); - $post->save(); - - // 如果是其他方式构建对象,则需要在新构建的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 - $post = LeanObject::create("Post", $post->getObjectId()); - $post->disableAfterHook(); - $post->save(); -}); -``` -```java -@EngineHook(className="Post", type = EngineHookType.afterUpdate) -public static void afterUpdatePost(LCObject post) throws LCException { - // 直接修改并保存对象不会再次触发 afterUpdate Hook 函数 - post.put("foo", "bar"); - post.save(); - - // 如果有 fetch 操作,则需要在新获得的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 - post.fetch(); - post.disableAfterHook(); - post.put("foo", "bar"); - - // 如果是其他方式构建对象,则需要在新构建的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数 - post = LCObject.createWithoutData("Post", post.getObjectId()); - post.disableAfterHook(); - post.save(); -} -``` -```cs -// 直接修改并保存对象不会再次触发 afterUpdate Hook 函数 -post["foo"] = "bar"; -await post.Save(); - -// 如果有 fetch 操作,则需要在新获得的对象上调用 DisableAfterHook 来确保不会再次触发 Hook 函数 -await post.Fetch(); -post.DisableAfterHook(); -post["foo"] = "bar"; - -// 如果是其他方式构建对象,则需要在新构建的对象上调用 DisableAfterHook 来确保不会再次触发 Hook 函数 -post = LCObject.CreateWithoutData("Post", post.ObjectId); -post.DisableAfterHook(); -await post.Save(); -``` -```go -// Go SDK 目前尚未提供 DisableAfterHook 接口。 -``` - - - -### BeforeDelete - -在删除一个对象之前做一些检查工作,比如在删除一个相册 `Album` 前,先检查一下该相册中还有没有照片 `Photo`: - - - -```js -AV.Cloud.beforeDelete('Album', function (request) { - // 查询 Photo 中还有没有属于这个相册的照片 - var query = new AV.Query('Photo'); - var album = AV.Object.createWithoutData('Album', request.object.id); - query.equalTo('album', album); - return query.count().then(function (count) { - if (count > 0) { - // delete 操作会被丢弃 - throw new AV.Cloud.Error('Cannot delete an album if it still has photos in it.'); - } - }, function (error) { - throw new AV.Cloud.Error('Error ' + error.code + ' occurred when finding photos: ' + error.message); - }); -}); -``` -```python -import leancloud - -@engine.before_delete('Album') # Album 为需要 hook 的 class 的名称 -def before_album_delete(album): - query = leancloud.Query('Photo') - query.equal_to('album', album) - try: - matched_count = query.count() - except leancloud.LeanCloudError: - raise engine.LeanEngineError(message='An error occurred with LeanEngine.') - if count > 0: - # delete 操作会被丢弃 - raise engine.LeanEngineError(message='Cannot delete an album if it still has photos in it.') -``` -```php -Cloud::beforeDelete("Album", function($album, $user) { - $query = new Query("Photo"); - $query->equalTo("album", $album); - try { - $count = $query->count(); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred when getting photo count: {$ex->getMessage()}"); - } - if ($count > 0) { - // delete 操作会被丢弃 - throw new FunctionError("Cannot delete an album if it still has photos in it."); - } -}); -``` -```java -@EngineHook(className = "Album", type = EngineHookType.beforeDelete) -public static LCObject albumBeforeDeleteHook(LCObject album) throws Exception { - LCQuery query = new LCQuery("Photo"); - query.whereEqualTo("album", album); - int count = query.count(); - if (count > 0) { - // delete 操作会被丢弃 - throw new Exception("Cannot delete an album if it still has photos in it."); - } else { - return album; - } -} -``` -```cs -[LCEngineClassHook("Album", LCEngineObjectHookType.BeforeDelete)] -public static async Task AlbumBeforeDelete(LCObject album) { - LCQuery query = new LCQuery("Photo"); - query.WhereEqualTo("album", album); - int count = await query.Count(); - if (count > 0) { - throw new Exception("Cannot delete an album if it still has photos in it."); - } - return album; -} -``` -```go -leancloud.BeforeDelete("Album", func(req *ClassHookRequest) (interface{}, error) { - photo := new(Photo) - if err := req.Object.Clone(photo); err != nil { - return nil, err - } - - count, err := client.Class("Photo").NewQuery().EqualTo("album", photo.Album).Count() - if err != nil { - return nil, err - } - - if count > 0 { - return nil, leancloud.CloudError{Code: 500, Message: "Cannot delete an album if it still has photos in it."} - } - - fmt.Println("Deleted.") - - return nil, nil -}) -``` - - - -### AfterDelete - -在一个对象后被删执行操作,例如递减计数、删除关联对象等等。同样以相册为例,这次我们不在删除相册前检查是否还有照片,而是在删除后,同时删除相册中的照片: - - - -```js -AV.Cloud.afterDelete('Album', function (request) { - var query = new AV.Query('Photo'); - var album = AV.Object.createWithoutData('Album', request.object.id); - query.equalTo('album', album); - return query.find().then(function (posts) { - return AV.Object.destroyAll(posts); - }).catch(function (error) { - console.error('Error ' + error.code + ' occurred when finding photos: ' + error.message); - }); -}); -``` -```python -import leancloud - -@engine.after_delete('Album') # Album 为需要 hook 的 class 的名称 -def after_album_delete(album): - query = leancloud.Query('Photo') - query.equal_to('album', album) - try: - query.destroy_all() - except leancloud.LeanCloudError: - raise leancloud.LeanEngineError(message='An error occurred with LeanEngine.') -``` -```php -Cloud::afterDelete("Album", function($album, $user) { - $query = new Query("Photo"); - $query->equalTo("album", $album); - try { - $photos = $query->find(); - LeanObject::destroyAll($photos); - } catch (CloudException $ex) { - throw new FunctionError("An error occurred when getting photo count: {$ex->getMessage()}"); - } -}); -``` -```java -@EngineHook(className = "Album", type = EngineHookType.afterDelete) -public static void albumAfterDeleteHook(LCObject album) throws Exception { - LCQuery query = new LCQuery("Photo"); - query.whereEqualTo("album", album); - List result = query.find(); - if (result != null && !result.isEmpty()) { - LCObject.deleteAll(result); - } -} -``` -```cs -[LCEngineClassHook("Album", LCEngineObjectHookType.AfterDelete)] -public static async Task AlbumAfterDelete(LCObject album) { - LCQuery query = new LCQuery("Photo"); - query.WhereEqualTo("album", album); - ReadOnlyCollection result = await query.Find(); - if (result != null && result.Count > 0) { - await LCObject.DeleteAll(result); - } -} -``` -```go -leancloud.AfterDelete("Album", func(req *ClassHookRequest) error { - photo := new(Photo) - if err := req.Object.Clone(photo); err != nil { - return nil, err - } - - count, err := client.Class("Photo").NewQuery().EqualTo("album", photo.Album).Count() - if err != nil { - return nil, err - } - - if count > 0 { - return nil, leancloud.CloudError{Code: 500, Message: "An error occurred with LeanEngine."} - } - - fmt.Println("Deleted.") - - return nil, nil -}) -}) -``` - - - -### OnVerified - -当用户通过邮箱或者短信验证时,对该用户执行特定操作。比如: - - -<> - -```js -AV.Cloud.onVerified('sms', function (request) { - console.log('User ' + request.object + ' is verified by SMS.'); -}); -``` - -上面的代码示例中的 `object` 换成 `currentUser` 也可以。因为这里被操作的对象正好是发起操作的用户。 -下面的 `onLogin` 函数同理。 - - -<> - -```python -@engine.on_verified('sms') -def on_sms_verified(user): - print user -``` - - -<> - -```php -Cloud::onVerifed("sms", function($user, $meta) { - error_log("User {$user->getUsername()} is verified by SMS."); -}); -``` - - -<> - -```java -@EngineHook(className = "_User", type = EngineHookType.onVerifiedSMS) -public static void userOnVerifiedHook(LCUser user) throws Exception { - LOGGER.d("用户 " + user.getObjectId() + " 已通过短信验证。"); -} - -@EngineHook(className = "_User", type = EngineHookType.onVerifiedEmail) -public static void userOnVerifiedHook(LCUser user) throws Exception { - LOGGER.d("用户 " + user.getObjectId() + " 已通过邮箱验证。"); -} -``` - - - -<> - -```cs -[LCEngineUserHook(LCEngineUserHookType.OnSMSVerified)] -public static void OnVerifiedSMS(LCUser user) { - Console.WriteLine($"用户 {user.ObjectId} 已通过短信验证。"); -} - -[LCEngineUserHook(LCEngineUserHookType.OnEmailVerified)] -public static void OnVerifiedEmail(LCUser user) { - Console.WriteLine($"用户 {user.ObjectId} 已通过邮箱验证。"); -} -``` - - - -<> - -```go -leancloud.OnVerified("sms", func(req *ClassHookRequest) error { - fmt.Println("用户 ", req.User.ID, " 已通过短信验证。") -}) - -leancloud.OnVerified("email", func(req *ClassHookRequest) error { - fmt.Println("用户 ", req.User.ID, " 已通过邮箱验证。") -}) -``` - - - - -数据库中相关的验证字段,如 `emailVerified` 不需要修改,系统会自动更新。 - -该 hook 属于 after 类 hook. - -### OnLogin - -在用户登录之时执行指定操作,比如禁止在黑名单上的用户登录: - - - -```js -AV.Cloud.onLogin(function (request) { - // 因为此时用户还没有登录,所以用户信息是保存在 request.object 对象中 - console.log('User ' + request.object + ' is trying to log in.'); - if (request.object.get('username') === 'noLogin') { - // 如果是 error 回调,则用户无法登录(收到 401 响应) - throw new AV.Cloud.Error('Forbidden'); - } -}); -``` -```python -@engine.on_login -def on_login(user): - print user - if user.get('username') == 'noLogin': - # 如果抛出 LeanEngineError,则用户无法登录(收到 401 响应) - raise LeanEngineError('Forbidden') - # 没有抛出异常,函数正常执行完毕的话,用户可以登录 -``` -```php -Cloud::onLogin(function($user) { - error_log("User {$user->getUsername()} is trying to log in."); - if ($user->get("blocked")) { - // 如果抛出异常,则用户无法登录(收到 401 响应) - throw new FunctionError("Forbidden"); - } - // 没有抛出异常,函数正常执行完毕的话,用户可以登录 -}); -``` -```java -@EngineHook(className = "_User", type = EngineHookType.onLogin) -public static LCUser userOnLoginHook(LCUser user) throws Exception { - if ("noLogin".equals(user.getUsername())) { - throw new Exception("Forbidden"); - } else { - return user; - } -} -``` -```cs -[LCEngineUserHook(LCEngineUserHookType.OnLogin)] -public static LCUser OnLogin(LCUser user) { - if (user.Username == "noLogin") { - throw new Exception("Forbidden"); - } - return user; -} -``` -```go -leancloud.OnLogin(func(req *ClassHookRequest) error { - fmt.Println("用户 ", req.User.ID, " 已登录。") -}) -``` - - - -该 hook 属于 before 类 hook. - -### 即时通讯 Hook 函数 - -参见[即时通讯指南第四篇](/v2/sdk/im/guide/systemconv)的《万能的 Hook 机制》章节。 - -### Hook 函数错误响应码 - -为 `BeforeSave` 这类的 hook 函数定义错误码,需要这样: - - - -```js -AV.Cloud.beforeSave('Review', function (request) { - // 使用 JSON.stringify() 将 object 变为字符串 - throw new AV.Cloud.Error(JSON.stringify({ - code: 123, - message: 'An error occurred.' - })); -}); -``` -```python -@engine.before_save('Review') # Review 为需要 hook 的 class 的名称 -def before_review_save(review): - comment = review.get('comment') - if not comment: - raise leancloud.LeanEngineError( - code=123, - message='An error occurred.' - ) -``` -```php -Cloud::beforeSave("Review", function($review, $user) { - $comment = $review->get("comment"); - if (!$comment) { - throw new FunctionError(json_encode(array( - "code" => 123, - "message" => "An error occurred.", - ))); - } -}); -``` -```java -@EngineHook(className = "Review", type = EngineHookType.beforeSave) -public static LCObject reviewBeforeSaveHook(LCObject review) throws Exception { - throw new LCException(123, "An error occurred."); -} -``` -```cs -[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeDelete)] -public static void ReviewBeforeDelete(LCObject review) { - throw new LCException(123, "An error occurred."); -} -``` -```go -leancloud.BeforeSave("Review", func(req *ClassHookRequest) (interface{}, error) { - return nil, leancloud.CloudError{Code: 123, Message: "An error occurred."} -}) -``` - - - -客户端收到的响应为 `Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." }`。可通过 **截取字符串** 的方式取出错误信息,再转换成需要的对象。 - -### Hook 函数超时 - -Before 类 Hook 函数的超时时间为 10 秒,其他类 Hook 函数的超时时间为 3 秒。如果 Hook 函数被其他的云函数调用(比如因为保存对象而触发 `BeforeSave` 和 `AfterSave`),那么它们的超时时间会进一步被其他云函数调用的剩余时间限制。 - -例如,如果一个 `BeforeSave` 函数是被一个已经运行了 13 秒的云函数触发,那么它就只剩下 2 秒的时间来运行。同时请参考 [云函数超时及处理方案](#云函数超时)。 - - -## 在线编写云函数 - -很多人使用云引擎是为了在服务端提供一些个性化的方法供各终端调用,而不希望关心诸如代码托管、npm 依赖管理等问题。为此我们提供了在线维护云函数的功能。使用此功能需要注意: - -- 在线定义的函数会覆盖你之前用 Git 或命令行部署的项目。 -- 目前只能在线编写云函数和 Hook,不支持托管静态网页、编写动态路由。 -- 只能使用 JavaScript SDK 和一些内置的 Node.js 模块(详见下节表格),无法引入其他模块作为依赖。 - -**功能地址** - -![](https://capacity-files.lcfile.com/5DF59OXNHAQFIygBz9KCRchixpyLRnQf/Frame%202.png) - -在 **云服务控制台 > 云引擎 > 云引擎分组 > 部署 > 在线编辑** 标签页,可以: - -- **创建函数**:指定函数类型、函数名称、函数体的具体代码、注释等信息,点击「保存」即可创建一个云函数。 -- **部署**:选择要部署的环境,点击「部署」即可看到部署过程和结果。 -- **预览**:会将所有函数汇总并生成一个完整的代码段,可以确认代码,或者将其保存为 `cloud.js` 覆盖项目模板的同名文件,即可快速的转换为使用项目部署。 -- **维护云函数**:可以编辑已有云函数,查看保存历史,以及删除云函数。 - -云函数编辑之后需要点击 **部署** 才能生效。 - -### 在线编写的 SDK 版本 - -目前在线编辑仅支持 Node.js,提供了 4 种 SDK 版本: - -在线编辑版本 |Node.js SDK|JS SDK|Node.js| 备注 | 可用依赖 ----|---|---|---|---|--- -v0|0.x|0.x|0.12| 已不推荐使用 |moment, request, underscore -v1|1.x|1.x|4||async, bluebird, co, ejs, handlebars, joi, lodash, marked, moment, q, request, superagent, underscore -v2|2.x|2.x|6| 需要使用 Promise 写法 |async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js -v3|3.x|3.x|8| 需要使用 Promise 写法 |async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js - -**从 v0 升级到 v1:** - -- JS SDK 升级到了 [1.0](https://github.com/leancloud/javascript-sdk/releases/tag/v1.0.0)。 -- 需要从 `request.currentUser` 获取用户,而不是 `AV.User.current`。 -- 在调用 `AV.Cloud.run` 时需要手动传递 user 对象。 - -**从 v1 升级到 v2:** - -- JS SDK 升级到 [2.0](https://github.com/leancloud/javascript-sdk/releases/tag/v2.0.0)(必须使用 Promise,不再支持 callback 风格)。 -- 删除了 `AV.Cloud.httpRequest`。 -- 在云函数中 **必须** 返回 Promise 作为云函数的值,抛出 `AV.Cloud.Error` 来表示错误。 - -**从 v2 升级到 v3:** - -- JS SDK 升级到了 [3.0](https://github.com/leancloud/javascript-sdk/releases/tag/v3.0.0)(`AV.Object.toJSON` 的行为变化等)。 - -## 查看和运行云函数 - -**云服务控制台 > 云引擎 > 云函数**页面以表格的形式展示了应用所属各分组上定义的云函数(包括 Hook)的基本信息,包括云函数名称、所属分组、QPM(每分钟请求数)。 -在云函数表格中,点击**运行**按钮可以通过控制台调用云函数。 -通过左上角的控制按钮则可以刷新页面、切换预备环境和生产环境。 - -## 定时任务 - -定时任务可以按照设定,以一定间隔自动完成指定动作,比如半夜清理过期数据,每周一向所有用户发送推送消息等等。定时任务的最小时间单位是 **秒**,正常情况下时间误差都可以控制在秒级别。 - -定时任务是普通的云函数,也会遇到 [超时问题](#云函数超时),具体请参考 [超时处理方案](#超时的处理方案)。 - -一个定时任务如果在 24 小时内收到了超过 30 次的 `400`(Bad Request)或 `502`(Bad Gateway)的应答,它将会被云引擎禁用,同时系统会向开发者发出相关的禁用通知邮件。在控制台的日志中,对应的错误信息为 `timerAction short-circuited and no fallback available`。 - -部署云引擎之后,进入 **云服务控制台 > 云引擎 > 定时任务**,点击 **创建定时任务**,然后设定执行的函数名称、执行环境等等。例如定义一个打印循环打印日志的任务 `logTimer`: - - - -```js -AV.Cloud.define('logTimer', function (request) { - console.log('This log is printed by logTimer.'); -}); -``` -```python -@engine.define -def logTimer(movie, **params): - print('This log is printed by logTimer.') -``` -```php -Cloud::define("logTimer", function($params, $user) { - error_log("This log is printed by logTimer."); -}); -``` -```java -@EngineFunction("logTimer") -public static float logTimer throws Exception { - LogUtil.avlog.d("This log is printed by logTimer."); -} -``` -```cs -[LCEngineFunction("logTimer")] -public static void LogTimer() { - Console.WriteLine("This log is printed by logTimer."); -} -``` -```go -leancloud.Define("logTimer", func(req *FunctionRequest) (interface{}, error) { - fmt.Println("This log is printed by logTimer.") - return nil, nil -}) -``` - - - -定时任务分为两类: - -- 使用 Cron 表达式安排调度 -- 以分钟为单位的简单循环调度 - -以 Cron 表达式为例,比如每周一早上 8 点打印日志(运行之前定义的 `logTimer` 函数),创建定时任务的时候,选择 **Cron 表达式** 并填入 `0 0 8 ? * MON`。 - -Cron 表达式的语法可以参考[云队列指南](/v2/sdk/engine/guide/cloudqueue)的《Cron 表达式》一节。 - -点击「非必填」会展开更多选项: - -- 运行参数:传递给云函数的参数(JSON 对象)。 -- 异常策略:任务因云函数超时失败后重试执行还是放弃执行。 - -定时任务创建后,会显示「最近一次执行时间」和「下次执行时间」。 -「最近一次执行时间」除了显示时间外,还会显示执行结果,点击「查看详情」还可以查看细节: - -- `status` 任务的状态,包括 `success`(成功)、`failed`(失败) -- `uniqueId` 任务的唯一 ID -- `finishedAt` 执行完成的精确时间(仅限成功任务) -- `statusCode` 云函数响应的 HTTP 状态码(仅限成功任务) -- `result` 来自云函数的响应(仅限成功任务) -- `error` 错误提示(仅限失败任务) -- `retryAt` 下次重试时间(仅限失败任务) - -具体的执行日志可以在**云服务控制台 > 云引擎 > 云引擎分组 > 日志** 查看。 -例如: - -``` -CloudQueue 运行失败 24e1b480-aeb5-4222-ab7d-7d4b8ee170b9: hello !! {"error":"Error: ESOCKETTIMEDOUT"} -``` - -如果希望暂停定时任务(比如遇到报错需要排查),可以点击状态栏的「暂停」按钮。 -相应地,点击「启用」按钮可以重新启用暂停的任务。 -点击操作栏的「编辑」、「删除」按钮则可以修改、删除定时任务。 - -## Master Key 和超级权限 - -因为云引擎运行在可信的服务器端环境中,所以你可以全局开启超级权限(`Master Key`),这样云端会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据,当然这种方式也允许调用一些仅供 `Master Key` 使用的 API。开启 `Master Key` 的方法如下: - - -<> - -```js -// 通常位于 server.js -AV.Cloud.useMasterKey(); -``` - -如果没有添加这些代码,默认是没有超级权限的,这意味着在云引擎中你也不能修改被 ACL 保护的数据,你需要在进行操作时手动指定 `sessionToken`,让操作以这个用户的权限来执行: - -```js -const post = new Post(); -post.save( - { author: user }, - // 或者使用 request.sessionToken(网站托管中需启用 `Cloud.CookieSession`) - // { - // sessionToken: user.getSessionToken() - // } -); -``` - -或者你也可单独对某一个操作使用 `Master Key`,跳过权限检查: - -```js -post.destroy({ useMasterKey: true }); -``` - -当然你也可以在启用了超级权限的情况下使用 `useMasterKey: false` 来对单个操作关掉超级权限。 - - -<> - -```python -# 通常位于 wsgi.py -leancloud.use_master_key(True) -``` - - -<> - -```php -// 通常位于 src/app.php -Client::useMasterKey(true); -``` - - -<> - -```java -// 通常位于 src/…/AppInitListener.java -RequestSignImplementation.setMasterKey(appMasterKey); -``` - - -<> - -```cs -// 暂不支持 -``` - - -<> - -SDK 中每个请求都可以使用 `UseMasterKey()` 为请求带上 `Master Key` 来开启超级权限,只需要作为可选参数传入最后即可,例如 `Create` `Set` `Update` 等操作。 - - - - -那么究竟是否应该使用超级权限呢,我们的建议如下: - -- 如果你的云引擎代码中特权操作比较多、操作不属于用户的全局数据比较多,那么建议全局开启 `Master Key`,并自行做好对于用户请求的权限检查。 -- 如果你的云引擎代码中的请求通常和单个用户自己的数据相关、需要遵守 ACL,那么建议不开启 `Master Key`,将用户请求的 `sessionToken` 传入数据修改的相关操作。 - -关于云引擎上的权限问题,还可以参考[《ACL 权限管理开发指南》](https://leancloud.cn/docs/acl-guide.html)和[《在云引擎中使用 ACL》](/sdk/storage/guide/engine-acl/)。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/05-cli.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/05-cli.mdx deleted file mode 100644 index 06cab80ae..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/05-cli.mdx +++ /dev/null @@ -1,405 +0,0 @@ ---- -id: cli -title: 云引擎命令行工具使用指南 -sidebar_label: 命令行工具 ---- - - - -命令行工具是用来管理和部署云引擎项目的工具。它不仅可以部署、发布和回滚云引擎代码,对同一个云引擎项目做多应用管理,还能查看云引擎日志,批量将文件上传到云端。 - -## 安装命令行工具 - -### macOS - -目前 `tds` 暂未发布到 Homebrew,macOS 用户请先到 [GitHub releases 页面] 下载二进制文件 `tds-macos-x64`,赋予可执行权限(`chmod a+x tds-macos-x64`),重命名为 `tds` 并放到已经在 PATH 环境变量中声明的任意目录中即可。 - -[GitHub releases 页面]: https://releases.leanapp.cn/#/leancloud/lean-cli/releases - -### Windows - -Windows 用户可以在 [GitHub releases 页面]根据操作系统版本下载最新的 32 位 或 64 位 **msi** 安装包进行安装,安装成功之后在 Windows 命令提示符(或 PowerShell)下直接输入 `tds` 命令即可使用。 - -也可以选择编译好的绿色版 **exe** 文件,下载后将此文件更名为 `tds.exe`,并将其路径加入到系统 **PATH** 环境变量([设置方法](https://www.java.com/zh_CN/download/help/path.xml))中去。这样使用时在 Windows 命令提示符(或 PowerShell)下,在任意目录下输入 `tds` 就可以使用命令行工具了。当然也可以将此文件直接放到已经在 PATH 环境变量中声明的任意目录中去,比如 `C:\Windows\System32` 中。 - -### Linux - -基于 Debian 的发行版可以从 [GitHub releases 页面] 下载 deb 包安装。 - -其他发行版可以从 [GitHub releases 页面] 下载预编译好的二进制文件 `tds-linux-x64`,赋予可执行权限(`chmod a+x tds-linux-x64`),重命名为 `tds` 并放到已经在 PATH 环境变量中声明的任意目录中即可。 - -### 通过源码安装 - -请参考项目源码 [README](https://github.com/leancloud/lean-cli)。 - -### 升级 - -下载最新的文件,重新执行一遍安装流程,即可把旧版本的命令行工具覆盖,升级到最新版。 - -## 使用 - -安装成功之后,直接在 terminal 终端运行 `tds help`,输出帮助信息: - -```sh -NAME: - tds - Command line to manage and deploy LeanCloud apps - -USAGE: - tds [global options] command [command options] [arguments...] - -VERSION: - 0.25.0 - -COMMANDS: - login Log in to LeanCloud - switch Change the associated LeanCloud app - metric Obtain LeanStorage performance metrics of current project - info Show information about the current user and app - up Start a development instance locally - init Initialize a LeanEngine project - deploy Deploy the project to LeanEngine - publish Publish code from staging to production - upload Upload files to the current application (available in the '_File' class) - logs Show LeanEngine logs - debug Start the debug console without running the project - env Output environment variables used by the current project - cache LeanCache shell - cql Start CQL interactive mode (warn: CQL is deprecated) - help, h Show all commands or help info for one command - -GLOBAL OPTIONS: - --version, -v print the version -``` - -简单介绍下主要的子命令: - -命令 | 用途 -- | - -`login` | 登录 LeanCloud 账号 -`switch` | 切换关联的云引擎项目 -`metric` | 当前项目的 LeanStorage 统计信息 -`info` | 当前用户、应用 -`up` | 启动本地开发调试实例 -`init` | 初始化云引擎项目 -`deploy` | 部署项目至云引擎 -`publish` | 部署至生产环境 -`upload` | 上传文件至当前应用(可以在 `_File` 类中查看) -`logs` | 显示云引擎日志 -`debug` | 单独运行云函数调试功能,而不在本地运行项目本身 -`env` | 显示当前项目的环境变量 -`cache` | LeanCache 命令行 - -可以通过 `--version` 选项查看版本: - -```sh -$ tds --version -tds version 0.25.0 -``` - -`tds command -h` 可以查看子命令的帮助信息,例如: - -```sh -$ tds deploy -h -NAME: - tds deploy - Deploy the project to LeanEngine - -USAGE: - tds deploy [command options] [arguments...] - -OPTIONS: - -g Deploy from git repo - --war Deploy .war file for Java project. The first .war file in target/ is used by default - --no-cache Force download dependencies - --overwrite-functions Overwrite cloud functions with the same name in other groups - --leanignore value Rule file for ignored files in deployment (default: ".leanignore") - --message value, -m value Comment for this deployment, only applicable when deploying from local files - --keep-deploy-file - --revision value, -r value Git revision or branch. Only applicable when deploying from Git (default: "master") - --options --options build-root=app&atomic=true Send additional deploy options to server, in urlencode format(like --options build-root=app&atomic=true) - --prod --prod 1 Deploy to production(--prod 1) or staging(`--prod 0`) environment, default to staging if it exists - --direct Upload project's tarball to remote directly -``` - -下文中凡是以 `$ tds` 开头的文字即表示在终端里执行命令。 - -## 登录 - -安装完命令行工具之后,首先第一步需要登录云服务账户。 -请进入开发者后台,点击左侧「创建游戏」按照需要填写基础信息和基础游戏资料,然后进入对应的游戏,依次进入**游戏服务 > 技术服务 > 云引擎 > 开启 > 部署项目 > 命令行工具部署**,按照指引登录你的云服务账户。 - -### 切换账户 - -要切换到另一账户,重新执行 `tds login` 即可。 - -## 初始化项目 - -登录完成之后,可以使用 `tds init` 命令来初始化一个项目,并且关联到已有的云服务应用上。 - -```sh -[?] Please select an app: - 1) AwesomeApp - 2) Foobar -``` - -选择项目语言/框架: - -```sh -[?] Please select a language - 1) Node.js - 2) Python - 3) Java - 4) PHP - 5) .Net - 6)Go - 7) Others -``` - -之后命令行工具会将此项目模版下载到本地,这样初始化就完成了: - -```sh -[INFO] Downloading templates 6.33 KiB / 6.33 KiB [==================] 100.00% 0s -[INFO] Creating project... -``` - -进入以应用名命名的目录就可以看到新建立的项目。 - -## 关联已有项目 - -如果已经使用其他方法创建好了项目,可以直接在项目目录执行: - -```sh -$ tds switch -``` - -将已有项目关联到云服务应用上。 - -## 切换分组 - -如果应用启用了云引擎多分组功能,同样可以使用 `$ tds switch` 命令切换当前目录关联的分组。 - -## 本地运行 - -如果想将一份代码简单地部署到服务器而不在本地运行和调试,可以暂时跳过此章节。 - -进入项目目录: - -```sh -$ cd AwesomeApp -``` - -安装此项目相关的依赖后,可以通过命令行工具启动应用: - -```sh -$ tds up -``` - -- 在浏览器中打开 ,进入 web 应用的首页。 -- 在浏览器中打开 ,进入云引擎云函数和 Hook 函数调试界面。 - -注意,如果想变更启动端口号,可以使用 `tds up --port 新端口号` 命令来指定。 - -旧版命令行工具可以在 `$ tds up` 的过程中,监测项目文件的变更,实现自动重启开发服务进程。新版命令行工具移除了这一功能,转由项目代码本身来实现,以便更好地与项目使用的编程语言或框架集成。 - -除了使用命令行工具来启动项目之外,还可以**原生地**启动项目,比如直接使用 `node server.js` 或者 `python wsgi.py`。这样能够将云引擎开发流程更好地集成到开发者管用的工作流程中,也可以直接和 IDE 集成。但是直接使用命令行工具创建的云引擎项目,默认会依赖一些环境变量,因此需要提前设置好这些环境变量。 - -使用命令 `tds env` 可以显示出这些环境变量,手动在当前终端中设置好之后,就可以不依赖命令行工具来启动项目了。另外使用兼容 `sh` shell 的用户,还可以直接使用 `eval $(tds env)`,自动设置好所有的环境变量。 - -启动时还可以给启动命令增加自定义参数,在 `tds up` 命令后增加两个横线 `--`,所有在横线后的参数会被传递到实际执行的命令中。比如启动 node 项目时,想增加 `--inspect` 参数给 node 进程,来启动 node 自带的远程调试功能,只要用 `tds up -- --inspect` 来启动项目即可。 - -另外还可以使用 `--cmd` 来指定启动命令,这样即可使用任意自定义命令来执行项目:`tds up --cmd=my-custom-command`。 - -有些情况下,我们需要让 IDE 来运行项目,或者需要调试在虚拟机/远程机器上的项目的云函数,这时可以单独运行云函数调试功能,而不在本地运行项目本身: - -```sh -$ tds debug --remote=http://remote-url-or-ip-address:remote-port --app-id=xxxxxx -``` - -更多关于云引擎开发的内容,请参考[云引擎服务总览](/v2/sdk/engine/guide/overview)。 - -## 部署 - -### 从本地代码部署 - -当开发和本地测试云引擎项目通过后,你可以直接将本地源码推送到 LeanCloud 云引擎平台运行: - -```sh -$ tds deploy -``` - -对于生产环境是**体验实例**的云引擎的应用,这个命令会将本地源码部署到线上的生产环境,无条件覆盖之前的代码(无论是从本地仓库部署、Git 部署还是在线定义);而对于生产环境是**标准实例**的云引擎的应用,这个命令会先部署到**预备环境**,后续需要使用 `tds publish` 来完成向生产环境的部署,如需直接部署到生产环境,可额外添加 `--prod 1` 选项: - -```sh -$ tds deploy --prod 1 -``` - -部署过程会实时打印进度: - -```sh -$ tds deploy -[INFO] Current CLI tool version: 0.21.0 -[INFO] Retrieving app info ... -[INFO] Preparing to deploy AwesomeApp(xxxxxx) to region: cn group: web staging -[INFO] Python runtime detected -[INFO] pyenv detected. Please make sure pyenv is configured properly. -[INFO] Uploading file 6.40 KiB / 6.40 KiB [=========================] 100.00% 0s -[REMOTE] 开始构建 20181207-115634 -[REMOTE] 正在下载应用代码 ... -[REMOTE] 正在解压缩应用代码 ... -[REMOTE] 运行环境: python -[REMOTE] 正在下载和安装依赖项 ... -[REMOTE] 存储镜像到仓库(0B)... -[REMOTE] 镜像构建完成:20181207-115634 -[REMOTE] 开始部署 20181207-115634 到 web-staging -[REMOTE] 正在创建新实例 ... -[REMOTE] 正在启动新实例 ... -[REMOTE] [Python] 使用 Python 3.7.1, Python SDK 2.1.8 -[REMOTE] 实例启动成功:{"version": "2.1.8", "runtime": "cpython-3.7.1"} -[REMOTE] 正在更新云函数信息 ... -[REMOTE] 部署完成:1 个实例部署成功 -[INFO] Deleting temporary files -``` - -默认部署备注为「从命令行工具构建」,显示在 **云服务控制台 > 云引擎 > 云引擎分组 > 日志** 中。你可以通过 `-m` 选项来自定义部署的备注信息: - -```sh -$ tds deploy -m 'fix #42' -``` - -部署之后需要绑定一个云引擎自定义域名,然后就可以通过 curl 命令来测试你的云引擎代码,或者通过浏览器访问相应的网址。 - -#### 部署时忽略部分文件 - -部署项目时,如果有一些临时文件或是项目源码管理软件用到的文件,不需要上传到服务器,可以将它们加入到 `.leanignore` 文件。 - -`.leanignore` 文件格式与 Git 使用的 `.gitignore` 格式基本相同(严格地说,`.leanignore` 支持的语法为 `.gitignore` 的子集),每行写一个忽略项,可以是文件或者文件夹。如果项目没有 `.leanignore` 文件,部署时会根据当前项目所使用的语言创建一个默认的 `.leanignore` 文件。请确认此文件中的 [默认配置][defaultIgnorePatterns] 是否与项目需求相符。 - -[defaultIgnorePatterns]: https://github.com/leancloud/lean-cli/blob/master/runtimes/ignorefiles.go#L13 - -### 从 Git 仓库部署 - -如果代码保存在某个 Git 仓库上,例如 [GitHub](https://github.com),并且在 LeanCloud 控制台已经正确设置了 git repo 地址以及 deploy key,你也可以请求云引擎从 Git 仓库获取源码并自动部署。这个操作可以在云引擎的部署菜单里完成,也可以在本地执行: - -```sh -$ tds deploy -g -``` - -- `-g` 选项要求从 Git 仓库部署,Git 仓库地址必须已经在云引擎菜单中保存。 -- 默认部署使用 **master** 分支的最新代码,你可以通过 `-r ` 来指定部署特定的 commit 或者 branch。 -- 设置 git repo 地址以及 deploy key 的方法可以参考[云引擎网站托管指南](/v2/sdk/engine/guide/webhosting)的《Git 部署》一节。 -## 发布到生产环境 - -以下步骤仅适用于生产环境是标准实例的用户。 - -如果预备环境如果测试没有问题,此时需要将预备环境的云引擎代码切换到生产环境,可以在 **云服务控制台 > 云引擎 > 云引擎分组 > 部署** 中发布,也可以直接运行 `publish` 命令: - -```sh -$ tds publish -``` - -这样预备环境的云引擎代码就发布到了生产环境: - -```sh -$ tds publish -[INFO] Current CLI tool version: 0.21.0 -[INFO] Retrieving app info ... -[INFO] Deploying AwesomeApp(xxxxxx) to region: cn group: web production -[REMOTE] 开始部署 20181207-115634 到 web1,web2 -[REMOTE] 正在创建新实例 ... -[REMOTE] 正在创建新实例 ... -[REMOTE] 正在启动新实例 ... -[REMOTE] 正在启动新实例 ... -[REMOTE] 实例启动成功:{"version": "2.1.8", "runtime": "cpython-3.7.1"} -[REMOTE] 实例启动成功:{"version": "2.1.8", "runtime": "cpython-3.7.1"} -[REMOTE] 正在更新云函数信息 ... -[REMOTE] 部署完成:2 个实例部署成功 -``` - -## 查看日志 - -使用 `logs` 命令可以查询云引擎的最新日志: - -```sh -$ tds logs - 2019-11-20 17:17:12 Deploying 20191120-171431 to web1 - 2019-11-20 17:17:12 Creating new instance ... - 2019-11-20 17:17:22 Starting new instance ... -web1 2019-11-20 17:17:22 -web1 2019-11-20 17:17:22 > node-js-getting-started@1.0.0 start /home/leanengine/app -web1 2019-11-20 17:17:22 > node server.js -web1 2019-11-20 17:17:22 -web1 2019-11-20 17:17:23 Node app is running on port: 3000 - 2019-11-20 17:17:23 Instance started: {"runtime":"nodejs-v12.13.1","version":"3.4.0"} - 2019-11-20 17:17:23 Updating cloud functions metadata ... - 2019-11-20 17:17:23 Deploy finished: 1 instances deployed -``` - -默认返回最新的 30 条,最新的在最下面。 - -可以通过 `-l` 选项设定返回的日志数目,例如返回最近的 100 条: - -```sh -$ tds logs -l 100 -``` - -也可以加上 `-f` 选项来自动滚动更新日志,类似 `tail -f` 命令的效果: - -```sh -$ tds logs -f -``` - -新的云引擎日志产生后,都会被自动填充到屏幕下方。 - -如果想查询某一段时间的日志,可以指定 `--from` 和 `--to` 参数: - -``` -$ tds logs --from=2017-07-01 --to=2017-07-07 -``` - -单独使用 `--from` 参数导出从某一天到现在的日志: - -``` -$ tds logs --from=2017-07-01 -``` - -另外可以配合重定向功能,将一段时间内的 JSON 格式日志导出到文件,再配合本地工具进行查看: - -``` -$ tds logs --from=2017-07-01 --to=2017-07-07 --format=json > leanengine.logs -``` - -`--from`、`--to` 的时区为本地时区(运行 lean-cli 命令行工具的机器的本地时区)。 - -## 多应用管理 - -一个项目的代码可以同时部署到多个云服务应用上。 - -### 查看当前应用状态 - -使用 `tds info` 可以查看当前项目关联的应用: - -```sh -$ tds info -[INFO] Retrieving user info from region: cn -[INFO] Retrieving app info ... -[INFO] Current region: cn User: lan (lan@leancloud.rocks) -[INFO] Current region: cn App: AwesomeApp (xxxxxx) -[INFO] Current group: web -``` - -此时,执行 `deploy`、`publish`、`logs` 等命令都是针对当前被激活的应用。 - -### 切换应用 - -如果需要将当前项目切换到其他应用,可以使用 `switch` 命令: - -```sh -$ tds switch -``` - -之后运行向导会给出可供切换的应用列表。 - -另外还可以直接执行 `$ tds switch 其他应用的id` 来快速切换关联应用。 - -## 贡献 - -`lean-cli` 是开源项目,基于 [Apache](https://github.com/leancloud/lean-cli/blob/master/LICENSE.txt) 协议,源码托管在 ,欢迎大家贡献。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/06-rest.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/06-rest.mdx deleted file mode 100644 index 192cd68a3..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/06-rest.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -id: rest -title: 云引擎 REST API -sidebar_label: REST API ---- - - - -云服务提供了统一的访问云函数的 REST API 接口,所有的客户端 SDK 也都是封装了这个接口从而实现对云函数的调用。 - -我们推荐使用 [Postman](http://www.getpostman.com/) 来调试 REST API。 - -## Base URL - -REST API 请求的 Base URL 可以在**云服务控制台 > 设置 > 应用 Keys > 服务器地址**查看。 - -## 概览 - - - - - - - - - - - - - - - - - - - - - -
    URLHTTP功能
    /1.1/functions/<functionName>POST调用云函数
    /1.1/call/<functionName>POST调用云函数,支持 LCObject 作为参数和结果
    - -## 预备环境和生产环境 - -在客户端通过 REST API 调用云函数时,可以设置 HTTP 头 `X-LC-Prod` 来区分调用的环境。 - -* `X-LC-Prod: 0` 表示调用预备环境 -* `X-LC-Prod: 1` 表示调用生产环境 - -通过 SDK 调用云函数时,SDK 会根据当前环境设置 `X-LC-Prod` HTTP 头,详见[云函数指南](/v2/sdk/engine/guide/cloudfunction)中《切换云引擎环境》一节的说明。 - -## 云函数 - -通过 `POST /functions/:name` 可以调用云函数,参数和结果都是 JSON 格式。 -例如,我们传入电影的名字来获取电影的目前的评分: - -```sh -curl -X POST -H "Content-Type: application/json; charset=utf-8" \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -d '{"movie":"夏洛特烦恼"}' \ -https://{{host}}/1.1/functions/averageStars -``` - -响应: - -```json -{ - "result": { - "movie": "夏洛特烦恼", - "stars": "2.5" - } -} -``` - -如果调用的云函数需要关联用户,那么可以通过 `X-LC-Session` 传入相应的 `sessionToken`: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \ - -H "Content-Type: application/json" \ - -d '{}' \ - https://{{host}}/1.1/functions/hello -``` - -有些时候我们希望使用 LCObject 作为云函数的参数,或者希望以 LCObject 为云函数的返回值,这时我们可以使用 `POST /1.1/call/:name` 这个 RPC 调用的 API,云函数 SDK 会将参数解释为一个 LCObject,同时在返回 LCObject 时提供必要的元信息: - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -H "Content-Type: application/json" \ - -d '{"__type": "Object", "className": "Post", "pubUser": "LeanCloud 官方客服"}' \ - https://{{host}}/1.1/call/addPost -``` - -响应: - -```json -{ - "result": { - "__type": "Object", - "className": "Post", - "pubUser": "官方客服" - } -} -``` - -RPC 调用时,不仅可以返回单个 LCObject,还可以返回包含 LCObject 的数据结构。 -例如,假设有一个云函数返回一个数组,其中包含一个数字和一个 Todo 对象,那么 RPC 调用的结果为: - -```json -{ - "result": [ - 1, - { - "title": "工程师周会", - "createdAt": { - "__type": "Date", - "iso": "2019-04-28T08:34:12.932Z" - }, - "updatedAt": { - "__type": "Date", - "iso": "2019-04-28T08:34:12.932Z" - }, - "objectId": "5cc5658443e78cb53fe7b731", - "__type": "Object", - "className": "Todo" - } - ] -} -``` - -在通过 SDK 进行 RPC 调用时,SDK 会据此自动反序列化。 - -如果云函数超时,客户端会收到 HTTP status code 为 503、524、141 等的响应。 - -你还可以阅读[云函数指南](/v2/sdk/engine/guide/cloudfunction)来获取更多的信息。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/07-redis.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/07-redis.mdx deleted file mode 100644 index aa9f4599c..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/07-redis.mdx +++ /dev/null @@ -1,366 +0,0 @@ ---- -id: redis -title: LeanCache 指南 -sidebar_label: LeanCache ---- - - - -LeanCache 使用 [Redis](http://redis.io/) (3.0.x)来提供高性能、高可用的 Key-Value 内存存储,主要用作缓存数据的存储,也可以用作持久化数据的存储。它非常适合用于以下场景: - -* 某些数据量少,但是读写比例很高,比如某些应用的菜单可以通过后台调整,所有用户会频繁读取该信息。 -* 需要同步锁或者队列处理,比如秒杀、抢红包等场景。 -* 多个云引擎节点的协同和通信。 - -下图为 LeanCache 和云引擎配合使用的架构: - -![LeanCache 架构](/img/leancache_arch.png) - -恰当使用 LeanCache 不仅可以极大地提高应用的服务性能,还能**降低成本**,因为某些高频率的查询不需要走存储服务(存储服务按调用次数收费)。你可以在 [leanengine-nodejs-demos](https://github.com/leancloud/leanengine-nodejs-demos) 中找到一些有关 LeanCache 的示例: - -- [associated-data](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/associated-data.js) 缓存关联数据 -- [leaderboard](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/leaderboard.js) 实现排行榜 -- [limited-stock-rush](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/limited-stock-rush.js) 实现秒杀抢购 -- [redlock](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/redlock.js) 实现分布式锁 - - -## 主要特性 - -* **高性能**:接近 7 万的 QPS -* **高可用**:基于 [AOF 持久化](http://www.redis.cn/topics/persistence.html) 的 Master-Slave 主从热备份。 -* **在线扩容**:在线调整容量,数据平滑迁移。 -* **多实例**:满足更大容量或更高性能的需求。 - -## 创建实例 - -在**云服务控制台 > 云引擎 > LeanCache (Redis)**页面点击**创建实例**即可创建新实例。 - -**LeanCache 实例一旦生成,就开始计费,因此请认真对待该操作。** - -创建实例时可设置的参数有: - -* **实例名称**:最大长度不超过 32 个字符,限英文、数字、下划线,只能以字母开头。每个开发者账户下 LeanCache 实例名称必须唯一。 -* **实例容量**:可选 128 MB、256 MB、512 MB、1 GB、2 GB、4 GB、8 GB。 -* **删除策略**:内存满时对 key 的删除策略,默认为 `volatile-lru`,更多选择请参考 [数据删除策略](#数据删除策略)。 - -创建好的实例会在控制台显示基本信息,包括实例名称、创建时间、运行状态、当前连接数、已用容量、访问地址、数据删除策略、Redis 版本。 -点击**调整容量**按钮可以扩容或缩容。 -点击**管理共享**按钮可以查看可以访问该 LeanCache 实例的应用, -### 数据删除策略 - -目前我们支持如下几种策略: - -| 策略 | 说明 | -| ---------------------------------------- | ------------------------------------ | -| `noeviction` | 不删除,当内存满时,直接返回错误。 | -| `allkeys-lru` | 优先删除最近最少使用的 key,以释放内存。 | -| `volatile-lru` | 优先删除设定了过期时间的 key 中最近最少使用的 key,以释放内存。 | -| `allkeys-random` | 随机删除一个 key,以释放内存。 | -| `volatile-random` | 从设定了过期时间的 key 中随机删除一个,以释放内存。 | -| `volatile-ttl` | 从设定了过期时间的 key 中删除最老的 key,以释放内存。 | - -请注意,如果所有的 key 都不设置过期时间,那么 `volatile-lru`、`volatile-random`、`volatile-ttl` 这三种策略会等同于 `noeviction`(不删除)。更详细的内容请参考 [Using Redis as an LRU cache](http://redis.io/topics/lru-cache)。 - -**LeanCache 实例一旦生成后,该属性不可修改。** - -## 删除实例 - -在**云服务控制台 > 云引擎 > LeanCache (Redis)**页面的每个实例卡片中,点击**删除**按钮即可删除实例。 - -删除**其他应用共享的实例**需要至该实例所属应用删除。 - -## 使用 - -LeanCache 目前支持通过云引擎访问。实例创建完毕后,云引擎应用就可以从环境变量中获取 `REDIS_URL_<实例名称>` 的 Redis 连接字符串,通过该信息连接并使用 Redis。 - -LeanCache 不提供外网直接访问。如果需要进行简单的数据操作或者查看状态,可以查看控制台: - -![image](/img/leancache_status.png) - -或者使用命令行工具。 - -### 在命令行工具中使用 - -LeanCache 用户可以使用命令行工具来连接线上的 LeanCache 实例,对数据进行增删改查。 - -在一个已经关联过 LeanCache 实例的云引擎项目中,使用 `lean cache` 命令,即可连上对应的 LeanCache 实例。另外需要注意的是,每个 LeanCache 实例,默认会分成 16 个 db,方便管理。没有特殊设置的话,默认使用的都是 db0。 - -连接成功之后,可以直接执行命令来对数据进行操作,比如查看某个 key 的值: - -``` -LeanCache (db 0) > GET foo -"bar" -``` - -LeanCache 基于 Redis,所以大部分 Redis 命令都可以使用。关于 Redis 的命令,请参考[官方文档](https://redis.io/commands) 。 - -可以通过下列命令查询当前应用有哪些 LeanCache 实例: - -```sh -$ lean cache list -``` - -**注意**:命令行工具操作 LeanCache 时,是通过 HTTPS 请求来进行通讯的,因此类似 `pub/sub`、`blpop` 等需要长连接的命令不能直接使用。但是线上没有这个限制,可以直接使用。 - -### 在云引擎中使用(Node.js 环境) - -首先添加相关依赖到云引擎应用中: - -```json -"dependencies": { - "ioredis": "^4.9.0" -} -``` - -然后可以使用下列代码获取 Redis 连接:(假定实例名称为 `MYCACHE`) - -```js -const Redis = require('ioredis') - -const client = new Redis(process.env['REDIS_URL_MYCACHE']); -client.on('error', function(err) { - return console.error('redis err: ', err); -}); -``` - -### 在云引擎中使用(Python 环境) - -首先添加相关依赖到云引擎应用的 `requirements.txt` 中: - -```python -Flask>=0.10.1 -leancloud-sdk>=1.0.9 -... -redis -``` - -然后可以使用下列代码获取 Redis 连接:(假定实例名称为 `MYCACHE`) - -```python -import os -import redis - -r = redis.from_url(os.environ.get("REDIS_URL_MYCACHE")) -``` - -### 在云引擎中使用(PHP 环境) - -首先添加 redis 库的依赖,比如 predis: - -```sh -composer require 'predis/predis:1.1.*' -``` - -然后在 PHP 应用中通过环境变量获取 Redis 地址并创建链接,如:(假定实例名称为 `MYCACHE`) - -```php -use Predis; -$redis = new Predis\Client(getenv("REDIS_URL_MYCACHE")); -$redis->ping(); -``` - -### 在云引擎中使用(Java 环境) - -在 `pom.xml` 中添加 redis client 的依赖: - -```xml - - redis.clients - jedis - 3.2.0 - -``` - -并引入依赖: - -```java -import redis.clients.jedis.Jedis; -``` - -从环境变量中获取链接字符串,然后再创建 redis client 实例即可。(假定实例名称为 `MYCACHE`) - -```java -String redisUrl = System.getenv("REDIS_URL_MYCACHE"); -Jedis jedis = new Jedis(redisUrl); -jedis.set("foo", "bar"); -String value = jedis.get("foo"); -jedis.close(); -``` - -并发请求较高的情况下可以考虑使用连接池: - -```java -public class RedisHelper { - private final JedisPool jedisPool; - public RedisHelper() { - // 创建应用时,先创建连接池;使用默认配置 - jedisPool = new JedisPool(System.getenv("REDIS_URL_jedis_128m")); - } - - // 使用;从连接池取出一个 jedis 连接使用 - // 注意,使用完了之后,要调用返回的 jedis 对象的 close 方法返还连接。`jedis.close()` - public Jedis getJedis() { - Jedis jedis = jedisPool.getResource(); - return jedis; - } - - public void closePool() { - // 关闭应用时关闭连接池 - jedisPool.close(); - } -} -``` - -### 在云引擎中使用(.NET Core 环境) - -首先在项目里面安装 nuget 依赖: - -```sh -dotnet add LeanCloud.Engine.Middleware.AspNetCore -``` - -直接使用 [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/) 里面构建的方式。 - -假设在控制台创建了一个名字叫做 `dev` LeanCache 实例,如下代码将演示如何连接这个实例,并且存储、读取数据: - -```cs -// 获取 dev 实例 -var leancache = new LeanCache("dev"); -// 获取 dev 的配置 -var redisConfiguration = leancache.CurrentConfigurations; -// 构建 StackExchange.Redis.ConfigurationOptions -ConfigurationOptions config = new ConfigurationOptions -{ - ServiceName = this.InstanceName, - ClientName = this.InstanceName, - EndPoints = - { - { - redisConfiguration.Host, redisConfiguration.Port - }, - }, - KeepAlive = 180, - DefaultVersion = new Version(2, 8, 8), - Password = redisConfiguration.Password, - AbortOnConnectFail = false, - ConnectRetry = 3, -}; -// 直接连接 -var conn = ConnectionMultiplexer.Connect(config); -IDatabase db = conn.GetDatabase(); -db.StringSet("foo", "bar"); -var bar = db.StringGet("foo"); -``` - -关于 `IConnectionMultiplexer` 的用法和相关文档请参阅:[StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/),这个库是 .NET Core 环境中比较推荐的 Redis Client。 - -### 在本地调试依赖 LeanCache 的应用 - -目前不支持直接连接线上的 LeanCache 进行调试,所以需要先在本地安装好 Redis。 - -* Mac 上运行 `brew install redis` 来安装 Redis,然后使用 `redis-server` 启动服务。 -* Debian/Ubuntu 上运行 `apt-get install redis-server` -* CentOS/RHEL 运行 `yum install redis` -* Windows 尚无官方支持,可以下载 [微软的分支版本](https://github.com/MSOpenTech/redis/releases) 安装包。 - -默认情况下,在本地运行时程序没有 LeanCache 的环境变量,因此会使用本地的 Redis 服务器地址。 - -```js -// 在本地 process.env['REDIS_URL_<实例名称>'] 为 undefined,会连接默认的 127.0.0.1:6379 -const client = new Redis(process.env['REDIS_URL_MYCACHE']); // 假定实例名称为 MYCACHE -``` - -如果部署到预备或生产环境时遇到类似 `redis err: Error: Redis connection to 127.0.0.1:6379 failed - connect ECONNREFUSED 127.0.0.1:6379` 错误,请核实以上代码中 `REDIS_URL_<实例名称>` 这个环境变量的值是否替换正确,也可参考 [在云引擎中使用(Node.js 环境)](#在云引擎中使用_Node_js_环境_) 的示例。 - -更详细的 Redis 操作说明请参考 [Redis 官方文档](http://redis.io/documentation)。 - - -### 多应用间共享使用 - -LeanCache 实例在开发者账户内全局可见,并不与某个应用固定绑定。所以在某个应用内创建的 LeanCache 实例,其他应用也一样可以使用,其调用方法和上述例子一样。 - -对于某些使用场景,譬如 O2O 行业的用户端和管理端,或者网络租约车平台的乘客端和司机端,需要多个应用共享同一个 LeanCache 数据,这一点将会非常有用。 - -## 性能 - -下面是使用 redis-benchmark 测试一个典型的容量为 2 GB 的 LeanCache 实例的性能表现: - -```sh -$ redis-benchmark -n 100000 -q -PING_INLINE: 69783.67 requests per second -PING_BULK: 68306.01 requests per second -SET: 68634.18 requests per second -GET: 67659.00 requests per second -INCR: 67294.75 requests per second -LPUSH: 61236.99 requests per second -LPOP: 62460.96 requests per second -SADD: 63451.78 requests per second -SPOP: 64724.92 requests per second -LPUSH (needed to benchmark LRANGE): 64808.82 requests per second -LRANGE_100 (first 100 elements): 62189.05 requests per second -LRANGE_300 (first 300 elements): 64267.35 requests per second -LRANGE_500 (first 450 elements): 66934.41 requests per second -LRANGE_600 (first 600 elements): 61462.82 requests per second -MSET (10 keys): 60096.15 requests per second -``` - -## 可靠性 - -每个 LeanCache 实例使用 Redis Master-Slave 主从热备,其下的多个观察节点每隔 1 秒钟观察一次主节点的状态。如果「主节点」最后一次有效响应在 5 秒之前,则该观察节点认为主节点失效。如果超过总数一半的观察节点发现主节点失效,则自动将「从节点」切换为主节点,并会有新的从节点启动重新组成主从热备。这个过程对应用完全透明,不需要修改连接字符串或者重启,整个切换过程应用只有几秒钟会出现访问中断。 - -与此同时,从节点还会以 [AOF 方式](http://www.redis.cn/topics/persistence.html) 将数据持久化存储到可靠的中央文件中,每秒刷新一次。如果很不巧主从节点同时失效,则马上会有新的 Redis 节点启动,并从 AOF 文件恢复,完成后即可再次提供服务,并且会有新的从节点与之构成主从热备。 - -### 极端情况下的数据丢失 - -当一个实例中的主节点失效,而最新的数据没有同步到对应的从节点时,主从切换会造成这部分数据丢失。 - -当主、从节点同时失效,未同步到从节点和从节点未刷新到磁盘 AOF 文件中的数据将会丢失。 - -## 在线扩容 - -你可以在线扩大(或者缩小) LeanCache 实例的最大内存容量。整个过程可能会持续一段时间,在此期间 LeanCache 会中断几秒钟进行切换,其他时间都正常提供服务。如果你的应用访问量较大的话,LeanCache 中断的这几秒可能会对你的云引擎实例产生较为明显的影响(例如内存增加),可以考虑将扩容安排在低峰时刻。 - -**缩小容量之前,请务必确认现有数据体积小于目标容量,否则可能造成数据丢失。** - -## 多实例 - -有些时候,你可能希望在一个应用里创建多个 LeanCache 实例: - -* **需要存储的数据大于 8 GB**:目前我们提供的实例最大容量为 8 GB。如果有大于此容量的数据,建议你创建多个实例,然后根据功能来划分,比如一个用来做持久化,另一个用来做缓存。 -* **需要更高的性能**:如果单实例的性能已经成为应用的瓶颈,你可以创建多个实例,然后在云引擎中同时连接,并自己决定 key 的分片策略,使请求分散到不同的实例来获得更高的性能。 - -添加实例的方式请参考 [创建实例](#创建实例)。 - -## 价格 - -因为用户可能需要随时调整 LeanCache 实例的容量,所以为了方便计算,我们按照每个实例当天所使用的「最大容量」来结算,而不是「实际使用容量」。 - -不同容量的 LeanCache 实例的价格,请参考官网报价。不同的节点使用不同的结算货币,价格会有差异,敬请留意。 - -### 费用计算 - -LeanCache 采取按天扣费,使用时间不足一天按一天收费,次日凌晨系统从账户余额中扣费。付费范围包括当前账户下隶属于每个应用的所有 LeanCache 实例,取每个实例当天使用的最大容量的价格,累计相加计算出总的使用费用。 - -如果在系统扣费之时,账户没有充足余额,那么在扣费当天的上午 10 点,账户内所有应用使用的**全部实例会停止服务**,但数据仍会保留,期限为 1 个月。 - -已停止服务的实例状态显示为「未运行」。要恢复服务,需要向账户充值。在账户余额补足后的 5 分钟内,已停止服务的所有实例将会自动恢复运行。 - -### 删除无用实例 - -为了避免发生不必要的使用费,请及时删除不再使用的实例,步骤请参考 [删除实例](#删除实例)。 - -## 常见问题 - -### 与自建的 HashTable 相比较,LeanCache 有什么优势? - -与自己在程序的全局作用域中维护一个 HashTable 相比,使用 LeanCache 的优势在于: - -- **多实例之间的数据共享**:云引擎支持多实例运行,自行维护的 HashTable 数据无法[跨实例共享](#多应用间共享使用)。 -- **数据持久化存储**:在程序重启或重新部署后数据不会丢失,Redis 会帮你完成数据持久化的工作。LeanCache 还会为你的 Redis 做热备,具有非常高的[可靠性](#可靠性)。 -- **原子操作和性能**:Redis 提供了常见的数据结构和大量原子操作,其文档中列出了每个操作符的时间复杂度,而自行实现的 HashTable 的性能则很大程度依赖于具体语言的实现。 - -### 报错:Redis connection gone from end event - -LeanCache 或者任何网络程序都有可能出现连接闪断的问题,可能是因为网络波动,或是服务器负载、容量调整等等。这时只需要重建连接即可使用。而 Redis Client 一般都有断开重连的机制,未连接期间指令会保存到队列,待连接成功后再发送队列中的指令([Redis client library](https://www.npmjs.com/package/redis) 便是如此实现)。所以如果这个错误偶尔发生,一般不会有什么问题;同时建议在应用中 [增加 Redis 的 on error 事件处理](#在云引擎中使用_Node_js_环境_)。 - -如果这个错误**频繁出现**,那么很可能 LeanCache 节点处于非受控状态,请联系技术支持进行处理。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/08-mysql.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/08-mysql.mdx deleted file mode 100644 index 4d9f4e173..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/08-mysql.mdx +++ /dev/null @@ -1,172 +0,0 @@ ---- -id: mysql -title: LeanDB MySQL 指南 -sidebar_label: LeanDB MySQL ---- - - - - -LeanDB 是 LeanCloud 推出的数据库托管方案,开发者可以在「控制台 => 云引擎 => LeanDB」中创建托管在 LeanCloud 的数据库实例。 - -开发者可以在云引擎中连接到自己的 LeanDB 实例,使用通用的 MySQL 客户端类库,访问完整的 MySQL 功能。 - -## 实例规格 - -- LeanDB MySQL 提供 MySQL 5.6 版本。 -- LeanDB MySQL 的规格分为 `0.5G`、`1G`、`2G`、`4G` 几种,代表不同的运算能力。 -- 每个实例默认有 20G 的存储空间,如果不够的话还可以选配 100G 或者 500G 的存储空间。 -- 具体的价格可以在控制台上点击「创建 LeanDB 实例」来查看。 - -## 在线管理 - -为方便开发和调试,我们为开发者提供了一个 Web 界面来对 MySQL 进行管理,你可以在控制台上点击「管理员面板」链接来访问这个 Web 界面。 - -开发者可以在这个页面上进行 SQL 查询和更新,创建和管理数据库,创建和管理索引等操作。 - -## 在云引擎中使用 - -LeanDB 所在的应用的云引擎在部署时,会被注入几个包含 MySQL 连接信息的环境变量,包括: - -- `MYSQL_HOST_` -- `MYSQL_PORT_` -- `MYSQL_ADMIN_USER_` -- `MYSQL_ADMIN_PASSWORD_` - -其中 `` 是你在创建 LeanDB 时为它指定的名字,如果你的 LeanDB 名为 `MYRDB` 的话,就会有名为 `MYSQL_HOST_MYRDB` 的环境变量(以及其他三个)。 - -### Node.js - -在 Node.js 中你可以这样连接到 MySQL: - -```javascript -const mysql = require('mysql') -const Promise = require('bluebird') - -const mysqlPool = Promise.promisifyAll(mysql.createPool({ - host: process.env['MYSQL_HOST_MYRDB'], - port: process.env['MYSQL_PORT_MYRDB'], - user: process.env['MYSQL_ADMIN_USER_MYRDB'], - password: process.env['MYSQL_ADMIN_PASSWORD_MYRDB'], - database: 'test', - connectionLimit: 10 -})) - -mysqlPool.queryAsync('SELECT 1 + 1 AS solution').then( rows => { - console.log('The solution is', rows[0].solution) -}).catch( err => { - console.error(err) -}) -``` - -- 你需要运行 `npm install --save mysql bluebird` 来安装上面代码中用到的依赖 -- 更多的用法请参考 [mysqljs/mysql 的文档](https://github.com/mysqljs/mysql) - -### PHP - -在 PHP 中你可以这样连接到 MySQL: - -```php -try { - $mysqlHost = getenv('MYSQL_HOST_MYRDB'); - $mysqlPort = getenv('MYSQL_PORT_MYRDB'); - $pdo = new PDO("mysql:host=$mysqlHost:$mysqlPort;dbname=test", getenv('MYSQL_ADMIN_USER_MYRDB'), getenv('MYSQL_ADMIN_PASSWORD_MYRDB')); - - foreach($pdo->query('SELECT 1 + 1 AS solution') as $row) { - print "The solution is {$row['solution']}"; - } -} catch (PDOException $e) { - print $e->getMessage(); -} -``` - -- 更多的用法请参考 [PDO 的文档](https://www.php.net/manual/zh/class.pdo.php) - -### Java - -在 Java 中你可以这样连接到 MySQL: - -```java -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.Statement; -import java.sql.ResultSet; -import java.sql.SQLException; - -String host = System.getenv("MYSQL_HOST_MYRDB"); -String port = System.getenv("MYSQL_PORT_MYRDB"); -String user = System.getenv("MYSQL_ADMIN_USER_MYRDB"); -String password = System.getenv("MYSQL_ADMIN_PASSWORD_MYRDB"); -try { - Class.forName("com.mysql.jdbc.Driver").newInstance(); -} catch (Exception ex) { - // 处理异常 -} -try { - Connection connection = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port + "/test?" + - "user=" + user + "&password=" + password); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT 1 + 1 AS solution"); - resultSet.first(); - System.out.format("The solution is %d", resultSet.getInt("solution")); -} catch (SQLException ex) { - // 处理异常 -} -``` - -- 需要在 `pom.xml` 中加入 mysql connector 依赖: - - ```xml - - mysql - mysql-connector-java - 8.0.16 - - ``` - -- 更多的用法请参考 [MySQL Connector/J 文档](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-usagenotes-connect-drivermanager.html) - -### Python - -在 Python 中你可以这样连接到 MySQL: - -```python -import os -import mysql.connector - -result = '' - -host = os.environ['MYSQL_HOST_MYRDB'] -port = os.environ['MYSQL_PORT_MYRDB'] -user = os.environ['MYSQL_ADMIN_USER_MYRDB'] -password = os.environ['MYSQL_ADMIN_PASSWORD_MYRDB'] -try: - cnx = mysql.connector.connect( - user=user, password=password, database='test', host=host, port=port) -except mysql.connector.Error as err: - if err.errno == errorcode.ER_ACCESS_DENIED_ERROR: - print("username or password error") - elif err.errno == errorcode.ER_BAD_DB_ERROR: - print("Database does not exist") - else: - print(err) - else: - cursor = cnx.cursor() - cursor.execute('SELECT 1 + 1 AS solution') - for row in cursor: - result = "The solution is {}".format(row[0]) - - cursor.close() - cnx.close() -``` - -- 上面用的是 MySQL 官方的 python driver,你需要在 `requirements.txt` 中列出这一依赖,例如:`mysql-connector-python>=8.0.16,<9.0.0` -- 更多的用法请参考 [MySQL Connector/Python 文档](https://dev.mysql.com/doc/connector-python/en/) - - -## 常见问题 - -- 目前 LeanDB 只支持从云引擎(和控制台的 Web 界面)中访问,在本地调试时无法访问。 -- 目前 LeanDB 不提供自助扩容的能力,如需扩容请联系我们的技术支持。 -- 如账户欠费超过 3 天,LeanDB 及其中的数据会被彻底删除。 -- LeanDB 每天扣费,不足一天按照一天扣费。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/09-mongo.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/09-mongo.mdx deleted file mode 100644 index d3488127a..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/09-mongo.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: mongo -title: LeanDB MongoDB 指南 -sidebar_label: LeanDB MongoDB ---- - - - -LeanDB 是 LeanCloud 推出的数据库托管方案,开发者可以在「控制台 => 云引擎 => LeanDB」中创建托管在 LeanCloud 的数据库实例,这篇文章主要介绍其中的 MongoDB 数据库。 - -开发者可以在云引擎中连接到自己的 LeanDB 实例,使用通用的 MongoDB 客户端类库,访问完整的 MongoDB 功能。 - -## 实例规格 - -- LeanDB MongoDB 提供 MongoDB 4.2 版本。 -- LeanDB MongoDB 的规格分为 `512`、`1024`、`2048`、`4096`、`8192` 几种,代表不同的运算能力。 -- 每种规格有固定的连接数和存储空间限制,如需要更多连接数或存储空间需要升级到更高的规格。 -- 具体的价格可以在控制台上点击「创建 LeanDB 实例」来查看。 - -## 在云引擎中使用 - -LeanDB 所在的应用的云引擎在部署时,会被注入包含 MongoDB 连接字符串的环境变量 `MONGODB_URL_`,其中 `` 是你在创建 LeanDB 时为它指定的名字,如果你的 LeanDB 名为 `MYDB` 的话,就会有名为 `MONGODB_URL_MYDB` 的环境变量。 - -### Node.js - -在 Node.js 中你可以这样连接到 MongoDB(假定 LeanDB 名称为 `MYDB`): - -```javascript -const {MongoClient} = require('mongodb') - -const mongoClient = new MongoClient(process.env['MONGODB_URL_MYDB'], { - useUnifiedTopology: true, - poolSize: 10 -}) - -mongoClient.connect().then( () => { - console.log('Connected to MongoDB') -}).catch( err => { - console.eror('Connect to MongoDB failed', err.message) -}) - -app.get('/', (req, res) => { - const cats = mongoClient.collection('cats') - - res.json(cats.find({}, {limit: 10})) -}) -``` - -- 你需要运行 `npm install mongodb` 来安装上面代码中用到的依赖 -- 更多的用法请参考 [MongoDB Node Driver 官方文档](https://docs.mongodb.com/drivers/node/) - -## 常见问题 - -- 目前 LeanDB 只支持从云引擎中访问,在本地调试时无法访问。 -- 目前 LeanDB 不提供自助扩容的能力,如需扩容请联系我们的技术支持。 -- 如账户欠费超过 3 天,LeanDB 及其中的数据会被彻底删除。 -- LeanDB 每天扣费,不足一天按照一天扣费。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/10-es.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/10-es.mdx deleted file mode 100644 index 40a845fa6..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/10-es.mdx +++ /dev/null @@ -1,87 +0,0 @@ ---- -id: es -title: LeanDB Elasticsearch 指南 -sidebar_label: LeanDB Elasticsearch ---- - - - -LeanDB 是 LeanCloud 推出的数据库托管方案,开发者可以在「控制台 => 云引擎 => LeanDB」中创建托管在 LeanCloud 的数据库实例。这篇文章主要介绍其中的 Elasticsearch 数据库。 - -开发者可以在云引擎中连接到自己的 LeanDB 实例,通过 HTTP 或者 使用 Elasticsearch 官方客户端类库,访问完整的 Elasticsearch 功能。 - -## 主要特性 -* **高可用**:多节点集群方案,可以容忍单节点故障。 -* **在线扩容**:在线调整容量和规格,数据平滑迁移。 -* **多实例**:满足更大容量或更高性能的需求。 -* **中文分词**:内置中文分词插件并支持自定义词库。 - -## 实例规格 -* LeanDB Elasticsearch 提供 Elasticsearch 7.9 版本。 -* LeanDB Elasticsearch 的规格分为 `512`、`1024`、`2048`、`4096`、`8192` 几种,代表不同的运算能力。 -* 每种规格有固定的存储空间限制,如需要更多存储空间需要升级到更高的规格。 -* 具体的价格可以在控制台上点击「创建 LeanDB 实例」来查看。 - -## 在云引擎中使用 -LeanDB 所在的应用的云引擎部署时,会被注入包含 Elasticsearch 连接信息的环境变量 `ELASTICSEARCH_URL_`, -其中 `NAME` 是你在创建 LeanDB 时为它指定的名字,如果你的 LeanDB 名为 `MYES` 的话,就会有名为 `ELASTICSEARCH_URL_MYES` 的环境变量。 -该环境变量的格式是 `http://username:password@host:port`,其中包含了所有连接 Elasticsearch 所需的信息,包括认证信息。 -### Node.js -在 Node.js 中你可以这样连接到 Elasticsearch: -```javascript -const { Client } = require('@elastic/elasticsearch') -const client = new Client({ - node: process.env.ELASTICSEARCH_URL_MYES -}) - -// promise API -const result = await client.search({ - index: 'my-index', - body: { - query: { - match: { hello: 'world' } - } - } -}) - -// callback API -client.search({ - index: 'my-index', - body: { - query: { - match: { hello: 'world' } - } - } -}, (err, result) => { - if (err) console.log(err) -}) -``` - -* 你需要运行 `npm install @elastic/elasticsearch` 来安装上面代码中用到的依赖 -* 更多的用法请参考 [Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html) - -## 中文分词 -除了 elasticsearch 自带的分词器,我们还提供了 -[Elasticsearch ik plugin](https://github.com/medcl/elasticsearch-analysis-ik) 以支持中文分词。 -我们可以通过以下途径指定使用 IK 插件进行中文分词: -1. 在搜索时,指定分词器 -2. 在创建索引时,为特定 `field` 指定搜索分词器 -3. 在创建索引时,指定索引的默认分词器 -4. 在创建索引时,为特定 `field` 指定分词器 - -它们的优先级依次降低,当都未指定时,会使用默认的标准分词器(standard analyzer)。具体细节及参数见 [specify an analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/specify-analyzer.html)。 - -### 自定义词库 -除此之外,自定义词库也是支持的。用户可以在控制台上传自定义词库。 -词库文件要求为 UTF-8 编码,每个词单独一行,文件大小不能超过 10MB,例如: -> 面向对象编程 -> 函数式编程 -> 高阶函数 -> 响应式设计 - -将其保存为文本文件,例如 `dict.txt`,上传即可。上传之后,分词将于 2 分钟后生效。开发者可以通过 [analyze API](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/test-analyzer.html) 来测试。需要注意使用 analyze API 时要指定 index,使用 `curl -X POST "localhost:9200/my-index/_analyze?pretty"` 的形式。 - -## 常见问题 -* 目前 LeanDB Elasticsearch 只支持从云引擎中访问,在本地调试时无法访问。 -* 如账户欠费超过 3 天,LeanDB 及其中的数据会被彻底删除。 -* LeanDB 每天扣费,不足一天按照一天扣费。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/11-cloudqueue.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/11-cloudqueue.mdx deleted file mode 100644 index f47ab1085..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/11-cloudqueue.mdx +++ /dev/null @@ -1,210 +0,0 @@ ---- -id: cloudqueue -title: 云队列指南 -sidebar_label: 云队列 ---- - - - -云队列提供了一种在云引擎之外调度云函数的能力,它基于云引擎已有的「云函数」这个概念实现了重试、去重、结果查询、延时任务、定时任务等功能,是对云函数功能的一个补充。尚未运行的任务会以一种可靠的方式暂存在云队列,即使你的云引擎因部署、过载、崩溃而重启,任务也不会丢失,云队列会等待你的云引擎实例恢复正常后继续运行它们。 - -目前云队列还是一个实验性功能,可以免费使用,在正式上线后将会是一项单独计费的功能,因为云队列被设计用于应对突发流量,所以收费的指标将会与「每小时入队任务数量(对应队列的处理能力)」和「队列内剩余任务峰值(对应队列空间的占用)」相关,按实际用量计费,不需要预估容量。 - -云队列实际上运行在云引擎之外,它通过 HTTP 接受入队等操作,同时也通过 HTTP 来调用云引擎容器中的云函数来完成实际的任务执行,我们的 SDK 会封装这些细节。云队列的入队接口(`Cloud.enqueue`)目前仅限在云引擎内使用 masterKey 权限调用(后续可能会开放客户端调用);结果查询接口(`Cloud.getTaskInfo`)则允许客户端直接使用 uniqueId 调用。 - -## 功能和使用场景 - -云队列提供的功能包括: - -- **重试** 任务在执行失败后会默认进行一次重试,可以通过选项来配置重试次数(`attempts`)和重试间隔(`backoff`),重试时 uniqueId 不会改变。 -- **去重** 可以为任务提供唯一 ID(`uniqueId`,如不提供则会随机生成),在任务存在于队列期间(包括已完成的),云队列不会接受有 uniqued 的任务。 -- **结果查询** 任务在完成后会继续被保留在队列中一段时间(可通过 `keepResult` 配置),客户端可以使用 uniqueId 来进行高性能的结果查询。 -- **延时任务** 可以通过 `delay` 来延迟执行一个任务。 -- **定时任务** 现在定时任务是云队列的一个子功能,你可以在控制台上创建和管理任意数量的定时任务,可以使用 [CORN 表达式](#CORN_表达式) 或设置间隔时间。 -- **并发控制** 云队列会将入队的任务暂存起来,以 1 个并发的速度逐步地执行,避免云引擎实例过载(后续我们会引入更智能的并发调节算法)。 -- **优先级** 可以为特定任务设置优先级(`priority`),在队列拥堵时,高优先级的任务会优先执行。 - -你可以在 [leanengine-nodejs-demos](https://github.com/leancloud/leanengine-nodejs-demos) 中找到一些有关云队列的示例: - -- [queue-delay-retry](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/queue-delay-retry.js) 延时和重试云函数 -- [queue-result-query](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/queue-result-query.js) 结果查询 -- [crawler](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/crawler.js) 抓取一个站点下所有网页的爬虫(去重和控制并发) - -## 简单使用 - -```javascript -const {Cloud} = require('leanengine') - -// 被执行的云函数,这里只是一个例子,实际业务中注意鉴权 -Cloud.define('closeOrder', async function({params}) { - try { - const order = await new Query('Order').get(params.id) - // 返回值可用于后续的结果查询,也会被记录到日志中 - return await order.save({status: 'closed'}) - } catch (err) { - // 抛出异常使云函数执行失败(JavaScript 自身或依赖库的异常也会使云函数执行失败) - throw new Cloud.Error(`Some error happened: ${err.message}`) - } -}) - -// 添加任务,enqueue 本身在添加队列成功就返回一个 uniqueId(不等待任务实际被执行) -// 你可以在所有云引擎代码(包括云函数也包括网站托管中的自定义路由)使用 Cloud.enqueue -const {uniqueId} = await Cloud.enqueue('closeOrder', {id: 1234}) - -// 查询任务结果,可将 uniqueId 发给客户端,由客户端进行查询 -// 只有存在于队列中的任务才能查询,可以用 keepResult 来调整已完成任务的保留时间 -console.log(await Cloud.getTaskInfo(uniqueId)) - -// 调整默认选项,增加重试次数,减少重试间隔 -Cloud.enqueue('closeOrder', {id: 1234}, {attempts: 10, backoff: 10000}) - -// 添加延时任务,closeOrder 会在一分钟之后执行 -Cloud.enqueue('closeOrder', {id: 1234}, {delay: 600000}) - -// 指定 uniqueId,如果已有相同 uniqueId 的任务会抛出异常 -// 只有存在于队列中的任务会参与去重,可以用 keepResult 来调整已完成任务的保留时间 -Cloud.enqueue('closeOrder', {id: 1234}, {uniqueId: '1234'}) -``` - -如果你希望某个云函数仅限被云队列调用(而不允许客户端直接调用)的话,可以为 `Cloud.define` 加上 [`internal` 选项](https://github.com/leancloud/leanengine-node-sdk/blob/master/API.md#avclouddefine),通过这种方式定义的云函数只能被 Cloud Queue 或其他具有 masterKey 权限的代码调用。 - -## API - -目前我们只在 Node SDK(3.4 以上版本)添加了云队列支持: - -``` -Cloud.enqueue(functionName, params, options?): Promise<{uniqueId: string}> -Cloud.getTaskInfo(uniqueId): Promise -``` - -用法: - -```javascript -const {Cloud} = require('leanengine') - -// 添加任务,enqueue 本身在添加队列成功就返回一个 uniqueId(不等待任务实际被执行) -const {uniqueId} = await Cloud.enqueue('sendMail', {userId: 1234}) - -// 延时任务 -Cloud.enqueue('closeOrder', {id: 1234}, {delay: 600000}) - -// 查询任务结果 -console.log(await Cloud.getTaskInfo(uniqueId)) -``` - -`options` 的属性包括: - -- `attempts?: number`:最大重试次数,默认 `1` -- `backoff?: number`:重试间隔(毫秒),默认 `60000`(一分钟) -- `delay?: number`:延时执行(毫秒) -- `deliveryMode?: string`:超时时的行为,值是 `atLeastOnce`(至少一次,可能会重试多次)、`atMostOnce`(至多一次,不会重试),默认是 `atLeastOnce` -- `keepResult?: number` 在队列中保留结果的时间(毫秒),默认 `300000`(五分钟) -- `priority?: number`:优先级,默认是当前时间戳,设置为更小的值可以在队列拥堵时让特定任务更快地被执行 -- `timeout?: number`:超时时间(毫秒),默认 `15000`,目前最大也是 `15000`,后续会提供更长的时间 -- `uniqueId?: string`:任务的唯一 ID,会据此进行去重,最长 32 个字符,默认是随机的 UUID - -`TaskInfo` 的属性包括: - -- `uniqueId: string`:任务的唯一 ID -- `status: string`:任务的状态,包括 `queued`(等待或正在执行)、`success`(执行成功)、`failed`(执行失败) - -执行完成的 `TaskInfo` 会有: - -- `finishedAt?: string` 执行完成(成功或失败)的时间 -- `statusCode?: number` 云函数响应的 HTTP 状态码 -- `result?: object` 来自云函数的响应 - -执行失败的 `TaskInfo` 会有: - -- `error?: string` 错误提示 -- `retryAt?: string` 下次重试的时间 - -## 性能和可靠性 - -云队列的入队接口(`Cloud.enqueue`)被设计用于应对较高的突发流量(例如 1000 QPS 以上),云队列会将这些任务存储起来,以 1 个并发的速度逐步地执行,减少对于云引擎容器的压力。 - -云队列的调度并非在云引擎容器中进行,这意味着即使云引擎容器故障、重启也不会影响到云队列(任务不会丢失,失败的任务会重试),可以用于支持突发流量。同时任务本身又是以云函数的形式在云引擎容器中运行的(占用云引擎的 CPU、内存资源),和直接调用云函数的执行环境完全相同。 - -## 拥堵和排队 - -如果执行任务的并发达到了上限,那么新的任务(包括延时任务和形式任务的触发)会进入到一个抽象的「等待队列」中,每当有正在执行的任务完成了,就会从等待队列中抽取优先级最高(`priority` 值最低)的任务来执行。 - -priority 的值默认是入队时(对于定时任务则为触发时)的毫秒时间戳(例如 `2019-05-20T17:32:07.166+08:00` 的时间戳是 `1558344727166`),也就是说默认情况下等待队列中的任务会按时间顺序被执行。你可以覆盖 priority 的值,改为一个较小的值来让重要的任务尽快地被执行;或改成一个较大的值让任务更迟执行。 - -## CRON 表达式 - -CRON 表达式的基本语法为: - -``` -秒 分钟 小时 日期 day-of-month 月份 星期 day-of-week -``` - -位置 | 字段 | 约束 | 取值 | 可使用的特殊符号 ----|---|---|---|--- -1| 秒 | 必须 |0-59|`, - * /` -2| 分钟 | 必须 |0-59|`, - * /` -3| 小时 | 必须 |0-23(0 为午夜)|`, - * /` -4| 日期 | 必须 |1-31|`, - * ? /` -5| 月份 | 必须 |1-12、JAN-DEC|`, - * /` -6| 星期 | 必须 |1-7、SUN-SAT|`, - ? /` - -特殊符号的用法: - -符号 | 含义 | 用法 ----|---|--- -`*`| 所有值 | 代表一个字段的所有可能取值。如将 `分钟` 设为 **\***,表示每一分钟。 -`?`| 不指定值 | 用于可以使用该符号的两个字段中的一个,在一个表达式中只能出现一次。如任务执行时间为每月 10 号,星期几无所谓,那么表达式中 `日期` 设为 **10**,`星期` 设为 **?**。 -`-`| 范围 | 如 `小时` 为 **10-12**,即 10 点、11 点、12 点。 -`,`| 分隔多个值 | 如 `星期` 为 **MON,WED,FRI**,即周一、周三、周五。 -`/`| 间隔 | 如 `秒` 设为 **\*/15**,即表示每隔 15 秒执行一次,包括 0、15、30、45 秒。 - -各字段以空格或空白隔开。`JAN-DEC`、`SUN-SAT` 这些值不区分大小写,比如 `MON` 和 `mon` 效果一样。 - -举例如下: - -表达式 | 说明 ----|--- -`0 */5 * * * ?`| 每隔 5 分钟执行一次 -`10 */5 * * * ?`| 每隔 5 分钟执行一次,每次执行都在分钟开始的 10 秒,例如 10:00:10、10:05:10 等等。 -`0 30 10-13 ? * WED,FRI`| 每周三和每周五的 10:30、11:30、12:30、13:30 执行。 -`0 */30 8-9 5,20 * ?`| 每个月的 5 号和 20 号的 8 点和 10 点之间每隔 30 分钟执行一次,也就是 8:00、8:30、9:00 和 9:30。 - -Cron 表达式的时区为东八区(国内版)、UTC 零时区(国际版)。 - -## 测试期间的限制 - -在测试期间我们有一些默认的限制: - -- 入队请求限制为每应用 100 QPS -- 每天入队请求限制为每应用 10000 -- 队列中的最大任务数量限制为每应用 1000 - -如果你达到了这些限制的话可以联系我们的技术支持。 - -## FAQ - -### 接下来还会有什么功能? - -这里列出的是后续可能会实施的一些计划,如果你非常需要其中某个功能,请通过工单或论坛联系我们,让我们知道。 - -- **允许客户端调用** 我们有计划为用户提供一个「允许客户端调用云队列」的选项,开启这个选项后客户端将会可以进行入队操作(`Cloud.enqueue`),但客户端不能指定 `options` 中的任何选项,只能传递参数。 -- **超时时间** 因为之前我们的云函数一直将超时时间限制在 15 秒,所以目前云队列受限于云函数,超时也是 15 秒。我们正在调整相关的基础设施,计划在后续让从云队列调用云函数的超时时间最长可以达到 5 分钟。 -- **并发限制** 目前所有应用的并发限制都是 1,我们有计划实现一种自动控制并发的机制:在任务较多的情况下,并发会逐渐增加,直到负载体现在实例的 CPU 或内存压力上。 -- **使用单独的分组运行任务** 我们有计划为用户提供一个「在独立分组中运行定时任务」的选项,开启这个选项后会自动创建一个特殊分组,所有定时任务都在这个分组中运行。 -- **定时任务的可编程接口** 我们有计划为定时任务提供可编程接口,但优先级较低。 -- **云队列的日志** 目前云队列会将执行日志直接打印到云引擎的应用日志中,后续我们准备把云队列的日志显示在一个单独的 Tab 中。 -- **在其他 SDK 中使用云函数** 后续我们会在 Python SDK、Java SDK、PHP SDK 中添加云队列的支持。 - -### 异常处理策略(deliveryMode)具体影响哪些情况? - -异常处理策略(deliveryMode)用于在以下几种不确定任务是否已经执行或是否应该执行的情况下,来决定是否重试: - -- 云队列已将请求发到云函数,但云函数未在超时时间内给出成功或失败的响应。 -- 因云队列本身的故障导致失去对正在执行的任务的追踪。 -- 因云队列本身的故障导致定时任务没有在指定的时间触发。 - -如果选择「放弃(atMostOnce)」在出现上述情况时任务可能不会执行;如果选择「重试(atLeastOnce)」在发生上述情况时任务可能会执行多次。 - -### 如何在本地调试时使用云队列? - -你可以在本地调试时远程调用云队列相关的 API,但云队列只会在线上的云引擎中运行指定的云函数。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/12-faq.mdx b/versioned_docs/version-v2/sdk/09-engine/02-guide/12-faq.mdx deleted file mode 100644 index 0283e1034..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/12-faq.mdx +++ /dev/null @@ -1,1730 +0,0 @@ ---- -title: 云引擎 FAQ -sidebar_label: FAQ ---- - -import MultiLang from '/src/docComponents/MultiLang'; - - - -## 综合 -### 云引擎都支持哪些语言 - -目前支持 Node.js、Python、Java、PHP、.NET、Go 运行环境,未来可能还会引入其他语言。 - -### 云引擎支持托管纯静态网站吗 - -支持。命令行工具初始化项目选择语言环境时,依次选择 Others > Static Site 即可。 - -### 云引擎支持 HTTPS 吗 - -- 自定义域名在绑定时启用 SSL 即可支持 HTTPS。 -- 如需配置自动跳转,请看[云引擎下如何重定向到 HTTPS?](#云引擎下如何重定向到-https?)。 - -### 云引擎采用什么样的休眠策略? - -标准实例不会休眠。 - -体验实例会执行休眠策略: - -* 如果应用最近一段时间(半小时)没有任何外部请求,则休眠。 -* 休眠后如果有新的外部请求实例则马上启动。访问者的体验是第一个请求响应时间是 5 ~ 30 秒(视实例启动时间而定),后续访问响应速度恢复正常。 -* 强制休眠:如果最近 24 小时内累计运行超过 18 小时,则强制休眠。此时新的请求会收到 503 的错误响应码,该错误可在 **云服务控制台 > 云引擎 > 云引擎分组 > 统计** 中查看。 - -### 云引擎的请求有哪些限制? - -云引擎的负载均衡组件限制了请求不能超过 100 MB(包括直接上传文件到云引擎)、请求处理不得超过 60 秒,WebSocket 60 秒无数据会被断开连接。 - -国内节点未绑定独立 IP 的云引擎默认为纯静态站点优化。请求会先经过边缘节点,再视缓存命中情况回源到负载均衡组件,最后到达你的应用。 -边缘节点额外限制了请求不能超过 60 MB、请求处理不得超过 10 秒,另外边缘节点不支持 WebSocket 请求和 HTTP PATCH 方法,也不支持获取客户端 IP。 -因此,如果您在国内节点云引擎托管动态网站,我们建议您绑定独立 IP,使用独立入口,不经过边缘节点,自然也就没有上述限制。 - -### 云引擎运行日志大小有限制吗? - -日志单行最大 4096 个字符,多余部分会被丢弃;日志输出频率大于 600 行/分钟,多余的部分会被丢弃。 - -### 云引擎使用什么时区? - -国内版使用北京时间(东八区),国际版使用 UTC+0 时区。 - -### 如何查看云引擎的出入口 IP 地址? - -如果开发者希望在第三方服务平台(如微信开放平台)上配置 IP 白名单而需要获取云引擎的入口或出口 IP 地址,请进入 **云服务控制台 > 云引擎 > 设置 > 出入口 IP** 来自助查询。 - -我们会尽可能减少出入口 IP 的变化频率,但 IP 突然变换的可能性仍然存在。因此在遇到与出入口 IP 相关的问题,我们建议先进入控制台来核实一下 IP 列表是否有变化。 - -如需保持入口 IP 不变,建议为云引擎绑定独立 IP。 - -### 如何访问云引擎预备环境中托管的网站? - -需要在控制台手动绑定一个 `stg-` 开头的域名。`stg-` 开头的自定义域名(例如 stg-web.example.com)会被自动地绑定到预备环境。 - -### 如何判断当前云引擎是预备环境还是生产环境? - -默认情况,云引擎只有一个「生产环境」,对应的域名是 web.example.com。在生产环境中有一个「体验实例」来运行应用。 - -当生产环境的体验实例升级到「标准实例」后会有一个额外的「预备环境」,对应域名 stg-web.example.com,两个环境所访问的都是同样的数据,你可以用预备环境测试你的云引擎代码,每次修改先部署到预备环境,测试通过后再发布到生产环境;如果你希望有一个独立数据源的测试环境,建议单独创建一个应用。 - -另外,stg-web.example.com 域名是需要在控制台自行绑定的。 - -### Application not found 错误 - -访问云引擎服务时,服务端返回错误「Application not found」或在云引擎日志中出现这个错误,可能有以下原因: - -* 调用错了环境。最常见的情况是,免费的体验实例是没有预备环境,开发者却主动设置去调用预备环境。 -* 云引擎自定义域名填错了,比如微信回调地址。 -* 因为免费版(体验版)的云引擎是有休眠的,休眠期间被调用会出现这个错误。建议升级到标准实例以保证实例一直运行。 - -### 云引擎会重复提交请求吗? - -云引擎的负载均衡对于幂等的请求(GET、PUT),在 HTTP 层面出错或超时的情况下是会重试的。 -可以使用正确的谓词(例如 POST)避免此类重试。 - -### 云引擎中如何处理用户登录和 Cookie? - -如果你的页面主要由服务端渲染,可以使用我们在部分 SDK 中提供的管理 Cookie 和 Session 的中间件或模块,也可以其他第三方的中间件或模块,在 Cookie 中维护用户状态。 - -使用 Cookie 作为鉴权方式需要注意防范 [CSRF](https://github.com/pillarjs/understanding-csrf) 攻击(其他站点伪造带有正确 Cookie 的恶意请求)。 -业界通常使用 CSRF Token 来防御 CSRF 攻击,你需要传递给客户端一个随机字符串(即 CSRF Token,可通过 Cookie 传递),客户端在每个有副作用的请求中都要将 CSRF 包含在请求正文或 Header 中,服务器端需要校验这个 CSRF Token 是否正确。 - -如果你的页面主要是由浏览器端渲染,那么建议在前端使用 SDK 登录用户,调用 SDK 的接口获取 session token,通过 HTTP Header 等方式将 session token 发送给后端。 - -例如,在前端登录用户并通过 `user.getSessionToken()` 获取 `sessionToken` 并发送给后端: - -```js -AV.User.login(user, pass).then(user => { - return fetch('/profile', { - headers: { - 'X-LC-Session': user.getSessionToken() - } - }); -}); -``` - -相应的后端 Node.js 代码: - -```js -app.get('/profile', function (req, res) { - AV.User.become(req.headers['x-lc-session']).then(user => { - res.send(user); - }).catch(err => { - res.send({ error: err.message }); - }); -}); - -app.post('/todos', function (req, res) { - var todo = new Todo(); - todo.save(req.body, { sessionToken: req.headers['x-lc-session'] }).then(() => { - res.send(todo); - }).catch(err => { - res.send({ error: err.message }); - }); -}); -``` - -### 云引擎下如何管理用户会话? - -使用各框架自带的组件或第三方模块即可。 - -例如: - -- Node.js 的 Express 框架可以使用 [cookie-session](https://github.com/expressjs/cookie-session) 组件。它和 `AV.Cloud.CookieSession` 组件可以并存。注意,Express 框架的 `express.session.MemoryStore` 在云引擎中是无法正常工作的,因为云引擎是多主机、多进程运行,因此内存型 session 是无法共享的。 -- Python 的 Flask 框架和 Django 框架都自带 session 组件。 -- PHP 可以使用 SDK 提供的 `CookieStorage` 保存会话属性。注意,PHP 默认的 `$_SESSION` 在云引擎中是无法正常工作的,因为云引擎是多主机、多进程运行,因此内存型 session 是无法共享的。 -### 云引擎下如何发送 HTTP 请求? - -使用各语言的标准库或社区提供的模块即可。 - -例如: - -- Node.js 项目可以使用 [superagent] 等社区提供的模块。 -- Python 项目可以使用标准库中的 `urllib.request` 模块或社区的 [requests] 模块。 -- PHP 项目可以使用 PHP 内置的 `curl` 模块或 [guzzle] 等第三方库。 -- Java 项目可以使用 `URL` 或者是 `HttpClient` 等基础类或 [OkHttp] 等第三方库。 - -[superagent]: https://www.npmjs.com/package/superagent -[requests]: https://docs.python-requests.org/en/master/ -[guzzle]: https://docs.guzzlephp.org/en/stable/ -[OkHttp]: https://square.github.io/okhttp/ - -### 云引擎下如何获取客户端 IP? - -如果你想获取客户端的 IP,可以直接从用户请求的 HTTP 头的 `x-real-ip` 字段获取。 -下面给出各语言的示例代码。 - -Node.js(Express): - -```js -app.get('/', function (req, res) { - var ipAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress; - console.log(ipAddress); - res.send(ipAddress); -}); -``` - -Python(Flask): - -```python -from flask import Flask -from flask import request - -app = Flask(__name__) - -@app.route('/') -def index(): - print(request.headers['x-real-ip']) - return 'ok' -``` - -Python(Django): - -```python -def index(request): - print(request.META['HTTP_X_REAL_IP']) - return render(request, 'index.html', {}) -``` - -PHP: - -```php -$app->get('/', function($req, $res) { - error_log($_SERVER['HTTP_X_REAL_IP]); - return $res; -}); -``` - -Java: - -```java -EngineRequestContext.getRemoteAddress(); -``` - -Go(Echo): - -```go -func fetchRealIP(c echo.Context) error { - realIP = c.RealIP() - //... -} -``` - -注意,国内节点的云引擎应用,如果启用了边缘节点加速功能,由于边缘节点的限制,可能无法获取客户端 IP。 -如需获取客户端 IP,建议绑定独立 IP。 - -### 云引擎如何上传文件? - -托管在云引擎的网站可以使用相应 SDK 提供的接口上传文件。 -不过,一般情况下建议在客户端 SDK 上传文件,而不是通过云引擎中转,以免增加不必要的云引擎流量。 - -### 云引擎下如何重定向到 HTTPS? - -大部分 SDK 提供了重定向至 HTTPS 的中间件。 -部署并发布到生产环境之后,访问你的 LeanEngine 网站都会强制通过 HTTPS 访问。 - -Node.js(Express): - -```js -app.enable('trust proxy'); -app.use(AV.Cloud.HttpsRedirect()); -``` - -Node.js(Koa): - -```js -app.proxy = true; -app.use(AV.Cloud.HttpsRedirect({ framework: 'koa' })); -``` - -Python: - -```python -import leancloud - -application = get_your_wsgi_func() - -application = leancloud.HttpsRedirectMiddleware(application) -``` - -PHP(Slim): - -```php -SlimEngine::enableHttpsRedirect(); -$app->add(new SlimEngine()); -``` - -Java: - -```java -LeanEngine.setHttpsRedirectEnabled(true); -``` - -Go SDK 暂未提供跳转至 HTTPS 的中间件。 - -.NET: - -```cs -app.UseHttpsRedirection(); -``` - -### 如何判断请求是通过 HTTPS 还是 HTTP 访问的? - -因为 HTTPS 加密是在负载均衡层面处理的,所以通常部署在云引擎上的 web 框架获取的请求 URL 总是使用 HTTP 协议,建议通过 `X-Forwarded-Proto` HTTP 头来判断原请求是通过 HTTP 还是 HTTPS 访问的。 - - -### 每个应用最多有几个实例? - -每个应用最多拥有 12 个实例,如果需要更多资源请通过工单联系我们的技术支持。 - -### 在线上无法读取到项目中的文件怎么办? - -建议先检查文件大小写是否正确,线上的文件系统是区分大小写的,而 Windows 和 macOS 通常不区分大小写。 - -### 云引擎响应时间增加怎么办 - -响应时间的增加有很多种原因:可能因为只是单纯的请求处理的数据更加复杂导致耗时变长;也有可能是因为请求量过高实例的处理能力不足从而导致响应时间增加。 -建议分析当前的代码并参考 CPU、内存占用量找出瓶颈,确定是否需要调高实例规格或增加实例数量。 -如果需要定位具体是哪些 API 或云函数响应较慢,可以下载访问日志分析。 - -### 如何下载云引擎的应用日志和访问日志 - -云引擎的应用日志(程序的标准输出和标准错误输出)可以在 **云服务控制台 > 云引擎 > 云引擎分组 > 日志** 查看;并且可以使用命令行工具导出最长 7 天的日志。 - -云引擎的访问日志(Access Log)同样可以在**云服务控制台 > 云引擎 > 访问日志**导出。 -## 部署 - -### 云引擎下如何自定义系统级依赖? - -在云引擎的线上环境中,你可以通过 `leanengine.yaml` 文件的 `systemDependencies` 部分来自定义系统级依赖: - -```yaml -systemDependencies: - - imagemagick -``` - -目前支持的选项包括: - -- `ffmpeg` 一个音视频处理工具库。 -- `imagemagick` 一个图片处理工具库。 -- `fonts-wqy` 文泉驿点阵宋体、文泉驿微米黑,通常和 `phantomjs` 或 `chrome-headless` 配合来显示中文。 -- `fonts-noto` 思源黑体(体积较大)。 -- `phantomjs` 一个无 UI 的 WebKit 浏览器(该项目已停止维护)。 -- `chrome-headless` 一个无 UI 的 Chrome 浏览器(体积很大,会显著增加部署耗时,运行时也会消耗大量 CPU 和内存;如果使用 `puppeter` 的话,需要给 `puppeteer.launch` 传递这些参数:`{executablePath: '/usr/bin/google-chrome', args: ['--no-sandbox', '--disable-setuid-sandbox']}`;暂不支持 Java)。 -- `node-canvas` 安装 `node-canvas` 所需要的系统级依赖(你仍需要安装 `node-canvas`)。 -- `python-talib` 金融市场数据分析库。 - -注意添加系统依赖将会拖慢部署速度,因此请不要添加未用到的依赖。 - -### 云引擎中设置的环境变量无效? - -默认情况下,应用在运行阶段才能够读取到内置环境变量和自定义环境变量。 -如果希望在安装依赖或编译阶段就能读取到这些环境变量,需要在 `leanengine.yaml` 里设置: - -```yaml -exposeEnvironmentsOnBuild: true -``` - -云引擎运行环境默认提供的环境变量(以及 Node.js 环境变量 `NODE_ENV`)无法被自定义环境变量覆盖(覆盖无效)。 - -### 部署更新云引擎会导致服务中断吗? - -服务不会中断。在代码部署时,系统会优先启动使用新版本代码的实例,待新实例通过了健康检查,系统修改路由将请求转发至新实例后,再关闭旧版本的实例,让服务保持零中断。 - - -### 部署时长时间卡在「正在下载和安装依赖」怎么办? - -这个步骤对应在云端调用各个语言的包管理器(`npm`、`pip`、`composer`、`maven`)安装依赖的过程,我们有一个依赖缓存机制来加速这个安装过程,但缓存可能会因为很多原因失效(比如修改了依赖列表),在缓存失效时会比平时慢很多,请耐心等待。如果你在 `leanengine.yaml` 中指定了系统依赖也会在这个步骤中安装,因此请不要添加未用到的依赖。 - -对于 Node.js 建议检查是否在 `package-lock.json` 或 `yarn.lock` 中指定了较慢的源。 - -### 部署到多个实例时,部分实例失败需要重新部署吗? - -同一环境(预备/生产)下有多个实例时,云引擎会同时在所有实例上部署项目。如因偶然因素部分实例部署不成功,会在几分钟后自动尝试再次部署,无需手动重新部署。 - -### 云引擎实例部署后控制台多次显示「部署中」是怎么回事? - -控制台显示的「部署中」状态泛指所有运维操作,例如唤醒休眠实例、服务器偶发故障引起的重新部署,不只是用户主动进行的部署。 - -### 云引擎的健康检查是什么? - -云引擎的管理系统会每隔几分钟检查所有实例的工作状态(通过 HTTP 检查,详见[云引擎网站托管指南](/v2/sdk/engine/guide/webhosting)的《健康监测》一节。 -如果实例无法正确响应的话,管理系统会触发一次重新部署,并在控制台上打印类似下面的日志: - -> 健康检查失败:web1 检测到 Error connect ECONNREFUSED 10.19.30.220:51797 - -如果一周内发生一两次属正常现象(有可能是我们的服务器出现偶发的故障,因为会立刻重新部署,对服务影响很小),如果频繁发生可能是你的程序资源不足,或存在其他问题(运行一段时间后不再响应 HTTP 请求),需结合具体情况来分析。 - -### 不使用 SDK 的情况下,该如何实现健康监测和云函数元信息路由? - -不使用 SDK 的情况下,需要自行实现相关路由。 -下面给出 Java 和 PHP 的例子供参考。 - -健康监测: - -```java -// 健康监测 router -@WebServlet(name = "LeanEngineHealthServlet", urlPatterns = {"/__engine/1/ping"}) -public class LeanEngineHealthCheckServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { - resp.setHeader("content-type", "application/json; charset=UTF-8"); - JSONObject result = new JSONObject(); - result.put("runtime", System.getProperty("java.version")); - result.put("version", "custom"); - resp.getWriter().write(result.toJSONString()); - } -} -``` - -```php -$app->get('/__engine/1/ping', function($req, $res) { - // PSR-7 response is immutable - $response = $res->withHeader("Content-Type", "application/json"); - $response->getBody()->write(json_encode(array( - "runtime" => "php-" . phpversion(), - "version" => "custom" - ))); - return $response; -}); -``` - -云函数元信息: - -```java -@WebServlet(name = "LeanEngineMetadataServlet", urlPatterns = {"/1.1/functions/_ops/metadatas"}) -public class LeanEngineMetadataServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, - IOException { - resp.setContentType("application/json; charset=UTF-8"); - resp.getWriter().write("{\"result\":[]}"); - } -} -``` - -```php -app.get('/1.1/_ops/functions/metadatas', function(req, res) { - $response = $res->withHeader("Content-Type", "application/json"); - $response->getBody()->write(json_encode(array( - "result" => array() - ))); - return $response; -}); -``` - -### 云引擎的启动限制时间是多久? - -你的应用在启动时,云引擎的管理程序会每秒去检查你的应用是否启动成功,如果超过启动时间限制仍未启动成功,即认为启动失败。 -启动时间限制默认为 30 秒,如需延长或缩短,可以在 leanengine.yaml` 文件中指定 `startupTimeout`,可设置范围为 15 – 120 秒。 - -### 多次部署同一个项目时镜像大小为什么差别那么大? - -云引擎底层有一套缓存机制以加速构建过程,所以部署时显示的「存储镜像到仓库」后面的大小表示本次构建新产生的数据,可用于评估是否利用到了缓存,不代表整个项目的大小。 - -### Gitlab 部署常见问题 - -很多用户自己使用 [Gitlab](http://gitlab.org/) 搭建了自己的源码仓库,有时可能会遇到无法部署到 LeanCloud 的问题,即使设置了 Deploy Key,却仍然要求输入密码。 - -可能的原因和解决办法如下: - -* 确保你 Gitlab 运行所在服务器的 /etc/shadow 文件里的 git(或者 gitlab)用户一行的 `!` 修改为 `*`,原因参考 [Stackoverflow - SSH Key asks for password](http://stackoverflow.com/questions/15664561/ssh-key-asks-for-password),并重启 SSH 服务:`sudo service ssh restart`。 -* 在拷贝 Deploy Key 时,确保没有多余的换行符号。 -* Gitlab 目前不支持有注释的 Deploy Key。早期 LeanCloud 用户生成的 Deploy Key 末尾可能带有注释(类似于 `App dxzag3zdjuxbbfufuy58x1mvjq93udpblx7qoq0g27z51cx3's cloud code deploy key`),需要删除掉这部分再保存到 Gitlab。 - -## 命令行工具 -### 使用 Homebrew 安装命令行工具失败 - -有些地区 Homebrew 访问网络可能很慢,可以通过设置环境变量 `http_proxy`、`https_proxy`、`all_proxy` 加速访问(详见 [man brew]),或者也可以配置 Homebrew [索引][tuna]和[二进制预编译包][tuna-bottles]的镜像。 - -[man brew]: https://docs.brew.sh/Manpage -[tuna]: https://mirror.tuna.tsinghua.edu.cn/help/homebrew/ -[tuna-bottles]: https://mirrors.tuna.tsinghua.edu.cn/help/homebrew-bottles/ - -或者也可以在 [GitHub releases 页面]下载适用于 macOS 的二进制文件,重命名为 `lean` 后移动到 `$PATH` 下的路径,并添加可执行权限(`chmod a+x /path/to/lean`)。 -如果运行 `lean` 时 macOS 报错「来自身份不明的开发者」,那么需要在 macOS 系统设置「隐私与安全」下配置一下,详见 [Apple 官方文档][HT202491]。 - -[GitHub releases 页面]: https://releases.leanapp.cn/#/leancloud/lean-cli/releases -[HT202491]: https://support.apple.com/en-us/HT202491 - -### 之前使用 `npm` 装过旧版的命令行工具,如果升级到新版? - -如果之前使用 `npm` 安装过旧版本的命令行工具,为了避免与新版本产生冲突,建议使用 `npm uninstall -g leancloud-cli` 卸载旧版本命令行工具。或者直接按照 `homebrew` 的提示,执行 `brew link --overwrite lean-cli` 覆盖掉之前的 `lean` 命令来解决。 - -### 命令行工具初始化项目时报错 `please login first`,可是之前明明已经通过 `lean login` 成功登录了? - -如果通过 `lean login` 登录的账号名下没有 LeanCloud 应用,会碰到这一问题。 -需要创建一个应用再重新运行一下 `lean login`,之后就可以正常使用了。 - -### 使用命令行工具部署失败怎么办? - -部署失败有多种原因,请根据显示的报错信息耐心排查。 -一般来说,如果您使用命令行工具部署,首先建议您检查命令行工具是否是最新版,如果不是最新版,先升级到最新版再重试。 - -### 命令行工具在本地调试时提示 `Error: listen EADDRINUSE :::3000`,无法访问应用 - -`listen EADDRINUSE :::3000` 表示你的程序默认使用的 3000 端口被其他应用占用了,可以按照下面的方法找到并关闭占用 3000 端口的程序: - -* [macOS 使用 `lsof` 和 `kill`](http://stackoverflow.com/questions/3855127/find-and-kill-process-locking-port-3000-on-mac) -* [Linux 使用 `fuser`](http://stackoverflow.com/questions/11583562/how-to-kill-a-process-running-on-particular-port-in-linux) -* [Windows 使用 `netstat` 和 `taskkill`](http://stackoverflow.com/questions/6204003/kill-a-process-by-looking-up-the-port-being-used-by-it-from-a-bat) - -也可以修改命令行工具默认使用的 3000 端口: -``` -lean -p 3002 -``` - -### 同一个项目如何批量部署到多个应用的云引擎? - -可以通过 `lean switch` 切换项目所属应用,然后通过 `lean deploy` 部署。 -`lean switch` 支持通过参数以非交互的方式使用: - -```sh -lean switch --region REGION --group GROUP_NAME APP_ID -lean deploy --prod 1 -``` - -上述命令中,`REGION` 代表应用所在区域,目前支持的值为 `cn-n1`(华北节点)、`cn-e1`(华东节点)、`us-w1`(国际版)。 -`--prod 1` 表示部署到生产环境,如果希望部署到预备环境,换成 `lean deploy` 即可。 -基于这两个命令可以自行编写 CI 脚本快速部署至多个应用的云引擎实例。 - -### 命令行工具的 metric 命令有什么用? - -使用 `metric` 命令可以查看 LeanStorage 的状态报告: - -```sh -$ lean metric --from 2017-09-07 -[INFO] Retrieving xxxxxx storage report -Date 2017-09-07 2017-09-08 2017-09-09 -API Requests 49 35 14 -Max Concurrent 2 2 2 -Mean Concurrent 1 1 1 -Exceed Time 0 0 0 -Max QPS 5 5 5 -Mean Duration Time 9ms 21ms 7ms -80% Duration Time 15ms 22ms 9ms -95% Duration Time 26ms 110ms 25ms -``` - -相关状态的描述如下: - - - - - - - - - - - - - - - - -
    状态描述
    `Date`日期
    `API Requests`API 请求次数
    `Max Concurrent`最大工作线程数
    `Mean Concurrent`平均工作线程数
    `Exceed Time`超限请求数
    `Max QPS`最大 QPS
    `Mean Duration Time`平均响应时间
    `80% Duration Time`80% 响应时间
    `95% Duration Time`95% 响应时间
    - -`metric` 接收参数与 `logs` 类似,具体参考 `lean metric -h`。 - -### 如何通过命令行工具上传文件至文件服务? - - -```sh -$ lean upload public/index.html -Uploads /Users/dennis/programming/avos/new_app/public/index.html successfully at: http://ac-7104en0u.qiniudn.com/f9e13e69-10a2-1742-5e5a-8e71de75b9fc.html -``` - -文件上传成功后会自动生成在云端的 URL,即上例中 `successfully at:` 之后的信息。 - -上传 images 目录下的所有文件: - -```sh -$ lean upload images/ -``` - -### 如何扩展命令行工具的功能? - -有时我们需要对某个应用进行特定并且频繁的操作,比如查看应用 `_User` 表的记录总数,这样可以使用命令行工具的自定义命令来实现。 - -只要在当前系统的 `PATH` 环境变量下,或者在项目目录 `.leancloud/bin` 下存在一个以 `lean-` 开头的可执行文件,比如 `lean-usercount`,那么执行 `$ lean usercount`,命令行工具就会自动调用这个可执行文件。与直接执行 `$ lean-usercount` 不同的是,这个命令可以获取与应用相关的环境变量,方便访问对应的数据。 - -例如将如下脚本放到当前系统的 `PATH` 环境变量中(比如 `/usr/local/bin`): - -```python -#! /bin/env python - -import sys - -import leancloud - -app_id = os.environ['LEANCLOUD_APP_ID'] -master_key = os.environ['LEANCLOUD_APP_MASTER_KEY'] - -leancloud.init(app_id, master_key=master_key) -print(leancloud.User.query.count()) -``` - -同时赋予这个脚本可执行权限 `$ chmod +x /usr/local/bin/lean-usercount`,然后执行 `$ lean usercount`,就可以看到当前应用对应的 `_User` 表中记录总数了。 - -## 云函数 - -### 云函数有哪些限制? - -云函数是 LeanCloud 提供的一个 **相对受限** 的自定义服务器端逻辑的功能,和我们的 SDK 有比较 **深度的集成**。我们将云函数设计为一种类似 **RPC** 的机制,在云函数中你只能关注参数和结果,而不能自定义超时时间、HTTP Method、URL,不能读取和设置 Header。如果希望更加自由地使用这些 HTTP 的语义化功能,或者希望使用第三方的框架提供标准的 RESTful API,请使用云引擎的网站托管功能自行来处理 HTTP 请求。 - -### 项目部署成功了,但云函数和 Hook 不可用? - -为了支持云引擎的云函数和 Hook 功能,云引擎的管理程序会使用 `/1.1/functions/_ops/metadatas` 这个 URL 和 SDK 交互,请确保将这个 URL 交给 SDK 处理。 -默认情况下,云引擎会尝试从 `/1.1/functions/_ops/metadatas` 获取云函数和 Hook 的元信息,如果失败,则云函数和 Hook 功能不可用,但不会中断部署。 -如果希望在获取元信息失败后中断部署,可以在 `leanengine.yaml` 文件中指定 `functionsMode` 为 `strict`。 -如果应用不使用云函数和 Hook 功能,那么你可以: - -- 在 `leanengine.yaml` 中不指定 `functionsMode`,同时 `/1.1/functions/_ops/metadatas` **返回一个 HTTP `404`** 表示不使用云函数和 Hook 相关的功能; -- 或者在 `leanengine.yaml` 中指定 `functionsMode` 为 `disabled`。注意,这种情况下,即使应用代码中定义了云函数和 Hook,Hook 也不会生效,云函数调用(通过 SDK 发起远程调用或通过 REST API 向 API 域名发起云函数调用)有可能因为被转发到错误的云引擎分组而失败。 - -### 部署中断,提示有同名云函数怎么办? - -云引擎支持多个分组。 -如果当前部署代码中部分云函数与其他组的同名,默认情况会提示错误并中断部署,防止意外重复定义云函数。 -我们建议你移除不需要的云函数,毕竟重复定义的云函数并不易于理解和维护。 -不过,你也可以通过在每次部署时额外指定 `--overwrite-functions` 参数强制替换其他组云函数的实现。 - -### 为什么 Class Hook 没有被运行? - -首先确认一下 Hook 被调用的时机是否与你的理解一致: - -* `beforeSave`:对象保存或创建之前 -* `afterSave`:对象保存或创建之后 -* `beforeUpdate`:对象更新之前 -* `afterUpdate`:对象更新之后 -* `beforeDelete`:对象删除之前 -* `afterDelete`:对象删除之后 -* `onVerified`:用户通过邮箱或手机验证后 -* `onLogin`:用户在进行登录操作时(`become(sessionToken)` 不是登录操作,因此不会调用 `onLogin`) - -还需注意在本地进行云引擎调试时,运行的会是线上预备环境的 Hook,如果没有预备环境则不会运行。 - -然后检查 Hook 函数是否被执行过: - -可以先在 Hook 函数的入口打印一行日志,然后进行操作,再到云引擎日志中检查该行日志是否被打印出来,如果没有看到日志原因可能包括: - -* 代码没有被部署到正确的应用 -* 代码没有被部署到生产环境(或没有部署成功) -* Hook 的类名不正确 - -如果日志已打出,则继续检查函数是否成功,检查控制台上是否有错误信息被打印出。如果是 before 类 Hook,需要保证 Hook 函数在 15 秒内结束,否则会被系统认为超时。 - -after 类 Hook 超时时间为 3 秒,如果你的体验实例已经休眠,很可能因为启动时间过长无法收到 after 类 Hook,建议升级到云引擎的标准实例避免休眠。 - -### 可以在云函数中未登录的情况下查询 _User 表吗? - -在云函数里可以用 masterKey 跳过权限检查,未登录也可直接查询 _User 表。 - -因为云引擎运行在可信的服务器端环境中,所以你可以全局开启超级权限(`Master Key`),这样云端会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据。具体细节可以参考[云函数指南](/v2/sdk/engine/guide/cloudfunction)的《Master Key 和超级权限》一节。 - -### 调用云函数时,如何指定请求所发往的环境? - -云引擎应用有「生产环境」和「预备环境」之分。在云引擎通过 SDK 调用云函数时,包括显式调用以及隐式调用(由于触发 hook 条件导致 hook 函数被调用),SDK 会根据云引擎所属环境(预备、生产)调用相应环境的云函数。例如,假定定义了 `beforeDelete` 云函数,在预备环境通过 SDK 删除一个对象,会触发预备环境的 `beforeDelete` hook 函数。 - -在云引擎以外的环境通过 SDK 显式或隐式调用云函数时,`X-LC-Prod` 的默认值一般为 `1`,也就是调用生产环境。但由于历史原因,各 SDK 的具体行为有一些差异: - -- 在 Node.js、PHP、Java、C# 这三个 SDK 下,默认总是调用生产环境的云函数。 -- 在 Python SDK 下,配合 lean-cli 本地调试时,且应用存在预备环境时,默认调用预备环境的云函数,其他情况默认调用生产环境的云函数。 -- 云引擎 Java 环境的模板项目 [java-war-getting-started] 和 [spring-boot-getting-started] 做了处理,配合 lean-cli 本地调试时,且应用存在预备环境时,默认调用预备环境的云函数,其他情况默认调用生产环境的云函数(与 Python SDK 的行为一致)。 - -[java-war-getting-started]: https://github.com/leancloud/java-war-getting-started/ -[spring-boot-getting-started]: https://github.com/leancloud/spring-boot-getting-started/ - -你还可以在 SDK 中指定客户端将请求所发往的环境: - - - -```cs -LCCloud.IsProduction = true; // production (default) -LCCloud.IsProduction = false; // stage -``` -```java -LCCloud.setProductionMode(true); // production -LCCloud.setProductionMode(false); // stage -``` -```objc -[LCCloud setProductionMode:YES]; // production (default) -[LCCloud setProductionMode:NO]; // stage -``` -```swift -// production by default - -// stage -do { - let environment: LCApplication.Environment = [.cloudEngineDevelopment] - let configuration = LCApplication.Configuration(environment: environment) - try LCApplication.default.set( - id: {{appid}}, - key: {{appkey}}, - serverURL: "https://please-replace-with-your-customized.domain.com", - configuration: configuration) -} catch { - print(error) -} -``` -```dart -LCCloud.setProduction(true); // production (default) -LCCloud.setProduction(false); // stage -``` -```js -AV.setProduction(true); // production (default) -AV.setProduction(false); // stage -``` -```python -leancloud.use_production(True) # production (default) -leancloud.use_production(False) # stage -# 需要在 SDK 初始化语句 `leancloud.init` 之前调用 -``` -```php -LeanClient::useProduction(true); // production (default) -LeanClient::useProduction(false); // stage -``` -```go -// 暂不支持(总是使用生产环境) -``` - - - -免费版云引擎应用只有「生产环境」,因此请不要切换到预备环境。 - -### 云引擎创建的新的分组,可以调试云函数吗? - -云引擎的各个分组都支持定义云函数(包括 Hook 函数和定时任务)。 - -每个分组都有独立的预备环境用于测试代码、独立的域名供外部访问,每个分组的环境变量、代码仓库等设置也是独立的,可以单独对一个组部署代码。你可以在分组中创建和管理实例,如果组中没有实例就无法响应请求,如果组中有多个实例便可以提供负载均衡和高可用的能力。 - -### 客户端如何调用云引擎分组中的云函数? - -2020 年 10 月份云引擎已经[在所有分组上支持了云函数](https://leancloudblog.com/cloud-functions-on-all-groups/),如果你的应用的不同分组上不存在重复定义的云函数,客户端直接调用云函数,在云引擎这边能自动根据名称路由到正确的分组(对客户端来说是透明的)。 - -## 定时任务和云队列 - -### 定时任务应该在预备环境还是生产环境执行? - -系统赠送的预备环境体验实例会自动休眠,可能干扰定时任务的执行,因此一般建议在预备环境测试定时任务,在生产环境正式执行定时任务。 -如果定时任务 CPU、内存占用非常高,担心影响生产环境的网站托管功能或其他云函数访问,那么可以在预备环境购买标准实例,并在预备环境执行定时任务。 - -## Node.js - -### 怎么添加第三方模块 - -只需要像普通的 Node.js 项目那样,在项目根目录的 `package.json` 中添加依赖即可: - -``` -{ - "dependencies": { - "lodash": "^4.17.11", - "nanoid": "^3.1.10" - } -} -``` - -`dependencies` 内的内容表明了该项目依赖的三方模块(比如示例中的 `lodash` 和 `nanoid`)。关于 `package.json` 的更多信息见[云引擎网站托管指南](/v2/sdk/engine/guide/webhosting)。 - -然后即可在代码中使用第三方包(`const { nanoid } = require("nanoid");`),如需在本地调试还需运行 `npm install` 来安装这些包。 - -**注意**:命令行工具部署时不会上传 `node_modules` 目录,因为云引擎服务器会根据 `package.json` 的内容自动下载三方包。所以也建议将 `node_modules` 目录添加到 `.gitignore` 中,使其不加入版本控制。 - -### Node.js 项目的 `devDependencies` 没有安装? - -云引擎会在部署时用 `npm ci` 为你安装项目依赖,包括 `devDependencies`。 -不过,如果项目的 Node.js 版本小于 10,则会使用 `npm install --production` 安装依赖,相应地,`devDependencies` 中列出的依赖**不会**安装。 -如需安装 `devDependencies`,请在项目的 `leanengine.yaml` 中指定 `installDevDependencies: true`。 - -### `npm ERR! peer dep missing` 错误怎么办? - -部署时出现类似错误: - -``` -npm ERR! peer dep missing: graphql@^0.10.0 || ^0.11.0, required by express-graphql@0.6.11 -``` - -说明有一部分 peer dependency 没有安装成功,因为 Node.js 版本小于 10 时,线上只会安装 dependencies 部分的依赖,所以请确保 dependencies 部分依赖所需要的所有依赖也都列在了 dependencies 部分(而不是 devDependencies)。 - -你可以在本地删除 node_modules,然后用 `npm install --production` 重新安装依赖来重现这个问题。 - -或者,你也可以考虑将项目升级到 Node.js 10 以上的版本。 - -### Node.js 项目中同时包含 `package-lock.json` 和 `yarn.lock` 时,以哪个文件为准? - -- 如果你的应用目录中含有 `package-lock.json`,那么会根据 lock 中的描述进行安装(需要 Node.js 8.0 以上)。 -- 如果你的应用目录中含有 `yarn.lock`,那么会使用 `yarn install` 代替 `npm install` 来安装依赖(需要 Node.js 4.8 以上)。 -- 如果你的应用目录中同时包含 `package-lock.json` 和 `yarn.lock`,云引擎会使用 `yarn install`。换言之,`yarn.lock` 优先。 - -如果不希望使用 `yarn.lock`,请将它们加入 `.gitignore`(Git 部署时)或 `.leanignore`(命令行工具部署时)。 - -另外,也请注意 `yarn.lock` 中包含了下载依赖的 URL,请选择合适的源,否则可能拖慢云引擎部署。 - -### Node.js 项目如何打印 SDK 发出的网络请求? - -你可以通过设置一个 `DEBUG=leancloud:request` 的环境变量来打印由 SDK 发出的网络请求。在本地调试时你可以通过这样的命令启动程序: - -```sh -env DEBUG=leancloud:request lean up -``` - -当有对 LeanCloud 的调用时,你可以看到类似这样的日志: - -```sh -leancloud:request request(0) +0ms GET https://{{host}}/1.1/classes/Todo?&where=%7B%7D&order=-createdAt { where: '{}', order: '-createdAt' } -leancloud:request response(0) +220ms 200 {"results":[{"content":"1","createdAt":"2016-08-09T06:18:13.028Z","updatedAt":"2016-08-09T06:18:13.028Z","objectId":"57a975a55bbb5000643fb690"}]} -``` - -我们不建议在线上生产环境开启这个日志,否则将会打印大量的日志。如有必要,可以指定 `DEBUG=leancloud:request:error`,只打印出错的网络请求。 - -### 如何排查云引擎 Node.js 内存使用过高(内存泄漏)? - -首先建议检查云引擎日志,检查每分钟请求数、响应时间、CPU、内存统计,查看是否存在其他异常情况,如果有的话,先解决其他的问题。如果是从某个时间点开始内存使用变高,建议检查这个时间点之前是否有部署新版本,然后检查新版本的代码改动或尝试回滚版本。 - -然后这里有一些常见的原因可供对照检查: - -- 如果使用 cluster 多进程运行,内存使用会成倍增加(取决于运行几个 Worker)。 -- 如果在代码中不断地向一个全局对象(或生命周期较长的对象)上添加新的对象或闭包的引用(例如某种缓存),那么在运行过程中内存使用会逐渐增加(即内存泄漏)。 -- 如果响应时间增加(例如对请求的处理卡在慢查询、第三方网络请求),那么程序同时处理的请求数量会增加(即请求堆积),会占用很多内存。 -- 也有可能是业务本身在增加,确实需要这么多内存。 - -如果还不能找到原因,可以尝试一些更高级的工具: - -- [heapdump](https://github.com/bnoordhuis/node-heapdump) 或 [v8-profiler](https://github.com/node-inspector/v8-profiler):可以导出一份内存快照,快照文件可以被下载到本地,通过 Chrome 打开,可以看到内存被哪些对象占用。如果能在本地复现的话,建议尽量在本地运行,如需线上运行则你需要自己编码实现发送信号、下载快照文件等功能。 -- `--trace_gc`:可以在 GC 时在标准输出打印简要的日志,包括 GC 的类型、耗时,GC 前后的堆体积和对象数量(在 `package.json` 的 `scripts.start` 里改成 `node --trace_gc server.js` 来开启)。 - -其他参考资料(第三方): - -- [Node.js 调试 GC 以及内存暴涨的分析](https://blog.devopszen.com/node-js_gc) -- [Node.js Performance Tip: Managing Garbage Collection](https://strongloop.com/strongblog/node-js-performance-garbage-collection/) -- [Node.JS Profile 1.2 V8 GC 详解](https://xenojoshua.com/2018/01/node-v8-gc/) - -### 如何排查云引擎 Node.js CPU 使用过高(响应缓慢)? - -首先建议检查云引擎日志,检查每分钟请求数、响应时间、CPU、内存统计,查看是否存在其他异常情况,如果有的话,先解决其他的问题。如果是从某个时间点开始 CPU 使用变高,建议检查这个时间点之前是否有部署新版本,然后检查新版本的代码改动或尝试回滚版本。 - -然后这里有一些常见的原因可供对照检查: - -- 如果内存使用率较高、或频繁分配和释放大量对象,那么 GC 会占用一些 CPU 也会导致卡顿,可以用 `--trace_gc` 打开 GC 日志来确认 GC 的影响。 -- 程序中有死循环或失去控制(数量不断增加)的 setTimeout 或 setInterval。 -- 也有可能是业务本身在增加,确实需要这么多 CPU。 - -因为 Node.js 是基于单线程的事件循环模型,如果事件循环中新的任务一直得不到执行(即事件循环被「阻塞」),那么就会造成 CPU 不高,但响应缓慢或不响应的情况。导致事件循环被阻塞的场景情况包括: - -- 密集的纯计算,例如执行时间非常长的同步循环、序列化(JSON 等)、复杂的数学(密码学)运算、复杂度非常高的正则表达式。 -- 同步的 IO 操作,例如 `fs.readFileSync`、`child_process.execSync` 或含有这些同步操作的第三方包(例如 `sync-request`)。 -- 不断向事件循环中添加新的任务,例如 `process.nextTick` 或 `setImmediate`,导致事件队列中的任务一直执行不完。 - -其他会导致 Node.js 响应慢或不响应的情况: - -- 使用了 Node.js 中底层采用线程数实现的 API,包括 `dns.lookup`(多数 HTTP 客户端间接使用了该函数)、所有文件系统 API,如果大量使用这些 API 或这些操作非常慢,则会产生额外的等待。 - -如果能不能找到原因,可以尝试一些更高级的工具: - -- `node --prof`:可以统计程序中每一个函数的执行耗时和调用关系,导出一份日志文件,可以用 `node --prof-process` 来生成一份报告,包括占用 CPU 时间最多的函数(包括 JavaScript 和 C++ 部分)列表。`node --prof` 对性能本身的影响很大,长时间运行生成的日志也很大,建议尽量在本地运行,如需在线上运行则需要你自己编码实现生成报告、下载报告等功能。 -- [v8-profiler](https://github.com/node-inspector/v8-profiler):可以生成一份 CPU 日志,可以下载到本地后通过 Chrome 打开,可以看到每个函数的执行时间和调用关系。v8-profiler 对性能本身的影响很大,长时间运行生成的日志也很大,建议尽量在本地运行,如需在线上运行则需要你自己编码实现开始生成日志、下载报告等功能。 - -其他参考资料(第三方): - -- [不要阻塞你的事件循环](https://nodejs.org/zh-cn/docs/guides/dont-block-the-event-loop/) -- [Node.JS Profile 4.1 Profile 实践](https://xenojoshua.com/2018/02/node-profile-practice/) -- [手把手测试你的 JS 代码性能](https://cnodejs.org/topic/58b562f97872ea0864fee1a7) -- [Speed Up JavaScript Execution](https://developers.google.com/web/tools/chrome-devtools/rendering-tools/js-execution) - -### Maximum call stack size exceeded 如何解决? - -**将 JavaScript SDK 和 Node SDK 升级到 1.2.2 以上版本可以彻底解决该问题。** - -如果你的应用时不时出现 `Maximum call stack size exceeded` 异常,可能是因为在 hook 中调用了 `AV.Object.extend`。有两种方法可以避免这种异常: - -- 升级 leanengine 到 v1.2.2 或以上版本 -- 在 hook 外定义 Class(即定义在 `AV.Cloud.define` 方法之外),确保不会对一个 Class 执行多次 `AV.Object.extend` - -### 「在线编辑」和「项目部署」可以混用吗? - -「在线编辑」的产生是为了方便大家初次体验云引擎,或者只是需要一些简单 hook 方法的应用使用。我们的实现方式就是把定义的函数拼接起来,生成一个云引擎项目然后部署。 - -所以可以认为「在线编辑」和 「项目部署」最终是一样的,都是一个完整的项目。 - -定义函数是一个单独功能,可以不用使用基础包,git 等工具快速的生成和编辑云引擎。 - -当然,你也可以使用基础包,自己写代码并部署项目。 - -这两条路是分开的,任何一个部署,就会导致另一种方式失效掉。 - -### 如何从「在线编辑」迁移到项目部署? - -1. 按照[云引擎命令行工具使用指南](/v2/sdk/engine/guide/cli)安装命令行工具,使用 `lean new` 初始化项目,模板选择 `Node.js > Express`(我们的 Node.js 示例项目)。 -2. 在**云服务控制台 > 云引擎 > 云引擎分组 > 部署 > 在线编辑**点击 **预览**,将全部函数的代码拷贝到新建项目中的 `cloud.js`(替换掉原有内容)。 -3. 运行 `lean up`,在 的调试界面中测试云函数和 Hook,然后运行 `lean deploy` 部署代码到云引擎(使用标准实例的用户还需要执行 `lean publish`)。 -4. 部署后请留意云引擎控制台上是否有错误产生。 - -如果在线编辑使用的是 0.x 版本 的 Node.js SDK,那么还需要修改不兼容的代码。 -比如将 `AV.User.current()` 改为 `request.currentUser`。 -详见 [升级到云引擎 Node.js SDK 1.0](https://leancloud.cn/docs/leanengine-node-sdk-upgrade-1.html)。 - -### 在云引擎 Node.js 环境下如何本地调用云函数? - -云引擎 Node.js 环境下,默认会直接进行一次本地的函数调用,而不会像客户端一样发起一个 HTTP 请求。 - -```js -AV.Cloud.run('averageStars', { - movie: '夏洛特烦恼' -}).then(function (data) { - // 调用成功,得到成功的应答 data -}, function (error) { - // 处理调用失败 -}); -``` - -如果你希望发起 HTTP 请求来调用云函数,可以传入一个 `remote: true` 的选项。当你在云引擎之外运行 Node.js SDK(包括调用位于其他分组上的云函数)时这个选项非常有用: - -```js -AV.Cloud.run('averageStars', { movie: '夏洛特烦恼' }, { remote: true }).then(function (data) { - // 成功 -}, function (error) { - // 处理调用失败 -}); -``` - -上面的 `remote` 选项实际上是作为 `AV.Cloud.run` 的可选参数 options 对象的属性传入的。这个 `options` 对象包括以下参数: - -- `remote?: boolean`:上面的例子用到的 `remote` 选项,默认为假。 -- `user?: AV.User`:以特定的用户运行云函数(建议在 `remote` 为假时使用)。 -- `sessionToken?: string`:以特定的 `sessionToken` 调用云函数(建议在 `remote` 为真时使用)。 -- `req?: http.ClientRequest | express.Request`:为被调用的云函数提供 `remoteAddress` 等属性。 - -### 云引擎下如何通过 JavaScript SDK 创建推送? - -请参考 SDK 的 API 文档 [AV.Push](https://leancloud.github.io/javascript-sdk/docs/AV.Push.html)。 -这里举两个简单的例子: - -推送给所有订阅了 `public` 频道的设备: - -```js -AV.Push.send({ - channels: [ 'public' ], - data: { - alert: 'public message' - } -}); -``` - -如果希望按照某个 `_Installation` 表的查询条件来推送,例如推送给某个 `installationId` 的 Android 设备,可以传入一个 `AV.Query` 对象作为 `where` 条件: - -```js -const query = new AV.Query('_Installation'); -query.equalTo('installationId', installationId); -AV.Push.send({ - where: query, - data: { - alert: 'Public message' - } -}); -``` - -### 如何在云引擎中使用 Node.js SDK 提供的 CookieSession 中间件? - -如果你的页面主要是由服务器端渲染(例如使用 EJS、Pug),在前端不需要使用 JavaScript SDK 进行数据操作,那么建议你使用我们提供的一个 `CookieSession` 中间件,在 Cookie 中维护用户状态: - -```js -app.use(AV.Cloud.CookieSession({ secret: 'my secret', maxAge: 3600000, fetchUser: true })); -``` - -Koa 需要添加一个 `framework: 'koa'` 的参数: - -```js -app.use(AV.Cloud.CookieSession({ framework: 'koa', secret: 'my secret', maxAge: 3600000, fetchUser: true })); -``` - -使用 `CookieSession` 的同时需要添加 CSRF Token 来防御 CSRF 攻击。 - -你需要传入一个 `secret` 用于签名 Cookie(必须提供),这个中间件会将 `AV.User` 的登录状态信息记录到 Cookie 中,用户下次访问时自动检查用户是否已经登录,如果已经登录,可以通过 `req.currentUser` 获取当前登录用户。 - -`AV.Cloud.CookieSession` 支持的选项包括: - -- **fetchUser**:是否自动 `fetch` 当前登录的 `AV.User` 对象。默认为 `false`。如果设置为 `true`,每个 HTTP 请求都将发起一次 LeanCloud API 调用来 `fetch` 用户对象。如果设置为 `false`,默认只可以访问 `req.currentUser` 的 `id`(`_User` 表记录的 `objectId`)和 `sessionToken` 属性,你可以在需要时再手动 `fetch` 整个用户。 -- **name**:Cookie 的名字,默认为 `avos.sess`。 -- **maxAge**:Cookie 的过期时间。单位为毫秒。 - -在 Node SDK 1.x 之后我们不再允许通过 `AV.User.current()` 获取登录用户的信息,而是需要你: - -- 通过 `request.currentUser` 获取用户信息。 -- 在后续的方法调用显式传递 user 对象。 - -你可以这样简单地实现一个具有登录功能的站点: - -```js -app.post('/login', function (req, res) { - AV.User.logIn(req.body.username, req.body.password).then(function (user) { - res.saveCurrentUser(user); // save cookie - res.redirect('/profile'); - }, function (error) { - res.redirect('/login'); - }); -}) - -app.get('/profile', function (req, res) { - if (req.currentUser) { - res.send(req.currentUser); - } else { - res.redirect('/login'); - } -}); - -app.get('/logout', function (req, res) { - req.currentUser.logOut(); - res.clearCurrentUser(); // clear cookie - res.redirect('/profile'); -}); -``` - -### 跨域 POST 请求未携带 Cookie 怎么办? - -Chrome 80 起 `SameSite` 的默认值为 `Lax`,如果你的应用的前端没部署在云引擎上,又需要向云引擎发送携带 Cookie 的 POST 请求,那么需要设置 `SameSite` 为 `none`。 -`AV.Cloud.CookieSession` 会将所有参数都传递给浏览器的 `cookies.set()`,所以你可以将 `sameSite` 传入: - -```js -AV.Cloud.CookieSession({sameSite: 'none'}) -``` - -注意: - -0. `SameSite` 要求与 `Secure` 标记一同发送,因此请确保你的客户端是通过 HTTPS 协议访问云引擎的。 -1. 请仅在有必要的时候设置 `SameSite` 为 `none`,以免平白增加 CSRF 风险。 - -### 为什么云函数中 include 的字段没有被完整地发给客户端? - -> 将 JavaScript SDK 和 Node SDK 升级到 3.0 以上版本可以彻底解决该问题。 - -云函数在响应时会调用到 `AV.Object#toJSON` 方法,将结果序列化为 JSON 对象返回给客户端。在早期版本中 `AV.Object#toJSON` 方法为了防止循环引用,当遇到属性是 Pointer 类型会返回 Pointer 元信息,不会将 include 的其他字段添加进去,我们在 [JavaScript SDK 3.0](https://github.com/leancloud/javascript-sdk/releases/tag/v3.0.0) 中对序列化相关的逻辑做了重新设计,**将 JavaScript SDK 和 Node SDK 升级到 3.0 以上版本便可以彻底解决该问题**。 - -如果暂时无法升级 SDK 版本,可以通过这样的方式绕过: - -```javascript -AV.Cloud.define('querySomething', function(req, res) { - var query = new AV.Query('Something'); - // user 是 Something 表的一个 Pointer 字段 - query.include('user'); - query.find().then(function(results) { - // 手动进行一次序列化 - results.forEach(function(result){ - result.set('user', result.get('user') ? result.get('user').toJSON() : null); - }); - // 再返回查询结果给客户端 - res.success(results); - }).catch(res.error); -}); -``` - -Python SDK 也存在类似的问题,只会返回 Pointer 元信息,因此也需要额外进行一次查询并手动进行序列化。 - -### RPC 调用云函数时,为什么会返回预期之外的空对象? - -使用 Node SDK 定义的云函数,如果返回一个不是 AVObject 的值,比如字符串、数字,RPC 调用得到的是空对象(`{}`)。 -类似地,如果返回一个包含非 AVObject 成员的数组,RPC 调用的结果中该数组的相应成员也会被序列化为 `{}`。 -这个问题将在 Node SDK 的下一个大版本(4.0)中修复。 -目前绕过这一个问题的方法是将返回结果放在对象(`{}`)中返回。 - -### `node --max-http-header-size` 无效? - -云引擎负载均衡限制 HTTP Header 大小为 8 KB(和[Node.js 的默认值][cli_max_http_header_size_size]保持一致)。 -因此无法通过 `--max-http-header-size` 指定大于 8 KB 的值。 - -[cli_max_http_header_size_size]: https://nodejs.org/api/cli.html#cli_max_http_header_size_size - -### 如何使用云引擎批量更新数据? - -可以参考我们的 [Demo: batch-update](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/routes/batch-update.js)。 - -### 如何接入 Node.js 框架? - -细心的开发者已经发现在示例项目中的 `package.json` 中引用了一个流行的 Node Web 框架 [Express](http://expressjs.com/)。 - -Node.js SDK 为 [Express](http://expressjs.com/) 和 [Koa](http://koajs.com/) 提供了集成支持。 - -如果你已经有了现成的项目使用的是这两个框架,只需通过下面的方式加载 Node.js SDK 提供的中间件到当前项目中即可: - -```sh -npm install --save leanengine leancloud-storage -``` - -引用和配置的代码如下: - -#### Express - -```js -var express = require('express'); -var AV = require('leanengine'); - -AV.init({ - appId: process.env.LEANCLOUD_APP_ID || '{{appid}}', - appKey: process.env.LEANCLOUD_APP_KEY || '{{appkey}}', - masterKey: process.env.LEANCLOUD_APP_MASTER_KEY || '{{masterkey}}' -}); - -var app = express(); -app.use(AV.express()); -app.listen(process.env.LEANCLOUD_APP_PORT); -``` - -其中,`AV.express` 接受一个可选参数 `options`,`options` 是一个对象,目前支持以下两个可选属性: - -- `onError`:全局错误处理函数,云函数(包括 Hook 函数)抛出异常时会调用该函数。该函数的使用场景包括统一发送错误报告。 -- `ignoreInvalidSessionToken`:布尔值,为真时忽略客户端发来的错误的 `sessionToken`(`X-LC-session` 头),为假时抛出 `401` 错误 `{"code": 211, "error": "Verify sessionToken failed, maybe login expired: ..."}`。客户端 SDK 发送请求时会统一发送 `X-LC-session` 头(其中指定了 `sessionToken`),`sessionToken` 可能因种种原因失效,而云函数在很多情况下并不关心 `sessionToken`。因此,云引擎提供了 `ignoreInvalidSessionToken` 这个选项,设为真时忽略 `sessionToken` 错误。反之,如果该选项设为假,客户端收到相应报错时,需要重新登录。 - -你可以使用 Express 的路由定义功能来提供自定义的 HTTP API: - -```js -app.get('/', function (req, res) { - res.render('index', { title: 'Hello World' }); -}); - -app.get('/time', function (req, res) { - res.json({ - time: new Date() - }); -}); - -app.get('/todos', function (req, res) { - new AV.Query('Todo').find().then(function (todos) { - res.json(todos); - }).catch(function (err) { - res.status(500).json({ - error: err.message - }); - }); -}); -``` - -更多最佳实践请参考我们的 [项目模板](https://github.com/leancloud/node-js-getting-started) 和 [云引擎 Node.js Demo 仓库](https://github.com/leancloud/leanengine-nodejs-demos)。 - -#### Koa - -```js -var koa = require('koa'); -var AV = require('leanengine'); - -AV.init({ - appId: process.env.LEANCLOUD_APP_ID || '{{appid}}', - appKey: process.env.LEANCLOUD_APP_KEY || '{{appkey}}', - masterKey: process.env.LEANCLOUD_APP_MASTER_KEY || '{{masterkey}}' -}); - -var app = koa(); -app.use(AV.koa()); -app.listen(process.env.LEANCLOUD_APP_PORT); -``` - -`AV.koa` 同样接受可选参数 `options`,关于 `options` 对象的具体说明,请参考上节。 - -你可以使用 Koa 来渲染页面、提供自定义的 HTTP API: - -```js -app.use(function* (next) { - if (this.url === '/todos') { - return new AV.Query('Todo').find().then(todos => { - this.body = todos; - }); - } else { - yield next; - } -}); -``` - -使用 Koa 时建议将 `package.json` 中的 Node.js 的版本设置为 `4.x` 以上。 - -#### 其他 Web 框架 - -你也可以使用其他的 Web 框架进行开发,但你需要自行去实现云引擎健康监测的逻辑。 -下面是一个使用 Node.js 内建的 [`http`](https://nodejs.org/api/http.html) 实现的最简示例,可供参考: - -```js -require('http').createServer(function (req, res) { - if (req.url == '/') { - res.statusCode = 200; - res.end(); - } else { - res.statusCode = 404; - res.end(); - } -}).listen(process.env.LEANCLOUD_APP_PORT); -``` - -你需要将 Web 服务监听在 `0.0.0.0` 上(Node.js 和 Express 的默认行为)而不是 `127.0.0.1`。 - -可参考[在云引擎中使用其他 Node 框架](https://leancloud.cn/docs/leanengine-web-frameworks.html)这篇指南。 - -#### 路由超时设置 - -因为 Node.js 的异步调用容易因运行时错误或编码疏忽中断,为了减少在这种情况下对服务器内存的占用,也为了客户端能够更早地收到错误提示,所以需要添加这个设置,一旦发生超时,服务端会返回一个 HTTP 错误码给客户端。 - -使用 Express 框架实现自定义路由的时候,请求默认的超时时间为 15 秒,该值可以在 `app.js` 中进行调整: - -```js -// 设置默认超时时间 -app.use(timeout('15s')); -``` - -### 自行接入 Node.js 框架时如何使用云服务的数据存储功能? - -模板项目已经集成了 Node.js SDK,并且包含 SDK 初始化的逻辑。 - -如果项目自行接入 Web 框架,那么需要安装 Node.js SDK (`leanengine`),另外, JavaScript SDK(`leancloud-storage`)也需要作为 peer dependency 一同安装,在升级 Node.js SDK 时也请记得升级 JavaScript SDK: - -```sh -npm install --save leanengine leancloud-storage -``` - -同时也需要自行初始化 SDK(注意我们在云引擎中开启了 masterKey 权限,这将会跳过 ACL 和其他权限限制): - -```js -const AV = require('leanengine'); - -AV.init({ - appId: process.env.LEANCLOUD_APP_ID, - appKey: process.env.LEANCLOUD_APP_KEY, - masterKey: process.env.LEANCLOUD_APP_MASTER_KEY -}); - -AV.Cloud.useMasterKey(); -``` - -### Node.js SDK 不同版本的主要差异? - -Node SDK 的历史版本: - -- `0.x`:最初的版本,对 Node.js 4.x 及以上版本兼容不佳,建议用户参考[升级到云引擎 Node.js SDK 1.0](https://leancloud.cn/docs/leanengine-node-sdk-upgrade-1.html) 来更新。 -- `1.x`:彻底废弃了全局的 `currentUser`,依赖的 JavaScript 也升级到了 1.x 分支,支持了 Koa 和 Node.js 4.x 及以上版本。 -- `2.x`:提供了对 Promise 风格的云函数、Hook 写法的支持,移除了一些被弃用的特性(`AV.Cloud.httpRequest`),不再支持 Backbone 风格的回调函数。 -- `3.x`:**推荐使用** 的版本,指定 JavaScript SDK 为 peer dependency(允许自定义 JS SDK 的版本),升级 JS SDK 到 3.x。 - -详见 Node.js SDK 的 [更新日志](https://github.com/leancloud/leanengine-node-sdk/releases)。 - -## Python - -### 云引擎支持哪些 Python 版本? - -目前仅支持 CPython 版本,暂时不支持 PyPy、Jython、IronPython 等其他 Python 实现。 -另外建议尽量使用 3.6 或以上版本的 Python 进行开发,如果仍然在使用 Python 2 ,请使用 Python 2.7 进行开发。 - -### 自行接入 Python WSGI 框架时如何使用云服务的数据存储功能? - -模板项目已经集成了 Python SDK,并且包含 SDK 初始化的逻辑。 - -如果项目自行接入 Web 框架,那么需要将 `leancloud` 添加到 `requirements.txt` 中,部署到线上即可自动安装此依赖。在本地运行和调试项目的时候,可以在项目目录下使用如下命令进行依赖安装: - -```sh -pip install -r requirements.txt -``` - -同时也需要自行初始化 SDK。 -因为 `wsgi.py` 是项目最先被执行的文件,推荐在此文件进行 Python SDK 的初始化工作: - -```python -import os -import leancloud - -APP_ID = os.environ['LEANCLOUD_APP_ID'] -APP_KEY = os.environ['LEANCLOUD_APP_KEY'] -MASTER_KEY = os.environ['LEANCLOUD_APP_MASTER_KEY'] - -leancloud.init(APP_ID, app_key=APP_KEY, master_key=MASTER_KEY) - -leancloud.use_master_key(True) -``` - -注意我们在云引擎中开启了 masterKey 权限,这将会跳过 ACL 和其他权限限制。 - -### PyPI 上有 `leancloud-sdk` 和 `leancloud` 两个包,该用哪一个? - -请使用 `leancloud`。 - -`leancloud-sdk` 是旧版的 Python SDK,已经不再维护。 - -不同版本的差别详见 Python SDK 的[更新日志](https://github.com/leancloud/python-sdk/blob/master/changelog)。 - -### 如何在云引擎使用 Python SDK 提供的 WSGI 中间件管理 Cookies? - -Python SDK 提供了一个 `leancloud.engine.CookieSessionMiddleware` 的 WSGI 中间件,使用 Cookie 来维护用户(`leancloud.User`)的登录状态。要使用这个中间件,可以在 `wsgi.py` 中将: - -```python -application = engine -``` - -替换为: - -```python -application = leancloud.engine.CookieSessionMiddleware(engine, secret=YOUR_APP_SECRET) -``` - -你需要传入一个 `secret` 的参数用于签名 Cookie(必须提供),这个中间件会将 `AV.User` 的登录状态信息记录到 Cookie 中,用户下次访问时自动检查用户是否已经登录,如果已经登录,可以通过 `leancloud.User.get_current()` 获取当前登录用户。 - -`leancloud.engine.CookieSessionMiddleware` 初始化时支持的非必须选项包括: - -- **name**:在 cookie 中保存的 session token 的 key 的名称,默认为 `leancloud:session`。 -- **excluded_paths**:指定哪些 URL path 不处理 session token,比如在处理静态文件的 URL path 上不进行处理,防止无谓的性能浪费。接受参数类型 `list`。 -- **fetch_user**:处理请求时是否要从存储服务获取用户数据,如果为 `False` 的话,`leancloud.User.get_current()` 获取到的用户数据上除了 `session_token` 之外没有任何其他数据,需要自己调用 `fetch()` 来获取。为 `True` 的话,会自动在用户对象上调用 `fetch()`,这样将会产生一次数据存储的 API 调用。默认为 `False`。 -- **expires**:设置 cookie 的失效日期(参考 [Werkzeug Document](http://werkzeug.pocoo.org/docs/0.12/http/#werkzeug.http.dump_cookie))。 -- **max_age**:设置 cookie 在多少秒后失效(参考 [Werkzeug Document](http://werkzeug.pocoo.org/docs/0.12/http/#werkzeug.http.dump_cookie))。 - -### 在云引擎 Python 环境下如何本地调用云函数? - -云引擎 Python 环境下,默认会进行远程调用。 -例如,以下代码会发起一次 HTTP 请求,去请求部署在云引擎上的云函数。 - -```python -from leancloud import cloud - -cloud.run('averageStars', movie='夏洛特烦恼') -``` - -如果想要直接调用本地(当前进程)中的云函数,或者发起调用就是在云引擎中,想要省去一次 HTTP 调用的开销,可以使用 `leancloud.cloud.run.local` 来取代 `leanengine.cloud.run`,这样会直接在当前进程中执行一个函数调用,而不会发起 HTTP 请求来调用此云函数。 - -## PHP - -### 云引擎支持哪些 PHP 版本? - -目前云引擎支持 `5.6`、`7.0`、`7.1`、`7.2`、`7.3`、`7.4`、`8.0` 这几个版本,后续如果有新版本发布,也会添加支持。 - -### 云引擎的 PHP 环境支持哪些扩展? - -所有版本的 PHP 默认开启以下扩展:`fpm`、`curl`、`mysql`、`zip`、`xml`、`mbstring`、`gd`、`soap`、`sqlite3`。 -7.0 以上版本的 PHP 还默认开启了 `mongodb` 扩展。 -在 PHP 7.2 中官方从核心中移除了 `mcrypt` 这个拓展,云引擎以选装的方式继续提供支持,在 `composer.json` 的 `require` 中加入 `ext-mcrypt: *` 即可,使用 `mcrypt` 会增加部署耗时,如果没有用到请不要加。 -如果您需要用到其他扩展,请提交工单联系我们。 - -### 云引擎的 PHP 环境如何指定 FPM Worker 数量? - -对于 PHP 项目,我们默认每 64 MB 内存分配一个 PHP-FPM Worker,如果希望自定义 Worker 数量,可以在云引擎设置页面的「自定义环境变量」中添加名为 `PHP_WORKERS` 的环境变量,值是一个数字。设置过低会导致收到新请求时无可用的 Worker;过高会导致内存不足、请求处理失败,建议谨慎调整。 - -### 是否支持 `path` 类型的 composer 本地仓库? - -由于构建时会复制 `composer.json` 和 `composer.lock` 到专门的目录安装依赖,因此不支持 `path` 类型的 composer 本地仓库。 -如果您的项目使用了 `path` 类型的本地仓库,我们建议改为 `vcs` 类型。 - -### 何时使用 PHP SDK 的 `Cloud::start`? - -PHP SDK 提供了 `Cloud::start` 函数,可以方便快捷地初始化云函数服务。例如,一个专门提供云函数服务的云引擎项目的 `index.php`: - -```php - "夏洛特烦恼")); -} catch (\Exception $ex) { - // 云函数错误 -} -``` - -如果想要通过 HTTP 调用,可以使用 `runRemote` 方法: - -```php -try { - $token = User::getCurrentSessionToken(); // 以特定的 `sessionToken` 调用云函数,可选 - $result = Cloud::runRemote("averageStars", array("movie" => "夏洛特烦恼"), $token); -} catch (\Exception $ex) { - // 云函数错误 -} -``` - -### PHP 项目从 files.phpcomposer.com 下载文件失败,部署失败怎么办? - -phpcomposer.com 镜像已经停止服务,PHP 项目的 `composer.lock` 文件如果包含了这个地址的 url,会导致依赖安装失败。 -解决方法有两种: - -1. 移除 `composer.lock` 后再部署(云引擎会直接根据 `composer.json` 安装依赖)。 -2. 在本地正确配置仓库地址后,运行 `composer update --lock` 更新 `composer.lock` 文件中的下载链接(不改变具体的版本)。 - -### 自行接入 PHP 框架时如何使用云服务的数据存储功能? - -模板项目已经集成了 PHP SDK,并且包含 SDK 初始化的逻辑。 - -如果自行接入 [Slim 框架](http://www.slimframework.com),可以参考示例项目直接使用 SDK 提供的中间件。 - -如果自行接入其他框架,则需要自己配置依赖: - -```sh -composer require leancloud/leancloud-sdk -``` - -同时也需要自行初始化 SDK(注意我们在云引擎中开启了 masterKey 权限,这将会跳过 ACL 和其他权限限制)。 - -```php -use \LeanCloud\Client; - -Client::initialize( - getenv("LEANCLOUD_APP_ID"), - getenv("LEANCLOUD_APP_KEY"), - getenv("LEANCLOUD_APP_MASTER_KEY") -); - -Client::useMasterKey(true); -``` - -### 如何在云引擎使用 PHP SDK 提供的 CookieStorage 模块? - -云引擎提供了一个 `LeanCloud\Storage\CookieStorage` 模块,用 Cookie 来维护用户(`User`)的登录状态,要使用它可以在 `app.php` 中添加下列代码: - -```php -use \LeanCloud\Storage\CookieStorage; -Client::setStorage(new CookieStorage(60 * 60 * 24, "/")); -``` - -`CookieStorage` 支持传入秒作为过期时间,以及路径作为 cookie 的作用域。默认过期时间为 7 天。 - -可以通过 `User::getCurrentUser()` 来获取当前登录用户。你可以这样简单地实现一个具有登录功能的站点: - -```php -$app->get('/login', function($req, $res) { - // login page -}); - -$app->post('/login', function($req, $res) { - $params = $req->getQueryParams(); - try { - User::logIn($params["username"], $params["password"]); - return $res->withRedirect('/profile'); - } catch (Exception $ex) { - return $res->withRedirect('/login'); - } -}); - -$app->get('/profile', function($req, $res) { - $user = User::getCurrentUser(); - if ($user) { - return $res->getBody()->write($user->getUsername()); - } else { - return $res->withRedirect('/login'); - } -}); - -$app->get('/logout', function($req, $res) { - User::logOut(); - return $res->redirect("/"); -}); -``` - -一个简单的登录页面可以是这样: - -```html - - - -
    - - - - - -
    - - -``` - -`CookieStorage` 也支持保存其他属性: - -```php -$cookieStorage = Client::getStorage(); -$cookieStorage->set("key", "val"); -``` - -## Java - -### 如何定制 Java 的堆内存大小? - -云引擎运行 Java 应用时,会自动将 `-Xmx` 参数设置为实例规格的 70%,剩下的 30% 留给堆外内存和其他开销。如果你的应用比较特殊(比如大量使用堆外内存)可以自己定制 `-Xmx` 参数。假设使用 2 GB 内存规格的实例运行,则可以在云引擎的设置页面增加「自定义环境变量」,名称为 `JAVA_OPTS`,值为 `-Xmx1500m`,这样会限制 JVM 堆最大为 1.5 GB,剩下 500 MB 留给持久代、堆外内存或者其他一些杂项使用。**注意:`-Xmx` 参数如果设置得过小可能会导致大量 CPU 消耗在反复的 GC 任务上。** - -### 如何脱离命令行工具本地启动云引擎 Java 项目? - -设置云引擎运行需要的环境变量后,可以通过脱离命令行工具,直接运行相应命令或使用 IDE 本地启动 Java 项目。 - -通过命令行启动 Jetty 项目或 JAR 项目,先设置环境变量: - -```sh -eval "$(lean env)" -``` - -提示:命令 `lean env` 可以输出当前应用所需环境变量的设置语句,外层的 `eval` 是直接执行这些语句。 -Windows 系统下需要手动设置 `lean env` 输出的环境变量。 - -如果是 Jetty 项目,运行: - -``` -mvn jetty:run -``` - -如果是 JAR 项目,使用 Maven 打包项目并运行: - -```sh -mvn package -java -jar target/{zipped jar file} -``` - -使用 Eclipse 启动应用: - -首先确保 Eclipse 已经安装 Maven 插件,并将项目以 **Maven Project** 方式导入 Eclipse 中。 - -在 **Package Explorer** 视图右键点击项目: - -- 如果是 Jetty 项目,选择 **Run As** > **Maven build…**,将 **Main** 标签页的 **Goals** 设置为 `jetty:run`。 -- 如果是 JAR 项目,选择 **Run As** > **Run Configurations…**,选择 `Application`,设置 `Main class:`(示例项目为 `cn.leancloud.demo.todo.Application`)。 - -最后在 **Environment** 标签页增加以下环境变量和相应的值: - -名称 | 值 ---- | --- -`LEANCLOUD_APP_ENV` | `development` -`LEANCLOUD_APP_ID` | `{{appid}}` -`LEANCLOUD_APP_KEY` | `{{appkey}}` -`LEANCLOUD_APP_MASTER_KEY` | `{{masterkey}}` -`LEANCLOUD_APP_PORT` | `3000` - -配置完成后,以后只需点击 run 按钮即可启动应用。 - -### 自行接入 Java 框架时如何使用云服务的数据存储功能? - -模板项目已经集成了 [Java Unified SDK](https://github.com/leancloud/java-unified-sdk) 的 [engine-core](https://github.com/leancloud/java-unified-sdk/tree/master/leanengine) 模块,engine-core 又依赖于存储核心模块 storage-core,因此开发者可以直接使用云服务的数据存储功能。 -模板项目也包含了 SDK 初始化的逻辑。 - -如果自行接入其他框架,则需要在 `pom.xml` 中增加依赖配置来增加 LeanEngine Java SDK 的依赖: - -```xml - - - cn.leancloud - engine-core - 7.2.6 - - -``` - -同时也需要自行初始化 SDK(注意我们在云引擎中开启了 masterKey 权限,这将会跳过 ACL 和其他权限限制)。 - -```java -import cn.leancloud.LCCloud; -import cn.leancloud.LCObject; -import cn.leancloud.core.GeneralRequestSignature; -import cn.leancloud.LeanEngine; - - -String appId = System.getenv("LEANCLOUD_APP_ID"); -String appKey = System.getenv("LEANCLOUD_APP_KEY"); -String appMasterKey = System.getenv("LEANCLOUD_APP_MASTER_KEY"); -String hookKey = System.getenv("LEANCLOUD_APP_HOOK_KEY"); - -LeanEngine.initialize(appId, appKey, appMasterKey); - -GeneralRequestSignature.setMasterKey(appMasterKey); -``` - -### 如何在云引擎使用 PHP SDK 提供的 CookieStorage 模块? - -云引擎提供了一个 `LeanCloud\Storage\CookieStorage` 模块,用 Cookie 来维护用户(`User`)的登录状态,要使用它可以在 `app.php` 中添加下列代码: - -```php -use \LeanCloud\Storage\CookieStorage; -Client::setStorage(new CookieStorage(60 * 60 * 24, "/")); -``` - -`CookieStorage` 支持传入秒作为过期时间,以及路径作为 cookie 的作用域。默认过期时间为 7 天。 - -可以通过 `User::getCurrentUser()` 来获取当前登录用户。你可以这样简单地实现一个具有登录功能的站点: - -```php -$app->get('/login', function($req, $res) { - // login page -}); - -$app->post('/login', function($req, $res) { - $params = $req->getQueryParams(); - try { - User::logIn($params["username"], $params["password"]); - return $res->withRedirect('/profile'); - } catch (Exception $ex) { - return $res->withRedirect('/login'); - } -}); - -$app->get('/profile', function($req, $res) { - $user = User::getCurrentUser(); - if ($user) { - return $res->getBody()->write($user->getUsername()); - } else { - return $res->withRedirect('/login'); - } -}); - -$app->get('/logout', function($req, $res) { - User::logOut(); - return $res->redirect("/"); -}); -``` - -一个简单的登录页面可以是这样: - -```html - - - -
    - - - - - -
    - - -``` - -`CookieStorage` 也支持保存其他属性: - -```php -$cookieStorage = Client::getStorage(); -$cookieStorage->set("key", "val"); -``` - -### 在云引擎 Java 环境下如何本地调用云函数? - -Java SDK 不支持本地调用云函数。 -如有代码复用需求,建议将公共逻辑提取成普通函数(Java 方法),在多个云函数中调用。 - -## .NET - -### 自行接入 .NET 框架时如何使用云服务的数据存储功能? - -模板项目已经集成了 .NET SDK,并且包含 SDK 初始化的逻辑。 - -如果自行接入其他框架,则需要自己添加依赖: - -```sh -dotnet add package LeanCloud.Storage -``` - -同时也需要自行初始化 SDK: - -```cs -LCEngine.Initialize(services); -``` - -### 在云引擎 .NET 环境下如何本地调用云函数? - -.NET SDK 不支持本地调用云函数。 -如有代码复用需求,建议将公共逻辑提取成普通函数,在多个云函数中调用。 - -## Go - -### 如何接入 Go 框架? - -细心的开发者已经发现示例项目是一个基于 [echo](https://github.com/labstack/echo) 的 Web 应用。 - -Go SDK 以标准库 HTTP 方法的形式提供了可供任意框架接入的接口,以 **echo** 为示例: - -```go -// ./adapters/echo.go -//... -func Echo(e *echo.Echo) { - e.Any("/1/*", echo.WrapHandler(leancloud.Handler(nil)), setResponseContentType) - e.Any("/1.1/*", echo.WrapHandler(leancloud.Handler(nil)), setResponseContentType) - e.Any("/__engine/*", echo.WrapHandler(leancloud.Handler(nil)), setResponseContentType) -} - -func setResponseContentType(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - c.Response().Header().Set("Content-Type", "application/json; charset=UTF-8") - return next(c) - } -} -``` - -函数 **Echo** 接收 echo 实例对象,将 Go SDK 中提供 LeanEngine 相关功能的接口绑定到 `/1/` `/1.1/` 和 `/__engine/` 开头的路由前缀上,保证 LeanEngine 相关的底层功能正常。 - -函数 `setResponseContentType` 设置所有和 LeanEngine 相关的请求的 `Content-Type` 为 `application/json`,并且编码为 `UTF-8`。 - -大多数 Go Web 框架均提供将标准库 HTTP Handler 转换为特有 Handler 的方法,只要保证能够在其他框架中接入以上两个部件,即可将 LeanEngine 集成入你喜爱的 Go Web 框架中。 - -### 自行接入 Go 框架时如何使用云服务的 - -模板项目已经集成了 Go SDK,并且包含 SDK 初始化的逻辑。 - -如果自行接入其他框架,则需要自己添加依赖: - -```go -import "github.com/leancloud/go-sdk/leancloud" -``` - -同时也需要自行初始化 SDK: - -```go -client := leancloud.NewEnvClient() -``` - -### 在云引擎 Go 环境下如何本地调用云函数? - -云引擎下默认会在本地调用: - -```go -averageStars, err := leancloud.Run("averageStars", Review{Movie: "夏洛特烦恼"}) -if err != nil { - panic(err) -} -``` - -如果你希望发起 HTTP 请求来调用云函数,可以传入 `WithRemote()` 参考。 -`Run` 的可选参数如下: - -- `WithRemote()` 强制云函数远程执行 -- `WithSessionToken(token)` 为当前的调用请求传入 `sessionToken` -- `WithUser(user)` 为当前的调用请求传入对应的用户对象 - - -## 计费 - -### 如果更改了实例规格或数量,当天的云引擎费用如何收取? - -云引擎资源使用量按 **当天最大的实例数量** 计算,次日凌晨从账户余额中扣费,假设某天从 0 点至 24 点之间: - -* 应用本来有 4 个 standard-512 的实例; -* 发现资源数量不足,将实例规格调整到了 standard-1024; -* 发现资源过多,减少到 2 个实例。 - -则当天费用按照 standard-1024 的价格乘上 4 个实例计算。 - -### 云引擎如何收费? - -云引擎中如果有云服务的存储等 API 调用,按 API 收费策略照常收费。 -云引擎标准实例也会产生使用费,具体请参考[云引擎运行方案](/v2/sdk/engine/guide/plan)。 - -### Hook 函数算 API 请求次数吗,afterUpdate 执行一次算 1 次请求次数吗? - -AfterUpdate 是在云引擎内执行的,执行 afterUpdate 不算 API 请求,自然也不计入 API 请求数。如果 afterUpdate 里发起了 API 请求,那么照常计算 API 请求数(和客户端请求 API 一样)。 - -### 组管理功能收费吗? - -组管理功能免费使用,但组下面创建的实例按照实例价格收取费用。 - -## 国际版 - -### 国际版云引擎必须绑定自定义域名吗? - -国际版可以在「控制台 > 云引擎 > 设置」配置 `avosapps.us` 子域名,也可以绑定云引擎自定义域名(不要求备案)。 - -### 国际版云引擎可以绑定裸域名吗? - -如果希望在国际版云引擎绑定裸域名,我们建议选择支持 ANAME 或 CNAME Flattening 记录的域名服务商。 diff --git a/versioned_docs/version-v2/sdk/09-engine/02-guide/_category_.json b/versioned_docs/version-v2/sdk/09-engine/02-guide/_category_.json deleted file mode 100644 index 586e68f44..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/02-guide/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/09-engine/_category_.json b/versioned_docs/version-v2/sdk/09-engine/_category_.json deleted file mode 100644 index e255a2e23..000000000 --- a/versioned_docs/version-v2/sdk/09-engine/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "云引擎", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/10-im/01-features.mdx b/versioned_docs/version-v2/sdk/10-im/01-features.mdx deleted file mode 100644 index 63ff636fb..000000000 --- a/versioned_docs/version-v2/sdk/10-im/01-features.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -id: features -title: 即时通讯功能介绍 -sidebar_label: 功能介绍 ---- - -即时通讯主要解决产品内即时通信、实时数据同步等需求,现在已经被广泛使用在游戏内玩家社交、沟通协同、客服、超大型赛事和电视直播,以及游戏状态同步等多种业务场景之中。 - - -## 功能特色 -#### 1、支持为现有游戏快速加入多种通讯能力 -针对稳定运行的游戏,可以无缝集成即时通讯功能。即时通讯服务也可以在应用账户系统独立的情况下,快速接入并稳定安全地运行。同时,我们支持了多种典型通讯场景,提供了丰富的 UI 库和脚手架来帮助开发者快速接入。并且考虑到业务运行环境,我们提供了全平台支持的 SDK。 - - - -#### 2、强大的自定义机制满足业务各种扩展需求 -系统默认支持了文本、图片、音视频、地理位置、二进制等多种类型的消息收发,同时也允许开发者来扩展自己的消息类型和 UI 样式。并且在基本功能之外,我们也支持更多定制化需求,例如消息撤回与修改、@ 成员提醒、暂态消息、「已读」回执、离线推送、敏感内容过滤等等与消息收发相关的功能,或者超大规模用户参与的「开放聊天室」,以及类似于微信公众号的「系统对话」能力。 - -#### 3、具备严格的安全和权限控制机制 -客户端与云端使用 WebSocket 全双工通讯,全程 TLS 加密传输。在用户登录和操作权限的控制上,我们专门设计了第三方操作签名的机制,让应用层在快速接入的同时,也可以实时、完整地控制用户在即时通讯系统内的所有活动。在群聊和开放聊天室等业务场景里,我们也提供了成员角色管理和黑名单的机制,以满足产品运营管理的多样需求。 - - - -#### 4、专业技术支持服务,减少开发者生产运维成本 -我们的资深工程师, 7 × 24 小时对接,提供专业的技术支持服务,帮助开发者快速、有效地完成产品集成,缩短开发周期。此外在产品运营阶段,开发者可以彻底摆脱后端系统日常的运维细节和突发的软硬件故障处理,同时也不用担心因用户量和流量的变化,而产生的系统不稳定的情况。 - - - -## 功能说明 - - -#### 1、基本聊天功能 -支持多种聊天场景。除了普通的单聊、群聊之外,我们还提供不限人数的「开放聊天室」,适合活动直播、公开课、游戏中的世界聊天等海量用户在一个群里互动的场合,也提供了可用来实现应用内的公众号、服务号的「系统对话」,还有专为客服系统准备的「临时对话」。通过提炼不同场景的共通需求,我们提供了功能各异但接口一致的解决方案。 -用户之间可以发送多种多样的消息,如文本、图片、语音、音视频、地理位置等,也可以发送 二进制消息,以及更多的应用层自定义消息。 -聊天消息自动保存在云端,支持各种复杂的查找和翻页方式。 - - -#### 2、特殊的消息收发需求 -带有提醒功能的 @ 消息(如微信里面的 @ 某人) -消息的 撤回和修改 -消息送达和对方已读的 回执通知 -群聊里面为了避免过于干扰,允许用户开启 消息免打扰 开关 -发送譬如聊天过程中「某某正在输入…」这样的 状态信息 -在消息接收方离线时,自动转为 推送通知(Push Notification) -对于大型的开放聊天室,为了防止重要消息被淹没或丢弃,支持不同 优先级 的发送选项,确保重要的消息能优先、迅速送达客户端 - - -#### 3、多端登录与消息同步 -针对一个用户使用多个设备登录的情况,我们既支持单个账号在多个设备同时登录、即时通讯同步到所有设备,也支持 单点登录,可由业务层自主选择。 移动设备网络不稳定也是常态,聊天过程中用户难免会偶尔掉线,我们的 消息同步机制 可以确保用户消息及时得到同步,重要消息从不丢失。 - - -#### 4、管理与运营支持能力 -对于大的群组和开放聊天室,我们提供了管理员、普通成员的 权限管理功能,也支持邀请、踢人、黑名单和禁言 等众多运营管理需求。 对于聊天消息,我们默认提供了 实时过滤敏感内容 的能力,允许各个产品设置自己的敏感词列表,并且也支持开发者实现自己的敏感词过滤插件,来确保产品运营层面合规合法。 - -#### 5、安全控制能力 -任何终端用户要开启即时通讯服务,只需要提供一个唯一标识自己的 clientId 即可,这种与产品自有账户系统解耦合的方式,带来了集成的便利,也可以促使通讯服务商专注做好底层的「信使」角色。 同时我们也提供 第三方鉴权 的机制,通过在聊天流程中加入开发者服务器签名授权这一环节,来确保通讯操作的安全。 而且,即时通讯 SDK 与云端是 WebSocket 全双工通讯,且全程使用 TLS 安全加密。 - -#### 6、强大的业务扩展能力 -对于很多典型的需求,我们提供了默认的实现,而为了支持业务的多样性和特殊性,我们也提供了丰富的扩展机制: - -- 为了和产品自有用户系统进行对接,我们提供了第三方操作鉴权的扩展接口,确保在用户登录、创建/加入/退出对话群组、以及拉取聊天记录时,所有操作都得到了授权。 -- 同时我们还支持开发者对消息传递的过程进行 hook 处理,在消息到达云端但是还没有投递之前和投递之后,分别完成自定义的处理逻辑,例如过滤掉竞品的品牌,以及自定义离线推送消息,等等。 -- 我们也支持通过简单的 web hook 来完成云端和应用后端的消息同步。 -- 在提供移动端的 SDK 之外,我们还提供了 REST API,以帮助产品在可信环境下更好地实现业务处理。 \ No newline at end of file diff --git a/versioned_docs/version-v2/sdk/10-im/02-guide/00-overview.mdx b/versioned_docs/version-v2/sdk/10-im/02-guide/00-overview.mdx deleted file mode 100644 index dd7d2c0bf..000000000 --- a/versioned_docs/version-v2/sdk/10-im/02-guide/00-overview.mdx +++ /dev/null @@ -1,470 +0,0 @@ ---- -id: overview -title: 即时通讯总览 -sidebar_label: 总览 ---- - - - -即时通讯是主要解决产品内即时通信(Instant Messaging)、实时数据同步等需求,其设计上的主要目标是: - -* **支持为现有应用快速加入多种通讯能力** - - 我们很多客户的产品都已经达到一个比较稳定的形态,即时通讯只是其中一个锦上添花的功能,所以如何和现有系统无缝集成,是我们设计上一个重要的出发点。即时通讯服务可以在应用账户系统独立的情况下,快速接入并稳定安全地运行。 - - 我们支持了多种典型通讯场景,提供了丰富的 UI 库和脚手架来帮助开发者快速接入。并且考虑到业务运行环境,我们提供了全平台支持的 SDK。 - -* **强大的自定义机制满足业务各种扩展需求** - - 我们默认支持了文本、图片、音视频、地理位置、二进制等多种类型的消息收发,同时也允许产品开发者来扩展自己的消息类型和 UI 样式。并且在基本功能之外,我们也支持更多高阶需求,例如消息撤回与修改、@ 成员提醒、暂态消息、「已读」回执、离线推送、敏感内容过滤等等与消息收发相关的功能,或者超大规模用户参与的「开放聊天室」,以及类似于微信公众号的「系统对话」,在这里都可以得到满足。 - -* **安全和权限控制** - - 我们始终把系统安全性放在首要位置,客户端与云端使用 WebSocket 全双工通讯,全程 TLS 加密传输。在用户登录和操作权限的控制上,我们专门设计了第三方操作签名的机制,让应用层在快速接入的同时,也可以实时、完整地控制用户在即时通讯系统内的所有活动。在群聊和开放聊天室等业务场景里,我们也提供了成员角色管理和黑名单的机制,以满足产品运营管理的多样需求。 - -* **最大限度降低客户的生产运维成本** - - 我们提供专业的技术支持服务,富有经验的资深工程师 7 × 24 小时对接,以帮助开发者快速、有效地完成产品集成,缩短开发周期。此外在产品运营阶段,让开发者彻底摆脱后端系统日常的运维细节和突发的软硬件故障处理,也不用关心用户量和流量的变化。帮助客户在享受高品质技术服务的同时,也可以最大限度降低生产运维成本,并且以更快的速度推进产品迭代,是我们始终追求的目标。 - -即时通讯服务现在已经被广泛使用在应用内社交、工作协同、客服系统、超大型赛事和电视直播、以及游戏状态同步等多种业务场景之中。 - -## 功能和特性 - -即时通讯服务提供的主要功能有: - -* **基本聊天功能**,包括: - - - 支持多种聊天场景。除了普通的单聊、群聊之外,我们还提供不限人数的「**开放聊天室**」,适合活动直播、公开课、游戏中的世界聊天等海量用户在一个群里互动的场合,也提供了可用来实现应用内的公众号、服务号的「**系统对话**」,还有专为客服系统准备的「**临时对话**」。通过提炼不同场景的共通需求,我们提供了功能各异但接口一致的解决方案。 - - 用户之间可以发送多种多样的消息,如文本、图片、语音、音视频、地理位置等,也可以发送 **二进制消息**,以及更多的应用层自定义消息。 - - 聊天消息自动保存在云端,支持各种复杂的查找和翻页方式。 - -* **特殊的消息收发需求**。除了普通的消息收发之外,我们还支持: - - - 带有提醒功能的 **@ 消息**(如微信里面的 @ 某人) - - 消息的 **撤回和修改** - - 消息送达和对方已读的 **回执通知** - - 群聊里面为了避免过于干扰,允许用户开启 **消息免打扰** 开关 - - 发送譬如聊天过程中「某某正在输入…」这样的 **状态信息** - - 在消息接收方离线时,自动转为 **推送通知**(Push Notification) - - 对于大型的开放聊天室,为了防止重要消息被淹没或丢弃,支持不同 **优先级** 的发送选项,确保重要的消息能优先、迅速送达客户端 - -* **多端登录与消息同步** - - 现在一个用户使用多个设备登录已经是比较常见的需求,我们既支持单个账号在多个设备同时登录、即时通讯同步到所有设备,也支持 **单点登录**,可由业务层自主选择。 - 移动设备网络不稳定也是常态,聊天过程中用户难免会偶尔掉线,我们的 **消息同步机制** 可以确保用户消息及时得到同步,重要消息从不丢失。 - -* **管理与运营支持** - - 对于大的群组和开放聊天室,我们提供了管理员、普通成员的 **权限管理功能**,也支持邀请、踢人、**黑名单和禁言** 等众多运营管理需求。 - 对于聊天消息,我们默认提供了 **实时过滤敏感内容** 的能力,允许各个产品设置自己的敏感词列表,并且也支持开发者实现自己的敏感词过滤插件,来确保产品运营层面合规合法。 - -* **安全控制** - - 任何终端用户要开启即时通讯服务,只需要提供一个唯一标识自己的 `clientId` 即可,这种与产品自有账户系统解耦合的方式,带来了集成的便利,也可以促使通讯服务商专注做好底层的「信使」角色。 - 同时我们也提供 **第三方鉴权** 的机制,通过在聊天流程中加入开发者服务器签名授权这一环节,来确保通讯操作的安全。 - 而且,即时通讯 SDK 与云端是 WebSocket 全双工通讯,且全程使用 TLS 安全加密。 - -* **强大的业务扩展能力** - - 对于很多典型的需求,我们提供了默认的实现,而为了支持业务的多样性和特殊性,我们也提供了丰富的扩展机制: - - - 为了和产品自有用户系统进行对接,我们提供了第三方操作鉴权的扩展接口,确保在用户登录、创建/加入/退出对话群组、以及拉取聊天记录时,所有操作都得到了授权。 - - 同时我们还支持开发者对消息传递的过程进行 **hook 处理**,在消息到达云端但是还没有投递之前和投递之后,分别完成自定义的处理逻辑,例如过滤掉竞品的品牌,以及自定义离线推送消息,等等。 - - 我们也支持通过简单的 **web hook** 来完成云端和应用后端的消息同步。 - - 在提供移动端的 SDK 之外,我们还提供了 REST API,以帮助产品在可信环境下更好地实现业务处理。 - - 我们相信灵活性和扩展性也是云服务的核心竞争力。 - -## 全平台 SDK 和 Demo 支持 - -目前,我们提供了主流平台的客户端 SDK,而且它们源代码都是公开的,开发者可以在我们的 [GitHub 账户](https://github.com/leancloud) 下自由下载,可以与我们工程师同步讨论遇到的问题和需求。 - -在 SDK 之外,我们也公开了一些 Demo 项目来帮助开发者快速熟悉我们的产品,详见页眉导航栏的 Demos 链接。 -## 核心概念说明 - -在深入了解之前,我们先跟大家解释几个核心概念,这些概念在 API 或者后面的开发指南中都会出现,了解它们会让后续的文档阅读变得简单和轻松很多。 - -### `clientId`、用户和登录 - -即时通讯服务中的每一个终端称为一个「Client」。Client 拥有一个在应用内唯一标识自己的 ID(`clientId`)。这个 ID 由应用自己定义,必须是 **只由英文字母、数字、半角下划线与半角短横线组成,不以数字开头,不超过 64 个字符的字符串**。在大部分场合,Client 都可以对应到应用中的某个「用户」,但是并不是只有真的用户才能作为「Client」,你完全可以把一个探测器当成一个「Client」,把它收集到的数据通过即时通讯服务广播给更多「人」。 - -要使用即时通讯服务,每一个终端设备需要首先建立与即时通讯云端的 WebSocket 长连接,并使用唯一的 `clientId` 来加入即时通讯服务,我们把这一过程称为「登录」。请注意这里的登录仅仅指客户端登录即时通讯服务,与应用层面的用户账户注册登录是不一样的。 - -默认情况下,即时通讯服务允许一个 `clientId` 在多个不同的设备上登录,也允许一个设备上有多个 `clientId` 同时登录。如果使用场景中需要限制用户只在一处登录,可以在登录时明确设置当前设备的 tag,当云端检测到同一个 tag 的设备出现冲突时,会自动踢出已存在设备上的登录状态。开发者可以根据自己的应用场景选择合适的登录方式。 - -客户端通过即时通讯 SDK 完成登录后,开发者就不必再关心底层的网络连接状态,SDK 会自动为开发者保持连接状态,并在网络状态变化之后进行自动重连。当应用在前台时 SDK 会保持连接,而当应用退到后台时,连接自动断开我们默认会激活平台原生的推送服务,来保证消息及时送达。 - -### 对话(Conversation) - -用户登录之后,与其他人进行消息沟通,即为开启了一个「对话(`Conversation`)」。在即时通讯服务中,「对话」包含了沟通的用户群体(成员),也是所有消息依托的媒介:消息都是由某一个 Client 发往一个「对话」。终端用户在开始聊天之前,需要先创建或者加入一个对话,然后再邀请其他人进来(可选),之后所有参与者在这个对话内进行交流。 - -用户每创建一个对话,就会在云端的 `_Conversation` 表中增加一条记录,可以进入 **云服务控制台 > 存储 > 结构化数据** 来查看该数据。一个「对话」在创建之后,我们还可以给他指定一些应用层的属性,例如名字、成员、以及自定义的扩展属性,等等。对话的各个属性与 `_Conversation` 表中字段的对应关系为: - -属性名 | 表字段 | 类型 | 约束 | 说明 ----|---|---|---|--- -`name`|`name`|`String`| 可选 | 对话的名字,可为群组命名。 -`attributes`|`attr`|`Object`| 可选 | 自定义属性,供开发者扩展使用。 -`conversationId`|`objectId`|`String`|| 对话 ID(只读),由云端为该对话生成的一个全局唯一的 ID。 -`creator`|`c`|`String`|| 对话创建者的 `clientId`(只读)。 -`members`|`m`|`Array`|| 普通对话的所有参与者(仅针对普通对话,聊天室和系统对话并不支持持久化的成员列表,具体的开发指南中会有详细解释)。 -`mute`|`mu`|`Array`|| 将对话设为静音的参与者,这部分参与者不会收到推送(仅对 iOS 以及开启了混合推送的 Android 用户有效,具体的开发指南中会有详细解释)。 -`lastMessageAt`|`lm`|`Date`|| 对话中最后一条消息的发送或接收时间。 -`transient`|`tr`|`Boolean`| 可选 | 对话类型标志,是否为聊天室,后面会说明。 -`system`|`sys`|`Boolean`| 可选 | 对话类型标志,是否是系统对话,后面会说明。 -`unique`|`unique`|`Boolean`| 可选 | 内部字段,对话类型标志,标记根据成员原子创建的对话,后面会说明。 - -虽然我们统一使用 `_Conversation` 表来存储所有对话,但是根据业务场景的不同我们 SDK 里面提供了多种不同的对话类型可供选择。 - -#### 业务场景的需求 - -在解释对话类型之前,我们先列举一下即时通讯可能的使用场景。 - -* **单聊/私聊** - - 就是两个 Client 之间的对话,公开与否(能否让其他人看到这个对话存在)由应用层自己控制。通常的业务场景里它是私密的,并且加入新的成员之后,会切换成新的群聊(当然,也可以依然不离开当前对话,这一点还是由应用层来决定)。 - -* **群聊** - - 就是两个(含)以上 Client 之间的对话,通常可以添加和删除成员,并且会赋予群聊一个名字,例如「家人群」、「朋友群」、「部门同事群」等等。随着成员的减少,群聊也可能只有两个甚至一个成员(成员的多少并不是区分群聊和单聊的关键)。群聊能否公开(譬如支持名字搜索),由应用自己决定。 - -* **聊天室** - - 游戏中的世界聊天,直播产品使用的开放聊天室、弹幕、网页直播等都可以抽象成开放「聊天室」,它与群聊类似,都是多人参与的群组,但是也有一些区别:其一在于聊天室人数可能远大于群聊人数;其二在于聊天室强调的是在线人数,所有参与者进入聊天界面就算加入,关闭界面就算退出,所以聊天室不需要离线消息和推送通知,在线成员数比具体成员列表更有意义。 - -* **公众号、机器人** - - 很多产品都会加入类似微信的公众号、服务号的功能,让一些特殊的账号可以给订阅用户发全员广播,也可以和特定用户进行一对一的信息交流。也有一些业务需要实现一个智能机器人的功能,可以自动与其他用户进行交流,解决客服或者问答类的需求。 - -* **临时客服通道** - - 有一些客服系统,会为每一个反馈问题的终端用户建立一个与在席客服的临时沟通渠道,在通道内用户和客服人员如单聊一般进行沟通,随着问题的解决自动关闭该通道。 - -即时通讯系统设计了四种类型的「对话」来满足不同的需求,下面我们看看如何把上面的业务场景映射到具体的「对话」类型。 - -#### 普通对话(Conversation) - -这是我们使用最多的「对话」,一般的单聊和群聊都可以通过它来实现。普通对话支持的功能有: - -* 成员之间发送和接收消息 -* 允许增加、删除成员(最大成员数不超过 500),且会全局通知成员变动事件 -* 支持查询成员在线状态 -* 支持更多的消息收发选项,例如 @ 成员提醒消息、撤回和修改消息、暂态消息、消息送达和已读的回执通知、Will 消息、离线推送通知等等 -* 部分成员离线状态下,可以收到消息推送通知,并且上线之后会进行消息同步,确保不丢消息 -* 消息记录自动保存在云端,支持多种查询方式 - -建议:开发者将单聊/群聊、私密/公开等属性存入到 `Conversation.attributes` 之中,在应用层进行区分。 -#### 聊天室(Chat Room) - -专门用来处理不限人数的「聊天室」的这种需求的「对话」(在老版本 SDK 中也叫 Transient Conversation,在 `_Conversation` 表中,以 `tr` 为 `true` 来标记)。与普通对话一样,它支持创建、自身主动加入、自身主动退出对话等操作,消息记录会被保存并可供获取,但其不同之处在于: - -* **不限成员上限**,没有固定成员概念,加入即为成员,断线即为退出(`m` 列将被忽略) -* 不支持查询成员列表,你可以通过相关 API **查询在线人数** -* 不支持离线消息、离线推送通知、消息回执等功能 -* 没有成员加入、离开的通知 -* 不支持邀请加入、踢出成员这两个操作 -* 一个用户一次登录只能加入一个聊天室,加入新的聊天室后会自动离开旧的聊天室 -* 加入之后如果半小时内断网重连会自动加入原聊天室,超过这个时间则需要重新加入 - -建议:虽然「聊天室」不限制成员数量,但从实际经验来看,如果人数过多,那么聊天室内消息被放大的效果会非常明显,对于终端用户而言即表现为过量消息不断刷屏,反而影响用户体验。我们建议每个聊天室的上限人数控制在 **5000** 人左右。开发者可以考虑从应用层面将大聊天室拆分成多个较小的聊天室。 - -#### 系统对话(System Conversation) - -这是用于实现智能机器人、公众号、服务账号等场景的「对话」,也可以用作发送应用内通知的通道(在 `_Conversation` 表中,以 `sys` 为 `true` 来标记)。这种对话具有以下特点: - -* 加入即订阅,离开即退订,订阅人数没有限制(`m` 列将被忽略) -* 系统对话的创建必须由服务端发起,在客户端仅允许订阅/取消订阅一个已经存在的系统对话 -* 可以通过系统对话给所有订阅者发送全局消息,也可以单独某一个或者某几个用户发定向消息 -* 用户给系统对话发送的上行消息是单向的,消息和相关信息会存储在数据存储中的 `_SysMessage` 表,并不会被其他订阅用户收到 -* 开发者可以配置 Hook 地址接收用户发给系统对话的消息,并利用 REST API 发消息回复 - -#### 临时对话(Temporary Conversation) - -临时对话的数据不会被保存到 `_Conversation` 表中,它解决的是一种特殊的聊天场景: - -- 对话存续时间短 -- 聊天参与的人数较少(最多为 10 个 Client) -- 聊天记录的存储不是强需求 - -这种对话场景,诸如电商售前和售后在线聊天的客服系统,我们推荐使用临时对话。与普通对话相比,它具有如下特点: - -* 不支持消息静音/取消静音的操作 -* 无法更新对话属性 -* 其他消息收发与成员查询操作,和普通对话完全一样 - -建议:临时对话在使用上与普通对话类似,其最大特点是较短的有效期(不会被保存到 `_Conversation` 表中),这带来的优势是可以 **减轻对话的持久化存储在服务端占用的存储资源规模**,从而 **降低开发者的使用成本**。 - -#### 不同类型的对比总结 - -对话类型 | 使用场景 | 成员管理 | 收发消息 | 消息记录 ---- | --- | --- | --- | --- -**普通对话** | 单聊、群聊 | 成员持久化保存,最高支持 500 个成员 | 只有成员可以收发消息 | 支持 -**聊天室** | 聊天室、弹幕、网页实时评论 | 没有持久化的成员数据,自主加入,不支持邀请,成员数量没有限制 | 所有用户都可以发消息,当前在线的成员可以收到消息 | 支持 -**系统对话** | 公众号、机器人、下发加好友通知、自定义消息 | 没有成员概念,开发者维护订阅关系,订阅人数没有上限 | 开发者通过 API 给特定用户发消息,支持业务方配置 Web Hook 来备份处理消息 | 支持 -**临时对话** | 临时客服通道 | 成员固定,无法增加/删除成员 | 只有成员可以收发消息 | 不支持 - -### 消息(Message) - -即时通讯服务中单次交互的数据单元。用户可以一次传输不超过 **5 KB** 的消息数据。即时通讯系统对消息格式没有任何要求,允许开发者传输任何基于文本的消息数据,开发者可以在文本协议基础上定义自己的应用层协议。 - -根据发送参数的不同,消息可分为「普通消息」和「暂态消息」。云端对于普通消息会提供接收回执、自动持久化存储、离线推送等功能。但对暂态消息,则不会被自动保存,也不支持延迟接收,离线用户更不会收到推送通知。譬如聊天过程中「某某正在输入中…」这样的状态信息,就适合按照暂态消息来发送,而用户输入的正式消息,则应该用普通消息来发送。 - -我们对普通消息提供「至少一次」的到达保证,并且在官方 SDK 中支持对消息的去重,开发者无需关心。开发者可以通过 SDK 或 REST API 发送消息。SDK 通常用于最终用户发送消息,而 REST API 是开发者从云端发送消息的接口。当从 REST API 发送消息时,开发者可以指定消息的发送者、对话 ID,对于系统对话还可以指定消息的接收者。 - -#### 富媒体消息 - -为了方便开发者的使用,我们提供了几种封装好的基于 JSON 格式的富媒体消息类型(`TypedMessage`),譬如: - -- 文本(`TextMessage`) -- 图片(`ImageMessage`) -- 音频(`AudioMessage`) -- 视频(`VideoMessage`) -- 位置(`LocationMessage`) - -这些消息类型的层次关系为: - -![TypedMessage 继承自 Message。TextMessage、ImageMessage、AudioMessage、VideoMessage、LocationMessage 和其他消息类型继承自 TypedMessage。](/img/realtime_v2_message_types.svg) - -如上所述,富媒体消息基于 JSON 格式,通过 REST API 发送时需要序列化为包含以下属性的 JSON 字符串(使用客户端 SDK 发送消息时,SDK 会自动完成相应的转换): - -属性 | 约束 | 说明 ---- |---|--- -`_lctype` | | 富媒体消息的类型
    消息类型
    文本消息-1
    图像消息-2
    音频消息-3
    视频消息-4
    位置消息-5
    文件消息-6
    以上类型均使用负数,所有正数留给自定义扩展类型使用,0 作为「没有类型」被保留起来。 -`_lctext` | | 富媒体消息的文字说明 -`_lcattrs` | |JSON 字符串,用来给开发者存储自定义属性。 -`_lcfile` | | 如果是包含了文件(图像、音频、视频、通用文件)的消息 ,`_lcfile` 就包含了它的文件实体的相关信息。 -`url` | | 文件在上传之后的物理地址(注意,绑定或换绑自定义域名后,历史消息中的 url 不会更新) -`objId` | 可选 | 文件对应的在 `_File` 表里面的 objectId -`metaData` | 可选 | 文件的元数据 - -以上为所有类型的富媒体消息共有的属性。 - -开发者可以基于我们的框架,方便地扩展出自己的消息类型。 - -下面给出内置富媒体消息类型序列化为 JSON 的例子。 - -##### 文本消息 - -```json -{ - "_lctype": -1, - "_lctext": "这是一个纯文本消息", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - } -} -``` - -##### 图像消息 - -```json -{ - "_lctype": -2, // 必要参数 - "_lctext": "图像的文字说明", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对", - "b": true, - "c": 12 - }, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", // 必要参数 - "objId": "54699d87e4b0a56c64f470a4", // 文件对应的 LCFile.objectId - "metaData": { - "name": "IMG_20141223.jpeg", // 图像的名称 - "format": "png", // 图像的格式 - "height": 768, // 单位:像素 - "width": 1024, // 单位:像素 - "size": 18 // 单位:b - } - } -} -``` - -上面是完整的例子,如果只想简单的发送图像 URL: - -```json -{ - "_lctype": -2, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25" - } -} -``` - -##### 音频消息 - -```json -{ - "_lctype": -3, - "_lctext": "这是一个音频消息", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - }, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/246b8acc-2e12-4a9d-a255-8d17a3059d25", - "objId": "54699d87e4b0a56c64f470a4", // 文件对应的 LCFile.objectId - "metaData": { - "name": "我的滑板鞋.wav", - "format": "wav", - "duration": 26, // 单位:秒 - "size": 2738 // 单位:b - } - } -} -``` - -简略版: - -```json -{ - "_lctype": -3, - "_lcfile": { - "url": "http://www.somemusic.com/x.mp3" - } -} -``` - -##### 视频消息 - -```json -{ - "_lctype": -4, - "_lctext": "这是一个视频消息", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - }, - "_lcfile": { - "url": "http://ac-p2bpmgci.clouddn.com/99de0f45-171c-4fdd-82b8-1877b29bdd12", - "objId": "54699d87e4b0a56c64f470a4", // 文件对应的 LCFile.objectId - "metaData": { - "name": "录制的视频.mov", - "format": "avi", - "duration": 168, // 单位:秒 - "size": 18689 // 单位:b - } - } -} -``` - -简略版: - -```json -{ - "_lctype": -4, - "_lcfile": { - "url": "http://www.somevideo.com/Y.flv" - } -} -``` - -##### 通用文件消息 - -```json -{ - "_lctype": -6, - "_lctext": "这是一个普通文件类型", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - }, - "_lcfile": { - "url": "http://www.somefile.com/jianli.doc", - "name": "我的简历.doc", - "size": 18689 // 单位:b - } -} -``` - -简略版: - -```json -{ - "_lctype": -6, - "_lcfile": { - "url": "http://www.somefile.com/jianli.doc", - "name": "我的简历.doc" - } -} -``` - -##### 地理位置消息 - -```json -{ - "_lctype": -5, - "_lctext": "这是一个地理位置消息", - "_lcattrs": { - "a": "_lcattrs 是用来存储用户自定义的一些键值对" - }, - "_lcloc": { - "longitude": 23.2, - "latitude": 45.2 - } -} -``` - -简略版: - -```json -{ - "_lctype": -5, - "_lcloc": { - "longitude": 23.2, - "latitude": 45.2 - } -} -``` - -#### 与消息相关的其他功能需求 - -前面章节说明功能特性的时候,我们提到过,在正常收发消息之外我们还支持: - -- 带有提醒功能的 @ 消息(如微信里面的 @ 某人) -- 消息的撤回和修改 -- 消息内容的实时过滤 -- 消息送达和对方已读的回执通知 -- 群聊里面为了避免过于干扰,允许用户开启消息免打扰开关 -- 发送譬如聊天过程中「某某正在输入…」这样的状态信息 -- 在消息接收方离线时,自动转为推送通知(Push Notification) -- 对于大型的开放聊天室,为了防止重要消息被淹没或丢弃,支持不同优先级的发送选项,确保重要的消息能优先、迅速送达客户端 - -具体的使用方法可以参考文档[即时通讯开发指南第二篇](/v2/sdk/im/guide/intermediate)的《消息收发的更多方式》一节和[第三篇](/v2/sdk/im/guide/senior)的《消息的内容过滤》一节。 - -## 系统限制 - -* 对于客户端主动发起的操作会按照操作类型限制其频率。发消息操作限制为 **每分钟 60 次**,历史消息查询操作限制为 **每分钟 120 次**,其他类型操作包括加入对话、离开对话、登录服务、退出服务等均限制为 **每分钟 30 次**。当调用超过限制时,云端会拒绝响应这些超限的操作,这样如果操作本由 SDK 发起则表现为不会走回调。如果使用 REST API 发起各种操作,则不会受到上述频率的限制。 -* 应用全局服务器下发消息速度默认最高可达到每秒钟 160000 次,超过部分会被服务器丢弃。如果你的应用会超过此限制,请联系我们。 -* 客户端发送的单条消息大小不得超过 5 KB(`pushData` 等附加信息也计入消息大小)。 -* 单个普通对话的成员上限为 500 个,如果您通过数据存储 API 向 `m` 字段加入了超过 500 个 ID,我们只会使用其中的前 500 个。 -* 请不要使用相同的 ID 在大量设备上同时登录,如果系统检测到某个 ID 同时在超过 5 个不同的 IP 上登录,会认为此 ID 是重复使用的 ID,之后此 ID 当日的每次登录会按照「ID + IP」的组合作为计费的独立用户。 -* 如果单个用户有超过 50 个的对话存在未接收的离线消息,那么当该用户登录时服务端只会 **随机** 下发 50 个对话的离线消息或未读消息数量。也就是说服务端不会再下发超出对话数量限制的那部分离线消息,也不会下发离线消息数量,离线消息不会丟失但需要从历史记录中拉取得到。 -* 单个对话未接收的离线消息数最多 100 条,超过后,系统会以先入先出方式存储新的离线消息,同时移除当前对话存储的最早的一条离线消息。被移除的离线消息可以通过历史消息记录查询,但不会产生离线消息提醒,也不会计入对话的未读消息计数。 -* 切换文件访问地址(**云服务控制台 > 存储 > 文件 > 设置**)不会自动更新历史消息(包括富媒体消息)中的 URL。 -* 调用消息操作有关的 REST API 有请求频率以及总量的限制,详见[即时通讯 REST API](/v2/sdk/im/guide/rest)。 - -### 对话的有效期 - -一个对话(包括普通、暂态、系统对话)如果 **6 个月内** 没有通过 SDK 或者 REST API 发送过新的消息,或者它在 `_Conversation` 表中的任意字段没有被更新过,即被视为 **不活跃对话**,云端会自动将其删除。(查询对话的消息记录并不会更新 `_Conversation` 表,所以只查询不发送消息的对话仍会被视为不活跃对话。) - -不活跃的对话被删除后,当客户端再次通过 SDK 或 REST API 对其发送消息时,会遇到 `4401 INVALID_MESSAGING_TARGET` 错误,表示该对话已经不存在了。同时,与该对话相关的消息历史也无法获取。 - -反之,活跃的对话会一直保存在云端。 - -### 消息的有效期 - -一个对话的消息记录会在云端保留 **6 个月**,也就是说一个对话可以查询到半年之内的历史消息记录。开发者可以付费来延长这一期限,如有需要,请提交工单联系技术支持。 -你也随时可以通过 REST API 将聊天记录同步到自己的服务器上。 -## 即时通讯 Hook 机制 - -详见[即时通讯开发指南第四篇](/v2/sdk/im/guide/systemconv)的《万能的 Hook 机制》一节。 - -## 开发指南 - -按功能区分,可以参考如下文档: - -- [一,从简单的单聊、群聊、收发图文消息开始](/v2/sdk/im/guide/beginner) -- [二,消息收发的更多方式,离线推送与消息同步,多设备登录](/v2/sdk/im/guide/intermediate) -- [三,安全与签名、黑名单和权限管理、玩转直播聊天室和临时对话](/v2/sdk/im/guide/senior) -- [四,详解消息 hook 与系统对话](/v2/sdk/im/guide/systemconv) - -具体的 REST API 规范,可以参考: - -- [即时通讯 REST API 使用指南](/v2/sdk/im/guide/rest) diff --git a/versioned_docs/version-v2/sdk/10-im/02-guide/01-beginner.mdx b/versioned_docs/version-v2/sdk/10-im/02-guide/01-beginner.mdx deleted file mode 100644 index 39950609a..000000000 --- a/versioned_docs/version-v2/sdk/10-im/02-guide/01-beginner.mdx +++ /dev/null @@ -1,4669 +0,0 @@ ---- -id: beginner -title: 一,从简单的单聊、群聊、收发图文消息开始 -sidebar_label: 基础功能 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import Mermaid from '/src/docComponents/Mermaid'; - - - -## 本章导读 - -在很多产品里面,都存在让用户实时沟通的需求,例如: - -- 员工与客户之间的实时交流,如房地产行业经纪人与客户的沟通,商业产品客服与客户的沟通,等等。 -- 企业内部沟通协作,如内部的工作流系统、文档/知识库系统,增加实时互动的方式可能就会让工作效率得到极大提升。 -- 直播互动,不论是文体行业的大型电视节目中的观众互动、重大赛事直播,娱乐行业的游戏现场直播、网红直播,还是教育行业的在线课程直播、KOL 知识分享,在支持超大规模用户积极参与的同时,也需要做好内容审核管理。 -- 应用内社交,游戏公会嗨聊,等等。社交产品要能长时间吸引住用户,除了实时性之外,还需要更多的创新玩法,对于标准化通讯服务会存在更多的功能扩展需求。 - -根据功能需求的层次性和技术实现的难易程度不同,我们分为多篇文档来一步步地讲解如何利用即时通讯服务实现不同业务场景需求: - -- 本篇文档,我们会从实现简单的单聊/群聊开始,演示创建和加入「对话」、发送和接收富媒体「消息」的流程,同时让大家了解历史消息云端保存与拉取的机制,希望可以满足在成熟产品中快速集成一个简单的聊天页面的需求。 -- 第二篇文档会介绍一些特殊消息的处理,例如 @ 成员提醒、撤回和修改、消息送达和被阅读的回执通知等,离线状态下的推送通知和消息同步机制,多设备登录的支持方案,以及如何扩展自定义消息类型,希望可以满足一个社交类产品的多方面需求。 -- 第三篇文档会介绍一下系统的安全机制,包括第三方的操作签名,以及「对话」成员的权限管理和黑名单机制,同时也会介绍直播聊天室和临时对话的用法,希望可以帮助开发者提升产品的安全性和易用性,并满足特殊场景的需求。 -- 第四篇文档会介绍即时通讯服务端 Hook 机制,系统对话的用法,以及给出一个基于这两个功能打造一个属于自己的聊天机器人的方案,希望可以满足业务层多种多样的扩展需求。 - -希望开发者最终顺利完成产品开发的同时,也对即时通讯服务的体系结构有一个清晰的了解,以便于产品的长期维护和定制化扩展。 - -阅读准备 - -在阅读本章之前,如果您还不太了解即时通讯服务的总体架构,建议先阅读[即时通讯服务总览](/v2/sdk/im/guide/overview)。 -另外,如果您还没有下载对应开发环境(语言)的 SDK,请参考相应语言的 SDK 配置指南完成 SDK 安装与初始化。 - -## 一对一单聊 - -在开始讨论聊天之前,我们需要介绍一下在即时通讯 SDK 中的 `IMClient` 对象: - -> `IMClient` 对应实体的是一个用户,它代表着一个用户以客户端的身份登录到了即时通讯的系统。 - -具体可以参考[即时通讯服务总览](/v2/sdk/im/guide/overview)中《clientId、用户和登录》一节的说明。 - -### 创建 `IMClient` - -假设我们产品中有一个叫「Tom」的用户,首先我们在 SDK 中创建出一个与之对应的 `IMClient` 实例: -(创建实例前请确保已经成功初始化了 SDK) - - - -```cs -LCIMClient tom = new LCIMClient("Tom"); -``` -```java -// clientId 为 Tom -LCIMClient tom = LCIMClient.getInstance("Tom"); -``` -```objc -// 定义一个常驻内存的属性变量 -@property (nonatomic) LCIMClient *tom; -// 初始化 -NSError *error; -tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error]; -if (error) { - NSLog(@"init failed with error: %@", error); -} else { - NSLog(@"init succeeded"); -} -``` -```js -// Tom 用自己的名字作为 clientId 来登录即时通讯服务 -realtime.createIMClient('Tom').then(function(tom) { - // 成功登录 -}).catch(console.error); -``` -```swift -// 定义一个常驻内存的全局变量 -var tom: IMClient -// 初始化 -do { - tom = try IMClient(ID: "Tom") -} catch { - print(error) -} -``` -```dart -// clientId 为 Tom -Client tom = Client(id: 'Tom'); -``` - - - -注意这里一个 `IMClient` 实例就代表一个终端用户,我们需要把它全局保存起来,因为后续该用户在即时通讯上的所有操作都需要直接或者间接使用这个实例。 - -### 登录即时通讯服务器 - -创建好了「Tom」这个用户对应的 `IMClient` 实例之后,我们接下来需要让该实例「登录」即时通讯服务器。 -只有登录成功之后客户端才能开始与其他用户聊天,也才能接收到云端下发的各种事件通知。 - -这里需要说明一点,有些 SDK (比如 C# SDK) 在创建 `IMClient` 实例的同时会自动进行登录,另一些 SDK (比如 iOS 和 Android SDK)则需要调用开发者手动执行 `open` 方法进行登录: - - - -```cs -await tom.Open(); -``` -```java -// Tom 创建了一个 client,用自己的名字作为 clientId 登录 -LCIMClient tom = LCIMClient.getInstance("Tom"); -// Tom 登录 -tom.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient client, LCIMException e) { - if (e == null) { - // 成功打开连接 - } - } -}); -``` -```objc -// 定义一个常驻内存的属性变量 -@property (nonatomic) LCIMClient *tom; -// 初始化,然后登录 -NSError *error; -tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error]; -if (error) { - NSLog(@"init failed with error: %@", error); -} else { - [tom openWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // open succeeded - } - }]; -} -``` -```js -// Tom 用自己的名字作为 clientId 登录,并且获取 IMClient 对象实例 -realtime.createIMClient('Tom').then(function(tom) { - // 成功登录 -}).catch(console.error); -``` -```swift -// 定义一个常驻内存的全局变量 -var tom: IMClient -// 初始化,然后登录 -do { - tom = try IMClient(ID: "Tom") - tom.open { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -// Tom 创建了一个 client,用自己的名字作为 clientId 登录 -Client tom = Client(id: 'Tom'); -// Tom 登录 -await tom.open(); -``` - - - -### 使用 `_User` 登录 - -除了应用层指定 `clientId` 登录之外,我们也支持直接使用 `_User` 对象来创建 `IMClient` 并登录。这种方式能直接利用云端内置的用户鉴权系统而省掉登录签名操作,更方便地将存储和即时通讯这两个模块结合起来使用。示例代码如下: - - - -```cs -var user = await LCUser.Login("USER_NAME", "PASSWORD"); -var client = new LCIMClient(user); -``` -```java -// 以 LCUser 的用户名和密码登录到存储服务 -LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer() { - public void onSubscribe(Disposable disposable) {} - public void onNext(LCUser user) { - // 登录成功,与服务器连接 - LCIMClient client = LCIMClient.getInstance(user); - client.open(new LCIMClientCallback() { - @Override - public void done(final LCIMClient avimClient, LCIMException e) { - // 执行其他逻辑 - } - }); - } - public void onError(Throwable throwable) { - // 登录失败(可能是密码错误) - } - public void onComplete() {} -}); -``` -```objc -// 定义一个常驻内存的属性变量 -@property (nonatomic) LCIMClient *client; -// 登录 User,然后使用登录成功的 User 初始化 Client 并登录 -[LCUser logInWithUsernameInBackground:USER_NAME password:PASSWORD block:^(LCUser * _Nullable user, NSError * _Nullable error) { - if (user) { - NSError *err; - client = [[LCIMClient alloc] initWithUser:user error:&err]; - if (err) { - NSLog(@"init failed with error: %@", err); - } else { - [client openWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - // open succeeded - } - }]; - } - } -}]; -``` -```js -var AV = require('leancloud-storage'); -// 以 AVUser 的用户名和密码登录即时通讯服务 -AV.User.logIn('username', 'password').then(function(user) { - return realtime.createIMClient(user); -}).catch(console.error.bind(console)); -``` -```swift -// 定义一个常驻内存的全局变量 -var client: IMClient -// 登录 User,然后使用登录成功的 User 初始化 Client 并登录 -LCUser.logIn(username: USER_NAME, password: PASSWORD) { (result) in - switch result { - case .success(object: let user): - do { - client = try IMClient(user: user) - client.open { (result) in - // handle result - } - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } -} -``` -```dart -// 暂不支持 -``` - - - -### 创建对话 `Conversation` - -用户登录之后,要开始与其他人聊天,需要先创建一个「对话」。 - -对话(`Conversation`)是消息的载体,所有消息都是发送给对话,即时通讯服务端会把消息下发给所有在对话中的成员。 - -Tom 完成了登录之后,就可以选择用户聊天了。现在他要给 Jerry 发送消息,所以需要先创建一个只有他们两个成员的 `Conversation`: - - - -```cs -var conversation = await tom.CreateConversation(new string[] { "Jerry" }, name: "Tom & Jerry", unique: true); -``` -```java -tom.createConversation(Arrays.asList("Jerry"), "Tom & Jerry", null, false, true, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if(e == null) { - // 创建成功 - } - } -}); -``` -```objc -// 创建与 Jerry 之间的对话 -[self createConversationWithClientIds:@[@"Jerry"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) { - // handle callback -}]; -``` -```js -// 创建与 Jerry 之间的对话 -tom.createConversation({ // tom 是一个 IMClient 实例 - // 指定对话的成员除了当前用户 Tom(SDK 会默认把当前用户当做对话成员)之外,还有 Jerry - members: ['Jerry'], - // 对话名称 - name: 'Tom & Jerry', - unique: true -}).then(/* 略 */); -``` -```swift -do { - try tom.createConversation(clientIDs: ["Jerry"], name: "Tom & Jerry", isUnique: true, completion: { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - // 创建与 Jerry 之间的对话 - Conversation conversation = await tom.createConversation( - isUnique: true, members: {'Jerry'}, name: 'Tom & Jerry'); -} catch (e) { - print('创建会话失败:$e'); -} -``` - - - -`createConversation` 这个接口会直接创建一个对话,并且该对话会被存储在 `_Conversation` 表内,可以打开 **云服务控制台 > 存储 > 结构化数据** 查看数据。不同 SDK 提供的创建对话接口如下: - - - -```cs -/// -/// Creates a conversation -/// -/// The list of clientIds of participants in this conversation (except the creator) -/// The name of this conversation -/// Whether this conversation is unique; -/// if it is true and an existing conversation contains the same composition of members, -/// the existing conversation will be reused, otherwise a new conversation will be created. -/// Custom attributes of this conversation -/// -public async Task CreateConversation( - IEnumerable members, - string name = null, - bool unique = true, - Dictionary properties = null) { - return await ConversationController.CreateConv(members: members, - name: name, - unique: unique, - properties: properties); -} -``` -```java -/** - * 创建或查询一个已有 conversation - * - * @param members 对话的成员 - * @param name 对话的名字 - * @param attributes 对话的额外属性 - * @param isTransient 是否是聊天室 - * @param isUnique 如果已经存在符合条件的会话,是否返回已有回话 - * 为 false 时,则一直为创建新的回话 - * 为 true 时,则先查询,如果已有符合条件的回话,则返回已有的,否则,创建新的并返回 - * 为 true 时,仅 members 为有效查询条件 - * @param callback 结果回调函数 - */ -public void createConversation(final List members, final String name, - final Map attributes, final boolean isTransient, final boolean isUnique, - final LCIMConversationCreatedCallback callback); -/** - * 创建一个聊天对话 - * - * @param members 对话参与者 - * @param attributes 对话的额外属性 - * @param isTransient 是否为聊天室 - * @param callback 结果回调函数 - */ -public void createConversation(final List members, final String name, - final Map attributes, final boolean isTransient, - final LCIMConversationCreatedCallback callback); -/** - * 创建一个聊天对话 - * - * @param conversationMembers 对话参与者 - * @param name 对话名称 - * @param attributes 对话属性 - * @param callback 结果回调函数 - * @since 3.0 - */ -public void createConversation(final List conversationMembers, String name, - final Map attributes, final LCIMConversationCreatedCallback callback); -/** - * 创建一个聊天对话 - * - * @param conversationMembers 对话参与者 - * @param attributes 对话属性 - * @param callback 结果回调函数 - * @since 3.0 - */ -public void createConversation(final List conversationMembers, - final Map attributes, final LCIMConversationCreatedCallback callback); -``` -```objc -/// The option of conversation creation. -@interface LCIMConversationCreationOption : NSObject -/// The name of the conversation. -@property (nonatomic, nullable) NSString *name; -/// The attributes of the conversation. -@property (nonatomic, nullable) NSDictionary *attributes; -/// Create or get an unique conversation, default is `true`. -@property (nonatomic) BOOL isUnique; -/// The time interval for the life of the temporary conversation. -@property (nonatomic) NSUInteger timeToLive; -@end - -/// Create a Normal Conversation. Default is a Normal Unique Conversation. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains current client's ID. if the created conversation is unique, and server has one unique conversation with the same members, that unique conversation will be returned. -/// @param callback Result callback. -- (void)createConversationWithClientIds:(NSArray *)clientIds - callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback; - -/// Create a Normal Conversation. Default is a Normal Unique Conversation. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains current client's ID. if the created conversation is unique, and server has one unique conversation with the same members, that unique conversation will be returned. -/// @param option See `LCIMConversationCreationOption`. -/// @param callback Result callback. -- (void)createConversationWithClientIds:(NSArray *)clientIds - option:(LCIMConversationCreationOption * _Nullable)option - callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback; - -/// Create a Chat Room. -/// @param callback Result callback. -- (void)createChatRoomWithCallback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback; - -/// Create a Chat Room. -/// @param option See `LCIMConversationCreationOption`. -/// @param callback Result callback. -- (void)createChatRoomWithOption:(LCIMConversationCreationOption * _Nullable)option - callback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback; - -/// Create a Temporary Conversation. Temporary Conversation is unique in it's Life Cycle. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID. -/// @param callback Result callback. -- (void)createTemporaryConversationWithClientIds:(NSArray *)clientIds - callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback; - -/// Create a Temporary Conversation. Temporary Conversation is unique in it's Life Cycle. -/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID. -/// @param option See `LCIMConversationCreationOption`. -/// @param callback Result callback. -- (void)createTemporaryConversationWithClientIds:(NSArray *)clientIds - option:(LCIMConversationCreationOption * _Nullable)option - callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback; -``` -```js -/** - * 创建一个对话 - * @param {Object} options 除了下列字段外的其他字段将被视为对话的自定义属性 - * @param {String[]} options.members 对话的初始成员列表,必要参数,默认包含当前 client - * @param {String} [options.name] 对话的名字,可选参数,如果不传默认值为 null - * @param {Boolean} [options.transient=false] 是否为聊天室,可选参数 - * @param {Boolean} [options.unique=false] 是否唯一对话,当其为 true 时,如果当前已经有相同成员的对话存在则返回该对话,否则会创建新的对话 - * @param {Boolean} [options.tempConv=false] 是否为临时对话,可选参数 - * @param {Integer} [options.tempConvTTL=0] 可选参数,如果 tempConv 为 true,这里可以指定临时对话的生命周期。 - * @return {Promise.} - */ -async createConversation({ - members: m, - name, - transient, - unique, - tempConv, - tempConvTTL, - // 可添加更多属性 -}); -``` -```swift -/// Create a Normal Conversation. Default is a Unique Conversation. -/// -/// - Parameters: -/// - clientIDs: The set of client ID. it's the members of the conversation which will be created. the initialized members always contains current client's ID. if the created conversation is unique, and server has one unique conversation with the same members, that unique conversation will be returned. -/// - name: The name of the conversation. -/// - attributes: The attributes of the conversation. -/// - isUnique: True means create or get a unique conversation, default is true. -/// - completion: callback. -public func createConversation(clientIDs: Set, name: String? = nil, attributes: [String : Any]? = nil, isUnique: Bool = true, completion: @escaping (LCGenericResult) -> Void) throws - -/// Create a Chat Room. -/// -/// - Parameters: -/// - name: The name of the chat room. -/// - attributes: The attributes of the chat room. -/// - completion: callback. -public func createChatRoom(name: String? = nil, attributes: [String : Any]? = nil, completion: @escaping (LCGenericResult) -> Void) throws - -/// Create a Temporary Conversation. Temporary Conversation is unique in it's Life Cycle. -/// -/// - Parameters: -/// - clientIDs: The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID. -/// - timeToLive: The time interval for the life of the temporary conversation. -/// - completion: callback. -public func createTemporaryConversation(clientIDs: Set, timeToLive: Int32, completion: @escaping (LCGenericResult) -> Void) throws -``` -```dart -/// To create a normal [Conversation]. -/// -/// [isUnique] is a special parameter, default is `true`, it affects the creation behavior and property [Conversation.isUnique]. -/// * When it is `true` and the relevant unique [Conversation] not exists in the server, this method will create a new unique [Conversation]. -/// * When it is `true` and the relevant unique [Conversation] exists in the server, this method will return that existing unique [Conversation]. -/// * When it is `false`, this method always create a new non-unique [Conversation]. -/// -/// [members] is the [Conversation.members]. -/// [name] is the [Conversation.name]. -/// [attributes] is the [Conversation.attributes]. -/// -/// Returns an instance of [Conversation]. -Future createConversation({ - bool isUnique = true, - Set members, - String name, - Map attributes, -}) async {} - -/// To create a new [ChatRoom]. -/// -/// [name] is the [Conversation.name]. -/// [attributes] is the [Conversation.attributes]. -/// -/// Returns an instance of [ChatRoom]. -Future createChatRoom({ - String name, - Map attributes, -}) async {} - -/// To create a new [TemporaryConversation]. -/// -/// [members] is the [Conversation.members]. -/// [timeToLive] is the [TemporaryConversation.timeToLive]. -/// -/// Returns an instance of [TemporaryConversation]. -Future createTemporaryConversation({ - Set members, - int timeToLive, -}) async {} -``` - - - -虽然不同语言/平台接口声明有所不同,但是支持的参数是基本一致的。在创建一个对话的时候,我们主要可以指定: - -1. `members`:必要参数,包含对话的初始成员列表,请注意当前用户作为对话的创建者,是默认包含在成员里面的,所以 `members` 数组中可以不包含当前用户的 `clientId`。 - -2. `name`:对话名字,可选参数,上面代码指定为了「Tom & Jerry」。 - -3. `attributes`:对话的自定义属性,可选。上面示例代码没有指定额外属性,开发者如果指定了额外属性的话,以后其他成员可以通过 `LCIMConversation` 的接口获取到这些属性值。附加属性在 `_Conversation` 表中被保存在 `attr` 列中。 - -4. `unique`/`isUnique` 或者是 `LCIMConversationOptionUnique`:唯一对话标志位,可选。 - - - 如果设置为唯一对话,云端会根据完整的成员列表先进行一次查询,如果已经有正好包含这些成员的对话存在,那么就返回已经存在的对话,否则才创建一个新的对话。 - - 如果指定 `unique` 标志为假,那么每次调用 `createConversation` 接口都会创建一个新的对话。 - - 未指定 `unique` 时,SDK 默认值为真。 - - 从通用的聊天场景来看,不管是 Tom 发出「创建和 Jerry 单聊对话」的请求,还是 Jerry 发出「创建和 Tom 单聊对话」的请求,或者 Tom 以后再次发出创建和 Jerry 单聊对话的请求,都应该是同一个对话才是合理的,否则可能因为聊天记录的不同导致用户混乱。 - -5. 对话类型的其他标志,可选参数,例如 `transient`/`isTransient` 表示「聊天室」,`tempConv`/`tempConvTTL` 和 `LCIMConversationOptionTemporary` 用来创建「临时对话」等等。什么都不指定就表示创建普通对话,对于这些标志位的含义我们先不管,以后会有说明。 - -创建对话之后,可以获取对话的内置属性,云端会为每一个对话生成一个全局唯一的 ID 属性:`Conversation.id`,它是其他用户查询对话时常用的匹配字段。 - -### 发送消息 - -对话已经创建成功了,接下来 Tom 可以在这个对话中发出第一条文本消息了: - - - -```cs -var textMessage = new LCIMTextMessage("Jerry,起床了!"); -await conversation.Send(textMessage); -``` -```java -LCIMTextMessage msg = new LCIMTextMessage(); -msg.setText("Jerry,起床了!"); -// 发送消息 -conversation.sendMessage(msg, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - Log.d("Tom & Jerry", "发送成功!"); - } - } -}); -``` -```objc -LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"耗子,起床!" attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` -```js -var { TextMessage } = require('leancloud-realtime'); -conversation.send(new TextMessage('Jerry,起床了!')).then(function(message) { - console.log('Tom & Jerry', '发送成功!'); -}).catch(console.error); -``` -```swift -do { - let textMessage = IMTextMessage(text: "Jerry,起床了!") - try conversation.send(message: textMessage) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -try { - TextMessage textMessage = TextMessage(); - textMessage.text = 'Jerry,起床了!'; - await conversation.send(message: textMessage); -} catch (e) { - print(e); -} -``` - - - -上面接口实现的功能就是向对话中发送一条消息,同一对话中其他在线成员会立刻收到此消息。 - -现在 Tom 发出了消息,那么接收者 Jerry 他要在界面上展示出来这一条新消息,该怎么来处理呢? - -### 接收消息 - -在另一个设备上,我们用 `Jerry` 作为 `clientId` 来创建一个 `IMClient` 并登录即时通讯服务(与前两节 Tom 的处理流程一样): - - - -```cs -var jerry = new LCIMClient("Jerry"); -``` -```java -// Jerry 登录 -LCIMClient jerry = LCIMClient.getInstance("Jerry"); -jerry.open(new LCIMClientCallback(){ - @Override - public void done(LCIMClient client,LCIMException e){ - if(e==null){ - // 登录成功后的逻辑 - } - } -}); -``` -```objc -NSError *error; -jerry = [[LCIMClient alloc] initWithClientId:@"Jerry" error:&error]; -if (!error) { - [jerry openWithCallback:^(BOOL succeeded, NSError *error) { - // handle callback - }]; -} -``` -```js -var { Event } = require('leancloud-realtime'); -// Jerry 登录 -realtime.createIMClient('Jerry').then(function(jerry) { -}).catch(console.error); -``` -```swift -do { - let jerry = try IMClient(ID: "Jerry") - jerry.open { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -Client jerry = Client(id: 'Jerry'); -await jerry.open(); -``` - - - -Jerry 作为消息的被动接收方,他不需要主动创建与 Tom 的对话,可能也无法知道 Tom 创建好的对话信息,Jerry 端需要通过设置即时通讯客户端事件的回调函数,才能获取到 Tom 那边操作的通知。 - -即时通讯客户端事件回调能处理多种服务端通知,这里我们先关注这里会出现的两个事件: - -- 用户被邀请进入某个对话的通知事件。Tom 在创建和 Jerry 的单聊对话的时候,Jerry 这边就能立刻收到一条通知,获知到类似于「Tom 邀请你加入了一个对话」的信息。 -- 已加入对话中新消息到达的通知。在 Tom 发出「Jerry,起床了!」这条消息之后,Jerry 这边也能立刻收到一条新消息到达的通知,通知中带有消息具体数据以及对话、发送者等上下文信息。 - -现在,我们看看具体应该如何响应服务端发过来的通知。Jerry 端会分别处理「加入对话」的事件通知和「新消息到达」的事件通知: - - - -```cs -jerry.OnInvited = (conv, initBy) => { - WriteLine($"{initBy} 邀请 jerry 加入 {conv.Id} 对话"); -}; -jerry.OnMessage = (conv, msg) => { - if (msg is LCIMTextMessage textMessage) { - // textMessage.ConversationId 是该条消息所属于的对话 ID - // textMessage.Text 是该文本消息的文本内容 - // textMessage.FromClientId 是消息发送者的 clientId - } -}; -``` -```java -// Java/Android SDK 通过定制自己的对话事件 Handler 处理服务端下发的对话事件通知 -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本方法来处理当前用户被邀请到某个聊天对话事件 - * - * @param client - * @param conversation 被邀请的聊天对话 - * @param operator 邀请你的人 - * @since 3.0 - */ - @Override - public void onInvited(LCIMClient client, LCIMConversation conversation, String invitedBy) { - // 当前 clientId(Jerry)被邀请到对话,执行此处逻辑 - } -} -// 设置全局的对话事件处理 handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); - -// Java/Android SDK 通过定制自己的消息事件 Handler 处理服务端下发的消息通知 -public static class CustomMessageHandler extends LCIMMessageHandler{ - /** - * 重载此方法来处理接收消息 - * - * @param message - * @param conversation - * @param client - */ - @Override - public void onMessage(LCIMMessage message,LCIMConversation conversation,LCIMClient client){ - if(message instanceof LCIMTextMessage){ - Log.d(((LCIMTextMessage)message).getText()); // Jerry,起床了 - } - } - } -// 设置全局的消息处理 handler -LCIMMessageManager.registerDefaultMessageHandler(new CustomMessageHandler()); -``` -```objc -// Objective-C SDK 通过实现 LCIMClientDelegate 代理来处理服务端通知 -// 不了解 Objective-C 代理(delegate)概念的读者可以参考: -// https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/DelegatesandDataSources/DelegatesandDataSources.html -jerry.delegate = delegator; - -/*! - 当前用户被邀请加入对话的通知。 - @param conversation - 所属对话 - @param clientId - 邀请者的 ID - */ -- (void)conversation:(LCIMConversation *)conversation invitedByClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"当前 clientId(Jerry)被 %@ 邀请,加入了对话",clientId]); -} - -/*! - 接收到新消息(使用内置消息格式)。 - @param conversation - 所属对话 - @param message - 具体的消息 - */ -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - NSLog(@"%@", message.text); // Jerry,起床了! -} -``` -```js -// JS SDK 通过在 IMClient 实例上监听事件回调来响应服务端通知 - -// 当前用户被添加至某个对话 -jerry.on(Event.INVITED, function invitedEventHandler(payload, conversation) { - console.log(payload.invitedBy, conversation.id); -}); - -// 当前用户收到了某一条消息,可以通过响应 Event.MESSAGE 这一事件来处理。 -jerry.on(Event.MESSAGE, function(message, conversation) { - console.log('收到新消息:' + message.text); -}); -``` -```swift -let delegator: Delegator = Delegator() -jerry.delegate = delegator - -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - print(message) - default: - break - } - default: - break - } -} -``` -```dart -jerry.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - if (message.stringContent != null) { - print('收到的消息是:${message.stringContent}'); - } -}; -``` - - - -Jerry 端实现了上面两个事件通知函数之后,就顺利收到 Tom 发送的消息了。之后 Jerry 也可以回复消息给 Tom,而 Tom 端实现类似的接收流程,那么他们俩就可以开始愉快的聊天了。 - -我们现在可以回顾一下 Tom 和 Jerry 发送第一条消息的过程中,两方完整的处理时序: - ->Cloud: 1. Tom 将 Jerry 加入对话 -Cloud-->>Jerry: 2. 下发通知:你被邀请加入对话 -Jerry-->>UI: 3. 加载聊天的 UI 界面 -Tom->>Cloud: 4. 发送消息 -Cloud-->>Jerry: 5. 下发通知:接收到有新消息 -Jerry-->>UI: 6. 显示收到的消息内容 -`} /> - -在聊天过程中,接收方除了响应新消息到达通知之外,还需要响应多种对话成员变动通知,例如「新用户 XX 被 XX 邀请加入了对话」、「用户 XX 主动退出了对话」、「用户 XX 被管理员剔除出对话」,等等。 -云端会实时下发这些事件通知给客户端,具体细节可以参考后续章节:[成员变更的事件通知总结](#成员变更的事件通知总结)。 - -## 多人群聊 - -上面我们讨论了一对一单聊的实现流程,假设我们还需要实现一个「朋友群」的多人聊天,接下来我们就看看怎么完成这一功能。 - -从即时通讯云端来看,多人群聊与单聊的流程十分接近,主要差别在于对话内成员数量的多少。群聊对话支持在创建对话的时候一次性指定全部成员,也允许在创建之后通过邀请的方式来增加新的成员。 - -### 创建多人群聊对话 - -在 Tom 和 Jerry 的对话中(假设对话 ID 为 `CONVERSATION_ID`,这只是一个示例,并不代表实际数据),后来 Tom 又希望把 Mary 也拉进来,他可以使用如下的办法: - - - -```cs -// 首先根据 ID 获取 Conversation 实例 -var conversation = await tom.GetConversation("CONVERSATION_ID"); -// 邀请 Mary 加入对话 -await conversation.AddMembers(new string[] { "Mary" }); -``` -```java -// 首先根据 ID 获取 Conversation 实例 -final LCIMConversation conv = client.getConversation("CONVERSATION_ID"); -// 邀请 Mary 加入对话 -conv.addMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() { - @Override - public void done(LCIMException e, List successfulClientIds, List failures) { - // 添加成功 - } -}); -``` -```objc -// 首先根据 ID 获取 Conversation 实例 -LCIMConversationQuery *query = [self.client conversationQuery]; -[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) { - // 邀请 Mary 加入对话 - [conversation addMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"邀请成功!"); - } - }]; -}]; -``` -```js -// 首先根据 ID 获取 Conversation 实例 -tom.getConversation('CONVERSATION_ID').then(function(conversation) { - // 邀请 Mary 加入对话 - return conversation.add(['Mary']); -}).then(function(conversation) { - console.log('添加成功', conversation.members); - // 此时对话成员为:['Mary', 'Tom', 'Jerry'] -}).catch(console.error.bind(console)); -``` -```swift -do { - let conversationQuery = client.conversationQuery - try conversationQuery.getConversation(by: "CONVERSATION_ID") { (result) in - switch result { - case .success(value: let conversation): - do { - try conversation.add(members: ["Mary"], completion: { (result) in - switch result { - case .allSucceeded: - break - case .failure(error: let error): - print(error) - case let .slicing(success: succeededIDs, failure: failures): - if let succeededIDs = succeededIDs { - print(succeededIDs) - } - for (failedIDs, error) in failures { - print(failedIDs) - print(error) - } - } - }) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -List conversations; -try { -// 首先根据 ID 获取 Conversation 实例 - ConversationQuery query = tom.conversationQuery(); - query.whereEqualTo('objectId', 'CONVERSATION_ID'); - conversations = await query.find(); -} catch (e) { - print(e); -} -try { - Conversation conversation = conversations.first; -// 邀请 Mary 加入对话 - MemberResult addResult = await conversation.addMembers( - members: {'Mary'}, - ); -} catch (e) { - print(e); -} -``` - - - -而 Jerry 端增加「新成员加入」的事件通知处理函数,就可以及时获知 Mary 被 Tom 邀请加入当前对话了: - - -<> - -```cs -jerry.OnMembersJoined = (conv, memberList, initBy) => { - WriteLine($"{initBy} 邀请了 {memberList} 加入了 {conv.Id} 对话"); -} -``` - -其中 `AVIMOnInvitedEventArgs` 参数包含如下内容: - -1. `InvitedBy`:该操作的发起者 -2. `JoinedMembers`:此次加入对话的包含的成员列表 -3. `ConversationId`:被操作的对话 - - -<> - -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本方法以处理聊天对话中的参与者加入事件 - * - * @param client - * @param conversation - * @param members 加入的参与者 - * @param invitedBy 加入事件的邀请人,有可能是加入的参与者本身 - * @since 3.0 - */ - @Override - public void onMemberJoined(LCIMClient client, LCIMConversation conversation, - List members, String invitedBy) { - // 手机屏幕上会显示一小段文字:Mary 加入到 551260efe4b01608686c3e0f;操作者为:Tom - Toast.makeText(LeanCloud.applicationContext, - members + " 加入到 " + conversation.getConversationId() + ";操作者为:" - + invitedBy, Toast.LENGTH_SHORT).show(); - } -} -// 设置全局的对话事件处理 handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); -``` - - -<> - -```objc -jerry.delegate = delegator; - -#pragma mark - LCIMClientDelegate -/*! - 对话中有新成员加入时所有成员都会收到这一通知。 - @param conversation - 所属对话 - @param clientIds - 加入的新成员列表 - @param clientId - 邀请者的 ID - */ -- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"%@ 加入到对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]); -} -``` - - -<> - -```js -// 有用户被添加至某个对话 -jerry.on(Event.MEMBERS_JOINED, function membersjoinedEventHandler(payload, conversation) { - console.log(payload.members, payload.invitedBy, conversation.id); -}); -``` - -其中 `payload` 参数包含如下内容: - -1. `members`:字符串数组,被添加的用户 `clientId` 列表 -2. `invitedBy`:字符串,邀请者 `clientId` - - -<> - -```swift -jerry.delegate = delegator - -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .joined(byClientID: byClientID, at: atDate): - print(byClientID) - print(atDate) - case let .membersJoined(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` - - -<> - -```dart -// 加入成员通知 -jerry.onMembersJoined = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('成员 ${members.toString()} 加入会话'); -}; -``` - - - - -这一流程的时序图如下: - ->Cloud: 1. 添加 Mary -Cloud->>Tom: 2. 下发通知:Mary 被你邀请加入了对话 -Cloud-->>Mary: 2. 下发通知:你被 Tom 邀请加入对话 -Cloud-->>Jerry: 2. 下发通知:Mary 被 Tom 邀请加入了对话 -`} /> - -而 Mary 端如果要能加入到 Tom 和 Jerry 的对话中来,Ta 可以参照 [一对一单聊](#一对一单聊) 中 Jerry 侧的做法监听 `INVITED` 事件,就可以自己被邀请到了一个对话当中。 - -而 **重新创建一个对话,并在创建的时候指定全部成员** 的方式如下: - - - -```cs -var conversation = await tom.CreateConversation(new string[] { "Jerry","Mary" }, name: "Tom & Jerry & friends", unique: true); -``` -```java -tom.createConversation(Arrays.asList("Jerry","Mary"), "Tom & Jerry & friends", null, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if (e == null) { - // 创建成功 - } - } - }); -``` -```objc -// Tom 建立了与朋友们的会话 -[tom createConversationWithClientIds:@[@"Jerry", @"Mary"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) { - if (!error) { - NSLog(@"创建成功!"); - } -}]; -``` -```js -tom.createConversation({ - // 创建的时候直接指定 Jerry 和 Mary 一起加入多人群聊,当然根据需求可以添加更多成员 - members: ['Jerry','Mary'], - // 对话名称 - name: 'Tom & Jerry & friends', - unique: true, -}).catch(console.error); -``` -```swift -do { - try tom.createConversation(clientIDs: ["Jerry", "Mary"], name: "Tom & Jerry & friends", isUnique: true, completion: { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - Conversation conversation = await jerry.createConversation( - isUnique: true, - members: {'Jerry', 'Mary'}, - name: 'Tom & Jerry & friends'); -} catch (e) { - print(e); -} -``` - - - -### 群发消息 - -多人群聊中一个成员发送的消息,会实时同步到所有其他在线成员,其处理流程与单聊中 Jerry 接收消息的过程是一样的。 - -例如,Tom 向好友群发送了一条欢迎消息: - - - -```cs -var textMessage = new LCIMTextMessage("大家好,欢迎来到我们的群聊对话!"); -await conversation.Send(textMessage); -``` -```java -LCIMTextMessage msg = new LCIMTextMessage(); -msg.setText("大家好,欢迎来到我们的群聊对话!"); -// 发送消息 -conversation.sendMessage(msg, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - Log.d("群聊", "发送成功!"); - } - } -}); -``` -```objc -[conversation sendMessage:[LCIMTextMessage messageWithText:@"大家好,欢迎来到我们的群聊对话!" attributes:nil] callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` -```js -conversation.send(new TextMessage('大家好,欢迎来到我们的群聊对话')); -``` -```swift -do { - let textMessage = IMTextMessage(text: "大家好,欢迎来到我们的群聊对话!") - try conversation.send(message: textMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - TextMessage textMessage = TextMessage(); - textMessage.text = '大家好,欢迎来到我们的群聊对话!'; - await conversation.send(message: textMessage); -} catch (e) { - print(e); -} -``` - - - -而 Jerry 和 Mary 端都会有 `Event.MESSAGE` 事件触发,利用它来接收群聊消息,并更新产品 UI。 - -### 将他人踢出对话 - -三个好友的群其乐融融不久,后来 Mary 出言不逊,惹恼了群主 Tom,Tom 直接把 Mary 踢出了对话群。Tom 端想要踢人,该怎么实现呢? - - - -```cs -await conversation.RemoveMembers(new string[] { "Mary" }); -``` -```java -conv.kickMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() { - @Override - public void done(LCIMException e, List successfulClientIds, List failures) { - } -}); -``` -```objc -[conversation removeMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"踢人成功!"); - } -}]; -``` -```js -conversation.remove(['Mary']).then(function(conversation) { - console.log('移除成功', conversation.members); -}).catch(console.error.bind(console)); -``` -```swift -do { - try conversation.remove(members: ["Mary"], completion: { (result) in - switch result { - case .allSucceeded: - break - case .failure(error: let error): - print(error) - case let .slicing(success: succeededIDs, failure: failures): - if let succeededIDs = succeededIDs { - print(succeededIDs) - } - for (failedIDs, error) in failures { - print(failedIDs) - print(error) - } - } - }) -} catch { - print(error) -} -``` -```dart -try { - MemberResult removeMemberResult = await conversation.removeMembers(members: {'Mary'}); -} catch (e) { - print(e); -} -``` - - - -Tom 端执行了这段代码之后会触发如下流程: - ->Cloud: 1. 对话中移除 Mary -Cloud-->>Mary: 2. 下发通知:你被 Tom 从对话中剔除了 -Cloud-->>Jerry: 2. 下发通知:Mary 被 Tom 移除 -Cloud-->>Tom: 2. 下发通知:Mary 被移除了对话 -`} /> - -这里出现了两个新的事件:当前用户被踢出对话 `KICKED`(Mary 收到的),成员 XX 被踢出对话 `MEMBERS_LEFT`(Jerry 和 Tom 收到的)。其处理方式与邀请人的流程类似: - - - -```cs -jerry.OnMembersLeft = (conv, leftIds, kickedBy) => { - WriteLine($"{leftIds} 离开对话 {conv.Id};操作者为:{kickedBy}"); -} -jerry.OnKicked = (conv, initBy) => { - WriteLine($"你已经离开对话 {conv.Id};操作者为:{initBy}"); -}; -``` -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本方法以处理聊天对话中的参与者离开事件 - * - * @param client - * @param conversation - * @param members 离开的参与者 - * @param kickedBy 离开事件的发动者,有可能是离开的参与者本身 - * @since 3.0 - */ - @Override - public abstract void onMemberLeft(LCIMClient client, - LCIMConversation conversation, List members, String kickedBy) { - Toast.makeText(LeanCloud.applicationContext, - members + " 离开对话 " + conversation.getConversationId() + ";操作者为:" - + kickedBy, Toast.LENGTH_SHORT).show(); - } - /** - * 实现本方法来处理当前用户被踢出某个聊天对话事件 - * - * @param client - * @param conversation - * @param kickedBy 踢出你的人 - * @since 3.0 - */ - @Override - public abstract void onKicked(LCIMClient client, LCIMConversation conversation, - String kickedBy) { - Toast.makeText(LeanCloud.applicationContext, - "你已离开对话 " + conversation.getConversationId() + ";操作者为:" - + kickedBy, Toast.LENGTH_SHORT).show(); - } -} -// 设置全局的对话事件处理 handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); -``` -```objc -jerry.delegate = delegator; - -#pragma mark - LCIMClientDelegate -/*! - 对话中有成员离开时所有剩余成员都会收到这一通知。 - @param conversation - 所属对话 - @param clientIds - 离开的成员列表 - @param clientId - 操作者的 ID - */ -- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray * _Nullable)clientIds byClientId:(NSString * _Nullable)clientId { - ; -} -/*! - 当前用户被踢出对话的通知。 - @param conversation - 所属对话 - @param clientId - 操作者的 ID - */ -- (void)conversation:(LCIMConversation *)conversation kickedByClientId:(NSString * _Nullable)clientId { - ; -} -``` -```js -// 有成员被从某个对话中移除 -jerry.on(Event.MEMBERS_LEFT, function membersjoinedEventHandler(payload, conversation) { - console.log(payload.members, payload.kickedBy, conversation.id); -}); -// 有用户被踢出某个对话 -jerry.on(Event.KICKED, function membersjoinedEventHandler(payload, conversation) { - console.log(payload.kickedBy, conversation.id); -}); -``` -```swift -jerry.delegate = delegator - -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .left(byClientID: byClientID, at: atDate): - print(byClientID) - print(atDate) - case let .membersLeft(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` -```dart -// 有成员被从某个对话中移除 -jerry.onMembersLeft = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('成员 ${members.toString()} 离开会话,操作者为: $byClientID'); -}; -// 有用户被踢出某个对话 -jerry.onKicked = ({ - Client client, - Conversation conversation, - String byClientID, - DateTime atDate, -}) { - print('你已离开对话,操作者为: $byClientID'); -}; -``` - - - -### 用户主动加入对话 - -把 Mary 踢走之后,Tom 嫌人少不好玩,所以他找到了 William,说他和 Jerry 有一个很好玩的聊天群,并且把群的 ID(或名称)告知给了 William。William 也很想进入这个群看看他们究竟在聊什么,他自己主动加入了对话: - - - -```cs -var conv = await william.GetConversation("CONVERSATION_ID"); -await conv.Join(); -``` -```java -LCIMConversation conv = william.getConversation("CONVERSATION_ID"); -conv.join(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // 加入成功 - } - } -}); -``` -```objc -LCIMConversationQuery *query = [william conversationQuery]; -[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) { - [conversation joinWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"加入成功!"); - } - }]; -}]; -``` -```js -william.getConversation('CONVERSATION_ID').then(function(conversation) { - return conversation.join(); -}).then(function(conversation) { - console.log('加入成功', conversation.members); - // 此时对话成员为:['William', 'Tom', 'Jerry'] -}).catch(console.error.bind(console)); -``` -```swift -do { - let conversationQuery = client.conversationQuery - try conversationQuery.getConversation(by: "CONVERSATION_ID") { (result) in - switch result { - case .success(value: let conversation): - do { - try conversation.join(completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -List conversations; -try { - ConversationQuery query = william.conversationQuery(); - query.whereEqualTo('objectId', 'CONVERSATION_ID'); - conversations = await query.find(); -} catch (e) { - print(e); -} - -try { - Conversation conversation = conversations.first; - MemberResult joinResult = await conversation.join(); -} catch (e) { - print(e); -} -``` - - - -执行了这段代码之后会触发如下流程: - ->Cloud: 1. 加入对话 -Cloud-->>William: 2. 下发通知:你已加入对话 -Cloud-->>Tom: 2. 下发通知:William 加入对话 -Cloud-->>Jerry: 2. 下发通知:William 加入对话 -`} /> - -其他人则通过订阅 `MEMBERS_JOINED` 来接收 William 加入对话的通知 : - - - -```cs -jerry.OnMembersJoined = (conv, memberList, initBy) => { - WriteLine($"{memberList} 加入了 {conv.Id} 对话;操作者为:{initBy}"); -} -``` -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - @Override - public void onMemberJoined(LCIMClient client, LCIMConversation conversation, - List members, String invitedBy) { - // 手机屏幕上会显示一小段文字:William 加入到 551260efe4b01608686c3e0f;操作者为:William - Toast.makeText(LeanCloud.applicationContext, - members + " 加入到 " + conversation.getConversationId() + ";操作者为:" - + invitedBy, Toast.LENGTH_SHORT).show(); - } -} -``` -```objc -- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"%@ 加入到对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]); -} -``` -```js -jerry.on(Event.MEMBERS_JOINED, function membersJoinedEventHandler(payload, conversation) { - console.log(payload.members, payload.invitedBy, conversation.id); -}); -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .membersJoined(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` -```dart -jerry.onMembersJoined = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('成员 ${members.toString()} 加入会话'); -}; -``` - - - -### 用户主动退出对话 - -随着 Tom 邀请进来的人越来越多,Jerry 觉得跟这些人都说不到一块去,他不想继续呆在这个对话里面了,所以选择自己主动退出对话,这时候可以调用下面的方法完成退群的操作: - - - -```cs -await conversation.Quit(); -``` -```java -conversation.quit(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // 退出成功 - } - } -}); -``` -```objc -[conversation quitWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"退出成功!"); - } -}]; -``` -```js -conversation.quit().then(function(conversation) { - console.log('退出成功', conversation.members); -}).catch(console.error.bind(console)); -``` -```swift -do { - try conversation.leave(completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - MemberResult quitResult = await conversation.quit(); -} catch (e) { - print(e); -} -``` - - - -执行了这段代码 Jerry 就离开了这个聊天群,此后群里所有的事件 Jerry 都不会再知晓。各个成员接收到的事件通知流程如下: - ->Cloud: 1. 离开对话 -Cloud-->>Jerry: 2. 下发通知:你已离开对话 -Cloud-->>Mary: 2. 下发通知:Jerry 已离开对话 -Cloud-->>Tom: 2. 下发通知:Jerry 已离开对话 -`} /> - -而其他人需要通过订阅 `MEMBERS_LEFT` 来接收 Jerry 离开对话的事件通知: - - - -```cs -mary.OnMembersLeft = (conv, members, initBy) => { - WriteLine($"{members} 离开了 {conv.Id} 对话;操作者为:{initBy}"); -} -``` -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - @Override - public void onMemberLeft(LCIMClient client, LCIMConversation conversation, List members, - String kickedBy) { - // 有其他成员离开时,执行此处逻辑 - } -} -``` -```objc -// Mary 登录之后,Jerry 退出了对话,在 Mary 所在的客户端就会激发以下回调 -- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray *)clientIds byClientId:(NSString *)clientId { - NSLog(@"%@", [NSString stringWithFormat:@"%@ 离开了对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]); -} -``` -```js -mary.on(Event.MEMBERS_LEFT, function membersLeftEventHandler(payload, conversation) { - console.log(payload.members, payload.kickedBy, conversation.id); -}); -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .membersLeft(members: members, byClientID: byClientID, at: atDate): - print(members) - print(byClientID) - print(atDate) - default: - break - } -} -``` -```dart -mary.onMembersLeft = ({ - Client client, - Conversation conversation, - List members, - String byClientID, - DateTime atDate, -}) { - print('成员 ${members.toString()} 离开会话'); -}; -``` - - - -### 成员变更的事件通知总结 - -前面的时序图和代码针对成员变更的操作做了逐步的分析和阐述,为了确保开发者能够准确的使用事件通知,如下表格做了一个统一的归类和划分: - -假设 Tom 和 Jerry 已经在对话内了: - -操作 | Tom | Jerry | Mary | William ---- | --- | --- | --- -Tom 添加 Mary | `MEMBERS_JOINED` | `MEMBERS_JOINED` | `INVITED` | / -Tom 剔除 Mary | `MEMBERS_LEFT` | `MEMBERS_LEFT` | `KICKED` | / -William 加入 | `MEMBERS_JOINED` | `MEMBERS_JOINED` | / | `MEMBERS_JOINED` -Jerry 主动退出 | `MEMBERS_LEFT` | `MEMBERS_LEFT` | / | `MEMBERS_LEFT` - -## 文本之外的聊天消息 - -上面的示例都是发送文本消息,但是实际上可能图片、视频、位置等消息也是非常常见的消息格式,接下来我们就看看如何发送这些富媒体类型的消息。 - -即时通讯服务默认支持文本、文件、图像、音频、视频、位置、二进制等不同格式的消息,除了二进制消息之外,普通消息的收发接口都是字符串,但是文本消息和文件、图像、音视频消息有一点区别: - -- 文本消息发送的就是本身的内容 -- 而其他的多媒体消息,例如一张图片,实际上即时通讯 SDK 会首先调用存储服务的 `AVFile` 接口,将图像的二进制文件上传到存储服务云端,再把图像下载的 URL 放入即时通讯消息结构体中,所以 **图像消息不过是包含了图像下载链接的固定格式文本消息**。 - -图像等二进制数据不随即时通讯消息直接下发的主要原因在于,文件存储服务默认都是开通了 CDN 加速选项的,通过文件下载对于终端用户来说可以有更快的展现速度,同时对于开发者来说也能获得更低的存储成本。 - -### 默认消息类型 - -即时通讯服务内置了多种结构化消息用来满足常见的需求: - -- `TextMessage` 文本消息 -- `ImageMessage` 图像消息 -- `AudioMessage` 音频消息 -- `VideoMessage` 视频消息 -- `FileMessage` 普通文件消息(.txt/.doc/.md 等各种) -- `LocationMessage` 地理位置消息 - -所有消息均派生自 `LCIMMessage`,每种消息实例都具备如下属性: - - -<> - -| 属性 | 类型 | 描述 | -| --- | --- | --- | -| `content` | `String` | 消息内容。 | -| `clientId` | `String` | 消息发送者的 `clientId`。 | -| `conversationId` | `String` | 消息所属对话 ID。 | -| `messageId` | `String` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `timestamp` | `long` | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 | -| `receiptTimestamp` | `long` | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 | -| `status` | `AVIMMessageStatus` 枚举 | 消息状态,有五种取值:

    `AVIMMessageStatusNone`(未知)
    `AVIMMessageStatusSending`(发送中)
    `AVIMMessageStatusSent`(发送成功)
    `AVIMMessageStatusReceipt`(被接收)
    `AVIMMessageStatusFailed`(失败) | -| `ioType` | `AVIMMessageIOType` 枚举 | 消息传输方向,有两种取值:

    `AVIMMessageIOTypeIn`(发给当前用户)
    `AVIMMessageIOTypeOut`(由当前用户发出) | - - -<> - -| 属性 | 类型 | 描述 | -| --- | --- | --- | -| `content` | `String` | 消息内容。 | -| `clientId` | `String` | 消息发送者的 `clientId`。 | -| `conversationId` | `String` | 消息所属对话 ID。 | -| `messageId` | `String` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `timestamp` | `long` | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 | -| `receiptTimestamp` | `long` | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 | -| `status` | `MessageStatus` 枚举 | 消息状态,有五种取值:

    `StatusNone`(未知)
    `StatusSending`(发送中)
    `StatusSent`(发送成功)
    `StatusReceipt`(被接收)
    `StatusFailed`(失败) | -| `ioType` | `MessageIOType` 枚举 | 消息传输方向,有两种取值:

    `TypeIn`(发给当前用户)
    `TypeOut`(由当前用户发出) | - - -<> - -| 属性 | 类型 | 描述 | -| --- | --- | --- | -| `content` | `NSString` | 消息内容。 | -| `clientId` | `NSString` | 消息发送者的 `clientId`。 | -| `conversationId` | `NSString` | 消息所属对话 ID。 | -| `messageId` | `NSString` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `sendTimestamp` | `int64_t` | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 | -| `deliveredTimestamp` | `int64_t` | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 | -| `status` | `AVIMMessageStatus` 枚举 | 消息状态,有五种取值:

    `LCIMMessageStatusNone`(未知)
    `LCIMMessageStatusSending`(发送中)
    `LCIMMessageStatusSent`(发送成功)
    `LCIMMessageStatusDelivered`(被接收)
    `LCIMMessageStatusFailed`(失败) | -| `ioType` | `LCIMMessageIOType` 枚举 | 消息传输方向,有两种取值:

    `LCIMMessageIOTypeIn`(发给当前用户)
    `LCIMMessageIOTypeOut`(由当前用户发出) | - - -<> - -Swift - -| 属性 | 类型 | 描述 | -| --- | --- | --- | -| `content` | `IMMessage.Content` | 消息内容,支持 `String` 和 `Data` 两种格式。 | -| `fromClientID` | `String` | 消息发送者的 `clientId`。 | -| `currentClientID` | `String` | 消息接收者的 `clientId`。 | -| `conversationID` | `String` | 消息所属对话 ID。 | -| `ID` | `String` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `sentTimestamp` | `int64_t` | 消息发送的时间。消息发送成功之后,云端赋予的全局的时间戳。 | -| `deliveredTimestamp` | `int64_t` | 消息被对方接收到的时间戳。 | -| `readTimestamp` | `int64_t` | 消息被对方阅读的时间戳。 | -| `patchedTimestamp` | `int64_t` | 消息被修改的时间戳。 | -| `isAllMembersMentioned` | `Bool` | @ 所有会话成员。 | -| `mentionedMembers` | `[String]` | @ 会话成员。 | -| `isCurrentClientMentioned` | `Bool` | 当前 `Client` 是否被 @。 | -| `status` | `IMMessage.Status` | 消息状态,有 6 种取值:

    `none`(无状态)
    `sending`(发送中)
    `sent`(发送成功)
    `delivered`(已被接收)
    `read`(已被读)
    `failed`(发送失败) | -| `ioType` | `IMMessage.IOType` | 消息传输方向,有两种取值:

    `in`(当前用户接收到的)
    `out`(由当前用户发出的) | - - -<> - -JavaScript - -| 属性 | 类型 | 描述 | -| --- | --- | --- | -| `from` | `String` | 消息发送者的 `clientId`。 | -| `cid` | `String` | 消息所属对话 ID。 | -| `id` | `String` | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 | -| `timestamp` | `Date` | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 | -| `deliveredAt` | `Date` | 消息送达时间。 | -| `status` | `Symbol` | 消息状态,其值为枚举 [`MessageStatus`](https://leancloud.github.io/js-realtime-sdk/docs/module-leancloud-realtime.html#.MessageStatus) 的成员之一:

    `MessageStatus.NONE`(未知)
    `MessageStatus.SENDING`(发送中)
    `MessageStatus.SENT`(发送成功)
    `MessageStatus.DELIVERED`(已送达)
    `MessageStatus.FAILED`(失败) | - - -
    - -我们为每一种富媒体消息定义了一个消息类型,即时通讯 SDK 自身使用的类型是负数(如下面列表所示),所有正数留给开发者自定义扩展类型使用,`0` 作为「没有类型」被保留起来。 - -消息 | 类型 ---- | --- -文本消息 | `-1` -图像消息 | `-2` -音频消息 | `-3` -视频消息 | `-4` -位置消息 | `-5` -文件消息 | `-6` - -### 图像消息 - -#### 发送图像文件 - -即时通讯 SDK 支持直接通过二进制数据,或者本地图像文件的路径,来构造一个图像消息并发送到云端。其流程如下: - ->Local: 1. 获取图像实体内容 -Tom-->>Storage: 2. SDK 后台上传文件(LCFile)到云端 -Storage-->>Tom: 3. 返回图像的云端地址 -Tom-->>Cloud: 4. SDK 将图像消息发送给云端 -Cloud->>Jerry: 5. 收到图像消息,在对话框里面做 UI 展现 -`} /> - -图解: - -1. Local 可能是来自于 `localStorage`/`camera`,表示图像的来源可以是本地存储例如 iPhone 手机的媒体库或者直接调用相机 API 实时地拍照获取的照片。 -2. `LCFile` 是云服务提供的文件存储对象。 - -对应的代码并没有时序图那样复杂,因为调用 `send` 接口的时候,SDK 会自动上传图像,不需要开发者再去关心这一步: - - - -```cs -var image = new LCFile("screenshot.png", new Uri("http://example.com/screenshot.png")); -var imageMessage = new LCIMImageMessage(image); -imageMessage.Text = "发自我的 Windows"; -await conversation.Send(imageMessage); -``` -```java -LCFile file = LCFile.withAbsoluteLocalPath("San_Francisco.png", Environment.getExternalStorageDirectory() + "/San_Francisco.png"); -// 创建一条图像消息 -LCIMImageMessage m = new LCIMImageMessage(file); -m.setText("发自我的小米手机"); -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` -```objc -NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); -NSString *documentsDirectory = [paths objectAtIndex:0]; -NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:@"Tarara.png"]; -NSError *error; -LCFile *file = [LCFile fileWithLocalPath:imagePath error:&error]; -LCIMImageMessage *message = [LCIMImageMessage messageWithText:@"萌妹子一枚" file:file attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` -```js -// 图像消息等富媒体消息依赖存储 SDK 和富媒体消息插件, -// 具体的引用和初始化步骤请参考 SDK 配置指南 - -var fileUploadControl = $('#photoFileUpload')[0]; -var file = new AV.File('avatar.jpg', fileUploadControl.files[0]); -file.save().then(function() { - var message = new ImageMessage(file); - message.setText('发自我的 Ins'); - message.setAttributes({ location: '旧金山' }); - return conversation.send(message); -}).then(function() { - console.log('发送成功'); -}).catch(console.error.bind(console)); -``` -```swift -do { - if let imageFilePath = Bundle.main.url(forResource: "image", withExtension: "jpg")?.path { - let imageMessage = IMImageMessage(filePath: imageFilePath, format: "jpg") - try conversation.send(message: imageMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` -```dart -import 'package:flutter/services.dart' show rootBundle; - -// 假设项目根目录有 assets 文件夹存放图片,并且在 pubspec.yaml 中已经将 assets 文件夹添加到工程中。 -ByteData imageData = await rootBundle.load('assets/test.png'); -// image message -ImageMessage imageMessage = ImageMessage.from( - binaryData: imageData.buffer.asUint8List(), - format: 'png', - name: 'image.png', -); -try { - conversation.send(message: imageMessage); -} catch (e) { - print(e); -} -``` - - - -#### 发送图像链接 - -除了上述这种从本地直接发送图片文件的消息之外,在很多时候,用户可能从网络上或者别的应用中拷贝了一个图像的网络连接地址,当做一条图像消息发送到对话中,这种需求可以用如下代码来实现: - - - -```cs -var image = new LCFile("girl.gif", new Uri("http://example.com/girl.gif")); -var imageMessage = new LCIMImageMessage(image); -imageMessage.Text = "发自我的 Windows"; -await conversation.Send(imageMessage); -``` -```java -LCFile file = new LCFile("萌妹子","http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif", null); -LCIMImageMessage m = new LCIMImageMessage(file); -m.setText("萌妹子一枚"); -// 创建一条图像消息 -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` -```objc -// Tom 发了一张图片给 Jerry -LCFile *file = [LCFile fileWithURL:[self @"http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif"]]; -LCIMImageMessage *message = [LCIMImageMessage messageWithText:@"萌妹子一枚" file:file attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` -```js -var AV = require('leancloud-storage'); -var { ImageMessage } = require('leancloud-realtime-plugin-typed-messages'); -// 从网络链接直接构建一个图像消息 -var file = new AV.File.withURL('萌妹子', 'http://pic2.zhimg.com/6c10e6053c739ed0ce676a0aff15cf1c.gif'); -file.save().then(function() { - var message = new ImageMessage(file); - message.setText('萌妹子一枚'); - return conversation.send(message); -}).then(function() { - console.log('发送成功'); -}).catch(console.error.bind(console)); -``` -```swift -do { - if let url = URL(string: "http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif") { - let imageMessage = IMImageMessage(url: url, format: "gif") - try conversation.send(message: imageMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` -```dart -ImageMessage imageMessage = ImageMessage.from( - url: 'http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif', - format: 'png', - name: 'image.png', -); -try { - conversation.send(message: imageMessage); -} catch (e) { - print(e); -} -``` - - - -#### 接收图像消息 - -图像消息的接收机制和之前是一样的,只需要修改一下接收消息的事件回调逻辑,根据消息类型来做不同的 UI 展现即可,例如: - - - -```cs -client.OnMessage = (conv, msg) => { - if (e.Message is LCIMImageMessage imageMessage) { - WriteLine(imageMessage.Url); - } -} -``` -```java -LCIMMessageManager.registerMessageHandler(LCIMImageMessage.class, - new LCIMTypedMessageHandler() { - @Override - public void onMessage(LCIMImageMessage msg, LCIMConversation conv, LCIMClient client) { - // 只处理 Jerry 这个客户端的消息 - // 并且来自 conversationId 为 55117292e4b065f7ee9edd29 的 conversation 的消息 - if ("Jerry".equals(client.getClientId()) && "55117292e4b065f7ee9edd29".equals(conv.getConversationId())) { - String fromClientId = msg.getFrom(); - String messageId = msg.getMessageId(); - String url = msg.getFileUrl(); - Map metaData = msg.getFileMetaData(); - if (metaData.containsKey("size")) { - int size = (Integer) metaData.get("size"); - } - if (metaData.containsKey("width")) { - int width = (Integer) metaData.get("width"); - } - if (metaData.containsKey("height")) { - int height = (Integer) metaData.get("height"); - } - if (metaData.containsKey("format")) { - String format = (String) metaData.get("format"); - } - } - } -}); -``` -```objc -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - LCIMImageMessage *imageMessage = (LCIMImageMessage *)message; - - // 消息的 ID - NSString *messageId = imageMessage.messageId; - // 图像文件的 URL - NSString *imageUrl = imageMessage.file.url; - // 发该消息的 clientId - NSString *fromClientId = message.clientId; -} -``` -```js -var { Event, TextMessage } = require('leancloud-realtime'); -var { ImageMessage } = require('leancloud-realtime-plugin-typed-messages'); - -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - var file; - switch (message.type) { - case ImageMessage.TYPE: - file = message.getFile(); - console.log(' 收到图像消息,URL:' + file.url()); - break; - } -} -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - switch message { - case let imageMessage as IMImageMessage: - print(imageMessage) - default: - break - } - default: - break - } - default: - break - } -} -``` -```dart -lient.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - if (message is ImageMessage) { - print('收到图像消息,URL:${message.url}'); - } -}; -``` - - - -### 发送音频消息/视频/文件 - -#### 发送流程 - -对于图像、音频、视频和文件这四种类型的消息,SDK 均采取如下的发送流程: - -如果文件是从 **客户端 API 读取的数据流(Stream)**,步骤为: - -1. 从本地构造 `LCFile` -2. 调用 `LCFile` 的上传方法将文件上传到云端,并获取文件元信息(`metaData`) -3. 把 `LCFile` 的 `objectId`、URL、文件元信息都封装在消息体内 -4. 调用接口发送消息 - -如果文件是 **外部链接的 URL**,则: - -1. 直接将 URL 封装在消息体内,不获取元信息(例如,音频消息的时长),不包含 `objectId` -2. 调用接口发送消息 - -以发送音频消息为例,基本流程是:读取音频文件(或者录制音频)> 构建音频消息 > 消息发送。 - - - -```cs -var audio = new LCFile("tante.mp3", Path.Combine(Application.persistentDataPath, "tante.mp3")); -var audioMessage = new LCIMAudioMessage(audio); -audioMessage.Text = "听听人类的神曲"; -await conversation.Send(audioMessage); -``` -```java -LCFile file = LCFile.withAbsoluteLocalPath("忐忑.mp3",localFilePath); -LCIMAudioMessage m = new LCIMAudioMessage(file); -m.setText("听听人类的神曲"); -// 创建一条音频消息 -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` -```objc -NSError *error = nil; -LCFile *file = [AVFile fileWithLocalPath:localPath error:&error]; -if (!error) { - LCIMAudioMessage *message = [LCIMAudioMessage messageWithText:@"听听人类的神曲" file:file attributes:nil]; - [conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } - }]; -} -``` -```js -var AV = require('leancloud-storage'); -var { AudioMessage } = require('leancloud-realtime-plugin-typed-messages'); - -var fileUploadControl = $('#musicFileUpload')[0]; -var file = new AV.File('忐忑.mp3', fileUploadControl.files[0]); -file.save().then(function() { - var message = new AudioMessage(file); - message.setText('听听人类的神曲'); - return conversation.send(message); -}).then(function() { - console.log('发送成功'); -}).catch(console.error.bind(console)); -``` -```swift -do { - if let filePath = Bundle.main.url(forResource: "audio", withExtension: "mp3")?.path { - let audioMessage = IMAudioMessage(filePath: filePath, format: "mp3") - audioMessage.text = "听听人类的神曲" - try conversation.send(message: audioMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` -```dart -import 'package:flutter/services.dart' show rootBundle; - -// 假设项目根目录有 assets 文件夹存放 mp3 文件,并且在 pubspec.yaml 中已经将 assets 文件夹添加到工程中。 -ByteData audioData = await rootBundle.load('assets/test.mp3'); -AudioMessage audioMessage = AudioMessage.from( - binaryData: audioData.buffer.asUint8List(), - format: 'mp3', -); -audioMessage.text = '听听人类的神曲'; -try { - await conversation.send(message: audioMessage); -} catch (e) { - print(e); -} -``` - - - -与图像消息类似,音频消息也支持从 URL 构建: - - - -```cs -var audio = new LCFile("apple.aac", new Uri("https://some.website.com/apple.aac")); -var audioMessage = new LCIMAudioMessage(audio); -audioMessage.Text = "来自苹果发布会现场的录音"; -await conversation.Send(audioMessage); -``` -```java -LCFile file = new LCFile("apple.aac", "https://some.website.com/apple.aac", null); -LCIMAudioMessage m = new LCIMAudioMessage(file); -m.setText("来自苹果发布会现场的录音"); -conv.sendMessage(m, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` -```objc -LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"https://some.website.com/apple.aac"]]; -LCIMAudioMessage *message = [LCIMAudioMessage messageWithText:@"来自苹果发布会现场的录音" file:file attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` -```js -var AV = require('leancloud-storage'); -var { AudioMessage } = require('leancloud-realtime-plugin-typed-messages'); - -var file = new AV.File.withURL('apple.aac', 'https://some.website.com/apple.aac'); -file.save().then(function() { - var message = new AudioMessage(file); - message.setText('来自苹果发布会现场的录音'); - return conversation.send(message); -}).then(function() { - console.log('发送成功'); -}).catch(console.error.bind(console)); -``` -```swift -do { - if let url = URL(string: "https://some.website.com/apple.aac") { - let audioMessage = IMAudioMessage(url: url, format: "aac") - audioMessage.text = "来自苹果发布会现场的录音" - try conversation.send(message: audioMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } -} catch { - print(error) -} -``` -```dart -AudioMessage audioMessage = AudioMessage.from( - url: 'https://some.website.com/apple.aac', - name: 'apple.aac', -); -try { - await conversation.send(message: audioMessage); -} catch (e) { - print(e); -} -``` - - - -### 发送地理位置消息 - -地理位置消息构建方式如下: - - - -```cs -var location = new LCGeoPoint(31.3753285, 120.9664658); -var locationMessage = new LCIMLocationMessage(location); -await conversation.Send(locationMessage); -``` -```java -final LCIMLocationMessage locationMessage = new LCIMLocationMessage(); -// 开发者可以通过设备的 API 获取设备的具体地理位置,此处设置了 2 个经纬度常量作为演示 -locationMessage.setLocation(new LCGeoPoint(31.3753285,120.9664658)); -locationMessage.setText("蛋糕店的位置"); -conversation.sendMessage(locationMessage, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (null != e) { - e.printStackTrace(); - } else { - // 发送成功 - } - } -}); -``` -```objc -LCIMLocationMessage *message = [LCIMLocationMessage messageWithText:@"蛋糕店的位置" latitude:31.3753285 longitude:120.9664658 attributes:nil]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!"); - } -}]; -``` -```js -var AV = require('leancloud-storage'); -var { LocationMessage } = require('leancloud-realtime-plugin-typed-messages'); - -var location = new AV.GeoPoint(31.3753285, 120.9664658); -var message = new LocationMessage(location); -message.setText('蛋糕店的位置'); -conversation.send(message).then(function() { - console.log('发送成功'); -}).catch(console.error.bind(console)); -``` -```swift -do { - let locationMessage = IMLocationMessage(latitude: 31.3753285, longitude: 120.9664658) - try conversation.send(message: locationMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -LocationMessage locationMessage = LocationMessage.from( - latitude: 22, - longitude: 33, -); -try { - await conversation.send(message: locationMessage); -} catch (e) { - print(e); -} -``` - - - -### 再谈接收消息 - - -<> - -C# SDK 通过 `OnMessage` 事件回调来通知新消息: - -```cs -jerry.OnMessage = (conv, msg) => { - if (msg is LCIMImageMessage imageMessage) { - - } else if (msg is LCIMAudioMessage audioMessage) { - - } else if (msg is LCIMVideoMessage videoMessage) { - - } else if (msg is LCIMFileMessage fileMessage) { - - } else if (msg is AVIMLocationMessage locationMessage) { - - } else if (msg is InputtingMessage) { - WriteLine($"收到自定义消息 {inputtingMessage.TextContent} {inputtingMessage.Ecode}"); - } -} -``` - - -<> - -Java/Android SDK 中定义了 `LCIMMessageHandler` 接口来通知应用层新消息到达事件发生,开发者通过调用 `LCIMMessageManager.registerDefaultMessageHandler` 方法来注册自己的消息处理函数。`LCIMMessageManager` 提供了两个不同的方法来注册默认的消息处理函数,或特定类型的消息处理函数: - -```java -/** - * 注册默认的消息 handler - * - * @param handler - */ -public static void registerDefaultMessageHandler(LCIMMessageHandler handler); -/** - * 注册特定消息格式的处理单元 - * - * @param clazz 特定的消息类 - * @param handler - */ -public static void registerMessageHandler(Class clazz, MessageHandler handler); -/** - * 取消特定消息格式的处理单元 - * - * @param clazz - * @param handler - */ -public static void unregisterMessageHandler(Class clazz, MessageHandler handler); -``` - -消息处理函数需要在应用初始化时完成设置,理论上我们支持为每一种消息(包括应用层自定义的消息)分别注册不同的消息处理函数,并且也支持取消注册。 - -多次调用 `LCIMMessageManager` 的 `registerDefaultMessageHandler`,只有最后一次调用有效;而通过 `registerMessageHandler` 注册的 `LCIMMessageHandler`,则是可以同存的。 - -当客户端收到一条消息的时候,SDK 内部的处理流程为: - -- 首先解析消息的类型,然后找到开发者为这一类型所注册的处理响应 handler chain,再逐一调用这些 handler 的 `onMessage` 函数。 -- 如果没有找到专门处理这一类型消息的 handler,就会转交给 `defaultHandler` 处理。 - -这样一来,在开发者为 `AVIMTypedMessage`(及其子类)指定了专门的 handler,也指定了全局的 `defaultHandler` 了的时候,如果发送端发送的是通用的 `LCIMMessage` 消息,那么接收端就是 `LCIMMessageManager.registerDefaultMessageHandler()` 中指定的 handler 被调用;如果发送的是 `LCIMTypedMessage`(及其子类)的消息,那么接收端就是 `LCIMMessageManager#registerMessageHandler()` 中指定的 handler 被调用。 - -```java -// 1. 注册默认 handler,只有其他 handle 都没有被调用到时才会调用 -LCIMMessageManager.registerDefaultMessageHandler(new LCIMMessageHandler(){ - public void onMessage(LCIMMessage message, LCIMConversation conversation, LCIMClient client) { - // 接收消息 - } - - public void onMessageReceipt(LCIMMessage message, LCIMConversation conversation, LCIMClient client) { - // 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。 - // 因此别忘了在这里处理未知类型,例如提示用户升级客户端至最新版本。 - } -}); -// 2. 为每一种消息类型注册 handler -LCIMMessageManager.registerMessageHandler(LCIMTypedMessage.class, new LCIMTypedMessageHandler(){ - public void onMessage(LCIMTypedMessage message, LCIMConversation conversation, LCIMClient client) { - switch (message.getMessageType()) { - case LCIMMessageType.TEXT_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMTextMessage textMessage = (LCIMTextMessage)message; - break; - case LCIMMessageType.IMAGE_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMImageMessage imageMessage = (LCIMImageMessage)message; - break; - case LCIMMessageType.AUDIO_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMAudioMessage audioMessage = (LCIMAudioMessage)message; - break; - case LCIMMessageType.VIDEO_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMVideoMessage videoMessage = (LCIMVideoMessage)message; - break; - case LCIMMessageType.LOCATION_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMLocationMessage locationMessage = (LCIMLocationMessage)message; - break; - case LCIMMessageType.FILE_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMFileMessage fileMessage = (LCIMFileMessage)message; - break; - case LCIMMessageType.RECALLED_MESSAGE_TYPE: - // 执行其他逻辑 - LCIMRecalledMessage recalledMessage = (LCIMRecalledMessage)message; - break; - case 123: - // 这是一个自定义消息类型 - // 执行其他逻辑 - CustomMessage customMessage = (CustomMessage)message; - break; - } - } - - public void onMessageReceipt(LCIMTypedMessage message, LCIMConversation conversation, LCIMClient client) { - // 执行收到消息后的逻辑 - } -}); -``` - - -<> - -Objective-C SDK 是通过实现 `LCIMClientDelegate` 代理来响应新消息到达通知的,并且,分别使用了两个方法来分别处理普通的 `LCIMMessage` 消息和内建的多媒体消息 `LCIMTypedMessage`(包括应用层由此派生的自定义消息: - -```objc -/*! - 接收到新的普通消息。 - @param conversation - 所属对话 - @param message - 具体的消息 - */ -- (void)conversation:(LCIMConversation *)conversation didReceiveCommonMessage:(LCIMMessage *)message; - -/*! - 接收到新的富媒体消息。 - @param conversation - 所属对话 - @param message - 具体的消息 - */ -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message; -``` - -```objc -// 处理默认类型消息 -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - if (message.mediaType == kLCIMMessageMediaTypeImage) { - LCIMImageMessage *imageMessage = (LCIMImageMessage *)message; // 处理图像消息 - } else if(message.mediaType == kLCIMMessageMediaTypeAudio){ - // 处理音频消息 - } else if(message.mediaType == kLCIMMessageMediaTypeVideo){ - // 处理视频消息 - } else if(message.mediaType == kLCIMMessageMediaTypeLocation){ - // 处理位置消息 - } else if(message.mediaType == kLCIMMessageMediaTypeFile){ - // 处理文件消息 - } else if(message.mediaType == kLCIMMessageMediaTypeText){ - // 处理文本消息 - } else if(message.mediaType == 123){ - // 处理自定义的消息类型 - } -} - -// 处理未知消息类型 -- (void)conversation:(LCIMConversation *)conversation didReceiveCommonMessage:(LCIMMessage *)message { - // 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。 - // 因此别忘了在这里处理未知类型,例如提示用户升级客户端至最新版本。 -} -``` - - -<> - -Swift SDK 是通过实现 `IMClientDelegate` 代理来响应新消息到达通知的: - -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - print(message) - default: - break - } - default: - break - } -} -``` - -```swift -// 处理默认类型消息 -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - if let categorizedMessage = message as? IMCategorizedMessage { - switch categorizedMessage { - case let textMessage as IMTextMessage: - print(textMessage) - case let imageMessage as IMImageMessage: - print(imageMessage) - case let audioMessage as IMAudioMessage: - print(audioMessage) - case let videoMessage as IMVideoMessage: - print(videoMessage) - case let fileMessage as IMFileMessage: - print(fileMessage) - case let locationMessage as IMLocationMessage: - print(locationMessage) - case let recalledMessage as IMRecalledMessage: - print(recalledMessage) - case let customMessage as CustomMessage: - print("customMessage 是自定义消息类型") - default: - break - } else { - // 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。 - // 因此别忘了在默认分支中处理未知类型,例如提示用户升级客户端至最新版本。 - print("收到未知类型消息") - } - default: - break - } - default: - break - } -} -``` - - -<> - -```dart -jerry.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - if (message.binaryContent != null) { - print('收到二进制消息:${message.binaryContent.toString()}'); - } else if (message is TextMessage) { - print('收到文本类型消息:${message.text}'); - } else if (message is LocationMessage) { - print('收到地理位置消息,坐标:${message.latitude},${message.longitude}'); - } else if (message is FileMessage) { - if (message is ImageMessage) { - print('收到图像消息,图像 URL:${message.url}'); - } else if (message is AudioMessage) { - print('收到音频消息,消息时长:${message.duration}'); - } else if (message is VideoMessage) { - print('收到视频消息,消息时长:${message.duration}'); - } else { - print('收到 .txt/.doc/.md 等各种类型的普通文件消息,URL:${message.url}'); - } - } else if (message is CustomMessage) { - // CustomMessage 是自定义的消息类型 - print('收到自定义类型消息'); - } else { - // 这里可以继续添加自定义类型的判断条件 - print('收到未知消息类型'); - if (message.stringContent != null) { - print('收到普通消息:${message.stringContent}'); - } - } -}; -``` - - -<> - -不管消息类型如何,JavaScript SDK 都是是通过 `IMClient` 上的 `Event.MESSAGE` 事件回调来通知新消息的,应用层只需要在一个地方,统一对不同类型的消息使用不同方式来处理即可。 - -```js -// 在初始化 Realtime 时,需加载 TypedMessagesPlugin -var { Event, TextMessage } = require('leancloud-realtime'); -var { FileMessage, ImageMessage, AudioMessage, VideoMessage, LocationMessage } = require('leancloud-realtime-plugin-typed-messages'); -// 注册 MESSAGE 事件的 handler -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - // 请按自己需求改写 - var file; - switch (message.type) { - case TextMessage.TYPE: - console.log('收到文本消息,内容:' + message.getText() + ',ID:' + message.id); - break; - case FileMessage.TYPE: - file = message.getFile(); // file 是 AV.File 实例 - console.log('收到文件消息,URL:' + file.url() + ',大小:' + file.metaData('size')); - break; - case ImageMessage.TYPE: - file = message.getFile(); - console.log('收到图像消息,URL:' + file.url() + ',宽度:' + file.metaData('width')); - break; - case AudioMessage.TYPE: - file = message.getFile(); - console.log('收到音频消息,URL:' + file.url() + ',长度:' + file.metaData('duration')); - break; - case VideoMessage.TYPE: - file = message.getFile(); - console.log('收到视频消息,URL:' + file.url() + ',长度:' + file.metaData('duration')); - break; - case LocationMessage.TYPE: - var location = message.getLocation(); - console.log('收到位置消息,纬度:' + location.latitude + ',经度:' + location.longitude); - break; - case 1: - console.log('OperationMessage 是自定义消息类型'); - default: - // 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。 - // 因此别忘了在默认分支中处理未知类型,例如提示用户升级客户端至最新版本。 - console.warn('收到未知类型消息'); - } -}); - -// 同时,对应的 conversation 上也会派发 `MESSAGE` 事件: -conversation.on(Event.MESSAGE, function messageEventHandler(message) { - // 这里补充业务逻辑 -}); -``` - - - - - -上面的代码示例中涉及到接受自定义消息。 -我们将在[即时通讯开发指南第二篇](/v2/sdk/im/guide/intermediate)的《自定义消息类型》一节介绍。 - -## 扩展对话:支持自定义属性 - -「对话(`Conversation`)」是即时通讯的核心逻辑对象,它有一些内置的常用的属性,与控制台中 `_Conversation` 表是一一对应的。默认提供的 **内置** 属性的对应关系如下: - - - -| `AVIMConversation` 属性名 | `_Conversation` 字段 | 含义 | -| --- | --- | --- | -| `CurrentClient` | N/A | 对话所属的 `AVIMClient` 对象 | -| `ConversationId` | `objectId` | 全局唯一的 ID | -| `Name` | `name` | 成员共享的统一的名字 | -| `MemberIds` | `m` | 成员列表 | -| `MuteMemberIds` | `mu` | 静音该对话的成员 | -| `Creator` | `c` | 对话创建者 | -| `IsTransient` | `tr` | 是否为聊天室 | -| `IsSystem` | `sys` | 是否为系统对话 | -| `IsUnique` | `unique` | 是否为相同成员的唯一对话 | -| `IsTemporary` | N/A | 是否为临时对话(临时对话数据不保存到 `_Conversation` 表中 ) | -| `CreatedAt` | `createdAt` | 创建时间 | -| `UpdatedAt` | `updatedAt` | 最后更新时间 | -| `LastMessageAt` | `lm` | 该对话最后一条消息,也可以理解为最后一次活跃时间 | - -| `LCIMConversation` get 方法名 | `_Conversation` 字段 | 含义 | -| --- | --- | --- | -| `getAttributes` | `attr` | 自定义属性 | -| `getConversationId` | `objectId` | 全局唯一的 ID | -| `getCreatedAt` | `createdAt` | 创建时间 | -| `getCreator` | `c` | 对话创建者 | -| `getLastDeliveredAt` | N/A | (仅限单聊)最后一条已送达对方的消息时间 | -| `getLastMessage` | N/A | 最后一条消息,可能会空 | -| `getLastMessageAt` | `lm` | 该对话最后一条消息,也可以理解为最后一次活跃时间 | -| `getLastReadAt` | N/A | (仅限单聊)最后一条对方已读的消息时间 | -| `getMembers` | `m` | 成员列表 | -| `getName` | `name` | 成员共享的统一的名字 | -| `getTemporaryExpiredat` | N/A | 临时对话存活时间 | -| `getUniqueId` | `uniqueId` | `Unique Conversation` 全局唯一的 `ID` | -| `getUnreadMessagesCount` | N/A | 未读消息数 | -| `getUpdatedAt` | `updatedAt` | 最后更新时间 | -| `isSystem` | `sys` | 是否为系统对话 | -| `isTemporary` | N/A | 是否为临时对话(临时对话数据不保存到 `_Conversation` 表中 ) -| `isTransient` | `tr` | 是否为聊天室 | -| `isUnique` | `unique` | 是否是 `Unique Conversation` | -| `unreadMessagesMentioned` | N/A | 未读消息是否 @ 了当前的 `Client` | - -| `LCIMConversation` 属性名 | `_Conversation` 字段 | 含义 | -| --- | --- | --- | -| `clientID` | N/A | 会话所属的 `Client` 的 `ID` | -| `conversationId` | `objectId` | 全局唯一的 ID | -| `creator` | `c` | 对话创建者 | -| `createdAt` | `createdAt` | 创建时间 | -| `updatedAt` | `updatedAt` | 最后更新时间 | -| `lastMessage` | N/A | 最后一条消息,可能会空 | -| `lastMessageAt` | `lm` | 最后一条消息发送时间,也可以理解为最后一次活跃时间 | -| `lastReadAt` | N/A | (仅限单聊)最后一条对方已读的消息时间 | -| `lastDeliveredAt` | N/A | (仅限单聊)最后一条已送达对方的消息时间 | -| `unreadMessagesCount` | N/A | 未读消息数 | -| `unreadMessageContainMention` | N/A | 未读消息是否 @ 了当前的 `Client` | -| `name` | `name` | 成员共享的统一的名字 | -| `members` | `m` | 成员列表 | -| `attributes` | `attr` | 自定义属性 | -| `uniqueId` | `uniqueId` | `Unique Conversation` 全局唯一的 `ID` | -| `unique` | `unique` | 是否是 `Unique Conversation` | -| `transient` | `tr` | 是否为暂态会话 | -| `system` | `sys` | 是否为系统对话 | -| `temporary` | N/A | 是否为临时对话(临时对话数据不保存到 `_Conversation` 表中 ) -| `temporaryTTL` | N/A | 临时对话存活时间 | -| `muted` | N/A | 当前用户是否静音该对话 | -| `imClient` | N/A | 对话所属的 `LCIMClient` 对象 | - -| `IMConversation` 属性名 | `_Conversation` 字段 | 含义 | -| --- | --- | --- | -| `client` | N/A | 会话所属的 `Client` | -| `ID` | `objectId` | 会话的全局唯一 `ID` | -| `clientID` | N/A | 会话所属的 `Client` 的 `ID` | -| `isUnique` | `unique` | 是否是 `Unique Conversation` | -| `uniqueID` | `uniqueId` | `Unique Conversation` 全局唯一的 `ID` | -| `name` | `name` | 会话的名称 | -| `creator` | `c` | 会话的创建者 | -| `createdAt` | `createdAt` | 会话的创建时间 | -| `updatedAt` | `updatedAt` | 会话的最后更新时间 | -| `attributes` | `attr` | 会话的自定义属性 | -| `members` | `m` | 会话的成员列表 | -| `isMuted` | N/A | 当前用户是否静音该对话 | -| `isOutdated` | N/A | 会话的属性是否过期,可以根据该属性来决定是否更新会话的数据 | -| `lastMessage` | N/A | 最新一条消息,可能会空 | -| `unreadMessageCount` | N/A | 未读消息数 | -| `isUnreadMessageContainMention` | N/A | 未读消息是否 @ 了当前的 `Client` | -| `memberInfoTable` | N/A | 成员信息表 | - -| `Conversation` 属性名 | `_Conversation` 字段 | 含义 | -| --- | --- | --- | -| `attributes` | `attr` | 自定义属性 | -| `client` | N/A | 对话所属的 `Client` 对象 | -| `createdAt` | `createdAt` | 创建时间 | -| `creator` | `c` | 对话创建者 | -| `id` | `objectId` | 全局唯一的 ID | -| `isMuted` | N/A | 当前用户是否静音该对话 | -| `isUnique` | `unique` | 是否是 `Unique Conversation` | -| `lastDeliveredAt` | N/A | (仅限单聊)最后一条已送达对方的消息时间 | -| `lastMessage` | N/A | 最后一条消息,可能会空 | -| `lastReadAt` | N/A | (仅限单聊)最后一条对方已读的消息时间 | -| `members` | `m` | 成员列表 | -| `name` | `name` | 成员共享的统一的名字 | -| `uniqueID` | `uniqueId` | `Unique Conversation` 的唯一 ID | -| `unreadMessagesCount` | N/A | 未读消息数 | -| `unreadMessagesMentioned` | N/A | 未读消息是否 @ 了当前的 `Client` | -| `updatedAt` | `updatedAt` | 最后更新时间 | - -| `Conversation` 属性名 | `_Conversation` 字段 | 含义 | -| --- | --- | --- | -| `createdAt` | `createdAt` | 创建时间 | -| `creator` | `c` | 对话创建者 | -| `id` | `objectId` | 全局唯一的 ID | -| `lastDeliveredAt` | N/A | (仅限单聊)最后一条已送达对方的消息时间 | -| `lastMessage` | N/A | 最后一条消息,可能会空 | -| `lastMessageAt` | `lm` | 最后一条消息发送时间,也可以理解为最后一次活跃时间 | -| `lastReadAt` | N/A | (仅限单聊)最后一条对方已读的消息时间 | -| `members` | `m` | 成员列表 | -| `muted` | N/A | 当前用户是否静音该对话 | -| `mutedMembers` | `mu` | 静音该对话的成员 | -| `name` | `name` | 成员共享的统一的名字 | -| `system` | `sys` | 是否为服务号(系统对话) | -| `transient` | `tr` | 是否为暂态会话 | -| `unreadMessagesCount` | N/A | 未读消息数 | -| `updatedAt` | `updatedAt` | 最后更新时间 | - - - -不过,我们不建议直接对 `_Conversation` 进行写操作,因为: - -- 客户端 SDK 查询会话数据是走 websocket 长连接,会首先从即时通讯服务器的内存缓存中查。直接操作 `_Conversation` 表,不会更新即时通讯服务器的缓存,这就带来了缓存不一致问题。 -- 直接操作 `_Conversation` 表的情况下,即时通讯服务器不会下发相应的事件通知客户端,客户端自然也就无从响应。 -- 如果定义了即时通讯服务的 hook 函数,直接操作 `_Conversation` 表不会触发这些 hook。 - -如有管理需求,我们推荐调用专门的即时通讯 REST API 接口。 - -另外,我们可以通过「自定义属性」来在「对话」中保存更多业务层数据。 - -### 创建自定义属性 - -在最开始介绍 [创建单聊对话](#创建对话-Conversation) 的时候,我们提到过 `IMClient#createConversation` 接口支持附加自定义属性,现在我们就来演示一下如何使用自定义属性。 - -假如在创建对话的时候,我们需要添加两个额外的属性值对 `{ "type": "private", "pinned": true }`,那么在调用 `IMClient#createConversation` 方法时可以把附加属性传进去: - - - -```cs -var properties = new Dictionary { - { "type", "private" }, - { "pinned", true } -}; -var conversation = await tom.CreateConversation("Jerry", name: "Tom & Jerry", unique: true, properties: properties); -``` -```java -HashMap attr = new HashMap(); -attr.put("type","private"); -attr.put("pinned",true); -client.createConversation(Arrays.asList("Jerry"),"猫和老鼠", attr, false, true, - new LCIMConversationCreatedCallback(){ - @Override - public void done(LCIMConversation conv,LCIMException e){ - if(e==null){ - // 创建成功 - } - } - }); -``` -```objc -// Tom 创建名称为「猫和老鼠」的会话,并附加会话属性 -LCIMConversationCreationOption *option = [LCIMConversationCreationOption new]; -option.name = @"猫和老鼠"; -option.attributes = @{ - @"type": @"private", - @"pinned": @(YES) -}; -[self createConversationWithClientIds:@[@"Jerry"] option:option callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"创建成功!"); - } -}]; -``` -```js -tom.createConversation({ - members: ['Jerry'], - name: '猫和老鼠', - unique: true, - type: 'private', - pinned: true, -}).then(function(conversation) { - console.log('创建成功。ID:' + conversation.id); -}).catch(console.error.bind(console)); -``` -```swift -do { - try tom.createConversation(clientIDs: ["Jerry"], name: "猫和老鼠", attributes: ["type": "private", "pinned": true], isUnique: true, completion: { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - Conversation conversation = await jerry.createConversation( - members: {'client1.id', 'client2.id'}, - attributes: { - 'members': ['Jerry'], - 'name': '猫和老鼠', - 'unique': true, - 'type': 'private', - 'pinned': true, - }, - ); -} catch (e) { - print(e); -} -``` - - - -**自定义属性在 SDK 级别是对所有成员可见的。**我们也支持通过自定义属性来查询对话,请参见 [使用复杂条件来查询对话](#使用复杂条件来查询对话)。 - -### 修改和使用属性 - -在 `Conversation` 对象中,系统默认提供的属性,例如对话的名字(`name`),如果业务层没有限制的话,所有成员都是可以修改的,示例代码如下: - - - -```cs -await conversation.UpdateInfo(new Dictionary { - { "name", "聪明的喵星人" } -}); -``` -```java -LCIMConversation conversation = client.getConversation("55117292e4b065f7ee9edd29"); -conversation.setName("聪明的喵星人"); -conversation.updateInfoInBackground(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // 更新成功 - } - } -}); -``` -```objc -conversation[@"name"] = @"聪明的喵星人"; -[conversation updateWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"修改成功!"); - } -}]; -``` -```js -conversation.name = '聪明的喵星人'; -conversation.save(); -``` -```swift -do { - try conversation.update(attribution: ["name": "聪明的喵星人"], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - await conversation.updateInfo(attributes: { - 'name': '聪明的喵星人', - }); -} catch (e) { - print(e); -} -``` - - - -而 `Conversation` 对象中自定义的属性,即时通讯服务也是允许对话内其他成员来读取、使用和修改的,示例代码如下: - - - -```cs -// 获取自定义属性 -var type = conversation["type"]; -// 为 pinned 属性设置新的值 -await conversation.UpdateInfo(new Dictionary { - { "pinned", false } -}); -``` -```java -// 获取自定义属性 -String type = conversation.get("attr.type"); -// 为 pinned 属性设置新的值 -conversation.set("attr.pinned",false); -// 保存 -conversation.updateInfoInBackground(new LCIMConversationCallback(){ - @Override - public void done(LCIMException e){ - if(e==null){ - // 更新成功 - } - } -}); -``` -```objc -// 获取自定义属性 -NSString *type = conversation.attributes[@"type"]; -// 为 pinned 属性设置新的值 -[conversation setObject:@(NO) forKey:@"attr.pinned"]; -// 保存 -[conversation updateWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"修改成功!"); - } -}]; -``` -```js -// 获取自定义属性 -var type = conversation.get('attr.type'); -// 为 pinned 属性设置新的值 -conversation.set('attr.pinned',false); -// 保存 -conversation.save(); -``` -```swift -do { - let type = conversation.attributes?["type"] as? String - try conversation.update(attribution: ["attr.pinned": false]) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -try { -// 获取自定义属性 - String type = conversation.attributes['type']; -// 为 pinned 属性设置新的值 - await conversation.updateInfo(attributes: { - 'pinned': false, - }); -} catch (e) { - print(e); -} -``` - - - -对自定义属性名的说明 - -在 `IMClient#createConversation` 接口中指定的自定义属性,会被存入 `_Conversation` 表的 `attr` 字段,所以在之后对这些属性进行读取或修改的时候,属性名需要指定完整的路径,例如上面的 `attr.type`,这一点需要特别注意。 - -### 对话属性同步 - -对话的名字以及应用层附加的其他属性,一般都是需要全员共享的,一旦有人对这些数据进行了修改,那么就需要及时通知到全部成员。在前一个例子中,有一个用户对话名称改为了「聪明的喵星人」,那其他成员怎么能知道这件事情呢? - -即时通讯云端提供了实时同步的通知机制,会把单个用户对「对话」的修改同步下发到所有在线成员(对于非在线的成员,他们下次登录上线之后,自然会拉取到最新的完整的对话数据)。对话属性更新的通知事件声明如下: - - - -```cs -jerry.OnConversationInfoUpdated = (conv, attrs, initBy) => { - WriteLine($"对话:${conv.Id} 被更新"); -}; -``` -```java -// 在 LCIMConversationEventHandler 接口中有如下定义 -/** - * 对话自身属性变更通知 - * - * @param client - * @param conversation - * @param attr 被更新的属性 - * @param operator 该操作的发起者 ID - */ -public void onInfoChanged(LCIMClient client, LCIMConversation conversation, JSONObject attr, - String operator) -``` -```objc -/// Notification for conversation's attribution updated. -/// @param conversation Updated conversation. -/// @param date Updated date. -/// @param clientId Client ID which do this update. -/// @param updatedData Updated data. -/// @param updatingData Updating data. -- (void)conversation:(LCIMConversation *)conversation didUpdateAt:(NSDate * _Nullable)date byClientId:(NSString * _Nullable)clientId updatedData:(NSDictionary * _Nullable)updatedData updatingData:(NSDictionary * _Nullable)updatingData; -``` -```js -/** - * 对话信息被更新 - * @event IMClient#CONVERSATION_INFO_UPDATED - * @param {Object} payload - * @param {Object} payload.attributes 被更新的属性 - * @param {String} payload.updatedBy 该操作的发起者 ID - */ -var { Event } = require('leancloud-realtime'); -client.on(Event.CONVERSATION_INFO_UPDATED, function(payload) { -}); -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case let .dataUpdated(updatingData: updatingData, updatedData: updatedData, byClientID: byClientID, at: atDate): - print(updatingData) - print(updatedData) - print(byClientID) - print(atDate) - default: - break - } -} -``` -```dart -jerry.onInfoUpdated = ({ - Client client, - Conversation conversation, - Map updatingAttributes, - Map updatedAttributes, - String byClientID, - DateTime atDate, -}) { - print('会话:${conversation.id} 被更新'); -}; -``` - - - -使用提示: - -应用层在该事件的响应函数中,可以获知当前什么属性被修改了,也可以直接从 SDK 的 `Conversation` 实例中获取最新的合并之后的属性值,然后依据需要来更新产品 UI。 - -### 获取群内成员列表 - -群内成员列表是作为对话的属性持久化保存在云端的,所以要获取一个 `Conversation` 对象的成员列表,我们可以在调用这个对象的更新方法之后,直接获取成员属性即可。 - - - -```cs -await conversation.Fetch(); -``` -```java -// fetchInfoInBackground 方法会执行一次刷新操作,以获取云端最新对话数据。 -conversation.fetchInfoInBackground(new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - conversation.getMembers(); - } - } -}); -``` -```objc -// fetchWithCallback 方法会执行一次刷新操作,以获取云端最新对话数据。 -[conversation fetchWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"", conversation.members); - } -}]; -``` -```js -// fetch 方法会执行一次刷新操作,以获取云端最新对话数据。 -conversation.fetch().then(function(conversation) { - console.log('members: ', conversation.members); -).catch(console.error.bind(console)); -``` -```swift -do { - try conversation.refresh { (result) in - switch result { - case .success: - if let members = conversation.members { - print(members) - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -// 暂不支持 -``` - - - -使用提示: - -成员列表是对 ***普通对话*** 而言的,对于像「聊天室」「系统对话」这样的特殊对话,并不存在「成员列表」属性。 - -## 使用复杂条件来查询对话 - -除了在事件通知接口中获得 `Conversation` 实例之外,开发者也可以根据不同的属性和条件来查询 `Conversation` 对象。例如有些产品允许终端用户根据名字或地理位置来匹配感兴趣聊天室,也有些业务场景允许查询成员列表中包含特定用户的所有对话,这些都可以通过对话查询的接口实现。 - -### 根据 ID 查询 - -ID 对应就是 `_Conversation` 表中的 `objectId` 的字段值,这是一种最简单也最高效的查询(因为云端会对 ID 建立索引): - - - -```cs -var query = tom.GetQuery(); -var conversation = await query.Get("551260efe4b01608686c3e0f"); -``` -```java -LCIMConversationsQuery query = tom.getConversationsQuery(); -query.whereEqualTo("objectId","551260efe4b01608686c3e0f"); -query.findInBackground(new LCIMConversationQueryCallback(){ - @Override - public void done(List convs,LCIMException e){ - if(e==null){ - if(convs!=null && !convs.isEmpty()){ - // convs.get(0) 就是想要的 conversation - } - } - } -}); -``` -```objc -LCIMConversationQuery *query = [tom conversationQuery]; -[query getConversationById:@"551260efe4b01608686c3e0f" callback:^(LCIMConversation *conversation, NSError *error) { - if (succeeded) { - NSLog(@"查询成功!"); - } -}]; -``` -```js -tom.getConversation('551260efe4b01608686c3e0f').then(function(conversation) { - console.log(conversation.id); -}).catch(console.error.bind(console)); -``` -```swift -do { - let conversationQuery = tom.conversationQuery - try conversationQuery.getConversation(by: "551260efe4b01608686c3e0f") { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -// 我们建议开发者首先尝试从内存中获取对话,以减少不必要的网络请求。 - -String convID = '551260efe4b01608686c3e0f'; -Conversation conversation = tom.conversationMap[convID]; -if (conversation == null) { - try { - ConversationQuery query = tom.conversationQuery(); - query.whereEqualTo('objectId', convID); - conversation = await query.find(); - } catch (e) { - print(e); - } -} -``` - - - -### 基础的条件查询 - -即时通讯 SDK 提供了丰富的条件查询方式,可以满足各种复杂的业务需求。 - -我们首先从最简单的 `equalTo` 开始。例如查询所有自定义属性 `type`(字符串类型)为 `private` 的对话,需要如下代码: - - - -```cs -var query = tom.GetQuery() - .WhereEqualTo("type", "private"); -await query.Find(); -``` -```java -LCIMConversationsQuery query = tom.getConversationsQuery(); -query.whereEqualTo("attr.type","private"); -// 执行查询 -query.findInBackground(new LCIMConversationQueryCallback(){ - @Override - public void done(List convs,LCIMException e){ - if(e == null){ - // convs 就是想要的结果 - } - } -}); -``` -```objc -LCIMConversationQuery *query = [tom conversationQuery]; -[query whereKey:@"attr.type" equalTo:@"private"]; -// 执行查询 -[query findConversationsWithCallback:^(NSArray *objects, NSError *error) { - NSLog(@"找到 %ld 个对话!", [objects count]); -}]; -``` -```js -var query = client.getQuery(); -query.equalTo('attr.type','private'); -query.find().then(function(conversations) { - // conversations 就是想要的结果 -}).catch(console.error.bind(console)); -``` -```swift -do { - let conversationQuery = tom.conversationQuery - try conversationQuery.where("attr.type", .equalTo("private")) - try conversationQuery.findConversations { (result) in - switch result { - case .success(value: let conversations): - print(conversations) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -try { - ConversationQuery query = jerry.conversationQuery(); - query.whereEqualTo('attr.type', 'private'); -// conversations 就是想要的结果 - List conversations = await query.find(); -} catch (e) { - print(e); -} -``` - - - -熟悉数据存储服务的开发者可以更容易理解对话的查询构建,因为对话查询和数据存储服务的对象查询在接口上是十分接近的: - -- 可以通过 `find` 获取当前结果页数据 -- 支持通过 `count` 获取结果数 -- 支持通过 `first` 获取第一个结果 -- 支持通过 `skip` 和 `limit` 对结果进行分页 - -与 `equalTo` 类似,针对 `Number` 和 `Date` 类型的属性还可以使用大于、大于等于、小于、小于等于等,详见下表: - - - -| 逻辑比较 | `AVIMConversationQuery` 方法 | -| --- | --- | -| 等于 | `WhereEqualTo` | -| 不等于 | `WhereNotEqualsTo` | -| 大于 | `WhereGreaterThan` | -| 大于等于 | `WhereGreaterThanOrEqualsTo` | -| 小于 | `WhereLessThan` | -| 小于等于 | `WhereLessThanOrEqualsTo` | - -| 逻辑比较 | `LCIMConversationsQuery` 方法 | -| --- | --- | -| 等于 | `whereEqualTo` | -| 不等于 | `whereNotEqualsTo` | -| 大于 | `whereGreaterThan` | -| 大于等于 | `whereGreaterThanOrEqualsTo` | -| 小于 | `whereLessThan` | -| 小于等于 | `whereLessThanOrEqualsTo` | - -| 逻辑比较 | `LCIMConversationQuery` 方法 | -| --- | --- | -| 等于 | `equalTo` | -| 不等于 | `notEqualTo` | -| 大于 | `greaterThan` | -| 大于等于 | `greaterThanOrEqualTo` | -| 小于 | `lessThan` | -| 小于等于 | `lessThanOrEqualTo` | - -| 逻辑比较 | `IMConversationQuery` 的 `Constraint` | -| --- | --- | -| 等于 | `equalTo` | -| 不等于 | `notEqualTo` | -| 大于 | `greaterThan` | -| 大于等于 | `greaterThanOrEqualTo` | -| 小于 | `lessThan` | -| 小于等于 | `lessThanOrEqualTo` | - -| 逻辑比较 | `ConversationQuery` 方法 | -| --- | --- | -| 等于 | `equalTo` | -| 不等于 | `notEqualTo` | -| 大于 | `greaterThan` | -| 大于等于 | `greaterThanOrEqualTo` | -| 小于 | `lessThan` | -| 小于等于 | `lessThanOrEqualTo` | - - - -使用注意:默认查询条件 - -为了防止用户无意间拉取到所有的对话数据,在客户端不指定任何 `where` 条件的时候,`ConversationQuery` 会默认查询包含当前用户的对话。如果客户端添加了任一 `where` 条件,那么 `ConversationQuery` 会忽略默认条件而严格按照指定的条件来查询。如果客户端要查询包含某一个 `clientId` 的对话,那么使用下面的 [数组查询](#数组查询) 语法对 `m` 属性列和 `clientId` 值进行查询即可,不会和默认查询条件冲突。 - -### 正则匹配查询 - -`ConversationsQuery` 也支持在查询条件中使用正则表达式来匹配数据。比如要查询所有 `language` 是中文的对话: - - - -```cs -query.WhereMatches("language", "[\\u4e00-\\u9fa5]"); // language 是中文字符 -``` -```java -query.whereMatches("language","[\\u4e00-\\u9fa5]"); // language 是中文字符 -``` -```objc -[query whereKey:@"language" matchesRegex:@"[\\u4e00-\\u9fa5]"]; // language 是中文字符 -``` -```js -query.matches('language',/[\\u4e00-\\u9fa5]/); // language 是中文字符 -``` -```swift -try conversationQuery.where("language", .matchedRegularExpression("[\\u4e00-\\u9fa5]", option: nil)) -``` -```dart -// 暂不支持 -``` - - - -### 字符串查询 - -**前缀查询** 类似于 SQL 的 `LIKE 'keyword%'` 条件。例如查询名字以「教育」开头的对话: - - - -```cs -query.WhereStartsWith("name", "教育"); -``` -```java -query.whereStartsWith("name","教育"); -``` -```objc -[query whereKey:@"name" hasPrefix:@"教育"]; -``` -```js -query.startsWith('name','教育'); -``` -```swift -try conversationQuery.where("name", .prefixedBy("教育")) -``` -```dart -// 暂不支持 -``` - - - -**包含查询** 类似于 SQL 的 `LIKE '%keyword%'` 条件。 -例如查询名字中包含「教育」的对话: - - - -```cs -query.WhereContains("name", "教育"); -``` -```java -query.whereContains("name","教育"); -``` -```objc -[query whereKey:@"name" containsString:@"教育"]; -``` -```js -query.contains('name',' 教育 '); -``` -```swift -try conversationQuery.where("name", .matchedSubstring("教育")) -``` -```dart -// 暂不支持 -``` - - - -**不包含查询** 则可以使用 [正则匹配查询](#正则匹配查询) 来实现。 -例如查询名字中不包含「教育」的对话: - - - -```cs -query.WhereMatches("name", "^((?!教育).)* $ "); -``` -```java -query.whereMatches("name","^((?!教育).)* $ "); -``` -```objc -[query whereKey:@"name" matchesRegex:@"^((?!教育).)* $ "]; -``` -```js -var regExp = new RegExp('^((?!教育).)*$', 'i'); -query.matches('name', regExp); -``` -```swift -try conversationQuery.where("name", .matchedRegularExpression("^((?!教育).)* $ ", option: nil)) -``` -```dart -// 暂不支持 -``` - - - -### 数组查询 - -可以使用 `containsAll`、`containedIn`、`notContainedIn` 来对数组进行查询。例如查询成员中包含「Tom」的对话: - - - -```cs -var members = new List { "Tom" }; -query.WhereContainedIn("m", members); -``` -```java -query.whereContainedIn("m", Arrays.asList("Tom")); -``` -```objc -[query whereKey:@"m" containedIn:@[@"Tom"]]; -``` -```js -query.containedIn('m', ['Tom']); -``` -```swift -try conversationQuery.where("m", .containedIn(["Tom"])) -``` -```dart -// 暂不支持 -``` - - - -### 空值查询 - -空值查询是指查询相关列是否为空值的方法,例如要查询 `lm` 列为空值的对话: - - - -```cs -query.WhereDoesNotExist("lm"); -``` -```java -query.whereDoesNotExist("lm"); -``` -```objc -[query whereKeyDoesNotExist:@"lm"]; -``` -```js -query.doesNotExist('lm') -``` -```swift -try conversationQuery.where("lm", .notExisted) -``` -```dart -// 暂不支持 -``` - - - -反过来,如果要查询 `lm` 列不为空的对话,则替换为如下条件即可: - - - -```cs -query.WhereExists("lm"); -``` -```java -query.whereExists("lm"); -``` -```objc -[query whereKeyExists:@"lm"]; -``` -```js -query.exists('lm') -``` -```swift -try conversationQuery.where("lm", .existed) -``` -```dart -// 暂不支持 -``` - - - -### 组合查询 - -查询年龄小于 18 岁,并且关键字包含「教育」的对话: - - - -```cs -query.WhereContains("keywords", "教育") - .WhereLessThan("age", 18); -``` -```java -query.whereContains("keywords", "教育"); -query.whereLessThan("age", 18); -``` -```objc -[query whereKey:@"keywords" containsString:@"教育"]; -[query whereKey:@"age" lessThan:@(18)]; -``` -```js -// 查询 keywords 包含「教育」且 age 小于 18 的对话 -query.contains('keywords', '教育').lessThan('age', 18); -``` -```swift -try conversationQuery.where("keywords", .matchedSubstring("教育")) -try conversationQuery.where("age", .lessThan(18)) -``` -```dart -// 暂不支持 -``` - - - -另外一种组合的方式是,两个查询采用 `or` 或者 `and` 的方式构建一个新的查询。 - -查询年龄小于 18 或者关键字包含「教育」的对话: - - - -```cs -// 暂不支持 -``` -```java -LCIMConversationsQuery ageQuery = tom.getConversationsQuery(); -ageQuery.whereLessThan('age', 18); - -LCIMConversationsQuery keywordsQuery = tom.getConversationsQuery(); -keywordsQuery.whereContains('keywords', '教育'); - -LCIMConversationsQuery query = LCIMConversationsQuery.or(Arrays.asList(priorityQuery, statusQuery)); -``` -```objc -LCIMConversationQuery *ageQuery = [tom conversationQuery]; -[ageQuery whereKey:@"age" greaterThan:@(18)]; - -LCIMConversationQuery *keywordsQuery = [tom conversationQuery]; -[keywordsQuery whereKey:@"keywords" containsString:@"教育"]; - -LCIMConversationQuery *query = [LCIMConversationQuery orQueryWithSubqueries:[NSArray arrayWithObjects:ageQuery,keywordsQuery,nil]]; -``` -```js -// JavaScript SDK 暂不支持 -``` -```swift -do { - let ageQuery = tom.conversationQuery - try ageQuery.where("age", .greaterThan(18)) - - let keywordsQuery = tom.conversationQuery - try keywordsQuery.where("keywords", .matchedSubstring("教育")) - - let conversationQuery = try ageQuery.or(keywordsQuery) -} catch { - print(error) -} -``` -```dart -// 暂不支持 -``` - - - -### 结果排序 - -可以指定查询结果按照部分属性值的升序或降序来返回。例如: - - - -```cs -query.OrderByDescending("createdAt"); -``` -```java -query.orderByDescending("createdAt"); -``` -```objc -[query orderByDescending:@"createdAt"]; -``` -```js -// 对查询结果按照 name 升序,然后按照创建时间降序排序 -query.addAscending('name').addDescending('createdAt'); -``` -```swift -try conversationQuery.where("createdAt", .descending) -``` -```dart -// 暂不支持 -``` - - - -### 不带成员信息的精简模式 - -普通对话最多可以容纳 500 个成员,在有些业务逻辑不需要对话的成员列表的情况下,可以使用「精简模式」进行查询,这样返回结果中不会包含成员列表(`members` 字段为空数组),有助于提升应用的性能同时减少流量消耗。 - - - -```cs -query.Compact = true; -``` -```java -query.setCompact(true); -``` -```js -query.compact(true); -``` -```swift -conversationQuery.options = [.notContainMembers] -``` -```objc -query.option = LCIMConversationQueryOptionCompact; -``` -```dart -query.excludeMembers = true; -``` - - - -### 让查询结果附带一条最新消息 - -对于一个聊天应用,一个典型的需求是在对话的列表界面显示最后一条消息,默认情况下,针对对话的查询结果是不带最后一条消息的,需要单独打开相关选项: - - - -```cs -query.WithLastMessageRefreshed = true; -``` -```java -query.setWithLastMessagesRefreshed(true); -``` -```objc -query.option = LCIMConversationQueryOptionWithMessage; -``` -```js -// withLastMessagesRefreshed 方法可以指定让查询结果带上最后一条消息 -query.withLastMessagesRefreshed(true); -``` -```swift -conversationQuery.options = [.containLastMessage] -``` -```dart -query.includeLastMessage = true; -``` - - - -需要注意的是,这个选项真正的意义是「刷新对话的最后一条消息」,这意味着由于 SDK 缓存机制的存在,将这个选项设置为 `false` 查询得到的对话也还是有可能会存在最后一条消息的。 - -### 查询缓存 - - -<> - -.NET SDK 暂不支持缓存功能。 - - -<> - -通常,将查询结果缓存到磁盘上是一种行之有效的方法,这样就算设备离线,应用刚刚打开,网络请求尚未完成时,数据也能显示出来。或者为了节省用户流量,在应用打开的第一次查询走网络,之后的查询可优先走本地缓存。 - -值得注意的是,默认的策略是先走本地缓存的再走网络的,缓存时间是一小时。`LCIMConversationsQuery` 中有如下方法: - -```java -// 设置 LCIMConversationsQuery 的查询策略 -public void setQueryPolicy(LCQuery.CachePolicy policy); -``` - -有时你希望先走网络查询,发生网络错误的时候,再从本地查询,可以这样: - -```java -LCIMConversationsQuery query = client.getConversationsQuery(); -query.setQueryPolicy(LCQuery.CachePolicy.NETWORK_ELSE_CACHE); -query.findInBackground(new LCIMConversationQueryCallback() { - @Override - public void done(List conversations, LCIMException e) { - - } -}); -``` - - -<> - -通常,将查询结果缓存到磁盘上是一种行之有效的方法,这样就算设备离线,应用刚刚打开,网络请求尚未完成时,数据也能显示出来。或者为了节省用户流量,在应用打开的第一次查询走网络,之后的查询可优先走本地缓存。 - -值得注意的是,默认的策略是先走本地缓存的再走网络的,缓存时间是一小时。`LCIMConversationQuery` 中有如下方法: - -```objc -// 设置缓存策略,默认是 kLCCachePolicyCacheElseNetwork -@property (nonatomic) LCCachePolicy cachePolicy; - -// 设置缓存的过期时间,默认是 1 小时(1 * 60 * 60) -@property (nonatomic) NSTimeInterval cacheMaxAge; -``` - -有时你希望先走网络查询,发生网络错误的时候,再从本地查询,可以这样: - -```objc -LCIMConversationQuery *query = [client conversationQuery]; -query.cachePolicy = kLCCachePolicyNetworkElseCache; -[query findConversationsWithCallback:^(NSArray *objects, NSError *error) { - -}]; -``` - -各种查询缓存策略的行为可以参考[数据存储指南](/v2/sdk/storage/guide/dotnet)的《缓存查询》一节。 - - -<> - -Swift SDK 提供了会话的缓存功能,包括内存缓存和持久化缓存。 - -会话的内存缓存: - -```swift -client.getCachedConversation(ID: "CONVERSATION_ID") { (result) in - switch result { - case .success(value: let conversation): - print(conversation) - case .failure(error: let error): - print(error) - } -} - -client.removeCachedConversation(IDs: ["CONVERSATION_ID"]) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` - -会话的持久化缓存。**注意,使用「查询持久存储会话」以及「删除持久存储会话」的功能前,需调用 `prepareLocalStorage` 方法且回调结果为成功;`prepareLocalStorage` 方法只需要调用一次(返回成功),一般在 `IMClient.init()` 和 `IMClient.open()` 之间调用**: - -```swift -// Switch for Local Storage of IM Client -do { - // Client init with Local Storage feature - let clientWithLocalStorage = try IMClient(ID: "CLIENT_ID") - - // Client init without Local Storage feature - var options = IMClient.Options.default - options.remove(.usingLocalStorage) - let clientWithoutLocalStorage = try IMClient(ID: "CLIENT_ID", options: options) -} catch { - print(error) -} - -// Preparation for Local Storage of IM Client -do { - try client.prepareLocalStorage { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} - -// Get and Load Stored Conversations to Memory -do { - try client.getAndLoadStoredConversations(completion: { (result) in - switch result { - case .success(value: let conversations): - print(conversations) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} - -// Delete Stored Conversations and Messages belong to them -do { - try client.deleteStoredConversationAndMessages(IDs: ["CONVERSATION_ID"], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` - -注意: - -1. 聊天室和临时会话没有本地缓存机制。 -2. 会话有内存缓存和持久化(磁盘)缓存。消息只有持久化缓存,且只支持消息查询的结果(查询结果小于 3 时不缓存)。 - - -<> - -Flutter SDK 暂不支持缓存功能。 - - -<> - -JavaScript SDK 会对按照对话 ID 对对话进行内存字典缓存,但不会进行持久化的缓存。 - - - - - -### 性能优化建议 - -`Conversation` 数据是存储在云端数据库中的,与存储服务中的对象查询类似,我们需要尽可能利用索引来提升查询效率,这里有一些优化查询的建议: - -- `Conversation` 的 `objectId`、`updatedAt`、`createdAt` 等属性上是默认建了索引的,所以通过这些条件来查询会比较快。 -- 虽然 `skip` 搭配 `limit` 的方式可以翻页,但是在结果集较大的时候不建议使用,因为数据库端计算翻页距离是一个非常低效的操作,取而代之的是尽量通过 `updatedAt` 或 `lastMessageAt` 等属性来限定返回结果集大小,并以此进行翻页。 -- 使用 `m` 列的 `contains` 查询来查找包含某人的对话时,也尽量使用默认的 `limit` 大小 10,再配合 `updatedAt` 或者 `lastMessageAt` 来做条件约束,性能会提升较大。 -- 整个应用对话如果数量太多,可以考虑在云引擎封装一个云函数,用定时任务启动之后,周期性地做一些清理,例如可以归档或删除一些不活跃的对话。 - -## 聊天记录查询 - -消息记录默认会在云端保存 **180** 天, 开发者可以通过额外付费来延长这一期限(有需要的用户请提工单联系技术支持),也可以通过 REST API 将聊天记录同步到自己的服务器上。 - -SDK 提供了多种方式来拉取历史记录,iOS 和 Android SDK 还提供了内置的消息缓存机制,以减少客户端对云端消息记录的查询次数,并且在设备离线情况下,也能展示出部分数据保障产品体验不会中断。 - -### 从新到旧获取对话的消息记录 - -在终端用户进入一个对话的时候,最常见的需求就是由新到旧、以翻页的方式拉取并展示历史消息,这可以通过如下代码实现: - - - -```cs -// limit 取值范围 1~100,默认 20 -var messages = await conversation.QueryMessages(limit: 10); -foreach (var message in messages) { - if (message is LCIMTextMessage textMessage) { - - } -} -``` -```java -// limit 取值范围 1~100,如调用 queryMessages 时不带 limit 参数,默认获取 20 条消息记录 -int limit = 10; -conv.queryMessages(limit, new LCIMMessagesQueryCallback() { - @Override - public void done(List messages, LCIMException e) { - if (e == null) { - // 成功获取最新 10 条消息记录 - } - } -}); -``` -```objc -// 查询对话中最后 10 条消息,limit 取值范围 1~100,值为 0 时获取 20 条消息记录(使用服务端默认值) -[conversation queryMessagesWithLimit:10 callback:^(NSArray *objects, NSError *error) { - NSLog(@"查询成功!"); -}]; -``` -```js -conversation.queryMessages({ - limit: 10, // limit 取值范围 1~100,默认 20 -}).then(function(messages) { - // 最新的十条消息,按时间增序排列 -}).catch(console.error.bind(console)); -``` -```swift -do { - try conversation.queryMessage(limit: 10) { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -// limit 取值范围 1~100,如调用 queryMessage 时不带 limit 参数,默认获取 20 条消息记录 -try { - List messages = await conversation.queryMessage( - limit: 10, - ); -} catch (e) { - print(e); -} -``` - - - -`queryMessage` 接口也是支持翻页的。 -即时通讯云端通过消息的 `messageId` 和发送时间戳来唯一定位一条消息,因此要从某条消息起拉取后续的 N 条记录,只需要指定起始消息的 `messageId` 和发送时间戳作为锚定就可以了,示例代码如下: - - - -```cs -// limit 取值范围 1~1000,默认 100 -var messages = await conversation.QueryMessages(limit: 10); -var oldestMessage = messages[0]; -var start = new LCIMMessageQueryEndpoint { - MessageId = oldestMessage.Id, - SentTimestamp = oldestMessage.SentTimestamp -}; -var messagesInPage = await conversation.QueryMessages(start: start); -``` -```java -// limit 取值范围 1~1000,默认 100 -conv.queryMessages(10, new LCIMMessagesQueryCallback() { - @Override - public void done(List messages, LCIMException e) { - if (e == null) { - // 成功获取最新 10 条消息记录 - // 返回的消息一定是时间增序排列,也就是最早的消息一定是第一个 - LCIMMessage oldestMessage = messages.get(0); - - conv.queryMessages(oldestMessage.getMessageId(), oldestMessage.getTimestamp(),20, - new LCIMMessageQueryCallback(){ - @Override - public void done(List messagesInPage,LCIMException e){ - if(e== null){ - // 查询成功返回 - Log.d("Tom & Jerry", "got " + messagesInPage.size()+" messages "); - } - } - }); - } - } -}); -``` -```objc -// 查询对话中最后 10 条消息 -[conversation queryMessagesWithLimit:10 callback:^(NSArray *messages, NSError *error) { - NSLog(@"第一次查询成功!"); - // 以第一页的最早的消息作为开始,继续向前拉取消息 - LCIMMessage *oldestMessage = [messages firstObject]; - [conversation queryMessagesBeforeId:oldestMessage.messageId timestamp:oldestMessage.sendTimestamp limit:10 callback:^(NSArray *messagesInPage, NSError *error) { - NSLog(@"第二次查询成功!"); - }]; -}]; -``` -```js -// JS SDK 通过迭代器隐藏了翻页的实现细节,开发者通过不断的调用 next 方法即可获得后续数据。 -// 创建一个迭代器,每次获取 10 条历史消息 -var messageIterator = conversation.createMessagesIterator({ limit: 10 }); -// 第一次调用 next 方法,获得前 10 条消息,还有更多消息,done 为 false -messageIterator.next().then(function(result) { - // result: { - // value: [message1, ..., message10], - // done: false, - // } -}).catch(console.error.bind(console)); -// 第二次调用 next 方法,获得第 11~20 条消息,还有更多消息,done 为 false -// 迭代器内部会记录起始消息的数据,无需开发者显示指定 -messageIterator.next().then(function(result) { - // result: { - // value: [message11, ..., message20], - // done: false, - // } -}).catch(console.error.bind(console)); -``` -```swift -do { - let start = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID", - sentTimestamp: 31415926, - isClosed: false - ) - try conversation.queryMessage(start: start, limit: 10, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -List messages; -try { -// 第一次查询成功 - messages = await conversation.queryMessage( - limit: 10, - ); -} catch (e) { - print(e); -} - -try { - // 返回的消息一定是时间增序排列,也就是最早的消息一定是第一个 - Message oldMessage = messages.first; - // 以第一页的最早的消息作为开始,继续向前拉取消息 - List messages2 = await conversation.queryMessage( - startTimestamp: oldMessage.sentTimestamp, - startMessageID: oldMessage.id, - startClosed: true, - limit: 10, - ); -} catch (e) { - print(e); -} -``` - - - -### 按照消息类型获取 - -除了按照时间先后顺序拉取历史消息之外,即时通讯服务云端也支持按照消息的类型来拉取历史消息,这一功能可能对某些产品来说非常有用,例如我们需要展现某一个聊天群组里面所有的图像。 - -`queryMessage` 接口还支持指定特殊的消息类型,其示例代码如下: - - - -```cs -// 传入泛型参数,SDK 会自动读取类型的信息发送给服务端,用作筛选目标类型的消息 -var imageMessages = await conversation.QueryMessages(messageType: -2); -``` -```java -int msgType = LCIMMessageType.IMAGE_MESSAGE_TYPE; -conversation.queryMessagesByType(msgType, limit, new LCIMMessagesQueryCallback() { - @Override - public void done(List messages, LCIMException e){ - } -}); -``` -```objc -[conversation queryMediaMessagesFromServerWithType:kLCIMMessageMediaTypeImage limit:10 fromMessageId:nil fromTimestamp:0 callback:^(NSArray *messages, NSError *error) { - if (!error) { - NSLog(@"查询成功!"); - } -}]; -``` -```js -conversation.queryMessages({ type: ImageMessage.TYPE }).then(messages => { - console.log(messages); -}).catch(console.error); -``` -```swift -do { - try conversation.queryMessage(limit: 10, type: IMTextMessage.messageType, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - List messages = await conversation.queryMessage(type: -2); -} catch (e) { - print(e); -} -``` - - - -如要获取更多图像消息,可以效仿前一章节中的示例代码,继续翻页查询即可。 - -### 从旧到新反向获取历史消息 - -即时通讯云端支持的历史消息查询方式是非常多的,除了上面列举的两个最常见需求之外,还可以支持按照由旧到新的方向进行查询。如下代码演示从对话创建的时间点开始,从前往后查询消息记录: - - - -```cs -var earliestMessages = await conversation.QueryMessages(direction: LCIMMessageQueryDirection.OldToNew); -``` -```java -LCIMMessageInterval interval = new LCIMMessageInterval(null, null); -conversation.queryMessages(interval, DirectionFromOldToNew, limit, - new LCIMMessagesQueryCallback(){ - public void done(List messages, LCIMException exception) { - // 处理结果 - } -}); -``` -```objc -[conversation queryMessagesInInterval:nil direction:LCIMMessageQueryDirectionFromOldToNew limit:20 callback:^(NSArray * _Nullable messages, NSError * _Nullable error) { - if (messages.count) { - // 处理结果 - } -}]; -``` -```js -var { MessageQueryDirection } = require('leancloud-realtime'); -conversation.queryMessages({ - direction: MessageQueryDirection.OLD_TO_NEW, -}).then(function(messages) { - // 处理结果 -}.catch(function(error) { - // 处理异常 -}); -``` -```swift -do { - try conversation.queryMessage(direction: .oldToNew, limit: 10, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - List messages = await conversation.queryMessage( - direction: MessageQueryDirection.oldToNew, - ); -} catch (e) { - print(e); -} -``` - - - -这种情况下要实现翻页,接口会稍微复杂一点,请继续阅读下一节。 - -### 从某一时间戳往某一方向查询 - -即时通讯服务云端支持以某一条消息的 ID 和时间戳为准,往一个方向查: - -- 从新到旧:以某一条消息为基准,查询它 **之前** 产生的消息 -- 从旧到新:以某一条消息为基准,查询它 **之后** 产生的消息 - -这样我们就可以在不同方向上实现消息翻页了。 - - - -```cs -var earliestMessages = await conversation.QueryMessages(direction: LCIMMessageQueryDirection.OldToNew, limit: 1); -// 获取 earliestMessages.Last() 之后的消息 -var start = new LCIMMessageQueryEndpoint { - MessageId = earliestMessages.Last().Id -}; -var nextPageMessages = await conversation.QueryMessages(start: start); -``` -```java -LCIMMessageIntervalBound start = LCIMMessageInterval.createBound(messageId, timestamp, false); -LCIMMessageInterval interval = new LCIMMessageInterval(start, null); -LCIMMessageQueryDirection direction; -conversation.queryMessages(interval, direction, limit, - new LCIMMessagesQueryCallback(){ - public void done(List messages, LCIMException exception) { - // 处理结果 - } -}); -``` -```objc -LCIMMessageIntervalBound *start = [[LCIMMessageIntervalBound alloc] initWithMessageId:nil timestamp:timestamp closed:false]; -LCIMMessageInterval *interval = [[LCIMMessageInterval alloc] initWithStartIntervalBound:start endIntervalBound:nil]; -[conversation queryMessagesInInterval:interval direction:direction limit:20 callback:^(NSArray * _Nullable messages, NSError * _Nullable error) { - if (messages.count) { - // 处理结果 - } -}]; -``` -```js -var { MessageQueryDirection } = require('leancloud-realtime'); -conversation.queryMessages({ - startTime: timestamp, - startMessageId: messageId, -startClosed: false, - direction: MessageQueryDirection.OLD_TO_NEW, -}).then(function(messages) { - // 处理结果 -}.catch(function(error) { - // 处理异常 -}); -``` -```swift -do { - let start = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID", - sentTimestamp: 31415926, - isClosed: true - ) - try conversation.queryMessage(start: start, direction: .oldToNew, limit: 10, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - List messages = await conversation.queryMessage( - startTimestamp: textMessage.sentTimestamp, - startMessageID: textMessage.id, - startClosed: true, - direction: MessageQueryDirection.oldToNew, - limit: 10, - ); -} catch (e) { - print(e); -} -``` - - - -### 获取指定区间内的消息 - -除了顺序查找之外,我们也支持获取特定时间区间内的消息。假设已知 2 条消息,这 2 条消息以较早的一条为起始点,而较晚的一条为终点,这个区间内产生的消息可以用如下方式查询: - -注意:**每次查询也有 100 条限制,如果想要查询区间内所有产生的消息,替换区间起始点的参数即可。** - - - -```cs -var earliestMessage = await conversation.QueryMessages(direction: LCIMMessageQueryDirection.OldToNew, limit: 1); -var latestMessage = await conversation.QueryMessages(limit: 1); -var start = new LCIMMessageQueryEndpoint { - MessageId = earliestMessage[0].Id -}; -var end = new LCIMMessageQueryEndpoint { - MessageId = latestMessage[0].Id -}; -// messagesInInterval 最多可包含 100 条消息 -var messagesInInterval = await conversation.QueryMessages(start: start, end: end); -``` -```java -LCIMMessageIntervalBound start = LCIMMessageInterval.createBound(messageId, timestamp, false); -LCIMMessageIntervalBound end = LCIMMessageInterval.createBound(endMessageId, endTimestamp, false); -LCIMMessageInterval interval = new LCIMMessageInterval(start, end); -LCIMMessageQueryDirection direction; -conversation.queryMessages(interval, direction, limit, - new LCIMMessagesQueryCallback(){ - public void done(List messages, LCIMException exception) { - // 处理结果 - } -}); -``` -```objc -LCIMMessageIntervalBound *start = [[LCIMMessageIntervalBound alloc] initWithMessageId:nil timestamp:startTimestamp closed:false]; -LCIMMessageIntervalBound *end = [[LCIMMessageIntervalBound alloc] initWithMessageId:nil timestamp:endTimestamp closed:false]; -LCIMMessageInterval *interval = [[LCIMMessageInterval alloc] initWithStartIntervalBound:start endIntervalBound:end]; -[conversation queryMessagesInInterval:interval direction:direction limit:100 callback:^(NSArray * _Nullable messages, NSError * _Nullable error) { - if (messages.count) { - // 处理结果 - } -}]; -``` -```js -conversation.queryMessages({ - startTime: timestamp, - startMessageId: messageId, - endTime: endTimestamp, - endMessageId: endMessageId, -}).then(function(messages) { - // 处理结果 -}.catch(function(error) { - // 处理异常 -}); -``` -```swift -do { - let start = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID_1", - sentTimestamp: 31415926, - isClosed: true - ) - let end = IMConversation.MessageQueryEndpoint( - messageID: "MESSAGE_ID_2", - sentTimestamp: 31415900, - isClosed: true - ) - try conversation.queryMessage(start: start, end: end, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - List messages = await conversation.queryMessage( - startTimestamp: textMessage.sentTimestamp, - startMessageID: textMessage.id, - startClosed: true, - endTimestamp: fileMessage.sentTimestamp, - endMessageID: fileMessage.id, - endClosed: true, - ); -} catch (e) { - print(e); -} -``` - - - -### 客户端消息缓存 - -iOS 和 Android SDK 针对移动设备的特殊性,实现了客户端消息的缓存。开发者无需进行特殊设置,只要接收或者查询到的新消息,默认都会进入被缓存起来,该机制给开发者提供了如下便利: - -1. 客户端可以在未联网的情况下进入对话列表之后,可以获取聊天记录,提升用户体验 -2. 减少查询的次数和流量的消耗 -3. 极大地提升了消息记录的查询速度和性能 - -客户端缓存是默认开启的,如果开发者有特殊的需求,SDK 也支持关闭缓存功能。例如有些产品在应用层进行了统一的消息缓存,无需 SDK 层再进行冗余存储,可以通过如下接口来关闭消息缓存: - - - -```cs -// 暂不支持 -``` -```java -// 需要在调用 LCIMClient.open(callback) 函数之前设置,关闭历史消息缓存开关。 -LCIMOptions.getGlobalOptions().setMessageQueryCacheEnabled(false); -``` -```objc -// 需要在调用 [avimClient openWithCallback:callback] 函数之前设置,关闭历史消息缓存开关。 -avimClient.messageQueryCacheEnabled = false; -``` -```js -// 暂不支持 -``` -```swift -// Switch for Local Storage of IM Client -do { - // Client init with Local Storage feature - let clientWithLocalStorage = try IMClient(ID: "CLIENT_ID") - - // Client init without Local Storage feature - var options = IMClient.Options.default - options.remove(.usingLocalStorage) - let clientWithoutLocalStorage = try IMClient(ID: "CLIENT_ID", options: options) -} catch { - print(error) -} - -// Message Query Policy -enum MessageQueryPolicy { - case `default` - case onlyNetwork - case onlyCache - case cacheThenNetwork -} - -do { - try conversation.queryMessage(policy: .default, completion: { (result) in - switch result { - case .success(value: let messages): - print(messages) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -// 暂不支持 -``` - - - -## 用户退出与网络状态变化 - -### 用户退出即时通讯服务 - -如果产品层面设计了用户退出登录或者切换账号的接口,对于即时通讯服务来说,也是需要完全注销当前用户的登录状态的。在 SDK 中,开发者可以通过调用 `LCIMClient` 的 `close` 系列方法完成即时通讯服务的「退出」: - - - -```cs -await tom.Close(); -``` -```java -tom.close(new LCIMClientCallback(){ - @Override - public void done(LCIMClient client,LCIMException e){ - if(e==null){ - // 登出成功 - } - } -}); -``` -```objc -[tom closeWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"退出即时通讯服务"); - } -}]; -``` -```js -tom.close().then(function() { - console.log('Tom 退出登录'); -}).catch(console.error.bind(console)); -``` -```swift -tom.close { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` -```dart -await tom.close(); -``` - - - -调用该接口之后,客户端就与即时通讯服务云端断开连接了,从云端查询前一 `clientId` 的状态,会显示「离线」状态。 - -### 客户端事件与网络状态响应 - -即时通讯服务与终端设备的网络连接状态休戚相关,如果网络中断,那么所有的消息收发和对话操作都会失败,这时候产品层面需要在 UI 上给予用户足够的提示,以免影响使用体验。 - -我们的 SDK 内部和即时通讯云端会维持一个「心跳」机制,能够及时感知到客户端的网络变化,同时将底层网络变化事件通知到应用层。具体来讲,当网络连接出现中断、恢复等状态变化时,SDK 会派发以下事件: - - -<> - -`LCIMClient` 上会有如下事件通知: - -- `OnPaused` 指网络连接断开事件发生,此时聊天服务不可用 -- `OnResume` 指网络连接恢复正常,此时聊天服务变得可用 -- `OnClose` 指连接关闭,且不会自动重连 - - -<> - -`LCIMClientEventHandler` 上会有如下事件通知: - -- `onConnectionPaused()` 指网络连接断开事件发生,此时聊天服务不可用。 -- `onConnectionResume()` 指网络连接恢复正常,此时聊天服务变得可用。 -- `onClientOffline()` 指单点登录被踢下线的事件。 - - -<> - -在 `LCIMClientDelegate` 里,可以接收到如下所示的事件通知: - -* `imClientResumed`:连接自动恢复了 -* `imClientPaused`:连接断开了;该事件被触发的常见场景:网络无法访问、应用进入后台 -* `imClientResuming`:正在重新建立连接 -* `imClientClosed`:连接关闭,且不会自动重连;该事件被触发的常见场景:单设备登录冲突、后台主动把该 client 下线 - -```objc -- (void)imClientResumed:(LCIMClient *)imClient -{ - -} - -- (void)imClientResuming:(LCIMClient *)imClient -{ - -} - -- (void)imClientPaused:(LCIMClient *)imClient error:(NSError * _Nullable)error -{ - -} - -- (void)imClientClosed:(LCIMClient *)imClient error:(NSError * _Nullable)error -{ - -} -``` - - -<> - -在 `IMClientDelegate` 的 `IMClientDelegate.client(_:event:)` 函数里,可以接收到如下所示的事件通知: - -* `sessionDidOpen`:连接自动恢复了 -* `sessionDidPause`:连接断开了;该事件被触发的常见场景:网络无法访问、应用进入后台 -* `sessionDidResume`:正在重新建立连接 -* `sessionDidClose`:连接关闭,且不会自动重连;该事件被触发的常见场景:单设备登录冲突、后台主动把该 client 下线 - -```swift -func client(_ client: IMClient, event: IMClientEvent) { - switch event { - case .sessionDidOpen: - break - case .sessionDidPause(error: let error): - print(error) - case .sessionDidResume: - break - case .sessionDidClose(error: let error): - print(error) - } -} -``` - - -<> - -`Client` 有如下事件通知: - -- `onOpened` 用户登录即时通信的服务。 -- `onClosed` 用户退出即时通信服务。 -- `onResuming` 指网络正在尝试重连,此时聊天服务不可用。 -- `onDisconnected` 指网络连接断开事件发生,此时聊天服务不可用。 - - -<> - -* `DISCONNECT`:与服务端连接断开,此时聊天服务不可用。 -* `OFFLINE`:网络不可用。 -* `ONLINE`:网络恢复。 -* `SCHEDULE`:计划在一段时间后尝试重连,此时聊天服务仍不可用。 -* `RETRY`:正在重连。 -* `RECONNECT`:与服务端连接恢复,此时聊天服务可用。 - -```js -var { Event } = require('leancloud-realtime'); - -realtime.on(Event.DISCONNECT, function() { - console.log(' 服务器连接已断开 '); -}); -realtime.on(Event.OFFLINE, function() { - console.log(' 离线(网络连接已断开)'); -}); -realtime.on(Event.ONLINE, function() { - console.log(' 已恢复在线 '); -}); -realtime.on(Event.SCHEDULE, function(attempt, delay) { - console.log(delay + ' ms 后进行第 ' + (attempt + 1) + ' 次重连 '); -}); -realtime.on(Event.RETRY, function(attempt) { - console.log(' 正在进行第 ' + (attempt + 1) + ' 次重连 '); -}); -realtime.on(Event.RECONNECT, function() { - console.log(' 与服务端连接恢复 '); -}); -``` - - - - - -## 其他开发建议 - -### 如何根据活跃度来展示对话列表 - -不管是当前用户参与的「对话」列表,还是全局热门的开放聊天室列表展示出来了,我们下一步要考虑的就是如何把最活跃的对话展示在前面,这里我们把「活跃」定义为最近有新消息发出来。我们希望有最新消息的对话可以展示在对话列表的最前面,甚至可以把最新的那条消息也附带显示出来,这时候该怎么实现呢? - -我们专门为 `LCIMConversation` 增加了一个动态的属性 `lastMessageAt`(对应 `_Conversation` 表里的 `lm` 字段),记录了对话中最后一条消息到达即时通讯云端的时间戳,这一数字是服务器端的时间(精确到秒),所以不用担心客户端时间对结果造成影响。另外,`LCIMConversation` 还提供了一个方法可以直接获取最新的一条消息。这样在界面展现的时候,开发者就可以自己决定展示内容与顺序了。 - -### 自动重连 - -如果开发者没有明确调用退出登录的接口,但是客户端网络存在抖动或者切换(对于移动网络来说,这是比较常见的情况),我们 iOS 和 Android SDK 默认内置了断线重连的功能,会在网络恢复的时候自动建立连接,此时 `IMClient` 的网络状态可以通过底层的网络状态响应接口得到回调。 - -### 更多「对话」类型 - -即时通讯服务提供的功能就是让一个客户端与其他客户端进行在线的消息互发,对应不同的使用场景,除了前两章节介绍的 [一对一单聊](#一对一单聊) 和 [多人群聊](#多人群聊) 之外,我们也支持其他形式的「对话」模型: - -- 开放聊天室,例如直播中的弹幕聊天室,它与普通的「多人群聊」的主要差别是允许的成员人数以及消息到达的保证程度不一样。有兴趣的开发者可以参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)的《玩转直播聊天室》一节。 - -- 临时对话,例如客服系统中用户和客服人员之间建立的临时通道,它与普通的「一对一单聊」的主要差别在于对话总是临时创建并且不会长期存在,在提升实现便利性的同时,还能降低服务使用成本(能有效减少存储空间方面的花费)。有兴趣的开发者可以参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)的《使用临时对话》一节。 - -- 系统对话,例如在微信里面常见的公众号/服务号,系统全局的广播账号,与普通「多人群聊」的主要差别,在于「服务号」是以订阅的形式加入的,也没有成员限制,并且订阅用户和服务号的消息交互是一对一的,一个用户的上行消息不会群发给其他订阅用户。有兴趣的开发者可以参考[即时通讯开发指南第四篇](/v2/sdk/im/guide/systemconv)《「系统对话」的使用》一节。 -## 进一步阅读 - -《二,消息收发的更多方式,离线推送与消息同步,多设备登录》 - -《三,安全与签名、黑名单和权限管理、玩转直播聊天室和临时对话》 - -《四,详解消息 hook 与系统对话》 diff --git a/versioned_docs/version-v2/sdk/10-im/02-guide/02-intermediate.mdx b/versioned_docs/version-v2/sdk/10-im/02-guide/02-intermediate.mdx deleted file mode 100644 index e65a4d739..000000000 --- a/versioned_docs/version-v2/sdk/10-im/02-guide/02-intermediate.mdx +++ /dev/null @@ -1,1948 +0,0 @@ ---- -id: intermediate -title: 二,消息收发的更多方式,离线推送与消息同步,多设备登录 -sidebar_label: 离线消息 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - - - -## 本章导读 - -在前一章[从简单的单聊、群聊、收发图文消息开始](/v2/sdk/im/guide/beginner)里面,我们说明了如何在产品中增加一个基本的单聊/群聊页面,并响应服务端实时事件通知。接下来,在本篇文档中我们会讲解如何实现一些更复杂的业务需求,例如: - -- 支持消息被接收和被阅读的状态回执,实现「Ding」一下的效果 -- 发送带有成员提醒的消息(@ 某人),在超多用户群聊的场合提升目标用户的响应积极性 -- 支持消息的撤回和修改 -- 解决成员离线状态下的推送通知与重新上线后的消息同步,确保不丢消息 -- 支持多设备登录,或者强制用户单点登录 -- 扩展新的消息类型 - -## 消息收发的更多方式 - -在一个偏重工作协作或社交沟通的产品里,除了简单的消息收发之外,我们还会碰到更多需求,例如: - -- 在消息中能否直接提醒某人,类似于很多 IM 工具中提供的 @ 消息,这样接收方能更明确地知道哪些消息需要及时响应; -- 消息发出去之后才发现内容不对,这时候能否修改或者撤回? -- 除了普通的聊天内容之外,是否支持发送类似于「XX 正在输入」这样的状态消息? -- 消息是否被其他人接收、读取,这样的状态能否反馈给发送者? -- 客户端掉线一段时间之后,可能会错过一批消息,能否提醒并同步一下未读消息? - -等等,所有这些需求都可以通过即时通讯服务解决,下面我们来逐一看看具体的做法。 - -### @ 成员提醒消息 - -在一些多人聊天群里面,因为消息量较大,很容易就导致某一条重要的消息没有被目标用户看到就被刷下去了,所以在消息中「@成员」是一种提醒接收者注意的有效办法。在微信这样的聊天工具里面,甚至会在对话列表页对有提醒的消息进行特别展示,用来通知消息目标高优先级查看和处理。 - -一般提醒消息都使用「@ + 人名」来表示目标用户,但是这里「人名」是一个由应用层决定的属性,可能有的产品使用全名,有的使用昵称,并且这个名字和即时通讯服务里面标识一个用户使用的 `clientId` 可能根本不一样(毕竟一个是给人看的,一个是给机器读的)。使用「人名」来圈定用户,也存在一种例外,就是聊天群组里面的用户名是可以改变的,如果消息发送的时候「王五」还叫「王五」,等发送出来之后他恰好同步改成了「王大麻子」,这时候接收方的处理就比较麻烦了。还有第三个原因,就是「提醒全部成员」的表示方式,可能「@all」、「@group」、「@所有人」都会被选择,这是一个完全依赖应用层 UI 的选项。 - -所以「@ 成员」提醒消息并不能简单在文本消息中加入「@ + 人名」,解决方案是给普通消息(`LCIMMessage`)增加两个额外的属性: - -- `mentionList`,是一个字符串的数组,用来单独记录被提醒的 `clientId` 列表; -- `mentionAll`,是一个 `Bool` 型的标志位,用来表示是否要提醒全部成员。 - -带有提醒信息的消息,有可能既有提醒全部成员的标志,也还单独设置了 `mentionList`,这由应用层去控制。发送方在发送「@ 成员」提醒消息的时候,如何输入、选择成员名称,这是业务方 UI 层面需要解决的问题,即时通讯 SDK 不关心其实现逻辑,SDK 只要求开发者在发送一条「@ 成员」消息的时候,调用 `mentionList` 和 `mentionAll` 的 setter 方法,设置正确的成员列表即可。示例代码如下: - - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("@Tom 早点回家") { - MentionIdList = new string[] { "Tom" } -}; -await conversation.Send(textMessage); -``` -```java -String content = "@Tom 早点回家"; -LCIMTextMessage message = new LCIMTextMessage(); -message.setText(content); -List list = new ArrayList<>(); // 部分用户的 mention list,你可以像下面代码这样来填充 -list.add("Tom"); -message.setMentionList(list); -imConversation.sendMessage(message, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` -```objc -LCIMMessage *message = [LCIMTextMessage messageWithText:@"@Tom 早点回家" attributes:nil]; -message.mentionList = @[@"Tom"]; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError * _Nullable error) { - /* 一条提及 Tom 的消息已发出 */ -}]; -``` -```js -const message = new TextMessage(`@Tom 早点回家`).setMentionList(['Tom']); -conversation.send(message).then(function(message) { - console.log('发送成功!'); -}).catch(console.error); -``` -```swift -do { - let message = IMTextMessage(text: "@Tom 早点回家") - message.mentionedMembers = ["Tom"] - try conversation.send(message: message, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - TextMessage message = TextMessage(); - message.text = '@Tom 早点回家'; - message.mentionMembers = ['Tom']; - await conversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - - -或者也可以通过设置 `mentionAll` 属性值提醒所有人: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("@all") { - MentionAll = true -}; -await conv.Send(textMessage); -``` -```java -String content = "@all"; -LCIMTextMessage message = new LCIMTextMessage(); -message.setText(content); - -boolean mentionAll = true; // 指示是否提及了所有人 -message.mentionAll(mentionAll); - -imConversation.sendMessage(message, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` -```objc -LCIMMessage *message = [LCIMTextMessage messageWithText:@"@all" attributes:nil]; -message.mentionAll = YES; -[conversation sendMessage:message callback:^(BOOL succeeded, NSError * _Nullable error) { - /* 一条提及所有用户的消息已发出 */ -}]; -``` -```js -const message = new TextMessage(`@all`).mentionAll(); -conversation.send(message).then(function(message) { - console.log('发送成功!'); -}).catch(console.error); -``` -```swift -do { - let message = IMTextMessage(text: "@all") - message.isAllMembersMentioned = true - try conversation.send(message: message, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - TextMessage message = TextMessage(); - message.text = 'content'; - message.mentionAll = true; - await conversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - - - -对于消息的接收方来说,可以通过调用 `mentionList` 和 `mentionAll` 的 getter 方法来获得提醒目标用户的信息,示例代码如下: - - - -```cs -jerry.onMessage = (conv, msg) => { - List mentionIds = msg.MentionIdList; -}; -``` -```java -@Override -public void onMessage(LCIMAudioMessage msg, LCIMConversation conv, LCIMClient client) { - // 读取消息 @ 的 clientId 列表 - List currentMsgMentionUserList = message.getMentionList(); -} -``` -```objc -// 示例代码演示 LCIMTypedMessage 接收时,获取该条消息提醒的 clientId 列表,同理可以用类似的代码操作 LCIMMessage  的其他子类 -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - // 读取消息 @ 的 clientId 列表 - NSArray *mentionList = message.mentionList; -} -``` -```js -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - var mentionList = receivedMessage.getMentionList(); -}); -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - if let mentionedMembers = message.mentionedMembers { - print(mentionedMembers) - } - if let isAllMembersMentioned = message.isAllMembersMentioned { - print(isAllMembersMentioned) - } - default: - break - } - default: - break - } -} -``` -```dart -jerry.onMessage = ({ - Client client, - Conversation conversation, - Message message, -}) { - List mentionList = message.mentionMembers; -}; -``` - - - - - -此外,并且为了方便应用层 UI 展现,我们特意为 `LCIMMessage` 增加了两个标识位,用来显示被提醒的状态: - -- 一个是 `mentionedAll` 标识位,用来表示该消息是否提醒了当前对话的全体成员。只有 `mentionAll` 属性为 `true`,这个标识位才为 `true`,否则就为 `false`。 -- 另一个是 `mentioned` 标识位,用来快速判断该消息是否提醒了当前登录用户。如果 `mentionList` 属性列表中包含有当前登录用户的 `clientId`,或者 `mentionAll` 属性为 `true`,那么 `mentioned` 方法都会返回 `true`,否则返回 `false`。 - -调用示例如下: - - - -```cs -client.OnMessage = (conv, msg) => { - bool mentioned = msg.MentionAll || msg.MentionList.Contains("Tom"); -}; -``` -```java -@Override -public void onMessage(LCIMAudioMessage msg, LCIMConversation conv, LCIMClient client) { - // 读取消息是否 @ 了对话的所有成员 - boolean currentMsgMentionAllUsers = message.isMentionAll(); - // 读取消息是否 @ 了当前用户 - boolean currentMsgMentionedMe = message.mentioned(); -} -``` -```objc -// 示例代码演示 LCIMTypedMessage 接收时,获取该条消息是否 @ 了当前对话里的所有成员或当前用户,同理可以用类似的代码操作 LCIMMessage  的其他子类 -- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message { - // 读取消息是否 @ 了对话的所有成员 - BOOL mentionAll = message.mentionAll; - // 读取消息是否 @ 了当前用户 - BOOL mentionedMe = message.mentioned; -} -``` -```js -client.on(Event.MESSAGE, function messageEventHandler(message, conversation) { - var mentionedAll = receivedMessage.mentionedAll; - var mentionedMe = receivedMessage.mentioned; -}); -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case .received(message: let message): - print(message.isCurrentClientMentioned) - default: - break - } - default: - break - } -} -``` -```dart -// 暂不支持 -``` - - - - - -### 修改消息 - -在 **云服务控制台 > 即时通讯 > 设置 > 即时通讯选项** 启用 「允许通过 SDK 编辑消息」后,终端用户可以对自己已经发送的消息进行修改(`Conversation#updateMessage` 方法)。目前即时通讯服务端并没有在时效性上进行限制,不过只允许用户修改自己发出去的消息,不允许修改别人的消息。 - -修改已经发送的消息,并不是直接在老的消息对象上修改,而是像发新消息一样创建一个消息实例,然后调用 `Conversation#updateMessage(oldMessage, newMessage)` 方法来向云端提交请求,示例代码如下: - - - -```cs -LCIMTextMessage newMessage = new LCIMTextMessage("修改后的消息内容"); -await conversation.UpdateMessage(oldMessage, newMessage); -``` -```java -LCIMTextMessage textMessage = new LCIMTextMessage(); -textMessage.setContent("修改后的消息"); -imConversation.updateMessage(oldMessage, textMessage, new LCIMMessageUpdatedCallback() { - @Override - public void done(LCIMMessage avimMessage, LCException e) { - if (null == e) { - // 消息修改成功,avimMessage 即为被修改后的最新的消息 - } - } -}); -``` -```objc -LCIMMessage *oldMessage = <#MessageYouWantToUpdate#>; -LCIMMessage *newMessage = [LCIMTextMessage messageWithText:@"Just a new message" attributes:nil]; - -[conversation updateMessage:oldMessage - toNewMessage:newMessage - callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"消息已被修改。"); - } -}]; -``` -```js -var newMessage = new TextMessage('new message'); -conversation.update(oldMessage, newMessage).then(function() { - // 修改成功 -}).catch(function(error) { - // 异常处理 -}); -``` -```swift -do { - let newMessage = IMTextMessage(text: "Just a new message") - try conversation.update(oldMessage: oldMessage, to: newMessage, completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - Message updatedMessage = await conversation.updateMessage( - oldMessage: oldMessage, - newMessage: newMessage, - ); -} catch (e) { - print(e); -} -``` - - - - -消息修改成功之后,对话内的其他成员会立刻接收到 `MESSAGE_UPDATE` 事件: - - - -```cs -tom.OnMessageUpdated = (conv, msg) => { - if (msg is LCIMTextMessage textMessage) { - WriteLine($"内容 {textMessage.Text}, 消息 ID {textMessage.Id}"); - } -}; -``` -```java -void onMessageUpdated(LCIMClient client, LCIMConversation conversation, LCIMMessage message) { - // message 即为被修改的消息 -} -``` -```objc -/* 实现 delegate 方法,以处理消息修改的事件 */ -- (void)conversation:(LCIMConversation *)conversation messageHasBeenUpdated:(LCIMMessage *)message reason:(LCIMMessagePatchedReason * _Nullable)reason { - /* 有消息被修改 */ -} -``` -```js -var { Event } = require('leancloud-realtime'); -conversation.on(Event.MESSAGE_UPDATE, function(newMessage, reason) { - // newMessage 为修改后的消息 - // 在视图层可以通过消息的 ID 找到原来的消息并用 newMessage 替换 - // reason (可选)对象表示消息修改的原因, - // reason 不存在表示发送者主动修改。 - // reason 的 code 属性为正数时,表示因触发云引擎 hook 而导致消息修改 - // (具体数值由开发者在 hook 函数定义中自行指定), - // reason 的 code 属性为负数时,表示因触发系统内置机制而导致消息修改, - // 例如 -4408 表示因敏感词过滤被修改。 - // reason 的 detail 属性是一个字符串,指明具体的修改原因。 -}); -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .updated(updatedMessage: updatedMessage, reason: _): - print(updatedMessage) - default: - break - } - default: - break - } -} -``` -```dart -tom.onMessageUpdated = ({ - Client client, - Conversation conversation, - Message updatedMessage, - int patchCode, - String patchReason, -}) { - // updatedMessage 即为被修改的消息 -}; -``` - - - - -对于 Android 和 iOS SDK 来说,如果开启了消息缓存的选项的话(默认开启),SDK 内部会先从缓存中修改这条消息记录,然后再通知应用层。所以对于开发者来说,收到这条通知之后刷新一下目标聊天页面,让消息列表更新即可(这时候消息列表会出现内容变化)。 - -如果系统修改了消息(例如触发了内置的敏感词过滤功能,或者云引擎的 hook 函数),发送者会收到 `MESSAGE_UPDATE` 事件,其他对话成员接收到的是修改过的消息。 - -### 撤回消息 - -除了修改消息,终端用户还可以撤回一条自己之前发送过的消息。 -和修改消息类似,这一功能需要在控制台启用(**云服务控制台 > 即时通讯 > 设置 > 即时通讯选项** 启用「允许通过 SDK 撤回消息」)。 -同样,即时通讯服务端并没有在时效性上进行限制,不过只允许用户撤回自己发出去的消息,不允许撤回别人的消息。 - -撤回消息调用 `Conversation#recallMessage` 方法,示例代码如下: - - - -```cs -await conversation.RecallMessage(message); -``` -```java -conversation.recallMessage(message, new LCIMMessageRecalledCallback() { - @Override - public void done(LCIMRecalledMessage recalledMessage, LCException e) { - if (null == e) { - // 消息撤回成功,可以更新 UI - } - } -}); -``` -```objc -LCIMMessage *oldMessage = <#MessageYouWantToRecall#>; - -[conversation recallMessage:oldMessage callback:^(BOOL succeeded, NSError * _Nullable error, LCIMRecalledMessage * _Nullable recalledMessage) { - if (succeeded) { - NSLog(@"消息已被撤回。"); - } -}]; -``` -```js -conversation.recall(oldMessage).then(function(recalledMessage) { - // 撤回成功 - // recalledMessage 是一个 RecalledMessage -}).catch(function(error) { - // 异常处理 -}); -``` -```swift -do { - try conversation.recall(message: oldMessage, completion: { (result) in - switch result { - case .success(value: let recalledMessage): - print(recalledMessage) - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - RecalledMessage recalledMessage = await conversation.recallMessage( - message: oldMessage, - ); -} catch (e) { - print(e); -} -``` - - - - - -成功撤回消息后,对话内的其他成员会接收到 `MESSAGE_RECALL` 的事件: - - - -```cs -tom.OnMessageRecalled = (conv, recalledMsg) => { - // recalledMsg 即为被撤回的消息 -}; -``` -```java -void onMessageRecalled(LCIMClient client, LCIMConversation conversation, LCIMMessage message) { - // message 即为被撤回的消息 -} -``` -```objc -/* 实现 delegate 方法,以处理消息撤回的事件 */ -- (void)conversation:(LCIMConversation *)conversation messageHasBeenRecalled:(LCIMRecalledMessage *)message reason:(LCIMMessagePatchedReason * _Nullable)reason { - /* 有消息被撤回 */ -} -``` -```js -var { Event } = require('leancloud-realtime'); -conversation.on(Event.MESSAGE_RECALL, function(recalledMessage, reason) { - // recalledMessage 为已撤回的消息 - // 在视图层可以通过消息的 ID 找到原来的消息并用 recalledMessage 替换 - // reason (可选) 为撤回消息的原因,详见下文修改消息部分的说明。 -}); -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .updated(updatedMessage: updatedMessage, reason: _): - if let recalledMessage = updatedMessage as? IMRecalledMessage { - print(recalledMessage) - } - default: - break - } - default: - break - } -} -``` -```dart -tom.onMessageRecalled = ({ - Client client, - Conversation conversation, - RecalledMessage recalledMessage, -}) { - // recalledMessage 即为被撤回的消息 -}; -``` - - - - - -对于 Android 和 iOS SDK 来说,如果开启了消息缓存的选项的话(默认开启),SDK 内部需要保证数据的一致性,所以会先从缓存中删除这条消息记录,然后再通知应用层。对于开发者来说,收到这条通知之后刷新一下目标聊天页面,让消息列表更新即可(此时消息列表中的消息会直接变少,或者显示撤回提示)。 - -### 暂态消息 - -有时候我们需要发送一些特殊的消息,譬如聊天过程中「某某正在输入…」这样的实时状态信息,或者当群聊的名称修改以后给该群成员发送「群名称被某某修改为 XX」这样的通知信息。这类消息与终端用户发送的消息不一样,发送者不要求把它保存到历史记录里,也不要求一定会被送达(如果成员不在线或者现在网络异常,那么没有下发下去也无所谓),这种需求可以使用「暂态消息」来实现。 - -「暂态消息」是一种特殊的消息,与普通消息相比有以下几点不同: - -- 它不会被自动保存到云端,以后在历史消息中无法找到它 -- 只发送给当时在线的成员,不支持延迟接收,离线用户更不会收到推送通知 -- 对当时在线成员也不保证百分百送达,如果因为当时网络原因导致下发失败,服务端不会重试 - -我们可以用「暂态消息」发送一些实时的、频繁变化的状态信息,或者用来实现简单的控制协议。 - -暂态消息的数据和构造方式与普通消息是一样的,只是其发送方式与普通消息有一些区别。到目前为止,我们演示的 `LCIMConversation` 发送消息接口都是这样的: - - - -```cs -public async Task Send(LCIMMessage message, LCIMMessageSendOptions options = null); -``` -```java -/** - * 发送一条消息 - */ -public void sendMessage(LCIMMessage message, final LCIMConversationCallback callback) -``` -```objc -/*! - 往对话中发送消息。 - */ -- (void)sendMessage:(LCIMMessage *)message - callback:(void (^)(BOOL succeeded, NSError * _Nullable error))callback; -``` -```js -/** - * 发送消息 - * @param {Message} message 消息,Message 及其子类的实例 - * @return {Promise.} 发送的消息 - */ -async send(message) -``` -```swift -/// Send Message. -/// -/// - Parameters: -/// - message: The message to be sent. -/// - options: @see `MessageSendOptions`. -/// - priority: @see `IMChatRoom.MessagePriority`. -/// - pushData: The push data of APNs. -/// - progress: The file uploading progress. -/// - completion: callback. -public func send(message: IMMessage, options: MessageSendOptions = .default, priority: IMChatRoom.MessagePriority? = nil, pushData: [String : Any]? = nil, progress: ((Double) -> Void)? = nil, completion: @escaping (LCBooleanResult) -> Void) throws -``` -```dart -Future send({ - @required Message message, -}) async {} -``` - - - - -其实即时通讯 SDK 还允许在发送一条消息的时候,指定额外的参数 `LCIMMessageOption`,`LCIMConversation` 完整的消息发送接口如下: - - - -```cs -/// -/// Sends a message in this conversation. -/// -/// The message to send. -/// -public async Task Send(LCIMMessage message, LCIMMessageSendOptions options = null); -``` -```java -/** - * 发送消息 - * @param message - * @param messageOption - * @param callback - */ -public void sendMessage(final LCIMMessage message, final LCIMMessageOption messageOption, final LCIMConversationCallback callback); -``` -```objc -/*! - 往对话中发送消息。 - @param message - 消息对象 - @param option - 消息发送选项 - @param callback - 结果回调 - */ -- (void)sendMessage:(LCIMMessage *)message - option:(nullable LCIMMessageOption *)option - callback:(void (^)(BOOL succeeded, NSError * _Nullable error))callback; -``` -```js -/** - * 发送消息 - * @param {Message} message 消息,Message 及其子类的实例 - * @param {Object} [options] since v3.3.0,发送选项 - * @param {Boolean} [options.transient] since v3.3.1,是否作为暂态消息发送 - * @param {Boolean} [options.receipt] 是否需要回执,仅在普通对话中有效 - * @param {Boolean} [options.will] since v3.4.0,是否指定该消息作为「遗愿消息」发送, - * 「遗愿消息」会延迟到当前用户掉线后发送,常用来实现「下线通知」功能 - * @param {MessagePriority} [options.priority] 消息优先级,仅在聊天室中有效, - * see: {@link module:leancloud-realtime.MessagePriority MessagePriority} - * @param {Object} [options.pushData] 消息对应的离线推送内容,如果消息接收方不在线,会推送指定的内容。其结构说明参见: {@link https://url.leanapp.cn/pushData 推送消息内容 } - * @return {Promise.} 发送的消息 - */ -async send(message, options) -``` -```swift -/// Message Sending Option -public struct MessageSendOptions: OptionSet { - /// Get Receipt when other client received message or read message. - public static let needReceipt = MessageSendOptions(rawValue: 1 << 0) - - /// Indicates whether this message is transient. - public static let isTransient = MessageSendOptions(rawValue: 1 << 1) - - /// Indicates whether this message will be auto delivering to other client when this client disconnected. - public static let isAutoDeliveringWhenOffline = MessageSendOptions(rawValue: 1 << 2) -} - -/// Send Message. -/// -/// - Parameters: -/// - message: The message to be sent. -/// - options: @see `MessageSendOptions`. -/// - priority: @see `IMChatRoom.MessagePriority`. -/// - pushData: The push data of APNs. -/// - progress: The file uploading progress. -/// - completion: callback. -public func send(message: IMMessage, options: MessageSendOptions = .default, priority: IMChatRoom.MessagePriority? = nil, pushData: [String : Any]? = nil, progress: ((Double) -> Void)? = nil, completion: @escaping (LCBooleanResult) -> Void) throws -``` -```dart -Future send({ - @required Message message, - bool transient, - bool receipt, - bool will, - MessagePriority priority, - Map pushData, -}) async {} -``` - - - - -通过 `LCIMMessageOption` 参数我们可以指定: - -- 是否作为暂态消息发送(设置 `transient` 属性); -- 服务端是否需要通知该消息的接收状态(设置 `receipt` 属性,消息回执,后续章节会进行说明); -- 消息的优先级(设置 `priority` 属性,后续章节会说明); -- 是否为「遗愿消息」(设置 `will` 属性,后续章节会说明); -- 消息对应的离线推送内容(设置 `pushData` 属性,后续章节会说明),如果消息接收方不在线,会推送指定的内容。 - -如果我们需要让 Tom 在聊天页面的输入框获得焦点的时候,给群内成员同步一条「Tom 正在输入…」的状态信息,可以使用如下代码: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("Tom 正在输入…"); -LCIMMessageSendOptions option = new LCIMMessageSendOptions() { - Transient = true -}; -await conversation.Send(textMessage, option); -``` -```java -String content = "Tom 正在输入…"; -LCIMTextMessage message = new LCIMTextMessage(); -message.setText(content); - -LCIMMessageOption option = new LCIMMessageOption(); -option.setTransient(true); - -imConversation.sendMessage(message, option, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` -```objc -LCIMMessage *message = [LCIMTextMessage messageWithText:@"Tom 正在输入…" attributes:nil]; -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.transient = true; -[conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - /* 一条暂态消息已发出 */ -}]; -``` -```js -const message = new TextMessage('Tom 正在输入…'); -conversation.send(message, {transient: true}); -``` -```swift -do { - let message = IMTextMessage(text: "Tom 正在输入…") - try conversation.send(message: message, options: [.isTransient], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - TextMessage message = TextMessage(); - message.text = 'Tom 正在输入…'; -// 发送一条暂态消息 - await conversation.send(message: message, transient: true); -} catch (e) { - print(e); -} -``` - - - - - -暂态消息的接收逻辑和普通消息一样,开发者可以按照消息类型进行判断和处理,这里不再赘述。上面使用了内建的文本消息只是一种示例,从展现端来说,我们如果使用特定的类型来表示「暂态消息」,是一种更好的方案。即时通讯 SDK 并没有提供固定的「暂态消息」类型,可以由开发者根据自己的业务需要来实现专门的自定义,具体可以参考后述章节:[扩展自己的消息类型](#扩展自己的消息类型)。 - -### 消息回执 - -即时通讯服务端在进行消息投递的时候,会按照消息上行的时间先后顺序下发(先收到的消息先下发,保证顺序性),且内部协议上会要求 SDK 对收到的每一条消息进行确认(ack)。如果 SDK 收到了消息,但是在发送 ack 的过程中出现网络丢包,即时通讯服务端还是会认为消息没有投递下去,之后会再次投递,直到收到 SDK 的应答确认为止。与之对应,SDK 内部也进行了消息去重处理,保证在上面这种异常条件下应用层也不会收到重复的消息。所以我们的消息系统从协议上是可以保证不丢任何一条消息的。 - -不过,有些业务场景会对消息投递的细节有更高的要求,例如消息的发送方要能知道什么时候接收方收到了这条消息,什么时候 ta 又点开阅读了这条消息。有一些偏重工作写作或者私密沟通的产品,消息发送者在发送一条消息之后,还希望能看到消息被送达和阅读的实时状态,甚至还要提醒未读成员。这样「苛刻」的需求,就依赖于我们的「消息回执」功能来实现。 - -与上一节「暂态消息」的发送类似,要使用消息回执功能,需要在发送消息时在 `LCIMMessageOption` 参数中标记「需要回执」选项: - - - -```cs -LCIMTextMessage textMessage = new LCIMTextMessage("一条非常重要的消息。"); -LCIMMessageSendOptions option = new LCIMMessageSendOptions { - Receipt = true -}; -await conversation.Send(textMessage, option); -``` -```java -LCIMMessageOption messageOption = new LCIMMessageOption(); -messageOption.setReceipt(true); -imConversation.sendMessage(message, messageOption, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - } -}); -``` -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.receipt = true; -[conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"发送成功!需要回执。"); - } -}]; -``` -```js -var message = new TextMessage('一条非常重要的消息。'); -conversation.send(message, { - receipt: true, -}); -``` -```swift -do { - let message = IMTextMessage(text: "一条非常重要的消息。") - try conversation.send(message: message, options: [.needReceipt], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - TextMessage message = TextMessage(); - message.text = '一条非常重要的消息。'; - await conversation.send(message: message, receipt: true); -} catch (e) { - print(e); -} -``` - - - - -> 注意: -> -> 只有在发送时设置了「需要回执」的标记,云端才会发送回执,默认不发送回执,且目前消息回执只支持单聊对话(成员不超过 2 人)。 - -那么发送方后续该如何响应回执的通知消息呢? - -#### 送达回执 - -当接收方收到消息之后,云端会向发送方发出一个回执通知,表明消息已经送达。**请注意与「已读回执」区别开。** - - - -```cs -// Tom 用自己的名字作为 clientId 建立了一个 LCIMClient -LCIMClient client = new LCIMClient("Tom"); -// Tom 登录到系统 -await client.Open(); - -// 设置送达回执 -client.OnMessageDelivered = (conv, msgId) => { - // 在这里可以书写消息送达之后的业务逻辑代码 -}; -// 发送消息 -LCIMTextMessage textMessage = new LCIMTextMessage("夜访蛋糕店,约吗?"); -await conversation.Send(textMessage); -``` -```java -public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本地方法来处理对方已经接收消息的通知 - */ - public void onLastDeliveredAtUpdated(LCIMClient client, LCIMConversation conversation) { - ; - } -} - -// 设置全局的对话事件处理 handler -LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); -``` -```objc -// 监听消息是否已送达实现 `conversation:messageDelivered` 即可。 -- (void)conversation:(LCIMConversation *)conversation messageDelivered:(LCIMMessage *)message { - NSLog(@"%@", @"消息已送达。"); // 打印消息 -} -``` -```js -var { Event } = require('leancloud-realtime'); -conversation.on(Event.LAST_DELIVERED_AT_UPDATE, function() { - console.log(conversation.lastDeliveredAt); - // 在 UI 中将早于 lastDeliveredAt 的消息都标记为「已送达」 -}); -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .delivered(toClientID: toClientID, messageID: messageID, deliveredTimestamp: deliveredTimestamp): - if messageID == message.ID { - message.deliveredTimestamp = deliveredTimestamp - } - default: - break - } - default: - break - } -} -``` -```dart -tom.onMessageDelivered = ({ - Client client, - Conversation conversation, - String messageID, - String toClientID, - DateTime atDate, -}) { - // 消息已送达,在这里可以书写消息送达之后的业务逻辑代码 -}; -``` - - - - -请注意这里送达回执的内容,不是某一条具体的消息,而是当前对话内最后一次送达消息的时间戳(`lastDeliveredAt`)。最开始我们有过解释,服务端在下发消息的时候,是能够保证顺序的,所以在送达回执的通知里面,我们不需要对逐条消息进行确认,只给出当前确认送达的最新消息的时间戳,那么在这之前的所有消息就都是已经送达的状态。在 UI 层展示的时候,可以将早于 `lastDeliveredAt` 的消息都标记为「已送达」。 - -#### 已读回执 - -消息送达只是即时通讯服务端和客户端之间的投递行为完成了,可能终端用户并没有进入对话聊天页面,或者根本没有激活应用(Android 平台应用在后台也是可以收到消息的),所以「送达」并不等于终端用户真正「看到」了这条消息。 - -即时通讯服务还支持「已读」消息的回执,不过这首先需要接收方显式完成消息「已读」的确认。 - -由于即时通讯服务端是顺序下发新消息的,客户端不需要对每一条消息单独进行「已读」确认。我们设想的场景如下图所示: - -![在一个标题为「欢迎回来」的对话框中写着「好久不见!你有 5002 条未读消息。是否跳过这些消息?(选择“是”将清除所有未读消息标记)」。对话框的底部有两个按钮,分别为「是,跳过」和「否」。](/img/realtime_read_confirm.png) - -用户在进入一个对话的时候,一次性清除当前对话的所有未读消息即可。`Conversation` 的清除接口如下: - - - -```cs -/// -/// Mark the last message of this conversation as read. -/// -/// -public Task Read(); -``` -```java -/** - * 清除未读消息 - */ -public void read(); -``` -```objc -/*! - 将对话标记为已读。 - 该方法将本地对话中其他成员发出的最新消息标记为已读,该消息的发送者会收到已读通知。 - */ -- (void)readInBackground; -``` -```js -/** - * 将该会话标记为已读 - * @return {Promise.} self - */ -async read(); -``` -```swift -/// Clear unread messages that its sent timestamp less than the sent timestamp of the parameter message. -/// -/// - Parameter message: The default is the last message. -public func read(message: IMMessage? = nil) -``` -```dart -await conversation.read(); -``` - - - - - -对方「阅读」了消息之后,云端会向发送方发出一个回执通知,表明消息已被阅读。 - -Tom 和 Jerry 聊天,Tom 想及时知道 Jerry 是否阅读了自己发去的消息,这时候双方的处理流程是这样的: - -1. Tom 向 Jerry 发送一条消息,且标记为「需要回执」: - - - - ```cs - LCIMTextMessage textMessage = new LCIMTextMessage("一条非常重要的消息。"); - LCIMMessageSendOptions options = new LCIMMessageSendOptions { - Receipt = true - }; - await conversation.Send(textMessage); - ``` - ```java - LCIMClient tom = LCIMClient.getInstance("Tom"); - LCIMConversation conv = client.getConversation("551260efe4b01608686c3e0f"); - - LCIMTextMessage textMessage = new LCIMTextMessage(); - textMessage.setText("Hello, Jerry!"); - - LCIMMessageOption option = new LCIMMessageOption(); - option.setReceipt(true); /* 将消息设置为需要回执。 */ - - conv.sendMessage(textMessage, option, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - /* 发送成功 */ - } - } - }); - ``` - ```objc - LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; - option.receipt = YES; /* 将消息设置为需要回执。 */ - - LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"Hello, Jerry!" attributes:nil]; - - [conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (!error) { - /* 发送成功 */ - } - }]; - ``` - ```js - var message = new TextMessage('一条非常重要的消息。'); - conversation.send(message, { - receipt: true, - }); - ``` - ```swift - do { - let message = IMTextMessage(text: "Hello, Jerry!") - try conversation.send(message: message, options: [.needReceipt], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) - } catch { - print(error) - } - ``` - ```dart - try { - TextMessage message = TextMessage(); - message.text = '一条非常重要的消息。'; - await conversation.send(message: message, receipt: true); - } catch (e) { - print(e); - } - ``` - - - -2. Jerry 阅读 Tom 发的消息后,调用对话上的 `read` 方法把「对话中最近的消息」标记为已读: - - - - ```cs - await conversation.Read(); - ``` - ```java - conversation.read(); - ``` - ```objc - [conversation readInBackground]; - ``` - ```js - conversation.read().then(function(conversation) { - ; - }).catch(console.error.bind(console)); - ``` - ```swift - conversation.read() - ``` - ```dart - await conversation.read(); - ``` - - - -3. Tom 将收到一个已读回执,对话的 `lastReadAt` 属性会更新。此时可以更新 UI,把时间戳小于 `lastReadAt` 的消息都标记为已读: - - - - ```cs - tom.OnLastReadAtUpdated = (conv) => { - // Jerry 阅读了你的消息。可以通过调用 conversation.LastReadAt 来获得对方已经读取到的时间 - }; - ``` - ```java - public class CustomConversationEventHandler extends LCIMConversationEventHandler { - /** - * 实现本地方法来处理对方已经阅读消息的通知 - */ - public void onLastReadAtUpdated(LCIMClient client, LCIMConversation conversation) { - /* Jerry 阅读了你的消息。可以通过调用 conversation.getLastReadAt() 来获得对方已经读取到的时间点 */ - } - } - - // 设置全局的对话事件处理 handler - LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler()); - ``` - ```objc - // Tom 可以在 client 的 delegate 方法中捕捉到 lastReadAt 的更新 - - (void)conversation:(LCIMConversation *)conversation didUpdateForKey:(LCIMConversationUpdatedKey)key { - if ([key isEqualToString:LCIMConversationUpdatedKeyLastReadAt]) { - NSDate *lastReadAt = conversation.lastReadAt; - /* Jerry 阅读了你的消息。可以使用 lastReadAt 更新 UI,例如把时间戳小于 lastReadAt 的消息都标记为已读。 */ - } - } - ``` - ```js - var { Event } = require('leancloud-realtime'); - conversation.on(Event.LAST_READ_AT_UPDATE, function() { - console.log(conversation.lastReadAt); - // 在 UI 中将早于 lastReadAt 的消息都标记为「已读」 - }); - ``` - ```swift - func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .message(event: let messageEvent): - switch messageEvent { - case let .read(byClientID: byClientID, messageID: messageID, readTimestamp: readTimestamp): - if messageID == message.ID { - message.readTimestamp = readTimestamp - } - default: - break - } - default: - break - } - } - ``` - ```dart - jerry.onLastReadAtUpdated = ({ - Client client, - Conversation conversation, - }) { - // 在 UI 中将早于 lastReadAt 的消息都标记为「已读」 - }; - ``` - - - -注意: - -要使用已读回执,应用需要在初始化的时候开启 [未读消息数更新通知](#未读消息数更新通知) 选项。 - -### 消息免打扰 - -假如某一用户不想再收到某对话的消息提醒,但又不想直接退出对话,可以使用静音操作,即开启「免打扰模式」。具体可以参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)的《消息免打扰》一节。 - -### Will(遗愿)消息 - -即时通讯服务还支持一类比较特殊的消息:Will(遗愿)消息。「Will 消息」是在一个用户突然掉线之后,系统自动通知对话的其他成员关于该成员已掉线的消息,好似在掉线后要给对话中的其他成员一个妥善的交待,所以被戏称为「遗愿」消息,如下图中的「Tom 已断线,无法收到消息」: - -![在一个名为「Tom & Jerry」的对话中,Jerry 收到内容为「Tom 已断线,无法收到消息」的 Will 消息。这条消息看起来像一条系统通知,与普通消息的样式不同。](/img/lastwill-message.png) - -要发送 Will 消息,用户需要设定好消息内容发给云端,云端并不会将其马上发送给对话的成员,而是缓存下来,一旦检测到该用户掉线,云端立即将这条遗愿消息发送出去。开发者可以利用它来构建自己的断线通知的逻辑。 - - - -```cs -LCIMTextMessage message = new LCIMTextMessage("我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。"); -LCIMMessageSendOptions options = new LCIMMessageSendOptions { - Will = true -}; -await conversation.Send(message, options); -``` -```java -LCIMTextMessage message = new LCIMTextMessage(); -message.setText("我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。"); - -LCIMMessageOption option = new LCIMMessageOption(); -option.setWill(true); - -conversation.sendMessage(message, option, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } -}); -``` -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.will = YES; - -LCIMMessage *willMessage = [LCIMTextMessage messageWithText:@"我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。" attributes:nil]; - -[conversation sendMessage:willMessage option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - if (succeeded) { - NSLog(@"遗愿消息已发出。"); - } -}]; -``` -```js -var message = new TextMessage('我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。'); -conversation.send(message, { will: true }).then(function() { - // 发送成功,当前 client 掉线的时候,这条消息会被下发给对话里面的其他成员 -}).catch(function(error) { - // 异常处理 -}); -``` -```swift -do { - let message = IMTextMessage(text: "我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。") - try conversation.send(message: message, options: [.isAutoDeliveringWhenOffline], completion: { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - }) -} catch { - print(error) -} -``` -```dart -try { - TextMessage message = TextMessage(); - message.text = '我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。'; - await conversation.send(message: message, will: true); -} catch (e) { - print(e); -} -``` - - - - - -客户端发送完毕之后就完全不用再关心这条消息了,云端会自动在发送方异常掉线后通知其他成员,接收端则根据自己的需求来做 UI 的展现。 - -Will 消息有 **如下限制**: - -- Will 消息是与当前用户绑定的,并且只对最后一次设置的「对话 + 消息」生效。如果用户在多个对话中设置了 Will 消息,那么只有最后一次设置有效;如果用户在同一个对话中设置了多条 Will 消息,也只有最后一次设置有效。 -- Will 消息不会进入目标对话的消息历史记录。 -- 当用户主动退出即时通讯服务时,系统会认为这是计划性下线,不会下发 Will 消息(如有)。 - -### 消息内容过滤 - -对于多人参与的聊天群组来说,内容的审核和实时过滤是产品运营上的基本要求。我们即时通讯服务默认提供了敏感词过滤的功能,具体可以参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)的《消息内容的实时过滤》一节。 - -### 本地发送失败的消息 - -有时你可能需要将发送失败的消息临时保存到客户端本地的缓存中,等到合适时机再进行处理。例如,将由于网络突然中断而发送失败的消息先保留下来,在消息列表中展示这种消息时,额外添加出错的提示符号和重发按钮,待网络恢复后再由用户选择是否重发。 - -即时通讯 Android 和 iOS SDK 默认提供了消息本地缓存的功能,消息缓存中保存的都是已经成功上行到云端的消息,并且能够保证和云端的数据同步。为了方便开发者,SDK 也支持将一时失败的消息加入到缓存中。 - -将消息加入缓存的代码如下: - - - -```cs -// 暂不支持 -``` -```java -conversation.addToLocalCache(message); -``` -```objc -[conversation addMessageToCache:message]; -``` -```js -// 暂不支持 -``` -```swift -do { - try conversation.insertFailedMessageToCache(failedMessage) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -// 暂不支持 -``` - - - - -将消息从缓存中删除: - - - -```cs -// 暂不支持 -``` -```java -conversation.removeFromLocalCache(message); -``` -```objc -[conversation removeMessageFromCache:message]; -``` -```js -// 暂不支持 -``` -```swift -do { - try conversation.removeFailedMessageFromCache(failedMessage) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -// 暂不支持 -``` - - - - - -从缓存中取出来的消息,在 UI 展示的时候可以根据 `message.status` 的属性值来做不同的处理,`status` 属性为 `LCIMMessageStatusFailed` 时即表示是发送失败了的本地消息,这时可以在消息旁边显示一个重新发送的按钮。通过将失败消息加入到 SDK 缓存中,还有一个好处就是,消息从缓存中取出来再次发送,不会造成服务端消息重复,因为 SDK 有做专门的去重处理。 - - -## 离线消息同步 - -如果用户不上线,即时通讯的消息就总是无法下发,客户端如果长时间下线,会导致大量消息堆积在云端,此后如果用户再上线,我们该如何处理才能保证消息完全不丢失呢? - -即时通讯服务提供两种方式进来同步离线消息: - -- 一种是云端主动往客户端「推」的方式。云端会记录用户在每一个参与对话中接收消息的位置,在用户登录上线后,会以对话为单位来主动、多次下发消息(客户端按照收到新消息进行处理)。对每个对话,云端至多下发 20 条离线消息,更多消息则不会继续下发。 -- 另一种是客户端主动从云端「拉」的方式。云端会记录下用户在每一个参与对话中接收的最后一条消息的位置,在用户重新登录上线后,实时计算出用户离线期间产生未读消息的对话列表及对应的未读消息数,以「未读消息数更新」的事件通知到客户端,然后客户端在需要的时候来主动拉取这些离线消息。 - -第一种方式实现简单,但是因为云端对一个对话只主动「推」 20 条离线消息,更多未读消息对客户端来说是透明的,所以只能满足一些轻量级的应用需求,如果产品层要在一个对话内显示消息阅读的进度,或者用精确的未读消息数来提示用户,就无法做到了。因此我们现在都切换到了第二种客户端主动「拉」的方式。 - -由于历史原因,不同平台的 SDK 对两种方式的支持度是不一样的: -1. Android、iOS SDK 同时支持这两种方式,且默认是「拉」的方式 -2. JavaScript SDK 仅支持「拉」的方式 -3. .NET SDK 目前还不支持第二种方式。 - -> 注意,请不要混合使用上面两种方式,比如在 iOS 平台使用第一种方式获取离线消息,而 Android 平台使用第二种方式获取离线消息,可能导致所有离线消息无法正常获取。 - -### 未读消息数更新通知 - -在客户端重新登录上线后,即时通讯云端会实时计算下线时间段内当前用户参与过的对话中的新消息数量。 - -客户端只有设置了主动拉取的方式,云端才会在必要的时候下发这一通知。如前所述,对于 JavaScript / Android / iOS SDK 来说,仅支持客户端主动拉取未读消息,所以不需要再做什么设置。 - -客户端 SDK 会在 `IMConversation` 上维护一个 `unreadMessagesCount` 字段,来统计当前对话中存在有多少未读消息。 - -客户端用户登录之后,云端会以「未读消息数更新」事件的形式,将当前用户所在的多对 `` 数据通知到客户端,这就是客户端维护的 `` 初始值。之后 SDK 在收到新的在线消息的时候,会自动增加对应的 `unreadMessageCount` 计数。直到用户把某一个对话的未读消息清空,这时候云端和 SDK 的 `` 计数都会清零。 - -> 注意:开启未读消息数后,在开发者没有主动重置未读消息的情况下,未读消息数将一直累计。 -> 客户端再次离线并不会重置未读消息数。 -> 包括客户端在线时收到的消息,也会导致未读消息数增加。 -> 因此开发者需要在合适时机通过将对话标记为已读主动清除未读消息数。 - -客户端 SDK 在 `` 数字变化的时候,会通过 `IMClient` 派发「未读消息数量更新(`UNREAD_MESSAGES_COUNT_UPDATE`)」事件到应用层。开发者可以监听 `UNREAD_MESSAGES_COUNT_UPDATE` 事件,在对话列表界面上更新这些对话的未读消息数量。建议开发者在应用层面对未读计数的结果进行持久化缓存,如果同一个对话有两个不同的未读数,则使用新数据直接覆盖老数据,这样对话列表里面展示的未读数会比较准确。 - - - -```cs -tom.OnUnreadMessagesCountUpdated = (convs) => { - foreach (LCIMConversation conv in convs) { - // conv.Unread 即该 conversation 的未读消息数量 - } -}; -``` -```java -// 实现 LCIMConversationEventHandler 的代理方法 onUnreadMessagesCountUpdated 来得到未读消息的数量变更的通知 -onUnreadMessagesCountUpdated(LCIMClient client, LCIMConversation conversation) { - // conversation.getUnreadMessagesCount() 即该 conversation 的未读消息数量 -} -``` -```objc -// 使用代理方法 conversation:didUpdateForKey: 来观察对话的 unreadMessagesCount 属性 -- (void)conversation:(LCIMConversation *)conversation didUpdateForKey:(LCIMConversationUpdatedKey)key { - if ([key isEqualToString:LCIMConversationUpdatedKeyUnreadMessagesCount]) { - NSUInteger unreadMessagesCount = conversation.unreadMessagesCount; - /* 有未读消息产生,请更新 UI,或者拉取对话。 */ - } -} -``` -```js -var { Event } = require('leancloud-realtime'); -client.on(Event.UNREAD_MESSAGES_COUNT_UPDATE, function(conversations) { - for(let conv of conversations) { - console.log(conv.id, conv.name, conv.unreadMessagesCount); - } -}); -``` -```swift -func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) { - switch event { - case .unreadMessageCountUpdated: - print(conversation.unreadMessageCount) - default: - break - } -} -``` -```dart -tom.onUnreadMessageCountUpdated = ({ - Client client, - Conversation conversation, -}) { - // conversation.unreadMessageCount 即该 conversation 的未读消息数量 -}; -``` - - - - - -对开发者来说,在 `UNREAD_MESSAGES_COUNT_UPDATE` 事件响应的时候,SDK 传给应用层的 `Conversation` 对象,其 `lastMessage` 应该是当前时点当前用户在当前对话里面接收到的最后一条消息,开发者如果要展示更多的未读消息,就需要通过消息拉取的接口来主动获取了(参见[即时通讯开发指南第一篇](/v2/sdk/im/guide/beginner)的《聊天记录查询》一节。 - -清除对话未读消息数的唯一方式是调用 `Conversation#read` 方法将对话标记为已读,一般来说开发者至少需要在下面两种情况下将对话标记为已读: - -- 在对话列表点击某对话进入到对话页面时 -- 用户正在某个对话页面聊天,并在这个对话中收到了消息时 - -iOS 和 Android 应用层需要持久化缓存未读计数的细节说明 - -对于未读通知的下发时机和数量,iOS 和 Java/Android 两个平台的 SDK 在内部处理上稍有差异:iOS SDK(Objective-C 和 Swift 都包括)在每次登录即时通讯云端的时候,都会获得云端下发的**大量**未读通知;而 Java/Android SDK 由于内部持久化缓存了通知的时间戳(能减轻服务端压力),所以登录即时通讯云端之后客户端只会收到上次通知时间戳之后发生了变化的**部分**未读数通知。 - -因此 Java SDK 的开发者需要在应用层缓存收到的未读数通知(同一个对话的未读数采用覆盖的方式来更新),而 iOS SDK 这里收到的**大量未读通知并不等于全量数据(云端追踪的有未读消息的对话数不超过 50 个)**,所以也是一样需要在应用层面缓存收到的未读计数结果,这样才能保证对话列表超过 50 个之后未读计数值的准确性。 - - -## 多端登录与单设备登录 - -一个用户可以使用相同的账号在不同的客户端上登录(例如 QQ 网页版和手机客户端可以同时接收到消息和回复消息,实现多端消息同步),而有一些场景下,需要禁止一个用户同时在不同客户端登录,例如我们不能用同一个微信账号在两个手机上同时登录。即时通讯服务提供了灵活的机制,来满足 ***多端登录*** 和 ***单设备登录*** 这两种完全相反的需求。 - -即时通讯 SDK 在生成 `IMClient` 实例的时候,允许开发者在 `clientId` 之外,增加一个额外的 `tag` 标记。云端在用户主动登录的时候,会检查 `` 组合的唯一性。如果当前用户已经在其他设备上使用同样的 `tag` 登录了,那么云端会强制让之前登录的设备下线。如果多个 `tag` 不发生冲突,那么云端会把他们当成独立的设备进行处理,应该下发给该用户的消息会分别下发给所有设备,不同设备上的未读消息计数则是合并在一起的(各端之间消息状态是同步的);该用户在单个设备上发出来的上行消息,云端也会默认同步到其他设备。 - -基于以上机制,即时通讯可以支持应用实现多种业务需求: - -1. 无限制的多端登录:不设置 `tag`,默认对用户的多端登录不作限制。用户可以在多个设备上登录,比如在手机和平板上同时登录,甚至在两台不同的手机上登录,多个设备可以同时接收和回复消息。 -2. 单设备登录:在所有客户端都设置同一个 `tag`,限制用户只能在一台设备上登录。 -3. 有限制的多端登录:通过设置不同的 `tag`,允许用户在多台不同类型的设备上登录。例如,我们可以设计三种 `tag`:`Mobile`、`Pad`、`Web`,分别对应三种类型的设备:手机、平板和电脑,那么用户分别在三种设备上登录就都是允许的,但是却不能同时在两台电脑上登录。详见下面的代码示例。 - -### 设置登录标记 - -按照上面的方案,以手机端登录为例,在创建 `IMClient` 实例的时候,我们增加 `tag: Mobile` 这样的标记: - - - -```cs -LCIMClient client = new LCIMClient(clientId, "Mobile", "your-device-id"); -``` -```java -// 第二个参数:登录标记 tag -LCIMClient currentClient = LCIMClient.getInstance(clientId, "Mobile"); -currentClient.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient avimClient, LCIMException e) { - if(e == null){ - // 与云端建立连接成功 - } - } -}); -``` -```objc -NSError *error; -LCIMClient *currentClient = [[LCIMClient alloc] initWithClientId:@"Tom" tag:@"Mobile" error:&error]; -if (!error) { - [currentClient openWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - // 与云端建立连接成功 - } - }]; -} -``` -```js -realtime.createIMClient('Tom', { tag: 'Mobile' }).then(function(tom) { - console.log('Tom 登录'); -}); -``` -```swift -do { - let client = try IMClient(ID: "CLIENT_ID", tag: "Mobile") - client.open { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -try { - Client tom = Client(id: 'Tom', tag: 'Mobile'); - await tom.open(); -} catch (e) { - print(e); -} -``` - - - - -之后如果同一个用户在另一个手机上再次登录,则较早前登录系统的客户端会被强制下线。 - -### 处理登录冲突 - -即时通讯云端在登录用户的 `` 相同的时候,总是踢掉较早登录的设备,这时候较早登录设备端会收到被云端下线(`CONFLICT`)的事件通知: - - - -```cs -tom.OnClose = (code, detail) => { - -}; -``` -```java -public class AVImClientManager extends LCIMClientEventHandler { - /** - * 实现本方法以处理当前登录被踢下线的情况 - * - * - * @param client - * @param code 状态码说明被踢下线的具体原因 - */ - @Override - public void onClientOffline(LCIMClient avimClient, int i) { - if(i == 4111){ - // 适当地弹出友好提示,告知当前用户的 clientId 在其他设备上登录了 - } - } -} - -// 自定义实现的 LCIMClientEventHandler 需要注册到 SDK 后,SDK 才会通过回调 onClientOffline 来通知开发者 -LCIMClient.setClientEventHandler(new AVImClientManager()); -``` -```objc -- (void)imClientClosed:(LCIMClient *)imClient error:(NSError * _Nullable)error -{ - if ([error.domain isEqualToString:kLeanCloudErrorDomain] && - error.code == 4111) { - // 适当的弹出友好提示,告知当前用户的 clientId 在其他设备上登录了 - } -} -``` -```js -var { Event } = require('leancloud-realtime'); -tom.on(Event.CONFLICT, function() { - // 弹出提示,告知当前用户的 clientId 在其他设备上登录了 -}); -``` -```swift -func client(_ client: IMClient, event: IMClientEvent) { - switch event { - case .sessionDidClose(error: let error): - if error.code == 4111 { - // 弹出提示,告知当前用户的 clientId 在其他设备上登录了 - } - default: - break - } -} -``` -```dart -tom.onClosed = ({ - Client client, - RTMException exception, -}) { - if (exception.code == '4111') { - // 适当的弹出友好提示,告知当前用户的 clientId 在其他设备上登录了 - } -}; -``` - - - - -如上述代码中,被动下线的时候,云端会告知原因,因此客户端在做展现的时候也可以做出类似于 QQ 一样友好的通知。 - -以上提到的登录均指用户主动进行登录操作。 -已登录用户在应用启动、网络中断等场景下,SDK 会自动重新登录。 -这种情况下,如果触发登录冲突,云端并不会踢掉较早登录的设备,自动重新登录的设备则会收到登录冲突的报错,登录失败。 - -相应地,应用开发者如果希望在用户主动登录触发冲突时,不踢掉较早登录的设备,而提示用户登录失败,可以在登录时传入参数指明这一点: - - - -```cs -await tom.Open(false); -``` -```java -LCIMClientOpenOption openOption = new LCIMClientOpenOption(); -openOption.setReconnect(true); -LCIMClient currentClient = LCIMClient.getInstance(clientId, "Mobile"); -currentClient.open(openOption, new LCIMClientCallback() { - @Override - public void done(LCIMClient avimClient, LCIMException e) { - if(e == null){ - // 与云端建立连接成功 - } - } -}); -``` -```objc -NSError *err; -LCIMClient *currentClient = [[LCIMClient alloc] initWithClientId:@"Tom" tag:@"Mobile" error:&err]; -if (err) { - NSLog(@"init failed with error: %@", err); -} else { - [currentClient openWithOption:LCIMClientOpenOptionReopen callback:^(BOOL succeeded, NSError * _Nullable error) { - if ([error.domain isEqualToString:kLeanCloudErrorDomain] && - error.code == 4111) { - // 冲突时登录失败,不会踢掉较早登录的设备 - } - }]; -} -``` -```js -realtime.createIMClient('Tom', { tag: 'Mobile', isReconnect: true }).then(function(tom) { - console.log('冲突时登录失败,不会踢掉较早登录的设备'); -}); -``` -```swift -do { - let client = try IMClient(ID: "Tom", tag: "Mobile") - client.open(options: [.reconnect]) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - if error.code == 4111 { - // 冲突时登录失败,不会踢掉较早登录的设备 - } - } - } -} catch { - print(error) -} -``` -```dart -try { - Client tom = Client(id: 'Tom', tag: 'Mobile'); - // 冲突时登录失败,不会踢掉较早登录的设备 - await tom.open(reconnect: true); -} catch (e) { - print(e); -} -``` - - - - - -## 扩展自己的消息类型 - -尽管即时通讯服务默认已经包含了丰富的消息类型,但是我们依然支持开发者根据业务需要扩展自己的消息类型,例如允许用户之间发送名片、红包等等。这里「名片」和「红包」就可以是应用层定义的自己的消息类型。 - -### 自定义消息属性 - -即时通讯 SDK 默认提供了多种消息类型用来满足常见的需求: - -- `TextMessage` 文本消息 -- `ImageMessage` 图像消息 -- `AudioMessage` 音频消息 -- `VideoMessage` 视频消息 -- `FileMessage` 普通文件消息(.txt/.doc/.md 等各种) -- `LocationMessage` 地理位置消息 - -这些消息类型还支持应用层设置若干 key-value 自定义属性来实现扩展。譬如有一条文本消息需要附带城市信息,这时候开发者使用消息类中预留的 `attributes` 属性就可以保存额外信息了。 - - - -```cs -LCIMTextMessage messageWithCity = new LCIMTextMessage("天气太冷了"); -messageWithCity["city"] = "北京"; -``` -```java -LCIMTextMessage messageWithCity = new LCIMTextMessage(); -messageWithCity.setText("天气太冷了"); -HashMap attr = new HashMap(); -attr.put("city", "北京"); -messageWithCity.setAttrs(attr); -``` -```objc -NSDictionary *attributes = @{ @"city": @"北京" }; -LCIMTextMessage *messageWithCity = [LCIMTextMessage messageWithText:@"天气太冷了" attributes:attributes]; -``` -```js -var messageWithCity = new TextMessage("天气太冷了"); -messageWithCity.setAttributes({ city: "北京" }); -``` -```swift -let messageWithCity = IMTextMessage(text: "天气太冷了") -messageWithCity.attributes = ["city": "北京"]; -``` -```dart -TextMessage message = TextMessage(); -message.text = '天气太冷了'; -message.attributes = {'city': '北京'}; -``` - - - - - -### 自定义消息类型 - -在默认的消息类型完全无法满足需求的时候,可以实现和使用自定义的消息类型。 - - -<> - -继承于 `LCIMTypedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -* 首先定义一个自定义的子类继承自 `LCIMTypedMessage`。 -* 然后在初始化的时候注册这个子类。 - -```cs -class EmojiMessage : LCIMTypedMessage { - public const int EmojiMessageType = 1; - - public override int MessageType => EmojiMessageType; - - public string Ecode { - get { - return data["ecode"] as string; - } set { - data["ecode"] = value; - } - } -} - -// 注册子类 -LCIMTypedMessage.Register(EmojiMessage.EmojiMessageType, () => new EmojiMessage()); -``` - - -<> - -继承于 `LCIMTypedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -* 实现新的消息类型,继承自 `LCIMTypedMessage`。这里需要注意: - * 在 class 上增加一个 `@LCIMMessageType(type=123)` 的 Annotation
    具体消息类型的值(这里是 `123`)由开发者自己决定。内建消息类型使用负数,所有正数都预留给开发者扩展使用。 - * 在消息内部声明字段属性时,要增加 `@LCIMMessageField(name="")` 的 Annotation
    `name` 为可选字段,同时自定义的字段要有对应的 getter/setter 方法。 - * **请不要遗漏空的构造方法**(参考下面的示例代码),否则会造成类型转换失败。 -* 调用 `LCIMMessageManager.registerLCIMMessageType()` 函数进行类型注册。 -* 调用 `LCIMMessageManager.registerMessageHandler()` 函数进行消息处理 handler 注册。 - -注意:如果你是使用 Kotlin 来开发,由于 Kotlin 对反射的处理方式与 Java 有细微差异,导致 `LCIMMessageField` 注释不能产生作用,所以 SDK 实际发送的自定义消息数据不全。我们已经在 `6.4.4` 版本的 SDK 中对这一问题进行了优化,请 Kotlin 开发者升级到 6.4.4 及其后续版本来定制子类化消息。 - -```java -@LCIMMessageType(type = 123) -public class CustomMessage extends LCIMTypedMessage { - // 空的构造方法,不可遗漏 - public CustomMessage() { - - } - - @LCIMMessageField(name = "_lctext") - String text; - @LCIMMessageField(name = "_lcattrs") - Map attrs; - - public String getText() { - return this.text; - } - - public void setText(String text) { - this.text = text; - } - - public Map getAttrs() { - return this.attrs; - } - - public void setAttrs(Map attr) { - this.attrs = attr; - } -} - -// 注册自定义类型 -LCIMMessageManager.registerLCIMMessageType(CustomMessage.class); -``` - - -<> - -继承于 `LCIMTypedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -* 实现 `LCIMTypedMessageSubclassing` 协议; -* 子类将自身类型进行注册,一般可在子类的 `+load` 方法或者 `UIApplication` 的 `-application:didFinishLaunchingWithOptions:` 方法里面调用 `[YourClass registerSubclass]`。 - -```objc -// 定义 - -@interface CustomMessage : LCIMTypedMessage - -+ (LCIMMessageMediaType)classMediaType; - -@end - -@implementation CustomMessage - -+ (LCIMMessageMediaType)classMediaType { - return 123; -} - -@end - -// 注册子类 -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [CustomMessage registerSubclass]; -} -``` - - -<> - -通过继承 `TypedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -* 申明新的消息类型,继承自 `TypedMessage` 或其子类,然后: - * 对 class 使用 `messageType(123)` 装饰器,具体消息类型的值(这里是 `123`)由开发者自己决定(内建消息类型使用负数,所有正数都预留给开发者扩展使用)。 - * 对 class 使用 `messageField(['fieldName'])` 装饰器来声明需要发送的字段。 -* 调用 `Realtime#register()` 函数注册这个消息类型。 - -举个例子,实现一个在 [暂态消息](#暂态消息) 中提出的 `OperationMessage`: - -```js -// TypedMessage, messageType, messageField 都是由 leancloud-realtime 这个包提供的 -// 在浏览器中则是 var { TypedMessage, messageType, messageField } = AV; -var { TypedMessage, messageType, messageField } = require('leancloud-realtime'); -// 定义 OperationMessage 类,用于发送和接收所有的用户操作消息 -export class OperationMessage extends TypedMessage {} -// 指定 type 类型,可以根据实际换成其他正整数 -messageType(1)(OperationMessage); -// 声明需要发送 op 字段 -messageField('op')(OperationMessage); -// 注册消息类,否则收到消息时无法自动解析为 OperationMessage -realtime.register(OperationMessage); -``` - - -<> - -继承于 `IMCategorizedMessage`,开发者也可以扩展自己的富媒体消息。其要求和步骤是: - -* 实现 `IMMessageCategorizing` 协议; -* 子类将自身类型进行注册,一般可在 `AppDelegate` 的 `application(_:didFinishLaunchingWithOptions:)` 方法里面调用 `try CustomMessage.register()`。 - -```swift -// 定义 CustomMessage 类 -class CustomMessage: IMCategorizedMessage { - - // 指定 type 类型,可以根据实际换成其他正整数 - class override var messageType: MessageType { - return 1 - } -} - -// 注册消息类型 -func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - do { - try CustomMessage.register() - } catch { - print(error) - return false - } - - return true -} -``` - - -<> - -继承于 `TypedMessage`,开发者也可以扩展自己的富媒体消息。步骤是: - -```dart -// 自定义消息类型 CustomMessage -class CustomMessage extends TypedMessage { - @override - - int get type => 123; - CustomMessage() : super(); - CustomMessage.from({ - @required String text, - //... - }) { - this.text = text; - } -} -TypedMessage.register(() => CustomMessage()); -``` - - -
    - - -自定义消息的接收,可以参看[即时通讯开发指南第一篇](/v2/sdk/im/guide/beginner)的《再谈接收消息》。 - -## 进一步阅读 - -- [即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)《安全与签名、黑名单和权限管理、玩转直播聊天室和临时对话》。 -- [即时通讯开发指南第四篇](/v2/sdk/im/guide/systemconv)[详解消息 hook 与系统对话](/v2/sdk/im/guide/systemconv)。 diff --git a/versioned_docs/version-v2/sdk/10-im/02-guide/03-senior.mdx b/versioned_docs/version-v2/sdk/10-im/02-guide/03-senior.mdx deleted file mode 100644 index 3a4ddcf75..000000000 --- a/versioned_docs/version-v2/sdk/10-im/02-guide/03-senior.mdx +++ /dev/null @@ -1,1640 +0,0 @@ ---- -id: senior -title: 三,安全与签名、黑名单和权限管理、玩转聊天室和临时对话 -sidebar_label: 权限与聊天室 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import Mermaid from '/src/docComponents/Mermaid'; - - - -## 本章导读 - -在前一篇[消息收发的更多方式,离线推送与消息同步,多设备登录](/v2/sdk/im/guide/intermediate)中,我们演示了与消息相关的更多特殊需求的实现方法,现在,我们会更进一步,从系统安全和成员权限管理的角度,给大家详细说明: - -- 如何通过第三方鉴权来控制客户端登录与操作 -- 如何对成员权限进行限制,以保证聊天流程能被运营人员很好管理起来 -- 如何实现一个不限人数的直播聊天室 -- 如何对大型群聊中的消息进行实时内容过滤 -- 如何使用临时对话 - -## 安全与签名 - -即时通讯服务有一大特色就是让应用账户系统和聊天服务解耦,终端用户只需要登录应用账户系统就可以直接使用即时通讯服务,同时从系统安全角度出发,我们还提供了第三方操作签名的机制来保证聊天通道的安全性。 - -该机制的工作架构是,在客户端和即时通讯云端之间,增加应用自己的鉴权服务器(也就是即时通讯服务之外的「第三方」),在客户端开始一些有安全风险的操作命令(如登录聊天服务、建立对话、加入群组、邀请他人等)之前,先通过鉴权服务器获取签名,之后即时通讯云端会依据它和第三方鉴权服务之间的协议来验证该签名,只有附带有效签名的请求才会被执行,非法请求全部会被阻止下来。 - -使用操作签名可以保证聊天通道的安全,这一功能默认是关闭的,可以在 **云服务控制台 > 即时通讯 > 设置 > 即时通讯选项** 中进行开启: - -- **登录启用签名认证**,用于控制所有的用户登录 -- **对话操作启用签名认证**,用于控制新建或加入对话、邀请/踢出对话成员等操作 -- **聊天记录查询启用签名认证**,用于控制聊天记录查询操作 -- **黑名单操作启用签名认证**,用于控制修改对话的黑名单用户列表操作(关于黑名单,请参考下一节) - -开发者可根据实际需要进行选择。一般来说,**登录认证** 是最基本的安全机制,我们强烈建议开发者开启登录认证。 - ->应用鉴权服务器: 1. 携登录、新建会话、加入群组、邀请他人、踢出成员等行为请求签名 -应用鉴权服务器-->>终端: 2. 生成时间戳、随机字符串和签名返回给客户端 -终端->>即时通讯服务云端: 3. 将签名编码到请求中发给即时通讯服务器 -即时通讯服务云端-->>终端: 4. 对请求的内容和签名进行验证,执行后续操作 -`} /> - -1. 客户端进行登录或新建对话等操作,SDK 会调用 `SignatureFactory` 的实现,并携带用户信息和用户行为(登录、新建对话或群组操作)请求签名; -2. 应用自有的权限系统,或应用在云引擎上的签名程序收到请求,进行权限验证,如果通过则利用下文所述的 [签名算法](#用户登录签名) 生成时间戳、随机字符串和签名返回给客户端; -3. 客户端获得签名后,编码到请求中,发给即时通讯服务器; -4. 即时通讯服务器对请求的内容和签名做一遍验证,确认这个操作是被应用服务器允许的,进而执行后续的实际操作。 - -签名采用 **HMAC-SHA1** 算法,输出字节流的十六进制字符串(hex dump)。针对不同的请求,开发者需要拼装不同组合的字符串,加上 UTC timestamp 以及随机字符串作为签名的消息(参见后续格式说明)。总体上,签名就是使用特定的密钥(在这里我们使用应用的 `Master Key`),对输入的消息(即「签名的消息」)进行哈希计算,得到一串十六进制的字符串,这就是最终的「签名」。 - -对于使用 `LCUser` 的应用,可使用 REST API 获取登录签名进行登录认证。 - -### 签名格式说明 - -下面我们详细说明一下不同操作的签名消息格式。 - -#### 用户登录签名 - -签名的消息格式如下,注意 `clientid` 与 `timestamp` 之间是两个冒号: - -``` -appid:clientid::timestamp:nonce -``` - -参数 | 说明 ---- | --- -`appid` | 应用的 ID。 -`clientid` | 登录时使用的 `clientId`。 -`timestamp` | 当前的 UTC 时间距离 Unix epoch 的 **毫秒数**。 -`nonce` | 随机字符串。 - -> 注意:签名的 key **必须** 是应用的 `Master Key`,你可以在 **云服务控制台 > 设置 > 应用凭证** 里找到。**请保护好 Master Key,不要泄露给任何无关人员。** - -开发者可以实现自己的 `SignatureFactory`,调用远程服务器的签名接口获得签名。如果你没有自己的服务器,可以直接在云引擎上通过 **网站托管** 来实现自己的签名接口。在移动应用中直接进行签名的做法 **非常危险**,它可能导致你的 **Master Key** 泄漏。 - -签名的有效期是 6 个小时,强制下线后签名立即失效。 -签名失效不影响当前在线的 client。 - -#### 开启对话签名 - -新建一个对话的时候,签名的消息格式为: - -``` -appid:clientid:sorted_member_ids:timestamp:nonce -``` - -* `appid`、`clientid`、`timestamp` 和 `nonce` 的含义 [同上](#用户登录签名)。 -* `sorted_member_ids` 是以半角冒号(`:`)分隔、**升序排序** 的 `clientId`,即邀请参与该对话的成员列表。 - -#### 群组功能的签名 - -在群组功能中,我们对 **加群**、**邀请** 和 **踢出群** 这三个动作也允许加入签名,签名的消息格式是: - -``` -appid:clientid:convid:sorted_member_ids:timestamp:nonce:action -``` - -* `appid`、`clientid`、`sorted_member_ids`、`timestamp` 和 `nonce` 的含义同上。对创建群的情况,这里 `sorted_member_ids` 是空字符串。 -* `convid` 是此次行为关联的对话 ID。 -* `action` 是此次行为的动作,`invite` 表示加群和邀请,`kick` 表示踢出群。 - -#### 查询聊天记录的签名 - -``` -appid:client_id:convid:nonce:timestamp -``` - -各参数的含义同上。 - -注意,此签名仅用于通过 REST API 查询历史消息,客户端 SDK 不适用。 - -#### 黑名单的签名 - -由于黑名单有两种情况,所以签名的消息格式也有两种: - -1. `client` 对 `conversation` - - ``` - appid:clientid:convid::timestamp:nonce:action - ``` - - - `action` 是此次行为的动作,`client-block-conversations` 表示添加黑名单,`client-unblock-conversations` 表示取消黑名单。 - -2. `conversation` 对 `client` - - ``` - appid:clientid:convid:sorted_member_ids:timestamp:nonce:action - ``` - - - `action` 是此次行为的动作,`conversation-block-clients` 表示添加黑名单,`conversation-unblock-clients` 表示取消黑名单。 - - `sorted_member_ids` 同上。 - -### 云引擎签名范例 - -为了帮助开发者理解云端签名的算法,我们开源了一个用「Node.js + 云引擎」实现签名的云端,供开发者学习和使用:[即时通讯云引擎签名 Demo](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/rtm-signature.js)。 - -### 客户端如何支持操作签名 - -上面的签名算法,都是对第三方鉴权服务器如何进行签名的协议说明,在开启了操作签名的前提下,客户端这边的使用流程需要进行相应的改变,增加请求签名的环节,才能让整套机制顺利运行起来。 - -即时通讯 SDK 为每一个 `AVIMClient` 实例都预留了一个 `Signature` 工厂接口,这个接口默认不设置就表示不使用签名,启动签名的时候,只需要在客户端实现这一接口,调用远程服务器的签名接口获得签名,并把它绑定到 `AVIMClient` 实例上即可: - - - -```cs -public class LocalSignatureFactory : ILCIMSignatureFactory { - const string MasterKey = "pyvbNSh5jXsuFQ3C8EgnIdhw"; - - public Task CreateConnectSignature(string clientId) { - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, string.Empty, timestamp.ToString(), nonce); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - public Task CreateStartConversationSignature(string clientId, IEnumerable memberIds) { - string sortedMemberIds = string.Empty; - if (memberIds != null) { - List sortedMemberList = memberIds.ToList(); - sortedMemberList.Sort(); - sortedMemberIds = string.Join(":", sortedMemberList); - } - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, sortedMemberIds, timestamp.ToString(), nonce); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - public Task CreateConversationSignature(string conversationId, string clientId, IEnumerable memberIds, string action) { - string sortedMemberIds = string.Empty; - if (memberIds != null) { - List sortedMemberList = memberIds.ToList(); - sortedMemberList.Sort(); - sortedMemberIds = string.Join(":", sortedMemberList); - } - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, conversationId, sortedMemberIds, timestamp.ToString(), nonce, action); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - public Task CreateBlacklistSignature(string conversationId, string clientId, IEnumerable memberIds, string action) { - string sortedMemberIds = string.Empty; - if (memberIds != null) { - List sortedMemberList = memberIds.ToList(); - sortedMemberList.Sort(); - sortedMemberIds = string.Join(":", sortedMemberList); - } - long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); - string nonce = NewNonce(); - string signature = GenerateSignature(LCApplication.AppId, clientId, conversationId, sortedMemberIds, timestamp.ToString(), nonce, action); - return Task.FromResult(new LCIMSignature { - Signature = signature, - Timestamp = timestamp, - Nonce = nonce - }); - } - - private static string SignSHA1(string key, string text) { - HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key)); - byte[] bytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(text)); - string signature = BitConverter.ToString(bytes).Replace("-", string.Empty); - return signature; - } - - private static string NewNonce() { - byte[] bytes = new byte[10]; - using (RandomNumberGenerator generator = RandomNumberGenerator.Create()) { - generator.GetBytes(bytes); - } - return Convert.ToBase64String(bytes); - } - - private static string GenerateSignature(params string[] args) { - string text = string.Join(":", args); - string signature = SignSHA1(MasterKey, text); - return signature; - } -} - -// 设置签名工程 -LCIMClient tom = new LCIMClient("tom", signatureFactory: new LocalSignatureFactory()); -``` -```java -// 这是一个依赖云引擎完成签名的示例 -public class KeepAliveSignatureFactory implements SignatureFactory { - @Override - public Signature createSignature(String peerId, List watchIds) throws SignatureException { - Map params = new HashMap(); - params.put("self_id",peerId); - params.put("watch_ids",watchIds); - - try{ - Object result = LCCloud.callFunction("sign",params); - if(result instanceof Map){ - Map serverSignature = (Map) result; - Signature signature = new Signature(); - signature.setSignature((String)serverSignature.get("signature")); - signature.setTimestamp((Long)serverSignature.get("timestamp")); - signature.setNonce((String)serverSignature.get("nonce")); - return signature; - } - }catch(LCException e){ - throw (SignatureFactory.SignatureException) e; - } - return null; - } - - @Override - public Signature createConversationSignature(String convId, String peerId, - List targetPeerIds,String action) throws SignatureException{ - Map params = new HashMap(); - params.put("client_id",peerId); - params.put("conv_id",convId); - params.put("members",targetPeerIds); - params.put("action",action); - - try{ - Object result = LCCloud.callFunction("sign2",params); - if(result instanceof Map){ - Map serverSignature = (Map) result; - Signature signature = new Signature(); - signature.setSignature((String)serverSignature.get("signature")); - signature.setTimestamp((Long)serverSignature.get("timestamp")); - signature.setNonce((String)serverSignature.get("nonce")); - return signature; - } - }catch(LCException e){ - throw (SignatureFactory.SignatureException) e; - } - return null; - } - - @Override - public Signature createBlacklistSignature(String clientId, String conversationId, List memberIds, - String action) throws SignatureException { - Map params = new HashMap(); - params.put("client_id",clientId); - params.put("conv_id",conversationId); - params.put("members",memberIds); - params.put("action",action); - - try{ - Object result = LCCloud.callFunction("sign3",params); - if(result instanceof Map){ - Map serverSignature = (Map) result; - Signature signature = new Signature(); - signature.setSignature((String)serverSignature.get("signature")); - signature.setTimestamp((Long)serverSignature.get("timestamp")); - signature.setNonce((String)serverSignature.get("nonce")); - return signature; - } - }catch(LCException e){ - throw (SignatureFactory.SignatureException) e; - } - return null; - } -} - -// 将签名工厂类的实例绑定到 LCIMClient 上 -LCIMOptions.getGlobalOptions().setSignatureFactory(new KeepAliveSignatureFactory()); -``` -```objc -// 实现 LCIMSignatureDataSource 协议 -- (void)client:(LCIMClient *)client - action:(LCIMSignatureAction)action - conversation:(LCIMConversation * _Nullable)conversation - clientIds:(NSArray * _Nullable)clientIds -signatureHandler:(void (^)(LCIMSignature * _Nullable))handler -{ - if ([action isEqualToString:LCIMSignatureActionOpen]) { - // 开启了签名认证的模块,需返回对应的签名 - LCIMSignature *signature; - /* - ... - ... - 具体实现可以参考章节「云引擎签名范例」 - */ - handler(signature); - } else { - // 没有开启签名认证的模块,需返回 nil - handler(nil); - } -} - -// 设置协议代理者 -NSError *error; -LCIMClient *imClient = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error]; -if (!error) { - imClient.signatureDataSource = signatureDelegator; -} -``` -```js -// 基于云引擎进行登录签名的 signature 工厂方法 -var signatureFactory = function(clientId) { - return AV.Cloud.rpc('sign', { clientId: clientId }); // AV.Cloud.rpc 返回一个 Promise -}; -// 基于云引擎进行对话创建/加入、邀请成员、踢出成员等操作签名的 signature 工厂方法 -var conversationSignatureFactory = function(conversationId, clientId, targetIds, action) { - return AV.Cloud.rpc('sign-conversation', { - conversationId: conversationId, - clientId: clientId, - targetIds: targetIds, - action: action, - }); -}; -// 基于云引擎进行对话黑名单操作签名的 signature 工厂方法 -var blacklistSignatureFactory = function(conversationId, clientId, targetIds, action) { - return AV.Cloud.rpc('sign-blacklist', { - conversationId: conversationId, - clientId: clientId, - targetIds: targetIds, - action: action, - }); -}; - -realtime.createIMClient('Tom', { - signatureFactory: signatureFactory, - conversationSignatureFactory: conversationSignatureFactory, - blacklistSignatureFactory: blacklistSignatureFactory -}).then(function(tom) { - console.log('Tom 登录'); -}).catch(function(error) { - // 如果 signatureFactory 抛出了异常,或者签名没有验证通过,会在这里被捕获 -}); -``` -```swift -class SignatureDelegator: IMSignatureDelegate { - - // 基于云引擎的获取客户端登录签名的函数 - func getClientOpenSignature(completion: (IMSignature) -> Void) { - // 具体实现可以参考章节「云引擎签名范例」 - } - - func client(_ client: IMClient, action: IMSignature.Action, signatureHandler: @escaping (IMClient, IMSignature?) -> Void) { - switch action { - case .open: - // 开启了签名认证的模块,需返回对应的签名 - self.getClientOpenSignature { (signature) in - signatureHandler(client, signature) - } - default: - // 没有开启签名认证的模块,需返回 nil - signatureHandler(client, nil) - } - } -} - -do { - let signatureDelegator = SignatureDelegator() - let client = try IMClient(ID: "Tom", signatureDelegate: signatureDelegator) -} catch { - print(error) -} -``` -```dart - -``` - - - - - -需要强调的是:开发者切勿在客户端直接使用 `Master Key` 进行签名操作,因为 `Master Key` 一旦泄露,会造成应用的数据处于高危状态,后果不容小视。因此,强烈建议开发者将签名的具体代码托管在安全性高稳定性好的云端服务器上(例如云引擎)。" - -### 内建账户系统(User)的签名机制 - -`User` 是存储服务提供的默认账户系统,对于使用了它来完成用户注册、登录的产品来说,终端用户通过 `User` 账户系统的登录认证之后,转到即时通讯服务上,是无需再进行登录签名操作的。 -使用 `User` 账号系统登录即时通讯服务的示例如下: - - - -```cs -LCUser user = await LCUser.Login("username", "password"); -CIMClient client = new LCIMClient(user); -await client.Open(); -``` -```java -// 以 LCUser 的用户名和密码登录到内建账户系统 -LCUser.logInInBackground("username", "password", new LogInCallback() { - @Override - public void done(LCUser user, LCException e) { - if (null != e) { - return; - } - // 以 LCUser 实例创建了一个 client - LCIMClient client = LCIMClient.getInstance(user); - // 登录即时通讯云端 - client.open(new LCIMClientCallback() { - @Override - public void done(final LCIMClient avimClient, LCIMException e) { - // 执行其他逻辑 - } - }); - } -}); -``` -```objc -// 以 LCUser 的用户名和密码登录到内建账户系统 -[LCUser logInWithUsernameInBackground:username password:password block:^(LCUser * _Nullable user, NSError * _Nullable error) { - // 以 LCUser 实例创建了一个 client - NSError *err; - LCIMClient *client = [[LCIMClient alloc] initWithUser:user error:&err]; - if (!err) { - // 登录即时通讯云端 - [client openWithCallback:^(BOOL succeeded, NSError * _Nullable error) { - // 执行其他逻辑 - }]; - } -}]; -``` -```js -var AV = require('leancloud-storage'); -// 以用户名和密码登录内建账户系统 -AV.User.logIn('username', 'password').then(function(user) { - // 直接使用 LCUser 实例登录即时通讯服务 - return realtime.createIMClient(user); -}).catch(console.error.bind(console)); -``` -```swift -_ = LCUser.logIn(username: "username", password: "password") { (result) in - switch result { - case .success(object: let user): - do { - let client = try IMClient(user: user) - client.open(completion: { (result) in - // 执行其他逻辑 - }) - } catch { - print(error) - } - case .failure(error: let error): - print(error) - } -} -``` -```dart -// 暂不支持 -``` - - - - - -内置账户系统与即时通讯服务可以共享登录签名信息,这里我们直接用 `logIn` 成功之后的 `LCUser` 实例来创建 `IMClient`,在即时通讯服务的用户登录环节,云端会自动关联账户系统来确认用户身份的合法性,这样可以省掉 SDK 向第三方申请登录签名的操作,进一步简化开发流程。 - -`IMClient` 完成即时通讯系统登录之后,其他功能的使用就和之前的介绍没有任何区别了。 - -## 权限管理与黑名单 - -第三方鉴权是一种服务端对全局进行控制的机制,具体到单个对话的群组,例如开放聊天室,出于产品运营的需求,我们还需要对成员权限进行区分,以及允许管理员来限时/永久屏蔽部分用户。下面我们详细说明一下这样的需求该如何实现。 - -### 设置成员权限 - -「成员权限」是指将对话内成员划分成不同角色,实现类似 QQ 群管理员的效果。使用这个功能需要在 **云服务控制台 > 即时通讯 > 设置 > 即时通讯选项** 中开启「对话成员属性功能(成员角色管理功能)」。 - -目前系统内的角色与管理功能的对应关系: - -| 角色 | 功能列表 | -| --- | --- | -| `Owner` | 永久性禁言、踢人、加人、拉黑、更新他人权限 | -| `Manager` | 永久性禁言、踢人、加人、拉黑、更新他人权限 | -| `Member` | 加人 | - -角色的操作权限大小是按照 `Owner` -> `Manager` -> `Member` 的顺序逐级递减的,高级别的角色可以修改低级别角色的权限,但反过来的修改是不允许的。同时,对于加人和踢人的操作,在前面文档中我们可以看到,是所有成员都可以执行的操作,在成员角色管理功能开启之后,就变成 `Owner` 和 `Manager` 专属的功能的,普通成员发起这两种请求都会报错。 - -一个对话的 `Owner` 是不可变更的,我们 SDK 提供了 `Conversation#updateMemberRole` 方法,支持把一个终端用户在 `Manager` 和 `Member` 之间切换角色: - - - -```cs -/// -/// Updates the role of a member of this conversation. -/// -/// The member to update. -/// The new role of the member. -/// -public async Task UpdateMemberRole(string memberId, string role); -``` -```java -/** - * 更新成员的角色信息 - * @param memberId 成员的 clientId - * @param role 角色 - * @param callback 结果回调函数 - */ -public void updateMemberRole(final String memberId, final ConversationMemberRole role, final LCIMConversationCallback callback); -``` -```objc -/** - 更新成员的角色信息 - - @param memberId 成员的 clientId - @param role 角色 - @param callback 结果回调函数 - */ -- (void)updateMemberRoleWithMemberId:(NSString *)memberId - role:(LCIMConversationMemberRole)role - callback:(void (^)(BOOL succeeded, NSError * _Nullable error))callback; -``` -```js -/** - * 更新指定用户的角色 - * @since 4.0.0 - * @param {String} memberId 成员 ID - * @param {module:leancloud-realtime.ConversationMemberRole | String} role 角色 - * @return {Promise.} self - */ -async updateMemberRole(memberId, role); -``` -```swift -/// Updating role of the member in the conversation. -/// -/// - Parameters: -/// - role: The role will be updated. -/// - memberID: The ID of the member who will be updated. -/// - completion: Result of callback. -/// - Throws: If role parameter is owner, throw error. -public func update(role: MemberRole, ofMember memberID: String, completion: @escaping (LCBooleanResult) -> Void) throws -``` -```dart -/// - role: The role will be updated. -/// - memberId: The ID of the member who will be updated. -Future updateMemberRole({String role, String memberId}) -``` - - - - - -### 获取成员权限 - -`Conversation` 对象提供了两种方法来获取成员权限信息: - -#### 获取所有成员的权限信息 - - ```js - /** - * 获取所有成员的对话属性 - * @since 4.0.0 - * @return {Promise.} 所有成员的对话属性列表 - */ - async getAllMemberInfo({ noCache = false } = {}) - ``` - ```swift - /// Fetching the table of member information in the conversation. - /// The result will be cached by the property `memberInfoTable`. - /// - /// - Parameter completion: Result of callback. - public func fetchMemberInfoTable(completion: @escaping (LCBooleanResult) -> Void) - - /// The table of member information. - public var memberInfoTable: [String : MemberInfo]? { get } - ``` - ```objc - /** - 获取当前对话的所有角色信息。默认使用缓存。 - - @param callback 结果回调函数 - */ - - (void)getAllMemberInfoWithCallback:(void (^)(NSArray * _Nullable memberInfos, NSError * _Nullable error))callback; - - /** - 获取当前对话的所有角色信息。 - - @param ignoringCache 缓存选项 - @param callback 结果回调函数 - */ - - (void)getAllMemberInfoWithIgnoringCache:(BOOL)ignoringCache - callback:(void (^)(NSArray * _Nullable memberInfos, NSError * _Nullable error))callback; - ``` - ```java - /** - * 获取当前对话的所有角色信息 - * @param offset 查询结果的起始点 - * @param limit 查询结果集上限 - * @param callback 结果回调函数 - */ - public void getAllMemberInfo(int offset, int limit, final LCIMConversationMemberQueryCallback callback); - ``` - ```cs - /// - /// Gets all member roles. - /// - /// - public async Task> GetAllMemberInfo(); - ``` - ```dart - // 暂不支持 - ``` - -#### 获取指定成员的权限信息 - - ```js - /** - * 获取指定成员的对话属性 - * @since 4.0.0 - * @param {String} memberId 成员 ID - * @return {Promise.} 指定成员的对话属性 - */ - async getMemberInfo(memberId); - ``` - ```swift - /// Get information of one member in the conversation. - /// - /// - Parameters: - /// - memberID: The ID of the member. - /// - completion: Result of callback. - public func getMemberInfo(by memberID: String, completion: @escaping (LCGenericResult) -> Void) - ``` - ```objc - /** - 获取对话内指定成员的角色信息。默认使用缓存。 - - @param memberId 成员的 clientid - @param callback 结果回调函数 - */ - - (void)getMemberInfoWithMemberId:(NSString *)memberId - callback:(void (^)(LCIMConversationMemberInfo * _Nullable memberInfo, NSError * _Nullable error))callback; - ``` - ```java - /** - * 获取对话内指定成员的角色信息 - * @param memberId 成员的 clientid - * @param callback 结果回调函数 - */ - public void getMemberInfo(final String memberId, final LCIMConversationMemberQueryCallback callback); - ``` - ```cs - /// - /// Gets the role of a specific member. - /// - /// The member to query. - /// - public async Task GetMemberInfo(string memberId); - ``` - ```dart - // 暂不支持 - ``` - -这两类函数的返回值都是包含 `` 信息的三元组(数组)。 - -### 让部分用户禁言 - -`Owner` 和 `Manager` 作为聊天群组管理员的权限之一,就是能够让部分用户禁言。被禁言的用户,只能接收群组里面的消息,而不能再往外发送消息,否则会报错。 - -`LCIMConversation` 类提供了对成员进行禁言操作的相关方法: - - - -```cs -/// -/// Mutes members of this conversation. -/// -/// Member list. -/// -public async Task MuteMembers(IEnumerable clientIds); -/// -/// Unmutes members of this conversation. -/// -/// Member list. -/// -public async Task UnmuteMembers(IEnumerable clientIds); -/// -/// Queries muted members. -/// -/// Limits the number of returned results. -/// Can be used for pagination with the limit parameter. -/// -public async Task QueryMutedMembers(int limit = 10, string next = null); -``` -```java -/** - * 将部分成员禁言 - * @param memberIds 成员列表 - * @param callback 结果回调函数 - */ -public void muteMembers(final List memberIds, final LCIMOperationPartiallySucceededCallback callback); -/** - * 将部分成员解除禁言 - * @param memberIds 成员列表 - * @param callback 结果回调函数 - */ -public void unmuteMembers(final List memberIds, final LCIMOperationPartiallySucceededCallback callback); -/** - * 查询被禁言的成员列表 - * @param offset 查询结果的起始点 - * @param limit 查询结果集上限 - * @param callback 结果回调函数 - */ -public void queryMutedMembers(int offset, int limit, final LCIMConversationSimpleResultCallback callback); -``` -```objc -/** - 将部分成员禁言。 - - @param memberIds 成员列表 - @param callback 结果回调函数 - */ -- (void)muteMembers:(NSArray *)memberIds - callback:(void (^)(NSArray * _Nullable successfulIds, NSArray * _Nullable failedIds, NSError * _Nullable error))callback; -/** - 将部分成员解除禁言。 - - @param memberIds 成员列表 - @param callback 结果回调函数 - */ -- (void)unmuteMembers:(NSArray *)memberIds - callback:(void (^)(NSArray * _Nullable successfulIds, NSArray * _Nullable failedIds, NSError * _Nullable error))callback; -/** - 查询被禁言的成员列表。 - - @param limit 查询结果集上限 - @param next 查询结果的起始点;若 next 是 nil 或为空,则意味着没有更多被禁言的成员 - @param callback 结果回调函数 - */ -- (void)queryMutedMembersWithLimit:(NSInteger)limit - next:(NSString * _Nullable)next - callback:(void (^)(NSArray * _Nullable mutedMemberIds, NSString * _Nullable next, NSError * _Nullable error))callback; -``` -```js -/** - * 在该对话中禁言成员 - * @param {String|String[]} clientIds 成员 clientId - * @return {Promise.} 部分成功结果,包含了成功的 ID 列表、失败原因与对应的 ID 列表 - */ -async muteMembers(clientIds); - -/** - * 对话中解除禁言 - * @param {String|String[]} clientIds 成员 clientId - * @return {Promise.} 部分成功结果,包含了成功的 ID 列表、失败原因与对应的 ID 列表 - */ -async unmuteMembers(clientIds); - -/** - * 查询该对话禁言成员列表 - * @param {Object} [options] - * @param {Number} [options.limit] 返回的成员数量,服务器默认值 10 - * @param {String} [options.next] 从指定 next 开始查询,与 limit 一起使用可以完成翻页。 - * @return {PagedResults.} 查询结果。其中的 cureser 存在表示还有更多结果。 - */ -async queryMutedMembers({ limit, next } = {}); -``` -```swift -/// Muting members in the conversation. -/// -/// - Parameters: -/// - members: The members will be muted. -/// - completion: Result of callback. -/// - Throws: When parameter `members` is empty. -public func mute(members: Set, completion: @escaping (MemberResult) -> Void) throws - -/// Unmuting members in the conversation. -/// -/// - Parameters: -/// - members: The members will be unmuted. -/// - completion: Result of callback. -/// - Throws: When parameter `members` is empty. -public func unmute(members: Set, completion: @escaping (MemberResult) -> Void) throws - -/// Get the muted members in the conversation. -/// -/// - Parameters: -/// - limit: Count limit. -/// - next: Offset. -/// - completion: Result of callback. -/// - Throws: When parameter `limit` out of range. -public func getMutedMembers(limit: Int = 50, next: String? = nil, completion: @escaping (LCGenericResult) -> Void) throws - -/// Check if one member has been muted in the conversation. -/// -/// - Parameters: -/// - ID: The ID of member. -/// - completion: Result of callback. -public func checkMuting(member ID: String, completion: @escaping (LCGenericResult) -> Void) -``` -```dart -/// - members: The members will be muted. -Future muteMembers({Set members}) - -/// - members: The members will be unmuted. -Future unmuteMembers({Set members}) - -/// Get the muted members in the conversation. -/// -/// [limit]'s default is `50`, should not more than `100`. -/// [next]'s default is `null`. -/// -/// Returns a list of members. -Future queryMutedMembers({int limit = 50, String next}) -``` - - - - - -注意这里对用户禁言/解除禁言的结果与以往的操作结果不一样,这里是 ***部分成功结果***,里面包含三部分数据: - -- `error`/`exception`,表示整体是否成功。如果整体操作失败,这里会有异常信息返回,此时不必再看下面两部分结果。 -- `successfulClientIds`,表示操作成功了的成员 ID 列表。 -- `failedIds`,表示所有操作失败了的成员信息,以 `List>` 的形式列出了所有的失败原因以及对应的成员 ID 列表。 - -#### 禁言的通知事件 - -管理员把部分用户禁言之后,即时通讯服务端会把这一事件下发给该群组里面的所有成员。 - -### 黑名单 - -「黑名单」功能可以实现类似微信「屏蔽」的效果,目前分为两大类 - -- 对话 --> 成员,是指为某个对话设置的黑名单,禁止名单中的用户加入该对话。 -- 成员 --> 对话,是指某个用户自己设置的对话黑名单,表示禁止其他人把自己拉入这些对话,实现类似于「永久退群」的效果。 - -使用这个功能需要在 **云服务控制台 > 即时通讯 > 设置 > 即时通讯选项** 中开启「黑名单功能」。 - -`LCIMConversation` 类提供了对对话黑名单进行操作的方法: - - - -```cs -/// -/// Adds members to the blocklist of this conversation. -/// -/// Member list. -/// -public async Task BlockMembers(IEnumerable clientIds); -/// -/// Removes members from the blocklist of this conversation. -/// -/// Member list. -/// -public async Task UnblockMembers(IEnumerable clientIds); -/// -/// Queries blocked members. -/// -/// Limits the number of returned results. -/// Can be used for pagination with the limit parameter. -/// -public async Task QueryBlockedMembers(int limit = 10, string next = null); -``` -```java -/** - * 将部分成员加入黑名单 - * @param memberIds 成员列表 - * @param callback 结果回调函数 - */ -public void blockMembers(final List memberIds, final LCIMOperationPartiallySucceededCallback callback); -/** - * 将部分成员从黑名单移出来 - * @param memberIds 成员列表 - * @param callback 结果回调函数 - */ -public void unblockMembers(final List memberIds, final LCIMOperationPartiallySucceededCallback callback); -/** - * 查询黑名单的成员列表 - * @param offset 查询结果的起始点 - * @param limit 查询结果集上限 - * @param callback 结果回调函数 - */ -public void queryBlockedMembers(int offset, int limit, final LCIMConversationSimpleResultCallback callback); -``` -```objc -/** - 将部分成员加入黑名单 - - @param memberIds 成员列表 - @param callback 结果回调函数 - */ -- (void)blockMembers:(NSArray *)memberIds - callback:(void (^)(NSArray * _Nullable successfulIds, NSArray * _Nullable failedIds, NSError * _Nullable error))callback; - -/** - 将部分成员从黑名单移出来 - - @param memberIds 成员列表 - @param callback 结果回调函数 - */ -- (void)unblockMembers:(NSArray *)memberIds - callback:(void (^)(NSArray * _Nullable successfulIds, NSArray * _Nullable failedIds, NSError * _Nullable error))callback; - -/** - 查询黑名单的成员列表 - - @param limit 查询结果集上限 - @param next 查询结果的起始点;若 next 是 nil 或为空,则意味着没有更多黑名单成员 - @param callback 结果回调函数 - */ -- (void)queryBlockedMembersWithLimit:(NSInteger)limit - next:(NSString * _Nullable)next - callback:(void (^)(NSArray * _Nullable blockedMemberIds, NSString * _Nullable next, NSError * _Nullable error))callback; -``` -```js -/** - * 将用户加入该对话黑名单 - * @param {String|String[]} clientIds 成员 clientId - * @return {Promise.} 部分成功结果,包含了成功的 ID 列表、失败原因与对应的 ID 列表 - */ -async blockMembers(clientIds); - -/** - * 将用户移出该对话黑名单 - * @param {String|String[]} clientIds 成员 clientId - * @return {Promise.} 部分成功结果,包含了成功的 ID 列表、失败原因与对应的 ID 列表 - */ -async unblockMembers(clientIds); - -/** - * 查询该对话黑名单 - * @param {Object} [options] - * @param {Number} [options.limit] 返回的成员数量,服务器默认值 10 - * @param {String} [options.next] 从指定 next 开始查询,与 limit 一起使用可以完成翻页 - * @return {PagedResults.} 查询结果。其中的 cureser 存在表示还有更多结果。 - */ -async queryBlockedMembers({ limit, next } = {}); -``` -```swift -/// Blocking members in the conversation. -/// -/// - Parameters: -/// - members: The members will be blocked. -/// - completion: Result of callback. -/// - Throws: When parameter `members` is empty. -public func block(members: Set, completion: @escaping (MemberResult) -> Void) throws - -/// Unblocking members in the conversation. -/// -/// - Parameters: -/// - members: The members will be unblocked. -/// - completion: Result of callback. -/// - Throws: When parameter `members` is empty. -public func unblock(members: Set, completion: @escaping (MemberResult) -> Void) throws - -/// Get the blocked members in the conversation. -/// -/// - Parameters: -/// - limit: Count limit. -/// - next: Offset. -/// - completion: Result of callback. -/// - Throws: When limit out of range. -public func getBlockedMembers(limit: Int = 50, next: String? = nil, completion: @escaping (LCGenericResult) -> Void) throws - -/// Check if one member has been blocked in the conversation. -/// -/// - Parameters: -/// - ID: The ID of member. -/// - completion: Result of callback. -public func checkBlocking(member ID: String, completion: @escaping (LCGenericResult) -> Void) -``` -```dart -/// - members: The members will be blocked. -Future blockMembers({Set members}) - -/// - members: The members will be un unblocked. -Future unblockMembers({Set members}) - -/// Get the blocked members in the conversation. -/// -/// [limit]'s default is `50`, should not more than `100`. -/// [next]'s default is `null`. -/// -/// Returns a list of members. -Future queryBlockedMembers({int limit = 50, String next}) -``` - - - - - -> 注意这里对黑名单操作的结果与禁言操作一样,是 ***部分成功结果***。 - -用户被加入黑名单之后,就被从对话的成员中移除出去了,以后都无法再接收到对话里面的新消息,并且除非解除黑名单,其他人都无法再把 ta 加为对话成员了。 - -#### 黑名单的通知事件 - -管理员把部分用户加入黑名单之后,即时通讯服务端会把这一事件下发给该群组里面的所有成员。 - -#### 屏蔽某用户发送的消息 - -还有一种场景是某个用户不希望收到特定用户发来的消息。这可以通过即时通讯 hook 函数实现,详见[即时通讯开发指南第四篇](/v2/sdk/im/guide/systemconv)。 - -## 玩转直播聊天室 - -在即时通讯服务总览中,我们比较了不同的业务场景与对话类型,现在就来看看如何使用「聊天室」完成一个直播弹幕的需求。 - -### 创建聊天室 - -`IMClient` 提供了专门的 `createChatRoom` 方法来创建聊天室: - - - -```cs -// 最直接的方式,传入 name 即可 -tom.CreateChatRoom("聊天室"); -``` -```java -tom.createChatRoom("聊天室", null, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conv, LCIMException e) { - if (e == null) { - // 创建成功 - } - } -}); -``` -```objc -[client createChatRoomWithCallback:^(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error) { - if (chatRoom && !error) { - LCIMTextMessage *textMessage = [LCIMTextMessage messageWithText:@"这是一条消息" attributes:nil]; - [chatRoom sendMessage:textMessage callback:^(BOOL success, NSError *error) { - if (success && !error) { - - } - }]; - } -}]; -``` -```js -tom.createChatRoom({ name:'聊天室' }).catch(console.error); -``` -```swift -do { - try client.createChatRoom(name: "聊天室", attributes: nil) { (result) in - switch result { - case .success(value: let chatRoom): - print(chatRoom) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -ChatRoom chatRoom = await jerry.createChatRoom(name: '聊天室'); -``` - - - - - -在创建聊天室的时候,开发者可以指定聊天室的名字和附加属性(非必须),与创建普通对话的接口相比,有如下差异: - -- 聊天室因为没有成员列表,所以创建的时候指定 `members` 是没有意义的 -- 同样的原因,创建聊天室的时候指定 `unique` 标志也是没有意义的(云端无需根据成员 ID 来去重) - -> 尽管我们调用 `createConversation` 接口,通过传递合适的参数(`{ transient: true }`),也可以创建一个聊天室,但是还是建议大家直接使用 `createChatRoom` 方法。 - -### 查找聊天室 - -在即时通讯开发指南第一篇中,我们已经了解了构造复杂条件来查询对话的方法,`ConversationsQuery` 依然适用于查询聊天室,只需要添加 `transient = true` 的限制条件即可。 - - - -```cs -LCIMConversationQuery query = new LCIMConversationQuery(tom); -query.WhereEqualTo("tr", true); -``` -```java -LCIMConversationsQuery query = tom.getChatRoomQuery(); -query.findInBackground(new LCIMConversationQueryCallback() { - @Override - public void done(List conversations, LCIMException e) { - if (null != e) { - // 获取成功 - } else { - // 获取失败 - } - } -}); -``` -```objc -LCIMConversationQuery *query = [tom conversationQuery]; -[query whereKey:@"tr" equalTo:@(YES)]; -``` -```js -var query = tom.getQuery().equalTo('tr',true); // 聊天室对象 -query.find().then(function(conversations) { - // conversations 就是想要的结果 -}).catch(console.error); -``` -```swift -do { - let query = client.conversationQuery - try query.where("tr", .equalTo(true)) - try query.findConversations { (result) in - switch result { - case .success(value: let conversations): - guard conversations is [IMChatRoom] else { - return - } - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -try { - ConversationQuery query = tom.conversationQuery(); - query.whereEqualTo('tr', true); - // conversations 就是想要的结果 - List conversations = await query.find(); -} catch (e) { - print(e); -} -``` - - - - -> 上面示例中 Java / Android SDK 专门提供了 `LCIMClient#getChatRoomQuery` 方法来生成聊天室查询对象,屏蔽了 `transient` 属性的细节,建议开发者优先使用这种高层 API。 - -### 加入和离开聊天室 - -查询到聊天室之后,加入和离开聊天室与普通对话的对应接口没有区别,详细请参考[即时通讯开发指南第一篇](/v2/sdk/im/guide/beginner)《多人群聊》。 - -在成员管理与变更通知方面,聊天室与普通对话的最大区别就是: - -- 在聊天室内无法邀请或者踢出成员,只能由用户主动加入和退出; -- 除了用户主动退出之外,客户端断线也会被认为是退出了聊天室。为了防止网络抖动,如果客户端临时异常断线,只要在半小时内重新上线,都会自动加入原聊天室(主动退出的除外); -- 云端不会下发成员加入、退出的变更通知; -- 不支持查询成员列表,但提供专门的 API 来查询实时在线人数。 - -另外,也请注意 ***聊天室也不支持离线推送通知、离线消息同步、消息回执等功能***。 - -### 查询成员数量 - -`LCIMConversation#memberCount` 方法可以用来查询普通对话的成员总数,在聊天室中,它返回的就是实时在线的人数: - - - -```cs -int membersCount = await conversation.GetMembersCount(); -``` -```java -private void TomQueryWithLimit() { - LCIMClient tom = LCIMClient.getInstance("Tom"); - tom.open(new LCIMClientCallback() { - - @Override - public void done(LCIMClient client, LCIMException e) { - if (e == null) { - // 登录成功 - LCIMConversationsQuery query = tom.getConversationsQuery(); - query.setLimit(1); - // 获取第一个对话 - query.findInBackground(new LCIMConversationQueryCallback() { - @Override - public void done(List convs, LCIMException e) { - if (e == null) { - if (convs != null && !convs.isEmpty()) { - LCIMConversation conv = convs.get(0); - // 获取第一个对话的在线人数 - conv.getMemberCount(new LCIMConversationMemberCountCallback() { - - @Override - public void done(Integer count, LCIMException e) { - if (e == null) { - Log.d("Tom & Jerry 对话的在线人数为 " + count); - } - } - }); - } - } - } - }); - } - } - }); -} -``` -```objc -// 查询在线人数 -[conversation countMembersWithCallback:^(NSInteger number, NSError *error) { - NSLog(@"%ld",number); -}]; -``` -```js -chatRoom.count().then(function(count) { - console.log('在线人数:' + count); -}).catch(console.error.bind(console)); -``` -```swift -do { - chatRoom.getOnlineMembersCount { (result) in - switch result { - case .success(count: let count): - print(count) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -int count = await chatRoom.countMembers(); -``` - - - - - -### 消息等级 - -为了保证消息的时效性,当聊天室消息过多导致客户端连接堵塞时,服务器端会选择性地丢弃部分非高等级的消息。目前支持的消息等级有: - -| 消息等级 | 描述 | -| ------------------------ | ------------------------------------------------------------------ | -| `MessagePriority.HIGH` | 高等级,针对时效性要求较高的消息,比如直播聊天室中的礼物、打赏等。 | -| `MessagePriority.NORMAL` | 中等级,比如普通非重复性的文本消息。 | -| `MessagePriority.LOW` | 低等级,针对时效性要求较低的消息,比如直播聊天室中的弹幕。 | - -消息等级默认为 `NORMAL`。 - -消息等级在发送接口的参数中设置。以下代码演示了如何发送一个高等级的消息: - - - -```cs -LCIMTextMessage message = new LCIMTextMessage("现在比分是 0:0,下半场中国队肯定要做出人员调整"); -LCIMMessageSendOptions options = new LCIMMessageSendOptions { - Priority = LCIMMessagePriority.High -}; -await chatRoom.Send(message, options); -``` -```java -LCIMClient tom = LCIMClient.getInstance("Tom"); - tom.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient client, LCIMException e) { - if (e == null) { - // 创建名为「猫和老鼠」的对话 - client.createConversation(Arrays.asList("Jerry"), "猫和老鼠", null, - new LCIMConversationCreatedCallback() { - @Override - public void done(LCIMConversation conv, LCIMException e) { - if (e == null) { - LCIMTextMessage msg = new LCIMTextMessage(); - msg.setText("耗子,起床!"); - - LCIMMessageOption messageOption = new LCIMMessageOption(); - messageOption.setPriority(LCIMMessageOption.MessagePriority.High); - conv.sendMessage(msg, messageOption, new LCIMConversationCallback() { - @Override - public void done(LCIMException e) { - if (e == null) { - // 发送成功 - } - } - }); - } - } - }); - } - } - }); -``` -```objc -LCIMMessageOption *option = [[LCIMMessageOption alloc] init]; -option.priority = LCIMMessagePriorityHigh; -[chatRoom sendMessage:[LCIMTextMessage messageWithText:@"耗子,起床!" attributes:nil] option:option callback:^(BOOL succeeded, NSError * _Nullable error) { - // 在这里处理发送失败或者成功之后的逻辑 -}]; -``` -```js -var { Realtime, TextMessage, MessagePriority } = require('leancloud-realtime'); -var realtime = new Realtime({ appId: 'GDBz24d615WLO5e3OM3QFOaV-gzGzoHsz', appKey: 'dlCDCOvzMnkXdh2czvlbu3Pk' }); -realtime.createIMClient('host').then(function (host) { - return host.createConversation({ - members: ['broadcast'], - name: '2094 世界杯决赛梵蒂冈对阵中国比赛直播间', - transient: true - }); -}).then(function (conversation) { - console.log(conversation.id); - return conversation.send(new TextMessage('现在比分是 0:0,下半场中国队肯定要做出人员调整'), { priority: MessagePriority.HIGH }); -}).then(function (message) { - console.log(message); -}).catch(console.error); -``` -```swift -do { - let message = IMTextMessage(text: "现在比分是 0:0,下半场中国队肯定要做出人员调整") - try chatRoom.send(message: message, priority: .high) { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -try { - TextMessage message = TextMessage(); - message.text = '现在比分是 0:0,下半场中国队肯定要做出人员调整'; - await chatRoom.send(message: message, priority: MessagePriority.high); -} catch (e) { - print(e); -} -``` - - - - - -> 注意: -> -> 此功能仅针对聊天室消息有效。普通对话的消息不需要设置等级,即使设置了也会被系统忽略,因为普通对话的消息不会被丢弃。 - -### 消息免打扰 - -假如某一用户不想再收到某对话的消息提醒,但又不想直接退出对话,可以使用静音操作,即开启「免打扰模式」。 - -比如 Tom 工作繁忙,对某个对话设置了静音: - - - -```cs -await chatRoom.Mute(); -``` -```java -LCIMClient tom = LCIMClient.getInstance("Tom"); -tom.open(new LCIMClientCallback(){ - - @Override - public void done(LCIMClient client,LCIMException e){ - if(e==null){ - // 登录成功 - LCIMConversation conv = client.getConversation("551260efe4b01608686c3e0f"); - conv.mute(new LCIMConversationCallback(){ - - @Override - public void done(LCIMException e){ - if(e==null){ - // 设置成功 - } - } - }); - } - } -}); -``` -```objc -// Tom 将会话设置为静音 -[conversation muteWithCallback:^(BOOL succeeded, NSError *error) { - if (succeeded) { - NSLog(@"修改成功!"); - } -}]; -``` -```js -tom.getConversation('CONVERSATION_ID').then(function(conversation) { - return conversation.mute(); -}).then(function(conversation) { - console.log('静音成功'); -}).catch(console.error.bind(console)); -``` -```swift -conversation.mute { (result) in - switch result { - case .success: - break - case .failure(error: let error): - print(error) - } -} -``` -```dart -await chatRoom.mute(); -``` - - - - - -设置静音之后,iOS 及启用混合推送的 Android 用户就不会收到推送消息了。与之对应的就是取消静音的操作(`Conversation#unmute` 方法),即取消免打扰模式。 - -> 使用建议: -> -> - 对话内消息的静音/取消静音操作不光对聊天室有效,普通的群聊对话也可以执行该操作。 -> - `mute` 和 `unmute` 操作会修改云端 `_Conversation` 里面的 `mu` 属性。**开发者切勿在控制台中对 `mu` 随意进行修改**,否则可能会引起即时通讯云端的离线推送功能失效。 - -### 消息内容的实时过滤 - -对于开放聊天室来说,内容的审核和实时过滤是产品运营上的一个基本要求。我们即时通讯服务默认提供了敏感词过滤的功能,多人的 **普通对话、聊天室和系统对话里面的消息都会进行实时过滤**。 -命中的敏感词将会被替换为 `***`。 -消息内容实时过滤属于系统层面的修改消息,发送者会收到 `MESSAGE_UPDATE` 事件。 -应用可以在客户端监听该事件,实现相应的业务逻辑,相关代码示例可以参考[即时通讯开发指南第二篇](/v2/sdk/im/guide/intermediate)的《修改消息》一节。 - -过滤的词库由即时通讯服务统一提供。商用版应用还支持开发者使用自定义敏感词词库,只需在 **云服务控制台 > 即时通讯 > 设置** 中上传敏感词文件。 -敏感词文件为 UTF-8 编码的纯文本文件,一行一个敏感词。 -开发者上传的自定义敏感词词库会替换默认提供的词库。 - -如果开发者有较为复杂的过滤需求,我们推荐使用云引擎 hook `_messageReceived` 来实现过滤,在 hook 中开发者对消息的内容有完全的控制力。 - -## 使用临时对话 - -临时对话是一个全新的概念,它解决的是一种特殊的聊天场景: - -- 对话存续时间短 -- 聊天参与的人数较少(最多为 10 个 `clientId`) -- 聊天记录的存储不是强需求 - -临时对话最大的特点是 **较短的有效期**,这个特点可以解决对话的持久化存储在服务端占用的存储资源越来越大、开发者需要支付的成本越来越高的问题,也可以应对一些临时聊天的场景。诸如电商售前和售后在线聊天的客服系统,我们推荐使用临时对话。 - -### 临时对话实例 - -`IMConversation` 有专门的 `createTemporaryConversation` 方法用于创建临时对话: - - - -```cs -LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" }); -``` -```java -tom.createTemporaryConversation(Arrays.asList(members), 3600, new LCIMConversationCreatedCallback(){ - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if (null == e) { - LCIMTextMessage msg = new LCIMTextMessage(); - msg.setText("这里是临时对话,一小时之后,这个对话就会消失"); - conversation.sendMessage(msg, new LCIMConversationCallback(){ - @Override - public void done(LCIMException e) { - } - }); - } - } -}); -``` -```objc -[self createTemporaryConversationWithClientIds:@[@"Jerry", @"William"] callback:^(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error) { - if (temporaryConversation) { - // success - } -}]; -``` -```js -realtime.createIMClient('Tom').then(function(tom) { - return tom.createTemporaryConversation({ - members: ['Jerry', 'William'], - }); -}).then(function(conversation) { - return conversation.send(new AV.TextMessage('这里是临时对话')); -}).catch(console.error); -``` -```swift -do { - try client.createTemporaryConversation(clientIDs: ["Jerry", "William"], timeToLive: 3600) { (result) in - switch result { - case .success(value: let tempConversation): - print(tempConversation) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -TemporaryConversation temporaryConversation; -try { - temporaryConversation = await jerry.createTemporaryConversation( - members: {'Jerry', 'William'}, - ); -} catch (e) { - print(e); -} - -try { - TextMessage message = TextMessage(); - message.text = '这里是临时对话'; - await temporaryConversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - - -与其他对话类型不同的是,临时对话有一个 **重要** 的属性:TTL。它标记着这个对话的有效期,系统默认是 1 天,但是在创建对话的时候是可以指定这个时间的,最高不超过 30 天。如果您的需求是一定要超过 30 天,请使用普通对话。传入 TTL 创建临时对话的代码如下: - - - -```cs -LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" }, - ttl: 3600); -``` -```java -LCIMClient client = LCIMClient.getInstance("Tom"); -client.open(new LCIMClientCallback() { - @Override - public void done(LCIMClient avimClient, LCIMException e) { - if (null == e) { - String[] members = {"Jerry", "William"}; - avimClient.createTemporaryConversation(Arrays.asList(members), 3600, new LCIMConversationCreatedCallback(){ - @Override - public void done(LCIMConversation conversation, LCIMException e) { - if (null == e) { - LCIMTextMessage msg = new LCIMTextMessage(); - msg.setText("这里是临时对话,一小时之后,这个对话就会消失"); - conversation.sendMessage(msg, new LCIMConversationCallback(){ - @Override - public void done(LCIMException e) { - } - }); - } - } - }); - } - } -}); -``` -```objc -LCIMConversationCreationOption *option = [LCIMConversationCreationOption new]; -option.timeToLive = 3600; -[self createTemporaryConversationWithClientIds:@[@"Jerry", @"William"] option:option callback:^(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error) { - if (temporaryConversation) { - // success - } -}]; -``` -```js -realtime.createIMClient('Tom').then(function(tom) { - return tom.createTemporaryConversation({ - members: ['Jerry', 'William'], - ttl: 3600, - }); -}).then(function(conversation) { - return conversation.send(new AV.TextMessage('这里是临时对话,一小时之后,这个对话就会消失')); -}).catch(console.error); -``` -```swift -do { - try client.createTemporaryConversation(clientIDs: ["Jerry", "William"], timeToLive: 3600) { (result) in - switch result { - case .success(value: let tempConversation): - print(tempConversation) - case .failure(error: let error): - print(error) - } - } -} catch { - print(error) -} -``` -```dart -TemporaryConversation temporaryConversation; -try { - temporaryConversation = await jerry.createTemporaryConversation( - members: {'Jerry', 'William'}, - timeToLive: 3600, - ); -} catch (e) { - print(e); -} - -try { - TextMessage message = TextMessage(); - message.text = '这里是临时对话,一小时之后,这个对话就会消失'; - await temporaryConversation.send(message: message); -} catch (e) { - print(e); -} -``` - - - - -临时对话的其他操作与普通对话无异。 - -## 进一步阅读 - -即时通讯开发指南第四篇[详解消息 hook 与系统对话](/v2/sdk/im/guide/systemconv) diff --git a/versioned_docs/version-v2/sdk/10-im/02-guide/04-systemconv.mdx b/versioned_docs/version-v2/sdk/10-im/02-guide/04-systemconv.mdx deleted file mode 100644 index 0404eb8cd..000000000 --- a/versioned_docs/version-v2/sdk/10-im/02-guide/04-systemconv.mdx +++ /dev/null @@ -1,1382 +0,0 @@ ---- -id: systemconv -title: 四,详解消息 hook 与系统对话 -sidebar_label: Hook 与系统对话 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import Mermaid from '/src/docComponents/Mermaid'; - - - -## 本章导读 - -在前一篇[安全与签名、黑名单和权限管理、玩转聊天室和临时对话](/v2/sdk/im/guide/senior)中,我们解释了一些第三方鉴权以及成员权限设置方面的问题,在这里我们会更进一步,给大家说明: - -- 即时通讯的消息 Hook 机制 -- 系统对话的使用方法 - -## 万能的 Hook 机制 - -完全开放的架构,支持强大的业务扩展能力,是即时通讯服务的特色之一,这种优势的体现就是这里将要给大家介绍的「Hook 机制」。 - -### Hook 与即时通讯服务的关系 - -Hook 也可以称为「钩子」,是一种特殊的消息处理机制,与 Windows 平台下的中断机制类似,允许应用方拦截并处理即时通讯过程中的多种事件和消息,从而达到实现自定义业务逻辑的目的。 - -以 **_messageRecieved** Hook 为例,它在消息送达服务器后会被调用,在 Hook 内可以捕获消息内容、消息发送者、消息接收者等信息,这些信息均能在 Hook 内做修改并将修改后的值转交回服务器,服务器会使用修改后的消息继续完成消息投递工作。最终收消息用户收到的会是被 Hook 修改过后的消息,而不再是最初送达服务器的原始消息。Hook 也可以选择拒绝消息发送,服务器会在给客户端回复消息被 Hook 拒绝后丢弃消息不再完成后续消息处理及转发流程。 - -**需要注意的是,默认情况下如果 Hook 调用失败,例如超时、返回状态码非 200 的结果等,服务器会忽略 Hook 的错误继续处理原始请求**。如果您需要改变这个行为,可以在**云服务控制台 > 即时通讯 > 设置 > 即时通讯选项** 内开启 「Hook 调用失败时返回错误给客户端并放弃继续处理请求」。开启后如果 Hook 调用失败,服务器会返回错误信息给客户端告知 Hook 调用错误,并拒绝继续处理请求。 - -### 消息类 Hook - -一条消息,在即时通讯的流程中,从终端用户 A 发送开始,到其他用户接收到为止,考虑到存在接收方在线/不在线的可能,会经历多个不同阶段,这里每一个阶段都会触发 Hook 函数: - -* **_messageReceived**
    - 消息达到服务器,群组成员已解析完成之后,发送给收件人之前调用。开发者在这里还可以修改消息内容,实时改变消息接收者的列表,以及其他类似操作。 -* **_messageSent**
    - 消息发送完成后调用。开发者在这里可以完成业务统计,或将消息中转备份到己方服务器,以及其他类似操作。 -* **_receiversOffline**
    - 消息发送完成,存在离线的收件人,在发推送给收件人之前调用。开发者在这里可以动态修改离线推送的通知内容,或通知目的设备的列表,以及其他类似操作。 -* **_messageUpdate**
    - 收到消息修改请求,发送修改后的消息给收件人之前调用。与新发消息一样,开发者在这里可以再次修改消息内容,实时改变消息接收者的列表,以及其他类似操作。 - -### 对话类 Hook - -在对话创建和成员变动等更改性操作前后,都可以触发 Hook 函数,进行额外的处理: - -* **_conversationStart**
    - 创建对话,在签名校验(如果开启)之后,实际创建之前调用。开发者在这里可以为新的「对话」添加其他内部属性,或完成操作鉴权,以及其他类似操作。 -* **_conversationStarted**
    - 创建对话完成后调用。开发者在这里可以完成业务统计,或将对话数据中转备份到己方服务器,以及其他类似操作。 -* **_conversationAdd**
    - 向对话添加成员,在签名校验(如果开启)之后,实际加入之前调用,包括主动加入和被其他用户加入两种情况。开发者可以在这里根据内部权限设置批准或驳回这一请求,以及其他类似操作。 -* **_conversationRemove**
    - 从对话中踢出成员,在签名校验(如果开启)之后,实际踢出之前调用,用户自己退出对话不会调用。开发者可以在这里根据内部权限设置批准或驳回这一请求,以及其他类似操作。 -* **_conversationAdded**
    - 用户加入对话,在加入成功后调用。 -* **_conversationRemoved**
    - 用户离开对话,在离开成功后调用。 -* **_conversationUpdate**
    - 修改对话名称、自定义属性,设置或取消对话消息提醒,在实际修改之前调用。开发者在这里可以为新的「对话」添加其他内部属性,或完成操作鉴权,以及其他类似操作。 - -### 客户端上下线 Hook - -在客户端上线和下线的时候,可以触发 Hook 函数: - -* **_clientOnline**
    - 客户端上线,客户端登录成功后调用。 -* **_clientOffline**
    - 客户端下线,客户端登出成功或意外下线后调用。 - -开发者可以利用这两个 Hook 函数,结合 LeanCache 来完成一组客户端实时状态查询的 endpoint,具体可以参考文档[《即时通讯中的在线状态查询》](https://leancloud.cn/docs/realtime-guide-onoff-status.html)。 - -### Hook 与云引擎的关系 - -因为 Hook 发生在即时通讯的在线处理环节,而即时通讯服务端每秒钟需要处理的消息和对话事件数量远超大家的想象,出于性能考虑,我们要求开发者使用云引擎来实现 Hook 函数。 - -即时通讯的云引擎 Hook 要求云引擎部署在云引擎的 **生产环境**,测试环境仅用于开发者手动调用测试。由于缓存的原因,首次部署的云引擎 Hook 需要至多三分钟来正式生效,后续修改会实时生效。 - -### Hook API 细节与使用场景详解 - -与 `conversation` 相关的 hook 可以在应用签名之外增加额外的权限判断,控制对话是否允许被建立、某些用户是否允许被加入对话等。你可以用这一 hook 实现黑名单功能。 - -#### `_messageReceived` - -这个 hook 发生在消息到达云端之后。如果是群组消息,我们会解析出所有消息收件人。 - -你可以通过返回参数控制消息是否需要被丢弃,删除个别收件人,还可以修改消息内容,例如过滤应用中的敏感词。返回空对象(`response.success({})`)则会执行系统默认的流程。 - -请注意,在这个 hook 的代码实现的任何分支上 **请确保最终会调用 `response.success` 返回结果**,使得消息可以尽快投递给收件人。这个 hook 将 **阻塞发送流程**,因此请尽量减少无谓的代码调用,提升效率。 - -如果你使用了默认提供的富媒体消息格式,云引擎参数中的 `content` 接收的是 JSON 结构的字符串形式。关于这个结构的详细说明,请参考[即时通讯 REST API 使用指南](/v2/sdk/im/guide/rest)的《富媒体消息格式说明》一节。 - -参数: - -参数 | 说明 ---- | --- -`fromPeer` | 消息发送者的 ID。 -`convId` | 消息所属对话的 ID。 -`toPeers` | 解析出的对话相关的 `clientId`。 -`transient` | 是否是 transient 消息。 -`bin` | 原始消息内容是否为二进制消息。 -`content` | 消息体字符串。如果 `bin` 为 `true`,则该字段为原始消息内容做 Base64 转码后的结果。 -`receipt` | 是否要求回执。 -`timestamp` | 服务器收到消息的时间戳(毫秒)。 -`system` | 是否属于系统对话消息。 -`sourceIP` | 消息发送者的 IP。 - -参数示例: - -```json -{ - "fromPeer": "Tom", - "receipt": false, - "groupId": null, - "system": null, - "content": "{\"_lctext\":\"来我们去 XX 传奇玩吧\",\"_lctype\":-1}", - "convId": "5789a33a1b8694ad267d8040", - "toPeers": ["Jerry"], - "bin": false, - "transient": false, - "sourceIP": "121.239.62.103", - "timestamp": 1472200796764 -}; -``` - -返回值: - -参数 | 约束 | 说明 ---- | --- | --- -`drop` | 可选 | 如果返回真值,消息将被丢弃。 -`code` | 可选 | 当 `drop` 为 `true` 时可以下发一个应用自定义的整型错误码。 -`detail` | 可选 | 当 `drop` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 -`bin` | 可选 | 返回的 `content` 内是否为二进制消息,如果不提供则为请求中的 `bin` 值。 -`content` | 可选 | 修改后的 `content`,如果不提供则保留原消息。如果 `bin` 为 `true`,则 `content` 需要是二进制消息内容做 Base64 转码后的结果。 -`toPeers` | 可选 | 数组,修改后的收件人,如果不提供则保留原收件人。 - -示例代码: - - - - - -```js -AV.Cloud.onIMMessageReceived((request) => { - let content = request.params.content; - let processedContent = content.replace('XX 传奇', '**'); - // 必须含有以下语句给服务端一个正确的返回,否则会引起异常 - return { - content: processedContent - }; -}); -``` -```python -import json - -@engine.define -def _messageReceived(**params): - content = json.loads(params['content']) - text = content['_lctext'] - content['_lctext'] = text.replace('XX 传奇', '**') - # 必须含有以下语句给服务端一个正确的返回,否则会引起异常 - return { - 'content': json.dumps(content) - } -``` -```php -Cloud::define("_messageReceived", function($params, $user) { - $content = json_decode($params["content"], true); - $text = $content["_lctext"]; - $content["_lctext"] = preg_replace("XX 传奇", "**", $text); - // 必须含有以下语句给服务端一个正确的返回,否则会引起异常 - return array("content" => json_encode($content)); -}); -``` -```java -@IMHook(type = IMHookType.messageReceived) - public static Map onMessageReceived(Map params) { - Map result = new HashMap(); - String content = (String)params.get("content"); - String processedContent = content.replace("XX 传奇", "**"); - result.put("content", processedContent); - // 必须含有以下语句给服务端一个正确的返回,否则会引起异常 - return result; - } -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.MessageReceived)] -public static object OnMessageReceived(Dictionary parameters) { - string content = parameters["content"] as string; - string processedContent = content.Replace("XX 中介", "**"); - return new Dictionary { - { "content", processedContent } - }; -} -``` -```go -// 暂不支持 -``` - - - - -而实际上启用上述代码之后,一条消息的时序图如下: - ->RTM: 1. 发送消息 -RTM-->>Engine: 2. 触发 _messageReceived hook 调用 -Engine-->>RTM: 3. 返回 hook 函数处理结果 -RTM-->>SDK: 4. 将 hook 函数处理结果发送给接收方 -`} /> - -- 上图假设的是对话所有成员都在线,而如果有成员不在线,流程有些不一样,下一节会做介绍。 -- RTM 表示即时通讯服务集群,Engine 表示云引擎服务集群,它们基于内网通讯。 - -#### `_receiversOffline` - -这个 hook 发生在有收件人离线的情况下,你可以通过它来自定义离线推送行为,包括推送内容、被推送用户或略过推送。你也可以直接在 hook 中触发自定义的推送。发往聊天室的消息不会触发此 hook。 - -参数: - -参数 | 说明 ---- | --- -`fromPeer` | 消息发送者 ID。 -`convId` | 消息所属对话的 ID。 -`offlinePeers` | 数组,离线的收件人列表。 -`content` | 消息内容。 -`timestamp` | 服务器收到消息的时间戳(毫秒)。 -`mentionAll` | 布尔类型,表示本消息是否 @ 了所有成员。 -`mentionOfflinePeers` | 被本消息 @ 且离线的成员 ID。如果 `mentionAll` 为 `true`,则该参数为空,表示所有 `offlinePeers` 参数内的成员全部被 @。 - -返回值: - -参数 | 约束 | 说明 ---- | --- | --- -`skip` | 可选 | 如果为真将跳过推送(比如已经在云引擎里触发了推送或者其他通知)。 -`offlinePeers`| 可选 | 数组,筛选过的推送收件人。 -`pushMessage` | 可选 | 推送内容,支持自定义 JSON 结构。 -`force` | 可选 | 如果为真将强制推送给 `offlinePeers` 里 `mute` 的用户,默认 `false`。 - -示例代码: - - - -```js -AV.Cloud.onIMReceiversOffline((request) => { - let params = request.params; - let content = params.content; - - // params.content 为消息的内容 - let shortContent = content; - - if (shortContent.length > 6) { - shortContent = content.slice(0, 6); - } - - console.log('shortContent', shortContent); - - return { - pushMessage: JSON.stringify({ - // 自增未读消息的数目,不想自增就设为数字 - badge: "Increment", - sound: "default", - // 使用开发证书 - _profile: "dev", - alert: shortContent - }) - } -}); -``` -```python -@engine.define -def _receiversOffline(**params): - print('_receiversOffline start') - # params['content'] 为消息内容 - content = params['content'] - short_content = content[:6] - print('short_content:', short_content) - payloads = { - # 自增未读消息的数目,不想自增就设为数字 - 'badge': 'Increment', - 'sound': 'default', - # 使用开发证书 - '_profile': 'dev', - 'alert': short_content, - } - print('_receiversOffline end') - return { - 'pushMessage': json.dumps(payloads), - } -``` -```php -Cloud::define('_receiversOffline', function($params, $user) { - error_log('_receiversOffline start'); - // content 为消息的内容 - $shortContent = $params["content"]; - if (strlen($shortContent) > 6) { - $shortContent = substr($shortContent, 0, 6); - } - - $json = array( - // 自增未读消息的数目,不想自增就设为数字 - "badge" => "Increment", - "sound" => "default", - // 使用开发证书 - "_profile" => "dev", - "alert" => shortContent - ); - - $pushMessage = json_encode($json); - return array( - "pushMessage" => $pushMessage, - ); -}); -``` -```java -@IMHook(type = IMHookType.receiversOffline) - public static Map onReceiversOffline(Map params) { - // content 为消息内容 - String alert = (String)params.get("content"); - if(alert.length() > 6){ - alert = alert.substring(0, 6); - } - System.out.println(alert); - Map result = new HashMap(); - JSONObject object = new JSONObject(); - // 自增未读消息的数目 - // 不想自增就设为数字 - object.put("badge", "Increment"); - object.put("sound", "default"); - // 使用开发证书 - object.put("_profile", "dev"); - object.put("alert", alert); - result.put("pushMessage", object.toString()); - return result; -} -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ReceiversOffline)] -public static Dictionary OnReceiversOffline(Dictionary parameters) { - string alert = parameters["content"] as string; - if (alert.Length > 6) { - alert = alert.Substring(0, 6); - } - Dictionary pushMessage = new Dictionary { - { "badge", "Increment" }, - { "sound", "default" }, - { "_profile", "dev" }, - { "alert", alert }, - }; - return new Dictionary { - { "pushMessage", JsonSerializer.Serialize(pushMessage) } - }; -} -``` -```go -// 暂不支持 -``` - - - - -#### `_messageSent` - -在消息发送完成后执行,对消息发送性能没有影响,可以用来执行相对耗时的逻辑。 - -参数: - -参数 | 说明 ---- | --- -`fromPeer` | 消息发送者的 ID。 -`convId` | 消息所属对话的 ID。 -`msgId` | 消息 ID。 -`onlinePeers` | 当前在线发送的用户 ID。 -`offlinePeers` | 当前离线的用户 ID。 -`transient` | 是否是 transient 消息。 -`system` | 是否是 system conversation。 -`bin` | 是否是二进制消息。 -`content` | 消息体字符串。 -`receipt` | 是否要求回执。 -`timestamp` | 服务器收到消息的时间戳(毫秒)。 -`sourceIP` | 消息发送者的 IP。 - -参数示例: - -```json -{ - "fromPeer": "Tom", - "receipt": false, - "onlinePeers": [], - "content": "12345678", - "convId": "5789a33a1b8694ad267d8040", - "msgId": "fptKnuYYQMGdiSt_Zs7zDA", - "bin": false, - "transient": false, - "sourceIP": "114.219.127.186", - "offlinePeers": [ "Jerry" ], - "timestamp": 1472703266522 -} -``` - -返回值: - -这个 hook 不会对返回值进行检查。只需返回 `{}` 即可。 - -示例代码: - -下面代码演示了日志记录相关的操作(在消息发送完后,在云引擎中打印一下日志): - - - -```js -AV.Cloud.onIMMessageSent((request) => { - console.log('params', request.params); -}); -``` -```python -@engine.define -def _messageSent(**params): - print('_messageSent start') - print('params:', params) - print('_messageSent end') - return {} -``` -```php -Cloud::define('_messageSent', function($params, $user) { - error_log('_messageSent start'); - error_log('params' . json_encode($params)); - return array(); -}); -``` -```java -@IMHook(type = IMHookType.messageSent) - public static Map onMessageSent(Map params) { - System.out.println(params); - Map result = new HashMap(); - return result; -} -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.MessageSent)] -public static Dictionary OnMessageSent(Dictionary parameters) { - Console.WriteLine(JsonSerializer.Serialize(parameters)); - return default; -} -``` -```go -// 暂不支持 -``` - - - - -#### `_messageUpdate` - -这个 hook 发生在修改消息请求到达云端,云端正式修改消息之前。 - -你可以通过返回参数控制修改消息请求是否需要被丢弃,删除个别收件人,或再次修改这个修改消息请求中的消息内容。 - -请注意,在这个 hook 的代码实现的任何分支上 **请确保最终会调用 `response.success` 返回结果**,使得修改消息可以尽快投递给收件人。这个 hook 将 **阻塞发送流程**,因此请尽量减少无谓的代码调用,提升效率。 - -如果你使用了默认提供的富媒体消息格式,云引擎参数中的 `content` 接收的是 JSON 结构的字符串形式。关于这个结构的详细说明,请参考[即时通讯 REST API 使用指南](/v2/sdk/im/guide/rest)的《富媒体消息》一节。 - -参数: - -参数 | 说明 ---- | --- -`fromPeer` | 消息发送者的 ID。 -`convId` | 消息所属对话的 ID。 -`toPeers` | 解析出的对话相关的 `clientId`。 -`bin` | 原始消息内容是否为二进制消息。 -`content` | 消息体字符串。如果 `bin` 为 `true`,则该字段为原始消息内容做 Base64 转码后的结果。 -`timestamp` | 服务器收到消息的时间戳(毫秒)。 -`msgId` | 被修改的消息 ID。 -`sourceIP` | 消息发送者的 IP。 -`recall` | 是否撤回消息。 -`system` | 是否属于系统对话消息。 - -返回值: - -参数 | 约束 | 说明 ---- | --- | --- -`drop` | 可选 | 如果返回真值,修改消息请求将被丢弃。 -`code` | 可选 | 当 `drop` 为 `true` 时可以下发一个应用自定义的整型错误码。 -`detail` | 可选 | 当 `drop` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 -`bin` | 可选 | 返回的 `content` 内是否为二进制消息,如果不提供则为请求中的 `bin` 值。 -`content` | 可选 | 修改后的 `content`,如果不提供则保留原消息。如果 `bin` 为 `true`,则 `content` 需要是二进制消息内容做 Base64 转码后的结果。 -`toPeers` | 可选 | 数组,修改后的收件人,如果不提供则保留原收件人。 - -#### `_conversationStart` - -在创建对话时调用,发生在签名验证(如果开启)之后、创建对话之前。 - -参数: - -参数 | 说明 ---- | --- -`initBy` | 由谁发起的 `clientId`。 -`members` | 初始成员数组,包含初始成员。 -`attr` | 创建对话时的额外属性。 - -参数示例: - -``` -{ - "initBy": "Tom", - "members": ["Tom", "Jerry"], - "attr": { - "name": "Tom & Jerry" - } -} -``` - -返回值: - -参数 | 约束 | 说明 ---- | --- | --- -`reject` | 可选 | 是否拒绝,默认为 `false`。 -`code` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的整型错误码。 -`detail` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 - -例如,初始成员不足四人,不允许创建对话: - - - -```js -AV.Cloud.onIMConversationStart((request) => { - if (request.params.members.length < 4) { - return { - "reject": true, - "code": 1234, - "detail": "至少邀请 3 人开启对话", - }; - } else { - return {}; - } -}); -``` -```python -@engine.define -def _conversationStart(**params): - if len(params["members"]) < 4: - return { - "reject": True, - "code": 1234, - "detail": "至少邀请 3 人开启对话", - } - else: - return {} -``` -```php -Cloud::define('_conversationStart', function($params, $user) { - if (count($params["members"]) < 4) { - return [ - "reject" => true, - "code" => 1234, - "detail" => "至少邀请 3 人开启对话", - ]; - } else { - return array(); - } -}); -``` -```java -@IMHook(type = IMHookType.conversationStart) -public static Map onConversationStart(Map params) { - String[] members = (String[])params.get("members"); - Map result = new HashMap(); - if (members.length < 4) { - result.put("reject", true); - result.put("code", 1234); - result.put("detail", "至少邀请 3 人开启对话"); - } - return result; -} -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationStart)] -public static object OnConversationStart(Dictionary parameters) { - List members = parameters["members"] as List; - if (members.Count < 4) { - return new Dictionary { - { "reject", true }, - { "code", 1234 }, - { "detail", "至少邀请 3 人开启对话" } - }; - } - return default; -} -``` -```go -// 暂不支持 -``` - - - - -#### `_conversationStarted` - -对话创建后调用。 - -参数: - -参数 | 说明 ---- | --- -`convId` | 新生成的对话 ID。 - -返回值: - -这个 hook 不对返回值进行处理,只需返回 `{}` 即可。 - -例如,创建对话后,将对话的 ID 存储到 LeanCache 的最近创建对话列表: - - - -```js -AV.Cloud.onIMConversationStarted((request) => { - redisClient.lpush("recent_conversations", request.params.convId); - return {}; -}); -``` -```python -@engine.define -def _conversationStarted(**params): - redis_client.lpush("recent_conversations", params["convId"]) - return {} -``` -```php -Cloud::define('_conversationStarted', function($params, $user) { - $redis->lpush("recent_conversations", $params["convId"]); - return array(); -}); -``` -```java -@IMHook(type = IMHookType.conversationStarted) -public static Map onConversationStarted(Map params) throws Exception { - String convId = (String)params.get("convId"); - jedis.lpush("recent_conversations", params.get("convId")); - Map result = new HashMap(); - return result; -} -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationStarted)] -public static object OnConversationStarted(Dictionary parameters) { - string convId = parameters["convId"] as string; - Console.WriteLine($"{convId} started"); - return default; -} -``` -```go -// 暂不支持 -``` - - - - -#### `_conversationAdd` - -在将用户加入到对话时调用,发生在签名验证(如果开启)之后、加入对话之前,包括主动加入和被其他用户加入两种情况都会触发。**注意如果在创建对话时传入了其他用户的 `clientId` 作为成员,则不会触发该 hook。**如果是自己加入,那么 `initBy` 和 `members` 的唯一元素是一样的。 - -参数: - -参数 | 说明 ---- | --- -`initBy` | 由谁发起的 `clientId`。 -`members` | 要加入的成员,数组。 -`convId` | 对话 ID。 - -返回值: - -参数 | 约束 | 说明 ---- | --- | --- -`reject` | 可选 | 是否拒绝,默认为 `false`。 -`code` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的整型错误码。 -`detail` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 - -例如,不允许某成员创建的对话新增成员: - - - -```js -AV.Cloud.onIMConversationAdd((request) => { - if (request.params.initBy === "Tom") { - return { - "reject": true, - "code": 9890, - "detail": "会话已封闭,不允许新增成员。" - }; - } else { - return {} - } -}); -``` -```python -@engine.define -def _conversationAdd(**params): - if params["initBy"] == "Tom": - return { - "reject": True, - "code": 9890, - "detail": "会话已封闭,不允许新增成员。" - } - else: - return {} -``` -```php -Cloud::define('_conversationAdd', function($params, $user) { - if ($params["initBy"] === "Tom") { - return [ - "reject" => true, - "code" => 9890, - "detail" => "会话已封闭,不允许新增成员。", - ]; - } else { - return array(); - } -}); -``` -```java -@IMHook(type = IMHookType.conversationAdd) -public static Map onConversationAdd(Map params) { - Map result = new HashMap(); - if ("Tom".equals(params.get("initBy"))) { - result.put("reject", true); - result.put("code", 9890); - result.put("detail", "会话已封闭,不允许新增成员。") - } - return result; -} -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationAdd)] -public static object OnConversationAdd(Dictionary parameters) { - if ("Tom".Equals(parameters["initBy"])) { - return new Dictionary { - { "reject", true }, - { "code", 9890 }, - { "detail", "会话已封闭,不允许新增成员。" } - }; - } - return default; -} -``` -```go -// 暂不支持 -``` - - - - -#### `_conversationRemove` - -从对话中移除成员,在签名校验(如果开启)之后、实际踢出之前触发,用户自己退出对话不会触发。 - -参数: - -参数 | 说明 ---- | --- -`initBy` | 由谁发起。 -`members` | 要踢出的成员,数组。 -`convId` | 对话 iD。 - -返回值: - -参数 | 约束 | 说明 ---- | --- | --- -`reject` | 可选 | 是否拒绝,默认为 `false`。 -`code` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的整型错误码。 -`detail` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 - -例如,某个应用会让官方运营人员加入每个聊天群,希望加上群主无法踢掉官方运营的限制: - - - -```js -AV.Cloud.onIMConversationRemove(async (request) => { - const supporters = ["Bast", "Hypnos", "Kthanid"]; - const members = request.params.members; - for (const member of members) { - if (supporters.includes(member)) { - return { - "reject": true, - "code": 1928, - "detail": `不允许移除官方运营人员 ${member}`, - }; - } - } - return {}; -} -``` -```python -@engine.define -def _conversationRemove(**params): - supporters = ["Bast", "Hypnos", "Kthanid"] - members = params["members"] - for member in members: - if member in supporters: - return { - "reject": True, - "code": 1928, - "detail": f"不允许移除官方运营人员 {member}" - } - return {} -``` -```php -Cloud::define('_conversationRemove', function($params, $user) { - $supporters = array("Bast", "Hypnos", "Kthanid"); - $members = $params["members"]; - foreach ($members as $member) { - if (in_array($member, $supporters)) { - return [ - "reject" => true, - "code" => 1928, - "detail" => "不允许移除官方运营人员 $member", - ]; - } - } - return array(); -}); -``` -```java -@IMHook(type = IMHookType.conversationRemove) -public static Map onConversationRemove(Map params) { - String[] supporters = {"Bast", "Hypnos", "Kthanid"}; - String[] members = (String[])params.get("members"); - Map result = new HashMap(); - for (String member : members) { - if (Arrays.asList(supporters).contains(member)) { - result.put("reject", true); - result.put("code", 1928); - result.put("detail", "不允许移除官方运营人员 " + member); - } - } - return result; -} -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationRemove)] -public static object OnConversationRemove(Dictionary parameters) { - List supporters = new List { "Bast", "Hypnos", "Kthanid" }; - List members = parameters["members"] as List; - foreach (object member in members) { - if (supporters.Contains(member as string)) { - return new Dictionary { - { "reject", true }, - { "code", 1928 }, - { "detail", $"不允许移除官方运营人员 {member}" } - }; - } - } - return default; -} -``` -```go -// 暂不支持 -``` - - - - -#### `_conversationAdded` - -成员成功加入对话后调用。 - -参数: - -参数 | 说明 ---- | --- -`initBy` | 由谁发起。 -`convId` | 对话 ID。 -`members` | 新加入的用户 ID 数组 - -返回值: - -这个 hook 不会对返回值进行检查。 - -例如,如果一个群一次性加入了 10 个以上的成员,那么给运营人员发送一条通知短信: - - - -```js -AV.Cloud.onIMConversationAdded((request) => { - if (request.params.members.length > 10) { - AV.Cloud.requestSmsCode({ - mobilePhoneNumber: "18200008888", - template: "Group_Notice", - sign: "sign_example", - conv_id: request.params.convId, - }).then( - function () { /* 调用成功 */ }, - function (err) { /* 调用失败 */} - ); - } -}); -``` -```python -@engine.define -def _conversationAdded(**params): - if len(params["members"]) > 10: - cloud.request_sms_code( - "18200008888", - template="Group_Notice", sign: "sign_example", - params={"conv_id": params["convId"]} - ) -``` -```php -Cloud::define('_conversationAdded', function($params, $user) { - if (count($params["members"]) > 10) { - $options = [ - "template" => "Group_Notice", - "name" => "sign_example", - "conv_id" => $params["convId"], - ]; - SMS::requestSmsCode("18200008888", $options); - } -}); -``` -```java -@IMHook(type = IMHookType.conversationAdded) -public static void onConversationAdded(Map params) { - String[] members = (String[])params.get("members"); - if (members.length > 10) { - LCSMSOption option = new LCSMSOption(); - option.setTemplateName("Group_Notice"); - option.setSignatureName("sign_example"); - Map parameters = new HashMap(); - parameters.put("conv_id", params.get("convId")); - option.setEnvMap(parameters); - LCSMS.requestSMSCodeInBackground("18200008888", option).subscribe(new Observer() { - @Override - public void onSubscribe(Disposable disposable) {} - @Override - public void onNext(LCNull avNull) { - Log.d("TAG","Result: Successfully sent text message."); - } - @Override - public void onError(Throwable throwable) { - Log.d("TAG","Result: Failed to send text message. Reason: " + throwable.getMessage()); - } - @Override - public void onComplete() {} - }); - } -} -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationAdded)] -public static async Task OnConversationAdded(Dictionary parameters) { - List members = (parameters["members"] as List) - .Cast() - .ToList(); - if (members.Count > 10) { - Dictionary variables = new Dictionary { - { "conv_id", request.Params["convId"] } - }; - try { - await LCSMSClient.RequestSMSCode("18200008888", "Group_Notice", "sign_example", variables: variables); - Console.WriteLine("Successfully sent text message."); - } catch (Exception e) { - Console.WriteLine($"Failed to send text message. Reason: {e.Message}"); - } - } -} -``` -```go -// 暂不支持 -``` - - - - -#### `_conversationRemoved` - -成员成功离开对话后调用。 - -参数: - -参数 | 说明 ---- | --- -`initBy` | 由谁发起。 -`convId` | 对话 ID。 -`members` | 新加入的用户 ID 数组 - -返回值: - -这个 hook 不会对返回值进行检查。 - -例如,如果用户自行离开了对话,那么将这个对话的 ID 存储到 LeanCache(应用可以利用这些数据实现展示「最近离开的对话」乃至重新加入的功能): - - - -```js -AV.Cloud.onIMConversationRemoved((request) => { - const initBy = request.params.initBy; - const members = request.params.members; - if (members.length === 1) { - if (members[0] === initBy) { - redisClient.lpush(initBy, request.params.convId); - } - } -}); -``` -```python -@engine.define -def _conversationRemoved(**params): - init_by = params["initBy"] - members = params["members"] - if len(members) == 1: - if members[0] == init_by: - redis_client.lpush(init_by, params["convId"]) -``` -```php -Cloud::define('_conversationRemoved', function($params, $user) { - $initBy = $params['initBy']; - $members = $params['members']; - if (count($members) === 1) { - if (members[0] === $initBy) { - $redis->lpush($initBy, $params["convId"]); - } - } -}); -``` -```java -@IMHook(type = IMHookType.conversationRemoved) -public static void onConversationRemoved(Map params) { - String[] members = (String[])params.get("members"); - String initBy = (String)params.get("initBy"); - if (members.length == 1) { - if (initBy.equals(members[0])) { - jedis.lpush(initBy, params.get("convId")); - } - } -} -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationRemoved)] -public static void OnConversationRemoved(Dictionary parameters) { - List members = (parameters["members"] as List) - .Cast() - .ToList(); - string initBy = parameters["initBy"] as string; - if (members.Count == 1 && members[0].Equals(initBy)) { - Console.WriteLine($"{parameters["convId"]} removed."); - } -} -``` -```go -// 暂不支持 -``` - - - - -#### `_conversationUpdate` - -在修改对话名称、自定义属性,设置或取消对话消息提醒之前调用。 - -参数: - -参数 | 说明 ---- | --- -`initBy` | 由谁发起。 -`convId` | 对话 ID。 -`mute` | 是否关闭当前对话提醒。 -`attr` | 待设置的对话属性。 - -`mute` 和 `attr` 参数互斥,不会同时传递。 - -返回值: - -参数 | 约束 | 说明 ---- | --- | --- -`reject` | 可选 | 是否拒绝,默认为 `false`。 -`code` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的整型错误码。 -`detail` | 可选 | 当 `reject` 为 `true` 时可以下发一个应用自定义的错误说明字符串。 -`attr` | 可选 | 修改后的待设置对话属性,如果不提供则保持原参数中的对话属性。 -`mute` | 可选 | 修改后的关闭对话提醒设置,如果不提供则保持原参数中的关闭提醒设置。 - -`mute` 和 `attr` 参数互斥,不能同时返回。并且返回值必须与请求对应,请求中如果带着 `attr`,则返回值中只有 `attr` 参数有效,返回 `mute` 会被丢弃。同理,请求中如果带着 `mute`,返回值中如果有 `attr` 则 `attr` 会被丢弃。 - -例如,不允许修改对话名称: - - - -```js -AV.Cloud.onIMConversationUpdate((request) => { - if ('attr' in request.params && 'name' in request.params.attr) { - return { - "reject": true, - "code": 1949, - "detail": "对话名称不可修改", - }; - } -}); -``` -```python -@engine.define -def _conversationUpdate(**params): - if ('attr' in params) and ('name' in params['attr']): - return { - "reject": True, - "code": 1949, - "detail": "对话名称不可修改" - } -``` -```php -Cloud::define('_conversationUpdate', function($params, $user) { - if (array_key_exists('attr', $params) && array_key_exists('name', $params["attr"])) { - return [ - "reject" => true, - "code" => 1949, - "detail" => "对话名称不可修改", - ]; - } -}); -``` -```java -@IMHook(type = IMHookType.conversationUpdate) -public static Map onConversationUpdate(Map params) { - Map result = new HashMap(); - Map attr = (Map)params.get("attr"); - if (attr != null && attr.containsKey("name")) { - result.put("reject", true); - result.put("code", 1949); - result.put("detail", "对话名称不可修改"); - } - return result; -} -``` -```cs -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ConversationUpdate)] -public static object OnConversationUpdate(Dictionary parameters) { - Dictionary attr = parameters["attr"] as Dictionary; - if (attr != null && attr.ContainsKey("name")) { - return new Dictionary { - { "reject", true }, - { "code", 1949 }, - { "detail", "对话名称不可修改" } - }; - } - return default; -} -``` -```go -// 暂不支持 -``` - - - - -#### `_clientOnline` - -客户端上线,客户端登录成功后调用。 - -请注意本 Hook 仅作为用户上线后的通知。如果用户快速的进行上下线切换或有多个设备同时进行上下线,则上线和下线 Hook 调用顺序并不做严格保证,即可能出现某用户 `_clientOffline` Hook 先于 `_clientOnline` Hook 而调用。 - -参数: - -参数 | 说明 ------ | ------ -peerId | 登录者的 ID -sourceIP | 登录者的 IP -tag | 无值或值为 "default" 表示主动登录时不会根据 tag 踢其他设备下线,其他情况下会踢当前登录的相同 tag 的设备下线 -reconnect | 标识客户端本次登录是否是自动重连,无值或值为 0 表示主动登录,值为 1 表示自动重连 - -返回: - -这个 hook 不会对返回值进行检查。 - -例如,客户端上线后更新 LeanCache,供查询客户端的实时在线状态: - - - - -```js -AV.Cloud.onIMClientOnline((request) => { - // 1 表示在线 - redisClient.set(request.params.peerId, 1) -}) -``` -```python -@engine.define -def _clientOnline(**params): - # 1 表示在线 - redis_client.set(params["peerId"], 1) -``` -```php -Cloud::define('_clientOnline', function($params, $user) { - // 1 表示在线 - $redis->set($params["peerId"], 1); -} -``` -```java -@IMHook(type = IMHookType.clientOnline) -public static void onClientOnline(Map params) { - // 1 表示在线 - jedis.set(params.get("peerId"), 1); -} -``` -```cs -// 注意,C# 代码示例中没有更新 LeanCache,仅仅输出了用户状态 -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ClientOnline)] -public static void OnClientOnline(Dictionary parameters) { - Console.WriteLine($"{parameters["peerId"]} online."); -} -``` -```go -// 暂不支持 -``` - - - - -#### `_clientOffline` - -在客户端登出成功或意外下线后调用。 - -请注意本 Hook 仅作为用户下线后的通知。如果用户快速的进行上下线切换或有多个设备同时进行上下线,则上线和下线 Hook 调用顺序并不做严格保证,即可能出现某用户 `_clientOffline` Hook 先于 `_clientOnline` Hook 而调用。 - -参数: - -参数 | 说明 ------ | ------ -peerId | 登出者的 ID -closeCode | 登出的方式,1 代表用户主动登出,2 代表连接断开,3 代表用户由于 `tag` 冲突被踢下线,4 代表用户被 API 踢下线 -closeMsg | 对登出方式的描述信息 -sourceIP | 执行关闭会话操作的用户的 IP,连接断开时不传递此参数 -tag | 由用户在会话创建时传递而来,无值或值为 `default` 表示主动登录时不会根据 tag 踢其他设备下线,其他情况下会踢当前登录的相同 tag 的设备下线 -errorCode | 导致连接断开的错误码,可选 -errorMsg | 导致连接断开的错误信息,可选 - -可能出现的错误信息说明: - -错误码 | 错误信息 | 解释 ------ | ------- | --- -4107 | READ_TIMEOUT | 长时间未发送消息或心跳包导致连接超时 -4108 | LOGIN_TIMEOUT | 一定时间内未登录而导致超时 -4109 | FRAME_TOO_LONG | WebSocket 帧过长 -4114 | UNPARSEABLE_RAW_MSG | 消息格式错误无法解析 -4200 | INTERNAL_ERROR | 服务器内部错误 - -返回: - -这个 hook 不会对返回值进行检查。 - -例如,客户端下线后更新 LeanCache,供查询客户端的实时在线状态: - - - -```js -AV.Cloud.onIMClientOffline((request) => { - // 0 表示下线 - redisClient.set(request.params.peerId, 0) -}) -``` -```python -@engine.define -def _clientOffline(**params): - # 0 表示下线 - redis_client.set(params["peerId"], 0) -``` -```php -Cloud::define('_clientOffline', function($params, $user) { - // 0 表示下线 - $redis->set($params["peerId"], 0); -} -``` -```java -@IMHook(type = IMHookType.clientOffline) -public static void onClientOffline(Map params) { - // 0 表示下线 - jedis.set(params.get("peerId"), 0); -} -``` -```cs -// 注意,C# 代码示例中没有更新 LeanCache,仅仅输出了用户状态 -[LCEngineRealtimeHook(LCEngineRealtimeHookType.ClientOffline)] -public static void OnClientOffline(Dictionary parameters) { - Console.WriteLine($"{parameters["peerId"]} offline"); -} -``` -```go -// 暂不支持 -``` - - - - -[《即时通讯中的在线状态查询》](https://leancloud.cn/docs/realtime-guide-onoff-status.html)提供了完整的 Node.js 示例(包括 LeanCache 连接,久未上线的客户端清理,配套的返回在线状态的云函数,以及如何在客户端调用),可以参考。 - -## 「系统对话」的使用 - -系统对话可以用于实现机器人自动回复、公众号、服务账号等功能。在我们的 [官方聊天 Demo](https://leancloud.github.io/leanmessage-demo/) 中就有一个使用系统对话 hook 实现的机器人 MathBot,它能计算用户发送来的数学表达式并返回结果,[其服务端源码](https://github.com/leancloud/leanmessage-demo/tree/master/server) 可以从 GitHub 上获取。 - -### 系统对话的创建 - -系统对话也是对话的一种,创建后也是在 `_Conversation` 表中增加一条记录,只是该记录 `sys` 列的值为 `true`,从而与普通会话进行区别。具体创建方法请参考[即时通讯 REST API 使用指南](/v2/sdk/im/guide/rest)的《创建服务号》一节。 -### 系统对话消息的发送 - -系统对话给用户发消息请参考[即时通讯 REST API 使用指南](/v2/sdk/im/guide/rest)的《给任意用户单独发消息》一节。用户给系统对话发送消息跟用户给普通对话发消息方法一致。 - -您还可以利用系统对话发送广播消息给全部用户。相比遍历所有用户 ID 逐个发送,广播消息只需要调用一次 REST API。广播消息具有以下特征: - -* 广播消息必须与系统对话关联,广播消息和一般的系统对话消息会混合在系统对话的记录中 -* 用户离线时发送的广播消息,上线后会作为离线消息收到 -* 广播消息具有实效性,可以设置过期时间;过期的消息不会作为离线消息发送给用户,不过仍然可以在历史消息中获取到 -* 新用户第一次登录后,会收到最近一条未过期的系统广播消息 - -除此以外广播消息与普通消息的处理完全一致。广播消息的发送可以参考[即时通讯 REST API 使用指南](/v2/sdk/im/guide/rest)的《全局广播》一节。 - -### 获取系统对话消息记录 - -获取系统对话给用户发送的消息记录请参考:[即时通讯 REST API 使用指南](/v2/sdk/im/guide/rest)的《查询服务号给某用户发的消息》一节。 - -获取用户给系统对话发送的消息记录有以下两种方式实现: - -- `_SysMessage` 表方式,在应用首次有用户发送消息给某系统对话时自动创建,创建后我们将所有由用户发送到系统对话的消息都存储在该表中。 -- [Web Hook](#Web_Hook) 方式,这种方式需要开发者自行定义 [Web Hook](#Web_Hook),用于实时接收用户发给系统对话的消息。 - -### 系统对话消息结构 - -#### `_SysMessage` - -存储用户发给系统对话的消息,各字段含义如下: - -字段 | 说明 ---- | --- -`ackAt` | 消息送达的时间。 -`bin` | 是否为二进制类型消息。 -`conv` | 消息关联的系统对话 `Pointer`。 -`data` | 消息内容。 -`from` | 发消息用户的 `clientId`。 -`fromIp` | 发消息用户的 IP。 -`msgId` | 消息的内部 ID。 -`timestamp` | 消息创建的时间。 - -#### Web Hook - -需要开发者自行在 **云服务控制台 > 即时通讯 > 设置 > 系统对话消息回调设置** 定义,来实时接收用户发给系统对话的消息,消息的数据结构与上文所述的 `_SysMessage` 一致。 - -当有用户向系统对话发送消息时,我们会通过 HTTP POST 请求将 JSON 格式的数据发送到用户设置的 Web Hook 上。请注意,我们调用 Web Hook 时并不是一次调用只发送一条消息,而是会以批量的形式将消息发送过去。从下面的发送消息格式中能看到,JSON 的最外层是个 `Array`。 - -超时时间为 5 秒,当用户 hook 地址超时没有响应,我们会重试至多 3 次。 - -发送的消息格式为: - -```json -[ - { - "fromIp": "121.238.214.92", - "conv": { - "__type": "Pointer", - "className": "_Conversation", - "objectId": "55b99ad700b0387b8a3d7bf0" - }, - "msgId": "nYH9iBSBS_uogCEgvZwE7Q", - "from": "A", - "bin": false, - "data": "你好,sys", - "createdAt": { - "__type": "Date", - "iso": "2015-07-30T14:37:42.584Z" - }, - "updatedAt": { - "__type": "Date", - "iso": "2015-07-30T14:37:42.584Z" - } - } -] -``` - -## 即时通讯开发指南一览 - -《服务总览》 - -《一,从简单的单聊、群聊、收发图文消息开始》 - -《二,消息收发的更多方式,离线推送与消息同步,多设备登录》 - -《三,安全与签名、黑名单和权限管理、玩转直播聊天室和临时对话》 diff --git a/versioned_docs/version-v2/sdk/10-im/02-guide/05-rest.mdx b/versioned_docs/version-v2/sdk/10-im/02-guide/05-rest.mdx deleted file mode 100644 index 5c1f6a10b..000000000 --- a/versioned_docs/version-v2/sdk/10-im/02-guide/05-rest.mdx +++ /dev/null @@ -1,2225 +0,0 @@ ---- -id: rest -title: 即时通讯 REST API -sidebar_label: 即时通讯 REST API ---- - - - -## 概览 - -请求的 Base URL 可以在**云服务控制台 > 设置 > 应用 Keys > 服务器地址**查看。 -对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP Header 的 Content-Type 需要设置为 `application/json`。 -请求的鉴权是通过 HTTP Header 里面包含的键值对来进行的,详见[数据存储 REST API 使用详解](/v2/sdk/storage/guide/rest)中《请求格式》一节的说明。 - - -`_Conversation` 表包含一些内置的关键字段定义了对话的属性、成员等,单聊、群聊、聊天室、服务号均在此表中,详见[即时通讯总览](/v2/sdk/im/guide/overview)的《对话》一节。 -不过为了避免出现数据不一致问题,我们不推荐调用数据存储相关的 API 直接操作 `_Conversation` 表中的数据。 - -当前的 API 版本为 `1.2`: - -- 单聊、群聊相关 API 以 `rtm/conversations` 标示 -- 聊天室相关 API 以 `rtm/chatrooms` 标示,在 `_Conversation` 表内用字段 `tr` 为 true 标示。 -- 服务号相关 API 以 `rtm/service-conversations` 标示,在 `_Conversation` 表内用字段 `sys` 为 true 标示。 - -除此之外,与 client 相关的请求以 `rtm/clients` 标示。 -最后,一些全局性质的 API 直接以 `rtm/{function}` 标示,如 `rtm/all-conversations` 可查询所有类型的对话。 - -## 单聊、群聊 - -### 创建对话 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"My First Conversation", "m": ["BillGates", "SteveJobs"], "unique": true}' \ - https://{{host}}/1.2/rtm/conversations -``` - -上面的例子会创建一个最简单的对话,包括两个 client ID 为 BillGates 和 SteveJobs 的初始成员。对话创建成功会返回 objectId,即即时通讯中的对话 ID,客户端就可以通过这个 ID 发送消息了。新创建的对话可以在 `_Conversation` 表内找到。 -对话的字段可参考[即时通讯总览](/v2/sdk/im/guide/overview)的《对话》一节。 -传入 `"unique": true` 参数可以保证对话的唯一性。 - -返回 - -```json -{ - "unique":true, - "updatedAt":"2020-05-26T06:42:31.492Z", - "name":"My First Conversation", - "objectId":"5eccba570d3a42c5fd4e25c3", - "m":["BillGates","SteveJobs"], - "createdAt":"2020-05-26T06:42:31.482Z", - "uniqueId":"6c7b0e5afcae9aa1139a0afa25833dec" -} -``` - -需要注意,群聊与单聊的唯一区别是 client 数量,API 层面是一致的。 - -### 查询对话 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'where={"name": "first conversation"}' \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/conversations -``` - -参数 | 约束 | 说明 ----|---|--- -skip | 可选 | -limit | 可选 | 与 skip 联合使用实现分页 -where | 可选 | 参见[数据存储 REST API 使用详解](/v2/sdk/storage/guide/rest)的《查询》一节 - - -返回 - -```json -{"results": [ - {"name":"test conv1", - "m":["tom", "jerry"], - "createdAt":"2018-01-17T04:15:33.386Z", - "updatedAt":"2018-01-17T04:15:33.386Z", - "objectId":"5a5ecde6c3422b738c8779d7"} -]} -``` - -### 更新对话 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"Updated Conversation"}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id} -``` - -`_Conversation` 表除 m 字段均可通过这个接口更新。 - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - - -### 删除对话 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id} -``` - -返回 - -```json -{} -``` - -### 增加成员 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/members -``` - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - -### 移除成员 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/members -``` - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - - -### 查询成员 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/members -``` - -返回 - -```json -{"result": ["client1", "client2"]} -``` - - -### 增加静音用户 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/mutes -``` - -参数 | 说明 ---- | --- -client_ids | 要静音的 `Client ID`,数组 - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - -### 移除静音用户 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/mutes -``` - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - -### 查询静音用户 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/mutes -``` - -返回 - -```json -{"result": ["client1", "client2"]} -``` - - -### 单聊、群聊-发消息 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages -``` - -**注意**,由于这个接口的管理性质,当你通过这个接口发送消息时,我们不会检查 **from_client** 是否有权限给这个对话发送消息,而是统统放行,请谨慎使用这个接口。 -如果你在应用中使用了我们内部定义的富媒体消息格式,在发送消息时 **message** 字段需要遵守相应的格式要求。 - - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client Id -message | 必填 | 消息内容(这里的消息内容的本质是字符串,但是我们对字符串内部的格式没有做限定,理论上开发者可以随意发送任意格式,只要大小不超过 5 KB 限制即可。) -transient | 可选 | 是否为暂态消息,默认 false -no_sync | 可选 | 默认情况下消息会被同步给在线的 from_client 用户的客户端,设置为 true 禁用此功能。 -push_data | 可选 | 以消息附件方式设置本条消息的离线推送通知内容。如果目标接收者使用的是 iOS 设备并且当前不在线,我们会按照该参数填写的内容来发离线推送。请参看[即时通讯开发指南第二篇](/v2/sdk/im/guide/intermediate)的《离线推送通知》一节的说明。 -priority | 可选 | 定义消息优先级,可选值为 high、normal、low,分别对应高、中、低三种优先级。该参数大小写不敏感,默认为中优先级 normal。本参数仅对暂态消息或聊天室的消息有效,高优先级下在服务端与用户设备的连接拥塞时依然排队。 -mention_all | 可选 | 布尔类型,用于提醒对话内所有成员注意本消息。 -mention_client_ids | 可选 | 数组类型,表示需要提醒注意本消息的对话内成员 client_id 列表,最多能包含 20 个 client Id。 - -返回说明: - -默认情况下发送消息 API 使用异步的方式,调用后返回消息 id 和接收消息的服务器时间戳,例如 `{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151}`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 查询历史消息 - -该接口要求使用 master key。 -为了保证获取聊天记录的安全性,可以开启签名认证,具体可以参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)的《安全与签名》一节。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages -``` - -参数 | 约束 | 说明 ---- | --- | --- -msgid | 可选 | 起始的消息 id,**使用时必须加上对应消息的时间戳 timestamp 参数,作为查询的起点** -timestamp | 可选 | 查询起始的时间戳。默认是当前时间,单位是毫秒 -till_msgid | 可选 | 查询终止的消息 id。**使用时必须加上消息的时间戳 till_timestamp 参数,作为查询的终点** -till_timestamp | 可选 | 查询终止的时间戳,默认为 0,单位是毫秒 -include_start | 可选 | 是否包含由 timestamp 与 msgid 确定的起始消息。布尔值,默认为 false -include_stop | 可选 | 是否包含由 till_timestamp 与 till_msgid 确定的终止消息。布尔值,默认为 false -reversed | 可选 | 以默认排序(默认按时间降序)相反的方向返回结果,这时 till_timestamp 默认为当前时间戳,timestamp 默认为 0。布尔值,默认为 false -limit | 可选 | 返回条数限制,可选,默认 100 条,最大 1000 条 -client_id | 可选 | 查看者 id(签名参数) -nonce | 可选 | 签名随机字符串(签名参数) -signature_ts | 可选 | 签名时间戳(签名参数),单位是毫秒 -signature | 可选 | 签名(签名参数) - -本接口时间参数较多,这里举一示例供大家参考。比如某对话内有三条消息,id 分别为 id1、id2、id3,发消息的时间分别是 t1、t2、t3(t1 < t2 < t3),下面列举出不同参数组合的查询结果(空白表示使用默认值): - -| timestamp| msgid| till_timestamp| till_msgid| include_start| include_stop| reversed| 结果 | -| ---------|---------|---------|---------|---------|---------|---------|--------- | -| t3| id3| t1| id1| | | | id2 | -| t3| id3| t1| id1| true| | | id3 id2 | -| t3| id3| t1| id1| | true| | id2 id1 | -| t1| id1| t3| id3| | | true| id2 | -| t1| id1| t3| id3| true| | true| id1 id2 | -| t1| id1| t3| id3| | true| true| id2 id3 | - -返回数据格式,JSON 数组,默认按消息记录从新到旧排序,设置请求参数 `reversed` 后以相反的方向排序。 - -返回: - -```json -[ - { - "timestamp": 1408008498571, - "conv-id": "219946ef32e40c515d33ae6975a5c593", - "data": "今天天气不错!", - "from": "u111872755_9d0461adf9c267ae263b3742c60fa", - "msg-id": "vdkGm4dtRNmhQ5gqUTFBiA", - "is-conv": true, - "is-room": false, - "to": "5541c02ce4b0f83f4d44414e", - "bin": false, - "from-ip": "202.117.15.217" - }, - ... -] -``` - -如需查询某个用户发出的消息,可以调用 `GET /rtm/clients/{client_id}/messages` 这个接口。 -如需查询整个应用的历史消息,可以调用 `GET /rtm/messages` 这个接口。 - -### 单聊、群聊-修改消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages/{message_id} -``` - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -message | 必填 | 消息体 -timestamp | 必填 | 消息的时间戳 - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 单聊、群聊-撤回消息 - -该接口要求使用 master key。需要相应 SDK 的支持,具体可参考之前的修改消息接口。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages/{message_id}/recall -``` - - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -timestamp | 必填 | 消息的时间戳 - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 删除消息 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'from_client=some-client-id' \ - --data-urlencode 'timestamp=123' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/messages/{message_id} -``` - -注意,该接口仅删除服务端的消息,对客户端无影响。 - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -timestamp | 必填 | 消息的时间戳 - - -返回: - -```json -{} -``` - -### 增加临时性禁言用户 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_id": "some-client-id", "ttl": 50}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/temporary-silenceds -``` - -参数 | 说明 ---- | --- -client_id | 要禁言的 `Client ID`,字符串 -ttl | 禁言的时间,秒数,最长 24 小时 - -返回 - -```json -{} -``` - -### 移除临时性禁言用户 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'client_id=some-client-id' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/temporary-silenceds -``` - -返回 - -```json -{} -``` - -### 对话权限 - -该功能介绍可参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)中的《权限管理与黑名单》一节。 - -#### 增加权限 - -该接口要求使用 master key。 -每个对话最多允许添加 500 个永久性禁言用户。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"clientId": "client", "role": "role"}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/member-infos -``` - -参数 | 说明 ---- | --- -clientId | 用户 ID,字符串 -role | 角色,可选值 Member、Manager、Owner - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - -#### 删除权限 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/member-infos/{info-id} -``` - -参数 | 说明 ---- | --- -info-id | 该记录对应的 objectId - -返回 - -```json -{} -``` - -#### 更新权限 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"clientId": "client", "role": "role"}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/member-infos/{info-id} -``` - -参数 | 说明 ---- | --- -clientId | 用户 ID,字符串 -role | 角色,可选值 Member、Manager、Owner。可选 -info-id | 该记录对应的 objectId - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - - -#### 查询权限 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/member-infos -``` - -参数 | 说明 ---- | --- -skip | -limit | 与 skip 联合使用实现分页 -role | 本次查询只希望包含该角色 - -返回 - -```json -{"results": [{"clientId":"client1", "objectId":"5a5d7433c3422b31ed845e76", "role": "Manager"}]} -``` - - -#### 增加永久性禁言用户 - -该接口要求使用 master key。 -每个对话最多允许添加 500 个永久性禁言用户。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/permanent-silenceds -``` - -参数 | 说明 ---- | --- -client_ids | 要禁言的 `Client ID` 列表,数组 - -返回 - -```json -{} -``` - -#### 移除永久性禁言用户 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/permanent-silenceds -``` - -返回 - -```json -{} -``` - -#### 查询永久性禁言列表 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/permanent-silenceds -``` - -参数 | 约束 | 说明 ----|---|--- -limit | 可选 | 与 next 联合使用实现分页,默认 10 -next | 可选 | 第一次查询时返回,后面的查询带着这个参数,实现分页查询 - -返回 - -```json -{"client_ids": ["client1", "client2"]} -``` - -### 黑名单 - -该功能介绍可参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)中的《权限管理与黑名单》一节。 - -#### 增加对话黑名单 - -该接口要求使用 master key。 - -加入黑名单的 client 不允许再加入该对话,如果之前在该对话中将被移除。每个对话最多允许添加 500 个黑名单。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/blacklists -``` - -参数 | 说明 ---- | --- -client_ids | 要拉黑的 `Client ID` 列表,数组 - -返回 - -```json -{} -``` - -#### 移除对话黑名单 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/blacklists -``` - -返回 - -```json -{} -``` - -#### 查询对话黑名单 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/conversations/{conv_id}/blacklists -``` - -参数 | 约束 | 说明 ----|---|--- -limit | 可选 | 与 next 联合使用实现分页,默认 10 -next | 可选 | 第一次查询时返回,后面的查询带着这个参数,实现分页查询 - -返回 - -```json -{"client_ids": ["client1", "client2"]} -``` - -## 聊天室 - -### 创建聊天室 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"My First Chatroom"}' \ - https://{{host}}/1.2/rtm/chatrooms -``` - -对话的字段可参考[即时通讯总览](/v2/sdk/im/guide/overview)的《对话》一节。 - -返回 - -```json -{"objectId":"5a5d7432c3422b31ed845e75", "createdAt":"2018-01-16T03:40:32.814Z"} -``` - -### 查询聊天室 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'where={"name": "chatroom"}' \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/chatrooms -``` - -参数 | 约束 | 说明 ----|---|--- -skip | 可选 | -limit | 可选 | 与 skip 联合使用实现分页 -where | 可选 | 请参考[数据存储 REST API 使用详解](/v2/sdk/storage/guide/rest)的《查询》一节。 - - -返回 - -```json -{"results":[ - {"name":"My First Chatroom", - "createdAt":"2018-01-17T04:15:33.386Z", "updatedAt":"2018-01-17T04:15:33.386Z", - "objectId"=>"5a5ecde6c3422b738c8779d7"} -]} -``` - -### 更新聊天室 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"Updated Chatroom"}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id} -``` - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - -### 删除聊天室 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id} -``` - -返回 - -```json -{} -``` - -### 随机获取在线成员 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/members -``` - -返回 - -```json -{"result": ["clientid1", "clientid2", "clientid3"]} -``` - -### 查询在线成员数 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/members/online-count -``` - -返回 - -```json -{"result": 3} -``` - - -### 聊天室-发消息 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages -``` - -**注意**,由于这个接口的管理性质,当你通过这个接口发送消息时,我们不会检查 **from_client** 是否有权限给这个聊天室发送消息,而是统统放行,请谨慎使用这个接口。 -如果你在应用中使用了我们内部定义的富媒体消息格式,在发送消息时 **message** 字段需要相应的格式要求。 -此外,聊天室目前**不支持**将消息同步发送给在线的 **from_client**。 - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client Id -message | 必填 | 消息内容(这里的消息内容的本质是字符串,但是我们对字符串内部的格式没有做限定,理论上开发者可以随意发送任意格式,只要大小不超过 5 KB 限制即可。) -transient | 可选 | 是否为暂态消息,默认 false -priority | 可选 | 定义消息优先级,可选值为 high、normal、low,分别对应高、中、低三种优先级。该参数大小写不敏感,默认为中优先级 normal。本参数仅对暂态消息或聊天室的消息有效,高优先级下在服务端与用户设备的连接拥塞时依然排队。 -mention_all | 可选 | 布尔类型,用于提醒对话内所有成员注意本消息。 -mention_client_ids | 可选 | 数组类型,表示需要提醒注意本消息的对话内成员 client_id 列表,最多能包含 20 个 client Id。 - -返回说明: - -默认情况下发送消息 API 使用异步的方式,调用后返回消息 id 和接收消息的服务器时间戳,例如 `{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151}`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 查询历史消息 - -该接口要求使用 master key。 -为了保证获取聊天记录的安全性,可以开启签名认证,具体可以参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)的《安全与签名》一节。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/messages -``` - -参数 | 约束 | 说明 ---- | --- | --- -msgid | 可选 | 起始的消息 id,**使用时必须加上对应消息的时间戳 timestamp 参数,作为查询的起点** -timestamp | 可选 | 查询起始的时间戳。默认是当前时间,单位是毫秒 -till_msgid | 可选 | 查询终止的消息 id。**使用时必须加上消息的时间戳 till_timestamp 参数,作为查询的终点** -till_timestamp | 可选 | 查询终止的时间戳,默认为 0,单位是毫秒 -include_start | 可选 | 是否包含由 timestamp 与 msgid 确定的起始消息。布尔值,默认为 false -include_stop | 可选 | 是否包含由 till_timestamp 与 till_msgid 确定的终止消息。布尔值,默认为 false -reversed | 可选 | 以默认排序(默认按时间降序)相反的方向返回结果,这时 till_timestamp 默认为当前时间戳,timestamp 默认为 0。布尔值,默认为 false -limit | 可选 | 返回条数限制,可选,默认 100 条,最大 1000 条 -client_id | 可选 | 查看者 id(签名参数) -nonce | 可选 | 签名随机字符串(签名参数) -signature_ts | 可选 | 签名时间戳(签名参数),单位是毫秒 -signature | 可选 | 签名(签名参数) - -本接口时间参数较多,这里举一示例供大家参考。比如某对话内有三条消息,id 分别为 id1、id2、id3,发消息的时间分别是 t1、t2、t3(t1 < t2 < t3),下面列举出不同参数组合的查询结果(空白表示使用默认值): - -| timestamp| msgid| till_timestamp| till_msgid| include_start| include_stop| reversed| 结果 | -| ---------|---------|---------|---------|---------|---------|---------|--------- | -| t3| id3| t1| id1| | | | id2 | -| t3| id3| t1| id1| true| | | id3 id2 | -| t3| id3| t1| id1| | true| | id2 id1 | -| t1| id1| t3| id3| | | true| id2 | -| t1| id1| t3| id3| true| | true| id1 id2 | -| t1| id1| t3| id3| | true| true| id2 id3 | - -返回数据格式,JSON 数组,默认按消息记录从新到旧排序,设置请求参数 `reversed` 后以相反的方向排序。 - -返回: - -```json -[ - { - "timestamp": 1408008498571, - "conv-id": "219946ef32e40c515d33ae6975a5c593", - "data": "今天天气不错!", - "from": "u111872755_9d0461adf9c267ae263b3742c60fa", - "msg-id": "vdkGm4dtRNmhQ5gqUTFBiA", - "is-conv": true, - "is-room": false, - "to": "5541c02ce4b0f83f4d44414e", - "bin": false, - "from-ip": "202.117.15.217" - }, - ... -] -``` - -如需查询某个用户发出的消息,可以调用 `GET /rtm/clients/{client_id}/messages` 这个接口。 -如需查询整个应用的历史消息,可以调用 `GET /rtm/messages` 这个接口 - -### 聊天室-修改消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages/{message_id} -``` - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -message | 必填 | 消息体 -timestamp | 必填 | 消息的时间戳 - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 聊天室-撤回消息 - -该接口要求使用 master key。需要相应 SDK 的支持,具体可参考上面的「修改消息」接口。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages/{message_id}/recall -``` - - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -timestamp | 必填 | 消息的时间戳 - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 删除消息 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'from_client=some-client-id' \ - --data-urlencode 'timestamp=123' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/messages/{message_id} -``` - -注意,该接口仅删除服务端的消息,对客户端无影响。 - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -timestamp | 必填 | 消息的时间戳 - - -返回: - -```json -{} -``` - -### 增加临时性禁言用户 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_id": "some-client-id", "ttl": 50}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/temporary-silenceds -``` - -参数 | 说明 ---- | --- -client_id | 要禁言的 id,字符串 -ttl | 禁言的时间,秒数,最长 24 小时 - -返回 - -```json -{} -``` - -### 移除临时性禁言用户 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'client_id=some-client-id' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/temporary-silenceds -``` - -返回 - -```json -{} -``` - -### 对话权限 - -该功能介绍可参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)中的《权限管理与黑名单》一节。 - -#### 增加权限 - -该接口要求使用 master key。 -每个聊天室最多允许添加 10,000 个永久性禁言用户。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"clientId": "client", "role": "role"}' \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/member-infos -``` - -参数 | 说明 ---- | --- -clientId | 用户 ID,字符串 -role | 角色,可选值 Member、Manager、Owner - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - -#### 删除权限 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/member-infos/{info-id} -``` - -参数 | 说明 ---- | --- -info-id | 该记录对应的 objectId - -返回 - -```json -{} -``` - -#### 更新权限 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"clientId": "client", "role": "role"}' \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/member-infos/{info-id} -``` - -参数 | 说明 ---- | --- -client_id | 要禁言的 id,字符串。可选 -role | 角色,可选值 Member、Manager、Owner。可选 -info-id | 该记录对应的 objectId - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - - -#### 查询权限 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/member-infos -``` - -参数 | 说明 ---- | --- -skip | -limit | 与 skip 联合使用实现分页 -role | 本次查询只希望包含该角色 - -返回 - -```json -{"results": [{"clientId":"client1", "objectId":"5a5d7433c3422b31ed845e76", "role": "Manager"}]} -``` - - -#### 增加永久性禁言用户 - -该接口要求使用 master key。 -每个聊天室最多允许添加 500 个永久性禁言用户。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/permanent-silenceds -``` - -参数 | 说明 ---- | --- -client_ids | 要禁言的 `Client ID` 列表,数组 - -返回 - -```json -{} -``` - -#### 移除永久性禁言用户 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/permanent-silenceds -``` - -返回 - -```json -{} -``` - -#### 查询永久性禁言列表 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/chatrooms/{conv_id}/permanent-silenceds -``` - -参数 | 约束 | 说明 ----|---|--- -limit | 可选 | 与 next 联合使用实现分页,默认 10 -next | 可选 | 第一次查询时返回,后面的查询带着这个参数,实现分页查询 - -返回 - -```json -{"client_ids": ["client1", "client2"]} -``` - - -### 黑名单 - -该功能介绍可参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)中的《权限管理与黑名单》一节。 - -#### 增加聊天室黑名单 - -该接口要求使用 master key。 - -加入黑名单的 client 不允许再加入该聊天室,如果之前在该聊天室中将被移除。每个聊天室最多允许添加 10,000 个黑名单。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/blacklists -``` - -参数 | 说明 ---- | --- -client_ids | 要拉黑的 `Client ID` 列表,数组 - -返回 - -```json -{} -``` - -#### 移除聊天室黑名单 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/blacklists -``` - -返回 - -```json -{} -``` - -#### 查询聊天室黑名单 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/chatrooms/{chatroom_id}/blacklists -``` - -参数 | 约束 | 说明 ----|---|--- -limit | 可选 | 与 next 联合使用实现分页,默认 10 -next | 可选 | 第一次查询时返回,后面的查询带着这个参数,实现分页查询 - -返回 - -```json -{"client_ids": ["client1", "client2"]} -``` - -## 服务号 - -### 创建服务号 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"My First Service-conversation"}' \ - https://{{host}}/1.2/rtm/service-conversations -``` - -对话的字段可参考[即时通讯总览](/v2/sdk/im/guide/overview)的《对话》一节。 - -返回 - -```json -{"objectId":"5a5d7432c3422b31ed845e75", "createdAt":"2018-01-16T03:40:32.814Z"} -``` - -### 查询服务号 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'where={"name": "service"}' \ - --data-urlencode 'skip=1' \ - --data-urlencode 'limit=20' \ - https://{{host}}/1.2/rtm/service-conversations -``` - -参数 | 约束 | 说明 ----|---|--- -skip | 可选 | -limit | 可选 | 与 skip 联合使用实现分页 -where | 可选 | 请参考[数据存储 REST API 使用详解](/v2/sdk/storage/guide/rest)的《查询》一节。 - - -返回 - -```json -{"results":[ - {"name":"My First Service-conversation", - "createdAt":"2018-01-17T04:15:33.386Z", - "updatedAt":"2018-01-17T04:15:33.386Z", - "objectId":"5a5ecde6c3422b738c8779d7"} -]} -``` - -### 更新服务号 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"name":"Updated Service-conversation"}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id} -``` - -返回 - -```json -{"updatedAt":"2018-01-16T03:40:37.683Z", "objectId":"5a5d7433c3422b31ed845e76"} -``` - - -### 删除服务号 - -在 `_Conversation` 表默认 ACL 权限下本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id} -``` - -返回 - -```json -{} -``` - -### 订阅服务号 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_id":"client_id"}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers -``` - -返回 - -```json -{} -``` - -### 取消订阅 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/{client_id} -``` - -返回 - -```json -{} -``` - -### 遍历查询订阅者 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers -``` - -参数 | 约束 | 说明 ----|---|--- -limit | 可选 | 返回条数限制,默认是 50 条,最大 50 条。 -client_id | 可选 | 查询起始订阅者 client id,不填则从订阅者列表起始位置开始遍历。查询结果不会再包含当前指定的订阅者 client id。 - -返回 - -```json -[{"timestamp":1491467841116,"subscriber":"client id 1","conv_id":"55b871"}, - {"timestamp":1491467852768,"subscriber":"client id 2","conv_id":"55b872"}, ...] -``` -其中 timestamp 表示用户订阅系统对话的时间,subscriber 是订阅用户的 client id。如果一次没有获取完,需要从结果列表中取最后一个订阅者的 client id,作为 client_id 参数再次调用本接口以获取下一批订阅者列表。 - - -### 查询订阅者数 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/count -``` - -返回 - -```json -{"count": 100} -``` - -### 给所有订阅者发消息 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/broadcasts -``` - -参数 | 约束 | 说明 ----|---|--- -from_client | 必选 | 消息的发件人 client ID -message | 必选 | 消息体 -push | 可选 | 附带的推送内容,如果设置,所有 iOS 和 Android 用户会收到这条推送通知。字符串或 JSON 对象 - -返回: - -```json -{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151} -``` - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 修改给所有订阅者发送的消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id} -``` - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -message | 必填 | 消息体 -timestamp | 必填 | 消息的时间戳 - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 撤回给所有订阅者发送的消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id}/recall -``` - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -timestamp | 必填 | 消息的时间戳 - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 给任意用户单独发消息 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": ""}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages -``` - -**注意**,由于这个接口的管理性质,当你通过这个接口发送消息时,我们不会检查 **from_client** 是否有权限给这个服务号发送消息,而是统统放行,请谨慎使用这个接口。 -如果你在应用中使用了我们内部定义的富媒体消息格式,在发送消息时 **message** 字段需要遵守相应的格式要求。 - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client Id -to_clients | 必填 | 数组类型,表示接收消息的 client Id 列表,最多能包含 20 个 client Id -message | 必填 | 消息内容(这里的消息内容的本质是字符串,但是我们对字符串内部的格式没有做限定,
    理论上开发者可以随意发送任意格式,只要大小不超过 5 KB 限制即可。) -transient | 可选 | 是否为暂态消息,默认 false -no_sync | 可选 | 默认情况下消息会被同步给在线的 from_client 用户的客户端,设置为 true 禁用此功能。 -push_data | 可选 | 以消息附件方式设置本条消息的离线推送通知内容。如果目标接收者使用的是 iOS 设备并且当前不在线,我们会按照该参数填写的内容来发离线推送。请参看[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)的《离线推送通知》一节。 -priority | 可选 | 定义消息优先级,可选值为 high、normal、low,分别对应高、中、低三种优先级。该参数大小写不敏感,默认为中优先级 normal。本参数仅对暂态消息或聊天室的消息有效,高优先级下在服务端与用户设备的连接拥塞时依然排队。 - -返回说明: - -默认情况下发送消息 API 使用异步的方式,调用后返回消息 id 和接收消息的服务器时间戳,例如 `{"msg-id":"qNkRkFWOeSqP65S9fDyHJw", "timestamp":1495431811151}`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 修改给用户单独发送的消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123, "to_clients":["a","b","c"]}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id} -``` - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -message | 必填 | 消息体 -timestamp | 必填 | 消息的时间戳 -to_clients | 必填 | 数组类型,表示接收目标消息的 client Id 列表,最多能包含 20 个 client Id - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 撤回给用户单独发送的消息 - -该接口要求使用 master key。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "timestamp": 123, "to_clients":["a","b","c"]}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id}/recall -``` - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -timestamp | 必填 | 消息的时间戳 -to_clients | 必填 | 数组类型,表示接收目标消息的 client Id 列表,最多能包含 20 个 client Id - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 删除给用户单独发送的消息 - -本接口要求使用 master key,并且只能删除订阅消息或给用户单独发送的消息,无法删除广播消息。 -广播消息请调用 `DELETE /1.2/rtm/broadcasts/{message_id}` 接口删除。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'from_client=some-client-id' \ - --data-urlencode 'timestamp=123' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/{client_id}/messages/{message_id} -``` - -注意,该接口仅删除服务端的消息,对客户端无影响。 - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -timestamp | 必填 | 消息的时间戳 - -返回: - -```json -{} -``` - -### 查询服务号给某用户发的消息 - -该接口要求使用 master key。查询结果包含服务号发送的订阅广播消息也包含单独发送的消息。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/subscribers/{client_id}/messages -``` - -参数和返回值与查询历史消息接口相同。 - -### 黑名单 - -该功能介绍可参考[即时通讯开发指南第三篇](/v2/sdk/im/guide/senior)中的《权限管理与黑名单》一节。 - -#### 增加服务号黑名单 - -该接口要求使用 master key。 - -加入黑名单的 client 不允许再加入该服务号,如果之前在该服务号中将被移除。每个服务号最多允许添加 10,000 个黑名单。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/blacklists -``` - -参数 | 说明 ---- | --- -client_ids | 要拉黑的 `Client ID` 列表,数组 - -返回 - -```json -{} -``` - -#### 移除服务号黑名单 - -该接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/blacklists -``` - -返回 - -```json -{} -``` - -#### 查询服务号黑名单 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -d '{"client_ids": ["client1", "client2"]}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/blacklists -``` - -参数 | 约束 | 说明 ----|---|--- -limit | 可选 | 与 next 联合使用实现分页,默认 10 -next | 可选 | 第一次查询时返回,后面的查询带着这个参数,实现分页查询 - -返回 - -```json -{"client_ids": ["client1", "client2"]} -``` - -## 用户 - -### 查询在线成员 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"client_ids": ["Tom", "Jerry"]}' \ - https://{{host}}/1.2/rtm/clients/check-online -``` - -参数 | 约束 | 说明 ----|---|--- -client_ids | 必选 | 要查询的 client ID 列表,最多 20 个 - -返回在线的 ID 列表 - -```json -{"results":["client1"]} -``` - -注意,该接口不判断用户是否存在,「用户不存在」视同「用户不在线」。 - -### 查询未读消息数 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'conv_id=...' \ - https://{{host}}/1.2/rtm/clients/{client_id}/unread-count -``` - -参数 | 约束 | 说明 ----|---|--- -conv_id | 可选 | 对话 ID,若不传此参数,查询 client 在所有对话中的未读消息数 - -返回 - -```json -{"count":1} -``` - -### 强制下线 - -该接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"reason": "why"}' \ - https://{{host}}/1.2/rtm/clients/{client_id}/kick -``` - -参数 | 约束 | 说明 ----|---|--- -reason | 可选 | 下线原因,字符串,不超过 20 个字符 - -返回 - -```json -{} -``` - -### 查询订阅的服务号 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -G \ - --data-urlencode 'conv_id=...' \ - --data-urlencode 'timestamp=...' \ - --data-urlencode 'limit=...' \ - --data-urlencode 'direction=...' \ - https://{{host}}/1.2/rtm/clients/{client_id}/service-conversations -``` - -参数 | 约束 | 类型 | 说明 ----|---|---|--- -conv_id | 可选 | 字符串 | 查询起始服务号 id,不填则从订阅列表起始位置开始遍历。查询结果不会再包含本对话 -timestamp | 可选 | 数字 | 查询起始对话被订阅时间。虽然是可选字段但当提供 conv_id 时本字段必填,值必须为订阅 conv_id 参数所指定系统对话的时间,单位是毫秒 -limit | 可选 | 数字 | 返回条数限制,默认是 50 条 -direction | 可选 | 字符串 | 查询结果按时间排序方式,old 表示降序,new 表示升序,默认是 new。使用 old 则先返回最近订阅的对话,使用 new 则先返回最早订阅的对话 - -返回目标用户订阅系统对话的列表: - -```json -[{"timestamp":1482994126561,"subscriber":"XXX","conv_id":"convId1"}, - {"timestamp":1491467945277,"subscriber":"XXX","conv_id":"convId2"}, ...] -``` - -其中 `timestamp` 表示用户订阅系统对话的时间,`subscriber` 是订阅用户的 client id。如果一次没有获取完,需要从结果列表中取最后一个服务号 ID 和订阅时间,分别作为 conv_id 和 timestamp 参数再次调用本接口以获取下一批订阅的系统对话。 - -### 查询用户发送消息 - -该接口要求使用 master key。 -使用这个接口可以查询某 client_id 在单聊、群聊与聊天室里发的消息。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/clients/{client_id}/messages -``` - -参数与返回值可以参考 `GET /1.2/rtm/conversations/{conv_id}/messages` 接口。 - -### 增加黑名单 - -该接口要求使用 master key。 -一个 client 可以把一个 群聊/聊天室/服务号 加入黑名单,这样其他人就无法要求其加入该对话了。 -目前不支持 client 把另一个 client 加入黑名单。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"conv_id": ""}' \ - https://{{host}}/1.2/rtm/clients/{client_id}/blacklists -``` - -参数 | 约束 | 说明 ----|---|--- -conv_id | 必选 | 拉黑的 群聊/聊天室/服务号 ID - -返回 - -```json -{} -``` - -### 移除黑名单 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"conv_id": ""}' \ - https://{{host}}/1.2/rtm/clients/{client_id}/blacklists -``` - -返回 - -```json -{} -``` - -### 查询黑名单 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/clients/{client_id}/blacklists -``` - -参数 | 约束 | 说明 ----|---|--- -limit | 可选 | 与 next 联合使用实现分页,默认 10 -next | 可选 | 第一次查询时返回,后面的查询带着这个参数,实现分页查询 - -返回 - -```json -{"conv_ids":["conv1"], "next":"1"} -``` - -### 获取登录签名 - -本接口可以让使用了 LCUser 的应用方便快捷地实现登录认证。 -登录认证默认关闭,可以进入 **云服务控制台 > 消息 > 即时通讯 > 设置 > 即时通讯选项**,勾选 **登录启用签名认证** 进行开启。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{appkey}}" \ - -G \ - --data-urlencode 'session_token=some-token' \ - https://{{host}}/1.2/rtm/clients/sign -``` - -参数 | 约束 | 说明 ----|---|--- -session_token | 必选 | LCUser 的 sessionToken - -返回 - -```json -{ - "signature":"bc884dbb617aab1efc228229210e487330abfc7d", - "nonce":"akywke3f28", - "client_id":"5fb4ff18d0deed36ea501c8a", - "timestamp":1614237989966 -} -``` - -注意,虽然这是一个 GET 请求,但并不是幂等的,每次调用返回的签名都不相同。 - -为了方便用户进行细粒度控制,实现自定义功能(如黑名单),本接口提供了一个 hook `_rtmClientSign`,在验证 sessionToken 后去调用,传入的参数为 LCUser 构成的 JSON 对象: - -```json -{ - "email": "", - "sessionToken": "", - "updatedAt": "", // 格式:2017-07-11T07:58:10.149Z - "phone": "", - "objectId": "", - "username": "", - "createdAt": "", // 格式:2017-07-11T07:58:10.149Z - "emailVerified": true/false, - "mobilePhoneVerified": true/false -} -``` - -可以返回两类结果: - -```json -{"result": true} // 允许签名 -{"result": false, "error": "error message"} // 拒绝签名 -``` - -## 全局 API - -### 查询用户数 - -本接口会返回应用当前在线用户总数,以及当天有登录记录的独立用户总数。本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/stats -``` - -返回 - -```json -{"result":{"online_user_count":10212,"user_count_today":1002324}} -``` - -其中 `online_user_count` 表示当前应用在线用户总数,`user_count_today` 表示当天有登录记录的独立用户总数。 - -### 查询所有对话 - -本接口会返回所有的 单聊群聊/聊天室/服务号。在 `_Conversation` 表默认 ACL 权限下要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/all-conversations -``` - -参数 | 约束 | 说明 ----|---|--- -skip | 可选 | -limit | 可选 | 与 skip 联合使用实现分页 -where | 可选 | 参考《存储 REST API 指南》中的《查询》一节 - -返回 - -```json -{"results":[ - {"name":"conversation", - "createdAt":"2018-01-17T04:15:33.386Z", "updatedAt":"2018-01-17T04:15:33.386Z", - "objectId":"5a5ecde6c3422b738c8779d7"} -]} -``` - -### 全局广播 - -该接口可以给该应用所有 client 广播一条消息,每天最多 30 条。本接口要求使用 master key。 - -```sh -curl -X POST \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "1a", "message": "{\"_lctype\":-1,\"_lctext\":\"这是一个纯文本消息\",\"_lcattrs\":{\"a\":\"_lcattrs 是用来存储用户自定义的一些键值对\"}}", "conv_id": "..."}' \ - https://{{host}}/1.2/rtm/broadcasts -``` - -参数 | 约束 | 类型 | 说明 ----|---|---|--- -from_client | 必填 | 字符串 | 消息的发件人 id -conv_id | 必填 | 字符串 | 发送到对话 id,仅限于服务号 -message | 必填 | 字符串 | 消息内容(这里的消息内容的本质是字符串,但是我们对字符串内部的格式没有做限定,理论上开发者可以随意发送任意格式,只要大小不超过 5 KB 限制即可。) -valid_till | 可选 | 数字 | 过期时间,UTC 时间戳(毫秒),最长为 1 个月之后。默认值为 1 个月后。 -push | 可选 | 字符串或 JSON 对象 | 附带的推送内容,如果设置,**所有** iOS 和 Android 用户会收到这条推送通知。 -transient | 可选 | 布尔值 | 默认为 false。该字段用于表示广播消息是否为暂态消息,暂态消息只会被当前在线的用户收到,不在线的用户再次上线后也收不到该消息。 - -Push 的格式与《推送 REST API 指南》的《消息内容 Data》一节中 `data` 下面的部分一致。如果您需要指定开发证书推送,需要在 push 的 json 中设置 `"_profile": "dev"`,例如: - -```json -{ - "alert": "消息内容", - "category": "通知分类名称", - "badge": "Increment", - "_profile": "dev" -} -``` - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 修改广播消息 - -该接口要求使用 master key。 - -广播消息修改仅对当前还未收到该广播消息的设备生效,如果目标设备已经收到了该广播消息则无法修改。请慎重发送广播消息。 - -```sh -curl -X PUT \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - -H "Content-Type: application/json" \ - -d '{"from_client": "", "message": "", "timestamp": 123}' \ - https://{{host}}/1.2/rtm/service-conversations/{conv_id}/messages/{message_id} -``` - -参数 | 约束 | 说明 ----|---|--- -from_client | 必填 | 消息的发件人 client ID -message | 必填 | 消息体 -timestamp | 必填 | 消息的时间戳 - -成功则返回状态码 `200 OK`。 - -频率限制: - -此接口受频率限制,详见后文[接口请求频率限制](#接口请求频率限制)一节。 - -### 删除广播消息 - -调用此 API 将删除已发布的广播消息,仅对还未收到广播消息的设备生效,已收到广播消息的设备无法删除消息。本接口要求使用 master key。 - -```sh -curl -X DELETE \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/broadcasts/{message_id} -``` - -参数 | 约束 | 说明 ---- | --- | --- -message_id | 必填 | 要删除的消息 id,字符串 - -成功则返回状态码 `200 OK`。 - -### 查询广播消息 - -调用此 API 可查询目前有效的广播消息。本接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/broadcasts?conv_id={conv_id} -``` - -参数 | 约束 | 说明 ---- | --- | --- -conv_id | 必填 | 服务号 id -limit | 可选 | 返回消息条数 -skip | 可选 | 跳过消息条数,用于翻页 - -### 查询应用内所有历史消息 - -该接口要求使用 master key。 - -```sh -curl -X GET \ - -H "X-LC-Id: {{appid}}" \ - -H "X-LC-Key: {{masterkey}},master" \ - https://{{host}}/1.2/rtm/messages -``` - -参数与返回值可以参考 `GET /1.2/rtm/conversations/{conv_id}/messages` 接口。 - -## 接口请求频率限制 - -本文档中和消息操作有关的 REST API 有请求频率以及总量的限制(**即时通讯客户端 SDK 的 API 不受此限制影响**),具体如下: - -### 普通消息 - -1.1 版本: - -* 发送消息、系统对话给用户发消息(`/1.1/rtm/messages`) -* 修改与撤回消息 (`/1.1/rtm/patch/message`) - -1.2 版本: - -* [单聊、群聊-发消息](#单聊、群聊-发消息) -* [单聊、群聊-修改消息](#单聊、群聊-修改消息) -* [单聊、群聊-撤回消息](#单聊、群聊-撤回消息) -* [聊天室-发消息](#聊天室-发消息) -* [聊天室-修改消息](#聊天室-修改消息) -* [聊天室-撤回消息](#聊天室-撤回消息) -* [服务号-给任意用户单独发消息](#给任意用户单独发消息) -* [服务号-修改给用户单独发送的消息](#修改给用户单独发送的消息) -* [服务号-撤回给用户单独发送的消息](#撤回给用户单独发送的消息) - -#### 限制 - -| 商用版(每应用) | 开发版(每应用)| -|----------|---------------| -| 最大 9000 次/分钟,默认 1800 次/分钟 |120 次/分钟 | - -所有接口共享额度。超过额度限制后一分钟内 LeanCloud 会拒绝请求持续返回 429 错误码,一分钟后会重新处理请求。 - -商用版应用的上限可以在 **云服务控制台 > 消息 > 即时通讯 > 设置 > 服务阈值 > 普通消息 API 调用频率上限** 修改。 -按照每日调用频率峰值实行阶梯收费,如下表所示: - -| 每分钟调用频率 | 费用 | -| - | - | -| 0 ~ 1800 | 免费 | -| 1801 ~ 3600 | ¥20 元 / 天 | -| 3601 ~ 5400 | ¥30 元 / 天 | -| 5401 ~ 7200 | ¥40 元 / 天 | -| 7201 ~ 9000 | ¥50 元 / 天 | - -国际版 - -| 每分钟调用频率 | 费用 | -| - | - | -| 0 ~ 1800 | 免费 | -| 1801 ~ 3600 | $6 USD / 天 | -| 3601 ~ 5400 | $9 USD / 天 | -| 5401 ~ 7200 | $12 USD / 天 | -| 7201 ~ 9000 | $15 USD / 天 | - -每日调用频率峰值可以在 **控制台 > 消息 > 即时通讯 > 统计 > API 请求频率峰值** 中查看。 - -### 订阅消息 - -1.1 版本: - -* 系统对话发送订阅消息 (`/1.1/rtm/broadcast/subscriber`) - -1.2 版本: - -* [给所有订阅者发消息](#给所有订阅者发消息) -* [修改给所有订阅者发送的消息](#修改给所有订阅者发送的消息) -* [撤回给所有订阅者发送的消息](#撤回给所有订阅者发送的消息) - -#### 限制 - -| 限制 | 商用版 | 开发版 | -|----------|----------|---------------| -| 频率限制 | 每应用 30 次/分钟 | 每应用 10 次/分钟 | -| 总量限制 | 全天最多 1000 次 | 全天最多 100 次 | - -所有接口共享额度。超过频率限制后 1 分钟内云端会拒绝请求持续返回 429 错误码,一分钟后会重新处理请求;超过总量限制后当天会拒绝之后的所有请求并返回 429 错误码。 - -### 广播消息 - -1.1 版本: - -* 发送广播消息 (`/1.1/rtm/broadcast`) - -1.2 版本: - -* [全局广播](#全局广播) -* [修改广播消息](#修改广播消息) - -#### 限制 - -| 限制 | 商用版 | 开发版 | -|----------|----------|---------------| -| 频率限制 | 每应用 10 次/分钟 | 每应用 1 次/分钟 | -| 总量限制 | 全天最多 30 次 | 全天最多 10 次 | - -所有接口共享以上额度。超过频率限制后 1 分钟内云端会拒绝请求持续返回 429 错误码,一分钟后会重新处理请求;超过总量限制后当天会拒绝之后的所有请求并返回 429 错误码。 diff --git a/versioned_docs/version-v2/sdk/10-im/02-guide/_category_.json b/versioned_docs/version-v2/sdk/10-im/02-guide/_category_.json deleted file mode 100644 index 586e68f44..000000000 --- a/versioned_docs/version-v2/sdk/10-im/02-guide/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true -} diff --git a/versioned_docs/version-v2/sdk/10-im/_category_.json b/versioned_docs/version-v2/sdk/10-im/_category_.json deleted file mode 100644 index c7a4fa101..000000000 --- a/versioned_docs/version-v2/sdk/10-im/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "即时通讯", - "collapsed": true -} diff --git a/versioned_docs/version-v2/tap-download.mdx b/versioned_docs/version-v2/tap-download.mdx deleted file mode 100644 index 6ae7bfab1..000000000 --- a/versioned_docs/version-v2/tap-download.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: tap-download -title: 资源下载 -sidebar_label: 资源下载 -slug: /tap-download ---- - -## SDK - -- [Unity](https://github.com/taptap/TapSDK-Unity/releases/tag/2.1.8) -- [Android](https://github.com/taptap/TapSDK-Android/releases/tag/v2.1.8) -- [iOS](https://github.com/taptap/TapSDK-iOS/releases/tag/v2.1.7) - -## Demo - -- [Unity](https://github.com/taptap/TapSDK-Unity-Demo/tree/ef42cb92f4513c558628531a96aa5eefa40576a6) -- [Android](https://github.com/taptap/TapSDK-Android/tree/83dc40c35505da1df54f5b0b8548f67a2d8e89ed) -- [iOS](https://github.com/taptap/TapSDK-iOS/tree/bdcc72154c6d5e6081676c210a440baf51ce22aa) - -## 登录按钮素材 - -点击下载 [icon.zip](https://capacity-files.lcfile.com/z7xSKYDvAc1ff19cDfq3Vx01v50KNR6j/TapTapLoginButton.zip) \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/_partials/android-package-visibility.mdx b/versioned_docs/version-v4/sdk/_partials/android-package-visibility.mdx deleted file mode 100644 index 02f52effc..000000000 --- a/versioned_docs/version-v4/sdk/_partials/android-package-visibility.mdx +++ /dev/null @@ -1,38 +0,0 @@ -Android 11(API level 30)之后加强了隐私保护策略,引入了大量变更和限制,其中一个重要变更——[软件包可见性](https://developer.android.com/about/versions/11/privacy/package-visibility),将会导致第三方应用无法拉起 TapTap 客户端,从而影响 TapTap 相关功能的正常使用,包括但不限于更新唤起 TapTap、购买验证等功能。 - -如果没有完成适配,Android 版本为 11 及更高版本的客户端打开游戏会提示「本游戏需要最新版 TapTap 服务支持」,无法正常进入游戏。异常呈现如下图所示: - -![图片描述](/img/android-package-visibility-android11.png) - -对此提供如下两种适配方案: - -**方案一:** - -编译时将 `targetSdkVersion` 改为 29(目前设置成 >= 30 会触发该问题)。 - -**方案二:** - -1. 将 gradle build tools 改为 4.1.0+: - - ```java - classpath 'com.android.tools.build:gradle:4.1.0' - ``` - -2. 在 AndroidManifest.xml 里添加 `` 标签中的内容: - - ```xml - - - - - - - - - - - - ``` diff --git a/versioned_docs/version-v4/sdk/_partials/languages.mdx b/versioned_docs/version-v4/sdk/_partials/languages.mdx deleted file mode 100644 index 9371d7fc6..000000000 --- a/versioned_docs/version-v4/sdk/_partials/languages.mdx +++ /dev/null @@ -1,192 +0,0 @@ -import MultiLang from "/src/docComponents/MultiLang"; - - - -<> - -```cs -TapTapSDK.UpdateLanguage(TapTapLanguageType.Auto); -``` - -支持如下语言: - -```cs -namespace TapSDK.Core -{ - public enum TapTapLanguageType - { - Auto = 0,// 自动 - zh_Hans,// 简体中文 - en,// 英文 - zh_Hant,// 繁体中文 - ja,// 日文 - ko,// 韩文 - th,// 泰文 - id,// 印度尼西亚语 - de,// 德语 - es,// 西班牙语 - fr,// 法语 - pt,// 葡萄牙语 - ru,// 俄罗斯语 - tr,// 土耳其语 - vi// 越南语 - } -} -``` - - - -<> - -```kotlin -TapTapSdk.setPreferredLanguage(TapTapLanguage.AUTO) -``` - -支持如下语言: - -```kotlin - /** - * 自动 - */ - AUTO("auto"), - - /** - * 简体中文 - */ - ZH_HANS("zh_CN"), - - /** - * 英语(美国) - */ - EN("en_US"), - - /** - * 繁体中文 - */ - ZH_HANT("zh_TW"), - - /** - * 日文 - */ - JA("ja_JP"), - - /** - * 韩文 - */ - KO("ko_KR"), - - /** - * 泰语 - */ - TH("th_TH"), - - /** - * 印尼语 - */ - ID("id_ID"), - - /** - * 德语 - */ - DE("de"), - - /** - * 西班牙语 - */ - ES("es_ES"), - - /** - * 法语 - */ - FR("fr"), - - /** - * 葡萄牙语 - */ - PT("pt_PT"), - - /** - * 俄语 - */ - RU("ru"), - - - /** - * 土耳其语 - */ - TR("tr"), - - /** - * 越南语 - */ - VI("vi_VN"); -``` - - - -<> - -```swift -TapTapSDK.update(TapLanguageType.auto) -``` - -支持如下语言: - -```objc -typedef NS_ENUM (NSInteger, TapLanguageType) { - TapLanguageType_Auto = 0,// 自动 - TapLanguageType_zh_Hans,// 简体中文 - TapLanguageType_en,// 英文 - TapLanguageType_zh_Hant,// 繁体中文 - TapLanguageType_ja,// 日文 - TapLanguageType_ko,// 韩文 - TapLanguageType_th,// 泰文 - TapLanguageType_id,// 印度尼西亚语 - TapLanguageType_de,// 德语 - TapLanguageType_es,// 西班牙语 - TapLanguageType_fr,// 法语 - TapLanguageType_pt,// 葡萄牙语 - TapLanguageType_ru,// 俄罗斯语 - TapLanguageType_tr,// 土耳其语 - TapLanguageType_vi,// 越南语 -}; -``` - - - -<> - -```cpp -TapUECommon::SetLanguage(ELanguageType::AUTO); -``` - -支持如下语言: - -```cpp -UENUM(BlueprintType) -enum class ELanguageType : uint8 -{ - AUTO = 0, // 自动 - ZH, // 简体中文 - EN, // 英文,海外默认语言 - ZHTW, // 繁体中文 - JA, // 日语 - KO, // 韩语 - TH, // 泰文 - ID, // 印尼文 - DE, // 德语 - ES, // 西班牙语 - FR, // 法语 - PT, // 葡萄牙语 - RU, // 俄语 - TR, // 土耳其语 - VI, // 越南语 -}; -``` - - - - - -「自动」会尝试根据系统语言设置语言,如果系统语言不在上述支持的语言之中,那么会根据 [SDK 初始化时配置的区域](/sdk/start/quickstart/#初始化)设置语言。 -区域为中国大陆时会设置为简体中文,否则会设置为英文。 diff --git a/versioned_docs/version-v4/sdk/_partials/setup-domain.mdx b/versioned_docs/version-v4/sdk/_partials/setup-domain.mdx deleted file mode 100644 index a09a82153..000000000 --- a/versioned_docs/version-v4/sdk/_partials/setup-domain.mdx +++ /dev/null @@ -1,64 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - - - - - -使用 TDS 提供的云服务,初始化客户端 SDK 需要在 `server_url` 处填入 API 域名,可前往 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置** 获取 TDS 提供的 **共享域名**。 - - - - - -使用 TDS 提供的云服务需要绑定 **API 自定义域名**,以便和其他厂商的应用隔离入口,避免其他应用受到 DDoS 攻击时相互牵连。 - -初始化客户端 SDK 时 `server_url` 处填入的就是 API 域名。 - -### 绑定 API 域名 - -绑定 API 域名的前提是,你拥有一个**已经完成备案的域名**。 - -
    -点击查看 API 域名绑定步骤 - -假设你的域名为 `example.com`,API 域名绑定步骤和状态如下: - -![domain guide](https://capacity-files.lcfile.com/RonCpipde80meo5BL8fxrjHTee39Wit6/domain-guide.png) - -- 进入 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置 > API** ,点击 **「绑定新域名」** 按钮。API 域名不支持直接绑定裸域名,需要在主域名的前面添加自定义名称,也就是绑定一个子域名,比如这里你可以绑定 `api.example.com`。 -- 控制台显示 **「正在检查备案信息」**,请等待一会儿。 -- 如果域名没有完成备案,将会显示 **「绑定失败」**。 -- 域名备案检查通过,域名下方显示 **「请配置 DNS」**。 -- 此时需要到你的域名服务商控制台,进入域名解析设置页面,添加一条记录,记录类型为 A(A 记录可以将域名指向一个 IP 地址),请将前面填到开发者后台的自定义域名和 **「推荐 DNS 配置」** 下方 A 记录值复制到对应位置。 -- DNS 解析记录和证书申请(如果选择了自动管理 SSL 证书)都需要一定时间,请耐心等待。记录生效后,控制台便会显示 **「已绑定」**。 - -
    - -绑定成功后,初始化 SDK 时请传入绑定的自定义域名 `https://api.example.com`。这里用的是示例,请换成你自己绑定的 API 域名,注意不要遗漏前面的 `https://`。 - -配置域名需要一定的时间,TDS 为开发者提供了 **共享域名** 在游戏测试时使用,但共享域名没有可用性保证,容易受到 DDoS 攻击影响。游戏上线前,一定要确认使用的 API 访问域名是开发者自己绑定的域名,请勿将共享域名用于生产环境。 - -### 绑定文件域名 - -如果使用了数据存储中的文件服务,需要绑定**文件访问域名**。 - -可前往 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置 > 域名配置 > 文件** 绑定文件域名,步骤和 API 自定义域名基本相同。但有两点不一样: - -1. API 域名解析使用 A 记录,文件域名解析使用 CNAME 记录。文件域名同样不支持绑定裸域名,需要绑定子域名。例如你的主域名是 `example.com`,可以绑定文件域名为 `files.example.com`。 -2. 绑定成功后,还需在 **开发与构建 > 数据存储 > 文件 > 设置 > 文件访问地址** 点击「修改」按钮进行切换。 - -:::info - -每个子域名只能绑定到一个游戏,且 API 域名和文件域名不可使用同样的子域名。如果你已经在 TDS 控制台绑定了某个子域名,重复绑定时控制台会显示「该域名已经被其他应用所绑定」,此时可以更换同一主域名下不同的子域名,来完成后续绑定步骤。 - -::: - -
    - -
    - - - -请参考 [域名绑定指南](/sdk/domain/guide/)。 - - diff --git a/versioned_docs/version-v4/sdk/_partials/tap-login-profile.mdx b/versioned_docs/version-v4/sdk/_partials/tap-login-profile.mdx deleted file mode 100644 index 73f7b7d3f..000000000 --- a/versioned_docs/version-v4/sdk/_partials/tap-login-profile.mdx +++ /dev/null @@ -1,36 +0,0 @@ -import { Conditional } from "/src/docComponents/conditional"; - -这里获取的 `Profile` 类根据游戏申请的[授权范围](/sdk/taptap-login/guide/start/#不同的授权范围)有所差异。 - -其中可能会包含如下信息: - - - -| 参数 | 说明 | -| --------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `name` | 玩家在 TapTap 平台的昵称 | -| `avatar` | 玩家在 TapTap 平台的头像 url | -| `openid` | 通过用户信息和游戏信息生成的**用户唯一标识**,每个玩家在每个游戏中的 `openid` 都是不一样的 | -| `unionid` | 通过用户信息加上厂商信息生成的用户唯一标识,一个玩家在同一个厂商的所有游戏中 `unionid` 都是一样的,不同厂商下 `unionid` 不同 | -| `email` | 用户在 TapTap 平台注册使用的邮箱 | -| `emailVerified` | 用户在 TapTap 平台注册使用的邮箱是否经过验证 | - - - - - -| 参数 | 说明 | -| --------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `name` | 玩家在 TapTap 平台的昵称 | -| `avatar` | 玩家在 TapTap 平台的头像 url | -| `openid` | 通过用户信息和游戏信息生成的**用户唯一标识**,每个玩家在每个游戏中的 `openid` 都是不一样的 | -| `unionid` | 通过用户信息加上厂商信息生成的用户唯一标识,一个玩家在同一个厂商的所有游戏中 `unionid` 都是一样的,不同厂商下 `unionid` 不同 | - - - -`openid` 和 `unionid` 使用[标准的 Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4)(带 Padding)编码,包含的字符有 `A-Za-z0-9+/=`。`openid` 和 `unionid` 长度最大值为 50 个字符。 - -:::info -由于 `unionid` 与游戏所属厂商有强关联性,因此 `unionid` 适用于如下场景:厂商使用测试服进行付费删档等测试,正式服需要对于之前参与测试的老玩家进行返利等操作。因为同一个玩家在同一个厂商下的所有游戏中的 `unionid` 不变。 -一个游戏在厂商转移后同一个用户的 `unionid` 会发生改变,如果游戏使用了 `unionid`,TDS 技术支持会在转移前通过工单和游戏开发者确认相关数据的处理方案,保证迁移前后用户数据不错乱。 -::: diff --git a/versioned_docs/version-v4/sdk/_partials/unity-sdk-installation.mdx b/versioned_docs/version-v4/sdk/_partials/unity-sdk-installation.mdx deleted file mode 100644 index 604104d67..000000000 --- a/versioned_docs/version-v4/sdk/_partials/unity-sdk-installation.mdx +++ /dev/null @@ -1,109 +0,0 @@ -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "../../sdkVersions"; - -SDK 需**通过 Unity Package Manager 导入**,具体设置如下: - -#### 1. 添加外部依赖 - -SDK 使用的 JSON 解析库为 `Newtonsoft-json`,如果当前工程已接入该依赖库,则不需额外处理,否则需在 `Packages/manifest.json` 添加如下依赖: - -```json -"com.unity.nuget.newtonsoft-json":"3.2.1" -``` - -SDK 使用 `com.google.external-dependency-manager` -管理 Android、iOS 依赖,如果当前工程已接入该依赖库,则不需额外处理,否则需在 `Packages/manifest.json` 添加如下依赖: - -```json -{ - "dependencies": { - "com.google.external-dependency-manager": "1.2.179" - }, - "scopedRegistries": [ - { - "name": "package.openupm.com", - "url": "https://package.openupm.com", - "scopes": [ - "com.google.external-dependency-manager" - ] - } - ] -} -``` - -#### 2. 添加 SDK 依赖 - -TapSDK 使用 NPMJS 安装,优势是只需要配置版本号,并且支持嵌套依赖。 - - -在项目的 `Packages/manifest.json` 文件中添加以下依赖: - - - - {`"dependencies":{ - ${props.npmDeps.map(dep => `"${dep}":"${sdkVersions.taptap.unity}",`).join('\n ')} -}`} - - - -但需要注意的是,需在 `Packages/manifest.json` 中 `dependencies` 同级下声明 `scopedRegistries`: - - -```json -"scopedRegistries":[ - { - "name": "NPMJS", - "url": "https://registry.npmjs.org/", - "scopes": ["com.taptap"] - } -] -``` - - -[//]: # (##### GitHub 安装) - -[//]: # () -[//]: # () -[//]: # (在项目的 `Packages/manifest.json` 文件中添加以下依赖:) - -[//]: # () -[//]: # () -[//]: # () - -[//]: # () -[//]: # ( {`"dependencies":{) - -[//]: # () -[//]: # ( ${props.githubDeps.map(dep => `"${dep.package}":"${dep.url}",`).join('\n ')} ) - -[//]: # () -[//]: # ( }`}) - -[//]: # () -[//]: # () - -[//]: # () -[//]: # () -[//]: # (在 Unity 顶部菜单中选择 **Window > Package Manager** 可查看已经安装在项目中的包。) - - - diff --git a/versioned_docs/version-v4/sdk/access/_category_.json b/versioned_docs/version-v4/sdk/access/_category_.json deleted file mode 100644 index 2a00c2715..000000000 --- a/versioned_docs/version-v4/sdk/access/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发准备", - "collapsed": true, - "position": 1 -} diff --git a/versioned_docs/version-v4/sdk/access/android-md5.mdx b/versioned_docs/version-v4/sdk/access/android-md5.mdx deleted file mode 100644 index 663c89162..000000000 --- a/versioned_docs/version-v4/sdk/access/android-md5.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Android 平台获取 MD5 -sidebar_position: 3 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -### 如何获取 Android 应用的 MD5 值? - -#### 一、通过 APP 工具获取 - -当只有 APK 文件包时,为了正确填写签名 MD5 值,可以使用如下的工具进行获取:[**GenSignatureMD5**](https://capacity-files.lcfile.com/vW65JxH2b2KwDS8JcbVUfiwLHSeHTlD5/tds_getsign.apk),工具使用方式:使用正式的签名证书对游戏应用进行签名打包,然后将 APK 包安装到手机上。与此同时,将 GenSignatureMD5 工具也安装到同一部手机上,然后打开该工具输入游戏包名就可以得到签名 MD5 值。 - -#### 二、通过 Android Studio 获取 - -通过 Android Studio Terminal 输入以下命令获取: - -```sh -keytool -exportcert -alias {alias} -keystore {storefile} | openssl dgst -md5 -``` -或者使用如下命令获取: - -```sh -./gradlew signingReport -``` - - -就可以在命令窗口看到签名文件的信息,包括了 SHA1 值和 MD5 值。 - -除了以上方法还可以使用 Android Studio 自带的 Gradle Tasks 查看,双击下图中的 signingReport 后调试窗口会输出 MD5 值。 - -![](https://capacity-files.lcfile.com/y7fcVDW6cUFKfG4ATDXj8KKE9L2jWprB/%E8%8E%B7%E5%8F%96MD5%E7%A4%BA%E4%BE%8B1.jpeg) - -:::caution - -注意,运行 signingReport 调试窗口输出的 MD5 值带冒号分隔符,绑定到开发者中心时需要手动删除冒号。 -> TapTap 开发者中心绑定 MD5 格式举例: -> -> 正确格式:6EB4347CF9C098BE1C8D965D539C42E2 -> -> 错误格式:6E:B4:34:7C:F9:C0:98:BE:1C:8D:96:5D:53:9C:42:E2 - -::: - -如果右侧 Gradle 面板没有 Gradle Tasks 选项卡,在设置中关掉下图所示选项,重新 Sync Gradle,即可看到 Gradle Tasks 选项卡。 - -![](https://capacity-files.lcfile.com/8dEVF81X34JFtUE50tnqj6OIoxxDdXsU/%E8%8E%B7%E5%8F%96MD5%E7%A4%BA%E4%BE%8B2.jpeg) diff --git a/versioned_docs/version-v4/sdk/access/get-ready.mdx b/versioned_docs/version-v4/sdk/access/get-ready.mdx deleted file mode 100644 index 7a5887e89..000000000 --- a/versioned_docs/version-v4/sdk/access/get-ready.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: 开发者中心配置 -sidebar_position: 0 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -为了能使用 TapTap 开发者服务(TapTap Developer Services,简称 TDS 服务),你需要完成前期的配置工作。 - -## 创建应用 - -在使用 TDS 服务之前,你需要创建一个应用,来完成对接前的准备工作。创建应用请参考[商店指南](/store/)。 - -## 开启应用配置 - -依次进入 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 应用配置**,点击「立即开启」,获得当前应用的基本信息。 - -### 基本信息 - -`Client ID` 是一个应用实体包在 TapTap 开发者中心的唯一身份标识,TapTap 通过 `Client ID` 来鉴别应用的身份。每个应用仅能拥有一个 `Client ID`,如同一个应用区分测试服与正式服,需要创建两个不同的应用,分别开启应用配置。 - -### 适用地区 - - - -一个 client 仅能对应一个地区。这是由于在 TapTap 的账号系统内,将中国大陆用户与全球用户做了隔离区分,互不相通。 - - - - - -适用于中国大陆以外的国家和地区。 - -![](https://capacity-files.lcfile.com/SaYP7m4TEQQTpuvy5n2r0GjxAzgim624/io_tap_get_ready.png) - - - - - -## 隐私声明 - -集成账号服务的功能,需要先签订[《TapTap 平台开发者协议》](/store/store-devagreement/)。使用 TDS 服务,视为你同意前述所有协议,且你将基于这些协议承担相应的法律责任与义务。 - - -## 配置签名证书 - -为了更高的安全性,TapTap 登录服务需要校验你的游戏。你需要提交游戏的 package name(Android 包名)、Bundle ID(iOS 包名)以及 Android 签名。 - -:::tip - -1. Android 的包名请使用符合 Android 规范的命名方式。参考文档:[Android 开发者 - 设置应用 ID](https://developer.android.com/studio/build/application-id) - -2. Android 的签名为 Keystore 文件中的 MD5 字符串(32 位),填入时请去除特殊符号。 - -3. iOS 的 Bundle ID 请使用符合苹果规范的命名方式。参考文档:[Property List Key - CFBundle 标识符](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier) - -::: diff --git a/versioned_docs/version-v4/sdk/access/quickstart.mdx b/versioned_docs/version-v4/sdk/access/quickstart.mdx deleted file mode 100644 index 799c978a4..000000000 --- a/versioned_docs/version-v4/sdk/access/quickstart.mdx +++ /dev/null @@ -1,305 +0,0 @@ ---- -title: TapSDK集成 -sidebar_label: TapSDK 集成 -sidebar_position: 1 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import { Conditional } from "/src/docComponents/conditional"; -import sdkVersions from '../../sdkVersions'; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -本文介绍如何快速接入 TapSDK 。 - -## 环境要求 - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - -<> - -- Android 5.0(API level 21)或更高版本 - - -<> - -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - -<> - -* 安装 UE 4.26 及以上版本 -* iOS 12 或更高版本 -* Android 5.0(API level 21)或更高版本 - -**支持平台**:Android / iOS - - - - - - -## 项目配置 - - -<> - - - - -<> - -1. 项目根目录的 build.gradle 添加仓库地址: - - -{ -`allprojects { - repositories { - google() - mavenCentral() - } -}`} - - -2. app module 的 build.gradle 添加对应模块依赖(如:登录以及内嵌动态): - - -{ -`dependencies { - implementation 'com.taptap.sdk:tap-core:${sdkVersions.taptap.android}' - implementation 'com.taptap.sdk:tap-login:${sdkVersions.taptap.android}' // 登录 - implementation 'com.taptap.sdk:tap-moment:${sdkVersions.taptap.android}' // 内嵌动态 -}` -} - - - -3. 在 `AndroidManifest.xml` 添加网络权限: - - -{ -`` -} - - -4. 旧版 Android 额外配置 - - 如果 `targetSdkVersion < 29`,还需要添加如下配置: - - - `manifest` 节点添加 `xmlns:tools="http://schemas.android.com/tools"` - - `application` 节点添加 `tools:remove="android:requestLegacyExternalStorage"` - - -<> - -#### 导入 SDK - -iOS 提供通过添加 cocosPod 远程依赖和使用本地文件导入两种集成方式,推荐使用远程依赖方式。 - -##### 远程依赖 - -1. 在工程 Podfile 文件中对应模块下添加依赖: - -{` pod 'TapTapCoreSDK', '~> ${sdkVersions.taptap.ios }'`} - -2. 执行 `Pod install` 下载对应依赖文件 - -##### 本地文件依赖 - -1. 在下载页下载如下文件: - -- `tapsdkcorecpp.xcframework` 基础库 -- `TapTapBasicToolsSDK.xcframework` 基础库 -- `TapTapCoreSDK.xcframework` 核心库 -- `TapTapGidSDK.xcframework` 基础库 -- `TapTapNetworkSDK.xcframework` 基础库 -- `THEMISLite.xcframework` 基础库 - - -2. 在工程中添加 `framework` 静态库,注意添加时选择 Embed 方式为 **Do Not Embed** -3. SDK 内部使用了 [`Protobuf` 依赖库](https://cocoapods.org/pods/Protobuf),开发者应提前通过远程或文件导入方式添加对应依赖。 - -#### 配置编译选项 - -- 在 Build Setting 中的 Other Link Flag 中添加 `-ObjC` 和 `-Wl -ld_classic`。 - -- 在 Build Setting 中的 Always Embed Swift Standard Libraries 设置为 YES,即始终引入 Swift 标准库,避免 App 启动时报错「无法找到 Swift 标准库之类」。如果项目中找不到,可以建立一个空 Swift 文件,Xcode 会自动建立桥接关系。 - -- 在 Build Setting 中的 Swift Compiler - Language/Swift Language Version 选择 Swift 5。 - - - - - -## 初始化 - -初始化 TapSDK 时需传入 `Client ID`、区域等应用配置信息。 - -新版 TapSDK 提供统一初始化,业务模块(如:成就 登录等)无需单独初始化。 - - - -<> - -```cs -using TapSDK.Core; - -// 核心配置 -TapTapSdkOptions coreOptions = new TapTapSdkOptions -{ - // 客户端 ID,开发者后台获取 - clientId = clientId, - // 客户端令牌,开发者后台获取 - clientToken = clientToken, - // 地区,CN 为国内,Overseas 为海外 - region = TapTapRegionType.CN, - // 语言,默认为 Auto,默认情况下,国内为 zh_Hans,海外为 en - preferredLanguage = TapTapLanguageType.zh_Hans, - // 是否开启日志,Release 版本请设置为 false - enableLog = true -}; -// TapSDK 初始化 -TapTapSDK.Init(coreOptions); - - -// 当需要添加其他模块的初始化配置项,例如合规认证、成就等, 请使用如下 API -TapTapSdkBaseOptions[] otherOptions = new TapTapSdkBaseOptions[] -{ - // 其他模块配置项 -}; -TapTapSDK.Init(coreOptions, otherOptions); - -``` - - -<> - -**请确保 TapSDK 的初始化在主线程(UI 线程)中执行。** - -```kotlin -TapTapSdk.init( - this, - TapTapSdkOptions( - clientId, // 游戏client id - clientToken, // 游戏client token - region, // 游戏可玩区域: [TapTapRegion.CN]=国内 [TapTapRegion.GLOBAL]=海外 - gameVersion, // 游戏版本号 - enableLog, // 是否开启 log,建议 Debug 开启,Release 关闭,默认关闭 log - preferredLanguage, // 多语言设置,默认为中文 - ), - TapTapAchievementOptions(enableToast = true), // 成就初始化配置 - TapTapComplianceOptions(showSwitchAccount = true, useAgeRange = true) // 合规认证初始化配置 -) -``` - - -<> - -```swift -import TapTapCoreSDK - -let options = TapTapSdkOptions() -options.clientId = "your_client_id" // 必须,开发者中心对应 Client ID -options.clientToken = "your_client_token" // 必须,开发者中心对应 Client Token -options.region = .CN // .CN:中国大陆,.overseas:其他国家或地区 -options.enableLog = true // 是否开启 log,建议 Debug 开启,Release 关闭,默认关闭 log -options.preferredLanguage = TapLanguageType.auto // 语言设置,默认跟随系统,当系统语言不支持时,国内为中文,海外为英文 - -// 初始化 SDK -TapTapSDK.initWith(options) - - -// 当需要添加其他模块的初始化配置项,例如合规认证、成就等,可调用如下 API -var otherOptions:[TapTapSdkBaseOptions] = [] - -// 添加其他模块初始化配置项 -// otherOptions.append(moduleOptions) moduleOptions 为其他模块初始化配置项 - -// 初始化 SDK -TapTapSDK.initWith(options, otherOptions: otherOptions) - -``` - - - - -初始化的时候,**必须填入** `client_id` 以及 `client_token` - -- `client_id`、`client_token` 信息可在 **开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 查看。 - -## 接入功能 - -TapSDK 提供了众多功能。请在初始化 SDK 后,根据项目需要,参考相应功能的文档,接入相应功能。 -绝大多数游戏都会接入 TapTap 登录,所以我们推荐从这一功能开始。 - - -### 配置签名证书 - -Android 和 iOS 应用需要在 TapTap 开发者中心进入你的游戏,依次选择 **游戏服务 > 开发与构建 > TapTap 登录** 配置应用的相关信息(如下图所示),否则 Android -应用测试登录功能时会返回 `signature not match` 报错信息,iOS 会返回 `sdk_not_matched` 报错信息,无法正常使用 TapTap 登录功能。 - -Android 签名处填写 MD5 值,详情可参考:[如何获取 MD5 值](/v4/sdk/access/android-md5)。 - - - - -![](https://capacity-files.lcfile.com/MM13UMrcN5n1WSJyClE7QQHb5f9ue4o6/io-login-config.png) - - - -接下来,就可以打包应用,测试 TapTap 登录功能了。 - -### Android 代码混淆 - -TapSDK 已经做了混淆处理,再次混淆会导致不可预期的错误,请在项目的混淆脚本中添加如下配置,跳过对 TapSDK 的混淆操作: - -```java --keep class com.taptap .**{*;} - --keep class com.tapsdk .**{*;} -``` - -## 打包 - -Android 或 iOS 请按通常的 Android APK 或者 iOS 应用打包流程操作即可。这里介绍一下 Unity 打包流程: - -### 打包 APK - -第一步,配置 package name 和签名文件: - -![](https://capacity-files.lcfile.com/qooIRbr5qtLrnhsP0hWjOSnBYW12eNg6/tap_unity_android_build.png) - -第二步,检查 **File > Build Settings > Player Settings > Other Settings > Target API Level** 版本,当 API Level 小于 29 时,需要配置 -manifest,在 `application` 节点添加: - -```json -tools:remove="android:requestLegacyExternalStorage" -``` - -这是因为 SDK 内部默认配置了 `android:requestLegacyExternalStorage = true`,当 `targetSdkVersion < 29` 时会报错 `Android resource linking failed`。 - -### 导出 Xcode 工程 - -需要配置 icon 和 `BundleID`: - -![](https://capacity-files.lcfile.com/Nke4QO6zdEz5mRd2Kwd8R9ydyP8QYaJy/tap_ios_build.png) diff --git a/versioned_docs/version-v4/sdk/achievement/_category_.json b/versioned_docs/version-v4/sdk/achievement/_category_.json deleted file mode 100644 index 236d14f6a..000000000 --- a/versioned_docs/version-v4/sdk/achievement/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "成就系统", - "collapsed": true, - "position": 7 -} diff --git a/versioned_docs/version-v4/sdk/achievement/bestpractice.mdx b/versioned_docs/version-v4/sdk/achievement/bestpractice.mdx deleted file mode 100644 index c20f9f504..000000000 --- a/versioned_docs/version-v4/sdk/achievement/bestpractice.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: 《泰拉瑞亚》 使用 Tap 成就来吸引更多玩家 -sidebar_label: 最佳实践 -sidebar_position: 3 ---- - -《泰拉瑞亚》是一个跨越手机、PC、主机平台的常青树游戏,在全球都有海量的忠实玩家。在中国大陆,中国港澳台发行的手机版《泰拉瑞亚》,目前在 TapTap 上销量也超过了 200 万份,收获了 9.3 的 Tap 评分。 - - -## 支持跨平台的 TDS 成就 -由于泰拉瑞亚可以在多个平台和渠道发行,他们使用了不受平台和引擎的限制的 TDS 的成就系统,不论游戏发布在 iOS AppStore、Android 各大渠道、PC、甚至主机平台,都能帮助游戏实现跨平台的成就系统。 -截止到 2022 年 5 月底,游戏总计触发成就人数超过 200 万人,解锁白金成就(即获得了全成就)的玩家超过 3500 人,游戏社区中也有不少玩家晒自己的成就进度、讨论成就的具体达成方法。 - -![img](https://capacity-files.lcfile.com/zqc1dU4x4cV06hcCok4CwCxFWoowsf4v/achievement_show_on_taptap.png) - - -我们也即将在未来几个月在 TapTap 客户端内增加更多成就系统相关的功能和露出,包括在动态显示好友获得的成就、对比成就、成就专题页等,为喜欢成就的玩家提供更多的实用和分享功能,也能帮助接入了成就的游戏进行更好的宣发和曝光。 - - -## 白金成就 - -我们会为那些获得 TapTap 玩家和 TapTap 编辑认可的游戏,提供白金奖杯,以此来激励玩家冲击游戏全成就,并且白金成就也可作为一种社交资产被玩家所拥有。 - -如果您的游戏符合至少以下条件,就可以在开发者中心后台申请开通白金成就,我们的编辑会来评估您的游戏是否可以开通白金: -- 游戏时长在同类游戏中处于正常水平 -- 免费游戏中,用户不需要付费,仍然可以获得全成就 -- 付费游戏中,用户不需要强制购买主线流程外的额外 DLC 或内购,仍然可以获得白金成就 - -![img](https://capacity-files.lcfile.com/3kBnhO30aI8ukszjt61L4527zHbyAaW5/baijin_achievement.png) - - -## 成就稀有度 -成就的稀有度在一定的冷启动数据后,会开始自动计算。稀有度可以反映这个成就的获得难度,对于稀有度极高的成就,玩家挑战完成会有强烈的成就感。 -![img](https://capacity-files.lcfile.com/C15DpYOKoz9DBFRcUmhy85GDX9Mv8PRT/achievement_rare.png) - - -## 开发者中心的成就配置 -成就后台的配置也较为简单,并且可以看到每一个成就的解锁人数、完成率等数据。 -![img](https://capacity-files.lcfile.com/dTh8PAoMPzbySH1Vw1D6XvF9gswNY6KK/achievement_list.png) - - -## 接入成就的方法 - -目前成就系统提供 SDK 上报成就与服务端上报成就 2 种方式。 - -《泰拉瑞亚》采用了服务端的方式接入了 TDS 成就系统,这样能够比较好的做好防作弊的处理,也能比较及时和稳定的做到成就的同步。 - -如果您的成就已经存储在服务端,那么我们优先建议您使用服务端上报的方式。 - -如果您的游戏是个单机游戏,没有服务端,或者没有存储过用户成就,那么也可以选择接入 TapSDK 来上报玩家的成就。当您设置到成就的触发点后,玩家联网时 SDK 会上报成就数据;若玩家在断网状态,SDK 也会在本地存储成就数据,待网络恢复后上报。 -在后台配置完成就,并确保您的游戏已经接入完成并且测试无误后,您就可以点击发布,将成就发布到 TapTap 上了。 - - -## 将玩家的成就展示在 TapTap - -如果希望在 TapTap 客户端内的成就列表中看到您的游戏,那就需要在游戏中接入 TapTap 登录。您可以把 TapTap 登录作为游戏的一种登录方式,可以 将 TapTap 登录作为游戏账号可以绑定的一个第三方账号,专门用于做成就的同步。 - - -## 立即开始使用 TDS 成就服务 - -如果希望了解如何接入TDS 成就系统,可以访问我们的 [成就产品指南](/v4/sdk/achievement/features/) 和 [成就开发指南](/v4/sdk/achievement/guide/)。整个系统的接入非常简单,有任何问题也欢迎通过 TapTap 开发者中心的工单系统来与我们取得联系。 \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/achievement/faq.mdx b/versioned_docs/version-v4/sdk/achievement/faq.mdx deleted file mode 100644 index c82a162c6..000000000 --- a/versioned_docs/version-v4/sdk/achievement/faq.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -### 成就编辑完成后在待发布状态,如何进行测试? - -如果需要添加测试人员体验「待发布」状态的成就,请进入开发者中心通过右上角「工单」联系 TDS 技术支持。「工单」中请提供应用的 Client ID 以及测试账号实现 TapTap 登录后返回的 Object ID。 - -### 游戏上线了,还能再申请「白金成就」吗? - -白金成就不受游戏上线的影响。既能在游戏上线前创建好白金成就,也可以在游戏上线后创建。 - -### 「白金成就」审核时,还能发布新的普通成就吗? - -白金成就的申请和创建并不影响普通成就的发布,可以随时提交已经准备好的普通成就。 - -### 在申请「白金成就」资质通过前,玩家已经获得全部成就了,还能补领吗? - -如果有玩家在白金成就发布前,已经获得到全部的普通成就,再为游戏创建白金成就时,玩家依然可以自动获得。 - -### 如果已经是上架的老游戏了,对于已经使用自建账户登录或者第三方登录的用户,怎么接入使用 TDS「内建账户」呢? - -答:针对老用户来说,就使用自建账号/第三方账号登录,登录后需要调用 TapSDK 的绑定接口,对老用户进行绑定。绑定后,在 TDS User(内建账户) 这里都会生成一个玩家的 ID,在使用成就系统后,TapSDK 这边会根据这个 TDS User ID 来确定该玩家身份。如果是新用户的话,直接 TapTap 登录就可以了,就不需要进行绑定这一步骤了。 - -### 如果是已经上架的老游戏接成就系统,那老用户已经完成过的成就,还能再触发吗? - -老用户已完成的数据有两个方式可以同步: -- 通过服务器 API 的方式提前同步一份数据到 TDS 成就服务; -- 若游戏本身记录了成就达成的数据,那么游戏找合适的时间点把以前记录的数据转化为「成就 ID」通过 SDK 达成一次(注:需要临时关一下通知,不然顶部会有多个提醒通知)。 - -### 如果玩家已经达成了全部成就获得「白金成就」,开发者在后台又新增了一个普通成就,玩家的「白金成就」标志会消失吗? - -不会,白金成就的范围仅限在分组为本体成就之内,新创建的普通成就是为拓展成就,将不会影响白金成就。 - -### 游戏有多个区服或可以创建多个角色,如果重复获得成就,SDK 的逻辑是怎样的? - -成就记录跟着账号走,每个成就只记录第一次获得的行为,之后重复获得将不做展示。 - -### 调用初始化数据接口时遇到 `Empty sign or session` 报错,可能的原因是什么? - -因为成就系统是基于内建账户系统(`TDSUser`)的,需要在进行成就系统初始化数据(`[TapAchievement initData];`)之前接入内建账户功能,并且对 `TDSUser` 对象进行实例化。如果 `TDSUser` 为空,会报错 `Empty sign or session`。 - diff --git a/versioned_docs/version-v4/sdk/achievement/features.mdx b/versioned_docs/version-v4/sdk/achievement/features.mdx deleted file mode 100644 index ce9ea0e6c..000000000 --- a/versioned_docs/version-v4/sdk/achievement/features.mdx +++ /dev/null @@ -1,295 +0,0 @@ ---- -title: 成就系统功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -开发者可以在开发者中心后台配置并发布游戏成就,玩家在游戏内触发并获得成就,从而提升玩家在游戏中的参与度,鼓励玩家以不同的玩法来玩游戏。TapTap 为游戏增加了点亮「白金成就」荣誉标识,对于那些孜孜不倦完成全部成就的玩家给予奖励。 - - -## 核心优势 - -**对于游戏开发者**: -- 更多曝光: 用户可以在 TapTap 展示自己的游戏成就,从而给游戏带来更多曝光。 -- 提高用户生命周期价值:建立用户成长体系(精神激励),提升玩家在游戏中的参与度。 -- 数据验证:以用户分层来完善用户画像,帮助开发者用数据评估游戏内的玩法设计难易度。 -- 支持多端:TapSDK 4.3.0 以上版本支持 iOS、Andriod、Unity 版本,**同时包含 PC 端展示** - -**对于游戏玩家**: -- 游戏体验:成就内容的趣味性,提升玩家在游戏内的体验。 -- 情绪激发:激发玩家的荣誉感、收集欲望,提升用户粘性。 -- 数据资产:可以在 TapTap 记录自己的游戏里程碑。 - - -## 前期准备 - -为了确保 TapTap 能记录到用户的成就数据,需要用户进行 TapTap 账号登录,故此建议开发者接入 **[TapTap 登录](/sdk/taptap-login/features/)**。 -如果游戏本身自带账号系统,可以将游戏账号进行 TapTap 账号绑定。 - -## 名词解释 - -### 基础信息 - -| 名词 | 释义 | 规则 | -| --- | --- | --- | -| 成就 ID | 向 SDK 上报成就的唯一标识,可以按照游戏自己的要求来定义成就 ID | 只允许英文+数字,最多可以包含 40 个字符 | -| 名称 | 成就的简称,例如「驾车高手」 | 最多可以包含 40 个字符 | -| 简介 | 成就的简要说明,通常用来告诉玩家如何获得成就,例如「驾驶汽车连续躲避 10 个障碍物」 | 最多可以包含 200 个字符 | -| 图标 | 与成就内容相关的方形图标 | 图标 UI 规则:只需要提供成就解锁时的 512×512 PNG 或 JPG 彩色图片,TapTap 会自动生成未解锁状态时的灰度版本 | -| 初始状态 | 成就的初始化状态,分为隐藏成就和显示成就 | 一旦初始设置,后续不可再修改 | - - -### 成就稀有度 - -获得成就人数的百分比,值越低,获得人数越少,成就就越稀有。 - -计算公式 = 解锁此成就的人数/使用 TapTap 账号登录人数。 - -:::tip -为了确保稀有度的保真性,在用户触达成就模块前,引导用户进行 TapTap 登录授权 -::: - -### 解锁状态 - -当玩家在游戏内触达到开发者设定的目标时,成就状态会发生改变。 -- 未解锁:提交成就发布后的初始化状态,即玩家未达成成就; -- 已解锁:玩家解锁成就,分为: - - 非分步成就:当玩家触达成就后,「未解锁」状态直接变为「已解锁」状态; - - 分步成就:当玩家触达到成就某步骤数时,「未解锁」状态上会展示完成度百分比,当完成所有步骤后,状态变为「已解锁」状态。 - - -### 分步成就 - -分步成就会让玩家用更长的时间逐步达成成就。随着玩家逐步取得成就,可以向 TapTap 上报玩家的进度。TapTap 会记录进度信息,并在玩家达到解锁该成就的必要条件时提醒游戏,同时告知玩家成就达成。 - -**数值范围**:创建时必须定义解锁成就所需的步骤总数(此数字必须介于 2 到 100,000,000 之间)。步骤总数达到解锁值后,成就即被解锁(即使它已被隐藏)。开发者无需存储用户的累积进度。 - -**不可重置**:分步成就在游戏进程中是累积的,并且进度无法从游戏内删除或重置。例如,「赢得 20 场比赛」将被视为分步成就。而「连续赢得 5 局游戏」则不行,因为当玩家输掉游戏时,其进度将被重置。「拥有 2,000 个金币」也不符合条件,因为玩家在玩游戏时可能会获得、也会失去金币。对于后两项成就,你可以把成就名称设定为「连续获胜」或「金币总数」,并在玩家达成目标时解锁的标准成就。 - - - -### 隐藏成就 -设为隐藏成就后,用户无法看到隐藏成就的解锁明细包括成就图片、成就名称、成就简介,只有用户解锁后才能查看。在创建时,开发者可以将自己的成就初始状态置为「显示成就」或者「隐藏成就」。 - - - -### 白金成就 - -对于高品质的游戏,当玩家获得有价值的**普通成就**时,会额外解锁白金成就。 - -定义范围:正式发布时被选为**白金成就解锁条件的普通成就**,未发布的成就不算在内。一旦发布了白金成就后,范围不可再被修改,即不能增加也不能移除。 - -白金徽章:开通白金成就后,TapTap会为游戏定制专属的白金徽章,当用户解锁白金成就时,也能佩戴上对应的白金徽章。 - - -![](https://img.tapimg.com/market/images/2b9b2064064438b48baf7104c4ddc59f.png) - - - -
    - - -## 功能说明 - -### 创建普通成就 - -要为新游戏创建一个成就,你可以在**开发者中心**、**游戏服务**下找到**开发与构建**标签。选择**成就**菜单,然后点击**创建普通成就**。 - -填写成就的相关信息,点击**保存**,这个成就进入到**审核中**状态。审核通过后状态会置为**待发布**,审核不通过后状态会置为**已驳回**。 - - -![](https://img.tapimg.com/market/images/47ad5af3b10a6996ed8eec096cec71e6.png) - - - -### 编辑普通成就 - -成就发布前,需要编辑已经创建的成就,请在成就列表里点击**编辑**。此时,可以看到与第一次创建成就时一样的配置页面,根据需要进行编辑。 - -一旦成就发布后,**成就 ID**、**分步成就**、**成就分组**和**初始状态**这四项配置将无法修改。 - - -### 成就测试 - -1. 开发者可以前往「测试」Tab 下,可以添加若干个**非注销**的TapTap 账号成为**测试人员**,最多可以添加 100 个; -2. 测试人员可以体验到「审核中」「已驳回」「待发布」状态的成就数据,来进行测试; -3. 在进行测试后发现数据不合理,修改了后想重新测试,只需[「重置进度」或「删除成就」](#删除重置成就)即可; -4. 如果测试人员的数据在发布前,没有进行重置,是会带到正式环境的,和其他用户的数据共同展示(注:是否需要删档测试,取决于运营需求)。 -5. 在发布前,仅「游戏管理员」和「测试人员」可以预览成就用户端的页面,确保配置和数据上报正确。 - - -![](https://img.tapimg.com/market/images/e1edfee896b0fffa9883a9a03e70055f.png) - - -### 发布普通成就 - -当成就测试完毕后,将处于**待发布**状态,至少要完成创建 **5** 个普通成就才能发布,点击**发布成就**,所有成就将发布到正式环境,请谨慎操作。 - - -### 删除/重置成就 - -成就发布前,通过点击成就列表末尾的按钮,来**删除**和**重置**未发布的成就。 - -成就发布后,**不能被删除和重置**。 - - - -### 创建白金成就 - -为了确保白金成就的质量,在创建白金成就时审核人员会根据游戏质量是否高于平均水品来判断审核结果。详细请参考**[白金成就审核标准](/sdk/taptap-login/features/)**。 - -拥有白金成就的游戏在 TapTap 上会有显眼的白金奖杯标识,可以给游戏带来不错的品质提升和效应传播,从而获得更多的曝光。 - -为了确保白金成就的挑战性,开发者需要创建 10 个**白金解锁条件**内的普通成就后,才能创建白金成就,提交时需要配置**图标**、**名称**和**简介**,当白金成就审核通过后,就可以发布了。 - - -![](https://img.tapimg.com/market/images/51dbdb84c2bd00ce20b2f0532683aaf6.png) - - -### 白金成就审核标准 - -#### 审核标准 - -1. 不适用于评分低于 5 分的游戏 - -2. 游戏关注量大于 5 万的游戏 - -3. 游戏时长或内容量在同品类产品中处于较为正常的水平(除特殊情况) - -4. 不接入io游戏 - -5. 不接入玩法单一的益智游戏(如数独) - -6. 不接入休闲小游戏 - -7. 不接入传奇类游戏 - -8. 不接入纯视觉小说(无互动、无选项,无多路线) - -9. 免费游戏中,用户不需要付费,仍然可以获得所有成就 - -10. 付费游戏中,用户不需要购买DLC或其他内购,仍然可以获得所有成就。 - -11. 游戏不破坏平台整体调性 - -#### 推荐成就类型 - -| 类型 | 范例 | -| --- | --- | -| 进度类 | 人物等级提升至20级 | -| 首次类 | 第一次使用传送点系统 | -| 挑战类 | 10秒内击杀XX | -| 彩蛋类 | 发现隐藏 Npc | -| 排行类 | XX拿到第一名 | -| 收集类 | 获得所有卡牌(非付费内容) | -| 成就类 | 获得其他所有成就(即白金成就) | - -#### 平台禁止使用成就类型 - -1. 氪金总量类(如累计充值等) - -2. 明显的引导攀比类 - -3. 引导好评等 - -4. 其他违反法律、普世价值观等等 - -5. 广告类 - - -### 发布白金成就 -发布白金成就后,**无法再更改白金成就解锁条件**,如果发生误操作不小心发布了成就,请在带成就包体上架前 3 天工单告知我们,及时为游戏进行调整。 -另外,在发布后的 2 周内,TapTap 会为有白金成就的游戏,额外制作**白金徽章**,当玩家解锁白金成就时,就能佩戴专属徽章啦! - - -
    - - - -### 游戏内成就通知 - -- 当玩家在游戏中触发成就行为,在游戏顶部推送冒泡。界面的成就队列只有 1 条,如同时触发多条成就,则会排队显示。 -- 开发者对于游戏内的成就展示页和冒泡通知可以选择显示或隐藏。 - - -![](https://img.tapimg.com/market/images/3178576661340ec4768fcb1498048e58.png) - - -### 游戏内查看成就 - -- 若用户安装了 TapTap 客户端,可以在游戏内直接拉起端内的成就页面 -- 若用户没有安装 TapTap 客户端,可以在游戏内 Webview 打开成就页面 -:::tip -需要用户在游戏内进行过 TapTap 登录授权 -::: - - - -### TapTap 上展示成就 -- TapTap 登录游戏:使用 TapTap 登录的玩家可以直接在 「TapTap 客户端-我的-游戏成就」查看已解锁和未解锁的游戏成就。 -- 非 TapTap 登录游戏:如果游戏本身存在账号体系,需要游戏自己绘制一个 TapTap 账号绑定入口,将游戏账号和 TapTap 账号关联起来,将成就数据同步到 TapTap 上。 - -![](https://img.tapimg.com/market/images/2139f3837e93b40a55bde28b899e08e8.png) - - -:::tip -**关于绑定 TapTap 账号的两种场景如何处理** -- 场景 1:用户有一个非 TapTap 登录账号(账号 A),和一个没登录过游戏的 TapTap 号,需要游戏这里做账号绑定,那 TapTap 上展示的是账号 A 的成就数据 -- 场景 2:用户有一个非 TapTap 登录账号(账号 A),和一个已经登录过游戏的 TapTap 号(账号 B),那 TapTap 上展示的是账号 B 的成就数据。那如果用户想要展示账号 A 的数据,那就游戏自身需要有解绑账号的功能,即把账号 B 和 TapTap 账号解除绑定,把 TapTap 账号置换出来和账号 A 进行绑定 -::: - - - -## 接入说明 - -### 接入准备 - -1. 入驻成为 TapTap 的开发者; -2. 在 TapTap 开发者中心创建游戏应用,且需要开通「游戏服务」生成应用配置; -3. 若要给子账号添加权限,请至「权限管理」中给该账号设置「游戏管理员」权限; -4. 下载 TapSDK(最低支持版本 v4.3.0)集成到游戏包内。 - -### 接入流程 - -![](https://img.tapimg.com/market/images/a3f191811eab92fad8bb43d325346d08.png) - -### 接入指南 - -见 **[成就系统 > 开发指南](/sdk/achievement/guide/)**。 - - - - -## 常见问题 - -#### 游戏上线了,还能再申请「白金成就」吗? - -白金成就不受游戏上线的影响。既能在游戏上线前创建好白金成就,也可以在游戏上线后创建。 - -#### 「白金成就」审核时,还能发布新的普通成就吗? - -白金成就的申请和创建并不影响普通成就的发布,可以随时提交已经准备好的普通成就。 - -#### 在申请「白金成就」通过前,玩家已经获得全部成就了,还能补领吗? - -如果有玩家在白金成就发布前,已经获得到全部的普通成就,再为游戏创建白金成就时,玩家依然可以自动获得。 - -#### 如果已经是上架的老游戏了,对于已经使用自建账户登录或者第三方登录的用户,怎么接入使用 TapTap 成就系统呢? - -答:针对老用户来说,就使用自建账号/第三方账号登录,登录后需要调用 TapSDK 的绑定接口,对老用户进行绑定。绑定成功后,需要同步本地的成就数据给到 TapTap 成就服务。如果是新用户的话,直接 TapTap 登录就可以了,就不需要进行绑定这一步骤了。 - -#### 如果是已经上架的老游戏接成就系统,那老用户已经完成过的成就,还能再触发吗? - -老用户已完成的数据有两个方式可以同步: -- 通过服务器 API 的方式提前同步一份数据到 TapTap 成就服务; -- 若游戏本身记录了成就达成的数据,那么游戏找合适的时间点把以前记录的数据转化为「成就 ID」通过 SDK 达成一次(注:需要临时关一下通知,不然顶部会有多个提醒通知)。 - -#### 如果玩家已经解锁了「白金成就」,开发者在后台又新增了一个普通成就,玩家的「白金成就」标志会消失吗? - -不会,白金成就的范围在发布后是不会修改的,新创建的普通成就不会被计算在白金解锁条件范围内,故也不会影响白金成就。 - -#### 游戏有多个区服或可以创建多个角色,如果重复获得成就,SDK 的逻辑是怎样的? - -成就记录跟着账号走,每个成就只记录第一次获得的行为,之后重复获得将不做展示。 diff --git a/versioned_docs/version-v4/sdk/achievement/guide.mdx b/versioned_docs/version-v4/sdk/achievement/guide.mdx deleted file mode 100644 index cee6d1d37..000000000 --- a/versioned_docs/version-v4/sdk/achievement/guide.mdx +++ /dev/null @@ -1,537 +0,0 @@ ---- -title: 成就系统开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; - -import sdkVersions from '../../sdkVersions'; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; -import Languages from "../_partials/languages.mdx"; - -本文介绍如何在游戏中加入成就系统。 - -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | -| 网络状态权限 | 用于检查网络连接状态(如 Wi-Fi 或移动数据是否可用) | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下权限: - -```xml - - -``` - - - -<> - - - - - - -## 集成前准备 - -1. 参考 [准备工作](/v4/sdk/access/get-ready/) 创建应用、开启成就服务。 - -## SDK 获取 - -由于TapSDK V4 依赖于TapTapCore核心库,所以需要在TapTapCore的基础上,另外添加 `TapTapAchievement` 、 `TapTapLogin` 模块: - - -<> - - - - - -<> -1. 项目根目录的 build.gradle 添加仓库地址: - -```groovy -allprojects { - repositories { - google() - mavenCentral() - } -} -``` - -2. app module 的 build.gradle 添加对应依赖: - -{ -`dependencies { - implementation 'com.taptap.sdk:tap-core:${sdkVersions.taptap.android}' - implementation 'com.taptap.sdk:tap-achievement:${sdkVersions.taptap.android}' -}` -} - - - -<> -iOS 提供通过添加 cocosPod 远程依赖和使用本地文件导入两种集成方式,推荐使用远程依赖方式。 - -#### 远程依赖 - -1. 在工程 Podfile 文件中对应模块下添加依赖: - -{` pod 'TapTapAchievementSDK', '~> ${sdkVersions.taptap.ios }'`} - -2. 执行 `Pod install` 下载对应依赖文件 -3. 将工程 Pods 目录下 `TapTapAchievementSDK/Frameworks/TapTapAchievementResource.bundle` 和 `TapTapLoginSDK/Frameworks/TapTapLoginResource.bundle` - 等资源文件导入工程中 - -#### 本地文件依赖 - -成就模块依赖于 Tap 登录模块,使用本地文件方式添加依赖时,需先参考 [入门指南](/v4/sdk/access/quickstart) 和 [TapTap 登录](/v4/sdk/taptap-login/guide) 添加对应本地文件依赖项。 - -1. 在下载页下载如下文件: - -- `TapTapAchievementSDK ` 合规认证依赖库 -- `TapTapAchievementResource.bundle` 合规认证资源文件 - -2. 在工程中添加 `framework` 静态库,注意添加时选择 Embed 方式为 **Do Not Embed**,导入 `bundle` 资源文件 - - - - - -## SDK 初始化 - -详见 [TapTapSDK 初始化文档](/v4/sdk/access/quickstart#初始化)。 - - -<> - -`TapTapSdkOptions` 详细参数见 [入门指南#快速开始](/v4/sdk/access/quickstart/#初始化) - -```cs -using TapSDK.Core; -using TapSDK.Achievement; - -// 核心配置 详细参数见 [TapTapSDK] -TapTapSdkOptions coreOptions = new TapTapSdkOptions(); -// 成就配置 -TapTapAchievementOptions achievementOptions = new TapTapAchievementOptions -{ - // 成就达成时 SDK 是否需要展示一个气泡弹窗提示 - enableToast = true -}; -// 其他模块配置项 -TapTapSdkBaseOptions[] otherOptions = new TapTapSdkBaseOptions[] -{ - achievementOptions -}; -// TapSDK 初始化 -TapTapSDK.Init(coreOptions, otherOptions); -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapSdk -import com.taptap.sdk.core.TapTapSdkOptions -import com.taptap.sdk.achievement.options.TapTapAchievementOptions -import com.taptap.sdk.core.TapTapLanguage -import com.taptap.sdk.core.TapTapRegion - -TapTapSdk.init( - context = context, - sdkOptions = TapTapSdkOptions( - clientId = clientId, - clientToken = clientToken, - region = TapTapRegion.CN, - preferredLanguage = TapTapLanguage.ZH_HANS, - enableLog = false - ), - options = arrayOf( - TapTapAchievementOptions( - // 成就达成时 SDK 是否需要展示一个气泡弹窗提示 - enableToast = true - ) - ) -) -``` - - -<> - -```swift -import TapTapAchievementSDK -import TapTapCoreSDK - -// 核心配置 -let coreOptions = TapTapSdkOptions() -// TODO: coreOptions 配置 - -// 成就配置 -let achievementOptions = TapTapAchievementOptions() -// 成就达成时 SDK 是否需要展示一个气泡弹窗提示 -achievementOptions.enableToast = true - -// 其他模块配置项 -let otherOptions = [achievementOptions] - -// TapSDK 初始化 -TapTapSDK.initWith(coreOptions, otherOptions: otherOptions) -``` - - - - -## 注册监听回调 - -成就 SDK 中包含多个监听回调,分别会在初始化数据成功、初始化数据失败以及成就进度更新时被调用。 - - - -<> - -```cs -using TapSDK.Achievement; - -AchievementCallback callback = new AchievementCallback(); -TapTapAchievement.RegisterCallBack(callback); -TapTapAchievement.UnRegisterCallBack(callback); - -class AchievementCallback : ITapAchievementCallback -{ - - public AchievementCallback(){} - - public void OnAchievementSuccess(int code, TapAchievementResult result) - { - // 成就状态更新成功 - // code 70001 解锁成就成功 - // code 70002 增加步长成功 - // result 成就数据详情 - } - - public void OnAchievementFailure(string achievementId, int errorCode, string errorMsg) - { - // 成就状态更新失败或其他错误 - // achievementId 触发失败的成就 ID, 如果调用的是 [ShowAchievements] 接口,则为 "" 空字符串。 - // errorCode 错误码 - // errorMsg 错误描述 - } - -} -``` - -成就常量定义: - -```cs -namespace TapSDK.Achievement -{ - public class TapTapAchievementConstants - { - // 未初始化 - public static readonly int NOT_INITIALIZED = 80000; - // 区域不支持 - public static readonly int REGION_NOT_SUPPORTED = 80001; - // 当前未登录,需要登录 - public static readonly int NOT_LOGGED = 80002; - // 当前登录失效,需要重新登录 - public static readonly int ACCESS_DENIED = 80010; - // 无效参数 - public static readonly int INVALID_REQUEST = 80020; - // 网络异常 - public static readonly int NETWORK_ERROR = 80030; - // 未知错误,如:代理导致网络错误 - public static readonly int UNKNOWN_ERROR = 80100; - - // unlock解锁成就成功 - public static readonly int UNLOCK_SUCCESS = 70001; - // 增加步长成功 - public static readonly int INCREMENT_SUCCESS = 70002; - } -} -``` - - -<> - -```kotlin -import com.taptap.sdk.achievement.TapTapAchievement -import com.taptap.sdk.achievement.TapAchievementCallback -import com.taptap.sdk.achievement.TapTapAchievementResult - -val callback = object : TapAchievementCallback { - override fun onAchievementSuccess(code: Int, result: TapTapAchievementResult?) { - // 成就状态更新成功 - } - - override fun onAchievementFailure(achievementId: String, errorCode: Int, errorMessage: String) { - // 成就状态更新失败 - } -} - -TapTapAchievement.registerCallback(callback = callback) -TapTapAchievement.unregisterCallback(callback) -``` - - -<> - -```swift -import TapTapAchievementSDK - -class CallbackImpl: NSObject, TapTapAcheievementCallback { - - override init() {} - - // 成就状态更新成功 - func onAchievementSuccess(code: Int, result: TapTapAchievementResult) { - // code 70001 解锁成就成功 - // code 70002 增加步长成功 - // result 成就数据详情 - } - - // 成就状态更新失败或其他错误 - func onAchievementFailure(achievementId: String, errorCode: Int, errorMsg: String) { - // achievementId 触发失败的成就 ID, 如果调用的是 [ShowAchievements] 接口,则为 "" 空字符串。 - // errorCode 错误码 - // errorMsg 错误描述 - } -} -let callback = CallbackImpl() -TapTapAchievement.registerCallback(callback: callback) - -// 不需要接收回调时调用接口移除之前设置的回调对象 -TapTapAchievement.unregisterCallback(callback: callback) -``` -成就常量定义: - -```swift -public class TapAchievementCallbackCode: NSObject { - // 解锁成就成功,包括步长达到阈值解锁和直接解锁 - public static let UNLOCK_SUCCESS = 70001 - - // 增加步长成功 - public static let INCREMENT_SUCCESS = 70002 - - // 上报失败,未初始化 - public static let NOT_INITIALIZED = 80000 - - // 区域不支持 - public static let REGION_NOT_SUPPORTED = 80001 - - // 上报失败,未登录 - public static let NOT_LOGGED = 80002 - - // 认证信息无效 - public static let ACCESS_DENIED = 80010 - - // 无效请求 - public static let INVALID_REQUEST = 80020 - - // 网络异常 - public static let NETWORK_ERROR = 80030 - - // 未知异常 - public static let UNKNOWN_ERROR = 80100 -} -``` - - - - -### 成功数据详情 - -`TapAchievementResult` - -| **字段** | **含义** | -|-------------------|------------------------------------------------------------------------------| -| `achievementId` | 成就id | -| `achievementName` | 成就名称 | -| `achievementType` | 成就类型,分为普通成就(`TapAchievementType.NORMAL`)、白金成就(`TapAchievementType.PLATINUM`) | -| `currentSteps` | 当前成就进度,如果不是分步式成就该值为 0 | - -### 错误码详情 - -| **errorCode** | **错误类型** | **错误原因** | -|---------------|------------------------|------------------------------------------| -| 80000 | `NOT_INITIALIZED` | 未初始化,在调用API之前请先初始化 | -| 80001 | `REGION_NOT_SUPPORTED` | 区域不支持,成就目前只支持区域 `CN` , 请确认初始化参数 `region` | -| 80002 | `NOT_LOGGED` | 当前未登录,请在登录之后再使用报错API | -| 80010 | `ACCESS_DENIED` | 当前登录失效,需要重新登录 | -| 80020 | `INVALID_REQUEST` | 无效参数,请检查传入的参数是否正确 | -| 80030 | `NETWORK_ERROR` | 网络异常, 请确认当前网络连接是否正常 | -| 80100 | `UNKNOWN_ERROR` | 请确认当前网络连接是否正常 ,或者联系技术支持 | - -## 解锁某个成就 - -当玩家达成某一成就时,可以使用以下方式解锁成就,解锁成功后会触发 callback 的 `OnAchievementSuccess` 回调。 -解锁失败会触发 callback 的 `OnAchievementFailure` 回调。具体错误码请参考 [错误码详情](#错误码详情) - - - -```cs -using TapSDK.Achievement; - -// achievementId 是在开发者中心中添加成就时自行设定的 成就 Id -TapTapAchievement.Unlock(achievementId : achievementId); -``` - -```kotlin -import com.taptap.sdk.achievement.TapTapAchievement - -// achievementId 是在开发者中心中添加成就时自行设定的 成就 Id -TapTapAchievement.unlock(achievementId) -``` - -```swift -import TapTapAchievementSDK - -// achievementId 是在开发者中心中添加成就时自行设定的成就 Id -TapTapAchievement.unlock(achievementId: achievementId) -``` - - - -## 分步成就增长步数 - -如果成就是增量类型(即,需要几个步骤才能解锁它)请使用以下API。SDK 会自动计算当前全量步数。 -解锁成功后会触发 callback 的 `OnAchievementSuccess` 回调。 -解锁失败会触发 callback 的 `OnAchievementFailure` 回调。具体错误码请参考 [错误码详情](#错误码详情) - - - -```cs -using TapSDK.Achievement; - -// achievementId 是在开发者中心中添加成就时自行设定的 成就 Id -string achievementId = "achievementId"; -// step 是增长的步数 -int step = 1; -TapTapAchievement.Increment(achievementId : achievementId, step : step); -``` - -```kotlin -import com.taptap.sdk.achievement.TapTapAchievement - -TapTapAchievement.increment(achievementId = "achievementId", steps = 1) -``` - -```swift -import TapTapAchievementSDK - -// achievementId 为在开发者中心中添加成就时自行设定的成就 Id -let achievementId = "achievementId" -// steps 为增长的步数 -let steps = 1 -// 更新成就步长数据 -TapTapAchievement.increment(achievementId: achievementId, steps: steps) -``` - - - -## 设置冒泡开关 - -默认情况下,成就达成时 SDK 会自行展示一个冒泡浮窗提示玩家已达成相应成就。需要关闭请调用如下接口或者在初始化的时候设置 -TapTapAchievementOptions.enableToast 为 false。 详见 [成就初始化配置](#成就初始化配置) - - - -```cs -using TapSDK.Achievement; - -TapTapAchievement.SetToastEnable(false); -``` - -```kotlin -import com.taptap.sdk.achievement.TapTapAchievement - -TapTapAchievement.setToastEnable(enable = false) -``` - -```swift -import TapTapAchievementSDK - -TapTapAchievement.setToastEnable(enable: false) -``` - - - -## 打开成就展示页 - -SDK 自带一个展示所有成就和已达成成就情况的页面。 -当玩家设备上有安装TapTap客户端时,会跳转到TapTap客户端的成就页面,否则会在游戏内展示成就页面。 - - -<> - -请注意:在PC平台上,调用此接口会打开外部浏览器展示成就页面。 - -```cs -using TapSDK.Achievement; - -TapTapAchievement.ShowAchievements(); -``` - - - -<> - -```kotlin -import com.taptap.sdk.achievement.TapTapAchievement - -TapTapAchievement.showAchievements() -``` - - - -<> - -```swift -import TapTapAchievementSDK - -TapTapAchievement.showAchievements() -``` - - - - - -## 国际化 - -TapTapAchievement 支持设置语言:目前只支持简体中文 - -[//]: # () - - diff --git a/versioned_docs/version-v4/sdk/anti-addiction/_category_.json b/versioned_docs/version-v4/sdk/anti-addiction/_category_.json deleted file mode 100644 index 31a276160..000000000 --- a/versioned_docs/version-v4/sdk/anti-addiction/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "合规认证", - "collapsed": true, - "position": 4 - } \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/anti-addiction/faq.mdx b/versioned_docs/version-v4/sdk/anti-addiction/faq.mdx deleted file mode 100644 index 3fffb90a0..000000000 --- a/versioned_docs/version-v4/sdk/anti-addiction/faq.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -import MultiLang from '/src/docComponents/MultiLang'; - -## 常见问题 - - - -### 个人/团体的开发者需要接入防沉迷吗 - -按照国家新闻出版署[《关于进一步严格管理切实防止未成年人沉迷网络游戏的通知》](https://www.nppa.gov.cn/xxfb/tzgs/202108/t20210830_666285.html),各游戏出版运营企业均须在游戏内落实游戏实名认证和防沉迷新策略。 -对于没有版号的游戏,可以接入 TDS 推出的 [实名认证防沉迷](/v4/sdk/anti-addiction/features/)。 - -### 防沉迷和登录有什么关系? - -防沉迷依赖于 TapTap 登录 SDK,详情参考 [开发指南](/v4/sdk/anti-addiction/guide/) 。同时,配套使用 TapTap 登录 + 合规认证服务,可实现自动静默授权、认证,无需玩家手动授权、输入实名信息等,极大提升流畅度。即使开发者需要使用必须经玩家手动同意的授权项,配套使用 TapTap 登录 + 合规认证服务 也可让玩家通过「快速认证服务」便捷的完成实名。 - - -### 实名认证通过后退出账号,再次启动游戏,会跳过实名认证流程 - -家首次进入游戏会触发实名认证弹窗,让玩家授权游戏获取 TapTap 实名信息或输入身份信息,认证通过后,后面每一次以同样的 `userIdentifier` (唯一标识)调用认证接口都会直接拿到第一次认证的结果,不再触发弹窗。如玩家需要换用其他账号登录,需要主动退出账号。 - - - -### 如何开通实名认证与防沉迷服务 - -可以在 **开发者中心后台 > 游戏服务 > 开发与构建 > 合规认证** 处自助开通服务。目前提供两种方案,游戏开通时选择其中一种: - -* **有版号**。完成控制台提示的前置条件,点击开通,然后配置好 [中宣部参数](/v4/sdk/anti-addiction/features/#注册中宣部实名认证系统): - * 将中宣部系统后台的**游戏备案识别码、应用标识、应用密钥**填写到 TapTap 开发者中心后台对应处。 - * 将 TapTap 开发者中心后台显示的 **IP 白名单地址**复制、填写到中宣部系统后台。 -* **暂无版号**。无版号游戏无法配置中宣部参数,可直接选择开通,等游戏有了版号,可以配置 [中宣部参数](/v4/sdk/anti-addiction/features/#注册中宣部实名认证系统) 并切换到有版号方案。切换后客户端代码不受影响,不需要修改。 - -### SDK 自带哪些用户界面(UI) - -实名认证和防沉迷 SDK 提供的用户界面主要在防沉迷授权阶段,可参考 [功能介绍文档](/v4/sdk/anti-addiction/features/) 中的界面预览。 - -### 授权失败 - -在实名认证时使用 TapTap 快速认证服务提示「授权失败」,请至后台 [配置签名证书](/v4/sdk/access/quickstart/#配置签名证书)。 - -### 未查询到实名认证配置 - -实名认证时使用手动输入实名信息服务提示「未查询到实名认证配置」,原因是未开启实名认证服务,需要在 开发者中心后台 > 游戏服务 > 开发与构建 > 合规认证 处自助开通服务。 - -### userIdentifier is empty - -调用认证接口时需要传入的玩家唯一标识 userIdentifier 参数值为空,建议开发者对此做非空判断。 - -### 未弹出实名认证窗口/未收到回调 - -这种情况一般是仅调用了初始化防沉迷 UI 模块代码,也就是说只完成了 SDK 的初始化,同时注册防沉迷的消息监听。 - -触发实名认证弹窗**必须调用** [认证接口](/v4/sdk/anti-addiction/guide/#防沉迷认证),之后才会收到回调。 - -### 重复认证 - -我们预期同一个玩家认证过一次之后不再触发弹窗,防沉迷服务直接使用第一次认证的结果,这样用户体验更好。 - -如果出现重复认证,可以按照以下思路排查: - -* 首先确认游戏使用的 [玩家唯一标识 userIdentifier](#玩家唯一标识-useridentifier-参数说明) 符合要求。如果同一个玩家用的 userIdentifier 会发生改变,在防沉迷服务中会被视为不同用户,导致重复认证。这个时候需要游戏传入合适的 userIdentifier,建议使用 [单纯 TapTap 用户认证](/v4/sdk/taptap-login/guide/),传入 `openid` 或 `unionid` 作为玩家唯一标识。 -* 如果是**有版号游戏**,请确认在 开发者中心后台游戏服务 > 开发与构建 > 合规认证 处填写的参数无误。如果参数有问题,请求中宣部接口会失败,导致重复认证。除此之外,还需要确定应用在中宣部是否审核通过、接口测试是否完成。请按照如下步骤进行排查: - * **IP 白名单地址**全部填入中宣部系统后台。 - * **游戏备案识别码、应用标识**和中宣部后台保持一致。 - * **应用密钥**在有效期内(有效期为半年,注意在失效前更新)。 - * **中宣部接口测试**是否完成,完成的状态应该为**已通过**,中宣部目前需要测试的接口用例为 8 个。 - * 检查游戏在中宣部是否处于审核通过状态。 -### iOS 使用快速认证完成后,实名弹窗未自动关闭 - -可以参考 [配置跳转 TapTap 应用](/v4/sdk/taptap-login/guide/)文档,在 info.plist 中添加配置; - -## 注意事项 - -### 玩家唯一标识 userIdentifier 参数说明 - -第一次认证会触发实名认证弹窗,让玩家授权游戏获取 TapTap 实名信息或输入身份信息,认证通过后,后面每一次以同样的 userIdentifier 调用认证接口都会直接拿到第一次认证的结果,不再触发弹窗。 - -因此,同一个用户的唯一标识应该要保证唯一性。 - -### 测试实名认证环境 - -无论是 Android 还是 iOS 项目,不支持在 Unity Editor 环境里调试,请对应打包到真实设备或者移动端的模拟器中进行测试实名认证防沉迷的相关功能。 - - -### 使用 TapTap 快速认证报:获取实名信息失败,请稍后重试。 - -- 检查 TapTap 客户端登录的账号是否在 TapTap 客户端进行了实名,TapTap 客户端登录的账号未在 TapTap 客户端实名时进行 TapTap 快速认证并不会报该异常,而是跳转到 TapTap 客户端进行实名认证。 -- 检查设备时间是否开启联网同步了,设备时间不准确也会导致该异常发生。 diff --git a/versioned_docs/version-v4/sdk/anti-addiction/features.mdx b/versioned_docs/version-v4/sdk/anti-addiction/features.mdx deleted file mode 100644 index c6d053632..000000000 --- a/versioned_docs/version-v4/sdk/anti-addiction/features.mdx +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: 实名认证和防沉迷功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -合规认证提供实名认证、防沉迷服务,帮助开发者达成国家新闻出版署 [《关于进一步严格管理 切实防止未成年人沉迷网络游戏的通知》](https://www.nppa.gov.cn/xxfb/tzgs/202108/t20210830_666285.html) 要求。 -开发者请按功能指引和开发指南文档规范接入,若恶意绕过防沉迷功能,违反防沉迷要求,开发者将承担全部法律责任,并且相关游戏将被下架。 - -:::tip -暂无版号的游戏也可以使用实名认证和防沉迷功能。 -::: - - -## 准备工作 - -在使用实名认证和防沉迷服务之前,需要在对应的游戏里找到 **游戏服务 > 合规认证** 开通服务。如下图,可选「已有版号」或「暂无版号」方案,然后点击立即开通即可。 - -![](https://img.tapimg.com/market/images/8dfc08c01bb0ae1ea62a2f574acdbc49.png) - -「已有版号」或「暂无版号」方案根据游戏的实际情况选择。 - -### 暂无版号 - -版号还未申请或正在国家新闻出版署审核中的游戏,可选「暂无版号」方案。游戏拿到版号后,可以再切换到有版号方案。「暂无版号」方案只需要点击上图的立刻开通按钮,就操作完成了。 - -### 已有版号 - -#### 注册中宣部实名认证系统 - -有版号游戏需要完成中宣部实名认证系统的注册,无版号游戏暂时不需要注册。注册步骤如下: - -在 [中宣部网络游戏防沉迷实名认证系统](https://wlc.nppa.gov.cn/fcm_company/index.html#/login?redirect=%2F) 完成应用的注册,应用注册后还需要完成中宣部「接口测试」并且游戏需要通过中宣部的审核。 - -获取相关凭证: - -- 游戏备案识别码 `bizId` -- 应用标识 `APPID` -- 应用密钥 `Secret Key` - -![](/img/anti-addiction/biz-id.png) - -![](/img/anti-addiction/secretkey.png) - -游戏在中宣部的审核状态必须为「审核通过」的状态。 - -![](https://capacity-files.lcfile.com/BC7gUR6wfpOh9PHj8XwKybxseeHhl6Fq/anti-examine-success.png) - -游戏在中宣部必须完成接口测试,中宣部目前需要测试的接口用例为 8 个,需要注意的是在中宣部 **测试接口 > 预置参数 > IP 白名单** 这里的「IP 白名单」需要配置游戏侧自己的公网 IP,中宣部接口测试是临时性的,通过之后便不再使用了。 - -![](/img/anti-addiction/testcase.png) - -然后在 TapTap 开发者中心后台完成参数配置,使用 TapTap 开发者中心后台提供的 IP 白名单地址填入中宣部系统后台: - -![](https://img.tapimg.com/market/images/37abdc0ce0f37e0ad199761d370cb8d6.png) - -### 接入 TapTap 登录 -防沉迷模块依赖于 [TapTap 登录模块](https://developer.taptap.cn/docs/sdk/taptap-login/features/),开发者接入防沉迷前应先接入 TapTap 登录相关依赖。 - - -## 接入实名认证和防沉迷服务 - -完成准备工作后,方可接入实名认证和防沉迷服务SDK。 - -按照国家新闻出版署[《关于进一步严格管理切实防止未成年人沉迷网络游戏的通知》](https://www.nppa.gov.cn/xxfb/tzgs/202108/t20210830_666285.html),各游戏出版运营企业均须在游戏内落实游戏实名认证和防沉迷新策略。《通知》中要求,所有用户必须使用真实有效身份信息进行游戏账号注册并登录网络游戏。 - -合规认证服务为开发者自动对接了中宣部网络游戏防沉迷实名认证系统,并以开发者主体进行信息上报,符合中宣部对游戏企业接入的合规要求。 - -### 自动静默认证 - -推荐配套使用 TapTap 登录 + 合规认证服务( 3.29.0 及以上版本 SDK),可实现自动静默授权、认证,无需玩家手动授权、输入实名信息等,极大提升流畅度。 - -### TapTap 快速认证 - -合规认证提供 TapTap 快速认证服务,便于玩家使用 TapTap 账号中通过国家认证的实名信息,快速完成游戏实名认证流程。 - -:::tip -推荐配套使用 TapTap 登录 + 合规认证服务( 3.29.0 及以上版本 SDK),快速认证弹窗步骤也可省略,实现自动静默授权、认证,无需玩家手动授权、输入实名信息等。 -::: - -![](/img/anti-addiction/image2021-10-18_17-57-51.png) - -- 若玩家点击「使用」并同意授权后,将允许使用其 TapTap 账号中通过国家认证的实名信息,快速完成游戏实名认证流程。注意,当玩家使用 TapTap 登录时,通过授权步骤即可快速完成实名认证。若玩家拒绝授权,将出现此快速认证弹窗,此时仅展示「使用」快速认证服务按钮,以便玩家尽快完成实名。 - -![](/img/anti-addiction/image2021-10-19_17-4-12.png) - -- 若用户点击「不使用」,将唤起「手动填写实名信息」弹窗,用户填写并提交身份信息后完成游戏实名认证流程。 - - -## 防沉迷策略 - -《通知》中要求,游戏企业必须严格管理未成年的游戏时间以及付费额度。SDK 封装了相应接口,游戏可通过云端查询未成年玩家是否处于可以游戏的时间段内,以及未成年玩家的某笔消费是否受限。 - -![](/img/anti-addiction/not-allow-play.png) - -### 游戏时间限制 - -仅允许未成年人在**周五、周六、周日和法定节假日的 20:00 至 21:00** 进行游戏。 - -### 充值额度限制 - -- 不满 8 岁,无法在游戏内进行充值。 - -**同一网络游戏企业**所提供的游戏付费服务需要满足: - -- 满 8 岁,不满 16 岁的用户,单次充值 ≤ 50 元 ,单月充值累计 ≤ 200 元 -- 满 16 岁,不满 18 岁的用户,单次充值 ≤ 100 元 ,单月充值累计 ≤ 400 元 - -注意,计算限制时,同一企业的所有网络游戏的充值共同累计。 -例如,假定一家企业名下有两款网络游戏,一个满 8 岁,不满 16 岁的用户某月在其中一款游戏下累计充值了 100 元,那么他在另一款游戏下最多只能再充 100 元。 - - -## 测试账号 - -:::tip -无论是否有版号,都可以使用测试账号。最新的测试账号功能,已经去除了测试模式,同时不强制使用 unionid 作为玩家唯一标识,你可以使用自定义的唯一标识。 -::: - -![](https://img.tapimg.com/market/images/e971df09e5496b35b4c321daa34fa657.png) - -测试账号根据使用场景分为两大类: -- 自测专用:对游戏团队内部,用于进行实名认证防沉迷功能测试的测试账号。 -- 版审专用:提供给版审单位,用于申请游戏版号的测试账号。 - -### 自测专用测试账号 -- 可自主设置账号可玩时段,便于测试防沉迷时间限制 -- 可自主设置账号已充值金额,便于测试防沉迷消费限制 -- 未实名账号测试实名后,可自主重置回未实名状态,便于多次测试账号实名 -- 共提供 12 个不同实名状态、年龄段测试账号 - -### 版审专用测试账号 -- 不可设置可玩时段和已充值金额,避免版审时误操作导致审核驳回 -- 未实名账号测试实名后,可自主重置回未实名状态,用于多次版审时,未实名账号均已实名,需要恢复未实名的场景 -- 提供 30 个标准版审要求测试账号,及 12 个备用测试账号 - - 未实名空账号一组,共 3 个 - - 未成年人账号两组,共 18 个,每组含高、中、低(满 16 周岁未满 18 周岁,满 8 周岁未满 16 周岁,未满 8 周岁)各 3 个 - - 成年人账号一组(满 18 周岁),共 9 个 - - 12 个备用账号包含:满 8 周岁未满 12 周岁账号 6 个;满 18 周岁账号 6 个 - -实名认证防沉迷提供的测试账号强依赖于 TapTap 登录,请确保已完成对应能力接入。 - - -## 可玩年龄限制 - -![](https://img.tapimg.com/market/images/9fbbed39c52b2fbf963106478b73aea9.png) - -开启该功能,可限制未满所配置年龄的玩家进入游戏。 -该功能主要用于:希望根据游戏适龄、审核要求等情况,在《关于进一步严格管理切实防止未成年人沉迷网络游戏的通知》 要求基础上,额外限制「8/12/16/18 周岁以下用户不可游戏」的场景。 - -该功能需在 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 合规认证 > 可玩年龄限制** 中开启使用。对应开发文档请参考 [可玩年龄限制开发指南](/v4/sdk/anti-addiction/guide) diff --git a/versioned_docs/version-v4/sdk/anti-addiction/guide.mdx b/versioned_docs/version-v4/sdk/anti-addiction/guide.mdx deleted file mode 100644 index 67b1e96ec..000000000 --- a/versioned_docs/version-v4/sdk/anti-addiction/guide.mdx +++ /dev/null @@ -1,582 +0,0 @@ ---- -title: 实名认证和防沉迷开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '../../sdkVersions'; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -:::tip -使用合规认证服务之前,需要在 **开发者中心后台 > 游戏服务 > 开发与构建 > 合规认证** 处开通服务,可选择「已有版号」或「暂无版号」方案。 -::: - -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -|----------|----------------|-----------------| -| 网络权限 | 用于正常网络请求 | 用户首次使用该功能时会申请权限 | -| 获取网络状态权限 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下权限: - -```xml - - -``` - - - -<> - - - - - -## 集成前准备 - -1. 参考 [准备工作](/v4/sdk/access/get-ready/) 创建应用、开启应用配置。 -2. 参考[合规认证功能介绍](/v4/sdk/anti-addiction/features/#准备工作)中准备工作开通防沉迷服务。 -3. 合规认证模块依赖于 [TapTap 登录模块](/v4/sdk/taptap-login/features),开发者接入前应先接入 TapTap 登录 相关依赖。 - -## SDK 配置 - -可以在 [下载页](/tap-download) 获得 TapSDK,引入防沉迷模块。 - - - -<> - - - - - -<> -1. 项目根目录的 build.gradle 添加仓库地址: - -```groovy -allprojects { - repositories { - google() - mavenCentral() - } -} -``` - -2. app module 的 build.gradle 添加对应依赖: - -{ -`dependencies { - implementation 'com.taptap.sdk:tap-core:${sdkVersions.taptap.android}' - implementation 'com.taptap.sdk:tap-compliance:${sdkVersions.taptap.android}' -}` -} - - -<> - -iOS 提供通过添加 cocosPod 远程依赖和使用本地文件导入两种集成方式,推荐使用远程依赖方式。 - -#### 远程依赖 - -1. 在工程 Podfile 文件中对应模块下添加依赖: - -{` pod 'TapTapComplianceSDK', '~> ${sdkVersions.taptap.ios }'`} - -2. 执行 `Pod install` 下载对应依赖文件 -3. 将工程 Pods 目录下 `TapTapCompilanceSDK/Frameworks/TapTapComplianceResource.bundle` 和 `TapTapLoginSDK/Frameworks/TapTapLoginResource.bundle` - 等资源文件导入工程中 - -#### 本地文件依赖 - -合规认证依赖于初始化和 TapTap 登录模块,使用本地文件方式添加依赖时,需先参考 [TapSDK 集成](/v4/sdk/access/quickstart/#本地文件依赖) 和 [TapTap 登录](/v4/sdk/taptap-login/guide/#本地文件依赖) 添加对应本地文件依赖项。 - -1. 在下载页下载如下文件: - -- `TapTapComplianceSDK.xcframework` 合规认证依赖库 -- `TapTapComplianceResource.bundle` 合规认证资源文件 - -2. 在工程中添加 `framework` 静态库,注意添加时选择 Embed 方式为 **Do Not Embed**,导入 `bundle` 资源文件 - - - - - -防沉迷 SDK 需要联网和发送请求数据的权限,请开发者注意在项目中声明相应权限。 - -## 初始化 - -合规认证模块支持设置**是否显示切换账号**和**使用年龄段信息**,开发者可在 [TapSDK 初始化](/v4/sdk/access/quickstart#初始化) 时添加对应的配置,具体如下: - - -<> - -```cs -using TapSDK.Core; -using TapSDK.Compliance; - -// 核心配置 -TapTapSdkOptions coreOptions = new TapTapSdkOptions(); -// TODO: coreOptions 应用参数配置 - -// 合规认证配置 -TapTapComplianceOption complianceOption = new TapTapComplianceOption -{ - showSwitchAccount = true, - useAgeRange = false -}; -// 其他模块配置项 -TapTapSdkBaseOptions[] otherOptions = new TapTapSdkBaseOptions[] -{ - complianceOption -}; -// TapSDK 初始化 -TapTapSDK.Init(coreOptions, otherOptions); -``` - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapSdk -import com.taptap.sdk.core.TapTapSdkOptions -import com.taptap.sdk.compliance.options.TapTapComplianceOptions -import com.taptap.sdk.core.TapTapLanguage -import com.taptap.sdk.core.TapTapRegion - -TapTapSdk.init( - context = context, - sdkOptions = TapTapSdkOptions( - clientId = clientId, - clientToken = clientToken, - region = TapTapRegion.CN, - preferredLanguage = TapTapLanguage.ZH_HANS, - enableLog = false - ), - options = arrayOf( - TapTapComplianceOptions( - // - showSwitchAccount = true, - // - useAgeRange = false, - ) - ) -) -``` - - -<> - -```swift -import TapTapComplianceSDK -import TapTapCoreSDK - -// 核心配置 -let coreOptions = TapTapSdkOptions() -// TODO: coreOptions 应用参数配置 - -// 合规认证配置 -let complianceOptions = TapTapComplianceOptions() -complianceOptions.useAgeRange = false -complianceOptions.showSwitchAccount = true - -// 其他模块配置项 -let otherOptions = [complianceOptions] -// TapSDK 初始化 -TapTapSDK.initWith(coreOptions, otherOptions: otherOptions) -``` - - - - - -##### 参数说明 - -- `showSwitchAccount` 是否显示切换账号按钮,默认为 false 。如果游戏支持切换账号功能则可以设置为 true,当玩家点击切换账号按钮后,SDK 会触发 `1001` - 回调,游戏可根据该回调 code 做相应处理 -- `useAgeRange` 游戏是否需要获取真实年龄段信息,默认为 true 。当为 true 时,使用 Tap 实名时会需要用户主动授权,当设置为 false 时,使用 Tap 实名时会进行无 UI 交互的静默授权 - -![切换账号界面](/img/anti-addiction/switch-account.png) - -## 回调设置 - -为了处理用户在进行合规认证时触发的不同事件,游戏需通过设置回调进行监听。示例如下: - - -<> - -```cs -using TapSDK.Compliance; - -private Action callback => (code, s) => { - // do something -}; - -TapTapCompliance.RegisterComplianceCallback(callback); -``` - - -<> - -```kotlin -import com.taptap.sdk.compilance.TapTapCompliance -import com.taptap.sdk.compilance.TapTapComplianceCallback - -TapTapCompliance.registerComplianceCallback( - callback = object : TapTapComplianceCallback { - - override fun onComplianceResult(code: Int, extra: Map?) { - when (code) { - LOGIN_SUCCESS -> { - // 登录成功 - } - - EXITED -> { - // 退出登录 - } - - SWITCH_ACCOUNT -> { - // 切换账号 - } - } - } - } -) -``` - - -<> - -```swift -import TapTapComplianceSDK - -// 实现 TapTapComplianceDelegate 协议 -extension GameMainController: TapTapComplianceDelegate { - func complianceCallback(with code: TapComplianceResultHandlerCode, extra: String?) { - print("TapCompliance result code = \(code.rawValue)") - } -} -// 注册合规认证回调 -TapTapCompliance.register(self) -``` - - - - - -回调参数中 code 用于标识回调类型, extra 为对应提示信息 - -| 回调 code | 回调类型 | 触发逻辑 | -|---------|-----------------------------------|--------------------------------------------------| -| 500 | `LOGIN_SUCCESS` | 玩家未受到限制,正常进入游戏 | -| 1000 | `EXITED` | 退出防沉迷认证及检查,当开发者调用 Exit 接口时或用户认证信息无效时触发,游戏应返回到登录页 | -| 1001 | `SWITCH_ACCOUNT` | 用户点击切换账号,游戏应返回到登录页 | -| 1030 | `PERIOD_RESTRICT` | 用户当前时间无法进行游戏,此时用户只能退出游戏或切换账号 | -| 1050 | `DURATION_LIMIT` | 用户无可玩时长,此时用户只能退出游戏或切换账号 | -| 1100 | `AGE_LIMIT` | 当前用户因触发应用设置的年龄限制无法进入游戏 | -| 1200 | `INVALID_CLIENT_OR_NETWORK_ERROR` | 数据请求失败,游戏需检查当前设置的应用信息是否正确及判断当前网络连接是否正常 -| 9002 | `REAL_NAME_STOP` | 实名过程中点击了关闭实名窗,游戏可重新开始防沉迷认证 | - -## 开始认证 - -防沉迷开始认证时需传入玩家唯一标识 `userIdentifier`,建议使用[TapTap 用户认证](/v4/sdk/taptap-login/guide/#获取用户信息) -中的 `openid` 或 `unionid`。 - - - -<> - -```cs -using TapSDK.Compliance; - -TapTapAccount account = await TapTapLogin.Instance.GetCurrentAccount(); -if (account != null) { - string userIdentifier = account.uniontId; - TapTapCompliance.Startup(userIdentifier); -} -``` - - -<> - -```kotlin -import com.taptap.sdk.compilance.TapTapCompliance - -TapTapCompliance.startup(activity = this, userId = "userId") -``` - - -<> - -```swift -import TapTapComplianceSDK -import TapTapLoginSDK - -let userIdentifier = TapTapLogin.currentAccount?.userInfo?.unionId -TapTapCompliance.startup(userIdentifier) -``` - - - - - -## 充值额度限制 - -不同年龄段的玩家,充值金额有不同的上限,开发者需在玩家充值前检查是否受限,并在玩家成功充值后上报充值金额,具体流程如下: - -### 检查充值限制 - -游戏在收到玩家的充值请求后,调用以下接口检查本次充值行为是否受限: - - - -```cs -using TapSDK.Compliance; - -// 充值金额,单位为分 -var amount = 1000; - -TapTapCompliance.CheckPaymentLimit( - amount: amount, - handleCheckPayLimit: checkResult => - { - // 充值 status 结果: {checkResult.status} - // status 为 1 时可以继续支付流程,为 0 时 SDK 会弹出对应提示窗口,开发者不需要额外处理 - }, - handleCheckPayLimitException: errorMsg => - { - // 检查充值异常, 处理异常 - } -); -``` - -```kotlin -import com.taptap.sdk.compilance.TapTapCompliance -import com.taptap.sdk.kit.internal.callback.TapTapCallback - -TapTapCompliance.checkPaymentLimit( - activity = this, - amount = amount, - callback = object : TapTapCallback { - - override fun onSuccess(result: CheckPaymentResult) { - if (result.status) { - // 充值没有限制 - } - // 在充值被限制时,SDK 会弹出对应提示窗口,开发者不需要额外处理 - } - - override fun onFail(exception: TapTapException) { - // 查询失败,开发者可以继续重试,但建议最多重试 3次 - } - } -) -``` - -```swift -import TapTapComplianceSDK - -// 充值金额,单位为分 -let amount = 100 -TapTapCompliance.checkPaymentLimit(amount) { status, title, desc in - // 充值没有限制 - if status { - // TODO: 完成后续充值流程 - } - // 在充值被限制时,SDK 会弹出对应提示窗口,开发者不需要额外处理 -} failureHandler: { msg in - // 查询失败,开发者可以继续重试,但建议最多重试三次 -} -``` - - - -充值金额的单位为分。 - -### 上报充值金额 - -当玩家充值成功后,开发者需调用如下接口上报玩家充值金额: - - - -```cs -using TapSDK.Compliance; - -// 充值金额,单位为分 -long amount = 100; -TapTapCompliance.SubmitPayment( - amount: amount, - handleSubmitPayResult: () => - { - // 成功 - }, - handleSubmitPayResultException: (exception) => - { - // 处理异常 - } -); -``` - -```kotlin -import com.taptap.sdk.compilance.TapTapCompliance -import com.taptap.sdk.kit.internal.callback.TapTapCallback - -TapTapCompliance.submitPayment( - amount = amount, - callback = object : TapTapCallback { - override fun onSuccess(result: JsonElement) { - // 上报充值成功 - } - - override fun onFail(exception: TapTapException) { - // 上报充值失败 - } - } -) -``` - -```swift -import TapTapComplianceSDK - -// 充值金额,单位为分 -let amount = 100 -TapTapCompliance.submitPayment(amount) { success in - // 上报成功 -} failureHandler: { msg in - // 上报失败,开发者可以继续重试,但建议最多重试三次 -} -``` - - - -上报充值金额时,传入的充值金额的单位同样为分。 - -## 退出认证 - -玩家在游戏内退出账号时调用,重置防沉迷状态。 - - - -```cs -using TapSDK.Compliance; - -TapTapCompliance.Exit(); -``` - -```kotlin -import com.taptap.sdk.compilance.TapTapCompliance - -TapTapCompliance.exit() -``` - -```swift -import TapTapComplianceSDK - -TapTapCompliance.exit() -``` - - - -## 其他 - -### 获取玩家年龄段 - -开发者可调用如下接口获取玩家所处年龄段: - - - -```cs -using TapSDK.Compliance; - -int ageRange = await TapTapCompliance.GetAgeRange(); -``` - -```kotlin -import com.taptap.sdk.compilance.TapTapCompliance - -TapTapCompliance.getAgeRange() -``` - -```swift -import TapTapComplianceSDK - -let ageRange = TapTapCompliance.getAgeRange().rawValue -``` - - - -上例中的 `ageRange` 是一个整数,表示玩家所处年龄段的下限(最低年龄)。 - -| 类型数值 | 含义 | -|------|-----------| -| -1 | 未知 | -| 0 | 0 到 7 岁 | -| 8 | 8 到 15 岁 | -| 16 | 16 到 17 岁 | -| 18 | 成年玩家 | - -`-1` 表示「未知」,说明该用户还未实名或未授予年龄段权限,通常有以下几个原因: - -1. 开发者在用户未完成实名时调用该接口,应该在收到用户是否可进入游戏的回调时(回调 code 为 500 / 1030 / 1050)时,再进行调用 -2. 在防沉迷初始化时配置的参数 useAgeRange 设置为 false 导致,需设置为 true -3. 该游戏无版号且在 TapPlay 中运行 - -### 获取剩余时长 - -获取玩家当前剩余时长: - - - -```cs -using TapSDK.Compliance; - -int time = await TapTapCompliance.GetRemainingTime(); // 单位:秒 -``` - -```kotlin -import com.taptap.sdk.compilance.TapTapCompliance - -TapTapCompliance.getRemainingTime() -``` - -```swift -import TapTapComplianceSDK - -let remainingTimeInSeconds = TapTapCompliance.getRemainingTime() // 单位:秒 -``` - - - -### 设置适龄限制 - -当除了需要满足防沉迷政策外,应用需要对用户年龄有额外限制时,例如只允许 16 周岁以上使用,开发者可在 开发者中心页面配置对应的年龄限制,SDK 将在用户完成实名后 -且 根据时长限制规则显示 UI 前检查用户是否符合游戏要求,满足要求时,SDK 会继续进行后续时长业务及回调处理,否则会直接返回 code 为 `1100` 的年龄限制回调通知开发者。 \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/anti-addiction/practice.mdx b/versioned_docs/version-v4/sdk/anti-addiction/practice.mdx deleted file mode 100644 index a9205bdb9..000000000 --- a/versioned_docs/version-v4/sdk/anti-addiction/practice.mdx +++ /dev/null @@ -1,473 +0,0 @@ ---- -title: 最佳实践 -sidebar_label: 最佳实践 -sidebar_position: 3 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/src/docComponents/sdkVersions'; - - -## 准备工作 - -### 创建应用获取应用参数 -在 [TapTap 开发者中心](https://developer.taptap.cn) 创建游戏应用,获取应用 Client ID、Client Token 等参数,用于初始化 SDK; - -![](https://capacity-files.lcfile.com/nnQKxgJJzgErOlIOcxnbIHt8Vc1RmGYe/tap_get_ready.png) - -### 开通 TapTap 登录服务 -合规认证服务依赖于 TapTap 登录服务,因此,厂商需要在 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 应用配置** 开启「TapTap 登录」; - -![](https://img.tapimg.com/market/images/168f902edd3de84cf0d5eb5fa640e78d.png) - -### 配置应用包名和签名信息 -Android 签名处填写 MD5 值,详情可参考:[如何获取 MD5 值](/v4/sdk/access/android-md5); - -![](https://img.tapimg.com/market/images/3c725fc6859363f630d90471d0c8929b.png) - -### 开通合规认证服务 -找到 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 开发与构建 > 合规认证**,根据游戏实际情况,选择「已有版号」或「暂无版号」方案,然后点击**立即开通** - -![](https://img.tapimg.com/market/images/90e6d759ba528aa7e9ef29077387edbb.png) - -:::tip -若游戏选择的是「已有版号」方案则还需要完成中宣部实名认证系统的注册以及相应配置,具体的操作请参考 [注册中宣部实名认证系统](/v4/sdk/anti-addiction/features/#已有版号) -::: - -### 可玩年龄限制 -应用需要对用户年龄有额外限制时,可在 **TapTap 开发者中心 > 你的游戏 > 游戏服务 > 合规认证 > 可玩年龄限制** 中开启此功能,并配置所需的**最低年龄要求** - -在后续的代码集成中,也需要处理相关的 `1100` 回调 - -![](https://img.tapimg.com/market/images/e7fab2ce4099b792bd32390c04e28986.png) - -## 代码接入 - -下面模拟一个小游戏来进行接入示例,该游戏主要包括两个场景: -- 登录场景 LoginScene: 用于初始化 SDK 、用户登录、切换账号、显示合规认证异常提示 UI 等 -- 游戏商店及设置场景 GameStoreAndSettignsScene: 用于展示充值页面、设置菜单等 - -另外,因合规认证模块在游戏整个生命周期中运行,所以通过全局的单例管理工具类 GameSDKManager 来处理 SDK 的初始化及回调。 - -完整示例代码可参考 [TDS-Unity-Demo](https://github.com/taptap/TDS-Unity-Demo) - -### 导入 SDK 包体 - - - -<> - -在游戏项目的 `Packages/manifest.json` 文件中添加以下依赖: - - - {`"dependencies":{ - "com.taptap.tds.login":"https://github.com/TapTap/TapLogin-Unity.git#${sdkVersions.taptap.unity}", - "com.taptap.tds.common":"https://github.com/TapTap/TapCommon-Unity.git#${sdkVersions.taptap.unity}", - "com.tapsdk.antiaddiction":"https://github.com/TapTap/TapAntiAddiction-Unity.git#${sdkVersions.taptap.unity}", -}`} - - - -在 Unity 顶部菜单中选择 **Window > Package Manager** 可查看已经安装在项目中的包。 - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - - - -### 初始化与设置回调 - -在 GameSDKManager 工具类中完成 SDK 的初始化及全局回调的设置,示例如下: - - - -<> - -```cs -using System; -using TapSDK.Login; -using TapSDK.Compliance; -using UnityEngine; - -/// -/// SDK 初始化及合规认证回调处理管理类 -/// -public sealed class GameSDKManager -{ - // 游戏在 TapTap 开发者中心对应的 Client ID - private readonly string clientId = "游戏的 Client ID"; - // 游戏在 TapTap 开发者中心对应的 Client Token - private readonly string clientToken = "游戏的 Client Token"; - - // 是否已初始化 - private readonly bool hasInit = false; - - // 是否已通过合规认证检查 - public bool hasCheckedCompliance { get; private set; } - - private static readonly Lazy lazy - = new Lazy(() => new GameSDKManager()); - - public static GameSDKManager Instance { get { return lazy.Value; } } - - private GameSDKManager() { } - - // 声明合规认证回调 - private readonly Action ComplianceCallback = (code, errorMsg) => - { - // 根据回调返回的参数 code 添加不同情况的处理 - switch (code) - { - - case 500: // 玩家未受限制,可正常进入 - Instance.hasCheckedCompliance = true; - // TODO: 显示开始游戏按钮 - break; - - case 1000: // 防沉迷认证凭证无效时触发 - case 1001: // 当玩家触发时长限制时,点击了拦截窗口中「切换账号」按钮 - case 9002: // 实名认证过程中玩家关闭了实名窗口 - TapLogin.Logout(); // 如果游戏有其他账户系统,此时也应执行退出 - // TODO: 切换到登录页面 例如:SceneManager.LoadScene("Login"); - break; - - case 1100: // 当前用户因触发应用设置的年龄限制无法进入游戏 - // TODO: 游戏应自行绘制适龄限制提示,并引导玩家退出游戏 - break; - - case 1200: // 数据请求失败,应用信息错误或网络连接异常 - // TODO: 引导玩家确认网络连接是否正常,并重新调用开始认证接口 - break; - - default: - Debug.Log("其他可选回调"); - break; - } - - }; - - /// - /// 初始化 TapSDK 并注册合规认证回调 - /// - public void InitSDK() - { - if (!hasInit) - { - TapTapSdkOptions coreOptions = new TapTapSdkOptions - { - clientId = clientId, - clientToken = clientToken - }; - TapTapComplianceOption complianceOption = new TapTapComplianceOption - { - showSwitchAccount = false, // 是否显示切换账号按钮 - useAgeRange = true // 是否使用年龄段信息 - }; - // 创建其他选项数组 - TapTapSdkBaseOptions[] otherOptions = new TapTapSdkBaseOptions[] - { - complianceOption - }; - TapTapSDK.Init(coreOptions, otherOptions); - TapTapCompliance.RegisterComplianceCallback(ComplianceCallback); - } - } - - /// - /// 开始合规认证检查 - /// - /// 用户唯一标识 - public void StartCheckCompliance(string userIdentifier) - { - hasCheckedCompliance = false; - TapTapCompliance.Startup(userIdentifier); - } -} -``` - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - - -### TapTap 登录 & 开始实名认证 - -登录场景为游戏的第一个场景,在该脚本中需调用 `GameSDKManager` 中初始化接口,并实现 Tap 登录、开始合规认证,示例如下: - -:::tip -TapTap 登录按钮素材需要使用官方提供的[登录按钮素材](https://assets.tapimg.com/img/TapTap_Login_Source.zip); -::: - - - -<> - -```cs -using UnityEngine; -using System; -using TapSDK.Login; -using TapSDK.Core; - -/// -/// 登录场景 -/// -public class LoginScene : MonoBehaviour -{ - /// - /// 初始化 SDK 并判断本地是否已登录,已登录时开始合规认证检查,否则显示登录按钮 - /// - async void Start() - { - // 初始化 SDK - GameSDKManager.Instance.InitSDK(); - - TapTapAccount account = null; - try - { - // 检查本地是否已存在 TapToken - account = await TapTapLogin.Instance.GetCurrentAccount(); - } - catch (Exception e) - { - Debug.Log("本地无有效 token"); - } - - if (currentToken == null) - { - // TODO: 显示登录按钮 - } - else - { - // 如果当前还未通过合规认证检查,开始认证 - if (!GameSDKManager.Instance.hasCheckedCompliance) - { - // 开始合规认证检查 - StartCheckCompliance(); - } - } - } - - /// - /// 登录按钮点击后执行 Tap 登录 - /// - public async void OnTapLoginButtonClick() - { - try - { - // 发起 Tap 登录并获取用户信息 - var account = await TapTapLogin.Instance.Login([TapTapLogin.TAP_LOGIN_SCOPE_PUBLIC_PROFILE]); - - // 开始合规认证检查 - StartCheckCompliance(); - } - catch (Exception e) - { - // 登录取消或错误,提示用户重新登录 - Debug.Log("用户登录取消或错误"); - } - } - - /// - /// 开启合规认证检查 - /// - public async void StartCheckCompliance() - { - // 获取当前已登录用户的 Profile 信息 - TapTapAccount account = null; - try - { - account = await TapTapLogin.Instance.GetCurrentAccount(); - } - catch (Exception exception) - { - Debug.Log($"获取 Profile 信息出现异常:{exception}"); - } - if (account == null) - { - // 无法获取 Profile 时,登出并显示登录按钮 - TapTapLogin.Instance.Logout(); - // TODO: 显示登录按钮 - return; - } - - // 使用当前 Tap 用户的 unionid 作为用户标识进行合规认证检查 - string userIdentifier = account.uniontId; - GameSDKManager.Instance.StartCheckCompliance(userIdentifier); - } - -} -``` - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - - -### 限制未成年人消费额度 - -:::tip -每次充值之前,游戏侧请 **务必** 调用 `CheckPayLimit` 接口进行判断,检验当前玩家的充值行为是否被限制。充值金额的单位为分。 -::: - - - -<> - -```cs -// 100 表示 100 分,即 1 元 -long amount = 100; -TapTapCompliance.CheckPayLimit(amount, (result) => - { - // 获取检查状态 - int status = result.status; - - // 当前充值不受限 - if (status == 1) - { - // TODO: 进行后续充值流程,充值完成后调用上报充值金额接口 - } - else - { - // 本次充值触发合规限制,游戏侧需停止后续充值流程 - } - }, - (exception) => - { - // TODO: 处理参数或网络异常后重试 - } -); -``` - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - - - -:::tip -充值成功后,游戏侧请 **务必** 调用 `SubmitPayResult` 接口上报玩家充值金额。上报充值金额时,传入的充值金额的单位同样为分。 -::: - - - -<> - -```cs -// 100 表示 100 分,即 1 元 -long amount = 100; -TapTapCompliance.SubmitPayResult(amount, () => - { - // 提交成功 - }, (exception) => - { - // TODO: 处理参数或网络异常后重试 - } -); -``` - - -<> - -```java - -``` - - -<> - -```objc - -``` - - -<> - -```cpp - -``` - - - - diff --git a/versioned_docs/version-v4/sdk/copyright-verification/_category_.json b/versioned_docs/version-v4/sdk/copyright-verification/_category_.json deleted file mode 100644 index 4dfca5ea1..000000000 --- a/versioned_docs/version-v4/sdk/copyright-verification/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "正版验证", - "collapsed": true, - "position": 5 -} diff --git a/versioned_docs/version-v4/sdk/copyright-verification/faq.mdx b/versioned_docs/version-v4/sdk/copyright-verification/faq.mdx deleted file mode 100644 index d8c946aea..000000000 --- a/versioned_docs/version-v4/sdk/copyright-verification/faq.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 3 ---- - -## 在 TapTap 上售卖的游戏是否必须接入正版验证 SDK? - -在 TapTap 上售卖的游戏不强制要求接入正版验证 SDK。 -如果开发者选择不接入正版验证 SDK,那么 TapTap 只能控制未授权的玩家无法从 TapTap 下载游戏,无法在游戏启动时验证玩家是否购买过游戏,包括: - -- 玩家从其他途径下载到游戏安装包后可以安装、启动、进入游戏。 -- 玩家在 TapTap 付费下载游戏后,申请退款。退款后,玩家无法再次下载该游戏,但仍然可以进入之前下载的游戏。 - -如果开发者希望限制未授权的玩家进入游戏,需要自行实现相应的防盗版机制。 -如无特殊需求,我们建议开发者接入正版验证 SDK,节省添加防盗版机制的开发成本。 - -目前,如果没有在开发者中心开启正版验证服务,售卖设置的入口不会显示。 -所以,即使开发者不打算接入正版验证 SDK,仅希望使用 TapTap 的付费下载功能,也需要先在开发者中心申请开启正版验证服务。 -待审核通过,成功开启正版验证后,开发者即可进行售卖设置,无需实际接入正版验证 SDK。 - -## TapTap 分成比例是多少? - -TapTap 不分成。 -不过,国内会有 5% 的支付手续费,海外根据实际沟通确定(第三方支付渠道的手续费和税务成本)。 diff --git a/versioned_docs/version-v4/sdk/copyright-verification/features.mdx b/versioned_docs/version-v4/sdk/copyright-verification/features.mdx deleted file mode 100644 index 870e1f100..000000000 --- a/versioned_docs/version-v4/sdk/copyright-verification/features.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: 正版验证 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { Conditional } from "/src/docComponents/conditional"; - - -TapTap 的正版验证服务适用于买断制游戏,用于检测用户开始游戏时是否完成付费,是否具备下载资格及 DLC 解锁资格。 - -![](https://img.tapimg.com/market/images/e0582de465379e63ba2df5ad3937de4a.png) - -## 付费下载 - -用户需要在 TapTap 完成订单支付后,才可以下载游戏 APK 。同时,如果用户使用一些不正常手段获取游戏 APK 的,也会被正版验证 SDK 拦截,无法进入游戏。 - -在使用付费下载正版验证服务前,请确保您的游戏存在一个用户可见的游戏详情页(可预约、可关注均可)。完成页面上架后,请前往 [开发指南](/v4/sdk/copyright-verification/guide/) 完成开发接入。 - -## DLC - -当您的游戏需开放新章节、新主线等付费的 DLC 内容时,您同样可使用 DLC 正版验证服务完成用户的解锁资格验证。 - -在使用 DLC 正版验证服务前,请确保您的游戏存在一个用户可见的游戏详情页(可预约、可关注均可),并前往开发者中心 > 商店 > 游戏售卖创建 DLC 并提交审核。完成审核后, 请前往 [开发指南](/sdk/copyright-verification/guide/) 完成开发接入。 \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/copyright-verification/guide.mdx b/versioned_docs/version-v4/sdk/copyright-verification/guide.mdx deleted file mode 100644 index 0e9455f23..000000000 --- a/versioned_docs/version-v4/sdk/copyright-verification/guide.mdx +++ /dev/null @@ -1,469 +0,0 @@ ---- -title: 正版验证开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import { Conditional } from "/src/docComponents/conditional"; -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from '../../sdkVersions'; -import AndroidFaq from "../_partials/android-package-visibility.mdx"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -正版验证适用于在 TapTap 上架的**付费下载**游戏。 - -游戏集成 TapSDK 的正版验证之后,当玩家第一次启动游戏时(包含卸载后再次安装),SDK 会前往 TapTap 查询玩家是否已购买游戏: - -- 如已购买,则可正常进入游戏。 -- 如查询到未购买,将出现「游戏未激活,请前往 TapTap 购买此游戏」弹窗,玩家必须选择「打开 TapTap」购买之后才能游玩。 - -## SDK 获取 - -可以在 [下载页](/tap-download) 获得 TapSDK,添加以下依赖: - - - -<> - - - - - -<> - -1. 项目根目录的 build.gradle 添加仓库地址: - - -{ -`allprojects { - repositories { - google() - mavenCentral() - } -}` -} - - - -2. app module 的 build.gradle 添加对应依赖: - - -{ -`dependencies { - implementation 'com.taptap.sdk:tap-core:${sdkVersions.taptap.android}' - implementation 'com.taptap.sdk:taptap-license:${sdkVersions.taptap.android}' -}` -} - - - - - - -## SDK 初始化 - - - -<> - -`TapTapSdkOptions` 详细参数见 [入门指南#快速开始](/v4/sdk/access/quickstart/#初始化) - -```cs -using TapSDK.Core; - -// 核心配置 详细参数见 [TapTapSDK] -TapTapSdkOptions coreOptions = new TapTapSdkOptions(); -// TapSDK 初始化 -TapTapSDK.Init(coreOptions); -``` - - - -<> - -`TapTapSdkOptions` 详细参数见 [入门指南#快速开始](/v4/sdk/access/quickstart/#初始化) - -```kotlin -import com.taptap.sdk.core.TapTapSdk -import com.taptap.sdk.core.TapTapSdkOptions -import com.taptap.sdk.core.TapTapRegion -import com.taptap.sdk.core.TapTapLanguage - -TapTapSdk.init( - context = context, - sdkOptions = TapTapSdkOptions( - clientId = clientId, - clientToken = clientToken, - region = TapTapRegion.CN, - preferredLanguage = TapTapLanguage.ZH_HANS, - enableLog = false - ) -) -``` - - - - - -## 正版验证 - -### 权限说明 - - - -<> - - - -<> - -该模块需要访问设备中已安装的 Tap 客户端信息,在 Android 11 且工程 targetVersion > 29 时需要 -开发者在应用 `AndroidManifest.xml` 中添加如下配置: - -```xml - - - - - -``` -该模块将在应用中添加如下权限: - -```xml - -``` - - - - -### 测试模式设置 - -测试模式开启后,可以模拟线上正版验证流程,方便进行调试。 - -测试模式需要在 License 其他功能调用之前开启。 - -:::info -切记在上线前关闭测试模式 -::: - - - -```cs -// 需要引入 `License` 库 -using TapSDK.License; - -// 设置是否开启测试环境 -TapTapLicense.SetTestEnvironment(bool isTest); -``` - -```kotlin -import com.taptap.sdk.license.TapTapLicense - -// 设置是否开启测试环境 -TapTapLicense.setTestEnvironment(activity: Activity, true: Boolean) -``` - - - -### 设置授权回调 -可注册多个回调, - - - -```cs -// 需要引入 `License` 库 -using TapSDK.License; - -// 默认情况下 SDK 会弹出不可由玩家手动取消的弹窗来避免未授权玩家进入游戏,如果需要回调来触发流程,请添加如下代码 -TapTapLicense.RegisterLicenseCallBack(ITapLicenseCallback callback); - - -public interface ITapLicenseCallback -{ - // 授权成功回调 - void OnLicenseSuccess(); -} -``` - -```kotlin -import com.taptap.sdk.license.TapTapLicense -import com.taptap.sdk.license.TapTapLicenseCallback - -// 默认情况下 SDK 会弹出不可由玩家手动取消的弹窗来避免未授权玩家进入游戏,如果需要回调来触发流程,请添加如下代码 -TapTapLicense.registerLicenseCallback(object : TapTapLicenseCallback { - override fun onLicenseSuccess() { - // your code - } -}) -``` - - - -### 检查是否购买游戏 - -Check 方法中参数默认值为 false,表示为 SDK 首次触发以及在距离首次触发的第 5 天之后再一次会通过 TapTap 客户端来确认当前登录用户是否购买游戏,如果使用 true 的话,则每次接口调用都会通过 TapTap 客户端来确认当前登录用户是否购买过游戏。 - - - -```cs -using TapSDK.License; - -// 普通检查 -TapTapLicense.CheckLicense(); - -// 强制检查 -TapTapLicense.CheckLicense(true); - -``` - -```kotlin -import com.taptap.sdk.license.TapTapLicense - -// forceCheck参数可不传,默认为false -TapTapLicense.checkLicense(activity: Activity, forceCheck: Boolean) -``` - - - -### Android 11 及更高版本的适配 - - - -## DLC - -### 权限说明 - - - -<> - - - -<> - -该模块需要访问设备中已安装的 Tap 客户端信息,在工程 targetVersion > 29 时需要 -开发者在应用 AndroidManifest.xml 中添加如下配置: - -```xml - - - - - -``` - - - - - -### 测试模式设置 - -测试模式开启后,可以模拟线上 DLC 的查询与购买,方便进行调试。 - -测试模式需要在 License 其他功能调用之前开启。 - -:::info -切记在上线前关闭测试模式 -::: - - - -```cs -// 需要引入 `License` 库 -using TapSDK.License; - -// 设置是否开启测试环境 -TapTapLicense.SetTestEnvironment(isTest); -``` - -```kotlin -// 设置是否开启测试环境 -TapTapLicense.setTestEnvironment(activity: Activity, true: Boolean) -``` - - - -### DLC 查询和购买 - -可以在 [下载页](/tap-download) 获得 TapSDK,添加以下依赖: - - - -<> - - - - - -<> - -app module 的 build.gradle 添加对应依赖: - - -{ -`dependencies { - implementation 'com.taptap.sdk:tap-core:latest.release - implementation 'com.taptap.sdk:taptap-license:latest.release -}` -} - - - - - - - -#### DLC 回调设置 - -DLC 回调包含查询回调和购买回调。 - - - -```cs -using TapSDK.License; - -public class MyTapDLCCallback:ITapDlcCallback -{ - public void OnQueryCallBack(TapLicenseQueryCode code, Dictionary queryList) - { - // 查询回调 - } - - public void OnOrderCallBack(string sku, TapLicensePurchasedCode status) - { - // 购买回调 - } -} - -TapTapLicense.RegisterDLCCallback(new MyTapDLCCallback()); -``` - -```kotlin -import com.taptap.sdk.license.TapTapLicense -import com.taptap.sdk.license.TapTapDLCCallback - -TapTapLicense.registerDLCCallback(object : TapTapDLCCallback { - override fun onQueryResult(resultCode: Int, resultList: HashMap?) { - // 查询回调 - } - - override fun onPurchaseResult(skuId: String, status: Int) { - // 购买回调 - } - -}) -``` - - - -#### DLC 查询 - -对应的查询回调会返回具体的查询结果,查询成功时会返回当前 Tap 玩家是否已经购买过对应商品,在查询回调中返回的键值对类型参数 `skuIds` 中可以获取,该参数 `key` 为查询的商品 `skuid`,`value` 表示该商品当前查询用户的购买状态:0 表示未购买,1 表示已购买。 - - - -```cs -TapTapLicense.QueryDLC(string[] skuIds); -``` - -```kotlin -import com.taptap.sdk.license.TapTapLicense - -TapTapLicense.queryDLC(activity: Activity, skuIds: List) -``` - - - -#### DLC 购买 - - - -```cs -TapTapLicense.PurchaseDLC(string skuId); -``` - -```kotlin -import com.taptap.sdk.license.TapTapLicense - -TapTapLicense.purchaseDLC(activity: Activity, skuId: String) -``` - - - -#### 参数说明 - -##### TapLicenseQueryCode - -| 回调 | 回调值 | 说明 | -| ------------------------------- | ------ | ------------------------------ | -| QUERY_RESULT_OK | 0 | 查询成功 | -| QUERY_RESULT_NOT_INSTALL_TAPTAP | 1 | 检查测试机未安装 TapTap 客户端 | -| QUERY_RESULT_ERR | 2 | 查询失败 | -| ERROR_CODE_UNDEFINED | 80000 | 未知错误 | - -##### skuId - - - -请前往 [DLC 商品](/store/buyout/dlc-products) 查看如何创建 DLC 商品并获得 skuid。 - - - - -请前往 DLC 商品查看如何创建 DLC 商品并获得 skuid。 - - - - -## 如何测试 - -### SDK 开启测试模式 - -在 License 模块初始化后,调用 TapTapLicense.setTestEnvironment(activity:Activity, true:Boolean) 开启/关闭测试模式。 - -### 填写测试包名、测试用户 - -在开发者中心 > 游戏服务 > 正版验证添加测试包名,同时将测试用户的 TapTap ID 加入测试白名单。 - -![](https://img.tapimg.com/market/images/02d44a76f74acfde8d7a38f41c84f073.png) - -### 完成支付 -加入测试白名单的用户将正常拉起 TapTap 的支付功能,请正常操作完成购买。 - -:::tip - -- 测试用户所使用的 TapTap 请确保是最新正式版本。 -- 测试环境的购买仅为了模拟正式环境的购买流程,并不会产生真正的付款订单。 -- 单个测试用户在测试环境下仅可完成 1 次订单支付。如需重复测试,请从白名单中移除此测试账号后再加入。 -- 在正式上线前,请关闭测试环境。 - -::: - -![](https://img.tapimg.com/market/images/c22970392e74be4a59bd3d12138d205e.png) \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/embedded-moments/_category_.json b/versioned_docs/version-v4/sdk/embedded-moments/_category_.json deleted file mode 100644 index d61e4b681..000000000 --- a/versioned_docs/version-v4/sdk/embedded-moments/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "内嵌动态", - "collapsed": true, - "position": 8 -} diff --git a/versioned_docs/version-v4/sdk/embedded-moments/bestpractice.mdx b/versioned_docs/version-v4/sdk/embedded-moments/bestpractice.mdx deleted file mode 100644 index b64e8aafd..000000000 --- a/versioned_docs/version-v4/sdk/embedded-moments/bestpractice.mdx +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: 内嵌动态最佳实践 -sidebar_label: 最佳实践 -sidebar_position: 3 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; - -![](https://capacity-files.lcfile.com/Fo0jjeOf1F4nIsWsuvGt3zUxqt7CRTvQ/head_cover.png) - ---- - -## 游戏内社区对游戏的价值体现 - -**内容「生产」和「消费」** - -- 内容生产:玩家可以轻松将游戏内容一键分享到社区中 - -- 内容消费:根据算法智能推荐,玩家更容易发现优质的游戏内容 - -**链接「玩家」和「官方」** - -- 内容分发:官方发布的内容会进入玩家的关注流,可在内嵌动态为玩家精准分发内容 - -- 快速反馈:玩家发布的内容,官方可以及时反馈和给予帮助解答 - -**链接「玩家」和「玩家」** - -- 社交需求:在游戏过程中,能与其他玩家进行延时性的交流和互动 - -- 帮助决策:在游戏关卡中,遇到困难,能快速查到游戏攻略和大神解说 - ---- - -## 实用的运营工具能辅助建设良好的社区形态 - -### 快捷登录 —— 玩家登录之后可以深度体验社区玩法 - -**游客模式**:玩家不需要登录也能浏览社区内的帖子 - -**TapTap 登录**:玩家发布动态或者进行互动时,需要进行 TapTap 账号登录 - - - - - - -
    - - 唤起 TapTap 原⽣应⽤授权登录 - - - 本机无客户端时可唤起 Webview 账号登录 -
    - -### 模块详解 —— 如何巧妙运用「一块」「二位」「三页」来快速搭建社区 - -**子版块**:TapTap 论坛内的顶部导航,可以将帖子分类管理,包含「全部」「官方」「精华」「视频」4 个不可删改的固定 Tab,和可配置的子版块包含「攻略」「反馈」。 - -**运营位**:运营位可以帮助开发者展示重要轮播图,如活动和新功能上线 - -**推荐位**:推荐位可以帮助开发者配置快捷导航,此处可以关联不同类型的落地页 - -![](https://capacity-files.lcfile.com/l8BvC3sp42vx4eYN817kkf4vNpPYCP8B/3part_show.png) - - -**索引块**:用以承载结构化内容组织需求,目前有小索引块(5 列)、中索引块(3 列)、大索引块(2 列) - -:::tip - -可用于制作成游戏的「WIKI」「百科」「游戏资料库」 - -::: - -![](https://capacity-files.lcfile.com/qTvpSWM571lS8SG7jDCOOHBBCMMVFqcE/suoyingkuai1.png) - -![](https://capacity-files.lcfile.com/EOd3psIWI1iI23Tbp42k6wGYEFC92tQ9/suoyingkuai2.png) - - -**合集页**:TapTap 论坛内的某一类主题帖子的集合 - -:::tip - -可用于整合专题类的攻略或同人集合,如「大神进阶」「新手教程」「同人漫画」等 - -::: - -![](https://capacity-files.lcfile.com/Ts0THmlavVwrbpYneg3wjuC4b8BgvBSu/hejiye.png) - -**详情页**:TapTap 论坛内的帖子详情页 - -![](https://capacity-files.lcfile.com/47q8lN0dNFwddsOAAku0U8pEMFzUB8NQ/detail_page.png) - -### 对战引导关闭 —— 逛社区玩游戏两不误 - -在匹配对战类游戏中,如 Moba 类或 FPS 类游戏中,可以引导玩家关闭动态回到游戏进行对局。 - -![](https://capacity-files.lcfile.com/3CPL9rC7Em4GBnwKjvnv5F6tNl55yc73/close_moment.png) - -## 优质游戏社区实战技巧分享 - -### 入口红点 —— 触达用户的神兵利器 - -开发者可以在游戏内放置内嵌动态的入口,红点可以引导玩家进入内嵌动态里,入口红点和内嵌动态内「关注」的红点逻辑是一致的,玩家关注的用户发布了新内容将触发消息通知。 - -:::tip - -红点对提升使用率至关重要,建议将入口放在游戏内显眼的位置 - -::: - -![](https://capacity-files.lcfile.com/yIb7UR7pIQW7Ul9AREmVIvjNTRDRsej6/redspot.png) - -| 不接入「红点」前 😖 | 拥有了「红点」之后 😆 | -| -------------------------- | ---------------------------- | -| 使用率降低,社区无人问津 | 社区活跃提升促进用户内容消费 | -| 官方发布新消息用户无法感知 | 官方活动公告一触即达 | -| 入口成了一个摆设 | 活跃的社区可以提升用户留存 | - -### 游戏社区风格 —— 将 TapTap 与游戏风格融会贯通,给玩家更好的沉浸体验 - -入口样式经典案例,好的入口是打开的关键因素 - -![](https://capacity-files.lcfile.com/FvA7941IMNj1kLPRinJo6r0kzi9hXRWv/rukouyangshi.png) - -横屏游戏经典案例,善用空白处给游戏增添品牌效果 - -![](https://capacity-files.lcfile.com/weMWPAcwdNTDsWvmySmvuT7CtlriS3hU/hengpingyangshianli.png) - -竖屏游戏经典案例,运营位、推荐位、背景图需要高度统一 - -![](https://capacity-files.lcfile.com/aUYFPhyCAny748Gk4H96j3kBlbE2DFm4/shupingyangshianli.png) - -### 场景化入口 —— 在游戏内自由跳转页面 - -开发者可以在游戏内某一场景处绘制入口,然后在开发者中心后台配置玩家点击按钮后的落地页,帮助玩家在游戏内遇到困难和问题时给予决策辅助。入口样式最好结合游戏场景去绘制,跳转的落地页根据开发者自身诉求自定义配置。 - -:::tip - -适用场景:玩法介绍、英雄攻略、问题反馈、赛事新闻 - -::: - -![](https://capacity-files.lcfile.com/IVjvBoaoiN4CRSo9odbaT1n53qK02mcE/changjinghuarukou_anli.png) - -### 内嵌攻略站 ——TapTap 助力开发者建设攻略站 - -还在为生产攻略而烦恼吗?还在为找不到攻略而发愁吗?游戏官方需要准备好游戏素材,自备官方攻略或选取社区内玩家的优质攻略进行投稿。 TapTap 会将这些内容进行归类,玩家在游戏内可以轻轻松松地找到它们。 - -:::tip - -为了给玩家更好阅读体验,建议横屏游戏使用自动转屏至竖屏 - -::: - -![](https://capacity-files.lcfile.com/4hHLFMqhfy8erSuwVxCgc7apIRRKsdxD/strategy.png) - -### 一键发布 —— 分享游戏内任意场景截图 - -:::tip - -适用场景:成就分享、战绩分享、段位分享、主页分享,开发者可以结合内嵌动态提供的回调,结合场景基于玩家一些奖励激励玩家进行内容生产 - -::: - -![](https://capacity-files.lcfile.com/XUtgbv5RBjxC2LtURbVfrGfArJca6Tzh/share_data.gif) - -### 为好友列表赋能,轻松了解好友动态 - -![](https://capacity-files.lcfile.com/DMPtG4lr2okFv8D0ehmcs66WJKwsbg6H/friend_moments.gif) - -### 问题反馈 —— 游戏内测阶段的好帮手 - -![](https://capacity-files.lcfile.com/OxpqhhT9Apukr21Vd0MC7ErmpviSGQIz/feedback.gif) diff --git a/versioned_docs/version-v4/sdk/embedded-moments/features.mdx b/versioned_docs/version-v4/sdk/embedded-moments/features.mdx deleted file mode 100644 index 01c2a411a..000000000 --- a/versioned_docs/version-v4/sdk/embedded-moments/features.mdx +++ /dev/null @@ -1,290 +0,0 @@ ---- -title: 内嵌动态功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -## 产品介绍 - -玩家可以在游戏内进入 TapTap 的社区论坛,查看攻略资讯,分享自己的游戏精彩瞬间,也可以参与其他玩家、官方和大神之间的互动。 - -## 核心优势 - -**对于游戏开发者:** - -- 内容生产:可以通过一键分享,引导玩家分享自己在游戏内的内容 -- 内容触达:官方发布的内容会进入玩家的关注流,可在内嵌动态为玩家精准分发内容 -- 内容反馈:对玩家发布的内容可以及时反馈和给予帮助解答 - -**对于游戏玩家:** - -- 社交需求:在游戏过程中,能与其他玩家进行延时性的交流和互动 -- 帮助决策:在游戏场景中,遇到困难,能快速查到游戏攻略和大神解说 - -## 账号体系 - -当玩家在内嵌动态内发布动态或者进行互动时,需要进行 TapTap 账号登录,故此建议开发者接入 **[TapTap 登录](/v4/sdk/taptap-login/features/)**。 - -若游戏没有接入 TapTap 登录,玩家可以以**游客模式**使用内嵌动态,当触发到需要登录的功能时会在内嵌动态中唤起的登录界面。 -![](https://capacity-files.lcfile.com/ol3k5gyUteul2kh1R0BE2yoaclL5Dmoc/taplogin-moment.png) - -## 动态功能 - -### 游戏论坛 - -玩家可以直接在「游戏」模块中访问 TapTap 论坛: - -![](https://capacity-files.lcfile.com/gOaifpUWFGT6hYUsc84F8sckfu74d67b/game.png) - -其中各模块分别为: - -![](https://capacity-files.lcfile.com/FS5XnJzoKmHqmQyTBw6Vp2V1jgYQCQ22/game-detail.png) - -1. **子版块**:子版块可以帮助用户分类筛选信息流,在 TapTap 论坛和内嵌动态内是互通的,该模块可在「论坛管理者中心-子版块管理」中编辑,过审后即可展示。 - -2. **运营位**:运营位可以帮助开发者展示重要的信息和活动,是内嵌动态独有的模块,该模块可在「游戏服务-内嵌动态-运营位配置」中编辑,过审后即可在内嵌动态中展示。 - -3. **推荐位**:推荐位可以帮助开发者配置快捷导航,此处可以关联「帖子」「子版块」「索引页」,在 TapTap 论坛和内嵌动态内是互通的,该模块可在「论坛管理者中心-推荐位管理」中编辑,过审后即可展示。 - -4. **信息流**:玩家进入论坛时默认查看热门推荐的信息流,右上角可以根据回复时间和发布时间筛选信息流排序。 - -除此之外,还提供以下功能: - -- **发布动态**:玩家可以在论坛内发布图文动态,和发布视频动态 - -![](https://capacity-files.lcfile.com/iTcFOySJpv4c4OxbXDQvaapJqXwpo5aJ/post.png) - -- **社区互动**:玩家可以**点赞、评论、转发**其他玩家的动态 - -![](https://capacity-files.lcfile.com/auTGp3LqRNUNS2le7lIVP8KIoNiApP7m/repost.png) - -### 攻略站 - -#### 工具箱 - -用于放置游戏工具,如帖子、外链等一些链接。 - -![](https://capacity-files.lcfile.com/tIcfpiiADXplIwMJ5iS8BaFIJlrcHIMo/toolbox.png) - -#### 信息板 - -展示热门攻略等内容,或是近期比较常用的攻略。 -![](https://capacity-files.lcfile.com/medcErLrYf6k3mWxyNY5pMWiPMlGo07f/message_board.png) - - -#### 索引块 - -用以承载结构化内容组织需求,目前有小索引块(5 列)、中索引块(3 列)、大索引块(2 列) - -![](https://capacity-files.lcfile.com/tazGj9CpfLvDx5ahC77sh5CJtIvGvHbi/suo_yin_kuai.png) - - -#### 合集页 -TapTap 论坛内的某一类主题帖子的集合,用于归纳整理 - -![](https://capacity-files.lcfile.com/PMDmtQeWLuvLDcPz0zUJKImCAKQacKMz/hejiye1.png) - -#### 攻略站须知 - -##### 为什么我的游戏内嵌动态里没有「攻略」模块,如何开启? - -首先需要游戏论坛内超过 20 个相关的攻略帖子,开发者可以在 **开发者中心后台 - 游戏运营 - 游戏攻略** 进行开启 - -##### TapTap 开发者中心能配置「攻略」吗?怎么配置? - -目前我们仅对部分游戏开放编辑权限,开发这可以前往 **论坛管理中心 - 攻略管理** 进行操作 - -##### 哪些人可以配置攻略? - -游戏开发者、论坛版主(被允许了攻略配置权限的) - - -### 关注流 - -已登录 TapTap 的用户,可在此查看 TapTap 上关注的好友发布的动态和官方动态。当有新发布的内容时,「关注」的导航栏上会有**红点提醒**,让玩家不会错过重要发布的内容。 - -![](https://capacity-files.lcfile.com/oFlBqtty93dB4ok6fdV2OA8uBpRnMahI/follow.png) - -### 个人页 - -玩家可以在「我的」页面找到自己发布过的动态,可以再进行外部分享和内容删除。 - -![](https://capacity-files.lcfile.com/3fjfuorViRYEpfAeoF1wEf7nDW0I6NfO/me.png) - -在右上角的「小闹铃」处可以查询到最新消息,玩家之间的互动会触发通知,有助于玩家之间更好地建立联系和沉淀关系: - -![](https://capacity-files.lcfile.com/75Rdbv9hGFsTEV2lujF67lS8Bcg2Js19/msg.png) - -## SDK 功能 - -### 场景化入口 - -开发者可以在游戏内某一场景处绘制入口,然后在[开发者中心后台配置](#场景化入口配置)玩家点击按钮后的落地页,帮助玩家在游戏内遇到困难和问题时给予决策辅助。 - -:::tip - -1. 入口样式最好结合游戏场景去绘制,TDS 不提供样式规范,目的是为了让玩家进入时不会有违和感。 -2. 跳转的落地页可以配置成某一篇文章或者是某一个模块,根据开发者自身诉求自定义配置。 - -::: - -![](https://capacity-files.lcfile.com/URQeeUTcT801oxSRfKq3sjMC36uxsmKM/scenario-portal.png) - -### 入口红点 - -开发者可以在游戏内放置内嵌动态的入口,红点可以引导玩家进入内嵌动态里。 - -![](https://capacity-files.lcfile.com/nDoYy5JPwia8wyXUUnfDERfXgB7sW7UL/red-dot.png) - -:::tip - -1. 红点对提升使用率至关重要,建议将入口放在游戏内显眼的位置 -2. 入口红点和内嵌动态内「关注」的红点逻辑是一致的,玩家关注的用户发布了新内容将触发消息通知,获取新消息间隔为 1 分钟一次(1 分钟是最小单位,轮询时长开发者可自行调整为 3 分钟、5 分钟等) -3. 当玩家打开内嵌动态后,游戏需要清除小红点展示,再继续下一次轮询请求来展示红点 - -::: - -### 一键发布 - -游戏内任意场景需要截图分享的,TDS 提供一键发布到内嵌动态内,仅支持图文动态发布。 - -![](https://capacity-files.lcfile.com/jSHc1QzSFPTKva5uKWQp8QiT870Y9gaO/share.png) - -### 动态关闭引导弹窗 - -若需要玩家立即退出内嵌动态回到游戏内及时响应的(如即时对战),开发者可以自定义弹窗引导和弹窗触发的节点。 - -![](https://capacity-files.lcfile.com/3DVl39Uz882GKT4Epc29Fd6wTIdo8o7S/popup.png) - -## 后台功能 - -### 主题配置 - -为了更好地结合游戏场景,让玩家不会有割裂感,TDS 提供开发者自定义配置内嵌状态的样式主题,你可以在「游戏服务」-「内嵌动态」-「主题配置」中上传背景图片和设置字体配色。 - -:::tip - -1. 可参考[内嵌动态设计指南](/design/design-moment/)。 -2. 如果游戏仅支持横屏或者竖屏,只需要上传一张即可,若是支持转屏则需要上传两张。 -3. 图片是需要进行人工审核的,一般会在 2 个工作日内完成。 - -::: - -![](https://capacity-files.lcfile.com/Gboeocmn2zmPlN0779P1RHI4vj0AivGH/tap_moment_bg.png) - -### 运营位配置 - -为了更好地帮助游戏进行活动运营,TDS 提供开发者自定义配置内嵌状态的运营位,你可以在「游戏服务」-「内嵌动态」-「运营位配置」中新增运营位,需要提供**标题、图片 Banner**和**链接**。 - -:::tip - -1. 最多同时配置 5 个运营位,TDS 对跳转链接域名不做限制。 -2. 运营位是需要进行人工审核的,一般会在当天内审核完。 - -::: - -![](https://capacity-files.lcfile.com/VcFEYFk5H9OxFCUNpkHgGTFIpWhzKE86/tap-moment-banner-config.png) - -### 场景化入口配置 - -场景化入口可以在「游戏服务」-「内嵌动态」-「场景化入口配置」中创建,开发者提交**入口名称、落地页类型、落地页**后,生成的入口 ID 可以在游戏内使用。该模块是不需要审核的,开发者可以自由变更跳转路径。 - -![](https://capacity-files.lcfile.com/NgUth0N8CfkrDs5VdsPuipJARRkbvOdw/tap-moment-scenario-based-portal-configuration.png) - - -## 论坛管理中心 - -![](https://capacity-files.lcfile.com/K8YfR6iVwtyRSbAC7sBONQasqPhhUu6C/forum-management.png) - -### 论坛数据查看 - -为了更好地体现内嵌动态的价值和内容质量的反馈,我们提供开发者查看内嵌动态内数据的功能,在「游戏服务」-「内嵌动态」-「论坛管理中心」,点击直接跳转查看。(注:查看数据需要开权限) - - -### 子版块配置 - -子版块配置可以从右上角「论坛管理中心」入口进入,前往「子版块管理」提供**子版块名称(包含多语言)**,子版块的新增和修改都是需要人工审核的。 - - -### 推荐位配置 - -推荐位配置可以从右上角「论坛管理中心」入口进入,前往「推荐位管理」提供**标题、图片、跳转路径**,推荐位的新增和修改都是需要人工审核的。 - - -### 攻略配置 - -攻略配置可以从右上角「论坛管理中心」入口进入,前往「攻略管理」提供**封面图片、帖子链接、实体** - -#### 什么是实体词? - -1、「实体」是算法应用中产生的概念,例如在《原神》中,“圣遗物”、“夜兰”、“3.6版本”都可能是一个实体。 - - -2、在攻略配置中,「实体」所指代的是所有与该名词相关的攻略的集合。 - - -3、如果为某一模块配置了「实体」,算法将会自动为其收录添加最新最热的攻略,并进行排序。 - - - -#### 工具箱配置 - -1、点击「新建模块」,选择「工具箱」 - - -2、输入工具箱的标题名称,如“常用攻略集合” - - -3、点击「新增内容」,填写名称,如“近战攻略”,跳转的内容类型包含**论坛帖子、外链、实体** - - -4、需要上传图片尺寸需要满足 **144x144 px**,点击「提交」完成保存 - - -5、最少需要配置 2 个,最多无上限 - - - -![](https://capacity-files.lcfile.com/Loh12C6iq4a07NN2yXUUpOTcOA3dDJM2/toolbox_config.png) - - - -#### 信息板配置 - -1、点击「新建模块」,选择「信息板-banner」或者「信息板-实体」 - - -2、输入信息板的标题名称,如“热门攻略” - - -3、点击「新增内容」,填写名称,如“地图攻略”,跳转的内容类型包含**链接、实体** - - -4、若配置「信息板-Banner」需要上传图片尺寸需要满足 **1000x563 px**,点击「提交」完成保存 - - -5、最少需要配置 1 个,最多 4 个 - - -![](https://capacity-files.lcfile.com/7Hw2JdGk0qgYM2OGpU1fEtpCWzhCasoP/message_config.png) - - -#### 索引块配置 - - -1、点击「新建模块」,选择「索引块-大」或者「索引块-中」或者「索引块-小」 - - -2、输入信息板的标题名称,如“英雄图鉴” - - -3、点击「新增内容」,填写名称,如“投掷道具” - - -4、点击「+」添加一个快,填写名称,如"爱心烟雾弹",跳转的内容类型包含**论坛帖子、外链、实体** - - -5、需要上传图片尺寸需要满足,「索引块-小」**186x186 px**,「索引块-中」**500x280 px**,「索引块-大」**500x280 px**,点击「提交」完成保存 - - -![](https://capacity-files.lcfile.com/5kh14qdvQHky0gqdGzs06sWvIqM2qrtj/suoyinkuai_config.png) diff --git a/versioned_docs/version-v4/sdk/embedded-moments/guide.mdx b/versioned_docs/version-v4/sdk/embedded-moments/guide.mdx deleted file mode 100644 index dcfba3fb9..000000000 --- a/versioned_docs/version-v4/sdk/embedded-moments/guide.mdx +++ /dev/null @@ -1,578 +0,0 @@ ---- -title: 内嵌动态开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import Languages from "../_partials/languages.mdx"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; -import sdkVersions from '../../sdkVersions'; - -本文介绍如何在游戏中接入 TapTap 内嵌动态功能,使用内嵌动态功能需依赖 [TapTap 登录](/v4/sdk/taptap-login/features/)。 - -## 权限说明 - - - -<> - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | -| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | -| 读写存储权限 | 用于发布或下载动态页面内图片、视频 | 下载或使用本地图片发布动态时申请 | - -该模块将在应用中添加如下权限: - -```xml - - - - - -``` - - - -<> - -使用内嵌动态服务需要相册、相机权限,所以开发者需在工程的 `info.plist` 配置相关权限并**替换授权文案**: - -```xml - -NSPhotoLibraryUsageDescription -说明为何应用需要此项权限 -NSCameraUsageDescription -说明为何应用需要此项权限 -``` - - - - - - - - -## 集成前准备 - -1. 参考 [准备工作](/v4/sdk/access/get-ready/) 创建应用、开启内嵌动态应用配置; -2. 内嵌动态依赖于 [TapTap 登录模块](/v4/sdk/taptap-login/features),开发者接入前应先接入 TapTap 登录 相关依赖。 - -## SDK 获取 - - -完成 SDK 获取,然后在此基础上可以通过 [下载](/tap-download) 获得 TapSDK,添加 `TapMoment` 模块: - - - -<> - - -#### iOS 配置 - -在 `Assets/Plugins/iOS/Resource` 目录下创建 `TDS-Info.plist` 文件 (如果已创建,只需添加内容即可),配置相关权限并**替换授权文案**: - -:::tip - -复制使用以下内容时,**请删除空行以及注释**,以免出现 XML 解析时报错,`ApplicationException: expected a key node`。 - -::: - -```xml - - - - - - NSPhotoLibraryUsageDescription - 说明为何应用需要此项权限 - NSCameraUsageDescription - 说明为何应用需要此项权限 - - -``` - - - - -<> -1. 项目根目录的 build.gradle 添加仓库地址: - - -{ -`allprojects { - repositories { - google() - mavenCentral() - } -}` -} - - - -2. app module 的 build.gradle 添加对应依赖: - -内嵌动态模块依赖于 `TapLogin`, 需额外添加 `TapLogin` 模块依赖。 - - -{ -`dependencies { - implementation 'com.taptap.sdk:tap-core:${sdkVersions.taptap.android}' - implementation 'com.taptap.sdk:tap-login:${sdkVersions.taptap.android}' - implementation 'com.taptap.sdk:tap-moment:${sdkVersions.taptap.android}' -}` -} - - - - - -<> - -iOS 提供通过添加 cocosPod 远程依赖和使用本地文件导入两种集成方式,推荐使用远程依赖方式。 - -#### 远程依赖 - -1. 在工程 Podfile 文件中对应模块下添加依赖: - -{` pod 'TapTapMomentSDK', '~> ${sdkVersions.taptap.ios }'`} - -2. 执行 `Pod install` 下载对应依赖文件 -3. 将工程 Pods 目录下 `TapTapMomentSDK/Frameworks/TapTapMomentResource.bundle` 和 `TapTapLoginSDK/Frameworks/TapTapLoginResource.bundle` - 等资源文件导入工程中 - -#### 本地文件依赖 - -合规认证模块依赖于初始化和 TapTap 登录模块,使用本地文件方式添加依赖时,需先参考 [TapSDK 集成](/v4/sdk/access/quickstart#本地文件依赖) 和 [TapTap 登录](/v4/sdk/taptap-login/guide#本地文件依赖) 添加对应本地文件依赖项。 - -1. 在下载页下载如下文件: - -- `TapTapMomentSDK.xcframework` 合规认证依赖库 -- `TapTapMomentResource.bundle` 合规认证资源文件 - -2. 在工程中添加 `framework` 静态库,注意添加时选择 Embed 方式为 **Do Not Embed**,导入 `bundle` 资源文件 - - - - - -## SDK 初始化 - - - -<> - -`TapTapSdkOptions` 详细参数见 [入门指南#快速开始](/v4/sdk/access/quickstart/#初始化) - -```cs -using TapSDK.Core; - -// 核心配置 详细参数见 [TapTapSDK] -TapTapSdkOptions coreOptions = new TapTapSdkOptions(); -// TapSDK 初始化 -TapTapSDK.Init(coreOptions); -``` - - - -<> - -`TapTapSdkOptions` 详细参数见 [入门指南#快速开始](/v4/sdk/access/quickstart/#初始化) - -```kotlin -import com.taptap.sdk.core.TapTapSdk -import com.taptap.sdk.core.TapTapSdkOptions -import com.taptap.sdk.core.TapTapRegion -import com.taptap.sdk.core.TapTapLanguage - -TapTapSdk.init( - context = context, - sdkOptions = TapTapSdkOptions( - clientId = clientId, - clientToken = clientToken, - region = TapTapRegion.CN, - preferredLanguage = TapTapLanguage.ZH_HANS, - enableLog = false - ) -) -``` - - - -<> - -```swift -import TapTapCoreSDK - -let options = TapTapSdkOptions() -options.clientId = "your_client_id" // 必须,开发者中心对应 Client ID -options.clientToken = "your_client_token" // 必须,开发者中心对应 Client Token -options.region = .CN // .CN:中国大陆,.overseas:其他国家或地区 -options.enableLog = enableLog.selectedSegmentIndex == 0 // 是否开启 log,建议 Debug 开启,Release 关闭,默认关闭 log -options.preferredLanguage = TapLanguageType.auto // 语言设置,默认跟随系统,当系统语言不支持时,国内为中文,海外为英文 - -// 初始化 SDK -TapTapSDK.initWith(options) - -``` - - - - - -## 设置回调 - -设置回调以获取动态的状态变化。 - - - -```cs -using TapSDK.Moment; - -TapTapMoment.SetCallback((code, msg) => -{ - // 根据 code 处理动态事件 -}); - -// 移除 callback -TapTapMoment.SetCallback(null); -``` - -```kotlin -import com.taptap.sdk.moment.TapTapMoment - -TapTapMoment.setCallback( - callback = object : TapTapMoment.TapTapMomentCallback { - override fun onCallback(code: Int, msg: String?) { - // 根据 code 处理事件 - } - } -) - -// 移除 callback -TapTapMoment.setCallback(null); -``` - -```swift -import TapTapMomentSDK - -// 实现 TapTapMomentDelegate 协议 -extension GameMainScene:TapTapMomentDelegate{ - func onMomentCallback(withCode code: Int, msg: String) { - // 根据 code 处理动态事件 - } -} - -// 注册回调 -TapTapMoment.setDelegate(self) -``` - - - - -回调方法中 code 表示事件类型,现支持的回调类型如下: - -| 回调 | 回调值 | 说明 | -| -------------------------------- | ------ | ---------------------------------------- | -| CALLBACK_CODE_PUBLISH_SUCCESS | 10000 | 动态发布成功 | -| CALLBACK_CODE_PUBLISH_FAIL | 10100 | 动态发布失败 | -| CALLBACK_CODE_PUBLISH_CANCEL | 10200 | 关闭动态发布页面 | -| CALLBACK_CODE_GET_NOTICE_SUCCESS | 20000 | 获取新消息成功 | -| CALLBACK_CODE_GET_NOTICE_FAIL | 20100 | 获取新消息失败 | -| CALLBACK_CODE_MOMENT_APPEAR | 30000 | 动态页面打开 | -| CALLBACK_CODE_MOMENT_DISAPPEAR | 30100 | 动态页面关闭 | -| CALLBACK_CODE_CLOSE_CANCEL | 50000 | 取消关闭所有动态界面(弹框点击取消按钮) | -| CALLBACK_CODE_CLOSE_CONFIRM | 50100 | 确认关闭所有动态界面(弹框点击确认按钮) | -| CALLBACK_CODE_LOGIN_SUCCESS | 60000 | 动态页面内登录成功 | -| CALLBACK_CODE_SCENE_EVENT | 70000 | 场景化入口回调 | - -## 获取新消息 - -定时调用获取消息通知的接口,有新信息时可以在 TapTap 动态入口显示小红点,提醒玩家查看新动态。 - - - -```cs -using TapSDK.Moment; - -TapTapMoment.FetchNotification(); -``` - -```kotlin -import com.taptap.sdk.moment.TapTapMoment - -TapTapMoment.fetchNotification() -``` - -```swift -import TapTapMomentSDK - -TapTapMoment.fetchNotification() -``` - - - -获取消息通知的结果会在本文刚开始设置的回调中返回,`code` 为 `CALLBACK_CODE_GET_NOTICE_SUCCESS`(`20000`)表示获取成功,`CALLBACK_CODE_GET_NOTICE_FAIL`(`20100`)表示获取失败。 -获取成功时,`msg` 为新消息数量,`0` 表示没有新消息。 - -:::tip - -为了方便玩家查看好友动态、游戏公告等,我们建议将 TapTap 动态入口放在显眼的位置,**每分钟调用 1 次**获取消息通知的接口。 - -获取消息通知时,如果没有新消息(`msg` 为 `0`),那么游戏需要清除界面上的小红点。 -同样,打开 TapTap 动态页面后,游戏也需要清除界面上的小红点。 - -::: - -## 显示动态页面 - -在游戏中显示 TapTap 动态页面,玩家可以查看及发布动态。 - - - -```cs -using TapSDK.Moment; - -TapTapMoment.open(); -``` - - -```kotlin -import com.taptap.sdk.moment.TapTapMoment - -TapTapMoment.open() -``` - -```swift -import TapTapMomentSDK - -TapTapMoment.open() -``` - - - -:::note - -打开动态页面时,请先屏蔽游戏自身的声音,以免干扰动态内的视频声音。 - -如需要动态能支持横竖屏随设备自动旋转,需要游戏自身能支持横竖屏。 - -如前所述,打开动态页面后别忘了清除动态页面入口处的小红点。 - -::: - -动态页面的背景图可以配置,步骤如下图所示。 -背景图需要人工审核后才能生效,请预留充足的时间。 - -![](https://capacity-files.lcfile.com/Gboeocmn2zmPlN0779P1RHI4vj0AivGH/tap_moment_bg.png) - -## 场景化入口 - -开发者可以[结合游戏场景绘制入口](/v4/sdk/embedded-moments/features/#场景化入口),玩家打开入口跳转到指定的页面。使用前需要在开发者中心后台完成[场景化入口配置](/v4/sdk/embedded-moments/features/#场景化入口配置)。 - - -<> - -```cs -using TapSDK.Moment; - -// sceneId 为在开发者中心后台创建场景化入口后生成的「入口 ID」 -TapTapMoment.OpenScene(sceneId); -``` - - - -<> - -```kotlin -import com.taptap.sdk.moment.TapTapMoment - -TapTapMoment.openScene(sceneId = sceneId) -``` - - - -<> - -```swift -import TapTapMomentSDK - -// sceneId 为在开发者中心后台创建场景化入口后生成的「入口 ID」 -TapTapMoment.openScene(sceneId) -``` - - - - - -### 场景化入口回调格式说明 - -**SDK 回调结构** - -| 字段名 | 值类型 | required | 说明 | -| ------------ | ------ | -------- | ----------------------------------------- | -| sceneId | 字符串 | 是 | 场景化入口 ID | -| eventType | 字符串 | 是 | 枚举的事件类型,如 VIEW,FORWARD,VOTE 等 | -| eventPayload | 字符串 | 是 | 根据类型自定义的 JSON 字符串 | -| timestamp | 整数 | 是 | unix 时间戳,ms | - -**事件类型** - -| eventType | eventPayload (未序列化) | 说明 | -| --------- | ----------------------- | ----------------------------------------------- | -| READY | {} | 场景化页面已打开 | -| REPOST | {} | 转发,仅帖子本身 | -| VOTE | { isCancel: boolean } | 点赞(含是否取消),仅帖子本身 | -| FOLLOW | { isCancel: boolean } | 关注(含是否取消),仅帖子本身 | -| COMMENT | {} | 评论,仅帖子本身 | - -## 关闭动态页面 - -玩家可以在动态页面主动退出,但在特定场景下,游戏可能需要主动关闭动态页面。 - -比如,玩家排位等待结束,准备进入对局时提示玩家关闭动态页面,玩家确认后关闭,此时调用如下接口: - - - -```cs -using TapSDK.Moment; - -TapTapMoment.CloseWithTitle("提示", "匹配成功,进入游戏"); -``` - -```kotlin -import com.taptap.sdk.moment.TapTapMoment - -TapTapMoment.closeWithTitle("提示", "匹配成功,进入游戏"); -``` - -```swift -import TapTapMomentSDK - -TapTapMoment.close(withTitle: "标题", content: "匹配成功,是否离开动态?", showConfirm: true) -``` - - - -接口中第一个参数为确认弹窗中的标题,第二个参数为确认弹窗中的内容。 - -用户的选择会通过回调返回: - -- `CALLBACK_CODE_CLOSE_CANCEL`(50000),表示玩家点了「取消」,选择不关闭动态页面。 -- `CALLBACK_CODE_CLOSE_CONFIRM`(50100),表示玩家点了「确认」,选择关闭动态页面。 - -如果需要直接关闭动态窗口,不弹出二次确认框,调用如下接口: - - - -```cs -using TapSDK.Moment; - -TapTapMoment.Close(); -``` - -```kotlin -import com.taptap.sdk.moment.TapTapMoment - -TapTapMoment.close() -``` - -```swift -import TapTapMomentSDK - -TapTapMoment.close() -``` - - - -## 一键发布 - -:::info - -这是可选功能,请根据项目需求决定是否在游戏中加入这一功能。 - -::: - -我们推荐游戏让玩家直接在动态页面发布新动态。 -不过,SDK 也提供了发布图文动态的 API,以支持「一键发动态」等需求。 -图文动态包括单张或多张图片及相应的文字内容。 - - - -```cs -using TapSDK.Moment; - -PublishMetaData publishMetaData = new PublishMetaData(content: "动态文字内容", imagePaths: new List { "file://..." }); -TapTapMoment.Publish(publishMetaData) -``` - -```kotlin -import com.taptap.sdk.moment.TapTapMoment - -TapTapMoment.publish( - publishMetaData = PublishMetaData( - content = "content", - imagePaths = listOf( - "路径1", - "路径2", - "路径3" - ) - ) -) -``` - -```swift -import TapTapMomentSDK - -// 初始化要发布的图文数据 -let postData = TapTapMomentImageData() -// 设置发布的文字内容 -postData.content = "动态文字内容" -// 设置发布的图片地址 -let imagePath = "file://..." -postData.images = [imagePath] -// 发布图文动态 -TapTapMoment.publish(postData) -``` - - - - -:::info - -玩家在动态页面可以发布图文动态和视频动态。 -「一键发布」只支持发布图文动态。 - -::: - -## 国际化 - -内嵌动态支持设置语言: - - diff --git a/versioned_docs/version-v4/sdk/start/_category_.json b/versioned_docs/version-v4/sdk/start/_category_.json deleted file mode 100644 index b31683d72..000000000 --- a/versioned_docs/version-v4/sdk/start/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "服务概述", - "collapsed": true, - "position": 0 -} diff --git a/versioned_docs/version-v4/sdk/start/overview.mdx b/versioned_docs/version-v4/sdk/start/overview.mdx deleted file mode 100644 index fb1a23385..000000000 --- a/versioned_docs/version-v4/sdk/start/overview.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: 概览 -slug: /sdk -sidebar_position: 1 ---- - -import { Conditional } from "/src/docComponents/conditional"; - -TapTap 开发者服务(TapTap Developer Services,简称 TDS 服务)旨在运用 TapTap 平台资源、技术积累、发行经验,帮助开发者降低游戏研发、运营成本,连接目标玩家,提升各项数据,最终取得游戏成功。 - - - -:::info - -2022 年 3 月 25 日 0 点之后,在 [TapTap 开发者中心](https://developer.taptap.cn/) 创建的新游戏已经不再支持发布到除中国大陆外的其他国家/地区。 - -出海游戏请前往 [**TapTap.io 开发者中心**](https://developer.taptap.io/) 创建游戏、开启游戏服务、使用国际版文档。 - -::: - - - -TDS 提供以下技术服务,开发者可以通过在游戏中集成 TapSDK 来开启使用: - -- **TapTap 登录**:提供 TapTap 登录方式,玩家可以通过 TapTap 授权快速开始游戏。 - -- **数据分析**:提供了一套专注于解决游戏项目数据需求的分析工具,通过简单的接入就可以获得丰富实用的数据看板和广告追踪能力,让数据分析和广告投放变得轻松易操作,同时也可以用于分析人群画像,帮助开发者更好地理解用户。 - -- **更新唤起**:当游戏 apk 有更新时,支持玩家从游戏内直接跳转至 TapTap 进行游戏 apk 更新。 - - - -- **合规认证**:可实现 TapTap 账号的快速实名认证。对使用 TapTap 账号登录游戏的玩家,在经过玩家同意授权之后,允许玩家使用 TapTap 的实名信息快速完成游戏中的认证流程。 - - - -- **正版验证**:帮助买断制游戏验证付费下载资格和 DLC 解锁资格。 - - - -- **成就**:提供成就体系,玩家在游戏内获得的成就,可在 TapTap 上展示,增加平台上游戏的曝光。玩家成就资产累积的同时,提高游戏活跃。 - - - -- **内嵌动态**:玩家可以在游戏内访问 TapTap 的社区论坛(官方公告、游戏攻略、问题反馈、热门话题等),并参与其他玩家、官方和大神之间的互动。 - -- **礼包系统**:TDS 全套礼包系统,旨在帮助开发者快速生成、核销礼包码。 - - - -- **TapLink**:帮助玩家从 TapTap 跳转到游戏领取礼包。 - -- **APK 加固**:避免游戏包体被破解篡改,保障游戏安全。 - - - - - -- **TapCanary**:将游戏应用的早期版本,发布给内部测试人员或受信任的用户进行封闭式测试,支持云玩模式和 TapPlay(沙盒)模式。 - - - -使用对应的服务请先完成[开发者注册](https://developer.taptap.cn/)[开发者注册](https://developer.taptap.io/),之后登录开发者中心开启「游戏服务」。 diff --git a/versioned_docs/version-v4/sdk/tapdb/_category_.json b/versioned_docs/version-v4/sdk/tapdb/_category_.json deleted file mode 100644 index 9ba1f6886..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "数据分析", - "collapsed": true, - "position": 5 -} diff --git a/versioned_docs/version-v4/sdk/tapdb/changelog/_category_.json b/versioned_docs/version-v4/sdk/tapdb/changelog/_category_.json deleted file mode 100644 index 23ab30266..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/changelog/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "功能更新", - "position": 8 -} diff --git a/versioned_docs/version-v4/sdk/tapdb/changelog/android.mdx b/versioned_docs/version-v4/sdk/tapdb/changelog/android.mdx deleted file mode 100644 index 3defff371..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/changelog/android.mdx +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: Android -sidebar_position: 3 ---- - -## 2.2.0 | 2022-07-14 发布 -**优化**
    - -1. 调整已冻结项目的界面展示 - - -## 2.1.0 | 2022-04-30 发布 -**新增**
    - -1. 新增多账号切换功能,开发者可添加多个国内或海外账号,并在之间进行切换。 - -**优化**
    - -1. 优化部分体验 - - -## 2.0.0 | 2022-01-13 发布 - -**新增**
    - -1. 新增看板查看功能:可通过客户端查看 web 端发布的看板
    - -**优化**
    - -1. 修复广告模块的数据显示 - -## 1.9.0 | 2021-10-29 发布 - -**新增**
    - -1. 增加 TapTap 登录
    - -**优化**
    - -1. 修复了一些 bug,优化了体验 - -## 2019-10-08 V1.7.4 发布 - -**新增** - -1. 新添加了支持运营事件功能 - -**优化** - -1. 优化了正在变化的数据的显示问题 -2. 优化了图标的样式 -3. 改变了图表线条颜色 - ---- - -## 2019-08-20 V1.7.3 发布 - -**优化** - -1. 修复了广告排序后显示错误的问题 -2. 长按复制后提示复制成功 -3. 优化了多语言的翻译问题 -4. 广告自定义列避免重复命名 -5. 优化了留存中的日周月变化 - ---- - -## 2019-08-12 V1.7.2 发布 - -**新增** - -1. 用户 LTV 图表变为累积 LTV 走势图 -2. 支持长按复制功能 - -**优化** - -1. 修正了概览中切换对比日出现日期错误的数据显示 -2. 优化了部分按钮 -3. 优化了图表排版设计 -4. 全新的更新弹窗设计 -5. 对比日的自定义时间支持双向选择 - ---- - -## 2019-06-25 V1.7.0 发布 - -**新增** - -1. 支持广告效果查询 -2. 优化表格排版 -3. 增加 8 - 13 日 LTV、DAU - 14 日用户 - -**优化** - -1. ARPU、ARPPU、LTV 显示 2 位小数 - ---- - -## 2019-05-20 V1.6.2 发布 - -**新增** - -1. 支持多语言 - -**优化** - -1. 修复二级表格默认排序问题 -2. 优化过滤条件无法生效时提示机制 - ---- - -## 2019-04-11 V1.6.1 发布 - -**优化** - -1. 过滤条件区服名称调整为首次区服 -2. 付费数据下过滤条件新增事件区服 -3. 已选择的过滤条件置顶 -4. 支持截屏 - ---- - -## 2019-04-03 V1.6.0 发布 - -**新增** - -1. 支持对比日期 - -**优化** - -1. 表格总计/平均行固定在底部 -2. 表格为机型主维度时加载优化 -3. 更新「付费方式」,「商品名称」以条形图形式展现 -4. 增加安全验证密码错误提示 -5. 活跃账号/活跃设备下,增加非日期维度提示信息 -6. 修复登录界面无法查看错误提示问题 -7. 优化概览页面自动刷新机制 -8. 优化付费下不同类型数据以双轴折线图展现 - ---- - -## 2019-03-04 V1.5.11 发布 - -**新增** - -1. 可以使用指纹解锁了 -2. 支持查看活跃设备数据 - -**优化** - -1. 多次点击,防止重复进同一个页面 -2. 消息详情页支持下载文件 -3. 游戏列表页刷新时,可点击进入概览页面 - -**修复** - -1. 消息中心链接返回页面错误 -2. 修复无网/弱网状态下登录丢失问题 - ---- - -## 2019-01-28 V1.5.9 发布 - -**新增** - -1. 表格支持排序 -2. 首页支持对比日选择 -3. 长按表头可显示完整信息 -4. 付费-用户模块新增 2、4、5、6 日 LTV 数据 -5. 活跃-行为模块支持查看小时数据 -6. 付费下增加 120 日贡献与 150 日贡献 -7. 登录页面新增 DEMO 入口 - -**优化** - -1. 整体设计样式优化与调整 -2. 活跃下实时在线支持查看七天以上数据 -3. 过滤条件筛选优化交互体验 -4. 修复了过滤条件中不包含未生效的问题 -5. 权限更改后在首页刷新即可生效 -6. 饼图数据显示优化 -7. 设置按钮位置调整到右上角 -8. 优化网络环境较差时的交互与提示 -9. 优化自定义日期控件样式 diff --git a/versioned_docs/version-v4/sdk/tapdb/changelog/ios.mdx b/versioned_docs/version-v4/sdk/tapdb/changelog/ios.mdx deleted file mode 100644 index 4210efa27..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/changelog/ios.mdx +++ /dev/null @@ -1,362 +0,0 @@ ---- -title: iOS -sidebar_position: 2 ---- - -## 2.2.0 | 2022-07-18 发布 -**优化**
    - -1. 优化 iPad 横屏体验 -2. 调整已冻结项目的界面展示 -3. 修复周活跃、月活跃日期显示不准确问题 - - -## 2.1.0 | 2022-04-30 发布 -**新增**
    - -1. 新增多账号切换功能,开发者可添加多个国内或海外账号,并在之间进行切换。 - -**优化**
    - -1. 优化部分体验 - - -## 2.0.0 | 2022-01-13 发布 - -**新增**
    - -1. 新增看板查看功能:可通过客户端查看 web 端发布的看板
    - -**优化**
    - -1. 修复广告模块的数据显示 - -## 1.9.0 | 2021-10-29 发布 - -**新增**
    - -1. 增加 TapTap 登录
    - -**优化**
    - -1. 修复了一些 bug,优化了体验 - -## 2019-10-08 V1.7.4 发布 - -**新增** - -1. 新添加了支持运营事件功能 - -**优化** - -1. 优化了正在变化的数据的显示问题 -2. 优化了图标的样式 -3. 改变了图表线条颜色 - ---- - -## 2019-08-20 V1.7.3 发布 - -**优化** - -1. 修复了广告排序后显示错误的问题 -2. 长按复制后提示复制成功 -3. 优化了多语言的翻译问题 -4. 广告自定义列避免重复命名 -5. 优化了留存中的日周月变化 - ---- - -## 2019-08-12 V1.7.2 发布 - -**新增** - -1. 用户 LTV 图表变为累积 LTV 走势图 -2. 支持长按复制功能 - -**优化** - -1. 修正了概览中切换对比日出现日期错误的数据显示 -2. 优化了部分按钮 -3. 优化了图表排版设计 -4. 全新的更新弹窗设计 -5. 对比日的自定义时间支持双向选择 - ---- - -## 2019-06-25 V1.7.0 发布 - -**新增** - -1. 支持广告效果查询 -2. 优化表格排版 -3. 增加 8 - 13 日 LTV、DAU - 14 日用户 - -**优化** - -1. ARPU、ARPPU、LTV 显示 2 位小数 - ---- - -## 2019-05-21 V1.6.2 发布 - -**新增** - -1. 支持多语言 -2. 截屏时,新增谨慎分享提示 -3. 增加版本更新提醒 - -**优化** - -1. 修复二级表格默认排序问题 -2. 优化过滤条件无法生效时提示机制 - ---- - -## 2019-04-11 V1.6.1 发布 - -**优化** - -1. 过滤条件区服名称调整为首次区服 -2. 付费数据下过滤条件新增事件区服 -3. 已选择的过滤条件置顶 - ---- - -## 2019-04-03 V1.6.0 发布 - -**新增** - -1. 支持对比日期 - -**优化** - -1. 表格总计/平均行固定在底部 -2. 表格为机型主维度时加载优化 -3. 更新「付费方式」,「商品名称」以条形图形式展现 -4. 增加安全验证密码错误提示 -5. 活跃账号/活跃设备下,增加非日期维度提示信息 -6. 修复登录界面无法查看错误提示问题 -7. 优化概览页面自动刷新机制 -8. 修复无法使用 Face ID 解锁或重复使用其解锁问题 -9. 优化付费下不同类型数据以双轴折线图展现 -10. 修复在来源/活跃/留存/付费页面白屏问题 - ---- - -## 2019-02-14 V1.5.10 发布 - -**新增** - -1. 支持查看活跃设备数据了 - ---- - -## 2019-01-22 V1.5.9 发布 - -**新增** - -1. 表格支持排序 -2. 长按表头可显示完整信息 -3. 付费下增加 120 日贡献与 150 日贡献 - -**优化** - -1. 活跃下实时在线支持查看七天以上数据 -2. 修复 iPad 闪退问题 -3. 饼图数据显示优化 -4. 游戏列表星标位置调整 -5. 优化密码解锁体验 -6. 优化日历以及自定义日期标签样式 - ---- - -## 2018-12-07 V1.5.7 发布 - -**修复** - -1. 修复部分时区下对比日错误的问题 - ---- - -## 2018-12-04 V1.5.6 发布 - -**修复** - -1. 修复 iOS 11 及以下系统部分设备闪退问题 -2. 修复部分项目「在线」下点击过滤按钮无法收起列表的问题 - ---- - -## 2018-12-03 V1.5.5 发布 - -**新增** - -1. 支持保存常用的过滤条件 -2. 首页支持选择日期和对比日期 -3. 活跃-周活跃下新增 N 周活跃,细分周活跃数据 -4. 支持拼音搜索 - -**优化** - -1. 修复了过滤条件中不包含未生效的问题 -2. 优化概览页自定义日期页左滑返回的交互 -3. 优化使用 1Password 登录时的交互体验 -4. 权限更改后在首页刷新即可生效 -5. 修复使用搜索功能时键盘无法收起的问题 -6. 优化进入首页 loading 时默认 icon 显示错误的问题 - ---- - -## 2018-10-26 V1.5.4 发布 - -**优化** - -1. 修复概览页零点对比日「昨日」更新延迟问题 -2. 修复公告播放不连贯的问题 -3. 修复横屏下公告缺失的问题 -4. 优化自定义日期控件样式 - ---- - -## 2018-10-22 V1.5.3 发布 - -**新增** - -1. 首页支持对比日选择 - -**优化** - -1. 适配 iPhone XS Max -2. LTV 动态数据标红显示 -3. 修复无网络时 DEMO 无反馈的问题 - ---- - -## 2018-09-06 V1.5.2 发布 - -**新增** - -1. 可以在消息中心接收到最新的通知 -2. 如果碰到问题,可以在设置-问题反馈中向我们反馈 -3. 付费数据新增推广费用曲线 -4. 来源下过滤条件筛选支持付费数据筛选 - -**优化** - -1. 游戏列表昨日数据根据项目时区变化 -2. 修复概览页面今日柱状图时间错位问题 -3. 过滤条件筛选交互优化 -4. 常用货币增加国家 logo -5. 优化网络环境较差时的交互与提示 - ---- - -## 2018-08-09 V1.5.1 发布 - -**新增** - -1. 新增 DEMO 入口 - -**优化** - -1. 修复部分场景下进入客户端闪退的问题 - ---- - -## 2018-07-31 V1.5.0 发布 - -**新增** - -1. 项目根据项目所在时区显示数据 -2. 当你所在时区和项目时区不一致时,增加提示信息 - -**优化** - -1. 整体布局以及细节样式优化 -2. 优化数据显示样式,对未满足天数标红;今日数据标绿 -3. 下拉刷新体验优化 -4. 过滤条件页面支持右滑返回 -5. 无网络连接提示更新 -6. 修复表格全屏时样式问题 -7. 修复登录页 1Password 与清除图标重合的问题 -8. 解决 TouchID/FaceID 锁定状态下的问题 -9. 修复唤醒 App 后卡在开屏图的问题 - ---- - -## 2018-06-11 V1.4.4 发布 - -**修复** - -1. 修复在来源、活跃、付费下不能下拉刷新的 bug - ---- - -## 2018-06-11 V1.4.3 发布 - -**新增** - -1. 付费-用户模块新增了 2、4、5、6 日 LTV 数据 -2. 付费-周期模块改版,支持多日数据对比查看 -3. 付费下支持按「付费方式」「商品名称」查看数据 -4. 活跃-行为模块支持查看小时数据 -5. 新增「关于 TapDB」,可查看当前版本以及功能介绍 - -**优化** - -1. 设置按钮位置调整到右上角 -2. 指纹解锁在开启后显示 -3. 修复 iOS10,iPhone6 闪退问题 -4. 表头内容显示优化 - ---- - -## 2018-05-14 V1.4.2 发布 - -**新增** - -1. 支持将当前设备加入测试白名单,用于测试新增数据 -2. 可以按照日、周、月查看活跃数据 -3. 增加顶部公告 - -**优化** - -1. 全面优化结构,提高 App 响应速度 -2. 概览支持右滑返回上一级界面 -3. 图表支持长按查看数据 -4. 已标记项目按照收入排序 - ---- - -## 2017-12-22 V1.3.0 发布 - -**新增** - -1. 游戏支持置顶,让你更快的找到常用游戏 -2. 来源中支持按照累计付费、首日付费、付费次数进行数据筛选 -3. 可以在设置中提交问题和建议 - -**优化** - -1. 游戏列表按照收入排序 -2. 界面布局优化 -3. 修复了过滤条件内搜索不全的 bug -4. 显示货币支持越南盾 - ---- - -## 2017-11-15 V1.1.0 发布 - -**优化** - -1. 适配 iPhoneX - ---- - -## 2017-10-20 V1.0.0 发布 - -**新增** - -1. 支持在概览页以及活跃下查看实时在线数据 -2. 可按照不同的货币类型查看收入数据 diff --git a/versioned_docs/version-v4/sdk/tapdb/changelog/web.mdx b/versioned_docs/version-v4/sdk/tapdb/changelog/web.mdx deleted file mode 100644 index c0fe1334c..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/changelog/web.mdx +++ /dev/null @@ -1,614 +0,0 @@ ---- -title: 网页版 -sidebar_position: 1 ---- - -## 2.17.0 | 2024-07-01 发布 -**功能**更新 -1. 分析模块中,维度支持重命名 -2. 新增数据预警功能,触发预警后,可发送到企业微信、Slack、邮箱 -3. 显示游戏在 TapTap 的具体状态,如测试服、先行服,方便区分同名项目 -4. TapTap 广告渠道支持自定义开启回传配置 -5. 支持全渠道的广告回调日志查询 -6. 新增 31 - 60 日留存 -7. 游戏列表页支持筛选活跃的项目 -8. 支持隐藏项目,隐藏后不再游戏列表显示 -9. 过滤条件现在可以共享给项目成员了 -10. 付费新增退款数据 -11. 用户价值(LTV)支持显示每日的 LTV 相比首日 LTV 的倍数 -12. 新增全局观察者角色,该角色有企业下所有项目的权限(含新项目),但是没有管理权限 -13. 概览的收入模块增加周、月推广费用 - -## 2.16.0 | 2022-12-16 发布 -**功能**更新 -1. 新增用户精查功能 -2. 国际版支持在分析模型中切换查询时区 - -## 2.15.2 | 2022-09-08 发布 -**体验**优化 -1. 诊断:崩溃分析、错误分析问题详情页,新增打开新窗口按钮 - - -## 2.15.1 | 2022-09-01 发布 -**体验**优化 -1. 崩溃分析、错误分析可通过链接分享部分筛选条件 - - -## 2.15.0 | 2022-08-26 发布 -**功能**更新 -1. 新增诊断模块:包含崩溃分析、错误分析、符号表管理、标签管理 4 个功能。 -2. 使用诊断模块需要接入 TapSDK 3.14.0 或更新版本。 - -**功能**优化 - -1. 为了提升分析能力,我们为所有项目提供了手机硬件相关的一些字段,可以在「分析」模块中使用。 - -| 字段名 | 显示名 | -| --- | --- | -| product_name | 产品名 | -| release_year | 发售年份 | -| chipset_brand | 芯片组品牌 | -| chipset_model | 芯片组型号 | -| single_core_score | 单核跑分 | -| multi_core_score | 多核跑分 | -| price_1_median 1 | 当前市价 | -| price_2_median 2 | 当前二手价 | - - -## 2.14.3 | 2022-08-11 发布 -**体验**优化 -1. 优化了「配置模块 - 预警管理」的操作体验 - - -## 2.14.2 | 2022-07-28 发布 -**体验**优化 -1. 优化了配置模块大部分功能的的操作体验 - -## 2.14.1 | 2022-07-14 发布 -**体验**优化 -1. 优化了「配置 - 用户标签」的操作体验 - -## 2.14.0 | 2022-06-29 发布 - -**自定义分析**更新 - -1. 事件分析不再需要预先选择分析主体,现在支持同时对多个指标选择不同的分析主体 -2. SQL 分析中简化了表名 -3. SQL 分析中添加了表字段的描述,方便大家使用时同步查阅 - -## 2.13.0 | 2022-06-09 发布 - -**多语言支持**升级 -1. 将语言切换到英文时,菜单支持英文显示了 - -## 2.12.0 | 2022-05-26 发布 - -**可视化**升级 - -1. 自定义分析可视化支持添加对比日期后的可视化 -2. 自定义分析支持更多种可视化选项 -3. 看板同步支持新的可视化选项 - -## 2.11.4 | 2022-04-21 发布 - -**体验**优化 - -1. 小时、分钟颗粒度的时间选择器体验优化 - -## 2.11.3 | 2022-04-07 发布 - -**体验**优化 - -1. 维度表上传时,不再丢弃未匹配到属性值的行 -2. 留存分析的全局筛选中「相对事件时间」修改为「相对初始事件时间」与「相对回访事件时间」 -3. 修复下载报表时指标名与系统中报表展示不一致的 bug - -## 2.11.2 | 2022-03-03 发布 - -**功能**更新 - -1. 优化「事件分析」中的「总计」逻辑,所有总计均为在数据明细层面的计算 - -## 2.11.1 | 2022-02-17 发布 - -**功能**更新 - -1. 「事件分析」与「看板」可视化中新增「分组的筛选排序」功能:可以查询结果按照指标值、分组名进行排序并进行筛选 -2. 优化了部分功能体验 - -**文档**更新 - -1. 更新「[看板](/sdk/tapdb/features/custom-event/kanban)」文档,以便于大家了解「分组的筛选排序」功能 - -## 2.11.0 | 2022-01-20 发布 - -**功能**更新 - -1. 新增「预警管理」功能:大家可以通过预警管理功能实时监控关键运营指标的变化情况 -2. 优化了部分功能体验 - -**文档**更新 - -1. 新增「[预警管理](/sdk/tapdb/features/custom-event/alert)」文档,以便于大家更方便的使用预警管理功能 - -## 2.10.0 | 2022-01-13 发布 - -**功能**更新 - -1. 新增「用户标签」功能:大家可以通过用户标签功能将用户划分为不同群体,便于对用户深度分析 - -**文档**更新 - -1. 新增「[用户标签](/sdk/tapdb/features/custom-event/user-tag)」文档,以便于大家更方便的使用用户标签功能 - -## 2.9.2 | 2021-12-16 发布 - -**功能**更新 - -1. 「测试设备」、「数据导出」和「数据大屏」功能下线 -2. 点击「工具」可以看到新的「测试设备」功能入口 - -**文档**更新 - -1. 调整「测试设备」文档,以便于大家更方便的使用测试设备功能 - -## 2.9.1 | 2021-12-09 发布 - -**自定义事件分析**更新 - -1. 时间筛选器优化 - -**文档**更新 - -1. 在[接入问题](/sdk/tapdb/faq/tran)新增接入方面的常见问题,以便于大家更方便的接入 SDK - -## 2.9.0 | 2021-11-25 发布 - -**自定义事件分析**更新 - -1. 新增「虚拟事件」功能:大家可以将事件进行拆解或者组合形成新的「虚拟事件」,提高分析效率 -2. 优化了很多功能体验 - -**文档**更新 - -1. 新增「[虚拟事件](/sdk/tapdb/features/custom-event/virtual-event)」文档,以便于大家更好的了解和使用虚拟事件功能 -2. 新增「[广告管理](/sdk/tapdb/features/custom-event/ad)」文档,以便大家更好的了解和使用广告功能 - -## 2.8.0 | 2021-11-11 发布 - -**自定义事件分析**更新 - -1. 新增「SQL 查询」功能:大家可以使用标准 SQL 对 TapDB 的所有数据进行查询 -2. 优化了部分功能体验 - -**文档**更新 - -1. 新增「[SQL 查询](/sdk/tapdb/features/custom-event/sqlide)」文档,以便于大家更好的了解和使用 SQL 查询功能 - -## 2.7.1 | 2021-10-29 发布 - -**新增**功能
    - -1. 增加 TapTap 登录
    - -## 2.7.0 | 2021-10-21 发布 - -**自定义事件分析**更新 - -1. 新增「维度表」功能:大家可以从系统外部引入维度表对现有属性进行映射加工,丰富分析维度信息 -2. 优化了「分布分析」:支持通过公式构建指标 -3. 优化了很多功能体验 - -**文档**更新 - -1. 新增「[维度表](/sdk/tapdb/features/custom-event/dimension-table)」文档,以便于大家更好的了解和使用维度表功能 - -## 2.6.1 | 2021-10-14 发布 - -**自定义事件分析**更新 - -1. 优化了「分布分析」功能:支持对分析结果创建用户分群 - -## 2.6.0 | 2021-09-29 发布 - -**自定义事件分析**更新 - -1. 新增「属性分析」功能,帮助大家分析玩家群体的属性特征 - -**文档**更新 - -1. 新增「[属性分析](/sdk/tapdb/features/custom-event/property-analyse)」文档,以便于大家更好的了解和使用属性分析功能 - -## 2.5.0 | 2021-09-16 发布 - -**自定义事件分析**更新 - -1. 新增「分布分析」功能:分析用户行为的总次数或属性值按用户聚合后的分布情况 -2. 对「漏斗分析」、「查询主体」切换、「虚拟属性」进行了使用体验优化 - -**文档**更新 - -1. 新增「[分布分析](/sdk/tapdb/features/custom-event/distribution-analyse)」文档,以便于大家更好的了解和使用分布分析功能 -2. 在「[接入问题](/sdk/tapdb/faq/tran)」里新增了一些问题说明 -3. 新增搜索框,方便大家更快捷的查找问题答案 - -## 2.4.0 | 2021-09-02 发布 - -**自定义事件分析**更新 - -1. 新增「留存分析」可视化趋势图,帮助大家更直观的进行留存分析 -2. 优化了「元数据管理」的信息展示 -3. 优化了埋点入库的判断逻辑,以便于更准确的监控埋点上报状况 - -**运营报表**优化 - -1. 优化了运营报表的过滤条件,显示更完整的过滤条件值 - -**文档**更新 - -1. 对「[留存分析](/sdk/tapdb/features/custom-event/retention-analyse)」内容进行修改,帮助大家更好的了解和使用留存分析功能 - -## 2.3.0 | 2021-08-20 发布 - -**自定义事件分析**更新 - -1. 新增「虚拟属性」功能:用于对事件属性和用户属性二次加工,产生一个新的属性值 -2. 在「上报明细」增加监测开关 - -**运营报表**优化 - -1. 对「运营」-「付费」-「付费数据」报表的付费人数、活跃人数在跨天计算时进行了去重优化 - -**文档**更新 - -1. 新增「[虚拟属性](/sdk/tapdb/features/custom-event/virtual)」,以便于大家更好的了解和使用虚拟属性功能 -2. 在「[埋点管理](/sdk/tapdb/features/custom-event/tracking-management)」补充了监测开关使用说明 - -## 文档更新 | 2021-08-12 发布 - -**文档**更新 - -1. 新增「[关于我们](/sdk/tapdb/features)」,以便于大家更好的了解 TapDB -2. 新增「[埋点设计指南](/sdk/tapdb/features/custom-event/data-model)」,用于指导大家更好的设计自定义事件 -3. 新增「[服务端接入文档](/sdk/tapdb/sdk/server-side-integration)」,介绍如何进行服务端接入 -4. 对「[快速接入]、「[数据模型](/sdk/tapdb/sdk/data-spec)」和「[事件分析](/sdk/tapdb/features/custom-event/event-analyse)」的内容进行了修改,帮助大家更好的理解接入过程、基础概念和事件分析功能 -5. 调优了文档结构 - ---- - -## 2.2.0 | 2021-07-29 发布 - -**埋点管理**上线 - -1. 这是一个测试项目数据上报的功能,用于校验埋点的正确性 - -**文档**更新 - -1. 新增「[埋点管理](/sdk/tapdb/features/custom-event/tracking-management)」、「[用户分群](/sdk/tapdb/features/custom-event/cluster)」的使用文档 -2. 调优了文档结构 -3. 优化了基础指标的说明,以便于大家更准确地理解指标含义 - -部分功能下线 - -1. 调试模式下线:其功能已被埋点管理所覆盖 -2. cocos-2d SDK 下线 - ---- - -## 2.1.0 | 2021-07-15 发布 - -**自定义事件分析**更新 - -1. 新增「**结果分群**」功能:可以直接在报表中将查询 「触发用户数」 的指标结果快捷保存为用户分群 -2. 优化了针对自定义事件分析的权限分配,以保证数据安全和防止误改配置信息 -3. 针对上传 ID 创建用户分群的效率进行了调优 - -**SDK**更新 - -1. 调优了 GAID(谷歌广告 ID) 的获取方式 -2. 调优了在 iOS 上获取 & 生成设备 ID 的方式,使其更加稳定了 - ---- - -## 2.0.0 | 2021-06-17 发布 - -**自定义事件分析**功能限时免费中,欢迎大家体验! - -**自定义事件分析**更新 - -1. 新增「**看板**」功能:用于将关注的核心指标和报表放到一屏内快速查看,使用方法详见**自定义事件分析 → 看板** - -**TapDB Demo** 更新:现在可以在 Demo 中体验**看板**功能了! - ---- - -## 1.9.1 | 2021-05-13 发布 - -**自定义事件分析**更新 - -1. 新增「**事件分析**」可视化图表:用于查看趋势和进行对比 - -**TapDB Demo** 更新:现在可以在 Demo 中体验更完整的 TapDB 功能了! - -1. 新增**自定义事件分析**模块 -2. 新增**广告**模块 -3. 新增**工具**模块 - ---- - -## 1.9.0 | 2021-04-15 发布 - -新的分析模块:新增了自定义事件分析,现在可以更自由地查询更多的用户行为了! - -关于新功能的使用和接入建议详细阅读使用手册 - -**自定义事件分析** - -1. 新增「**事件分析**」:分析用户行为的次数、人数、地区分布等 -2. 新增「**留存分析**」:分析广义留存(请查看使用手册或体验产品)用户的行为 -3. 新增「**漏斗分析**」:分析用户逐步( 2~N 步)转化的转化率,步骤间有严格的先后顺序 - -**配置工具** - -1. 新增「**事件管理**」:用于管理事件,所有自定义事件上报前需要在此进行预登记 -2. 新增「**事件属性管理**」:用于管理事件属性,所有事件可以自由配置事件属性 -3. 新增「**用户属性管理**」:用于管理用户属性,所有事件可以自由配置用户属性 -4. 新增「**用户分群**」:设定条件生成用户分群,用户分群可以作为筛选条件应用于自定义事件分析 - ---- - -## 2019-01-18 更新日志 - -**新功能** - -1. 在图表上,对**周末**进行了**标记**,方便查看数据趋势 -2. 增加**活跃设备**数据 -3. **在线分析**支持查询**7 天以上**的数据 -4. 支持在图表上直接编辑**运营事件** - ---- - -## 2018-11-23 更新日志 - -**新功能** - -1. 新增**企业运营报告**,可以在右上角企业中查看,按自然月查询企业所有项目的关键指标 -2. 数据导出功能增加**分包渠道**数据导出 - ---- - -## 2018-08-03 更新日志 - -**新功能** - -1. 新增**原始付费数据**导出,可以在工具-数据导出中下载 - -**修复** - -1. 过滤条件在部分场景下的搜索排序问题 -2. 外部成员中复制链接的 flash 提示问题 -3. 收起侧边栏时的图表渲染问题 - ---- - -## 2018-07-27 更新日志 - -**新功能** - -1. 新版报表中心,下载同时可以进入其他页面看数据,未来将支持更多原始数据导出 -2. 新增特殊用户归类,可以将内部用户、刷榜用户、特殊用户标记为特殊广告渠道,方便查询和过滤 -3. 广点通支持授权模式 -4. 概览支持按日、周、月查询数据 -5. 广告默认查询有数据的广告,提高响应速度 -6. 广告增加硬件平台筛选 - -**修复** - -1. LTV 指标优化,更精准地描述指标定义 -2. 修复部分场景下选择一日,没有显示小时的问题 -3. 搜索优先出精确匹配的结果 -4. 广告下使用设备、账号相关的过滤条件(如机型、地区)时,不展现点击信息,避免误解 - ---- - -## 2018-06-08 更新日志 - -**新功能** - -1. 首页游戏列表支持选择**对比日期** - -2. 付费支持按照**商品**查询数据 - ![图片描述](/img/changelog/2018_06_08_01.png) - -3. **官网**全面更新 - -**修复** - -1. 部分场景下,总计显示乱码的问题 -2. 部分场景下,图表没有渲染的问题 - ---- - -## 2018-05-18 更新日志 - -**新功能** - -1. **用户价值(LTV)** 支持查看第 2、4、5、6 日的数据 -2. TapDB 开始支持多种语言了,目前支持 **简体中文**、 **繁体中文** 、**英语**,即将支持 **韩语** 、**日语** -3. **设备白名单** ,如果需要测试新的分包渠道,新的广告是否调通,可以将设备加入白名单,这样可以在一台设备上反复测试不同渠道。 - -![图片描述](/img/changelog/2018_05_21_01.png) - -**修复** - -1. 广告中的作弊主维度下,修复了存疑广告会将自己计算在内的问题 - ---- - -## 2018-05-04 更新日志 - -**新功能** - -1. **防作弊基础版** ,提供 3 种基础防作弊规则:点击 IP 离散作弊、激活 IP 离散作弊、点击激活时长作弊。 - -2. 支持 2 个新的广告平台: **陌陌** 、 **百度信息流 ocpc** - -3. 按 **支付渠道** 查询收入 - ![图片描述](/img/changelog/2018_05_08_01.png) - -4. **隐藏渠道** ,可以将不常用、测试、传递错误的渠道归类到隐藏渠道 - ![图片描述](/img/changelog/2018_05_08_02.png) - -**修复** - -1. 概览下的新增设备未显示运营事件 - ---- - -## 广告概览 - -###### 更新日期 2018-04-12 - -广告概览可以帮助你 - -- 监控今日的点击、新增、转化是否正常 - -![](/img/changelog/2018_04_12_1.png) - -- 了解今日投放量 Top10 的广告平台 - -![](/img/changelog/2018_04_12_2.png) - -- 分析近 30 日的新用户获取趋势、支出和费用趋势 - -![](/img/changelog/2018_04_12_3.png) - ---- - -## 导出细分数据 - -###### 更新日期 2018-01-11 - -广告监测支持导出广告平台、广告、标签的细分数据,导出后方便查看每个广告下每天的数据 - -![](/img/changelog/2018_01_11_01.jpeg) - ---- - -## 小时活跃、周活跃和月活跃 - -###### 更新日期 2017-12-15 - -1. 现在可以查看不同日期粒度下的活跃了: - -- **小时活跃**(HAU):日期选择某一天 -- **日活跃**(DAU):日期选择某一段时间 -- **自然周活跃**(WAU):活跃页面下切换到「周活跃」 -- **自然月活跃**(MAU):活跃页面下切换到「月活跃」 - -其中周活跃、月活跃可以细分不同的分包渠道来查看渠道活跃明细。 - -![](/img/changelog/2017_12_15_1.png) - -2. 概览中增加月活跃曲线(MAU) - ![](/img/changelog/2017_12_15_2.png) - ---- - -## 选择时区 - -###### 更新日期 2017-12-06 - -创建项目的时候可以选择不同的时区,方便不同地区发行的游戏更好的分析数据 - -![](/img/changelog/2017_12_06_01.jpeg) - ---- - -## 显示货币 - -###### 更新日期 2017-10-13 - -现在可以按照不同的货币类型来查看收入数据了 - -![](/img/changelog/2017_10_17_01.jpeg) - ---- - -## 数据导出 - -###### 更新日期 2017-07-20 - -在工具中使用,可以根据广告平台或广告标签导出新增用户的具体信息,比如新增帐号,IDFA 等 - -![](/img/changelog/2017_07_25_6.png) - ---- - -## 操作日志 - -###### 更新日期 2017-07-13 - -可在企业设置里查看操作日志,目前已上线项目、权限、广告三种日志类型,方便您更加快捷地了解项目状态、人员及权限变化、广告投放等情况。 - -![](/img/changelog/2017_07_25_5.png) - ---- - -## 实时在线 - -###### 更新日期 2017-07-12 - -「实时在线」功能可帮你进行实时在线分析,通过分钟级在线量展现实时现数据动态。 -您可在[【工具-拓展功能】](//tapdb.com/dm/m/t/extensions)或[【在线分析】](//tapdb.com/dm/m/g/active/online)中自主开启并使用了。 - -![](/img/changelog/2017_07_25_4.png) - ---- - -## 180/360 日 LTV - -###### 更新日期 2017-07-07 - -运营和广告下增加 180/360 日 LTV - -- 运营统计中可在付费-用户价值中查看 -- 广告监测中可在自定义列中添加并查看 - ---- - -## 对比日期 - -###### 更新日期 2017-06-02 - -对比日期,在运营和广告模块增加对比日期功能,可以对不同时间段的数据进行对比,在图和表中均可查看对比数据的详情 - -![](/img/changelog/2017_07_25_3.png) - ---- - -## 现在可以钉住常用的对比日了 - -###### 更新日期 2017-05-25 - -在选择游戏概览中的对比日期时,新增了历史日记录功能,最多记录您最近 5 次选择的日期,您可以钉住其中常用的日期,并为之特殊命名,钉住的日期将一直保存,方便您下次使用。 - -![](/img/changelog/2017_07_25_2.jpeg) - ---- - -## 为渠道命名 - -###### 更新日期 2017-04-16 - -可以对分包渠道的名称进行备注,在工具栏里可以使用 - -![](/img/changelog/2017_07_25_1.jpeg) - ---- - -## 存疑新增 - -###### 更新日期 2017-04-16 - -广告页面增加存疑新增功能,标记了匹配了多个广告新增设备 diff --git a/versioned_docs/version-v4/sdk/tapdb/faq/_category_.json b/versioned_docs/version-v4/sdk/tapdb/faq/_category_.json deleted file mode 100644 index 9af634a73..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/faq/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "常见问题", - "position": 6 -} diff --git a/versioned_docs/version-v4/sdk/tapdb/faq/ads.mdx b/versioned_docs/version-v4/sdk/tapdb/faq/ads.mdx deleted file mode 100644 index 58367722e..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/faq/ads.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: 广告相关问题 -sidebar_position: 3 ---- - -### Q:如何创建广告计划? - -A:进入 TapDB,广告页面,点击广告管理,选择右上角的新增广告活动,选择广告平台,注意看下匹配方式,填写信息,生成投放链接。 - -### Q:什么是 IP 匹配? - -A:IP 匹配是通过用户点击广告时的 IP 和打开游戏时的 IP 是否一致进行匹配的,适用所有广告。创建广告活动时如果没有发现对应平台可以创建自定义广告。 - -### Q:什么是设备匹配? - -A:设备匹配是 TapDB 和广告平台已经对接完成,广告平台将点击广告的数据返回 TapDB,通过设备信息来匹配新增用户的,十分精准,但仅使用于和 TapDB 对接完成的广告平台。 - -### Q:IP 匹配和设备匹配怎么投放? - -A:TapDB 的广告匹配分为 IP 匹配和设备匹配: - -IP 匹配: 在新增广告活动时,选择广告平台,IP 匹配的广告平台都有标注,且 IP 匹配的广告需要填写下载链接,生成链接后,直接将 IP 匹配生成的投放链接填入广告平台的落地页 URL 即可。 - -设备匹配: 设备匹配填写相应参数后可以直接生成回调链接,填入广告平台的第三方监控链接(名称可能不同)中即可,落地页 URL 填写游戏本身的下载地址。 - -### Q:不能使用 TapDB 的链接投放广告时怎么办? - -A:比如说游戏的域名是 a.b.c,需要把 a.b.c 以 CNAME 的形式解析到 [l.tapdb.net](http://l.tapdb.net),然后如果 TapDB 给出的连接是 `https://l.tapdb.net/xxxx` ,直接替换 [l.tapdb.net](http://l.tapdb.net) 为 a.b.c,https 改为 http,变成 `http://a.b.c/xxxx` ,使用该链接投递广告。 - -### Q:为什么投放广告匹配到其他系统的玩家? - -A:广告匹配到其他系统的玩家可能有以下原因: - -(1)投放在 web 上,iOS 和 Android 用户都会被匹配。 - -(2)iOS 的投放链接,Android 用户(能看到该广告)可能会点击后主动去搜索该 app 下载,就会被匹配上,同理 Android 广告也可能被 iOS 用户匹配。 - -### Q:如何开启 TapTap 广告渠道的数据回调? - -A:前往广告->广告管理->渠道设置,可以为 TapTap 广告渠道开启数据回调,目前支持回调激活、注册、付费、次留数据 \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/tapdb/faq/tran.mdx b/versioned_docs/version-v4/sdk/tapdb/faq/tran.mdx deleted file mode 100644 index 83886edcd..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/faq/tran.mdx +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: 接入问题 -sidebar_position: 1 ---- - -### Q:SDK 初始化成功,为什么没有新增数据? - -A:在排查之前,你要粗略了解数据上报的流程: - -一、游戏 App 调 SDK 的相关接口上报数据; - -二、DB 平台收到上报的数据,并在「上报明细」展示; - -排查方法也是基于这个流程一步一步进行。 - -打开「配置」-「上报明细」,然后进行排查: - -1、检查初始化是否是最早调用?若没有最早调用,请调整后重试; - -2、观察「上报明细」功能里是否显示上报的数据;如果显示数据,则表明数据正常上报;若不显示数据,则进入 3; - -3、打开手机抓包工具,测试设备是否正常上报数据; - -### Q:SDK 初始化成功,为什么新增数据不正确? - -A:你可以使用「上报明细」功能来验证埋点上报准确性。 - -### Q:如何查看上报的数据? - -A:你可以通过「上报明细」和「埋点管理」来查看上报的数据 - -「上报明细」:一般在 SDK 的接入调试阶段以及在埋点测试时,你可以使用「上报明细」查看实时上报的埋点数据; - -「埋点管理」:可以查看最近 7 日项目内数据接收情况,快速了解埋点上报整体情况,以及错误上报详情与抽样示例; - -### Q:为什么 SDK 初始化失败? - -可以尝试对 gameversion 字段调试,若无值,需填入值。 - -### Q:单机游戏的 userId 如何储存? - -A:单机游戏的 userId 需要注意以下几点: - -(1)iOS 得自己生成一个 ID 存到证书空间,iOS 有个存储空间,一个是应用,一个是证书(企业)。 - -(2)安卓尽量存到 SD 卡,用一个用户操作或者 1 分钟后调用 `setUser`。 - -(3)随机生成一个唯一用户 ID,并保存到本地。 - -### Q:为什么 SDK 接入后没有新增数据? - -A:请检查 SDK 初始化是否调用成功,另外初始化应该最早调用。若初始化失败,则根据失败日志做对应处理。 - -### Q:服务端和客户端都需要传递充值数据么? - -A:在服务端和客户端选择一种方式传递充值数据即可。若同时传递,则充值数据会翻倍。 - -### Q:为什么服务端传递充值数据 TapDB 页面没有显示收入或者收入比实际要多? - -A:首先服务端和客户端的接口只能用其中一个(若都使用的话会展示双倍充值金额,并确保充值成功再发送充值数据),其次服务端「identify」: 「user_id」里的 user_id 和文档「纪录一个玩家」中的 setUser 里的「userId」需要保持一致。 - -### Q:为什么实时在线数据发送后 TapDB 显示没有数据? - -A:没有显示数据可能有以下几种原因: - -(1)检查文件格式,参数类型(返回 400 报错说明格式不对) - -(2)注意时间戳单位(秒),并且只能发送近 7 日数据,太早的数据不会保存 - -**(3)注意:是否有必须头信息:`Content-Type: application/json`** - -### Q:可以为空的参数能不填么? - -A:不能,可以为空的参数填 `null`。 - -### Q:为什么填了 channel 在 TapDB 里找不到这个分包渠道? - -A:必须要有一个新增数据 TapDB 才能接收到这个渠道,才能在这个页面里显示这个分包渠道。 - -### Q:TapDB 上报事件时,事件是大小写敏感的吗? - -A:所有的 key **不区分**大小写,所有的 value **区分**大小写。事件名是 value,属性名是 key,属性值是 value。 - -### Q:上报自定义事件时,属性类型必须和登记的完全一致吗?是否有什么兼容策略? - -A:必须完全一致,无兼容策略。错误类型的会被 agent 直接当做脏数据抛弃 - -### Q:能否进行私有化部署? - -A:暂时不支持私有化部署。但我们会保证你的数据安全。 - -### Q:如何设置权限? - -A:在「企业设置」-「权限管理」-「编辑成员」可以对相应账号进行编辑和权限去除操作。 - -### Q:自定义事件上报后,后台查不到数据 - -A:请先检查代码中是否设置了 [user_id](/sdk/tapdb/sdk/client-side-integration/#设置账号-id) 。如果不确定的话可以去后台使用 SQL 查询,在结果中可以看到曾经上报过的 user_id。 - -![ SQL 查询地址](/img/数据分析-Sql查询.png) - -```SQL -SELECT * FROM hive_saas1.tapdb."users" -WHERE user_id LIKE'%dTJTp6sA+OWsZ7Jf0JmGg==%' -LIMIT 100 -``` -检查上报的 user_id 里有没有需要上报的用户的 user_id。 - - diff --git a/versioned_docs/version-v4/sdk/tapdb/faq/user.mdx b/versioned_docs/version-v4/sdk/tapdb/faq/user.mdx deleted file mode 100644 index 839427a35..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/faq/user.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: 使用问题 -sidebar_position: 2 ---- - -### Q:如何查看各种维度下的数据? - -A:在来源,在线,留存,付费四个页面里的表格中都有主维度的切换选项,每个维度下显示的数据都不同,可以根据不同情况来查看各个维度下的数据。 - -### Q:什么是分包渠道? - -A:分包渠道即为上报数据时其中的 channel 字段信息,方便大家对数据进行拆分。接入方法详见 [TapSDK 初始化](/sdk/tapdb/sdk/client-side-integration#tapsdk-init)。 - -### Q:如何使用过滤条件? - -A:过滤条件可以根据不同的选项,不同的范围来选择 TapDB 表格内展现的数据,方便区分各种类型的用户。可以添加多个过滤条件来精确筛选满足条件的数据,也可以保存设定好的过滤条件方便下次使用。 - -### Q:为什么来源的收入比实际的收入要少? - -A:来源只显示新增用户的付费收入情况,老用户的付费情况请在付费页面查看。 - -### Q:为什么游戏的转化率很低? - -A:转化率低可能有以下几种原因: - -(1)技术人员确保 SDK 在用户登录的时候调用了 `setUser`。 - -(2)运营人员注意是否有大量的新增设备,可能有作弊机器在刷新增。 - -### Q:如何查询项目接入了什么版本的 TapDB SDK? - -A:在事件分析中,使用「事件发生时的 SDK 版本」作为维度,「任意事件」作为指标查询即可。 - -### Q:如何验证埋点上报数据准确性? - -A:使用埋点管理验证埋点准确性。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features.mdx b/versioned_docs/version-v4/sdk/tapdb/features.mdx deleted file mode 100644 index 6f35f6beb..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: TapDB 功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -TapDB 是一套专注于解决游戏项目数据需求的分析工具,致力于帮开发者实现低成本、高效率的接入与查询体验。 - -## 初衷 - -同为游戏开发者,我们深知做游戏的不易。思考如何打磨游戏产品就足够令人头疼了,选择一套适合自己的数据工具并正确地用起来可能会耗费相当的精力且效果一般。于是我们相信提供一套低门槛又足够好用、精确的工具一定是有高价值的。 - -在做游戏的这些年,我们积累了一些自己认可的经验和方法,希望可以通过 TapDB 这个产品将这些经验和能力共享给大家。我们坚信 TapTap 和良好的行业生态具有共生性:帮助开发者创造优质的游戏就是在帮助 TapTap 成长。 - -## 实用功能 - -TapDB 服务提供的主要功能有: - -### 基础 BI - -非常实用、无门槛上手的四大数据报表,沉淀着我们数十年的游戏从业经验。 - -### 衍生事件 - -为了开发者的使用体验,我们通过一系列衍生事件将基础 BI 的查询时间缩短至 1/10 不到,确保你能快速访问所需要的数据。 - -### 广告投放跟踪 - -对接了全球主流广告投放系统(如巨量引擎、AMS、AppsFlyer 等),轻松完成广告投放追踪、回调以及数据分析。 - -### 权限角色 - -给每个使用者配备独立账号并单独控制权限,确保数据安全。 - -### 自定义事件分析 - -- 自由定制你所想要的事件,关注那些你最关心的行为。 - -- 多维透视,快速拆解数据,助你更高效地找到问题根源。 - -- 日志级查询能力,用户做了什么了如指掌。 - -## 优势和特色 - -- 低门槛接入:接入基础事件非常简单,而这已足够让你获得非常完善的分析和广告投放能力。 - -- 完全无延迟:上报后可以立刻查到数据,时间就是生命。 - -- 保持更新的游戏分析框架:我们会将最新的经验和方法持续地分享给你。 - -- 结合 TapTap 生态的数据,为开发者提供全链路的游戏数据分析能力。 - -- 免费使用。 - -:::tip -如需使用自定义事件分析和看板功能,请通过工单联系我们申请开通。 -::: diff --git a/versioned_docs/version-v4/sdk/tapdb/features/_category_.json b/versioned_docs/version-v4/sdk/tapdb/features/_category_.json deleted file mode 100644 index 875bf03cc..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "功能指南", - "position": 4 -} diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/_category_.json b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/_category_.json deleted file mode 100644 index 38c4107a3..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "自定义事件", - "position": 1 -} diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/ad.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/ad.mdx deleted file mode 100644 index 59cfd3270..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/ad.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: 广告管理 -sidebar_position: 16 ---- - -## 1. 广告概览 - -对于互联网产品,每天的新增用户的数量是个非常受关注的指标。同时,围绕着新增用户这个指标,还可以有很多更加深入的分析,例如: - -- 分析不同渠道带来的新增用户的数量,从而优化后续的投放策略; -- 分析不同渠道带来的新增用户的留存以及转化,从而考察不同渠道带来的用户的质量; -- 分析每天的收入,由不同渠道带来的贡献的比例; - -在广告概览里,可以看到所有广告下的概览数据。也可以看到各个广告平台下各个广告链接的投放数据。 - -![](/img/customEvent/ad/广告概览new.png) - -## 2. 广告管理 - -你可以在广告管理功能里,对广告进行管理,包含广告设置、成本管理、归因设置、查看已删除广告。 - -### 2.1 广告设置 - -你可以对广告活动进行新增、编辑、查询和删除操作。具体功能如图: - -![](/img/customEvent/ad/广告管理.png) - -A:新增广告活动
    -点击后你可以创建广告,输入广告名称、选择要投放的广告平台、选择广告标签、选择子维度。 -点击「生成投放链接」,系统会自动生成投放链接。 - -![](/img/customEvent/ad/新增广告.png) - -B:标签管理
    -广告标签是你为每个广告活动做的备注和标记,可以让你快捷的筛选出具备某个特点的广告活动。在标签管理里可查看已有的标签及标签对应的广告活动,还可以创建新的标签,同时可删除不需要的标签。 - -![](/img/customEvent/ad/标签管理.png) - -C:搜索广告
    -输入搜索词,可以基于广告活动名称进行搜索。 - -D:创建类似广告
    -当你一次性需要创建多个相似广告进行监测时,可以点击创建类似广告,对每个广告活动进行标记。 - -E:编辑广告
    -对广告活动进行编辑 - -F:删除广告
    -已删除的广告活动会显示在这里,你可以恢复广告活动,删除超过 7 天的广告将自动清除。 - -### 2.2 成本管理 - -在成本管理页面可以看到每个广告活动每天的成本,同时广告概览页面里和成本相关的指标也会基于这里的数据进行计算。前提是你要先将成本录入系统。你可以直接在成本管理界面录入,也可以点击「成本导入」以模板的形式批量导入成本。 - -![](/img/customEvent/ad/成本管理.png) - -![](/img/customEvent/ad/上传成本.png) - -### 2.3 归因设置 - -可设置归因「默认窗口期」,同时针对每个平台可以设置单独的归因窗口期。当广告平台未设置单独的归因窗口期时,归因窗口期将跟随系统的窗口期。 - -![](/img/customEvent/ad/归因设置.png) - -### 2.4 已删除广告 - -勾选已删除的广告活动后点击「恢复」,可以恢复对应的广告活动。 - -![](/img/customEvent/ad/恢复广告.png) - -### 2.5 工具-创建链接 - -在「工具」-「创建链接」功能里,你可以把广告数据分享给其他人。你可以选择分享给普通外部成员和广告代理商: - -1. 分享给普通外部成员:对方只能查询广告相关数据; -2. 分享给广告代理商:对方可以查询广告相关数据、在标签内创建修改广告以及管理标签内广告成本。 - -![](/img/customEvent/ad/创建链接.png) diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/alert.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/alert.mdx deleted file mode 100644 index 9bf4abeb1..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/alert.mdx +++ /dev/null @@ -1,173 +0,0 @@ ---- -title: 预警管理 -sidebar_position: 18 ---- - -## 1. 概述 - -通过设置指标和筛选分组项,构建一组时间序列数据。按照时间颗粒度定时查询并与历史数据进行比较,触发预警规则后通知项目组成员。 - -目前支持以「事件分析」的形式设置指标,以邮件方式进行预警通知。 - -![概述](/img/customEvent/alert_1.png) - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ---------- | ------------------------------------- | -| 分析师 / 业务人员 | 对重点或异常数据进行预警监控,实时掌握产品动态,并可在第一时间发现异常问题 | - -## 3. 新建数据预警 - -点击预警列表右上角的新建预警。 - -![新建数据预警](/img/customEvent/alert_2.png) - -### 3.1 基础信息 - -在「基础信息」部分依次录入或选择预警名、查询主体与预警指标。 - -![填写基础信息](/img/customEvent/alert_3.png) - -预警名展示在预警列表中,是识别一个预警的依据。 - -查询主体可选择「账号」或「设备」,与「事件分析」中查询主体类似。 - -### 3.2 预警规则 - -#### 3.2.1 添加预警规则 - -在预警规则中,可选择分组维度,并通过多选分组值同时构造多组时间序列数据,当无分组维度时,默认分组项为「总体」。 - -![预警规则](/img/customEvent/alert_4.png) - -可选预警时间粒度、比较基准与参数类型的关系如下表: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    时间粒度比较基准参数类型
    固定值数值
    上一天、上周同一天数值、百分比
    过去7天均值、过去30天均值数值、百分比、标准差
    小时固定值数值
    上一小时、昨天同一小时数值、百分比
    过去24时均值数值、百分比、标准差
    - -每个预警下可同时设置多条预警规则,每个预警规则下可添加多个分组。 - -#### 3.2.2 预警规则详情 - -**数值** - -固定值作为比较基准时: - -高:指标实际值 > 数值 - -低:指标实际值 < 数值 - -非固定值作为比较基准时: - -高:指标实际值 > 比较基准 + 数值 - -低:指标实际值 < 比较基准 - 数值 - -**百分比** - -高:指标实际值 > 比较基准 * (1 + 百分比) - -低:指标实际值 < 比较基准 * (1 - 百分比) - -**标准差** - -高:指标实际值 > 比较基准 + 参数 * 标准差 - -低:指标实际值 < 比较基准 - 参数 * 标准差 - -其中,标准差为比较基准均值对应的数量与时间颗粒度周期内的所有指标实际值的标准,如「过去 7 天均值」对应的标准差为,过去 7 天每天的指标实际值的标准差。 - -### 3.3 通知设置 - -目前支持邮件,[企业微信群](https://open.work.weixin.qq.com/help2/pc/14931?person_id=1&is_tencent),[Slack](https://api.slack.com/messaging/webhooks) 渠道进行预警通知。 - -![通知设置](https://capacity-files.lcfile.com/skDpU63nDH6cMru3StmXsvcmsid3pXtp/push.png) - -## 4. 预警通知 - -根据每条预警规则设置的时间颗粒度,系统会在每天或每小时结束后对预警指标按照设置的分组进行查询。 - -触发预警规则后将对通知设置中的渠道进行告警,如图: - -![邮件示例](https://capacity-files.lcfile.com/J7DoenoNMw4NbW0Qq8nTseY65aeKIt5l/push_1.png) - -![企业微信群示例](https://capacity-files.lcfile.com/An78t1qdEnvrWzENHBgLHXY9s9wMdLcN/push_2.png) - -![Slack 示例](https://capacity-files.lcfile.com/1W2n3oKOqeu4KDVH8IsC1lqY0hCIxNpe/push_3.png) - - -## 5. 预警的管理与详情 - -### 5.1 预警列表管理 - -已创建的数据预警会以列表的形式展示在数据预警页,可对预警进行详情查看、启动 / 暂停、编辑、复制、删除等操作。 - -![预警列表管理](/img/customEvent/alert_7.png) - -数据预警的使用进度以「预警实例」作为最基本的单位,预警规则下的一个分组为一个「预警实例」,上限为 30,可通过删除预警、预警规则或勾除分组释放使用进度。 - -### 5.2 预警详情 - -点击预警列表中的预警名跳转至该预警的详情页,点击预警规则跳转至该预警详情页,并筛选相应的预警规则。 - -![预警详情_1](/img/customEvent/alert_8.png) - -页面左侧可看到该预警的「基础信息」、「预警规则」与「通知设置」。 - -页面右侧可看到当前筛选的预警规则与分组数据的历史趋势图与数据表,有预警发生的时刻以红点形式标记在趋势图中,可点击下载按钮下载数据表。 - -![预警详情_2](/img/customEvent/alert_9.png) - -## 6. 最佳实践 - -### 6.1 通过「固定值」预警监控业绩指标 - -对于数值确定的业绩目标,如:充值金额、活跃用户数,可通过「固定值」预警监控其是否完成业绩目标。 - -### 6.2 通过「百分比」预警监控指标环比、同比变化 - -对于日常运营指标,如:活跃用户数、新增用户数,可通过「百分比」预警密切关注其变化趋势,一旦出现异常变化趋势,便于及时关注并干预。 - -### 6.3 通过「标准差」预警监控具有长期变化趋势的指标的短期异常变化 - -对于具有长期增长或衰减的指标,「标准差」预警可消除长期变化趋势对于短期变化的影响,从而监控该指标的短期异常变化情况。 - -### 6.4 多规则预警监控预警区间 - -对于某些重要指标,如:活跃用户数,可创建多条预警规则,同时监控是否完成业绩目标、变化趋势情况等,同时可以对同类监控规则创建「高」、「低」2 条规则,对指标的变化区间进行监控。 - -### 6.5 多分组预警探查异常数据维度 - -对于某些发生了异常变化的指标,可通过拆分分组,找到引起变化的主要维度,便于进一步进行下钻分析。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/cluster.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/cluster.mdx deleted file mode 100644 index f2d65ffd4..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/cluster.mdx +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: 用户分群 -sidebar_position: 9 ---- - -## 1. 概述 - -将符合一定属性和行为特征的用户划分为一个群体,并对该群体进行研究和分析的方式,即用户分群。 - -TapDB 的分析模型中分别以「账号」、「设备」作为查询主体进行查询,在用户分群中同样支持分别以「账号」、「设备」作为主体进行分群。 - -目前提供「条件分群」、「ID 分群」、「结果分群」3 种分群方式。 - -![概述](/img/customEvent/cluster_summary.png) - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ---------- | ---------------------------- | -| 分析师 / 业务人员 | 划分特定用户群体,用以聚焦分析、排除干扰或导出用户列表等 | - -## 3. 创建用户分群 - -### 3.1 条件分群 - -点击分群列表右上角的新建分群,选择「新建条件分群」;此处请注意分群使用进度。 - -新建条件分群页分为「基础信息」、「分群规则」两部分。 - -![新建条件分群](/img/customEvent/cluster_create_condition_cluster1.png) - -在「基础信息」页,依次录入或选择「分群名、分群主体、分群代码、更新方式、备注」。 - -分群名展示在分群列表、分析模型中,是业务人员识别分群的依据。 - -分群主体,支持「账号 ID」或「设备 ID」,根据业务场景做选择。 - -分群代码是分群存储在系统后台的唯一标识,为方便数据分析人员直接查询数据库表,可命名为带有业务含义的参数名。 - -更新方式分为「手动更新」与「自动更新」。「手动更新」指在完成首次计算后,系统不会自动更新用户群,用户需要手动进行更新;「自动更新」会在每日 0 点后,以前一日作为基准进行用户群更新,可以设置更新延迟以确保所有前一日的数据都接收到,保证数据完整性。 - -![新建条件分群](/img/customEvent/cluster_create_condition_cluster2.png) - -在「分群规则」页,规则分为「属性」、「行为」两部分,两部分之间可以切换「且或」关系。 - -在「属性」规则部分,基于选择的分群主体,设置相应的属性条件,各个属性条件可以切换「且或」关系。 - -在「行为」规则部分,可分为「未做过事件」、「做过事件」两类,两类条件均可多次添加,行为条件可以切换「且或」关系。 - -「未做过事件」即,用户在选定的时间段内,未做过该行为。 - -「做过事件」即,用户在选定的事件段内,做过该行为,同时可以对行为发生的结果进行筛选。 - -### 3.2 ID 分群 - -点击分群列表右上角的新建分群,选择「新建 ID 分群」。 - -![新建 ID 分群](/img/customEvent/cluster_create_id_cluster.png) - -「分群名、分群主体、分群代码」与条件分群相同,不再赘述。 - -在新建 ID 分群页,上传 ID 文件,系统将把文件中的 ID 按照选择的「分群主体」与系统中已有的用户数据进行关联,找到符合条件的用户。 - -文件格式要求:ID 文件中每一行记录一个 ID 字段,使用 UTF-8 编码的 CSV 文本格式记录。若上传内容中存在未匹配项(即项目中不存在 ID 对应的用户),则该项会直接跳过,不会被纳入分群中。可下载模板作为参考。 - -### 3.3 结果分群 - -在各个分析模型中,如果指标是用户数(如事件分析中的「触发用户数」、留存或漏斗分析中某环节的留存或流失用户),则在结果报表中可点击「创建结果分群」来创建分群。 - -![新建结果分群](/img/customEvent/cluster_create_result_cluster1.png) - -创建分群时,可设置结果分群的名称以及备注,形成对结果分群的描述。 - -![新建结果分群](/img/customEvent/cluster_create_result_cluster2.png) - -结果分群不能修改创建规则以及更新,只能修改分群的名称以及备注。 - -## 4. 对用户分群的各类操作 - -创建的分群会以列表形式展示在用户分群页 - -![对用户分群的各类操作](/img/customEvent/cluster_operation.png) - -用户可以对分群进行查看、编辑、删除、更新、下载、复制操作,如下: - -| 操作 | 位置 | 效果 | -| -- | --- | ---------------- | -| 查看 | 分群名 | 查看分群详情 | -| 编辑 | 操作栏 | 进入编辑分群弹窗 | -| 删除 | 操作栏 | 删除分群 | -| 更新 | 操作栏 | 手动启动分群计算,并更新分群结果 | -| 下载 | 操作栏 | 下载当前分群结果的用户列表 | -| 复制 | 操作栏 | 新建一个与当前各参数相同的分群 | - -## 5. 使用用户分群 - -### 5.1 聚焦或排除部分用户 - -聚焦符合特定条件的高价值用户,如:付费金额大于 100 的用户,从而在分析模型中对强付费能力用户进行定向分析,了解其行为特征; - -排除符合特定条件的可疑用户,如:在同一设备上活跃过 3 个账号以上的设备,从而将可以的工作室设备进行排除,不再对正常用户的分析结果。 - -### 5.2 用户数据导入与输出 - -将外部用户数据导入系统创建分群,如:公司在其他游戏项目中已有一批高付费能力的用户、设备,因此可将其导入,探索其在本项目中的活跃、付费情况,更好的引导潜在高付费用户的付费。 - -下载分群结果作为其他系统的操作依据,如:导出疑似工作室设备、账号,从而在游戏运营系统中,对其进行处罚、封禁。 - -### 5.3 分析结果的下钻分析 - -结果分群基于分析模型的结果报表,因而非常适合作为下钻分析的基础。 - -例如,通过漏斗分析计算用户浏览商品、发起订单、支付订单的漏斗转化情况,发现大量用户在发起订单后便流失了。此时便可对该步骤的流失用户进行结果分群,通过各分析模型分析该分群用户的各属性、行为,探索其发起订单但却未成功支付的原因。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/data-model.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/data-model.mdx deleted file mode 100644 index 5c446b6c5..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/data-model.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: 埋点设计指南 -sidebar_position: 2 ---- - -## 1. 从案例开始 - -你是否有过这种困扰:好不容易让一些用户接触到了自己的游戏,但在他们登录账号前就因为一些技术问题流失了不少。下面是一个我们利用自定义事件分析完成「用户从打开 App 到真正创建角色」过程中流失的真实案例。 -我们当时按照以下思路设计了分析步骤: - -**1. 确定分析目标**:了解用户在创建角色之前的流失情况; - -**2. 明确具体流程**:对用户打开 App 到创建角色的流程进行拆解: - -- 点击游戏 icon -- Unity 初始化 -- 出现安卓存储权限允许界面 -- SDK 初始化 (新增设备记录点) -- 弹出隐私协议确认框【选「否」会退出即 SDK 初始化失败】 -- 检查版本 -- 确认下载按钮(4G 环境下)/ WIFI 环境下自动下载 -- 开始下载资源 -- 资源下载中 -- 资源下载完成,进入登录界面 -- 点击 TapTap 登录按钮 -- 跳出弹窗,可选 Tap 打开或者 Tap 加速器打开 -- 选择完成,跳转唤起登录授权 -- 点击同意,返回游戏 -- TapTap 登录完成 (转化设备记录点) -- 游戏服务器认证用户 -- 登录游戏服务器 -- 输入昵称创角 -- 创建成功,进入大厅 - -**3. 定义分析指标**:根据上述流程,我们以漏斗思维,设计的几个关键指标为: - -- 资源确认下载率(确认下载的用户 / 更新弹窗的曝光用户); -- 资源下载成功率(下载成功的用户 / 开始下载资源的用户); -- TapTap 登录率(登录成功的用户 / 点击登录的用户); -- 角色创建率(角色创建成功的用户 / 登录成功的用户); - -**4. 明确事件**:一般来说,事件 Event 会有三个类型: - -| 事件类型 | 描述 | -| ---- | ----------------- | -| 曝光 | XX 页面的曝光、XX 弹窗的曝光 | -| 点击 | XX 按钮的点击 | -| 系统事件 | 初始化、检查版本、资源下载等 | - -在上一步,我们确定了分析指标。接下来我们明确了统计哪些事件可以得到以上数据,初步确定了事件名称和事件类型: - -- 曝光:【更新提示弹窗】 -- 点击:【更新提示弹窗 - 确认下载按钮】 -- 系统事件:【更新下载成功】 -- 点击:【首页 - TapTap 登录按钮】 -- 系统事件:【登录成功】 -- 系统事件:【成功创建角色】 - -**5. 定义事件属性**: -基于事件名和事件类型,我们整理了更完整的埋点文档如下: - -| 事件名 | 事件显示名 | 属性名 | 属性显示名 | 属性值 | 触发时机 | -| ---------------- | ---------------- | ----------- | ----- | ---------------- | --------- | -| pv_download | 更新提示弹窗 | #ts | 时间戳 | | 弹窗展示时触发 | -| click_download | 更新提示弹窗 - 确认下载按钮 | #networtype | 网络类型 | WiFi、4g、5g、3g、2g | 点击按钮时触发 | -| download_success | 更新下载成功 | #ts | 时间戳 | | 系统后台触发 | -| login_start | 首页 - TapTap 登录按钮 | #ts | 时间戳 | | 点击按钮时触发 | -| login_success | 登录成功 | #ts | 时间戳 | | 系统后台触发 | -| create_role | 创建角色 | #ts | 时间戳 | | 创建角色成功时触发 | - -通过以上步骤,我们就完成了埋点的设计。 -随后我们将准确无误的埋点信息(事件名、事件显示名、属性名、属性显示名)录入 TapDB 的「**配置**」-「**事件管理**」。 -研发根据的埋点文档进行埋点开发,开发完成后进行数据校验。 -埋点上线后,我们使用「事件」和「看板」功能对「用户从打开 App 到真正创建角色」过程流失用户进行分析,如下图所示: - -![](/img/customEvent/49e3e7c0d12cd20cdd4f4aed2c8d0044.png) - -「更新下载成功(步骤 3)」到「首页 - TapTap 登录按钮(步骤 4)」的转化率非常低,接下来就需要进行详细的数据分析。点击 [TapDB 使用指南](/sdk/tapdb/features/custom-event/event-analyse) 查看详细数据分析方法。 - -## 2. 埋点设计思路 - -我们根据这个案例总结了以下思路,建议参考以下步骤进行埋点设计: -**1. 确定分析场景**; -**2. 明确具体流程**; -**3. 定义分析指标**; -**4. 明确事件:设计 Event,确定其类型,名称等细节**; -**5. 定义事件属性:明确所需要的信息,通过事件属性完成上报**; - -## 3. 其他设计技巧 - -### 3.1 Event 的同类抽象 - -在进行事件设计时,可能会遇到以下问题: - -1. 要统计三个关卡 A、B、C 的通关情况,对每个关卡设计一个通过事件吗? -2. 在设计「申请验证码」的功能埋点时,用户注册时、用户登录时、修改密码时等多场景都会下「申请验证码」,对每个场景设计一个「申请验证码」事件吗? -关于不同场景下的行为是否设置为同一个 Event,我们可以基于同类抽象判断原则: - -- 重要的事件行为或者特别关注的事件行为,可以单独将事件行为作为单独 Event 进行跟踪和属性设置; -- 重要程度一般的行为,比如只需要分析参与次数、参与人数的行为,可以将多个相似类型的行为设置成一个 Event,通过属性的方式标识具体的行为; - -1. 不同关卡通关情况的事件设计:如果简单统计三个关卡 A、B、C 的通关情况时,不需要做成 「A 关卡通关」、「B 关卡通关」、「C 关卡通关」 三个事件,只需设计成 「关卡通关」 事件,将 A、B、C 三个关卡名以属性 「关卡名称」 进行标识。 -2. 「申请验证码」的功能埋点:可将其定义为一个事件「申请验证码」,并在属性字段增加 「场景」,借此从场景来区分用户究竟在什么情况下申请验证码。 - -### 3.2 Event 的命名规范 - -在设计事件显示名时,注意确保事件没有二义性。按照 页面名-模块名-具体事件名的方式来命名,可以帮助分析师通过名字与备注准确理解事件所代表的用户行为。要避免因为说明的不准确而引发错误的分析结论。 -对 App 页面和模块命名进行统一的梳理和维护是有意义的准备工作,它帮助我们确保了所有人的理解是一致的。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/dimension-table.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/dimension-table.mdx deleted file mode 100644 index 46a5387fc..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/dimension-table.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: 维度表属性 -sidebar_position: 13 ---- - -## 1 概述 - -对于已经上报的事件属性和用户属性,可以通过上传维度表将原先上传的数据映射为另一种展示值或计算值,使初始埋点时的属性值不与展示值相同。 - -维度表属性相比于虚拟属性,可从系统外部引入原先上传的数据中未包含的信息,而非仅基于系统内数据的逻辑转化。 - -## 2 适用角色与用途 - -| 角色 | 用途 | -| :------------------------ | :------------------------------- | -| 埋点设计人员(数据产品经理 / 分析师) | 避免埋点设计中的过度冗余字段,提高数据模型范式度和埋点设计灵活度 | -| 数据分析人员(数据产品经理 / 分析师 / 运营) | 自助引入系统外部数据作为分析维度,满足更多个性化分析需求 | - -## 3 创建维度表属性 - -可在事件属性管理、用户属性管理中「维度表」列,创建该属性的维度表属性。 - -维度表属性可基于预置属性、自定义属性和虚拟属性创建,但不包括以下类型的虚拟属性: -1. 基于维度表属性创建的虚拟属性; -2. 基于用户属性创建的虚拟事件属性。 - -![创建维度表属性](/img/customEvent/dimension_table_1.png) - -### 3.1 上传入口 - -在需要添加维度表的基础属性字段上选择「上传」维度表属性。 - -![上传入口](/img/customEvent/dimension_table_2.png) - -### 3.2 文件编码要求 {#encoding} - -系统支持 `UTF-8` 或 `UTF-8 with Bom` 编码格式的 `CSV` 文件,文件大小不超过 2G。 - -可使用 Excel 在保存文档时选择「**CSV UTF-8(逗号分隔符)(.csv)**」格式; - -![Excel](/img/customEvent/dimension_table_7.png) - -或使用 WPS 在保存文档时选择「**CSV (逗号分隔符)(*.csv)**」格式。 - -![WPS](/img/customEvent/dimension_table_8.png) - -同样可使用 Sublime、NotePad ++ 等文本编辑工具,将文件以 `UTF-8` 或 `UTF-8 with Bom` 编码保存。 - -![Sublime、NotePad ++ 等](/img/customEvent/dimension_table_9.png) - -### 3.3 文件格式要求 - -首行内容将作为维度表属性的字段名称,需要以英文开头,英文、数字或 `_` 组成。 - -首列内容将和原始属性进行关联,取值需要和原始属性值对应,并保证值唯一,如遇到重复,则以首条为准,后续重复信息将被抛弃。 - -维度表属性不超过 10 列,内容会与录入的该属性的数据类型进行适配,规则如下: - -| 所选数据类型 | 文件内容 | -| :----- | :----------------------------------------------------------- | -| 文本 | 任何内容均可 | -| 数值 | 数字均可,0 开头的,把 0 抹掉 | -| 时间 | 时间戳,或 yyyy-MM-dd HH:mm:ss.SSS、yyyy-MM-dd HH:mm:ss、yyyy-MM-dd | -| 布尔 | true 或 false | - -对于所选数据类型与文件内容类型不一致的行,该行将被完全抛弃,请注意保持数据类型一致。 - -### 3.4 填写显示名和字段类型 - -根据需求填写映射后的维度表字段显示名和数据类型。 - -![填写显示名和字段类型](/img/customEvent/dimension_table_3.png) - -### 3.5 解析结果 - -展示解析总行数、成功行数、错误丢弃行数与错误丢弃原因。 - -![解析结果](/img/customEvent/dimension_table_4.png) - -### 3.6 替换 - -若解析结果不符合预期,可更新文件后进行替换。 - -![替换](/img/customEvent/dimension_table_5.png) - -## 4 维度表属性的使用 - -### 4.1 管理维度表属性 - -可在事件属性管理或用户属性管理页面中,对维度表属性进行管理。 - -维度表属性折叠在基础属性列中,展开后可进行进一步操作。 - -![管理维度表属性](/img/customEvent/dimension_table_6.png) - -### 4.2 模型中使用的注意事项 - -维度表属性在使用上和通常的属性一致,根据类型决定其计算逻辑以及筛选条件。 - -事件属性的维度表属性可以在其基础属性关联的事件中被使用。 - -用户属性的维度表属性,可用场景等同一般的用户属性。 - -## 5 最佳实践 - -### 5.1 提高埋点设计灵活度 - -采集用户的游戏商城浏览、下单信息时,可只采集商品 ID,商品名称、售价等信息可通过基于「商品 ID」创建维度表属性满足分析需求。 - -| 商品 ID(基础属性) | (维度表属性) | 售价(维度表属性) | -| :---------- | :------ | :-------- | -| 1 | 补签券 | 50 | -| 2 | 世界频道喇叭 | 10 | - -### 5.2 引入外部信息作为分析维度 - -系统中采集了用户设备的机型信息,结合通过爬虫收集各机型在电商平台的售价信息,从而得到各类机型的档次,用以对辨别用户价值。 - -| 机型(基础属性) | 售价(维度表属性) | 用户价值(维度表属性) | -| :------- | :-------- | :---------- | -| model_1 | 999 | 低 | -| model_2 | 1299 | 低 | -| model_3 | 1999 | 中 | -| model_4 | 4999 | 高 | diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/distribution-analyse.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/distribution-analyse.mdx deleted file mode 100644 index c66598e5e..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/distribution-analyse.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: 分布分析 -sidebar_position: 6 ---- - -## 一、引入案例 - -玩家付费是一个非常重要的游戏内行为,我们希望了解最近一周玩家在游戏内的付费总金额分布情况,例如:0-100 元,101-200 元,201-300 元 ..., 的用户数量分别是多少。这时候,我们可以用「分布分析」的功能来呈现。 - -**1、设置事件**:我们选择事件「用户付费」,设置自定义区间,以 100 作为分段; - -![](/img/customEvent/distribution/fenbu-1-1.png) - -**2、设置维度**:我们关注的每天的金额分布变化情况,所以在维度里选择「事件发生时间」; - -![](/img/customEvent/distribution/fenbu-1-2-2.png) - -**3、设置展示结果**:设置时间段和其他参数,并点击查询查看分析结果; - -![](/img/customEvent/distribution/fenbu-1-2-3.png) - -**4、保存报表**:将查询结果保存为报表。再将报表转变为看板,就可以每天方便快捷的看分布分析。 - -![](/img/customEvent/distribution/fenbu-1-4.png) - -## 二、什么是分布分析 - -分布分析功能,主要用来了解不同区间事件发生频次,不同事件计算变量加和,以及不同页面浏览时长等区间的用户数量分布。 - -## 三、分布分析的支持场景 - -分布分析可以帮助揭示以下问题:
    - -- 新版本上线后,用户每天玩游戏的次数是否增加? -- 不同广告渠道来源带来的用户,在不同金额区间,例如:0-100 元,101-200 元,201-300 元 ..., 的用户数量有什么差异; -- 不同层级的玩家(新玩家、普通玩家、重度玩家)玩游戏的时长分布有什么差异; - -## 四、如何做分布分析 - -如案例所述,分布分析可以分为 4 个步骤:**设置事件、设置维度、设置展示结果、保存报表**。 - -后面三个步骤在**事件分析**里均有比较详细的介绍,所以这里重点介绍设置事件。 - -在分布分析页面,点击「选择指标」,可以看到「指标选择」界面。指标的具体设置方式,你可点击事件分析查看。点击「设置」按钮,可设置计算结果的展示类型「离散」、「默认区间」和「自定义区间」:
    -**「离散」**:系统将展示每个数字下的分布值;
    -**「默认区间」**:系统会根据计算结果展示默认区间分布;
    -**「自定义区间」**:你可以自己设置每个区间段的起始值和结束值; - -![](/img/customEvent/distribution/fenbu-1-1.png) - -## 五、分布分析的计算原理 - -分布分析有 2 种统计方法,按次数统计和按事件属性的统计指标:
    -**按次数统计**:统计用户在一天 / 周 / 月中,进行某项操作的次数,发生一次就记录一次。
    -**按事件属性的统计指标统计**:统计用户在一天 / 周 / 月中,发生事件的某属性的统计指标值。属性的统计指标与事件分析一致,有总和、均值、最大值、最小值、去重数。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/event-analyse.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/event-analyse.mdx deleted file mode 100644 index e4fadf539..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/event-analyse.mdx +++ /dev/null @@ -1,266 +0,0 @@ ---- -title: 事件分析 -sidebar_position: 3 ---- - -## 一、引入案例 - -新增用户和 DAU 作为常用指标,经常需要分析和展示,我们可以用「事件分析」的功能来对场景指标进行分析和呈现。 - -**1、确定指标和筛选条件**:要分析的是账号下的 DAU 和新增用户,分 iOS 和 Android 两端显示。 - -点击「选择指标」,在弹出的窗口里我们选择事件和筛选条件,并且对指标重命名(我们也可以在事件分析的主界面进行筛选条件的设置)。 - -![](/img/customEvent/event/event-2.png) - -**2、确定维度**:iOS 和 Android 就是我们分析指标的维度,所以我们在维度选择里选择「设备系统类型」。 - -![](/img/customEvent/event/event-3.png) - -**3、选择时间段**:我们需要关注最近 30 天的数据变化,所以在日期选择器里选择「近 30 日」 - -![](/img/customEvent/event/event-4.png) - -**4、选择对比时间段**:我们用最近 30 天的数据变化,对比前一个月。 - -![](/img/customEvent/event/event-5.png) - -条件已经设置完成,点击「查询」,输出结果。 - -![](/img/customEvent/event/event-6.png) - -**5、保存报表**:我们将查询结果保存为报表。再将报表转变为看板,就可以每天方便快捷的看最终报表数据。 - -![](/img/customEvent/event/event-7.png) - -**以上就是我们通过事件分析来查看分系统的日活用户的步骤**。 - -## 二、什么是事件分析 - -事件分析本质上就是对事件的 what、how、when、where 进行分析。 - -## 三、事件分析的支持场景 - -1、人数:有多少人触发了某一事件 - -- 比如 UV、DAU 指标趋势; -- 比如说同一个按钮的文案做了调整,调整前文案为 A,调整后文案为 B,进行对照试验,分析 A 和 B 的点击情况; - -2、次数:某一事件触发了多少次,比如对关卡每天的通关次数; - -3、人均次数:某一事件(行为)平均触发多少次; - -4、活跃比:在一个时间区间内,触发某一事件的人数占当前时间段内所有活跃人数的比; - -5、事件细分维度下的数据:查看按钮点击事件,我按照设备类型展开,我就可以得到不同的设备触发的任意事件次数,这样我们就可以知道不同设备上的事件发生情况; - -6、复杂指标的四则运算:当我们想要的数据没有在上报的数据里,怎么办?我们可以通过加减乘除进行指标计算得到一个新的指标,这种方式就叫自定义指标,比如 ROI,ARPU 值; - -## 四、如何做事件分析 - -就像案例里提到的 5 个步骤: - -**1、选择指标和筛选条件;** - -**2、选择维度;** - -**3、选择时间段;** - -**4、选择对比时间段;** - -**5、保存看板;** - -### 4.1 选择指标 - -在确定指标时,同时需确定是否开启「近似计算」。 - -#### 4.1.1 近似计算 - -开启之后仅查询部分样本的数据,准确率 99.9%,查询速度更快。 - -![](/img/customEvent/event/event-2-1.png) - -任意设置的调整,需点击「查询」按钮才能生效。 - -#### 4.1.2 选择指标界面 - -在选择指标界面,包括「选择事件」、「选择属性」、「重命名」、「切换指标公式」和「筛选」功能,如下图: - -![](/img/customEvent/event/event-2-2.png) - -「**选择事件**」:**下拉选项里可选择所有预置事件和自定义事件**; - -「**选择属性或指标名**」:下拉选项里展示事件的分析角度以及属性,如下图: - -![](/img/customEvent/event/event-2-3.png) - -A:分析指标:任何事件都至少有这三个分析指标:总次数,触发用户数,人均次数; - -B:属性:事件属性; - -C:属性的分析指标:根据属性值类型,可存在以下分析指标: - -| 数值类型 | 分析角度 | -| ------------------------------------------------------------- | ------------------------- | -| 数值型 | 总和、中位数,均值、最大值、最小值、人均值、去重数 | -| 列表型 | 列表去重数、列表元素去重数 | -| 布尔型 | 去重数、为真数、为假数、为空数、不为空数 | -| 非数值、布尔型 | 去重数 | -| 时间(支持的格式为 `yyyy-MM-dd HH:mm:ss` 或者 `yyyy-MM-dd HH:mm:ss.SSS`) | 去重数 | - -**「重命名」**:将待分析的指标重命名,注意使用自己能理解同时别人也能理解的描述; - -**「切换指标公式」**:点击后切换为四则运算型指标。这个功能主要用于某些特殊的比例型分析场景,我们来列举两个例子: - -- 当天的活跃用户数占当月活跃用户数的比率,此场景下会将当前日期所在月份的活跃用户数作为分母参与计算; -- 当天的付费用户数占选定时间范围的活跃用户数的比率,此场景下会将所选时间范围的总活跃用户数作为分母参与计算; - -比如我们通过指标公式来构建付费率指标 - -- 我们自定义指标名为「付费率」; -- 我们设置事件和指标公式「用户付费 - 触发用户数」/「账号登录 - 触发用户数」; -- 在设置好指标计算公式后,可以选择「百分比」、「两位小数」、「整数」三种展现样式,我们选择两位小数; - -![](/img/customEvent/event/event-2-4.png) - -#### 4.1.3「筛选」 - -点击后设置指标的限制条件。可在【指标选择】界面设置单个指标的筛选,也可在事件分析主界面,设置全局筛选。 - -![](/img/customEvent/event/event-2-5.png) - -##### 4.1.4.1 筛选的数据类型 - -共有 5 种,分别对应支持不同的数学逻辑: - -1、数值。比如:充值金额。支持数学逻辑:等于、不等于、小于、大于、有值、无值、区间 - -2、字符串。比如:事件发生的城市。支持数学逻辑:等于、不等于、包含、不包含、有值、无值、正则匹配 - -3、列表 ID。 比如:名单。支持数学逻辑:存在元素、不存在元素、元素位置、有值、无值 - -4、时间。比如:注册时间(yyyy-MM-dd HH:mm:ss.SSS 或 yyyy-MM-dd HH:mm:ss)。支持数学逻辑:绝对时间、相对当前时间、相对事件发生时刻、有值、无值 - -5、布尔。比如:Wifi 使用。支持数学逻辑:为真、为假、有值、无值 - -##### 4.1.4.2 等于 / 不等于和包含 / 不包含的区别 - -等于 / 不等于:筛选项目中严格符合所选值才可被过滤,比如筛选条件:次大陆等于美洲,则仅查询美洲的数据。 - -包含 / 不包含:筛选项目中包含所选值即可被过滤,比如次大陆包含美洲,则美洲,北美洲,南美洲的数据都可以被过滤出。 - -绝对时间:指客观的现实时间。比如 2021-03-02 19:24:52 至 2021-03-08 19:24:52,前一时间必须在后一时间之前。点击「选择时间」可以精确到选择秒。 - -![](/img/customEvent/event_analyse_time_filter.png) - -相对当前时间:指相对现在来说,过去 n 天。 - -![](/img/customEvent/event_analyse_relative_time_1.png) - -相对事件时间:指相对所选的事件的前后 n 天。此处可选负数,比如 -1 天,则含义为所选事件发生之前的一天,如果所选为正数,比如 1 天,含义为所选事件发生之后的一天。 - -![](/img/customEvent/event_analyse_relative_time_2.png) - -##### 4.1.4.3 「且」「或」逻辑 - -可添加多个筛选条件。当筛选条件至少 2 个或以上时,会出现且 / 或切换按钮,默认是「且」。「且」即为多个筛选条件取交集,「或」为多个筛选条件取并集。 - -选择完筛选条件以后,点击查询按钮生效。 - -### 4.2 选择维度 - -#### 4.2.1 维度介绍 - -维度是对事实的分解,它将指标进行分割以观察规律: - -「事件发生的国家 / 地区」是一个维度,它的作用是将事实按照其所发生的国家 / 地区进行分组,从而观察指标的规律; - -「用户群的年龄」是一个维度,查看不同年龄段的用户的付费数据; - -「渠道包」是一个维度,查看不同渠道包下用户的留存数据; - -时间是最基础的维度。 - -#### 4.2.2 维度的分类 - -维度下拉框里可选择事件属性、用户属性、用户分群: - -- 事件属性:描述事件发生时候的状态,比如:用户在美国充了多少钱?这里查询在美国(充钱的用户现在可能不在美国)发生的付费事件。 -- 用户属性:描述触发事件的用户的状态。比如:现在在美国的用户充了多少钱?这里查询现在在美国的用户(包含曾经不在美国)触发的付费事件。 -- 用户分群:即用户属于所选中的分群里的用户。 - -![](/img/customEvent/event/event-2-6.png) - -其次阐述二者在数据逻辑上的区别: - -- 事件属性:即每一条事件所带的参数。比如通过 SDK 传「中秋节」事件,该事件带有 3 个属性,「中秋道具个数」、「粽子种类」和「用户购买龙舟数量」。在选择事件属性的维度时,可选的维度受到所选事件的约束,比如事件选定为「中秋节」的情况下,事件属性只能选择这 3 个。 -- 用户属性:即用户表里的字段。用户属性不受所选事件的约束。 - -当所选维度为数值类型的字段时,比如付费金额,会出现区间的选择,离散区间即按照该字段所具有的所有数值进行聚合查询,默认区间为系统按照某规则设定聚合区间,自定义区间为客户可以自己定义区间段来聚合数据。 - -![](/img/customEvent/event_analyse_custom_range.png) - -当所选维度有「事件发生时间」时,可选择时间的颗粒度,支持按天,小时,分钟,周,月。 - -![](/img/customEvent/event_analyse_time_granularity.png) - -### 4.3 选择日期 - -选择我们关注的日期。 - -![](/img/customEvent/event/event-2-7.png) - -### 4.4 选择对比日期 - -对比日期与所选日期区间范围要保持一致,比如下图例子中,所选日期为 2021-3-2 到 2021-3-8,间隔为 6 天,则对比日期间隔也为 6 天。在选择对比日期时,只需要选择一个日期(比如 2021-02-23),系统会自动往后推算 6 天(至 2021-03-01)。 - -![](/img/customEvent/event_analyse_time_compare.png) - -### 4.5 保存报表 - -选择完主维度和指标后,点击保存报表将该报表保存下来,输入名字后点击确认,则该报表被保存至「已保存报表」中。 - -![](/img/customEvent/event_analyse_save_report_1.png) - -首次保存某报表后,再次打开该报表即会出现「另存为」按钮,点击后另存为一张新的报表,若点击「保存报表」,则是更新之前保存的报表。 - -![](/img/customEvent/event_analyse_save_report_2.png) - -被保存 / 另存为的报表展现在屏幕右侧「我的报表」,可以点击自己保存的报表直接查询。 - -![](/img/customEvent/event_analyse_save_report_3.png) - -## 五、透视表 - -透视表是一种可交互的数据报表形态,其特点为所进行的计算与数据跟数据透视表中的排列有关。之所以称为数据透视表,是因为可动态改变数据的聚合维度的先后顺序,从而使得客户可以用多重角度观察数据。下面我们对比以下两张透视表: - -![](/img/customEvent/event_analyse_pivot_table_1.png) - -![](/img/customEvent/event_analyse_pivot_table_2.png) - -上下两张透视表(分别简称上表和下表,以下同)。 - -上表中,想要查询中国各省份当中,各设备系统及使用各网络运营商的用户的充值金额总和,即以「省份」视角透视拆分「设备系统」数据,进而又透视拆分「网络运营商」数据。 - -下表中,想要查询中国各网络运营商当中,各设备系统及各省份的用户的充值金额总和,即以「网络运营商」视角透视拆分「设备系统」数据,进而又透视拆分「省份」数据。 - -### 5.1 可拖拽 - - 透视表的每一列均可以用鼠标拖拽从而改变顺序。其中,维度被拖拽改变前后顺序以后,数据重新进入查询,即如同上表下表改变了数据透视拆分的角度。灵活运用拖拽可提高数据查询的效率。目前,透视表可最多勾选 5 个维度和 20 个指标。 - -### 5.2 搜索 - -透视表的维度表头处,可以点击搜索输入文字,快速筛选出符合条件的项目,这里为结果筛选,筛选后不会重新进入查询。 - -![](/img/customEvent/event_analyse_table_filter.png) - -### 5.3 排序 - -透视表的默认排序逻辑: -首先,对第一列主维度对应的所要排序指标的总计数据做从大到小排序; -进而,对第二列主维度对应的所要排序指标的总计数据做从大到小排序; -以此类推,对第 N 列主维度对应的所要排序指标的总计数据做从大到小排序; - -### 5.4 下载数据 - -透视表右侧点击下载按钮,可下载平铺数据,下载结果为当前查询结果,可以实现所见即所得。提供 CSV / PDF / 图片三种格式。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/funnel-analyse.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/funnel-analyse.mdx deleted file mode 100644 index f849d1430..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/funnel-analyse.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: 漏斗分析 -sidebar_position: 5 ---- - -## 一、引入案例 - -在[埋点设计指南](/sdk/tapdb/features/custom-event/data-model)里,我们基于「用户从打开 App 到真正创建角色」的转化过程进行了埋点设计,在这里我们用「漏斗分析」功能对这个转化过程来进行分析。 - -**1、设置步骤**:将用户从打开 App 到真正创建角色的全过程创建事件,并按照用户操作顺序设置为漏斗步骤; -![](/img/customEvent/funnel/案例-1.png) - -**2、设置维度**:不同系统类型可能会对转化情况有影响,所以在维度里选择「设备系统类型」; - -![](/img/customEvent/funnel/案例-2.png) - -**3、设置漏斗周期**:我们将漏斗窗口期设置为 7 天; - -![](/img/customEvent/funnel/案例-3.png) - -**4、设置展示结果**:设置时间段和其他参数,并点击查询查看分析结果; - -![](/img/customEvent/funnel/案例-4.6.png) - -**5、保存报表**:将查询结果保存为报表。再将报表转变为看板,就可以每天方便快捷的看漏斗分析。 - -![](/img/customEvent/funnel/案例-5.2.png) - -## 二、什么是漏斗分析 - -漏斗分析是分析从起点到终点各阶段用户的转化率情况的分析模型,可以衡量每个节点的转化效果。在理想情况下,用户会沿着产品设计的路径到达最终目标事件,但实际情况是用户的行为路径是多种多样。通过埋点事件配置关键业务路径,可以分析多种业务场景下转化和流失的情况,不仅找出产品潜在问题的位置,还可以定位每个环节流失用户,通过产品手段或营销手段促进转化。 - -## 三、漏斗分析的支持场景 - -漏斗分析支持的场景很多,比较典型的场景如下: - -- 游戏流量很大,但注册用户很少,是过程中哪个环节除了问题? -- 用户从「注册 – 创建角色 - 体验游戏 - 付费」 总体转化率如何? -- 不同地区的用户支付转化率有什么差异? -- 两个推广渠道带来不同的用户,哪个渠道的注册转化率高? - -## 四、如何做漏斗分析 - -如案例所述,漏斗分析可以分为 5 个步骤: - -**1、设置步骤;** - -**2、设置维度;** - -**3、设置漏斗周期;** - -**4、设置展示结果;** - -**5、保存报表;** - -### 4.1 设置漏斗 - -在漏斗分析页面,点击「设置步骤」,可以看到「添加漏斗步骤」界面。 - -![](/img/customEvent/funnel/exp/1-漏斗分析-设置漏斗.png) - -在「添加漏斗步骤」界面,可以选择漏斗步骤和添加漏斗步骤: - -1、步骤:由一个事件(可添加一个或者多个筛选条件)组成,表示一个转化流程中的一个关键性的步骤。 - -一个漏斗中至少包含 2 个步骤,每个步骤对应一个事件。可增加更多步骤,拖动步骤前的序号可以改变步骤顺序。 - -同样可对步骤设置筛选条件。也可在漏斗分析主界面设置全局筛选。 - -2、添加步骤:给要分析的漏斗增加更多步骤。 - -![](/img/customEvent/funnel/exp/2-漏斗分析-设置漏斗.png) - -### 4.2 设置维度 - -在维度下拉框,展示的内容包括事件属性(步骤 1)、用户属性和用户分群。 - -![](/img/customEvent/funnel/exp/3-漏斗分析-选择维度.png) - -### 4.3 设置漏斗周期 - -**漏斗周期**:用户触发步骤 1 起,在窗口期内完成后续步骤,算作后续步骤的转化。 - -![](/img/customEvent/funnel/exp/4-漏斗分析-漏斗窗口期.png) - -**限制窗口期在时间区间内**:表示只有在这个时间范围内,用户从第一个步骤,行进到最后一个步骤,才被视为一次完整的漏斗转化。 - -### 4.4 设置展示结果 - -可选择进行分析的步骤范围,并根据「对比 / 趋势」、「转化 / 流失」设置,能够得到 4 类报表:转化对比表、流失对比表、转化趋势表、流失趋势表。 - -![展示结果](/img/customEvent/funnel_analyse_result_type.png) - -转化对比表:用以分析从步骤一到后续步骤的累计的转化率 - -![转化对比表](/img/customEvent/funnel_analyse_table_1.png) - -流失对比表:用以分析每个步骤之间的流失率 - -![流失对比表](/img/customEvent/funnel_analyse_table_2.png) - -转化趋势表:用以分析不同日期的转化率变化趋势 - -![转化趋势表](/img/customEvent/funnel_analyse_table_3.png) - -流失趋势表:用以分析不同日期的流失率变化趋势 - -![流失趋势表](/img/customEvent/funnel_analyse_table_4.png) - -### 4.5 保存到看板 - -将设置好的查询结果保存为报表,再基于报表创建看板,漏斗分析结果一触即达。 - -![](/img/customEvent/funnel/exp/5-漏斗分析-保存报表.png) - -## 五. 漏斗分析原理 - -接下来将会描述漏斗分析原理,尤其是有分组和筛选情况时,计算原理就会显得较为复杂,此处将详细说明。 - -### 5.1. 基本计算原理 - -假设一个由步骤 1、2、3、4、5 构成的漏斗,选择的时间范围为 2021 年 3 月 1 日到 2021 年 3 月 7 日,窗口期是 3 天,如果用户在 2021 年 3 月 1 日到 2021 年 3 月 7 日触发了步骤 1,并且在步骤 1 发生的 3 天内,依顺序依次触发了步骤 2、3、4、5,则认为该用户完成了一次完整的漏斗转化,若依次触发了步骤 1 > 2 > 4 > 5,则该用户仅完成了步骤 1 > 2 的转化。 - -如果步骤中间夹杂了一些其他的步骤,如用户的行为顺序是 1 > X > 2 > X > 3 > 4 > X > 5(其中 X 代表其他事件),则依然认为该用户完成了一次完整的漏斗转化。 - -当一个用户在所选时段内有多个事件都符合某个转化步骤的定义,则会优先选择更靠近最终转化目标的事件作为转化事件,并在第一次达到最终转化目标时停止转化计算。 - -假设一个漏斗的步骤定义为:浏览商城、选择道具、生成订单、支付成功,那么不同用户的行为序列及实际转化步骤(加粗部分)如下: - -例 1:**浏览商城** > 选择道具(道具 B ) > **选择道具(道具 A )** > **生成订单** > 支付成功 - -例 2:浏览商城 > 选择道具(道具 B ) > **浏览商城** > **选择道具(道具 A )** > **生成订单** > **支付成功** - -例 3:浏览商城 > 选择道具(道具 B ) > **浏览商城** > **选择道具(道具 A )** > **生成订单** > **支付成功** > 选择道具(道具 A ) > 生成订单 > 支付成功 - -漏斗分析中展示的数字代表转化 / 流失的独立用户数,而非触发的事件次数。在该时间范围内,即使一个用户多次完成漏斗,也仅计数一次。 - -### 5.2 分组与筛选 - -漏斗分析的分组与筛选,均基于完成转化 / 流失的用户。 -基于用户属性、用户分群的分组与筛选:在完成转化 / 流失的用户的基础上,根据用户属性、用户分群进行分组与筛选。 -基于事件属性的分组与筛选:在完成转化 / 流失的用户的基础上,以该用户在步骤 1 的事件属性进行分组与筛选。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/kanban.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/kanban.mdx deleted file mode 100644 index 9c16754b5..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/kanban.mdx +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: 看板 -sidebar_position: 8 ---- - -## 1. 概述 - -看板是多张报表的集合,将构建的指标、留存、漏斗等保存为报表后,可将报表添加至看板中,方便日常数据的监控。 - -![概述](/img/customEvent/kanban_summary.png) - -## 2. 使用看板进行数据分析与协作 - -在该部分将以 demo 项目为例,演示从新建一个看板,到在团队中通过看板进行数据协作的全流程。 - -### 2.1 进入看板页 - -![进入看板](/img/customEvent/kanban_layout.png) - -数据看板由 「看板目录、看板设置、报表展示」 三部分构成: - -- 1 看板目录:创建看板 / 文件夹,查看自建看板或团队成员的共享看板; - -- 2 看板设置:包括添加看板报表、设置看板共享、调整看板设置、看板刷新、为看板报表设置全局筛选等; - -- 3 报表展示:展示每张看板报表的信息,支持看板报表拖拽排序,自定义大小及图表展示类型。 - -### 2.2 新建看板 - -现在,我们需要将一些核心指标形成日报进行每日汇报,因而决定将日常核心指标的相关报表汇总在一个看板中。 - -![新建看板](/img/customEvent/kanban_create_1.png) - -![新建看板](/img/customEvent/kanban_create_2.png) - -我们在看板左侧栏点击右上角「+」,选择「新建看板」,并命名为「日报看板」。 - -### 2.3 编辑、重命名、删除看板 - -![编辑、重命名、删除看板](/img/customEvent/kanban_operation.png) - -对于已经存在的看板,我们可以编辑该看板的相关信息,或删除该看板。 - -### 2.4 使用文件夹对看板进行管理 - -文件夹用以收纳看板,我们可新建文件夹,并进行重命名、删除,系统内置「未分组」文件夹和「共享给我的」文件夹。 - -![文件夹](/img/customEvent/kanban_create_folder.png) - -在看板左侧栏点击右上角「+」,选择「新建文件夹」,并命名为「游戏运营」,用以收纳游戏运营的相关看板。 - -![移动](/img/customEvent/kanban_move.png) - -此时,我们可以将之前新建的看板「日报看板」移动到文件夹「游戏运营」中。 - -### 2.5 将报表添加到看板 - -报表是组成看板的基本元素,我们可在事件分析、留存分析、漏斗分析等分析模型功能中新建报表。 - -为了满足日报汇报需求,我们在事件分析中构建出登录账号数、App 启动设备数等报表,在留存分析中构建出用户 App 启动 7 日留存等报表,现在我们将这些报表添加到看板中。 - -![添加](/img/customEvent/kanban_add_report_1.png) - -点击看板右上方的「报表」按钮,点击「+」添加报表。 - -![添加](/img/customEvent/kanban_add_report_2.png) - -鼠标移入右侧的待添加报表中的报表行,点击「+」可将报表添加至当前看板。 - -### 2.6 设置报表 - -![设置](/img/customEvent/kanban_setting_1.png) - -![设置](/img/customEvent/kanban_setting_2.png) - -对于添加到看板中的报表,可以调整报表的大小尺寸、可视化方式、时间筛选、展示指标、展示分组等信息。 - -展示分组可按分组对应的指标值或分组名进行排序,并设置为选中「前 N 项目」,看板后续将根据查询时的数据进行排序并动态变更选中的分组。 - -![看板](/img/customEvent/kanban_function.png) - -将活跃账号数、活跃设备数等报表窗口尺寸设置为小,便于我们快速浏览当日的流量情况。 - -将「各国家活跃设备数」窗口尺寸设置为中,图表类型选择趋势图,便于观察各个国家活跃设备数在近期的变化趋势。 - -将「各系统活跃用户分布」窗口尺寸设置为中,图表类型选择分布图,便于观察各个系统的用户分布。 - -将「App 启动 30 日留存」窗口尺寸设置为大,便于同时看到更多期群的留存数据。 - -### 2.7 设置看板 - -将报表添加到看板之后,我们可以对看板的更新和是否近似计算进行设置。 - -![设置看板](/img/customEvent/kanban_refresh.png) - -看板默认为定时更新,系统将每天定时为看板刷新计算结果,缓存该结果以便下次查看,对于看板中加载报表较多或计算量较大的场景建议打开定时更新,以提高展示效率。 - -看板可被设置为实时更新,即每次刷新计算时都对看板进行更新,适合需要实时刷新的指标,如当日的广告投放数据。 - -看板默认为精确计算,即每次计算都将按照条件算出准确值。如果对查询效率有需求,可以选择「近似计算」选项。开启后,对触发用户数、人均次数、人均值、去重数等数据,都将采取近似算法,可以极大减少性能开销并减少计算时间。 - -对于本次创建的「日常看板」,我们更关注近期的主要指标变化趋势,因此我们选择每日凌晨 1 点定时更新并开启近似计算。 - -### 2.8 共享看板 - -在工作中我们希望团队其他成员也可以查看新建的看板,甚至共同维护更新该看板,因此我们可以将看板权限共享给团队其他成员。 - -共享权限分为共享、可见两类,两类权限互相独立,可分别授予,用户可将共享、可见权限授予「全部用户」,任何成员加入该项目后即拥有该权限,不必频繁更新。 - -![共享看板](/img/customEvent/kanban_share.png) - -作为整个团队都关注的「日常看板」且并不包含敏感的营收等敏感收入,因此我们将可见权限授予「全部用户」,同时我们希望另一位运营同事与我们一同维护、更新该看板,因此我们将协作权限共享给该同事。 - -### 2.9 看板订阅 - -在某些需要定期查看数据的场景下,频繁打开看板会带来使用流程上的不便,为此我们提供「看板订阅」功能。 - -![](https://capacity-files.lcfile.com/o70Y2aNmVeloTAPRh6KsVhDOeNJ5lSVu/tap_db_1.png) - -作为看板的「所有者」/「协作者」,你可以通过入口访问订阅设置界面,并基于本身业务设定订阅规则。 - -![](https://capacity-files.lcfile.com/VBWQ9kY882IkRrxqr2RDIHUEwXNXPMQC/tap_db_2.png) - -- 订阅标题:即此条订阅的标题内容; - -- 订阅说明:即此条订阅的详细说明内容; - -- 订阅推送:即此条订阅的推送时间,支持按「天」「周」「月」进行定期推送,时区固定为 UTC+8 ; - -- 订阅方式:即订阅推送的渠道,目前支持添加邮箱,[企业微信群](https://open.work.weixin.qq.com/help2/pc/14931?person_id=1&is_tencent),[Slack](https://api.slack.com/messaging/webhooks) 渠道; - -- 订阅状态:即订阅任务的当前状态,若需要暂停/开启订阅,可以手动更改状态; - -- 订阅操作:除了基础「保存」操作外,可以基于当前内容「立即推送」一条订阅,用于测试验证订阅任务。 - -附:订阅推送效果示例图 - -![](https://capacity-files.lcfile.com/R4MzASkfE5GotyeUte1ct7MY6mEJ5BNR/image-1.png) - -![](https://capacity-files.lcfile.com/q6NzsismFRQ9SfD1NM54SbAKTjbhF0sd/image-2.png) - -![](https://capacity-files.lcfile.com/xrqRfvksMadial52GrcjvumE3XAbwE4r/image-3.png) - - - - - diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/meta-data.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/meta-data.mdx deleted file mode 100644 index 0b45aeefe..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/meta-data.mdx +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: 元数据管理 -sidebar_position: 10 ---- - -## 1. 概述 - -元数据管理是系统中的数据管理模块,是用户用以统一管理元数据的地方。 - -TapDB 采用元数据预登记模式,在接收 SDK 数据前,必须将需要收集的事件、属性登记到元数据管理相应的功能模块中,系统在接收数据时,需按照预登记的元数据进行校对,对于符合条件的数据进行入库,不符合要求的数据将被拒绝。 - -该模式可以有效地提高数据的准确性,从根本上解决数据报告和存储不正确的问题。 - -元数据管理由事件管理、事件属性管理和用户属性管理 3 部分构成。 - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ----------- | -------------- | -| 管理员 | 录入埋点方案,管理系统元数据 | -| 业务人员 | 查看元数据,了解数据业务含义 | -| 客户端 / 前端工程师 | 查看埋点需求 | - -## 3. 事件管理 {#event-manage} - -「事件」是系统中各类数据分析模型的基本分析对象,系统内置「预置事件」,并提供上报「自定义事件」的功能。 - -在「事件管理」功能中,可在对「自定义事件」在上报前进行预登记,并从多方面对「事件」进行管理。 - -前端工程师通过查看「曾否上报」为「否」的预登记自定义事件,进行埋点开发。 - -![事件管理](/img/customEvent/metadata_event_overview.png) - -### 3.1 概念解释 - -「事件」相关的「概念」及其「解释」如下: - -| 概念 | 解释 | -| ---- |-----------------------------------------------------------------------------------------------------------------------------------------| -| 事件名 | 事件的唯一标识 | -| 显示名 | 事件在分析模型中的显示名称 | -| 说明 | 描述埋点的各方面信息,如:描述触发时机,帮助技术人员更准确埋点;描述业务内涵,帮助业务人员更深入理解 | -| 事件类型 | 目前分为「预置」、「自定义」两类:
    **预置**:系统内置的事件,广泛适用于各类游戏项目,仅需在 SDK 中打开埋点开关即可上报
    **自定义**:自定义创建的事件,满足游戏项目的个性化需求,上报前需在事件管理中录入 | -| 曾否上报 | 事件是否有过上报记录 | -| 接收开关 | 系统接收事件上报与否的开关 | -| 状态 | 目前分为「正常」、「隐藏」、「删除中」、「已删除」四种状态:
    **正常**:处于正常状态的事件
    **隐藏**:不在分析模型中展示
    **删除中**:已上报数据的事件在被删除后将进入「删除中」状态,72 小时内可撤回,在此期间依然接收数据,但不展示在分析模型中
    **已删除**:已上报数据的事件在被最终删除后,将以「已删除」状态记录在元数据管理中,不再接收数据与展示在分析模型中,且不占用自定义事件数据使用进度 | -| 事件属性 | 事件所需采集的属性信息,在新建、编辑事件中可选择绑定相应的事件属性。目前事件属性分为「预置」、「自定义」两类:
    **预置**:新建的事件默认处于「调试」状态,不在分析模型中展示
    **自定义**:自定义创建的事件属性,满足游戏项目的个性化需求。 | -| 数据限制 | 为保证系统性能,处于「正常」、「隐藏」、「删除中」状态的自定义事件数不可超过 100。「已删除」的自定义事件不占用使用进度,可通过删除自定义事件释放使用进度。 | - -### 3.2 创建自定义事件 - -![创建自定义事件](/img/customEvent/metadata_event_create2.png) - -填写事件的基本信息,此时系统默认关联所有的预置属性,且默认不关联所有的自定义属性。 - -选择需要关联的自定义属性,若不满足需求可新建自定义事件属性,以及解绑不需要关联的预置属性,即完成自定义事件的创建。 - -### 3.3 查看、编辑、删除事件 - -![查看、编辑、删除事件](/img/customEvent/metadata_event_edit.png) - -点击事件名可查看到此事件的基本信息,以及所有关联属性。 - -在操作栏,可编辑事件的基本信息以及变更与事件属性的关联关系,对于已发生上报的事件,「事件名」不可再被编辑。 - -在操作栏,可对已有的自定义事件进行删除操作。未上报数据的事件将被彻底删除,不留下任何记录;已发生上报的事件,将进入「删除中」状态,72 小时内可进行撤销操作,否则将进入「已删除」状态,不再接收数据与展示在分析模型中,同时释放自定义事件数据使用进度限制。 - -### 3.4 Q&A - -Q1:为什么有的事件被删除后不保留任何记录,而有的事件被删除后以「已删除」状态留在元数据管理中? - -A1:未发生上报的事件不保留记录,已发生上报的事件则会保留。在埋点的设计过程中,业务人员会因为需求变更、需求合并处理等问题对埋点设计进行多次变动,因而在没有进行上报行为之前,可以认为所有的录入行为都是在「打草稿」,我们希望用户在此过程中可以无所顾忌,不用担心把事件管理列表弄得一团糟;而一旦数据发生上报,相当于定稿,我们希望系统可以忠实的记录项目曾经采用的所有数据采集方案。 - -## 4. 事件属性管理 {#event-props} - -「事件属性」是用来描述「事件」的属性信息,TapDB 内置「预置事件属性」,并提供上报「自定义事件属性」功能。 - -在「事件属性管理」功能中,可在对「自定义事件属性」进行预登记,并从多方面对「事件属性」进行管理。 - -前端工程师通过查看「曾否上报」为「否」的预登记自定义事件属性,进行埋点开发。 - -![事件属性管理](/img/customEvent/metadata_event_prop_overview.png) - -### 4.1 概念解释 - -「事件属性」相关的「概念」及其「解释」如下: - -| 概念 | 解释 | -| ---- | ---------------------------------------------------------------------------------------------------------- | -| 属性名 | 事件属性的唯一标识 | -| 显示名 | 事件属性在分析模型中的显示名称 | -| 说明 | 描述属性的各方面信息,如:描述业务内涵,帮助业务人员更深入理解 | -| 数据类型 | 属性的数据类型,类型相符的数据方可入库 | -| 单位 | 统计值的单位,在分析模型、报表中作相应展示 | -| 属性类型 | 目前分为「预置」、「自定义」两类:
    **预置**:系统内置的事件属性,广泛适用于游戏项目中的各个事件,仅需在 SDK 中打开埋点开关即可上报:
    **自定义**:自定义创建的事件属性,满足游戏项目的个性化需求,上报前需在事件属性管理中录入 | -| 曾否上报 | 事件属性是否有过上报记录 | -| 接收开关 | 系统接收事件属性上报与否的开关 | -| 状态 | 目前分为「正常」、「隐藏」两种状态:
    **正常**:处于正常状态的事件
    **隐藏**:不在各个分析模型中展示 | -| 数据限制 | 为保证系统性能,自定义事件属性数不可超过 300 | - -### 4.2 创建 - -![事件属性创建](/img/customEvent/metadata_event_prop_create.png) - -填写事件属性的基本信息,即完成自定义属性的创建。可在「事件管理」中将其与事件进行关联。 - -### 4.3. 查看与编辑 - -![事件属性查看与编辑](/img/customEvent/metadata_event_prop_edit.png) - -点击属性名可查看到此属性的基本信息,以及所有关联事件。 - -在操作栏,可编辑除「属性名」、「数据类型」之外的其他事件属性基本信息。 - -### 4.4 Q&A - -Q1:为什么不能删除未进行任何数据上报行为的自定义事件属性,而自定义事件却可以被删除? - -A1:增加一个新事件在数据表中仅为一条数据记录,而增加一个新属性则需要在数据表中增加新的一列,改变原有的数据结构,两者在实现上难度相差很大。因而在初版中暂不支持删除自定义属性功能,后续会迭代删除自定义属性功能,敬请期待。 - -## 5. 用户属性管理 {#user-props} - -「用户属性」是用来描述于「用户」的属性信息,TapDB 内置「预置用户属性」,并提供上报「自定义用户属性」功能。 - -TapDB 分别以「账号」、「设备」作为用户标识对不同主体进行分析,在用户属性管理中,「账号」、「设备」也将分别作为用户标识的组成部分 - -在「用户属性管理」功能中,可在对「自定义用户属性」进行预登记,并从多方面对「用户属性」进行管理。 - -前端工程师通过查看「曾否上报」为「否」的预登记自定义用户属性,进行埋点开发。 - -![用户属性管理](/img/customEvent/metadata_user_prop_overview.png) - -### 5.1 概念解释 - -「用户属性」相关的「概念」及其「解释」如下: - -| 概念 | 解释 | -| ---- | ---------------------------------------------------------------------------------------------------------- | -| 用户标识 | 用户属性的标识之一,分为「账号」、「设备」两类 | -| 属性名 | 用户属性的标识之一,与「用户标识」共同组成唯一标识 | -| 显示名 | 用户属性在分析模型中的显示名称 | -| 说明 | 描述属性的各方面信息,如:描述触发时机,帮助技术人员更准确埋点;描述业务内涵,帮助业务人员更深入理解 | -| 数据类型 | 属性的数据类型,类型相符的数据方可入库 | -| 单位 | 统计值的单位,在分析模型、报表中作相应展示 | -| 属性类型 | 目前分为「预置」、「自定义」两类:
    **预置**:系统内置的用户属性,广泛适用于游戏项目中的各个事件,仅需在 SDK 中打开埋点开关即可上报
    **自定义**:自定义创建的用户属性,满足游戏项目的个性化需求,上报前需在用户属性管理中录入 | -| 曾否上报 | 用户属性是否有过上报记录 | -| 接收开关 | 系统接收用户属性上报与否的开关 | -| 状态 | 目前分为「正常」、「隐藏」两种状态:
    **正常**:处于正常状态的事件
    **隐藏**:不在分析模型中展示 | -| 数据限制 | 为保证系统性能,自定义用户属性数不可超过 100 | - -### 5.2 创建 - -![创建](/img/customEvent/metadata_user_prop_create.png) - -填写用户属性的基本信息,即完成自定义属性的创建。目前,自定义用户属性一旦被创建便不可被删除。 - -### 5.3 查看、编辑与复制 - -![查看、编辑与复制](/img/customEvent/metadata_user_prop_edit.png) - -点击属性名可查看到此属性的基本信息。 - -在操作栏,可编辑除「属性名」、「数据类型」之外的其他事件属性基本信息。 - -在操作栏,可通过复制功能新建一个各项信息相同但切换了用户标识的自定义用户属性,方便快速对设备、账号两个用户识别体快速同步创建自定义用户属性。 - -### 5.4 Q&A - -Q1:为什么需要区分「设备」、「账号」两个主体? - -A1:在不同场景中,以不同的主体来标识用户,从而更能切合业务需求。如广告投放相关业务中,设备是更好的用户标识,而分析用户具体游玩行为时,账号可能是更好的用户标识。另外,可善用复制操作,对两类标识的用户属性进行同步创建。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/overview.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/overview.mdx deleted file mode 100644 index 317f83f0b..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/overview.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: 自定义事件分析 -sidebar_position: 1 ---- - -## 为什么需要自定义事件分析 - -我们为 TapDB SDK 预设了一些上报事件,从而构造出了非常强大且完整的运营模块。但随着大家对数据分析的深入,我们发现完全模板化、预置化的分析模式会有明显的局限。为此我们设计了自定义事件分析,让大家能更自由地上报与查询所需要的数据。它有一定的接入和理解门槛,但通过自定义事件分析深挖玩家行为的上限是明显高于预置的报表模板的,因此我们强烈建议你认真学习并用好它。 - -## 事件分析 - -事件分析是最基础的分析模型,它的分析对象是事件 - -- 查看事件相关的所有信息 - - 「进行 PVP 对战」事件的触发总次数 / 触发用户数 / 人均次数 - - 「购买礼包」事件的购买金额总数 / 人均次数 - - 最近 7 天,「抽卡」事件在不同省份发生的人均次数对比 - - 每天的「账号登录」事件中,有多少比例是 TapTap 登录,有多少比例是微信登录 - -## 留存分析 - -留存分析是对用户广义留存行为进行分析的模型,它的分析对象是设备 / 账号 - -- 查看那些触发了 A 事件又触发了 B 事件用户的情况,初始事件和回访事件可以是同一个 - - 「付费」的账号,在之后 7 日内「登录」的情况 - - 「升到 10 级」的账号,在之后 7 日内「付费」的情况 - - 「购买月卡」的账号,在之后 30 日内「领取奖励」的情况 - - 首次触发「登录」的账号,在之后 90 日内「登录」的情况:这是我们通常说「账号留存」 - -## 漏斗分析 - -漏斗分析是对按顺序触发特定事件的用户进行分析的模型,它的分析对象是设备 / 账号 - -- 查看那些严格按照漏斗步骤转化的用户的信息,可以设置漏斗窗口期 - - 「完成签到」的账号数,以及在「完成签到」之后,又在 30 分钟内「参与 PVP」的账号数 - - 「购买月卡」的账号数,以及在「购买月卡」之后,又在 7 天内「购买新手礼包」的账号数 - -## 用户分群 - -用户分群是自定义事件分析中极其强大的一个功能。它的使用思路为 - -- 通过观察数据,找到一批待分析用户,如:留存率极低 / ARPPU 很高 / 持续签到但等级很低的用户,将其保存为一个用户分群 -- 使用这个用户分群作为条件,单独观察 / 筛除其行为数据,从而尝试找到一些行为特征,如:留存率极低的用户 Android 版本都低于 7,那推测很可能是因为其中有大量的模拟器用户 - -用户分群打通了寻找特征到持续深挖的链路,是通过自定义事件分析问题必备的技能 - -## 看板 - -顾名思义,看板是非常适合用于观察数据的一个功能。它往往会用在「组建核心指标日报」,「持续观察某个特定问题」的场景下。当你在分析模型中将某个报表保存下来后,就可以将其添加到看板中。 -看板的一大核心能力是分享:你可以将自己构建的看板分享给其他用户,从而大幅降低沟通成本。注意构建看板时要思考其核心主题,一个优秀的看板应该是围绕一个特定且明确的主题的,要避免将不相关的信息组合到一个看板中,否则反而会造成干扰。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/property-analyse.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/property-analyse.mdx deleted file mode 100644 index 52c5bd7e3..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/property-analyse.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: 属性分析 -sidebar_position: 7 ---- - -## 一、什么是属性分析 - -经常会遇到分析玩家在各省的分布情况、玩家的年龄分布情况等,通过这些属性的分析,可以快速描绘出整体用户群的用户画像,而这些分析可以用属性分析功能来实现。
    -属性分析是一种专门分析用户属性的统计与分布情况的模型,模型按照用户属性进行归类,可以同时查看不同分组下用户的统计情况。 -通过属性分析功能,可以帮助开发者多角度、全方位的掌握指定玩家群体的特征,宏观上把握整体玩家的组成与偏好,从而为精细化运营提供依据。 - -## 二、属性分析的支持场景 - -属性分析可以帮助揭示以下问题:
    - -- 所有不同会员等级的用户的平均消费金额是多少; -- 不同省份玩家的分布情况; - -## 三、如何做属性分析 - -属性分析可以分为 4 个步骤:**设置指标、设置维度、设置展示结果、保存报表**。 -后面三个步骤在**事件分析**里均有比较详细的介绍,所以这里重点介绍**设置指标**。 -在属性分析页面,点击「选择指标」,可以看到「指标选择」界面。 - -![](/img/customEvent/character/character1.png) - -对于所有类型的属性都可以将「去重数」作为分析指标,对于数值类型的属性可以将「总和」「均值」「最大值」「最小值」作为分析指标。 - -1. **用户数:** 所有用户数。 -2. **去重数:** 在所有用户中,该属性出现的独立去重个数。 -3. **总和:** 在所有用户中,该属性的取值求和。 -4. **均值:** 在所有用户中,该属性取值的算术平均值。 -5. **最大值:** 在所有用户中,该属性取值的最大值。 -6. **最小值:** 在所有用户中,该属性取值的最小值。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/retention-analyse.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/retention-analyse.mdx deleted file mode 100644 index fb671912b..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/retention-analyse.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: 留存分析 -sidebar_position: 4 ---- - -## 一、引入案例 - -游戏经常会进行更新或改版,那改版之后的留存率有什么变化,我们可以用「留存分析」的功能进行分析和呈现。 - -**1、设置事件**:由于我们聚焦在用户对游戏的整体留存情况,所以我们将用户的初始事件和回访事件设定为「账号登录」。 - -![](/img/customEvent/retention/LC-1-1.png) - -**2、设置维度**:由于我们关注的是版本更新对留存是否有影响,所以在维度里选择「App 版本号」; -![](/img/customEvent/retention/LC-1-2.png) - -**3、设置展示结果**:设置时间区间和其他参数; -![](/img/customEvent/retention/LC-1-3.png) - -**4、保存报表**:查看分析结果,并将分析结果保存为报表。 -![](/img/customEvent/retention/LC-1-4.png) - -## 二、什么是留存分析 - -留存,就是玩家在你的游戏中留下来、持续使用。 - -只有做好了留存,才能保障新玩家在注册后不会白白流失。有时候我们仅看日活(DAU),会觉得数据不错,但有可能是因为近期有密集的运营拉新活动,注入了大量的新用户,但是留下来的用户不一定在增长,可能在减少,只不过被新用户数掩盖了所以看不出来。这就好像一个不断漏水的篮子,如果不去修补底下的裂缝,而只顾着往里倒水,是很难获得持续的增长的。 - -一般我们讲的留存率,是指「目标玩家」在一段时间内「回到游戏中完成某个行为」的比例。常见的指标有次日留存率、七日留存率、次周留存率等。比如:某个时间获取的「新玩家」的「次日留存率」常用来度量拉新效果。 - -## 三、留存分析的支持场景 - -1. 完成登录(初始事件)的玩家中,有多少玩家在接下来一个月进行了充值操作(回访事件); -2. 升级到 VIP 9 级(初始事件)的玩家在接下来一个月购买了多少礼包(回访事件); -3. 想判断某项游戏改动是否奏效,如新增了一个英雄角色,观察是否有人因新增角色而多使用游戏几个月; - -## 四、如何做留存分析 - -如案例所述,留存分析可以分为 4 个步骤: - -**1、设置事件**; - -**2、设置维度;** - -**3、设置展示结果;** - -**4、保存报表;** - -### 4.1 设置事件 - -#### 4.1.1 设置初始事件和回访事件 - -在漏斗分析页面,点击「选择事件」,在「选择事件」界面分别选择初始事件以及回访事件,可以分别对其做筛选。值得注意的是,针对初始 / 回访的筛选条件里, 只允许选择事件属性,如果想要选择用户属性的过滤,可以在全局筛选中进行。 -![](/img/customEvent/retention/LC-2-1.png) - -#### 4.1.2 设置「同时展示」 - -设计该功能的主要目的是对触发回访事件的用户进行深入分析。比如: -我想统计完成登录的用户中有多少用户完成了付费,并且,我还想统计这些完成付费的用户的充值总金额。 -在下图中,就是使用「同时展示」功能对付费用户再做一次付费金额阶段累计总和的统计。 - -![](/img/customEvent/retention/LC-2-2-1.png) - -与事件分析模版不同,同时展示功能中,针对数值类型的属性,分析角度由「总和、中位数、均值、最大值、最小值、人均值、去重数」变为了「总和、人均值、阶段累计总和、阶段累计人均」。而且,此处仅可选数值类型和布尔类型的属性,不能选时间、字符串、列表类型的属性,因为这些类型的属性不能统计「阶段累计」。利用「同时展示」功能,我们可以分析完成回访事件的用户在接下来一段时间的某某属性值的阶段累计总和 / 人均,比如 LTV,n 日付费,累计副本伤害值,累计人均购买礼包数等等。 - -| 指标描述 / 数据类型 | 分析角度 | -| ----------- | -------------------- | -| 数值型 | 总和、人均值、阶段累计总和、阶段累计人均 | -| 布尔型 | 为真数、为假数、为空数、不为空数 | - -**除上表列出的各数值类型的分析角度之外,任意事件任意数值类型都具备的默认分析角度为:总次数、触发用户数、人均次数。** -**「阶段累计」是同时展示功能的核心价值体现。同时展示最多只能做一项分析。** - -### 4.2 设置维度 - -留存分析里「事件发生时间」为必选维度,因为留存天然与时间关联。除了时间维度外,还可再选最多 4 个维度。 - -![](/img/customEvent/retention/LC-2-3.png) - -### 4.3 设置展示结果 - -留存分析的报表依然是透视表形态,与事件分析报表不同的是,「事件发生时间」这一个维度是不能拖拽改变先后聚合顺序的,只能在第一列。 - -![](/img/customEvent/retention/LC-2-4.png) - -#### 4.3.1 选择留存的分析期限 - -![](/img/customEvent/retention/LC-2-5.png) - -留存的分析期限默认为 7 日。点击之后下拉框可选:当日、次日、7 日、14 日、30 日、当周、次周、4 周、8 周、16 周、当月、次月、3 月、6 月、12 月。 - -除上述时间以外,客户也可以自己手动填写 n 日,n 周,n 月。 - -如果所选留存的分析期限过长,比如选了 180 天,会因为数据量过大导致计算缓慢,此时可以使用「仅显示关键日期」, - -- 日的关键日期为:1、7、14、21、30、60、90、120、150、180、360 日; -- 周的关键日期为:1、4、8、16、24、32、40、48、52 周; -- 月的关键日期为:1、3、6、12、24 月; - -勾选「仅显示关键日期」后,仅查询上述时间点的留存情况,从而可以缩短查询时间。当所选留存期限大于 90 天时,系统会自动勾选「仅显示关键日期」。 - -#### 4.3.2 留存 / 流失 - -![](/img/customEvent/retention/LC-2-6.png) - -n 日留存的判断逻辑:触发了初始事件的玩家(假设数量为 a)在第 n 日有 b 人触发了回访事件,「留存百分比」即为 b/a。 -n 日流失的判断逻辑:触发了初始事件的玩家(假设数量为 a)在之后的第 1 日至第 n 日(持续时间)有 b 人没有触发回访事件,b 即为第 n 日的「流失用户」数量。「流失百分比」即为 b/a。 - -#### 4.3.3 显示数量 / 百分比 - -报表右侧可选择显示全部 / 仅显示百分比 / 仅显示数量。 - -![](/img/customEvent/retention/LC-2-7-2.png) - -### 4.4 保存报表 - -![](/img/customEvent/retention/LC-2-7-3.png) - -将设置好的查询结果保存为报表,再基于报表创建看板,留存分析结果一触即达。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/sqlide.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/sqlide.mdx deleted file mode 100644 index cff7d52d3..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/sqlide.mdx +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: SQL 查询 -sidebar_position: 14 ---- - -## 1 概述 - -使用 SQL 查询可以实现自由编写 SQL 代码查询项目内所有数据,满足 TapDB 固有分析模型中无法满足的个性化取数、分析需求。 - -可从「分析」模块下的「SQL 查询」进入 SQL 查询功能,页面由语句「编写框」、「标签页」构成,其中标签页由「表结构」、「查询历史」、「语句书签」和「查询结果」构成。 - -![](/img/customEvent/sql/sql_1.png) - -## 2 适用角色与用途 - -| 角色 | 用途 | -| :-------- | :------------------------------------------------- | -| 管理员 / 分析师 | 了解项目当前数据资产。 | -| 分析师 | 自由编写 SQL 查询项目所有数据,满足 TapDB 固有分析模型中无法满足的个性化取数、分析需求。 | -| 业务人员 | 替换分析师 SQL 代码中的动态参数,满足持续取数、分析需求。 | - -## 3 用表范围与注意事项 - -### 3.1 用表范围 - -在 TapDB SQL 查询功能中,可查询库表范围如下: - -| 表 | 库名 | 表名 | -| :---: | :-------: | :---------------------: | -| 事件表 | tapdb | view_{{项目 ID}}_events | -| 设备表 | tapdb | view_{{项目 ID}}_devices | -| 用户表 | tapdb | view_{{项目 ID}}_users | -| 用户分群表 | tapdb | view_{{项目 ID}}_cluster | -| 维度属性表 | tapdb_dim | view_{{项目 ID}}_{{维度表名}} | - -建议通过「数据表列表」中的「复制表名」功能,将复制该表的表名至剪切板后粘贴至语句编写框,详见本文 5.1.2 部分。 - -### 3.2 使用用户分群表的注意事项 - -所有用户分群数据均存储在同一张表 view_{{项目 ID}}_cluster 中。 - -![](/img/customEvent/sql/sql_2.png) - -可通过筛选分群名,并选择相应分群主体字段,得到该分群下的用户 ID,如下: - -分群主体为账号的分群: - -```sql -select - user_id -from hive.tapdb.view_{{项目 ID}}_cluster -where cluster_name = ‘{{cluster_name}}’ -``` - -分群主体为设备的分群: - -```sql -select - device_id -from hive.tapdb.view_{{项目 ID}}_cluster -where cluster_name = ‘{{cluster_name}}’ -``` - -## 4 编写与执行 SQL 语句 - -SQL 语句的编写与执行主要在语句编写框内进行。 - -![](/img/customEvent/sql/sql_3.png) - -### 4.1 基本语法 - -TapDB 采用 Presto 查询引擎,适用标准 SQL 语法,但仅可以使用 select 语句以及 with 子句,可以访问 [presto 文档](https://trino.io/docs/332/functions.html) 获取 Presto 的语法以及函数的使用方法。 - -数据表中的字段名建议使用双引号 `" "` 括起,也可以缺省,但如果查询字段名带有特殊符号(如 `$`、`#` 等),则必须使用双引号, -字符串必须使用单引号 `' '` 括起。 - -### 4.2 分区与时区 - -查询事件表时,必须使用分区键 `$part_date` 进行条件筛选,避免全表扫描。 - -![](/img/customEvent/sql/sql_4.png) - -建议使用以下类型的分区限制条件: - -```sql -"$part_date" = '2021-11-01' -"$part_date" in ('2021-11-01', '2021-11-02, '2021-11-03') -"$part_date" between '2021-11-01' and '2021-11-11' -``` - -SQL 查询功能默认按照东 8 区对时间类型的字段进行转化展示,如事件表中的 `time`,设备表、账号表中的 `activation_time`、`last_login_time`、`first_charge_time`、`last_charge_time`。 - -若项目不处于东 8 区,则可使用时间函数对其进行转化: - -```sql -format_datetime("time" at time zone 'America/Chicago', 'yyyy-MM-dd') -``` - -`part_date`、`$part_date` 为按照项目时区进行转化后的字符串格式的日期,若无精准查询时、分、秒的需求,建议使用分区键的筛选满足时间筛选需求。 - -### 4.3 动态参数 - -使用动态参数功能可对查询语句中的参数进行替换,后续查询时只需在参数输入框下方输入参数值即可满足新的查询需求。 - -动态参数的表达式规则为 `${参数名}`,参数名需以英文字母开头,可包括英文字母、数字和下划线,参数名相同的参数视为一个参数,可以创建多个参数变量,下方输入框按照各参数首次出现的顺序与动态参数对应,多个同一参数名的参数仅对应一个参数输入框。 - -![](/img/customEvent/sql/sql_5.png) - -### 4.4 工具栏操作 - -工具栏位于输入框下方,可执行以下操作: - -格式化:将查询语句进行格式化 - -复制语句:将输入框内的查询语句复制到剪切板 - -添加书签:将查询语句保存为书签,方便后续进行查询或进行修改 - -![](/img/customEvent/sql/sql_6.png) - -### 4.5 快捷操作 - -光标位于输入框时,可执行以下快捷操作: - -Ctrl + Enter:执行计算 - -Ctrl + Shift + F:格式化当前查询语句 - -Ctrl + Z:撤销上一步操作 - -Ctrl + Y:恢复上一步操作 - -### 4.6 执行查询 - -完成 SQL 语句编写后,可点击「计算」按钮,或者快捷键 Ctrl + Enter ,发起数据查询。 - -默认单次查询最多 10000 条数据,系统会为查询语句自动添加「limit 10000」,对查询行数进行限制,前端最多展示 500 行,可通过「查询历史」、「查询结果」中的下载功能查看所有数据。 - -## 5 标签页 - -标签页由「表结构」、「查询历史」、「语句书签」和「查询结果」构成。 - -![](/img/customEvent/sql/sql_8.png) - -### 5.1 表结构 - -表结构页可查看数据库、数据表、表字段的详细信息,从左至右由数据库列表、数据表列表、表字段列表 3 部分构成。 - -![](/img/customEvent/sql/sql_9.png) - -#### 5.1.1 数据库列表 - -数据库列表中可查看项目下的数据库,点击列表中的库名,右侧将会展示该库下的数据表列表。 - -![](/img/customEvent/sql/sql_10.png) - -#### 5.1.2 数据表列表 - -数据表列表中可查看选中的数据库下的数据表,点击列表中的表名,右侧将会展示该表下的字段列表。 - -点击「复制表名」按钮,将复制该表的表名至剪切板。 - -![](/img/customEvent/sql/sql_11.png) - -#### 5.1.3 表字段列表 - -表字段列表中可查看选中表的所有字段的信息,包括字段名、数据类型、解释。 - -![](/img/customEvent/sql/sql_12.png) - -### 5.2 查询历史 - -查询历史页中可查看执行过的查询语句,包括语句的完成时间、计算耗费时间、查询语句等信息,并可对查询语句进行搜索,快速找到相应语句。 - -![](/img/customEvent/sql/sql_13.png) - -点击「查询 ID」,可跳转至相应查询结果; - -点击「查询语句」右上角「键入」按钮,可将该语句将替换至输入框中; - -点击「下载」,将该查询结果以 csv 格式文件的形式下载到本地,查询结果页面展示上限为 500 条,超过上限的数据可以使用下载功能下到本地后进行进一步查看与分析,数据下载上限为 10000 条。 - -![](/img/customEvent/sql/sql_14.png) - -### 5.3 语句书签 - -语句书签页中可查看已保存的语句书签。 - -点击「设置」,书签中的内容将替换语句输入框中的内容,点击「删除」,可删除该书签。 - -![](/img/customEvent/sql/sql_15.png) - -### 5.4 查询结果 - -查询结果页中可查看历史查询结果。 - -点击「格式化」,将查询结果中的 json、map 等类型的对象进行格式化展示; - -点击「下载」,将该查询结果以 csv 格式文件的形式下载到本地,同「查询历史」中的「下载」功能。 - -![](/img/customEvent/sql/sql_16.png) - -## 6 最佳实践 - -### 6.1 日志导出 - -导出用户表或事件表,如导出近 7 日用户的所有事件日志: - -```sql -select - * -from hive.tapdb.view_{{项目 ID}}_events -where "$part_date" between '2021-11-05' and '2021-11-11' -``` - -### 6.2 数据清洗与提取 - -提取复杂字段,如:url、json、map 中的关键信息,如提取 url 中后 10 位数字的商品 ID: - -```sql -select - substring("#url", -10) as product_id -from hive.tapdb.view_{{项目 ID}}_events -where "$part_date" between '2021-11-05' and '2021-11-11' -``` - -### 6.3 个性化取数与分析 - -各种 TapDB 现有分析模型无法满足的个性化分析需求。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/tracking-management.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/tracking-management.mdx deleted file mode 100644 index 814f7fc59..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/tracking-management.mdx +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: 埋点管理 -sidebar_position: 11 ---- - -## 1. 概述 - -埋点管理为数据接入测试和日常使用过程中提供的数据管理功能,包括埋点信息、上报明细 2 个子模块,各子模块功能说明如下: - -埋点信息页面可查看最近 7 日项目内数据接收情况,方便快速了解埋点上报整体情况,以及错误上报详情与抽样示例。 - -上报明细页面可查看「监测开关」开启期间近 1000 条的埋点上报明细,实时查看埋点上报日志,帮助用户快速测试、验收埋点开发。 - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ---------------------------- | -------------------------------------------- | -| 埋点设计人员(数据产品经理 / 分析师) | 测试与验收新埋点需求开发的上报结果,日常了解埋点整体运行情况,及时发现埋点错误 | -| 埋点开发人员(前端开发 / 客户端开发 / 测试工程师) | 实时查看埋点上报日志,测试埋点效果,快速定位埋点开发环节中的错误(建议在测试项目中启用) | - -## 3. 埋点信息 - -埋点信息由「埋点信息主页」、「错误详情页」2 部分构成。 - -埋点信息主页 - -![埋点信息主页](/img/customEvent/tracking_manager_1.png) - -错误详情页 - -![错误详情页](/img/customEvent/tracking_manager_2.png) - -### 3.1 埋点信息主页 - -埋点信息支持查看最近 7 日项目内数据接收情况。 - -埋点信息主页由查询设置、区间数据、数据详情区域构成。 - -![埋点信息主页](/img/customEvent/tracking_manager_3.png) - -#### 3.1.1 查询设置 - -查询时间筛选:查询时间指数据的接收时间。支持自定义查看最近 7 日任一时间段的数据接收情况,默认统计今日的数据。 - -属性名、显示名搜索:根据关键词对属性名、显示名进行过滤展示。 - -![查询设置](/img/customEvent/tracking_manager_4.png) - -#### 3.1.2 区间数据 - -展示所筛选时间段内所有数据的已接收、已入库、错误入库和入库失败数。 - -![区间数据](/img/customEvent/tracking_manager_5.png) - -#### 3.1.3 数据详情 - -数据名称:事件的埋点上报为相应的事件名,设置用户属性的埋点上报为「用户属性」,无法识别为以上两类的则为「未知数据」; - -显示名:数据名称对应的显示名; - -已接收:系统接收的数据; - -已入库:包括正确入库和错误入库数据; - -错误入库:包含如事件属性名不合法、属性类型不合法等属性级别错误的数据,该类数据可正常入库,错误属性置空; - -入库失败:因数据格式不合法、数据名不合法导致无法入库的数据。 - -错误详情:点击进入该数据的错误详情页,查看按照错误原因展示的错误详情。 - -![数据详情](/img/customEvent/tracking_manager_6.png) - -### 3.2 错误详情页 - -数据详情页展示各事件或用户属性下的错误入库和入库失败数据,错误信息按照错误原因展示。 - -错误详情页由查询设置、区间数据、数据详情区域构成。 - -![错误详情页](/img/customEvent/tracking_manager_7.png) - -#### 3.2.1 数据详情 - -错误条数:指含有该错误原因的数据条数。 - -错误类型、处理结果:指该错误的所属错误类型与 TapDB 对其处理的结果,便于用户对具体错误原因进行筛选。 - -查看抽样:查看符合该错误原因的上报数据的详细内容,每条上报数据均可选择格式化查看与一键复制。 - -![数据详情](/img/customEvent/tracking_manager_8.png) - -#### 3.2.2 错误条数的计算原理 - -「错误条数」是对触发该错误原因的上报数据进行计数得到的结果,而埋点管理中的「错误原因」,是发生在「属性」层面的。 - -当仅上报 1 条某事件的日志时,其中字段 1 属性名不合法,字段 2 属性类型不合法,则该日志会同时触发 2 个错误原因,分别在两类错误原因下计算错误条数,因而数据详情汇总的错误条数的加和存在大于错误日志条数的可能。 - -## 4. 上报明细 {#realtime} - -上报明细展示最近 1000 条「监测开关」开启期间的上报日志明细,并展示其入库的处理结果。 - -![上报明细](/img/customEvent/tracking_manager_9.png) - -「监测开关」开启时,系统接收的埋点日志将实时展示在上报明细中,开启时长达到 1 小时,则自动关闭,不再对数据进行实时展示,任何时候重新开启开关都将重置 1 小时的时间进度。建议在测试、验收埋点开发前,手动开启「监测开关」。 - -点击刷新按钮,可刷新页面获取最新数据。 - -每行上报日志明细都可进行格式化查看、复制操作。 - -## 5. 使用埋点管理 - -### 5.1 及时发现埋点错误 - -使用埋点信息页面,掌握埋点整体运行情况,当出现未知数据,或部分数据错误入库、入库失败时,可在错误详情页定位错误,防止数据资产流失。 - -### 5.2 使用上报日志对埋点需求进行测试、验收 - -在埋点开发人员完成开发后,埋点设计人员可模拟用户在游戏中的各类点击行为,而后在「上报日志」中查看实时上报日志,检查上报时机与日志明细是否符合埋点设计方案,从而对埋点需求进行测试、验收。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/user-seq.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/user-seq.mdx deleted file mode 100644 index 05b1afac5..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/user-seq.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: 用户精查 -sidebar_position: 18 ---- - -## 1. 什么是用户精查 - -用户精查用于精细排查符合某一类特征或某一确切用户的行为表现。 - -## 2. 用户精查支持的场景 - -* 通过漏斗分析找到了用户在登录或新手教程阶段发生流失,找出在流失前用户产生了哪些行为。 -* 回顾用户在 Crash 或问题前的行为序列,找出问题场景。 - -## 3. 如何使用用户精查 - -通过分析模型得出分析结果若为人群(触发账号数、触发设备数),点击分析结果即可展示符合条件的用户列表及这些用户的详细信息。也可在用户精查界面中,直接使用用户属性作为条件筛选匹配的用户。点击列表中的用户可获取该用户在某一时间段内上报的行为分布、行为序列、用户属性等。 - -### 3.1 用户搜索 - -点击右上方「用户精查」,开启用户精查界面: - -![](/img/customEvent/user/user_seq_1.png) - -选择主体并设置查询条件,可以找出匹配的设备或账号,也可通过账号或设备 ID 进行精确搜索。 - -![](/img/customEvent/user/user_seq_2.png) - -### 3.2 用户列表 - -从分析模型或用户精查可进入到用户列表: - -![](/img/customEvent/user/user_seq_3.png) - -### 3.3 用户行为序列 - -在用户列表中点击用户 ID 可进入用户行为序列查询: -![](/img/customEvent/user/user_seq_4.png) - -**行为事件总量** - -柱状图将展示筛选时间范围内的上报事件量趋势,开启右侧「展示事件分布」可打开行为分布饼状图。 - - -**事件明细** - -* 该用户的行为序列将被按照时间顺序展示。 -* 点击展开按钮可以查看该事件的全部事件属性。 -* 当事件被展开后,鼠标悬停在事件属性上时,可将其设置为外显属性。设置为外显属性的属性,无需展开事件也可在事件名后面查看到该事件属性。 - - -**用户属性** -右侧用户属性中将展示当前用户的属性,可以使用「自定义属性」功能配置这些属性的显隐。 \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/user-tag.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/user-tag.mdx deleted file mode 100644 index c53db4f71..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/user-tag.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: 用户标签 -sidebar_position: 17 ---- - -## 1. 概述 - -用户标签如同用户属性,将用户某类特征作为「用户标签」,用户在该特征下的具体表现作为「标签值」,用以将用户划分为多个不同的群体,便于在各种分析模型中使用标签进行维度分组或筛选。 - -TapDB 的分析模型中分别以「账号」、「设备」作为查询主体进行查询,在用户标签中同样支持分别以「账号」、「设备」作为主体创建标签。 - -目前支持通过「指标值」创建用户标签,后续将开放更多标签创建方式。 - -![概述](/img/customEvent/userTag_1.png) - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ---------- | --------------------------------- | -| 分析师 / 业务人员 | 计算用户在一段时间范围内的行为指标,在分析模型对用户进行分组、筛选 | - -## 3. 创建用户标签 - -点击标签列表右上角的创建标签,选择「指标值标签」。 - -![创建用户标签_1](/img/customEvent/userTag_2.png) - -新建指标值标签页分为「基础信息」、「指标值配置」两部分。 - -![创建用户标签_2](/img/customEvent/userTag_3.png) - -### 3.1 基础信息 - -在「基础信息」部分,依次录入或选择「标签名」、「标签主体」、「标签显示名」、「更新方式」、「备注」。 - -![基础信息](/img/customEvent/userTag_4.png) - -标签显示名展示在分群列表、分析模型中,是业务人员识别标签的依据。 - -标签主体,支持「账号」或「设备」,根据业务场景做选择。 - -标签名是分群存储在系统后台的唯一标识,为方便数据分析人员直接查询数据库表,可命名为带有业务含义的参数名。 - -更新方式分为「手动更新」与「自动更新」。「手动更新」指在完成首次计算后,系统不会自动更新用户标签,用户需要手动进行更新;「自动更新」会在每日 0 点后,以前一日作为基准进行用户标签更新。 - -### 3.2 指标值配置 - -指定时段内,用户完成事件的聚合指标,作为标签值。 - -![指标值配置](/img/customEvent/userTag_5.png) - -完成事件的用户将属于标签,未完成该事件的用户无标签值。 - -通过构建指标确定用户的标签值,在「事件分析」创建指标方法的基础上,增加「事件」的「天数」、「小时数」,同样支持属性筛选与编辑公式。 - -## 4. 用户标签的管理与使用 - -### 4.1 管理用户标签 - -创建的标签会以列表形式展示在用户标签页,用户可以对分群进行查看、编辑、删除、更新、下载、复制操作。 - -![管理用户标签](/img/customEvent/userTag_6.png) - -其中,「数量」代表在该标签下有值的用户数,「数据更新时间」为标签最近一次进行计算的时间。 - -### 4.2 用户标签详情 - -由于指标值标签的值为离散且数量众多的数值,因而在标签详情中,可对指标值的展示分布情况进行设置,以便进行阅读,但不改变指标值本身。 - -![用户标签详情](/img/customEvent/userTag_7.png) - -### 4.3 在分析模型中使用用户标签 - -与用户属性相同,用户标签可作为筛选条件。 - -![在分析模型中使用用户标签_1](/img/customEvent/userTag_8.png) - -同样,用户标签可作为分组维度。 - -![在分析模型中使用用户标签_2](/img/customEvent/userTag_9.png) - -## 5. 最佳实践 - -在「指标值」标签中可计算每个用户在某个时间段内行为的指标,比如登录天数、付费金额等。这些指标值标签可以作为事件分析、属性分析中的分组维度对用户进行分组分析,或作为筛选项,对用户进行进一步的下钻分析。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/virtual-event.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/virtual-event.mdx deleted file mode 100644 index 2301765db..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/virtual-event.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: 虚拟事件 -sidebar_position: 15 ---- - -## 1. 概述 - -可以将多个业务含义相似的事件组成虚拟事件,任一基础事件被触发即视为该虚拟事件被触发; - -也可将某一事件通过不同的筛选条件拆分为多个事件,符合筛选条件的基础事件被触发视作该虚拟事件触发。 - -可在事件管理中新建、管理虚拟事件。 - -![概述](/img/customEvent/virtualEvent_1.png) - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ------------------------- | ------------------------------------ | -| 埋点设计人员(数据产品经理 / 分析师) | 对埋点事件进行转化,降低埋点方案设计的复杂度,弥补埋点设计、开发中的缺陷 | -| 数据分析人员(数据产品经理 / 分析师 / 运营) | 对相似业务含义的事件进行组合,或对某一事件进行拆分,提高日常分析使用效率 | - -## 3. 新建虚拟事件 - -点击事件管理右上角「新建事件」,选择「虚拟事件」。 - -![新建虚拟事件](/img/customEvent/virtualEvent_2.png) - -### 3.1 填写基础信息 - -填入虚拟事件名、虚拟事件显示名和说明,事件名默认以「#ve@」开头。 - -![填写基础信息](/img/customEvent/virtualEvent_4.png) - -### 3.2 编辑虚拟事件的定义规则 - -构成虚拟事件的基础事件之间为「或」的关系,任一基础事件被触发(若对基础事件进行筛选则此处需满足筛选条件)即视为该虚拟事件被触发。 - -当虚拟事件由 2 个或以上基础事件构成时,可对其进行全局筛选,全部基础事件均需满足该筛选条件。 - -![编辑虚拟事件的定义规则](/img/customEvent/virtualEvent_3.png) - -## 4. 虚拟事件的管理与使用 - -### 4.1 管理虚拟事件 - -可在事件管理页面中,对虚拟事件进行管理,虚拟事件可创建数量上限为 300,可以进行编辑、复制和删除操作。 - -![管理虚拟事件](/img/customEvent/virtualEvent_5.png) - -### 4.2 分析模型中使用的注意事项 - -虚拟事件在使用上和预置、自定义事件一致,其所关联的事件属性为所有基础属性的关联事件属性的交集。 - -虚拟事件的触发用户数含有对基础事件联合去重的逻辑,因而在部分场景中,虚拟事件的触发用户数会小于所有基础事件触发用户数的直接加和。 - -## 5. 最佳实践 - -### 5.1 将多个业务含义相似的事件合并为单个事件 - -埋点设计中,将「大世界战斗」、「副本战斗」、「PVP 战斗」作为 3 种不同的事件上报。 - -现需对用户所有战斗行为进行统计,因而可在虚拟事件中将该 3 个事件共同作为构成虚拟事件「战斗」的基础事件,用户触发任意类型的战斗行为便视作触发「战斗」,从而实现对用户的所有战斗行为进行统计。 - -### 5.2 将单个事件拆分为业务含义更具体的多个事件 - -埋点设计中,将所有页面的浏览统一上报为「页面浏览」,并通过事件属性「页面类型」来对各页面进行区分。 - -日常分析中,首页、商店页、充值页的分析需求较为频繁,因而可在虚拟事件中将「页面浏览」作为基础事件并对其「页面类型」进行筛选,构造出 3 个虚拟事件「首页浏览」、「商店页浏览」、「充值页浏览」,这样可以实现快速统计并满足日常分析需求。 diff --git a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/virtual.mdx b/versioned_docs/version-v4/sdk/tapdb/features/custom-event/virtual.mdx deleted file mode 100644 index 035381a7b..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/custom-event/virtual.mdx +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: 虚拟属性 -sidebar_position: 12 ---- - -## 1. 概述 - -对于已经上报的事件属性和用户属性,可以通过设置虚拟属性将原先上传的数据映射为另一种展示值或计算值,使初始埋点时的属性值不与展示值相同,并可在后期可对数据进行加工,提高灵活性。 - -虚拟属性通过 SQL 表达式对基础属性字段进行计算,得到一个新的属性字段。 - -可在事件属性管理中新建、管理虚拟事件属性,在用户属性管理中创建、管理虚拟用户属性。 - -## 2. 适用角色与用途 - -| 角色 | 用途 | -| ------------------------- | ------------------------------------ | -| 埋点设计人员(数据产品经理 / 分析师) | 对埋点字段进行转化,降低埋点方案设计的复杂度,弥补埋点设计、开发中的缺陷 | -| 数据分析人员(数据产品经理 / 分析师 / 运营) | 自助从现埋点数据中提取部分低频或少量场景使用的分析维度,快速响应分析需求 | - -## 3. 新建虚拟属性 - -点击事件属性管理、用户属性管理右上角「新建事件属性」或「新建用户属性」,选择「新建虚拟属性」。 - -![新建虚拟属性](/img/customEvent/virtual_1.png) - -### 3.1 填写基础信息 - -#### 3.1.1 通用基础信息 - -填入或选择属性名、显示名、数据类型、单位、说明,属性名默认以 `#vp@` 开头。 - -![通用基础信息](/img/customEvent/virtual_2.png) - -#### 3.1.2 虚拟事件属性的独有基础信息 - -选择「关联主体」,不同的选择对应不同的基础属性*(预置属性、自定义属性统称为基础属性)*范围,以及该虚拟属性应用范围: - -| 关联主体 | 基础属性范围 | 虚拟属性应用范围 | -| ---- |-----------------------| ------------- | -| 无主体 | 事件属性 | 以设备或账号作为查询主体时 | -| 账号 | 事件属性 & 账号属性
    账号属性 | 以账号作为查询主体时 | -| 设备 | 事件属性 & 设备属性
    设备属性 | 以设备作为查询主体时 | - -选择「关联事件」,不同的选择对应不同的关联逻辑,如下: - -- 自动识别:关联 SQL 代码所涉及基础事件属性所关联的事件的并集 - -- 全部事件:关联全部事件,若新增事件则自动关联 - -- 指定事件:关联指定事件 - -![事件属性独有](/img/customEvent/virtual_3.png) - -#### 3.1.3 虚拟用户属性的独有基础信息 - -选择「用户标识」,不同的选择对应不同的基础属性范围,以及该虚拟属性应用范围: - -| 用户标识 | 基础属性范围 | 虚拟属性应用范围 | -| ---- | ------ | --------- | -| 账号 | 账号属性 | 以账号为查询主体时 | -| 设备 | 设备属性 | 以设备为查询主体时 | - -![户属性独有基础信息](/img/customEvent/virtual_4.png) - -### 3.2 编辑虚拟属性的创建规则 - -在左侧列表中可以快速获取当前可用的基础属性的信息,在文本框输入 SQL 表达式。 - -![创建规则](/img/customEvent/virtual_5.png) - -虚拟属性的 SQL 表达式使用 presto 语法,可以访问 [presto 文档](https://trino.io/docs/332/functions.html) 获取 presto 的语法以及函数的使用方法。 - -可通过「插入模板」功能,插入常用的应用场景所需的代码片段,通过替换字段名和修改逻辑即可简单快捷实现虚拟属性的创建。 - -### 3.3 逻辑校验 - -输入内容后,可点击「校验」检查基础属性范围、SQL 语法,校验成功则会在「调试」板块出现属性的值输入框。 - -输入校验值后查看结果,可以校验结果是否符合需求。如果符合需求,可以点击「下一步」完成属性创建,如果不符合预期则可继续修改创建规则,并重复校验。 - -![逻辑校验](/img/customEvent/virtual_6.png) - -## 4. 虚拟属性的管理与使用 - -### 4.1 管理虚拟属性 - -可在事件属性管理或用户属性管理页面中,对虚拟属性进行管理,虚拟事件属性、虚拟用户属性可创建数量上限均为 300,可以进行编辑、删除操作。 - -![管理虚拟属性](/img/customEvent/virtual_7.png) - -### 4.2 模型中使用的注意事项 - -虚拟属性在使用上和通常的属性一致,根据类型决定其计算逻辑以及筛选条件。 - -虚拟事件属性可以在计算其关联的事件时被使用,关联关系可以自定义。 - -虚拟用户属性,可用场景等同一般的用户属性。 - -## 5. 最佳实践 - -### 5.1 弥补埋点开发中的字段类型上报错误 - -错误将用户年龄字段「age」上报为文本,在重新发版前,现暂时紧急需对用户年龄进行分析: - -```sql -cast("age" as int) -``` - -### 5.2 计算用户生命周期 - -根据用户的激活时间「activation_time」和事件发生时间「time」,现需计算用户行为发生时的生命周期: - -```sql -date_diff('day', date("activation_time"), date("time")) -``` - -### 5.3 通过算术运算完成单位转化 - -已有以「分」为单位的支付金额字段「amount」,现需要将其转化为以「元」为单位: - -```sql -"amount" / 100 -``` - -### 5.4 标识独立角色或其他所希望进行分析的主体 - -一个用户可在不同服务器创建不同角色,根据用户身份标识字段「user_id」和服务器标识字段「server_id」生成标识角色的字段: - -```sql -concat("server_id", "user_id") -``` - -### 5.5 截取字段获得复杂字段中的关键维度信息 - -根据论坛帖子的 ID「post_id」*(实例:10086202012310001)*截取发帖月份: - -```sql -substring("post_id", 6, 6) -``` - -### 5.6 将页面按照功能模块分类 - -埋点记录了用户访问的 url 地址,现需要了解用户使用的功能模块的情况: - -```sql -case - - when "url" like '%home%' then '首页' - - when "url" like '%store%' then '商城' - - when "url" like '%stage%' then '剧情' - - else '其他' - -end -``` diff --git a/versioned_docs/version-v4/sdk/tapdb/features/diagnosis.md b/versioned_docs/version-v4/sdk/tapdb/features/diagnosis.md deleted file mode 100644 index b7b6b194c..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/diagnosis.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: 诊断 -sidebar_position: 3 ---- - -## 1.概述 - -「诊断」是基于 TapTap 自研的崩溃监测 SDK - Themis 监控到的数据生成报表,并提供相关的日志信息帮助开发者高效定位并解决问题的 TapDB 功能模块 - -## 2.如何使用「诊断」? - -「诊断」功能面向开放 TapPlay 的游戏及接入了 TapSDK 并启用了 Themis 功能的游戏,如何在接入过程中调整配置,请参考:[接入指南 - 诊断模块](/sdk/tapdb/sdk/client-side-integration/#themis) - -## 3.「诊断」能够覆盖什么场景? - -- 监控游戏过程中产生的崩溃、闪退、报错,获取用户行为日志 - -## 4.「诊断」包含哪些模块? - - - 崩溃分析:监控游戏过程中产生的崩溃闪退,提供定位条件及报表,可具体到某一特定用户的崩溃信息 - - - 错误分析:监控游戏过程中产生的报错、自定义日志等信息,提供定位条件及报表,可具体到某一特定用户的上报信息 - - - 符号表管理:上传对应的符号表可以对 APP 发生 Crash 的堆栈进行解析和还原,快速并准确地定位用户 APP 发生 Crash 的代码位置 - -![「诊断」包含哪些模块?](/img/customEvent/diagnosis/diagnosis-1.png) - -## 5.崩溃分析及错误分析如何使用? - -*「崩溃分析」及「错误分析」功能一致,仅是监测的条件不同,故一并说明* - -### 通过「概览」观测程序的运行稳定性 - -1. 观察选定日期内程序的运行稳定性趋势,通过报错率、上报趋势图确定选定日期内是否存在问题 - -![观测程序的运行稳定性](/img/customEvent/diagnosis/diagnosis-2.png) - -2. 通过「高占比统计」柱状分布图快速定位问题可能存在的场景(如游戏接入了 TapDB 且开放了 TapPlay,则会额外提供游戏在 TapTap 应用版本的上报分布图) - -![高占比统计](/img/customEvent/diagnosis/diagnosis-3.png) - -3. 通过「Top 10 问题列表」的详细信息进入报错最多的问题详情,快速锚定问题 - -![Top 10 问题列表](/img/customEvent/diagnosis/diagnosis-4.png) - -### 如何定位具体问题原因? - -1. 通过「平台」筛选您要查看的应用所属平台为 Andorid 还是 IOS,默认选中安卓 - -2. 通过「数据」筛选您要查看的是哪个 SDK 上报的数据,包含 TapDB 及 TapPlay - -3. 通过筛选器设置条件定位特定条件的报错人群,设置条件后,将重新加载数据筛选出符合条件的信息 - -![通过筛选器设置条件定位特定条件的报错人群](/img/customEvent/diagnosis/diagnosis-5.png) - -4. 通过设置的过滤条件将筛选出符合条件的报错详细信息,不同用户的相同报错会根据特定特征进行归类,合并为同一个问题 ID - - - a. 点击问题 ID 即可进入该类问题的详情页,可查看该类问题的上报趋势及分布情况 - - - b. 点击上报 ID,侧边栏弹出特定用户的报错详情,开发者可根据「出错堆栈」、「跟踪数据」等定位问题原因 - -![筛选出符合条件的报错详细信息](/img/customEvent/diagnosis/diagnosis-6.png) - -## 6.「符号表管理」如何使用? - -「符号表管理」用于管理开发者所要上传的符号表,向开发者提供上传,筛选、删除等能力,详细使用流程可见下图,如有疑问,可向我们提交工单咨询 - -![符号表管理](/img/customEvent/diagnosis/diagnosis-7.png) diff --git a/versioned_docs/version-v4/sdk/tapdb/features/exchange-rate.mdx b/versioned_docs/version-v4/sdk/tapdb/features/exchange-rate.mdx deleted file mode 100644 index 031d75b9e..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/exchange-rate.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: 汇率表 -sidebar_position: 6 ---- - -import { ExchangeTable } from "/src/docComponents/ExchangeTable"; - - diff --git a/versioned_docs/version-v4/sdk/tapdb/features/subcontinent.mdx b/versioned_docs/version-v4/sdk/tapdb/features/subcontinent.mdx deleted file mode 100644 index 8e555a295..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/features/subcontinent.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: 次大陆 -sidebar_position: 4 ---- - -次大陆是按照联合国的规定进行划分。每个次大陆包含的国家或地区如下所示 - -## 非洲 - -**东非** - -布隆迪 科摩罗 吉布提 厄立特里亚 埃塞俄比亚 肯尼亚 马达加斯加 马拉维 毛里求斯 马约特 莫桑比克 留尼汪 卢旺达 塞舌尔群岛 索马里 南苏丹 坦桑尼亚 乌干达 赞比亚 津巴布韦 法属南部领土 英属印度洋领地 - -**中非** - -安哥拉 喀麦隆 中非共和国 乍得 刚果 扎伊尔 赤道几内亚 加蓬 圣多美和普林西比 - -**北非** - -阿尔及利亚 埃及 阿拉伯利比亚民众国 摩洛哥 苏丹 突尼斯 西撒哈拉 - -**南非** - -博茨瓦纳 斯威士兰 莱索托 纳米比亚 南非 - -**西非** - -贝宁 布基纳法索 佛得角 象牙海岸 冈比亚 加纳 几内亚 几内亚比绍 利比里亚 马里 毛里塔尼亚 尼日尔 尼日利亚 塞内加尔 塞拉利昂 圣赫勒拿 多哥 - -## 美洲 - -**加勒比海地区** - -安圭拉 安提瓜和巴布达 阿鲁巴 巴巴多斯 巴哈马 英属维京群岛 博奈尔岛、圣尤斯达蒂斯和萨巴 开曼群岛 古巴 库拉索 多米尼加 多米尼加共和国 格林纳达 瓜德罗普岛 海地 马提尼克群岛 蒙塞拉特群岛 波多黎各 圣马丁岛 圣巴泰勒米 圣基茨和尼维斯 圣卢西亚 圣马丁 圣文森特和格林纳丁斯 特立尼达和多巴哥 特克斯和凯科斯群岛 美属维京群岛 牙买加 - -**中美洲** - -伯利兹 哥斯达黎加 危地马拉 萨尔瓦多 洪都拉斯 墨西哥 尼加拉瓜 巴拿马 - -**北美洲** - -百慕大 加拿大 格陵兰 圣皮埃尔和密克隆 美国 - -**南美洲** - -阿根廷 玻利维亚 布维特岛 巴西 智利 哥伦比亚 厄瓜多尔 福克兰群岛 法属圭亚那 圭亚那 巴拉圭 秘鲁 南乔治亚岛和南桑威齐群岛 苏里南 乌拉圭 委内瑞拉 - -## 亚洲 - -**中亚** - -哈萨克斯坦 吉尔吉克斯坦 塔吉克斯坦 土库曼斯坦 乌兹别克斯坦 - -**东亚** - -中国大陆 中国香港特别行政区 日本 中国澳门特别行政区 蒙古 朝鲜民主共和国 大韩民国 中国台湾地区 - -**东南亚** - -文莱 柬埔寨 印度尼西亚 老挝人民民主共和国 马来西亚 缅甸 菲律宾 新加坡 泰国 越南 东帝汶 - -**南亚** - -阿富汗 孟加拉 不丹 印度 伊朗伊斯兰共和国 马尔代夫 尼泊尔 巴基斯坦 斯里兰卡 - -**西亚** - -亚美尼亚 阿塞拜疆 巴林 塞浦路斯 格鲁吉亚 伊拉克 以色列 约旦 科威特 黎巴嫩 阿曼 - -巴勒斯坦领土 卡塔尔 沙特阿拉伯 叙利亚 土耳其 阿拉伯联合酋长国 也门 - -## 欧洲 - -**东欧** - -白俄罗斯 保加利亚 捷克共和国 匈牙利 摩尔多瓦共和国 波兰 罗马尼亚 俄罗斯 斯洛伐克共和国 乌克兰 - -**北欧** - -奥兰群岛 丹麦 爱沙尼亚 法罗群岛 芬兰 格恩西岛 冰岛 爱尔兰 曼岛 泽西岛 拉脱维亚 立陶宛 挪威 斯瓦尔巴特和扬马延 瑞典 英国 - -**南欧** - -阿尔巴尼亚 安道尔 波斯尼亚和黑山共和国 克罗地亚 直布罗陀 希腊 意大利 科索沃 马耳他 黑山共和国 前南斯拉夫马其顿共和国 葡萄牙 圣马力诺 塞尔维亚 斯洛文尼亚 西班牙 圣座(梵蒂冈) - -**西欧** - -奥地利 比利时 法国 德国 列支敦士登 卢森堡 摩纳哥 荷兰 瑞士 - -## 大洋洲 - -**澳大拉西亚** - -澳大利亚 圣诞岛 科科斯群岛 赫德与麦克唐纳群岛 新西兰 诺福克岛 - -**美拉尼西亚** - -斐济 新喀里多尼亚 巴布亚新几内亚 所罗门群岛 瓦努阿图 - -**密克罗尼西亚** - -关岛 基里巴斯 马绍尔群岛 密克罗尼西亚 瑙鲁 北马里亚纳群岛 帕劳 美国边远小岛 - -## 大洋洲海外地区 - -南极洲 - -**波利尼西亚** - -美属萨摩亚 库克群岛 法属波利尼西亚 纽埃 皮特凯恩群岛 托克劳 汤加 图瓦卢 瓦利斯和富图纳 萨摩亚 diff --git a/versioned_docs/version-v4/sdk/tapdb/sdk/_category_.json b/versioned_docs/version-v4/sdk/tapdb/sdk/_category_.json deleted file mode 100644 index 361b849f7..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/sdk/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "开发指南", - "position": 3 -} diff --git a/versioned_docs/version-v4/sdk/tapdb/sdk/client-side-integration.mdx b/versioned_docs/version-v4/sdk/tapdb/sdk/client-side-integration.mdx deleted file mode 100644 index 74203d58f..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/sdk/client-side-integration.mdx +++ /dev/null @@ -1,1213 +0,0 @@ ---- -title: 客户端接入 -sidebar_label: 客户端接入 -sidebar_position: 3 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; -import sdkVersions from '/versioned_docs/version-v4/sdkVersions'; -import {Conditional} from '/src/docComponents/conditional'; - -import UnitySDKInstallation from "../../_partials/unity-sdk-installation.mdx"; - -## 介绍 - -TapSDK 提供了一套可供游戏开发者收集账号数据的 API。 -系统会收集账号数据并进行分析,最终形成数据报表,帮助游戏开发者分析账号行为并优化游戏。 - -## 环境要求 - - - -<> - -- Unity 2019.4 或更高版本 -- iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) -- Android 5.0(API level 21)或更高版本 - - - -<> - -Android 5.0(API level 21)或更高版本 - - - -<> - -iOS 11 或更高版本,Xcode 版本 [14.1 或更高版本](https://developer.apple.com/news/?id=jd9wcyov) - - - - - -## 权限说明 - - - -<> - - - - -<> - -该模块需要如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 获取网络状态 | 用于检测当前网络连接是否有效 | 用户首次使用该功能时会申请权限 | -| 网络状态权限 | 用于检查网络连接状态(如 Wi-Fi 或移动数据是否可用) | 用户首次使用该功能时会申请权限 | - -对于可选权限,SDK 不会主动发起申请,只在用户已授权的情况下获取对应数据,需要由开发者决定是否申请。 - -该模块将在应用中添加如下权限: - -```xml - - - ``` - - - -<> - - - - - - -## 集成前准备 - -1. 参考 [准备工作](/sdk/start/get-ready/) 创建应用、开启应用配置开通数据分析服务; - -## SDK 集成 - -请先[下载](/tap-download) TapSDK,并添加相关依赖。 - - - -<> - -如果只需要单独使用 TapEvent,可以导入依赖 `com.taptap.sdk.core`。 - - - - - -<> - -1. 项目根目录的 build.gradle 添加仓库地址: - -```groovy -allprojects { - repositories { - google() - mavenCentral() - } -} -``` - -2. app module 的 build.gradle 添加对应依赖: - -{ -`dependencies { - implementation 'com.taptap.sdk:tap-core:${sdkVersions.taptap.android}' -}` -} - - - -<> - -iOS 提供通过添加 cocosPod 远程依赖和使用本地文件导入两种集成方式,推荐使用远程依赖方式。 - -##### 远程依赖 - -1. 在工程 Podfile 文件中对应模块下添加依赖: - -{` pod 'TapTapCoreSDK', '~> ${sdkVersions.taptap.ios }'`} - -2. 执行 `Pod install` 下载对应依赖文件 - -##### 本地文件依赖 - -1. 在下载页下载如下文件: - -- `tapsdkcorecpp.xcframework` 基础库 -- `TapTapBasicToolsSDK.xcframework` 基础库 -- `TapTapCoreSDK.xcframework` 核心库 -- `TapTapGidSDK.xcframework` 基础库 -- `TapTapNetworkSDK.xcframework` 基础库 -- `THEMISLite.xcframework` 基础库 - - -2. 在工程中添加 `framework` 静态库,注意添加时选择 Embed 方式为 **Do Not Embed** -3. SDK 内部使用了 [`Protobuf` 依赖库](https://cocoapods.org/pods/Protobuf),开发者应提前通过远程或文件导入方式添加对应依赖。 - -#### 配置编译选项 - -- 在 Build Setting 中的 Other Link Flag 中添加 `-ObjC` 和 `-Wl -ld_classic`。 - -- 在 Build Setting 中的 Always Embed Swift Standard Libraries 设置为 YES,即始终引入 Swift 标准库,避免 App 启动时报错「无法找到 Swift 标准库之类」。如果项目中找不到,可以建立一个空 Swift 文件,Xcode 会自动建立桥接关系。 - -- 在 Build Setting 中的 Swift Compiler - Language/Swift Language Version 选择 Swift 5。 - - - - - - -:::info -如果你曾使用 `<3.6.3` 的 TapSDK 的数据分析功能,且初始化时区域选择为国际(`IO`),那么升级 SDK 至 `>=3.6.3` 的 TapSDK 前需要先迁移数据。 -请提交工单联系我们迁移数据后再升级。 -::: - -## 初始化 SDK - -在 [TapSDK 初始化](/v4/sdk/access/quickstart#初始化) 时可以配置上报事件所携带的游戏包属性及当前设备的信息,具体使用方式如下: - - - -<> - -```cs -using TapSDK.Core; - -// 核心配置 -TapTapSdkOptions coreOptions = new TapTapSdkOptions -{ -// 客户端 ID,开发者后台获取 -clientId = clientId, -// 客户端令牌,开发者后台获取 -clientToken = clientToken, -// 地区,CN 为国内,Overseas 为海外 -region = TapTapRegionType.CN, -// 渠道,如 AppStore、GooglePlay -channel = "AppStore", -// 语言,默认为 Auto,默认情况下,国内为 zh_Hans,海外为 en -preferredLanguage = TapTapLanguageType.zh_Hans, -// 游戏版本号,如果不传则默认读取应用的版本号 -gameVersion = "1.1.0", -// 初始化时传入的自定义参数,会在初始化时上报到 device_login 事件 -propertiesJson = "{\"device_login_custom_key\": \"这是初始化的时候传入的数据,会上报到 device_login 事件\"}", -// CAID,仅国内 iOS -caid = "000-000-0000-00000", -// 是否能够覆盖内置参数,默认为 false -overrideBuiltInParameters = false, -// 是否开启广告商 ID 收集,默认为 false -enableAdvertiserIDCollection = true, -// 是否开启自动上报 IAP 事件 -enableAutoIAPEvent = true, -// OAID证书, 仅 Android,用于上报 OAID 仅 [TapTapRegion.CN] 生效 -oaidCert = null, -// 是否开启日志,Release 版本请设置为 false -enableLog = true -}; -// TapSDK 初始化 -TapTapSDK.Init(coreOptions); - - -// 如果有其他TapTap模块配置可以一起初始化配置, 请使用如下API -// 其他模块配置项 -TapTapSdkBaseOptions[] otherOptions = new TapTapSdkBaseOptions[] -{ -achievementOptions, -// ... -}; -TapTapSDK.Init(coreOptions, otherOptions); - -``` - -| 字段 | 可为空 | 说明 | -|------------------------------|-----|----------------------------------------------------| -| clientId | 否 | 客户端 ID,开发者后台获取 | -| clientToken | 否 | 客户端令牌,开发者后台获取 | -| region | 否 | 地区,CN 为国内,Overseas 为海外 | -| preferredLanguage | 是 | 默认为 TapTapLanguageType.Auto, 语言 | -| enableLog | 是 | 默认为 false, 是否开启日志,Release 版本请设置为 false | -| channel | 是 | 渠道,如 AppStore、GooglePlay | -| gameVersion | 是 | 游戏版本号,如果不传则默认读取应用的版本号 | -| propertiesJson | 是 | 初始化时传入的自定义参数,会在初始化时上报到 device_login 事件 | -| caid | 是 | CAID,仅国内 iOS | -| overrideBuiltInParameters | 是 | 是否能够覆盖内置参数,默认为 false | -| enableAdvertiserIDCollection | 是 | 是否开启广告商 ID 收集,仅 iOS ,默认为 false,开启时需添加[工程配置](#idfaios) | -| enableAutoIAPEvent | 是 | 是否开启自动上报 IAP 事件 | -| oaidCert | 是 | OAID证书, 仅 Android,用于上报 OAID 仅 [TapTapRegion.CN] 生效 | - - - - -<> - -`TapTapSdkOptions` 详细参数见 [入门指南#快速开始](/v4/sdk/access/quickstart/#初始化) - -```kotlin -import com.taptap.sdk.core.TapTapSdk -import com.taptap.sdk.core.TapTapSdkOptions -import com.taptap.sdk.core.TapTapRegion -import com.taptap.sdk.core.TapTapLanguage - -TapTapSdk.init( - context = context, - sdkOptions = TapTapSdkOptions( - clientId = clientId, - clientToken = clientToken, - region = TapTapRegion.CN, - preferredLanguage = TapTapLanguage.ZH_HANS, - enableLog = false - ) -) -``` - - - -<> - - - -```swift -import TapTapCoreSDK - -let options = TapTapSdkOptions() - -// TODO: 应用信息配置 - -// 数据分析相关属性配置 -options.channel = "channel name" // 分包渠道名称 -options.gameVersion = "gameVersion" // 游戏版本(默认会读取主包 info.plist 中的版本号) -options.caid = "caid" // 国内 iOS CAID -options.overrideBuiltInParameters = true // 自定义字段是否能覆盖内置字段,默认 false -options.enableAdvertiserIDCollection = false // 是否可以获取 IDFA,默认值为 false -options.enableAutoIAPEvent = false // 是否自动上报苹果内购支付成功事件 -// 自定义属性配置 -let properties = ["custom_key": "value"] -options.properties = properties // 自定义属性,启动首个预置事件(device_login)会携带该属性 - -// 初始化 SDK -TapTapSDK.initWith(options) - -``` -**注意**:启用 IDFA 时需添加[工程配置](#idfaios) - - - - - -## 设置账号 - -### 设置账号 ID - -调用该 API 记录一个账号,当账号登录时调用。 - -调用后会上报一个账号登录( `user_login` )事件,并将这个设备的是否有用户注册过( `has_user` )属性置为 `true`。 - -在重启应用或调用清除账号 ID( `clearUser` )前,上报的事件都会带有该账号 ID。 - - - -<> - -```cs -using TapSDK.Core - -// 自定义属性 -var dict = new Dictionary(); -dict.Add(key, value); -dict.Add(key2, value2); -string properties = dict.toJson(); - -// 设置用户 ID 及账号登录事件属性 -TapTapEvent.SetUserID(userId, properties); -``` - -| 字段 | 可为空 | 说明 | -|------------|-----|---------------------------------------------------------------------------------------| -| userId | 否 | 账号的唯一字符串,字符串长度不大于 256,只能包含数字、大小写字母、下划线(`_`)、短横(`-`)
    开发者需要保证不同账号的 `userId` 均不相同。 | -| properties | 是 | 账号登录( `user_login` )的事件属性 | - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.setUserId(userId = "userId", properties = properties) -``` - - -<> - -```swift -import TapTapCoreSDK - -// 设置用户 ID -TapTapEvent.setUserID(userId) - -// 设置用户 ID 及账号登录事件属性 -var properties = [String: Any]() -properties["currentPoints"] = 10 -TapTapEvent.setUserID(userId, properties: properties) -``` - -| 字段 | 可为空 | 说明 | -|------------|-----|--------------------------------------------------------------------------------------| -| userId | 否 | 账号的唯一字符串,字符串长度不大于 256,只能包含数字、大小写字母、下划线(`_`)、短横(`-`)
    开发者需要保证不同账号的 `userId` 均不相同 | -| properties | 是 | 账号登录( `user_login` )的事件属性 - - - -
    - -### 清除账号 ID - -当用户进行登出时,可调用 `clearUser` 清除当前 SDK 中保存的账号 ID,后续上报的事件将不会带有账号 ID,调用该接口不会上报任何事件。 - - - -<> - -```cs -using TapSDK.Core - -TapTapEvent.ClearUser(); -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.clearUser() -``` - - - -<> - -```swift -TapTapEvent.clearUser() -``` - - - - - -## 上报充值记录 - -在用户进行充值后,可调用该接口上报充值信息,调用后将上报 `charge` 事件,并将传入的参数作为事件的属性。 - - - -<> - -```cs -using TapSDK.Core - -TapTapEvent.LogChargeEvent( - orderID: orderID, - productName: productName, - amount: amount, - currencyType: currencyType, - paymentMethod: paymentMethod, - properties: properties -); -``` - -| 字段 | 可为空 | 说明 | -|---------------|-----|----------------------------------------------| -| orderID | 否 | 订单 ID | -| productName | 否 | 产品名称 | -| amount | 否 | 充值金额(单位分,即无论什么币种,都需要乘以 100) | -| currencyType | 否 | 货币类型,遵循 ISO 4217 标准。参考:人民币 CNY,美元 USD;欧元 EUR | -| paymentMethod | 否 | 支付方式,如:支付宝 | -| properties | 否 | 充值( charge )的事件属性 | - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent -import com.taptap.sdk.core.TapTapPurchasedEvent - -TapTapEvent.logPurchasedEvent( - purchasedEvent = TapTapPurchasedEvent( - orderId = orderId, - productName = productName, - amount = amount, - currencyType = currencyType, - paymentMethod = paymentMethod, - properties = properties - ) -) -``` - - - -<> - -```swift -let orderId = "订单 ID" -// 定义充值事件属性 -var properties = [String: Any]() -properties["on_sell"] = true -// 上报充值事件 -TapTapEvent.logPurchasedEvent(orderId, productName:"商品名", amount: 100, currencyType: "CNY", paymentMethod:"支付宝", properties: properties) -``` - - 参数 | 可为空 | 说明 ---------------|-----|---------------------------------------------- - orderId | 否 | 订单 ID - product | 是 | 产品名称 - amount | 否 | 充值金额(单位分,即无论什么币种,都需要乘以 100) - currencyType | 是 | 货币类型,遵循 ISO 4217 标准。参考:人民币 CNY,美元 USD;欧元 EUR - payment | 是 | 支付方式,如:支付宝 - properties | 是 | 充值( `charge` )的事件属性 - - - - - - -**注意:在条件允许的情况下推荐使用服务端充值统计接口,请参考 [服务端接入文档](/sdk/tapdb/sdk/server-side-integration#2.2.上报充值记录)** - -## 自定义事件 - -### 上报事件 - -在 SDK 初始化完成后可使用该接口上报事件 - - - -<> - -```cs -using TapSDK.Core - -var dict = new Dictionary -{ - { key, value }, - { key2, value2 } -}; -string properties = dict.toJson(); -TapTapEvent.LogEvent(name, properties); -``` - -| 字段 | 可为空 | 说明 | -|------------|-----|------------------| -| name | 否 | 事件的名称 | -| properties | 否 | 事件的属性 JSONString | - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.logEvent(name = title,properties = properties) -``` - - - -<> - -```swift -// 定义事件属性 -var properties = [String: Any]() -properties["#weapon"] = "axe" -properties["#level"] = 10 -// 发送自定义事件 -TapTapEvent.logEvent(eventName, properties: properties) -``` - - 参数 | 可为空 | 说明 -------------|-----|------- - eventName | 否 | 事件的名称 - properties | 是 | 事件的属性 - - - - - -**注意:** - -* 事件名支持上报预置事件和自定义事件,其中自定义事件应以 `#` 开头 -* 事件属性的 key 值为属性的名称,支持 NSString 类型 -* 事件属性的 value 值为属性的名称,支持 NSString(最大长度 `256` )、NSNumber(取值区间为 `[-9E15, 9E15]` )类型 -* 事件属性支持上报预置属性和自定属性,其中自定义属性应以 `#` 开头 -* 事件属性传入预置属性时,SDK 默认采集的预置属性将不被覆盖, 这个可以再初始化时候修改配置 `overrideBuiltInParameters` 控制。 - -### 设置通用事件属性 - -对于一些所有事件都要携带的属性,建议使用通用事件属性实现。 - -#### 添加静态通用事件属性 - - - -<> - -```cs -using TapSDK.Core - -// 添加单个属性 -var key = kv["key"]; -var value = kv["value"]; -TapTapEvent.AddCommonProperty(key, value); - -// 添加多个属性 -var key = kv["key"]; -var value = kv["value"]; -var key2 = kv["key2"]; -var value2 = kv["value2"]; -var dict = new Dictionary -{ - { key, value }, - { key2, value2 } -}; -string properties = dict.toJson(); -TapTapEvent.AddCommon(properties); -``` - -| 字段 | 可为空 | 说明 | -|------------|-----|------------------| -| properties | 否 | 事件的属性 JSONString | - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.addCommon(properties = properties) -``` - - - -<> - -```swift -// 定义通用事件属性 -var staticProperties = [String: Any]() -// 当设置属性 "#current_channel" 为 "TapDB" 时, 后续事件上报都会在事件属性中添加了 "#current_channel"字段 -staticProperties["#current_channel"] = "TapDB" -// 设置通用事件属性 -TapTapEvent.addCommon(staticProperties) -``` - - - -<> - -```cpp -TapUEDB::RegisterStaticProperties(Properties); -``` - - 字段 | 可为空 | 说明 -------------------|-----|------------ - staticProperties | 否 | 静态通用事件属性字典 - - - - - -#### 删除部分静态通用事件属性 - - - -<> - -```cs -using TapSDK.Core - -// 删除单个属性 -TapTapEvent.ClearCommonProperty(key); - -// 删除多个属性 -var key1 = kv["key"]; -var key2 = kv["value"]; -var keysToClear = new string[] { key1, key2 }; -TapTapEvent.ClearCommonProperties(keysToClear); -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.clearCommonProperties( - keys = arrayOf( - "key1", - "key2", - "key3" - ) -) -``` - - - -<> - -```swift -// 定义需要删除的通用事件属性名称 -let propertyNames = [propertyName1, propertyName2] -// 从通用事件属性中移除 -TapTapEvent.clearCommonProperties(propertyNames) -``` - - 参数 | 可为空 | 说明 ----------------|-----|------------ - propertyNames | 否 | 静态通用属性名称数组 - - - - - -#### 清空全部静态通用属性 - - - -<> - -```cs -using TapSDK.Core - -TapTapEvent.ClearAllCommonProperties(); -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.clearAllCommonProperties() -``` - - - -<> - -```objectivec -TapTapEvent.clearAllCommonProperties() -``` - - - - - - -#### 注册动态通用事件属性 - -对于可能随时发生变化的通用事件属性,可以注册动态通用事件属性回调,该回调会在每次调用时被触发,将计算好的属性添加到本次上报事件属性中。 - - - -<> - -```cs -using TapSDK.Core; - -// 后续上报的事件都将携带 #currentTime 属性,值为变量 currentTime 在事件上报时刻的值 -TapTapEvent.RegisterDynamicProperties(() => -{ - string currentTime = DateTime.Now.ToString("o"); - Debug.Log("RegisterDynamicProperties currentTime" + currentTime); - return $"{{ \"currentTime\": \"{currentTime}\" }}"; -}); -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.registerDynamicProperties( - dynamic = object : TapTapEvent.TapEventDynamicProperties { - override fun getDynamicProperties(): JSONObject { - // return JSONObject - } - } -) -``` - - - -<> - -```swift -// 后续上报的事件都将携带 #currentLevel 属性,值为变量 level 在事件上报时刻的值 -TapTapEvent.registerDynamicProperties { - return ["#currentLevel": level] -} -``` - - - -```cpp -TapUEDB::RegisterDynamicProperties(PropertiesBlock); -``` - - 参数 | 可为空 | 说明 --------------------|-----|-------------- - dynamicProperties | 否 | 动态通用事件属性计算回调 - - - - - -**注意:** - -在上报事件或通用属性中使用相同属性名会出现属性覆盖的现象,属性覆盖的优先级从高到低依次为:事件属性、动态通用事件属性、静态通用事件属性、预置属性(例如 `trackEvent` -中设置的事件属性将覆盖动态通用事件属性、静态通用事件属性、预置属性中的同名属性) - -## 修改用户属性 - -TapDB 支持两种用户主体:设备和账号,你可以通过如下接口对这两种用户的属性进行操作。 - -### 修改设备属性 - -#### 设备属性初始化 - -对于需要保证只有首次设置时有效的属性,可以使用该接口进行赋值操作,仅当前值为空时赋值操作才会生效,如当前值不为空,则赋值操作会被忽略。 - - - -<> - -```cs -using TapSDK.Core; - -string properties = "{\"firstActiveServer\":\"server1\"}"; -TapTapEvent.DeviceInitialize(properties); -// 此时设备表的 "#firstActiveServer" 字段值为 "server1" - -string properties2 = "{\"firstActiveServer\":\"server2\"}"; -TapTapEvent.DeviceInitialize(properties2); -// 此时设备表的 "#firstActiveServer" 字段值还是为 "server1" -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.deviceInitialize(properties = properties) -``` - - - -<> - -```swift -var properties = [String: Any]() -properties["firstActiveServer"] = "server1" -// 该方法执行后设备表的 "#firstActiveServer" 字段值为 "server1" -TapTapEvent.deviceInitialize(properties) -// 多次设置同一属性时,后续执行不会生效,例如如下调用不会更新 "firstActiveServer" 的值 -// TapTapEvent.deviceInitialize(["firstActiveServer":"server2"]) -``` - - 参数 | 可为空 | 说明 -------------|-----|------ - properties | 否 | 属性字典 - - - - - -#### 设备属性更新 - -对于常规的设备属性,可使用该接口进行赋值操作,新的属性值将会直接覆盖旧的属性值。 - - - -<> - -```cs - -using TapSDK.Core; - -string properties = "{\"currentPoints\":10}"; -TapTapEvent.DeviceUpdate(properties); -// 此时设备表的 "currentPoints" 字段值为 10 - -properties = "{\"currentPoints\":42}"; -TapTapEvent.DeviceUpdate(properties); -// 此时设备表的 "currentPoints" 字段值为 42 -``` - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.deviceUpdate(properties = properties) -``` - - -<> - -```swift -// 定义要更新的属性 -var properties = [String: Any]() -properties["currentPoints"] = 10 -// 该方法执行后,此时设备表的 "currentPoints" 字段值为 10 -TapTapEvent.deviceUpdate(properties) - -// 多次执行时会更新属性,例如执行以下方法时,此时设备表的 "currentPoints" 字段值为 42 -// TapTapEvent.deviceUpdate(["currentPoints":42]) - -``` - - 参数 | 可为空 | 说明 -------------|-----|------ - properties | 否 | 属性字典 - - - - - -#### 设备属性累加 - -对于数值类型的属性,可以使用该接口进行累加操作,调用后 TapDB 将对原属性值进行累加后保存结果值 - - - -<> - -```cs -using TapSDK.Core; - -string properties = "{\"totalPoints\":10}"; -TapTapEvent.DeviceAdd(properties); -// 此时设备表的 "totalPoints" 字段值为 10 - -properties = "{\"totalPoints\":-2}"; -TapTapEvent.DeviceAdd(properties); -// 此时设备表的 "totalPoints" 字段值为 8 -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.deviceAdd(properties = properties) -``` - - - -<> - -```swift -// 定义要累加的属性 -var properties =[String: Any]() -properties["totalPoints"] = 10 - -// 执行该方法后,此时设备表的 "totalPoints" 字段值为 10 -TapTapEvent.deviceAdd(properties) - -// 执行该方法后,此时设备表的 "totalPoints" 字段值为 8 -TapTapEvent.deviceAdd(["totalPoints":-2]) - -``` - - 参数 | 可为空 | 说明 -------------|-----|-------------------- - properties | 否 | 属性字典,value 仅支持数字类型 - - - - - -上述代码示例中,属性值为整数。 -累加操作也支持浮点数,不过浮点数相加有精度问题,开发者还需留意。 - -### 修改账号属性 - -#### 账号属性初始化 - -使用方法同设备属性初始化操作 - - - -<> - -```cs -using TapSDK.Core; - -string properties = "{\"firstActive\":\"active\"}"; -TapTapEvent.UserInitialize(properties); -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.userInitialize(properties = properties) -``` - - - -<> - -```swift -var properties = [String: Any]() -properties["firstActiveServer"] = "server1" -TapTapEvent.userInitialize(properties) -``` - - - - - -#### 账号属性更新 - -使用方法同设备属性更新操作 - - - -<> - -```cs -using TapSDK.Core; - -string properties = "{\"firstActive\":\"activeNew\"}"; -TapTapEvent.UserUpdate(properties); -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.userUpdate(properties = properties) -``` - - - -<> - -```swift -TapTapEvent.userUpdate(["firstActiveServer":"server2"]) -``` - - - - - -#### 账号属性累加 - -使用方法同设备属性累加操作 - - - -<> - -```cs -using TapSDK.Core; - -string properties = "{\"conut\":1}"; -TapTapEvent.UserAdd(properties); -``` - - - -<> - -```kotlin -import com.taptap.sdk.core.TapTapEvent - -TapTapEvent.userAdd(properties = properties) -``` - - - -<> - -```swift -TapTapEvent.userAdd(["totalPoints": 10]) -``` - - - - - -## 收集设备指纹 - -允许 SDK 采集设备指纹用于辅助数据分析、广告归因,将使统计结果更加精确, - -:::info -请在权限申请、设置 IDFA 开关等操作结束后初始化 SDK,以保证设备指纹能够正常上报。 -::: - -### OAID(Android) - -> 注意:SDK 版本 3.28.2 及以上支持 OAID 版本为 1.0.5 ~ 2.4.0; 3.15.0 ~ 3.28.0 支持 OAID 版本为 1.0.5 ~ 2.1.0; 3.14.0 及以下支持 OAID 版本为 1.0.5 ~ -> 1.0.25 - - -TapDB SDK 在应用接入 OAID 第三方库时,会在发送相关事件中携带该参数(key 为 `device_id4`)。现支持该第三方库版本为 1.0.5 ~ 2.4.0,因不同版本变更较大,所以针对不同版本接入的说明如下: - -对于 1.0.5 ~ 1.0.25 不需要额外配置,只需应用添加对应第三方库的依赖即可。 - -对于 1.0.26 ~ 2.4.0 除添加对应第三方库外,需要添加如下处理: - -#### 1. 设置证书信息及配置文件 - -证书信息为应用通过 移动安全联盟邮箱 申请的 `.cert.pem` 文件内容, 该文件与包名对应。现支持两种设置方式: - -1. 将 `cert.pem` 文件拷贝到应用 `assets` 目录,并注意该文件名应设置为 `packageName.cert.pem` , `packageName` 为当前应用包名。 -2. 通过 SDK 的接口 `setOAIDCert` 将证书文件的内容进行设置 - -以上两种方式选择一种即可,当两种同时使用时,优先使用通过接口设置的证书信息。 - -配置文件为 `supplierconfig.json`, 应用需要将内部 appid 对应的内容修改为应用在对应应用市场的应用 ID,其他部分不需要修改,并将修改后的文件拷贝到 `assets` -目录下。 - -#### 2. 在应用工程中加载对应库文件 - -不同版本 OAID 第三方库对应的库文件名称如下: - -| 版本号 | 库名称 | -|----------------|--------------------------| -| 1.0.30 ~ 2.4.0 | msaoaidsec | -| 1.0.29 | nllvm1632808251147706677 | -| 1.0.27 | nllvm1630571663641560568 | -| 1.0.26 | nllvm1623827671 | - -在 Android 项目工程自定义 `Application` 类的 `onCreate` 方法中添加加载第三方库代码,例如当应用接入的 OAID 版本为 1.2.1 时如下: - -```java -System.loadLibrary("msaoaidsec"); -``` - -#### 常见问题处理 - -当项目中已引入 OAID 库但上报时仍未发现设备 OAID 信息时,请检查以下几项 - -1. 设备时间是否正常 -2. 对于 1.0.26 及以上版本证书所对应的包名是否和当前包名对应 -3. 对于 1.0.26 及以上版本是否加载了其库文件以及库文件名称是否和版本对应 -4. 应用在 Android 12 报错 `java.lang.UnsatisfiedLinkError`且 应用 minSdkVersion 大于等于 23 ,建议在 AndroidManifest.xml 文件 application - 标签中添加 `android:extractNativeLibs="true"` - -### IMEI(Android) - -在 `AndroidManifest.xml` 增加如下条目,且用户同意权限的申请后,SDK 将自动采集 Android IMEI。 - - - -<> - -```xml - - -``` - - - -<> - -```xml - - -``` - - - -<> - -```xml -iOS 平台不适用 -``` - - - - - -### IDFA(iOS) - -由于 `iOS14.5` 以上系统,获取 IDFA 需要弹出窗口有用户确认,故 SDK 默认不获取 IDFA,可以调用接口开启 IDFA 获取。 - - - -<> - -请确保 `info.plist` 中添加了权限请求描述文字,SDK 在初始化时将自动弹出权限请求窗口。 - -```xml -NSUserTrackingUsageDescription -此标识符将用于向您推荐个性化广告(或其他描述) -``` - -在初始化时设置 `TapTapSdkOptions` 中的 `enableAdvertiserIDCollection` 为 `true` 开启 IDFA 采集开关。 - - - -<> - -```java -// Android 平台不适用 -``` - - - -<> - -请确保 `info.plist` 中添加了权限请求描述文字,SDK 在初始化时将自动弹出权限请求窗口。 - -```xml -NSUserTrackingUsageDescription -此标识符将用于向您推荐个性化广告(或其他描述) -``` - -在初始化时设置 `TapTapSdkOptions` 中的 `enableAdvertiserIDCollection` 为 `true` 开启 IDFA 采集开关。 - - - -<> - -iOS 独占方法 - -```cpp -TapUEDB::AdvertiserIDCollectionEnabled(true); -``` - - - - diff --git a/versioned_docs/version-v4/sdk/tapdb/sdk/data-spec.mdx b/versioned_docs/version-v4/sdk/tapdb/sdk/data-spec.mdx deleted file mode 100644 index 0a6840949..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/sdk/data-spec.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: 数据规范 -sidebar_position: 2 ---- - -## 设备 - -### 设备 ID (device_id) - -SDK 在初始化时,将为该终端生成唯一的 ID,我们称之为设备 ID。 - -#### 生成规则 - -| SDK 类型 | 生成规则 | -| --- | --- | -| Android | 依次尝试获取本地存储保存过的设备 ID、Android ID,如果都无法获取到正确结果则随机生成 UUID,最后获取或生成的设备 ID 会保存于本地存储 | -| iOS | 尝试获取本地钥匙串保存过的设备 ID,获取失败则随机生成 UUID,最后获取或生成的设备 ID 会保存于本地 | - -#### 设备属性 - -设备属性表示设备的不变的属性以及最新状态。可以按照设备的某个属性或特征与事件进行关联,来分析这些用户的行为。 - -#### 常见问题 - -**设备 ID 会发生变化么?** - -Android:在能获取到 Android ID 的情况下,设备 ID 是比较稳定的(参考 [Google 唯一标识符最佳做法](https://developer.android.com/training/articles/user-data-ids))。如果无法获取到 Android ID,设备 ID 将有随着重装应用或重置 Google 广告 ID 而改变的可能。 - -iOS:获取或生成好的设备 ID 保存于设备的钥匙串中,可以保证在不重置系统的情况下,保持设备 ID 的稳定。 - -## 账号 - -### 账号 ID (user_id) - -通常情况下是你的注册用户 ID,当用户在你的系统中发生注册或登录行为后,你可以将该用户在你的系统中的唯一 ID 直接或加密后设置到 SDK 中,这个 ID 会被作为今后用户在各个平台使用你的产品的身份识别 ID。 - -#### 账号属性 - -账号属性表示账号的不变的属性以及最新状态。可以按照账号的某个属性或特征与事件进行关联,来分析这些用户的行为。 - -## 事件 - -[什么是事件](/sdk/tapdb/sdk/user-event-model) - -### 预置事件 - -| 事件名 | 名称 | 说明 | -| --- | --- | --- | -| device_login | App 启动 | 调用 SDK 初始化接口时会上报此事件,首次上报一个设备 ID 时将在设备表产生一条记录 | -| user_login | 账号登录 | 调用 SDK SetUser 接口时会上报此事件,首次上报一个账号 ID 时将在账号表产生一条记录 | -| play_game | 游玩时长 | SDK 会以应用进入前台作为计时起点,置于后台时,上报此时间段的时长 | -| charge | 用户付费 | 调用 SDK Charge 接口时会上报此事件,通常情况下建议使用服务端 REST API 进行上报 | - -### 衍生事件 - -在上报预置事件时,TapDB 也会同时记录一些特殊事件,这类特殊的事件我们称之为衍生事件。衍生事件无法通过 API 直接上报,只会由预置事件上报后触发。 - -| 事件名 | 名称 | 说明 | -| --- | --- | --- | -| dau_device | App 当日首次次启动 | App 在每日首次上报 `device_login` 时触发,可用于快速查询设备 DAU | -| dvau_device | App 当日首次启动(按版本)| App 的不同版本在每日首次上报 `device_login` 时触发,可用于快速查询分版本的设备 DAU | -| wau_device | App 当周首次启动 | App 在每周首次上报 `device_login` 时触发,可用于快速查询设备 WAU | -| mau_device | App 当月首次启动 | App 在每月首次上报 `device_login` 时触发,可用于快速查询设备 MAU | -| dau_user | 账号当日首次登录 | 账号每日首次上报 `user_login` 时触发,可用于快速查询账号 DAU | -| wau_user | 账号当周首次登录 | 账号每周首次上报 `user_login` 时触发,可用于快速查询账号 WAU | -| mau_user | 账号当月首次登录 | 账号每月首次上报 `user_login` 时触发,可用于快速查询账号 MAU | - -### 自定义事件 - -除了预置事件和衍生事件外,也可以在事件管理中建立更多自定义事件。 - -## 数据规则 - -TapDB 的 REST API 支持传入数据格式为 URLEncode 后的 JSON 对象, -如果你直接使用 TapDB 的 REST API 则需要按照此格式进行上报。 -如果你使用 SDK 接入,数据也会转化成该格式进行上报。 - -### 事件数据 - -记录一个事件及其属性 - -```js -{ - ["index" | "client_id"]: ["APPID" | "ClientID"], - "device_id": "DeviceID", - "user_id": "UserID", - "type": "track", - "name": "EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue", - "event_uuid": "550e8400-e29b-41d4-a716-446655440000" - } -} -``` - -#### 系统字段 - -properties 同层级的字段为系统字段 - -| 名称 | 数据类型 | 说明 | -| --- | --- | --- | -| index | string | 项目的 APPID,在 TapDB 后台可以查看该 ID | -| client_id | string | 项目的 TapTap Client ID,在 TapTap 开发者中心可以查看该 ID | -| type | string | 数据类型,上报事件时传入 track | -| device_id | string | 事件发生时的设备 ID | -| user_id | string | 事件发生时的账号 ID | -| name | string | 事件名,可传入预置事件或自定义事件 | - -注意: - -- 预置事件 `device_login` 必须传入 `device_id` -- 预置事件 `user_login`、`play_game` 必须传入 `device_id`和 `user_id` -- 预置事件 `charge` 必须传入有效的 `user_id`(即上报过 `user_login` 的 `user_id`) - -#### 属性字段 - -properties 内的字段为属性字段,这些传入的字段会被当做事件的属性,你可以通过自定义属性进行扩展。 - -其中 SDK 预置事件属性: - -| 名称 | 类型 | 说明 | -| --- | --- | --- | -| os | string | 操作系统(支持传入 Android、iOS、Windows、Mac)| -| device_id1 | string | 预留设备 ID 槽位(iOS SDK 传入 IDFA,Android SDK 传入 IMEI)| -| device_id2 | string | 预留设备 ID 槽位(Android SDK 传入 Google 广告 ID)| -| device_id3 | string | 预留设备 ID 槽位(Android SDK 传入 Android ID)| -| device_id4 | string | 预留设备 ID 槽位(Android SDK 传入 OAID)| -| width | number | 屏幕宽度 | -| height | number | 屏幕高度 | -| device_model | string | 设备型号(设备厂商+设备型号)| -| os_version | string | 操作系统版本 | -| network | string | 网络类型(WiFi 传入 2、未知传入 3、2G 传入 4、3G 传入 5、4G 传入 6)| -| channel | string | 分包渠道 | -| app_version | string | App 版本 | -| sdk_version | string | SDK 版本 | -| event_uuid | string | 每条日志的唯一 ID,通常传入 UUID | - -### 属性操作 - -账号属性操作 - -```js -{ - ["index" | "client_id"]: ["APPID" | "ClientID"], - "user_id": "UserID", - "type": ["initialise" | "update" | "add"], - "properties": { - "level": 15, - "#custom": "custom" - } -} -``` - -设备属性操作 - -```js -{ - ["index" | "client_id"]: ["APPID" | "ClientID"], - "device_id": "DeviceID", - "type": ["initialise" | "update" | "add"], - "properties": { - "level": 15, - "#custom": "custom" - } -} -``` - -#### 系统字段 - -properties 同层级的字段为系统字段 - -| 名称 | 数据类型 | 说明 | -| --- | --- | --- | -| client_id | string | 项目的 TapTap Client ID,在 TapTap 开发者中心可以查看该 ID | -| type | string | 数据类型,属性操作支持 `initialise`、`update`、`add` | -| device_id | string | 进行属性操作的设备 ID | -| user_id | string | 进行属性操作的账号 ID | - -type 字段传入值的含义 - -| 名称 | 说明 | -| --- | --- | -| initialise | 对于需要保证只有首次设置时有效的属性,可以使用 `initialise` 进行赋值操作,仅当前值为空时赋值操作才会生效,如当前值不为空,则赋值操作会被忽略 | -| update | 对于常规的设备属性,可使用该接口进行赋值操作,新的属性值将会直接覆盖旧的属性值 | -| add | 对于数值类型的属性,可以使用该接口进行累加操作,调用后 TapDB 将对原属性值进行累加后保存结果值 | - -注意: - -- 进行属性操作时,`device_id` 和 `user_id` 只能二选一传入。传入 `device_id` 则对该设备 ID 的属性进行操作,传入 `user_id` 则对该账号 ID 的属性进行操作 - -#### 属性字段 - -`properties` 内的字段为属性字段,这些字段将被视为要操作的字段,按照 `type` 的值进行处理。 diff --git a/versioned_docs/version-v4/sdk/tapdb/sdk/server-side-integration.mdx b/versioned_docs/version-v4/sdk/tapdb/sdk/server-side-integration.mdx deleted file mode 100644 index 929cfb797..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/sdk/server-side-integration.mdx +++ /dev/null @@ -1,254 +0,0 @@ ---- -title: 服务端接入 -sidebar_label: 服务端接入 -sidebar_position: 4 ---- - -import {Conditional} from '/src/docComponents/conditional'; - -你可以直接使用 REST API 进行接入,可以在不依赖 SDK 的情况下直接将数据上报到 TapDB。 - -## 上报事件和属性 - -数据传输的格式和含义请参考 [数据规则](/sdk/tapdb/sdk/data-spec#数据规则)。 - -如果返回 Response Code 为 200,则代表数据上报成功,请在埋点管理中进一步查看事件的写入情况。 - -### 单条上报 - - - -`POST` `https://e.tapdb.net/v2/event` - - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/event` - - - -`Content-Type: application/json` - -请求内容示例: -```json -{ - "client_id": "test_appid", - "device_id": "test_device_id", - "user_id": "test_user_id", - "type": "track", - "name": "#EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue", - "event_uuid": "7656c71f-6d73-488e-b740-3ab370c6f3db" - } -} -``` - -### 批量上报 - - - -`POST` `https://e.tapdb.net/v2/batch` - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/batch` - - - -`Content-Type: application/json` - -请求内容示例: -```json -{ - "data": [ - { - "client_id": "test_appid", - "device_id": "test_device_id", - "user_id": "test_user_id", - "type": "track", - "name": "#EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue", - "event_uuid": "7656c71f-6d73-488e-b740-3ab370c6f3db" - } - }, - { - "client_id": "test_appid", - "device_id": "test_device_id", - "user_id": "test_user_id", - "type": "track", - "name": "#EventName", - "properties": { - "os": "Android", - "device_id1": "000", - "device_id2": "000", - "device_id3": "000", - "device_id4": "000", - "width": 256, - "height": 768, - "device_model": "pixel", - "os_version": "Android 10.0", - "provider": "O2", - "network": "1", - "channel": "Google Play", - "app_version": "1.0", - "sdk_version": "2.8.0", - "#custem_event_property_name": "CustomEventPropertyValue", - "event_uuid": "1c11f92e-6f18-417d-8aed-ffbe668a9feb" - } - } - ] -} - -``` - - - -### 常见问题 - -- 若当前事件的主体并非设备或账号,device_id 和 user_id 可以传入任意一个固定值 -- 为了保证服务端上报的事件也能使用设备维度进行分析,建议在客户端调用 SDK 的 `GetDeviceID` 接口取得 SDK 为该设备生产的唯一 ID 并上报到 App 的服务端 - -## 特殊类型事件上报 - -### 在线人数 - -由于 SDK 无法推送准确的在线数据,这里提供服务端在线数据推送接口。游戏服务端可以以小于 5 分钟的间隔自行统计在线人数(间隔 1 分钟为最佳),通过接口推送到 TapDB。TapDB 进行数据汇总展现。 - -*注意:在线人数使用 json 格式上报,这与其他通用事件上报的格式有差别,请注意区分。* - - - -`POST` `https://se.tapdb.net/tapdb/online` - - - - - -`POST` `https://se.tapdb.ap-sg.tapapis.com/tapdb/online` - - - -`Content-Type: application/json` - -请求内容: - -| 参数 | 参数类型 | 参数说明 | -| --- | --- | --- | -| client_id | string | 游戏的 ClientID | -| onlines | array | 多条在线数据(最多 100 条) | - -其中 onlines 数组的结构为 - -| 参数 | 参数类型 | 参数说明 | -| --- | --- | --- | -| server | string | 服务器。TapDB 对同一服务器每一个自然 5 分钟仅接受一次数据 | -| online | number | 在线人数 | -| timestamp | number | 当前统计数据的时间戳(秒)。TapDB 会按照自然 5 分钟进行数据对齐 | - -示例: - -```json -{ - "client_id":"ClientID", - "onlines":[{ - "server":"s1", - "online":123, - "timestamp":1489739590 - },{ - "server":"s2", - "online":188, - "timestamp":1489739560 - }] -} -``` - - -#### 常见问题 - -*Q:统计数据如何按 5 分钟对齐* - -A:在线人数图表展示的横坐标每个点间隔五分钟,分别是每个小时的 0、5、10 至 55 分,实际数据统计周期为该时间点前后 2.5 分钟。 -以 10 点 05 分为例,统计的是 10 点 02 分 30 秒至 10 点 07 分 30 秒这个时间范围(指上报数据内容的 timestamp 字段,而非实际请求接口的时间)内,各 server 在线人数的总和。 - -*Q:上报的数据需要多久后可以被显示* - -A:当一个统计周期结束后,该统计周期的数据将被展示。以 10 点 05 分为例,统计周期为 10 点 02 分 30 秒至 10 点 07 分 30 秒,这段时间内的数据将在 10 点 07 分 30 秒后被展示。 - -*Q:在线人数曲线有缺口是什么原因* - -A:请检查上报数据的频率是否低于 5 分钟一次。为了保证图表的展示的曲线平滑,可直接将上报频率调整为每分钟 1 次,并在上报失败时进行重试,直至上报成功。 - - -### 充值记录 - -由于 SDK 推送可能会不准确,建议直接使用服务端充值推送接口。注意需要停止客户端 SDK 中充值信息的上报,防止重复统计。 - - - -`POST` `https://e.tapdb.net/v2/event` - - - - - -`POST` `https://e.tapdb.ap-sg.tapapis.com/v2/event` - - - -`Content-Type: application/json` - -```json -{ - "name": "charge", // 事件名,固定为 charge - "client_id": "ClientID", // 必需。注意 ClientID 需要被替换成游戏的 ClientID - "user_id": "userId", // 必需。用户 ID。必须和 SDK 的 setUser 接口传递的 userId 一样,并且该用户已经通过 SDK 接口进行过推送 - "type": "track", // 必需。数据类型,请确保传入的值为 track - "properties": { - "ip": "8.8.8.8", // 可选。充值用户的 IP - "order_id": "100000", // 可选。长度大于 0 并小于等于 256。订单 ID。 - "amount": 100, // 必需。大于 0 并小于等于 100000000000。充值金额。单位分,即无论什么币种,都需要乘以 100 - "virtual_currency_amount": 100, //获赠虚拟币数量,必传,可为 0 - "currency_type": "CNY", // 可选。货币类型。国际通行三字母表示法,为空时默认 CNY。参考:人民币 CNY,美元 USD;欧元 EUR - "product": "item1", // 可选。长度大于 0 并小于等于 256。商品名称 - "payment": "alipay" // 可选。长度大于 0 并小于等于 256。充值渠道 - } -} -``` - -*TapDB 并不会对上报的充值数据进行去重,当接口返回 200 时则代表 TapDB 已经接受到该条日志数据,请到埋点概览中查看该条数据后续的落盘情况。反复上报相同订单记录将会导致数据被重复统计* - - diff --git a/versioned_docs/version-v4/sdk/tapdb/sdk/user-event-model.mdx b/versioned_docs/version-v4/sdk/tapdb/sdk/user-event-model.mdx deleted file mode 100644 index 69823375d..000000000 --- a/versioned_docs/version-v4/sdk/tapdb/sdk/user-event-model.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: User - Event 模型 -sidebar_position: 1 ---- - -我们很清楚,用户在玩游戏的过程中,会在游戏内产生各种不同类型的行为。比如: - -- 对 RPG 游戏而言,用户在游戏内会有打怪、升级、买装备等行为事件; -- 对 PVP 游戏而言,用户在游戏内会有添加好友等行为事件; -- 对卡牌游戏而言,用户在游戏内会有购买卡牌、使用卡牌等行为事件; - -在这里,**我们把「用户」定义为 「User」,把「行为事件」定义为 「Event」**。这样我们会发现数据分析就是对 **User** 和 **Event** 两个主体的分析。 - -用户在触发各类行为事件时,事件会有 Who、When、What、Where、How 等相关信息: - -- **Who**:触发 Event 的用户。 -- **What**:Event 本身的具体内容。 -- **When**:触发 Event 的时间。 -- **Where**:触发 Event 时的 IP、国家、省、市、区等位置信息。 -- **How**:用户是具体如何触发的 Event,比如用户使用的设备类型、操作系统类型、操作系统版本号、设备品牌、设备型号、设备分辨率、游戏 App 的版本等信息。 - -**其中 What、When、Where、How 都是 Event 的属性状态**。 - -以某对战竞技类游戏为例,「用户组队参加一场对战」可以看做是一个事件名为「对战」的 Event: - -- **Who**:即参加对战的用户有哪些? -- **What**:是什么类型的对战? -- **When**:什么时候进行的对战? -- **Where**:对战发生时的 IP、国家、省、市、区等位置信息是什么? -- **How**:对战发生时的用户使用的是苹果手机还是安卓手机?系统版本号是多少?用户手机分辨率是多少? - -以上就是 **Event** 模型(也叫事件模型)及 **Event** 属性信息。**游戏里的每个待分析的行为都可以定义为 Event,也应该定义为 Event,这是做好数据分析的前提。** - -Event 信息可在 TapDB 的「配置」-「事件管理」里录入。 - -每个 **User** 代表一个用户。**User 也会有很多信息,比如注册时间、注册地址、注册渠道、用户级别、设备类型、性别、年龄等信息,这些信息都是 User 的属性状态**。通过 User 的属性,您可以在用户行为分析时快速筛选出您要分析的用户,比如您想分析付费用户的活跃情况,则只需要分析「累计付费金额」这一用户属性的值大于 0 的用户即可。 - -同样以某对战竞技类游戏为例,「用户组队参加一场对战」可以看做是「事件 event」里的 who: - -- 参加对战的用户有哪些? -- 累计玩 游戏 多长时间? -- 用户得分多少? -- 用户玩过哪些英雄? -- 用户的性别是什么? -- 用户的年龄是什么? -- 用户的注册时间是什么时候? -- 用户的注册渠道是什么? -- 用户的累计付费金额是多少? - -以上就是 User 模型。 - -**Event 模型和 User 模型合称 User - Event 模型**。 - -基于 User - Event 模型设计埋点文档并采集信息,你可以: - -- PVP 游戏:分析不同段位下,使用不同角色的参战次数; -- 卡牌游戏:分析不同 VIP 等级的玩家参与中秋节活动的参与情况; -- SLG:分析用户在进入游戏前 7 天,被其他玩家掠夺资源是如何影响留存率; -- RPG:查看和分析关键等级的通过率,以关注关键流失点; -- SLG:查看最近 7 日,不同等级区间玩家的资源积累和消耗情况,以便确定该如何投放礼包。最好能可视化呈现净流入 / 流出; diff --git a/versioned_docs/version-v4/sdk/taplink/_category_.json b/versioned_docs/version-v4/sdk/taplink/_category_.json deleted file mode 100644 index e9927a2e2..000000000 --- a/versioned_docs/version-v4/sdk/taplink/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapLink", - "collapsed": true, - "position": 10 -} diff --git a/versioned_docs/version-v4/sdk/taplink/features.mdx b/versioned_docs/version-v4/sdk/taplink/features.mdx deleted file mode 100644 index 293d00ad3..000000000 --- a/versioned_docs/version-v4/sdk/taplink/features.mdx +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: TapLink 功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import CodeBlock from '@theme/CodeBlock'; - -## 能力介绍 - -- TapLink 是一种基于 deep linking 跳转机制的礼包分发能力。 -- 如果玩家在 TapTap 客户端点击「领取礼包」时已安装了您的游戏,TapLink可立即打开并跳转游戏,并直接进行道具兑换或将用户引导至已有的礼包领取页面。 -- 如果玩家在 TapTap 客户端点击「领取礼包」时未安装您的游戏,TapLink会引导玩家停留在原页面进行礼包码复制。 - -## 工作流程 - -![](https://capacity-files.lcfile.com/8HTgi9a15nTfqPvIiFARb4v1rMFSeI9f/0048bea7-49ae-45a4-aecc-9f1a334105ee.png) - -## 产品优势 - -### 礼包领取兑换体验提升,激发玩家兴趣,提高黏性 - -相较于传统礼包码复制,兑换的领取路径,TapLink 在用户体验上做到了自助跳转,从 **TapTap→游戏**一站式领取礼包,大幅缩短用户体验路径,操作简易,减少用户流失,提升领取率。 - -### 平台流量导入游戏,助力游戏促活拉新 - -TapTap 平台礼包入口一键跳转游戏礼包入口,助力游戏日活提升的同时将 TapTap 玩家成功转化为游戏新用户。 - -### 接入流程简易,降低开发成本 - -开发者仅需通过 API 解析相关URL,即可开发完成,无需增加额外的开发工作。 - -### 落地形式灵活,游戏可根据自身需求场景自由设计兑换方式 - -游戏可根据自身需求将设计落地形式,可直接将 TapLink 所携带的礼包信息解析后,直接下发道具给玩家;也可接入预设好的礼包领取落地页。 - -## 接入方式 - -开发者需自行集成 TapLink 链接,方可使用该能力。 - -![](https://capacity-files.lcfile.com/ky6P3MASLHWh9b40SsEmlpBnElbWiqJN/taplink.png) - -TapLink 格式为:`tds{clientID}://gifts?code={code}`(注意这里 `{clientID}` 和 `{code}` 表示变量)。具体的值和示例开发者可以登录控制台查看。一个完整的 URL Scheme 协议格式由 scheme、host、port、path 和 query 组成,其结构为:`://:/?`。游戏接入 TapLink 功能只需要在移动端配置 scheme 和 host 即可。需要注意的是 scheme 是大小写敏感的,配置时请务必严格和控制台给出的`链接配置`里的 scheme 保持一致。 - -
    -Unity-Android scheme 配置示例 - -```xml -假设控制台给出的「链接配置」为:tdsFwFdCIr6u71WQDQwQN://gifts?code={code} 则 scheme 为:tdsFwFdCIr6u71WQDQwQN - - - - - - - - - - - - - - - - - -``` - -
    - -
    -原生 Android scheme 配置示例 - -```xml -假设控制台给出的「链接配置」为:tdsFwFdCIr6u71WQDQwQN://gifts?code={code} 则 scheme 为:tdsFwFdCIr6u71WQDQwQN - - - - - - - - - - - - - - - -``` - -
    - -针对 Android 的包体可以在本地通过 ADB 命令来检测包体中 scheme 的配置是否正确,如果 ADB 命令可以在本地的设备上唤起应用则说明配置没有问题。 -```shell -# scheme 为控制台展示的 scheme, package 为应用的包名。注意:最终的命令里不包含大括号。 -adb shell am start \ - -W -a android.intent.action.VIEW \ - -d "{scheme}://gifts" {packageName} -``` - -游戏客户端可按照如下文档解析 TapLink,获得礼包码: -- [Unity: Deep linking on iOS](https://docs.unity3d.com/Manual/deep-linking-ios.html) -- [Unity: Deep linking on Android](https://docs.unity3d.com/Manual/deep-linking-android.html) -- [Android: 创建指向应用内容的深层链接](https://developer.android.com/training/app-links/deep-linking) -- [Deep linking and URL scheme in iOS](https://benoitpasquier.com/deep-linking-url-scheme-ios/) - -由于 deep linking 是一种标准的技术方案,开发者也可以自行搜索获取其解析方法。游戏客户端获取礼包码之后,去自己的服务端进行核销即可,这样就完成了一次礼包码的发放流程。 - -## 应用场景 - -当前支持:TapTap 签到活动 diff --git a/versioned_docs/version-v4/sdk/taptap-login/_category_.json b/versioned_docs/version-v4/sdk/taptap-login/_category_.json deleted file mode 100644 index ee0a19161..000000000 --- a/versioned_docs/version-v4/sdk/taptap-login/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "TapTap 登录", - "collapsed": true, - "position": 2 -} diff --git a/versioned_docs/version-v4/sdk/taptap-login/best-practice.mdx b/versioned_docs/version-v4/sdk/taptap-login/best-practice.mdx deleted file mode 100644 index 89ce148a3..000000000 --- a/versioned_docs/version-v4/sdk/taptap-login/best-practice.mdx +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: TapTap 登录最佳实践 -sidebar_label: 最佳实践 -sidebar_position: 3 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -## 登录流程 - -玩家在登录时,操作的步骤越少,路径越短,则转化率越高。建议使用相对简短的引导,保留必要的步骤,让玩家能快速进入游戏。如图所示: - - - -### 登录界面 - -为玩家提供 TapTap 登录按钮,按照[登录设计指南](/design/)绘制,并参考[功能介绍](/v4/sdk/taptap-login/features/) 中单个登录与多个登录的展现方式。 - -### 使用玩家公开信息 - -玩家在游戏内创建角色时,可以直接使用玩家登录后授权给游戏的公开信息,包括玩家的 TapTap 头像、昵称等,帮助玩家自动完成填写流程。 - -可以参考下面的流程图: - - - -*参考上一篇「开发指南」文档。 - -### 提供切换账号功能 - -建议游戏为玩家提供切换账号的功能。 - -- 玩家切换账号时,务必调用登出接口,以保证登录账号与其他游戏服务(内嵌动态)账号保持一致。 -- 当玩家已经完成了登出的操作,为玩家自动显示出登录的界面,让玩家可以使用另一个账号登录。 - -### 提供账号绑定功能 - -建议在游戏中添加账号绑定功能,为玩家提供多种登录方式。 - -如果游戏有自己的账户系统,仅使用单纯 TapTap 登录,则需要游戏自行实现账号绑定功能,方便玩家绑定游戏账号与 TapTap 登录后返回的用户唯一标识。 - -## Checklist - -向玩家提供登录功能前,开发者需要测试登录流程是否正常完成,检查以下事项: - -* 游戏是否达到 [SDK 环境要求](/v4/sdk/access/quickstart/#环境要求)。 -* 开发者是否了解 TapSDK 中两种 TapTap 登录方式,并选择了适合游戏的一种。参考[接入 TapTap 登录](/v4/sdk/taptap-login/guide/)。 -* 是否在 TapTap 开发者后台填写了 Android 平台或 iOS 平台相关配置。参考[配置签名证书](/v4/sdk/access/get-ready/#配置签名证书)。 -* 在未安装 TapTap 客户端的设备上打开游戏,是否能以 WebView 方式完成登录流程,是否能获取玩家授权的基本信息。 -* 在安装了最新版 TapTap 客户端的设备上打开游戏,是否能拉起 TapTap 客户端完成登录流程,是否能获取玩家授权的基本信息。 -* 登录授权完成后,退出游戏再次进入,是否可以[静默登录](/v4/sdk/taptap-login/features/#实现静默登录)。 -* 登录授权未完成就退出游戏,或者点了取消,再次进入游戏,是否能重新开始登录流程。 diff --git a/versioned_docs/version-v4/sdk/taptap-login/faq.mdx b/versioned_docs/version-v4/sdk/taptap-login/faq.mdx deleted file mode 100644 index c1f6bd925..000000000 --- a/versioned_docs/version-v4/sdk/taptap-login/faq.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from "/src/docComponents/sdkVersions"; -import { Conditional } from "/src/docComponents/conditional"; - -### TapTap 登录报:`请求的 scope(compliance)不匹配任何被允许的 scope` 异常提示。 -检查是否导入了实名认证防沉迷 SDK 的包体但是并没有对此进行初始化,如果不需要实名认证功能的话,则项目中不要导入实名认证的包体。 - -### TapTap For iOS 登录报 `accessToken: sdk_not_matched` 异常 -检查 TapTap 开发者后台的应用是否配置了应用的 Bundle ID。 - -### TapTap 登录报 `signature not match` 异常 -接入了 TapTap 登录但没有在开发者中心后台配置签名或者配置错了都会提示该异常。有的开发者可能没有调试出异常信息,可以通过这种方式进行验证:如果将 TapTap 客户端卸载,测试登录功能时会弹出 WebView 授权,而测试设备安装了 TapTap 客户端则无法拉起客户端授权,这种情况基本上就是因为签名配置问题导致的,请参考[文档](/v4/sdk/access/get-ready/#配置签名证书)完成配置。 - -### TapTap 登录提示 `暂无测试资格或不在测试期间,无法登录游戏` 异常提示。 -**TapTap 开发者中心 > 商店 > 版本发布 > 内部测试** 检查是否开启了内部测试功能,如果开启了检查是否没有将当前 TapTap ID 账号添加进测试用户中。 - -### TapTap 登录报 `state not equal` 异常 -检查当前设备系统时间是否已经开启联网同步、检查当前设备的 TapTap 客户端版本是否过低。 - -### TapTap 登录报 `java.lang.NoSuchFieldException: CACHE_ELSE_NETWORK` 异常 -由于 Android 项目开启了混淆操作,TapSDK 已经做了混淆处理,需要跳过对 TapSDK 的混淆操作。具体请参考 [Android 代码混淆](/v4/sdk/access/quickstart/#android-代码混淆)。针对 Android 原生目前暂不支持开启资源混淆操作,如果项目开启了资源混淆操作请关闭掉,`shrinkResources false`。 - -### TapTap 登录报 `{"code":36869,"error_description":"Unauthorized."}` 异常 -检查 TapSDK 初始化的 Client ID、Client Token 以及 ServerURL 参数赋值是否有误,此外,ServerURL 请**使用 HTTPS 协议**。 - -### TapTap 登录报 `Chain validation failed` 异常 -可以按照以下步骤排查: -1、检查设备是否连接代理并没有正常安装证书; -2、检查手机系统时间是否有修改。 - -### TapTap 登录报 `application id is empty` 异常 -可以按照以下步骤排查: -1、检查 TapSDK 的初始化操作是否在安卓 UI 线程也就是 main 线程中执行; -2、确保 TapSDK 的初始化已经完成,避免在 TapSDK 初始化后紧接着调用 TapTap 登录功能,建议 TapSDK 的初始化前置。 - -### 单纯 TapTap 用户认证 For Unity 集成报如下异常: -``` -Assembly 'Assets/TapTap/Common/Plugins/TapTap.Common.dll' will not be loaded due to errors: -Unable to resolve reference 'LC.Newtonsoft.Json'. Is the assembly missing or incompatible with the current platform? -``` -报错原因是因为使用 TapSDK Unity v3.7.1 及更高版本时并没有在项目的 `Packages/manifest.json` 文件中添加 `com.leancloud.storage` 模块导致的,参考[文档](/v4/sdk/taptap-login/guide)添加即可。 - -### 登录提示: this app is not allowed for this domain - -1. 检查开发者后台应用配置中是否开通了 TapTap 登录服务; -2. 检查项目初始化代码中 ClientId、ClientToken、ServerUrl (需要以 `https://` 开头)是否和开发者后台保持一致。 - - -![](https://dc-file.leanticket.cn/VIFonJ9YOJ4SAXb3WdVhMYWlF84xGKkN/CF18E02C-C819-4940-9C9B-60050062EDD0.png) - -### 玩家通过 TapTap 登录后,开发者能否获得玩家的手机号? - -手机号属于玩家的隐私信息,目前暂不支持开发者获取登录玩家的手机号。 - -### unity 项目集成 TapSDK 然后导出 Xcode 项目,运行时报错 `NullReferenceException: Object reference not set to an instance of an object TapTap.Login.Editor. TapLoginlOSProcessor.OnPostprocessBuild ...` - -请参考文档 [iOS 配置](https://developer.taptap.cn/docs/v4/sdk/taptap-login/guide)描述,检查是否有创建 `TDS-Info.plist` 文件。(注意复制配置内容时,请删除注释行) \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/taptap-login/features.mdx b/versioned_docs/version-v4/sdk/taptap-login/features.mdx deleted file mode 100644 index f9ecd0978..000000000 --- a/versioned_docs/version-v4/sdk/taptap-login/features.mdx +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: TapTap 登录功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { Conditional } from "/src/docComponents/conditional"; - -为了访问 TapTap Developer Services(以下简称 TDS)的相关服务功能,你的用户需要拥有一个 TapTap 账号。如果用户未使用 TapTap 账号,你的应用在调用 TDS 服务 API 时可能会遇到错误。本文档介绍了如何在你的应用中实现 TapTap 登录。 - -## 业务介绍 - -TapTap 账号服务是基于标准的 OAuth 2.0 协议构建的授权登录系统,为开发者提供了简单、安全、快速的账号登录授权功能,为用户免去输入账号密码的繁琐步骤,让用户只需一键通过 TapTap 账号授权,即可使用你的应用。 - -在取得用户授权之后,开发者可以通过接口调用的方式获得 TapTap 用户的相关公开信息,包括用户昵称、头像等,可用于提高应用的用户体验。 - -## 前期工作 - -请确认已经在 **TapTap 开发者中心 > 应用配置** 完成了开启操作。 - -## 实现交互式登录 - -检查当前如果没有用户登录状态时,需要为用户提供一个可视化点击交互的登录界面。TapTap 审核团队会在应用上架 TapTap 商店时审核你的登录界面,请务必参照[《登录按钮设计规范》](/design/)进行绘制。 - -### 单个登录方式 - -当应用中仅提供 TapTap 一种登录方式时,建议在开始游戏的主界面,绘制一个可交互的登录按钮。按钮的范围大小、按钮上的文案使用,均不能误导、不能阻碍用户的正常顺畅点击。 - -登录按钮的设计样式,在[《登录按钮设计规范》](/design/)允许的范围内,可适当添加与游戏气质相符的风格元素。此外,TDS 也为你准备了不同场景下 TapTap 登录按钮的设计图标,帮助你快速实现登录流程。请点击[登录按钮素材](/tap-download/#登录按钮素材)下载资源。 - - - - - - - - - - - - - -### 多种登录方式 - -如果游戏还有其他登录方式同时存在,应为用户提供合理布局的登录界面,尽可能地从外观明显区分每种登录方式的不同,让用户可以快速找到目标。 - - - -## 实现静默登录 - -静默登录可以帮助用户跳过登录的流程,通常用于用户下一次启动游戏时,仍需之前登录状态的场景。 - -当用户启动游戏时,你可以尝试检查用户是否已经在当前设备上登录、登录信息是否仍有效。 - -- 详见[检查登录状态和用户信息](/v4/sdk/taptap-login/guide/#检查登录状态和用户信息)。 - -这样可以尝试在不显示登录按钮或界面的情况下帮用户完成登录过程。 - -## 登录授权 - -移动应用的 TapTap 账号服务需要与 TapTap 移动客户端配合使用。TapSDK 会根据用户设备中 TapTap 客户端的安装情况来自动选择使用合适的登录流程。 - - - -[点击此处](https://www.taptap.cn/mobile) 下载 TapTap 移动客户端。 - - - - - -[点击此处](https://www.taptap.io/mobile) 下载 TapTap 移动客户端。 - - - -### 唤起 TapTap 客户端授权登录 - -当用户单击 TapTap 登录按钮时,如果 TapSDK 检测到用户设备中已经安装了 TapTap 客户端,会自动唤起设备中的 TapTap 客户端,并识别客户端中的登录信息,进行授权登录。 - - - - - - - - - - - - - - - -### 打开 WebView 授权登录 - - -当用户单击 TapTap 登录按钮时,如果 TapSDK 检测到用户设备中未安装 TapTap 客户端,则会打开 WebView 进行登录流程。 - - - - - - - -当用户点击 TapTap 登录按钮时,如果 TapSDK 检测到用户设备未安装 TapTap 客户端,则会提示用户下载 TapTap 客户端。 - - - - diff --git a/versioned_docs/version-v4/sdk/taptap-login/guide.mdx b/versioned_docs/version-v4/sdk/taptap-login/guide.mdx deleted file mode 100644 index cb48ad892..000000000 --- a/versioned_docs/version-v4/sdk/taptap-login/guide.mdx +++ /dev/null @@ -1,608 +0,0 @@ ---- -title: 开发指南 -sidebar_label: 开发指南 -sidebar_position: 1 ---- - -import MultiLang from '/src/docComponents/MultiLang'; -import { Conditional } from '/src/docComponents/conditional'; -import Profiles from "../../../../docs/sdk/_partials/tap-login-profile.mdx"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; -import sdkVersions from '../../sdkVersions'; -import CodeBlock from "@theme/CodeBlock"; - -## 权限说明 - - - -<> - - - -<> - -该模块依赖如下权限: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 | - -该模块将在应用中添加如下配置: - -```xml - -``` - - - -<> - - - - - -## 准备集成 SDK - -1. 参考 [准备工作](/v4/sdk/access/get-ready/) 创建应用、开启应用配置、绑定 API 域名; -2. 参考 [快速开始](/v4/sdk/access/get-ready/#配置签名证书) 配置包名与签名证书; - -## SDK 获取 - - - -<> - - - -#### iOS 配置 - -在 `Assets/Plugins/iOS/Resource` 目录下创建 `TDS-Info.plist` 文件,复制以下代码并且**替换其中的 `ClientId`**。 - -:::tip - -复制使用以下内容时,**请删除空行以及注释**,以免出现 XML 解析时报错,`ApplicationException: expected a key node`。 - -::: - -```xml - - - - - taptap - - client_id - ClientId - - - -``` - - - -<> - -1. 项目根目录的 build.gradle 添加仓库地址: - -```groovy -repositories { - google() - mavenCentral() -} -``` - -2. app module 的 build.gradle 添加对应依赖: - -{ -`dependencies { - implementation 'com.taptap.sdk:tap-core:${sdkVersions.taptap.android}' - implementation 'com.taptap.sdk:tap-kit:${sdkVersions.taptap.android}' - implementation 'com.taptap.sdk:tap-login:${sdkVersions.taptap.android}' -}` -} - -:::tip -如果 `targetSdkVersion < 29`,还需要添加如下配置: - -- `manifest` 节点添加 `xmlns:tools="http://schemas.android.com/tools"` -- `application` 节点添加 `tools:remove="android:requestLegacyExternalStorage"` -::: - - - -<> - -### 添加依赖 - -iOS 提供通过添加 cocosPod 远程依赖和使用本地文件导入两种集成方式,推荐使用远程依赖方式。 - -#### 远程依赖 - -1. 在工程 Podfile 文件中对应模块下添加依赖: - -{` pod 'TapTapLoginSDK', '~> ${sdkVersions.taptap.ios }'`} - -2. 执行 `Pod install` 下载对应依赖文件 -3. 将工程 Pods 目录下 `TapTapLoginSDK/Frameworks/TapTapLoginResource.bundle` - 等资源文件导入工程中 - -#### 本地文件依赖 - -TapTap 登录依赖于初始化模块,使用本地文件方式添加依赖时,需先参考 [TapSDK 集成](/v4/sdk/access/quickstart/#本地文件依赖) 添加对应本地文件依赖项。 - -1. 在下载页下载如下文件: - -- `TapTapLoginSDK.xcframework` 合规认证依赖库 -- `TapTapLoginResource.bundle` 合规认证资源文件 - -2. 在工程中添加 `framework` 静态库,注意添加时选择 Embed 方式为 **Do Not Embed**,导入 `bundle` 资源文件 -3. SDK 内部使用了 [`Kingfisher` 依赖库](https://cocoapods.org/pods/Kingfisher),开发者应提前通过远程或文件导入方式添加对应依赖。 - -### 工程配置 - -TapTap 客户端应用跳转配置 - -1. 打开 `info.plist`,添加如下配置(请替换 `clientID` 为你在控制台获取的 Client ID): - - ![](https://capacity-files.lcfile.com/xLmohBqQCMvHMpvxps7pReT9kI8CjSGj/tap_ios_info.png) - - ```xml - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - taptap - CFBundleURLSchemes - - - tt[clientID] - - - - - LSApplicationQueriesSchemes - - tapiosdk - tapsdk - taptap - - ``` - -2. 配置 openUrl - - a) 如果项目中有 `SceneDelegate.swift`,请先删除,然后请添加如下代码到 `AppDelegate.swift` 文件: - -```swift -import TapTapLoginSDK - -func application(_ app: UIApplication, open url: URL) -> Bool { - return TapTapLogin.open(url: url) -} - -func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - return TapTapLogin.open(url: url) -} - -func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { - return TapTapLogin.open(url: url) -} -``` - - b) 删除 `info.plist` 里面的 Application Scene Manifest - - ![](/img/tap_ios_appmanifest.png) - - c) 删除 `AppDelegate.swift` 文件中的两个管理 `Scenedelegate` 生命周期代理方法 - -```swift -func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - -} - -func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - -} -``` - - d) 在 `AppDelegate.swift` 中添加 `UIWindow` - - ```swift - var window: UIWindow? - ``` - - - - - -## SDK 初始化 - - - -:::info -游戏 [**适用地区**](/v4/sdk/access/get-ready/#适用地区) 在开启应用配置时选定。 - -* [TapTap 开发者中心](https://developer.taptap.cn/)适用地区为中国大陆。 - -* 出海游戏请前往 [**TapTap.io 开发者中心**](https://developer.taptap.io/) 开启游戏服务,适用地区为其他国家或地区。 - -::: - - - -TapTap 登录模块依赖于 TapTapSDK 初始化,具体参考 [TapSDK 集成](/v4/sdk/access/quickstart/#初始化) - - - -<> - -```cs -using TapSDK.Core; - -// 核心配置 详细参数见 [TapTapSDK] -TapTapSdkOptions coreOptions = new TapTapSdkOptions(); -// TapSDK 初始化 -TapTapSDK.Init(coreOptions); -``` - - - -<> - -`TapTapSdkOptions` 详细参数见 [入门指南#快速开始](/v4/sdk/access/quickstart/#初始化) - -```kotlin -import com.taptap.sdk.core.TapTapSdk -import com.taptap.sdk.core.TapTapSdkOptions -import com.taptap.sdk.core.TapTapRegion -import com.taptap.sdk.core.TapTapLanguage - -TapTapSdk.init( - context = context, - sdkOptions = TapTapSdkOptions( - clientId = clientId, - clientToken = clientToken, - region = TapTapRegion.CN, - preferredLanguage = TapTapLanguage.ZH_HANS, - enableLog = false - ) -) -``` - - - -<> - - -```swift -import TapTapCoreSDK - -// 核心配置项 -let options = TapTapSdkOptions() -options.clientId = "your_client_id" // 必须,开发者中心对应 Client ID -options.clientToken = "your_client_token" // 必须,开发者中心对应 Client Token -options.region = .CN // .CN:中国大陆,.overseas:其他国家或地区 -options.enableLog = enableLog.selectedSegmentIndex == 0 // 是否开启 log,建议 Debug 开启,Release 关闭,默认关闭 log -options.preferredLanguage = TapLanguageType.auto // 语言设置,默认跟随系统,当系统语言不支持时,国内为中文,海外为英文 - -// 初始化 SDK -TapTapSDK.initWith(options) - -``` - - - - - -## 登录 - -:::tip -当用户启动游戏时,可以先检查[用户登录状态](#检查登录状态和用户信息),如果玩家已经登录,则不显示登录按钮,让玩家直接进入游戏。 -::: - - - -<> - -```cs -using TapSDK.Login; -using System.Threading.Tasks; - -// 定义授权范围 -List permissions = new List(); -permissions.Add(TapTapLogin.TAP_LOGIN_SCOPE_PUBLIC_PROFILE); - -// 初始化登录请求 Task -Task task = TapTapLogin.Instance.LoginWithScopes(permissions.ToArray()); -var result = await task; - -// 判断登录结果 -if (task.IsCompleted) -{ - // 登录成功 -} -else if (task.IsCanceled) -{ - // 登录取消 -} -else -{ - // 登录失败 - Debug.Log($"登录失败: {task.Exception.Message}"); -} -``` - - - -<> - -```kotlin -import com.taptap.sdk.login.TapTapLogin -import com.taptap.sdk.kit.internal.callback.TapTapCallback -import com.taptap.sdk.login.TapTapAccount - -TapTapLogin.loginWithScopes( - activity, - permissions.toTypedArray(), - object : TapTapCallback { - - override fun onSuccess(result: TapTapAccount) { - // 成功 - } - - override fun onCancel() { - // 取消 - } - - override fun onFail(exception: TapTapException) { - // 失败 - } - } -) -``` - - -<> - -```swift -import TapTapLoginSDK - -// 定义授权范围 -var scopes: [Scope] = [Scope.publicProfile] - -// 发起 Tap 登录 -TapTapLogin.loginWithScopes(with: scopes) {[weak self] account, error, isCancel in - guard let self else { return } - if isCancel { - // 登录取消 - } else if let error = error { - // 登录失败 - NSLog("Tap登录失败:\(error.localizedDescription)") - } else if let account = account { - // 登录成功 - - // 登录 token - let token = account.accessToken - // 用户信息 - let userInfo = account.userInfo - } -} -``` - - - - - -### 不同的授权范围 - -TapTap 授权登录接口支持选择不同的授权范围且支持任意组合,但必须包含 `public_profile` 和 `basic_info` 中的一个,不然无法获得 openid 和 unionid。TapTap 授权登录接口可以传递 String 类型的数组作为参数,开发者可以根据自己业务需求添加不同的权限为数组的元素。 - - - -| 权限 | 说明 -|---|---| -| `public_profile` | 获得 openid、unionid、用户昵称、用户头像 -| `user_friends` | 获得访问 TapTap 好友相关数据的权限 -| `basic_info` | 获得 openid 和 unionid - -:::tip -若游戏发起 TapTap 授权登录时只请求 `basic_info` 的权限,则用户可享受无感登录的特性,即用户不需要手动确认授权即可自动授权完成登录。 -::: - - - - - -| 权限 | 说明 -|---|---|---| -| `public_profile` | 获得 openid、unionid、用户昵称、用户头像 -| `user_friends` | 获得访问 TapTap 好友相关数据的权限 -| `basic_info` | 获得 openid 和 unionid -| `email` | 获得 email 和 emailVerified 数据 - - - -### 获取用户信息 - -TapTap 用户登录成功之后,开发者可以通过如下方式获取到 TapTap 授权结果的详细信息: - - - -## 检查登录状态和用户信息 - -登录状态和用户信息存在本地缓存中,重新登录将会重置,登出将会清除。 - - - -<> - -```cs -using TapSDK.Login; - -try { - TapTapAccount account = await TapTapLogin.Instance.GetCurrentAccount(); - if (account == null) { - // 用户未登录 - } else { - // 用户已登录 - } -} catch (Exception e) { - Debug.Log($"获取用户信息失败 {e.Message}"); -} -``` - - - -<> - -```kotlin -import com.taptap.sdk.login.TapTapLogin -import com.taptap.sdk.login.TapTapAccount - -// 获取用户信息 -when (TapTapLogin.getCurrentTapAccount() == null) { - true -> { - // 未登录 - } - - else -> { - // 已登录 - } -} -``` - - -<> - -```swift -import TapTapLoginSDK - -if let account = TapTapLogin.getCurrentTapAccount() { - let token = account.accessToken - let profile = account.userInfo - if let token, let profile { - // 用户已登录 - } else { - NSLog("Tap账户未登录") - } -} else { - NSLog("Tap账户未登录") -} -``` - - - - -## 登出 - - - -<> - -```cs -using TapSDK.Login; - -TapTapLogin.Instance.Logout(); -``` - - - -<> - -```kotlin -import com.taptap.sdk.login.TapTapLogin - -TapTapLogin.logout() -``` - - -<> - -```swift -import TapTapLoginSDK - -TapTapLogin.logout() -``` - - - - -## Unity PC 登录配置 - -:::tip -SDK **默认支持扫码登录**,跳转浏览器登录需要额外配置,具体参考以下两节。 -::: - - - -![PC 登录](https://capacity-files.lcfile.com/BGyFDAQUNUrw9EuMcx8SyP7pek4BKz5u/taptap-login-pc.png) - - - - - -![PC 登录](https://capacity-files.lcfile.com/GI0d6OOdTq4Xaphumdiayqi16JBgbmMg/taptap-login-pc.png) - - - -### Windows 平台 - -如果想要在 Windows 使用跳转网页浏览器登录功能,需要在注册表添加相应配置: - -``` -Windows Registry Editor Version 5.00 - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}] -@="{游戏名称}" -"URL Protocol"="{程序.exe 安装路径}}" - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}] -@="{游戏名称}" - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}] - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}\Shell\Open] - -[HKEY_CLASSES_ROOT\open-taptap-{client_id}\Shell\Open\Command] -@="\"{程序.exe 安装路径}\" \"%1\"" -``` - -### macOS 平台 - -在 macOS 平台下,SDK 会自动配置 `CFBundleURLTypes`。 - -请通过以下步骤确认配置无误: - -- 在 Unity 下打开 `BuildSetting` 选择 `PC、Mac & Linux Standalone` Platform,`Target Platform` 选择 `macOS`。 -- 勾选 `Create XCode Project`,选择输出 `XCode` 工程进行编译。 -- 打开输出的 `XCode Project`,选择 `Target`,点击 `Info`,展开 `URL Types`,检查是否自动添加以下 `URL Scheme`:`TapWeb : open-taptap-{clientId}`,如未添加,则手动添加: - -```xml -CFBundleURLTypes - - - CFBundleURLName - TapWeb - CFBundleURLSchemes - - open-taptap-{client_id} - - - -``` diff --git a/versioned_docs/version-v4/sdk/taptap-login/guide/_category_.json b/versioned_docs/version-v4/sdk/taptap-login/guide/_category_.json deleted file mode 100644 index 3446da847..000000000 --- a/versioned_docs/version-v4/sdk/taptap-login/guide/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "开发指南", - "collapsed": true, - "position": 2 -} diff --git a/versioned_docs/version-v4/sdk/taptap-login/taptap-oauth.mdx b/versioned_docs/version-v4/sdk/taptap-login/taptap-oauth.mdx deleted file mode 100644 index f831bf292..000000000 --- a/versioned_docs/version-v4/sdk/taptap-login/taptap-oauth.mdx +++ /dev/null @@ -1,974 +0,0 @@ ---- -title: TapTap OAuth 接口 -sidebar_position: 4 ---- - -import {Red, Blue, Black, Gray} from '/src/docComponents/doc'; -import {Conditional} from '/src/docComponents/conditional'; - -## 概述 - -TapTap OpenAPI 采用统一的 Mac Token 头部签算来传递用户授权信息。 - -开发者接入 SDK [登录模块](/v4/sdk/taptap-login/guide),在用户授权你的应用程序后,将会生成访问令牌(Access Token)。这个访问令牌在加密后会生成 Mac Token 字符串。Access Token 长期有效,只有在用户更新其账户安全信息或解除对当前应用的授权时才会失效。开发人员应妥善管理 Access Token 在其服务器上,用作与 TapTap 服务器进行后续通讯的标识。 - -Mac Token 生成算法见文档中的 [MAC Token 算法](#mac-token-算法) 部分。 - - - -以下接口均为国内示例。当移动端初始化为海外时,登录即为海外,以下服务端文档流程不变,将示例中的请求域名 `open.tapapis.cn` 更换为海外域名 `open.tapapis.com` 即可。 - - - -## 流程 - -1. 移动端用 SDK 的 TapTap 登录,可以 [获取 AccessToken](/sdk/taptap-login/guide/tap-login/#检查登录状态和用户信息),里面包含: - - ```java - public String kid; - public String token_type; - public String mac_key; - public String mac_algorithm; - public Set scopeSet; - ``` - -2. 再把移动端获取的参数发到游戏服务器,服务端签算 mac token。 -3. 请求 `https://open.tapapis.cn/account/profile/v1``https://openapi.tap.io/account/profile/v1` , header 携带 `mac token`。 - -## API -当 SDK 只请求 basic_info 的权限时,请使用基础信息接口,请求 public_profile 时,请使用详细信息接口。 - -### 获取当前账户基础信息 - -> GET https://open.tapapis.cn/account/basic-info/v1?client_id=xxxhttps://openapi.tap.io/account/basic-info/v1?client_id=xxx
    Authorization mac token - -#### 请求参数 - -| 字段 | 类型 | 说明 | -| --------- | ------ | ------ | -| client_id | string | 该应用的 `Client ID`,应与约定相同 | - -#### 响应参数 - -字段 | 类型 | 说明 ---------------- | ------------- | ------------ -openid | string | 授权用户唯一标识,每个玩家在每个游戏中的 openid 都是不一样的,同一游戏获取同一玩家的 openid 总是相同 -unionid | string | 授权用户唯一标识,一个玩家在一个厂商的所有游戏中 unionid 都是一样的,不同厂商 unionid 不同 - -#### 请求示例 - -替换其中的 `MAC id` 和 `Client ID` 为自己签算的 mac token 和控制台的 `Client ID`。 - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://open.tapapis.cn/account/basic-info/v1?client_id=" -``` - - - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://openapi.tap.io/account/basic-info/v1?client_id=" -``` - - - - -### 获取当前账户详细信息 - - - -> GET https://open.tapapis.cn/account/profile/v1?client_id=xxxhttps://openapi.tap.io/account/profile/v1?client_id=xxx
    Authorization mac token - - -#### 请求参数 - -| 字段 | 类型 | 说明 | -| --------- | ------ | ------ | -| client_id | string | 该应用的 `Client ID`,应与约定相同 | - -#### 响应参数 - -字段 | 类型 | 说明 ---------------- | ------------- | ------------ -name | string | 用户名 -avatar | string | 用户头像图片地址 -openid | string | 授权用户唯一标识,每个玩家在每个游戏中的 openid 都是不一样的,同一游戏获取同一玩家的 openid 总是相同 -unionid | string | 授权用户唯一标识,一个玩家在一个厂商的所有游戏中 unionid 都是一样的,不同厂商 unionid 不同 - -#### 请求示例 - -替换其中的 `MAC id` 和 `Client ID` 为自己签算的 mac token 和控制台的 `Client ID`。 - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://open.tapapis.cn/account/profile/v1?client_id=" -``` - - - - - -``` -curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa -gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs -KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://openapi.tap.io/account/profile/v1?client_id=" -``` - - - -## 其他 - -### MAC Token 算法 - -MAC Token 包含以下字段: - -| 字段 | 类型 | 说明 | -| ------------- | ------ | ------------------------------- | -| kid | string | mac_key id, The key identifier. | -| access_token | string | 该字段暂无作用 | -| token_type | string | Token 类型,如 mac | -| mac_key | string | mac 密钥 | -| mac_algorithm | string | mac 计算的算法名称 hmac-sha-1 | - -使用 Mac Token 签算一个接口: - - -
    -Node.js 请求示例 - - - -```javascript -const http = require('http'); -const https = require('https'); -const crypto = require('crypto'); - -function getAuthorization(requestUrl, method, keyId, macKey) { - const url = new URL(requestUrl); - const time = Math.floor(Date.now() / 1000).toString().padStart(10, '0'); - const randomStr = getRandomString(16); - const host = url.hostname; - const uri = url.pathname + url.search; - const port = url.port || (url.protocol === 'https:' ? '443' : '80'); - const other = ''; - const sign = signData(mergeData(time, randomStr, method, uri, host, port, other), macKey); - - return `MAC id="${keyId}", ts="${time}", nonce="${randomStr}", mac="${sign}"`; -} - -function getRandomString(length) { - return crypto.randomBytes(length).toString('base64'); -} - -function mergeData(time, randomCode, httpType, uri, domain, port, other) { - let prefix = - `${time}\n${randomCode}\n${httpType}\n${uri}\n${domain}\n${port}\n`; - - if (!other) { - prefix += '\n'; - } else { - prefix += `${other}\n`; - } - - return prefix; -} - -function signData(signatureBaseString, key) { - const hmac = crypto.createHmac('sha1', key); - hmac.update(signatureBaseString); - return hmac.digest('base64'); -} - -const client_id = "hskc**********kklm"; -const keyId = "1/VLDoiGUhNCIpUq827L**************zAJ-i8hT_w9vuPtPgdaPkWDv6K4eVe_yZnKz************EYep-T4ki5w3kyYACVnM61JJqDEKfpNnHoTZU********************iUArkgPsWEwOpZGxva7FnqbTwmpLT0a28UtiR5gyr4XXutbnE5tb4A-iSqRpqqtgABXBZd34U5Th3iJ1C666iYQFvuQL9uC-Zv7-xKCNjyPonBqU4ZWZnKLFf2mzprU5vJCA8q5by1SZxY63kZBQieHYxFjyOCQdJ-25gDlxiqDbNq08kmSdY6TB1qtQ68V37L6a8nIzyVHooX9uc2Yw"; -const macKey = 'VPDalRmxtBqi******************tH937GNKIvj3'; -const requestUrl = 'https://open.tapapis.cn/account/profile/v1?client_id='+ client_id ; -const method = 'GET'; - - -const authorization = getAuthorization(requestUrl, method, keyId, macKey); -console.log(authorization); - -const options = new URL(requestUrl); -const client = options.protocol === 'https:' ? https : http; - -const req = client.request({ - hostname: options.hostname, - port: options.port, - path: options.pathname + options.search, - method: 'GET', - headers: { - 'Authorization': authorization - } -}, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - console.log(data); - }); -}); - -req.end(); - - -``` - - - - - -```javascript -const http = require('http'); -const https = require('https'); -const crypto = require('crypto'); - -function getAuthorization(requestUrl, method, keyId, macKey) { - const url = new URL(requestUrl); - const time = Math.floor(Date.now() / 1000).toString().padStart(10, '0'); - const randomStr = getRandomString(16); - const host = url.hostname; - const uri = url.pathname + url.search; - const port = url.port || (url.protocol === 'https:' ? '443' : '80'); - const other = ''; - const sign = signData(mergeData(time, randomStr, method, uri, host, port, other), macKey); - - return `MAC id="${keyId}", ts="${time}", nonce="${randomStr}", mac="${sign}"`; -} - -function getRandomString(length) { - return crypto.randomBytes(length).toString('base64'); -} - -function mergeData(time, randomCode, httpType, uri, domain, port, other) { - let prefix = - `${time}\n${randomCode}\n${httpType}\n${uri}\n${domain}\n${port}\n`; - - if (!other) { - prefix += '\n'; - } else { - prefix += `${other}\n`; - } - - return prefix; -} - -function signData(signatureBaseString, key) { - const hmac = crypto.createHmac('sha1', key); - hmac.update(signatureBaseString); - return hmac.digest('base64'); -} - -const client_id = "5enu******wfy"; -const keyId = "1/JFZi8****IiumsGZI31iJH1q*****UKZ-eKA"; -const macKey = 'LMbNcKox*******kfmk7oWXbuRz'; -const requestUrl = 'https://openapi.tap.io/account/profile/v1?client_id='+ client_id ; -const method = 'GET'; - -const authorization = getAuthorization(requestUrl, method, keyId, macKey); -console.log(authorization); - -const options = new URL(requestUrl); -const client = options.protocol === 'https:' ? https : http; - -const req = client.request({ - hostname: options.hostname, - port: options.port, - path: options.pathname + options.search, - method: 'GET', - headers: { - 'Authorization': authorization - } -}, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - console.log(data); - }); -}); - -req.end(); - - -``` - - - - - - -
    - - -
    -Java 请求示例 - - - -```java -package com.taptap; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.*; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -public class Authorization { - public static void main(String[] args) throws IOException { - String client_id = "0RiAlMny7jiz086FaU"; - String kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"; // kid - String mac_key = "mSUQNYUGRBPXyRyW"; // mac_key - String method = "GET"; - String request_url = "https://open.tapapis.cn/account/profile/v1?client_id=" + client_id; // - String authorization = getAuthorization(request_url, method, kid, mac_key); - System.out.println(authorization); - URL url = new URL(request_url); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - // Http - conn.setRequestProperty("Authorization", authorization); - conn.setRequestMethod("GET"); - BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String line; - StringBuilder result = new StringBuilder(); - while ((line = rd.readLine()) != null) { - result.append(line); - } - rd.close(); - System.out.println(result.toString()); - } - /** - * @param request_url - * @param method "GET" or "POST" - * @param key_id key id by OAuth 2.0 - * @param mac_key mac key by OAuth 2.0 - * @return authorization string - */ - public static String getAuthorization(String request_url, String method, String key_id, String - mac_key) { - try { - URL url = new URL(request_url); - String time = String.format(Locale.US, "%010d", System.currentTimeMillis() / 1000); - String randomStr = getRandomString(16); - String host = url.getHost(); - String uri = request_url.substring(request_url.lastIndexOf(host) + host.length()); - String port = "80"; - if (request_url.startsWith("https")) { - port = "443"; - } - String other = ""; - String sign = sign(mergeSign(time, randomStr, method, uri, host, port, other), mac_key); - return "MAC " + getAuthorizationParam("id", key_id) + "," + getAuthorizationParam("ts", time) - + "," + getAuthorizationParam("nonce", randomStr) + "," + getAuthorizationParam("mac", - sign); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - return null; - } - private static String getRandomString(int length) { - byte[] bytes = new byte[length]; - new SecureRandom().nextBytes(bytes); - String base64String = Base64.getEncoder().encodeToString(bytes); - return base64String; - } - private static String mergeSign(String time, String randomCode, String httpType, String uri, - String domain, String port, String other) { - if (time.isEmpty() || randomCode.isEmpty() || httpType.isEmpty() || domain.isEmpty() || port.isEmpty()) - { - return null; - } - String prefix = - time + "\n" + randomCode + "\n" + httpType + "\n" + uri + "\n" + domain + "\n" + port - + "\n"; - if (other.isEmpty()) { - prefix += "\n"; - } else { - prefix += (other + "\n"); - } - return prefix; - } - private static String sign(String signatureBaseString, String key) { - try { - SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(signingKey); - byte[] text = signatureBaseString.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = mac.doFinal(text); - signatureBytes = Base64.getEncoder().encode(signatureBytes); - return new String(signatureBytes, StandardCharsets.UTF_8); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new IllegalStateException(e); - } - } - private static String getAuthorizationParam(String key, String value) { - if (key.isEmpty() || value.isEmpty()) { - return null; - } - return key + "=" + "\"" + value + "\""; - } -} -``` - - - - - -```java -package com.taptap; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.*; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -public class Authorization { - public static void main(String[] args) throws IOException { - String client_id = "0RiAlMny7jiz086FaU"; - String kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"; // kid - String mac_key = "mSUQNYUGRBPXyRyW"; // mac_key - String method = "GET"; - String request_url = "https://openapi.tap.io/account/profile/v1?client_id=" + client_id; // - String authorization = getAuthorization(request_url, method, kid, mac_key); - System.out.println(authorization); - URL url = new URL(request_url); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - // Http - conn.setRequestProperty("Authorization", authorization); - conn.setRequestMethod("GET"); - BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String line; - StringBuilder result = new StringBuilder(); - while ((line = rd.readLine()) != null) { - result.append(line); - } - rd.close(); - System.out.println(result.toString()); - } - /** - * @param request_url - * @param method "GET" or "POST" - * @param key_id key id by OAuth 2.0 - * @param mac_key mac key by OAuth 2.0 - * @return authorization string - */ - public static String getAuthorization(String request_url, String method, String key_id, String - mac_key) { - try { - URL url = new URL(request_url); - String time = String.format(Locale.US, "%010d", System.currentTimeMillis() / 1000); - String randomStr = getRandomString(16); - String host = url.getHost(); - String uri = request_url.substring(request_url.lastIndexOf(host) + host.length()); - String port = "80"; - if (request_url.startsWith("https")) { - port = "443"; - } - String other = ""; - String sign = sign(mergeSign(time, randomStr, method, uri, host, port, other), mac_key); - return "MAC " + getAuthorizationParam("id", key_id) + "," + getAuthorizationParam("ts", time) - + "," + getAuthorizationParam("nonce", randomStr) + "," + getAuthorizationParam("mac", - sign); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - return null; - } - private static String getRandomString(int length) { - byte[] bytes = new byte[length]; - new SecureRandom().nextBytes(bytes); - String base64String = Base64.getEncoder().encodeToString(bytes); - return base64String; - } - private static String mergeSign(String time, String randomCode, String httpType, String uri, - String domain, String port, String other) { - if (time.isEmpty() || randomCode.isEmpty() || httpType.isEmpty() || domain.isEmpty() || port.isEmpty()) - { - return null; - } - String prefix = - time + "\n" + randomCode + "\n" + httpType + "\n" + uri + "\n" + domain + "\n" + port - + "\n"; - if (other.isEmpty()) { - prefix += "\n"; - } else { - prefix += (other + "\n"); - } - return prefix; - } - private static String sign(String signatureBaseString, String key) { - try { - SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(signingKey); - byte[] text = signatureBaseString.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = mac.doFinal(text); - signatureBytes = Base64.getEncoder().encode(signatureBytes); - return new String(signatureBytes, StandardCharsets.UTF_8); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new IllegalStateException(e); - } - } - private static String getAuthorizationParam(String key, String value) { - if (key.isEmpty() || value.isEmpty()) { - return null; - } - return key + "=" + "\"" + value + "\""; - } -} -``` - - - -
    - -
    - -PHP 请求示例 - - - -```php - -``` - - - - -```php - -``` - - -
    - -
    - -Python3 请求示例 - - - -```python -import base64 -import hmac -import random -import string -import time -from hashlib import sha1 - - -def get_mac_token_signature(host, request_url, method, mac_key, kid): - mac_token_pattern = 'MAC id="{kid}",ts="{ts}",nonce="{nonce}",mac="{mac}"' - timestamp = str(int(time.time())) - nonce = ''.join(random.choices(string.ascii_lowercase + string.ascii_uppercase, k=16)) - sign_array = [timestamp, nonce, method, request_url, host, '443', ''] - seperator = '\n' - sign_input = seperator.join(sign_array) + seperator - hmac_code = hmac.new(mac_key.encode('UTF-8'), sign_input.encode('UTF-8'), sha1) - mac_str = base64.b64encode(hmac_code.digest()).decode('UTF-8') - return mac_token_pattern.format(kid=kid, ts=timestamp, nonce=nonce, - mac=mac_str) - -if __name__ == '__main__': - kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" - mac_key = "mSUQNYUGRBPXyRyW" - client_id = "0RiAlMny7jiz086FaU" - signature = get_mac_token_signature('open.tapapis.cn', '/account/profile/v1?client_id=' + client_id, - 'GET', mac_key, kid) - print(signature) -``` - - - - -```python -import base64 -import hmac -import random -import string -import time -from hashlib import sha1 - - -def get_mac_token_signature(host, request_url, method, mac_key, kid): - mac_token_pattern = 'MAC id="{kid}",ts="{ts}",nonce="{nonce}",mac="{mac}"' - timestamp = str(int(time.time())) - nonce = ''.join(random.choices(string.ascii_lowercase + string.ascii_uppercase, k=16)) - sign_array = [timestamp, nonce, method, request_url, host, '443', ''] - seperator = '\n' - sign_input = seperator.join(sign_array) + seperator - hmac_code = hmac.new(mac_key.encode('UTF-8'), sign_input.encode('UTF-8'), sha1) - mac_str = base64.b64encode(hmac_code.digest()).decode('UTF-8') - return mac_token_pattern.format(kid=kid, ts=timestamp, nonce=nonce, - mac=mac_str) - -if __name__ == '__main__': - kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" - mac_key = "mSUQNYUGRBPXyRyW" - client_id = "0RiAlMny7jiz086FaU" - signature = get_mac_token_signature('openapi.tap.io', '/account/profile/v1?client_id=' + client_id, - 'GET', mac_key, kid) - print(signature) -``` - - -
    - -
    - -Go 请求示例 - - - -```go -package main - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "fmt" - "io" - "net/http" - "strconv" - "time" -) - -func main() { - // 替换 clientId、accessToken、macKey 参数 - // clientId 参数在 TapDC 后台查看 - clientId := "请替换为控制台的 `Client ID`" - // TapTap 登录成功后 TapSDK 返回的 access_token - accessToken := "1/jvsVxFC6-PIUiXvZVVtv1hogX5q9Z1y_rp-AtVjE3iyHikXXfd_2h-i0wLmc9UjLJwhH6fQ8cvGrklONdvy2J5YfoqzV0ewGPMSLkQIkRv_xaLaYPariWbrkP1MtG2b4CzR1KHvuSCJHewCmTFZmsyNGojTJr5t75f5Nc8j-jjCYeDtFO0-XFI_J7kzktswzzsmISt7cx49QVess-VbaQcU31pEDb_OA03I28H5ehIvqQ0CQdf1LieLyONcH97l1IEU39AirioF_KGJccVG64QsgWmzxLPwmfTurw4cwBPo04yuXnas4YI5haE2UxtckNCpagP19drtGW57-HaAdww" - // TapTap 登录成功后 TapSDK 返回的 mac_key - macKey := "fTCuDUDDmNny7a36EWbhUDLaqpoDMQu2hCi9qAJ5" - - // 随机数,正式上线请替换 - nonce := "8IBTHwOdqNKAWeKl7plt66==" - // 时间戳转换成字符串 - timestamp := strconv.FormatInt(time.Now().Unix(), 10) - // 请求 url 相关 - reqHost := "open.tapapis.cn" - reqURI := "/account/profile/v1?client_id=" + clientId - reqURL := "https://" + reqHost + reqURI - - macStr := timestamp + "\n" + nonce + "\n" + "GET" + "\n" + reqURI + "\n" + reqHost + "\n" + "443" + "\n\n" - mac := hmacSha1(macStr, macKey) - authorization := "MAC id=" + "\"" + accessToken + "\"" + "," + "ts=" + "\"" + timestamp + "\"" + "," + "nonce=" + "\"" + nonce + "\"" + "," + "mac=" + "\"" + mac + "\"" - - client := http.Client{} - req, err := http.NewRequest(http.MethodGet, reqURL, nil) - if err != nil { - fmt.Println(err.Error()) - return - } - - // 添加请求头 - req.Header.Add("Authorization", authorization) - // 发送请求 - resp, err := client.Do(req) - if err != nil { - fmt.Println(err.Error()) - return - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Println(err.Error()) - return - } - fmt.Println(string(respBody)) -} - -/* -HMAC-SHA1 签名 -*/ -func hmacSha1(valStr, keyStr string) string { - key := []byte(keyStr) - mac := hmac.New(sha1.New, key) - mac.Write([]byte(valStr)) - - // 进行 Base64 编码 - return base64.StdEncoding.EncodeToString(mac.Sum(nil)) -} -``` - - - - -```go -package main - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "fmt" - "io" - "net/http" - "strconv" - "time" -) - -func main() { - // 替换 clientId、accessToken、macKey 参数 - // clientId 参数在 TapDC 后台查看 - clientId := "请替换为控制台的 `Client ID`" - // TapTap 登录成功后 TapSDK 返回的 access_token - accessToken := "1/jvsVxFC6-PIUiXvZVVtv1hogX5q9Z1y_rp-AtVjE3iyHikXXfd_2h-i0wLmc9UjLJwhH6fQ8cvGrklONdvy2J5YfoqzV0ewGPMSLkQIkRv_xaLaYPariWbrkP1MtG2b4CzR1KHvuSCJHewCmTFZmsyNGojTJr5t75f5Nc8j-jjCYeDtFO0-XFI_J7kzktswzzsmISt7cx49QVess-VbaQcU31pEDb_OA03I28H5ehIvqQ0CQdf1LieLyONcH97l1IEU39AirioF_KGJccVG64QsgWmzxLPwmfTurw4cwBPo04yuXnas4YI5haE2UxtckNCpagP19drtGW57-HaAdww" - // TapTap 登录成功后 TapSDK 返回的 mac_key - macKey := "fTCuDUDDmNny7a36EWbhUDLaqpoDMQu2hCi9qAJ5" - - // 随机数,正式上线请替换 - nonce := "8IBTHwOdqNKAWeKl7plt66==" - // 时间戳转换成字符串 - timestamp := strconv.FormatInt(time.Now().Unix(), 10) - // 请求 url 相关 - reqHost := "openapi.tap.io" - reqURI := "/account/profile/v1?client_id=" + clientId - reqURL := "https://" + reqHost + reqURI - - macStr := timestamp + "\n" + nonce + "\n" + "GET" + "\n" + reqURI + "\n" + reqHost + "\n" + "443" + "\n\n" - mac := hmacSha1(macStr, macKey) - authorization := "MAC id=" + "\"" + accessToken + "\"" + "," + "ts=" + "\"" + timestamp + "\"" + "," + "nonce=" + "\"" + nonce + "\"" + "," + "mac=" + "\"" + mac + "\"" - - client := http.Client{} - req, err := http.NewRequest(http.MethodGet, reqURL, nil) - if err != nil { - fmt.Println(err.Error()) - return - } - - // 添加请求头 - req.Header.Add("Authorization", authorization) - // 发送请求 - resp, err := client.Do(req) - if err != nil { - fmt.Println(err.Error()) - return - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Println(err.Error()) - return - } - fmt.Println(string(respBody)) -} - -/* -HMAC-SHA1 签名 -*/ -func hmacSha1(valStr, keyStr string) string { - key := []byte(keyStr) - mac := hmac.New(sha1.New, key) - mac.Write([]byte(valStr)) - - // 进行 Base64 编码 - return base64.StdEncoding.EncodeToString(mac.Sum(nil)) -} -``` - - -
    - -
    -脚本请求示例 - -可用此脚本验证直接替换参数,用来验证自己服务端签算的 mac token 是否正确。 - -CLIENT_ID 替换为控制台获取的 `Client ID`,ACCESS_TOKEN 和 MAC_KEY 为客户端登录成功后的 `access_token`、`mac_key`: - - - -``` -#!/usr/bin/env bash - -# 客户端 ID -CLIENT_ID="请替换为控制台的 `Client ID`" -# SDK 获取的 access_token -ACCESS_TOKEN="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" -# SDK 获取的 mac_key -MAC_KEY="mSUQNYUGRBPXyRyW" - -# 随机数,正式上线请替换 -NONCE="8IBTHwOdqNKAWeKl7plt8g==" -# 当前时间戳 -TS=$(date +%s) - -# 请求方法 -METHOD="GET" -# 请求地址 (带 query string) -REQUEST_URI="/account/profile/v1?client_id=${CLIENT_ID}" -# 请求域名 -REQUEST_HOST="open.tapapis.cn" - -MAC=$(printf "%s\n%s\n%s\n%s\n%s\n443\n\n" "${TS}" "${NONCE}" "${METHOD}" "${REQUEST_URI}" "${REQUEST_HOST}" | openssl dgst -binary -sha1 -hmac ${MAC_KEY} | base64) - -AUTHORIZATION=$(printf 'MAC id="%s",ts="%s",nonce="%s",mac="%s"' "${ACCESS_TOKEN}" "${TS}" "${NONCE}" "${MAC}") - -curl -s -H"Authorization:${AUTHORIZATION}" "https://${REQUEST_HOST}${REQUEST_URI}" -``` - - - - - - - -``` -#!/usr/bin/env bash - -# 客户端 ID -CLIENT_ID="请替换为控制台的 `Client ID`" -# SDK 获取的 access_token -ACCESS_TOKEN="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ" -# SDK 获取的 mac_key -MAC_KEY="mSUQNYUGRBPXyRyW" - -# 随机数,正式上线请替换 -NONCE="8IBTHwOdqNKAWeKl7plt8g==" -# 当前时间戳 -TS=$(date +%s) - -# 请求方法 -METHOD="GET" -# 请求地址 (带 query string) -REQUEST_URI="/account/profile/v1?client_id=${CLIENT_ID}" -# 请求域名 -REQUEST_HOST="openapi.tap.io" - -MAC=$(printf "%s\n%s\n%s\n%s\n%s\n443\n\n" "${TS}" "${NONCE}" "${METHOD}" "${REQUEST_URI}" "${REQUEST_HOST}" | openssl dgst -binary -sha1 -hmac ${MAC_KEY} | base64) - -AUTHORIZATION=$(printf 'MAC id="%s",ts="%s",nonce="%s",mac="%s"' "${ACCESS_TOKEN}" "${TS}" "${NONCE}" "${MAC}") - -curl -s -H"Authorization:${AUTHORIZATION}" "https://${REQUEST_HOST}${REQUEST_URI}" -``` - - - -
    - -### 通用接口错误信息 - -**统一格式** - -| 字段 | 类型 | 说明 | -| ----------------- | ------ | ---------------------------------------------------- | -| code | int | 预留字段,用于以后追踪问题 | -| error | string | 错误码,代码逻辑判断时使用 | -| error_description | string | 错误描述信息,开发的时候用来帮助理解和解决发生的错误 | - - -**错误响应** - -| 错误码 | 详细描述 | -| ------------------| ------------------------------------------------------------ | -| invalid_request | 请求缺少某个必需参数,包含一个不支持的参数或参数值,或者格式不正确 | -| invalid_time | MAC Token 算法中,ts 时间不合法,**应请求服务器时间重新构造** | -| invalid_client | client_id 参数无效 | -| access_denied | 授权服务器拒绝请求 **这个状态出现在拿着 token 请求用户资源时,如出现,客户端应退出本地的用户登录信息,引导用户重新登录** | -| forbidden | 用户没有对当前动作的权限,**引导重新身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交** | -| not_found | 请求失败,请求所希望得到的资源未被在服务器上发现。**在参数相同的情况下,不应该重复请求** | -| server_error | 服务器出现异常情况 **可稍等后重新尝试请求,但需有尝试上限,建议最多 3 次,如一直失败,则中断并告知用户** | - diff --git a/versioned_docs/version-v4/sdk/tds-gift/_category_.json b/versioned_docs/version-v4/sdk/tds-gift/_category_.json deleted file mode 100644 index c1f75ec93..000000000 --- a/versioned_docs/version-v4/sdk/tds-gift/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "礼包系统", - "collapsed": true, - "position": 9 -} diff --git a/versioned_docs/version-v4/sdk/tds-gift/faq.mdx b/versioned_docs/version-v4/sdk/tds-gift/faq.mdx deleted file mode 100644 index aa9fabfbe..000000000 --- a/versioned_docs/version-v4/sdk/tds-gift/faq.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 4 ---- - -### 兑换参数完备,但请求兑换时返回 403。 - -请检查您请求的 header 是否有带上特殊的 User Agent。 - -### 二次校验是需要游戏方请求两次兑换接口吗? - -使用二次校验需要您按照规范开发校验接口并将地址配置到后台中,由兑换系统发起请求。 - -### 无服务器返回值的 content 参数无法解析? - -返回值的 content 是一段嵌套的 json 字符串,可以先读出字符串再进行进一步 json 解析,除此之外还新增了 content_obj 字段,可以有选择的去使用。 - -### 如何判断兑换是否成功? - -可以根据返回值中的 error 字段是否为 0 来判断。 - -### 无服务器兑换的 sign 字段如何使用? - -可以参考文档的[签名部分](/v4/sdk/tds-gift/guide/#签名)。 - -### Unity 请求无服务器兑换接口报错 HTTP/1.1 422 Unprocessable Entity - -可以通过 **开发者后台 -> 礼包服务** 检查活动开关是否打开,以及礼包码是否失效。 - -### 礼包生成时出错,时间不能超过 2040 年 - -当前最大礼包时间为 2038-01-19 11:14:07。 - -### 三种兑换场景的区别是什么? - -| 兑换方式 | 所需接口 | 特点 | -| --------------- | ------------- |------------- | -| 游戏方校验 | 兑换接口、二次校验接口、礼包发放接口 | 游戏方需要维护校验与发放两个接口;可根据礼包领取条件自行判断是否符合,并可以将礼包条件校验逻辑与礼包发放逻辑分开维护处理 | -| 游戏方发送 | 兑换接口、礼包发放接口 | 游戏方仅需维护一个发放接口;可以通过发放接口进行领取条件的判断以及礼包的发放 | -| 无服务器兑换 | 兑换接口 | 仅判断兑换码是否有效以及码量,兑换码有效即返回礼包信息;流程更简单,无需自行维护接口 | - -### 调用礼包兑换接口报:{"error":100016,"message":"该礼包码无效码","info":{"dev_message":"invalid code","hint":"gift code is invalid"}} 异常。 - -检查接口中传递的礼包码是否错误,礼包码可以在 **Tap 开发者中心 > 你的游戏 > 运营工具 > 礼包 > 礼包活动 > 数据 > 导出** 进行导出。点击 `导出` 按钮后会生成 `.csv` 文件,可以在该文件中查看礼包码。 - -### 调用 `游戏方发送` 礼包兑换接口报:{"error":100015,"message":"发送道具失败"} 异常。 - -该异常说明调用接口时传递的参数正确、签算也正确,可能是游戏侧服务端的发送道具的接口出了异常,需要游戏侧检查自己的发送道具接口是否正常。 \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/tds-gift/features.mdx b/versioned_docs/version-v4/sdk/tds-gift/features.mdx deleted file mode 100644 index 563b74da7..000000000 --- a/versioned_docs/version-v4/sdk/tds-gift/features.mdx +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: 礼包系统功能介绍 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -## 产品简介 - -TDS 全套礼包系统,旨在帮助开发者快速生成、核销礼包码。 - -### 产品定位 - -- 开发者无须投入大量开发成本设计礼包券码生成+核销系统,接入相关能力,1 天内即可完成礼包系统开发,快速生成礼包码进入分发运营阶段。 -- 通过礼包系统生成的券码可直接通过现有兑换接口,实现在游戏客户端内或者网页内快速兑换核销。 - -### 产品优势 - -- 支持游戏生成不同类型券码:通兑码或唯一码,覆盖常见通用运营场景。 -- 支持券码定制,自定义兑换条件,道具灵活配置等个性化运营配置,满足丰富活动场景。 -- 降本提效,为开发者节省海量券码产生的存储费用;根据厂商需求及市场变化不断优化迭代,降低产品设计及维护成本。 -- 提供整体活动、单条券码的消费数据情况,运营可清晰查看当前活动券码消耗及库存,判断活动效果。 - -## 礼包系统使用流程 - -![](/img/tds-gift/gift01.png) - -## 礼包系统使用指引 - -### 礼包系统入口 - -进入 **「开发者中心」**→**「游戏服务」**→**「运营工具」**→**「礼包系统」** - -### 创建/编辑礼包活动 - -#### 配置区服 - -- 未配置区服,请先前往 **「服务器管理」** 配置区服信息 -- 已配置区服,根据实际发放需求,勾选区服信息 - -![](/img/tds-gift/gift02.png) - -#### 配置礼包基础信息 - -- 礼包活动名称 - - 填写礼包名称(该名称不做显示用途) -- 礼包活动有效期 - - 填写礼包活动时间,礼包码的生效周期等于该活动时间,超时礼包码失效,无法兑换 -- 礼包码类型 - - 通兑码:一个码支持多用户兑换;通兑码支持定制,长度限制最长为 13 个字符,仅限数字、英文 - - 唯一码:一个码仅支持一个用户兑换 -- 礼包码生成规则 - - 随机生成:13 位礼包码均由系统随机生成 - - 输入前缀(仅针对唯一码):唯一码支持定制前缀,为了保证生成券码数量,前缀长度建议不超过 6 位 -- 自定义条件 - - 游戏在基础条件限制上新增自定义条件,自定义条件可以叠加,自定义条件最大数量不超过 10 个。未有自定义条件可不勾选 - - 条件:填写限制条件名称(赛季、注册时间等);判断条件:等于,小于,小于等于,大于,大于等于;值:填写限制条件的值 - - 例如:注册时间为 2022 年的用户才可领取该礼包:注册时间(条件)等于(判断逻辑)2022(值) -- 礼包码物品及数量 - - 物品名称:实际发放物品名称,例如原石、甜甜鸡 - - 数量:实际发放物品数量 - -:::tip - -**注意**:每次仅能新增一个物品,切勿将多个物品填写进同一行。新增物品种类最多不超过 10 个。 - -::: - -#### 上架礼包活动 - -前往 **「礼包活动」** 列表,点击开关,上架礼包活动使券码生效 - -![](/img/tds-gift/gift03.png) - -> **注意**:礼包活动上架后礼包码才可被成功兑换,活动上线时务必打开此开关。 - -#### 编辑礼包活动 - -点击 **「编辑」** 进入编辑礼包活动页面,仅「区服」、「礼包活动名称」、「礼包活动有效期」支持编辑 - -### 活动补码/补量 - -**唯一码补码** - -点击「补码」,输入新增礼包码个数,点击「生成并导出」系统将自动下载新增的券码至本地 - -**通兑码补量** - -点击「补量」,输入通兑码新增兑换次数,点击「确认」新增数量,同时系统将再次自动导出该通兑码至本地 - -:::tip - -**注意**:通兑码不会新生成礼包码,仅在原兑换码上新增兑换次数,如需新增礼包码,前往「创建礼包码」新增活动 - -::: - -### 历史记录 - -前往礼包系统首页,点击 **「数据」** 查看历史生成记录 - -![](/img/tds-gift/gift04.png) - -### 服务器配置 - -- 服务器名称 - - 填写游戏服务器名称 -- 服务器 code - - 道具发放通知接收地址 -- 接受道具下发通知地址 - - check_url -- 接收二次校验结果通知地址 - - 开启二次校验服务需填写此字段,如不开启,则不需要 - -### 数据查询 - -**单一 CDKEY 或 单一用户数据** - -- 输入「CDKEY」即可查询到对应礼包码消费状态 -- 输入「uid」即可查询单个用户消费礼包码历史记录 - -![](/img/tds-gift/gift05.png) - -**礼包活动数据** - -- 入口:礼包活动首页 -- 输入任意以下筛选条件,即可查询礼包活动整体消费库存情况 - - 礼包码类型 - - 礼包码活动 - - 礼包活动 ID 或名称 - -![](/img/tds-gift/gift06.png) - -## 常见 FAQ - -Q:提示报错「保存用户 client id 失败」? - -A:为了确保礼包服务能正常使用,请先前往:开发者中心 →「游戏服务」→「应用配置」,开启服务,获取 client id。 - -Q:为什么看不见「游戏服务」tab? - -A:游戏管理员新增用户角色时,需在权限设置中,勾选应用配置权限,才可看到「游戏服务」tab 及其内容。 diff --git a/versioned_docs/version-v4/sdk/tds-gift/guide.mdx b/versioned_docs/version-v4/sdk/tds-gift/guide.mdx deleted file mode 100644 index 902e4da50..000000000 --- a/versioned_docs/version-v4/sdk/tds-gift/guide.mdx +++ /dev/null @@ -1,681 +0,0 @@ ---- -title: 礼包系统开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -:::tip -接入礼包系统之前,需完成应用配置。在 **游戏服务 - 应用配置** 中可获取 **Client ID 和 Server Secret**。请妥善保管这些信息,勿传给他人。 -::: - -## 开始 - -礼包系统接入适用于有请求接口能力的游戏,目前尚未提供完全离线的兑换码服务。 - -根据您的使用场景,您可以灵活对接以下不同兑换接口之一完成兑换流程。 - -| 方式 | 接口名 | 需游戏方校验后台设置的兑换条件 | 需游戏方提供发送道具接口 | -| -------------- | -------------------------------- | :----------------------------- | :----------------------- | -| 游戏方校验 | /api/v1.0/cdk/game/submit-check | ✓ | ✓ | -| 游戏方发送 | /api/v1.0/cdk/game/submit-send | ✗ | ✓ | -| 无服务器兑换 | /api/v1.0/cdk/game/submit-simple | ✗ | ✗ | -| 游戏方校验 Web | /api/v1.0/cdk/page/submit-check | ✓ | ✓ | -| 游戏方发送 Web | /api/v1.0/cdk/page/submit-send | ✗ | ✓ | - -### 接入顺序 - -- 完成应用配置获取 **Client ID 和 Server Secret** 信息。 -- 进入 **运营工具-礼包系统-服务器配置** 选择性填写游戏方服务器信息并在其中配置发送通知接收地址用来发送相应道具。 -- 创建礼包活动导出兑换码,礼包配置完成准备测试。 -- 根据实际业务场景使用对应兑换接口完成兑换。 - -### 兑换流程 - -兑换流程大致是: - -1. 根据接口文档提交 **兑换码** 与 **配置信息等参数** 给兑换接口。 -2. 兑换接口校验数据合法性。 -3. 兑换系统根据业务场景选择是否调用 **游戏方提供的二次校验接口**。(可选,当后台配置二次校验接口时会调用) -4. 校验全部通过后根据不同接口判断是否调用 **游戏方提供的发送道具接口** 完成兑换。(可选,当后台配置发送道具接口时会调用) - -有三种不同的兑换接口可以进行兑换,整体对接流程只要对接其一接口即可完成: - -| 名称 | 描述 | -| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| 游戏方校验 | 如果您将使用服务器对接兑换系统,可以安全地保存密钥信息,并希望每次兑换时能通过您的应用程序来校验是否允许兑换,调用您的接口来发送道具,请使用此流程。 | -| 游戏方发送 | 如果您将使用服务器对接兑换系统,可以安全地保存密钥信息,并希望调用您的应用程序接口来发送每次兑换的道具,请使用此流程。 | -| 无服务器兑换 | 如果您希望不开发接口完成兑换流程,调用兑换接口后根据返回值来判断是否兑换失败,请使用此流程。 | - -### 三种兑换场景 - -#### 游戏方校验 - -当您请求兑换码 submit-check 接口进行兑换时,兑换系统会核对库存以及提交数据是否正确,兑换系统核对通过后会调用您在后台配置的二次校验接口并将后台设置的条件作为参数传给您来校验。 -当您的接口校验通过后会继续将后台配置的奖品内容作为参数调用您的发送道具接口,当发送道具成功完成后本次兑换完成。 -![游戏方校验泳道图](/img/tds-gift/submit_check.png) - -#### 游戏方发送 - -当您请求兑换码 submit-send 接口进行兑换时,兑换系统会核对库存以及提交数据是否正确,核对完成后会继续将后台配置的奖品内容作为参数调用您的发送道具接口,当发送道具成功完成后本次兑换完成。 -![游戏方发送泳道图](/img/tds-gift/submit_send.png) - -#### 无服务器兑换 - -当您请求兑换码 submit-simple 接口进行兑换时,兑换系统会核对库存以及提交数据是否正确,兑换信息验证通过后本次兑换完成并将后台配置的道具信息返回给您。 -![无服务器兑换泳道图](/img/tds-gift/submit_simple.png) - -## 对接兑换接口 - -### 游戏方二次校验规范 - -礼包活动在后台配置了自定义兑换条件后,使用带二次校验能力的接口进行兑换时,兑换系统会将此礼包活动的自定义兑换条件发送给游戏方进行二次校验,下图配置会对您在后台配置的接口发起如下请求: - -![自定义兑换条件](/img/tds-gift/custom_redemption.png) - -``` -curl --location -g --request POST <后台配置的游戏方二次校验接口地址> \ ---header 'Content-Type: application/json' \ ---data-raw '{"activity_id":<后台活动ID>,"character_id":<用户ID>,"content":"[{\"condition\":\"level\",\"operate":\"gt\",\"value\":\"10\"}]","content_obj": [{"condition":"level","operate":"gt","value":"10"}],"ext":<兑换提交的ext参数>,"gift_code":<礼包码>,"nonce_str":"abcdc","server_code": <后台配置的code>,"sign":"42d2b58a8cd58b5e63d90524aaf63ef97e04f223","timestamp":1658825322,"custom":{<自定义维度>:[<自定义维度字段1>,<自定义维度字段2>]}}' -``` - -请在校验完成后返回带有特定字段的 JSON 返回值告诉兑换系统校验结果。 - -```json -{ - "status": true, - "errorCode": "字符串类型的错误code" -} -``` - -如果您希望返回字段命名风格保持一致也可以使用下划线命名风格返回信息。 - -```json -{ - "status": true, - "error_code": "字符串类型的错误code" -} -``` - -如果您并不要求兑换系统知晓兑换的错误信息可以直接返回 **非 200 HTTP code** 表示兑换失败,或者返回 **HTTP code 200** 表示兑换成功。 -这样就无需按兑换系统的格式来返回信息。 - -| 请求参数 | 类型 | 含义 | -| --------------------- | ----------------------------- | -------------------- | -| activity_id | 字符串 | 礼包活动 ID | -| character_id | 字符串 | 兑换的用户 ID | -| content | 字符串(嵌套 JSON 字符串) | 后台配置的自定义条件 | -| content.condition | 字符串 | 自定义条件-key | -| content.operate | 字符串 | 关系-详情见其他 | -| content.value | 字符串 | 自定义条件-value | -| content_obj | 对象数组(和 content 含义相同) | 后台配置的自定义条件 | -| content_obj.condition | 字符串 | 自定义条件-key | -| content_obj.operate | 字符串 | 关系-详情见其他 | -| content_obj.value | 字符串 | 自定义条件-value | -| ext | 字符串 | 请求接口时传递的数值 | -| gift_code | 字符串 | 用户此次兑换的礼包码 | -| nonce_str | 字符串 | 此次请求的随机字符串 | -| server_code | 字符串 | 服务器 code | -| sign | 字符串 | 签名 | -| timestamp | 数字 | 秒级时间戳 | -| custom | 对象 | 维度信息 | -| custom.xxx | 字符数组 | 自定义维度 | - -| 返回参数 | 类型 | 含义 | -| --------- | -------------- | ----------------------------------------------- | -| errorCode | 字符串 | 错误类型,error_code 含义相同,优先取 errorCode | -| ext | 字符串(可选) | 需要透传给发送道具接口的字符串 | -| status | 布尔值 | 是否校验成功,true 为成功,false 为失败 | - -### 游戏方发送道具规范 - -后台设置的物品信息,在兑换系统完成兑换校验后会带上配置的参数请求 **游戏方的发送道具接口**。下图配置会对您在后台配置的接口发起如下请求: -![自定义兑换条件](/img/tds-gift/custom_prizes.png) - -``` -curl --location -g --request POST <后台配置的游戏方发送道具接口地址> \ ---header 'Content-Type: application/json' \ ---data-raw '{"activity_id": <后台活动ID>,"character_id":"uid","content":"[{\"name\": \"奖品名称\", \"number\": 2}]","content_obj": [{"name": "奖品名称", "number": 2}],"ext":<兑换提交的ext>,"gift_code":"gift_code","nonce_str":"abcdc","server_code":<后台配置的code>,"sign":"fbe23e6f6f5193355b29e9ea2913b1992af56346","check_ext":<二次兑换接口返回的ext>,"timestamp":1658827492,"custom":{<自定义维度>:[<自定义维度字段1>,<自定义维度字段2>]}}' -``` - -同样请在发送完成后返回带有特定字段的 JSON 返回值告诉兑换系统发送道具结果,也可以使用下划线命名返回字段。 - -```json -{ - "status": true, - "errorCode": "字符串类型的错误code" -} -``` - -如果您并不要求兑换系统知晓兑换的错误信息可以直接返回 **非 200 HTTP code** 表示兑换失败,或者返回 **HTTP code 200** 表示兑换成功。 -这样就无需按兑换系统的格式来返回信息。 - -| 请求参数 | 类型 | 含义 | -| ------------------ | ----------------------------- | ---------------------------- | -| activity_id | 字符串 | 礼包活动 ID | -| character_id | 字符串 | 兑换的用户 ID | -| server_code | 字符串 | 服务器 code | -| content | 字符串(嵌套 JSON 字符串) | 后台配置的自定义奖品 | -| content.name | 字符串 | 奖品名称 | -| content.number | 数字 | 奖品数量 | -| content_obj | 对象数组(和 content 含义相同) | 后台配置的自定义奖品 | -| content_obj.name | 字符串 | 奖品名称 | -| content_obj.number | 整型 | 奖品数量 | -| ext | 字符串 | 请求接口时传递的数值 | -| gift_code | 字符串 | 用户此次兑换的礼包码 | -| nonce_str | 字符串 | 此次请求的随机字符串 | -| sign | 字符串 | 签名 | -| check_ext | 字符串 | 二次校验透传过来的字符串参数 | -| timestamp | 数字 | 秒级时间戳 | -| custom | 对象 | 维度信息 | -| custom.xxx | 字符数组 | 自定义维度 | - -| 返回参数 | 类型 | 含义 | -| --------- | ------ | ----------------------------------------------- | -| errorCode | 字符串 | 错误类型,error_code 含义相同,优先取 errorCode | -| status | 布尔值 | 是否校验成功,true 为成功,false 为失败 | - -### 无服务器兑换 - -使用无服务器兑换接口进行兑换时无需游戏方开发校验与发送接口。 - -但要注意在 **后台配置礼包时需创建无服务器礼包**。 - -兑换系统在接收到兑换请求后直接将兑换结果返回,游戏根据返回结果判断是否发放道具。 - -接口参数的详细说明请看文档下面的 [三种兑换接口 - 无服务器兑换接口](/v4/sdk/tds-gift/guide/#无服务器兑换接口) - -### 维度配置 - -在礼包后台中可以配置自定义维度信息,配置完成后创建活动礼包时可以勾选上相应维度信息。这些勾选上维度的礼包在二次校验,发送道具,以及无服务器返回结果里会带上勾选的维度参数,方便您对礼包做进一步分类业务的处理。 - -### 请求域名 - -| TapTap 版本 | 域名 | -| --------------- | ------------------------- | -| 国内 taptap.cn | https://poster-api.xd.cn | -| 海外 taptap.io | https://poster-api.xd.com | - -### 签名 - -游戏方与兑换系统的接口通讯时将使用签名参数作为基础的安全验证,其中 **Secret 为后台 游戏服务 - 应用配置 中 Server Secret**。 - -:::tip -为了保证安全性,请勿将 Server Secret 信息放在客户端中使用以防敏感信息泄漏。 -::: - -计算签名伪代码如下: - -``` -sign == sha1(timestamp + nonce_str + secret) -``` - -您也可以通过单元测试验证签名计算过程正确与否,如下是 golang 示例: - -```go -func TestMakeSign(t *testing.T) { - timestamp := 1655724586 - nonceStr := "abcde" - secret := "abc" - sign := MakeSign(int64(timestamp), nonceStr, secret) - assert.Equal(t, sign, "3cb8c38833fa742e7873378faddcbe5b56088482") - //output: 3cb8c38833fa742e7873378faddcbe5b56088482 -} -``` - -为了 Secret 不存放在客户端中,无服务器兑换返回的签名通过 **Client ID 代替 Secret 进行验签**。 - -``` -sign == sha1(timestamp + nonce_str + client_id) -``` - -``` -c_sign == sha1(timestamp + content + client_id) -``` - -### 二次校验兑换接口 - -POST /api/v1.0/cdk/game/submit-check - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------ | ------ | -------- | ------------------------------------------------- | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| server_code | 字符串 | 必传 | 后台配置的服务器 code | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| sign | 字符串 | 必传 | 签名 | -| timestamp | 数字 | 必传 | 时间戳,单位为秒,有效期为一分钟 | -| ext | 字符串 | 非必传 | 该字段会原封不动出现在 ext 中传给游戏二次校验接口 | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -请求示例: - -``` -curl --location --request POST '/api/v1.0/cdk/game/submit-check' \ ---header 'Content-Type: application/json' \ ---data-raw '{"client_id":<后台ClientId>,"gift_code":<这次使用的兑换码>,"server_code":<服务器code>,"character_id":<游戏用户ID>,"nonce_str":<随机字符串>,"sign":<签名>,"timestamp":<时间戳>,"ext": <透传字段>}' -``` - -### 无二次校验兑换接口 - -POST /api/v1.0/cdk/game/submit-send - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------ | ------ | -------- | -------------------------------- | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| server_code | 字符串 | 必传 | 后台配置的服务器 code | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| sign | 字符串 | 必传 | 签名 | -| timestamp | 数字 | 必传 | 时间戳,单位为秒,有效期为一分钟 | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -请求示例: - -``` -curl --location --request POST '/api/v1.0/cdk/game/submit-send' \ ---header 'Content-Type: application/json' \ ---data-raw '{"client_id":<后台ClientId>,"gift_code":<这次使用的兑换码>,"server_code":<服务器code>,"character_id":<游戏用户ID>,"nonce_str":<随机字符串>,"sign":<签名>,"timestamp":<时间戳>,"ext": <透传字段>}' -``` - -### 无服务器兑换接口 - -POST /api/v1.0/cdk/game/submit-simple - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------ | ------ | -------- | ----------------------------------- | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| sign | 字符串 | 必传 | 签名,该签名用 ClientId 代替 Secret | -| timestamp | 整型 | 必传 | 时间戳(秒) | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -返回参数: - -| 返回参数 | 类型 | 含义 | -| ------------------ | ----------------------------- | -------------------------------- | -| activity_id | 字符串 | 礼包活动 ID | -| nonce_str | 字符串 | 随机字符串 | -| timestamp | 整型 | 时间戳 | -| content | 字符串(嵌套 JSON 字符串) | 后台配置的自定义奖品 | -| content.name | 字符串 | 奖品名称 | -| content.number | 数字 | 奖品数量 | -| content_obj | 数组对象(含义与 content 相同) | 后台配置的自定义奖品 | -| content_obj.name | 字符串 | 奖品名称 | -| content_obj.number | 整型 | 奖品数量 | -| sign | 字符串 | 返回签名,验证返回数据没有被修改 | -| c_sign | 字符串 | 内容签名,验证返回数据没有被修改 | -| error | 整形 | 0 为成功,其他为失败码 | -| success | 布尔 | 是否成功 | -| custom | 对象 | 维度信息 | -| custom.xxx | 字符数组 | 自定义维度 | - -请求示例: - -``` -curl --location --request POST '/api/v1.0/cdk/game/submit-simple' \ ---header 'Content-Type: application/json' \ ---data-raw '{"client_id":<后台ClientId>,"gift_code":<这次使用的兑换码>,"character_id":<游戏用户ID>,"nonce_str":<随机字符串>,"sign":<签名>,"timestamp":<时间戳>}' -``` - -
    -Shell 请求示例: - -``` -#! /usr/bin/env bash - -# TapDC 后台应用的 Client ID -client_id="请替换为控制台的 `Client ID`" -# 本次兑换的兑换码 在“礼包活动面板-数据-导出” 然后导出的文件里面可以看到格式如 S0LLC8ICB2MP2 的兑换码 -gift_code="请替换为礼包面板中的兑换码" - -# 随机字符串,建议五位随机字符串 -nonce_str="A2B3Z" -# 游戏用户 ID -character_id="6347de128b****3ee825e029" -# 签名,该签名用 ClientId 代替 Secret -signed=$(echo -n $(date +%s)$nonce_str$client_id |openssl sha1) -RESULT_REQUEST=`curl --location --request POST 'https://poster-api.xd.cn/api/v1.0/cdk/game/submit-simple' \ ---header 'Content-Type: application/json' \ ---data-raw "{\"client_id\":\"$client_id\",\"gift_code\":\"$gift_code\",\"character_id\":\"$character_id\",\"nonce_str\":\"$nonce_str\",\"sign\":\"$signed\",\"timestamp\":$(date +%s)}"` - -echo $RESULT_REQUEST -``` - -
    - -
    -Android 请求示例: - -可以参考[ Android Demo ](https://github.com/taptap/TapSDK-Android-Demo/blob/main/app/src/main/java/com/tds/demo/fragment/GiftFragment.java) 中礼包系统功能 - -
    - -
    -C# 请求示例: - -``` -using UnityEngine.Networking; -using System; -using System.Text; -using System.Security.Cryptography; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -public class APIClient : MonoBehaviour -{ - private const string apiUrl = "https://poster-api.xd.cn/api/v1.0/cdk/game/submit-simple"; - private const string contentType = "application/json"; - private const string clientId = "hskcocvse6x1cgkklm"; - private const string giftCode = "NZ4mp2cztRMXH"; - private const string characterId = "879a0cdd9nnb917055ee"; - private const string nonceStr = "RFG7U"; - - public GUISkin demoSkin; - - - void Start() - { - - } - - - private void OnGUI() - { - - GUI.skin = demoSkin; - float scale = 1.0f; - - - float btnWidth= Screen.width / 5 * 2; - float btnWidth2 = btnWidth + 80 * scale; - - float btnHeight = Screen.height / 25; - float btnTop = 30 * scale; - float btnGap = 20 * scale; - - GUI.skin.button.fontSize = Convert.ToInt32(13 * scale); - - var style = new GUIStyle(GUI.skin.button) { fontSize = 20 }; - var labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 30 }; - - if (GUI.Button(new Rect((Screen.width - btnGap) / 2 - btnWidth, btnTop, btnWidth /2, btnHeight), "返回", style)) - { - UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(0); - - } - - btnTop += btnHeight + 20 * scale; - - if (GUI.Button(new Rect((Screen.width - btnGap) / 2 - btnWidth, btnTop, btnWidth, btnHeight), "无服务器兑换", style)) - { - StartCoroutine(StartRequest()); - } - - - } - - private IEnumerator StartRequest() - { - // 获取当前时间戳(秒) - int timestamp = (int)(System.DateTime.UtcNow.Subtract(new System.DateTime(1970, 1, 1))).TotalSeconds; - - // 拼接并加密 sign 参数 - string sign = GetSign(timestamp, nonceStr, clientId); - - // 构建请求参数 - string requestBody = "{\"client_id\":\"" + clientId + "\",\"gift_code\":\"" + giftCode + "\",\"character_id\":\"" + characterId + "\",\"nonce_str\":\"" + nonceStr + "\",\"timestamp\":" + timestamp + ",\"sign\":\"" + sign + "\"}"; - - // 创建 UnityWebRequest 对象 - UnityWebRequest request = new UnityWebRequest(apiUrl, "POST"); - - // 设置请求头 - request.SetRequestHeader("Content-Type", contentType); - - // 设置请求体 - byte[] bodyRaw = Encoding.UTF8.GetBytes(requestBody); - request.uploadHandler = new UploadHandlerRaw(bodyRaw); - request.downloadHandler = new DownloadHandlerBuffer(); - - // 发送请求 - yield return request.SendWebRequest(); - - // 处理响应 - if (request.result == UnityWebRequest.Result.Success) - { - Debug.Log("API request succeeded"); - Debug.Log(request.downloadHandler.text); - } - else - { - - Debug.Log("API request failed: " + request.error); - Debug.Log(request.downloadHandler.text); - - } - } - - private string GetSign(int timestamp, string nonceStr, string clientId) - { - // 拼接参数并进行 SHA1 加密 - string signString = timestamp.ToString() + nonceStr + clientId; - byte[] signBytes = Encoding.UTF8.GetBytes(signString); - byte[] signHash = new SHA1CryptoServiceProvider().ComputeHash(signBytes); - string sign = BitConverter.ToString(signHash).Replace("-", "").ToLowerInvariant(); - - return sign; - } -} - - -``` - -
    - -返回示例: - -```json -{ - "activity_id":"TDS20220928151122U9H", - "content":"[{\"name\": \"奖品\", \"number\": 2}]", - "content_obj":[ - { - "name":"奖品名称", - "number":2 - } - ], - "nonce_str":"0DD4B", - "sign":"5895f87d3dfb5e0918c1b195c015d9284609d122", - "timestamp":1664354023, - "success":true, - "error":0, - "c_sign":"12c6fc06c99a462375eeb3f43dfd832b08ca9e17", - "custom":{ - "seasons":[ - "101", - "102" - ] - } -} -``` - -#### 返回格式 - -成功返回 - -```json -{ - "success": true, - "messages": "成功", - "error": 0 -} -``` - -错误返回 - -| 返回参数 | 含义 | -| ---------------- | ------------ | -| error | 错误码 | -| message | 错误大致信息 | -| info | 详细信息 | -| info.dev_message | 开发提示 | -| info.hint | 详细提示 | - -```json -{ - "error": 100001, - "message": "输入有误", - "info": { - "dev_message": "", - "hint": "test error" - } -} -``` - -### Web 嵌入兑换接口 - -兑换系统提供了一套支持简易图形验证码的兑换接口,用来支持类似 Web 活动页的接入,与上述兑换接口流程相同但区别在于部分请求参数的改变。 - -#### 获取验证码 - -:::tip -验证码有效期为五分钟,失效后请重新获取新的验证码。 -::: - -GET /api/v1.0/cdk/page/captcha-img - -Header Content-Type:application/json - -| 返回参数 | 含义 | -| -------- | -------------------------------- | -| img | base64 格式的图片信息 | -| key | 验证码 key,提交兑换时会需要带上 | - -返回示例: - -```json -{ - "img": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAA8CAYAAAAjW/WRAAANc0lEQVR4nOydCXAUV3qA/z7mHs2t0bWSkJCEECCO5Ta2w5GFmMCGrGHNemuXXZwYluxhQrEVkrWzIkUS29hlOxgfYFypJBATbONgG7ABGxwOg7GQQAJJCB0gCWkOzT3T09Od6haSR6PRXJpL0vuqKKZfv379uvt9+t97/UYihdWf/AEQCERQ8FRXAIFIZ5AgCEQIkCAIRAiQIAhECJAgCEQIkCAIRAiQIAhECJAgCEQIkCAIRAjIVFcgnTAUaqpSXQcOXavp2VTXAdHHuBMkXSQIRag6InmSy5gVZDSIEAvBrgtJkzjGjCBjVYhIQNIkjlEpSKJkSJdGFY/rCywjXa5ttIGNluXu8ZJitDcUdB+SS9oKMtKGMJ4aQDykGU/3KxrSSpCRPOgx+YBZwHLqjeqcelO2ssupETm80uofTrzcNUltDndoOt5LpdWare61lEvdrmLSS+twlpWwADQlFN4zqtUXujN1txJx3uPz1w17Lw7985Ihae8u3jRw/SkVJNaHOCZl8OPx7efWYwyLszjG2LVio6lA0WXOk5szDC75xPMd3z9atfAQJSXpaMpM5b1WWa05uZ1dj3HtzSaX19rksrsOqdRIkyQl8nhkOpO5XGs2L7VmyC/fKSg4NdLzhRICgkjhL0QgSR+kIynC4xWTLtzHkAIXLXVoxBbAAPJuGPKVnU49J46iyyE1FCut0ZQZeP8ifQ7++WJ5BlqTqSjLYFjcmaU/aVap2gP3u8Vi293cnMu9SsXtiS2tT2caja09Wm1TqDI3nHljoE5P/N3psHVYcfG9mNtOUgSJRYrxJEQg3SXKNhbDWMCA1Tf1FiruO3N6ipVNGMMQDo3YSEkFUUWPYMQiTCyy6A3GR4xq9flgcvhjl8lMZqXyrNZknr+y5vDPhssXToiRyBAM7Ml1k9nhdh7esTvmkyEpYkfXbFEUX+qa4FSLHPnVPSViGyW36aWGa39eXG0oUlhZHBv2mcWDaJ9dqOc2/UbdDqNaffJeTta3ABirNxhKVRbrzAynrQJnfVBgawC518Ln9RBiaFJOhymmS/x2oqNDJAwag6zd9bcR35hg8sTzxiL6WLzn2kMZ3c7Mrsma5q+fmFST7PPH+kz7u0Gdsgkg9LnAKtSCm5QChYsH8hbabkKXtADKzVf5ccCxRT+p+uOr22Hnr3YFLTvRMgQj4kF6NPLsffPAsPuQFNHBCSI3uLQdU7RN3zxeWpfKuvjLsu3d4I04EK6f2JZRBr2iTGABGJdEUmdUq7555y9/83O3WASv/OMm+MMzL/J5W0oL4H+fWAG/3vkWv50KIQIZdgxSfP9qVlnHhTKZx6JgMJzp/uGMD6+UrLrhEKk8EHCzNj/9i0HHBm77sxagaiRdt3BgHpK/Jlbg8wHOsuDDccxDEIRVJMJtQhFVajIm6twJgWUxWkhQMqNLkczT+g+E+9kWIv+LG3YMu+/05DVwdcF0mHnxGq4026YCAPcPGqaWwKwLfUGRk6HsdvOP3CJR54qL752Py0XEgSERRGu7K3/s6r+t1lnbygMzC4USWPHo38C0SUvDFuwfKUbadYsKBsN4MYJAGCUS8bdZhY5lLTdHdI4k8sjbtfNENq/UkiszxruLFUyC4Qg1FRrYDbuSsyxkWZwMOpOpKNNgfLSxuOi/aJKk5A6Hpqi1bUPdpLLXfAThjbReiWaQIApnj2T9uX/4Kyll03kEst6zFT858umcP91YXXcCTp/fz+fBcQKe2fjfIBbJhxQWS/cp7vI8EERYr8tUHaxYjzkEGm+hpcb0myvvc7vV+2Ysd1d217vmdrRFW9dUMPfgrUpVhz2TITDf57+b9UU0x0YjAISRYDjCvXPgmN35+aDtoy0dcPLufTiqz37eLRLZubTyxqYnzSrllfuZmQl5WRgrg7pYq668vJqTg8Vw36/X7lZVaSZs5NIXzFoLnd0NUN90DgSkCAh8cM9sJOOKUI0+UJ5QMg2U8yB6ZBwr+QHmJFWOZS1HOGnILrmczrbbvbm2TtENXeloEcSlFDp1LT4BsCAM3JcMAQIJJ4T/uCHYAP/V603QbnfCC/MrYQ9JbOfSDt1uh5cF5MV0kwP8I0hl66mipTX7+cHDjIrlsHLJM4MyUl4X1DeehbzsctBpClM+2B5OFm5QiAELF41nQCM8C2rhV3B22+p/0r4895c9z371hvTLglLx9czJpi3ffJT8WkcHJwBmEABRJwegMaD/xBTyS9LxEMCfSKJDpANpTpY365qhy+WGv581GUgMA5plYc/1JtCKRfDT0oKBvKluW/7wgnCV/+DELqhrPMsn/nTNv0Jh3vQhmdOp4sOBm8XiH+3dsuOcoRrK5M8CidvASU+AFudv4WHdDLB6Z8Bd18+gQrGVz5/ICYNICBcFMDsBxGUlL8a+59Y9x9ufIKKJDlGCldy5s1LgpTVfLJtbQmAYmD0UvHCtAdYU5cKcTE3Qg9KhvfXN+LAMNLZ8zSdgGAZ52ZP5zxZbN6y6dn6fXax2tmVOM6S2qpFBWERirtFnb4Wq6qmbPnHN7ryl2TtrC2AgOLxj93Mrq16volnVQP5wY6CRCjTSbpDARZMrL3292iMROOVGl8iuk7hHUp9+4hkdwlHYfneRyEPl3SqZeCCrzUxxaRW3Gp4SUVTB5W7ToLw4hsEUtQJeWjh9xMtc4gEviNHcDl5v332XSdVw8/ZXcPDLN7ulHot+OcBTXLpDpLx/ZtqGDxpz5nWkoqKRgnkJgvufxYBlZF6X/ETxQ5gPE/oyqG4u/fjmbf+i2TN1w+Edu18Pdny0455EjwO8EpKmZCQnh1bTblPEKkgCo0NYVFbrwi595oc0SVL9aXWTyvb55/GX4Y265iFl9O9Ptii8IHbndxbbHSY4evJ5EBLCDLM857aYsqsklE0r81iy/uzq67+g5wjfvqOf2Z3MSkYFjfO9dFbgc/q0LovsdOEaPjnf1sj9z8i8XozBhn3/Exgx/AVwXGgclJeTxxFhOdGib+pVVpxsnSozuVU4zRD3puluVh5rnjv/P+rVDo3Y+Onv5xxnSJwJdmwyo0MksBgwDI77QuXxb/hb3dSw9U92VOEbyqZbNf/5MMCT/YltmVMvHZv9uxMeUkpzl7fu/M7Vecab3ycYr2hJzYG/2L9s5luJrljMPJjF8kw1nJedzV+EsYDTWlcrK/DxLzgJk0QC7OChbqRRQLagdNgIENOMWwhmv9ewsHledj0lFdzRtVh1tBCn75eqWyU2qierwVySU2/S3Jum47u9qYwOkeASSxrlDkd+uFW6/Zww9La/1Gp6O9wyl2TIwgtCCaQDoc8jkFqOzN/x8XdZMPazyqc+//kX22dgLEMoXIbvlXVcyGvIXXAvERUaKayI5le6+jQui+RqVhn3eUrWkUKCdRUWnDm5xGwqh1q2AB4LIUUss0Hxnq4mKZ9w2vGWh2gBQblUol5um5KQ7lUnPn7Uiwvgx5v/fZKcDr7iPdVCBGJWKmv0BsNiADgTJiuGsQyOMQy/YMu/0adKFl4Qi1RnH0jwUZLATGZ5rsMq0d5TOnv4ubgJ3TWF6SSIfwTweJQgumOB6rqt0I1/D8QSA6jUN0Gp7vvhxQlixzWGdxdvejVZ9YtFHoKRgdCjh0eutgPB0ECwfT0UAUMBBgz0y5FuMgTDoNXcyenuXjWz9npEkdqSkfF/gWn9jT6aZfnxEIUXpF07xcjghBdnfAKCoYUqR5e0V5bt9M/oEUgHJBJ77bKRnjgaohkIswzZFwEWc9X/kk9rBu6H12K+e6X9bN4WVkx7ElnfaPCXZ2hXaeiKmP2r8x986nsE/UKlero6DGzt5PJX4lFQsqMKLwiDk4xFqm9T2zsnctslnZfzr5SsGvRWU0i7pf2fXcIMeywnC0W81gXhvWIx+CicMEgkPr3TCT4ME3TKM8TVWUXiK9mLcDehoLIcQ6dJUsBIxg7RdNsg/QWKiWTIMvAmfX7DkSkLbh35Mfe5U11afWjRH9/vzySmbIK//uxXvycYH7/c4YN5219r0c/oieZikroswofjGR+VzBQ1aCpwm0iHeXEJ0LgACMZL5ziaLOvrPqGz7XGXPBTJnllK6gLRNCOa77CEk2VAEJyh8Y2nfvu03G3OYQFjT1X+8kBt4dIWbt/yb/cuqrh77gfc5y7VxJqDD+/8n8CCUrEuKF1Jt2nWQMaLPPH4At+g1bw55gbV4xd2beYG6iyG+wyK/AacoUmt7V4pt58mMsAuqQQWiLAnG8sCBJLu06zRMFbliVWWId8HKeip1S2tfWe1ynF/Qn8ai5HgEeSCS1gw8CdFxpMA/qR7dEgkY0WeSGXhJBn2K7dZvc1KnbVN5RQp3K2ZlT3cQD7uNR0FjKXokEhGqzzh/tREWv1mxVQznqNDIolGHkihQMF+4fe4FgRFh/QgHaMPJ8u4EgRFh9FJquUZs4Kg6DD2ScbL0TEhCIoOiEQxKv/CFIoOiGSR9oKg6IBIJWknCIoOiHQipYKg6IBId5ImCJIBMRpJmCCoq4QYC8RFEBQdEGOVmARB0QExXggrCIoOiPHMEEFQdEAgvoMXJJQUSAjEeGZMrMVCIBLF/wcAAP//ettpiq6bhWEAAAAASUVORK5CYII=", - "key": "XfNTN1gQyvA2AcNOu1UN" -} -``` - -#### 二次校验网页版 - -POST /api/v1.0/cdk/page/submit-check - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------------- | ------ | -------- | ------------------------------------------------- | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| server_code | 字符串 | 必传 | 后台配置的服务器 code | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| verify_captcha_key | 字符串 | 必传 | 验证码 KEY | -| verify_captcha_code | 字符串 | 必传 | 用户提交的验证码答案 | -| ext | 字符串 | 非必传 | 该字段会原封不动出现在 ext 中传给游戏二次校验接口 | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -#### 无二次校验网页版 - -POST /api/v1.0/cdk/page/submit-send - -Header Content-Type:application/json - -请求参数: - -| 请求参数 | 类型 | 是否必选 | 含义 | -| ------------------- | ------ | -------- | ------------------------------ | -| client_id | 字符串 | 必传 | 开发者后台 ClientId 信息 | -| gift_code | 字符串 | 必传 | 这次兑换的兑换码 | -| server_code | 字符串 | 必传 | 后台配置的服务器 code | -| character_id | 字符串 | 必传 | 游戏用户 ID | -| nonce_str | 字符串 | 必传 | 随机字符串,建议五位随机字符串 | -| verify_captcha_key | 字符串 | 必传 | 验证码 KEY | -| verify_captcha_code | 字符串 | 必传 | 用户提交的验证码答案 | -| lang | 字符串 | 非必传 | 提示的语言类型,详情见其它 | - -## 其他 - -### 错误码 - -| 自定义状态码 | 含义 | 复现场景 | -| ------------ | ---------------------------------------------- | ---------------------------------------- | -| 100001 | 输入有误 | 发起请求的参数错误或者缺失部分必要参数 | -| 100004 | 验证码输入错误,请重试 | 校验图形验证码错误 | -| 100005 | 参数错误 | 提交的 ClientId 或者 ServerCode 错误 | -| 100006 | 礼包码次数已达上限 | 通兑码可领取次数达到上限(正常情况) | -| 100025 | 礼包码已经兑换过了 | 唯一码已经被兑换过了 | -| 100008 | 礼包码已经过期了 | 礼包活动已过期 | -| 100009 | 礼包活动暂未开放 | 礼包活动暂停兑换,还没到活动开始时间 | -| 100010 | 超出服务使用范围 | 提交的 ServerCode 错误,不在礼包设置的范围 | -| 100011 | 点击过快,请稍候再试 | 唯一码连续提交 | -| 100012 | 礼包码次数已达上限,请稍候再试 | 通兑码库存不足(并发兑换时可能触发) | -| 100013 | 校验 cdkey 未通过 | 二次校验游戏方返回了不通过结果 | -| 100014 | 操作的人太多了,服务器正在拼命处理中 | 兑换系统已达流量极限 | -| 100015 | 发送道具失败 | 发送道具游戏方返回了失败结果 | -| 100016 | 该礼包码无效 | 异常兑换码的提交 | -| 100017 | 礼包码次数已达上限 | 相同用户使用礼包中超过礼包设置的兑换数量 | -| 100018 | 该活动无法使用此兑换接口 | 礼包无服务器设置错误 | -| 100022 | 该国家地区不支持此兑换码 | 非此 ClientId 的唯一兑换码 | -| 500001 | 服务器故障 | 礼包系统出现通用性错误 | -| 500002 | 外部接口校验错误 | 与游戏方接口通讯发生了异常 | - -### 多语言 Lang 参数 -| Lang | 语言 | -|---------------|-------------| -| ar_AR | 阿拉伯语 | -| de_DE | 德语 | -| zh_TW | 中文(繁體)| -| zh_CN | 中文(简体)| -| en_US | 英语 | -| es_ES | 西班牙语 | -| fr_FR | 法语 | -| id_ID | 印度尼西亚语 | -| it_IT | 意大利语 | -| ja_JP | 日语 | -| ko_KR | 韩语 | -| pt_PT | 葡萄牙语 | -| ru_RU | 俄语 | -| tr_TR | 土耳其语 | -| vi_VN | 越南语 | - -### 自定义条件关系表 - -| 关系 code | 含义 | -| --------- | -------- | -| lt | 小于 | -| le | 小于等于 | -| eq | 等于 | -| ne | 不等于 | -| ge | 大于等于 | -| gt | 大于 | - -### 兑换系统出口 IP - -如果您配置的接口对请求 IP 有安全限制,请根据需要允许兑换系统的 IP 访问。 - -| TapTap 版本 | IP | -| --------------- | ------------- | -| 国内 taptap.cn | 59.110.228.98 | -| 海外 taptap.io | 8.214.95.148 | diff --git a/versioned_docs/version-v4/sdk/tds-gift/no-server-guide.mdx b/versioned_docs/version-v4/sdk/tds-gift/no-server-guide.mdx deleted file mode 100644 index 7b28bcd1a..000000000 --- a/versioned_docs/version-v4/sdk/tds-gift/no-server-guide.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: 无服务器兑换开发指南 -sidebar_label: 无服务器兑换 -sidebar_position: 3 ---- - -:::tip -这篇是说明无服务器兑换方式的接入流程,如果需要完整开发内容请看 [**开发指南**](/v4/sdk/tds-gift/guide/)。 -::: - -## 介绍 - -对接礼包兑换码需要游戏方自行维护一到两个 Web 接口是比较繁琐的,固我们简化兑换的流程为: - -**游戏方请求兑换接口进行兑换,根据接口返回的信息判断兑换是否成功。** - -这样游戏方只需调用一个接口就能完成对接。 - -:::tip -客户端直接收到兑换结果是可以被截取的,所以此方式安全系数没有其他方式高。 -::: - -## 对接 - -### 兑换接口 - -域名按开发者中心分为两个地址,两个地址的礼包数据是不互通的: - -| TapTap 版本 | 域名 | -| --------------- | ------------------------- | -| 国内 taptap.cn | https://poster-api.xd.cn | -| 海外 taptap.io | https://poster-api.xd.com | - -兑换接口参数在[**开发指南 - 无服务器兑换**](/v4/sdk/tds-gift/guide/#无服务器兑换接口)中有详细介绍。 - -发起兑换时需要生成签名,无服务兑换接口签名是由 clientId 等信息计算出来的,签名计算过程见[**文档**](/v4/sdk/tds-gift/guide/#签名)。 - -### 返回值 - -#### 结果 - -根据返回值中的 error 字段是否为 0 来判断兑换行为是否成功,当 error 字段判断不为 0 时可以根据[**错误码**](/v4/sdk/tds-gift/guide/#错误码)来查看具体原因。 - -#### 验证 - -为了防止返回信息未被窜改,建议您做以下措施: -1. 为防止重放攻击您需要校验下返回时间戳是否与客户端时间相差过多; -2. 根据返回值计算出签名,验证是否与返回值里的签名相同,签名计算过程见[**文档**](/v4/sdk/tds-gift/guide/#签名); -2. 建议奖品内容的 name 可以是加密后的名称。 - -#### 奖品内容 - -根据返回值中的 content 或者 content_obj 字段都可以获取该兑换码对应的奖品内容,而区别是 content 是一段嵌套的 json 字符串,直接按对象方式解析可能会报错。 - -其中奖品内容设置是不限中英文的,为了方便开发判断建议最好维护一份道具表。 - -## 其他 -### 开发环境 - -使用无服务器兑换接口区分测试正式环境需在开发者中心新建不同应用来完成。 - -如果无法分应用处理环境问题建议使用[无二次校验兑换接口](/v4/sdk/tds-gift/guide/#无二次校验兑换接口)通过配置服务器信息,配置服务器 code 转发到不同环境完成兑换。 - diff --git a/versioned_docs/version-v4/sdk/update/_category_.json b/versioned_docs/version-v4/sdk/update/_category_.json deleted file mode 100644 index e1427d397..000000000 --- a/versioned_docs/version-v4/sdk/update/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "更新唤起", - "collapsed": true, - "position": 3 -} diff --git a/versioned_docs/version-v4/sdk/update/faq.mdx b/versioned_docs/version-v4/sdk/update/faq.mdx deleted file mode 100644 index da18e2af6..000000000 --- a/versioned_docs/version-v4/sdk/update/faq.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: 常见问题 -sidebar_label: 常见问题 -sidebar_position: 3 ---- - -### 弹窗提示『请求失败,请重试』 - -可以参考[集成前准备](/sdk/update/guide/#集成前准备)检查是否已经提交 APK 并通过审核,以及发布设置是否为立即上线状态。 - - -### 从 v3.23.0 以前的版本更新到新版本的升级流程 -将原先 `updateGame` 相关的接口代码删除,并重新按 [sdk-获取](/v4/sdk/update/guide/#sdk-获取)的流程接入 - -### Unity 唤醒更新会增加哪些 Android 依赖库 -首先 Unity 唤醒更新只会在 **Android** 平台生效,其次唤醒更新新加的依赖库会在打包过程中自动添加到 [Unity Gradle Template](https://docs.unity3d.com/Manual/gradle-templates.html) 中,您可以到 `唤醒更新模块/Mobile/Editor/TapTapUpdateDependencies.xml` 中查看到,具体来说会增加 3 个依赖库: -1. com.squareup.okhttp3:okhttp:3.12.1 -2. androidx.core:core:1.6.0 -3. com.google.android.flexbox:flexbox:3.0.0 - -## 单机游戏没有服务器,如何接入该功能? - -单机游戏暂不需要接入更新唤起功能。 - -## 更新唤起功能支持 iOS 版本吗? - -受限于苹果政策,iOS 平台的 TapTap 客户端无法提供游戏更新功能,更新唤起能力仅支持 Android 平台使用。 - -## 更新唤起功能是否只适用于强制更新的游戏版本? - -游戏在强更场景必须使用「更新唤起」服务,**当游戏需要包体强更时,请求 SDK 更新接口,唤起 TapTap 更新即可**,同时平台也不会干涉游戏自身的热更操作,如果游戏在非强更场景,也希望用户更新的话,可以尝试在公告等场景对用户进行提醒。 - -## 接入更新唤起功能后,如何进行测试? - -建议在测试环境中验收接入效果,具体测试流程如下: - -1. 需模拟游戏实际更新场景,测试的包体版本号(Version Code)需低于游戏在服务器上设置的最新版本; -2. 当打开版本号较低的测试包体时,游戏需自行获取服务器上的版本号并能与当前测试包体的版本号进行判断对比; -3. 判断当前测试包体版本号较低的情况下,能成功调起更新唤起功能接口; -4. 功能验证完成。 - -## 接入更新唤起游戏打包报 `Android resource linking failed,AAPT:error:resource android:attr/xx/xx not found.` 异常 - -检查项目打包时 compileSdkVersion 版本号,该异常时 Gradle 版本和 compileSdkVersion 匹配问题导致,建议将 compileSdkVersion 升级到 28 或更高版本。 - -## 接入更新唤起游戏打包报 `getUpdateInfo error: org.json.JSONExcetion: {"date": "{xxxx}"}` 异常 - -检查项目是否开启了混淆操作,TapSDK 已经做了混淆处理,再次混淆会导致不可预期的错误,请在项目的混淆脚本中添加如下配置,跳过对 TapSDK 的混淆操作: -```java --keep class com.tds.** { *;} --keep class com.taptap.** { *;} --keep class com.tapsdk.** { *;} --keep class tds.androidx.** { *;} -``` - -### 引入 TapUpdate 模块后闪退报错:Unable to get provider com.taptap.services.update.TapUpdateFileProvider,具体表现是弹出「更新游戏,需安装 TapTap 客户端」页面,点击「安装 TapTap」没有相应。 - -检查游戏是否存在二次打包的情况,检查二次打包后 APK 中的 AndroidManifest.xml 文件中如下的 `provider` 标签里面的 `android:authorities` 字段值是否为 `项目包名.com.taptap.services.update.fileProvider`,如果不是的话,请手动更正。 - -```xml - - - - -``` \ No newline at end of file diff --git a/versioned_docs/version-v4/sdk/update/features.mdx b/versioned_docs/version-v4/sdk/update/features.mdx deleted file mode 100644 index a8c75de91..000000000 --- a/versioned_docs/version-v4/sdk/update/features.mdx +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: 更新唤起 -sidebar_label: 功能介绍 -sidebar_position: 1 ---- - -## 功能介绍 - - -- 更新唤起服务主要应用于在 TapTap 国内商店分发的游戏包体更新场景,目前仅支持安卓。 -- 使用更新唤起功能,需接入 TapSDK,开发者在判断游戏需要强更后调用 TapSDK 更新接口,就会唤起 TapTap 客户端,引导用户前往 TapTap 完成更新。 - -## 接入优势 -- **节省开发及流量成本**:游戏接入更新唤起功能后,无需自行开发 APK 包更新流程,同时也可以节省下载新版本所需要付出的流量成本。 - -- **帮助玩家规范游戏更新流程,提升游戏热度**:TapSDK 为玩家提供了标准化的更新流程,与苹果应用商店等更新流程体验保持一致。同时游戏的更新下载热度是影响 TapTap 热门榜单的因素之一,用户前往 TapTap 更新后,在包体强更的节点时将有机会在热门榜中露出,获得更多流量曝光,帮助游戏吸引新老用户的新增与回流。 - - -## 接入要求 - -- 游戏需保持全网更新方式的统一性,平台不会干涉游戏自身的热更操作,**游戏只需要在包体强更时,请求 SDK 更新接口,唤起 TapTap 更新即可**。 - -- 开发者需确保**在 TapTap 商店的游戏包体是最新版本**,否则会导致用户打开 TapTap 后发现商店版本较低,影响用户体验。 - -- 针对已上架的游戏,开发者需确保**更新资料版本中的包名和已上架的游戏包名保持一致**,否则会导致玩家因包名不一致而更新失败。 - -- 针对新游戏,开发者需要在 TapTap 上线一个包含 APK 包**(用于平台获取游戏包名,包名需与后续更新的包名保持一致)**的商店资料版本,如果 APK 包当前无法对外,可将发布状态设置为「敬请期待」或「预约」。 - - -## 游戏接入建议 -**图示 UI 仅供参考,非 TapSDK 提供** - -1、**包体强更场景**:玩家必须更新到最新包体才可以玩游戏。 -- 游戏判断当前包体需强更时,在 APP 启动或者用户登陆成功之后,打开强更弹窗(如下图),告知玩家当前版本过低,当用户点击「更新」按钮时,则直接调用 TapSDK 更新接口。点击「取消」时,则关闭游戏或者提示玩家“不更新至最新版本,游戏无法玩”。 -![](https://capacity-files.lcfile.com/ESiXBqnxhUDN8r7IteGSnPGcuFk14pyq/01.png) - -2、**普通更新场景**:游戏内支持热更,或者玩家无需更新到最新包体也可以玩游戏。 -- 平台不要求开发者在普通更新场景去请求 TapSDK 更新接口,对玩家进行强制更新引导,开发者如果需要玩家更新,可在公告等其他场景进行弱引导提醒即可。 - - - -## 更新唤起流程说明 - -游戏调用 TapSDK 更新接口后,TapSDK 会检查当前设备是否已安装 TapTap 客户端来进入不同的更新流程。 - -### 玩家已安装 TapTap 客户端 - -- TapSDK 会直接唤起 TapTap 客户端,并跳转至游戏更新页面,引导用户完成更新。在唤起 TapTap 客户端的时候,设备可能会有打开第三方 APP 的提示(不同设备提示样式会有差异,下图仅为参考),用户需同意后,才可打开 TapTap,然后根据页面引导完成游戏更新即可。 - -![](https://capacity-files.lcfile.com/7TN6cV9vEljCHNOpIa01oBbPk4m3za1D/02.png) - -### 玩家未安装 TapTap 客户端 - -- TapSDK 如果检查当前设备未安装 TapTap 客户端,首先会询问用户“是否需要使用 TapTap 更新”(见下图,该弹窗由 TapSDK 提供)。 - -![](https://capacity-files.lcfile.com/Tc9HkqxzgmWUVq5tcqbsece4R0QBMUd4/03.png) - -- 用户确认更新后,则会开始直接下载 TapTap 客户端,下载完成后会自动向设备请求安装,用户根据设备引导完成安装即可(见下图)。 - -![](https://capacity-files.lcfile.com/05g1cDq1tLjDnHmmODMmbKUFHy0NzyS0/update-03.png) - -- TapTap 客户端下载并安装成功后,将会继续跳转 TapTap 更新游戏页面,有部分安卓设备会引导用户返回游戏,SDK 内也会提示用户“打开 TapTap 去更新“(见下图,该弹窗由 TapSDK 提供)。用户打开 TapTap 后,根据页面引导完成游戏更新即可。 - -![](https://capacity-files.lcfile.com/wuprYiOr9iBSe79U5pyG3NOuEakVTxc3/update-04.png) - -- 针对以上流程,用户如果选择关闭弹窗或取消等中断更新游戏的行为,TapSDK 会提供相关的回调接口,方便开发者接管用户后续行为。比如当用户取消「使用 TapTap 更新」TapSDK 会告知游戏更新失败。 - -## 最佳实践 - -### 月圆之夜 -游戏启动后,告知玩家需要更新到新版本,点击更新,唤起 TapTap 的流程。 -
    - -
    - -### 香肠派对 - -通过公告形式提醒玩家强制更新,但是更新后会给玩家相应奖励,鼓励玩家去更新。 -![](https://capacity-files.lcfile.com/9OslzFsggaPsGkp5BEObRyqbhxgmATkf/04.png) - -### 史莱姆连接 - -检测到玩家未安装 TapTap 客户端时,玩家选择更新,下载并安装 TapTap 客户端完成更新的流程。 - -
    - -
    - diff --git a/versioned_docs/version-v4/sdk/update/guide.mdx b/versioned_docs/version-v4/sdk/update/guide.mdx deleted file mode 100644 index 33d76a8f8..000000000 --- a/versioned_docs/version-v4/sdk/update/guide.mdx +++ /dev/null @@ -1,241 +0,0 @@ ---- -title: 更新唤起开发指南 -sidebar_label: 开发指南 -sidebar_position: 2 ---- - -import MultiLang from "/src/docComponents/MultiLang"; -import CodeBlock from "@theme/CodeBlock"; -import sdkVersions from '../../sdkVersions'; -import { Conditional } from "/src/docComponents/conditional"; -import AndroidFaq from "../../../../docs/sdk/_partials/android-package-visibility.mdx"; -import UnitySDKInstallation from "../_partials/unity-sdk-installation.mdx"; - -:::info -本文档只适用于国内版本,海外版本请参考 [文档](/tap-update-old-guide/) -::: - -## 权限说明 - - - -<> -该模块依赖权限如下: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于正常网络访问 | 用户首次使用该功能时会申请权限 | -| 安装 APK 权限 | 用于安装 Tap 客户端 | 用户首次使用该功能时会申请权限 | - - -<> - -该模块依赖权限如下: - -| 权限 | 使用目的 | 权限申请时机 | -| ---------------------- | ---------------------- | ---------------------- | -| 网络权限 | 用于正常网络访问 | 用户首次使用该功能时会申请权限 | -| 安装 APK 权限 | 用于安装 Tap 客户端 | 用户首次使用该功能时会申请权限 | - -同时该模块也会访问设备已安装的 Tap 客户端信息,所以接入 SDK 后将在应用 `AndroidManifest.xml` 中添加如下配置: - -```xml - - - - - - -``` - - - - - -## 集成前准备 - -使用更新唤起功能前提需要通过 **TapTap 开发者中心 > 商店 > 游戏资料 > 商店资料** 中已经上传 APK, 发布设置为 **立即上线** 并通过 **审核**(开发者包如果暂时不想对外,发布状态选 **敬请期待** 或者 **预约**)。 - - -## SDK 获取 - - - -<> - - - - - -<> - -1. 项目根目录的 build.gradle 添加仓库地址: - -{ - `allprojects { - repositories { - google() - mavenCentral() - } -}` -} - -2. app module 的 build.gradle 添加对应依赖: - -{ -`dependencies { - implementation 'com.taptap.sdk:tap-core:${sdkVersions.taptap.android}' - implementation 'com.taptap.sdk:tap-update:${sdkVersions.taptap.android}' -}` -} - - - -<> - -受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能 - - - - - -## 初始化 - -### TapSDK 初始化 - - - -<> - -`TapTapSdkOptions` 详细参数见 [入门指南#快速开始](/v4/sdk/access/quickstart/#初始化) - -```cs -using TapSDK.Core; -using TapSDK.Compliance; - -// 核心配置 详细参数见 [入门指南#快速开始] -TapTapSdkOptions coreOptions = new TapTapSdkOptions(); -// TapSDK 初始化 -TapTapSDK.Init(coreOptions); -``` - - - -<> - -`TapTapSdkOptions` 详细参数见 [入门指南#快速开始](/v4/sdk/access/quickstart/#初始化) - -```kotlin -import com.taptap.sdk.core.TapTapSdk -import com.taptap.sdk.core.TapTapSdkOptions -import com.taptap.sdk.core.TapTapRegion -import com.taptap.sdk.core.TapTapLanguage - -TapTapSdk.init( - context = context, - sdkOptions = TapTapSdkOptions( - clientId = clientId, - clientToken = clientToken, - region = TapTapRegion.CN, - preferredLanguage = TapTapLanguage.ZH_HANS, - enableLog = false - ), -) -``` - - - -受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能 - -```cpp -FTapUpdate::Init(TEXT("clientId"), TEXT("clientToken")); -``` - - - -## 开始更新 - -:::tip -TapSDK 如果检查当前设备未安装 TapTap 客户端,首先会弹窗询问用户「是否需要使用 TapTap 更新」,当点击「取消」按钮时取消更新的回调方法才会执行。 -::: - - - -<> - -```cs -using TapSDK.Update - -TapTapUpdate.UpdateGame(() => { - // 唤醒更新收到[取消]回调 -}); -``` - - -<> - -由于更新唤起 UI 依赖于开发者设置的 Activity 实例,所以为了避免因 Activity 发生重建导致更新唤起功能不可用,开发者应确保在屏幕旋转、配置修改时当前 Activity 不会发生重建,具体设置方式参考 [限制 activity 重新创建](https://developer.android.com/guide/topics/resources/runtime-changes?hl=zh-cn#restrict-activity) - -```kotlin -import com.taptap.sdk.update.TapTapUpdate -import com.taptap.sdk.update.TapTapUpdateCallback - -TapTapUpdate.updateGame( - activity = this, - callback = object : TapTapUpdateCallback { - - override fun onCancel() { - - Toast.makeText(this@DemoUpdateActivity, "取消更新", Toast.LENGTH_SHORT) - .show() - } - } -) -``` - - -<> - -受限于苹果政策,iOS 平台的 TapTap 客户端不提供游戏更新功能 - - - - - - -## 测试 -为了保证上线后,游戏对于用户是否正常使用更新唤起功能,请务必按照以下说明完成自测。 - -### 上传 APK - -新应用需要上传测试的 APK 至开发者中心,并通过审核。已上架的游戏,需确保更新资料版本中的 APK 包名和已上架的 APK 包名保持一致。 - -### 应用上线 - -针对已上架的游戏,开发者需确保**更新资料版本中的包名和已上架的游戏包名保持一致**,否则会导致玩家因包名不一致而更新失败。 - -针对新游戏,开发者需要在 TapTap 上线一个包含 APK 包并且通过审核**(用于平台获取游戏包名,包名需与后续更新的包名保持一致)**的商店资料版本,如果 APK 包当前无法对外,可将发布状态设置为「敬请期待」或「预约」。 - -### 开始测试 - -触发更新唤起功能后正常状态是可以唤起应用在 TapTap 商店的详情页面。 - - - - diff --git a/versioned_docs/version-v4/sdkVersions.ts b/versioned_docs/version-v4/sdkVersions.ts deleted file mode 100644 index 4a6638c41..000000000 --- a/versioned_docs/version-v4/sdkVersions.ts +++ /dev/null @@ -1,40 +0,0 @@ -const taptapUnity = "4.3.7" -const taptapIos = "4.3.2" -const taptapAndroid = "4.3.7" -const taptapUnreal = "3.29.2" - -const sdkVersions = { - taptap: { - unity: taptapUnity, - android: taptapAndroid, - ios: taptapIos, - unreal: taptapUnreal, - rtc: "1.1.0", - adr: "1.2.1" - }, - leancloud: { - objc: "13.9.0", - swift: "17.10.1", - js: { - storage: "4.13.2", - realtime: "5.0.0-rc.7", - }, - java: "8.2.24", - csharp: "2.3.0", - flutter: { - storage: "0.7.10", - realtime: "1.0.1", - } - }, - tapadn: { - unity: "3.16.3.31", - android: "3.16.3.31", - }, - tapGlobalPayments: { - unity: "4.0.14", - android: "4.2.5", - } -}; - -export default sdkVersions; - diff --git a/versioned_docs/version-v4/tap-download.mdx b/versioned_docs/version-v4/tap-download.mdx deleted file mode 100644 index 6ae7bfab1..000000000 --- a/versioned_docs/version-v4/tap-download.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: tap-download -title: 资源下载 -sidebar_label: 资源下载 -slug: /tap-download ---- - -## SDK - -- [Unity](https://github.com/taptap/TapSDK-Unity/releases/tag/2.1.8) -- [Android](https://github.com/taptap/TapSDK-Android/releases/tag/v2.1.8) -- [iOS](https://github.com/taptap/TapSDK-iOS/releases/tag/v2.1.7) - -## Demo - -- [Unity](https://github.com/taptap/TapSDK-Unity-Demo/tree/ef42cb92f4513c558628531a96aa5eefa40576a6) -- [Android](https://github.com/taptap/TapSDK-Android/tree/83dc40c35505da1df54f5b0b8548f67a2d8e89ed) -- [iOS](https://github.com/taptap/TapSDK-iOS/tree/bdcc72154c6d5e6081676c210a440baf51ce22aa) - -## 登录按钮素材 - -点击下载 [icon.zip](https://capacity-files.lcfile.com/z7xSKYDvAc1ff19cDfq3Vx01v50KNR6j/TapTapLoginButton.zip) \ No newline at end of file diff --git a/versioned_docs/version-v4/temp/TapSDKUnityGuide.md b/versioned_docs/version-v4/temp/TapSDKUnityGuide.md deleted file mode 100644 index f7d2ab022..000000000 --- a/versioned_docs/version-v4/temp/TapSDKUnityGuide.md +++ /dev/null @@ -1,172 +0,0 @@ -# TapTapSDK V4 Unity 接入指南 - -本文介绍如何在游戏中加入`TapTapSDK` 的登录、更新唤起模块。 - -## package 集成 - -### 依赖库 - -SDK 修改 JSON 解析库为 `Newtonsoft-json`,如果当前工程已接入该依赖库,则不需额外处理,否则需在 `Packages/manifest.json` 添加依赖 ,同时SDK -使用 `com.google.external-dependency-manager` -管理Android、iOS依赖,如果当前工程已接入该依赖库,则不需额外处理,否则需在 `Packages/manifest.json` 添加如下依赖: - -``` -{ - "dependencies": { - "com.unity.nuget.newtonsoft-json":"3.2.1", - "com.google.external-dependency-manager": "1.2.179" - }, - "scopedRegistries": [ - { - "name": "package.openupm.com", - "url": "https://package.openupm.com", - "scopes": [ - "com.google.external-dependency-manager" - ] - } - ] -} -``` - -### 导入UnityPackage - -1. 下载 `TapSDK-UnityPackage.zip`,解压到方便的位置,然后在 Unity 项目中导入。 -2. 在 Unity 项目中依次转到 **Assets > Import Packages > Custom Packages**,从解压后的 `TapSDK-UnityPackage.zip` 中,选择您希望在应用中使用的 **TapSDK** - 产品导入。 - `TapTapSDK_Core.unitypackage` : **必选**。TapSDK 核心库 - `TapTapSDK_Login.unitypackage` : **必选**。TapSDK 登录 - `TapTapSDK_Update.unitypackage` : **必选**。TapSDK 更新唤起 - -### iOS 配置 -为了在 iOS 设备中支持 Tap 登录跳转,游戏需在 Assets/Plugins/iOS/Resource 目录下创建 TDS-Info.plist 文件,复制以下代码并且替换其中的 ClientId。 - -``` - - - - - taptap - - client_id - ClientId - - - -``` - -## 初始化 - -TapSDK 所有模块使用统一初始化入口,使用方式如下: - -``` -using TapSDK.Core; - -// 添加应用配置 -TapTapSDKCoreOptions coreOptions = new TapTapSDKCoreOptions - { - clientId = "游戏在开发者中心的 ClientId", - clientToken = "游戏在开发者中心的 ClientToken", - region = TapTapRegionType.CN // 区域,海外为 TapTapRegionType.Overseas - } - -// 初始化 SDK -TapTapSDK.Init(coreOptions); -``` - - -### TapTap 登录 - -### 1. 使用 TapTap 登录 - -在移动端使用 Tap 登录时,如果当前设备已安装 Tap 客户端,会跳转到 Tap 客户端完成授权,否则会在游戏内使用网页登录完成授权。在 PC 中使用时,会跳转到外部浏览器打开对应登录页面完成授权。具体使用方式示例如下: - -``` -using TapSDK.Login; -using System.Threading.Tasks; - -// 设置登录请求的权限 -string[] permissions = new string[] {TapTapLogin.TAP_LOGIN_SCOPE_PUBLIC_PROFILE}; - -// 初始化登录请求 Task -Task loginTask = TapTapLogin.Instance.Login(permissions); - -// 发起登录请求 -var loginReult = await loginTask; - -// 判断登录结果 -if (loginTask.IsCanceled) -{ - // 登录取消 -} -else if (loginTask.IsCompleted) -{ - // 登录成功 - Debug.Log($"登录成功 token= : {result.accessToken}"); -} -else -{ - Debug.Log($"登录失败: {task.Exception.Message}"); -} - -``` -登录请求的权限分为如下几种: - -|参数名 | 对应值 | 场景| -| --- | --- | --- | -|TAP\_LOGIN\_SCOPE\_BASIC\_INFO | basic_info | 游戏只需要获取用户 openId 不需要头像、昵称等信息| -|TAP\_LOGIN\_SCOPE\_PUBLIC\_PROFILE | public_profile | 游戏需要获取用户 openId 、需要头像、昵称等完整信息| -| TAP\_LOGIN\_SCOPE\_USER\_FRIENDS | user_friends | 游戏需要获取用户好友信息 | - -登录成功后返回的 `TapTapAccount`信息如下: - -|字段名 | 说明| -| --- | --- | -| accessToken | 登录 accessToken | -| openid | 通过用户信息和游戏信息生成的用户唯一标识,每个玩家在每个游戏中的 openid 都是唯一| -| unionid| 通过用户信息和厂商信息生成的用户唯一标识,一个玩家在同一个厂商的所有游戏中 unionid 都相同,不同厂商下 unionid 不同| -| name| 玩家在 TapTap 平台的昵称| -| avatar| 玩家在 TapTap 平台的头像 url| - -其中 `openid` 和 `unionid` 使用标准的 Base64(带 Padding)编码,包含的字符有 A-Za-z0-9+/=, 最多为 50 个字符。 - -### 2. 获取当前登录状态 - -在发起登录请求前,游戏可以通过该接口判断用户是否已登录过(本地是否有登录信息),使用示例如下: - -``` -using TapSDK.Login; -using System.Threading.Tasks; - -try { - TapTapAccount account = await TapTapLogin.Instance.GetCurrentAccount(); - if (account == null) { - // 用户未登录 - } else { - // 用户已登录 - } -} catch (Exception e) { - Debug.Log($"获取用户信息失败 {e.Message}"); -} -``` -### 3. 登出 -当用户退出账号时,游戏需调用该接口清除本地用户登录信息,使用示例如下: - -``` -using TapSDK.Login; - -TapTapLogin.Instance.Logout(); -``` - -## 更新唤起 - -### 开始更新 - -开发者在判断游戏需要强更后调用该接口,如果当前设备已安装 TapTap 客户端,SDK 会跳转到客户端并引导用户完成更新;如果当前设备未安装,SDK 会引导用户下载及安装 TapTap 客户端,并在完成后引导用户更新,如果下载过程中用户选择取消,则会触发对应取消回调。具体调用示例如下: - -``` -using TapSDK.Update; - -TapTapUpdate.UpdateGame(() => { - // 用户取消更新 -}); -``` \ No newline at end of file diff --git a/versioned_sidebars/version-v2-sidebars.json b/versioned_sidebars/version-v2-sidebars.json deleted file mode 100644 index 9b5bb76bd..000000000 --- a/versioned_sidebars/version-v2-sidebars.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version-v2/sdk": [ - { - "type": "autogenerated", - "dirName": "sdk" } - ] -} diff --git a/versioned_sidebars/version-v4-sidebars.json b/versioned_sidebars/version-v4-sidebars.json deleted file mode 100644 index 6f8b98984..000000000 --- a/versioned_sidebars/version-v4-sidebars.json +++ /dev/null @@ -1,9 +0,0 @@ - -{ - "version-v4/sdk": [ - { - "type": "autogenerated", - "dirName": "sdk" - } - ] -} diff --git a/versions.json b/versions.json deleted file mode 100644 index 820e3f0a6..000000000 --- a/versions.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "v2","v4" -]