# 下载落地页(dl.ymaikj.com) 一个纯静态页,托管在阿里云 OSS 上,**不占服务器、不经 NPM**。 安装包也放同一个桶里,页面按 UA 决定跳商店还是给 APK 直链。 ``` deploy/dl/ ├── index.template.html ← 源文件,改页面改这个 ├── build.sh ← 把 App 图标内联进去,生成 index.html └── index.html ← 生成物,**上传的是它**(已入库,方便直接传) ``` **版本号、更新日志、各商店链接都不在页面里维护**,实时取自管理后台(见下文「三」)。 图标取自 `apps/client/assets/images/app_icon.png`,与 App 用的是同一份素材。 --- ## 一、为什么放 OSS 而不是放业务服务器 下载页一般要挂 APK 直链。微信、各家浏览器对「发 apk 的域名」判定很粗暴, 一旦被拦截或标记为风险,**同域名上的接口请求也会一起遭殃**。所以它必须与 `ym.ymaikj.com`(业务网关)分开。 放 OSS 还有两个顺带好处:页面和安装包同源(没有跨域、没有防盗链问题), 以及不占业务机的带宽——一个版本几十 MB,几千次下载就是几百 GB。 --- ## 二、部署步骤(首次) > ✅ 2026-09-23:`ymai-dl` 桶、`dl.ymaikj.com` 域名与证书**均已建好并实测可用** > (证书有效期至 2027-03-23,404 路由正确,与 `ym.ymaikj.com` 的跨域也已实测通过)。 > 下面三步留档,重建时照做。 ### 1. 准备存储桶 **建议新建一个专用桶**,例如 `ymai-dl`(上海,与 `ymaioss` 同地域): - 读写权限:**公共读**(安装包本来就要公开下载) - 不要复用 `ymaioss`。那个桶里是用户录音,把公开分发的东西混进去会让 权限、生命周期规则、流量账单都分不清谁是谁。 > 想省事复用 `ymaioss` 也能跑通,但页面要放在 `EAIMAR/dist/index.html` > 这种子路径下,且 `oss.ymaikj.com` 已经绑给它了,你还得再绑一个 `dl.ymaikj.com`。 > 收益不大,不推荐。 ### 2. 开启静态网站托管 桶 → **数据管理 → 静态页面**: | 项 | 填 | |---|---| | 默认首页 | `index.html` | | 默认 404 页 | `index.html`(可选,让任何路径都落到下载页) | | 子目录首页 | 开启 | 不开这一项,访问 `https://dl.ymaikj.com/` 会返回桶的 XML 列表或 403,而不是页面。 ### 3. 绑定域名 `dl.ymaikj.com` 桶 → **传输管理 → 域名管理 → 绑定域名**: 1. 填 `dl.ymaikj.com`,勾选「自动添加 CNAME 记录」(域名在同一个阿里云账号下时可用; 否则去 DNS 那边手工加一条 CNAME 指向桶的外网 Endpoint) 2. ⚠️ **中国大陆地域的桶绑自定义域名,域名必须已备案**。`ymaikj.com` 应该已备案, 确认一下子域名是否需要单独报备。 3. 证书:同一页的「证书托管」里传一张 `dl.ymaikj.com` 的证书。 阿里云「数字证书管理服务」可以申请免费的 DV 证书,签完直接在这里选。 **不配证书 = 只能 http 访问**,微信和现在的浏览器都会拦。 ### 4. 上传页面与安装包 用 ossutil(推荐,能重复执行): ```bash # 首次配置:ossutil config -e oss-cn-shanghai.aliyuncs.com -i -k # 页面(放桶根) ossutil cp deploy/dl/index.html oss://ymai-dl/index.html \ --meta "Content-Type:text/html; charset=utf-8#Cache-Control:no-cache, must-revalidate" # 安装包 ossutil cp eaimar-release-1.0.2-20260923.apk \ oss://ymai-dl/android/EAIMAR/eaimar-release-1.0.2-20260923.apk \ --meta "Content-Type:application/vnd.android.package-archive#Content-Disposition:attachment" ``` 也可以直接在 OSS 控制台点「上传文件」,然后在文件的「设置 HTTP 头」里改这两项。 ⚠️ **页面必须设 `Cache-Control: no-cache`**。默认缓存会让你改了页面之后老用户 还拿着旧版本,而这种问题根本没人会来报。 ⚠️ **APK 建议设 `Content-Disposition: attachment`**。不设的话部分浏览器会尝试 「预览」这个文件,表现为点了没反应或者打开一堆乱码。 --- ## 三、与管理后台联动(版本号、商店链接、更新日志) 页面打开时会调一次 `POST https://ym.ymaikj.com/api/home/user_getchannelapps` (该接口在网关白名单里,未登录可调),把后台配的东西取回来: | 页面上的 | 来自后台哪里 | 落到哪个字段 | |---|---|---| | 版本号 | 服务配置 → **版本与分发** → 发版配置 → 线上版本 | `app_release.version` | | 「本次更新」 | 版本与分发 → 发版配置 → 更新日志 | `app_release.changelog` | | App Store 链接 | 版本与分发 → **渠道分发** → 全球-苹果(101) 的下载地址 | `channel_app.address` | | 各安卓商店链接 | 渠道分发 → 华为(1) / 小米(2) / OPPO(3) / VIVO(4) / 荣耀(5) | 同上 | | APK 直链 | 渠道分发 → **官方渠道(100)** 的下载地址 | 同上 | **后台改完,页面刷新即生效,不需要重新上传。** 渠道地址留空时后端会自动回退到「发版配置」里的默认下载地址(`AppRelease.DownloadAddress`), 所以只想维护一套地址的话,只填那一处就够——填成 `https://dl.ymaikj.com` 即可兜住所有渠道。 ⚠️ **APK 直链约定挂在「官方渠道(100)」下**。这是本页面单方面的约定,后台界面上没有任何提示, 所以要么按这个约定配,要么去改 `index.template.html` 里 `CFG.channels` 的映射。 ⚠️ 接口打不通时(断网 / 网关挂了 / 跨域被拦)页面用 `CFG.fallback` 的兜底值渲染,**不报错**—— 用户是来下载的,给他看一个报错页没有任何用。代价是兜底值会比后台旧, 所以**别把 fallback 当配置入口去维护**。 --- ## 四、发版时要做什么 1. 打包,产物名按仓库约定:`eaimar-release-<版本号>-<时间>.apk` (**不要留 `app-release.apk`**) 2. 传安装包 —— 两条路任选: - **后台传(推荐)**:服务配置 → **版本与分发** → 「默认下载地址」旁边的 **上传 APK** 按钮。 传到分发桶的 `android/<应用>/` 目录(按应用分目录,先选应用才能传),带进度条, 传完**自动写进「渠道分发 → 官方渠道(100)」**的下载地址,不动「默认下载地址」那一格。 - 手工传:`oss://ymai-dl/android/`,见上文「二、4」 3. 到后台「版本与分发」改线上版本与更新日志(用「上传 APK」传的话,官方渠道地址已自动写好) ### ⚠️ 后台上传的前置:分发桶要配跨域(CORS) 浏览器是**直传到 OSS**(预签名 PUT 打的是 `ymai-dl.oss-cn-shanghai.aliyuncs.com`, 不是 `dl.ymaikj.com`),所以桶上必须有 CORS 规则,否则浏览器直接拦掉、 XHR 只报一句没有细节的「网络错误」。 OSS 控制台 → `ymai-dl` → **权限管理 → 跨域设置 → 创建规则**: | 项 | 填 | |---|---| | 来源 | `https://console.ymaikj.com`
`https://console-dev.ymaikj.com` | | 允许 Methods | `PUT`(顺带勾 GET、HEAD) | | 允许 Headers | `content-type`(或 `*`) | 对照:主桶 `ymaioss` 早就配好了(所以产品固件上传一直能用), 2026-09-23 实测 `ymai-dl` 的预检返回 **403**,需要照着补一条。 页面本身不用动。只有改页面文案/样式,或者 **App 图标换了**,才需要: ```bash cd deploy/dl && ./build.sh # 重新内联图标,生成 index.html # 再把 index.html 传一次 ``` --- ## 五、页面行为 | 环境 | 行为 | |---|---| | iOS | 跳 App Store | | Android(认出品牌) | 主按钮跳对应商店,副按钮给 APK 直链 | | Android(认不出品牌) | 直接给 APK 直链 | | 桌面浏览器 | 两个按钮都给,提示用手机打开 | | **微信 / QQ 内置浏览器** | **一进页面就弹遮罩**,引导「右上角 ··· → 在浏览器中打开」 | ⚠️ 微信那一层不是可选项。微信会拦截 apk 下载而且**静默失败**——点了什么都不发生, 用户只会认为这个页面坏了。所以是进页面就提示,而不是等他点了才说。 品牌判断的口径与 App 内 `getAndroidStoreInfoByBrand` 一致:**判的是手机品牌, 不是从哪家商店下载的**(国内五家共用一个包)。认不出就走 APK,这个兜底方向是安全的。 商店地址与 APK 地址相同时(后台该渠道没单独配、回退到了默认地址)只显示一个按钮—— 两个一模一样的按钮只会让人犹豫点哪个。 --- ## 六、待办 **首选是去后台「渠道分发」配**(见上文「三」),配了就不用动代码。 `index.template.html` 里 `CFG.fallback` 的商店链接目前是占位地址 `https://emai.ymaikj.com/1223`,搜 `TODO(store-links)` 能定位,它只在接口打不通时才会被用到。 各商店上架后可以顺手更新: | key | 从哪拿 | |---|---| | `appStore` | App Store Connect →「App 信息 → Apple ID」,一串纯数字,拼成 `https://apps.apple.com/cn/app/id<数字>` | | `huawei` / `honor` | 各自开发者后台的应用详情页链接,形如 `https://appgallery.huawei.com/app/C<数字>` | | `xiaomi` | `https://app.mi.com/details?id=com.yimai.eaimar` | | `oppo` / `vivo` | 各自开发者后台的详情页链接 | ⚠️ 包名是 **`com.yimai.eaimar`**(iOS bundle id 同名)。 --- ## 七、注意 - **别在这个页面做 Universal Link 唤起**:UL 域名是 `yimai.games`,不是 `ymaikj.com`, 在 ymaikj 的任何子域上配都不会生效。 - 桶是公共读,APK 谁都能下——这对安装包是对的。但**别把别的东西往这个桶里放**。 - 下载量上来之后再在 OSS 前面挂 CDN,页面不用改(域名不变)。 - 图标在页面里只内联一份(``),favicon 与 apple-touch-icon 由 JS 复用同一个 data URI。在 `` 里再写两个 href 会把几十 KB 的 base64 存三份,页面体积直接翻三倍。