2023年4月

以ChatGPT为代表的AIGC(人工智能生成内容)将成为新一轮范式转移的开始。

需要声明,我并不是人工智能专业,只是在愈演愈烈的AI焦虑下,不想被这个技术潮流割韭菜,为此我特意搜集了大量资料,体系化的梳理了AIGC相关的信息。

本文约4000字,目标是快速建立AIGC知识体系,含有大量的计算专业名词,建议阅读同时,对不了解的知识点扩展搜索一下。

一、行业现状

1、概念界定

区别于PGC与UGC不同的,AIGC是利用人工智能技术自动生成内容的新型生产方式。


2、数据模态

按照模态区分,AIGC又可分为音频生成、文本生成、图像生成、视频生成及图像、视频、文本间的跨模态生成,细分场景众多,其中跨模态生成值得重点关注。

模态是指数据的存在形式,比如文本、音频、图像、视频等文件格式
跨模态,指的是像以文生成图/视频或者以图生成文这种情况

例如,百度的文心一格就是典型的以文生成图:

3、发展历程

AIGC 的发展可以大致分为以下三个阶段:

  • 早期萌芽阶段:20 世纪 50 年代—90 年代中期,受限于科技水平,AIGC 仅限于小范围实验
  • 沉积积累阶段:20 世纪 90 年代中期—21 世纪 10 年代中期,AIGC 从实验向实用转变,受限于算法,无法直接进行内容生成
  • 快速发展阶段:21 世纪 10 年代中期—现在,深度学习算法不断迭代,AI 生成内容种类多样丰富且效果逼真

二、技术实现

AIGC 技术主要涉及两个方面:自然语言处理 NLP 和 AIGC 生成算法。

1、NLP自然语言处理

自然语言处理(NLP)赋予了AI理解和生成能力,是实现人与计算机之间如何通过自然语言进行交互的手段。

NLP技术可以分为两个方向:NLU和NLG。

1.1 自然语言理解 NLU

NLU使得计算机能够和人一样,具备正常人的语言理解能力。

过去,计算机只能处理结构化的数据,NLU 使得计算机能够识别和提取语言中的意图来实现对于自然语言的理解。

由于自然语言的多样性、歧义性、知识依赖性和上下文,计算机在理解上有很多难点,所以 NLU 至今还远不如人类的表现。

自然语言理解跟整个人工智能的发展历史类似,一共经历了 3 次迭代:基于规则的方法、基于统计的方法和基于深度学习的方法。

1.2 自然语言生成 NLG

NLG将非语言格式的数据转换成人类可以理解的语言格式,如文章、报告等。

NLG 的发展经历了三个阶段,从早期的简单的数据合并到模板驱动模式再到现在的高级 NLG,使得计算机能够像人类一样理解意图,考虑上下文,并将结果呈现在用户可以轻松阅读和理解的叙述中。

自然语言生成可以分为以下六个步骤:内容确定、文本结构、句子聚合、语法化、参考表达式生成和语言实现。

1.3 神经网络 RNN

神经网络,尤其是循环神经网络 (RNN) 是当前 NLP 的主要方法的核心。

其中,2017 年由 Google 开发的 Transformer 模型现已逐步取代长短期记忆(LSTM)等 RNN 模型成为了 NLP 问题的首选模型。

Transformer 的并行化优势允许其在更大的数据集上进行训练,这也促成了 BERT、GPT 等预训练模型的发展。

相关系统使用了维基百科、Common Crawl 等大型语料库进行训练,并可以针对特定任务进行微调。

1.4 Transformer 模型

Transformer 模型是一种采用自注意力机制的深度学习模型,这一机制可以按输入数据各部分重要性的不同而分配不同的权重。

与循环神经网络(RNN)一样,Transformer 模型旨在处理自然语言等顺序输入数据,可应用于翻译、文本摘要等任务。与 RNN 不同的是,Transformer 模型能够一次性处理所有输入数据。

注意力机制可以为输入序列中的任意位置提供上下文。如果输入数据是自然语言,则 Transformer 不必像 RNN 一样一次只处理一个单词,这种架构允许更多的并行计算,并以此减少训练时间。

ChatGPT是OpenAI从GPT-3.5、GPT-4系列中的模型进行微调产生的聊天机器人模型,能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,真正像人类一样来聊天交流。

2、AIGC算法

  • AIGC 生成算法主流的有生成对抗网络 GAN 和扩散模型
  • 扩散模型已经拥有了成为下一代图像生成模型的代表的潜力

2.1 生成对抗网络 GAN

GAN是生成模型的一种,透过两个神经网络相互博弈的方式进行学习。

GAN 被广泛应用于广告、游戏、娱乐、媒体、制药等行业,可以用来创造虚构的人物、场景,模拟人脸老化,图像风格变换,以及产生化学分子式等等。

2.2 扩散模型 Diffusion Model

GAN(生成对抗网络)有生成器和鉴别器,它们相互对抗,然后生成图像,由于模型本身具有对抗性,因此很难进行训练,利用扩散模型可以解决这个问题。

扩散模型也是生成模型,扩散模型背后的直觉来源于物理学。在物理学中气体分子从高浓度区域扩散到低浓度区域,这与由于噪声的干扰而导致的信息丢失是相似的。

Diffusion通过引入噪声,然后尝试通过去噪来生成图像。在一段时间内通过多次迭代,模型每次在给定一些噪声输入的情况下学习生成新图像。

2.3 Lora模型

LoRA是Low-Rank Adaption of large language model的缩写,是一种大语言模型fine-tune的方法。

Lora主要思路是在固定大网络的参数,并训练某些层参数的增量,且这些参数增量可通过矩阵分解变成更少的可训练参数,大大降低finetune所需要训练的参数量。

三、商业落地

1、A应用场景

  • AIGC 在文字、图像、音频、游戏和代码生成中商业模型渐显

2、产业地图

2.1 产业上游:数据服务

  • 人工智能的分析、创作、决策能力都依赖海量数据
  • 决定不同机器间能力差异的就是数据的数量与质量

2.2 产业中游:算法模型

算法模型是AIGC最核心的环节,是机器学习的关键所在。通常包含三类参与者:专门实验室、企业研究院、开源社区。

AI实验室:算法模型在AI系统中起决策作用,是它完成各种任务的基础,可以视为AI系统的灵魂所在。

企业研究院:一些集团型公司或企业往往会设立专注于前沿科技领域的大型研究院,下设不同领域的细分实验室,通过学术氛围更浓厚的管理方式为公司的科研发展添砖加瓦。

开源社区:社区对AIGC非常重要,它提供了一个共享成果、代码的平台,与其他人相互合作,共同推动AIGC相关技术的进步。根据覆盖领域的宽度和深度,这种社区可以分为综合型开源社区和垂直型开源社区。

2.3 产业下游:应用拓展

3、细分市场

3.1 文本处理

文本处理是AIGC相关技术距离普通消费者最近的场景,技术较为成熟。

一般说来文本处理可以细分为营销型、销售型、续写型、知识型、通用型、辅助型、交互型、代码型。

3.2 音频处理

目前的音频处理主要分为三类:音乐型、讲话型、定制型,AI的应用将优化供给效率,改善整体利润水平。

3.3 图片处理

图片的创作门槛比文字高,传递信息也更直观,随着AIGC应用的日益广泛,图片处理也就从广告、设计、编辑等角度带来更大更多的机遇。图片处理可细分为生成型、广告型、设计型、编辑型。

3.4 视频处理

视频日益成为新时代最主流的内容消费形态,将AIGC引入视频将是全新的赛道,也是技术难度最大的领域。视频处理可以细分为生成型、编辑型、定制型、数字虚拟人视频。

3.5 代码生成

以GitHub Copilot为例,Copilot是GitHub 和 OpenAI 合作产生的 AI 代码生成工具,可根据命名或者正在编辑的代码上下文为开发者提供代码建议。官方介绍其已经接受了来自 GitHub 上公开可用存储库的数十亿行代码的训练,支持大多数编程语言。

四、面临挑战

除了技术上亟待解决的算力、模型准确性之外,目前AIGC相关的挑战主要集中在版权、欺诈、违禁内容三方面。

1、版权问题

  • AIGC是机器学习的应用,而在模型的学习阶段一定会使用大量数据,但目前对训练后的生成物版权归属问题尚无定论
  • 为什么AI基于自己创作的作品生成的新作品却与自己无关?而且现行法律都是针对人类的行为规范而设立的
  • AI只是一种工具,不受法律约束与审判,即便证据充分,作者的维权之路通常也难言顺利
  • 不过对于AIGC与作者的关系将会随着时代发展而逐渐清晰,界定也将更有条理性

2、欺诈问题

  • 高科技诈骗手段层出不穷,AI经过训练后也可以创作出以假乱真的音视频,“换脸”“变声”等功能,滥用危害甚大
  • 部分诈骗分子利用“换脸”技术实施诈骗,也有不法分子恶意伪造他人视频,再转手兜售到灰色市场

3、违禁内容

  • AIGC取决于使用者的引导,AI对恶意诱导会不加分辨或判断,会根据学习到的信息输出极端或暴力言论
  • AIGC作为内容生产的新范式,也对国家相关法律法规机构及监管治理能力都提出了更高要求

参考资料

AIGC:内容生产力的革命—国海证券

AIGC发展趋势报告2023—腾讯研究院

2023AIGC行业研究报告—甲子光年

前言:

项目中时不时遇到查字典表等数据,只需要返回数据,不需要写其他业务,每个字典表可能都需要写一个接口给前端调用,比较麻烦,所以采用下面这种方式,前端只需传入实体名称即可,例如:SysUser

1、获取实体类型

var entityType=Assembly.Load("XXX.Entity").GetTypes().Where(a =>a.Name == entityName).FirstOrDefault();
如果需要加载多个类库(以下是其中一种方式)
1、先获取DBContext里面的对象来匹配实体名称得到他的命名空间
var name = DBContext.Model.GetEntityTypes().Where(a => a.ClrType.Name == "传入的实体名称").Select(a => a.ClrType.Namespace).FirstOrDefault();
2、根据命名空间匹配到程序集
var assemblyName = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName.Contains(name)).Select(a => a.FullName).FirstOrDefault();
3、获取实体类型
var entityType = Assembly.Load(assemblyName).GetTypes().Where(a => a.Name == "传入的实体名称").FirstOrDefault();

2、创建动态仓储

var repositoryType = typeof(Repository<>).MakeGenericType(entityType);//Repository<> 一般项目会封装仓储层放一些CRUD的公共方法
var repository = Activator.CreateInstance(repositoryType, DBContext);
var queryMethod = repositoryType.GetMethod("GetList"); //这里写需要调用的方法名称

3、设置查询参数

1、拼接lambda
var parameterExpression=Expression.Parameter(entityType,"x");
var propertyExpression = Expression.Property(parameterExpression, "字段名称");
var constantExpression = Expression.Constant(ConvertFieldValue("字段的值", propertyExpression.Type));
var equalExpression = Expression.Equal(propertyExpression,constantExpression);
var lambdaExpression = Expression.Lambda(equalExpression, parameterExpression);

private object ConvertFieldValue(string fieldValue, Type type)
{
 		    if (type == typeof(DateTime))
           {
               return DateTime.ParseExact(fieldValue, "dd/MM/yyyy HH:mm:ss",CultureInfo.InvariantCulture);
           }
           else if (type == typeof(bool))
           {
               return Convert.ToBoolean(fieldValue);
           }
           else if (type == typeof(int))
           {
               return Convert.ToInt32(fieldValue);
           }
           else if (type == typeof(long))
           {
               return Convert.ToInt64(fieldValue);
           }
           else
           {
               return fieldValue;
           }
}
2、设置参数
//获取参数列表
var parameters = queryMethod.GetParameters();
var arguments = new List<object>();
for (int i = 0; i < parameters.Length; i++)
{
   if (i == 0)
   {
      //如果不需要根据条件查询可以直接设置为默认值
      arguments.Add(lambdaExpression); 
   }
   else
   {
      //设置其他参数为默认值
      arguments.Add(Missing.Value);
   }
}

4、调用方法

var list = queryMethod.Invoke(repository, arguments.ToArray());

结尾

以上就是所有实现的代码,如果有更好的实现方式,可以在评论区留言,谢谢大家

我们是
袋鼠云数栈 UED 团队
,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。

本文作者:霜序(
掘金
)

前言

在我们的业务应用中越来越多的应用到编码内容,例如在 API 中,给到后端的 SQL 都是通过 Base64 加密的数据等等。

能够发现我们的代码中,使用的 window 对象上的 btoa 方法实现的 Base64 编码,那 btoa 具体是如何实现的呢?将在下面的内容中为大家讲解。

那我们就先从一些基础知识开始深入了解吧~

什么是编码

编码,是信息从一种形式转变为另一种形式的过程,简要来说就是语言的翻译。

将机器语言(二进制)转变为自然语言。

五花八门的编码

ASCII 码

ASCII 码是一种字符编码标准,用于将数字、字母和其他字符转换为计算机可以理解的二进制数。

它最初是由美国信息交换标准所制定的,它包含了 128 个字符,其中包括了数字、大小写字母、标点符号、控制字符等等。

在计算机中一个字节可以表示256众不同的状态,就对应256字符,从 00000000 到 66666666666611。ASCII 码一共规定了128字符,所以只需要占用一个字节的后面7位,最前面一位均为0,所以 ASCII 码对应的二进制位 00000000 到 06666666666661。

file

非 ASCII 码

当其他国家需要使用计算机显示的时候就无法使用 ASCII 码如此少量的映射方法。因此技术革新开始啦。

  • GB2312
    收录了6700+的汉字,使用两个字节作为编码字符集的空间
  • GBK
    GBK 在保证不和 GB2312/ASCII 冲突的情况下,使用两个字节的方式编码了更多的汉字,达到了2w
  • 等等

全面统一的 Unicode

面对五花八门的编码方式,同一个二进制数会被解释为不同的符号,如果使用错误的编码的方式去读区文件,就会出现乱码的问题。

那能否创建一种编码能够将所有的符号纳入其中,每一个符号都有唯一对应的编码,那么乱码问题就会消失。因此 Unicode 借此机会统一江湖。是由一个叫做
Unicode 联盟
的官方组织在维护。

Unicode 最常用的就是使用两个字节来表示一个字符(如果是更为偏僻的字符,可能所需字节更多)。现代操作系统都直接支持 Unicode。

Unicode 和 ASCII 的区别

  • ASCII 编码通常是一个字节,Unicode 编码通常是两个字节.
    字母 A 用 ASCII 编码十进制为 65,二进制位 01000001;而在 Unicode 编码中,需要在前面全部补0,即为 00000000 01000001
  • 问题产生了,虽然使用 Unicode 解决乱码的问题,但是为纯英文的情况,存储空间会大一倍,传输和存储都不划算。

问题对应的解决方案之UTF-8

UTF-8 全名为 8-bit Unicode Transformation Format

本着节约的精神,又出现了把 Unicode 编码转为可变长编码的 UTF-8。可以根据不同字符而变化字节长度,使用1~4字节表示一个符号。UTF-8 是 Unicode 的实现方式之一。

UTF-8 的编码规则

  1. 对于单字节的符号,字节的第一位设置为0,后面七位为该字符的 Unicode 码。因此对于英文字母,UTF-8 编码和 ASCII 编码是相同的。
  2. 对于 n 字节的符号,第一个字节的前 n 位都是1,第 n+1 位为0,后面的字节的前两位均为10。剩下的位所填充的二进制就是这个字符的 Unicode 码

对应的编码表格

Unicode 符号范围 UTF-8 编码方式
0000 0000-0000 007F (0-127) 0xxxxxxx
0000 0080-0000 07FF (128-2047) 110xxxxx 10xxxxxx
0000 0800-0000 FFFF (2048-65535) 6666660xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF (65536往上) 66666610xxx 10xxxxxx 10xxxxxx 10xxxxxxx

在 Unicode 对应表中查找到“杪”所在的位置,以及其对应的十六进制 676A,对应的十进制为 26474(110066666601101010),对应三个字节 6666660xxxx 10xxxxxx 10xxxxxx

将110066666601101010的最后一个二进制依次填充到6666660xxxx 10xxxxxx 10xxxxxx从后往前的 x ,多出的位补0即可,中,得到66666600110 10066666601 10101010 ,转换得到39a76a,即是杪字对应的 UTF-8 的编码

file

  • >> 向右移动,前面补 0, 如 104 >> 2 即 01101000=> 00011010
  • & 与运算,只有两个操作数相应的比特位都是 1 时,结果才为 1,否则为 0。如 104 & 3即 01101000 & 00000011 => 00000000,& 运算也用在取位时
  • | 或运算,对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1,否则为 0。如 01101000 | 00000011 => 01101011
function unicodeToByte(input) {
    if (!input) return;
    const byteArray = [];
    for (let i = 0; i < input.length; i++) {
        const code = input.charCodeAt(i); // 获取到当前字符的 Unicode 码
        if (code < 127) {
            byteArray.push(code);
        } else if (code >= 128 && code < 2047) {
            byteArray.push((code >> 6) | 192);
            byteArray.push((code & 63) | 128);
        } else if (code >= 2048 && code < 65535) {
            byteArray.push((code >> 12) | 224);
            byteArray.push(((code >> 6) & 63) | 128);
            byteArray.push((code & 63) | 128);
        }
    }
    return byteArray.map((item) => parseInt(item.toString(2)));
}

问题对应的解决方案之UTF-16

UTF-16 全名为 16-bit Unicode Transformation Format
在 Unicode 编码中,最常用的字符是0-65535,UTF-16 将0–65535范围内的字符编码成2个字节,超过这个的用4个字节编码

UTF-16 编码规则

  1. 对于 Unicode 码小于 0x10000 的字符, 使用2个字节存储,并且是直接存储 Unicode 码,不用进行编码转换
  2. 对于 Unicode 码在 0x10000 和 0x10FFFF 之间的字符,使用 4 个字节存储,这 4 个字节分成前后两部分,每个部分各两个字节,其中,前面两个字节的前 6 位二进制固定为 110110,后面两个字节的前 6 位二进制固定为 110666666,前后部分各剩余 10 位二进制表示符号的 Unicode 码 减去 0x10000 的结果
  3. 大于 0x10FFFF 的 Unicode 码无法用 UTF-16 编码

对应的编码表格

Unicode 符号范围 具体Unicode码 UTF-16 编码方式 字节
0000 0000-0000 FFFF (0-65535) xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 2字节
0001 0000-0010 FFFF (65536往上) yy yyyyyyyy xx xxxxxxxx 110110yy yyyyyyyy 110666666xx xxxxxxxx 4字节

“杪”字的 Unicode 码为 676A(26474),小于 65535,所以对应的 UTF-16 编码也为 676A
找一个大于 0x10000 的字符,
0x1101F
,进行 UTF-16 编码
file

字节序

对于上述讲到的 UTF-16 来说,它存在一个字节序的概念。

字节序就是字节之间的顺序,当传输或者存储时,如果超过一个字节,需要指定字节间的顺序。

最小编码单元是多字节才会有字节序的问题存在,UTF-8 最小编码单元是一个字节,所以它是没有字节序的问题,UTF-16 最小编码单元是两个字节,在解析一个 UTF-16 字符之前,需要知道每个编码单元的字节序。

为什么会出现字节序?
计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。
所以,除了计算机的内部处理,其他的场合比如网络传输和文件储存,几乎都是用的大端字节序。
正是因为这些原因才有了字节序。

比如:前面提到过,"杪"字的 Unicode 码是 676A,"橧"字的 Unicode 码是 6A67,当我们收到一个 UTF-16 字节流 676A 时,计算机如何识别它表示的是字符 "杪"还是 字符 "橧"呢 ?

对于多字节的编码单元需要有一个标识显式的告诉计算机,按着什么样的顺序解析字符,也就是字节序。

  • 大端字节序(Big-Endian),表示高位字节在前面,低位字节在后面。高位字节保存在内存的低地址端,低位字节保存在在内存的高地址端。
  • 小端字节序(Little-Endian),表示低位字节在前,高位字节在后面。高位字节保存在内存的高地址端,而低位字节保存在内存的低地址端。
    file

简单聊聊 ArrayBuffer 和 TypedArray、DataView

ArrayBuffer

ArrayBuffer 是一段存储二进制的内存,是字节数组。

它不能够被直接读写,需要创建视图来对它进行操作,指定具体格式操作二进制数据。

可以通过它创建连续的内存区域,参数是内存大小(byte),默认初始值都是 0

TypedArray

ArrayBuffer 的一种操作视图,数据都存储到底层的 ArrayBuffer 中

const buf = new ArrayBuffer(8);
const int8Array = new Int8Array(buf);
int8Array[3] = 44;
const int16Array = new Int16Array(buf);
int16Array[0] = 42;
console.log(int16Array); // [42, 11264, 0, 0]
console.log(int8Array);  // [42, 0, 0, 44, 0, 0, 0, 0]

使用 int8 和 int16 两种方式新建的视图是相互影响的,都是直接修改的底层 buffer 的数据

DataView

DataView 是另一种操作视图,并且支持设置字节序

const buf = new ArrayBuffer(24);
const dataview = new DataView(buf);
dataView.setInt16(1, 3000, true);  // 小端序

明确电脑的字节序

上述讲到,在存储多字节的时候,我们会采用不同的字节序来做存储。那对我们的操作系统来说是有一种默认的字节序的。下面就用上述知识来明确 MacOS 的默认字节序。

function isLittleEndian() {
    const buf = new ArrayBuffer(2);
    const view = new Int8Array(buf);
    view[0]=1;
    view[1]=0;
    console.log(view);
    const int16Array = new Int16Array(buf);
    return int16Array[0] === 1;
}
console.log(isLittleEndian());

通过上述代码我们可以得出此款 MacOS 是小端序列存储


一个

Java SpringBoot 通过javax.validation.constraints下的注解,实现入参数据自动验证
如果碰到
@NotEmpty
否则不生效,注意看下
@RequestBody
前面是否加上了
@Valid

Validation常用注解汇总

Constraint 详细信息
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@NotBlank 被注释的元素不能为空(空格视为空)
@NotEmpty 被注释的元素不能为空 (允许有空格)
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Pattern(value) 被注释的元素必须符合指定的正则表达式
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期

示例

 /**
   * 用户名
   */
  @NotBlank(message = "用户名不能为空")
  private String username;
  /**
   * 用户真实姓名
   */
  @NotBlank(message = "用户真实姓名不能为空")
  private String name;

  /**
   * 密码
   */
  @Pattern(regexp = "^(?:(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[^A-Za-z0-9]))(?=^[^\\u4e00-\\u9fa5]{0,}$).{8,20}$", message = "密码过于简单有被盗风险,请保证密码大于8位,并且由大小写字母、数字,特殊符号组成")  
  private String password;

  /**
   * 邮箱
   */
  @NotBlank(message = "邮箱不能为空")
  @Email(message = "邮箱格式不正确")
  private String email;

  /**
   * 手机号
   */
  @NotBlank(message = "手机号不能为空")
  @Pattern(regexp = "^(1[0-9])\\d{9}$", message = "手机号格式不正确")
  private String mobile;

Demo

入参对象上,添加注解及说明

package com.vipsoft.web.entity;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;

/**
 * 定时任务调度
 */
public class QuartzJob implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 任务序号
     */
    private long jobId;

    /**
     * 任务名称
     */
    @NotBlank(message = "任务名称不能为空")
    @Size(max = 10, message = "任务名称不能超过10个字符")
    private String jobName;

    /**
     * 任务组名
     */
    @NotBlank(message = "任务组名不能为空")
    @Size(max = 10, message = "任务组名不能超过10个字符")
    private String jobGroup;

    /**
     * 调用目标字符串
     */
    private String invokeTarget;

    /**
     * 执行表达式
     */
    private String cronExpression;

    /**
     * cron计划策略 0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行
     */
    private String misfirePolicy = "0";

    /**
     * 并发执行 0=允许,1=禁止
     */
    private String concurrent;

    /**
     * 任务状态(0正常 1暂停)
     */
    private String status;

    /**
     * 备注
     */
    private String remark;
}

Controller @RequestBody 前面必须加上 @Valid 否则不生效

import javax.validation.Valid;

@RestController
@RequestMapping("schedule")
public class ScheduleController {

    private Logger logger = LoggerFactory.getLogger(ScheduleController.class);
  
    @RequestMapping("/add")
    public ApiResult addTask(@Valid @RequestBody QuartzJob param) throws Exception {
        logger.info("添加调度任务 => {} ", JSONUtil.toJsonStr(param));
        
        return new ApiResult("添加成功");
    }
}

异常处理,统一返回对象,方便前端解析
GlobalExceptionHandler

package com.vipsoft.web.exception;

import cn.hutool.core.util.StrUtil;
import com.vipsoft.web.utils.ApiResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.List;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     * 处理自定义异常
     */
    @ExceptionHandler(CustomException.class)
    public ApiResult handleException(CustomException e) {
        // 打印异常信息
        logger.error("### 异常信息:{} ###", e.getMessage());
        return new ApiResult(e.getCode(), e.getMessage());
    }

    /**
     * 参数错误异常
     */
    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
    public ApiResult handleException(Exception e) {
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException validException = (MethodArgumentNotValidException) e;
            BindingResult result = validException.getBindingResult();
            StringBuffer errorMsg = new StringBuffer();
            if (result.hasErrors()) {
                List<ObjectError> errors = result.getAllErrors();
                errors.forEach(p -> {
                    FieldError fieldError = (FieldError) p;
                    errorMsg.append(fieldError.getDefaultMessage()).append(",");
                    logger.error("### 请求参数错误:{" + fieldError.getObjectName() + "},field{" + fieldError.getField() + "},errorMessage{" + fieldError.getDefaultMessage() + "}");
                });
                return new ApiResult(6001, errorMsg.toString());
            }
        } else if (e instanceof BindException) {
            BindException bindException = (BindException) e;
            if (bindException.hasErrors()) {
                logger.error("### 请求参数错误: {}", bindException.getAllErrors());
            }
        }
        return new ApiResult(6001, "参数无效");
    }

    /**
     * 处理HttpRequestMethodNotSupporte异常
     * @param e
     * @return
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Object methodHandler(HttpRequestMethodNotSupportedException e) {
        // 打印异常信息
        logger.error("### 异常信息:{} ###", e.getMessage());
        return new ApiResult(6000, e.getMessage());
    }

    /**
     * 处理所有不可知的异常
     */
    @ExceptionHandler(Exception.class)
    public ApiResult handleOtherException(Exception e) {
        // 打印异常信息
        logger.error("### 系统内部错误:{} ###", e.getMessage(), e);
        String warnMsg = StrUtil.isEmpty(e.getMessage()) ? "### 系统内部错误 ###" : e.getMessage();
        return new ApiResult(6000, "系统内部错误", e.getMessage());
    }

}

统一返回对像 ApiResult

package com.vipsoft.web.utils;


//import com.github.pagehelper.PageInfo;

import java.io.Serializable;

public class ApiResult implements Serializable {

    /**
     * 返回编码 0:失败、1:成功
     */
    private int code;

    /**
     * 返回消息
     */
    private String message;

    /**
     * 返回对象
     */
    private Object data;

    /**
     * 分页对象
     */
    private Page page;

    public ApiResult() {
        this.code = 1;
        this.message = "请求成功";
    }

    public ApiResult(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public ApiResult(Integer code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.setData(data);
    }

    public ApiResult(Object data) {
        this.code = 1;
        this.message = "请求成功";
        this.setData(data);
    }

//    public ApiResult(PageInfo pageInfo) {
//        this.code = 1;
//        this.message = "请求成功";
//        this.setData(pageInfo.getList());
//        this.setPage(convertToPage(pageInfo));
//    }
//
//    public Page convertToPage(PageInfo pageInfo) {
//        Page result = new Page();
//        result.setTotalCount(pageInfo.getTotal());
//        result.setPageNum(pageInfo.getPageNum());
//        result.setPageCount(pageInfo.getPages());
//        result.setPageSize(pageInfo.getPageSize());
//        result.setPrePage(pageInfo.getPrePage());
//        result.setNextPage(pageInfo.getNextPage());
//        return result;
//    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Page getPage() {
        return page;
    }

    public void setPage(Page page) {
        this.page = page;
    }
}

运行结果如下
image

在之前的文章
\(^{[ 1 ]}\)
中,原方案的设计,是基于功能实现的角度去设计的,对于功能性的拓展,考虑不全面,结合收到的反馈意见,对项目进行了拓展优化。完成的优化拓展有如下几个方面




固定会话

原实现方案中预留了
chatId
这个字段,原
chatId
并未起到实际作用,是为后续功能拓展所预留的字段,在原实现方案的网页聊天中,该
chatId
为随机生成的
UUID

网页会话

访问地址 特点 介绍
chat/web 每次进入网页,生成的会话ID都是随机的,保证不同用户的会话上下文不会产生干扰 原实现方案
chat/{chatId} 现进入网页,
chatId
由业务系统指定,可以用于记录消息历史记录,上下文由该
chatId
关联查询
新增方案

固定会话的意义由几个功能来体现

  1. 支持历史消息

  2. 支持用户与会话映射(由业务系统自行实现)


    类似
    ChatGPT
    官网,一个用户关联多个
    chatId
    ,根据选择的会话可以查询到之前询问的历史消息和继续联系上下文进行提问。

历史消息

当前实现方案提供的历史消息功能是基于
本地缓存
构建的,历史消息被存储在内存中,不是持久化存储,当项目重启或者垃圾回收,存储的历史消息记录就会丢失。为了方便业务系统更好的集成,持久化数据存储,提供了如下接口。

对如下接口进行拓展实现,覆盖默认的系统方案,即可完成持久化历史消息存储。

public interface ChatCacheService {
    /**
     * 根据会话记录查询历史记录
     * @param chatId
     * @return
     */
    List<ChatMessage> history(String chatId);

    /**
     * 查询历史记录
     * @param chatId 会话ID
     * @param limit 限制条数
     * @return
     */
    List<ChatMessage> history(String chatId,Integer limit);

    /**
     * 消息写入
     * @param message
     */
    void write(String chatId, ChatMessage message);

}

image

鉴权拓展

lucy-chat
提供了自定义注解
@ChatAuth
,并且将该注解添加到了所有的
API
上,第三方系统集成后,可以通过构建
@Aspect
对该注解进行拦截处理,以判断当前用户是否有权访问对应接口。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface ChatAuth {
    // it is anything
    String value() default "";
}

由业务系统自行实现切面,完成对接口的鉴权,该鉴权模式的优点是具有更大的自由度,可以抛出自定义异常,可以结合其他业务逻辑(如调用请求次数、频率)等进行鉴权处理。

如何构建切面鉴权,请查看
切面实现

开源地址

项目采用
MIT
协议

https://gitee.com/Kindear/lucy-chat

安装

请参考
文章
\(^{[1]}\)

文档

更多文档请参阅
Lucy-Chat 快速上手
\(^{[2]}\)

参考

[1]
Kindear.Springboot 接入 ChatGPT [EB/OL] 博客园

[2]
Lucy-Chat 快速上手