C# 纷享销客API接口调用

总体来讲,纷享销客的技术论坛或者网点真的太少,API接口文档有的描述还是错的,有问题只能找只懂界面操作的客服,稍微问到技术相关都是【非常抱歉 我反馈下技术部门看下这些问题~】,过后就没下文(起码上个月问完到现在都没回复),或者会建议让我们自己找对应的纷享业务经理?!虽然人家官方肯定有各种合理解释,但是不爽就是不爽 ~

虽然平台用起来对开发很不方便,但谁叫领导买了呢…希望各位用过的、正在用的看客看到此文用例可以相互交流学习下;

PS.C#功底不行看下有没有高手指点下,尤其是HTTP那块;

引用

引用

HTTP接口访问类【HttpHelper】:

	/// <summary>
    /// POST请求与获取结果
    /// </summary>
    public static string HttpPostForJson(string Url, object jsonObj, string encodStr = "UTF-8")
    {
        GC.Collect();// 强制对所有代码进行即时垃圾回收。
        var request = (HttpWebRequest)WebRequest.Create(Url);
        request.ContentType = "application/json";
        request.Method = "POST";

        using (var streamWriter = new StreamWriter(request.GetRequestStream()))
        {
            string json = new JavaScriptSerializer().Serialize(jsonObj);
            streamWriter.Write(json);
        }

        var response = (HttpWebResponse)request.GetResponse();
        using (var streamReader = new StreamReader(response.GetResponseStream()))
        {
            return  streamReader.ReadToEnd();
        }

    }

纷享接口调用工具类【FenXiangXiaoKeHelper】(主要)

大家注意这些接口很多都是没有唯一性验证的,亲测了下,例如重名客户是可以通过接口添加的
PS.里面的登录参数只能各位看客自己找人要了;
嫌弃篇幅长的试试这个↓

在这里插入图片描述

using JD_FXSynTool.Syn;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;

namespace JD_FXSynTool.Utils
{
    /// <summary>
    /// 纷享销客:
    /// Open API 的访问频次分时段控制规则:
    /// 00:00-07:00不能超过100次/20秒;
    /// 07:00-24:00不能超过60次/20秒。
    /// 服务器错误的Json返回
    /// {
    ///     "errorCode": 10001,
    ///     "errorMessage": "the parameter appId is missing"
    /// }
    /// 
    ///  所有返回对象属性使用【返回对象["属性名称"];】形式获取
    ///  (除【通过code换取openUserId】以及一些付费对象接口外都已测通)
    /// </summary>
    public static class FX_Helper
    {
        /// <summary>
        /// 查询数上限
        /// </summary>
        public const int QueryLimitSum = 100;

        /// <summary>
        /// 接口前缀
        /// </summary>
        private const string ApiUrlPre = "https://open.fxiaoke.com/";

        /// <summary>
        /// 调用时间记录锁
        /// </summary>
        private static object AskTImeListlockObj = new object();

        /// <summary>
        /// 是否处于暂停状态
        /// </summary>
        public static bool isPause = false;

        /// <summary>
        /// 企业应用访问公司合法性凭证的过期时间,单位为秒,取值在0~7200之间
        /// </summary>
        private static int expiresIn = 0;

        /// <summary>
        /// 调用时间集合(添加时保持集合大小)
        /// </summary>
        private static Queue<DateTime> askTimes = new Queue<DateTime>();

        /// <summary>
        /// 接口名称_接口地址
        /// </summary>
        private static readonly Dictionary<string, string> APIname_url = new Dictionary<string, string> {
            {  "获取 AppAccessToken"              ,ApiUrlPre + @"cgi/appAccessToken/get" },
            {  "获取 CorpAccessToken"             ,ApiUrlPre + @"cgi/corpAccessToken/get/V2" },
            {  "获取 JsapiTicket"                 ,ApiUrlPre + @"cgi/jsApiTicket/get" },
            {  "通过code换取openUserId"           ,ApiUrlPre + @"oauth2/openUserId/get" },
            {  "openUserId换取unid"               ,ApiUrlPre + @"cgi/unid/get" },
            {  "openUserId换取unid(批量)"       ,ApiUrlPre + @"cgi/unid/batch/get" },
            {  "unid换取openUserId"               ,ApiUrlPre + @"cgi/openId/get" },
            {  "unid换取openUserId(批量)"         ,ApiUrlPre + @"cgi/openId/batch/get" },
            {  "获取部门列表"                     ,ApiUrlPre + @"cgi/department/list" },
            {  "获取部门详情"                     ,ApiUrlPre + @"cgi/department/detail" },
            {  "添加部门"                         ,ApiUrlPre + @"cgi/department/add" },
            {  "修改部门"                         ,ApiUrlPre + @"cgi/department/update" },
            {  "设置部门状态"                     ,ApiUrlPre + @"cgi/department/setStatus" },
            {  "获取部门下成员信息(简略)"          ,ApiUrlPre + @"cgi/user/simpleList" },
            {  "获取部门下成员信息(详细)"        ,ApiUrlPre + @"cgi/user/list" },
            {  "添加员工"                         ,ApiUrlPre + @"cgi/user/add" },
            {  "修改员工信息"                     ,ApiUrlPre + @"cgi/user/update" },
            {  "设置员工状态"                     ,ApiUrlPre + @"cgi/user/setStatus" },
            {  "批量设置员工状态"                 ,ApiUrlPre + @"cgi/user/batchSetStatus" },
            {  "根据Id查询员工信息"               ,ApiUrlPre + @"cgi/user/get" },
            {  "获取停用成员信息"                 ,ApiUrlPre + @"cgi/user/disabled" },
            {  "根据时间段分页查询修改的员工列表"  ,ApiUrlPre + @"cgi/user/get/batchByUpdTime" },
            {  "发送文本消息"                     ,ApiUrlPre + @"cgi/message/send" },
            {  "获取外勤勤数据列表"               ,ApiUrlPre + @"cgi/outsideAttendance/find"  },
            {  "获取考勤数据列表"                 ,ApiUrlPre + @"cgi/attendance/find" },
            {  "获取高级外勤数据描述"             ,ApiUrlPre + @"cgi/seniorOutsideAttendance/describe" },
            {  "获取高级外勤数据列表"             ,ApiUrlPre + @"cgi/seniorOutsideAttendance/find" },
            {  "获取高级外勤数据详情"             ,ApiUrlPre + @"cgi/seniorOutsideAttendance/get" },
            {  "获取外勤类型"                    ,ApiUrlPre + @"cgi/seniorOutsideAttendance/getCheckType" },
            {  "创建外勤计划"                    ,ApiUrlPre + @"cgi/seniorOutsideAttendance/createPlan" },
            {  "获取审批类型列表"                ,ApiUrlPre + @"cgi/approvalTypeForms/get" },
            {  "获取审批列表"                    ,ApiUrlPre + @"cgi/approval/query" },
            {  "获取审批详情"                    ,ApiUrlPre + @"cgi/approval/get" },
            {  "获取企业CRM对象API Name列表"     ,ApiUrlPre + @"cgi/crm/v2/object/list" },
            {  "获取国家省份地市选项代码"         ,ApiUrlPre + @"cgi/crm/countryAreaOptions/get" },
            {  "订单确认已收货"                  ,ApiUrlPre + @"cgi/crm/v2/data/confirmReceive"},
            {  "订单确认已发货"                  ,ApiUrlPre + @"cgi/crm/v2/data/confirmDelivery"},
            {  "商机切换销售流程"                ,ApiUrlPre + @"cgi/crm/data/changeSalesProcess"},
            {  "商机变更销售阶段"                ,ApiUrlPre + @"cgi/crm/data/changeSalesStage"},
            {  "查询销售记录列表"                ,ApiUrlPre + @"cgi/crm/salesRecorder/query" },
            {  "获取销售记录详情"                ,ApiUrlPre + @"cgi/crm/salesRecorder/get" },
            {  "获取销售记录类型"                ,ApiUrlPre + @"cgi/crm/salesRecorderType/query" },
        };

        /// <summary>
        /// 错误码
        /// </summary>
        private static readonly Dictionary<int, string> errorCodeMsgs = new Dictionary<int, string>{
            // 【*】标志为:API文档列表未提及但实际接口会有返回
            { -2 ,"系统错误"},
            { -1 ,"系统繁忙"},
            { 0 ,"请求成功"},
            { 500 ,"未知异常,可尝试检查参数是否都为字符串类型"},
            { 9111 ,"获取参数错误"},
            { 10001 ,"缺少参数appId"},
            { 10002 ,"缺少参数appSecret"},
            { 10003 ,"缺少参数appAccessToken"},
            { 10004 ,"缺少参数redirectUri"},
            { 10005 ,"缺少参数responseType"},
            { 10006 ,"缺少参数scope"},
            { 10007 ,"缺少参数state"},
            { 10008 ,"缺少参数code"},
            { 10009 ,"缺少参数appAccount"},
            { 10010 ,"缺少参数openUserId"},
            { 10012 ,"缺少参数permanentCode"},
            { 10013 ,"缺少参数corpAccessToken"},
            { 10014 ,"缺少参数corpId"},
            { 10015 ,"缺少参数toUser"},
            { 10500 ,"CRM对象不允许执行操作"},
            { 11001 ,"参数appId不合法"},
            { 11002 ,"参数appSecret不合法"},
            { 11003 ,"参数appAccessToken不合法"},
            { 11004 ,"参数redirectUri不合法"},
            { 11005 ,"参数responseType不合法"},
            { 11006 ,"参数scope不合法"},
            { 11007 ,"参数state不合法"},
            { 11008 ,"参数openUserId不合法"},
            { 11009 ,"参数departmentId不合法"},
            { 11010 ,"参数code不合法"},
            { 11011 ,"参数appAccount不合法"},
            { 11013 ,"参数permanentCode不合法"},
            { 11014 ,"参数corpAccessToken不合法"},
            { 11015 ,"参数corpId不合法"},
            { 11016 ,"参数toUser不合法"},
            { 11017 ,"参数msgType不合法"},
            { 11018 ,"参数templateId不合法"},
            { 10104 ,"unid错误"},// *
            { 12002 ,"登录状态错误"},
            { 12003 ,"未支持的消息类型"},
            { 12004 ,"POST的数据包为空"},
            { 12005 ,"文本消息内容为空"},
            { 14001 ,"接口调用超过限制"},
            { 15002 ,"参数不合法"},
            { 15003 ,"APP没有访问权限"},
            { 20005 ,"accessToken不存在或者已经过期"},
            { 20006 ,"appId或appSecret错误"},
            { 20010 ,"CODE不存在或者已经过期"},
            { 20012 ,"openUserId未找到"},
            { 20014 ,"应用没有获取该员工的数据的权限"},
            { 20015 ,"永久授权码错误"},
            { 20016 ,"corpAccessToken不存在或者已经过期"},
            { 20017 ,"corpId未找到"},
            { 20020 ,"应用没有获取该企业的数据的权限"},
            { 20021 ,"在当前企业下,该app的状态为停用"},
            { 20022 ,"企业没有对该app授权"},
            { 20023 ,"APP没有访问department的权限"},
            { 30004 ,"接口调用频率过高"},// *
            { 30007 ,"部门不存在"},
            { 30027 ,"员工不存在"},
            { 32000 ,"参数错误"},
            { 40003 ,"参数非法异常"},// *
            { 40010 ,"templateId不合法"},// *
            { 85999 ,"输入字段错误"},// *
            { 40000420 ,"配置了唯一性规则:字段客户名称的值不能与系统中数据重复"},// *
            { 49999999 ,"给定关键字不在字典中"},// *
            { 210011090 ,"未查询到流程实例"},// *
            { 210011095,"任务不存在"},// *
            { 201112001 ,"已有相同唯一属性与当前值重复"},// *
            { 201112008 ,"状态冲突"},// *
            { 201112011 ,"属性数值类型异常"},// *
            { 201112013 ,"必填字段未填写"},// *
            { 320001401 ,"数据已作废或已删除/不可单独修改主从关系中从对象属性/状态" },// *
            { 320002500 ,"数据存储上不存在目标记录"},// *
        };

        /// <summary>
        /// 企业应用获取到的凭证
        /// </summary>
        public static string appAccessToken = "";

        /// <summary>
        /// 企业应用访问公司合法性凭证
        /// </summary>
        public static string corpAccessToken = "";

        /// <summary>
        /// 开放平台派发的公司帐号
        /// </summary>
        public static string corpId = "";

        /// <summary>
        /// 当前操作的开放平台派发的用户帐号
        /// </summary>
        public static string currentOpenUserId = "";

        /// <summary>
        /// 总等待时间
        /// </summary>
        public static int totalWaitTime = 0;

        /// <summary>
        /// 总调用接口次数
        /// </summary>
        public static int totalAskTimes = 0;

        #region 建立连接
        /// <summary>
        /// 获取 AppAccessToken
        /// AppAccessToken是应用的全局唯一票据,需要AppId和AppSecret来换取,
        /// 不同的AppSecret会返回不同的AppAccessToken。
        /// 正常情况下AppAccessToken有效期为2592000秒(30天),
        /// 有效期内重复获取返回相同结果,最后十分钟内会生成新的AppAccessToken,
        /// 在此窗口期新旧AppAccessToken可以同时使用。
        /// 正常返回:
        /// {
        ///     errorCode	返回码
        ///     errorMessage   对返回码的文本描述内容
        ///     appAccessToken 企业应用获取到的凭证
        ///     expiresIn  企业应用访问公司合法性凭证的过期时间,单位为秒,取值在0 ~7200之间
        /// }
        /// </summary>
        /// <param name="appId">企业应用ID</param>
        /// <param name="appSecret">企业应用凭证密钥</param>
        /// <returns></returns>
        public static JObject GetAppAccessToken(string appId, string appSecret)
        {
            if (string.IsNullOrWhiteSpace(appId))
                throw new ArgumentException("【企业应用ID】不允许为空", nameof(appId));

            if (string.IsNullOrWhiteSpace(appSecret))
                throw new ArgumentException("【企业应用凭证密钥】不允许为空", nameof(appSecret));

            object jsonObj = new { appId, appSecret };
            JObject jObject = HttpPostForJson(jsonObj, "获取 AppAccessToken");
            appAccessToken = jObject["appAccessToken"].ToString().Trim('"');
            return jObject;
        }

        /// <summary>
        /// 获取 CorpAccessToken
        /// CorpAccessToken是企业应用访问相应公司数据的全局唯一票据,
        /// 拉取信息和发送消息的接口都需要携带CorpAccessToken和CorpId。
        /// 正常情况下CorpAccessToken的有效期为7200秒,有效期内重复获取返回相同结果,并自动续期。
        /// 所以为了防止因为频率调用次数超出限制而影响功能正常使用的问题,
        /// 建议开发者将中间生成的 CorpAccessToken  进行缓存,
        /// 过期以后再重新获取。
        /// 同时由于每个应用的 CorpAccessToken 是彼此独立的,
        /// 所以进行缓存时需要区分应用来进行存储。
        /// 正常返回:
        /// {
        ///     errorCode	返回码
        ///     errorMessage   对返回码的文本描述内容
        ///     corpAccessToken 企业应用访问公司合法性凭证
        ///     corpId   开放平台派发的公司帐号
        ///     expiresIn  企业应用访问公司合法性凭证的过期时间,单位为秒,取值在0 ~7200之间
        /// }
        /// </summary>
        /// <param name="appId">企业应用ID</param>
        /// <param name="appSecret">企业应用凭证密钥</param>
        /// <param name="permanentCode">企业应用获得的公司永久授权码</param>
        /// <returns></returns>
        public static JObject GetCorpAccessToken(string appId, string appSecret, string permanentCode)
        {
            if (string.IsNullOrWhiteSpace(appId))
                throw new ArgumentException("【企业应用ID】不允许为空", nameof(appId));

            if (string.IsNullOrWhiteSpace(appSecret))
                throw new ArgumentException("【企业应用凭证密钥】不允许为空", nameof(appSecret));

            if (string.IsNullOrWhiteSpace(permanentCode))
                throw new ArgumentException("【企业应用获得的公司永久授权码】不允许为空", nameof(permanentCode));

            object jsonObj = new { appId, appSecret, permanentCode };
            JObject jObject = HttpPostForJson(jsonObj, "获取 CorpAccessToken");
            corpId = jObject["corpId"].ToString().Trim('"');
            corpAccessToken = jObject["corpAccessToken"].ToString().Trim('"');
            expiresIn = int.Parse(GetValue(jObject, "expiresIn"));
            return jObject;
        }

        /// <summary>
        /// 获取 JsapiTicket
        /// JsapiTicket是纷享应用调用纷享JS API的临时票据,在签名计算中使用。
        /// 正常情况下,JsapiTicket的有效期为7200秒(2小时)。
        /// 正常返回:
        /// {
        ///     errorCode	返回码
        ///     errorMessage   对返回码的文本描述内容
        ///     ticket   临时票据
        ///     expiresIn   ticket有效时间,以秒为单位
        /// }
        /// </summary>
        /// <returns></returns>
        public static JObject GetJsApiTicket()
        {
            if (string.IsNullOrWhiteSpace(corpAccessToken))
                throw new ArgumentException("【企业应用ID】不允许为空", nameof(corpAccessToken));

            if (string.IsNullOrWhiteSpace(corpId))
                throw new ArgumentException("【企业应用凭证密钥】不允许为空", nameof(corpId));

            object jsonObj = new { corpAccessToken, corpId };
            JObject jObject = HttpPostForJson(jsonObj, "获取 JsapiTicket");
            //jsApiTicket = jObject["ticket"].ToString().Trim('"');
            return jObject;
        }

        #endregion

        #region 身份验证
        /// <summary>
        /// 通过code换取openUserId
        /// 正常返回:
        /// {
        ///     errorCode	返回码
        ///     errorMessage   对返回码的文本描述内容
        ///     openUserId 开放平台派发的用户帐号
        ///     corpId  开放平台派发的公司帐号
        /// }
        /// </summary>
        /// <param name="code">员工身份临时票据,有效期为十分钟,有效期内使用一次后则会过期</param>
        /// <returns></returns>
        public static JObject GetOpenUserIdByCode(string code)
        {
            if (string.IsNullOrWhiteSpace(code))
                throw new ArgumentException("【员工身份临时票据】不允许为空", nameof(code));

            if (string.IsNullOrWhiteSpace(appAccessToken))
                throw new ArgumentException("【企业应用获取到的凭证】不允许为空", nameof(appAccessToken));

            object jsonObj = new { code, appAccessToken };
            return HttpPostForJson(jsonObj, "通过code换取openUserId");
        }

        /// <summary>
        /// openUserId换取unid
        /// unid是一个用户在纷享系统中存在的唯一id, 不同的应用换取同一个用户的unid都是一致的。 
        /// 正常返回:
        /// {
        ///     errorCode	返回码
        ///     errorMessage   对返回码的文本描述内容
        ///     unid  开放平台派发的用户帐号
        /// }
        /// </summary>
        /// <param name="openUserId">开放平台派发的用户帐号</param>
        /// <returns></returns>
        public static JObject GetUnidByOpenUserId(string openUserId)
        {
            if (string.IsNullOrWhiteSpace(openUserId))
                throw new ArgumentException("【用户开平账号】不允许为空", nameof(openUserId));

            VerifyCorpMsgNotNull();
            object jsonObj = new { corpAccessToken, corpId, openUserId };// API文档上的表格是错误的
            return HttpPostForJson(jsonObj, "openUserId换取unid");
        }

        /// <summary>
        /// openUserId换取unid(批量)
        /// unid是一个用户在纷享系统中存在的唯一id, 不同的应用换取同一个用户的unid都是一致的。 
        /// 正常返回:
        /// {
        ///     "failList": [ ],                        换取失败的openUserId列表
        ///     "unidList": [                           换取成功的openUserId->unid对应列表
        ///         {
        ///             "openUserId": "FSUID_XXX", 
        ///             "unid": "FSUNID_XXX"
        ///         }
        ///     ], 
        ///     errorCode	                            返回码
        ///     errorMessage                            对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="openUserIds">用户开平账号列表(String类型集合)</param>
        /// <returns></returns>
        public static JObject GetUnidsByOpenUserIds(List<string> openUserIds)
        {
            VerifyCorpMsgNotNull();
            VerifyArray_ElementNotNull("员工ID集合列表", nameof(openUserIds), openUserIds);
            object jsonObj = new { corpAccessToken, corpId, openUserIds };// API文档上的表格是错误的
            return HttpPostForJson(jsonObj, "openUserId换取unid(批量)");
        }

        /// <summary>
        /// unid换取openUserId
        /// 正常返回:
        /// {
        ///     errorCode	返回码
        ///     errorMessage   对返回码的文本描述内容
        ///     openUserId  用户开平账号
        /// }
        /// </summary>
        /// <param name="unid">用户唯一id</param>
        /// <returns></returns>
        public static JObject GetOpenUserIdByUnid(string unid)
        {
            if (string.IsNullOrWhiteSpace(unid))
                throw new ArgumentException("【用户唯一id】不允许为空", nameof(unid));

            VerifyCorpMsgNotNull();
            object jsonObj = new { corpAccessToken, corpId, unid };// API文档上的表格是错误的
            return HttpPostForJson(jsonObj, "unid换取openUserId");
        }

        /// <summary>
        /// unid换取openUserId(批量)
        /// 正常返回:
        /// {
        ///     "failList": [ ],                        换取失败的unid列表
        ///     "openIdList": [                         换取成功的unid->openUserId对应列表
        ///         {
        ///             "openUserId": "FSUID_XXX", 
        ///             "unid": "FSUNID_XXX"
        ///         }
        ///     ], 
        ///     errorCode	                            返回码
        ///     errorMessage                            对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="unids">用户唯一id列表(String 类型集合)</param>
        /// <returns></returns>
        public static JObject GetOpenUserIdsByUnids(List<string> unids)
        {
            VerifyArray_ElementNotNull("员工ID集合列表", nameof(unids), unids);
            VerifyCorpMsgNotNull();
            object jsonObj = new { corpAccessToken, corpId, unids };
            return HttpPostForJson(jsonObj, "unid换取openUserId(批量)");
        }

        #endregion

        #region 通讯录管理:通过通信录管理接口,你可以实现企业HR系统做与纷享开放平台之间的部门和人员信息同步。

        /// <summary>
        /// 获取部门列表
        /// 正常返回:
        /// {
        ///   "errorCode": 0,               返回码
        ///   "errorMessage": "success",    对返回码的文本描述内容
        ///   "departments": [              (二级对象)
        ///       {
        ///          "id": 11,             部门ID
        ///          "name": "事业部",     部门名称
        ///          "parentId": 1,         父部门ID,根部门ID为0,其它部门Id为非负整数
        ///          "isStop":false,        是否停用(true表示停用,false表示正常)
        ///          "order": 1             部门排序,序号越小,排序越靠前。最小值为1
        ///       },
        ///        ...
        ///     ]
        /// }
        /// </summary>
        /// <returns></returns>
        public static JObject QueryDepartmentList()
        {
            VerifyCorpMsgNotNull();
            object jsonObj = new { corpAccessToken, corpId };
            JObject jObject = HttpPostForJson(jsonObj, "获取部门列表");
            return jObject;
        }

        /// <summary>
        /// 获取部门详情
        /// 正常返回:
        ///{
        ///    "department": {
        ///        "enterpriseId": 2,                                           企业id
        ///        "departmentId": 1077,                                        部门id
        ///        "parentDepartmentId": 999999,                                父部门id
        ///        "name": "技术中心",                                          	部门名称部门名称
        ///        "nameSpell": "JISHUZHONGXIN",                                客户端拼音
        ///        "nameOrder": "J#BCBC#CAF5#D6D0#D0C4",                        客户端排序
        ///        "departmentOrder": 20,                                       部门排序
        ///        "isStop": false,                                             是否停用
        ///        "stopTime": 1524035224500,                                   停用时间(时间戳)
        ///        "description": "",                                           描述
        ///        "keywords": [],                                              搜索关键字
        ///        "principalId": "FSUID_862B8A2AA77CDFDB8017D07B494612FA",     部门负责人
        ///        "createTime": 946656000000,                                  创建时间(时间戳)
        ///        "updateTime": 1524035224500,                                 更新时间(时间戳)
        ///        "ancestors": [                                               所有父级部门(id)
        ///            999999
        ///        ],
        ///        "status": 1,                                                 部门状态1正常2停用3删除
        ///        "hideSuperWorkInfo": false                                   是否屏蔽上级工作信息
        ///    },
        ///    "errorCode": 0,
        ///    "errorMessage": "success"
        ///}
        /// </summary>
        /// <param name="departmentId">部门ID(ps.测试号无权限查)</param>
        /// <returns></returns>
        public static JObject QueryDepartmentDetail(int departmentId)
        {
            VerifyCorpMsgNotNull();
            if (departmentId < 0)
                throw new ArgumentException("【部门ID】只允许非负整数", nameof(departmentId));

            object jsonObj = new { corpAccessToken, corpId, departmentId };
            return HttpPostForJson(jsonObj, "获取部门详情");
        }

        /// <summary>
        /// 添加部门
        /// 正常返回:
        ///{
        ///    "errorCode":0,
        ///    "errorMessage":"success",
        ///    "departmentId":123,          增加的部门ID
        ///    "order":11                   增加的部门排序号
        ///}
        /// </summary>
        /// <param name="name">部门名称(不能重复,只支持字母,数字,汉字,短横线 -, 下划线 _ )</param>
        /// <param name="parentId">父部门ID(顶级部门为 0)</param>
        /// <param name="principalOpenUserId">部门负责人开放平台员工账号</param>
        /// <returns></returns>
        public static JObject AddDepartment(string name, int parentId, string principalOpenUserId = null)
        {
            VerifyCorpMsgNotNull();

            if (parentId < 0)
                throw new ArgumentException("【部门ID】只允许非负整数", nameof(parentId));

            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentException("【部门名称(不能重复,只支持字母,数字,汉字,短横线 -, 下划线 _ )】不允许为空", nameof(name));

            object jsonObj = new { corpAccessToken, corpId, department = new { name, parentId, principalOpenUserId } };
            return HttpPostForJson(jsonObj, "添加部门");
        }

        /// <summary>
        /// 修改部门
        /// 正常返回:
        ///{
        ///    "errorCode":0,
        ///    "errorMessage":"success",
        ///}
        /// </summary>
        /// <param name="id">部门ID(顶级部门为 0)</param>
        /// <param name="name">部门名称(不能重复,只支持字母,数字,汉字,短横线 -, 下划线 _ ) nul表示不修改</param>
        /// <param name="parentId">父部门ID(顶级部门为 0) nul表示不修改</param>
        /// <param name="order">部门排序号 nul表示不修改</param>
        /// <param name="principalOpenUserId">部门负责人开放平台员工账号 nul表示不修改</param>
        /// <returns></returns>
        public static JObject UpdateDepartment(int id, string name, int parentId, int order, string principalOpenUserId = null)
        {
            VerifyCorpMsgNotNull();
            if (parentId < 0)
                throw new ArgumentException("【部门ID】只允许非负整数", nameof(parentId));

            object jsonObj = new { corpAccessToken, corpId, department = new { id, name, parentId, order, principalOpenUserId } };
            return HttpPostForJson(jsonObj, "修改部门");
        }

        /// <summary>
        /// 设置部门状态(启用停用)
        /// 正常返回:
        ///{
        ///    "errorCode":0,
        ///    "errorMessage":"success",
        ///}
        /// </summary>
        /// <param name="departmentId">部门ID(顶级部门为 0)</param>
        /// <param name="status">状态(1:禁用,2:启用)</param>
        /// <returns></returns>
        public static JObject SetDepartmentStatus(int departmentId, int status)
        {
            VerifyCorpMsgNotNull();

            if (departmentId < 0)
                throw new ArgumentException("【部门ID】只允许非负整数", nameof(departmentId));

            if (status != 1 && status != 2)
                throw new ArgumentException("【状态】(1:禁用,2:启用)不允许为【" + status + "】", nameof(status));

            object jsonObj = new { corpAccessToken, corpId, departmentId, status };
            return HttpPostForJson(jsonObj, "设置部门状态");
        }

        /// <summary>
        /// 获取部门下成员信息(简略)
        /// 正常返回:
        ///{
        ///    "userList": [(网页API文档的拼写是错的)
        ///        {
        ///            "openUserId": "FSUID_XXXXXXXXXXXXXXXXXXXXXXXXXXX",   开放平台员工帐号
        ///            "name": "Andson",                                    员工姓名
        ///            "nickName": "Andson"                                 员工昵称
        ///        },
        ///        {
        ///            "openUserId": "FSUID_XXXXXXXXXXXXXXXXXXXXXXXXXXXX",  
        ///            "name": "Andy",
        ///            "nickName": "Andy"
        ///        },
        ///        ...
        ///    ],
        ///    "errorCode": 0,
        ///    "errorMessage": "success"
        ///}
        /// </summary>
        /// <param name="departmentId">部门ID, 为非负整数</param>
        /// <param name="fetchChild">如果为true,则同时获取其所有子部门员工; 如果为false或者不传,则只获取当前部门员工</param>
        /// <returns></returns>
        public static JObject QueryDepartmentMemberSimpleList(int departmentId, Boolean fetchChild = false)
        {
            VerifyCorpMsgNotNull();

            if (departmentId < 0)
                throw new ArgumentException("【部门ID】只允许非负整数", nameof(departmentId));

            object jsonObj = new { corpAccessToken, corpId, departmentId, fetchChild };
            return HttpPostForJson(jsonObj, "获取部门下成员信息(简略)");
        }

        /// <summary>
        /// 获取部门下成员信息
        /// 正常返回:
        ///{
        ///    "userList": [(网页API上是拼写错误的)
        ///        {
        ///            "openUserId": "FSUID_XXXXXXXXXXXXXXXXXX",                                开放平台员工帐号
        ///            "account": "13252412555",                                                员工账号
        ///            "name": "Andson",                                                        员工姓名
        ///            "nickName": "Andy",                                                      员工昵称
        ///            "isStop": false,                                                         员工状态,如果为true,则表示此员工离职,否则,该员工状态为在职
        ///            "email": "test@test.com",                                                邮箱
        ///            "mobile": "13252412555",                                                 手机号
        ///            "gender": "M",                                                           员工性别:M(男) F(女)
        ///            "position": "PM",                                                        员工职位
        ///            "profileImageUrl": "201502_09_99aa47b5-1f3d-4473-8c02-3d69a6769e57",     头像文件ID
        ///            "departmentIds": [                                                       员工所属部门及其父部门ID列表
        ///                18,
        ///                19
        ///            ],      
        ///            "qq": "13252412555",                                                     QQ(网页API未提供)
        ///            "weixin": "13252412555",                                                 微信(网页API未提供)
        ///            "mainDepartmentId":""                                                    员工主属部门ID
        ///            "attachingDepartmentIds":""                                              员工附属部门ID列表
        ///            "employeeNumber": "168XXX002",                                           员工编号
        ///            "hireDate": "2015/08/31",                                                入职日期
        ///            "birthDate": "1990/09/25",                                               员工生日
        ///            "startWorkDate": "2010-08-31",                                           参加工作日期
        ///            "createTime": 1504547315500,                                             创建时间
        ///            "leaderId": "FSUID_XXXXXXXXXXXXXXXXXX"                                   汇报对象
        ///        },
        ///        ...
        ///    ],
        ///    "errorCode": 0,
        ///    "errorMessage": "success"
        ///}
        /// </summary>
        /// <param name="departmentId">部门ID, 为非负整数</param>
        /// <param name="fetchChild">如果为true,则同时获取其所有子部门员工; 如果为false或者不传,则只获取当前部门员工</param>
        /// <param name="showDepartmentIdsDetail">如果为true,则会返回员工主属部门(mainDepartmentId)与附属部门(attachingDepartmentIds)</param>
        /// <returns></returns>
        public static JObject QueryDepartmentMemberList(int departmentId, Boolean fetchChild = false, Boolean showDepartmentIdsDetail = false)
        {
            VerifyCorpMsgNotNull();

            if (departmentId < 0)
                throw new ArgumentException("【部门ID】只允许非负整数", nameof(departmentId));

            object jsonObj = new { corpAccessToken, corpId, departmentId, fetchChild, showDepartmentIdsDetail };
            return HttpPostForJson(jsonObj, "获取部门下成员信息(详细)");
        }

        /// <summary>
        /// 添加员工
        /// 正常返回:
        ///{
        ///    "errorCode":0,
        ///    "errorMessage":"success",
        ///    "openUserId":"fsuid_23423423424fasd"
        ///}
        /// </summary>
        /// <param name="account">员工个人账号(不能重复),与手机号选填一个</param>
        /// <param name="password">员工初始化密码,填写个人账号时,必填密码</param>
        /// <param name="mobile">手机号码(不能重复),与个人账号选填一个</param>
        /// <param name="name">员工昵称(不能重复)</param>
        /// <param name="fullName">员工姓名</param>
        /// <param name="position">职位</param>
        /// <param name="gender">性别 可取{M,F}</param>
        /// <param name="email">邮箱地址</param>
        /// <param name="qq">qq号</param>
        /// <param name="weixin">微信号</param>
        /// <param name="mainDepartmentId">主属部门Id</param>
        /// <param name="attachingDepartmentIds">附属部门列表(int集合必须为非负数整数,最大值为:500)</param>
        /// <param name="employeeNumber">员工编号</param>
        /// <param name="hireDate">入职日期</param>
        /// <param name="birthDate">员工生日</param>
        /// <param name="startWorkDate">参加工作日期</param>
        /// <param name="leaderId">汇报对象</param>
        /// <returns></returns>
        public static JObject AddUser(string account, string password, string mobile, string name, string fullName, string position, string gender, string email, string qq, string weixin, int mainDepartmentId, List<int> attachingDepartmentIds, string employeeNumber, string hireDate, string birthDate, string startWorkDate, string leaderId)
        {
            VerifyCorpMsgNotNull();
            if (string.IsNullOrWhiteSpace(account) && string.IsNullOrWhiteSpace(mobile))
                throw new ArgumentException("【员工个人账号】与【手机号】必须选填一个或全部填写", nameof(account) + " & " + nameof(mobile));
            else if (!string.IsNullOrWhiteSpace(account) && string.IsNullOrWhiteSpace(password))
                throw new ArgumentException("填写【个人账号】时,必填【密码】", nameof(password));
            else if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentException("【员工昵称】不允许为空", nameof(name));
            else if (string.IsNullOrWhiteSpace(fullName))
                throw new ArgumentException("【员工姓名】不允许为空", nameof(fullName));
            else if (string.IsNullOrWhiteSpace(gender) || (!gender.Equals("M", StringComparison.CurrentCultureIgnoreCase) && !gender.Equals("F", StringComparison.CurrentCultureIgnoreCase)))
                throw new ArgumentException("【员工性别】不允许为空且只能为【M】/【F】", nameof(gender));
            else if (mainDepartmentId < 0)
                throw new ArgumentException("【部门ID】只允许非负整数", nameof(mainDepartmentId));
            else if (attachingDepartmentIds != null)
            {
                // 是否存在不合法元素
                Boolean isIllegal = attachingDepartmentIds.Any((attachingDepartmentId) =>
                {
                    return attachingDepartmentId > 500 || attachingDepartmentId < 0;
                });
                if (isIllegal)
                    throw new ArgumentException("【附属部门】列表元素必须为非负整数且最大值为500", nameof(attachingDepartmentIds));
            }

            object jsonObj = new
            {
                corpAccessToken,
                corpId,
                user = new
                {
                    account,
                    password,
                    mobile,
                    name,
                    fullName,
                    position,
                    gender,
                    email,
                    qq,
                    weixin,
                    mainDepartmentId,
                    attachingDepartmentIds,
                    employeeNumber,
                    hireDate,
                    birthDate,
                    startWorkDate,
                    leaderId
                }
            };
            return HttpPostForJson(jsonObj, "添加员工");
        }

        /// <summary>
        /// 修改员工(统一都重新传信息,按照API规定不传代表不修改)
        /// 正常返回:
        ///{
        ///    "errorCode":0,
        ///    "errorMessage":"success",
        ///}
        /// </summary>
        /// <param name="openUserId">员工ID</param>
        /// <param name="account">员工账号(不能重复),与手机号选填一个</param>
        /// <param name="name">员工昵称(不能重复)</param>
        /// <param name="fullName">员工姓名</param>
        /// <param name="position">职位</param>
        /// <param name="gender">性别 可取{M,F}</param>
        /// <param name="mobile">手机号码(不能重复),与个人账号选填一个</param>
        /// <param name="email">邮箱地址</param>
        /// <param name="qq">qq号</param>
        /// <param name="weixin">微信号</param>
        /// <param name="mainDepartmentId">主属部门Id</param>
        /// <param name="attachingDepartmentIds">附属部门列表(int集合必须为非负数整数,最大值为:500)</param>
        /// <param name="employeeNumber">员工编号</param>
        /// <param name="hireDate">入职日期</param>
        /// <param name="birthDate">员工生日</param>
        /// <param name="startWorkDate">参加工作日期</param>
        /// <param name="leaderId">汇报对象</param>
        /// <returns></returns>
        public static JObject UpdateUser(string openUserId, string account, string name, string fullName, string position, string gender, string mobile, string email, string qq, string weixin, int mainDepartmentId, List<int> attachingDepartmentIds, string employeeNumber, string hireDate, string birthDate, string startWorkDate, string leaderId)
        {
            VerifyCorpMsgNotNull();

            if (string.IsNullOrWhiteSpace(account) && string.IsNullOrWhiteSpace(mobile))
                throw new ArgumentException("【员工个人账号】与【手机号】必须选填一个或全部填写", nameof(account) + " & " + nameof(mobile));
            else if (string.IsNullOrWhiteSpace(email))
                throw new ArgumentException("【邮箱】不允许为空", nameof(email));
            else if (string.IsNullOrWhiteSpace(openUserId))
                throw new ArgumentException("【员工ID】不允许为空", nameof(openUserId));
            else if (string.IsNullOrWhiteSpace(fullName))
                throw new ArgumentException("【员工姓名】不允许为空", nameof(fullName));
            else if (string.IsNullOrWhiteSpace(gender) || (!gender.Equals("M", StringComparison.CurrentCultureIgnoreCase) && !gender.Equals("F", StringComparison.CurrentCultureIgnoreCase)))
                throw new ArgumentException("【员工性别】不允许为空且只能为【M】/【F】", nameof(gender));
            else if (mainDepartmentId < 0)
                throw new ArgumentException("【部门ID】只允许非负整数", nameof(mainDepartmentId));
            else if (string.IsNullOrWhiteSpace(hireDate))
                throw new ArgumentException("【入职日期】不允许为空", nameof(hireDate));
            else if (attachingDepartmentIds != null)
            {
                // 是否存在不合法元素
                Boolean isIllegal = attachingDepartmentIds.Any((attachingDepartmentId) =>
                {
                    return attachingDepartmentId > 500 || attachingDepartmentId < 0;
                });
                if (isIllegal)
                    throw new ArgumentException("【附属部门】列表元素必须为非负整数且最大值为500", nameof(attachingDepartmentIds));
            }

            object jsonObj = new { corpAccessToken, corpId, user = new { account, openUserId, mobile, name, fullName, position, gender, email, qq, weixin, mainDepartmentId, attachingDepartmentIds, employeeNumber, hireDate, birthDate, startWorkDate, leaderId } };
            return HttpPostForJson(jsonObj, "修改员工信息");
        }

        /// <summary>
        /// 设置员工(成员)状态(1:禁用,2:启用)
        /// 正常返回:
        ///{
        ///    "errorCode":0,
        ///    "errorMessage":"success",
        ///}
        /// </summary>
        /// <param name="openUserId">员工ID</param>
        /// <param name="status">状态(1:禁用,2:启用)</param>
        /// <returns></returns>
        public static JObject SetUserStatus(string openUserId, int status)
        {
            VerifyCorpMsgNotNull();

            if (string.IsNullOrWhiteSpace(openUserId))
                throw new ArgumentException("【员工ID】不允许为空", nameof(openUserId));
            else if (status != 1 && status != 2)
                throw new ArgumentException("【状态】(1:禁用,2:启用)不允许为【" + status + "】", nameof(status));

            object jsonObj = new { corpAccessToken, corpId, openUserId, status };
            return HttpPostForJson(jsonObj, "设置员工状态");
        }

        /// <summary>
        /// 批量设置员工(成员)状态(启用停用)
        /// 正常返回:
        ///{
        ///    "errorCode":0,
        ///    "errorMessage":"success",
        ///}
        /// </summary>
        /// <param name="openUserIds">员工ID集合列表</param>
        /// <param name="status">状态(1:禁用,2:启用)</param>
        /// <returns></returns>
        public static JObject SetUserStatus(List<string> openUserIds, int status)
        {
            VerifyCorpMsgNotNull();
            VerifyArray_ElementNotNull("员工ID集合列表", nameof(openUserIds), openUserIds);
            if (status != 1 && status != 2)
                throw new ArgumentException("【状态】(1:禁用,2:启用)不允许为【" + status + "】", nameof(status));

            object jsonObj = new { corpAccessToken, corpId, openUserIds, status };
            return HttpPostForJson(jsonObj, "批量设置员工状态");
        }

        /// <summary>
        /// 根据Id查询员工信息
        /// 正常返回:
        ///        {
        ///            "errorCode": 0,
        ///            "errorMessage": "success"
        ///            "openUserId": "FSUID_XXXXXXXXXXXXXXXXXX",                                开放平台员工帐号
        ///            "account": "13252412555",                                                员工账号
        ///            "name": "Andson",                                                        员工姓名
        ///            "nickName": "Andy",                                                      员工昵称
        ///            "isStop": false,                                                         员工状态,如果为true,则表示此员工离职,否则,该员工状态为在职
        ///            "mobile": "13252412555",                                                 手机号
        ///            "gender": "M",                                                           员工性别:M(男) F(女)
        ///            "position": "PM",                                                        员工职位
        ///            "profileImageUrl": "201502_09_99aa47b5-1f3d-4473-8c02-3d69a6769e57",     头像文件ID
        ///            "departmentIds": [                                                       员工所属部门及其父部门ID列表
        ///                18,
        ///                19
        ///            ],      
        ///            "qq": "13252412555",                                                     QQ(网页API未提供)
        ///            "weixin": "13252412555",                                                 微信(网页API未提供)
        ///            "employeeNumber": "168XXX002",                                           员工编号
        ///            "hireDate": "2015/08/31",                                                入职日期
        ///            "birthDate": "1990/09/25",                                               员工生日
        ///            "startWorkDate": "2010-08-31",                                           参加工作日期
        ///            "createTime": 1504547315500,                                             创建时间
        ///            "leaderId": "FSUID_XXXXXXXXXXXXXXXXXX"                                   汇报对象
        ///            "mainDepartmentId":""                                                    员工主属部门ID
        ///            "attachingDepartmentIds":""                                              员工附属部门ID列表
        ///        },
        /// </summary>
        /// <param name="openUserId">员工ID</param>
        /// <param name="showDepartmentIdsDetail">是否返回员工主属部门(mainDepartmentId)与附属部门(attachingDepartmentIds)</param>
        /// <returns></returns>
        public static JObject QueryUserMsgById(String openUserId, Boolean showDepartmentIdsDetail = false)
        {
            VerifyCorpMsgNotNull();
            if (string.IsNullOrWhiteSpace(openUserId))
                throw new ArgumentException("【员工ID】不允许为空", nameof(openUserId));

            object jsonObj = new { corpAccessToken, corpId, openUserId, showDepartmentIdsDetail };
            return HttpPostForJson(jsonObj, "根据Id查询员工信息");
        }

        /// <summary>
        /// 获取停用成员(员工)信息
        /// 正常返回:
        ///{
        ///    "userList": [
        ///        {
        ///            "openUserId": "FSUID_XXXXXXXXXXXXXXXXXX",                                开放平台员工帐号
        ///            "account": "13252412555",                                                员工账号
        ///            "name": "Andson",                                                        员工姓名
        ///            "nickName": "Andy",                                                      员工昵称
        ///            "isStop": false,                                                         员工状态,如果为true,则表示此员工离职,否则,该员工状态为在职
        ///            "email": "test@test.com",                                                邮箱
        ///            "mobile": "13252412555",                                                 手机号
        ///            "gender": "M",                                                           员工性别:M(男) F(女)
        ///            "position": "PM",                                                        员工职位
        ///            "profileImageUrl": "201502_09_99aa47b5-1f3d-4473-8c02-3d69a6769e57",     头像文件ID
        ///            "departmentIds": [                                                       员工所属部门及其父部门ID列表
        ///                18,
        ///                19
        ///            ],      
        ///            "qq": "13252412555",                                                     QQ(网页API未提供)
        ///            "weixin": "13252412555",                                                 微信(网页API未提供)
        ///            "mainDepartmentId":""                                                    员工主属部门ID
        ///            "attachingDepartmentIds":""                                              员工附属部门ID列表
        ///            "employeeNumber": "168XXX002",                                           员工编号
        ///            "hireDate": "2015/08/31",                                                入职日期
        ///            "birthDate": "1990/09/25",                                               员工生日
        ///            "startWorkDate": "2010-08-31",                                           参加工作日期
        ///            "createTime": 1504547315500,                                             创建时间
        ///            "leaderId": "FSUID_XXXXXXXXXXXXXXXXXX"                                   汇报对象
        ///        },
        ///        ...
        ///    ],
        ///    "errorCode": 0,
        ///    "errorMessage": "success"
        ///}
        /// </summary>
        /// <param name="openUserId">员工ID</param>
        /// <param name="showDepartmentIdsDetail">是否返回员工主属部门(mainDepartmentId)与附属部门(attachingDepartmentIds)</param>
        /// <returns></returns>
        public static JObject QueryDisabledUserMsg(Boolean showDepartmentIdsDetail = false)
        {
            VerifyCorpMsgNotNull();
            object jsonObj = new { corpAccessToken, corpId, showDepartmentIdsDetail };
            return HttpPostForJson(jsonObj, "获取停用成员信息");
        }

        /// <summary>
        /// 增量查询员工列表(根据时间段分页查询修改的员工列表)
        /// 正常返回:
        ///{
        ///    "employees": [
        ///        {
        ///            "openUserId": "FSUID_XXXXXXXXXXXXXXXXXX",                                开放平台员工帐号
        ///            "account": "13252412555",                                                员工账号
        ///            "name": "Andson",                                                        员工姓名
        ///            "nickName": "Andy",                                                      员工昵称
        ///            "isStop": false,                                                         员工状态,如果为true,则表示此员工离职,否则,该员工状态为在职
        ///            "email": "test@test.com",                                                邮箱
        ///            "mobile": "13252412555",                                                 手机号
        ///            "gender": "M",                                                           员工性别:M(男) F(女)
        ///            "position": "PM",                                                        员工职位
        ///            "profileImageUrl": "201502_09_99aa47b5-1f3d-4473-8c02-3d69a6769e57",     头像文件ID
        ///            "departmentIds": [                                                       员工所属部门及其父部门ID列表
        ///                18,
        ///                19
        ///            ],      
        ///            "qq": "13252412555",                                                     QQ(网页API未提供)
        ///            "weixin": "13252412555",                                                 微信(网页API未提供)
        ///            "mainDepartmentId":""                                                    员工主属部门ID
        ///            "attachingDepartmentIds":""                                              员工附属部门ID列表
        ///            "employeeNumber": "168XXX002",                                           员工编号
        ///            "hireDate": "2015/08/31",                                                入职日期
        ///            "birthDate": "1990/09/25",                                               员工生日
        ///            "startWorkDate": "2010-08-31",                                           参加工作日期
        ///            "createTime": 1504547315500,                                             创建时间
        ///        },
        ///        ...
        ///    ],
        ///    "pageNumber": 1,
        ///    "pageCount": 20,
        ///    "totalCount": 2,
        ///    "lastChangedTime": 1462776430797,
        ///    "errorCode": 0,
        ///    "errorMessage": "success"
        ///    "errorDescription": "成功"
        ///}
        /// </summary>
        /// <param name="startTime">开始时间戳,选填,可为空,为空或为0时不作为条件 ,以毫秒为单位</param>
        /// <param name="endTime">结束时间戳,选填,可为空,为空或为0时不作为条件 ,以毫秒为单位(endTime必须大于startTime)</param>
        /// <param name="pageNumber">页码,选填,可为空,为空时默认1,必须为大于0整数</param>
        /// <param name="pageSize">每页个数,选填,可为空,为空时默认20,最大为1000,必须为大于0整数</param>
        /// <param name="showDepartmentIdsDetail">如果为true,则会返回员工主属部门(mainDepartmentId)与附属部门(attachingDepartmentIds)</param>
        /// <returns></returns>
        public static JObject QueryBatchUserMsgByUpdTime(long startTime = 0, long endTime = 0, int pageNumber = 1, int pageSize = 20, Boolean showDepartmentIdsDetail = false)
        {
            VerifyCorpMsgNotNull();

            if (pageSize <= 0 || pageSize > 1000)
                throw new ArgumentException("【每页个数】(" + pageSize + ")选填,可为空,为空时默认20,最大为1000,必须为大于0整数", nameof(pageSize));
            else if (startTime < 0)
                throw new ArgumentException("【开始时间戳】(" + startTime + ")选填,可为空,为空或为0时不作为条件 ,以毫秒为单位", nameof(startTime));
            else if (startTime > 0 && endTime < startTime)
                throw new ArgumentException("【结束时间戳】(" + endTime + ")必须大于【开始时间戳】(" + startTime + ")", nameof(endTime));
            else if (pageNumber <= 0)
                throw new ArgumentException("【页码】(" + pageNumber + ")页码,选填,可为空,为空时默认1,必须为大于0整数", nameof(pageNumber));

            object jsonObj = new { corpAccessToken, corpId, startTime, endTime, pageNumber, pageSize, showDepartmentIdsDetail };
            return HttpPostForJson(jsonObj, "根据时间段分页查询修改的员工列表");
        }

        #endregion

        #region 发送消息
        /// <summary>
        /// 发送文本消息
        /// 正常返回:
        /// {
        ///     "errorCode":0
        ///     "errorMessage":success
        ///     "errorDescription": "成功"
        /// }
        /// </summary>
        /// <param name="toUser">开放平台员工ID列表(消息接收者,目前最多支持500人)</param>
        /// <param name="content">文本消息内容</param>
        /// <returns></returns>
        public static JObject SendMessage(List<string> toUser, string content)
        {
            VerifyCorpMsgNotNull();
            VerifyArray_ElementNotNull("员工ID集合列表", nameof(toUser), toUser);
            if (toUser.Count > 500)
                throw new ArgumentException("【员工ID集合列表】消息接收者,目前最多支持500人!", nameof(toUser));
            else if (content.Length > 2000)// 自定义设定
                content = content.Substring(0, 2000 - 3) + "...";

            object jsonObj = new { corpAccessToken, corpId, toUser, msgType = "text", text = new { content } };
            return HttpPostForJson(jsonObj, "发送文本消息");
        }
        
        /// <summary>
        /// 发送文本消息
        /// 正常返回:
        /// {
        ///     "errorCode":0
        ///     "errorMessage":success
        ///     "errorDescription": "成功"
        /// }
        /// </summary>
        /// <param name="toUser">开放平台员工ID</param>
        /// <param name="content">文本消息内容</param>
        /// <returns></returns>
        public static JObject SendMessage(string toUser, string content)
        {
            return SendMessage(new List<string> { toUser }, content);
        }

        /// <summary>
        /// 发送含链接文本消息
        /// 正常返回:
        /// {
        ///     "errorCode":0
        ///     "errorMessage":success
        ///     "errorDescription": "成功"
        /// }
        /// </summary>
        /// <param name="toUser">开放平台员工ID列表(消息接收者,目前最多支持500人)</param>
        /// <param name="content">内容</param>
        /// <param name="title">标题</param>
        /// <param name="linkAPIName">链接对象API</param>
        /// <param name="linkApiObjId">链接对象id</param>
        /// <param name="linkString">点击消息时跳转链接标题</param>
        /// <param name="first">内容标题</param>
        /// <param name="form">内容列表</param>
        /// <returns></returns>
        public static JObject SendLinkMessage(List<string> toUser, string content, string title, FxObjApiName linkAPIName, string linkApiObjId, string linkString, string first, List<Fx_Form> form)
        {
            VerifyCorpMsgNotNull();
            VerifyArray_ElementNotNull("员工ID集合列表", nameof(toUser), toUser);
            if (toUser.Count > 500)
                throw new ArgumentException("【员工ID集合列表】消息接收者,目前最多支持500人!", nameof(toUser));
            else if (content.Length > 2000)// 自定义设定
                content = content.Substring(0, 2000 - 3) + "...";

            object jsonObj = new
            {
                corpAccessToken,
                corpId,
                toUser,
                msgType = "composite",
                composite = new
                {
                    head = new
                    {
                        title
                    },
                    first,
                    form,
                    remark = new
                    {
                        content
                    },
                    link = new
                    {
                        title = linkString,
                        url = "fs://CRM/udobj?{\"objDescApiName\":\"" + linkAPIName + "\",\"objDataId\":\"" + linkApiObjId + "\"}",// fs://CRM/udobj?{\"objDescApiName\":\"AccountObj\",\"objDataId\":\"5ff2780b7132bb0001bade16\"}
                    },
                }
            };
            return HttpPostForJson(jsonObj, "发送文本消息");
        }

        /// <summary>
        /// 发送含链接文本消息
        /// 正常返回:
        /// {
        ///     "errorCode":0
        ///     "errorMessage":success
        ///     "errorDescription": "成功"
        /// }
        /// </summary>
        /// <param name="toUser">开放平台员工ID列表(消息接收者,目前最多支持500人)</param>
        /// <param name="content">内容</param>
        /// <param name="title">标题</param>
        /// <param name="linkAPIName">链接对象API</param>
        /// <param name="linkApiObjId">链接对象id</param>
        /// <param name="linkString">点击消息时跳转链接标题</param>
        /// <returns></returns>
        public static JObject SendLinkMessage(List<string> toUser, string content, string title, FxObjApiName linkAPIName, string linkApiObjId, string linkString = "*跳转链接*")
        {
            VerifyCorpMsgNotNull();
            VerifyArray_ElementNotNull("员工ID集合列表", nameof(toUser), toUser);
            if (toUser.Count > 500)
                throw new ArgumentException("【员工ID集合列表】消息接收者,目前最多支持500人!", nameof(toUser));
            else if (content.Length > 2000)// 自定义设定
                content = content.Substring(0, 2000 - 3) + "...";

            object jsonObj = new
            {
                corpAccessToken,
                corpId,
                toUser,
                msgType = "composite",
                composite = new
                {
                    head = new
                    {
                        title
                    },
                    remark = new
                    {
                        content
                    },
                    link = new
                    {
                        title = linkString,
                        url = "fs://CRM/udobj?{\"objDescApiName\":\"" + linkAPIName + "\",\"objDataId\":\"" + linkApiObjId + "\"}",// fs://CRM/udobj?{\"objDescApiName\":\"AccountObj\",\"objDataId\":\"5ff2780b7132bb0001bade16\"}
                    },
                }
            };
            return HttpPostForJson(jsonObj, "发送文本消息");
        }
        #endregion

        #region 素材管理:通过素材管理的接口,企业可以上传、下载或删除多媒体文件, 目前支持的是CRM素材和图文消息素材管理,对素材的获取和调用等操作,是通过mediaId来进行的。
        // 
        #endregion

        #region 办公协同

        #region 考勤外勤
        /// <summary>
        /// 获取考勤/外勤数据列表:
        /// 查询时间段内的考勤数据,查询时间间隔不超过40天。
        /// 正常返回:
        /// {
        ///    "datas": [                               结果记录列表
        ///        {// 考勤
        ///            "openUserId": ""                 用户开平账号
        ///            "userName": "test",              用户昵称
        ///            "createDateStr": "2016-11-17",   签到日期
        ///            "checkTime": 1479365385927,      打卡时间
        ///            "checkType": 0,                  签到打卡类型: 0-签到、1-签退
        ///            "locationType": 0,               打卡方式: 0-经纬度、1-wifi
        ///            "locationException": 0,          签到距离: 0-正常 、1-地点异常、2-wifi异常
        ///            "deviceId": "",                  签到设备号
        ///            "checkAddress": "",              打卡位置
        ///            "deviceException": 0,            设备异常: 0-正常、 1-异常
        ///            "systemException": 0             系统风险: 0- 正常、1- IOS越狱、2- Android 作弊软件、3- Android 伪造地址、4 模拟器、5 root
        ///        }, 
        ///        ...
        ///        {// 外勤
        ///             "openUserId": "",                       用户开平账号
        ///             "userName": "test",                     用户昵称
        ///             "checkinsTimeStamp": 1480409839250,     签到时间戳
        ///             "checkinsAddressDesc": "",              外勤签到地址
        ///             "customerId": 0,                        关联客户Id
        ///             "deviceRisk": false,                    设备异常: true-异常、false-正常
        ///             "contentText": "123466",                文字描述
        ///             "checkinsDistnace": 1455390,            签到距离
        ///             "cheatRisk": 0,                         系统风险: 0- 正常、1- IOS越狱、2- Android 作弊软件、3- Android 伪造地址、4 模拟器、5 root
        ///             "checkinsLon": 116.332,                 签到经度
        ///             "checkinsLat": 39.9766,                签到纬度
        ///             "checkoutTimeStamp": 1579139051105,     签退时间戳
        ///             "checkoutAddressDesc": "",              外勤签退地址
        ///             "checkoutLat": 22.542189,               签退经度
        ///             "checkoutLon": 113.95421                签退纬度
        ///         },
        ///         ...
        ///    ], 
        ///    "totalCount": 15,                        总记录数                            
        ///    "errorCode"	                            返回码
        ///    "errorMessage"                           对返回码的文本描述内容
        ///    "errorDescription": "成功"
        /// }
        /// </summary>
        /// <param name="openUserIds">被查询人员的openUserId列表;最多支持200人</param>
        /// <param name="startTime">开始时间</param>
        /// <param name="endTime">结束时间;endTime-startTime 不能大于40天</param>
        /// <param name="isOutside">是否外勤</param>
        /// <param name="pageSize">每页的条数,默认为20 ;最大值为1000</param>
        /// <param name="pageNumber">页码,默认为1</param>
        /// <returns></returns>
        public static JObject FindAttendance_OutsideAttendance(List<string> openUserIds, DateTime startTime, DateTime endTime, bool isOutside = false, int pageSize = 20, int pageNumber = 1)
        {
            VerifyCorpMsgNotNull();
            VerifyArray_ElementNotNull("员工ID集合列表", nameof(openUserIds), openUserIds);
            if (openUserIds.Count() > 200)
                throw new ArgumentException("【员工ID集合列表】最多支持200人", nameof(openUserIds));
            else if (startTime.Ticks > endTime.Ticks)
                throw new ArgumentException("【开始时间】不能大于【结束时间】", nameof(startTime));
            else if (startTime.AddDays(40).CompareTo(endTime) < 0)
                throw new ArgumentException("【结束时间】不能大于【开始时间】40天", nameof(endTime));
            else if (pageNumber < 1)
                throw new ArgumentException("【页码】不允许为小于1", nameof(pageNumber));
            else if (pageSize < 1 || pageSize > 1000)
                throw new ArgumentException("【每页的条数】不允许为小于1或大于1000", nameof(pageSize));

            object jsonObj = new { corpAccessToken, corpId, startTime = startTime.Ticks / 10000, endTime = endTime.Ticks / 10000, pageSize, pageNumber, openUserIds };
            string impName = isOutside ? "获取外勤勤数据列表" : "获取考勤数据列表";
            return HttpPostForJson(jsonObj, impName);
        }

        #region 获取高级外勤数据:高级外勤属于高级功能,需要先通过描述接口来获取该企业的所定义的高级外勤对象的描述,再通过相关的查询接口来获取数据
        /// <summary>
        /// 获取高级外勤数据描述
        /// 正常返回:
        /// {
        ///   "errorCode": 0,               返回码
        ///   "errorMessage": "success",    对返回码的文本描述内容
        ///    "datas": []                  详见CheckinsObj预设对象
        /// }
        /// </summary>
        /// <returns></returns>
        public static JObject QuerySeniorOutsideAttendanceDescribe()
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId };
            JObject jObject = HttpPostForJson(jsonObj, "获取高级外勤数据描述");
            return jObject;
        }

        /// <summary>
        /// 获取高级外勤数据列表
        /// 正常返回:
        /// {
        ///   "errorCode": 0,               返回码
        ///   "errorMessage": "success",    对返回码的文本描述内容
        ///    "datas": []                  详见CheckinsObj预设对象
        /// }
        /// </summary>
        /// <param name="limit">获取数据条数,默认20,最大值为100</param>
        /// <param name="offset">偏移量,从0开始、数值必须为limit的整数倍</param>
        /// <param name="conditionsList">过滤条件(精确查询)集合<字典<字段名称(name),字段值>></param>
        /// <param name="fieldNames">返回字段集合</param>
        /// <param name="rangeConditions">区间查询对象集合</param>
        /// <param name="orders">范围条件列表</param>
        /// <returns></returns>
        public static JObject FindSeniorOutsideAttendanceList(int limit = 20, int offset = 0, List<Dictionary<string, string>> conditionsList = null, List<string> fieldNames = null, List<RangeCondition> rangeConditions = null, List<Order> orders = null)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();

            if (limit < 1 || limit > 100)
                throw new ArgumentException("【获取数据条数】必须在1-100之间", nameof(limit));
            else if (offset < 0 || offset % limit != 0)
                throw new ArgumentException("【偏移量】必须从0开始、数值必须为limit的整数倍", nameof(offset));

            List<object> conditions = new List<object>();
            if (conditionsList != null)
                foreach (var condition in conditionsList)
                    conditions.Add(new
                    {
                        conditionType = "term_condition", //term_condition:表示精确匹配(目前只支持这种) 
                        conditions = condition,
                    });

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, searchQuery = new { offset, limit, conditions, dataProjection = new { fieldNames }, rangeConditions, orders } };
            JObject jObject = HttpPostForJson(jsonObj, "获取高级外勤数据列表");
            return jObject;
        }

        /// <summary>
        /// 获取高级外勤数据详情
        /// 正常返回:
        /// {
        ///   "errorCode": 0,               返回码
        ///   "errorMessage": "success",    对返回码的文本描述内容
        ///    "datas": []                  详见CheckinsObj预设对象
        /// }
        /// </summary>
        /// <param name="dataId">数据Id</param>
        /// <returns></returns>
        public static JObject QuerySeniorOutsideAttendance(string dataId)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, dataId };
            JObject jObject = HttpPostForJson(jsonObj, "获取高级外勤数据详情");
            return jObject;
        }

        #endregion

        #region 外勤计划创建相关接口:通过如下接口进行创建高级外勤相关的外勤计划,在创建外勤计划之前,需要先查询到该企业的外勤类型。
        /// <summary>
        /// 获取外勤类型
        /// 正常返回:
        /// {
        ///   "errorCode": 0,               返回码
        ///   "errorMessage": "success",    对返回码的文本描述内容
        ///    "data": [ 
        ///         {
        ///             "typeName": ,       类型名称
        ///             "typeId":           类型Id
        ///         },
        ///         ...
        ///    ]                   
        /// }
        /// </summary>
        /// <param name="mainApiName">关联主对象apiName</param>
        /// <param name="apiNames">关联子对象apiName集合</param>
        /// <returns></returns>
        public static JObject QuerySeniorOutsideAttendanceCheckType(string mainApiName = null, List<string> apiNames = null)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, mainApiName, apiNames };
            JObject jObject = HttpPostForJson(jsonObj, "获取外勤类型");
            return jObject;
        }

        /// <summary>
        /// 创建外勤计划
        /// 正常返回:
        /// {
        ///   "errorCode": 0,               返回码
        ///   "errorMessage": "success",    对返回码的文本描述内容
        ///    "data": [ 
        ///         {
        ///             "typeName": ,       类型名称
        ///             "typeId":           类型Id
        ///         },
        ///         ...
        ///    ]                   
        /// }
        /// </summary>
        /// <param name="planDateTime">计划执行时间</param>
        /// <param name="checkTypeId">外勤类型Id</param>
        /// <param name="executorId">计划执行人的openUserId</param>
        /// <param name="apiName">关联的主对象的apiName</param>
        /// <param name="dataId">关联的主对象的Id</param>
        /// <param name="info">客户的经纬度</param>
        /// <param name="assistantIds">协访人的openUserId列表</param>
        /// <param name="extFields">外勤计划的配置自定义字段模块,字段描述参考高级外勤(Dictionary<字段名称,对应字段值对象>)</param>
        /// <param name="referenceObjects">从对象数据</param>
        /// <returns></returns>
        public static JObject CreateSeniorOutsideAttendancePlan(DateTime planDateTime, string checkTypeId, string executorId, string apiName, string dataId, string info = null, List<string> assistantIds = null, Dictionary<string, object> extFields = null, List<Dictionary<string, string>> referenceObjects = null)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            object jsonObj = new
            {
                corpAccessToken,
                corpId,
                currentOpenUserId,
                data = new
                {
                    planTime = planDateTime.Ticks / 10000,
                    checkTypeId,
                    executorId,
                    assistantIds,
                    mainObject = new
                    {
                        apiName,
                        dataId,
                        info
                    },
                    extFields,
                    referenceObjects
                }
            };
            JObject jObject = HttpPostForJson(jsonObj, "创建外勤计划");
            return jObject;
        }

        #endregion
        #endregion

        #region 协同审批
        /// <summary>
        /// 获取审批类型列表
        /// 查询协同审批中的审批类型列表。
        /// 正常返回:
        /// {
        ///    "approvalTypeForms": [
        ///         {
        ///             "approveType": 1,
        ///             "name": "普通审批",
        ///             "id": "0076E076-2D2B-4392-8365-7982763840C4"
        ///         },
        ///         ...
        ///    ], 
        ///    "errorCode"	                            返回码
        ///    "errorMessage"                           对返回码的文本描述内容
        ///    "errorDescription": "成功"
        /// }
        /// </summary>
        /// <returns></returns>
        public static JObject QueryApprovalTypeForms()
        {
            VerifyCorpMsgNotNull();
            object jsonObj = new { corpAccessToken, corpId };
            return HttpPostForJson(jsonObj, "获取审批类型列表");
        }

        /// <summary>
        /// 获取审批列表
        /// 查询起始时间段内审批列表,查询时间间隔不超过180天(默认按审批最后更新时间倒序排序)。
        /// 正常返回:
        /// {
        ///    "approvals":  [
        ///         {
        ///             "id": 1,                        审批Id
        ///             "status": 1                     整个审批流程状态, 1待审批, 2同意, 3不同意, 4已取消, 5错误
        ///         }
        ///     ],
        ///     totalCount: 1000,                       总记录数
        ///     pageSize: 10,                           每页记录数
        ///     pageNumber: 1                           当前页
        ///    "errorCode"	                            返回码
        ///    "errorMessage"                           对返回码的文本描述内容
        ///    "errorDescription": "成功"
        /// }
        /// </summary>
        /// <param name="startDateTime">开始时间时间</param>
        /// <param name="endDateTime">结束时间时间</param>
        /// <param name="approvalFormId">审批类型ID</param>
        /// <param name="pageSize">每页记录数,默认为20(最大不能超过1000)</param>
        /// <param name="pageNumber">当前页,默认为1</param>
        /// <returns></returns>
        public static JObject QueryApprovals(DateTime startDateTime, DateTime endDateTime, string approvalFormId = null, int pageSize = 20, int pageNumber = 1)
        {
            VerifyCorpMsgNotNull();
            if (pageNumber < 1)
                throw new ArgumentException("【当前页】必须大于0", nameof(pageNumber));
            else if (pageSize < 1 || pageSize > 1000)
                throw new ArgumentException("【每页记录数】必须在1-1000之间", nameof(pageSize));

            object jsonObj = new { corpAccessToken, corpId, startTime = startDateTime.Ticks / 10000, endTime = endDateTime.Ticks / 10000, pageSize, pageNumber, approvalFormId };
            return HttpPostForJson(jsonObj, "获取审批列表");
        }

        /// <summary>
        /// 获取审批详情
        /// 查询起始时间段内审批列表,查询时间间隔不超过180天(默认按审批最后更新时间倒序排序)。
        /// 正常返回:
        /// 返回内容由对象定义决定,详见 https://open.fxiaoke.com/wiki.html#artiId=150 
        /// </summary>
        /// <param name="approvalId">审批类型ID</param>
        /// <returns></returns>
        public static JObject QueryApprovalById(string approvalId)
        {
            if (string.IsNullOrWhiteSpace(approvalId))
                throw new ArgumentException("【审批类型ID】不允许为空", nameof(approvalId));

            VerifyCorpMsgNotNull();

            object jsonObj = new { corpAccessToken, corpId, approvalId };
            return HttpPostForJson(jsonObj, "获取审批详情");
        }

        #endregion

        #endregion

        #region CRM接口
        /************CRM的接口分为四类,CRM基础接口,CRM对象接口,CRM业务接口及CRM流程接口***************************
        * 其中对象接口负责一些数据的基础类操作,如增删改查等;
        * 业务接口负责对象的业务操作,如商机的阶段变更,线索的退回或转移等;
        * 流程接口就是CRM的业务流,审批流及工作流相关的处理,这三类接口都依赖于CRM的对象API Name及对象描述等基础处理,
        * 基础接口就涵盖了这部分的处理;
        * 备注:接口中的openUserId是用户的OpenId, 需要通过通讯录接口来获取
        /********************************************************************************************************/
        #region CRM基础接口

        /// <summary>
        /// 获取企业CRM对象API Name列表
        /// 获取企业所有对象的API Name列表,包含了预设对象和自定义对象
        /// 正常返回:
        /// {
        ///     "errorCode": 0,                                         返回码
        ///     "errorMessage": "OK",                              对返回码的文本描述内容
        ///     "data": {
        ///         "objects": [                                        对象API Name数据集合
        ///             {
        ///                 "describeApiName": "MarketingEventObj",     对象API Name 
        ///                 "describeDisplayName": "市场活动",           对象显示名称
        ///                 "defineType": "package",                    对象类型(package为预设对象,custom为自定义对象)
        ///                 "isActive": true,                           对象是否启用
        ///                 "hideButton": false                         (API文档未提及)
        ///             },
        ///             ....
        ///         }
        ///     }
        /// }
        /// </summary>
        /// <returns></returns>
        public static JObject QueryObjectList()
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId };
            return HttpPostForJson(jsonObj, "获取企业CRM对象API Name列表");
        }

        /// <summary>
        /// 获取对象描述:
        /// 获取该对象的数据结构定义的描述说明,如对象间的引用关系,字段的名称,类型,默认值,是否必填等,以及该对象下的枚举值的属性集合。
        /// 正常返回:
        /// {
        ///     "errorCode": 0,                                         返回码
        ///     "errorMessage": "success",                              对返回码的文本描述内容
        ///     "data": {
        ///         "describe": {
        ///             "api_name": "ProductObj",                       字段api_name	:用于数据操作时对字段的唯一标识
        ///             "fields": {
        ///                 "字段名称":{
        ///                     "type":                                     字段类型:object_reference、email、phone_number、true_or_false、text、long_text、date_time、number、select_one、select_many、embedded_object_list、file_attachment 、image、employee、country、province、city、district
        ///                     "define_type":                              定义类型:system:系统内置,package:包(业务应用)定义,custom:企业客户定义
        ///                     "is_required":                              是否必填:添加时候是否必须输入
        ///                     "is_active":                                是否启用:用于表示该字段是否生效,false表示该字段被隐藏(禁用),管理员可以从字段管理中打开
        ///                     "is_auto_number":                           是否自动编号:自动编号的字段添加、更新时不允许输入
        ///                     "is_need_convert":                          是否需要转换:需要转换的字段,输入和返回值都是合法的openUserId
        ///                     "description":                              字段描述
        ///                     "help_text":                                帮助信息	
        ///                     "options": [
        ///                        {
        ///                            "label": "已上架",                  字段显示名称
        ///                            "value": "1"                        字段值
        ///                         },
        ///                     ],
        ///                 }
        ///             ....
        ///         }
        ///     }
        /// }
        /// {
        /// </summary>
        /// /// <param name="apiName">对象的api_name</param>
        /// <returns></returns>
        public static JObject QueryObjectDescribe(FxObjApiName apiName)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, apiName = apiName.ToString() };
            return HttpPostForJson(jsonObj, ApiUrlPre + @"cgi/crm/v2/object/describe", "获取对象【" + apiName + "】描述");
        }

        /// <summary>
        /// 获取国家省份地市选项代码
        /// 正常返回:
        ///{
        /// "countryAreaOptions": {
        ///		"country": {
        ///			"type": "country",
        ///			"define_type": "package",
        ///			"is_index": false,
        ///			"is_need_convert": false,
        ///			"is_required": false,
        ///			"is_unique": false,
        ///			"options": [
        ///			    {
        ///	                "resource_bundle_key": "",
        ///	                "child_options": [{
        ///		                "province": ["249", "250", "251", "252", "253", "254", "255", "256", "257", "258", "259", "260", "261", "262", "263", "264", "265", "266", "267", "268", "269", "270", "271", "272", "273", "274", "275", "276", "277", "278", "279", "280", "281", "282"]
        ///                 }],
        ///	                "label": "中国",
        ///	                "value": "248"
        ///             }, 
        ///             {
        ///	                "resource_bundle_key": "",
        ///	                "label": "阿尔巴尼亚",
        ///	                "value": "0"
        ///             },
        ///             ...
        ///         ],
        ///			"api_name": "district",
        ///			"cascade_parent_api_name": "city",
        ///			"status": "released",
        ///			"label": "区"
        ///		}
        /// },
        ///	"errorCode": 0,
        ///	"errorMessage": "success"
        ///}
        /// </summary>
        /// /// <param name="isIncludeDeleted">是否包括获取已删除的省市区信息,默认为true返回包括已删除的,false不返回已删除的</param>
        /// <returns></returns>
        public static JObject QueryCountryAreaOptions(bool isIncludeDeleted = true)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, isIncludeDeleted };
            return HttpPostForJson(jsonObj, "获取国家省份地市选项代码");
        }

        /// <summary>
        /// 锁定/解锁对象:
        /// 针对对象下的数据,通过数据权限和团队成员的设置可以由多人进行共同维护。
        /// 但是,在一些场景,用户期望数据不能被修改,以保证数据的正确性和记录当时的状态。
        /// 数据通过锁定操作,可以保证数据不能被更新,然后再通过解锁操作,让数据可以被使用者修改。
        /// 正常返回
        /// {
        ///    "errorCode": 0,
        ///    "errorMessage": "OK"
        /// }
        /// </summary>
        /// <param name="dataObjectApiName">对象的api_name</param>
        /// <param name="dataIds">锁定数据id的列表</param>
        /// <param name="detailObjStrategy">0表示只锁定当前数据对象,1表示级联锁定关联从对象,默认值为1</param>
        /// <param name="lockAction">锁定/解锁</param>
        /// <returns></returns>
        public static JObject LockObject(FxObjApiName dataObjectApiName, List<string> dataIds, int detailObjStrategy = 1, LockAction lockAction = LockAction.@lock)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            VerifyArray_ElementNotNull("锁定数据id的列表", nameof(dataIds), dataIds);
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataObjectApiName = dataObjectApiName.ToString(), dataIds, detailObjStrategy } };
            return HttpPostForJson(jsonObj, ApiUrlPre + @"cgi/crm/v2/object/" + lockAction, GetEnumDescription(lockAction) + "【" + dataObjectApiName + "】对象");
        }

        /// <summary>
        /// 解锁对象:
        /// 针对对象下的数据,通过数据权限和团队成员的设置可以由多人进行共同维护。
        /// 但是,在一些场景,用户期望数据不能被修改,以保证数据的正确性和记录当时的状态。
        /// 数据通过锁定操作,可以保证数据不能被更新,然后再通过解锁操作,让数据可以被使用者修改。
        /// 正常返回
        /// {
        ///    "errorCode": 0,
        ///    "errorMessage": "OK"
        /// }
        /// </summary>
        /// <param name="dataObjectApiName">对象的api_name</param>
        /// <param name="dataIds">锁定数据id的列表</param>
        /// <param name="detailObjStrategy">0表示只锁定当前数据对象,1表示级联锁定关联从对象,默认值为1</param>
        /// <returns></returns>
        public static JObject UnlockObject(FxObjApiName dataObjectApiName, List<string> dataIds, int detailObjStrategy = 1)
        {
            return LockObject(dataObjectApiName, dataIds, detailObjStrategy, LockAction.unlock);
        }
        #endregion

        #region CRM对象接口-预设 & 自定义对象
        /*********************************************************************************************************************************************************
         * CRM预设对象是指系统默认就能支持的对象模块,这些对象都存在一些通用的接口能力,
         * 目前支持已经开放的预设对象有:客户、订单、联系人、销售线索、回款、回款计划、退款、合同、退货单、开票申请、商机、商机2.0、商品、产品、仓库、入库单、
         * 库存、发货单、价目表、价目表明细、规格、规格值、工单、批次、批次库存、序列号、报价单、报价单明细、预存款、返利、客户-客户地址、客户-客户财务信息、
         * 设备、设备配件规格关系、领料单、退料单、合作伙伴、信用(不支持查询)、返利支出(只支持查询)、订单产品(只支持查询)、回款明细(只支持查询)、退货单产品(只支持查询)、
         * 商机2.0明细(只支持查询)、发货单产品(只支持查询)、市场活动(只支持查询)、客户账户(只支持查询)、领料单产品(只支持查询)、退料单产品(只支持查询)。
         *********************************************************************************************************************************************************/
        /// <summary>
        /// 添加(预设/自定义)对象
        /// 正常返回
        /// {
        ///    "dataId": "5a9ce894f125ae9befxxxxxx",    添加成功的数据Id
        ///    "errorCode": 0,                          返回码
        ///    "errorMessage": "OK",                    对返回码的文本描述内容
        ///    "errorDescription": "success"            对返回码的文本描述内容
        ///}
        /// </summary>
        /// <param name="object_data">主对象数据map(和对象描述中字段一一对应),其对象属性必须包括对象的api_name【dataObjectApiName】,示例:【"object_data": {"dataObjectApiName":"PaymentObj",...      }】</param>
        /// <param name="isCustom">是否为自定义对象,默认否(即预设对象)</param>
        /// <param name="details">明细对象数据map(和对象描述中字段一一对应),key为对象api_name字符串,value为子表object[],示例:【"子表api名": [{"子表属性名":"子表属性值",...},...】</param>
        /// <param name="triggerWorkFlow">是否触发工作流(不传时默认为true, 表示触发),该参数对所有对象均有效</param>
        /// <param name="includeDetailIds">主从对象一起创建时,是否返回从对象id列表,true返回,false不返回,默认不返回</param>
        /// <returns></returns>
        public static JObject AddObject(object object_data, bool isCustom = false, Dictionary<string, List<Dictionary<string, object>>> details = null, bool includeDetailIds = false, bool triggerWorkFlow = true)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();

            if (object_data == null || !ContainProperty(object_data, "dataObjectApiName"))
                throw new ArgumentException("【主对象数据】其对象不允许为空且其属性必须包括对象的api_name【dataObjectApiName】", nameof(object_data));

            string dataObjectApiName = (string)object_data.GetType().GetProperty("dataObjectApiName").GetValue(object_data, null);
            if (details != null && details.Any((detail) =>
            {
                return string.IsNullOrWhiteSpace(detail.Key);
            }))
                throw new ArgumentException("【明细对象数据】map,key值需为明细对象的api_name【dataObjectApiName】", nameof(object_data));

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, triggerWorkFlow, includeDetailIds, data = new { object_data, details } };
            string url = isCustom ? ApiUrlPre + @"cgi/crm/custom/data/create" : ApiUrlPre + @"cgi/crm/v2/data/create";
            string impName = isCustom ? "新增自定义对象【" + dataObjectApiName + "】数据" : "新增预设对象【" + dataObjectApiName + "】数据";
            return HttpPostForJson(jsonObj, url, impName);
        }

        /// <summary>
        /// 修改(预设/自定义)对象
        /// 正常返回
        /// {
        ///    "errorCode": 0,                          返回码
        ///    "errorMessage": "OK",                    对返回码的文本描述内容
        ///    "errorDescription": "success"            对返回码的文本描述内容
        ///}
        /// </summary>
        /// <param name="object_data">主对象数据map(和对象描述中字段一一对应),其对象属性必须包括对象的api_name【dataObjectApiName】以及对象标识【_id】,示例:【"object_data": {"dataObjectApiName":"PaymentObj","_id":"XXX"...      }】</param>
        /// <param name="isCustom">是否为自定义对象,默认否(即预设对象)</param>
        /// <param name="details">明细对象数据map(和对象描述中字段一一对应),key为对象api_name字符串,value为子表object[]且每个子表对象都包含更新的数据Id【_id】,示例:【"子表api名": [{"子表属性名":"子表属性值","_id":"XXX"...},...】</param>
        /// <param name="triggerWorkFlow">是否触发工作流(不传时默认为true, 表示触发),该参数对所有对象均有效</param>
        /// <returns></returns>
        public static JObject UpdateObject(object object_data, bool isCustom = false, Dictionary<string, List<Dictionary<string, object>>> details = null, bool triggerWorkFlow = true)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            string dataObjectApiName = (string)object_data.GetType().GetProperty("dataObjectApiName").GetValue(object_data, null);

            if (!ContainProperty(object_data, "dataObjectApiName") || !ContainProperty(object_data, "_id"))
                throw new ArgumentException("【主对象数据】其对象属性必须包括对象的api_name【" + dataObjectApiName + "】&&更新的数据Id【_id】", nameof(object_data));

            //if (details != null && details.Any((detail) =>
            //{
            //    return string.IsNullOrWhiteSpace(detail.Key) || (detail.Value != null && detail.Value.Any((obj) =>
            //    {
            //        return !ContainProperty(obj, "_id");
            //    }));
            //}))
            //{
            //    throw new ArgumentException("【明细对象数据】map,key值需为明细对象的api_name【"+ dataObjectApiName + "】且每个子表对象都包含更新的数据Id【_id】", nameof(details));
            //}

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, triggerWorkFlow, data = new { object_data, details } };
            string url = isCustom ? ApiUrlPre + @"cgi/crm/custom/data/update" : ApiUrlPre + @"cgi/crm/v2/data/update";
            string impName = isCustom ? "更新自定义对象【" + dataObjectApiName + "】数据" : "更新预设对象【" + dataObjectApiName + "】数据";
            return HttpPostForJson(jsonObj, url, impName);
        }

        /// <summary>
        /// 分页查询(预设/自定义)对象列表
        /// 正常返回
        /// {
        ///  "errorCode": 0,                                   返回码
        ///  "errorMessage": "OK",                             对返回码的文本描述内容
        ///  "errorDescription": "success",                    对返回码的文本描述内容
        ///  "data": {
        ///    "total": 1,                                     实际总记录数
        ///    "offset": 0,                                    偏移量
        ///    "dataList": [                                   数据列表
        ///      {
        ///        "name": "apitest客户2020/3/26 17:03:26",
        ///        "_id": "5e7c6fdc95215f00019704ca"
        ///      }
        ///    ],
        ///    "limit": 10                                     查询数据的条数
        ///  }
        /// }
        /// </summary>
        /// <param name="dataObjectApiName">查询对象的api_name</param>
        /// <param name="filters">过滤条件列表</param>
        /// <param name="fieldProjection">返回(结果)字段列表</param>
        /// <param name="limit">获取数据条数, 最大值为100</param>
        /// <param name="offset">偏移量,从0开始、数值必须为limit的整数倍</param>
        /// <param name="orders">排序</param>
        /// <param name="isCustom">是否为自定义对象,默认否(即预设对象)</param>
        /// <param name="find_explicit_total_num">true->返回total,false->不返回total总数。默认true。设置为false可以加快接口响应速度</param>
        /// <returns></returns>
        public static JObject QueryObjectList(FxObjApiName dataObjectApiName, List<Filter> filters, List<string> fieldProjection, int limit, int offset, List<Order> orders, bool isCustom = false, bool find_explicit_total_num = false)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();

            if (limit < 1 || limit > 100)
                throw new ArgumentException("【获取数据条数】必须在1-100之间", nameof(limit));
            else if (offset < 0 || offset % limit != 0)
                throw new ArgumentException("【偏移量】必须从0开始、数值必须为limit的整数倍", nameof(offset));
            else if (filters == null || !filters.Any() || filters.Any((filter) =>
                {
                    return filter == null || string.IsNullOrWhiteSpace(filter.field_name) || filter.field_values == null || !filter.field_values.Any();
                }))
                throw new ArgumentException("【过滤条件列表】及其内容不允许为空", nameof(filters));
            else if (orders == null || !orders.Any() || orders.Any((order) =>
                {
                    return order == null || string.IsNullOrWhiteSpace(order.fieldName);
                }))
                throw new ArgumentException("【排序】不允许为空及其字段名不允许为空", nameof(orders));

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { find_explicit_total_num, dataObjectApiName = dataObjectApiName.ToString(), search_query_info = new { limit, offset, filters, orders, fieldProjection } } };
            string url = isCustom ? ApiUrlPre + @"cgi/crm/custom/data/query" : ApiUrlPre + @"cgi/crm/v2/data/query";
            string impName = isCustom ? "分页查询自定义对象【" + dataObjectApiName + "】数据" : "分页查询预设对象【" + dataObjectApiName + "】数据";
            return HttpPostForJson(jsonObj, url, impName);
        }

        /// <summary>
        /// 查询所有(预设/自定义)对象
        /// </summary>
        /// <param name="dataObjectApiName">查询对象的api_name</param>
        /// <param name="filters">过滤条件列表</param>
        /// <param name="fieldProjection">返回(结果)字段列表</param>
        /// <param name="orders">排序</param>
        /// <param name="isCustom">是否为自定义对象,默认否(即预设对象)</param>
        /// <param name="find_explicit_total_num">true->返回total,false->不返回total总数。默认true。设置为false可以加快接口响应速度</param>
        public static List<JToken> QueryAllObjectList(FxObjApiName dataObjectApiName, List<Filter> filters, List<string> fieldProjection, bool isCustom = false, List<Order> orders = null, int startRow = 0, bool find_explicit_total_num = false)
        {
            JObject newQueryJObject;
            List<JToken> objects = new List<JToken>();
            if (orders == null || !orders.Any()) 
                orders = new List<Order> { new Order() };

            int fengxiangQueryCount = 0;
            do
            {
                newQueryJObject = QueryObjectList(dataObjectApiName, filters, fieldProjection, QueryLimitSum, startRow, orders, isCustom, find_explicit_total_num);
                JToken queryData = newQueryJObject["data"];

                if (queryData == null) // 正常不触发
                    continue;

                string tipMsg = "当前已查出【" + dataObjectApiName + "】条数:" + objects.Count;
                if (find_explicit_total_num)
                    tipMsg += "/" + queryData["total"];
                    
                JToken queryDataList = queryData["dataList"];
               
                if (queryDataList != null && queryDataList.HasValues)
                {
                    List<JToken> dataList = queryDataList.Children().ToList();
                    fengxiangQueryCount = dataList.Count;
                    objects.AddRange(dataList);
                    LogHelper.logger.Info(tipMsg);
                }
                else
                    fengxiangQueryCount = 0;

                startRow += QueryLimitSum;
            } while (fengxiangQueryCount == QueryLimitSum);
            return objects;
        }

        /// <summary>
        /// 根据Id查询(预设/自定义对象)数据详情
        /// 正常返回
        /// {
        ///  "errorCode": 0,                                   返回码
        ///  "errorMessage": "OK",                             对返回码的文本描述内容
        ///  "errorDescription": "success",                    对返回码的文本描述内容
        ///  "data": {
        ///    "total": 1,                                     实际总记录数
        ///    "offset": 0,                                    偏移量
        ///    "dataList": [                                   数据列表
        ///      {
        ///        "name": "apitest客户2020/3/26 17:03:26",
        ///        "_id": "5e7c6fdc95215f00019704ca"
        ///      }
        ///    ],
        ///    "limit": 10                                     查询数据的条数
        ///  }
        /// }
        /// </summary>
        /// <param name="dataObjectApiName">查询对象的api_name</param>
        /// <param name="objectDataId">数据Id</param>
        /// <param name="isCustom">是否为自定义对象,默认否(即预设对象)</param>
        /// <returns></returns>
        public static JObject QueryObjectById(FxObjApiName dataObjectApiName, string objectDataId, bool isCustom = false)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            if (string.IsNullOrWhiteSpace(objectDataId))
                throw new ArgumentException("【数据Id】不允许为空", nameof(objectDataId));

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataObjectApiName = dataObjectApiName.ToString(), objectDataId } };
            string url = isCustom ? ApiUrlPre + @"cgi/crm/custom/data/get" : ApiUrlPre + @"cgi/crm/v2/data/get";
            string impName = isCustom ? "根据Id查询自定义【" + dataObjectApiName + "】数据详情" : "根据Id查询预设【" + dataObjectApiName + "】数据详情";
            return HttpPostForJson(jsonObj, url, impName);
        }

        /// <summary>
        /// 根据Id作废预设对象数据(【CRM->数据维护工具->回收站】中可查询作废对象)
        /// 正常返回
        /// {
        ///    "errorCode": 0,                  返回码
        ///    "errorMessage": "success"        对返回码的文本描述内容
        ///    "errorDescription": "success"    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="dataObjectApiName">作废对象的api_name</param>
        /// <param name="objectDataId">数据Id</param>
        /// <returns></returns>
        public static JObject InvalidObjectById(FxObjApiName dataObjectApiName, string object_data_id)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            if (string.IsNullOrWhiteSpace(object_data_id))
                throw new ArgumentException("【数据Id】不允许为空", nameof(object_data_id));

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataObjectApiName = dataObjectApiName.ToString(), object_data_id } };
            return HttpPostForJson(jsonObj, ApiUrlPre + @"cgi/crm/v2/data/invalid", "根据Id作废预设对象【" + dataObjectApiName + "】数据");
        }

        /// <summary>
        /// 根据Id作废自定义对象数据(【CRM->数据维护工具->回收站】中可查询作废对象)
        /// 正常返回
        /// {
        ///    "errorCode": 0,                  返回码
        ///    "errorMessage": "success"        对返回码的文本描述内容
        ///    "errorDescription": "success"    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="dataObjectApiName">作废对象的api_name</param>
        /// <param name="idList">数据Id集合</param>
        /// <returns></returns>
        public static JObject InvalidCustomObjectById(FxObjApiName dataObjectApiName, List<string> idList)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            VerifyArray_ElementNotNull("数据Id集合", nameof(idList), idList);

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataObjectApiName = dataObjectApiName.ToString(), idList } };
            return HttpPostForJson(jsonObj, ApiUrlPre + @"cgi/crm/custom/data/invalid", "根据Id作废自定义对象【" + dataObjectApiName + "】数据");
        }

        /// <summary>
        /// 根据Id恢复对象数据
        /// 正常返回
        /// {
        ///    "errorCode": 0,                  返回码
        ///    "errorMessage": "success"        对返回码的文本描述内容
        ///    "errorDescription": "success"    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="dataObjectApiName">恢复对象的api_name</param>
        /// <param name="idList">数据Id集合</param>
        /// <param name="isCustom">是否为自定义对象,默认否(即预设对象)</param>
        /// <returns></returns>
        public static JObject RecoverObjectById(FxObjApiName dataObjectApiName, List<string> idList, bool isCustom = false)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            VerifyArray_ElementNotNull("数据Id集合", nameof(idList), idList);

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataObjectApiName = dataObjectApiName.ToString(), idList } };
            string url = isCustom ? ApiUrlPre + @"cgi/crm/custom/data/recover" : ApiUrlPre + @"cgi/crm/v2/data/recover";
            string impName = isCustom ? "根据Id恢复自定义对象【" + dataObjectApiName + "】数据" : "根据Id恢复预设对象【" + dataObjectApiName + "】数据";
            return HttpPostForJson(jsonObj, url, impName);
        }

        /// <summary>
        /// 根据Id删除对象数据
        /// 正常返回
        /// {
        ///    "errorCode": 0,                  返回码
        ///    "errorMessage": "success"        对返回码的文本描述内容
        ///    "errorDescription": "success"    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="dataObjectApiName">删除对象的api_name</param>
        /// <param name="idList">数据Id集合 (删除之前请先作废数据,价目表明细可以直接删除,错误码:201112008)</param>
        /// <param name="isCustom">是否为自定义对象,默认否(即预设对象)</param>
        /// <returns></returns>
        public static JObject DeleteObjectById(FxObjApiName dataObjectApiName, List<string> idList, bool isCustom = false)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            VerifyArray_ElementNotNull("数据Id集合", nameof(idList), idList);

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataObjectApiName = dataObjectApiName.ToString(), idList } };
            string url = isCustom ? ApiUrlPre + @"cgi/crm/custom/data/delete" : ApiUrlPre + @"cgi/crm/v2/data/delete";
            string impName = isCustom ? "根据Id删除自定义对象【" + dataObjectApiName + "】数据" : "根据Id删除预设对象【" + dataObjectApiName + "】数据";
            return HttpPostForJson(jsonObj, url, impName);
        }

        /// <summary>
        /// 变更数据对象负责人
        /// 正常返回
        /// {
        ///    "errorCode": 0,                        返回码
        ///    "errorMessage": "OK"                   对返回码的文本描述内容
        ///    "errorDescription": "success"          对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="dataObjectApiName">删除对象的api_name</param>
        /// <param name="datas">变更负责人信息,Dictionary<"数据id","负责人openUserId集合"></param>
        /// <param name="isCustom">是否为自定义对象,默认否(即预设对象)</param>
        /// <returns></returns>
        public static JObject ChangeObjectOwner(FxObjApiName dataObjectApiName, Dictionary<string, List<string>> datas, bool isCustom = false)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            if (datas == null)
                throw new ArgumentNullException("【Dictionary<\"数据对象id\",\"负责人openUserId集合\">】为空", nameof(datas));
            else if (datas.Count != 1 && !isCustom)// 预设对象
                throw new ArgumentException("【Dictionary<\"预设数据对象id\",\"负责人openUserId集合\">】一个预设数据对象id只能对应一组负责人id", nameof(datas));
            else if (isCustom && !datas.Any()) // 自定义对象
                throw new ArgumentException("【Dictionary<\"自定义数据对象id\",\"负责人openUserId集合\">】一个自定义数据对象id只能对应一组或以上负责人id", nameof(datas));

            List<object> Data = new List<object>();
            foreach (KeyValuePair<string, List<string>> data in datas)
            {
                string objectDataId = data.Key;
                if (string.IsNullOrWhiteSpace(objectDataId))
                    throw new ArgumentNullException("【数据Id】内容不能为空", nameof(objectDataId));

                List<string> ownerId = data.Value;
                VerifyArray_ElementNotNull("负责人openUserId集合", nameof(ownerId), ownerId);
                Data.Add(new { objectDataId, ownerId });
            }
            //预设示例:"Data": [{ "objectDataId": "5a9914fcf125ae0a1axxxxxx","ownerId": ["FSUID_7B8A3925E40FA68630C0D7E9C3XXXXXX"]},...]
            //自定义示例:"dataList": [{ "objectDataId": "5a9914fcf125ae0a1axxxxxx","ownerId": ["FSUID_7B8A3925E40FA68630C0D7E9C3XXXXXX"]},...]

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataObjectApiName = dataObjectApiName.ToString(), Data } };
            if (isCustom)
                jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataObjectApiName = dataObjectApiName.ToString(), dataList = Data } };

            string url = isCustom ? ApiUrlPre + @"cgi/crm/custom/data/changeOwner" : ApiUrlPre + @"cgi/crm/v2/data/changeOwner";
            string impName = isCustom ? "变更自定义对象【" + dataObjectApiName + "】负责人" : "变更预设对象【" + dataObjectApiName + "】负责人";
            return HttpPostForJson(jsonObj, url, impName);
        }

        /// <summary>
        /// CRM日志查询(待测)
        /// 正常返回
        /// {
        ///     "result": {
        ///         "msgs": [
        ///             {...},
        ///         ],
        ///    },
        ///    "errorCode": 0,                   返回码
        ///    "errorMessage": "success",        对返回码的文本描述内容
        ///    "errorDescription": "success",    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="FilterMainID">操作对象ID</param>
        /// <param name="Conditions">筛选条件</param>
        /// <returns></returns>
        public static JObject QueryAllCrmLog(CRMLogFilterMainID FilterMainID, List<CRMLogConditions> Conditions)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            int pageSize = 100;
            int pageNumber = 0;
            object jsonObj = new
            {
                corpAccessToken,
                corpId,
                currentOpenUserId,
                data = new
                {
                    pageSize,
                    pageNumber,
                    QueryInfo = new
                    {
                        FilterMainID,
                        Conditions,
                    },
                }
            };
            string url = "https://open.fxiaoke.com/cgi/crm/crmLog/query";
            string impName = "CRM日志查询";
            return HttpPostForJson(jsonObj, url, impName);
        }

        #endregion

        #region CRM业务接口
        /// <summary>
        /// 产品档案上下架
        /// 正常返回
        /// {
        ///    "errorCode": 0,                  返回码
        ///    "errorMessage": "success"        对返回码的文本描述内容
        ///    "errorDescription": "success"    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="productStatus">上下架状态</param>
        /// <param name="productIds">数据Id集合</param>
        /// <returns></returns>
        public static JObject ChangeProductStatusByIds(ProductStatus productStatus, List<string> productIds)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            VerifyArray_ElementNotNull("数据Id集合", nameof(productIds), productIds);
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { productStatus = (int)productStatus, productIds } };
            return HttpPostForJson(jsonObj, ApiUrlPre + @"cgi/crm/data/changeProductStatus", "产品档案" + GetEnumDescription(productStatus));
        }

        /// <summary>
        /// 订单(销售订单)确认已发货
        /// 正常返回
        /// {
        ///    "errorCode": 0,                  返回码
        ///    "errorMessage": "success"        对返回码的文本描述内容
        ///    "errorDescription": "success"    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="datas">数据列表(支持多条):Dictionary<订单数据Id,发货备注></param>
        /// <returns></returns>
        public static JObject ConfirmDelivery(Dictionary<string, string> datas)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            if (datas == null || !datas.Any())
                throw new ArgumentException("【数据列表】及其元素数量不允许为空", nameof(datas));

            List<object> dataList = new List<object>();
            foreach (var data in datas)
            {
                if (string.IsNullOrWhiteSpace(data.Key))
                    throw new ArgumentException("【数据列表】id不允许为空", nameof(datas));
                dataList.Add(new { dataId = data.Key, remark = data.Value });
            }

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataList } };
            return HttpPostForJson(jsonObj, "订单确认已发货");
        }

        /// <summary>
        /// 订单(销售订单)确认已收货
        /// 正常返回
        /// {
        ///    "errorCode": 0,                  返回码
        ///    "errorMessage": "success"        对返回码的文本描述内容
        ///    "errorDescription": "success"    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="dataList">数据列表(支持多条)</param>
        /// <returns></returns>
        public static JObject ConfirmReceive(List<string> dataIds)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            VerifyArray_ElementNotNull("数据Id集合", nameof(dataIds), dataIds);
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, data = new { dataIds } };
            return HttpPostForJson(jsonObj, "订单确认已收货");
        }

        /// <summary>
        /// 商机切换销售流程
        /// 正常返回
        /// {
        ///    "errorCode": 0,                  返回码
        ///    "errorMessage": "success"        对返回码的文本描述内容
        ///    "errorDescription": "success"    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="dataId">数据Id/销售流程Id</param>
        /// <param name="opportunityId">商机Id</param>
        /// <returns></returns>
        public static JObject ChangeSalesProcess(string dataId, string opportunityId)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();

            if (string.IsNullOrWhiteSpace(dataId))
                throw new ArgumentException("【数据Id/销售流程Id】不允许为空", nameof(dataId));
            else if (string.IsNullOrWhiteSpace(opportunityId))
                throw new ArgumentException("【商机Id】不允许为空", nameof(opportunityId));

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, apiName = "SaleActionObj", dataId, opportunityId };
            return HttpPostForJson(jsonObj, "商机切换销售流程");
        }

        /// <summary>
        /// 商机变更销售阶段
        /// 正常返回
        /// {
        ///    "errorCode": 0,                  返回码
        ///    "errorMessage": "success"        对返回码的文本描述内容
        ///    "errorDescription": "success"    对返回码的文本描述内容
        /// }
        /// </summary>
        /// <param name="dataId">数据Id/销售流程Id</param>
        /// <param name="opportunityId">商机Id</param>
        /// <param name="nextSaleStageId">待变更的销售阶段ID</param>
        /// <returns></returns>
        public static JObject ChangeSalesStage(string dataId, string opportunityId, string nextSaleStageId)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            if (string.IsNullOrWhiteSpace(dataId))
                throw new ArgumentException("【数据Id/销售流程Id】不允许为空", nameof(dataId));
            else if (string.IsNullOrWhiteSpace(opportunityId))
                throw new ArgumentException("【商机Id】不允许为空", nameof(opportunityId));
            else if (string.IsNullOrWhiteSpace(nextSaleStageId))
                throw new ArgumentException("【待变更的销售阶段ID】不允许为空", nameof(nextSaleStageId));

            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, apiName = "SaleActionObj", dataId, opportunityId, nextSaleStageId };
            return HttpPostForJson(jsonObj, "商机变更销售阶段");
        }

        /// <summary>
        /// 分页查询查询销售记录列表
        /// 正常返回
        /// {
        ///  "errorCode": 0,                                   返回码
        ///  "errorMessage": "OK",                             对返回码的文本描述内容
        ///  "errorDescription": "success",                    对返回码的文本描述内容
        ///  "total": 1,                                       实际总记录数
        ///  "salesRecorders": 
        ///  {
        ///    ...
        ///  }
        /// }
        /// </summary>
        /// <param name="dataId">关联的对象数据Id(不传时代表查询该对象所有数据的销售记录)</param>
        /// <param name="startTime">开始时间</param>
        /// <param name="endTime">结束时间</param>
        /// <param name="senderOpenUserId">发起人</param>
        /// <param name="pageSize">页面大小默认20</param>
        /// <param name="pageNumber">页码默认1</param>
        /// <returns></returns>
        public static JObject QuerySalesRecorderList(string dataId = null, string startTime = null, string endTime = null, string senderOpenUserId = null, int pageSize = 20, int pageNumber = 1)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, apiName = "ActiveRecordObj", dataId, startTime, endTime, senderOpenUserId, pageSize, pageNumber };
            return HttpPostForJson(jsonObj, "查询销售记录列表");
        }


        /// <summary>
        /// 获取销售记录详情
        /// 正常返回
        /// {
        ///  "errorCode": 0,                                   返回码
        ///  "errorMessage": "OK",                             对返回码的文本描述内容
        ///  "errorDescription": "success",                    对返回码的文本描述内容
        ///  "salesRecorders": 
        ///  {
        ///    ...
        ///  }
        /// }
        /// </summary>
        /// <param name="salesRecorderId">销售记录Id</param>
        /// <returns></returns>
        public static JObject QuerySalesRecorderById(int salesRecorderId)
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId, salesRecorderId };
            return HttpPostForJson(jsonObj, "获取销售记录详情");
        }

        /// <summary>
        /// 获取销售记录类型
        /// 正常返回:
        /// {
        ///   "errorCode": 0,               返回码
        ///   "errorMessage": "success",    对返回码的文本描述内容
        ///   "salesRecorderTypes": [       销售记录类型
        ///       {
        ///          "id": "",              ID
        ///          "name": "",            名称
        ///       },
        ///        ...
        ///     ]
        /// }
        /// </summary>
        /// <returns></returns>
        public static JObject QuerySalesRecorderType()
        {
            VerifyCorpMsg_currentOpenUserIdNotNull();
            object jsonObj = new { corpAccessToken, corpId, currentOpenUserId };
            return HttpPostForJson(jsonObj, "获取销售记录类型");
        }
        #endregion

        #endregion


        /// <summary>
        /// 调用频率控制
        /// Open API 的访问频次分时段控制规则:
        /// 00:00-07:00不能超过100次/20秒;
        /// 07:00-24:00不能超过60次/20秒。(某些接口可以达到80次【"{\"errorCode\":30004,\"errorMessage\":\"fktest251 call model crm.base.objectdescribe times is out of limit 80 every 20 seconds!\"}"】)
        /// 控制原理:调用前获取当前时间前N次的调用时间(N为当前时间段内的频率控制值 次/20s):
        ///         1.未够N次则不限制;
        ///         2.超过N次时算出首次时间与当前时间的时间差
        ///             时间差 >= 20s时,相当于频率未超过上限,不限制本次接口调用
        ///             时间差 < 20s时,频率就超过当前时段的频率上限,将时间差作为本次接口调用的等待时间
        /// </summary>
        private static void WaitForAskFrequency()
        {
            // 07:00-24:00不能超过60次/20秒;(经测试号实验65次/20s也可以,也许是66/20=3.3而65/20=3.25的缘故)
            //00:00 - 07:00不能超过100次 / 20秒;(借↑推算,当前时段100/20=5,5.25*20=105次/20s)
            int timePer20Sec = DateTime.Now.Hour < 7 ? 100 : 60;
            lock (AskTImeListlockObj)// 获取首个时间
            {
                if (askTimes.Count >= timePer20Sec - 1)// 从7点开始次数允许增加
                {
                    DateTime firstAskTime = askTimes.Dequeue();
                    long diffTime = (DateTime.Now.Ticks - firstAskTime.Ticks) / 10000;// 当前时间与首个记录时间的时间差(1Ticks = 0.0001毫秒)
                    //算法二: int ts = (int)new TimeSpan(DateTime.Now.Ticks).Subtract(new TimeSpan(firstAskTime)).TotalMilliseconds;

                    if (diffTime > 0 && diffTime < 20 * 1000)
                    {
                        int waitTime = 20 * 1000 - (int)diffTime;// 需等待时间ms
                        LogHelper.logger.Info("当前纷享接口调用频率为:" + (timePer20Sec * 20 * 1000 / diffTime) + "次/20s,当前时段接口频率限定为" + timePer20Sec + "次/20s,请等待:" + waitTime + "ms");
                        //new Thread(new ThreadStart(()=> {
                        //    long time = waitTime;
                        //    while (time > 1000) {
                        //        Thread.Sleep(1000);
                        //        time =- 1000;
                        //         logger.Info("需等待:" + time + "ms");
                        //    }
                        //})).Start();
                        //totalWaitTime += waitTime;// test
                        Thread.Sleep(waitTime);
                    }
                }
                askTimes.Enqueue(DateTime.Now);
                //totalAskTimes++;// test
            }
        }

        /// <summary>
        /// 接口返回结果转换及校验
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        private static JObject JsonConvert_Verification(JObject jObject, int errorCode, string ImpName, long takeTime, object jsonObj)
        {
            string errorCodeMsg;// 全局返回码信息
            try
            {
                errorCodeMsg = errorCodeMsgs[errorCode];
            }
            catch (Exception ex)
            {
                errorCodeMsg = "由于:" + ex.Message + "\n" + "未能识别错误码【" + errorCode + "】";
            }

            if (errorCode != 0)
            {
                string parameterMsg = "";
                if (jsonObj != null)
                    parameterMsg = "参数信息:" + JObject.FromObject(jsonObj) + "\n";

                string errorMsg = jObject["errorMessage"] + "\n错误码信息:" + errorCodeMsg;
                if (jObject["errorDescription"] != null)
                    errorMsg += "\n" + jObject["errorDescription"];

                throw new Exception("【" + ImpName + "】接口(" + takeTime + "ms):\n" + parameterMsg + "返回异常(errorCode=" + errorCode + "):" + errorMsg);
            }
            else
            {
                string errorMsg = "";
                if (jObject["errorDescription"] != null)
                    errorMsg += "-->" + jObject["errorDescription"];

                LogHelper.logger.Info("【" + ImpName + "】接口(" + takeTime + "ms)返回:【" + jObject["errorMessage"].ToString() + errorMsg + "】→" + errorCodeMsg);
            }
            return jObject;
        }

        /// <summary>
        /// 获取描述信息
        /// </summary>
        /// <param name="enumParm"></param>
        /// <returns></returns>
        private static string GetEnumDescription(Enum enumParm)
        {
            Type type = enumParm.GetType();
            MemberInfo[] memInfo = type.GetMember(enumParm.ToString());
            if (memInfo != null && memInfo.Length > 0)
            {
                object[] attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
                if (attrs != null && attrs.Length > 0)
                    return ((DescriptionAttribute)attrs[0]).Description;
            }
            return enumParm.ToString();
        }

        /// <summary>
        /// 利用反射来判断对象是否包含某个属性(废弃)
        /// </summary>
        /// <param name="instance">object</param>
        /// <param name="propertyName">需要判断的属性</param>
        /// <returns>是否包含</returns>
        private static bool ContainProperty(object instance, string propertyName)
        {
            if (instance != null && !string.IsNullOrEmpty(propertyName))
            {
                PropertyInfo _findedPropertyInfo = instance.GetType().GetProperty(propertyName);
                return (_findedPropertyInfo != null);
            }
            return false;
        }

        /// <summary>
        /// 验证corpAccessToken【企业应用ID】、corpId【企业应用凭证密钥】是否非空
        /// </summary>
        private static void VerifyCorpMsgNotNull()
        {
            if (string.IsNullOrWhiteSpace(corpAccessToken))
                throw new ArgumentException("【企业应用ID】不允许为空", nameof(corpAccessToken));
            else if (string.IsNullOrWhiteSpace(corpId))
                throw new ArgumentException("【企业应用凭证密钥】不允许为空", nameof(corpId));
        }

        /// <summary>
        /// 验证corpAccessToken【企业应用ID】、corpId【企业应用凭证密钥】、当前操作人的openUserId 是否非空
        /// </summary>
        private static void VerifyCorpMsg_currentOpenUserIdNotNull()
        {
            VerifyCorpMsgNotNull();
            if (string.IsNullOrWhiteSpace(currentOpenUserId))
                throw new ArgumentException("【当前操作人的openUserId】不允许为空", nameof(currentOpenUserId));
        }

        /// <summary>
        /// 验证字符串集合及其元素是否为空(最多验两层)
        /// </summary>
        /// <param name="parameterName">参数中文名称</param>
        /// <param name="nameOfParameter">参数名</param>
        /// <param name="list"></param>
        private static void VerifyArray_ElementNotNull<T>(string parameterName, string nameOfParameter, List<T> list)
        {
            if (list == null || !list.Any())
                throw new ArgumentException("【" + parameterName + "】及其元素内容不允许为空", nameOfParameter);

            foreach (var item in list)
                if (item is string)
                    if (string.IsNullOrWhiteSpace(item as string))
                        throw new ArgumentException("【" + parameterName + "】及其元素内容不允许为空", nameOfParameter);
                    else if (item is List<string>)
                        VerifyArray_ElementNotNull(parameterName, nameOfParameter, item as List<string>);
        }

        /// <summary>
        /// 发送Json请求并返回
        /// </summary>
        /// <param name="jsonObj">接口参数</param>
        /// <param name="apiInterfaceName">接口名称</param>
        /// <returns></returns>
        private static JObject HttpPostForJson(object jsonObj, string apiInterfaceName)
        {
            string url = APIname_url[apiInterfaceName];
            return HttpPostForJson(jsonObj, url, apiInterfaceName);
        }

        /// <summary>
        /// 发送Json请求并返回
        /// </summary>
        /// <param name="jsonObj">接口参数</param>
        /// <param name="url">接口地址</param>
        /// <param name="apiInterfaceName">接口名称</param>
        /// <returns></returns>
        private static JObject HttpPostForJson(object jsonObj, string url, string apiInterfaceName)
        {
            LogHelper.logger.Info("开始调用纷享【" + apiInterfaceName + "】接口...");
            JObject resultJobject = null;
            while (true)
            {
                if (!apiInterfaceName.EndsWith("CorpAccessToken"))// 获取接口调用凭证时不受暂停影响
                    WaitForPause(apiInterfaceName);

                WaitForAskFrequency();// 限制调用频率
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();// 开始计算耗时ms
                string result;
                try
                {
                    result = HttpHelper.HttpPostForJson(url, jsonObj);
                }
                catch (Exception ex)
                {
                    if (ex.Message == "操作超时")
                    {
                        string errorMsg = "纷享接口【" + apiInterfaceName + "】操作超时!稍后重新尝试...";
                        if (jsonObj != null)
                            errorMsg += "\n参数信息:" + JObject.FromObject(jsonObj);
                        Thread.Sleep(10 * 1000);
                        LogHelper.logger.Error(errorMsg, ex);
                        LogHelper.SendMsg(errorMsg, ex);
                        continue;
                    }
                    else
                        throw;
                }

                JObject jObject = JsonConvert.DeserializeObject<JObject>(result);
                int errorCode = int.Parse(jObject["errorCode"].ToString().Trim());
                if (errorCode == 30004)// 频率过高
                {
                    string errorMsg = "调用纷享接口【" + apiInterfaceName + "】时【" + errorCodeMsgs[errorCode] + "】!稍后重新尝试...";
                    LogHelper.logger.Error(errorMsg);
                    Thread.Sleep(10 * 1000);
                    LogHelper.SendMsg(errorMsg);
                    continue;
                }
                else if (errorCode == 20016)// token过期:由于过期时间一致,导致多个未访问的过期接口参数线程同时访问
                {
                    string errorMsg = "调用纷享接口【" + apiInterfaceName + "】时【" + errorCodeMsgs[errorCode] + "】!稍后尝试重新同步...";
                    LogHelper.logger.Error(errorMsg);
                    Thread.Sleep(10 * 1000);
                    GetCorpAccessToken(SynParams.FX_appId, SynParams.FX_appSecret, SynParams.FX_permanentCode);
                    LogHelper.SendMsg(errorMsg);
                    if (SynParams.synRestartStatus == SynRestartStatus.Standard)
                        SynParams.synRestartStatus = SynRestartStatus.WaitingForRestart;
                    break;
                }
                else
                {
                    resultJobject = JsonConvert_Verification(jObject, errorCode, apiInterfaceName, stopwatch.ElapsedMilliseconds, jsonObj);
                    LogHelper.logger.Info("纷享【" + apiInterfaceName + "】接口调用成功!");
                    break;
                }
            }
            return resultJobject;
        }

        /// <summary>
        /// 定时更新CorpAccessToken
        /// </summary>
        public static void AutoUpdateCorpAccess(string appId, string appSecret, string permanentCode)
        {
            GetCorpAccessToken(appId, appSecret, permanentCode);
            LogHelper.logger.Info("身份验证信息如下:\ncorpAccessToken:" + corpAccessToken + "\ncorpId:" + corpId + "\n剩余有效时间: " + StringHelper.LongToTimeString(expiresIn));
            LogHelper.SendMsg("身份验证信息如下:\ncorpAccessToken:" + corpAccessToken + "\ncorpId:" + corpId + "\n剩余有效时间: " + StringHelper.LongToTimeString(expiresIn));
            new Thread(new ThreadStart(() =>
            {
                while (true)
                {
                    Thread.Sleep(expiresIn * 1000);
                    try
                    {
                        isPause = true;
                        Thread.Sleep(1000);// 防止再次获取到相同验证信息,先停留1s
                        GetCorpAccessToken(appId, appSecret, permanentCode);
                        isPause = false;
                        LogHelper.logger.Info("身份验证信息如下:\ncorpAccessToken:" + corpAccessToken + "\ncorpId:" + corpId + "\n剩余有效时间: " + StringHelper.LongToTimeString(expiresIn));
                        LogHelper.SendMsg("身份验证信息如下:\ncorpAccessToken:" + corpAccessToken + "\ncorpId:" + corpId + "\n剩余有效时间: " + StringHelper.LongToTimeString(expiresIn));
                    }
                    catch (Exception ex)
                    {
                        LogHelper.logger.Error("获取CorpAccessToken异常!", ex);
                        LogHelper.SendMsg("获取CorpAccessToken异常!", ex);
                        Thread.Sleep(10 * 1000);// 10s后重试 
                                                // TODO 达到重试次数后...
                    }
                }
            })).Start();
        }

        /// <summary>
        /// 根据字段名称获取纷享对象字段值
        /// </summary>
        /// <param name="fieldName"></param>
        /// <returns></returns>
        public static string GetValue(JToken jToken, string fieldName)
        {
            string result = null;
            if (jToken == null || jToken[fieldName] == null)
                return null;
            else if (jToken[fieldName].HasValues)
            {
                foreach (JToken item in jToken[fieldName].Children())
                {
                    string val = Regex.Replace(item + "" + "", @"[\r\n]", "");
                    result += StringHelper.RemoveDoubleQuotes(val).Replace("\\\"", "\"") + ",";
                }
                return result.TrimEnd(',');
            }

            result = Regex.Replace(jToken[fieldName] + "", @"[\r\n]", "");
            return StringHelper.RemoveDoubleQuotes(result).Replace("\\\"", "\"");
        }

        /// <summary>
        /// 根据字段名称获取纷享对象字段值
        /// </summary>
        /// <param name="fieldName"></param>
        /// <returns></returns>
        public static string GetValue(string value)
        {
            if (string.IsNullOrEmpty(value))
                return null;

            string result = Regex.Replace(value, @"[\r\n]", "");
            return StringHelper.RemoveDoubleQuotes(result).Replace("\\\"", "\"");
        }

        /// <summary>
        /// 根据客户名称获取纷享客户id
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static Dictionary<string, string> QueryAccountMsgByName(string name)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentNullException("name", "客户名称为空!");

            Dictionary<string, string> msg = new Dictionary<string, string>();
            try
            {
                JObject accountObj = QueryObjectList(FxObjApiName.AccountObj, new List<Filter>() { new Filter("name", name) }, new List<string>() { "_id", "data_own_department", "owner" }, 1, 0, new List<Order> { new Order() },true);
                string total = StringHelper.RemoveDoubleQuotes(accountObj["data"]["total"].ToString());
                if ("1" == total)
                {
                    JToken msgJToken = accountObj["data"]["dataList"][0];
                    msg.Add("id", GetValue(msgJToken["_id"].ToString()));
                    msg.Add("owner", GetValue(msgJToken["owner"][0].ToString()));
                }
                else
                    throw new Exception("纷享系统中名为【" + name + "】客户数量为【" + total+"】");
            }
            catch (Exception ex)
            {
                throw new Exception("根据金蝶客户名称【" + name + "】查询纷享客户时发生异常!", ex);
            }
            return msg;
        }

        /// <summary>
        /// 纷享日期存储值转换(时分秒不准确)
        /// </summary>
        /// <param name="dateString"></param>
        /// <param name="dateFormat"></param>
        /// <returns></returns>
        public static long GetDateTimeByString(string dateString, string dateFormat)
        {
            DateTime startTime = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1, 0, 0, 0, 0), TimeZoneInfo.Local);
            if (string.IsNullOrWhiteSpace(dateString))
                return 0;

            DateTimeFormatInfo dateTimeFormatInfo = new DateTimeFormatInfo()
            {
                ShortDatePattern = dateFormat,
            };
            DateTime dateTime = Convert.ToDateTime(dateString.Split('T')[0], dateTimeFormatInfo);
            return (dateTime.Ticks - startTime.Ticks) / 10000;
        }

        /// <summary>
        /// 暂停等待
        /// </summary>
        public static void WaitForPause(string apiInterfaceName)
        {
            bool hasPause = isPause;
            if (isPause)
                LogHelper.logger.Info("已暂停访问【" + apiInterfaceName + "】接口...");

            while (isPause)
                Thread.Sleep(500);

            if (hasPause)
                LogHelper.logger.Info("即将继续访问【" + apiInterfaceName + "】接口...");
        }
    }

    /// <summary>
    /// 纷享接口列表
    /// </summary>
    public class Fx_Form
    {
        public Fx_Form() { }
        public Fx_Form(string label, string value)
        {
            this.label = label;
            this.value = value;
        }

        /// <summary>
        /// 列表标签
        /// </summary>
        public string label { get; set; }

        /// <summary>
        /// 列表值
        /// </summary>
        public string value { get; set; }
    }

    /// <summary>
    /// 上下架状态 1-上架、2-下架
    /// </summary>
    public enum ProductStatus
    {
        [Description("上架")]
        onShelve = 1,
        [Description("下架")]
        offShelveock = 2
    }

    /// <summary>
    /// 解锁/锁定动作
    /// </summary>
    public enum LockAction
    {
        [Description("锁定")]
        @lock = 1,
        [Description("解锁")]
        unlock = 2
    }

    /// <summary>
    /// 过滤条件
    /// </summary>
    public class Filter
    {
        public Filter(string field_name, List<string> field_values, Operator Operator)
        {
            this.field_name = field_name;
            this.field_values = field_values;
            this.Operator = Operator;
        }

        /// <summary>
        /// 单值精准查询
        /// </summary>
        /// <param name="field_name"></param>
        /// <param name="value"></param>
        public Filter(string field_name, string value)
        {
            this.field_name = field_name;
            this.field_values = new List<string>() { value };
            this.Operator = Operator.EQ;
        }

        public Filter() { }

        /// <summary>
        /// 字段名
        /// </summary>
        public string field_name { get; set; }

        /// <summary>
        /// 取值范围
        /// </summary>
        public List<string> field_values { get; set; }

        /// <summary>
        /// 支持操作
        /// </summary>
        public string @operator
        {
            get { return Operator.ToString(); }
            set { @operator = value; }
        }

        public Operator Operator { get; set; }
    }

    /// <summary>
    /// 排序
    /// </summary>
    public class Order
    {
        public Order(string fieldName = "_id", bool isAsc = true)
        {
            this.fieldName = fieldName;
            this.isAsc = isAsc;
        }

        /// <summary>
        /// 字段名
        /// </summary>
        public string fieldName { get; set; }

        /// <summary>
        /// 是否正序
        /// </summary>
        public bool isAsc = true;

        /// <summary>
        /// 是否正序
        /// </summary>
        public bool ascending
        {
            get { return isAsc; }
            set { isAsc = value; }
        }
    }

    /// <summary>
    /// 区间条件[{ fieldName, from,to}]
    /// </summary>
    public class RangeCondition
    {
        /// <summary>
        /// 字段名称
        /// </summary>
        public string fieldName { get; set; }

        /// <summary>
        /// 区间头
        /// </summary>
        public string from { get; set; }

        /// <summary>
        /// 区间尾
        /// </summary>
        public string to { get; set; }
    }

    /// <summary>
    /// 支持操作
    /// </summary>
    public enum Operator
    {
        [Description("等于【=】")]
        EQ = 1,

        [Description("小于【<】")]
        LT,

        [Description("小于等于【<=】")]
        LTE,

        [Description("大于等于【<】")]
        GT,

        [Description("大于等于【>=】")]
        GTE,

        [Description("不等于【<>】")]
        N,

        [Description("相似【like %??%】")]
        LIKE,

        [Description("非相似【not like %??%】")]
        NLIKE,

        [Description("是【is ??】")]
        IS,

        [Description("不是【is not ??】")]
        ISN,

        [Description("包括【in('','',...)】")]
        IN,

        [Description("不包括【not in('','',...)】")]
        NIN,

        [Description("在...之间【between ?? and ??】")]
        BETWEEN,

        [Description("不在...之间【not between ?? and ??】")]
        NBETWEEN,

        [Description("以...开头【like ??%】")]
        STARTWITH,

        [Description("以...结尾【like %??】")]
        ENDWITH,

        /// <summary>
        /// API中未有详细用法
        /// </summary>
        [Description("包含【Array 包含】")]
        CONTAINS,

    }

    /// <summary>
    /// CRM日志查询条件
    /// </summary>
    public class CRMLogConditions
    {
        /// <summary>
        /// 比较方式
        /// </summary>
        public CRMLogComparison Comparison { get; set; }

        /// <summary>
        /// 比较字段
        /// </summary>
        public CRMLogFieldName FieldName { get; set; }

        /// <summary>
        /// 比较值
        /// </summary>
        public string FilterValue { get; set; }
    }

    /// <summary>
    /// CRM日志查询行为
    /// </summary>
    public enum CRMLogBizOperationName
    {
        [Description("新建")]
        NEW = 1,

        [Description("编辑")]
        EDIT = 2,

        [Description("作废")]
        INVALID = 3,

        [Description("恢复")]
        RECOVERY = 4,

        [Description("删除")]
        DEL = 5,

        [Description("更换负责人")]
        CHANGEOWNER = 6,

        [Description("添加相关团队成员")]
        ADDRELATEDTEAMMEMBERS = 7,

        [Description("移除相关团队成员")]
        REMOVERELATEDTEAMMEMBERS = 8,

        [Description("分配")]
        DISTRIBUTION = 9,

        [Description("收回")]
        TAKEBACK = 10,

        [Description("导入")]
        IMPORT = 13,

        [Description("重用-新建")]
        RENEW = 14,

        [Description("通过修改记录恢复")]
        RESTOREBYMODIFYINGRECORDS = 15,

        [Description("重用")]
        REUSING = 16,

        [Description("设置协访")]
        SETINTERVIEW = 17,

        [Description("确认")]
        CONFIRM = 18,

        [Description("驳回")]
        REJECT = 19,

        [Description("撤回")]
        WITHDRAW = 20,

        [Description("转移")]
        TRANSFER = 21,

        [Description("转移出公海")]
        MOVEOUTHIGHSEAS = 22,

        [Description("启用")]
        ENABLE = 25,

        [Description("停用")]
        DISABLE = 26,

        [Description("编辑相关团队成员")]
        EDITMEMBERS = 27,

        [Description("添加地址")]
        ADDADDRRESS = 28,

        [Description("编辑地址")]
        EDITADDRRESS = 29,

        [Description("删除地址")]
        DELADDRRESS = 30,

        [Description("添加财务信息")]
        ADDFINANCIAL = 31,

        [Description("编辑财务信息")]
        EDITFINANCIAL = 32,

        [Description("删除财务信息")]
        DELFINANCIAL = 33,

        [Description("自动收回")]
        AUTORECOVER = 34,

        [Description("处理")]
        HANDLE = 37,

        [Description("确认发货")]
        CONFIRMDELIVERY = 38,

        [Description("确认收货")]
        CONFIRMRECEIVE = 39,

        [Description("重置")]
        RESET = 40,

        [Description("审批提交")]
        APPROVALSUBMISSION = 41,

        [Description("审批撤回")]
        APPROVALWITHDRAWAL = 42,

        [Description("取消操作")]
        CANCELOPERATION = 43,

        [Description("审批确认")]
        APPROVALCONFIRMATION = 44,

        [Description("审批驳回")]
        APPROVALREJECTED = 45,

        [Description("加锁")]
        LOCK = 46,

        [Description("更换合作伙伴")]
        CHANGINGPARTNERS = 50,

        [Description("更换合作伙伴负责人")]
        CHANGEPARTNEROWNER = 51,

        [Description("移除合作伙伴")]
        REMOVEPARTNER = 52,

        [Description("修改成交状态")]
        MODIFYTRANSACTIONSTATUS = 54,
    }

    /// <summary>
    /// CRM日志查询条件字段
    /// </summary>
    public enum CRMLogFieldName
    {
        [Description("操作人,CRMLogComparison只能取【7】")]
        UserName,

        [Description("操作时间,CRMLogComparison只能取【2,4,6】")]
        operationTime,

        [Description("行为")]
        bizOperationName,

        [Description("操作内容,CRMLogComparison只能取【7】")]
        textMessage,
    }

    /// <summary>
    /// 查询CRM日志比较方式Comparison 
    /// </summary>
    public enum CRMLogComparison
    {
        [Description("等于")]
        EQ = 1,

        [Description("不等于")]
        N = 2,

        [Description("大于等于")]
        GTE = 4,

        [Description("小于等于")]
        LTE = 6,

        [Description("包含")]
        IN = 7,
    }

    /// <summary>
    /// 查询CRM日志对象id列表
    /// </summary>
    public enum CRMLogFilterMainID
    {
        [Description("线索")]
        LeadsObj = 1,

        [Description("客户")]
        AccountObj = 2,

        [Description("")]
        ContactObj = 3,

        [Description("产品")]
        ProductObj = 4,

        [Description("退款")]
        RefundObj = 6,

        [Description("销售行为")]
        SaleActionObj = 7,

        [Description("商机")]
        OpportunityObj = 8,

        [Description("")]
        InvoiceApplicationObj = 9,

        [Description("销售订单")]
        SalesOrderObj = 11,

        [Description("")]
        ReturnedGoodsInvoiceObj = 12,

        [Description("访问")]
        VisitingObj = 13,

        [Description("")]
        ContractObj = 16,

        [Description("线索池")]
        LeadsPoolObj = 17,

        [Description("")]
        HighSeasObj = 18,

        [Description("")]
        MarketingEventObj = 20,

        [Description("")]
        AccountAttObj = 24,

        [Description("")]
        AccountCostObj = 26,

        [Description("")]
        ReturnedGoodsInvoiceProductObj = 27,

        [Description("")]
        SalesOrderProductObj = 28,

        [Description("添加账户")]
        AccountAddObj = 39,

        [Description("")]
        AccountFinInfoObj = 40,
    }
}



方法调用(业务代码)

new Thread(new ThreadStart(() =>
{
    while (true)
    {
        try
        {
            JObject jObject = FX_Helper.GetCorpAccessToken(FX_Helper.testAppId, FX_Helper.testAppSecret, FX_Helper.testPermanentCode);
            Console.WriteLine("corpAccessToken:" + FX_Helper.corpAccessToken);
            Console.WriteLine("corpId:" + FX_Helper.corpId);
            Console.WriteLine();
            Thread.Sleep(2 * 60 * 60 * 1000 - 5 * 1000);// 2小时内有效,每(2小时-5s)更新一次(可以用整个测试访问频率限制)
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            Thread.Sleep(10 * 1000);// 10s后重试 
            // TODO 达到重试次数后...
        }
    }
})).Start();

疑问

1.这个Code怎么拿的,有看客知道不?

在这里插入图片描述

2.API调用次数限制不太准,意义有点不明

在这里插入图片描述
我当天运行了好几百次,但后台统计才增加了十几次,不太明白他们后台是怎么算的?
按客服说法是从购买到目前的,那就更加不可能,我们测试都起码上千次访问了
(该不是后台多线程没加锁吧doge)
在这里插入图片描述
但从每天刷的数据看应该一天一刷(虽然显示数与我调用数少很多)
在这里插入图片描述

目前发现的API文档描述错误(仅测试账号范围)

1.应该是userList,【l】应该为大写【L】,另一个接口一样的描述错误。

在这里插入图片描述
在这里插入图片描述

2.自相矛盾的描述错误

在这里插入图片描述

3.示例格式错误

直接在http://www.bejson.com/能验出来
在这里插入图片描述
估计是问题太小根本没引起重视。。。。不知道大家看的时候改了没?

在这里插入图片描述在这里插入图片描述
不过有一说一,偶尔还是会有修复回应的(时隔2天)
在这里插入图片描述在这里插入图片描述

4.消息接口

APP端的复合消息是有字数限制的,但开发文档没提到
在这里插入图片描述
APP预览显示如下:
在这里插入图片描述
PC/WEB 显示预览如下:
在这里插入图片描述

PS.吐槽1:最近(2021.06.11)更新的也有一堆低级问题

每次看纷享的开发文档遇到问题找客服,都有股三流公司那种【一流客服公关,三流技术后台】的弱者气息…
要是这些问题刚好在用到的接口,只能求客服能尽快转达到他们技术得到反馈…
在这里插入图片描述
在这里插入图片描述

PS.吐槽2:最近(2021.09.30)报表问题

当你选择折线图的时候系统会默认不显示数值为0的数据;
每个报表的显示数据点的上限是2000个;
在这里插入图片描述
在这里插入图片描述
可以看到整天的数据间隔并不均匀,就是因为客服说的原因;
而整天的数据只能显示到5个多小时,就是因为一个图只能显示2000个点造成的

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值