HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

【经验分享】【微信小程序】简单在已有账号系统基础上引入uni-id使用clientDB的权限管理

微信小程序

折腾了一下午,终于给我的小程序成功引入了uni-id,之前每次想到要迁移就头大,今天终于下定决心换到clientDB了,也是为了减少云函数的用量。在这里就简单分享一下校验。

参考:https://en.uniapp.dcloud.io/uniCloud/uni-id.html(这个教程是老的uni-id,对我够用了,不想迁移到新版)

安装uni-id

  1. 导入插件。
  2. 写config-center的配置,复制上面教程的文件只需要改mp-weixin.oauth.weixin内的appid和secret。
  3. 创建uni-id-users和opendb-verify-codes表,后者用于验证码对小程序其实不必要,但我不知道删了能不能跑。

以上几步可以简单的安装好uni-id,然后便是迁移工作了。

迁移

首先我希望我的权限是依赖于openid的,所以我便利了我自己搭建的账号数据库,并逐条写入uni-id-user表,只需要写两个字段_id和wx_openid.mp-weixin,这两个字段同时设置为原先的openid。

登录

之后按照文档的微信登录段,新建一个云函数,内容如下

const uniID = require("uni-id");  
exports.main = async function (event, context) {  
    // const res = await uniID.loginByWeixin(event.code)  
    const res = await uniID.loginByWeixin({  
        code: event.code,  
    });  
    return res;  
};

注意这个云函数要选择公共依赖uni-id,安装的uni-id也需要选择公共依赖uni-open-bridge-common(我不知道为什么默认没选)。
以上是官方文档的,我将其修改为了

'use strict';  
const uniID = require('uni-id');  

const db = uniCloud.database();  
const dbUID = db.collection('uni-id-users');  
exports.main = async function (event) {  
    const res = await uniID.loginByWeixin({ code: event.code });  
    if (res.uid !== res.openid) {  
        await dbUID.doc(res.openid).set((await dbUID.doc(res.uid).field({ _id: false }).get()).data[0]);  
        await dbUID.doc(res.uid).remove();  
        res.uid = res.openid;  
        res.tokenExpired = 0;  
    }  
    return res;  
};

因为默认第一次登录的账户他会新建一条记录,而这条记录的uid不是原有的openid,所以需要手动修改。
同时修改以后原先的token是作废的,所以使用tokenExpired=0来让客户端手动重新二次调用。(每次都需要上传一个只能用一次的code)

然后我客户端用这个云函数取代了我原先用于获取openid的云函数,因为这个也会返回openid,正好还能登录一举两得。

角色

另一方面为了用上uni-id的角色系统,我还在我自己的关于权限变更的云函数里同时变更uni-id-users表的role字段,这样clientDB也可以用角色控制了xd

继续阅读 »

折腾了一下午,终于给我的小程序成功引入了uni-id,之前每次想到要迁移就头大,今天终于下定决心换到clientDB了,也是为了减少云函数的用量。在这里就简单分享一下校验。

参考:https://en.uniapp.dcloud.io/uniCloud/uni-id.html(这个教程是老的uni-id,对我够用了,不想迁移到新版)

安装uni-id

  1. 导入插件。
  2. 写config-center的配置,复制上面教程的文件只需要改mp-weixin.oauth.weixin内的appid和secret。
  3. 创建uni-id-users和opendb-verify-codes表,后者用于验证码对小程序其实不必要,但我不知道删了能不能跑。

以上几步可以简单的安装好uni-id,然后便是迁移工作了。

迁移

首先我希望我的权限是依赖于openid的,所以我便利了我自己搭建的账号数据库,并逐条写入uni-id-user表,只需要写两个字段_id和wx_openid.mp-weixin,这两个字段同时设置为原先的openid。

登录

之后按照文档的微信登录段,新建一个云函数,内容如下

const uniID = require("uni-id");  
exports.main = async function (event, context) {  
    // const res = await uniID.loginByWeixin(event.code)  
    const res = await uniID.loginByWeixin({  
        code: event.code,  
    });  
    return res;  
};

注意这个云函数要选择公共依赖uni-id,安装的uni-id也需要选择公共依赖uni-open-bridge-common(我不知道为什么默认没选)。
以上是官方文档的,我将其修改为了

'use strict';  
const uniID = require('uni-id');  

const db = uniCloud.database();  
const dbUID = db.collection('uni-id-users');  
exports.main = async function (event) {  
    const res = await uniID.loginByWeixin({ code: event.code });  
    if (res.uid !== res.openid) {  
        await dbUID.doc(res.openid).set((await dbUID.doc(res.uid).field({ _id: false }).get()).data[0]);  
        await dbUID.doc(res.uid).remove();  
        res.uid = res.openid;  
        res.tokenExpired = 0;  
    }  
    return res;  
};

因为默认第一次登录的账户他会新建一条记录,而这条记录的uid不是原有的openid,所以需要手动修改。
同时修改以后原先的token是作废的,所以使用tokenExpired=0来让客户端手动重新二次调用。(每次都需要上传一个只能用一次的code)

然后我客户端用这个云函数取代了我原先用于获取openid的云函数,因为这个也会返回openid,正好还能登录一举两得。

角色

另一方面为了用上uni-id的角色系统,我还在我自己的关于权限变更的云函数里同时变更uni-id-users表的role字段,这样clientDB也可以用角色控制了xd

收起阅读 »

uniCloud 短信服务1001报错问题

微信小程序 uni_app 短信

阿里云短信函数设置了定时触发,会有1001的报错,报错信息是smsKey不能为空,官方文档也标注了更新至最新后不需要填写smsKey,解决办法是在HBuilder X里,选中短信发送函数,右击后选中管理公共模块或扩展库依赖,然后勾选上uni-cloud-sms并确定即可

继续阅读 »

阿里云短信函数设置了定时触发,会有1001的报错,报错信息是smsKey不能为空,官方文档也标注了更新至最新后不需要填写smsKey,解决办法是在HBuilder X里,选中短信发送函数,右击后选中管理公共模块或扩展库依赖,然后勾选上uni-cloud-sms并确定即可

收起阅读 »

关于uni手机号一键登录价格调整的公告

一键登录

因三大运营商的集体涨价,自2024年10月01日零时起,uni手机号一键登录服务的价格将进行调整。

这是一次全行业涨价,其他如阿里云一键登陆等也都涨价了。

调整后的价格如下所示:

按量计费

按量价格为:0.03元/次

资源包

规格(次数) 价格(元) 相对按量计费单价(元/次)
1万 290 0.029
2万 570 0.0285
5万 1400 0.028
10万 2750 0.0275
20万 5400 0.027
50万 13250 0.0265
100万 26000 0.026
200万 51000 0.0255
500万 125000 0.025

注意:资源包自购买之日起6个月内有效。

调价后,uni一键登录依然比其他云厂商的竞品便宜

  • 阿里云

阿里云的号码验证(一键登录)按量计费起步定价为4分/次,比uni一键登录的3分/次贵了33%;

阿里云一键登录需月消耗需超过500万次后,才可达到3分/次的单价,实际上大多开发者的App,月消耗是远到不了500万次的,详见阿里云 - 号码认证 - 产品计费

阿里云的套餐包,单价也比uni一键登录要贵很多,以阿里云官网单价最低的100万次套餐为例:

100万次的一键登录套餐,阿里云比uni一键登录要贵6500元:

厂商 价格(元) 单价(元/次)
阿里云 32500 0.0325
uni一键登录 26000 0.026

实际上,阿里云100万次的套餐包,甚至比uni一键登录的按量计费还要贵不少。

  • 腾讯云

腾讯云的号码验证(一键登录)不支持在线自助开通,需要先填写申请表单,等待审核,且腾讯云仅支持按量计费,不支持资源包。

腾讯云的按量定价和阿里云一样,也是4分/次起步,同样比uni一键登录要贵33%;

详见:腾讯云号码认证 - 计费概述

即便加上uniCloud云函数的费用,uni一键登陆也更便宜。详见

总体而言,不管是按量计费,还是套餐包,uni一键登录都对中小开发者更为友好!

调价后,uni一键登录依然比短信更具性价比

uni一键登录相比短信验证码,具备明显的优势:

  • 用户体验好:一键登录,无需等待和复制短信验证码,能有效降低用户流失率,提升用户注册转换率;
  • 便宜:虽然调价了,但uni一键登录,平均每次验证仅需2分多,比短信验证码更为便宜;
  • 安全:采用运营商网关认证,避免短信劫持,有效提升安全性

相对短信验证码,我们更推荐开发者使用一键登录。

继续阅读 »

因三大运营商的集体涨价,自2024年10月01日零时起,uni手机号一键登录服务的价格将进行调整。

这是一次全行业涨价,其他如阿里云一键登陆等也都涨价了。

调整后的价格如下所示:

按量计费

按量价格为:0.03元/次

资源包

规格(次数) 价格(元) 相对按量计费单价(元/次)
1万 290 0.029
2万 570 0.0285
5万 1400 0.028
10万 2750 0.0275
20万 5400 0.027
50万 13250 0.0265
100万 26000 0.026
200万 51000 0.0255
500万 125000 0.025

注意:资源包自购买之日起6个月内有效。

调价后,uni一键登录依然比其他云厂商的竞品便宜

  • 阿里云

阿里云的号码验证(一键登录)按量计费起步定价为4分/次,比uni一键登录的3分/次贵了33%;

阿里云一键登录需月消耗需超过500万次后,才可达到3分/次的单价,实际上大多开发者的App,月消耗是远到不了500万次的,详见阿里云 - 号码认证 - 产品计费

阿里云的套餐包,单价也比uni一键登录要贵很多,以阿里云官网单价最低的100万次套餐为例:

100万次的一键登录套餐,阿里云比uni一键登录要贵6500元:

厂商 价格(元) 单价(元/次)
阿里云 32500 0.0325
uni一键登录 26000 0.026

实际上,阿里云100万次的套餐包,甚至比uni一键登录的按量计费还要贵不少。

  • 腾讯云

腾讯云的号码验证(一键登录)不支持在线自助开通,需要先填写申请表单,等待审核,且腾讯云仅支持按量计费,不支持资源包。

腾讯云的按量定价和阿里云一样,也是4分/次起步,同样比uni一键登录要贵33%;

详见:腾讯云号码认证 - 计费概述

即便加上uniCloud云函数的费用,uni一键登陆也更便宜。详见

总体而言,不管是按量计费,还是套餐包,uni一键登录都对中小开发者更为友好!

调价后,uni一键登录依然比短信更具性价比

uni一键登录相比短信验证码,具备明显的优势:

  • 用户体验好:一键登录,无需等待和复制短信验证码,能有效降低用户流失率,提升用户注册转换率;
  • 便宜:虽然调价了,但uni一键登录,平均每次验证仅需2分多,比短信验证码更为便宜;
  • 安全:采用运营商网关认证,避免短信劫持,有效提升安全性

相对短信验证码,我们更推荐开发者使用一键登录。

收起阅读 »

【报Bug】我之前发布的文章,为啥下面的评论,你们官方都删除了

隐私

【报Bug】我之前发布的文章,为啥下面的评论,你们官方都删除了???

【报Bug】我之前发布的文章,为啥下面的评论,你们官方都删除了???

uniCloud 外部系统联登 注册功能 C# 完整示例

uniCloud

APP端

this.$http  
        .post(`/uniCloudRegister`, {  
            clientInfo:JSON.stringify(uni.getSystemInfoSync())  
        })  
        .then(res =>{  
            uni.stopPullDownRefresh()  
            uni.hideNavigationBarLoading()  
            console.info(res)  
        }).catch(err => {  
            console.error(err)  
            uni.stopPullDownRefresh()  
            uni.hideNavigationBarLoading()  
        })

后台接口 UserController:ApiController

ConstantsConstants//业务系统登录后才需要联登到 uniCloud,所以不需要在注册时执行,而是单独给出了注册功能的接口  
[HttpPost]  
[Route("uniCloudRegister")]  
public async Task<HttpResponseMessage> uniCloudRegister([FromBody] UniClient client)  
{  
    HttpResponseMessage response = null;  
    try  
    {  
        //此为验证当前系统token并转为 user类的工具,这里就不给出详细示例了  
        var ui = JwtHelper.AnalysisToken(HttpContext.Current.Request);  
        //见 Utils  
        string uniAppUserName = Utils.GetExternalUid(ui);  
        string nonce = Utils.GetNonce();  
        // 获取当前时间戳(精确到毫秒)  
        long timestamp = Utils.GetNowTimeStamp();  
        Dictionary<string, string> paramsDictionary = new Dictionary<string, string>  
        {  
            { "externalUid", uniAppUserName},  
            { "nickname", ui.Name},  
        };  
        //签名算法见 Utils  
        string signature = Utils.GetSignature(paramsDictionary, nonce, timestamp);  
        //client.clientInfo 为JSON.stringify(uni.getSystemInfoSync()) 这里再转回json对象  
        var cliInfo = JsonConvert.DeserializeObject<JObject>(client.clientInfo);  
        Dictionary<string, object> param = new Dictionary<string, object>();  
        param.Add("clientInfo", cliInfo);  
        param.Add("uniIdToken", "");  
        param.Add("params", paramsDictionary);  

        // 将对象序列化为JSON字符串  
        string jsonContent = JsonConvert.SerializeObject(param);  
        Dictionary<string, string> headers = new Dictionary<string, string>()  
        {  
            {"uni-id-nonce",nonce },  
            {"uni-id-timestamp",timestamp + ""},  
            {"uni-id-signature",signature}  
        };  
        var res = await HttpHelper.SendPostAsync(Constants.PushRegister, jsonContent, headers);  
        if (res.Count > 0)  
        {  
            response = Request.CreateResponse(System.Net.HttpStatusCode.OK, res);  
        }  
        else  
        {  
            response = Request.CreateResponse(System.Net.HttpStatusCode.BadRequest, "error");  
        }  
    }  
    catch (Exception ex)  
    {  
        response = Request.CreateErrorResponse(System.Net.HttpStatusCode.InternalServerError, "error");  
    }  
    return response;  
}

Constants

  //uniapp 外部系统联登 https://doc.dcloud.net.cn/uniCloud/uni-id/cloud-object.html#external  
  public static readonly string PushUrl = "you url";  
  //uniapp 注册  
  public static readonly string PushRegister = PushUrl + "externalRegister";  
  //uniapp 登录  
  public static readonly string PushLogin = PushUrl + "externalLogin";  
  //uniapp 修改信息  
  public static readonly string PushUpdateUser = PushUrl + "updateUserInfoByExternal ";

UniClient

 public class UniClient  
  {  
      public string clientInfo { get; set; }  
      public string uniIdToken { get; set; }  
  }

Utils

 /// <summary>  
  /// 根据用户信息 获取uniapp的uid, 这个就根据自己的业务来处理  
  /// </summary>  
  /// <param name="ui"></param>  
  /// <returns></returns>  
  public static string GetExternalUid(UserInfo ui)  
  {  
    //分别为角色ID、 用户ID和用户账号  
    return  ui.Role + "_" + ui.Id + "_" + ui.userName;  
  }  

  //获取随机字符串 这里其实可以固定返回一组字符串  
  public static string GetNonce()  
  {  
      return GenerateRandomStringLinq(8);  
  }  

  private static string GenerateRandomStringLinq(int length)  
  {  
      const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";  
      Random random = new Random();  
      return new string(Enumerable.Repeat(chars, length)  
        .Select(s => s[random.Next(s.Length)]).ToArray());  
  }  

    /// <summary>  
    /// uniapp 鉴权签名算法  
    /// </summary>  
    /// <param name="parameters"></param>  
    /// <param name="nonce"></param>  
    /// <param name="timestamp"></param>  
    /// <returns></returns>  
    public static string GetSignature(Dictionary<string, string> parameters, string nonce, long timestamp)  
    {  
        string paramsStr = GetParamsString(parameters);  
        using (HMACSHA256 hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(Constants.RequestAuthSecret + nonce)))  
        {  
            string message = timestamp.ToString() + paramsStr;  
            byte[] messageBytes = Encoding.UTF8.GetBytes(message);  
            byte[] hashBytes = hmacSha256.ComputeHash(messageBytes);  

            return ByteArrayToHexString(hashBytes).ToUpper();  
        }  
    }  

  /// <summary>  
  /// 获取当前时间戳 单位毫秒  
  /// </summary>  
  /// <returns></returns>  
  public static long GetNowTimeStamp()  
  {  
      return (long)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds;  
  }  

  private static string GetParamsString(Dictionary<string, string> parameters)  
  {  
      var keys = new List<string>(parameters.Keys);  
      keys.Sort();  

      StringBuilder sb = new StringBuilder();  

      for (int i = 0; i < keys.Count; i++)  
      {  
          if (i != 0)  
          {  
              sb.Append("&");  
          }  

          sb.Append(keys[i]).Append("=").Append(parameters[keys[i]]);  
      }  

      return sb.ToString();  
  }  

  private static string ByteArrayToHexString(byte[] bytes)  
  {  
      StringBuilder sb = new StringBuilder();  

      foreach (byte b in bytes)  
      {  
          string hex = b.ToString("x2");  
          sb.Append(hex);  
      }  

      return sb.ToString();  
  }  

HttpHelper

  /// <summary>  
  /// POST异步请求  
  ///   
  /// </summary>  
  /// <param name="url">请求url</param>  
  /// <param name="jsonContent"></param>  
  /// <param name="headers"></param>  
  /// <returns></returns>  
  // 发送POST请求的函数    
  public static async Task<JObject> SendPostAsync(string url, string jsonContent, Dictionary<string, string> headers = null)  
  {  
      using (var client = new HttpClient())  
      {  
          var jsonObject = new JObject();  
          if (headers != null)  
          {  
              foreach (KeyValuePair<string, string> header in headers)  
              {  
                  client.DefaultRequestHeaders.Add(header.Key, header.Value);  
              }  
          }  
          try  
          {  
              // 创建一个HttpContent对象,用于发送JSON数据    
              var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");  

              // 发送POST请求    
              HttpResponseMessage response = await client.PostAsync(url, httpContent);  

              // 确保HTTP请求成功    
              //response.EnsureSuccessStatusCode();  

              // 读取响应内容    
              var responseBody = await response.Content.ReadAsStringAsync();  

              //LoggerHelper.Info("请求:" + url + ",参数:" + jsonContent + ",结果:" + responseBody);  
              jsonObject = JsonConvert.DeserializeObject<JObject>(responseBody);  
          }  
          catch (HttpRequestException e)  
          {  
              LoggerHelper.Error("请求失败!",e);  
          }  
          return jsonObject;  
      }  
  }

tips: 出现了 "clientInfo.uniPlatform" is required. 是没有传参数clientInfo

参考 官方文档

适配URL化

外部系统联登

URL化请求鉴权签名

云端配置config.json的说明

继续阅读 »

APP端

this.$http  
        .post(`/uniCloudRegister`, {  
            clientInfo:JSON.stringify(uni.getSystemInfoSync())  
        })  
        .then(res =>{  
            uni.stopPullDownRefresh()  
            uni.hideNavigationBarLoading()  
            console.info(res)  
        }).catch(err => {  
            console.error(err)  
            uni.stopPullDownRefresh()  
            uni.hideNavigationBarLoading()  
        })

后台接口 UserController:ApiController

ConstantsConstants//业务系统登录后才需要联登到 uniCloud,所以不需要在注册时执行,而是单独给出了注册功能的接口  
[HttpPost]  
[Route("uniCloudRegister")]  
public async Task<HttpResponseMessage> uniCloudRegister([FromBody] UniClient client)  
{  
    HttpResponseMessage response = null;  
    try  
    {  
        //此为验证当前系统token并转为 user类的工具,这里就不给出详细示例了  
        var ui = JwtHelper.AnalysisToken(HttpContext.Current.Request);  
        //见 Utils  
        string uniAppUserName = Utils.GetExternalUid(ui);  
        string nonce = Utils.GetNonce();  
        // 获取当前时间戳(精确到毫秒)  
        long timestamp = Utils.GetNowTimeStamp();  
        Dictionary<string, string> paramsDictionary = new Dictionary<string, string>  
        {  
            { "externalUid", uniAppUserName},  
            { "nickname", ui.Name},  
        };  
        //签名算法见 Utils  
        string signature = Utils.GetSignature(paramsDictionary, nonce, timestamp);  
        //client.clientInfo 为JSON.stringify(uni.getSystemInfoSync()) 这里再转回json对象  
        var cliInfo = JsonConvert.DeserializeObject<JObject>(client.clientInfo);  
        Dictionary<string, object> param = new Dictionary<string, object>();  
        param.Add("clientInfo", cliInfo);  
        param.Add("uniIdToken", "");  
        param.Add("params", paramsDictionary);  

        // 将对象序列化为JSON字符串  
        string jsonContent = JsonConvert.SerializeObject(param);  
        Dictionary<string, string> headers = new Dictionary<string, string>()  
        {  
            {"uni-id-nonce",nonce },  
            {"uni-id-timestamp",timestamp + ""},  
            {"uni-id-signature",signature}  
        };  
        var res = await HttpHelper.SendPostAsync(Constants.PushRegister, jsonContent, headers);  
        if (res.Count > 0)  
        {  
            response = Request.CreateResponse(System.Net.HttpStatusCode.OK, res);  
        }  
        else  
        {  
            response = Request.CreateResponse(System.Net.HttpStatusCode.BadRequest, "error");  
        }  
    }  
    catch (Exception ex)  
    {  
        response = Request.CreateErrorResponse(System.Net.HttpStatusCode.InternalServerError, "error");  
    }  
    return response;  
}

Constants

  //uniapp 外部系统联登 https://doc.dcloud.net.cn/uniCloud/uni-id/cloud-object.html#external  
  public static readonly string PushUrl = "you url";  
  //uniapp 注册  
  public static readonly string PushRegister = PushUrl + "externalRegister";  
  //uniapp 登录  
  public static readonly string PushLogin = PushUrl + "externalLogin";  
  //uniapp 修改信息  
  public static readonly string PushUpdateUser = PushUrl + "updateUserInfoByExternal ";

UniClient

 public class UniClient  
  {  
      public string clientInfo { get; set; }  
      public string uniIdToken { get; set; }  
  }

Utils

 /// <summary>  
  /// 根据用户信息 获取uniapp的uid, 这个就根据自己的业务来处理  
  /// </summary>  
  /// <param name="ui"></param>  
  /// <returns></returns>  
  public static string GetExternalUid(UserInfo ui)  
  {  
    //分别为角色ID、 用户ID和用户账号  
    return  ui.Role + "_" + ui.Id + "_" + ui.userName;  
  }  

  //获取随机字符串 这里其实可以固定返回一组字符串  
  public static string GetNonce()  
  {  
      return GenerateRandomStringLinq(8);  
  }  

  private static string GenerateRandomStringLinq(int length)  
  {  
      const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";  
      Random random = new Random();  
      return new string(Enumerable.Repeat(chars, length)  
        .Select(s => s[random.Next(s.Length)]).ToArray());  
  }  

    /// <summary>  
    /// uniapp 鉴权签名算法  
    /// </summary>  
    /// <param name="parameters"></param>  
    /// <param name="nonce"></param>  
    /// <param name="timestamp"></param>  
    /// <returns></returns>  
    public static string GetSignature(Dictionary<string, string> parameters, string nonce, long timestamp)  
    {  
        string paramsStr = GetParamsString(parameters);  
        using (HMACSHA256 hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(Constants.RequestAuthSecret + nonce)))  
        {  
            string message = timestamp.ToString() + paramsStr;  
            byte[] messageBytes = Encoding.UTF8.GetBytes(message);  
            byte[] hashBytes = hmacSha256.ComputeHash(messageBytes);  

            return ByteArrayToHexString(hashBytes).ToUpper();  
        }  
    }  

  /// <summary>  
  /// 获取当前时间戳 单位毫秒  
  /// </summary>  
  /// <returns></returns>  
  public static long GetNowTimeStamp()  
  {  
      return (long)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds;  
  }  

  private static string GetParamsString(Dictionary<string, string> parameters)  
  {  
      var keys = new List<string>(parameters.Keys);  
      keys.Sort();  

      StringBuilder sb = new StringBuilder();  

      for (int i = 0; i < keys.Count; i++)  
      {  
          if (i != 0)  
          {  
              sb.Append("&");  
          }  

          sb.Append(keys[i]).Append("=").Append(parameters[keys[i]]);  
      }  

      return sb.ToString();  
  }  

  private static string ByteArrayToHexString(byte[] bytes)  
  {  
      StringBuilder sb = new StringBuilder();  

      foreach (byte b in bytes)  
      {  
          string hex = b.ToString("x2");  
          sb.Append(hex);  
      }  

      return sb.ToString();  
  }  

HttpHelper

  /// <summary>  
  /// POST异步请求  
  ///   
  /// </summary>  
  /// <param name="url">请求url</param>  
  /// <param name="jsonContent"></param>  
  /// <param name="headers"></param>  
  /// <returns></returns>  
  // 发送POST请求的函数    
  public static async Task<JObject> SendPostAsync(string url, string jsonContent, Dictionary<string, string> headers = null)  
  {  
      using (var client = new HttpClient())  
      {  
          var jsonObject = new JObject();  
          if (headers != null)  
          {  
              foreach (KeyValuePair<string, string> header in headers)  
              {  
                  client.DefaultRequestHeaders.Add(header.Key, header.Value);  
              }  
          }  
          try  
          {  
              // 创建一个HttpContent对象,用于发送JSON数据    
              var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");  

              // 发送POST请求    
              HttpResponseMessage response = await client.PostAsync(url, httpContent);  

              // 确保HTTP请求成功    
              //response.EnsureSuccessStatusCode();  

              // 读取响应内容    
              var responseBody = await response.Content.ReadAsStringAsync();  

              //LoggerHelper.Info("请求:" + url + ",参数:" + jsonContent + ",结果:" + responseBody);  
              jsonObject = JsonConvert.DeserializeObject<JObject>(responseBody);  
          }  
          catch (HttpRequestException e)  
          {  
              LoggerHelper.Error("请求失败!",e);  
          }  
          return jsonObject;  
      }  
  }

tips: 出现了 "clientInfo.uniPlatform" is required. 是没有传参数clientInfo

参考 官方文档

适配URL化

外部系统联登

URL化请求鉴权签名

云端配置config.json的说明

收起阅读 »

UID短信无法登录问题,短信验证无法登录问题。

uid 登录 短信验证码

相信很多朋友在建立项目是碰见了这个问题:使用uid短信登陆,一切都弄好了,但是一运行就出现;

未找到scene=login-by-sms,的短信模版templateId。\n已启动测试模式,直接使用:123456作为短信验证码即可。\n如果是正式项目,请在路径:/common/uni-config-center/uni-id/config.json中service->sms中配置密钥等信息,

最后我试了很多遍,发现问题在这里;
官方在指引文件中给的方法是在UID的设置文件config中,选择填写
"service": {
"sms": {
"name": "模板名称",
"codeExpiresIn": 180,
"scene": {

            "bind-mobile-by-sms": {  
                "templateId": "模板ID",  
                "codeExpiresIn": 240  
            }  
        }  
    }  

}
但其实scene是场景的意思,也就是说,SMS应用场景有可能不止一个,而其中的bind-mobile-by-sms意思是指通过短信绑定手机号,并不是我们要的短信登陆,所以才导致我们填写完了相关信息却在登陆环节一直没有识别,导致出现未找到登陆对应的场景。以下借用其他朋友的编码解决该问题。

"service": {
"sms": {
"name": 模板名称",
"codeExpiresIn": 180,
"scene": {
"login-by-sms": {
"templateId": "模板id",
"codeExpiresIn": 240
},

            "bind-mobile-by-sms": {  
                "templateId": "模板id",  
                "codeExpiresIn": 240  
            },  
            "reset-pwd-by-sms": {  
                "templateId": "模板id",  
                "codeExpiresIn": 240  
            }  
        }  
    },  
    "univerify": {  
        "appid": ""  
    }  
}  

}
这样我们就在一个短信模板下加了三个场景,分别是登陆,绑定,重置密码。如果需要使用不同ID的短息模板的话,去自己服务器相关页面设置就好。
亲测有效。

继续阅读 »

相信很多朋友在建立项目是碰见了这个问题:使用uid短信登陆,一切都弄好了,但是一运行就出现;

未找到scene=login-by-sms,的短信模版templateId。\n已启动测试模式,直接使用:123456作为短信验证码即可。\n如果是正式项目,请在路径:/common/uni-config-center/uni-id/config.json中service->sms中配置密钥等信息,

最后我试了很多遍,发现问题在这里;
官方在指引文件中给的方法是在UID的设置文件config中,选择填写
"service": {
"sms": {
"name": "模板名称",
"codeExpiresIn": 180,
"scene": {

            "bind-mobile-by-sms": {  
                "templateId": "模板ID",  
                "codeExpiresIn": 240  
            }  
        }  
    }  

}
但其实scene是场景的意思,也就是说,SMS应用场景有可能不止一个,而其中的bind-mobile-by-sms意思是指通过短信绑定手机号,并不是我们要的短信登陆,所以才导致我们填写完了相关信息却在登陆环节一直没有识别,导致出现未找到登陆对应的场景。以下借用其他朋友的编码解决该问题。

"service": {
"sms": {
"name": 模板名称",
"codeExpiresIn": 180,
"scene": {
"login-by-sms": {
"templateId": "模板id",
"codeExpiresIn": 240
},

            "bind-mobile-by-sms": {  
                "templateId": "模板id",  
                "codeExpiresIn": 240  
            },  
            "reset-pwd-by-sms": {  
                "templateId": "模板id",  
                "codeExpiresIn": 240  
            }  
        }  
    },  
    "univerify": {  
        "appid": ""  
    }  
}  

}
这样我们就在一个短信模板下加了三个场景,分别是登陆,绑定,重置密码。如果需要使用不同ID的短息模板的话,去自己服务器相关页面设置就好。
亲测有效。

收起阅读 »

记一次使用支付宝云函数踩的坑:Error:用户函数代码语法或逻辑异常,日志错误信息“invalid ELF header”

云函数

在云函数使用了bcrypt包

const bcrypt = require("bcrypt");

本地调试没有任何问题,云端调试开始报错:

Error: [login]: 用户函数代码语法或逻辑异常

函数运行日志报错:

... node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: invalid ELF header ...

解决:

询问ai:(粘贴整段报错)

您遇到的错误信息与Node.js应用程序中的bcrypt模块有关,这个模块通常用于密码的散列和加密。错误信息“invalid ELF header”表明模块尝试加载的二进制文件(bcrypt_lib.node)存在问题。这个问题可能由以下几个原因引起:
架构不兼容:二进制文件可能为与您运行系统不同的CPU架构编译。例如,如果二进制文件是为x64架构编译的,但在ARM架构的系统上运行,就会发生这个错误。
Node ABI版本不匹配:二进制文件的Node应用二进制接口(ABI)版本可能与您使用的Node.js运行时的ABI版本不匹配。这通常在切换Node.js版本后没有重新编译本地模块时发生。二进制文件损坏或不正确:二进制文件可能已损坏,或者可能不是适用于您系统的正确文件。

ai没有给出具体可行的方案,继续询问:“换一个架构兼容的包”,回答:

安装预编译的二进制版本:一些包提供了预编译的二进制版本,这些版本可以在不同的架构上运行。尝试使用这些预编译版本可能会解决问题。例如,对于bcrypt,您可以尝试安装bcryptjs,这是一个纯JavaScript实现的bcrypt替代品,不需要编译

更换为 bcryptjs 成功解决问题

const bcrypt = require("bcryptjs");
继续阅读 »

在云函数使用了bcrypt包

const bcrypt = require("bcrypt");

本地调试没有任何问题,云端调试开始报错:

Error: [login]: 用户函数代码语法或逻辑异常

函数运行日志报错:

... node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: invalid ELF header ...

解决:

询问ai:(粘贴整段报错)

您遇到的错误信息与Node.js应用程序中的bcrypt模块有关,这个模块通常用于密码的散列和加密。错误信息“invalid ELF header”表明模块尝试加载的二进制文件(bcrypt_lib.node)存在问题。这个问题可能由以下几个原因引起:
架构不兼容:二进制文件可能为与您运行系统不同的CPU架构编译。例如,如果二进制文件是为x64架构编译的,但在ARM架构的系统上运行,就会发生这个错误。
Node ABI版本不匹配:二进制文件的Node应用二进制接口(ABI)版本可能与您使用的Node.js运行时的ABI版本不匹配。这通常在切换Node.js版本后没有重新编译本地模块时发生。二进制文件损坏或不正确:二进制文件可能已损坏,或者可能不是适用于您系统的正确文件。

ai没有给出具体可行的方案,继续询问:“换一个架构兼容的包”,回答:

安装预编译的二进制版本:一些包提供了预编译的二进制版本,这些版本可以在不同的架构上运行。尝试使用这些预编译版本可能会解决问题。例如,对于bcrypt,您可以尝试安装bcryptjs,这是一个纯JavaScript实现的bcrypt替代品,不需要编译

更换为 bcryptjs 成功解决问题

const bcrypt = require("bcryptjs");
收起阅读 »

我开源了一款高颜值云端一体的项目。欢迎体验!!!

富文本 开源

项目介绍

旅拍路书:旅行爱好者的专属记录伙伴,基于uniCloud + vue3的全栈项目,包括用户登录,更新个人信息,富文本编辑,分类管理以及智能助手等功能的项目。

预览

h5端扫码预览:

小程序扫码预览:

微信小程序提交审核未通过(你的小程序涉及用户自行生成内容(文字、图片、音/)的记录、分享,属社交-笔记范畴,为个人主体小程序未开放类目,建议申请企业主体小程序。)目前只有体验版,扫码申请体验权限,秒通过

直接链接访问,移动端项目需打开控制台调至移动端模式预览:

  • <https://env-00jxgo74vwaq-static.normal.cloudstatic.cn/index.html#/>

技术栈

  • vite
  • vue3
  • ts
  • uniCloud
  • wotDesignUni
  • sp-editor

环境要求

  • node > 16.20.0
  • npm > 8.19.4
  • HBuilder X > 4.07

部署

  • Serverless 服务
  • 云原生容器平台

部分截图

源码地址 免费开源哦!!!

https://ext.dcloud.net.cn/plugin?id=18177#detail

继续阅读 »

项目介绍

旅拍路书:旅行爱好者的专属记录伙伴,基于uniCloud + vue3的全栈项目,包括用户登录,更新个人信息,富文本编辑,分类管理以及智能助手等功能的项目。

预览

h5端扫码预览:

小程序扫码预览:

微信小程序提交审核未通过(你的小程序涉及用户自行生成内容(文字、图片、音/)的记录、分享,属社交-笔记范畴,为个人主体小程序未开放类目,建议申请企业主体小程序。)目前只有体验版,扫码申请体验权限,秒通过

直接链接访问,移动端项目需打开控制台调至移动端模式预览:

  • <https://env-00jxgo74vwaq-static.normal.cloudstatic.cn/index.html#/>

技术栈

  • vite
  • vue3
  • ts
  • uniCloud
  • wotDesignUni
  • sp-editor

环境要求

  • node > 16.20.0
  • npm > 8.19.4
  • HBuilder X > 4.07

部署

  • Serverless 服务
  • 云原生容器平台

部分截图

源码地址 免费开源哦!!!

https://ext.dcloud.net.cn/plugin?id=18177#detail

收起阅读 »

抖音火山引擎语音合成代码

文字转语音
// 抖音火山引擎语音合成  
function makerDouyinVoice(content) {  

    const host = "https://openspeech.bytedance.com/api/v1/tts"  
    const header = {  
        "Authorization": "Bearer;Uwn******************iw0V4"  // 换成自己的 token  
    }  

    const requestBody = {  
        "app": {  
            "appid": "123456", //换成自己的 抖音火山引擎账号 id  
            "token": "Uwn******************iw0V4", // 换成自己的 token  
            "cluster": "****", //换成自己的 cluster  
        },  
        "user": {  
            "uid": content.uid  
        },  
        "audio": {  
            "voice_type": "BV027_streaming",  // 角色   
            "encoding": "mp3",  
            "compression_rate": 1,  
            "rate": 24000,  
            "speed_ratio": 0.8,  // 语速  
            "volume_ratio": 1.0,  
            "pitch_ratio": 1.0,  
            "emotion": "happy",  
            "language": "en"  // 这里切换中英文  
        },  
        "request": {  
            "reqid": content.sentenceId,  
            "text": content.text,  
            "text_type": "plain",  
            "operation": "query",  
            "silence_duration": "125",  
            "with_frontend": "1",  
            "frontend_type": "unitTson",  
            "pure_english_opt": "1"  
        }  
    }  

    return uniCloud.httpclient.request(host, {  
        method: 'POST',  
        headers: header,  
        data: requestBody,  
        contentType: 'json',  
        dataType:'text', // 注意这个参数,要以字符串形式返回  
        timeout: 60000  
    })  
}

//----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

// 云对象调用
async makeVoiceByDouyin(content) {
const voiceRes = await makerDouyinVoice(content)
console.log(voiceRes)
if (voiceRes.status == 200) {

        // 火山引擎返回的数据是字符串 要转换成json对象  
        const voiceData = JSON.parse(voiceRes.data)  
        if(voiceData.code == 3000){  
            // 火山引擎返回的音频数据 base64 要转换成 Buffer  
            const voiceBuffer = Buffer.from(voiceData.data,'base64')  
            // 上传到云存储  
            const res = await uniCloud.uploadFile({  
                cloudPath: `${content.sentenceId}.mp3`,  
                fileContent: voiceBuffer  
            })  
            return res  
        }  
        return {  
            errCode:1,  
            errMsg:'语音合成失败'  
        }  

    }  
    return {  
        errCode:1,  
        errMsg: '合成失败'  
    }  

}    
继续阅读 »
// 抖音火山引擎语音合成  
function makerDouyinVoice(content) {  

    const host = "https://openspeech.bytedance.com/api/v1/tts"  
    const header = {  
        "Authorization": "Bearer;Uwn******************iw0V4"  // 换成自己的 token  
    }  

    const requestBody = {  
        "app": {  
            "appid": "123456", //换成自己的 抖音火山引擎账号 id  
            "token": "Uwn******************iw0V4", // 换成自己的 token  
            "cluster": "****", //换成自己的 cluster  
        },  
        "user": {  
            "uid": content.uid  
        },  
        "audio": {  
            "voice_type": "BV027_streaming",  // 角色   
            "encoding": "mp3",  
            "compression_rate": 1,  
            "rate": 24000,  
            "speed_ratio": 0.8,  // 语速  
            "volume_ratio": 1.0,  
            "pitch_ratio": 1.0,  
            "emotion": "happy",  
            "language": "en"  // 这里切换中英文  
        },  
        "request": {  
            "reqid": content.sentenceId,  
            "text": content.text,  
            "text_type": "plain",  
            "operation": "query",  
            "silence_duration": "125",  
            "with_frontend": "1",  
            "frontend_type": "unitTson",  
            "pure_english_opt": "1"  
        }  
    }  

    return uniCloud.httpclient.request(host, {  
        method: 'POST',  
        headers: header,  
        data: requestBody,  
        contentType: 'json',  
        dataType:'text', // 注意这个参数,要以字符串形式返回  
        timeout: 60000  
    })  
}

//----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

// 云对象调用
async makeVoiceByDouyin(content) {
const voiceRes = await makerDouyinVoice(content)
console.log(voiceRes)
if (voiceRes.status == 200) {

        // 火山引擎返回的数据是字符串 要转换成json对象  
        const voiceData = JSON.parse(voiceRes.data)  
        if(voiceData.code == 3000){  
            // 火山引擎返回的音频数据 base64 要转换成 Buffer  
            const voiceBuffer = Buffer.from(voiceData.data,'base64')  
            // 上传到云存储  
            const res = await uniCloud.uploadFile({  
                cloudPath: `${content.sentenceId}.mp3`,  
                fileContent: voiceBuffer  
            })  
            return res  
        }  
        return {  
            errCode:1,  
            errMsg:'语音合成失败'  
        }  

    }  
    return {  
        errCode:1,  
        errMsg: '合成失败'  
    }  

}    
收起阅读 »

关于unicloud账号 短信的等问题

uniCloud 短信

希望官方可以出一个关于查询当前unicloud账号下云短信账号余额,以及模版列表,签名列表,申请签名,申请短信模板,以及充值短信账号条数等的功能的接口

这样就可以在自己的项目的管理后台进行操作,申请短信模板,签名,充值余额啥的,这样方便交给客户之后 不懂技术的客户 好操作

其他模块最好也能出一下相关接口
就像微信开放文档这样

继续阅读 »

希望官方可以出一个关于查询当前unicloud账号下云短信账号余额,以及模版列表,签名列表,申请签名,申请短信模板,以及充值短信账号条数等的功能的接口

这样就可以在自己的项目的管理后台进行操作,申请短信模板,签名,充值余额啥的,这样方便交给客户之后 不懂技术的客户 好操作

其他模块最好也能出一下相关接口
就像微信开放文档这样

收起阅读 »

uniCloud使用nodemailer发送邮件|SMTP

uni-cloud

环境: 支付宝小程序云.
云对象: emailSend_Time.

目标: 定时调用nodemailer 邮件发送.

实测以下代码可以发送成功.

package.json

{  
    "name": "emailSend_Time",  
    "dependencies": {  
        "nodemailer": "^6.9.12"  
    },  
    "extensions": {  
        "uni-cloud-jql": {}  
    },  
    "cloudfunction-config": {  
        "memorySize": 256,  
        "triggers": [{  
            "name": "myEmailTrigger",  
            "type": "timer",  
            "config": "0 0 * * * * *"  
        }]  
    }  
}

emailSend_Time

const nodemailer = require('nodemailer');  

const sendEmail = async ({  
    to,  
    subject,  
    html  
}) => {  
    const transporter = nodemailer.createTransport({  
        host: 'smtp.163.com',  
        port: 465,   
        secure: true,  
        auth: {  
            user: '邮箱',  
            pass: '授权码'  
        },  
    });  

    await transporter.sendMail({  
        from: {  
            name: 'XXXX',  
            address: '邮箱'  
        },  
        to,  
        subject,  
        html  
    });  
}  

module.exports = {  
    _timing: async function() {  
        await sendEmail({  
            to: '发送人邮箱',  
            subject: '幸福通知',  
            html: `<h1 style="text-align: center;"><span style="font-family: helvetica, arial, sans-serif; font-size: 36pt; color: #e03e2d;">测试</span></h1>`  
        })  
        return " "  
    },  
    sendEmail: sendEmail  
}
继续阅读 »

环境: 支付宝小程序云.
云对象: emailSend_Time.

目标: 定时调用nodemailer 邮件发送.

实测以下代码可以发送成功.

package.json

{  
    "name": "emailSend_Time",  
    "dependencies": {  
        "nodemailer": "^6.9.12"  
    },  
    "extensions": {  
        "uni-cloud-jql": {}  
    },  
    "cloudfunction-config": {  
        "memorySize": 256,  
        "triggers": [{  
            "name": "myEmailTrigger",  
            "type": "timer",  
            "config": "0 0 * * * * *"  
        }]  
    }  
}

emailSend_Time

const nodemailer = require('nodemailer');  

const sendEmail = async ({  
    to,  
    subject,  
    html  
}) => {  
    const transporter = nodemailer.createTransport({  
        host: 'smtp.163.com',  
        port: 465,   
        secure: true,  
        auth: {  
            user: '邮箱',  
            pass: '授权码'  
        },  
    });  

    await transporter.sendMail({  
        from: {  
            name: 'XXXX',  
            address: '邮箱'  
        },  
        to,  
        subject,  
        html  
    });  
}  

module.exports = {  
    _timing: async function() {  
        await sendEmail({  
            to: '发送人邮箱',  
            subject: '幸福通知',  
            html: `<h1 style="text-align: center;"><span style="font-family: helvetica, arial, sans-serif; font-size: 36pt; color: #e03e2d;">测试</span></h1>`  
        })  
        return " "  
    },  
    sendEmail: sendEmail  
}
收起阅读 »