# wechat-miniprogram > WeChat Mini Program integration with Zion.app backend. Use when: (1) Developing WeChat Mini Programs, (2) Using CommonJS module system (require/module.exports), (3) Making GraphQL requests via wx.request, (4) Handling Mini Program file structure (.wxml, .wxss, .wxs, .js), (5) Integrating authentication, (6) Uploading files to Zion storage - Author: qinmao - Repository: functorz-tech/zion-aicoding-rules - Version: 20260120223928 - Stars: 7 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/functorz-tech/zion-aicoding-rules - Web: https://mule.run/skillshub/@@functorz-tech/zion-aicoding-rules~wechat-miniprogram:20260120223928 --- --- description: "WeChat Mini Program integration with Zion.app backend. Use when: (1) Developing WeChat Mini Programs, (2) Using CommonJS module system (require/module.exports), (3) Making GraphQL requests via wx.request, (4) Handling Mini Program file structure (.wxml, .wxss, .wxs, .js), (5) Integrating authentication, (6) Uploading files to Zion storage" alwaysApply: false applyToFiles: - "**/miniprogram/**" - "**/*.wxml" - "**/*.wxss" - "**/*.wxs" - "**/project.config.json" - "**/app.json" --- # 微信小程序 + Zion.app 开发规则 ## Overview 本文档规定了微信小程序与 Zion.app 后端集成时的开发规则和最佳实践。所有开发必须严格遵循本文档中的规则。 **相关文档**: - [Zion.app 后端架构](./zion-backend-architecture.mdc) - [Zion.app 二进制资源上传](./zion-binary-asset-upload-rules.mdc) - [Zion.app 数据库操作](./zion-database-gql-api-rules.mdc) --- ## 模块系统 ### 使用 CommonJS [MUST] **必须使用 CommonJS 模块系统**: ```javascript // ✅ 正确 const { functionName } = require('./utils/file.js') module.exports = { functionName } // ❌ 错误 import { functionName } from './utils/file.js' ``` --- ## 网络请求 ### 使用 wx.request [MUST] **必须使用 `wx.request` 进行网络请求**: ```javascript function graphqlRequest(query, variables = {}, token = null) { return new Promise((resolve, reject) => { wx.request({ url: 'https://zion-app.functorz.com/zero/{projectExId}/api/graphql-v2', method: 'POST', data: { query, variables }, header: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }, success: (res) => { if (res.data.errors) { reject(new Error(res.data.errors[0].message)) } else { resolve(res.data.data) } }, fail: reject }) }) } ``` --- ## 用户认证 ### 静默登录实现 [MUST] **实现微信小程序静默登录的步骤**: ```javascript // 1. 获取微信登录 code const loginRes = await new Promise((resolve, reject) => { wx.login({ success: resolve, fail: reject }) }) // 2. 调用 Zion GraphQL mutation 进行静默登录 const query = ` mutation LoginWithWechatMiniApp($code: String!) { loginWithWechatMiniApp(code: $code) { account { id username profileImageUrl // 必须包含 } jwt { token } } } ` const result = await graphqlRequest(query, { code: loginRes.code }) const { account, jwt } = result.loginWithWechatMiniApp // 3. 保存 token 和账户信息 wx.setStorageSync('token', jwt.token) wx.setStorageSync('account', account) ``` **开发要求**: - **必须请求 `profileImageUrl` 字段**:用于后续头像显示 - `wx.login` 返回的 `code` 只能使用一次,5 分钟内有效 --- ## 文件上传到 OSS ### 上传流程 [MUST] 上传文件到 Zion.app 的 OSS 存储必须遵循以下流程。详细协议请参考 [Zion.app 二进制资源上传规则](./zion-binary-asset-upload-rules.mdc)。 #### Step 1: 计算文件的 MD5 Base64 **必须使用 `wx.getFileInfo` API 计算 MD5**: ```javascript const fileInfo = await new Promise((resolve, reject) => { wx.getFileInfo({ filePath: filePath, digestAlgorithm: 'md5', success: resolve, fail: reject }) }) // fileInfo.digest 是十六进制字符串,需要转换为 Base64 // 转换逻辑:将十六进制字符串转换为字节数组,再转换为 Base64 ``` #### Step 2: 获取 Presigned Upload URL ```javascript const query = `mutation GetImagePresignedUrl($md5: String!, $suffix: MediaFormat!) { imagePresignedUrl(imgMd5Base64: $md5, imageSuffix: $suffix, acl: PRIVATE) { imageId uploadUrl uploadHeaders } }` const result = await graphqlRequest(query, { md5: md5Base64, suffix: 'JPEG' }, token) const { imageId, uploadUrl, uploadHeaders } = result.imagePresignedUrl ``` #### Step 3: 上传文件 **必须遵循以下规则**: 1. **必须保留所有 uploadHeaders**:presigned URL 的签名是基于这些 headers 计算的,移除任何 header 都会导致 403 错误 2. **必须确保上传的数据和计算 MD5 时使用的数据完全一致** 3. **必须使用真正的 ArrayBuffer**:不能依赖 `instanceof ArrayBuffer` 检查 ```javascript // 读取文件数据并转换为 ArrayBuffer const fileManager = wx.getFileSystemManager() let fileData = fileManager.readFileSync(filePath) let originalFileData = fileData instanceof ArrayBuffer ? fileData : (() => { const uint8Array = new Uint8Array(fileData) const buffer = new ArrayBuffer(uint8Array.length) new Uint8Array(buffer).set(uint8Array) return buffer })() // 上传文件(必须保留所有 uploadHeaders) await new Promise((resolve, reject) => { wx.request({ url: uploadUrl, method: 'PUT', data: originalFileData, header: { 'Content-Type': 'image/jpeg', ...uploadHeaders }, responseType: 'text', success: (res) => { if (res.statusCode === 200 || res.statusCode === 204) { resolve(imageId) } else { reject(new Error(`上传失败: ${res.statusCode}`)) } }, fail: reject }) }) ``` ### 常见错误处理 - **`InvalidDigest`**:必须使用 `wx.getFileInfo` API 计算 MD5,确保上传数据与计算 MD5 时使用的数据完全一致 - **`SignatureDoesNotMatch`**:必须保留所有 uploadHeaders,不要移除任何 header(特别是 Content-MD5) - **`文件数据格式不正确`**:使用属性检查而不是 `instanceof`,强制转换为真正的 ArrayBuffer ### 文件下载(处理网络 URL)[MUST] **处理网络资源时必须先下载到本地**: ```javascript if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) { const downloadRes = await new Promise((resolve, reject) => { wx.downloadFile({ url: avatarUrl, success: (res) => { if (res.statusCode === 200 && res.tempFilePath) { resolve(res) } else { reject(new Error('下载失败')) } }, fail: reject }) }) localFilePath = downloadRes.tempFilePath } ``` --- ## 用户信息编辑 ### 头像选择实现 [MUST] **使用微信小程序原生组件实现头像选择**: ```xml ``` ```javascript async onChooseAvatar(e) { let localFilePath = e.detail.avatarUrl // 处理网络 URL(微信默认头像需要先下载) if (localFilePath.startsWith('http://') || localFilePath.startsWith('https://')) { const downloadRes = await new Promise((resolve, reject) => { wx.downloadFile({ url: localFilePath, success: resolve, fail: reject }) }) localFilePath = downloadRes.tempFilePath } const imageId = await uploadImage(localFilePath, token) await updateAccount(accountId, imageId, null, token) } ``` ### 昵称编辑实现 [MUST] ```xml ``` ```javascript async onNicknameBlur(e) { const nickname = e.detail.value.trim() if (nickname && nickname !== this.data.userInfo.nickname) { await updateAccount(accountId, null, nickname, token) } } ``` --- ## 数据模型操作 ### 创建记录 [MUST] **当表不支持直接设置外键字段时,必须使用两步操作**: ```javascript // Step 1: 先创建空记录 const createResult = await graphqlRequest(` mutation CreateRecord { insert_record_one(object: {}) { id } } `, {}, token) // Step 2: 更新记录,设置关联字段 await graphqlRequest(` mutation UpdateRecord($id: bigint!, $img_id: bigint!, $user_id: bigint) { update_record_by_pk( pk_columns: {id: $id} _set: {img_id: $img_id, user_id: $user_id} ) { id } } `, { id: createResult.insert_record_one.id, img_id: imageId, user_id: userId }, token) ``` --- ## 开发要求 ### 头像上传和读取 [MUST] **必须遵循以下要求**: - **必须使用 `wx.getFileInfo` API 计算 MD5**,不要手动实现或使用第三方库 - **必须保留所有 uploadHeaders**(特别是 Content-MD5),presigned URL 的签名依赖所有 headers - **必须确保上传的数据和计算 MD5 时使用的数据完全一致** - **必须处理网络 URL**:微信默认头像等网络资源需要先使用 `wx.downloadFile` 下载到本地 - **登录时必须请求 `profileImageUrl` 字段**:`loginWithWechatMiniApp` 的 GraphQL 查询必须包含 `profileImageUrl` - **必须从服务器获取最新信息**:页面加载时优先从服务器获取账户信息,不能只依赖本地存储 ### 自定义 TabBar [MUST] **必须完成以下配置**: - **必须在 `app.json` 中设置 `"custom": true`** - **必须在页面 JSON 配置中引用组件**:`"usingComponents": { "custom-tab-bar": "/custom-tab-bar/index" }` - **必须在页面 `onLoad` 中更新选中状态**:使用 `this.getTabBar().setData({ selected: index })` - **必须为页面添加底部 padding**:避免内容被 tabBar 遮挡,建议 `padding-bottom: calc(160rpx + env(safe-area-inset-bottom))` ```javascript // app.json "tabBar": { "custom": true, "list": [...] } // 页面 JSON { "usingComponents": { "custom-tab-bar": "/custom-tab-bar/index" } } // 页面 JS onLoad() { if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ selected: 0 }) } } ``` ### 变量作用域 [MUST] **必须遵循以下规则**: - **循环内使用的变量必须在循环外声明**:避免作用域问题导致 `ReferenceError` - **必须在使用前初始化变量**:特别是可能为 `null` 或 `undefined` 的变量 ```javascript // ❌ 错误:在循环内声明,循环外使用 while (attempts < maxAttempts) { let imageId = null // 在循环内声明 } this.saveToHistory(imageUrl, imageId) // 会报错 // ✅ 正确:在循环外声明 let imageId = null while (attempts < maxAttempts) { if (content.image.id) { imageId = content.image.id } } this.saveToHistory(imageUrl, imageId) // 正常使用 ``` ### ArrayBuffer 处理 [MUST] **必须遵循以下规则**: - **不能依赖 `instanceof ArrayBuffer` 检查**:小程序环境中可能返回 `false` - **必须使用属性检查**:检查 `byteLength`、`buffer`、`byteOffset` 等属性 - **必须转换为真正的 ArrayBuffer**:确保上传时使用真正的 ArrayBuffer ### 页面布局 [MUST] **必须遵循以下规则**: - **自定义 tabBar 页面必须添加底部 padding**:避免内容被遮挡 - **必须考虑安全区域**:使用 `env(safe-area-inset-bottom)` 适配不同设备 ```css .container { padding-bottom: calc(160rpx + env(safe-area-inset-bottom)); } ``` ### 自定义组件中加载远程 Icon(SVG/图片)[MUST] **必须遵循以下规则**: - **自定义组件中不能直接 require 其他模块**:会导致 `can not find module` 错误 - **必须直接使用 `wx.request` 调用 GraphQL API**:避免 require 依赖问题 - **必须提供 fallback(emoji 或占位符)**:确保即使 icon 加载失败,组件也能正常显示 - **必须在 `ready` 生命周期中加载**:不阻塞组件初始化,确保组件立即显示 ```javascript // ✅ 正确:硬编码 GraphQL URL,直接使用 wx.request const app = getApp() const ZION_GRAPHQL_URL = 'https://zion-app.functorz.com/zero/{projectExId}/api/graphql-v2' Component({ data: { iconUrl: '' }, lifetimes: { attached() {}, // 立即显示组件(使用 fallback) ready() { this.loadIcon().catch(err => console.error('加载 icon 失败:', err)) } }, methods: { loadIcon() { return new Promise((resolve, reject) => { const token = app.getToken() || null wx.request({ url: ZION_GRAPHQL_URL, method: 'POST', header: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }, data: { query: `query GetImageById($imageId: bigint!) { getImageById(imageId: $imageId) { id url } }`, variables: { imageId: 7000000007117943 } }, success: (res) => { if (res.statusCode === 200 && res.data.data?.getImageById?.url) { this.setData({ iconUrl: res.data.data.getImageById.url }) resolve(res.data.data.getImageById.url) } else { reject(new Error('获取 icon 失败')) } }, fail: reject }) }) } } }) ``` ```xml 🎨 ``` --- ## Lottie 动画集成 ### 库文件引入 [MUST] **从 npm/unpkg 下载的 `lottie-miniprogram` 是 webpack 打包的 UMD 格式,必须进行以下处理**: 1. **初始化 exports 对象**:在文件开头添加 `var exports = typeof exports !== 'undefined' ? exports : {};` 2. **添加 module.exports**:在文件末尾添加 CommonJS 导出: ```javascript if (typeof module !== 'undefined' && module.exports) { module.exports = { setup: exports.setup, loadAnimation: exports.loadAnimation, freeze: exports.freeze, unfreeze: exports.unfreeze } } ``` ### Canvas 2D 使用 [MUST] **必须按以下方式使用 Canvas 2D**: ```javascript const query = wx.createSelectorQuery().in(this) query.select('#lottie-canvas').fields({ node: true, size: true }).exec((res) => { const canvas = res[0].node const ctx = canvas.getContext('2d') const dpr = wx.getSystemInfoSync().pixelRatio const width = res[0].width || 600 const height = res[0].height || 600 canvas.width = width * dpr canvas.height = height * dpr ctx.scale(dpr, dpr) lottie.setup(canvas) lottie.loadAnimation({ loop: true, autoplay: true, animationData: lottieData, rendererSettings: { context: ctx, clearCanvas: true } }) }) ``` **必须遵循的规则**: - Canvas 必须使用 `type="2d"` 和 `id` 属性 - 必须设置 `canvas.width` 和 `canvas.height`(考虑 DPR) - Lottie JSON 数据通过 `getFileById` 从 Zion OSS 获取,然后使用 `wx.request` 下载内容 --- ## 参考资源 - [微信小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/) - [Zion.app 文档](https://www.functorz.com/) - [Zion.app 二进制资源上传规则](./zion-binary-asset-upload-rules.mdc) - [Zion.app 数据库操作规则](./zion-database-gql-api-rules.mdc)