2024年6月

在Web应用程序中,安全性是一个至关重要的方面。Spring Security是Spring框架的一个子项目,用于提供安全访问控制的功能。通过集成Spring Security,我们可以轻松实现用户认证、授权、加密、会话管理等安全功能。本篇文章将指导大家从零开始,在Spring Boot项目中集成Spring Security,并通过MyBatis-Plus从数据库中获取用户信息,实现用户认证与授权。

环境准备

在开始之前,请确保你的开发环境已经安装了Java、Gradle和IDE(如IntelliJ IDEA或Eclipse)。同时,你也需要在项目中引入Spring Boot、Spring Security、MyBatis-Plus以及数据库的依赖。

创建Spring Boot项目

首先,我们需要创建一个Spring Boot项目。可以使用Spring Initializr(
https://start.spring.io/
)来快速生成项目结构。在生成项目时,选择所需的依赖,如Web、Thymeleaf(或JSP)、Spring Security等。

添加Spring Security依赖

在项目的pom.xml(Maven)或build.gradle(Gradle)文件中,添加Spring Security的依赖。
对于Gradle,添加以下依赖:

group = 'cn.daimajiangxin'
version = '0.0.1-SNAPSHOT'
description ='Spring Security'
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java:8.0.17'
    // MyBatis-Plus 依赖
    implementation 'com.baomidou:mybatis-plus-spring-boot3-starter:3.5.5'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

对于Maven,添加以下依赖:

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-security</artifactId>  
</dependency>

创建实体类

创建一个简单的实体类,映射到数据库表。

package cn.daimajiangxin.springboot.learning.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;

@TableName(value ="user")
@Data
public class User implements Serializable {
    /**
     * 学生ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 姓名
     */
    private String name;
    @TableField(value="user_name")
    private String userName;

    private String password;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 年龄
     */
    private Integer age;

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



    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

创建Mapper接口

创建对应的Mapper接口,通常放在与实体类相同的包下,并继承BaseMapper 接口。例如:

package cn.daimajiangxin.springboot.learning.mapper;

import cn.daimajiangxin.springboot.learning.model.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface UserMapper extends BaseMapper<User> {

}

创建Mapper XML文件

在resources的mapper目录下创建对应的XML文件,例如UserMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.daimajiangxin.springboot.learning.mapper.UserMapper">

    <resultMap id="BaseResultMap" type="cn.daimajiangxin.springboot.learning.model.User">
            <id property="id" column="id" jdbcType="BIGINT"/>
            <result property="name" column="name" jdbcType="VARCHAR"/>
            <result property="user_name" column="userName" jdbcType="VARCHAR"/>
            <result property="password" column="password" jdbcType="VARCHAR"/>
            <result property="email" column="email" jdbcType="VARCHAR"/>
            <result property="age" column="age" jdbcType="INTEGER"/>
            <result property="remark" column="remark" jdbcType="VARCHAR"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,name,email,age,remark
    </sql>
  
    <select id="findAllUsers"  resultMap="BaseResultMap">
     select
       <include refid="Base_Column_List"></include>
     from user
    </select>
</mapper>

创建Service 接口

在service目录下服务类接口UserService

package cn.daimajiangxin.springboot.learning.service;

import cn.daimajiangxin.springboot.learning.model.User;
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserService extends IService<User> {

}

创建Service实现类

在service目录下创建一个impl目录,并创建UserService实现类UserServiceImpl

package cn.daimajiangxin.springboot.learning.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.daimajiangxin.springboot.learning.model.User;
import cn.daimajiangxin.springboot.learning.service.UserService;
import cn.daimajiangxin.springboot.learning.mapper.UserMapper;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>implements UserService{

}

创建UserDetailsService实现类

package cn.daimajiangxin.springboot.learning.service.impl;

import cn.daimajiangxin.springboot.learning.model.User;
import cn.daimajiangxin.springboot.learning.service.UserService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Resource
    private  UserService userService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User>  queryWrapper=new LambdaQueryWrapper<User>();
        queryWrapper.eq(User::getUserName,username);
        User user=userService.getOne(queryWrapper);
        List<GrantedAuthority> authorities = new ArrayList<>();
        return new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(),authorities );
    }
}

Java配置类

创建一个配置类,并创建SecurityFilterChain 的Bean。

package cn.daimajiangxin.springboot.learning.config;

import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig   {
    @Resource
    private UserDetailsService userDetailsService;
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers("/", "/home")
                                .permitAll()
                                .anyRequest().authenticated() // 其他所有请求都需要认证
                )
                .formLogin(formLogin ->
                        formLogin
                                .loginPage("/login") // 指定登录页面
                                .permitAll() // 允许所有人访问登录页面
                )
                .logout(logout ->
                        logout
                                .permitAll() // 允许所有人访问注销URL
                )    // 注册重写后的UserDetailsService实现
                .userDetailsService(userDetailsService);
        return http.build();
        // 添加自定义过滤器或其他配置
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new StandardPasswordEncoder();
    }
}

创建登录页面

在src/main/resources/templates目录下创建一个Thymeleaf模板作为登录页面,例如login.html。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>登录</title>
</head>
<body>
<form th:action="@{/doLogin}" method="post">
    <div><label> User Name : <input type="text" name="username"/> </label></div>
    <div><label> Password: <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="登录"/></div>
</form>
</body>
</html>

创建控制器

创建一个UserController,

package cn.daimajiangxin.springboot.learning.controller;

import cn.daimajiangxin.springboot.learning.model.User;
import cn.daimajiangxin.springboot.learning.service.UserService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@RestController
public class UserController {
    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping({"/login",})
    public ModelAndView login(Model model) {
        ModelAndView mv = new ModelAndView("login");
        return mv ;
    }
    @PostMapping("/doLogin")
    public String doLogin(@RequestParam String username,@RequestParam String password) {
        QueryWrapper<User> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("user_name",username);
        User user=  userService.getOne(queryWrapper);
        if(user==null){
            return "登录失败,用户没有找到";
        }
        if(! user.getPassword().equals(password)){
            return "登录失败,密码错误";
        }
        return "登录成功";
    }

}

登录页面

运行你的Spring Boot应用程序,用浏览器访问
http://localhost:8080/login
.
20240601104136


和我一起学习更多精彩知识!!!关注我公众号:代码匠心,实时获取推送。

源文来自:
https://daimajiangxin.cn

源码地址:
https://gitee.com/daimajiangxin/springboot-learning

只有让LLM(大模型)学会使用工具,才能做出一系列实用的AI Agent,才能发挥出LLM真正的实力。本篇,我们让AI Agent使用更多的工具,比如:外部搜索、分析CSV、文生图、执行代码等。

1. 使用工具的必要性

LLM(大模型)如果没有使用工具的能力,那就相当于一个有着聪明大脑 但四肢僵硬的
渐冻人
,什么事儿也做不了。人类之所以区别于动物,正是因为学会了使用工具。因此,赋予LLM使用工具的能力至关重要。

我们需要 LLM去帮助执行各种任务。而Tool(工具)就是LLM 在执行任务过程中,能够调用的外部能力。比如:需要检索外部资料时,可以调用检索工具;需要执行一段代码时,可以调用自定义函数去执行。

2. LangChain的Tool规范

所有的工具肯定要遵守一套规范,才能让LLM随意调用。为此,LangChain 抽象出一个
Tool 层
,只要是遵守这套规范的函数就是
Tool
对象,就可以被 LLM调用。

2.1. Tool规范

Tool的规范也简单,只要有三个属性就行:
name

description

function

  • name:工具的名称。
  • description:对工具的功能描述,后续这个描述文本会添加到Prompt(提示词)中,LLM 将根据description来决定是否调用该工具。
  • function:此工具实际运行的函数。

只要遵守这个规范就行,使用形式可以有多种,下文的实践代码会介绍到。

2.2. Agent使用工具的流程

让AI Agent使用工具,需要定义
Agent

AgentExecutor

AgentExecutor
维护了
Tool.name

Tool

Map
结构。

LLM根据Prompt(包含了
Tool
的描述) 和 用户的问题,判断是否需要调用工具,确定某个工具后,在根据
Tool
的名称 和 调用参数,到映射
Map
中获找
Tool
实例,找到之后调用
Tool
实例的
function

3. 如何使用各种Tool

自定义Tool
只需要遵守以上规范就可以,下面以几个常用的工具做示例。

下文有些工具用到了
toolkits

toolkits

LangChain提供的工具包,旨在简化使用工具的成本

toolkits
里提供了丰富的工具,还在不断叠加,大部分的工具都可以在里面找到。

3.1. 外部搜索

使用外部搜索工具。本文使用的是
serpapi

serpapi
集成了Google、百度等多家搜索引擎,通过api的形式调用,非常方便。

官网地址:
https://serpapi.com/
。可以自行注册,有一些免费额度。外部搜索工具定义如下:

# 1. 使用@tool装饰器,定义搜索工具
@tool
def search(query: str) -> str:
    """只有在需要了解实时信息 或 不知道的事情的时候 才会使用这个工具,需要传入要搜索的内容。"""
    serp = SerpAPIWrapper()
    result = serp.run(query)
    return result

3.2. 文生图

文生图工具是使用LangChain社区提供的
DallEAPIWrapper
类,本文使用OpenAI的图片生成模型
Dall-E-3
,具体代码如下:

# 2. 使用Tool工具类,定义图片生成工具
dalle_image_generator = Tool(
    name="基于OpenAI Dall-E-3的图片生成器",
    func=DallEAPIWrapper(model="dall-e-3").run,
    description="OpenAI DALL-E API 的包装器。当你需要根据 描述的文本 生成图像时 使用此工具,需要传入 对于图像的描述。",
)

这里的
DallEAPIWrapper(model="dall-e-3").run
方法就是个函数,实际是去调用了OpenAI的接口。

3.3. 代码执行器

代码执行器工具,可以执行代码 或者 根据自然语言生成代码。主要使用LangChain提供的
PythonREPLTool
和 LangChain提供的
toolkits

比如
create_python_agent
就简化了创建Python解释器工具的过程。代码如下:

# 3. 使用toolkit,定义执行Python代码工具
python_agent_executor = create_python_agent(
    llm=model,
    tool=PythonREPLTool(),
    verbose=True,
    agent_executor_kwargs={"handle_parsing_errors": True},
)

3.4. 分析CSV

CSV工具,用来分析csv文件。依旧是使用
toolkits
工具包里的
create_csv_agent
函数快出创建工具。代码如下:

# 4. 使用toolkit,定义分析CSV文件工具
csv_agent_executor = create_csv_agent(
    llm=model,
    path="course_price.csv",
    verbose=True,
    agent_executor_kwargs={"handle_parsing_errors": True},
    allow_dangerous_code=True,
)

3.5. 完整代码

上面介绍了AI Agent的常用工具,定义好工具之后,在把工具放入到工具集中,最后在定义Agent 和 AgentExecutor就算完成了。短短几十行代码,就可以让LLM使用这么多工具了。

完整代码如下:

import os
from langchain import hub
from langchain_openai import ChatOpenAI
from langchain.agents import create_structured_chat_agent, AgentExecutor, Tool
from langchain.tools import BaseTool, StructuredTool, tool
from langchain_experimental.agents.agent_toolkits import (
    create_python_agent,
    create_csv_agent,
)
from langchain_community.utilities import SerpAPIWrapper
from langchain_experimental.tools import PythonREPLTool
from langchain_community.utilities.dalle_image_generator import DallEAPIWrapper

# 需要先安装serpapi, pip install serpapi, 还需要到 https://serpapi.com/ 去注册账号

# SERPAPI_API_KEY 和 OPENAI 相关密钥,注册到环境变量
os.environ["SERPAPI_API_KEY"] = (
    "9dd2b2ee429ed996c75c1daf7412df16336axxxxxxxxxxxxxxx"
)
os.environ["OPENAI_API_KEY"] = "sk-a3rrW46OOxLBv9hdfQPBKFZtY7xxxxxxxxxxxxxxxx"
os.environ["OPENAI_API_BASE"] = "https://api.302.ai/v1"

model = ChatOpenAI(model_name="gpt-3.5-turbo")


# 基于reAct机制的Prompt模板
prompt = hub.pull("hwchase17/structured-chat-agent")



# 各种方式定义工具

# 1. 使用@tool装饰器,定义搜索工具
@tool
def search(query: str) -> str:
    """只有在需要了解实时信息 或 不知道的事情的时候 才会使用这个工具,需要传入要搜索的内容。"""
    serp = SerpAPIWrapper()
    result = serp.run(query)
    return result


# 2. 使用Tool工具类,定义图片生成工具
dalle_image_generator = Tool(
    name="基于OpenAI Dall-E-3的图片生成器",
    func=DallEAPIWrapper(model="dall-e-3").run,
    description="OpenAI DALL-E API 的包装器。当你需要根据 描述的文本 生成图像时 使用此工具,需要传入 对于图像的描述。",
)

# 3. 使用toolkit,定义执行Python代码工具
python_agent_executor = create_python_agent(
    llm=model,
    tool=PythonREPLTool(),
    verbose=True,
    agent_executor_kwargs={"handle_parsing_errors": True},
)

# 4. 使用toolkit,定义分析CSV文件工具
csv_agent_executor = create_csv_agent(
    llm=model,
    path="course_price.csv",
    verbose=True,
    agent_executor_kwargs={"handle_parsing_errors": True},
    allow_dangerous_code=True,
)

# 定义工具集合
tool_list = [
    search,
    dalle_image_generator,
    Tool(
        name="Python代码工具",
        description="""
        当你需要借助Python解释器时,使用这个工具。
        比如当你需要执行python代码时,
        或者,当你想根据自然语言的描述生成对应的代码时,让它生成Python代码,并返回代码执行的结果。
        """,
        func=python_agent_executor.invoke,
    ),
    Tool(
        name="CSV分析工具",
        description="""
        当你需要回答有关course_price.csv文件的问题时,使用这个工具。
        它接受完整的问题作为输入,在使用Pandas库计算后,返回答案。
        """,
        func=csv_agent_executor.invoke,
    ),
]


# 将工具丢给Agent
agent = create_structured_chat_agent(
    llm=model,
    tools=tool_list,
    prompt=prompt
)

# 定义AgentExecutor
agent_executor = AgentExecutor.from_agent_and_tools(
    agent=agent, 
    tools=tool_list, 
    verbose=True, # 打印详细的 选择工具的过程 和 reAct的分析过程
    handle_parsing_errors=True
)



# 不会使用工具
agent_executor.invoke({"input": "你是谁?"})

# 使用查询工具
# agent_executor.invoke({"input": "南京今天的温度是多少摄氏度?现在外面下雨吗?"})

# 使用Python代码工具
# agent_executor.invoke(
#     {
#         "input": """
#         帮我执行```号里的python代码,
        
#         ```python
            
#             def add(a,b):
#                 return a+b
            
#             print("hello world : ", add(100,200))
#         ```
#         """
#     }
# )

# 使用图片生成工具
# agent_executor.invoke(
#     {
#         "input": "帮我生成一副图片,图片描述如下:一个非常忙碌的中国高中生在准备中国的高考,夜已经很深了,旁边他的妈妈一边看书一边在陪伴他,窗外是模糊的霓虹灯。"
#     }
# )

# 使用CSV分析工具
# agent_executor.invoke({"input": "course_price数据集里,一共有哪几个城市?用中文回答"})

一起看下使用工具后,reAct的整个过程。

以上代码经过完整调试,更换下openai和serpapi的密钥即可直接运行,如果遇到问题可以
关注公众号
给我留言。

4. 总结

本文主要聊了AI Agent的工具规范,以及常用工具。AI Agent只有借助工具才能发挥威力。

=====>>>>>>
关于我
<<<<<<=====

本篇完结!欢迎点赞 关注 收藏!!!

原文链接:
https://mp.weixin.qq.com/s/iSJExaJSCe7fXzous17pXg

本文介绍基于
R
语言中的
raster
包,遍历读取
多个文件夹
下的
多张栅格遥感影像
,分别批量对
每一个文件夹
中的
多个栅格图像
计算
平均值
,并将所得
各个结果栅格
分别加以保存的方法。

其中,本文是用
R
语言来进行操作的;如果希望基于
Python
语言实现类似的平均值求取操作,大家可以参考
Python ArcPy批量计算多时相遥感影像的各像元平均值

Python忽略NoData计算多张遥感影像的像元平均值:whitebox库
这两篇文章。

首先,来看一下本文所需实现的需求。如下图所示,现有多个文件夹,其中每一个文件夹内部都含有大量的栅格遥感影像。

image

其中,上图中的每一个文件夹的命名都是以遥感影像的
分幅条带号
为依据的。例如,打开第一个名为
47RMN
的文件夹,其中均为条带号为
47RMN
(即同一空间范围)、
不同成像时间
的遥感影像,如下图所示;其中,紫色框内的遥感影像文件名即可看出,这些图像是
同一条带号

不同时间
的遥感影像数据。

我们要做的,就是分别对每一个文件夹中的全部遥感影像计算平均值,从而得到
不同条带号
遥感影像的平均值;最终我们将得到
多张
结果图像,每一景结果图像就是
这一条带号

不同成像时间
对应的遥感影像的平均值。同时为了方便区分,我们需要将每一景结果图像文件的文件名设置为与
条带号
有关的内容。

明确了需求,我们即可开始代码的撰写。本文所用到的代码如下所示。

library(raster)
result_path <- r"(E:\02_Project\01_Chlorophyll\Select\Result)"
tif_folder <- list.files(path = r"(E:\02_Project\01_Chlorophyll\Select)", pattern = NULL, all.files = FALSE, full.names = TRUE)
for (folder in tif_folder){
  folder_name <- substr(folder, nchar(folder) - 4, nchar(folder))
  tif_file_name <- list.files(path = folder, pattern = ".tif$", full.names = TRUE, ignore.case = TRUE)
  tif_file_all <- stack(tif_file_name)
  NAvalue(tif_file_all) <- -10000
  tif_mean <- calc(tif_file_all, fun = mean, na.rm = TRUE)
  tif_mean_new <- tif_mean / 100
  # plot(tif_mean_new)
  result_file_name <- file.path(result_path, paste(folder_name, "_mean.tif", sep = ""))
  rf <- writeRaster(tif_mean_new, filename = result_file_name, overwrite = TRUE)
  cat(folder_name, "is completed!", "\n")
}

首先,需要通过
library(raster)
代码,导入本文所需的
R
语言
raster
包;关于这一包的配置,大家可以参考
基于R语言的raster包读取遥感影像
。接下来,我们需要指定结果存放的路径,并将其放入变量
result_path
中。

接下来,我们通过
list.files()
函数,将包含有各个条带号的
小文件夹

大文件夹
(也就是本文开头第一张图所示的文件夹)加以遍历,将每一个
小文件夹
的路径存入
tif_folder
。执行上述前
3
行代码后,得到的
tif_folder
结果如下图所示。

可以看到,
tif_folder
是一个字符串,其中每一个元素都是每一个
小文件夹
的路径。

接下来的
for
循环,就是对
tif_folder
加以遍历,即对每一个
小文件夹
进行操作。其中,我们首先通过
substr()
函数,获取当前操作的
小文件夹
名称,并将其存放于
folder_name
中;随后,对当前对应的
小文件夹
加以遍历,取出其中的全部
遥感影像文件
,并存放于
tif_file_name
;接下来,就是读取
全部遥感影像
,并计算其平均值;这里具体的代码解释大家可以参考文章
R语言求取大量遥感影像的平均值、标准差:raster库
。此外需要注意的是,由于我这里每一景遥感影像原本没有专门设置
NoData
值,而是用
-10000
作为其
NoData
值,因此需要通过
NAvalue(tif_file_all) <- -10000
这句代码,将值为
-10000
的像元作为
NoData
值的像元,防止后期计算平均值时对结果加以干扰。

接下来,我们通过
file.path()
函数配置一下输出结果的路径——其中,结果遥感影像文件的名称就可以直接以其所对应的
条带号
来设置,并在条带号后添加一个
_mean
后缀,表明这个是平均值的结果图像;但此外,这个仅仅是文件的名字,还需要将文件名与路径拼接在一起,才可以成为完整的保存路径,因此需要用到
file.path()
函数。最后,将结果图像通过
writeRaster()
函数加以保存即可,这句代码的解释大家同样参考
R语言求取大量遥感影像的平均值、标准差:raster库
这篇文章即可。

最后,由于我们要处理的文件夹比较多,因此可以通过
cat()
函数输出一下当前代码的运行进度。

运行上述代码,我们将在指定的结果保存路径中看到
每一个条带号
对应的平均值结果图像,如下图所示。

至此,大功告成。

简单安装一下MySQL

Windows下(5.7.x)

本体安装

1、首先先下载安装包,名字如下:

mysql-5.7.19-winx64.zip

2、配置环境变量,将解压之后的bin目录添加一下

3、在解压目录下创建my.ini文件,内容如下:

[ client ]
port=3306
default-character-set=utf8
[ mysqld ]
#设为自己MYSQL的安装目录
basedir=D:\zpg\MySQL\mysql-5.7.19-winx64
#设置为MYSQL的数据目录
datadir=D:\zpg\MySQL\mysql-5.7.19-winx64\data
port-3306
character_set_server=utf8
#跳过安全检查
#skip-grant-tables #如果不注释,进入MySQL时就不用输入密码

4、使用
管理员权限
打开cmd,安装mysql

切换到解压目录的bin目录下

cd D:\...\MySQL\mysql-5.7.19-winx64\bin

安装

mysqld -install
mysqld --initialize-insecure --user=mysql #创建data目录

启动MySQL服务

net start mysql

登录(此时没有密码,直接回车即可)

mysql -u root -p

5、修改MySQL密码

登录MySQL后会出现mysql的操作终端,输入以下指令

use mysql;
update user set authentication_string=password('ag') where user='root' and Host='localhost';

修改完成,此时如果在ini中注释了skip-grant-tables,那么下次登录就必须输入正确的密码

命令行连接

mysql -h 127.0.0.1 -P 3306 -u root -pag

Linux下(8.0)

5.79的安装教程:
https://blog.csdn.net/qq_39724355/article/details/131332473

以下是8.0的(即Ubuntu20.04默认支持的版本)

apt install -y mysql-server-8.0

ps:

  • 下载有问题记得
    换下源
  • 不加y有可能会在安装过程中要求设置密码

新建一个数据库

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.01 sec)

mysql> creat database game;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'creat database game' at line 1
mysql> create database game;
Query OK, 1 row affected (0.00 sec)

mysql>

在Linux的终端下进入MySQL-shell然后操作,别打错单词

GUI安装

教程:
https://www.cnblogs.com/FRIM/p/16978145.html

SQLyog安装(
免费

滚吧mtfk

卸载MySQL

如果你不幸安装了错误版本的MySQL,请使用以下方法将其卸载(生产环境别这样干)(
ref

dpkg --list|grep mysql # 查看MySQL依赖
sudo apt-get remove mysql-common # 卸载
# 版本对应即可
sudo apt-get autoremove --purge mysql-server-8.0
# 清除残留数据
dpkg -l|grep ^rc|awk '{print$2}'|sudo xargs dpkg -P
dpkg --list|grep mysql # 这里一般就没有输出了,如果有执行下一步
# 继续删除剩余依赖项
sudo apt-get autoremove --purge mysql-apt-config

远程连接MySQL

使用Navicat等数据库管理工具连接的话,要进行以下设置

设置root密码

root账号默认没有密码(前面可知,回车即可进入mysql-shell)

将root账号密码设置为"root",还是在user表中操作

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '102030';

注意,MySQL 8.0中不再使用
authentication_string
字段,而是使用
plugin

authentication_string
字段进行身份验证。因此以下语句错误

update user set authentication string =password('root') where user = 'root';

状态查看与配置文件位置

首先,你可以查看MySQL服务的状态

service mysql status
service mysql start / stop / restart
systemctl enable mysql 开机自启

要远程连接需要编辑MySQL的配置文件,可能位于以下常见位置

【Linux】
• /etc/my.cnf
• /etc/mysql/my.cnf
• /etc/mysql/debian.cnf【Ubuntu上看密码】
• /etc/mysql/mysql.conf.d/mysqld.cnf【Ubuntu上改IP配置】
【Windows】
• C:\ProgramData\MySQL\MySQL Server X.X\my.ini
• C:\Program Files\MySQL\MySQL Server X.X\my.ini
【Mac】
• /opt/homebrew/etc/my.cnf

以Ubuntu为例,找到
mysqld.cnf
文件后,修改其中的
bind-addres
参数为0.0.0.0(或者直接注释掉)

sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
# 修改 bind-address
bind-address = 0.0.0.0

然后重启MySQL服务

修改登录限制

首先进入MySQL-Shell

mysql -u root -p

默认情况下没密码,回车就行


show databases;
查看当前存在的表,
use mysql
切换到"mysql表"进行后续操作

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| game               |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

mysql> use mysql
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+------------------------------------------------------+
| Tables_in_mysql                                      |
+------------------------------------------------------+
| columns_priv                                         |
...                        
| user                                                 |
+------------------------------------------------------+
37 rows in set (0.01 sec)
mysql>

查找出表中用户名为"root"的内容

mysql> select host, user from user where user='root';
+-----------+------+
| host      | user |
+-----------+------+
| localhost | root |
+-----------+------+
1 row in set (0.00 sec)

mysql> 

要允许任何IP登录,需要将"host"的属性值更改成'%'

mysql> update user set host='%'where user='root' and host='localhost';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

刷新一下
flush privileges;

CURD快速实践

关键字说明

MySQL中的关键字分别负责不同的功能需求,可以分为以下几类(
ref
):

创建一个数据库以及修改表结构

可以在sql-shell里面直接用
SHOW DATABASES XX;
来创建数据库,之后在Navicat中就可以查看得到(也可以在Navicat中直接创建)

这里创建了一个
game
数据库

选择数据库game(
use game;
),就可以在
game
中创建要使用的表了

创建一个玩家表
player
用来保存玩家信息

CREATE TABLE player(
	id INT,
	name VARCHAR(100), # 长度为100的可变字符串
	leveal INT,
	exp INT,
	gold DECIMAL(10,2) # 长度为10并保留两位小数的十进制数
)

使用
DESC player;
来查看表结构(DESC是"描述"的缩写)

创建完表发现有问题怎么办?

使用
ALTER
关键字来
修改表结构

例如,修改玩家名称的长度限制,使用
ALTER
修改(
MODIFY
)表
player
中name列(
COLUMN
)的数据类型

ALTER TABLE player MODIFY COLUMN name VARCHAR(200);

想修改列的名称也可以,比如把name改为nick_name

ALTER TABLE player RENAME COLUMN name TO nick_name;

或者添加新的列也可以,比如添加玩家最后的登录时间

ALTER TABLE player ADD COLUMN final_login DATETIME;
ALTER TABLE player DROP COLUMN final_login; # 删掉

删除整个表的话->
DROP TABLE player;

表中数据的CURD

插入数据


INSERT
来插入数据,
player
后面可以不写列名,这样就会按照默认顺序插入

INSERT INTO player(id, name, leveal, exp, gold) VALUES (1, 'ag', 1, 1, 1);
# 插入多条
INSERT INTO player(id, name) VALUES (2, 'xixi'), (3, 'coco');
# 指定leveal字段的默认值为1
ALTER TABLE player MODIFY COLUMN leveal INT DEFAULT 1;
INSERT INTO player(id, name) VALUES (1, 'tutu');

插完用
SELECT
查询一下

SELECT * FROM player;

修改数据


UPDATE
配合
SET
修改已有列字段中的数据

UPDATE player SET exp = 20 WHERE name = 'ag';
# 当然,不加where条件就可以修改所有数据
UPDATE player SET leveal = 1, gold = 0, exp = 1;

实际中,在UPDATE或者DELETE时不加条件十分危险

删除数据

DELETE FROM player WHERE exp = 1;

数据的导入导出

导出

在终端中使用mysqldump来导出某个数据库

mysqldump -u root -p xxx game > game.sql

将game数据库导出到game.sql(是一条条的sql语句)

导入

假设你刚刚安装好MySQL

此时需要创建一个数据库game

create database game;

然后退出MySQL的命令行界面,在终端中执行如下命令来导入数据:

mysql -u root -p game < game.sql

数据源:
https://github.com/geekhournet/mysql-course

常用的语句

IN:指定多个值

在使用查询关键字
SELECT
时,可以通过
WHERE
关键字限定查询范围,
AND\OR\NOT
可以处理一些逻辑条件,他们的优先级为:NOT>AND>OR

例如,查找等级大于1小于5或者经验值大于1小于5的玩家

SELECT * FROM player WHERE level > 1 AND level < 5 OR exp > 1 AND exp < 5;

(Ps:括号可以改变优先级顺序)

要查找多个不同等级的玩家,就可以使用
IN
实现

例如,查找等级为1、3、5的玩家

SELECT * FROM player WHERE level IN (1,3,5);

(通常与子查询连用)

BETWEEN...AND:范围查询

这个很好理解,例如,查询等级1到10的玩家

SELECT * FROM player WHERE level BETWEEN 1 AND 10;

加上NOT可以取反,例如等级不在1到10之间的玩家

SELECT * FROM player WHERE level NOT BETWEEN 1 AND 10;

NOT可以加在任何条件语句之前

LIKE:模糊查询

实际上就是对于通配符和正则的一些使用

例如,查找名字里面有“王”的玩家

SELECT * FROM player WHERE name LIKE '王%'; # 姓王的玩家
SELECT * FROM player WHERE name LIKE '%王%'; # 包含王字的玩家
SELECT * FROM player WHERE name LIKE '王_'; # 姓王且名字只有两个字的玩家

在通配符中,‘%’表示匹配之后任意个字符,‘_’表示匹配一个字符【正则中没有这两个】

借助正则表达式进行查询

常用通配符:

'.' -- 任意一个字符;

'^' -- 开头; '$' -- 结尾; 'A|B' -- A或B;

'[abc]' -- 其中任意一个字符; '[a-z]' -- 范围内的任意一个字符(0-9也行);

还是查找王姓的两个字的玩家,用正则表达式可以这么写:

SELECT * FROM player WHERE name REGEXP '^王.$'; # ^表示开始位置,$表示结束位置,.是匹配任意字符
SELECT * FROM player WHERE name REGEXP '王'; # 查询包含王字的玩家

如果要查询包含王或者张的玩家,可以使用
'[]'
,其会匹配中括号中的任意字符

SELECT * FROM player WHERE name REGEXP '[王张]'; # 查询包含王或张字的玩家
SELECT * FROM player WHERE name REGEXP '王|张';
练习

1、查找邮件地址以zhangsan开头的玩家

SELECT * FROM player WHERE email REGEXP 'zhangsan';

2、查找邮件地址以a\b\c开头的玩家

SELECT * FROM player WHERE email REGEXP '^[abc]';
SELECT * FROM player WHERE email REGEXP '^[a-c]';

3、查找邮件地址以net结尾的玩家

SELECT * FROM player WHERE email REGEXP 'net$';
SELECT * FROM player WHERE email LIKE '%net';

NULL:空值

在查找空值数据的时候有一个需要注意的地方,即
不能使用
'='
作为判断条件

需要使用
"IS NULL"
来做,例如,查找没有填邮箱的玩家

SELECT * FROM player WHERE email IS NULL; # 邮箱为null值
SELECT * FROM player WHERE email IS NULL OR email = ''; # 邮箱为null或空值(空字符串)

ORDER BY:排序

为查询数据进行排序,
不指定顺序默认升序

例如,等级从小到大排序

SELECT * FROM player ORDER BY level; # 升序
SELECT * FROM player ORDER BY 5 DESC; # 或者使用列的序号也可以
SELECT * FROM player ORDER BY level DESC; # 降序

对多个列排序

追加列名就行

SELECT * FROM player ORDER BY level DESC, exp; #等级降序, 经验升序

聚合函数

常用聚合函数:

AVG() -- 返回集合的平均值; COUNT() -- 返回集合中的项目数量;

MAX() -- 返回最大值; MIN() -- 返回最小值;SUM() -- 求和;

例如,求玩家总数

SELECT COUNT(*) FROM player;
SELECT AVG(level) FROM player;

分组查询

GROUP BY
由于将查询后的结果进行分组,在关键字后面接上列名即可将指定列框定为一个“组”(group)

SELECT * FROM player GROUP BY level; # 按等级分组

与复合函数连用可以对分组后的数据进行计算处理

SELECT level, COUNT(level) FROM player GROUP BY level; # 统计每个等级的玩家数量
与HAVING配合

GROUP BY
常与
HAVING
连用,可对分组后的数据进行过滤筛选

例如,想知道等级大于4的玩家

SELECT level, COUNT(level) FROM player GROUP BY level HAVING COUNT(level) > 4; # 
与ORDER BY配合

GROUP BY

ORDER BY
连用通常原来为结果
排序

SELECT level, COUNT(level) FROM player GROUP BY level HAVING COUNT(level) > 4 ORDER BY COUNT(level) DESC; # 此处DESC表示降序
练习

统计每个姓氏玩家的数量,并将结果安装数量来降序排序,只显示数量大于等于5的姓氏

下意识会这么写:

SELECT name COUNT(name) FROM player GROUP BY name HAVING COUNT(name) > 5 ORDER BY COUNT(name) DESC;

但是不行,因为name是字符串,不能直接进行计数

并且我们需要统计的是姓氏,而不是整个名字出现的次数,显然需要对字符串进行
截取

这里需要使用一个函数:
SUBSTR
(跟c++一样™的)用于截取字符串,思路如下:

SELECT SUBSTR(name, 1, 1), COUNT(SUBSTR(name, 1, 1)) FROM player # 首先截取出姓氏并统计出现次数
GROUP BY SUBSTR(name, 1, 1) # 对截取出来的姓氏列进行分组
HAVING COUNT(SUBSTR(name, 1, 1)) >= 6 # 筛选出出现次数大于等于5次的姓氏
ORDER BY COUNT(SUBSTR(name, 1, 1)) DESC # 降序排序
LIMIT 3 # 如果只想返回三条结果
LIMIT 3,3 # 第一个3是偏移量,表示从第四名开始,第二个是返回数量,也就是第四名后三个
# ↑即分页查询的原理

DISTINCT:查询结构去重

SELECT DISTINCT sex FROM player;

UNION:合并查询结果(并集)

查询所有等级为1-4
以及
经验为1-3的玩家

SELECT * FROM player WHERE level BETWEEN 1 AND 3
UNION # UNION会默认去重,UNION ALL不会去重
SELECT * FROM player WHERE exp BETWEEN 1 AND 3;

tips:

  • 这种合并方式是全外连接,也就是将表连接起来
  • UNION与OR有点类似,但OR合并的是两个条件,而UNION合并的是两个查询结果
  • 注意,连接的两个语句不要写分号不然会有问题(非要写可以在最后写一个)

INTERSECT:合并结果集(交集)

INTERSECT
用于查找两个结果的交集

SELECT * FROM player WHERE level BETWEEN 1 AND 3
INTERSECT 
SELECT * FROM player WHERE exp BETWEEN 1 AND 3

EXCEPT:合并结果集(差集)

查找等级为1-3的但是经验不在1-3之间的玩家

SELECT * FROM player WHERE level BETWEEN 1 AND 3
EXCEPT
SELECT * FROM player WHERE exp BETWEEN 1 AND 3

子查询

如果想使用一个查询的结果作为另一个查询的条件,可以通过子查询实现

例如,先使用AVG求出所有玩家的平均等级,然后再查出大于平均等级的玩家

SELECT AVG(level) FROM player;
SELECT * FROM player WHERE level > (SELECT AVG(level) FROM player);

又例如,想查询所有玩家等级与平均等级的差值

SELECT level, ROUND((SELECT AVG(level) FROM player)) FROM player; # ROUND可以将浮点数取整
SELECT level, ROUND((SELECT AVG(level) FROM player)), level - ROUND((SELECT AVG(level) FROM player)) FROM player; # 作差即可

但是这样写又臭又长,可以用
AS
给列起个别名看起来方便一些

SELECT level, ROUND((SELECT AVG(level) FROM player)) AS average,
level - ROUND((SELECT AVG(level) FROM player)) AS diff
FROM player;

还可以用子查询来创建新的表

又又例如,我们想讲等级小于5的玩家先查询出来然后单独拎到一个表中保存

SELECT * FROM player WHERE level < 5 # 先查询
CREATE TABLE new_player SELECT * FROM player WHERE level < 5 # 使用子查询建表
SELECT * FROM new_player # 查询新表

插入等级在6-10之间的玩家到新表中

SELECT * FROM player WHERE level BETWEEN 6 AND 10; # 还是先查询
#使用INSERT INTO按子查询结果插入数据到新表
INSERT INTO new_player SELECT * FROM player WHERE level BETWEEN 6 AND 10; 

又又又例如,你想查询是否存在等级大于100的玩家,可以使用
EXISTS
配合子查询来实现

SELECT EXISTS(SELECT * FROM player WHERE level > 100)

表关联

表关联用于查询多个表中的数据,关联的表中需要含有相同字段

一般通过表的主键和外键来关联(概念理解
详见

以game数据库为例,里面除了玩家以外,还有装备数据。有时候我们希望将玩家与装备信息关联起来进行查询

DESC equip # 先查看一下equip表的结构

SELECT * FROM player # 查询player表中的所有项
INNER JOIN equip # 将player表和equip表关接起来
ON player.id = equip.player_id # 指定关联的字段(即两个表通过什么条件进行关联)

具体左右连接的区别,见
详情

表连接的本质就是笛卡尔积+过滤条件,所以如果没有正确使用条件的话会导致数据异常(会有一些奇怪的组合)

写作背景

写这篇文章主要是因为工业相机(海康、大恒等)提供的.NET开发文档和示例程序都是用WinForm项目来说明举例的,而在WPF项目中对图像的使用和处理与在WinForm项目中有很大不同。在WinForm中用System.Drawing.Bitmap来处理图像,而在WPF中是用System.Windows.Media.Imaging.WriteableBitmap来处理图像的。本文的主要内容也是对WriteableBitmap类使用的介绍。

从相机中接收图像

首先当然要创建一个WriteableBitmap,这里以PixelFormats.Bgr24像素格式举例说明

PropertyInfo dpiXProperty = typeof(SystemParameters).GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static);
PropertyInfo dpiYProperty = typeof(SystemParameters).GetProperty("Dpi", BindingFlags.NonPublic | BindingFlags.Static);
int dpiX = (int)dpiXProperty.GetValue(null);
int dpiY = (int)dpiYProperty.GetValue(null);
WriteableBitmap WBitmap = new WriteableBitmap(PhotoWidth, PhotoHeight, dpiX, dpiY, PixelFormats.Bgr24, BitmapPalettes.Halftone256);

接收相机中的照片数据得使用相机SDK提供的方法,一般都是向方法提供一个IntPtr变量,然后相机SDK会将图像数据复制一份到这个内存地址中。

WriteableBitmap对象表示像素数据的地址是WBitmap.BackBuffer。

而在WinForm中的Bitmap则有两种方式接收图像。

一种是创建指定大小和像素格式的Bitmap后使用LockBits获得BitmapData,BitmapData的scan0表示像素数据地址然后和前面的方式一样。

另一种是在创建Bitmap时使用Bitmap(int width, int height, int stride, PixelFormat format, IntPtr scan0)构造函数,使用代表像素数据的IntPtr传给scan0参数即可。

图像的显示

WriteableBitmap使用两个缓冲区,一个后端缓冲区和一个前端缓冲区,所以一个WriteableBitmap对象存着图像的两份数据。前面我们接收图像是把图像存入后端缓冲区中,而界面上Image控件

显示图像用的是前端缓冲区中的图像。所以现在我们需要把后端缓冲区中的数据更新到前端缓冲区中去,然后传给Image的Source属性即可。

WBitmap.Lock();
WBitmap.AddDirtyRect(new Int32Rect(0, 0, PhotoWidth, PhotoHeight));
WBitmap.Unlock();
MyImage.Source = WBitmap;

Lock锁定后端缓冲区,AddDirtyRect将后端缓冲区数据跟新到前端缓冲区,Unlock解锁后端缓冲区。AddDirtyRect的使用模式是固定的,都是先Lock然后Unlock。

像素操作

System.Drawing.Bitmap对象有GetPixel和SetPixel方法,读取、修改某点的像素值很方便。在WriteableBitmap中则需要用指针区操作。在前面【接收图像】中提到用一个指针地址去接受图像,

所以图像的所有像素数据都保存在这个起始地址的内存中,也就是后端缓冲区中。WBitmap.BackBuffer指向的就是坐标(0,0)点的像素数据。

下面以读取(100,200)坐标点的像素数据举例说明,先介绍要用到的两个属性:WBitmap.BackBufferStride表示一行图像数据的字节数,WBitmap.Format.BitsPerPixel表示一个像素的位数。

首先计算(100,200)处的偏移量应该是WBitmap.BackBufferStride*200 + WBitmap.Format.BitsPerPixel / 8*100,那么BackBuffer加上偏移量就是(100,200)处的地址 ,所以完整的读取像素值的代码如下:

int offset = WBitmap.BackBufferStride * 200 + PixelFormats.Bgr24.BitsPerPixel / 8 * 100;
unsafe {
    byte* pb = (byte*)WBitmap.BackBuffer.ToPointer();
    byte cB = pb[offset];
    byte cG = pb[offset + 1];
    byte cR = pb[offset + 2];
}

或者使用System.Runtime.InteropServices.Marshal.ReadByte,不需要unsafe模式

byte cB = Marshal.ReadByte(WBitmap.BackBuffer, offset);
byte cG = Marshal.ReadByte(WBitmap.BackBuffer, offset+1);
byte cR = Marshal.ReadByte(WBitmap.BackBuffer, offset+2);

像素修改也是同样的方法,把读取变成赋值即可,或者用Marshal.WriteByte写值。

图像的保存

与Bitmap使用Save不同,WriteableBitmap需要使用Encoder编码后才能保存成文件。

using FileStream stream = new FileStream(@"C:\newu8.bmp", FileMode.Create);
BmpBitmapEncoder encoder = new BmpBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(WBitmap));
encoder.Save(stream);

这里使用BmpBitmapEncoder编码器来保存bmp图像,要保存成其他格式则使用对应的编码器即可,如JpegBitmapEncoder等。

注意事项

1:像素格式问题,相机SDK提供转化成你需要的格式的方法,在接收图像时要确保两边像素格式一致。相机SDK中提供的像素格式、Bitmap的System.Drawing.Imaging.PixelFormat和WriteableBitmap的System.Windows.Media.PixelFormats对同一像素格式的命名是不同的。比如本文中的PixelFormats.Bgr24对应的是Bitmap中的PixelFormat.Format24bppRgb。可以通过解析同一张图像来确定两者之间的对应关系。

2:使用工业相机采图的方式一般都是使用回调函数的形式,所以在回调函数的多线程环境中执行显示图像的代码要注意控件的跨线程访问问题。

3:图像保存用的是后端缓冲区中的数据(再次证明前端缓冲区只是用来在界面上展示的),意味着只需要在界面上展示图像的时才调用AddDirtyRect。

4:修改部分像素点值后需要在界面上展示的,调用AddDirtyRect方法时Int32Rect参数应该是包含你修改位置的最小面积矩形区域,出于性能考虑不建议使用整个图像区域。