
在实际项目开发中无论是技术预研、方案验证还是向团队或客户展示核心功能一个清晰、可运行的 Demo 都是至关重要的。然而很多开发者习惯在本地随意创建文件夹代码、配置、文档混杂一旦需要回顾或分享往往难以理清脉络。一份规范的“Demo 开发记录”不仅能帮助开发者本人复盘技术细节更能成为团队内部高效传递技术方案的载体。本文将以一个典型的 Spring Boot Vue 前后端分离项目为例展示如何从零开始系统地记录一个具备钉钉免登录功能的 Demo 的开发全过程。我们将遵循“概念 - 环境 - 实现 - 验证 - 排错 - 优化”的主线确保每个环节都有据可查最终产出一份可直接用于技术分享或项目启动的完整记录。1. 理解 Demo 开发记录的核心价值与结构Demo 开发记录不是简单的代码仓库 README也不是流水账式的日记。它的核心价值在于可复现性和可理解性。一份好的记录应该让一个具备相关技术背景但未参与初始开发的同事能够根据文档独立完成环境搭建、配置、运行并理解核心逻辑。1.1 记录应该包含哪些内容一份完整的 Demo 开发记录通常包含以下几个层次项目概述与目标用一两句话说明这个 Demo 要验证或演示什么核心功能例如实现基于 Spring Boot 和 Vue 的钉钉扫码免登录集成。技术栈与版本明确记录所有关键组件的名称和具体版本号这是避免“在我机器上能跑”问题的关键。环境准备清单列出所有需要预先安装的软件、工具及其配置要求。项目结构与初始化说明如何创建项目骨架关键目录的职责是什么。核心配置详解对于集成了第三方服务如钉钉的 Demo配置是重中之重。需要记录如何申请凭证、配置哪些参数、参数的含义是什么。关键代码实现不是粘贴全部代码而是摘录最核心的、体现技术要点的代码片段并加以解释。运行与验证步骤提供清晰的命令行或 IDE 操作步骤并说明如何验证功能是否成功例如登录后如何查看用户信息。常见问题与排查将开发过程中遇到的典型错误、现象、原因和解决方案记录下来形成排查清单。总结与扩展方向回顾 Demo 的局限性并指出基于此 Demo 可以进一步探索哪些生产级特性。1.2 为什么版本信息如此重要在记录技术栈时必须精确到版本。以 Spring Boot 为例2.7.x 和 3.0.x 在配置和依赖上可能有重大差异。钉钉开放平台的 API 也会迭代。模糊的版本描述是后续复现失败的主要根源。记录时应采用表格形式清晰明了。组件版本说明JDK17推荐 LTS 版本Node.js18.16.0Vue 3 所需Spring Boot2.7.12选择稳定的次要版本Vue3.3.4组合式 API钉钉 JSAPI2.16.7前端扫码 SDK钉钉服务端 SDK2.0.1后端 API 调用2. 环境准备与项目初始化在开始编码前确保所有基础环境就位。我们将创建两个独立的工程一个 Spring Boot 后端一个 Vue 前端。2.1 后端 Spring Boot 项目初始化使用 Spring Initializr 或 IDE 的 Spring Boot 创建向导生成项目基础结构。关键依赖选择Spring Web: 提供 RESTful API 支持。Spring Security(可选): 用于更复杂的权限管理本 Demo 为简化可先不选。Lombok: 简化实体类代码。Spring Boot DevTools: 开发热重启。通过命令行初始化的示例# 使用 curl 从 start.spring.io 生成项目 curl https://start.spring.io/starter.zip -d typemaven-project -d languagejava -d bootVersion2.7.12 -d baseDirdingtalk-demo-backend -d groupIdcom.example -d artifactIddemo -d namedingtalk-demo-backend -d dependenciesweb,lombok,devtools -o backend.zip unzip backend.zip -d .初始化后的项目结构应类似dingtalk-demo-backend/ ├── src/ │ ├── main/ │ │ ├── java/com/example/demo/ │ │ │ ├── DemoApplication.java │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ └── config/ │ │ └── resources/ │ │ ├── application.properties │ │ └── static/ │ └── test/ └── pom.xml2.2 前端 Vue 项目初始化使用 Vue 官方脚手架 Vite 创建项目以获得更快的启动和构建速度。npm create vuelatest dingtalk-demo-frontend # 根据提示选择项目特性本Demo建议 # - TypeScript: No (为简化) # - JSX: No # - Vue Router: Yes (用于页面路由) # - Pinia: Yes (用于状态管理) # - Vitest: No # - ESLint: Yes # - Prettier: Yes cd dingtalk-demo-frontend npm install创建后安装钉钉前端 JSAPI 依赖npm install dingtalk-jsapi --save同时安装 Axios 用于 HTTP 请求npm install axios --save前端项目结构dingtalk-demo-frontend/ ├── src/ │ ├── views/ │ │ └── Login.vue # 登录页面 │ ├── router/ │ │ └── index.js # 路由配置 │ ├── stores/ │ │ └── user.js # 用户状态管理 │ ├── utils/ │ │ └── dingtalk.js # 钉钉 API 封装 │ └── App.vue ├── index.html ├── vite.config.js └── package.json3. 钉钉应用配置与核心原理这是本 Demo 最关键的环节。钉钉免登录扫码登录的核心流程是 OAuth2.0 的简化版依赖于钉钉内嵌的浏览器环境。3.1 创建钉钉企业内部应用登录 钉钉开发者后台 。进入“应用开发” - “企业内部开发” - “H5微应用”。点击“创建应用”填写应用名称、描述等。应用创建成功后记录以下关键信息AgentId: 应用标识。AppKey: 应用的唯一 Key。AppSecret: 应用的密钥务必保密。配置“开发管理”服务器出口IP: 填写你后端服务器的公网 IP。如果是本地开发可使用内网穿透工具如 ngrok、natapp获取临时域名并将其 IP 加入白名单。应用首页地址: 填写前端应用访问地址如http://your-ngrok-subdomain.ngrok.io或本地http://localhost:5173。PC端首页地址: 同上。配置“权限管理”为应用添加“成员信息读”权限否则无法获取用户详情。3.2 免登录流程详解前端触发用户在钉钉内访问 H5 应用前端 JS 检测到 DingTalk 环境。获取免登授权码前端调用dd.runtime.permission.requestAuthCode传入corpId企业 CorpId可在开发者后台首页查看和配置信息从钉钉客户端获取一个临时的authCode。这个authCode有效期很短通常5分钟且一次性的。向后端发送 Code前端将获取到的authCode发送给自己的后端服务器。后端换取用户信息后端服务器使用AppKey、AppSecret和authCode调用钉钉服务端 API (/getuserinfo)换取该用户的唯一标识userid和访问令牌access_token。获取用户详情后端再使用access_token和userid调用钉钉服务端 API (/user/get)获取用户的详细信息如姓名、部门、头像等。建立本地会话后端根据获取到的用户信息生成自定义的 Token如 JWT或建立 Session返回给前端完成登录过程。注意authCode只能使用一次且必须在后端换取用户信息。绝对不要在前端尝试用authCode去调用服务端 API因为AppSecret不能暴露。4. 后端核心代码实现后端主要负责接收前端传来的authCode并与钉钉服务端交互完成用户身份验证。4.1 添加钉钉服务端 SDK 依赖在pom.xml中添加钉钉官方 Java SDKdependency groupIdcom.aliyun/groupId artifactIddingtalk/artifactId version2.0.1/version /dependency4.2 配置钉钉应用参数在application.properties或application.yml中配置# 钉钉应用配置 dingtalk.corp-idyour_corp_id dingtalk.app-keyyour_app_key dingtalk.app-secretyour_app_secret4.3 创建配置类读取参数package com.example.demo.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; Configuration ConfigurationProperties(prefix dingtalk) Data public class DingTalkConfig { private String corpId; private String appKey; private String appSecret; }4.4 创建服务类处理钉钉 API 调用package com.example.demo.service; import com.dingtalk.api.DefaultDingTalkClient; import com.dingtalk.api.DingTalkClient; import com.dingtalk.api.request.OapiGettokenRequest; import com.dingtalk.api.request.OapiSnsGetuserinfoBycodeRequest; import com.dingtalk.api.request.OapiUserGetRequest; import com.dingtalk.api.response.OapiGettokenResponse; import com.dingtalk.api.response.OapiSnsGetuserinfoBycodeResponse; import com.dingtalk.api.response.OapiUserGetResponse; import com.example.demo.config.DingTalkConfig; import com.taobao.api.ApiException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; Slf4j Service RequiredArgsConstructor public class DingTalkService { private final DingTalkConfig dingTalkConfig; // 获取企业内部应用的access_token (注意与扫码登录的token不同) public String getAccessToken() throws ApiException { DingTalkClient client new DefaultDingTalkClient(https://oapi.dingtalk.com/gettoken); OapiGettokenRequest req new OapiGettokenRequest(); req.setAppkey(dingTalkConfig.getAppKey()); req.setAppsecret(dingTalkConfig.getAppSecret()); req.setHttpMethod(GET); OapiGettokenResponse rsp client.execute(req); if (!rsp.isSuccess()) { log.error(获取AccessToken失败: {}, rsp.getErrmsg()); throw new RuntimeException(钉钉服务调用失败: rsp.getErrmsg()); } return rsp.getAccessToken(); } // 通过临时授权码(authCode)获取用户信息 public OapiSnsGetuserinfoBycodeResponse.UserInfo getUserInfoByCode(String authCode) throws ApiException { DingTalkClient client new DefaultDingTalkClient(https://oapi.dingtalk.com/sns/getuserinfo_bycode); OapiSnsGetuserinfoBycodeRequest req new OapiSnsGetuserinfoBycodeRequest(); req.setTmpAuthCode(authCode); OapiSnsGetuserinfoBycodeResponse rsp client.execute(req, dingTalkConfig.getAppKey(), dingTalkConfig.getAppSecret()); if (!rsp.isSuccess()) { log.error(通过authCode获取用户信息失败: {}, rsp.getErrmsg()); throw new RuntimeException(钉钉服务调用失败: rsp.getErrmsg()); } return rsp.getUserInfo(); } // 根据userid获取用户详情需要access_token public OapiUserGetResponse getUserDetail(String userId, String accessToken) throws ApiException { DingTalkClient client new DefaultDingTalkClient(https://oapi.dingtalk.com/user/get); OapiUserGetRequest req new OapiUserGetRequest(); req.setUserid(userId); req.setHttpMethod(GET); OapiUserGetResponse rsp client.execute(req, accessToken); if (!rsp.isSuccess()) { log.error(获取用户详情失败: {}, rsp.getErrmsg()); throw new RuntimeException(钉钉服务调用失败: rsp.getErrmsg()); } return rsp; } }4.5 创建控制器提供登录接口package com.example.demo.controller; import com.dingtalk.api.response.OapiSnsGetuserinfoBycodeResponse; import com.dingtalk.api.response.OapiUserGetResponse; import com.example.demo.service.DingTalkService; import com.taobao.api.ApiException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; Slf4j RestController RequestMapping(/api/auth) RequiredArgsConstructor public class AuthController { private final DingTalkService dingTalkService; PostMapping(/dingtalk/login) public MapString, Object dingtalkLogin(RequestParam String authCode) { MapString, Object result new HashMap(); try { // 1. 用authCode换取用户基本信息包含unionid和userid OapiSnsGetuserinfoBycodeResponse.UserInfo userInfo dingTalkService.getUserInfoByCode(authCode); String userId userInfo.getUserid(); // 2. 获取access_token用于调用获取用户详情接口 String accessToken dingTalkService.getAccessToken(); // 3. 获取用户详情姓名、头像等 OapiUserGetResponse userDetail dingTalkService.getUserDetail(userId, accessToken); // 4. 此处应生成自定义Token如JWT或建立Session。为简化Demo直接返回用户信息。 // 生产环境务必不要返回敏感信息如手机号。 result.put(success, true); result.put(user, userDetail); // 5. 记录登录日志 log.info(用户登录成功: userId{}, name{}, userId, userDetail.getName()); } catch (ApiException e) { log.error(钉钉登录处理失败, e); result.put(success, false); result.put(message, 登录失败: e.getErrMsg()); } catch (Exception e) { log.error(系统异常, e); result.put(success, false); result.put(message, 系统异常); } return result; } }5. 前端核心代码实现前端负责在钉钉环境内获取authCode并调用后端登录接口。5.1 封装钉钉 JSAPI 工具在src/utils/dingtalk.js中import * as dd from dingtalk-jsapi; /** * 检查是否在钉钉环境内 * returns {boolean} */ export function isInDingTalk() { return dd.env.platform ! notInDingTalk; } /** * 获取免登授权码 (authCode) * param {string} corpId - 企业CorpId * returns {Promisestring} - 返回authCode */ export function getAuthCode(corpId) { return new Promise((resolve, reject) { if (!isInDingTalk()) { reject(new Error(当前不在钉钉环境无法使用免登功能)); return; } dd.ready(() { dd.runtime.permission.requestAuthCode({ corpId: corpId, onSuccess: (result) { console.log(获取authCode成功:, result.code); resolve(result.code); }, onFail: (err) { console.error(获取authCode失败:, err); reject(new Error(获取授权码失败: ${err.errorMessage || JSON.stringify(err)})); } }); }); dd.error((error) { console.error(DD SDK 加载错误:, error); reject(new Error(钉钉JSAPI加载失败)); }); }); }5.2 创建用户状态管理 Store在src/stores/user.js中使用 Piniaimport { defineStore } from pinia; import { ref } from vue; import axios from axios; export const useUserStore defineStore(user, () { const userInfo ref(null); const isLoggedIn ref(false); const loading ref(false); const error ref(null); // 钉钉免登登录 async function loginWithDingTalk(corpId) { loading.value true; error.value null; try { // 1. 动态导入工具函数避免非钉钉环境直接报错 const { getAuthCode } await import(/utils/dingtalk); const authCode await getAuthCode(corpId); // 2. 调用后端登录接口 const response await axios.post(/api/auth/dingtalk/login, { authCode: authCode }, { headers: { Content-Type: application/json } }); if (response.data.success) { userInfo.value response.data.user; isLoggedIn.value true; // 可在此处将token存储到localStorage或cookie console.log(登录成功:, userInfo.value); } else { throw new Error(response.data.message || 登录失败); } } catch (err) { error.value err.message; console.error(登录过程出错:, err); // 可以在此处跳转到错误页面或显示提示 } finally { loading.value false; } } function logout() { userInfo.value null; isLoggedIn.value false; // 清除本地存储的token } return { userInfo, isLoggedIn, loading, error, loginWithDingTalk, logout }; });5.3 创建登录页面组件在src/views/Login.vue中template div classlogin-container div v-if!isInDingTalkEnv classtip p请在钉钉应用内打开此页面以使用扫码免登功能。/p p当前环境: {{ currentEnv }}/p /div div v-else h2钉钉免登 Demo/h2 button clickhandleDingTalkLogin :disabledloading classlogin-button {{ loading ? 登录中... : 一键登录 }} /button div v-iferror classerror-message 登录失败: {{ error }} /div div v-ifuserStore.isLoggedIn classuser-info h3登录成功/h3 p欢迎你{{ userStore.userInfo.name }}/p img :srcuserStore.userInfo.avatar alt头像 width60 / p部门: {{ userStore.userInfo.department }}/p button clickuserStore.logout classlogout-button退出登录/button /div /div /div /template script setup import { computed, onMounted, ref } from vue; import { useUserStore } from /stores/user; const userStore useUserStore(); const isInDingTalkEnv ref(false); const currentEnv ref(); // 检查环境 onMounted(async () { try { const { isInDingTalk } await import(/utils/dingtalk); isInDingTalkEnv.value isInDingTalk(); currentEnv.value isInDingTalkEnv.value ? 钉钉环境 : 非钉钉环境; } catch (e) { console.log(环境检测失败可能未加载DD SDK); currentEnv.value 未知环境; } }); // 企业CorpId应从环境变量或配置中读取此处写死仅用于Demo const corpId your_corp_id_here; const handleDingTalkLogin async () { await userStore.loginWithDingTalk(corpId); }; const loading computed(() userStore.loading); const error computed(() userStore.error); /script style scoped .login-container { padding: 40px; text-align: center; } .tip { color: #666; margin-top: 100px; } .login-button { background-color: #0086f6; color: white; border: none; padding: 12px 30px; font-size: 16px; border-radius: 4px; cursor: pointer; margin-top: 30px; } .login-button:disabled { background-color: #ccc; cursor: not-allowed; } .error-message { color: #f56c6c; margin-top: 15px; } .user-info { margin-top: 30px; padding: 20px; border-top: 1px solid #eee; } .logout-button { margin-top: 15px; padding: 8px 20px; background-color: #f4f4f5; border: 1px solid #dcdfe6; border-radius: 4px; cursor: pointer; } /style6. 配置跨域与运行验证前后端分离项目需要解决跨域问题并确保服务可访问。6.1 后端配置跨域在 Spring Boot 中可以创建一个简单的配置类package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class CorsConfig { Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { Override public void addCorsMappings(CorsRegistry registry) { // 允许前端开发服务器的地址访问 registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) // Vue dev server .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }; } }6.2 前端配置代理开发环境在vite.config.js中配置代理避免前端直接请求后端端口导致的跨域问题import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:8080, // 你的后端地址 changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })6.3 运行与验证步骤启动后端服务cd dingtalk-demo-backend ./mvnw spring-boot:run # 或使用IDE启动 DemoApplication控制台应输出Started DemoApplication in X seconds。启动前端服务cd dingtalk-demo-frontend npm run dev控制台会输出本地访问地址如http://localhost:5173。配置内网穿透关键由于钉钉需要访问一个公网可访问的地址来加载你的前端应用你需要使用内网穿透工具如 ngrok将本地http://localhost:5173暴露到公网。# 以 ngrok 为例 ngrok http 5173命令执行后会生成一个临时的公网地址如https://abc123.ngrok.io。更新钉钉应用配置回到钉钉开发者后台将“应用首页地址”和“PC端首页地址”修改为上一步获得的内网穿透地址如https://abc123.ngrok.io。保存并发布。在钉钉中访问在钉钉工作台找到你创建的应用点击进入。页面应加载你的 Vue 应用并显示“一键登录”按钮。触发登录点击“一键登录”按钮。此时会调用钉钉 JSAPI 获取authCode并发送到你的后端。后端与钉钉服务器交互后应返回用户信息并显示在页面上。验证成功的关键标志前端控制台无红色错误日志。点击登录按钮后页面显示用户姓名、头像等信息。后端控制台打印出“用户登录成功”的日志并包含正确的 userId 和 name。7. 常见问题排查清单开发过程中你几乎一定会遇到以下问题。请按此清单逐一排查。问题现象可能原因检查点与解决方案前端页面在钉钉内打开白屏或无法加载1. 钉钉应用配置的“首页地址”错误。2. 内网穿透服务不稳定或已过期。3. 前端服务未启动。1. 确认钉钉后台配置的地址与ngrok提供的地址完全一致包括http/https。2. 重新运行ngrok命令更新后台配置。3. 在浏览器直接访问ngrok地址看是否能打开页面。点击登录按钮无反应前端控制台报错dd is not defined1. 页面未在钉钉环境内运行。2. 钉钉 JSAPI 未成功加载。1. 确保在钉钉 App 内打开应用而非手机浏览器。2. 检查index.html是否引入了正确的 JSAPI 脚本通常由钉钉环境自动注入。3. 使用dd.ready确保 SDK 加载完成后再调用 API。点击登录按钮后前端报错获取授权码失败1.corpId参数错误。2. 钉钉应用未正确配置“权限”或“安全域名”。3. 用户不在该应用的可见范围内。1. 核对corpId是否为开发者后台首页的“企业 CorpId”。2. 在开发者后台确认应用已添加“成员信息读”权限并已发布。3. 确认当前登录的钉钉账号在应用的可使用范围内。后端接口返回[400] invalid tmp_auth_code1.authCode已过期5分钟。2.authCode已被使用过。3. 调用换取用户信息接口时传入的AppKey/AppSecret与生成authCode的应用不匹配。1. 确保前端获取authCode后立即调用后端接口不要延迟。2. 检查后端配置的AppKey和AppSecret是否与钉钉后台创建的应用一致。后端接口返回[400] invalid app_key or app_secret1.AppKey或AppSecret配置错误。2. 应用的AppSecret在后台被重置但代码中未更新。1. 仔细核对application.properties中的配置注意大小写和空格。2. 去钉钉开发者后台查看应用确认AppSecret如有重置则更新配置。后端接口返回[500] 系统繁忙或超时1. 钉钉服务端临时故障。2. 网络问题导致请求无法到达钉钉服务器。1. 稍后重试。2. 检查服务器出口 IP 是否在钉钉后台正确配置。3. 在后端代码中添加重试机制和更详细的错误日志。跨域错误 (CORS)1. 后端未配置 CORS 或配置不正确。2. 前端请求的 Origin 不在允许列表中。1. 检查后端CorsConfig中allowedOrigins是否包含了前端地址如http://localhost:5173。2. 前端使用代理Vite proxy可以绕过浏览器跨域限制是开发环境的最佳实践。登录成功后前端无法获取用户状态1. 前端状态管理Pinia未持久化页面刷新后状态丢失。2. 后端返回的用户信息结构前端未正确解析。1. 登录成功后将 Token 或关键信息存入localStorage或cookie并在应用初始化时读取恢复状态。2. 对比后端返回的OapiUserGetResponse对象和前端的userInfo结构确保字段名匹配。8. 从 Demo 到生产环境的关键考量这个 Demo 实现了核心的免登流程但要用于生产环境还需要补充以下关键点8.1 安全加固Token 管理Demo 中直接返回了用户详情。生产环境应生成一个短期有效的 JWT Token 返回给前端后续接口凭此 Token 访问。Token 中应包含用户标识和必要声明而非全部用户信息。敏感信息保护AppSecret必须妥善保管绝不能出现在前端代码或版本控制系统中。应使用配置中心或环境变量管理。防重放攻击authCode一次性使用已由钉钉保证。但自己的 JWT Token 需要加入 jti (JWT ID) 或 nonce 防止被截获重用。权限校验后端接口应根据 Token 中的用户信息进行细粒度的权限校验。8.2 配置外部化将钉钉的corpId、appKey、appSecret等配置从application.properties移到环境变量或配置服务器中。# 启动命令示例 java -Ddingtalk.corp-id$DINGTALK_CORP_ID \ -Ddingtalk.app-key$DINGTALK_APP_KEY \ -Ddingtalk.app-secret$DINGTALK_APP_SECRET \ -jar your-app.jar8.3 异常处理与日志细化异常将ApiException转换为业务异常区分网络超时、参数错误、权限不足等不同情况给前端更明确的提示。请求日志记录所有钉钉 API 调用的请求和响应脱敏后便于问题追踪。熔断与降级考虑钉钉服务不可用时的降级方案例如启用备用登录方式。8.4 前端体验优化加载状态提供更友好的加载中提示。错误提示根据不同的错误类型如网络错误、权限错误展示不同的用户指引。多环境适配通过环境变量区分开发、测试、生产环境的配置如corpId、后端 API 地址。8.5 扩展方向基于此 Demo可以进一步探索用户信息本地同步将钉钉用户信息同步到本地数据库补充更多业务字段。部门与角色同步调用钉钉部门、角色 API实现组织架构同步。消息推送集成钉钉工作通知消息 API实现业务消息推送。微应用深度集成使用 JSAPI 调用钉钉的通讯录、日历、考勤等功能。通过这样一份详细的开发记录不仅完整记录了从零到一的搭建过程更重要的是提供了问题排查的路径和未来演进的方向。下次当你或你的同事需要回顾或基于此进行二次开发时这份记录的价值就会充分体现。