基于Redis验证码发送及校验方案实现
大家好,今天本人给大家带来文章《基于Redis验证码发送及校验方案实现》,文中内容主要涉及到发送、校验、Redis验证码,如果你对数据库方面的知识点感兴趣,那就请各位朋友继续看下去吧~希望能真正帮到你们,谢谢!
在我们的业务中,经常存在需要通过发送验证码、校验验证码来完成的一些业务逻辑,比如账号注册、找回密码、用户身份确认等。
在该类业务中,发送验证码的方式可以有各种各样,比如最常见的手机验证,最古老的邮箱验证,到现在相对少见的微信公众号、钉钉通知等;而验证码服务端存储的方式也可以各式各样,比如存储在关系型数据库中,当然也可以如本文标题所示,存储在Redis中。
既然已经预见到了各式各样的发送方式,也预见到了各式各样的存储方式,所以,虽然本文标题是基于Redis,但Redis其实只是其中的一种存储方式,如果需要,我们也应该可以和方便的切换到其它存储方式。
上代码前,我们先看下设计中的接口关系

ICodeHelper是最终提供发送验证码和校验验证码的最终接口,其关联了ICodeSender和ICodeStorage,ICodeSender即为验证码发送方式的约定接口,ICodeStorage则为验证码服务端持久化方式的约定接口。我们可以看到ICodeSender同样关联了IContentFormatter,因为作为发送方ICodeSender其实是不知道如何将要发送的内容组织成一段完整的文本内容的,这时候就需要IContentFormatter来组织文本内容,至于继承自IContentFormatter的IComplexContentFormatter,则只是IContentFormatter一个容器封装,毕竟对于不同的业务类型,我们需要组织成不同的文本内容,通过IComplexContentFormatter,我们可以将不同业务类型文本内容的组织过程,分散到不同的IContentFormatter中。
下面我们来看下上述接口的规范约定,考虑到代码的简便性,此处我们简单的将receiver接收方定义为了string,而不是泛型;业务标志bizFlag为了方便接入时无需调整代码,所以此处也没有将该值定义为枚举,而是同样定义成了通用性最强的string。
ICodeStorage
////// 校验码信息存储接口 /// public interface ICodeStorage { ////// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 /// /// 接收方 /// 业务标志 /// 校验码 /// 校验码有效时间范围 ///Task SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime); /// /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作 /// /// 接收方 /// 业务标志 ///Task IncreaseCodeErrors(string receiver, string bizFlag); /// /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 /// /// 接收方 /// 业务标志 /// 周期时间范围 ///Task SetPeriod(string receiver, string bizFlag, TimeSpan? period); /// /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 /// /// 接收方 /// 业务标志 ///Task IncreaseSendTimes(string receiver, string bizFlag); /// /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null /// /// 接收方 /// 业务标志 ///Task > GetEffectiveCode(string receiver, string bizFlag); /// /// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 /// /// /// ///Task GetAreadySendTimes(string receiver, string bizFlag); }
ICodeSender,请注意IsSupport方法约定。
////// 校验码实际发送接口 /// public interface ICodeSender { ////// 发送校验码内容模板 /// IContentFormatter Formatter { get; } ////// 判断接收者是否符合发送条件,例如当前发送者只支持邮箱,而接收方为手机号,则返回结果应当为false /// /// 接收方 ///bool IsSupport(string receiver); /// /// 发送校验码信息 /// /// 接收方 /// 业务标志 /// 校验码 /// 校验码有效时间范围 ///Task Send(string receiver, string bizFlag, string code, TimeSpan effectiveTime); }
IContentFormatter
////// 发送校验码内容模板接口 /// public interface IContentFormatter { ////// 将指定参数组织成待发送的文本内容 /// /// 接收方 /// 业务标志 /// 校验码 /// 校验码有效时间范围 ///string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime); }
IComplexContentFormatter
////// 基于业务标志的多内容模板 /// public interface IComplexContentFormatter : IContentFormatter { ////// 设置指定业务对应的内容模板 /// /// 业务标志 /// 内容模板 void SetFormatter(string bizFlag, IContentFormatter formatter); ////// 移除指定业务对应的内容模板,如果没有,则返回null /// /// 业务标志 ///IContentFormatter RemoveFormatter(string bizFlag); }
ICodeHelper
////// 业务校验码辅助接口 /// public interface ICodeHelper { ////// 校验码实际发送者 /// ICodeSender Sender { get; } ////// 校验码信息存储者 /// ICodeStorage Storage { get; } ////// 发送校验码 /// /// 接收方 /// 业务标志 /// 校验码 /// 校验码有效时间范围 /// 周期内最大允许发送配置,为null则表示无限制 ///Task SendCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit maxSendLimit); /// /// 验证校验码是否正确 /// /// 接收方 /// 业务标志 /// 校验码 /// 最大允许错误次数 ///Task VerifyCode(string receiver, string bizFlag, string code, int maxErrorLimit); }
下面则是接口约定中的一些定义的类和枚举。
////// 校验码发送周期设置 /// public class PeriodLimit { ////// 周期内允许的最大次数 /// public int MaxLimit { get; set; } ////// 周期时间,如果不设置,则表示无周期,此时 public TimeSpan? Period { get; set; } } ///代表总共只允许发送多少次 /// /// 校验码发送结果 /// public enum SendResult { ////// 发送成功 /// [Description("成功")] Success = 0, ////// 超出最大发送次数 /// [Description("超出最大发送次数")] MaxSendLimit = 11, ////// 发送失败,指 [Description("发送失败")] FailInSend = 12, ///的发送结果为false /// /// 无法发送, [Description("无法发送")] NotSupprot = 13, } ///结果为false /// /// 校验码校验结果 /// public enum VerificationResult { ////// 校验成功 /// [Description("成功")] Success = 0, ////// 校验码已过期 /// [Description("校验码已过期")] Expired = 31, ////// 校验码不一致,校验失败 /// [Description("校验失败")] VerificationFailed = 32, ////// 已经达到了最大错误尝试次数,需重新发送新的校验码 /// [Description("超出最大错误次数")] MaxErrorLimit = 33, }
再下来就是具体的接口实现了,当然这些实现也是通用实现
ContentFormatter
////// 通用的内容模板 /// public class ContentFormatter : IContentFormatter { private Func_func; /// /// 通用实现,这样就无需每种业务类型都要实现 /// 传递的委托,参数顺序与/// 一致 public ContentFormatter(Func func) { this._func = func ?? throw new ArgumentNullException(nameof(func)); } /// /// 将指定参数组织成待发送的文本内容 /// /// 接收方 /// 业务标志 /// 校验码 /// 校验码有效时间范围 ///public string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { return this._func.Invoke(receiver, bizFlag, code, effectiveTime); } }
ComplexContentFormatter
using System.Collections.Concurrent;
///
/// 基于业务标志的多内容模板实现
///
public class ComplexContentFormatter : IComplexContentFormatter
{
private ConcurrentDictionary _dic = new ConcurrentDictionary();
///
/// 设置指定业务对应的内容模板
///
/// 业务标志
/// 内容模板
public void SetFormatter(string bizFlag, IContentFormatter formatter)
{
if (!string.IsNullOrWhiteSpace(bizFlag) && formatter != null)
{
this._dic.AddOrUpdate(bizFlag, formatter, (k, v) => formatter);
}
}
///
/// 移除指定业务对应的内容模板,如果没有,则返回null
///
/// 业务标志
///
public IContentFormatter RemoveFormatter(string bizFlag)
{
if (!string.IsNullOrWhiteSpace(bizFlag)
&& this._dic.TryRemove(bizFlag, out IContentFormatter formatter))
{
return formatter;
}
return null;
}
///
/// 将指定参数组织成待发送的文本内容
///
/// 接收方
/// 业务标志
/// 校验码
/// 校验码有效时间范围
///
public string GetContent(string receiver, string bizFlag, string code, TimeSpan effectiveTime)
{
if (string.IsNullOrWhiteSpace(bizFlag))
{
throw new ArgumentNullException(nameof(bizFlag));
}
this._dic.TryGetValue(bizFlag, out IContentFormatter formatter);
if (formatter == null)
{
throw new KeyNotFoundException(nameof(formatter));
}
return formatter.GetContent(receiver, bizFlag, code, effectiveTime);
}
}
CodeHelper,注意该类除了实现ICodeHelper外,还提供了一个用于生成随机验证码的静态方法GetRandomNumber。
////// 业务校验码辅助接口实现 /// public class CodeHelper : ICodeHelper { ////// 基于接口实现,可依赖注入 /// /// /// public CodeHelper(ICodeSender sender, ICodeStorage storage) { this.Sender = sender ?? throw new ArgumentNullException(nameof(sender)); this.Storage = storage ?? throw new ArgumentNullException(nameof(storage)); } ////// 校验码实际发送者 /// public ICodeSender Sender { get; } ////// 校验码信息存储者 /// public ICodeStorage Storage { get; } ////// 发送校验码 /// /// 接收方 /// 业务标志 /// 校验码 /// 校验码有效时间范围 /// 周期内最大允许发送配置,为null则表示无限制 public async TaskSendCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime, PeriodLimit maxSendLimit) { var result = SendResult.NotSupprot; if (this.Sender.IsSupport(receiver)) { result = SendResult.MaxSendLimit; bool canSend = maxSendLimit == null; int sendTimes = 0; if (!canSend) { sendTimes = await this.Storage.GetAreadySendTimes(receiver, bizFlag).ConfigureAwait(false); canSend = sendTimes /// 验证校验码是否正确 /// /// 接收方 /// 业务标志 /// 校验码 /// 最大允许错误次数 /// public async Task VerifyCode(string receiver, string bizFlag, string code, int maxErrorLimit) { var result = VerificationResult.Expired; var vCode = await this.Storage.GetEffectiveCode(receiver, bizFlag).ConfigureAwait(false); if (vCode != null && !string.IsNullOrWhiteSpace(vCode.Item1)) { result = VerificationResult.MaxErrorLimit; if (vCode.Item2 /// 获取由数字组成的校验码 /// /// 校验码长度 /// public static string GetRandomNumber(int maxLength = 6) { if (maxLength = 10) { throw new ArgumentOutOfRangeException($"{nameof(maxLength)} must between {1} and {9}."); } var rd = Math.Abs(Guid.NewGuid().GetHashCode()); var tmpX = (int)Math.Pow(10, maxLength); return (rd % tmpX).ToString().PadLeft(maxLength, '0'); } }
除了上述标准通用实现,还有一些半通用实现,比如本文标题中的Redis,所谓半通用,就是指你可以直接拿来用,但有可能不符合你的技术场景,此时你需要自己重写一份。
CodeStorageWithRedisCache,注意该类库采用了StackExchange.Redis.Extensions.Core,你可以在nuget上下载该类库,如果你对默认的Redis键值生成方式不满意,你也可以通过重写GetKey方法来指定新的键值生成方式。当然,因为实际存储在Redis中的数据都只是一些简单数据,并不需要额外的序列化过程,实际你也可以直接使用StackExchange.Redis。
////// 校验码信息存储到Redis /// public class CodeStorageWithRedisCache : ICodeStorage { private readonly IRedisCacheClient _client; private const string CodeValueHashKey = "Code"; private const string CodeErrorHashKey = "Error"; private const string PeriodHashKey = "Period"; ////// Code缓存Key值前缀 /// public string CodeKeyPrefix { get; set; } = "CC"; ////// Period缓存Key值前缀 /// public string PeriodKeyPrefix { get; set; } = "CCT"; ////// 缓存写入Redis哪个库 /// public int DbNumber { get; set; } = 8; ////// 基于RedisCacheClient的构造函数 /// /// public CodeStorageWithRedisCache(IRedisCacheClient client) { this._client = client; } ////// 获取校验码周期内已发送次数,如果周期已到或未发送过任何验证码,则返回0 /// /// /// ///public async Task GetAreadySendTimes(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetPeriodKey(receiver, bizFlag); var times = await db.HashGetAsync (key, PeriodHashKey).ConfigureAwait(false); #if DEBUG Console.WriteLine("Method:{0} Result:{1}", nameof(GetAreadySendTimes), times); #endif return times; } /// /// 获取校验码及已尝试错误次数,如果校验码不存在或已过期,则返回null /// /// 接收方 /// 业务标志 ///public async Task > GetEffectiveCode(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetCodeKey(receiver, bizFlag); if (await db.ExistsAsync(key).ConfigureAwait(false)) { var code = await db.HashGetAsync (key, CodeValueHashKey).ConfigureAwait(false); var errors = await db.HashGetAsync (key, CodeErrorHashKey).ConfigureAwait(false); #if DEBUG Console.WriteLine("Method:{0} Result: Code {1} Errors {2} ", nameof(GetEffectiveCode), code, errors); #endif return Tuple.Create(code, errors); } return null; } /// /// 校验码错误次数+1,如果校验码已过期,则不进行任何操作 /// /// 接收方 /// 业务标志 ///public async Task IncreaseCodeErrors(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetCodeKey(receiver, bizFlag); if (await db.ExistsAsync(key).ConfigureAwait(false)) { var errors = await db.HashGetAsync (key, CodeErrorHashKey).ConfigureAwait(false); await db.HashSetAsync(key, CodeErrorHashKey, errors + 1).ConfigureAwait(false); } } /// /// 校验码周期内发送次数+1,如果周期已到,则不进行任何操作 /// /// 接收方 /// 业务标志 ///public async Task IncreaseSendTimes(string receiver, string bizFlag) { var db = this.GetDatabase(); var key = this.GetPeriodKey(receiver, bizFlag); if (await db.ExistsAsync(key).ConfigureAwait(false)) { var times = await db.HashGetAsync (key, PeriodHashKey).ConfigureAwait(false); await db.HashSetAsync(key, PeriodHashKey, times + 1).ConfigureAwait(false); } } /// /// 将校验码进行持久化,如果接收方和业务标志组合已经存在,则进行覆盖 /// /// 接收方 /// 业务标志 /// 校验码 /// 校验码有效时间范围 ///public async Task SetCode(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { var db = this.GetDatabase(); var key = this.GetCodeKey(receiver, bizFlag); await db.RemoveAsync(key).ConfigureAwait(false); var ret = await db.HashSetAsync(key, CodeValueHashKey, code).ConfigureAwait(false) && await db.HashSetAsync(key, CodeErrorHashKey, 0).ConfigureAwait(false) && await db.UpdateExpiryAsync(key, effectiveTime); #if DEBUG Console.WriteLine("Method:{0} Result:{1}", nameof(SetCode), ret); #endif return ret; } /// /// 校验码发送次数周期持久化,如果接收方和业务标志组合已经存在,则进行覆盖 /// /// 接收方 /// 业务标志 /// 周期时间范围 ///public async Task SetPeriod(string receiver, string bizFlag, TimeSpan? period) { var db = this.GetDatabase(); var key = this.GetPeriodKey(receiver, bizFlag); await db.RemoveAsync(key).ConfigureAwait(false); var ret = await db.HashSetAsync(key, PeriodHashKey, 1).ConfigureAwait(false); if (period.HasValue) { ret = ret && await db.UpdateExpiryAsync(key, period.Value); } #if DEBUG Console.WriteLine("Method:{0} Result:{1}", nameof(SetPeriod), ret); #endif return ret; } /// /// 组织Redis键值 /// /// /// /// ///protected virtual string GetKey(string receiver, string bizFlag, string prefix) { return string.Format("{0}:{1}:{2}", prefix, bizFlag, receiver); } private string GetPeriodKey(string receiver, string bizFlag) { return this.GetKey(receiver, bizFlag, this.PeriodKeyPrefix); } private string GetCodeKey(string receiver, string bizFlag) { return this.GetKey(receiver, bizFlag, this.CodeKeyPrefix); } private IRedisDatabase GetDatabase() { return this._client.GetDb(this.DbNumber); } }
最后,就是不可能通用的实现了,对于ICodeSender而言,先不说发送方式不同,就算相同,比如都是手机,那也还有不同的短信供应商,所以此处必须要使用者按自己的实际业务来实现,为了方便举例,这里我写了一个在控制台输出验证码内容的实现。
ConsoleSender,注意IsSupport在此处输出true,代表支持任意receiver
////// 在控制台输出校验码 /// public class ConsoleSender : ICodeSender { public ConsoleSender(IContentFormatter formatter) { this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); } public IContentFormatter Formatter { get; } public bool IsSupport(string receiver) => true; public TaskSend(string receiver, string bizFlag, string code, TimeSpan effectiveTime) { var content = this.Formatter.GetContent(receiver, bizFlag, code, effectiveTime); Console.WriteLine("发送内容:{0}", content); return Task.FromResult(true); } }
最后则是如何使用的代码例子,注意此处Redis序列化方式采用了StackExchange.Redis.Extensions.Newtonsoft,你可以根据实际需要采用其它序列化方式,比如StackExchange.Redis.Extensions.Protobuf等,你同样可以在nuget上下载到这些类库。
static void CheckCodeHelperDemo()
{
var redisConfig = new RedisConfiguration
{
Hosts = new RedisHost[] {
new RedisHost{
Host="127.0.0.1",
Port=6379
}
}
};
var bizFlag = "forgetPassword";
var receiver = "Receiver";
var effectiveTime = TimeSpan.FromMinutes(1);
var redisManager = new RedisCacheConnectionPoolManager(redisConfig);
var redisClient = new RedisCacheClient(redisManager,
new NewtonsoftSerializer(), redisConfig);//new ProtobufSerializer();
var storage = new CodeStorageWithRedisCache(redisClient);
var simpleFormatter = new ContentFormatter(
(r, b, c, e) => $"{r}您好,您的忘记密码验证码为{c},有效期为{(int)e.TotalSeconds}秒.");
var formatter = new ComplexContentFormatter();
formatter.SetFormatter(bizFlag, simpleFormatter);
var sender = new ConsoleSender(formatter); //如果就一个业务场景,也可以直接用simpleFormatter
//var tmp = storage.SetPeriod(receiver, bizFlag, TimeSpan.FromMinutes(20)).Result;
var helper = new CodeHelper(sender, storage);
var code = CodeHelper.GetRandomNumber();
var sendResult = helper.SendCode(receiver, bizFlag, code, effectiveTime, new PeriodLimit
{
MaxLimit = 5,
Period = TimeSpan.FromMinutes(20)
}).Result;
Console.WriteLine("发送结果:{0}", sendResult);
if (sendResult == SendResult.Success)
{
Console.WriteLine("*****************************");
while (true)
{
Console.WriteLine("请输入校验码:");
var vCode = Console.ReadLine();
var vResult = helper.VerifyCode(receiver, bizFlag, vCode, 3).Result;
Console.WriteLine("校验码 {0} 校验结果:{1}", vCode, vResult);
if (vResult != VerificationResult.VerificationFailed)
{
break;
}
}
}
redisManager.Dispose();
}
最后则是不同测试场景的一些截图
验证码校验失败达到允许次数上限

校验码已过期

校验码验证成功

校验码周期内允许的发送次数已达到上限

最后,上述完整的代码可见github。
到这里,我们也就讲完了《基于Redis验证码发送及校验方案实现》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于redis的知识点!
完美解决Redis在双击redis-server.exe出现闪退问题
- 上一篇
- 完美解决Redis在双击redis-server.exe出现闪退问题
- 下一篇
- redis如何清理缓存
-
- 善良的台灯
- 太详细了,码住,感谢大佬的这篇文章内容,我会继续支持!
- 2023-03-03 20:44:58
-
- 尊敬的冰棍
- 这篇技术贴太及时了,作者大大加油!
- 2023-02-28 09:41:26
-
- 瘦瘦的火龙果
- 真优秀,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢楼主分享技术文章!
- 2023-02-26 16:19:05
-
- 忧郁的手链
- 这篇文章内容太及时了,太全面了,写的不错,码起来,关注老哥了!希望老哥能多写数据库相关的文章。
- 2023-02-26 02:55:35
-
- 数据库 · Redis | 2天前 |
- RedisLua脚本实现复杂正则匹配方法
- 438浏览 收藏
-
- 数据库 · Redis | 3天前 |
- Redis客户端缓冲区优化技巧
- 146浏览 收藏
-
- 数据库 · Redis | 3天前 |
- RedisPSUBSCRIBE耗CPU原因解析
- 476浏览 收藏
-
- 数据库 · Redis | 3天前 |
- Redis分布式锁释放原子性保障方案
- 216浏览 收藏
-
- 数据库 · Redis | 3天前 |
- RedisLua脚本实现分布式事务补偿与回滚
- 180浏览 收藏
-
- 数据库 · Redis | 3天前 |
- Redis6.0线程优化与CPU绑定方法
- 326浏览 收藏
-
- 数据库 · Redis | 4天前 |
- Redis发布订阅支持消息压缩吗?
- 415浏览 收藏
-
- 数据库 · Redis | 4天前 |
- Redis缓存优化:调整淘汰策略提命中率
- 242浏览 收藏
-
- 数据库 · Redis | 4天前 |
- Redis集群节点负载查看技巧
- 369浏览 收藏
-
- 数据库 · Redis | 4天前 |
- Redis7.0IO多线程优化方法
- 251浏览 收藏
-
- 数据库 · Redis | 4天前 |
- Redis集群Pub/Sub如何减少广播消耗
- 451浏览 收藏
-
- 数据库 · Redis | 4天前 |
- Redis主从优化:延长repl-backlog-ttl设置
- 477浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 5930次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 6357次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 6165次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 8141次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 6739次使用
-
- Go语言实现服务端消息接收和发送
- 2023-01-07 323浏览
-
- GoFrame实现顺序性校验示例详解
- 2023-02-19 225浏览
-
- go语言实现银行卡号Luhn校验
- 2023-02-16 140浏览
-
- Go中Channel发送和接收操作指南
- 2023-01-09 393浏览
-
- go语言实战之实现比特币地址校验步骤
- 2022-12-27 458浏览

