【C语言项目】实现一个通讯录,一步一步详细讲解,小白也能看

目录

设计思路

代码实现 

代码改造1

代码改造2

完整代码

代码仓库 


设计思路

1. 通讯录存放的信息

这个通讯录保存的信息包括:名字,年龄,性别,电话,住址。

2. 通讯录的功能

1. 通讯录可以存放100个人的信息。

2. 增加联系人

3. 删除联系人

4. 修改联系人

5. 查询联系人

6. 显示所有人

3. 文件规划

我们准备三个文件完成这个项目。

test.c —— 负责测试

contact.h —— 负责函数和类型声明

contact.c —— 负责函数实现

4. 设计思路 

1. 通讯录保存的信息有很多,我们可以设计一个结构体来存放这些信息。

2. 通讯录要存放100个人的信息,那我们可以创建一个存放这个结构体的数组。


代码实现 

1. 设计存放信息的结构体

1. 我们先设计存放信息的结构体,因为这个结构体可能有多个文件会用到,所以我们统一放在头文件。

2. 名字,性别,电话,住址都是字符串所以用char数组,年龄用int。

3. 为了方便使用,用typedef重命名一下。

//通讯录保存的个人信息
typedef struct Info
{char name[10]; //名字char sex[5];   //性别int age;       //年龄char tele[12]; //电话char addr[20]; //住址
}Info;

4. 不同人有不同的个人信息,我们要把这些信息管理起来,所以我们要创建一个存放结构体的数组。

5. 我们还需要创建一个变量去记录数组元素的数量,以便于我们对数组信息的掌控。

6. 为了方便后续传参,我们再设计一个结构体把这两个变量作为结构体的成员。

//将Cont类型变量作为通讯录的基本单位
typedef struct Cont
{Info data[100]; //存放个人信息结构体的数组int cout;       //记录数组元素个数
}Cont;

2. 菜单实现 

1. 设计一个菜单,利用do while 和 switch case配合,根据不同的功能选择不同的数字。

//菜单
void menu()
{printf("-----------------------------\n");printf("--- 0.退出 --- 1.增加 -------\n");printf("-----------------------------\n");printf("--- 2.删除 --- 3.修改 -------\n");printf("-----------------------------\n");printf("--- 4.查询 --- 5.显示 -------\n");printf("-----------------------------\n");
}int main()
{int input;do{menu();printf("请输入要执行的操作->");scanf("%d", &input);switch (input){case 0:printf("退出成功\n");break;case 1://增加break;case 2://删除break;case 3://修改break;case 4://查询break;case 5://显示break;default:printf("输入无效\n");break;}} while (input);return 0;
}

3. 初始化通讯录

1. 初始化通讯录。利用memset将存放结构体的数组置0,计数也置为0。

void InitCont(Cont* cont)
{//将数组置为0memset(cont, 0, sizeof(cont->data));//将计数置为0cont->cout = 0;return;
}

4. 增加联系人 

1. 先判断是否满了,满了就不能增加了。

2. 如果没满,输入信息,将计数作为数组下标存放,计数加1。

void AddInfo(Cont* cont)
{if (cont->cout == DATA_MAX){printf("通讯录已满,不能新增\n");return;}//利用计数作为数组下标printf("姓名:");scanf("%s", cont->data[cont->cout].name);printf("性别:");scanf("%s", cont->data[cont->cout].sex);printf("年龄:");scanf("%d", &(cont->data[cont->cout].age));printf("电话:");scanf("%s", cont->data[cont->cout].tele);printf("住址:");scanf("%s", cont->data[cont->cout].addr);//增加一个元素,计数+1cont->cout++;
}

5. 显示所有联系人

1. 不用修改数据所以参数可以加const。

2. 根据计数得到元素个数然后遍历打印信息。

3. %10s 表示输出宽度为10并且右对齐,%-10s 加个负号表示左对齐。

void ShowInfo(const Cont* cont)
{printf("%-10s\t%-5s\t%-4s\t%-12s\t%-20s\n", "名字", "性别", "年龄", "电话", "住址");for (int i = 0; i < cont->cout; i++){printf("%-10s\t%-5s\t%-4d\t%-12s\t%-20s\n",cont->data[i].name,cont->data[i].sex,cont->data[i].age,cont->data[i].tele,cont->data[i].addr);}
}

6. 删除联系人

1. 在实现删除之前我们先写一个另外的函数,这个函数是根据输入的姓名,返回它所对应的数组下标。

2. 将最后一个元素覆盖到要删除的地方,然后计数减减。

void DelInfo(Cont* cont)
{printf("输入你要删除的名字->");char name[10];scanf("%s", name);//根据名字返回下标int index = FindByName(cont, name);//找不到if (index == -1){printf("没有这个人\n");return;}//找到了//将最后一个元素覆盖到要删除的地方,然后计数减减cont->data[index] = cont->data[cont->cout - 1];cont->cout--;
}

7. 查询联系人

1. 查询不会修改数据所以参数可以加const。

2. 复用前面写的FindByName函数,通过名字返回下标,根据下标打印数据。

void SeleInfo(const Cont* cont)
{printf("输入你要查询的名字->");char name[10];scanf("%s", name);int index = FindByName(cont, name);if (index == -1){printf("没有这个人\n");return;}printf("%-10s\t%-5s\t%-4s\t%-12s\t%-20s\n", "名字", "性别", "年龄", "电话", "住址");printf("%-10s\t%-5s\t%-4d\t%-12s\t%-20s\n",cont->data[index].name,cont->data[index].sex,cont->data[index].age,cont->data[index].tele,cont->data[index].addr);
}

7. 修改联系人

1. 输入名字找到下标。

2. 利用scanf修改信息。

void ModInfo(Cont* cont)
{printf("输入你要修改的名字->");char name[10];scanf("%s", name);int index = FindByName(cont, name);if (index == -1){printf("没有这个人\n");return;}//利用scanf修改信息printf("姓名:");scanf("%s", cont->data[index].name);printf("性别:");scanf("%s", cont->data[index].sex);printf("年龄:");scanf("%d", &(cont->data[index].age));printf("电话:");scanf("%s", cont->data[index].tele);printf("住址:");scanf("%s", cont->data[index].addr);
}

代码改造1

随着动态内存管理的学习,我们可以对通讯录进行优化改造。

改造目标:

1. 通讯录的空间不要固定,大小是可以调整的。

2. 默认能存放3个人的信息,满了就每次增加2个人的空间。

.

第一步

问题:数组大小写死了。

解决:将数组改成指针,这个指针指向动态开辟的空间。

问题:记录的变量只记录当前个数,不够。

解决:增加一个记录容量的变量。

typedef struct Cont
{Info* data; //指向动态开辟的空间int cout;   //记录当前元素个数int capa;   //记录容量
}Cont;

第二步,

问题:结构体成员发生变化对应初始化函数也需要变化。

解决:指针指向malloc开辟的空间,容量初始化为3。

void InitCont(Cont* cont)
{cont->capa = DEFAULT; cont->data = (Info*)malloc(DEFAULT*sizeof(Info));cont->cout = 0;return;
}

第三步

问题:原本的增加联系人函数放满就不能放了。

解决:增加一个检测容量的函数判断是否需要扩容,扩容考虑用realloc。

int CheckAdd(Cont* cont)
{//比较当前个数与容量if (cont->cout == cont->capa) return 1;else return 0;
}void AddInfo(Cont* cont)
{if (CheckAdd(&cont)){//需要扩容Info* ptr = (Info*)realloc(cont->data, sizeof(Info)*(cont->capa+CHANGE));//扩容失败if (ptr == NULL){perror("realloc");return;}//扩容成功else{cont->data = ptr;cont->capa += CHANGE;}}//利用计数作为数组下标printf("姓名:");scanf("%s", cont->data[cont->cout].name);printf("性别:");scanf("%s", cont->data[cont->cout].sex);printf("年龄:");scanf("%d", &(cont->data[cont->cout].age));printf("电话:");scanf("%s", cont->data[cont->cout].tele);printf("住址:");scanf("%s", cont->data[cont->cout].addr);//增加一个元素,计数+1cont->cout++;
}

第四步,

申请的空间记得释放。

void Des(Cont* cont)
{free(cont->data);cont->data = NULL;cont->capa = cont->cout = 0;
}

代码改造2

随着文件操作的学习,我们又可以对通讯录进行改造。

第一,我们想退出的时候把信息保存起来。

所以我们要保存通讯录信息到文件中。

void SaveCont(Cont* cont)
{//打开文件FILE* pf = fopen("Cont.dat", "wb");if (pf == NULL){perror("fopen");return;}//将信息写入文件for(int i=0; i<cont->cout; i++) fwrite(cont->data + i, sizeof(Info), 1, pf);//关闭文件fclose(pf);pf = NULL;
}

第二,下次运行程序的时候,在初始化的时候,我们可以把之前保存到文件的信息重新加载回通讯录中。

void LoadCont(Cont* cont)
{//打开文件FILE* pf = fopen("Cont.dat", "rb");if (pf == NULL){perror("fopen");return;}//读文件Info tmp;while (fread(&tmp, sizeof(Info), 1, pf)){//赋值之前先判断有没有位置if (0 == CheckAdd(cont)) return;cont->data[cont->cout] = tmp;cont->cout++;}//关闭文件fclose(pf);pf = NULL;
}void InitCont(Cont* cont)
{assert(cont);cont->capa = DEFAULT; cont->data = (Info*)malloc(DEFAULT*sizeof(Info));if (cont->data == NULL){perror("malloc");return;}cont->cout = 0;//将文件信息加载到通讯录中LoadCont(cont);return;
}

完整代码

1. contact.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>#define DATA_MAX 100
#define DEFAULT 3
#define CHANGE 2
//通讯录保存的个人信息
typedef struct Info
{char name[10]; //名字char sex[5];   //性别int age;       //年龄char tele[12]; //电话char addr[20]; //住址
}Info;//将Cont类型变量作为通讯录的基本单位
//typedef struct Cont
//{
//	Info data[DATA_MAX]; //存放个人信息结构体的数组
//	int cout;       //记录数组元素个数
//}Cont;
typedef struct Cont
{Info* data; //指向动态开辟的空间int cout;   //记录当前元素个数int capa;   //记录容量
}Cont;//初始化
void InitCont(Cont*);//增加联系人
void AddInfo(Cont*);//显示所有联系人
void ShowInfo(const Cont*);//删除联系人
void DelInfo(Cont*);//查询联系人
void SelInfo(const Cont*);//修改联系人
void ModInfo(Cont*);//释放空间
void Des(Cont*);//保存信息到文件中
void SaveCont(Cont*);

2. contact.c

#include "contact.h"//void InitCont(Cont* cont)
//{
//	//将数组置为0
//	memset(cont, 0, sizeof(cont->data));
//	//将计数置为0
//	cont->cout = 0;
//
//	return;
//}
//将文件信息加载到通讯录中
int CheckAdd(Cont* cont);
void LoadCont(Cont* cont)
{//打开文件FILE* pf = fopen("Cont.dat", "rb");if (pf == NULL){perror("fopen");return;}//读文件Info tmp;while (fread(&tmp, sizeof(Info), 1, pf)){//赋值之前先判断有没有位置if (0 == CheckAdd(cont)) return;cont->data[cont->cout] = tmp;cont->cout++;}//关闭文件fclose(pf);pf = NULL;
}void InitCont(Cont* cont)
{assert(cont);cont->capa = DEFAULT; cont->data = (Info*)malloc(DEFAULT*sizeof(Info));if (cont->data == NULL){perror("malloc");return;}cont->cout = 0;//将文件信息加载到通讯录中LoadCont(cont);return;
}//void AddInfo(Cont* cont)
//{
//	if (cont->cout == DATA_MAX)
//	{
//		printf("通讯录已满,不能新增\n");
//		return;
//	}
//
//	//利用计数作为数组下标
//	printf("姓名:");
//	scanf("%s", cont->data[cont->cout].name);
//	printf("性别:");
//	scanf("%s", cont->data[cont->cout].sex);
//	printf("年龄:");
//	scanf("%d", &(cont->data[cont->cout].age));
//	printf("电话:");
//	scanf("%s", cont->data[cont->cout].tele);
//	printf("住址:");
//	scanf("%s", cont->data[cont->cout].addr);
//
//	//增加一个元素,计数+1
//	cont->cout++;
//}
//判断是否需要增容
int CheckAdd(Cont* cont)
{//当前个数和容量相等,需要增容if (cont->cout == cont->capa){Info* ptr = (Info*)realloc(cont->data, sizeof(Info) * (cont->capa + CHANGE));if (ptr == NULL) //增容失败{perror("realloc");return 0;}else //增容成功{cont->data = ptr;cont->capa += CHANGE;}}return 1;
}void AddInfo(Cont* cont)
{if(0 == CheckAdd(cont)) return; //增容失败等于0//利用计数作为数组下标printf("姓名:");scanf("%s", cont->data[cont->cout].name);printf("性别:");scanf("%s", cont->data[cont->cout].sex);printf("年龄:");scanf("%d", &(cont->data[cont->cout].age));printf("电话:");scanf("%s", cont->data[cont->cout].tele);printf("住址:");scanf("%s", cont->data[cont->cout].addr);//增加一个元素,计数+1cont->cout++;
}void ShowInfo(const Cont* cont)
{//打印提示行printf("%-10s\t%-5s\t%-4s\t%-12s\t%-20s\n", "名字", "性别", "年龄", "电话", "住址");//遍历数组打印信息for (int i = 0; i < cont->cout; i++){printf("%-10s\t%-5s\t%-4d\t%-12s\t%-20s\n",cont->data[i].name,cont->data[i].sex,cont->data[i].age,cont->data[i].tele,cont->data[i].addr);}
}//根据名字找到下标
int FindByName(const Cont* cont, char* name)
{//遍历数组for(int i=0; i<cont->cout; i++) if(strcmp(cont->data[i].name, name) == 0) return i;return -1;
}void DelInfo(Cont* cont)
{printf("输入你要删除的名字->");char name[10];scanf("%s", name);//根据名字返回下标int index = FindByName(cont, name);//找不到if (index == -1){printf("没有这个人\n");return;}//找到了//将最后一个元素覆盖到要删除的地方,然后计数减减cont->data[index] = cont->data[cont->cout - 1];cont->cout--;
}void SelInfo(const Cont* cont)
{printf("输入你要查询的名字->");char name[10];scanf("%s", name);int index = FindByName(cont, name);if (index == -1){printf("没有这个人\n");return;}printf("%-10s\t%-5s\t%-4s\t%-12s\t%-20s\n", "名字", "性别", "年龄", "电话", "住址");printf("%-10s\t%-5s\t%-4d\t%-12s\t%-20s\n",cont->data[index].name,cont->data[index].sex,cont->data[index].age,cont->data[index].tele,cont->data[index].addr);
}void ModInfo(Cont* cont)
{printf("输入你要修改的名字->");char name[10];scanf("%s", name);int index = FindByName(cont, name);if (index == -1){printf("没有这个人\n");return;}//利用scanf修改信息printf("姓名:");scanf("%s", cont->data[index].name);printf("性别:");scanf("%s", cont->data[index].sex);printf("年龄:");scanf("%d", &(cont->data[index].age));printf("电话:");scanf("%s", cont->data[index].tele);printf("住址:");scanf("%s", cont->data[index].addr);
}void Des(Cont* cont)
{free(cont->data);cont->data = NULL;cont->capa = cont->cout = 0;
}void SaveCont(Cont* cont)
{//打开文件FILE* pf = fopen("Cont.dat", "wb");if (pf == NULL){perror("fopen");return;}//将信息写入文件for(int i=0; i<cont->cout; i++) fwrite(cont->data + i, sizeof(Info), 1, pf);//关闭文件fclose(pf);pf = NULL;
}

3.  test.c

#include "contact.h"//菜单
void menu()
{printf("-----------------------------\n");printf("--- 0.退出 --- 1.增加 -------\n");printf("-----------------------------\n");printf("--- 2.删除 --- 3.修改 -------\n");printf("-----------------------------\n");printf("--- 4.查询 --- 5.显示 -------\n");printf("-----------------------------\n");
}int main()
{Cont cont;InitCont(&cont); //初始化int input;do{menu();printf("请输入要执行的操作->");scanf("%d", &input);switch (input){case 0:SaveCont(&cont);Des(&cont);printf("退出成功\n");break;case 1:AddInfo(&cont);break;case 2:DelInfo(&cont);break;case 3:ModInfo(&cont);break;case 4:SelInfo(&cont);break;case 5:ShowInfo(&cont);break;default:printf("输入无效\n");break;}} while (input);return 0;
}

代码仓库 

Contact/Contact · 林宇恒/code_c - 码云 - 开源中国 (gitee.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/389318.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年 Java 面试八股文(20w字)

> &#x1f345;我是小宋&#xff0c; 一个只熬夜但不秃头的Java程序员。 > &#x1f345;关注我&#xff0c;带你**过面试,读源码**。提升简历亮点&#xff08;14个demo&#xff09; > &#x1f345;我的面试集已有12W 浏览量。 > &#x1f30f;号…

[C++] 深度剖析C_C++内存管理机制

文章目录 内存分布内存分布图解 C语言中动态内存管理方式malloc:callocrealloc C内存管理方式内置类型**自定义类型** operator new & operator deleteoperator new & operator delete函数operator newoperator delete **new T[N]** 与**delete[]** **定位new表达式(pl…

【C语言】指针由浅入深全方位详解!!!

目录 指针 野指针 二级指针 指针数组 字符指针 数组指针 数组参数&#xff0c;指针参数 函数指针 函数指针数组 回调函数 练习题 代码仓库 指针 1. 指针定义 1. 指针是内存中一个最小单元的编号&#xff0c;也就是地址。 2. 平时口语中说的指针&#xff…

【C++】如何巧妙运用C++命名空间:初学者必备指南

C语法相关知识点可以通过点击以下链接进行学习一起加油&#xff01; 本篇将带领大家走进C的旅途&#xff0c;为了更好地学习C这门语言&#xff0c;我们需要了解它的前世今生。在了解完C如何诞生后&#xff0c;将开始我们C之旅第一站"命名空间"。(老早说是C/C博主&…

Java 集合框架:HashMap 的介绍、使用、原理与源码解析

大家好&#xff0c;我是栗筝i&#xff0c;这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 020 篇文章&#xff0c;在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验&#xff0c;并希望进…

零基础入门转录组数据分析——GO+KEGG富集分析

零基础入门转录组数据分析——GOKEGG富集分析 目录 零基础入门转录组数据分析——GOKEGG富集分析1. 富集分析基础知识2. GO富集分析&#xff08;Rstudio&#xff09;——代码实操3. KEGG富集分析&#xff08;Rstudio&#xff09;——代码实操注&#xff1a;配套资源只要改个路径…

PyQt5| 界面设计 |利用Qt Designer实现简单界面交互

目录 1 QtDesigner简单界面设计2 代码部分2.1 ui文件转py文件2.2 界面文件代码2.3 主文件代码2.3.1 主体框架代码2.3.2 实现交互代码 3结果展示 准备工作&#xff1a; 配置好PyQt5相关的库、QtDesigner、pyuic 1 QtDesigner简单界面设计 点击“工具"——>“外部工具&a…

Matlab实现最小二乘法的几种方法

最小二乘法&#xff08;又称最小平方法&#xff09;是一种数学优化技术。它通过最小化误差的平方和寻找数据的最佳函数匹配。 按照图中所提出的问题&#xff08;如图1&#xff09;&#xff0c;要求已知多组解&#xff08;自变量和因变量&#xff09;&#xff0c;求出最佳和最恰…

【C++/STL深度剖析】priority_queue 最全解析(什么是priority_queue? priority_queue的常用接口有哪些?)

目录 一、前言 二、如何区分【优先级队列】与【队列】&#xff1f; 三、priority_queue的介绍 四、priority_queue 的构造 五、priority_queue 的常用接口 &#x1f4a7;push &#x1f4a7;pop &#x1f4a7;size &#x1f4a7;top &#x1f4a7;empty &…

C语言贪吃蛇课程设计实验报告(包含贪吃蛇项目源码)

文末有贪吃蛇代码全览,代码有十分细致的注释!!!文末有贪吃蛇代码全览,代码有十分细致的注释!!!文末有贪吃蛇代码全览,代码有十分细致的注释!!! 码文不易&#xff0c;给个免费的小星星和免费的赞吧&#xff0c;关注也行呀(⑅•͈ᴗ•͈).:*♡ 不要白嫖哇(⁍̥̥̥᷄д⁍̥̥…

【C++/STL】:vector容器的底层剖析迭代器失效隐藏的浅拷贝

目录 &#x1f4a1;前言一&#xff0c;构造函数1 . 强制编译器生成默认构造2 . 拷贝构造3. 用迭代器区间初始化4. 用n个val值构造5. initializer_list 的构造 二&#xff0c;析构函数三&#xff0c;关于迭代器四&#xff0c;有关数据个数与容量五&#xff0c;交换函数swap六&am…

SpringBoot整合Flink CDC实时同步postgresql变更数据,基于WAL日志

SpringBoot整合Flink CDC实时同步postgresql变更数据&#xff0c;基于WAL日志 一、前言二、技术介绍&#xff08;Flink CDC&#xff09;1、Flink CDC2、Postgres CDC 三、准备工作四、代码示例五、总结 一、前言 在工作中经常会遇到要实时获取数据库&#xff08;postgresql、m…

为何重视文件加密?用哪款加密软件好呢?

一、公司都重视文件加密的原因有哪些&#xff1f;保护数据安全&#xff1a;在数字化时代&#xff0c;数据是企业重要的资产之一。文件加密可以确保数据在存储和传输过程中不被未经授权的人员访问或窃取&#xff0c;从而保护数据的机密性和完整性。这对于包含敏感信息&#xff0…

Reat hook开源库推荐

Channelwill Hooks 安装 npm i channelwill/hooks # or yarn add channelwill/hooks # or pnpm add channelwill/hooksAPI 文档 工具 Hooks useArrayComparison: 比较两个数组的变化。useCommunication: 处理组件之间的通信。useCurrencyConverter: 货币转换工具。useCurre…

【Docomo】5G

我们想向您介绍第五代移动通信系统“5G”。 5G 什么是5G&#xff1f;支持5G的技术什么是 5G SA&#xff08;独立&#xff09;&#xff1f;实现高速率、大容量的5G新频段Docomo的“瞬时5G”使用三个宽广的新频段 什么是5G&#xff1f; 5G&#xff08;第五代移动通信系统&#x…

【Elasticsearch】Elasticsearch的分片和副本机制

文章目录 &#x1f4d1;前言一、分片&#xff08;Shard&#xff09;1.1 分片的定义1.2 分片的重要性1.3 分片的类型1.4 分片的分配 二、副本&#xff08;Replica&#xff09;2.1 副本的定义2.2 副本的重要性2.3 副本的分配 三、分片和副本的机制3.1 分片的创建和分配3.2 数据写…

Github Benefits 学生认证/学生包 新版申请指南

本教程适用于2024年之后的Github学生认证申请&#xff0c;因为现在的认证流程改变了很多&#xff0c;所以重新进行了总结这方面的指南。 目录 验证教育邮箱修改个人资料制作认证文件图片转换Base64提交验证 验证教育邮箱 进入Email settings&#xff0c;找到Add email address…

【一图学技术】5.OSI模型和TCP/IP模型关系图解及应用场景

OSI模型和TCP/IP模型关系图解 OSI模型和TCP/IP模型都是网络通信的参考模型&#xff0c;用于描述网络协议的层次结构和功能。下面是它们的定义和区别&#xff1a; OSI模型&#xff08;Open Systems Interconnection Model&#xff09; OSI模型是一个理论上的七层模型&#xff…

揭秘线性代数秩的奥秘:从理论到机器学习的跨越

一、线性代数中的秩&#xff1a;定义与性质 1.1 定义 在线性代数中&#xff0c;秩是一个核心概念&#xff0c;用于描述矩阵或向量组的复杂性和独立性。具体而言&#xff0c;一个矩阵的秩定义为该矩阵中非零子式的最高阶数&#xff0c;而一个向量组的秩则是其最大无关组所含的…

双 Token 三验证解决方案

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验 问题分析 以往的项目大部分解决方案为单 token&#xff1a; 用户登录后&#xff0c;服务端颁发 jwt 令牌作为 token 返回每次请求&#xff0c;前端携带 token 访问&#xff0c;服务端解析 token 进行校验和…