ℹ️关于云审批
云审批(cloud approve)
,一款专为小微企业打造,支持多租户的在线审批神器。它简化了申请和审批流程,让您随时随地通过手机或电脑完成请款操作。员工一键提交申请,审批者即时响应,方便快捷。同时,云审批提供全面的数据记录与分析,助力企业实现财务管理透明化、智能化,安全高效,让企业的信息数字化管理变得简单轻松!最后,重要的事情说三遍📢:开源、开源、所有代码开源。
👉GITHUB开源地址 👈
👉飞书在线文档👈
概述
钉钉免登
(此处专指自建H5微应用,官方文档)是一种便捷的登录机制,当用户已在钉钉客户端(包括PC端和移动端)完成登录后,通过工作台访问我们的网站时,系统能够自动识别并完成用户身份验证,无需重复输入登录信息。该功能广泛应用于微信、飞书、抖音等主流平台,为用户提供无缝的跨平台使用体验。
流程详解
数据表
登录模块设计到两个表:账号表/Account、员工表/Staff。
账号/Account
此表为登录到平台的账户信息,支持传统的账密方式、钉钉免登等方式,并记录与之关联的员工ID
字段名 | 中文名 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|---|
id | 编号 | Int | 是 | 唯一标识 | |
cid | 企业ID | Int | 是 | 关联企业 | |
name | 账号名称 | String | 是 | ||
pwd | 密码 | String | 加密 | ||
type | 类型 | String | 是 | 登录类型 | |
sid | 员工ID | Int | 关联员工 | ||
active | 是否生效 | Boolean | 是 | false | |
addOn | 录入日期 | Int | 是 |
登录类型:
- dingding=钉钉
- wechat=微信
- phone=手机号(未来支持手机验证码登录)
- other=其它(传统密码登录)
员工/Staff
字段名 | 中文名 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|---|
id | 编号 | Int | 是 | 唯一标识 | |
cid | 企业ID | Int | 是 | 关联企业 | |
name | 员工名称 | String | 是 | ||
phone | 电话号码 | String | |||
summary | 描述 | String |
免登流程
- 准备阶段
- 企业管理员登录
钉钉开发者后台
,创建应用并配置网页功能
- 获取应用的
AppKey
与AppSecrect
- 企业管理员登录
- 逻辑实现
- 新建钉钉登录专用页面(dingding.html)
- 在页面中获取两个参数
cid(企业ID)
、corpId(钉钉内企业ID)
- 前端调用钉钉接口获取授权码/CODE
- 后端拿到上述 CODE 后通过
AppKey
与AppSecrect
获取到用户信息(包含唯一编号 unionid、姓名 name 等) - 构建唯一账户名:D_{unionid}_{name}
- 检查强求账户名是否存在于 Account 表
- 如存在则判断是否生效,若生效返回token,否则报错
- 若不存在
- 自动创建
账户信息
- 检索企业下
同名员工
,若不存在则自动创建并关联到账户对象 - 若配置了账户自动生效,返回 token,否则前端提示账户未激活请联系管理员
- 自动创建
- 部署上线
- 部署平台获取到登录页 URL(https://{域名}/dingding.html)
- 在钉钉后台填入上述地址后发布应用版本
- 用户在钉钉客户端
工作台
添加应用后即可访问
新建 dingding.html
我们在前端项目代码下新建对应页面:
并在 rsbuild.config.mjs
中配置多页面:
export default defineConfig({source:{entry:{index: './src/index.js',dingding: './src/pages/dingding/index.js'}}
})
至此,我们可以通过 http://{IP}/dingding.html
访问到该页面,作为钉钉免登的入口😄。
编写登录页面逻辑
登录页主要组件 App.vue 代码如下:
<template><div style="width: 80%; margin: 40px auto;"><div class="text-center" v-if="!errMsg"><n-spin :show="working"><template #description>钉钉客户端登录中,请稍候...</template></n-spin></div><n-alert v-else :type show-icon title="钉钉自动登录失败" :bordered="false">{{ errMsg }}</n-alert></div>
</template><script setup>import { NSpin, NAlert, useMessage, NMessageProvider } from 'naive-ui'import { requestAuth } from "./dingding"import { checkLocalToken, saveLocalToken } from "../login"const msg = useMessage()let cid = undefinedlet corpId = undefinedconst debug = import.meta.env.DEV;let working = ref(true)let errMsg = ref("")let type = ref("info")const onMsg = (msg, isError=true)=>{errMsg.value = msgtype.value = isError?"error":"info"}const tryToAutoLogin = ()=>{requestAuth(corpId).then(code=>{msg.info(`CODE=${code}`)fetch("/common/login-with-dingding",{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ cid, code })}).then(response => response.json()).then(({ success, data, message }) => {if(success==true){msg.success(`登陆成功`)saveLocalToken(data)gotoIndex()}else{const messages = {E01 : "您的钉钉账户为首次登录,请先联系管理员完成激活",E02 : "您的钉钉账户关联未激活,请联系企业管理员",E03 : "找不到当前钉钉账户关联的员工"}onMsg(messages[message]||message, !messages[message])}})}).catch(e=>onMsg(typeof(e)=='string'?e:e.message))}const gotoIndex = ()=> location.href = "/"onMounted(() => {let u = new URL(location.href)cid = u.searchParams.get('cid')corpId = u.searchParams.get('corpId')if(checkLocalToken())return gotoIndex()tryToAutoLogin()})
</script>
// dingding.js
import { runtime } from 'dingtalk-jsapi'export const requestAuth = async (corpId)=> {let { code } = await runtime.permission.requestAuthCode({corpId})return code
}//login.js
const NAME = import.meta.env.PUBLIC_HEADER_TOKEN
const CREATED = `${NAME}_CREATED`/*** 检查本地 token 是否在有效期内* @param {Number} expired - token 有效期,默认12小时,单位毫秒* @returns {Boolean} true 时为 token 有效*/
export const checkLocalToken = (expired=12*60*60*1000)=>{let token = localStorage.getItem(NAME)if(!token) return falselet expire = localStorage.getItem(CREATED)||0if(Date.now() - expire>=expired)return falsereturn true
}export const saveLocalToken = token=>{localStorage.setItem(NAME, token)localStorage.setItem(CREATED, Date.now())
}
这里不得不吐槽下钉钉开发平台的官网文档,新旧版 API 文档特别容易让人混乱,引入dingtalk-jsapi
的话需要查看旧版文档😔。
编写后端与钉钉服务器的交互
const { get, post } = require('axios')
const { loadWithCidAndName } = require("./ConstantService")
const logger = require('../common/logger')/*** @typedef {Object} DDTokenResponse - 钉钉获取token效应值* @property {String} access_token - token值* @property {Number} expires_in - 有效期(单位秒)* @property {Number} errcode - 错误代码* @property {String} errmsg - 错误信息** @typedef {Object} DDUser - 钉钉用户信息* @property {String} userid* @property {String} unionid - 唯一编号* @property {String} name - 用户名称** @typedef {Object} DDUserResponse - 钉钉用户信息响应值* @property {DDUser} result* @property {String} request_id* @property {Number} errcode - 错误代码* @property {String} errmsg - 错误信息*/const DING_HOST = "https://oapi.dingtalk.com"let localToken = {value: "",expire: 0
}const isTokenExpired = ()=> !localToken.value || localToken.expire<=Date.now()
const log = (msg, level='info')=> logger[level](`[钉钉] ${msg}`)exports.loginWithCode = async (cid, code)=>{if(isTokenExpired()){/**@type {CompanyConfig} */let cfg = await loadWithCidAndName(cid)if(!cfg || !(cfg.ddAppKey && cfg.ddAppSecret))throw `企业未配置钉钉登录`let url = `${DING_HOST}/gettoken?appkey=${cfg.ddAppKey}&appsecret=${cfg.ddAppSecret}`/**@type {{data:DDTokenResponse}} */let { data } = await get(url)if(data.errcode != 0){log(`获取企业 token 失败:${data.errcode}|${data.errmsg}`, 'error')throw data.errmsg}localToken.value = data.access_tokenlocalToken.expire = Date.now() + data.expires_in*1000log(`更新 TOKEN 为 ${localToken.value}(EXPIRED=${data.expires_in})`)}let url = `${DING_HOST}/topapi/v2/user/getuserinfo?access_token=${localToken.value}`/**@type {{data:DDUserResponse}} */let { data } = await post(url, { code })if(data.errcode != 0){log(`[钉钉] 获取用户信息失败:${data.errcode}|${data.errmsg}`, 'error')throw data.errmsg}global.isDebug && log(`获取用户信息 ${data.result.userid}/${data.result.name}`, 'debug')return data.result
}
部署及上线
创建钉钉H5微应用
- 登录钉钉开发者后台。
- 单击应用开发 > 企业内部应用 > 钉钉应用 > 创建应用。
- 填写应用信息。
配置项 | 是否必填 | 配置说明 |
---|---|---|
应用名称 | 是 | 输入应用名称,应用名称最小长度为 2 个字符。 |
应用描述 | 是 | 简要描述应用提供的产品或服务,应用描述最小长度为 4 个字符。 |
应用图标 | 否 | 上传应用图标,图标要求 JPG/PNG 格式、240 px * 240 px 以上、1:1 、2 MB 以内的无圆角图标。 |
- 单击保存,进入应用详情页。
- 如果你需要开发 AI 应用、小程序、网页应用、酷应用和机器人功能,你需要添加应用能力。
发布应用
创建应用后,需要发布才能看到噢
接着在钉钉客户端
就能看到此应用啦🎉