fastapp-微信开发GPT项目第一课

0. 开发说明

在学习开发本项目之前,必须保证有以下知识储备和环境工具。

技术栈说明
python>=3.9、pydantic>=2.7.1python基础,http协议
fastapi>=0.111.0web协程异步框架,有web开发基础,异步编程,类型标注[python3.6提供的typing模块]
mysql>=8.0、Tortoise-ORM>=0.20.1mysql数据库相关
redis>= 6.xredis数据库相关
微信开发者工具、uni-app、HbuilderX编辑器开发小程序项目的UI框架,有小程序开发基础
vue>=3.x、vite前端web开发框架
git代码版本管理工具
docker、docker-compose镜像与容器基本操作

1. 项目构建

1.1 服务端构建

手动创建工程目录,路径不要使用中文或者特殊符号。

fastchat

创建虚拟环境,终端下执行命令如下:

conda create -n fastchat python=3.10

安装完成以后,需要激活当前虚拟环境[切换python解释器],终端执行如下命令:

conda activate fastchat

1.1.1 依赖安装

pip install -U python-dotenv -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -U fastapi -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -U uvicorn[standard] -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -U tortoise-orm[aiomysql] -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -U cryptography -i https://pypi.tuna.tsinghua.edu.cn/simple

1.1.2 项目目录

工程目录尽量和虚拟环境的名称保持一致,pycharm一般都可以自动识别。如果pycharm不能自动识别,则点击编辑器右下角选择自定义解释器即可。

fastchat/ # 工程目录
├─api/    # api服务端-基于fastAPI框架
│  ├─application/   # 项目代码存储目录
│  │  └─__init__.py  # 项目初始化文件【创建App应用对象的函数,各种模块初始化】
│  └─main.py # 服务端程序入口

api/main.py,代码:

import os
import uvicorn
from application import create_app, FastAPIapp: FastAPI = create_app()@app.get('/api')
async def api() -> dict:"""测试接口:return:"""return {'title': 'api测试接口'}if __name__ == '__main__':uvicorn.run('main:app',host='0.0.0.0',port=8000,reload=True)

api/application/__init__.py,代码:

from fastapi import FastAPIdef create_app() -> FastAPI:"""创建web应用对象"""app: FastAPI = FastAPI()return app

通过api/main.py启动api服务端项目,访问地址:http://127.0.0.1:8000/api,效果如下:

在这里插入图片描述

1.1.3 项目配置

在原目录结构基础上增加settings.py.env.gitignore,效果如下:

fastchat/ # 工程目录
├─api/    # api服务端-基于fastAPI框架
│  ├─application # 项目代码存储目录
│  │  ├─settings.py # 项目配置文件
│  │  └─__init__.py  # 项目初始化文件【创建App应用对象的函数,各种模块初始化】
│  ├─.env       # 环境配置文件[被settings.py加载],不会被git记录
│  └─main.py # 服务端程序入口
└─.gitignore   # git忽略文件配置

api/.env,代码:

# -------- 常规配置 --------
APP_ENV=dev        # 当前开发环境
APP_NAME=fastchat  # 应用默认名称
APP_PORT=8000      # web服务器监听端口
APP_HOST=0.0.0.0   # web服务器监听地址,0.0.0.0表示监听任意指向当前服务器的地址
APP_VERSION=v0.0.1 # 项目版本
APP_DEBUG=true     # 调试模式,true表示开启
APP_TIMEZONE=Asia/Shanghai  # 时区

.gitignore,代码:

.env
.idea
__pycache__

api/application/__init__.py,代码:

from fastapi import FastAPI
from dotenv import load_dotenvdef create_app() -> FastAPI:"""创建web应用对象"""app: FastAPI = FastAPI()# 加载.env文件中的环境变量load_dotenv()return app

api/main.py,代码:

import os
import uvicorn
from application import create_app, FastAPIapp: FastAPI = create_app()@app.get('/api')
async def api() -> dict:"""测试接口:return:"""return {'title': f'{os.environ.get("APP_NAME")}测试接口'}if __name__ == '__main__':uvicorn.run('main:app',host=os.environ.get('APP_HOST'),port=int(os.environ.get('APP_PORT')),reload=True)

重启项目,如果项目运行正常,并刷新浏览器后效果如下,则表示配置正确:

在这里插入图片描述

1.1.3.1 数据库配置

在终端下创建数据库,执行命令如下:

# 先进入数据库交互终端
mysql -uroot -p
# 执行数据库创建语句
create database fastchat;
# 创建管理账户,格式:CREATE USER '用户名'@'允许账号连接的主机地址' IDENTIFIED BY '密码';
CREATE USER 'fastchat'@'%' IDENTIFIED BY 'fastchat';
# 给新账户分配管理数据库的权限,格式:GRANT 管理权限 ON 数据库名.数据表名 TO '用户名'@'允许账号连接的主机地址';
GRANT ALL PRIVILEGES ON fastchat.* TO 'fastchat'@'%';

执行效果如下:

在这里插入图片描述

api/.env,环境配置中新增配置项,代码如下:

# -------- 数据库配置 --------
DB_HOST=127.0.0.1    # 数据库地址
DB_PORT=3306           # 数据库端口
DB_USER=fastchat      # 用户名
DB_PASSWORD=fastchat   # 密码
DB_DATABASE=fastchat     # 数据库名
DB_CHARSET=utf8mb4      # 连接编码
DB_POOL_MINSIZE=10      # 连接池中的最小连接数
DB_POOL_MAXSIZE=30     # 连接池中的最大连接数

手动创建配置文件api/application/settings.py,编写tortoise-orm的配置信息,代码:

import os
"""tortoise-orm数据库配置"""
TORTOISE_ORM = {"connections": {"default": {'engine': 'tortoise.backends.mysql',  # MySQL or Mariadb'credentials': {  # 连接参数'host': os.environ.get('DB_HOST', '127.0.0.1'),  # 数据库IP/域名地址'port': int(os.environ.get('DB_PORT', 3306)),  # 端口'user': os.environ.get('DB_USER', 'root'),  # 连接账户'password': os.environ.get('DB_PASSWORD', '123'),  # 连接密码'database': os.environ.get('DB_DATABASE', 'fastchat'),  # 数据库'charset': os.environ.get('DB_CHARSET', 'utf8mb4'),  # 编码'minsize': int(os.environ.get('DB_POOL_MINSIZE', 1)),  # 连接池中的最小连接数'maxsize': int(os.environ.get('DB_POOL_MAXSIZE', 5)),  # 连接池中的最大连接数"echo": bool(os.environ.get('DEBUG', True))  # 执行数据库操作时,是否打印SQL语句}}},'apps': {  # 默认所在的应用目录'models': {  # 数据模型的分组名'models': [],  # 模型所在目录文件的导包路径[字符串格式]'default_connection': 'default',  # 上一行配置中的模型列表的默认连接配置}},# 时区设置# 当use_tz=True,当前tortoise-orm会默认使用当前程序所在操作系统的时区,# 当use_tz=False时,当前tortoise-orm会默认使用timezone配置项中的时区'use_tz': False,'timezone': os.environ.get('APP_TIMEZONE', 'Asia/Shanghai')
}

注册Tortoise-ORM到FastAPI应用对象中。api/application/__init__.py,代码:

from fastapi import FastAPI
from dotenv import load_dotenv
from tortoise.contrib.fastapi import register_tortoise
from . import settingsdef create_app() -> FastAPI:"""创建web应用对象"""app: FastAPI = FastAPI()# 加载.env文件中的环境变量load_dotenv()# 把Tortoise-orm注册到App应用对象中register_tortoise(app,config=settings.TORTOISE_ORM,generate_schemas=False,  # 是否自动生成表结构add_exception_handlers=True,  # 是否启用自动异常处理)return app

完成上面的配置以后,因为tortoise-orm默认并没有连接数据库,因此我们需要编写一个数据表模型进行数据库连接操作以测试连接配置是否正确,不过这块我们先放一放,因为项目开发过程中有可能数据库需要保存很多数据,自然也就需要创建对应很多的数据表模型,而不同的数据对应的功能业务是不同的,因此我们需要分开写在不同的文件或者目录下,所以我们得先配置应用分组,不同的功能分属于不同的应用下,每一个应用都属于自己的数据表模型、api视图接口、路由数据等。

1.1.3.2 应用分组

首先创建分组应用存储目录apps,并在apps目录下先创建2个应用分组目录,分别是common公共数据应用分组与users用户数据应用分组,目录结构如下:

fastchat/ # 工程目录
├─api/    # api服务端-基于fastAPI框架
│  ├─application # 项目代码存储目录
│  │  ├─apps/     # 分组应用存储目录
│  │  │  ├─__init__.py
│  │  │  ├─common/        # 公共数据的应用分组
│  │  │  │  ├─models.py   # 表模型文件
│  │  │  │  ├─views.py      # api视图接口文件
│  │  │  │  └─scheams.py # 请求与响应数据模型文件
│  │  │  └─users/              # 用户数据的应用分组
│  │  │      ├─models.py   # 表模型文件
│  │  │      ├─views.py      # api视图接口文件
│  │  │      └─scheams.py # 请求与响应数据模型文件
│  │  ├─settings.py # 项目配置文件
│  │  └─__init__.py  # 项目初始化文件【创建App应用对象的函数,各种模块初始化】
│  ├─.env       # 环境配置[被settings.py加载],不会被git记录
│  └─main.py # 服务端程序入口
└─.gitignore   # git忽略文件配置

接下来,我们可以把最初编写在入口程序的测试api视图接口,转移到common应用分组目录下的views.py接口视图文件中,api/application/apps/common/views.py,代码:

import os
from fastapi import APIRouterapp = APIRouter()@app.get('/api')
async def api() -> dict:"""测试接口:return:"""return {'title': f'{os.environ.get("APP_NAME")}测试接口'}

入口程序文件api/main.py中不再编写api视图接口,代码:

import os
import uvicorn
from application import create_app, FastAPIapp: FastAPI = create_app()# @app.get('/api')
# async def api() -> dict:
#     """
#     测试接口
#     :return:
#     """
#     return {'title': f'{os.environ.get("APP_NAME")}测试接口'}if __name__ == '__main__':uvicorn.run('main:app',host=os.environ.get('APP_HOST'),port=int(os.environ.get('APP_PORT')),reload=True)

因为common应用分组是我们自定义的目录,所以FastAPI默认是不识别的,所以需要手动把应用分组下的路由注册到App应用对象中,api/application/__init__.py,代码:

from fastapi import FastAPI
from dotenv import load_dotenv
from tortoise.contrib.fastapi import register_tortoise
from . import settings
from .apps.common.views import app as common_appdef create_app() -> FastAPI:"""创建web应用对象"""app: FastAPI = FastAPI()# 加载.env文件中的环境变量load_dotenv()# 把Tortoise-orm注册到App应用对象中register_tortoise(app,config=settings.TORTOISE_ORM,generate_schemas=False,  # 是否自动生成表结构add_exception_handlers=True,  # 是否启用自动异常处理)# 注册各个应用分组下的路由信息,合并到App应用对象app.include_router(common_app, prefix='')return app

再次重启api服务端项目,访问http://127.0.0.1:8000/api,输出内容依旧则表示配置成功。

接下来,我们就可以在users分组应用中创建属于用户相关的数据表模型了,api/application/apps/users/models.py,代码:

from tortoise import models, fieldsclass User(models.Model):# 字段列表id = fields.IntField(pk=True, description='主键')username = fields.CharField(max_length=255, unique=True, description='账号')nickname = fields.CharField(max_length=255, index=True, description='昵称')password = fields.CharField(max_length=255, description='密码')openid   = fields.CharField(max_length=255, unique=True, description='OpenID')mobile   = fields.CharField(max_length=15, index=True, description='手机')avatar   = fields.CharField(max_length=500, null=True, description='头像')country   = fields.CharField(max_length=255, null=True, description='国家')province   = fields.CharField(max_length=255, null=True, description='省份')city   = fields.CharField(max_length=255, null=True, description='城市')sex = fields.BooleanField(default=True, null=True, description='性别')created_time = fields.DatetimeField(auto_now_add=True, description='创建时间')updated_time = fields.DatetimeField(auto_now=True, description="更新时间")deleted_time = fields.DatetimeField(null=True, description="删除时间")# 元数据class Meta:table = "user_info"description = "用户信息"def __repr__(self):return f"User (id={self.id}, username={self.username})"__str__ = __repr__

完成模型创建以后,接下来只需要在api/application/settings.py中把当前新增模型的路径添加到models配置项中,代码:

import os
"""tortoise-orm数据库配置"""
DEBUG = os.environ.get('DEBUG', True)TORTOISE_ORM = {"connections": {.....},'apps': {  # 默认所在的应用目录'models': {  # 数据模型的分组名'models': ['application.apps.users.models'],  # 模型所在目录文件的导包路径[字符串格式],从main.py所在路径开始编写'default_connection': 'default',  # 上一行配置中的模型列表的默认连接配置}},.....
}

注册模型到tortoise-orm中以后,在api/application/__init__.py初始化文件中,把generate_schemas的值改为True,让tortoise-orm自动根据模型建表。api/application/__init__.py,代码如下:

from fastapi import FastAPI
from dotenv import load_dotenv
from tortoise.contrib.fastapi import register_tortoise
from . import settings
from .apps.common.views import app as common_appdef create_app() -> FastAPI:"""创建web应用对象"""app: FastAPI = FastAPI()# 加载.env文件中的环境变量load_dotenv()# 把Tortoise-orm注册到App应用对象中register_tortoise(app,config=settings.TORTOISE_ORM,generate_schemas=True,  # 是否自动生成表结构add_exception_handlers=True,  # 是否启用自动异常处理)# 注册各个应用分组下的路由信息,合并到App应用对象app.include_router(common_app, prefix='')return app

OK,重启项目,查看终端如果正常启动,则表示上面的所有操作正确。继续登陆MySQL数据库,查看是否建表成功,效果如下:

在这里插入图片描述

建表成功表示tortoise-orm配置正确,接下来,我们可以考虑使用数据迁移来管理数据表模型与MySQL数据表的修改记录对应关系。所以先鼠标右键删除数据库中新建的user_info数据表,并在api/application/__init__.py初始化文件中,把generate_schemas的值改为False,操作与代码如下:

在这里插入图片描述

1.1.3.3 数据迁移

对Tortoise-ORM使用数据迁移根据模型创建数据表会更加友好更加方便,安装aerich数据迁移工具,执行命令如下:

pip install -U aerich -i https://pypi.tuna.tsinghua.edu.cn/simple

把aerich注册到Tortoise-ORM中,api/application/settings.py,代码:

# TORTOISE ORM的数据库连接配置
TORTOISE_ORM = {....'apps': {  # 默认所在的应用目录'models': {  # 数据模型的分组名'models': ['application.apps.users.models', 'aerich.models'],  # 模型所在目录文件的导包路径[字符串格式]'default_connection': 'default',  # 上一行配置中的模型列表的默认连接配置}},...
}

使用aerich进行迁移初始化,打开终端,执行命令如下:

cd api/
aerich init -t application.settings.TORTOISE_ORM

生成数据迁移文件,终端命令执行如下:

aerich init-db

操作效果如下:

在这里插入图片描述

1.1.3.4 日志配置

api/application/utils/log.py,代码:

import logging
from logging import handlers, Loggerdef getLogger(name: str='root') -> Logger:"""获取日志器对象:param name: 日期器名字,默认为root:return: 日志器对象"""# 1、创建一个logger日期器对象logger: Logger = logging.getLogger(name)# 2、设置下logger的日志的等级logger.setLevel(logging.DEBUG)if not logger.handlers:# 3、创建合适的Handler(FileHandler要有保存路径)th: logging.StreamHandler = logging.StreamHandler()  # 终端处理器rf: handlers.RotatingFileHandler = handlers.RotatingFileHandler(  # 按文件大小分割日志filename=f"log/{name}.log", # 日志文件名,日志目录log需要手动创建mode='a',  # a=append 追加写入maxBytes=300*1024*1024,  # 单个日志文件大小的最大值backupCount=10,  # 备份日志文件的数量,所有日志数量 = backupCount+filenameencoding='utf-8' # 日志文件内容的编码)# 4、设置下每个Handler的日志等级【Handler的日志等级会覆盖上面logger的日志的等级】th.setLevel(logging.DEBUG)rf.setLevel(logging.INFO)# 5、创建下日志的格式器对象formattersimple_formatter: logging.Formatter = logging.Formatter(fmt="{levelname} {asctime} {pathname}:{lineno} {message}",style="{")verbose_formatter: logging.Formatter = logging.Formatter(fmt="【{name}】{levelname} {asctime} {pathname}:{lineno} {message}",datefmt="%Y-%m-%d %H:%M:%S",style="{")# 6、向Handler中添加上面创建的格式器对象th.setFormatter(simple_formatter)rf.setFormatter(verbose_formatter)# 7、将上面创建的Handler处理器添加到logger日志器中logger.addHandler(th)logger.addHandler(rf)return loggerif __name__ == '__main__':# 8. 调用日志器对象logger打印输出日志logger = getLogger('dl')logger.info("这里是常规运行日志")logger.debug("开发人员在调试程序时自己手动打印的日志")logger.warning("这里是程序遇到未来会废弃的函数/方法时,输出的警告日志")logger.error("这里是程序发生错误时输出的日志")logger.critical("这是致命级别的日志,需要紧急修复的")# 多次调用实例化出来的日志对象,如果name相同,则得到的是同一个日志器对象(单例模式)logger1 = getLogger('dl')print(id(logger1), id(logger))

api/application/utils/middleware.py,代码:

import os, time
from .log import getLoggerasync def log_requests(request, call_next):"""日志中间件"""logger = getLogger(os.environ.get('APP_NAME'))start_time = time.time()response = await call_next(request)process_time = (time.time() - start_time) * 1000formatted_process_time = '{0:.2f}'.format(process_time)logger.info(f"path={request.url.path} timer={formatted_process_time}ms status_code={response.status_code}")return response

注册中间件到App应用对象,代码:

from fastapi import FastAPI
from dotenv import load_dotenv
from tortoise.contrib.fastapi import register_tortoise
from . import settings
from .apps.common.views import app as common_app
from .utils import middlewaredef create_app() -> FastAPI:"""创建web应用对象"""app: FastAPI = FastAPI()# 加载.env文件中的环境变量load_dotenv()# 把Tortoise-orm注册到App应用对象中register_tortoise(app,config=settings.TORTOISE_ORM,generate_schemas=False,  # 是否自动生成表结构add_exception_handlers=True,  # 是否启用自动异常处理)# 注册各个应用分组下的路由信息,合并到App应用对象app.include_router(common_app, prefix='')# 注册中间件函数http_middleware = app.middleware('http')http_middleware(middleware.log_requests)return app
1.1.3.5 异常处理
1.定义四个文件,exception.py(全局处理), main.py(主程序文件), user/user.py(业务模块), user/exception.py(用户模块自己的错误处理)2.exception.py文件
# from fastapi.exceptions import HTTPException
from starlette.exceptions import HTTPException  # 官方推荐注册异常处理器时,应该注册到来自 Starlette 的 HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse# 全局异常       
async def global_exception_handler(request, exc):if exc.status_code == 500:err_msg = 'Server Internal Error'else:err_msg = exc.detailreturn JSONResponse({'code': exc.status_code,'err_msg': err_msg,'status': 'Failed'})# 请求数据无效时的错误处理
""" 
example: http://127.0.0.1/user/{user_id}
success: http://127.0.0.1/user/1
failed: http://127.0.0.1/user/d
"""
async def validate_exception_handler(request, exc):err = exc.errors()[0]return JSONResponse({'code': 400,'err_msg': err['msg'],'status': 'Failed'})golbal_exception_handlers = {HTTPException: global_exception_handler,RequestValidationError: validate_exception_handler
}class BaseAPIException(HTTPException):status_code = 400detail = 'api error'def __init__(self, detail: str = None, status_code: int = None):self.detail = detail or self.detailself.status_code = status_code or self.status_code3.定义user/exception.py
from exception import BaseAPIExceptionclass UserDoesNotExistsException(BaseAPIException):status_code = 10000detail = 'user does not exists'4.定义uers/user.py
from fastapi.routing import APIRouterfrom .exception import UserDoesNotExistsExceptionrouter_user = APIRouter(prefix='/user', tags=['用户模块'])@router_user.get("/{user_id}")
async def get_id_by_user(user_id: int):if user_id != 1:# 这里使用我们自定义的用户错误处理# 返回的统一响应格式{"code":10000,"err_msg":"user does not exists","status":"Failed"}raise UserDoesNotExistsExceptionreturn {"user_id": user_id}5.定义main.py
from fastapi import FastAPI
from exception import golbal_exception_handlers
from user.user import router_userapp = FastAPI(debug=True, exception_handlers=golbal_exception_handlers)app.include_router(router_user, prefix='/api/v1')if __name__ == '__main__':import uvicornuvicorn.run(app='main:app', host='0.0.0.0', port=9002, reload=True)6.响应
# example: http://127.0.0.1:9002/api/v1/user/2
{"code":10000,"err_msg":"user does not exists","status":"Failed"}
# example: http://127.0.0.1:9002/api/v1/user/d
{"code":400,"err_msg":"value is not a valid integer","status":"Failed"}

1.2 客户端构建

启动hbuilderX编辑器,点击文件→新建(N)→1.项目

在这里插入图片描述

选择uni-app类型,输入项目名称,选择模板,点击创建(N),即可成功创建uni-app项目。这里我的项目名称:uniapp。

在这里插入图片描述

1.2.1 运行项目

使用快捷键Ctrl+Alt+,打开设置窗口→运行设置→小程序运行设置,填写微信开发者工具路径。

在这里插入图片描述

注意:如果没有安装,点击蓝色链接去下载安装,并在安装完成以后启动微信开发者工具,进入设置窗口→安全,把服务端端口和自动化接口…信任项目等配置项打开如下:

在这里插入图片描述

小程序配置中,可以直接使用测试AppID也可以使用真实账户的AppID,设置→基本设置→账号信息:

在这里插入图片描述

进入uniapp项目,点击工具栏的运行 -> 运行到小程序模拟器 -> 微信开发者工具:

1.2.2 目录结构

fastchat/
├─
├─api/    # 服务端-基于fastAPI框架
│  └── main.py # 服务端程序入口 
└─uniapp/├─unpackage          编译结果存储目录 (一般存放运行或发行的编译结果)├─pages/                代码文件存储目录 │   ├─index/│   │  └─index.vue      对话页面│   └─login/│        └─login.vue      登陆页面├─static/                 静态资源(如图片、视频等)的存储目录├─App.vue               应用配置,用来配置App全局样式以及监听、应用生命周期├─index.html           程序入口├─main.js                 Vue初始化入口文件├─manifest.json       配置应用名称、appid、logo、版本等打包信息├─pages.json           配置页面路由、导航条、选项卡等页面类信息└─uni.scss              内置的常用样式变量

pages.json ,配置页面路径和基本样式效果:

{"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages{"path": "pages/index/index","style": {"navigationBarTitleText": "Chat"}},{"path" : "pages/login/login","style" : {"navigationBarTitleText" : "login"}}],"window": {"navigationBarBackgroundColor": "#ff00ff","navigationBarTextStyle": "black","navigationBarTitleText": "Chat"},"globalStyle": {"navigationBarTextStyle": "black","navigationBarTitleText": "Chat","navigationBarBackgroundColor": "#F8F8F8","backgroundColor": "#F8F8F8"},"uniIdRouter": {}
}

1.2.3 界面效果

1.2.3.1 登陆页面

uniapp/pages/login/login.vue,代码:

<template><view class="content">    <view class="loginBox"><h3 style="text-align: center;margin-bottom:120rpx;">欢迎登录</h3><view class="inputBox"><view class="ipt"><uni-icons type="contact" size="24" color="rgb(66,157,250)"></uni-icons><input type="text" value="" placeholder="请输入账号"/></view><view class="ipt"><uni-icons type="eye" size="24" color="rgb(66,157,250)"></uni-icons><input type="passsword" value="" placeholder="请输入密码"/></view><view class="ipt"><uni-icons type="checkmarkempty" size="24" color="rgb(66,157,250)"></uni-icons><input type="text" value="" placeholder="请输入验证码"/><view class="yzm">验证码</view></view><button class="login-btn">登录</button></view><view class="tipbox"><view class="txt"> —— 其他账号登录 —— </view><view class="otherUser"><button><uni-icons type="qq" size="40" color="rgb(66,157,250)"></uni-icons></button><button open-type="getUserInfo" @getuserinfo="wxLogin"><uni-icons type="weixin" size="40" color="rgb(2,187,17)"></uni-icons></button></view></view></view></view>
</template><script setup></script><style scoped>svg {position: absolute;bottom: 0;left: 0;width: 100%;height:40%;box-sizing: border-box;display: block;background-color: #ffffff;}.loginBox{position: absolute;top: 50%;left: 50%;transform: translate(-50%,-60%);width: 90%;border-radius: 20rpx;padding: 60rpx;box-sizing: border-box;}h3{color:rgb(66,157,250);font-size: 40rpx;letter-spacing: 10rpx;margin-bottom: 40rpx;}.inputBox{}.ipt{height: 86rpx;display: flex;justify-content: flex-start;align-items: center;margin-bottom: 40rpx;background-color: #f5f5f5;border-radius: 10rpx;padding-left: 10rpx;}.ipt input{margin-left: 20rpx;font-size: 28rpx;}.ipt input{margin-left: 20rpx;}.forgetPwd{margin-top: 30rpx;font-size: 26rpx;color: #b5b5b5;text-align: end;padding:0 10rpx;display: flex;justify-content: space-between;}.login-btn{margin-top: 20rpx;line-height: 85rpx;text-align: center;background: rgb(66,157,250);border-radius: 40rpx;color: #fff;margin-top: 40rpx;}.tip{text-align: center;font-size: 28rpx;position: fixed;bottom: 50rpx;left: 50%;transform: translate(-50%,-50%);color: #f4f4f4;}.tipbox {text-align: center;margin-top: 100rpx;}.otherUser {margin-top: 30rpx;display: flex;justify-content: center;}.otherUser button{margin: 0 10px;padding: 0;height: 42px;line-height: 42px;background: transparent;border: 1px solid transparent;outline: none;}.txt {font-size: 28rpx;color: #cbcbcb;}.otherUser .uni-icons {margin-left: 20rpx;}.yzm{text-align: end;font-size: 24rpx;background: rgb(66,157,250);height: 60rpx;width: 150rpx;line-height: 60rpx;text-align: center;border-radius: 10rpx;color: #fff;}
</style>

展示效果如下:

在这里插入图片描述

1.2.3.2 聊天页面

uniapp/pages/index/index.vue,代码:

<template>
<view class="page-layout"><view class="page-body" id="x_chat"><view :key="index" v-for="(message, index) in messages"><view class="chat-item-body"><view class="chat-item-time">{{message.time}}</view><view key="index" v-if="message.type == 'ai'" class="chat-item-layout chat-left"><view class="chat-inner-layout"><view class="chat-item-name">{{message.sender}}</view><view class="chat-item-msg-layout"><image class="chat-item-photo" v-if="message.photoUrl" :src="message.photoUrl" mode="aspectFit"></image><view class="chat-inner-msg-left" v-html="message.text"></view></view></view></view></view><view :key="index" v-if="message.type == 'sender'" class="chat-item-layout chat-right"><view class="chat-inner-layout"><view class="chat-item-name-right">{{message.sender}}</view><view class="chat-item-msg-layout"><view class="chat-inner-msg-right" v-html="message.text"></view><image class="chat-item-photo" v-if="message.photoUrl" :src="message.photoUrl" mode="aspectFit"></image></view></view></view></view></view><view class="submit-layout"><input class="submit-input" placeholder="点击输入,开始聊天吧" v-model="userInput"/><view class="submit-submit" type="submit" size="mini" @click="sendMessage">发送</view></view>
</view>
</template><script setup>
import {ref} from "vue";
const userInput = ref("");
const messages = ref([{type: 'sender',text: '你是谁?',time: '2024-05-03 14:13:22',photoUrl: 'https://pic1.zhimg.com/80/v2-0aca47cf23db7047d051f03297312d64_720w.webp',},{type: 'ai',text: '我是ChatGPT,一个由OpenAI开发的大型语言模型。我基于GPT-4架构构建,旨在通过自然语言处理技术帮助用户解决各种问题、回答问题、提供建议和进行对话。<br><br>我能够理解和生成文本,处理从简单问题到复杂任务的广泛请求,包括但不限于编写代码、创建内容、提供解释和建议、以及进行翻译。我的知识库截止到2023年10月,这意味着我能提供的信息和回答基于我在那之前的训练数据。<br><br>我不是一个真人,而是一个由人工智能驱动的程序,旨在通过文本形式与用户进行互动。我的目的是帮助用户找到他们需要的信息,解决问题,或者提供有价值的对话。',time: '2024-01-26 13:43:15',photoUrl: 'https://www.lulinux.com/d/file/bigpic/az/234906/xldp0zb1vlw.png',}
])const pageScrollToBottom = ()=>{let that = this;wx.createSelectorQuery().select('#x_chat').boundingClientRect(function (rect) {let top = rect.height * messages.value.length;wx.pageScrollTo({scrollTop: top,duration: 100})}).exec()
}
pageScrollToBottom();const sendMessage = ()=>{if (userInput.value.trim() === '') return;const userMessage = {type: 'sender',text: userInput.value,time: '2024-01-26 13:59:12',photoUrl: 'https://pic2.zhimg.com/80/v2-ab37ad93a61fc94135f1c67ea2412c55_720w.webp',};messages.value.push(userMessage);userInput.value = '';pageScrollToBottom();
}</script><style>
.page-layout {width: 100%;height: 100%;box-sizing: border-box;
}.page-body {width: 100%;display: flex;flex-direction: column;padding-bottom: 56px;
}.chat-item-body {display: flex;flex-direction: column;margin-top: 20rpx;
}.chat-item-time {width: 100vw;text-align: center;font-size: 28rpx;color: #ccc;border-radius: 10rpx;margin-top: 40rpx;
}.chat-item-layout {display: block;max-width: 82%;margin: 1rpx 5rpx;box-sizing: border-box;padding: 0 1rpx;
}.chat-right {float: right;
}.chat-left {float: left;
}.chat-inner-layout {display: flex;flex-direction: column;
}.chat-item-photo {width: 70rpx;height: 70rpx;min-width: 70rpx;min-height: 70rpx;border-radius: 50%;
}.chat-item-msg-layout {display: flex;flex-direction: row;
}.chat-item-name {display: flex;flex-direction: row;align-items: center;font-size: 28rpx;color: #999;border-radius: 10rpx;margin: 5rpx 0 0 80rpx;
}.chat-item-name-right {display: flex;flex-direction: row;align-items: center;font-size: 28rpx;color: #999;border-radius: 10rpx;margin: 5rpx 0 0 5rpx;
}.chat-inner-msg-left {display: inline-block;flex-direction: row;align-items: center;color: #000;font-size: 30rpx;border-radius: 10rpx;background: #eee;padding: 15rpx 15rpx 15rpx 25rpx;margin-left: 12rpx;
}.chat-inner-msg-right {display: inline-block;color: #000;font-size: 30rpx;border-radius: 10rpx;background: #87EE5F;padding: 15rpx 5rpx 15rpx 15rpx;margin-right: 12rpx;
}.submit-layout {position: absolute;bottom: 0;width: 100%;background: #eee;flex-direction: row;
}.submit-layout {width: 100%;position: fixed;bottom: 0;border-top: 1px solid #ddd;padding: 10rpx 0;display: flex;flex-direction: row;align-items: center;
}.submit-input {flex: 1;background: #fff;margin: 5rpx 10rpx;border-radius: 5rpx;padding: 15rpx 20rpx;color: #333;font-size: 30rpx;
}.submit-submit {background-color: rgb(66,157,250);color: #fff;font-weight: 700;font-size: 30rpx;border-radius: 10rpx;padding: 18rpx 30rpx;margin-right: 10rpx;text-align: center;
}
</style>

访问效果如下:

在这里插入图片描述

2. 登陆注册

2.1 登陆功能实现

客户端用户点击获取用户登录需要的code,并把code和用户信息发送给服务端,服务端请求微信服务器,实现登陆流程如下:

在这里插入图片描述

2.1.1 服务端提供登陆接口

passlib 用于处理哈希密码的包,支持许多安全哈希算法以及配合算法使用的实用程序,推荐的算法是 Bcrypt,所以终端下执行命令如下:

pip install -U bcrypt==4.0.1
pip install -U passlib

api/utils.py,代码:

"""工具函数"""
from passlib.context import CryptContextclass Hashing(object):def __init__(self, schemes: str='bcrypt'):self.crypt = CryptContext(schemes=[schemes], deprecated="auto")def hash(self, raw_pwd: str) -> str:"""密码加密:param raw_pwd: 原始密码:return: 密码的哈希值"""return self.crypt.hash(raw_pwd)def verify(self, raw_pwd: str, hashed_pwd: str) -> bool:"""验证密码是否正确:param raw_pwd: 原始密码:param hashed_pwd: 密码的哈希值:return:"""return self.crypt.verify(raw_pwd, hashed_pwd)if __name__ == '__main__':hashing = Hashing()hashed_pwd = hashing.hash("123456")print(hashed_pwd) # 加密后要保存到数据库中的哈希串# 把原密码和加密后的哈希串进行配对,验证通过则返回结果为Trueret = hashing.verify("123456", hashed_pwd)print(ret)

api/scheam.py,代码:

import utils
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field, model_validatorclass UserIn(BaseModel):"""注册用户接口接受的数据格式"""username: str = Field(min_length=3, max_length=16)password: str = Field(min_length=6, max_length=16)mobile: str   = Field(min_length=11, max_length=15)re_password: str@model_validator(mode="after")def check_password(self):"""验证密码和确认密码是否一直:return: 务必返回当前对象"""if self.password != self.re_password:raise ValueError("密码与确认密码不一致!")# 对密码进行哈希加密hashing = utils.Hashing()self.password = hashing.hash(self.password)# 必须有数据返回,否则该数据就丢失了。return selfclass UserOut(BaseModel):"""注册成功返回数据格式"""id: intusername: stravatar: Optional[str] # 允许当前字段的值为None或者strsex: boolcreated_time: datetimeupdated_time: datetimeclass LoginIn(BaseModel):"""登录接口接受的数据格式"""account: str # 账号或手机号password: str # 密码class LoginOut(BaseModel):"""登录成功返回数据格式"""code: int  # 操作结果,数字表示msg: str   # 操作结果,文本表示token: str # 登录凭证,证明用户已经登录

视图文件中编写api接口,api/views.py,代码:

# 导入路由类
import scheams
import models
from utils import Hashing
from fastapi import APIRouter, status, HTTPException
from tortoise.expressions import Q
# 创建一个分组路由对象
router = APIRouter()@router.post('/login', status_code=status.HTTP_201_CREATED)
async def login(user_info: scheams.LoginIn) -> scheams.LoginOut:"""用户登录接口:param user_info: 用户登录信息:return: 登录凭证"""# 根据账号查询用户是否存在user = await models.User.filter(Q(username=user_info.account) | Q(mobile=user_info.account)).first()if not user:# 当前用户不存在raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,detail="当前用户不存在或密码错误!")# 密码验证hashing = Hashing()print(user.password, user_info.password)if hashing.verify(user_info.password, user.password):# 密码正确return {'code': 1,'msg': '登录成功!','token': 'daskdasldasdasd;as;d;asd;as', # 根据一定的规则随机生成}# 来到这里表示密码错误raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,detail="当前用户不存在或密码错误!")

2.1.2 客户端发送登陆请求

代码:

<template><view class="content">    <view class="loginBox"><h3 style="text-align: center;margin-bottom:120rpx;">欢迎登录</h3><view class="inputBox"><view class="ipt"><uni-icons type="contact" size="24" color="rgb(66,157,250)"></uni-icons><input type="text" value="" placeholder="请输入账号"/></view><view class="ipt"><uni-icons type="eye" size="24" color="rgb(66,157,250)"></uni-icons><input type="passsword" value="" placeholder="请输入密码"/></view><view class="ipt"><uni-icons type="checkmarkempty" size="24" color="rgb(66,157,250)"></uni-icons><input type="text" value="" placeholder="请输入验证码"/><view class="yzm">验证码</view></view><button class="login-btn">登录</button></view><view class="tipbox"><view class="txt"> —— 其他账号登录 —— </view><view class="otherUser"><button><uni-icons type="qq" size="40" color="rgb(66,157,250)"></uni-icons></button><button open-type="getUserInfo" @getuserinfo="wxLogin"><uni-icons type="weixin" size="40" color="rgb(2,187,17)"></uni-icons></button></view></view></view></view>
</template><script setup>
const wxLogin = (e)=>{console.log(e);console.log("微信登陆,获取session_key与openid");uni.login({provider: 'weixin',success(response) {console.log(response);let app_id = 'wx3ed3b5aa5674f9ca';let app_secret = 'c49648af2a1659eb1711844dec39e565';let code = response.code;let url = `https://api.weixin.qq.com/sns/jscode2session?appid=${app_id}&secret=${app_secret}&js_code=${code}&grant_type=authorization_code`;uni.request({url,success(response){console.log(response.data);}});}})
}
</script><style scoped>svg {position: absolute;bottom: 0;left: 0;width: 100%;height:40%;box-sizing: border-box;display: block;background-color: #ffffff;}.loginBox{position: absolute;top: 50%;left: 50%;transform: translate(-50%,-60%);width: 90%;border-radius: 20rpx;padding: 60rpx;box-sizing: border-box;}h3{color:rgb(66,157,250);font-size: 40rpx;letter-spacing: 10rpx;margin-bottom: 40rpx;}.inputBox{}.ipt{height: 86rpx;display: flex;justify-content: flex-start;align-items: center;margin-bottom: 40rpx;background-color: #f5f5f5;border-radius: 10rpx;padding-left: 10rpx;}.ipt input{margin-left: 20rpx;font-size: 28rpx;}.ipt input{margin-left: 20rpx;}.forgetPwd{margin-top: 30rpx;font-size: 26rpx;color: #b5b5b5;text-align: end;padding:0 10rpx;display: flex;justify-content: space-between;}.login-btn{margin-top: 20rpx;line-height: 85rpx;text-align: center;background: rgb(66,157,250);border-radius: 40rpx;color: #fff;margin-top: 40rpx;}.tip{text-align: center;font-size: 28rpx;position: fixed;bottom: 50rpx;left: 50%;transform: translate(-50%,-50%);color: #f4f4f4;}.tipbox {text-align: center;margin-top: 100rpx;}.otherUser {margin-top: 30rpx;display: flex;justify-content: center;}.otherUser button{margin: 0 10px;padding: 0;height: 42px;line-height: 42px;background: transparent;border: 1px solid transparent;outline: none;}.txt {font-size: 28rpx;color: #cbcbcb;}.otherUser .uni-icons {margin-left: 20rpx;}.yzm{text-align: end;font-size: 24rpx;background: rgb(66,157,250);height: 60rpx;width: 150rpx;line-height: 60rpx;text-align: center;border-radius: 10rpx;color: #fff;}
</style>

2.2 注册功能实现

2.2.1 服务端提供注册接口

基于pydantic提供的BaseModel定义接口的输入和输出的数据结构,api/scheam.py,代码:

from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field, model_validatorclass UserIn(BaseModel):username: str = Field(min_length=3, max_length=16)password: str = Field(min_length=6, max_length=16)mobile: str   = Field(min_length=11, max_length=15)re_password: str@model_validator(mode="after")def check_password(self):"""验证密码和确认密码是否一直:return: 务必返回当前对象"""if self.password != self.re_password:raise ValueError("密码与确认密码不一致!")# 必须有数据返回,否则该数据就丢失了。return selfclass UserOut(BaseModel):id: intusername: stravatar: Optional[str] # 允许当前字段的值为None或者strsex: boolcreated_time: datetimeupdated_time: datetime

创建API接口的视图文件,api/views.py,代码:

# 导入路由类
import scheams
import models
from fastapi import APIRouter, status
# 创建一个分组路由对象
router = APIRouter()@router.post('/', status_code=status.HTTP_201_CREATED)
async def register(user_info: scheams.UserIn) -> scheams.UserOut:"""用户注册接口:param user_info: 用户注册信息:return:"""user = await models.User.create(**dict(user_info))return user

views.py中的路由对象,注册到项目中,app.py,代码:

from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise
import settings
import viewsdef init_app():app = FastAPI()# 把Tortoise-orm注册到App应用对象中register_tortoise(app,config=settings.TORTOISE_ORM,generate_schemas=False, # 是否自动生成表结构add_exception_handlers=True, # 是否启用自动异常处理)# 注册api接口app.include_router(views.router, prefix='/user')return app

2.2.2 客户端实现注册功能

register/register.vue,代码:

<template><view class="content">    <view class="loginBox"><h3 style="text-align: center;margin-bottom:120rpx;">欢迎登录</h3><view class="inputBox"><view class="ipt"><uni-icons type="contact" size="24" color="rgb(66,157,250)"></uni-icons><input type="text" value="" placeholder="请输入账号"/></view><view class="ipt"><uni-icons type="eye" size="24" color="rgb(66,157,250)"></uni-icons><input type="passsword" value="" placeholder="请输入密码"/></view><view class="ipt"><uni-icons type="checkmarkempty" size="24" color="rgb(66,157,250)"></uni-icons><input type="text" value="" placeholder="请输入验证码"/><view class="yzm">验证码</view></view><button class="login-btn">登录</button></view><view class="tipbox"><view class="txt"> —— 其他账号登录 —— </view><view class="otherUser"><button><uni-icons type="qq" size="40" color="rgb(66,157,250)"></uni-icons></button><button open-type="getUserInfo" @getuserinfo="wxLogin"><uni-icons type="weixin" size="40" color="rgb(2,187,17)"></uni-icons></button></view></view></view></view>
</template><script setup></script><style scoped>svg {position: absolute;bottom: 0;left: 0;width: 100%;height:40%;box-sizing: border-box;display: block;background-color: #ffffff;}.loginBox{position: absolute;top: 50%;left: 50%;transform: translate(-50%,-60%);width: 90%;border-radius: 20rpx;padding: 60rpx;box-sizing: border-box;}h3{color:rgb(66,157,250);font-size: 40rpx;letter-spacing: 10rpx;margin-bottom: 40rpx;}.inputBox{}.ipt{height: 86rpx;display: flex;justify-content: flex-start;align-items: center;margin-bottom: 40rpx;background-color: #f5f5f5;border-radius: 10rpx;padding-left: 10rpx;}.ipt input{margin-left: 20rpx;font-size: 28rpx;}.ipt input{margin-left: 20rpx;}.forgetPwd{margin-top: 30rpx;font-size: 26rpx;color: #b5b5b5;text-align: end;padding:0 10rpx;display: flex;justify-content: space-between;}.login-btn{margin-top: 20rpx;line-height: 85rpx;text-align: center;background: rgb(66,157,250);border-radius: 40rpx;color: #fff;margin-top: 40rpx;}.tip{text-align: center;font-size: 28rpx;position: fixed;bottom: 50rpx;left: 50%;transform: translate(-50%,-50%);color: #f4f4f4;}.tipbox {text-align: center;margin-top: 100rpx;}.otherUser {margin-top: 30rpx;display: flex;justify-content: center;}.otherUser button{margin: 0 10px;padding: 0;height: 42px;line-height: 42px;background: transparent;border: 1px solid transparent;outline: none;}.txt {font-size: 28rpx;color: #cbcbcb;}.otherUser .uni-icons {margin-left: 20rpx;}.yzm{text-align: end;font-size: 24rpx;background: rgb(66,157,250);height: 60rpx;width: 150rpx;line-height: 60rpx;text-align: center;border-radius: 10rpx;color: #fff;}
</style>

3. AI助理

3.1 基于文生文实现AI会话

pip install -U langchain
pip install -U langchain-openai

3.1 服务端调用langchain对接ChatGPT

提供接口,chat/views.py代码:

import os, openai, gradio as gr
from langchain_openai import OpenAI
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationSummaryBufferMemory# 解决请求超时问题
os.environ["http_proxy"] = "http://localhost:7890"
os.environ["https_proxy"] = "http://localhost:7890"os.environ["OPENAI_API_KEY"] = "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"
openai.api_key = os.environ["OPENAI_API_KEY"]memory = ConversationSummaryBufferMemory(llm=ChatOpenAI(# openai_api_key="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", # 从OpenAI官方申请秘钥[可以是用户秘钥,也可以是项目秘钥]# model_name="gpt-3.5-turbo" # 默认是gpt-3.5-turbo),max_token_limit=2048
)conversation = ConversationChain(llm=OpenAI(# api_key="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",max_tokens=2048,temperature=0.5),memory=memory,
)"""基于记忆体实现对话的历史上下文管理"""
def chat(input, history=[]):history.append(input)response = conversation.predict(input=input)history.append(response)# history[::2] 切片语法,每隔两个元素提取一个元素,即提取出所有的输入,# history[1::2]表示从历史记录中每隔2个元素提取一个元素,即提取出所有的输出# zip函数把两个列表元素打包为元组的列表的方式responses = [(u, b) for u, b in zip(history[::2], history[1::2])]print("用户输入:", history[::2])print("AI回答:", history[1::2])print("上下文:", responses)return responses, history"""可视化界面中实现AI对话"""
with gr.Blocks(css="#chatbot{height:800px} .overflow-y-auto{height:800px}") as demo:chatbot = gr.Chatbot(elem_id="chatbot")state = gr.State([])with gr.Row():txt = gr.Textbox(show_label=False, placeholder="请输入你的问题.")txt.submit(chat, [txt, state], [chatbot, state])# 启动项目
demo.launch(share=True)

3.2 客户端请求服务端接口

<template><view class="content">    <view class="loginBox"><h3 style="text-align: center;margin-bottom:120rpx;">欢迎登录</h3><view class="inputBox"><view class="ipt"><uni-icons type="contact" size="24" color="rgb(66,157,250)"></uni-icons><input type="text" value="" placeholder="请输入账号"/></view><view class="ipt"><uni-icons type="eye" size="24" color="rgb(66,157,250)"></uni-icons><input type="passsword" value="" placeholder="请输入密码"/></view><view class="ipt"><uni-icons type="checkmarkempty" size="24" color="rgb(66,157,250)"></uni-icons><input type="text" value="" placeholder="请输入验证码"/><view class="yzm">验证码</view></view><button class="login-btn">登录</button></view><view class="tipbox"><view class="txt"> —— 其他账号登录 —— </view><view class="otherUser"><button><uni-icons type="qq" size="40" color="rgb(66,157,250)"></uni-icons></button><button open-type="getUserInfo" @getuserinfo="wxLogin"><uni-icons type="weixin" size="40" color="rgb(2,187,17)"></uni-icons></button></view></view></view></view>
</template><script setup></script><style scoped>svg {position: absolute;bottom: 0;left: 0;width: 100%;height:40%;box-sizing: border-box;display: block;background-color: #ffffff;}.loginBox{position: absolute;top: 50%;left: 50%;transform: translate(-50%,-60%);width: 90%;border-radius: 20rpx;padding: 60rpx;box-sizing: border-box;}h3{color:rgb(66,157,250);font-size: 40rpx;letter-spacing: 10rpx;margin-bottom: 40rpx;}.inputBox{}.ipt{height: 86rpx;display: flex;justify-content: flex-start;align-items: center;margin-bottom: 40rpx;background-color: #f5f5f5;border-radius: 10rpx;padding-left: 10rpx;}.ipt input{margin-left: 20rpx;font-size: 28rpx;}.ipt input{margin-left: 20rpx;}.forgetPwd{margin-top: 30rpx;font-size: 26rpx;color: #b5b5b5;text-align: end;padding:0 10rpx;display: flex;justify-content: space-between;}.login-btn{margin-top: 20rpx;line-height: 85rpx;text-align: center;background: rgb(66,157,250);border-radius: 40rpx;color: #fff;margin-top: 40rpx;}.tip{text-align: center;font-size: 28rpx;position: fixed;bottom: 50rpx;left: 50%;transform: translate(-50%,-50%);color: #f4f4f4;}.tipbox {text-align: center;margin-top: 100rpx;}.otherUser {margin-top: 30rpx;display: flex;justify-content: center;}.otherUser button{margin: 0 10px;padding: 0;height: 42px;line-height: 42px;background: transparent;border: 1px solid transparent;outline: none;}.txt {font-size: 28rpx;color: #cbcbcb;}.otherUser .uni-icons {margin-left: 20rpx;}.yzm{text-align: end;font-size: 24rpx;background: rgb(66,157,250);height: 60rpx;width: 150rpx;line-height: 60rpx;text-align: center;border-radius: 10rpx;color: #fff;}
</style>

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

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

相关文章

【HTTP】请求“报头”(Host、Content-Length/Content-Type、User-Agent(简称 UA))

Host 表示服务器主机的地址和端口号 URL 里面不是已经有 Host 了吗&#xff0c;为什么还要写一次&#xff1f; 这里的 Host 和 URL 中的 IP 地址、端口什么的&#xff0c;绝大部分情况下是一样的&#xff0c;少数情况下可能不同当前我们经过某个代理进行转发。过程中&#xf…

【Qt | QList 】QList<T> 容器详细介绍和例子代码

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; ⏰发布时间⏰&#xff1a; 2024-09-26 …

国产化低功耗窄带物联网无线通讯方案_ZETA技术

01 物联网系统中为什么要使用ZETA LPWAN 模组 物联网系统中使用ZETA LPWAN模组的原因主要基于以下几个方面&#xff1a; 1、技术优势 低功耗广域网&#xff08;LPWAN&#xff09;特性&#xff1a;ZETA技术是一种基于UNB的低功耗广域网技术协议标准&#xff0c;具有覆盖范围广…

从 Kafka 到 WarpStream: 用 MinIO 简化数据流

虽然 Apache Kafka 长期以来一直是流数据的行业标准&#xff0c;但新的创新替代方案正在重塑生态系统。其中之一是 WarpStream&#xff0c;它最近在 Confluent 的所有权下进入了新的篇章。此次收购进一步增强了 WarpStream 提供高性能、云原生数据流的能力&#xff0c;巩固了其…

【IOS】申请开发者账号(公司)

官网&#xff1a;Apple Developer (简体中文) 申请开发者账号前提 如果是第一次申请建议注册一个新的apple id作为组织的开发者账号。&#xff08;确保apple id的个人信息是真实的&#xff0c;不能是网名或者是其他名。后续的申请步骤需要能和apple id的个人信息对上。&#…

Navicat数据库管理工具实现Excel、CSV文件导入到MySQL数据库

1.所需要的工具和环境 navicat等第三方数据库管理工具云服务器中安装了 1Panel面板搭建的mysql数据库 2.基于 1Panel启动mysql容器 2.1 环境要求 安装前请确保您的系统符合安装条件&#xff1a; 操作系统&#xff1a;支持主流 Linux 发行版本&#xff08;基于 Debian / Re…

“类型名称”在Go语言规范中的演变

Go语言规范&#xff08;The Go Programming Language Specification&#xff09;[1]是Go语言的核心文档&#xff0c;定义了该语言的语法、类型系统和运行时行为。Go语言规范的存在使得开发者在实现Go编译器时可以依赖一致的标准&#xff0c;它确保了语言的稳定性和一致性&#…

c++----继承(初阶)

大家好呀&#xff0c;今天我们也是多久没有更新博客了&#xff0c;今天来讲讲我们c加加中的一个比较重要的知识点继承。首先关于继承呢&#xff0c;大家从字面意思看&#xff0c;是不是像我们平常日常生活中很容易出现的&#xff0c;比如说电视剧里面什么富豪啊&#xff0c;去了…

万魔头戴式耳机好用吗?万魔、西圣、索尼头戴式耳机决赛圈测评

现在耳机市场已经有各种不同类型的耳机&#xff0c;对于有降噪需求的人来说&#xff0c;头戴式耳机是一个不错的选择。那么对于后台有人私信说想知道万魔头戴式耳机好用吗&#xff1f;为了解答这个疑问&#xff0c;今天我就为大家评测西圣H1、万魔SonoFlow和索尼WH-CH520这三款…

【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器

文章目录 C list 容器详解&#xff1a;从入门到精通前言第一章&#xff1a;C list 容器简介1.1 C STL 容器概述1.2 list 的特点 第二章&#xff1a;list 的构造方法2.1 常见构造函数2.1.1 示例&#xff1a;不同构造方法2.1.2 相关文档 第三章&#xff1a;list 迭代器的使用3.1 …

【Linux】Linux 的 权限

一、 Linux 权限的概念 Linux下有两种用户&#xff1a;超级用户&#xff08;root&#xff09;、普通用户。 超级用户&#xff1a;可以在 linux 系统下做任何事情&#xff0c;不受限制普通用户&#xff1a;在 linux 下做有限的事情。超级用户的命令提示符是“#”&#xff0c;普…

初学51单片机之I2C总线与E2PROM

首先先推荐B站的I2C相关的视频I2C入门第一节-I2C的基本工作原理_哔哩哔哩_bilibili 看完视频估计就大概知道怎么操作I2C了&#xff0c;他的LCD1602讲的也很不错&#xff0c;把数据建立tsp和数据保持thd&#xff0c;比喻成拍照时候的摆pose和按快门两个过程&#xff0c;感觉还是…

Mysql之索引优化

指定索引 当一个字段上既有单列索引&#xff0c;又有复合索引时&#xff0c;我们可以通过以下的SQL提示来要求该SQL语句执行时采用哪个索引&#xff1a; use index(索引名称)&#xff1a;建议使用该索引&#xff0c;只是建议&#xff0c;底层mysql会根据实际效率来考虑是否使用…

使用豆包MarsCode 实现高可用扫描工具

以下是「 豆包MarsCode 体验官」优秀文章&#xff0c;作者郝同学测开笔记。 前言&#xfeff; 最近接触K8s&#xff0c;了解到K8s提供了非常方便的实现高可用的能力&#xff0c;再加上掘金推出「豆包MarsCode初体验」征文活动&#xff0c;所以打算使用豆包 MarsCode IDE来实现…

LeetCode(Python)-贪心算法

文章目录 买卖股票的最佳时机问题穷举解法贪心解法 物流站的选址&#xff08;一&#xff09;穷举算法贪心算法 物流站的选址&#xff08;二&#xff09;回合制游戏快速包装 买卖股票的最佳时机问题 给定一个数组&#xff0c;它的第 i 个元素是一支给定股票第 i 天的价格。如果你…

Qemu开发ARM篇-5、buildroot制作根文件系统并挂载启动

文章目录 1、 buildroot源码获取2、buildroot配置3、buildroot编译4、挂载根文件系统 在上一篇 Qemu开发ARM篇-4、kernel交叉编译运行演示中&#xff0c;我们编译了kernel&#xff0c;并在qemu上进行了运行&#xff0c;但到最后&#xff0c;在挂载根文件系统时候&#xff0c;挂…

python之装饰器、迭代器、生成器

装饰器 什么是装饰器&#xff1f; 用来装饰其他函数&#xff0c;即为其他函数添加特定功能的函数。 装饰器的两个基本原则&#xff1a; 装饰器不能修改被装饰函数的源码 装饰器不能修改被装饰函数的调用方式 什么是可迭代对象&#xff1f; 在python的任意对象中&#xff…

C# DotNetty客户端

1. 引入DotNetty包 我用的开发工具是VS2022&#xff0c;不同工具引入可能会有差异 工具——>NuGet包管理器——>管理解决方案的NuGet程序包 搜索DotNetty 2.新建EchoClientHandler.cs类 用于接收服务器返回数据 public class EchoClientHandler : SimpleChannelIn…

【AD那些事 10 】焊盘如何修改为自己想要的形状!!!!! 焊盘设计规则如何更改??????

左侧为修改前焊盘原图 右侧为修改后焊盘图 ——————————————————————————————————————————— 目录 修改焊盘内侧的大小 修改焊盘外侧的大小 更改焊盘设计规则 ——————————————————————————…

Pencils Protocol 即将登录各大 CEX,依旧看好 $DAPP

近期&#xff0c;Scroll生态头部DeFi协议Pencils Protocol迎来了系列重磅市场进展。自9月18日开始&#xff0c;$DAPP通证分别在Tonkensoft、Bounce以及Coresky等平台陆续开启了IDO&#xff0c;并且在短期内售罄。同时在通证售卖完成后&#xff0c;DAPP 通证又在9月27日陆续登录…