Open
feat(cloud): 为 db-init 增加可选 ADMIN_TOKEN 保护 (#2) #2
雅皮士Add on 9 days ago
feat(cloud): 为 db-init 增加可选 ADMIN_TOKEN 保护 (#2) #2
Open
Total: 32 files changed+658-90
| @@ -0,0 +1,21 @@ | |||
| 1 | +MIT License | ||
| 2 | + | ||
| 3 | +Copyright (c) 2026 Wenhao Jiang | ||
| 4 | + | ||
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 6 | +of this software and associated documentation files (the "Software"), to deal | ||
| 7 | +in the Software without restriction, including without limitation the rights | ||
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 9 | +copies of the Software, and to permit persons to whom the Software is | ||
| 10 | +furnished to do so, subject to the following conditions: | ||
| 11 | + | ||
| 12 | +The above copyright notice and this permission notice shall be included in all | ||
| 13 | +copies or substantial portions of the Software. | ||
| 14 | + | ||
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 21 | +SOFTWARE. | ||
| @@ -70,7 +70,8 @@ Function 执行端点,Function 用 Server SDK(API Key)读写 Database, | |||
| 70 | | 项目 | 要求 | 检查或获取方式 | | 70 | | 项目 | 要求 | 检查或获取方式 | |
| 71 | | --- | --- | --- | | 71 | | --- | --- | --- | |
| 72 | | 基础知识 | ArkTS、HTTP、数据库基础 | 可选:HarmonyOS 官方 ArkTS 教程 | | 72 | | 基础知识 | ArkTS、HTTP、数据库基础 | 可选:HarmonyOS 官方 ArkTS 教程 | |
| 73 | -| 开发环境 | Windows/macOS、DevEco Studio、Node.js | `node -v`、DevEco 界面路径 | | 73 | +| 开发环境 | Windows/macOS、DevEco Studio、Node.js、Python 3.8+ | `node -v`、`python --version`、DevEco 界面路径 | |
| 74 | +| Python 依赖 | 无(`deploy.py` 仅用标准库) | 无需 `pip install` | | ||
| 74 | | 测试设备 | 模拟器或真机 | `hdc list targets` | | 75 | | 测试设备 | 模拟器或真机 | `hdc list targets` | |
| 75 | | 账号与服务 | 自托管 Appwrite 服务器可达、项目与 API Key 已建 | `GET /v1/health` 返回 `pass` | | 76 | | 账号与服务 | 自托管 Appwrite 服务器可达、项目与 API Key 已建 | `GET /v1/health` 返回 `pass` | |
| 76 | | 权限与网络 | 服务器 80 端口可达;API Key 含 functions/databases 权限 | `GET /v1/functions` 正常返回 | | 77 | | 权限与网络 | 服务器 80 端口可达;API Key 含 functions/databases 权限 | `GET /v1/functions` 正常返回 | |
| @@ -87,7 +88,7 @@ Function 执行端点,Function 用 Server SDK(API Key)读写 Database, | |||
| 87 | 88 | ||
| 88 | | 占位符 | 含义 | 获取位置 | 安全存放位置 | | 89 | | 占位符 | 含义 | 获取位置 | 安全存放位置 | |
| 89 | | --- | --- | --- | --- | | 90 | | --- | --- | --- | --- | |
| 90 | -| `<appwrite_url>` | 服务器地址 | 服务器 IP + 端口 | 端侧公开配置 | | 91 | +| `APPWRITE_HOST` | 服务器地址 | 服务器 IP + 端口 | 端侧公开配置 | |
| 91 | | `<project_id>` | Appwrite 项目 ID | 控制台 → 项目设置 | 端侧公开配置 | | 92 | | `<project_id>` | Appwrite 项目 ID | 控制台 → 项目设置 | 端侧公开配置 | |
| 92 | | `<api_key>` | 服务器级 API Key | 控制台 → API Keys | 仅函数环境变量 / `.env` | | 93 | | `<api_key>` | 服务器级 API Key | 控制台 → API Keys | 仅函数环境变量 / `.env` | |
| 93 | 94 | ||
| @@ -96,12 +97,28 @@ Function 执行端点,Function 用 Server SDK(API Key)读写 Database, | |||
| 96 | 97 | ||
| 97 | ## 3. 快速体验 | 98 | ## 3. 快速体验 |
| 98 | 99 | ||
| 99 | -> 这条路线面向「先把现成案例跑起来」的读者,目标是在 10–15 分钟内看到第一个成功结果。原理和完整搭建过程见[完整实践](#4-完整实践)。 | 100 | +> 这条路线面向「先把现成案例跑起来」的读者,目标是约 1–2 小时(含一次性云环境准备)内看到第一个成功结果。原理和完整搭建过程见[完整实践](#4-完整实践)。 |
| 101 | + | ||
| 102 | +### 0)用 AI 辅助配置云环境(可选) | ||
| 103 | + | ||
| 104 | +想跳过下面的手动步骤?把本仓库 `README.md` 与 `SKILL.md` 交给 AI 编程助手(码道 / CodeArts、Cursor 等),让它读完自动配置并部署云环境: | ||
| 105 | + | ||
| 106 | +```text | ||
| 107 | +请阅读本仓库 README.md 与 SKILL.md,帮我自动配置并部署「单词打卡」云环境: | ||
| 108 | +1. 按第 2 章「配置与敏感信息」列出需要我填写的占位符(Appwrite 地址、Project ID、API Key),等我填好 harmony_cloud/.env; | ||
| 109 | +2. 运行 deploy/deploy.py 部署 6 个云函数(自动 npm install 并打包 tar.gz); | ||
| 110 | +3. 调用 db-init 建集合与种子数据; | ||
| 111 | +4. 把服务器地址与 Project ID 填入 HttpUtil.ets,编译并安装端侧; | ||
| 112 | +5. 按第 5 章验收表逐项联调。 | ||
| 113 | +每完成一步报告结果;报错时贴完整日志并定位根因;API Key 只写本地 .env,不写入代码、不提交仓库。 | ||
| 114 | +``` | ||
| 115 | + | ||
| 116 | +> **注意**:AI 只能操作你授权的环境。API Key 等敏感信息请只放在本地 `.env`(已被 `.gitignore` 忽略),不要让 AI 把凭据写进代码或提交。部署完成后,仍建议按下面各步骤的「完成标志」人工复核一遍。 | ||
| 100 | 117 | ||
| 101 | ### 1)获取已验证版本 | 118 | ### 1)获取已验证版本 |
| 102 | 119 | ||
| 103 | ```bash | 120 | ```bash |
| 104 | -git clone <repo-url> | 121 | +git clone https://gitcode.com/harmony-practice-center/word-check-in.git |
| 105 | cd word-check-in | 122 | cd word-check-in |
| 106 | ``` | 123 | ``` |
| 107 | 124 | ||
| @@ -113,7 +130,7 @@ cd word-check-in | |||
| 113 | cp harmony_cloud/.env.example harmony_cloud/.env | 130 | cp harmony_cloud/.env.example harmony_cloud/.env |
| 114 | ``` | 131 | ``` |
| 115 | 132 | ||
| 116 | -1. 将 `.env` 中的 API Key、URL、Project ID 替换为第 2 章收集的值。 | 133 | +1. 将 `.env` 中的 API Key、APPWRITE_HOST、Project ID 替换为第 2 章收集的值。 |
| 117 | 2. 在 `harmony_app/entry/src/main/ets/utils/HttpUtil.ets` 填入 `APPWRITE_HOST` 和 `PROJECT_ID`。 | 134 | 2. 在 `harmony_app/entry/src/main/ets/utils/HttpUtil.ets` 填入 `APPWRITE_HOST` 和 `PROJECT_ID`。 |
| 118 | 135 | ||
| 119 | **完成标志**:没有未替换占位符;敏感配置不会出现在 `git status` 中。 | 136 | **完成标志**:没有未替换占位符;敏感配置不会出现在 `git status` 中。 |
| @@ -129,10 +146,14 @@ curl -X POST http://<host>/v1/functions/db-init/executions \ | |||
| 129 | -d '{"body":"{}","async":false}' | 146 | -d '{"body":"{}","async":false}' |
| 130 | ``` | 147 | ``` |
| 131 | 148 | ||
| 149 | +> `deploy.py` 会为每个函数自动执行 `npm install --omit=dev` 并打包依赖,无需手动安装 `node-appwrite`。 | ||
| 150 | + | ||
| 132 | **完成标志**:`db-init` 执行后,`word_db` 下 3 个集合、58 个单词和管理员 `admin@company.com` / `123456` 已创建。 | 151 | **完成标志**:`db-init` 执行后,`word_db` 下 3 个集合、58 个单词和管理员 `admin@company.com` / `123456` 已创建。 |
| 133 | 152 | ||
| 134 | ### 4)构建并运行端侧 | 153 | ### 4)构建并运行端侧 |
| 135 | 154 | ||
| 155 | +> 首次编译前,先用 DevEco Studio 打开 `harmony_app` 目录,让它自动生成 `local.properties`(填入 `sdk.dir`);命令行编译还需设置环境变量 `DEVECO_SDK_HOME`。缺失会报 `00303217`,`sdk.dir` 被改写会报 `00308018`,路径含空格会报 `Space is not supported in HVIGOR_USER_HOME`。详见 [SKILL.md](./SKILL.md) 的「全量故障速查表」。 | ||
| 156 | + | ||
| 136 | ```powershell | 157 | ```powershell |
| 137 | Set-Location 'harmony_app' | 158 | Set-Location 'harmony_app' |
| 138 | ohpm install | 159 | ohpm install |
| @@ -146,12 +167,41 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 146 | 167 | ||
| 147 | > 这条路线面向「理解并复现案例」的读者。步骤按真实工作流排列,每一步都有可观察的完成标志。云函数编写规范、代码模板、打包与排障细节见 [SKILL.md](./SKILL.md)。 | 168 | > 这条路线面向「理解并复现案例」的读者。步骤按真实工作流排列,每一步都有可观察的完成标志。云函数编写规范、代码模板、打包与排障细节见 [SKILL.md](./SKILL.md)。 |
| 148 | 169 | ||
| 170 | +### 一句话让 AI 工作 | ||
| 171 | + | ||
| 172 | +不想手动逐项操作?把下面这句话发给码道(华为云 CodeArts 鸿蒙开发智能体,或任意支持 HarmonyOS / Node.js 的 AI 编程助手),即可让它从需求澄清一路推进到端云联调。你也可以把词库规模、打卡规则换成自己的版本。 | ||
| 173 | + | ||
| 174 | +```text | ||
| 175 | +我想开发一款「单词打卡学习」HarmonyOS 端云一体化应用:端侧 ArkTS/ArkUI(登录、首页、闪卡、测试 4 个页面),云侧 Appwrite 自托管 Functions(Node.js,6 个云函数)+ Appwrite Database(word_db,3 个集合、58 词词库),支持注册登录、每日单词列表、闪卡翻转学习、看词选义测试、进度条与连续打卡。 | ||
| 176 | + | ||
| 177 | +请按照标准研发流程推进:先提问澄清需求,再依次产出 requirements.md、design.md、tasks.md;然后分批实现 6 个云函数(注意 ESM 默认导出、node-appwrite v11、execute=["any"])、3 个集合的数据模型和端侧页面;最后给出部署(deploy.py 打包上传)与端云联调验收步骤。 | ||
| 178 | + | ||
| 179 | +每一步都要有可观察的完成标志,关键节点停下来等我确认。 | ||
| 180 | +``` | ||
| 181 | + | ||
| 182 | +### 如何指挥 AI 工作 | ||
| 183 | + | ||
| 184 | +「一句话」只是开工信号,真正决定质量的是后续每一轮的指挥方式: | ||
| 185 | + | ||
| 186 | +- **一次只给一个明确目标**:每轮指令聚焦单一产出(需求 / 设计 / 任务 / 某一批代码 / 部署),让 AI 给出可观察结果,而不是一次铺开。 | ||
| 187 | +- **要求可观察的完成标志**:每步都让 AI 明确「做完是什么样子」,你据此判断能否进入下一步。 | ||
| 188 | +- **关键节点人工确认**:需求澄清、技术选型、Appwrite 配置、部署上线等不可逆或高成本节点,先看 AI 的方案再放行。 | ||
| 189 | +- **把真实报错带回去**:编译或部署失败时,把完整日志原文贴给 AI 定位,而不是让它凭空猜测。 | ||
| 190 | + | ||
| 191 | +下面的步骤 1–5 每一步都附了「推荐指令」,可照此逐条指挥 AI;想对照手动操作时,看同一步骤的「操作」。 | ||
| 192 | + | ||
| 149 | ### 步骤 1:确认 Appwrite 服务器与收集信息 | 193 | ### 步骤 1:确认 Appwrite 服务器与收集信息 |
| 150 | 194 | ||
| 151 | **目标**:确认自托管 Appwrite 可用,收集 Project ID 与 API Key。 | 195 | **目标**:确认自托管 Appwrite 可用,收集 Project ID 与 API Key。 |
| 152 | 196 | ||
| 153 | **为什么这样做**:自托管环境的可用运行时与端口取决于运维配置,先确认能避免后续部署返工。 | 197 | **为什么这样做**:自托管环境的可用运行时与端口取决于运维配置,先确认能避免后续部署返工。 |
| 154 | 198 | ||
| 199 | +**推荐指令**: | ||
| 200 | + | ||
| 201 | +```text | ||
| 202 | +请帮我梳理这个案例需要确认的 Appwrite 环境信息:服务器地址、Project ID、API Key(含 functions 读写、databases 读写权限)、可用运行时(node-16.0)。把每项的获取位置和安全存放位置整理成表,并给出 /v1/health、/v1/functions/runtimes 等健康检查与权限验证方式。 | ||
| 203 | +``` | ||
| 204 | + | ||
| 155 | **操作**: | 205 | **操作**: |
| 156 | 206 | ||
| 157 | 1. 访问 `/v1/health` 确认服务器 `pass`。 | 207 | 1. 访问 `/v1/health` 确认服务器 `pass`。 |
| @@ -159,7 +209,7 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 159 | 3. 创建 API Key(含 functions.write/read、databases.write/read 权限)。 | 209 | 3. 创建 API Key(含 functions.write/read、databases.write/read 权限)。 |
| 160 | 4. 调用 `GET /v1/functions/runtimes` 确认可用运行时(本例 `node-16.0`)。 | 210 | 4. 调用 `GET /v1/functions/runtimes` 确认可用运行时(本例 `node-16.0`)。 |
| 161 | 211 | ||
| 162 | -**完成标志**:信息清单填齐,`<appwrite_url>` / `<project_id>` / `<api_key>` 均可用。 | 212 | +**完成标志**:信息清单填齐,`APPWRITE_HOST` / `<project_id>` / `<api_key>` 均可用。 |
| 163 | 213 | ||
| 164 | **如果失败**:`missing scopes (["projects.read"])` 说明 API Key 权限不足,用 `GET /v1/functions` 验证,见[验收与排障](#5-验收与排障)。 | 214 | **如果失败**:`missing scopes (["projects.read"])` 说明 API Key 权限不足,用 `GET /v1/functions` 验证,见[验收与排障](#5-验收与排障)。 |
| 165 | 215 | ||
| @@ -169,6 +219,12 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 169 | 219 | ||
| 170 | **为什么这样做**:Appwrite v1.9.0 不会自动安装依赖,且函数入口用 ESM 默认导出,写错会导致运行时报错。 | 220 | **为什么这样做**:Appwrite v1.9.0 不会自动安装依赖,且函数入口用 ESM 默认导出,写错会导致运行时报错。 |
| 171 | 221 | ||
| 222 | +**推荐指令**: | ||
| 223 | + | ||
| 224 | +```text | ||
| 225 | +请按 Appwrite Functions 编写规范实现 6 个云函数(register、login、getDailyWords、learnWord、getProgress、db-init)。注意:ESM 默认导出、node-appwrite@^11.0.0(Node 16 兼容)、Server SDK 用 API Key 读写数据库、统一 {code,data,msg} 返回。完成后给出每个函数的依赖安装与 tar.gz 打包要求(根目录含 main.js + node_modules)。 | ||
| 226 | +``` | ||
| 227 | + | ||
| 172 | **操作**: | 228 | **操作**: |
| 173 | 229 | ||
| 174 | 1. 按 [SKILL.md](./SKILL.md) 的「云函数编写规范」与「代码模板」编写 6 个函数。 | 230 | 1. 按 [SKILL.md](./SKILL.md) 的「云函数编写规范」与「代码模板」编写 6 个函数。 |
| @@ -184,6 +240,12 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 184 | 240 | ||
| 185 | **为什么这样做**:手动路线便于逐项确认权限与运行时,自动路线适合批量复用;两条路线产出相同,交叉创建同名资源会导致冲突。 | 241 | **为什么这样做**:手动路线便于逐项确认权限与运行时,自动路线适合批量复用;两条路线产出相同,交叉创建同名资源会导致冲突。 |
| 186 | 242 | ||
| 243 | +**推荐指令**: | ||
| 244 | + | ||
| 245 | +```text | ||
| 246 | +请编写部署脚本 deploy.py(为每个函数执行 npm install --omit=dev 并打包成 tar.gz 上传),并说明 db-init 建集合与种子数据(3 个集合、58 个单词、管理员 admin@company.com / 123456)的执行方式。同时给出为每个函数配置 API_KEY、DATABASE_ID 环境变量与 execute=["any"] 的要点。 | ||
| 247 | +``` | ||
| 248 | + | ||
| 187 | **操作**: | 249 | **操作**: |
| 188 | 250 | ||
| 189 | 1. 选择一条路线:**手动**在控制台逐项创建,或**自动**用 Python 脚本(见 SKILL 的「部署与打包」)。 | 251 | 1. 选择一条路线:**手动**在控制台逐项创建,或**自动**用 Python 脚本(见 SKILL 的「部署与打包」)。 |
| @@ -201,6 +263,12 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 201 | 263 | ||
| 202 | **为什么这样做**:端侧通过 `HttpUtil.ets` 指向 Appwrite 执行端点;SDK 版本、工具链路径等环境问题会阻塞编译。 | 264 | **为什么这样做**:端侧通过 `HttpUtil.ets` 指向 Appwrite 执行端点;SDK 版本、工具链路径等环境问题会阻塞编译。 |
| 203 | 265 | ||
| 266 | +**推荐指令**: | ||
| 267 | + | ||
| 268 | +```text | ||
| 269 | +请实现端侧 ArkTS 工程:在 HttpUtil.ets 统一配置 APPWRITE_HOST(http:// 协议)与 PROJECT_ID,并确认 functionId 映射与部署一致;4 个页面(登录、首页、闪卡、测试)通过函数执行端点调用云侧。注意配置 ohos.permission.INTERNET 权限;编译前核对 SDK 版本与 DEVECO_SDK_HOME,遇报错先定位根因再做最小修改。 | ||
| 270 | +``` | ||
| 271 | + | ||
| 204 | **操作**: | 272 | **操作**: |
| 205 | 273 | ||
| 206 | 1. 在 `HttpUtil.ets` 填入 `APPWRITE_HOST`(`http://` 协议)和 `PROJECT_ID`,确认 functionId 映射与部署一致。 | 274 | 1. 在 `HttpUtil.ets` 填入 `APPWRITE_HOST`(`http://` 协议)和 `PROJECT_ID`,确认 functionId 映射与部署一致。 |
| @@ -217,6 +285,12 @@ hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap | |||
| 217 | 285 | ||
| 218 | **为什么这样做**:这是验证「命令提交成功 ≠ 部署完成」的关键一步,只有实际调用通过才算完成。 | 286 | **为什么这样做**:这是验证「命令提交成功 ≠ 部署完成」的关键一步,只有实际调用通过才算完成。 |
| 219 | 287 | ||
| 288 | +**推荐指令**: | ||
| 289 | + | ||
| 290 | +```text | ||
| 291 | +请给我一份端云联调验收清单:逐项列出注册/登录、查看今日单词、闪卡翻转、看词选义测试、打卡状态与连续天数等操作与预期结果。并在模拟器/真机上执行一轮自动测试,逐项记录结果;发现问题先定位相关代码,修复后做针对性回归。 | ||
| 292 | +``` | ||
| 293 | + | ||
| 220 | **操作**:按第 5 章的业务验收表逐项执行。 | 294 | **操作**:按第 5 章的业务验收表逐项执行。 |
| 221 | 295 | ||
| 222 | **完成标志**:第 5 章验收项全部通过,打卡与进度数据正确。 | 296 | **完成标志**:第 5 章验收项全部通过,打卡与进度数据正确。 |
| @@ -107,7 +107,7 @@ X-Appwrite-Project: {project_id} | |||
| 107 | | 3 | **打包格式错误** | `storage_file_type_unsupported` (400) | 传了 `.zip` | 用 Python `tarfile` 打 `.tar.gz` | | 107 | | 3 | **打包格式错误** | `storage_file_type_unsupported` (400) | 传了 `.zip` | 用 Python `tarfile` 打 `.tar.gz` | |
| 108 | | 4 | **依赖未打包** | `Cannot find package 'node-appwrite'` | v1.9.0 不自动 npm install | 部署前 `npm install --omit=dev` | | 108 | | 4 | **依赖未打包** | `Cannot find package 'node-appwrite'` | v1.9.0 不自动 npm install | 部署前 `npm install --omit=dev` | |
| 109 | | 5 | **API Key 权限不足** | `missing scopes (["projects.read"])` | Application 级 Key 无 projects.read | 用 `GET /v1/functions` 验证连通性 | | 109 | | 5 | **API Key 权限不足** | `missing scopes (["projects.read"])` | Application 级 Key 无 projects.read | 用 `GET /v1/functions` 验证连通性 | |
| 110 | -| 6 | **HTTPS 不可用** | 443 超时 | 自托管只开 80 端口 | URL 改用 `http://` | | 110 | +| 6 | **HTTPS 不可用** | 443 超时 | 自托管只开 80 端口 | APPWRITE_HOST 改用 `http://` | |
| 111 | | 7 | **未授权 401** | 前端调用 401 | `execute` 默认不允许匿名 | 显式 `execute=["any"]` | | 111 | | 7 | **未授权 401** | 前端调用 401 | `execute` 默认不允许匿名 | 显式 `execute=["any"]` | |
| 112 | | 8 | **functionId 位置错** | `Param "functionId" is not optional` | 放到了 URL 路径 | 放在创建请求的 body 里 | | 112 | | 8 | **functionId 位置错** | `Param "functionId" is not optional` | 放到了 URL 路径 | 放在创建请求的 body 里 | |
| 113 | | 9 | **responseBody 为空** | 前端拿到空串 | 函数未调用 `res.json()` | 业务结束时 `return res.json(...)` | | 113 | | 9 | **responseBody 为空** | 前端拿到空串 | 函数未调用 `res.json()` | 业务结束时 `return res.json(...)` | |
| @@ -316,7 +316,7 @@ const url = APPWRITE_HOST + '/v1/functions/' + functionId + '/executions'; | |||
| 316 | ## 8. 安全加固 | 316 | ## 8. 安全加固 |
| 317 | 317 | ||
| 318 | 1. **密码 PBKDF2-SHA256 加盐哈希**(已实现):`register` 存 `password: hashPassword(password, salt)` + `salt`;`login` 用 `user.salt` 重算比对;`db-init` 管理员密码同样哈希。 | 318 | 1. **密码 PBKDF2-SHA256 加盐哈希**(已实现):`register` 存 `password: hashPassword(password, salt)` + `salt`;`login` 用 `user.salt` 重算比对;`db-init` 管理员密码同样哈希。 |
| 319 | -2. **凭据与代码分离**:`.env.example` 只放占位符,真实 API Key / URL / PROJECT_ID 放 git 忽略的 `.env`。 | 319 | +2. **凭据与代码分离**:`.env.example` 只放占位符,真实 API Key / APPWRITE_HOST / PROJECT_ID 放 git 忽略的 `.env`。 |
| 320 | 3. **`execute=["any"]` 是无鉴权公开访问**:`db-init` 也公开可调,任何人可清库重建。生产应改为 `execute=["users"]` 或加应用层鉴权。 | 320 | 3. **`execute=["any"]` 是无鉴权公开访问**:`db-init` 也公开可调,任何人可清库重建。生产应改为 `execute=["users"]` 或加应用层鉴权。 |
| 321 | 4. **HTTP 明文传输**:自托管只开 80 端口时,API Key、密码、用户数据都明文传输;生产必须配 HTTPS(TLS 反代)。 | 321 | 4. **HTTP 明文传输**:自托管只开 80 端口时,API Key、密码、用户数据都明文传输;生产必须配 HTTPS(TLS 反代)。 |
| 322 | 5. **API Key 泄漏要轮换**:真实 Key 一旦进入过仓库/文档/日志,必须在控制台吊销重发。 | 322 | 5. **API Key 泄漏要轮换**:真实 Key 一旦进入过仓库/文档/日志,必须在控制台吊销重发。 |
| @@ -329,7 +329,7 @@ const url = APPWRITE_HOST + '/v1/functions/' + functionId + '/executions'; | |||
| 329 | | `Cannot find package 'node-appwrite'` | 云函数 | 依赖未打包 | 部署前 `npm install --omit=dev` | | 329 | | `Cannot find package 'node-appwrite'` | 云函数 | 依赖未打包 | 部署前 `npm install --omit=dev` | |
| 330 | | `storage_file_type_unsupported` (400) | 部署 | 传了 `.zip` | 打 `.tar.gz` | | 330 | | `storage_file_type_unsupported` (400) | 部署 | 传了 `.zip` | 打 `.tar.gz` | |
| 331 | | `missing scopes (["projects.read"])` (401) | 部署 | API Key 权限不足 | 用 `GET /v1/functions` 验证 | | 331 | | `missing scopes (["projects.read"])` (401) | 部署 | API Key 权限不足 | 用 `GET /v1/functions` 验证 | |
| 332 | -| 443 超时 | 网络 | 只开 80 端口 | URL 用 `http://` | | 332 | +| 443 超时 | 网络 | 只开 80 端口 | APPWRITE_HOST 用 `http://` | |
| 333 | | `Param "functionId" is not optional` (400) | 部署 | functionId 放 URL 了 | 放到 body | | 333 | | `Param "functionId" is not optional` (400) | 部署 | functionId 放 URL 了 | 放到 body | |
| 334 | | EBADENGINE / npm 安装失败 | 打包 | Node 16 不兼容 node-appwrite v14 | 降级 `node-appwrite@^11.0.0` | | 334 | | EBADENGINE / npm 安装失败 | 打包 | Node 16 不兼容 node-appwrite v14 | 降级 `node-appwrite@^11.0.0` | |
| 335 | | `export 'Client' not found` | 云函数 | 命名导入不兼容 v11 | 默认导入 `import sdk from 'node-appwrite'` | | 335 | | `export 'Client' not found` | 云函数 | 命名导入不兼容 v11 | 默认导入 `import sdk from 'node-appwrite'` | |
| @@ -0,0 +1,27 @@ | |||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="2080" height="820" viewBox="0 0 2080 820" role="img" aria-labelledby="title desc"> | ||
| 2 | +<title id="title">单词打卡学习:端云一体化架构</title><desc id="desc">端侧 ArkUI 页面经 Appwrite Functions 执行端点调用云函数,云函数用 Server SDK 读写 Appwrite Database,端侧不直连数据库。</desc> | ||
| 3 | +<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#688f7e"/></marker></defs> | ||
| 4 | +<rect width="2080" height="820" rx="28" fill="#f1f6ef"/><g font-family="Microsoft YaHei, Noto Sans SC, sans-serif"> | ||
| 5 | +<text x="80" y="86" font-size="44" font-weight="700" fill="#243c36">单词打卡学习 · 端云一体化架构</text> | ||
| 6 | +<text x="80" y="137" font-size="26" fill="#5b7266">Appwrite 内建 HTTP 路由 + 文档型 NoSQL,省去独立网关层,端侧不直连数据库。</text> | ||
| 7 | +<rect x="80" y="230" width="400" height="270" rx="24" fill="#ffffff" stroke="#a6beaa" stroke-width="2"/><rect x="80" y="230" width="400" height="70" rx="24" fill="#355a4b"/><text x="104" y="277" font-size="29" font-weight="700" fill="#fff0ce">端侧 ArkUI</text> | ||
| 8 | +<text x="104" y="350" font-size="22" fill="#35564b">登录 / 首页(打卡状态+单词列表)</text> | ||
| 9 | +<text x="104" y="402" font-size="22" fill="#35564b">闪卡(翻转查看释义)</text> | ||
| 10 | +<text x="104" y="454" font-size="22" fill="#35564b">测试(看词选义+进度条)</text> | ||
| 11 | +<path d="M485 367 H535" stroke="#688f7e" stroke-width="4" marker-end="url(#arrow)"/> | ||
| 12 | +<rect x="545" y="230" width="400" height="270" rx="24" fill="#ffffff" stroke="#a6beaa" stroke-width="2"/><rect x="545" y="230" width="400" height="70" rx="24" fill="#355a4b"/><text x="569" y="277" font-size="29" font-weight="700" fill="#fff0ce">执行端点</text> | ||
| 13 | +<text x="569" y="350" font-size="22" fill="#35564b">POST /v1/functions/{id}/executions</text> | ||
| 14 | +<text x="569" y="402" font-size="22" fill="#35564b">execute=["any"] 公开执行</text> | ||
| 15 | +<text x="569" y="454" font-size="22" fill="#35564b">responseBody 二次解析</text> | ||
| 16 | +<path d="M950 367 H1000" stroke="#688f7e" stroke-width="4" marker-end="url(#arrow)"/> | ||
| 17 | +<rect x="1010" y="230" width="400" height="270" rx="24" fill="#ffffff" stroke="#a6beaa" stroke-width="2"/><rect x="1010" y="230" width="400" height="70" rx="24" fill="#355a4b"/><text x="1034" y="277" font-size="29" font-weight="700" fill="#fff0ce">Appwrite Functions</text> | ||
| 18 | +<text x="1034" y="350" font-size="22" fill="#35564b">6 个云函数(Node.js 16)</text> | ||
| 19 | +<text x="1034" y="402" font-size="22" fill="#35564b">ESM 默认导出 + Server SDK</text> | ||
| 20 | +<text x="1034" y="454" font-size="22" fill="#35564b">API Key 读写数据库</text> | ||
| 21 | +<path d="M1415 367 H1465" stroke="#688f7e" stroke-width="4" marker-end="url(#arrow)"/> | ||
| 22 | +<rect x="1475" y="230" width="400" height="270" rx="24" fill="#ffffff" stroke="#a6beaa" stroke-width="2"/><rect x="1475" y="230" width="400" height="70" rx="24" fill="#355a4b"/><text x="1499" y="277" font-size="29" font-weight="700" fill="#fff0ce">Appwrite Database</text> | ||
| 23 | +<text x="1499" y="350" font-size="22" fill="#35564b">3 个集合 / 文档型 NoSQL</text> | ||
| 24 | +<text x="1499" y="402" font-size="22" fill="#35564b">users_col / words_col</text> | ||
| 25 | +<text x="1499" y="454" font-size="22" fill="#35564b">learn_records_col / 58 词词库</text> | ||
| 26 | +<rect x="80" y="583" width="920" height="166" rx="24" fill="#e2ecd9"/><text x="106" y="630" font-size="29" font-weight="700" fill="#35564b">云函数规范输入</text><text x="106" y="682" font-size="26" fill="#35564b">ESM 默认导出 / node-appwrite@^11.0.0 / 统一 {code,data,msg} 返回</text><text x="106" y="723" font-size="26" fill="#35564b">tar.gz 打包上传(根目录含 main.js + node_modules)</text> | ||
| 27 | +<rect x="1020" y="583" width="855" height="166" rx="24" fill="#f3ead5"/><text x="1046" y="630" font-size="29" font-weight="700" fill="#665b3c">关键约束</text><text x="1046" y="682" font-size="26" fill="#665b3c">端侧不持有数据库凭据 / 执行端点是唯一入口</text><text x="1046" y="723" font-size="26" fill="#665b3c">自托管无独立网关层 / 命令成功 ≠ 部署完成</text></g></svg> | ||
| @@ -0,0 +1,105 @@ | |||
| 1 | + | ||
| 2 | +<h2>快速体验(约 10 分钟)</h2> | ||
| 3 | +<blockquote>这条路线面向「先把现成案例跑起来」的读者,目标是约 1–2 小时(含一次性云环境准备)内看到第一个成功结果。原理和完整搭建过程见完整实践。</blockquote> | ||
| 4 | +<h3>0)用 AI 辅助配置云环境(可选)</h3> | ||
| 5 | +<p>想跳过下面的手动步骤?把本仓库 <code>README.md</code> 与 <code>SKILL.md</code> 交给 AI 编程助手(码道 / CodeArts、Cursor 等),让它读完自动配置并部署云环境:</p> | ||
| 6 | +<pre><code>请阅读本仓库 README.md 与 SKILL.md,帮我自动配置并部署「单词打卡」云环境: | ||
| 7 | +1. 按第 2 章「配置与敏感信息」列出需要我填写的占位符(Appwrite 地址、Project ID、API Key),等我填好 harmony_cloud/.env; | ||
| 8 | +2. 运行 deploy/deploy.py 部署 6 个云函数(自动 npm install 并打包 tar.gz); | ||
| 9 | +3. 调用 db-init 建集合与种子数据; | ||
| 10 | +4. 把服务器地址与 Project ID 填入 HttpUtil.ets,编译并安装端侧; | ||
| 11 | +5. 按第 5 章验收表逐项联调。 | ||
| 12 | +每完成一步报告结果;报错时贴完整日志并定位根因;API Key 只写本地 .env,不写入代码、不提交仓库。</code></pre> | ||
| 13 | +<blockquote><strong>注意</strong>:AI 只能操作你授权的环境。API Key 等敏感信息请只放在本地 <code>.env</code>(已被 <code>.gitignore</code> 忽略),不要让 AI 把凭据写进代码或提交。部署完成后,仍建议按下面各步骤的「完成标志」人工复核一遍。</blockquote> | ||
| 14 | +<h3>1)获取已验证版本</h3> | ||
| 15 | +<pre><code>git clone https://gitcode.com/harmony-practice-center/word-check-in.git | ||
| 16 | +cd word-check-in</code></pre> | ||
| 17 | +<p><strong>完成标志</strong>:根目录存在 <code>README.md</code>、<code>harmony_app/</code> 和 <code>harmony_cloud/</code>。</p> | ||
| 18 | +<h3>2)准备最小配置</h3> | ||
| 19 | +<pre><code>cp harmony_cloud/.env.example harmony_cloud/.env</code></pre> | ||
| 20 | +<ol><li>将 <code>.env</code> 中的 API Key、APPWRITE_HOST、Project ID 替换为第 2 章收集的值。</li><li>在 <code>harmony_app/entry/src/main/ets/utils/HttpUtil.ets</code> 填入 <code>APPWRITE_HOST</code> 和 <code>PROJECT_ID</code>。</li></ol> | ||
| 21 | +<p><strong>完成标志</strong>:没有未替换占位符;敏感配置不会出现在 <code>git status</code> 中。</p> | ||
| 22 | +<h3>3)启动云侧</h3> | ||
| 23 | +<pre><code>cd harmony_cloud | ||
| 24 | +python deploy/deploy.py | ||
| 25 | +curl -X POST http://<host>/v1/functions/db-init/executions \ | ||
| 26 | + -H "Content-Type: application/json" \ | ||
| 27 | + -H "X-Appwrite-Project: <project_id>" \ | ||
| 28 | + -d '{"body":"{}","async":false}'</code></pre> | ||
| 29 | +<blockquote><code>deploy.py</code> 会为每个函数自动执行 <code>npm install --omit=dev</code> 并打包依赖,无需手动安装 <code>node-appwrite</code>。</blockquote> | ||
| 30 | +<p><strong>完成标志</strong>:<code>db-init</code> 执行后,<code>word_db</code> 下 3 个集合、58 个单词和管理员 <code>admin@company.com</code> / <code>123456</code> 已创建。</p> | ||
| 31 | +<h3>4)构建并运行端侧</h3> | ||
| 32 | +<blockquote>首次编译前,先用 DevEco Studio 打开 <code>harmony_app</code> 目录,让它自动生成 <code>local.properties</code>(填入 <code>sdk.dir</code>);命令行编译还需设置环境变量 <code>DEVECO_SDK_HOME</code>。缺失会报 <code>00303217</code>,<code>sdk.dir</code> 被改写会报 <code>00308018</code>,路径含空格会报 <code>Space is not supported in HVIGOR_USER_HOME</code>。详见 SKILL.md 的「全量故障速查表」。</blockquote> | ||
| 33 | +<pre><code>Set-Location 'harmony_app' | ||
| 34 | +ohpm install | ||
| 35 | +hvigorw assembleHap --mode module -p module=entry -p product=default -p buildMode=debug --no-daemon | ||
| 36 | +hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap</code></pre> | ||
| 37 | +<p><strong>完成标志</strong>:生成 HAP 并安装到设备,应用能启动到登录页并完成一次登录。</p> | ||
| 38 | +<h2>完整实践</h2> | ||
| 39 | + | ||
| 40 | +<p>目标:复现一款 HarmonyOS 端侧 + Appwrite 自托管 Serverless 云侧的端云一体化单词打卡应用,理解闪卡学习、连续打卡与 NoSQL 文档数据库的核心实现。预计约 120 分钟,不含环境安装、首次下载与签名配置。</p> | ||
| 41 | +<h3>1. 确认 Appwrite 服务器与收集信息(约 15 分钟)</h3> | ||
| 42 | +<p>访问 /v1/health 确认服务器返回 pass。在控制台创建项目并记录 Project ID,创建含 functions.write/read、databases.write/read 权限的 API Key。调用 GET /v1/functions/runtimes 确认 node-16.0 可用。</p> | ||
| 43 | +<p>从 <a href="https://gitcode.com/harmony-practice-center/word-check-in">单词打卡公开仓库</a>获取源码:</p> | ||
| 44 | +<pre><code>git clone https://gitcode.com/harmony-practice-center/word-check-in.git | ||
| 45 | +cd word-check-in</code></pre> | ||
| 46 | +<p>敏感信息只放在本地 .env(已被 .gitignore 忽略),不写入代码、不提交仓库。客户端不得包含 API Key;函数用 Server SDK 在服务端持有。</p> | ||
| 47 | +<p><strong>验收:</strong>信息清单填齐,APPWRITE_HOST / Project ID / API Key 均可用;GET /v1/functions 正常返回。</p> | ||
| 48 | +<h3>2. 编写并打包 6 个云函数(约 25 分钟)</h3> | ||
| 49 | +<p>按 Appwrite Functions 编写规范实现 6 个函数:register、login、getDailyWords、learnWord、getProgress、db-init。</p> | ||
| 50 | +<details><summary>查看 Appwrite Functions 编写规范要点</summary><pre><code>// 1. ESM 默认导出:export default async ({ req, res, log, error }) => { ... } | ||
| 51 | +// 2. node-appwrite@^11.0.0(Node 16 兼容,v14 要求 Node 18+) | ||
| 52 | +// 3. Server SDK 用 API Key 读写数据库: | ||
| 53 | +// const client = new Client().setEndpoint(HOST).setProject(PROJECT).setKey(API_KEY) | ||
| 54 | +// const db = new Databases(client) | ||
| 55 | +// 4. execute=["any"] 允许公开执行(学习演示配置) | ||
| 56 | +// 5. 统一返回结构:{ code: 0, data: ..., msg: '...' } | ||
| 57 | +// 6. 打包格式:tar.gz(不是 zip),根目录含 main.js + node_modules</code></pre></details> | ||
| 58 | +<p>每个函数目录执行 npm install --omit=dev 安装依赖。Appwrite v1.9.0 不会自动安装依赖,必须手动打包进 tar.gz。</p> | ||
| 59 | +<p><strong>验收:</strong>6 个函数目录都含 main.js、package.json 和 node_modules/node-appwrite。</p> | ||
| 60 | +<h3>3. 部署云侧(约 25 分钟)</h3> | ||
| 61 | +<p>运行 deploy.py 为每个函数自动执行 npm install --omit=dev 并打包 tar.gz 上传。为每个函数配置 API_KEY、DATABASE_ID 环境变量并设 execute=["any"]。调用 db-init 建集合与种子数据。</p> | ||
| 62 | +<pre><code>cd harmony_cloud | ||
| 63 | +python deploy/deploy.py | ||
| 64 | +curl -X POST http://<host>/v1/functions/db-init/executions \ | ||
| 65 | + -H "Content-Type: application/json" \ | ||
| 66 | + -H "X-Appwrite-Project: <project_id>" \ | ||
| 67 | + -d '{"body":"{}","async":false}'</code></pre> | ||
| 68 | +<p>db-init 执行后创建 3 个集合(users_col / words_col / learn_records_col)、58 个单词和管理员 admin@company.com / 123456。</p> | ||
| 69 | +<p>关键理念:"命令提交成功 ≠ 部署完成"。deploy.py 执行成功后需实际调用函数验证返回 code:0。</p> | ||
| 70 | +<p><strong>验收:</strong>6 个函数调用全部返回 code:0;word_db 下 3 个集合与 58 个单词就绪;管理员可登录。</p> | ||
| 71 | +<h3>4. 配置、编译并安装端侧(约 20 分钟)</h3> | ||
| 72 | +<p>在 HttpUtil.ets 填入 APPWRITE_HOST(http:// 协议)和 PROJECT_ID,确认 functionId 映射与部署一致。配置 ohos.permission.INTERNET 权限。</p> | ||
| 73 | +<pre><code>cd harmony_app | ||
| 74 | +ohpm install | ||
| 75 | +hvigorw assembleHap --mode module -p module=entry -p product=default -p buildMode=debug --no-daemon</code></pre> | ||
| 76 | +<p>推荐在 DevEco Studio 直接点 Run(自动签名、安装、启动)。命令行需先签名,再用 hdc 安装:</p> | ||
| 77 | +<pre><code>hdc install <签名后 HAP> | ||
| 78 | +hdc shell aa start -a EntryAbility -b com.example.word_check_in</code></pre> | ||
| 79 | +<p><strong>验收:</strong>应用启动后显示登录页,无白屏;用 admin@company.com / 123456 可成功登录。</p> | ||
| 80 | +<h3>5. 端云联调验收(约 25 分钟)</h3> | ||
| 81 | +<p>按业务验收表逐项执行:</p> | ||
| 82 | +<ul> | ||
| 83 | +<li>登录 → 进入首页,显示今日打卡状态与连续天数</li> | ||
| 84 | +<li>查看今日单词列表 → 首页展示 10 个未学单词</li> | ||
| 85 | +<li>闪卡翻转 → 点击卡片翻转查看释义</li> | ||
| 86 | +<li>看词选义测试 → 答对计入学习进度,进度条更新</li> | ||
| 87 | +<li>学完一个单词 → 打卡状态变为已完成,连续天数累加</li> | ||
| 88 | +<li>重启应用 → 进度与打卡数据保留(云端持久化)</li> | ||
| 89 | +</ul> | ||
| 90 | +<p>重点验证从 responseBody 二次解析的进度与打卡数据正确。端侧调用返回的 JSON 包在 responseBody 字段内,需二次解析才能获取实际数据。</p> | ||
| 91 | +<p><strong>验收:</strong>所有业务验收项通过,进度与打卡数据正确,端云链路完整。</p> | ||
| 92 | +<h3>6. 关键算法固定样例复核(约 10 分钟)</h3> | ||
| 93 | +<p>连续打卡(streak)累加逻辑、学习进度计算等核心业务规则均可复现。用固定输入验证连续天数递增与重置逻辑、学习进度计算是否符合预期。</p> | ||
| 94 | +<p>固定样例见 docs/quickstart.md"关键算法固定样例"章节。截图只能证明界面状态,不能替代云函数逻辑与数据库验证;后者由固定样例与端云联调步骤验证。</p> | ||
| 95 | +<p><strong>验收:</strong>固定样例的输入输出与预期一致;连续打卡与进度计算逻辑可复现。</p> | ||
| 96 | +<h2>高频问题排查</h2><table><thead><tr><th>问题</th><th>优先检查</th><th>解决方向</th></tr></thead><tbody> | ||
| 97 | +<tr><td>Runtime "node-18.0" is not supported</td><td>服务器可用运行时列表</td><td>改为 node-16.0,先查 GET /v1/functions/runtimes。</td></tr> | ||
| 98 | +<tr><td>Cannot find package 'node-appwrite'</td><td>依赖是否打进 tar.gz</td><td>npm install --omit=dev 后重新打包 tar.gz。</td></tr> | ||
| 99 | +<tr><td>storage_file_type_unsupported</td><td>上传格式是否为 tar.gz</td><td>打成 .tar.gz 格式上传,不支持 .zip。</td></tr> | ||
| 100 | +<tr><td>前端调用 401</td><td>execute 是否为 ["any"]</td><td>将函数 execute 设为 ["any"] 允许公开执行。</td></tr> | ||
| 101 | +<tr><td>missing scopes</td><td>API Key 权限是否完整</td><td>重新创建含 functions/databases 权限的 API Key。</td></tr> | ||
| 102 | +<tr><td>EBADENGINE</td><td>node-appwrite 版本是否兼容 Node 16</td><td>降级到 node-appwrite@^11.0.0。</td></tr> | ||
| 103 | +</tbody></table> | ||
| 104 | +<h2>可选拓展</h2><p>为 execute 接入正式鉴权(改为 ["users"] + 用户会话);为服务器配置 HTTPS 反代(Nginx/Caddy + TLS);db-init 改为幂等可重复执行。以上拓展不包含在基础成果声明中,完成后重新验证功能与安全性。</p> | ||
| 105 | +<h2>交付与授权</h2><p>交付可运行工程、6 个云函数 tar.gz、端云联调验收记录与关键算法固定样例复核结果。应用代码按 MIT 许可;第三方依赖按各自许可处理。本案例聚焦自托管 Serverless 学习,基础实现不包含正式鉴权、限流或 HTTPS 配置。</p> | ||
| @@ -5,8 +5,9 @@ | |||
| 5 | { | 5 | { |
| 6 | "name": "default", | 6 | "name": "default", |
| 7 | "signingConfig": "default", | 7 | "signingConfig": "default", |
| 8 | - "targetSdkVersion": "6.1.1(24)", | 8 | + "compileSdkVersion": "6.1.1(24)", |
| 9 | - "compatibleSdkVersion": "6.1.0(23)", | 9 | + "targetSdkVersion": "6.1.0(23)", |
| 10 | + "compatibleSdkVersion": "6.0.0(20)", | ||
| 10 | "runtimeOS": "HarmonyOS", | 11 | "runtimeOS": "HarmonyOS", |
| 11 | "buildOption": { | 12 | "buildOption": { |
| 12 | "strictMode": { | 13 | "strictMode": { |
| @@ -1,3 +1,3 @@ | |||
| 1 | API_KEY=<your_appwrite_api_key> | 1 | API_KEY=<your_appwrite_api_key> |
| 2 | -URL=http://<your_appwrite_host> | 2 | +APPWRITE_HOST=http://<your_appwrite_host> |
| 3 | PROJECT_ID=<your_project_id> | 3 | PROJECT_ID=<your_project_id> |
| @@ -1,4 +1,6 @@ | |||
| 1 | -# demo007 部署指南(Appwrite Functions 方案) | 1 | +# word-check-in 部署指南(Appwrite Functions 方案) |
| 2 | + | ||
| 3 | +> **本文为辅助文档,一切以主 [README.md](../README.md) 为准**。若与主 README 冲突,以主 README 为准;本文仅补充手动/自动部署的操作细节。 | ||
| 2 | 4 | ||
| 3 | ## 架构 | 5 | ## 架构 |
| 4 | 6 | ||
| @@ -31,7 +33,7 @@ | |||
| 31 | 33 | ||
| 32 | ### 步骤 1:进入 Appwrite 控制台 | 34 | ### 步骤 1:进入 Appwrite 控制台 |
| 33 | 35 | ||
| 34 | -浏览器打开 `https://XXX.XXX.XXX.XXX`,登录进入你的 Project。 | 36 | +浏览器打开 `http://XXX.XXX.XXX.XXX`,登录进入你的 Project。 |
| 35 | 37 | ||
| 36 | ### 步骤 2:创建数据库和集合 | 38 | ### 步骤 2:创建数据库和集合 |
| 37 | 39 | ||
| @@ -68,37 +70,29 @@ | |||
| 68 | 1. **Functions** → **Create Function** | 70 | 1. **Functions** → **Create Function** |
| 69 | 2. 填写: | 71 | 2. 填写: |
| 70 | - **Name**: 函数名(如 `register`) | 72 | - **Name**: 函数名(如 `register`) |
| 71 | - - **Runtime**: Node.js 18 | 73 | + - **Runtime**: Node.js 16.0 |
| 72 | - **Execute access**: `All users` 或 `Any`(允许公开调用) | 74 | - **Execute access**: `All users` 或 `Any`(允许公开调用) |
| 73 | 3. 创建后,进入 **Settings** → **Environment Variables**,添加: | 75 | 3. 创建后,进入 **Settings** → **Environment Variables**,添加: |
| 74 | - `API_KEY` = 你的 Server API Key | 76 | - `API_KEY` = 你的 Server API Key |
| 75 | - `DATABASE_ID` = `word_db` | 77 | - `DATABASE_ID` = `word_db` |
| 76 | 4. **Deployments** → **Create Deployment** | 78 | 4. **Deployments** → **Create Deployment** |
| 77 | - - 上传 `functions/{函数名}/` 目录下的 `main.js` + `package.json` 打包的 zip | 79 | + - 上传 `functions/{函数名}/` 目录下的 `main.js` + `package.json` 打包的 tar.gz |
| 78 | - **Entrypoint**: `main.js` | 80 | - **Entrypoint**: `main.js` |
| 79 | - **Activate**: 勾选 | 81 | - **Activate**: 勾选 |
| 80 | 82 | ||
| 81 | ### 步骤 4:配置前端 | 83 | ### 步骤 4:配置前端 |
| 82 | 84 | ||
| 83 | -部署完成后,将每个函数的 ID 填入前端: | 85 | +端侧 `harmony_app/entry/src/main/ets/utils/HttpUtil.ets` 已内置短名映射(`getFunctionId()` 直接返回 `register` / `login` / `db-init` / `getDailyWords` / `getProgress` / `learnWord`)。因此**函数的 functionId 必须与这些短名一致**: |
| 84 | 86 | ||
| 85 | -编辑 `harmony_app/entry/src/main/ets/utils/HttpUtil.ets`: | 87 | +- 自动部署脚本会以短名作为 functionId,无需额外处理。 |
| 88 | +- 手动在控制台创建函数时,需把函数 ID 设为对应短名;若控制台自动生成了随机 ID,则需同步修改 `HttpUtil.ets` 的 `getFunctionId()`。 | ||
| 86 | 89 | ||
| 87 | -```typescript | 90 | +此外还需在 `HttpUtil.ets` 顶部填入 `APPWRITE_HOST`(`http://` 协议)和 `PROJECT_ID`。 |
| 88 | -const FN: Record<string, string> = { | ||
| 89 | - register: 'abc123...', // ← 替换为实际 ID | ||
| 90 | - login: 'def456...', | ||
| 91 | - dbInit: 'ghi789...', | ||
| 92 | - getDailyWords: 'jkl012...', | ||
| 93 | - getProgress: 'mno345...', | ||
| 94 | - learnWord: 'pqr678...', | ||
| 95 | -}; | ||
| 96 | -``` | ||
| 97 | 91 | ||
| 98 | ### 步骤 5:初始化数据库 | 92 | ### 步骤 5:初始化数据库 |
| 99 | 93 | ||
| 100 | ```bash | 94 | ```bash |
| 101 | -curl -X POST https://XXX.XXX.XXX.XXX/v1/functions/{dbInit的函数ID}/executions \ | 95 | +curl -X POST http://XXX.XXX.XXX.XXX/v1/functions/{dbInit的函数ID}/executions \ |
| 102 | -H "Content-Type: application/json" \ | 96 | -H "Content-Type: application/json" \ |
| 103 | -H "X-Appwrite-Project: <project_id>" \ | 97 | -H "X-Appwrite-Project: <project_id>" \ |
| 104 | -d '{"body":"{}","async":false}' | 98 | -d '{"body":"{}","async":false}' |
| @@ -120,11 +114,11 @@ python deploy/deploy.py # 完整部署 | |||
| 120 | python deploy/deploy.py --check # 仅检查连通性 | 114 | python deploy/deploy.py --check # 仅检查连通性 |
| 121 | ``` | 115 | ``` |
| 122 | 116 | ||
| 123 | -**需要的环境变量**(自动从 `.env.example` 读取): | 117 | +**需要的环境变量**(优先读取 `.env`,不存在则回退 `.env.example`): |
| 124 | | 变量 | 说明 | | 118 | | 变量 | 说明 | |
| 125 | |------|------| | 119 | |------|------| |
| 126 | | `API_KEY` | Appwrite Server API Key | | 120 | | `API_KEY` | Appwrite Server API Key | |
| 127 | -| `URL` | Appwrite 服务器地址 | | 121 | +| `APPWRITE_HOST` | Appwrite 服务器地址 | |
| 128 | | `PROJECT_ID` | Appwrite Project ID | | 122 | | `PROJECT_ID` | Appwrite Project ID | |
| 129 | 123 | ||
| 130 | ### 方式 B: Node.js 脚本 | 124 | ### 方式 B: Node.js 脚本 |
| @@ -136,21 +130,21 @@ node deploy.js # 完整部署 | |||
| 136 | node deploy.js --check # 仅检查连通性 | 130 | node deploy.js --check # 仅检查连通性 |
| 137 | ``` | 131 | ``` |
| 138 | 132 | ||
| 139 | -需要 Node.js 18+ 环境。依赖会自动通过 `npm install` 安装。 | 133 | +`deploy.js` 会为每个函数自动执行 `npm install --omit=dev` 并打包成 `.tar.gz`,函数运行时同样为 `node-16.0`;脚本本身依赖 node-appwrite v14,本地运行需 Node.js 18+。 |
| 140 | 134 | ||
| 141 | ### 部署成功后的输出示例 | 135 | ### 部署成功后的输出示例 |
| 142 | 136 | ||
| 143 | ``` | 137 | ``` |
| 144 | ══════════════════════════════════ | 138 | ══════════════════════════════════ |
| 145 | - 函数 ID 映射 (复制到 HttpUtil.ets) | 139 | + 函数 ID(functionId 即短名) |
| 146 | ══════════════════════════════════ | 140 | ══════════════════════════════════ |
| 147 | 141 | ||
| 148 | - db-init : '6701a3b5c8f2e9d1a4b6', | 142 | + db-init : 'db-init', |
| 149 | - register : '6701a3b5c8f2e9d1a4b7', | 143 | + register : 'register', |
| 150 | - login : '6701a3b5c8f2e9d1a4b8', | 144 | + login : 'login', |
| 151 | - getDailyWords : '6701a3b5c8f2e9d1a4b9', | 145 | + getDailyWords : 'getDailyWords', |
| 152 | - getProgress : '6701a3b5c8f2e9d1a4c0', | 146 | + getProgress : 'getProgress', |
| 153 | - learnWord : '6701a3b5c8f2e9d1a4c1', | 147 | + learnWord : 'learnWord', |
| 154 | 148 | ||
| 155 | ✅ 部署完成! 6/6 个函数就绪 | 149 | ✅ 部署完成! 6/6 个函数就绪 |
| 156 | ``` | 150 | ``` |
| @@ -162,7 +156,7 @@ node deploy.js --check # 仅检查连通性 | |||
| 162 | 所有函数通过统一的执行端点调用: | 156 | 所有函数通过统一的执行端点调用: |
| 163 | 157 | ||
| 164 | ``` | 158 | ``` |
| 165 | -POST https://XXX.XXX.XXX.XXX/v1/functions/{functionId}/executions | 159 | +POST http://XXX.XXX.XXX.XXX/v1/functions/{functionId}/executions |
| 166 | Headers: | 160 | Headers: |
| 167 | X-Appwrite-Project: <project_id> | 161 | X-Appwrite-Project: <project_id> |
| 168 | Content-Type: application/json | 162 | Content-Type: application/json |
| @@ -193,7 +187,7 @@ Body: | |||
| 193 | 187 | ||
| 194 | ``` | 188 | ``` |
| 195 | API_KEY=<your_api_key> | 189 | API_KEY=<your_api_key> |
| 196 | -URL=http://<your_appwrite_host> | 190 | +APPWRITE_HOST=http://<your_appwrite_host> |
| 197 | PROJECT_ID=<your_project_id> | 191 | PROJECT_ID=<your_project_id> |
| 198 | ``` | 192 | ``` |
| 199 | 193 | ||
| @@ -1,7 +1,7 @@ | |||
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | # -*- coding: utf-8 -*- | 2 | # -*- coding: utf-8 -*- |
| 3 | """ | 3 | """ |
| 4 | -demo007 Appwrite Functions automated deployment script | 4 | +Appwrite Functions automated deployment script |
| 5 | 5 | ||
| 6 | Usage: | 6 | Usage: |
| 7 | python deploy.py # Full deployment | 7 | python deploy.py # Full deployment |
| @@ -16,6 +16,8 @@ import tarfile | |||
| 16 | import io | 16 | import io |
| 17 | import urllib.request | 17 | import urllib.request |
| 18 | import urllib.error | 18 | import urllib.error |
| 19 | +import shutil | ||
| 20 | +import subprocess | ||
| 19 | from pathlib import Path | 21 | from pathlib import Path |
| 20 | 22 | ||
| 21 | ROOT = Path(__file__).resolve().parent.parent | 23 | ROOT = Path(__file__).resolve().parent.parent |
| @@ -54,7 +56,7 @@ def load_env(path): | |||
| 54 | 56 | ||
| 55 | 57 | ||
| 56 | def api_request(env, method, path, body=None, headers=None): | 58 | def api_request(env, method, path, body=None, headers=None): |
| 57 | - url = env["URL"].rstrip("/") + path | 59 | + url = env["APPWRITE_HOST"].rstrip("/") + path |
| 58 | req_headers = { | 60 | req_headers = { |
| 59 | "X-Appwrite-Project": env["PROJECT_ID"], | 61 | "X-Appwrite-Project": env["PROJECT_ID"], |
| 60 | "X-Appwrite-Key": env["API_KEY"], | 62 | "X-Appwrite-Key": env["API_KEY"], |
| @@ -77,7 +79,7 @@ def api_request(env, method, path, body=None, headers=None): | |||
| 77 | 79 | ||
| 78 | 80 | ||
| 79 | def check_connection(env): | 81 | def check_connection(env): |
| 80 | - url = env["URL"].rstrip("/") | 82 | + url = env["APPWRITE_HOST"].rstrip("/") |
| 81 | print(f"\n[Check] Appwrite: {url}") | 83 | print(f"\n[Check] Appwrite: {url}") |
| 82 | 84 | ||
| 83 | # Health check (needs auth headers) | 85 | # Health check (needs auth headers) |
| @@ -108,7 +110,30 @@ def check_connection(env): | |||
| 108 | return True | 110 | return True |
| 109 | 111 | ||
| 110 | 112 | ||
| 113 | +def install_dependencies(func_name): | ||
| 114 | + """Install production dependencies so node-appwrite is bundled into the package.""" | ||
| 115 | + func_path = FUNC_DIR / func_name | ||
| 116 | + if not (func_path / "package.json").exists(): | ||
| 117 | + return | ||
| 118 | + if shutil.which("npm") is None: | ||
| 119 | + print(f" [WARN] npm not found; dependencies will NOT be bundled") | ||
| 120 | + return | ||
| 121 | + print(f" Installing dependencies (npm install --omit=dev)...") | ||
| 122 | + try: | ||
| 123 | + subprocess.run( | ||
| 124 | + "npm install --omit=dev", | ||
| 125 | + cwd=str(func_path), | ||
| 126 | + shell=True, | ||
| 127 | + check=True, | ||
| 128 | + capture_output=True, | ||
| 129 | + text=True, | ||
| 130 | + ) | ||
| 131 | + except subprocess.CalledProcessError as e: | ||
| 132 | + print(f" [WARN] npm install failed: {(e.stderr or e.stdout or '')[:300]}") | ||
| 133 | + | ||
| 134 | + | ||
| 111 | def create_tar_gz(func_name): | 135 | def create_tar_gz(func_name): |
| 136 | + install_dependencies(func_name) | ||
| 112 | func_path = FUNC_DIR / func_name | 137 | func_path = FUNC_DIR / func_name |
| 113 | tgz_path = DIST_DIR / f"{func_name}.tar.gz" | 138 | tgz_path = DIST_DIR / f"{func_name}.tar.gz" |
| 114 | DIST_DIR.mkdir(parents=True, exist_ok=True) | 139 | DIST_DIR.mkdir(parents=True, exist_ok=True) |
| @@ -167,8 +192,8 @@ def deploy_function(env, func_name, description): | |||
| 167 | print(f" Package: {os.path.basename(tgz_path)}") | 192 | print(f" Package: {os.path.basename(tgz_path)}") |
| 168 | 193 | ||
| 169 | # 4. Deploy code (multipart/form-data) | 194 | # 4. Deploy code (multipart/form-data) |
| 170 | - deploy_url = f"{env['URL'].rstrip('/')}/v1/functions/{function_id}/deployments" | 195 | + deploy_url = f"{env['APPWRITE_HOST'].rstrip('/')}/v1/functions/{function_id}/deployments" |
| 171 | - boundary = "---ZdxDeployBoundary2024" | 196 | + boundary = "---DeployBoundary2024" |
| 172 | entrypoint = "main.js" | 197 | entrypoint = "main.js" |
| 173 | 198 | ||
| 174 | # Build multipart body manually (no external deps needed) | 199 | # Build multipart body manually (no external deps needed) |
| @@ -219,7 +244,7 @@ def main(): | |||
| 219 | args = parser.parse_args() | 244 | args = parser.parse_args() |
| 220 | 245 | ||
| 221 | print("=" * 50) | 246 | print("=" * 50) |
| 222 | - print("demo007 - Appwrite Functions Deploy") | 247 | + print("Appwrite Functions Deploy") |
| 223 | print("=" * 50) | 248 | print("=" * 50) |
| 224 | 249 | ||
| 225 | if not ENV_FILE.exists(): | 250 | if not ENV_FILE.exists(): |
| @@ -227,7 +252,7 @@ def main(): | |||
| 227 | sys.exit(1) | 252 | sys.exit(1) |
| 228 | 253 | ||
| 229 | env = load_env(ENV_FILE) | 254 | env = load_env(ENV_FILE) |
| 230 | - print(f"Config: URL={env.get('URL')}, PROJECT={env.get('PROJECT_ID')}") | 255 | + print(f"Config: APPWRITE_HOST={env.get('APPWRITE_HOST')}, PROJECT={env.get('PROJECT_ID')}") |
| 231 | 256 | ||
| 232 | if not check_connection(env): | 257 | if not check_connection(env): |
| 233 | print("\n[FAIL] Appwrite unreachable, aborting") | 258 | print("\n[FAIL] Appwrite unreachable, aborting") |
Mharmony_cloud/functions/db-init/package-lock.json+2-2File content is under review, please try again later
Mharmony_cloud/functions/db-init/package.json+1-1File content is under review, please try again later
Mharmony_cloud/functions/getDailyWords/package-lock.json+2-2File content is under review, please try again later
Mharmony_cloud/functions/getDailyWords/package.json+1-1File content is under review, please try again later
Mharmony_cloud/functions/getProgress/package-lock.json+2-2File content is under review, please try again later
Mharmony_cloud/functions/getProgress/package.json+1-1File content is under review, please try again later
Mharmony_cloud/functions/learnWord/package-lock.json+2-2File content is under review, please try again later
Mharmony_cloud/functions/learnWord/package.json+1-1File content is under review, please try again later
Mharmony_cloud/functions/login/package-lock.json+2-2File content is under review, please try again later
Mharmony_cloud/functions/register/package-lock.json+2-2File content is under review, please try again later
Mharmony_cloud/functions/register/package.json+1-1File content is under review, please try again later