Unity程序做一个授权认证的功能

首先要导入3个类型转换的扩展类文件

代码如下:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;

namespace EH.Extend
{
    /// <summary>
    /// byte[] 扩展
    /// </summary>
    public static class EBytes
    {
        /// <summary>
        /// 二进制数组转为16进制字符串
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static string ToHex(this byte[] bytes)
        {
            if (bytes == null)
            {
                return "";
            }

            var sb = new StringBuilder();
            foreach (var b in bytes)
            {
                sb.Append(b.ToString("X2"));
            }

            return sb.ToString();
        }

        /// <summary>
        /// 字节数组数组转布尔数组
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static bool[] ToBits(this byte[] bytes)
        {
            var bits = new bool[bytes.Length * 8];
            for (var i = 0; i < bytes.Length; i++)
            {
                var bools = bytes[i].ToBits();
                Array.Copy(bools, 0, bits, i * 8, 8);
            }

            return bits;
        }

        /// <summary>
        /// 字节数组转UTF8字符串
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static string ToUTF8(this byte[] bytes)
        {
            return Encoding.UTF8.GetString(bytes);
        }
        /// <summary>
        /// 字节数组转UTF8字符串
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="offset"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        public static string ToUTF8(this byte[] bytes, int offset, int length)
        {
            return Encoding.UTF8.GetString(bytes, offset, length);
        }

        /// <summary>
        /// 字节数组转UTF8字符串,再通过JsonConvert转泛型对象实例
        /// </summary>
        /// <param name="bytes"></param>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static T To<T>(this byte[] bytes)
        {
            var str = bytes.ToUTF8();
            try
            {
                return JsonConvert.DeserializeObject<T>(str);
            }
            catch (Exception e)
            {
               
                return default(T);
            }
        }
        /// <summary>
        /// 字节数组转UTF8字符串,再通过JsonConvert转泛型对象实例
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="offset"></param>
        /// <param name="length"></param>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static T To<T>(this byte[] bytes, int offset, int length)
        {
            var str = bytes.ToUTF8(offset, length);
            try
            {
                return JsonConvert.DeserializeObject<T>(str);
            }
            catch (Exception e)
            {
                
                return default;
            }
        }

        /// <summary>
        /// 字节数组转UTF8字符串,再通过JsonConvert转类型实例
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="offset"></param>
        /// <param name="length"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public static object To(this byte[] bytes, int offset, int length, Type type)
        {
            var str = bytes.ToUTF8(offset, length);
            try
            {
                return JsonConvert.DeserializeObject(str, type);
            }
            catch (Exception e)
            {
                
                return null;
            }
        }

        /// <summary>
        /// 字节数组小尾读取指定长度的整型
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        public static int ReadIntLE(this byte[] bytes, int length)
        {
            var count = 0;
            var val = 0;
            for (var i = length - 1; i > -1; i--)
            {
                val |= bytes[i] << (8 * count++);
            }

            return val;
        }

        /// <summary>
        /// 字节数组小尾写入指定长度的整型
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="val"></param>
        /// <param name="length"></param>
        public static void WriteIntLE(this byte[] bytes, int val, int length)
        {
            var count = 0;
            for (var i = length - 1; i > -1; i--)
            {
                bytes[i] = (byte) (val >> (8 * count++));
            }
        }

        /// <summary>
        /// 字节数组Aes加密
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="Key"></param>
        /// <param name="iv"></param>
        /// <returns></returns>
        public static byte[] AesEncrypt(this byte[] bytes, byte[] Key, byte[] iv)
        {
            byte[] encrypted;
            var len = Key.Length;
            if (len < 32)
            {
                var k = new byte[32];
                Array.Copy(Key, k, len);
                Key = k;
            }

            len = iv.Length;
            if (len < 16)
            {
                var i = new byte[16];
                Array.Copy(iv, i, len);
                iv = i;
            }

            using (var aesAlg = Aes.Create())
            {
                aesAlg.Key = Key;
                aesAlg.IV = iv;

                var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

                using (var msEncrypt = new MemoryStream())
                {
                    using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (var bwEncrypt = new BinaryWriter(csEncrypt))
                        {
                            bwEncrypt.Write(bytes);
                        }

                        encrypted = msEncrypt.ToArray();
                    }
                }
            }

            return encrypted;
        }

        /// <summary>
        /// Aes加密
        /// </summary>
        /// <param name="bytes"></param>
        /// <param name="key"></param>
        /// <param name="iv"></param>
        /// <returns></returns>
        public static byte[] AesEncrypt(this byte[] bytes, string key, string iv)
        {
            return bytes.AesEncrypt(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(iv));
        }

       

        

        /// <summary>
        /// Aes解密
        /// </summary>
        /// <param name="cipherBytes"></param>
        /// <param name="key"></param>
        /// <param name="iv"></param>
        /// <returns></returns>
        public static byte[] AesDecrypt(this byte[] cipherBytes, byte[] key, byte[] iv)
        {
            var len = key.Length;
            if (len < 32)
            {
                var k = new byte[32];
                Array.Copy(key, k, len);
                key = k;
            }

            len = iv.Length;
            if (len < 16)
            {
                var i = new byte[16];
                Array.Copy(iv, i, len);
                iv = i;
            }

            byte[] bytes;

            using (var aesAlg = Aes.Create())
            {
                aesAlg.Key = key;
                aesAlg.IV = iv;

                var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

                using (var msDecrypt = new MemoryStream(cipherBytes))
                {
                    using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                    {
                        using (var brDecrypt = new BinaryReader(csDecrypt))
                        {
                            var l = (int) msDecrypt.Length;
                            bytes = brDecrypt.ReadBytes(l);
                        }
                    }
                }
            }

            return bytes;
        }

        /// <summary>
        /// Aes解密
        /// </summary>
        /// <param name="cipherBytes"></param>
        /// <param name="key"></param>
        /// <param name="iv"></param>
        /// <returns></returns>
        public static byte[] AesDecrypt(this byte[] cipherBytes, string key, string iv)
        {
            return cipherBytes.AesDecrypt(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(iv));
        }     
    }
}

using Newtonsoft.Json;

namespace EH.Extend
{
    public static class EObject
    {
        /// <summary>
        /// 对象转Json字符串
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static string ToJson(this object obj)
        {
            return JsonConvert.SerializeObject(obj);
        }
        
        /// <summary>
        /// 对象转Json字符串,忽略循环
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static string ToJsonIgnoreLoop(this object obj)
        {
            var setting = new JsonSerializerSettings
            {
                Formatting = Formatting.Indented, 
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            };
            return JsonConvert.SerializeObject(obj, setting);
        }
        
        /// <summary>
        /// 对象转字节数组
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static byte[] ToJsonBytes(this object obj)
        {
            return JsonConvert.SerializeObject(obj).ToBytes();
        }
    }
}

 

using System;
using System.IO;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace EH.Extend
{
    public static class EString
    {
        /// <summary>
        /// 按指定长度填补字符
        /// </summary>
        /// <param name="str"></param>
        /// <param name="fill"></param>
        /// <param name="len"></param>
        /// <param name="pre"></param>
        /// <returns></returns>
        public static string FillStr(this string str, string fill = "0", int len = 2, bool pre = true)
        {
            while (str.Length < len)
            {
                if (pre)
                {
                    str = fill + str;
                }
                else
                {
                    str += fill;
                }
            }

            return str;
        }

        /// <summary>
        /// Json字符串反序列化对象
        /// </summary>
        /// <param name="str"></param>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static T To<T>(this string str)
        {
            try
            {
                return JsonConvert.DeserializeObject<T>(str);
            }
            catch (Exception e)
            {
               
                return default(T);
            }
        }

        /// <summary>
        /// 字符串转JObject
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static JObject ToJObj(this string str)
        {
            try
            {
                return JsonConvert.DeserializeObject<JObject>(str);
            }
            catch (Exception e)
            {
               
                return null;
            }
        }

        /// <summary>
        /// 十六进制字符串转字节数组
        /// </summary>
        /// <param name="hex"></param>
        /// <returns></returns>
        public static byte[] HexToBytes(this string hex)
        {
            hex = hex.Replace(" ", "");
            if (hex.Length % 2 != 0)
            {
                hex += " ";
            }

            var len = hex.Length / 2;
            var bytes = new byte[len];
            for (var i = 0; i < len; i++)
            {
                bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
            }

            return bytes;
        }

        /// <summary>
        /// UTF8字符串转二进制数组
        /// </summary>
        /// <param name="utf8"></param>
        /// <returns></returns>
        public static byte[] ToBytes(this string utf8)
        {
            return Encoding.UTF8.GetBytes(utf8);
        }

        /// <summary>
        /// 删除文件
        /// </summary>
        /// <param name="path"></param>
        public static void Del(this string path)
        {
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }

       

        /// <summary>
        /// 文件是否存在
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public static bool Exists(this string path)
        {
            return File.Exists(path);
        }

        /// <summary>
        /// 保存二进制文件
        /// </summary>
        /// <param name="path"></param>
        /// <param name="bytes"></param>
        /// <param name="mode"></param>
        public static void Save(this string path, byte[] bytes, FileMode mode = FileMode.CreateNew)
        {
            var dir = Path.GetDirectoryName(path);
            if (!Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }

            using (var fs = File.Open(path, mode))
            {
                fs.Write(bytes, 0, bytes.Length);
            }
        }


        /// <summary>
        /// UTF8字符串保存文件
        /// </summary>
        /// <param name="path"></param>
        /// <param name="str"></param>
        /// <param name="mode"></param>
        public static void Save(this string path, string str, FileMode mode = FileMode.CreateNew)
        {
            path.Save(Encoding.UTF8.GetBytes(str), mode);
        }

     

        /// <summary>
        /// 按路径加载二进制文件
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public static byte[] Load(this string path)
        {
            var file = new FileInfo(path);
            if (!file.Exists) return null;
            using (var fs = File.OpenRead(path))
            {
                var bytes = new byte[fs.Length];
                fs.Read(bytes, 0, bytes.Length);
                return bytes;
            }
        }

     

        /// <summary>
        /// 遍历路径下的文件
        /// </summary>
        /// <param name="path"></param>
        /// <param name="filter"></param>
        public static void FilterFiles(this string path, Action<FileInfo> fileFilter, string searchPattern = null, Func<DirectoryInfo, bool> dirFilter = null)
        {
            var dir = new DirectoryInfo(path);
            if (!dir.Exists) return;

            var files = dir.GetFiles(searchPattern);
            foreach (var fileInfo in files)
            {
                fileFilter(fileInfo);
            }

            var dirs = dir.GetDirectories();
            foreach (var d in dirs)
            {
                if (dirFilter == null || dirFilter(d))
                {
                    FilterFiles(d.FullName, fileFilter, searchPattern, dirFilter);
                }
            }
        }

        /// <summary>
        /// 字符串转枚举
        /// </summary>
        /// <param name="str"></param>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static T ToEnum<T>(this string str)
        {
            return (T) Enum.Parse(typeof(T), str);
        }

//        /// <summary>
//        /// 编辑器下阻塞运行exe
//        /// </summary>
//        /// <param name="path"></param>
//        /// <param name="argument"></param>
//        /// <param name="workDir"></param>
//        /// <param name="wait"></param>
//        public static void ShellProcessCommand(this string path, string argument, string workDir = null,
//            bool wait = false)
//        {
#if !UNITY_EDITOR_WIN
           throw new NotSupportedException();
#endif
//            try
//            {
//                var start = new ProcessStartInfo(path)
//                {
//                    Arguments = argument,
//                    WorkingDirectory = workDir,
//                };
//
//                var process = Process.Start(start);
//                if (!wait) return;
//                process.WaitForExit();
//                process.Close();
//            }
//            catch (Exception ex)
//            {
//                Debug.Log("出错原因:" + ex.Message);
//            }
//        }
//
//        /// <summary>
//        /// 运行时异步调用exe
//        /// </summary>
//        /// <param name="path"></param>
//        /// <param name="argument"></param>
//        /// <param name="onComplete"></param>
//        /// <param name="onError"></param>
//        /// <param name="workDir"></param>
//        /// <param name="createNoWindow"></param>
//        /// <param name="errorDialog"></param>
//        public static void ProcessCommand(this string path, string argument, Action<string> onComplete = null,
//            Action<IError> onError = null, string workDir = null)
//        {
//            if (RuntimePlatform.WindowsEditor != Application.platform &&
//                RuntimePlatform.WindowsPlayer != Application.platform) return;
//
//            FW.Thread.Push(() =>
//            {
//                var arr = new string[2];
//                Process process = null;
//                try
//                {
//                    var start = new ProcessStartInfo(path)
//                    {
//                        Arguments = argument,
//                        CreateNoWindow = true,
//                        UseShellExecute = false,
//                        WorkingDirectory = workDir,
//                        RedirectStandardOutput = true,
//                        RedirectStandardError = true,
//                        RedirectStandardInput = true,
//                        StandardOutputEncoding = Encoding.UTF8,
//                        StandardErrorEncoding = Encoding.UTF8,
//                    };
//
//                    process = Process.Start(start);
//                    arr[0] = process.StandardOutput.ReadToEnd();
//                    arr[1] = process.StandardError.ReadToEnd();
//                    process.Close();
//                    return arr;
//                }
//                catch (Exception ex)
//                {
//                    arr[1] = ex.Message;
//                }
//                finally
//                {
//                    process?.Close();
//                }
//
//                return arr;
//            }, arr =>
//            {
//                if (!string.IsNullOrEmpty(arr[1]))
//                {
//                    onError?.Invoke(new Error(arr[1]));
//                }
//
//                onComplete?.Invoke(arr[0]);
//            });
//        }
    }
}

然后

using System;
using System.Collections;
using System.IO;
using EH.Extend;
using Newtonsoft.Json.Linq;
using UnityEngine;
using UnityEngine.Networking;

namespace LabsHuanKong
{
    /// <summary>
    ///授权系统
    ///版本0.1
    ///注意设置项目的公司名和项目名 
    /// </summary>
    public class SAuth:MonoBehaviour
    {
        public event Action<Auth.EAuth> OnVerify;
        public string company { get; private set; }
        public string product { get; private set; }
        
        private  string authCodePath;
        private  string timeCodePath;

        private Auth.AuthObj auth;
       

        public void Init()
        {
            authCodePath = Path.Combine(Application.persistentDataPath, "auth", "sn.txt");
            timeCodePath = Path.Combine(Application.persistentDataPath, "auth", "tm.txt");
            DontDestroyOnLoad(this);
            company = Application.companyName;
            if (string.IsNullOrEmpty(company))
            {
                Debug.Log("not set company name");
            }

            product = Application.productName;
            if (string.IsNullOrEmpty(product))
            {
                Debug.Log("not set product name");
            }
            InvokeRepeating("VerifyLocal", 0, 60);
        }
        
       

        private IEnumerator GetNetTime()
        {
            using( var req = UnityWebRequest.Get("https://www.baidu.com"))
            {
                yield return req.SendWebRequest();

                if (!req.isDone)
                {
                    yield break;
                }

                var date = req.GetResponseHeader("Date");

                if (!DateTime.TryParse(date, out var netDt))
                {
                    yield break;
                }

                var span = netDt - DateTime.Now;
                if (span.TotalHours < 2)
                {
                    yield break;
                }
                
                OnVerify?.Invoke(Auth.EAuth.LocalTimeError);
            }
        }

        /// <summary>
        /// 验证本地授权文件
        /// </summary>
        public void VerifyLocal()
        {
             StartCoroutine( GetNetTime());
            
            if (!File.Exists(authCodePath) || !File.Exists(timeCodePath))
            {
                OnVerify?.Invoke(Auth.EAuth.Unauthorized);
                return;
            }
            var timeCode = timeCodePath.Load().ToUTF8();
            var obj = Auth.DecryptCode(timeCode, company, product);
            if (obj == null)
            {
                OnVerify?.Invoke(Auth.EAuth.Unauthorized);
                return;
            }
            var start = obj.Value<string>("start");
            var last = obj.Value<string>("last");
            var end = obj.Value<string>("end");
            
            var st = new DateTime();
            if (!DateTime.TryParse(start, out st))
            {
                OnVerify?.Invoke(Auth.EAuth.Invalid);
                return;
            }
            var lt = new DateTime();
            if (!DateTime.TryParse(last, out lt))
            {
                OnVerify?.Invoke(Auth.EAuth.Invalid);
                return;
            }
            var et = new DateTime();
            if (!DateTime.TryParse(end, out et))
            {
                OnVerify?.Invoke(Auth.EAuth.Invalid);
                return;
            }

            if (st > lt || lt > et)
            {
                timeCodePath.Del();
                OnVerify?.Invoke(Auth.EAuth.Invalid);
                return;
            }
            
            var authCode = authCodePath.Load().ToUTF8();
            auth = Auth.Verify(authCode, company, product);

            if (!auth.start.Equals(st) || !auth.end.Equals(et))
            {
                OnVerify?.Invoke(Auth.EAuth.Invalid);
                return;
            }

            if (auth.auth == Auth.EAuth.Success)
            {
                SaveTimeObj();
            }
            OnVerify?.Invoke(auth.auth);
        }
        
        /// <summary>
        /// 保存授权文件,并保存时间数据
        /// </summary>
        /// <param name="code"></param>
        /// <returns></returns>
        public Auth.EAuth SaveAuthCode(string code)
        {
            var ao = Auth.Verify(code, company, product);
            if (ao.auth != Auth.EAuth.Success)
            {
                return Auth.EAuth.Invalid;
            }

            if (DateTime.Now < ao.start)
            {
                return Auth.EAuth.Invalid;
            }
            authCodePath.Del();
            authCodePath.Save(code.ToBytes());

            auth = ao;
            SaveTimeObj();

            return Auth.EAuth.Success;
        }
        
        /// <summary>
        /// 保存时间数据,注意如果没有正确的时间信息也被判断授权失效
        /// </summary>
        private void SaveTimeObj()
        {
            var obj = new JObject
            {
                {"start", auth.start.ToShortDateString()},
                {"last", DateTime.Now.ToShortDateString()},
                {"end", auth.end.ToShortDateString()}
            };
            timeCodePath.Del();
            timeCodePath.Save(Auth.EncryptCode(obj, company, product));
        }

        /// <summary>
        /// 获取请求码
        /// </summary>
        /// <returns></returns>
        public string GetRequestCode()
        {
            return Auth.Request(company, product);
        }
    }
}

using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;


namespace LabsHuanKong
{
    public class AuthForm : UGuiForm
    {
        [SerializeField]
        private TMP_InputField _authCodeIpt;
        [SerializeField]
        private TMP_InputField _verifyCodeIpt;
        [SerializeField]
        private Button _verifyBtn;
        [SerializeField]
        private TextMeshProUGUI _verifyTip;
        private static Action _onVerifySuccess;        
        
        
        // 需提前注册SAuth系统
        public void VerifyLocal(Action action = null)
        {
            _onVerifySuccess = action;
            ClientManager.Instance.GetSAuthSystem().OnVerify += OnVerify;
            ClientManager.Instance.GetSAuthSystem().VerifyLocal();
        }
        private  void OnVerify(Auth.EAuth type)
        {
            switch (type)
            {
                case Auth.EAuth.Invalid:
                case Auth.EAuth.Unauthorized:
                case Auth.EAuth.Expire:
                case Auth.EAuth.LocalTimeError:
                    ShowTip(type);
                    GameEntry.UI.OpenUIForm(UIFormId.AuthForm, this);
                    break;
                case Auth.EAuth.Success:
                    GameEntry.UI.GetUIForm(UIFormId.AuthForm).Close();
                    _onVerifySuccess?.Invoke();
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(type), type, null);
            }
            ClientManager.Instance.GetSAuthSystem().OnVerify -= OnVerify;
        }

        
        protected override void OnOpen(object userData)
        {
            base.OnOpen(userData);
            _authCodeIpt.text = ClientManager.Instance.GetSAuthSystem().GetRequestCode();
            // 取消注释,自动生成授权码
            //_verifyCodeIpt.text = Auth.Calculate("keda", Application.companyName, Application.productName, DateTime.Parse("2021-1-1"), _authCodeIpt.text);
            _verifyBtn.onClick.AddListener(OnVerifyClick);
        }
        protected  override void OnClose(object userData)
        {
            base.OnClose(userData);
            _verifyBtn.onClick.RemoveAllListeners();
        }
        

        private void OnVerifyClick()
        {
            var verifyCode = _verifyCodeIpt.text;
            if (string.IsNullOrEmpty(verifyCode))
            {
                _verifyTip.text = "请输入授权码";
                _verifyTip.color = Color.red;
                return;
            }

            var obj = ClientManager.Instance.GetSAuthSystem().SaveAuthCode(_verifyCodeIpt.text);
            _verifyTip.text = GetTip(obj);
            
            if (obj == Auth.EAuth.Success)
            {
                GameEntry.UI.GetUIForm(UIFormId.AuthForm).Close(); 
                
                _onVerifySuccess?.Invoke();
            }
        }
        private string GetTip(Auth.EAuth obj)
        {
            switch (obj)
            {
                case Auth.EAuth.Expire:
                    _verifyTip.color = Color.red;
                    return "授权码已过期";
                case Auth.EAuth.Invalid:
                    _verifyTip.color = Color.red;
                    return "无效的授权码";
                case Auth.EAuth.Unauthorized:
                    _verifyTip.color = Color.red;
                    return "未授权";
                case Auth.EAuth.LocalTimeError:
                    _verifyTip.color = Color.red;
                    return "本机时间异常";
                case Auth.EAuth.Success:
                    _verifyTip.color = Color.yellow;
                    return "授权成功";
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
        public void ShowTip(Auth.EAuth obj)
        {
            _verifyTip.text = GetTip(obj);
        }

        
    }
}
 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值