开发背景

随着教育培训市场的不断发展,机构面临着课程安排复杂、资源调配困难等问题。自动排课功能可以通过先进的算法和技术,根据教师、教室、学员等多种因素,高效地进行课程安排,大大提高排课效率和准确性,同时也能更好地满足学员的需求,提升学员体验。此外,它还能实现资源的优化配置,提高管理效率,降低运营成本。这一功能的出现,顺应了教培行业信息化、智能化的发展趋势

开发逻辑图

对接钉钉java 对接钉钉打卡机_javascript

搭建过程

表单搭建

老师科目表单

字段

类型

授课老师

成员

教学科目

多选

排课发起表单

字段

类型

授课老师

成员

预约科目

多选

预约时间

日期

学员信息

自定义

学员约课表单

字段

类型

学员信息

自定义

预约科目

多选

预约时间

日期

FAAS连接器自动排课后端搭建

导入axios三方库

npm install axios -S
//安装命令

dingOpenApiUtil文件引入axios库

var axios = require('axios');

dingOpenApiUtil文件定义三个方法

获取token 调用接口文档:获取企业内部应用的accessToken - 钉钉开放平台

获取所选科目老师信息 调用接口文档:获取流程实例 - 钉钉开放平台

发起排课 调用接口文档:发起宜搭审批流程 - 钉钉开放平台

getToken: async function () {
            // 定义常量dataKey,存储appKey和appSecret
            var dataKey = JSON.stringify({
                "appKey": "钉钉开放平台ak",
                "appSecret": "钉钉开放平台sk"
            });

            // 定义常量configKey,存储请求token的配置信息
            var configKey = {
                method: 'post',
                url: 'https://api.dingtalk.com/v1.0/oauth2/accessToken',//开放平台获取token接口
                headers: {
                    'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
                    'Content-Type': 'application/json',
                    'Accept': '*/*',
                    'Host': 'api.dingtalk.com',
                    'Connection': 'keep-alive'
                },
                data: dataKey
            };
            return new Promise(async (resolve) => {
                try {
                    const response = await axios(configKey); // 发起请求获取token
                    resolve(JSON.stringify(response.data)); // 将token数据转为JSON字符串并返回
                } catch (error) {
                    resolve(error,2222); // 返回错误信息
                }
                return null;
            });
        },
        getData: async function (token,data) {//获取所选科目教师信息
            // 请求配置信息
            var config = {
                method: 'post',
                url: 'https://api.dingtalk.com/v1.0/yida/processes/instances',
                headers: {
                    'x-acs-dingtalk-access-token': JSON.parse(token).accessToken, // 设置请求头中的token
                    'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
                    'Content-Type': 'application/json',
                    'Accept': '*/*',
                    'Host': 'api.dingtalk.com',
                    'Connection': 'keep-alive'
                },
                data: data
            };
            return new Promise(async (resolve) => {
                try {
                    const response = await axios(config); // 发起请求获取数据
                    resolve(JSON.stringify(response.data)); // 将响应数据转为JSON字符串并返回
                } catch (error) {
                    resolve(error,1111); // 返回错误信息
                }
                return null;  
            });
        },
        putData: async function (token,data) {//发起排课
            // 请求配置信息
            var config = {
                method: 'post',
                url: 'https://api.dingtalk.com/v1.0/yida/processes/instances/start',
                headers: {
                    'x-acs-dingtalk-access-token': JSON.parse(token).accessToken, // 设置请求头中的token
                    'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
                    'Content-Type': 'application/json',
                    'Accept': '*/*',
                    'Host': 'api.dingtalk.com',
                    'Connection': 'keep-alive'
                },
                data: data
            };
            return new Promise(async (resolve) => {
                try {
                    const response = await axios(config); // 发起请求获取数据
                    resolve(JSON.stringify(response.data)); // 将响应数据转为JSON字符串并返回
                } catch (error) {
                    resolve(error,1111); // 返回错误信息
                }
                return null;  
            });
        }

faasEntry文件内调用定义方法示例

注:内部字段编码以及处理数据格式和appType、syetemtoken、表单编码等需按照自己实际页面变更

const { openApi: OpenAPIUtil, yidaConnector: YidaConnectorUtil, httpUtil: HttpUtil } = require('./utils');
const { ConnectorTypeEnum } = YidaConnectorUtil;

module.exports = async function (faasInputs, context) {
    try {
        const { inputs = {}, yidaContext = {} } = faasInputs || {};
        //等价写法如下
        // const yidaContext = faasInputs["yidaContext"];
        // const inputs = faasInputs["inputs"];

        const userId = yidaContext ? yidaContext['userId'] : '';
        const corpId = yidaContext ? yidaContext['corpId'] : '';

        /**
         *用于调用钉钉开放平台OpenAPI的accessToken, 宜搭提供的, 仅申请了钉钉开放平台的部分OpenAPI的调用权限
         *如果此accessToken不满足您的需求, 可在钉钉开放平台创建您自己的钉钉应用并获取appKey和APPSecret并使用OpenApiUtil获取您自己的accessToken
         * @see OpenAPIUtil.getCustomAccessTokenThenCache(appKey, appSecret)
         */
        const accessToken = yidaContext ? yidaContext['accessToken'] : '';
        /**
         *调用宜搭的前端消费接口时使用
         */
        const consumeCode = yidaContext ? yidaContext['consumeCode'] : '';

        //设置钉开放平台访问token, 后续无需再设置
        OpenAPIUtil.setAccessToken(accessToken);

        /**
         *important: 如果需要优先使用您自己的钉钉应用获取accessToken, 那么只需要取消下面一行的代码注释即可, 请注意: 上一行的代码请不要注释, 因为setAccessToken和getCustomAccessTokenThenCache设置的accessToken优先级不同, 并不冲突
         */
        // await OpenAPIUtil.getCustomAccessTokenThenCache("您的钉钉应用appKey","您的钉钉应用appSecret");

        //设置连接器消费码(调用宜搭连接器和调用宜搭平台接口时起到关键作用), 后续无需再设置
        YidaConnectorUtil.setConsumeCode(consumeCode);


        const input = inputs;

        /**
         *在这里编写您的业务代码, 也可以将业务代码封装到其他类或方法里.
         */
        // const result = {};
        // try {
        //   let businessResult = await doYourBusiness(faasInputs);
        //    result.success= true;
        //    result["result"] = businessResult;
        //    result["error"]="";
        //    return result;
        // } catch (e) {
        //    result["success"]=false;
        //    result["result"]=null;
        //    result["error"]=e.name + " " + e.message;
        //    return result;
        // }
        /**
         *可调用宜搭连接器
         */
        // const result = {};
        // try {
        //    const connectorResult = await invokeYidaConnector(faasInputs);
        //    result["success"]=true;
        //    result["result"]=connectorResult;
        //    result["error"]="";
        //    return result;
        // } catch ( e) {
        //    result["success"]=false;
        //    result["result"]=null;
        //    result["error"]=JSON.stringify(e);
        //    return result;
        // }
        /**
         *调用钉开放平台OpenAPI
         */
        const result = {};
        try {
            const formInstanceIdList = await invokeDingOpenApi(input.params);
            const teacher = await timeMatching(input.time, formInstanceIdList);
            const code = await putData(input.time, teacher)
            result["success"] = true;
            result["result"] = JSON.parse(code).result;
            // result["result"] = formInstanceIdList;
            // result["result"] = input.params;
            result["error"] = "";
            return result;
        } catch (e) {
            result["success"] = false;
            result["result"] = null;
            result["error"] = JSON.stringify(e);
            return result;
        }
        /**
         * 返回的JSONObject并不是一定要带success、result、error, 下面的代码只是示例, 具体返回哪些key-value由您自己决定, 尽量与您在宜搭连接器工厂里配置的出参结构保持一致即可
         */

        // let result = {
        //   'success':true,
        //   'error':'',
        //   'result':''
        // }
        //
        // return result;
    } catch (err) {
        let result = {
            'success': false,
            'error': JSON.stringify(err) + " " + err.name + " " + err.message,
            'result': ''
        }
        return result;
    }


    async function invokeYidaConnector(faasInputs) {
        const { inputs, yidaContext } = faasInputs;
        const input = inputs || {};
        const userId = yidaContext["userId"];
        const corpId = yidaContext["corpId"];
        const connectorActionInputs = {};
        //注意: http连接器的入参是body,query等, 钉钉官方连接器则不是, 请参考宜搭帮助文档 https://www.yuque.com/yida/support/stbfik
        {
            connectorActionInputs["unionId"] = [userId];
            connectorActionInputs["subject"] = input["title"] || "Node Faas创建待办";
            connectorActionInputs["creatorId"] = [userId];
            connectorActionInputs["description"] = "Node Faas连接器里调用钉钉官方连接器创建待办";
            connectorActionInputs["dueTime"] = Date.now() + 600 * 1000;
            connectorActionInputs["priority"] = 10;
        }
        //请确保调用前设置了consumeCode
        //更多钉钉官方连接器ID和动作ID及出入参请参照  https://www.yuque.com/yida/support/stbfik#Mv0dK (如果目标段落不存在, 请访问 https://www.yuque.com/yida/support/stbfik)
        const response = await YidaConnectorUtil.invokeService("G-CONN-1016B8AEBED50B01B8D00009", "G-ACT-1016B8B1911A0B01B8D0000I", ConnectorTypeEnum.DING_INNER_CONNECTOR, null, connectorActionInputs);
        if (response && response.success) {
            try {
                return YidaConnectorUtil.extractYidaConnectorExecutionResult(response);
            } catch (err) {
                throw new Error(JSON.stringify(err) + " " + err.name + " " + err.message);
            }
        } else {
            throw new Error(response ? (response.errorCode + " " + response.errorMsg) : "执行宜搭连接器失败");
        }
    }

    async function doYourBusiness(faasInputs = {}) {
        //请在invokeDingOpenApi方法里填写你的宜搭应用编码等信息再启用下行注释的代码来观察效果
        // console.info("openAPI 返回结果", await invokeDingOpenApi());

        const result = await HttpUtil.doGet("https://www.aliwork.com", "/query/appTpl/listAppTpl.json", { 'Referer': 'www.aliwork.com' });

        console.info("查询宜搭模板 返回结果", result);

        //调用宜搭连接器依赖consumeCode, 由于cosumeCode的有效时间为1分钟,每次都由宜搭服务重新计算consumeCode并传入本faas函数内, 因此您无法在CloudIDE里直接测试调用宜搭连接器, 需要部署本faas连接器后去宜搭连接器工厂或者宜搭页面数据源或者集成自动化里调用该faas连接器
        const connectorResult = await invokeYidaConnector(faasInputs);

        console.info("调用宜搭连接器 返回结果", connectorResult);

        return { "templateResult": result, "connectorResult": connectorResult };
    }

    async function invokeDingOpenApi(parse) {
        const token = await OpenAPIUtil.getToken();
        var data = JSON.stringify({
            "appType": "应用appType",
            "systemToken": "应用systemToken",
            "userId": "调用人userid",
            "formUuid": "授课老师科目表单formUuid",
            // "searchFieldJson": JSON.stringify(inputs),
            "pageNumber": 1,
            "pageSize": 100

        });

        const jsonarrdata = await OpenAPIUtil.getData(token, data);
        const arrdata = JSON.parse(jsonarrdata).data;
        var arr = [];
        arrdata.map((item) => {
            const yseno = doesIncludeAll(item.data.multiSelectField_lutlzduc, parse)
            if (yseno) {
                arr.push({ label: item.data.textField_lu7qb3vo, value: item.data.textField_lu7qb3vn,iphon:item.data.textField_lv62sj5d })
            }

        })
        return arr.sort(function (a, b) {
            return 0.5 - Math.random();
        });
    }
    async function timeMatching(time, teachers) {
        const token = await OpenAPIUtil.getToken();
        for (let i = 0; i < teachers.length; i++) {
            const arr =
                [
                    { "key": "textField_luqnog89", "value": teachers[i].label, "type": "TEXT", "operator": "eq", "componentName": "TextField" },
                    { "key": "dateField_luqnog86", "value": [time, time + 60 * 60 * 1000], "type": "DOUBLE", "operator": "between", "componentName": "DateField" }
                ]

            var data = JSON.stringify({
                "appType": "应用编码",
                "systemToken": "应用密钥",
                "userId": "操作人userid",
                "formUuid": "排课发起formUuid",
                "searchFieldJson": JSON.stringify(arr),
                "pageNumber": 1,
                "pageSize": 100

            });

            const res = await OpenAPIUtil.getData(token, data);
            if (JSON.parse(res).totalCount === 0) {
                return teachers[i]
            }
        }



    }

    function doesIncludeAll(arr1, arr2) {
        if (arr1.length < arr2.length) {
            return false;
        }
        let isIncludeAll = true;
        for (let i = 0; i < arr2.length; i++) {
            if (!arr1.includes(arr2[i])) {
                isIncludeAll = false;
                break;
            }
        }
        return isIncludeAll;
    }
    async function putData(time, teachers) {
        const token = await OpenAPIUtil.getToken();
        // if(!teachers)
        // {

        // }
        const date = new Date(time + 28800000);
        const date2 = new Date(time + 60 * 60 * 1000 + 28800000);
        const arr = {
            "dateField_luqnog86": time,
            "dateField_luqnog88": time + 60 * 60 * 1000,
            "textField_luqnog89": teachers.label,
            "textField_luqnog8a": teachers.value,
            "employeeField_luqmf2dz": [teachers.value],
            "dateField_luqmf2e0": time,
            "dateField_luqmf2e2": time + 60 * 60 * 1000,
            "selectField_luqmf2e1": date.getHours().toString().padStart(2, '0') + ":" + date.getMinutes().toString().padStart(2, '0'),
            "selectField_luqmf2e3": date2.getHours().toString().padStart(2, '0') + ":" + date2.getMinutes().toString().padStart(2, '0'),
            "textField_luqncrm4": "running",
            "textField_lup78hhx": teachers.label,
            "textField_luw0r316": teachers.label,
            "textField_lv6bbj9e":teachers.iphon,
            "textField_lv6bbj9h":teachers.iphon

        }

        var data = JSON.stringify({
            "appType": "应用AppType",
            "systemToken": "应用systemToken",
            "userId": "165018306524152980",
            "formUuid": "排课发起表单formUuid",
            "formDataJson": JSON.stringify(arr),
            "pageNumber": 1,
            "pageSize": 100

        });

        const res = await OpenAPIUtil.putData(token, data);
        return res
    }
};

宜搭内集成自动化设置

对接钉钉java 对接钉钉打卡机_低代码_02

搭建效果

学员约课-匹配教师科目库与当前排课列表-自动根据约课时间与预约科目匹配教师-生成排课表