当前位置: 首页 > news >正文

苹果ASA归因对接以及API接入

一、归因概要

广告归因,目的是用于衡量广告带来的激活用户的成本以及后续进一步的用户质量表现。 Apple Ads 广告平台是基于 App Store(站内广告),同时属于自归因平台(通常称为 SAN)。这两个因素,决定了 ASA 与其他大部分广告平台(站外广告)的区别。 ASA 广告投放前,无需创建投放链接、监测链接,ASA 归因由 Apple 自身完成,可以保证用户隐私同时还能做到用户级归因 ,目前获取归因数据可采用自归因或第三方归因的方式。

以下是该方案的要点和广告主应对措施:

  • 此归因方案仅适用 Apple Search Ads 广告;仅支持 iOS 14.3 及更高版本;14.3 之前的版本,需使用 iAd Framework

    • 从 2023 年 2 月 7 日开始,iAd 框架停止用于归因 Apple Ads 的广告安装。

    • 所有通过iAd Framework发送的请求都会收到 "iad-attribution"=false的错误,具体请查看iAd Changelog | Apple Developer Documentation

    • 开发者需实施 AdSercices 框架以归因来自 Apple Ads 的广告安装。此框架于 2021 年 1 月发布,如果已完成实施、或使用主流 MMP 的开发者无需做更改。

    • AdServices[3] 仅支持 iOS 14.3 及更高版本的设备。

    • 届时 14.2 及更低旧版本的设备将无法归因苹果广告安装。

  • 此方案将极大提高 ASA 广告安装的激活率

  • 安装到激活的差距逐渐降至极低

  • 此方案涉及前后端的系统开发,需自己归因的开发者应尽早制定计划、安排实施

  • 使用 MMP 服务的开发者,与您的服务提供商沟通,了解其SDK对此方案的支持进度

  • 无论开发者自己实施还是采用 MMP,均需要发布新的app版本

  • 针对 iOS 14 及更高版本的设备,LAT 的概念已失效,受众的年龄和性别定向不再排除限制跟踪的用户

  • 什么是ASA?

      ASA(Apple Search Ads),也叫ASM(App Store Marketing)---应用商店广告,即用户在App Store 搜索应用时出现在搜索结果上方的广告。

      搜索广告有三种展现形式:标题+icon+截图/描述/视频, App 在搜索广告上的展示内容与其在自然搜索排名下的内容相同,开发者不必也不能单独为搜索广告设置素材,且无法选择或设置广告具体以哪种方式展现;

  • ASA广告竞价逻辑

ASA广告类似谷歌搜索广告,属于竞价广告模式,以点击计费;

广告展现量受5个因素影响,分别是相关性、出价、关键词热度、竞争者、投放位置;广告单价受3个因素影响,分别是相关性、出价、竞争者;

1)相关性(关键词和APP之间的相关性):一个关键词和这个 App 之间的相关性是由这个 App 的元数据和用户 行为所决定的;元数据即 App 的标题、icon、截图、描述等。

2)出价:在其他参数不变的情况下,价高者得;

其他影响因素:产品权重(安装总量/总榜/分类榜单越高,是受到用户喜爱得产品)、关键词热度(aso吸引力-100关键词的覆盖)

二、归因实施说明

归因业务流程图

4cc231f0a6354af0ad35fbf815004a6c.png

这个归因方案,包含两部分:客户端的 AdServices 框架,和从苹果Search Ads归因服务器获取归因数据的 RESTful API。

下图说明了结合使用 AdServices 框架和 RESTful API 来完成归因。

2e6a719abe614bd89afc79934d49f596.png

Request Token 获取 token

  • (NSString *)attributionTokenWithError:(NSError * _Nullable *)error;

AdServices 框架返回的 token 为字符串类型,并且只有 24 小时有效期。

AAAttributionErrorCode 归因错误枚举值 

typedef enum AAAttributionErrorCode : NSInteger {// 没有返回 token,网络不可用。AAAttributionErrorCodeNetworkError = 1,// 没有返回 token,发生了内部错误。AAAttributionErrorCodeInternalError = 2,// 没有返回 token,操作系统平台不支持。AAAttributionErrorCodePlatformNotSupported = 3
} AAAttributionErrorCode;

Request attribution 获取归因数据

您可以将 token 提供给 MMP,或者使用该 token 在 24 小时内进行 POST API 调用,以获取归因数据。在请求正文中带上 token:

POST https://api-adservices.apple.com/api/v1/
yourtokenyourtoken

Response Codes 返回状态码

Response状态码Description描述
200成功。API 找到了匹配的归因记录,返回值包含 attribution=true。如果API没有找到对应的归因记录,返回值为 attribution=false。在这种情况,状态码 200 仅表示服务器有数据返回。
400token 无效。
404没有找到。API 无法获取到请求的归因记录。Tokens 只有 24 小时的有效期。如果请求超过了 24 小时,苹果会返回 404 状态码。如果 token 是有效的,一个最佳实践:最多重试 3 次,每次间隔 5 秒钟。
500服务器暂时关闭或无法访问。请求可能是有效的,但是你需要选择合适的时间点进行重试。

代码示例:

- #import <AdServices/AdServices.h>
(void) methodToGetToken {if (@available(iOS 14.3, *)) {NSError *error;NSString *token = [AAAttribution attributionTokenWithError:&error];if (token != nil) {// 发送POST请求归因数据}} else {// Fallback on earlier versions}
}
- (void) attributionWithToken:(NSString *)token {NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];NSURL *url = [NSURL URLWithString:@"https://api-adservices.apple.com/api/v1/"];NSMutableURLRequest 
request = [NSMutableURLRequest requestWithURL:urlcachePolicy:NSURLRequestUseProtocolCachePolicytimeoutInterval:60.0];[request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];[request setHTTPMethod:@"POST"];
*  NSData*postData = [token dataUsingEncoding:NSUTF8StringEncoding];[request setHTTPBody:postData];NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {NSError *resError;NSMutableDictionary *resDic = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&resError];}];[postDataTask resume];
}

 

字段类型说明
attributionBoolean如果用户在应用下载前 30 天点击了 App Store、Apple News 以及 Stocks,则其值为 true。如果 API 找不到匹配的归因记录,则为false。
orgIdInteger广告系列所属的账户 ID。
campaignIdInteger广告系列 ID。
conversionTypeString表明是否首次下载。"Redownload" 说明用户在本设备下载/卸载过,或者用同一账户在其他设备下载过。
clickDateDate/time string用户点击相应广告的日期和时间。此字段仅出现在详细归因数据包中。
adGroupIdInteger广告组 ID。
countryOrRegionString国家或地区。
keywordIdInteger关键字的 ID。
creativeSetIdInteger广告素材集的 ID。

 

这里只要是attribution=true,就意味着用户是通过点击广告下载的app。反之会返回false。原则是用户在下载app之前,30天内是否有广告点击的记录。所以客户如果只想判断用户是不是通过广告带来的,只看这个字段就可以,=true的情况下click date一定是在30天内的。

conversiontype是用来判断用户是首次安装还是重新安装,对于判断用户是否来自广告投放没有太大的关系。

客户端调用AdService框架

  • AdServices framework发起调用请求生成 Token 。

  • AdServices framework生成Token 返回(Token有效期24小时)。更多详请,请参阅 attributionToken

  • 使⽤Token向Attribution API请求结果

  • Attribution API返回true or false,判断该客户是否点击过苹果搜索广告

    • True: ⽤户在过去30天内曾经点击过苹果广告,记录最近一次用户点击广告

    • False: ⽤户在过去30天没有点击过苹果广告(用户为IOS14.3以下版本请求Token就会报错)

  • MMP 或开发⼈员使⽤ Token 发起 RESTful API 获取归因记录请求,苹果的归因服务器响应请求。更多详请,请参阅Attribution Payload。

  • API 返回的归因数据中的键值与Apple Ads ⼴告系列管理 API 中的⼴告系列的字段相对应。更多详情,请参阅 Attribution Payload Descriptions。

*苹果官网参考文档:https://developer.apple.com/doc

三、ManagementAPI接⼊具体流程

授权广告主账户对应广告系列组“API只读”

  • 需要广告主提供一个开发者账号AppleID(即邮箱)

  • 授权后需要甲方到邮箱查收邀请邮件,并点击邮件中链接确认授权

开发者生成公钥+私钥,准备上传到ads.apple.com后台

  • 生成公钥私钥:

  • 如果您使用的是MacOS或类似UNIX的操作系统,OpenSSL可以原生运行。如果您使用的是Windows平台,则需要下载OpenSSL。

  • openssl ecparam -genkey -name prime256v1 -noout -out private-key.pemopenssl ec -in private-key.pem -pubout -out public-key.pem

    上传公钥私钥,生成clientid、keyid、teamid

  • 登录被授权的账号,查看右上⻆设置d41535b27c864ee29f84e7141c4fbbb1.png

  • 粘贴前⾯⽣成的公钥进去,点击保存按钮;并记录⽣成的clientid、keyid、teamid

  • 使⽤新⽣成的clientid、keyid、teamid,⽣成client_secret

41ff03a0d842462eb946cf38dad24df3.png

  • 生成出来的ID示例:

clientId SEARCHADS.aeb3ef5f-0c5a-4f2a-99c8-fca83f25a9
teamId SEARCHADS.hgw3ef3p-0w7a-8a2n-77c8-scv83f25a7
keyId a273d0d3-4d9e-458c-a173-0db8619ca7d7
  • 使⽤3个id⽣成client_secret的示例

import jwt
import datetime as dt
client_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577'
team_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577' 
key_id = 'bacaebda-e219-41ee-a907-e2c25b24d1b2' 
audience = 'https://appleid.apple.com'
alg = 'ES256'
Define issue timestamp.
issued_at_timestamp = int(dt.datetime.utcnow().timestamp())
Define expiration timestamp. May not exceed 180 days from issue timestamp.
expiration_timestamp = issued_at_timestamp + 86400*180 
Define JWT headers.
headers = dict()
headers['alg'] = alg
headers['kid'] = key_id
Define JWT payload.
payload = dict()
payload['sub'] = client_id
payload['aud'] = audience
payload['iat'] = issued_at_timestamp
payload['exp'] = expiration_timestamp
payload['iss'] = team_id 
Path to signed private key.
KEY_FILE = 'private-key.pem' 
with open(KEY_FILE,'r') as key_file:key = ''.join(key_file.readlines())
client_secret = jwt.encode(
payload=payload,  
headers=headers,
algorithm=alg,  
key=key
)
with open('client_secret.txt', 'w') as output:output.write(client_secret.decode("utf-8"))
  • JWT请求头和请求data示例

Header
{
"alg": "ES256",
"kid": "bacaebda-e219-41ee-a907-e2c25b24d1b2"
}
Payload
{
"iss": "SEARCHADS.hgw3ef3p-0w7a-8a2n-77c8-scv83f25a7", # 您上一步获得的teamId
"iat": 2234567891, # 创建客户端密钥时的 UNIX UTC 时间戳
"exp": 2234567900, # 客户端密码过期的 UNIX UTC 时间戳。该值必须大于当前时间
"aud": "https://appleid.apple.com",
"sub": "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577" # 您上一步获得的client_secret
}
  • 请求⽣成的client_secret示例

eyJraWQiOiJiYWNhZWJkYS1lMjE5LTQxZWUtYTkwNy1lMmMyNWIyNGQxYjIiLCJhbGciOiJFUzI1NiJ9.
eyJpc3MiOiJEcmVhbWNvbXBhbnkiLCJhdWQiOiJBdXRoZW50aWNhdG9yIiwiZXhwIjoxNTcxNjcwNjIx
LCJuYmYiOjE1NzE2NjcwMjEsInN1YiI6Im11c3RlciIsImNsaWVudF9pZCI6ImFiY2QxMjM0IiwiYWRt
aW4iOiJ0cnVlIn0.s4C3p9kVNFeRAB5tChatC3ldQX07v9mG7thL7FeEO6cClfNuiaLSgq8f8ymbfO3O
QYW_KuwaA1KYRuoy1JmKk 4DBbYLcz6aoABe0pzI5Z_6wgMzAyqz8pQtwDAcd4Idoi8JdRbtzZce9o-0
nZiFA4hVAXqYwpEYC4UU8ZmJO_z8tY4juHPTV3nDugdtqyNnmAiBoLryOfGNngQZccdY1_QvkXS1y0bg1
a0k8cVVtnq- _93fYJIt9Z64CTvlH3uOeh7uaEv3nIxpXhvhkTySpUmY8e04TO09oTyZijiloByv3KFQ9
2OOJ8L 5N5_CeEc5p9LWjT1pcX8ATamOycZz2Q

使⽤client_secret,请求苹果接⼝⽣成access_token

access_token存在过期的情况,通常为1小时,需要定时维护

  • 请求获取 access_token 示例

curl -X POST \
-H 'Host: appleid.apple.com' \
-H 'Content-Type: application/x-www-form-urlencoded' \
'https://appleid.apple.com/auth/oauth2/token?grant_type=client_credentials&
client_id=SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577&client_secret=eyJ0
eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.zI1NiIsImprdSI6Imh0dHBzOi8vYXV0aC5kZXYuYXB
pLnJpY29oL3YxL2Rpc2NvdmVyeS9rZXlzIiwia2lkIjoiMmIyZTgyMTA2NzkxZGM4ZmFkNzgxNW
Q3ZmI1NDRhNjJmNzJjMTZmYSJ9.eyJpc3MiOiJodHRwczovL2F1dGguZGV2LmFwaS5yaWNvaC8iL
CJhdWQiOiJodHRwczovL2lwcy5kZXYuYXBpLnJpY29oLyIsImlhdCI6MTQ5MDg1Mjc0MSwiZXhwI
joxNDkwODU2MzQxLCJjbGllbnRfaWQiOiI4ODQwMWU1MS05MzliLTQ3NzktYjdmNy03YzlmNGIzZj
kyYzAiLCJzY29wZSI6Imh0dHBzOi8vaXBzLmRldi5hcGkucmljb2gvdjEiLCJyaWNvaF9tc3Mi
OnsibWVkaWEiOnsicXVvdGEiOjEwLCJ0aHJvdHRsZSI6eyJ2YWx1ZSI6MCwid2luZG93IjowfX1
9fQ.jVq_c_cTzgsLipkJKBjAHzm8KDehW4rFA1Yg0EQRmqWmBDlEKtpRpDHZeF6ZSQfNH2OlrBW
FBiVDV9Th091QFEYrZETZ1IE1koAO14oj4kf8TCmhiG_CtJagvctvloW1wAdgMB1_Eubz9a8oim
cODqL7_uTmA5jKFx3ez9uoqQrEKZ51g665jSI6NlyeLtj4LrxpI9jZ4zTx1yqqjQx0doYQjBPhOB
06Z5bdiVyhJDRpE8ksRCC3kDPS2nsvDAal28sMgyeP8sPvfKvp5sa2UsH78WJmTzeZWcJfX2C2ba3
xwRMB5LaaVrQZlhj9xjum0MfDpIS1hJI6p5CHZ8w&scope=searchadsorg'
  • 请求接⼝的字段说明

Header
headers = {'Content-Type': 'application/x-www-form-urlencoded','Host': 'appleid.apple.com'
}
Payload
grant_type:固定值client_credentials;
client_id:前面获取的client_id;
client_secret:前面获取的client_secret;
scope:固定值searchadsorg
  • 响应示例

{"access_token":"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjpudWxsfQ..lXm332TFi0u2E9YZ.bVVBvsjcavoQbBnQVeDiqEzmUIlaH9zLKY6rl36A_TD8wvgvWxpyBXMQuhs-qWG_dxQ5nfuJEIxOp8bIndfLE_4a3AiYtW0BsppO3vkWxMe0HWnzglkFbKUHU3PaJbLHpimmnLvQr44wUAeNcv1LmUPaSWT4pfaBzv3dMe3PNHJJCLVLfzNlWTmPxViIivQt3xyiQ9laBO6qIQiKs9zX7KE3holGpJ-Wvo39U6ZmGs7uK9BoNBPaFtd_q914mb9ChHAKcQaxF3Gadtu_Z5rYFg.vD0iQuRwHGYVnDy27qexCw","token_type": "Bearer","expires_in": 3600,"scope": "searchadsorg"
}
  • 响应字段说明

access_token:返回的 access_token,用于请求苹果广告相关数据接口 https://developer.apple.com/documentation/apple_search_ads/calling_the_apple_search_ads_api
token_type:固定值Bearer
expires_in:token有效期,3600秒即1小时
scope:访问权限

使⽤access_token获取⼴告账户下的⼴告orgid

官方文档:Get User ACL | Apple Developer Documentation

  • 请求示例

curl --location --request GET 'https://api.searchads.apple.com/api/v4/acls'
--header 'Authorization: Bearer eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwia2
lkIjpudWxsfQ..FGvVpX2vBYfyC91X.FKDdsLoAZwsfE8lgd5Ts5vSTY-lScKSp2myVIP9Eh7H
wc-7vsXr63aHcvNaDec9p7MwFHYnCC8Zphuf-BoqhAgyEu3hg_oHuqNEhbyS7iZexDfdgScTop
zGD5IP7Kiag71n0bbKb8o68MlJV8P-faGcsZR1-FBuISuyIrP6ZdUwXjoNygNCr4RkOLLNLjng
ShnnFUICJ3Q7dvrMQk0kUI_OFP3OcfZrsbHOIPWgDxA0W6GYI000Ua9x3TPT46kW2RQ67Pk5RS
2Ft_sqPgVh8V-A.7Rs0pIsQR09nRccM8'响应示例:{"orgName": "org name example","orgId": 40669820,"currency": "USD","timeZone": "America/Los_Angeles","paymentModel": "PAYG","roleNames": ["Admin"],"parentOrgId": "27154130","displayName": "display name example"
}

使⽤ access_token 获取⼴告账户投放数据

7.1 请求头参数说明

  • Authorization 设定值时组合⽅式为:Bearer {access_token},即Bearer在前,后⾯加 access_token

  • X-AP-Context 的值为orgId={orgId},orgId=不能漏掉

  • orgId 即广告系列组ID,上面第6部分接口中获取到的orgId

7.2 请求示例 - 获取广告系列组下的广告组数据

  • 以下仅为1个示例,实际对接时可查看苹果官⽅Management API⽂档,根据业务需要确认请求接⼝

  • 下面示例的官方文档参考: Get an Ad Group | Apple Developer Documentation

7.2.1 请求说明

URL
https://api.searchads.apple.com/api/v4/campaigns/{campaignId}/adgroups/{adgroupId}请求方式
GET请求头参数
Authorization
X-AP-ContextURL中参数
campaignId  #广告系列ID
adgroupId  #广告组ID

7.2.2 响应状态码

200 请求成功
401 无权限,请检查请求参数
403 拒绝请求,请检查请求参数
404 请求资源为找到,请检查请求参数
500 苹果服务器异常,稍后重试

7.2.3 响应示例

{"id": 542370539,"campaignId": 56543219,"name": " ad group name example","cpaGoal": {"amount": "100","currency": "USD"},"startTime": "2021-04-08T12:00:22.788","endTime": "2021-04-09T12:00:22.788","automatedKeywordsOptIn": false,"defaultBidAmount": {"amount": "100","currency": "USD"},"pricingModel": "CPC","targetingDimensions": {"age": {"included": [{"minAge": 20,"maxAge": 25},{"minAge": 25,"maxAge": 55}]},"gender": {"included": ["M","F"]},"country": null,"adminArea": null,"locality": null,"deviceClass": {"included": ["IPAD","IPHONE"]},"daypart": {"userTime": {"included": [1,3,22]}},"appDownloaders": null},"orgId": 40669820,"modificationTime": "2020-04-08T19:00:24.105","status": "ENABLED","servingStatus": "RUNNING","servingStateReasons": null,"displayStatus": "RUNNING","deleted": false
}

示例代码:

import jwt
import datetime as dt
import requests
import json
import time
client_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577'
team_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577'
key_id = 'bacaebda-e219-41ee-a907-e2c25b24d1b2'
audience = 'https://appleid.apple.com'
alg = 'ES256'
Define issue timestamp.
issued_at_timestamp = int(dt.datetime.utcnow().timestamp())
issued_at_timestamp = int(time.time())
Define expiration timestamp. May not exceed 180 days from issue timestamp.
expiration_timestamp = issued_at_timestamp + 600
Define JWT headers.
headers = dict()
headers['alg'] = alg
headers['kid'] = key_id
Define JWT payload.
payload = dict()
payload['sub'] = client_id
payload['aud'] = audience
payload['iat'] = issued_at_timestamp
payload['exp'] = expiration_timestamp
payload['iss'] = team_id
Path to signed private key.
KEY_FILE = 'private-key.pem'
with open(KEY_FILE,'r') as key_file:
key = ''.join(key_file.readlines())
key = '''-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINEGa2CfhaseOXzsHoya/UW4kgQsij9ZW6j3+GQS2zEwoAoGCCqGSM49
AwEHoUQDQgAENEUUVYnQ+hWQRT3+YEwT3m2VGS5jlO6lanvZgLDHkWfOXhBiUfF8
Cyz/X3bzbkP8pOdJZ901qGdyeW73RV1RmA==
-----END EC PRIVATE KEY-----'''
获取 client_secret
client_secret = jwt.encode(
payload=payload,
headers=headers,
algorithm=alg,
key=key
)
client_secret = client_secret.decode('utf-8')
print('-------------------- client_secret --------------------')
print(client_secret)
获取 access_token
url = 'https://appleid.apple.com/auth/oauth2/token?grant_type=client_credentials&client_id=' + client_id + '&client_secret=' + client_secret + '&scope=searchadsorg'
token = requests.post(url=url)
token_json = json.loads(token.content.decode())
access_token = token_json['access_token']
print('-------------------- access_token --------------------')
print(access_token)
acls
url = "https://api.searchads.apple.com/api/v4/acls"
headers = {'Authorization': 'Bearer %s' % (access_token),'Content-Type': 'application/json'
}
acl = requests.get(url=url, headers=headers)
print('-------------------- acl --------------------')
print(acl.text)
org_id = 123456
campaign_id = 1234567890
headers = {'Authorization': 'Bearer %s' % (access_token),'Content-Type': 'application/json','X-AP-Context': 'orgId=%d' % (org_id),
}
获取 campaigns
url = 'https://api.searchads.apple.com/api/v4/campaigns'
campaigns = requests.get(url=url, headers=headers)
print('-------------------- campaign --------------------')
print(campaigns.text)
获取 campaign report
url = 'https://api.searchads.apple.com/api/v4/reports/campaigns'
data = {# "startTime": str(dt.date.today()),# "endTime": str(dt.date.today()),"startTime": "2022-09-01","endTime": "2022-09-01","selector": {"orderBy": [{"field": "localSpend","sortOrder": "ASCENDING"}],"conditions": [{"field": "campaignId","operator": "EQUALS","values": [campaign_id],"ignoreCase": False},{"field": "deleted","operator": "IN","values": ["false","true"]}],"pagination": {"offset": 0,"limit": 1000}},"returnRowTotals": True,"granularity": "DAILY","timeZone": "ORTZ","returnGrandTotals": True,"returnRecordsWithNoMetrics": True
}
data = json.dumps(data)
reports = requests.post(url=url, data=data, headers=headers)
print('-------------------- campaign reports --------------------')
print(reports.text)
package main
import ("fmt""io/ioutil""net/http""strings""time""github.com/dgrijalva/jwt-go/v4"
)
func main() {client_id := "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577"team_id := "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577"key_id := "bacaebda-e219-41ee-a907-e2c25b24d1b2"audience := "https://appleid.apple.com"alg := "ES256"issued_at_timestamp := time.Now().Unix()expiration_timestamp := issued_at_timestamp + 600claim := jwt.MapClaims{"sub": client_id,"aud": audience,"iat": issued_at_timestamp,"exp": expiration_timestamp,"iss": team_id,}private_pem := `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINEGa2CfhaseOXzsHoya/UW4kgQsij9ZW6j3+GQS2zEwoAoGCCqGSM49
AwEHoUQDQgAENEUUVYnQ+hWQRT3+YEwT3m2VGS5jlO6lanvZgLDHkWfOXhBiUfF8
Cyz/X3bzbkP8pOdJZ901qGdyeW73RV1RmA==
-----END EC PRIVATE KEY-----`private_key, _ := jwt.ParseECPrivateKeyFromPEM([]byte(private_pem))token := jwt.NewWithClaims(jwt.SigningMethodES256, claim)token.Header = map[string]interface{}{"alg": alg,"kid": key_id,}client_secret, _ := token.SignedString(private_key)fmt.Println("-------------------- client_secret --------------------")fmt.Println(client_secret)// 获取 access_tokenurl := "https://appleid.apple.com/auth/oauth2/token"res_token, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("grant_type=client_credentials&client_id="+client_id+"&client_secret="+client_secret+"&scope=searchadsorg"))if err != nil {fmt.Println(err.Error())}defer res_token.Body.Close()access_token, _ := ioutil.ReadAll(res_token.Body)fmt.Println("-------------------- access_token --------------------")fmt.Println(string(access_token))
}

ASAToken.java

- package com.appsa.asa;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.apache.commons.io.IOUtils;
import java.io.DataOutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.ECKey;
import java.util.Date;
import java.util.HashMap;
/**
apple ads java demo
*/
public class ASAToken {private static String client_id = "xxx";private static String team_id = "xxx";private static String key_id = "xxx";private static String aud = "https://appleid.apple.com";private static String alg = "ES256";// 生成私钥// openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem// 使用私钥生成公钥// openssl ec -in private-key.pem -pubout -out public-key.pem// 将PKCS1私钥转换为PKCS8(该格式一般Java调用)// openssl pkcs8 -topk8 -inform pem -in private-key.pem -outform pem -nocrypt -out private-key-new.pemprivate static final String PRIVATE_KEY_FILE_256 = "/Users/xxx/Downloads/private-key-new.pem";public static void main(String[] args) {System.out.println("client_id:" + client_id);System.out.println("  team_id:" + team_id);System.out.println("   key_id:" + key_id);try {String clientSecret = createClientSecret(PRIVATE_KEY_FILE_256);System.out.println("clientSecret 建议保存,有效期可设置最长 180 天");System.out.println(clientSecret);String url = "https://appleid.apple.com/auth/oauth2/token";String urlParameters = "grant_type=client_credentials&scope=searchadsorg&client_id=" + client_id + "&client_secret=" + clientSecret;byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);int postDataLength = postData.length;URL obj = new URL(url);HttpURLConnection con = (HttpURLConnection) obj.openConnection();con.setDoOutput(true);con.setRequestMethod("POST");con.setRequestProperty("charset", "utf-8");con.setRequestProperty("Content-Length", Integer.toString(postDataLength));con.setRequestProperty("Host", "appleid.apple.com");con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");con.setUseCaches(false);try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {wr.write(postData);}if (con.getResponseCode() == 200) {String result = IOUtils.toString(con.getInputStream(), StandardCharsets.UTF_8);JSONObject jsonObject = JSONObject.parseObject(result);System.out.println("access_token 有效期1个小时");System.out.println(jsonObject.getString("access_token"));}} catch (Exception e) {System.err.println("error");}}/*** Create a Client Secret** @return client secret*/public static String createClientSecret(String privateKeyPath) throws Exception {Algorithm algorithm = Algorithm.ECDSA256((ECKey) PemUtils.readPrivateKeyFromFile(privateKeyPath, "EC"));return JWT.create().withIssuer(team_id).withAudience(aud).withHeader(new HashMap() {{put("alg", alg);put("kid", key_id);}}).withSubject(client_id).withIssuedAt(new Date()).withExpiresAt(new Date(System.currentTimeMillis() + 86400 * 180 * 1000L)).sign(algorithm);}
}

PemUtils.java

package com.appsa.asa;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class PemUtils {private static byte[] parsePEMFile(File pemFile) throws IOException {if (!pemFile.isFile() || !pemFile.exists()) {throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));}PemReader reader = new PemReader(new FileReader(pemFile));PemObject pemObject = reader.readPemObject();byte[] content = pemObject.getContent();reader.close();return content;}private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) {PublicKey publicKey = null;try {KeyFactory kf = KeyFactory.getInstance(algorithm);EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);publicKey = kf.generatePublic(keySpec);} catch (NoSuchAlgorithmException e) {System.out.println("Could not reconstruct the public key, the given algorithm could not be found.");} catch (InvalidKeySpecException e) {System.out.println("Could not reconstruct the public key");}return publicKey;}private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) {PrivateKey privateKey = null;try {KeyFactory kf = KeyFactory.getInstance(algorithm);EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);privateKey = kf.generatePrivate(keySpec);} catch (NoSuchAlgorithmException e) {System.out.println("Could not reconstruct the private key, the given algorithm could not be found.");} catch (InvalidKeySpecException e) {System.out.println("Could not reconstruct the private key.");}return privateKey;}public static PublicKey readPublicKeyFromFile(String filepath, String algorithm) throws IOException {byte[] bytes = PemUtils.parsePEMFile(new File(filepath));return PemUtils.getPublicKey(bytes, algorithm);}public static PrivateKey readPrivateKeyFromFile(String filepath, String algorithm) throws IOException {byte[] bytes = PemUtils.parsePEMFile(new File(filepath));return PemUtils.getPrivateKey(bytes, algorithm);}
}

四、注意事项总结

1. 框架须知

  • AdServices.framework 这个框架是用于 ASA 归因的,不受 ATT 约束,就是无论用户是否允许跟踪,都可以归因,区别为是否能获取点击时间,仅支持 14.3 及更高版本系统,需 XCode 12.3 及更高版本支持,设置为 optional。

  • AppTrackingTransparency.framework 在 iOS 14 及更高版本用于征求用户跟踪许可的框架,就是弹窗询问用户是否同意跟踪;在 iOS 14.5 上苹果将强制要求开发者实施,也是获取 IDFA 的前提。

2.token相关注意事项

  • AdServices 获取的 token 不可作为唯一设备标识

  • 获取 token 需要设备联网,并且要做超时及容错处理

  • 获取token后需等待五秒再发送请求给归因API

  • 获取 token 的步骤,可以借助日志系统收集相关信息,用于排查问题和代码优化

  • APP首次激活时获取token,建议获取网络权限后,等待500ms-1000ms再做token请求

  • 如果 App 退出前台,在下一次打开进行 token 请求重试

  • 对token请求成功率进行监测,建议成功率下降>=5%应触发预警机制,并根据报错调查原因

  • token 有效期为 24 小时

  • 当 token 无效时,接口响应码为 400

  • 当 token 过期(有效期为 24 小时)时,接口响应码为 404

3.数据归因相关注意事项

  • AdServices 的 restful api 请求,失败后 每隔 5 秒重试,建议最多 3 次 ,也可根据需求增加重试次数,如重试之后仍然没有正常,统计返回的报错日志

  • 有数据差合理,如进行反馈需提供 APP Id、发生问题的时间、API返回结果

  • Apple Search Ads归因窗口期为30天,用户30天以前点击的广告我们无法归因

  • Apple Search Ads采用末次归因,用户如果30天或一天内惦记了搜索标签,又搜了关键词,归因返回的是用户最后一次点击广告的信息

  • 目前苹果搜索广告归因仅支持Adservice,并且Adservice仅支持14.3及以上的用户,14.3以下的用户不会被归因到,这些用户会被计入自然量

  • 14.3以下的用户还可请求API,返回结果为attribution = false

  • 目前14.3以下的用户占比非常低,过去4年推出的ipnone 90%使用ios16,81%所有设备使用ios16

4.归因字段缺失原因

  • AdServices 归因在 ATT弹框之前,不返回字段clickDate

  • ATT弹框后,用户不允许跟踪,不返回字段clickDate

  • AdServices 归因建议在 ATT 弹窗之后,用户如允许跟踪,则可以获得 clickDate

  • 开启 Search Match(搜索匹配)的广告组带来的激活,不返回字段 keywordId

  • 默认素材带来的激活,不返回字段 adId

5.客户端处理逻辑参考

  • 由于各种原因导致的获取归因包失败时,需要做容错处理,及时进行重试(必需)

  • 重试多次仍然失败的,应用在下次启动时再进行获取(必需)

  • 当归因包返回的 attribution 为 false,7 天后再请求归因(建议)

  • iOS 14.3 以下,设置项为限制跟踪时,7 天后再次请求归因(建议)

  • 当归因包返回的 attribution 为 true 时,30 天后再请求归因(建议)

 

五、参考文档

Apple Ads 归因 API

Apple Ads 归因 API 文档 (PDF)

ad_services

Apple Search Ads

 

 

相关文章:

苹果ASA归因对接以及API接入

一、归因概要 广告归因&#xff0c;目的是用于衡量广告带来的激活用户的成本以及后续进一步的用户质量表现。 Apple Ads 广告平台是基于 App Store&#xff08;站内广告&#xff09;&#xff0c;同时属于自归因平台&#xff08;通常称为 SAN&#xff09;。这两个因素&#xff…...

Git常用操作学习

目录 Git基础概述 1.1 什么是Git&#xff1f; 1.2 Git的优点Git工作流程 2.1 集中式工作流程 2.2 功能分支工作流程 2.3 Git Flow工作流程克隆仓库 3.1 使用git clone 3.2 克隆特定分支分支管理 4.1 创建分支 4.2 切换分支 4.3 合并分支 4.4 删除分支提交和推送更改 5.1 查看状…...

2.5D视觉——Aruco码定位检测

目录 1.什么是Aruco标记2.Aruco码解码说明2.1 Original ArUco2.2 预设的二维码字典2.3 大小Aruco二维码叠加 3.函数说明3.1 cv::aruco::detectMarkers3.2 cv::solvePnP 4.代码注解4.1 Landmark图说明4.2 算法源码注解 1.什么是Aruco标记 ArUco标记最初由S.Garrido-Jurado等人在…...

【PSQLException: An I/O error occurred while sending to the backend.】

PSQLException: An I/O error occurred while sending to the backend. java项目定时任务执行耗时很长的sql语句(很多条sql,从很多表中,很多数据中查询,处理)总之,耗时很长(PG数据库)。报错I/O error,Caused by : java.net.SocketTimeoutException: Read time out场景…...

图像基础算法学习笔记

目录 概要 一、图像采集 二、图像标注 四、图像几何变换 五、图像边缘检测 Sobel算子 Scharrt算子 Laplacian算子 Canny边缘检测 六、形态学转换 概要 参考书籍&#xff1a;《机器视觉与人工智能应用开发技术》 廖建尚&#xff0c;钟君柳 出版时间&#xff1a;2024-…...

【Elasticsearch】01-ES安装

1. 安装 安装elasticsearch。 docker run -d \--name es \-e "ES_JAVA_OPTS-Xms512m -Xmx512m" \-e "discovery.typesingle-node" \-v es-data:/usr/share/elasticsearch/data \-v es-plugins:/usr/share/elasticsearch/plugins \--privileged \--networ…...

网络性能测试

一、iperf网络性能测试工具 测试udp丢包率 在服务器启动 iperf 服务端 iperf -p 9000 -s -u -i 1参数说明&#xff1a; -p : 端口号 -s : 表示服务端 -u : 表示 udp 协议 -i : 检测的时间间隔(单位&#xff0c;秒) 在客户端&#xff0c;启动 iperf 客户端 iperf -c xxx.xxx.14…...

docker:docker: Get https://registry-1.docker.io/v2/: net/http: request canceled

无数次的拉镜像让人崩溃&#xff1a; rootnode11:~/ragflow/docker# more rag.sh #export HTTP_PROXYhttp://192.168.207.127:7890 #export HTTPS_PROXYhttp://192.168.207.127:7890 #export NO_PROXYlocalhost,127.0.0.1,.aliyun.com docker compose -f docker-compose-gpu-C…...

esp32c3开发板通过micropython的mqtt库连MQTT物联网消息服务器

MQTT介绍 MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级的消息协议&#xff0c;旨在设备之间进行通信&#xff0c;尤其是在网络条件较差的情况下。MQTT v3.1.1 和 MQTT v5 是该协议的两个主要版本。 MQTT v3.1.1&#xff1a; 优点&#xff…...

OceanBase 升级过程研究(4.2.1.6-4.2.1.8)

模拟业务 使用benchmark加载10仓数据模拟业务场景 升级方法 使用滚动升级方式来进行OB升级。该方法前提是OB集群必须满足官方规定的高可用架构(如果 Zone 个数小于 3&#xff0c;滚动升级时则无法构成多数派), 滚动升级的原理就是轮流完成每个ZONE的升级工作&#xff0c;由于…...

ubuntu下怎么设置机器程序开机自启?

在 Ubuntu 中&#xff0c;可以通过多种方法设置程序或脚本在系统启动时自动运行。以下是几种常见方法&#xff1a; 方法 1&#xff1a;使用 crontab crontab 是一个定时任务管理工具&#xff0c;可以用来设置程序在开机时自动运行。 1. 打开终端&#xff0c;编辑当前用户的 …...

Cesium 相机系统

Cesium 的相机系统是其 3D 地球渲染引擎的重要组成部分&#xff0c;它控制用户在虚拟地球上的视图和交互体验。Cesium 的相机系统具备灵活性和强大的功能&#xff0c;允许开发者自定义视图、导航和交互方式。以下是 Cesium 相机系统的主要特点和功能&#xff1a; 1. 相机的基本…...

数据结构(基本概念及顺序表——c语言实现)

基本概念&#xff1a; 1、引入 程序数据结构算法 数据&#xff1a; 数值数据&#xff1a;能够直接参加运算的数据&#xff08;数值&#xff0c;字符&#xff09; 非数值数据&#xff1a;不能够直接参加运算的数据&#xff08;字符串、图片等&#xff09; 数据即是信息的载…...

ZYNQ程序固化——ZYNQ学习笔记7

一、ZYNQ启动过程 二、 SD卡启动实操 1、对ZYNQ进行配置添加Flash 2、添加SD卡 3、重新生成硬件信息 4、创建vitis工程文件 5、勾选板级支持包 6、对系统工程进行整体编译&#xff0c;生成两个Debug文件&#xff0c;如图所示。 7、插入SD卡&#xff0c;格式化为 8、考入BOOT.…...

labview使用报表工具从数据库导出数据

之前写了一篇labview从数据库导出数据到excel电子表格&#xff0c;但是是基于调用excel的activeX控件&#xff0c;有时候会有一些bug&#xff0c;就比如我工作机就无法显示方法&#xff0c;后面大哥指点才知道没有的原因是excel安装不完整。像我的工作机就没有这个选项。就需要…...

#define定义宏(2)

大家好&#xff0c;今天给大家分享两个技巧。 首先我们应该先了解一下c语言中字符串具有自动连接的特点。注意只有将字符串作为宏参数的时候才可以把字符串放在字符串中。 下面我们来讲讲这两个技巧 1.使用#&#xff0c;把一个宏参数变成对应的字符串。 2.##的作用 可以把位…...

CentOS网络配置

上一篇文章&#xff1a;VMware Workstation安装Centos系统 在CentOS系统中进行网络配置是确保系统能够顺畅接入网络的重要步骤。本文将详细介绍如何配置静态IP地址、网关、DNS等关键网络参数&#xff0c;以帮助需要的人快速掌握CentOS网络配置的基本方法和技巧。通过遵循本文的…...

基于vue框架的的网上宠物交易管理系统46sn1(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,宠物分类,宠物信息 开题报告内容 基于Vue框架的网上宠物交易管理系统开题报告 一、研究背景 随着互联网技术的飞速发展和人们生活水平的提高&#xff0c;宠物已成为许多家庭的重要成员。宠物市场的繁荣不仅体现在实体店的遍地开…...

MySQL数据库:SQL语言入门 【2】(学习笔记)

目录 2&#xff0c;DML —— 数据操作语言&#xff08;Data Manipulation Language&#xff09; &#xff08;1&#xff09;insert 增加 数据 &#xff08;2&#xff09;delete 删除 数据 truncate 删除表和数据&#xff0c;再创建一个新表 &#xff08;3&#xf…...

MySQL深度剖析-索引原理由浅入深

什么是索引&#xff1f; 官方上面说索引是帮助MySQL高效获取数据的数据结构&#xff0c;通俗点的说&#xff0c;数据库索引好比是一本书的目录&#xff0c;可以直接根据页码找到对应的内容&#xff0c;目的就是为了加快数据库的查询速度。 索引是对数据库表中一列或多列的值进…...

路径规划——RRT-Connect算法

路径规划——RRT-Connect算法 算法原理 RRT-Connect算法是在RRT算法的基础上进行的扩展&#xff0c;引入了双树生长&#xff0c;分别以起点和目标点为树的根节点同时扩展随机树从而实现对状态空间的快速搜索。在此算法中以两棵随机树建立连接为路径规划成功的条件。并且&…...

数据科学与SQL:如何计算排列熵?| 基于SQL实现

目录 0 引言 1 排列熵的计算原理 2 数据准备 3 问题分析 4 小结 0 引言 把“熵”应用在系统论中的信息管理方法称为熵方法。熵越大&#xff0c;说明系统越混乱&#xff0c;携带的信息越少&#xff1b;熵越小&#xff0c;说明系统越有序&#xff0c;携带的信息越多。在传感…...

Redis/Codis性能瓶颈揭秘:网卡软中断的影响与优化

目录 现象回顾 问题剖析 现场分析 解决方案 总结与反思 1.调整中断亲和性&#xff08;IRQ Affinity&#xff09;&#xff1a; 2.RPS&#xff08;Receive Packet Steering&#xff09;和 RFS&#xff08;Receive Flow Steering&#xff09;&#xff1a; 近期&#xff0c;…...

微知-DOCA ARGP参数模块的相关接口和用法(config单元、params单元,argp pipe line,回调)

文章目录 1. 背景2. 设置参数的主要流程2.1 初始化2.2 注册某个params的处理方式以及回调函数2.4 定义好前面的params以及init指定config地点后start处理argv 3. 其他4. DOCA ARGP包相关4.1 主要接口4.2 DOCA ARGP的2个rpm包4.2.1 doca-sdk-argp-2.9.0072-1.el8.x86_64.rpm4.2.…...

PostgreSQL高可用Patroni安装(超详细)

目录 一 安装Patroni 0 Patroni 对Python的版本要求 1 卸载原来的Python 3.6 版本 2 安装Python 3.7 之上版本 3 安装依赖 psycopg3 4 安装patroni 5 卸载 patroni 二 安装ETCD 1 使用 yum 安装 etcd 2 etcd 配置文件 3 管理 etcd 4 设置密码 5 常用命令 三 安装…...

mcu之,armv7架构,contex-M4系列,时钟树,中断,IO架构(一)

写这篇文章的目的&#xff0c;是记录一下arm架构的32mcu&#xff0c;方便记忆芯片架构原理&#xff0c;方便我展开对&#xff0c;BootLoader的研究。 arm架构&#xff0c;时钟树&#xff0c;先做个记录&#xff0c;有空写。...

论文解析:基于区块链的去中心化服务选择,用于QoS感知的云制造(四区)

目录 论文解析:基于区块链的去中心化服务选择,用于QoS感知的云制造(四区) 基于区块链的去中心化云制造服务选择方法 一、核心内容概述 二、核心创新点及原理与理论 三、实验与理论分析 PBFT(实用拜占庭容错) 论文解析:基于区块链的去中心化服务选择,用于QoS感知的…...

详细解析STM32 GPIO引脚的8种模式

目录 一、输入浮空&#xff08;Floating Input&#xff09;&#xff1a;GPIO引脚不连接任何上拉或下拉电阻&#xff0c;处于高阻态 1.浮空输入的定义 2.浮空输入的特点 3.浮空输入的应用场景 4.浮空输入的缺点 5.典型配置方式 6.注意事项 二、输入上拉&#xff08;Inpu…...

【hacker送书第16期】Python数据分析、挖掘与可视化、AI全能助手ChatGPT职场工作效率提升技巧与案例

解锁数据分析与AI应用的双重秘密&#xff1a;全面推广《Python数据分析、挖掘与可视化从入门到精通》与《AI全能助手ChatGPT职场工作效率提升技巧与案例》 前言Python数据分析、挖掘与可视化从入门到精通&#x1f495;内容简介获取方式 AI全能助手ChatGPT职场工作效率提升技巧与…...

翼鸥教育:从OceanBase V3.1.4 到 V4.2.1,8套核心集群升级实践

引言&#xff1a;自2021年起&#xff0c;翼鸥教育便开始应用OceanBase社区版&#xff0c;两年间&#xff0c;先后部署了总计12套生产集群&#xff0c;其中核心集群占比超过四分之三&#xff0c;所承载的数据量已突破30TB。自2022年10月&#xff0c;OceanBase 社区发布了4.2.x 版…...

阿里云个人怎么免费做网站/新闻摘抄2022最新20篇

简单谈谈Server2008的NAP<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />刚才看到一个讨论NAP的帖子&#xff0c;感觉这里好像不少人对NAP还不是很了解。我就用我的理解给简单介绍一下吧。什么是NAP&#xff1f;NAP-Network Acc…...

现在能用的网站/开发网站多少钱

其实这个很简单&#xff0c;在img我们加入一个a标签&#xff0c;然后 <a hreftencent://message/?uinQQ号码&Site网站地址&Menuyes></a>转载于:https://www.cnblogs.com/jiangu66/archive/2013/04/12/3017508.html...

如何做微网站/活动策划公司

记录一些简单的表的管理知识,方便使用&#xff01;mysql> desc yql9;-------------------------------------------| Field | Type | Null | Key | Default | Extra |-------------------------------------------| id | int(11) | YES | | NULL | || v…...

让你做一个旅游网站你会怎么做/游戏推广公司怎么接游戏的

腾讯企点服务是什么&#xff1f;企点服务是在腾讯旗下各大社交数据与AI能力的基础上&#xff0c;专为满足企业现代化移动办公需求而研发打造的SAAS服务社会化客户关系管理平台&#xff0c;为企业的网络营销推广、内部办公协同、客服接待咨询提供解决方案&#xff0c;助力企业高…...

网站建设竞标需要怎么做/营业推广的目标通常是

选用ZIP Archive版本的win64下面我再把步骤说明一下&#xff1a;选中上图的版本&#xff0c;开始下载。2、解压安装包解压下载的安装包&#xff0c;直接放到系统根目录下&#xff0c;我放置的是D:\mysql3、配置my.ini文件解压后&#xff0c;根目录下&#xff0c;有一个my-defau…...

武汉建设局网站/企业官网

/*** 该示例展示了创建不同样式的 GeoODLine*/ var app new THING.App(); app.background [0, 0, 0]THING.Utils.dynamicLoad(https://www.thingjs.com/uearth/uearth.min.js, function () {// 创建一个地图var map app.create({type: Map,attribution: Google,style: {nigh…...