2024年11月

在PyCharm中打包Python项目并将其运行到服务器上的方法

在PyCharm中打包Python项目并将其运行到服务器上的过程,可以分解为几个关键步骤:创建项目、设置项目依赖、打包项目、配置服务器环境、上传可执行文件到服务器以及运行项目。以下是一个详细的指南,包括完整的代码示例,这些代码可以直接运行。

一、创建并设置Python项目

  1. 打开PyCharm并创建新项目:
    • 打开PyCharm,点击“File”菜单,选择“New Project”。
    • 设置项目名称和路径,确保勾选“Create virtual environment”以使用虚拟环境。
    • 点击“OK”完成项目创建。
  2. 设置项目依赖:
    • 在PyCharm中,点击“File”菜单,选择“Settings”。
    • 在左侧面板选择“Project: [项目名称]”,然后点击“Python Interpreter”选项卡。
    • 在右侧面板中,如果项目使用虚拟环境,切换到虚拟环境,并点击“+”按钮添加所需的第三方库(例如,
      flask
      )。

二、编写项目代码

在项目结构中添加Python文件,例如
main.py
,并编写代码。以下是一个简单的Flask Web应用示例:

# main.py
from flask import Flask
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return 'Hello, World!'
 
if __name__ == '__main__':
    app.run()

三、打包项目

  1. 安装PyInstaller:


    • 打开PyCharm的终端(Terminal)。

    • 输入以下命令安装PyInstaller:

      bash复制代码
      
      pip install pyinstaller
      
  2. 配置PyInstaller:


    • 在PyCharm中,通常不需要额外配置PyInstaller,除非有特定的需求。
  3. 打包项目:


    • 在终端中,导航到项目目录。

    • 输入以下命令打包项目:

      bash复制代码
      
      pyinstaller --onefile main.py
      
    • 这将生成一个
      dist
      目录,其中包含打包后的可执行文件。

四、配置服务器环境

  1. 选择并连接到服务器:


    • 确保服务器已经安装了Python环境。

    • 使用SSH连接到服务器,并检查Python版本:

      ssh username@your_server_ip
      python --version
      
  2. 安装依赖(如果需要):


    • 如果项目使用了第三方库,需要在服务器上安装这些库。例如,如果使用了Flask:

      bash复制代码
      
      pip install flask
      

五、上传可执行文件到服务器

使用
scp
命令将打包后的可执行文件上传到服务器:

bash复制代码

scp dist/main username@your_server_ip:/path/to/destination

六、在服务器上运行项目

  1. 登录到服务器

    bash复制代码
    
    ssh username@your_server_ip
    
  2. 导航到可执行文件所在的目录

    bash复制代码
    
    cd /path/to/destination
    
  3. 运行可执行文件

    bash复制代码
    
    ./main
    

七、注意事项

  • 如果我们的Flask应用需要运行在特定端口,确保服务器的防火墙已经开放了相应端口。

  • 如果想让应用在后台运行,可以使用

    nohup
    

    命令:

    bash复制代码
    
    nohup ./main &
    
  • 如果我们的项目需要与数据库交互,需要在服务器上安装相应的数据库驱动程序并配置连接信息。

八、总结

通过上述步骤,我们能够成功地将PyCharm中的Python项目打包并运行到服务器上。这一过程不仅帮助我们学习了一些基本的命令和工具的使用,还强化了对项目部署流程的理解。打包和部署是软件开发中不可或缺的一部分,掌握这些技能后,我们将能够更专业地进行程序开发和管理。

书接上回,我们继续来分享一些关于特殊时间获取的常用扩展方法。

01
、获取当天的开始时间

当天的开始时间指00:00:00时刻,因此只需要获取DateTime的Date属性只获取时间即可,具体代码如下:

//获取当天的开始时间
public static DateTime GetStartDateTimeOfDay(this DateTime dateTime)
{
    return dateTime.Date;
}

我们进行一个简单的单元测试,具体代码如下:

[Fact]
public void GetStartDateTimeOfDay()
{
    var datetime = new DateTime(2024, 11, 7, 14, 10, 10);
    var start = datetime.GetStartDateTimeOfDay();
    Assert.Equal(new DateTime(2024, 11, 7), start);
}

02
、获取当天的结束时间

该方法时候获取一天中最后一刻,也就是第二天的前一刻,我们可以用第二天的开始时间减去最小时间单位得到当天的结束时间,具体代码如下:

//获取当天的结束时间
public static DateTime GetEndDateTimeOfDay(this DateTime dateTime)
{
    return dateTime.Date.AddDays(1).AddTicks(-1);
}

下面我们通过单元测试验证,时间部分是否为“23:59:59 9999999”,具体代码如下:

[Fact]
public void GetEndDateTimeOfDay()
{
    var date4 = new DateTime(2024, 11, 7, 14, 10, 10);
    var end = date4.GetEndDateTimeOfDay();
    Assert.Equal("2024-11-07 23:59:59 9999999", end.ToString("yyyy-MM-dd HH:mm:ss fffffff"));
}

03
、获取当前日期所在周的第一天(周一)

要想获得当前日期所在周的周一,只需要知道当前是周几,然后计算出和周一相差几天,最后使用AddDays方法即可。

首先我们可以通过DayOfWeek获取到日期是周几枚举值,但是这个枚举值对应的int值是

0 = Sunday 周日, 1 = Monday 周一, ..., 6 = Saturday 周六。其中周日的0就显得很异类,处理起来也就比较麻烦。

因此如果当前日期是周日那么就会出现周日减周一等于0减1等于-1的情况,所有我们需要加7来保证结果为正数。

同样如果当前日期是周六那么就会出现周六减周一等于6减1加7等于12的情况,所以我们需要同取余%7,来保证两者相差在一周天数之内。

具体代码如下:

//获取当前日期所在周的第一天(周一)
public static DateTime GetFirstDayDateTimeOfWeek(this DateTime dateTime)
{
    //0 = Sunday 周日, 1 = Monday 周一, ..., 6 = Saturday 周六
    //首先获取当前日期星期枚举值,然后计算其和周一枚举值差值
    //结果+7,保证结果为正数
    //结果%7,保证结果在0-6之间,对于一周七天,从而表示要回退多少天到周一
    //+7 %7 巧妙的把周日当7处理,最后再转为0
    var diff = ((int)dateTime.DayOfWeek - (int)DayOfWeek.Monday + 7) % 7;
    return dateTime.AddDays(-diff).Date;
}

下面我们需要进行详细的单元测试,我们进行了四种情况的测试分别是:

(1) 验证当前日期是周五,而周一在上一个月的情况;

(2) 验证当前日期就是周一的情况;

(3) 验证当前日期是周四,而周一在当月的情况

(4) 验证当前日期是周日,而周一在当月的情况

具体代码如下:

[Fact]
public void GetFirstDayDateTimeOfWeek()
{
    //验证当前日期是周五,而周一在上一个月的情况
    var friday = new DateTime(2024, 11, 1, 14, 10, 10);
    var day_friday = friday.GetFirstDayDateTimeOfWeek();
    Assert.Equal(new DateTime(2024, 10, 28), day_friday);
    //验证当前日期就是周一的情况
    var monday = new DateTime(2024, 11, 4, 4, 10, 10);
    var day_monday = monday.GetFirstDayDateTimeOfWeek();
    Assert.Equal(new DateTime(2024, 11, 4), day_monday);
    //验证当前日期是周四的情况
    var thursday = new DateTime(2024, 11, 7, 4, 10, 10);
    var day_thursday = thursday.GetFirstDayDateTimeOfWeek();
    Assert.Equal(new DateTime(2024, 11, 4), day_thursday);
    //验证当前日期是周日的情况
    var sunday = new DateTime(2024, 11, 10, 4, 10, 10);
    var day_sunday = sunday.GetFirstDayDateTimeOfWeek();
    Assert.Equal(new DateTime(2024, 11, 4), day_sunday);
}

04
、获取当前日期所在周的最后一天(周日)

该方法和上面获取周一的思想一样,我们可以把周日枚举值就当作7来处理,具体代码如下:

//获取当前日期所在周的最后一天(周日)
public static DateTime GetLastDayDateTimeOfWeek(this DateTime dateTime)
{
    //0 = Sunday 周日, 1 = Monday 周一, ..., 6 = Saturday 周六
    //首先计算还差几天到周日
    //结果%7,保证结果在0-6之间
    //当周日时dateTime.DayOfWeek为0,(7-0)% 7 = 0
    //巧妙的把周日当7处理,最后再转为0
    var diff = (7 - (int)dateTime.DayOfWeek) % 7;
    return dateTime.AddDays(diff).Date;
}

同样的我们做类似获取周一的四种情况单元测试,具体代码如下:

[Fact]
public void GetLastDayDateTimeOfWeek()
{
    //验证当前日期是周六,而周日在下一个月的情况
    var sunday = new DateTime(2024, 11, 30, 14, 10, 10);
    var day_sunday = sunday.GetLastDayDateTimeOfWeek();
    Assert.Equal(new DateTime(2024, 12, 1), day_sunday);
    //验证当前日期就是周一的情况
    var monday = new DateTime(2024, 11, 4, 4, 10, 10);
    var day_monday = monday.GetLastDayDateTimeOfWeek();
    Assert.Equal(new DateTime(2024, 11, 10), day_monday);
    //验证当前日期是周四的情况
    var thursday = new DateTime(2024, 11, 7, 4, 10, 10);
    var day_thursday = thursday.GetLastDayDateTimeOfWeek();
    Assert.Equal(new DateTime(2024, 11, 10), day_thursday);
    //验证当前日期是周日的情况
    var sunday1 = new DateTime(2024, 11, 10, 4, 10, 10);
    var day_thursday1 = sunday1.GetLastDayDateTimeOfWeek();
    Assert.Equal(new DateTime(2024, 11, 10), day_thursday1);
}

05
、获取当前日期所在月的第一天

这个方法比较简单,只需要使用当前日期的年份和月份,然后直接构建当月第一天,具体代码如下:

//获取当前日期所在月的第一天
public static DateTime GetFirstDayDateTimeOfMonth(this DateTime dateTime)
{
    return new DateTime(dateTime.Year, dateTime.Month, 1, 0, 0, 0, 0, DateTimeKind.Local);
}

这个方法太简单了,我们就不列出单元测试代码了。

06
、获取当前日期所在月的最后一天

该方便也不复杂,可以先通过DaysInMonth获取当前月的总天数,然后再构建当月最后一天,具体代码如下:

//获取当前日期所在月的最后一天
public static DateTime GetLastDayDateTimeOfMonth(this DateTime dateTime)
{
    //获取当前月的总天数
    var days = DateTime.DaysInMonth(dateTime.Year, dateTime.Month);
    return new DateTime(dateTime.Year, dateTime.Month, days, 0, 0, 0, 0, DateTimeKind.Local);
}

同样的我们这个方法也不复杂,我们就不列举单元测试了。

07
、获取当前日期所在季度的第一天

如果想要获取当前日期所在季度的第一天那么首先需要获取当前日期所在季度的第一个月是多少。

我们知道三个月为一季度,因此我们可以使用当前月份除以3,如果这样直接除就会得到:1/3=0,2/3=0,3/3=1,这样1月到3月就不在同一个季度里,所以我们使用(moth - 1)/ 3,计算出0、1、2、3表示4个季度,这样就可以计算出当前日期所在第几个季度。

计算出所在季度后我们还需要计算出当前季度的第一个月即1月、4月、7月、10月,然后找出这4个月份与上面表示4个季度值的关系即可,最终得到如下公式:(moth - 1)/ 3 * 3 +1,即为当前日期所在季度的第一个月。

最后就是直接构建日期,具体代码如下:

//获取当前日期所在季度的第一天
public static DateTime GetFirstDayDateTimeOfQuarter(this DateTime dateTime)
{
    //计算当前日期所在季度的起始月
    var firstMonth = (dateTime.Month - 1) / 3 * 3 + 1;
    return new DateTime(dateTime.Year, firstMonth, 1, 0, 0, 0, 0, DateTimeKind.Local);
}

然后我们分别对这个方法做以下三种情况的单元测试:

(1) 一个季度第一个月取第一天的情况;

(2) 一个季度第二个月取中间的一天的情况;

(3) 一个季度第三个月取最后一天的情况;

[Fact]
public void GetFirstDayDateTimeOfQuarter()
{
    //一个季度第一个月取第一天的情况
    var month1 = new DateTime(2024, 10, 1, 14, 10, 10);
    var day_month1 = month1.GetFirstDayDateTimeOfQuarter();
    Assert.Equal(new DateTime(2024, 10, 1), day_month1);
    //一个季度第二个月取中间的一天的情况
    var month2 = new DateTime(2024, 11, 17, 4, 10, 10);
    var day_month2 = month2.GetFirstDayDateTimeOfQuarter();
    Assert.Equal(new DateTime(2024, 10, 1), day_month2);
    //一个季度第三个月取最后一天的情况
    var month3 = new DateTime(2024, 12, 31, 4, 10, 10);
    var day_month3 = month3.GetFirstDayDateTimeOfQuarter();
    Assert.Equal(new DateTime(2024, 10, 1), day_month3);
}

08
、获取当前日期所在季度的最后一天

该方法和上面获取季度的第一天思想一样,只是此方法获取当前日期所在季度的最后月份的计算公式有所差异,公式为:(moth + 2)/ 3 * 3,具体代码如下:

//获取当前日期所在季度的最后一天
public static DateTime GetLastDayDateTimeOfQuarter(this DateTime dateTime)
{
    //计算当前日期所在季度的最后月
    var lastMonth = (dateTime.Month + 2) / 3 * 3;
    //获取当前月的总天数
    var days = DateTime.DaysInMonth(dateTime.Year, lastMonth);
    return new DateTime(dateTime.Year, lastMonth, days, 0, 0, 0, 0, DateTimeKind.Local);
}

同样的我们对其进行三种情况单元测试,具体代码如下:

[Fact]
public void GetLastDayDateTimeOfQuarter()
{
    //一个季度第一个月取第一天的情况
    var month1 = new DateTime(2024, 10, 1, 14, 10, 10);
    var day_month1 = month1.GetLastDayDateTimeOfQuarter();
    Assert.Equal(new DateTime(2024, 12, 31), day_month1);
    //一个季度第二个月取中间的一天的情况
    var month2 = new DateTime(2024, 11, 17, 4, 10, 10);
    var day_month2 = month2.GetLastDayDateTimeOfQuarter();
    Assert.Equal(new DateTime(2024, 12, 31), day_month2);
    //一个季度第三个月取最后一天的情况
    var month3 = new DateTime(2024, 12, 31, 4, 10, 10);
    var day_month3 = month3.GetLastDayDateTimeOfQuarter();
    Assert.Equal(new DateTime(2024, 12, 31), day_month3);
}

09
、获取当前日期所在年的第一天

该方法比较简单,直接用当前日期所在年份和1月1号直接构建即可,代码如下:

//获取当前日期所在年的第一天
public static DateTime GetFirstDayDateTimeOfYear(this DateTime dateTime)
{
    return new DateTime(dateTime.Year, 1, 1, 0, 0, 0, 0, DateTimeKind.Local);
}

10
、获取当前日期所在年的最后一天

该方法也比较简单,直接用当前日期所在年份和12月31号直接构建即可,代码如下:

//获取当前日期所在年的最后一天
public static DateTime GetLastDayDateTimeOfYear(this DateTime dateTime)
{
    return new DateTime(dateTime.Year, 12, 31, 0, 0, 0, 0, DateTimeKind.Local);
}

稍晚些时候我会把库上传至Nuget,大家可以直接使用Ideal.Core.Common。


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Ideal

【1】引言(完整代码在最后面)

本项目旨在实现一个简单的“抛硬币”功能,用户可以通过点击屏幕上的地鼠图标来模拟抛硬币的过程。应用会记录并显示硬币正面(地鼠面)和反面(数字100面)出现的次数。为了增强用户体验,我们还添加了动画效果,使抛硬币的过程更加生动有趣。

【2】环境准备

电脑系统:windows 10

开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806

工程版本:API 12

真机:mate60 pro

语言:ArkTS、ArkUI

【3】应用结构

应用主要由两个部分组成:地鼠组件(Hamster)和主页面组件(CoinTossPage)。

地鼠组件(Hamster)

地鼠组件是应用的核心视觉元素之一,负责展示地鼠的形象。该组件通过@Component装饰器定义,并接收一个属性cellWidth,用于控制组件的大小。

主页面组件(CoinTossPage)

主页面组件是整个应用的入口点,负责组织和管理各个UI元素。该组件同样通过@Component装饰器定义,并包含多个状态变量用于跟踪硬币的状态和动画进度。

【4】功能解析

1. 地鼠组件:

• 通过Stack布局组合多个图形元素,创建了一个地鼠的形象。

• 每个图形元素都设置了具体的尺寸、颜色、边框等样式,并通过margin属性调整位置。

2. 主页面组件:

• 顶部有一个“抛硬币”的标题,下方是一个行布局,用于展示地鼠组件及正反两面出现的次数。

• 地鼠组件被放置在一个圆形区域内,背景采用线性渐变色。

• 点击地鼠时,会触发一系列动画效果,模拟硬币抛起再落下的过程。

• 通过计算最终的角度,判断是正面还是反面朝上,并更新相应的计数。

【完整代码】

// 定义地鼠组件
@Component
struct Hamster {
  @Prop cellWidth: number // 单元格宽度

  build() {
    Stack() { // 创建一个堆叠布局
      // 身体
      Text()
        .width(`${this.cellWidth / 2}lpx`)// 宽度为单元格宽度的一半
        .height(`${this.cellWidth / 3 * 2}lpx`)// 高度为单元格高度的2/3
        .backgroundColor("#b49579")// 背景颜色
        .borderRadius({ topLeft: '50%', topRight: '50%' })// 圆角
        .borderColor("#2a272d")// 边框颜色
        .borderWidth(1) // 边框宽度
      // 嘴巴
      Ellipse()
        .width(`${this.cellWidth / 4}lpx`)// 嘴巴的宽度
        .height(`${this.cellWidth / 5}lpx`)// 嘴巴的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#e7bad7")// 填充颜色
        .stroke("#563e3f")// 边框颜色
        .strokeWidth(1)// 边框宽度
        .margin({ top: `${this.cellWidth / 6}lpx` }) // 上边距
      // 左眼睛
      Ellipse()
        .width(`${this.cellWidth / 9}lpx`)// 左眼睛的宽度
        .height(`${this.cellWidth / 6}lpx`)// 左眼睛的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#313028")// 填充颜色
        .stroke("#2e2018")// 边框颜色
        .strokeWidth(1)// 边框宽度
        .margin({ bottom: `${this.cellWidth / 3}lpx`, right: `${this.cellWidth / 6}lpx` }) // 下边距和右边距
      // 右眼睛
      Ellipse()
        .width(`${this.cellWidth / 9}lpx`)// 右眼睛的宽度
        .height(`${this.cellWidth / 6}lpx`)// 右眼睛的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#313028")// 填充颜色
        .stroke("#2e2018")// 边框颜色
        .strokeWidth(1)// 边框宽度
        .margin({ bottom: `${this.cellWidth / 3}lpx`, left: `${this.cellWidth / 6}lpx` }) // 下边距和左边距
      // 左眼瞳
      Ellipse()
        .width(`${this.cellWidth / 20}lpx`)// 左眼瞳的宽度
        .height(`${this.cellWidth / 15}lpx`)// 左眼瞳的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#fefbfa")// 填充颜色
        .margin({ bottom: `${this.cellWidth / 2.5}lpx`, right: `${this.cellWidth / 6}lpx` }) // 下边距和右边距
      // 右眼瞳
      Ellipse()
        .width(`${this.cellWidth / 20}lpx`)// 右眼瞳的宽度
        .height(`${this.cellWidth / 15}lpx`)// 右眼瞳的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#fefbfa")// 填充颜色
        .margin({ bottom: `${this.cellWidth / 2.5}lpx`, left: `${this.cellWidth / 6}lpx` }) // 下边距和左边距
    }.width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`) // 设置组件的宽度和高度
  }
}

// 定义页面组件
@Entry
@Component
struct CoinTossPage {
  @State cellWidth: number = 50 // 单元格宽度
  @State headsCount: number = 0 // 正面朝上的次数
  @State tailsCount: number = 0 // 反面朝上的次数
  @State rotationAngle: number = 0 // 旋转角度
  @State verticalOffset: number = 0 // 纵向位移
  @State isAnimRun: boolean = false // 动画是否正在执行

  build() {
    Column() {
      // 页面标题
      Text('抛硬币')
        .height(50)// 高度设置为50
        .width('100%')// 宽度设置为100%
        .textAlign(TextAlign.Center)// 文本居中对齐
        .fontColor("#fefefe")// 字体颜色
        .fontSize(20); // 字体大小

      // 显示地鼠和计数
      Row({ space: 20 }) {
        Stack() {
          Hamster({ cellWidth: this.cellWidth }) // 创建地鼠组件
        }
        .borderRadius('50%') // 设置圆角
        .width(`${this.cellWidth}lpx`) // 设置宽度
        .height(`${this.cellWidth}lpx`) // 设置高度
        .linearGradient({
          // 设置线性渐变背景
          direction: GradientDirection.LeftBottom,
          colors: [['#ebcf2f', 0.0], ['#fef888', 0.5], ['#ebcf2f', 1.0]]
        });

        // 显示反面朝上的次数
        Text(`${this.tailsCount}`)
          .fontSize(20)
          .fontColor("#fefefe");

        Stack() {
          // 显示100
          Text("100")
            .fontColor("#9f7606")
            .fontSize(`${this.cellWidth / 2}lpx`);
        }
        .borderRadius('50%') // 设置圆角
        .width(`${this.cellWidth}lpx`) // 设置宽度
        .height(`${this.cellWidth}lpx`) // 设置高度
        .linearGradient({
          // 设置线性渐变背景
          direction: GradientDirection.LeftBottom,
          colors: [['#ebcf2f', 0.0], ['#fef888', 0.5], ['#ebcf2f', 1.0]]
        });

        // 显示正面朝上的次数
        Text(`${this.headsCount}`)
          .fontSize(20)
          .fontColor("#fefefe");

      }.width('100%').justifyContent(FlexAlign.Center); // 设置宽度和内容居中对齐

      Stack() {
        Stack() {
          // 创建放大版地鼠组件
          Hamster({ cellWidth: this.cellWidth * 3 })
            .visibility(this.isHeadsFaceUp() ? Visibility.Visible : Visibility.Hidden); // 根据状态显示或隐藏

          // 显示100
          Text("100")
            .fontColor("#9f7606")// 字体颜色
            .fontSize(`${this.cellWidth / 2 * 3}lpx`)// 字体大小
            .visibility(!this.isHeadsFaceUp() ? Visibility.Visible : Visibility.Hidden)// 根据状态显示或隐藏
            .rotate({
              // 旋转180度
              x: 1,
              y: 0,
              z: 0,
              angle: 180
            });
        }
        .borderRadius('50%') // 设置圆角
        .width(`${this.cellWidth * 3}lpx`) // 设置宽度
        .height(`${this.cellWidth * 3}lpx`) // 设置高度
        .linearGradient({
          // 设置线性渐变背景
          direction: GradientDirection.LeftBottom,
          colors: [['#ebcf2f', 0.0], ['#fef888', 0.5], ['#ebcf2f', 1.0]]
        })
        .rotate({
          // 根据当前角度旋转
          x: 1,
          y: 0,
          z: 0,
          angle: this.rotationAngle
        })
        .translate({ x: 0, y: this.verticalOffset }) // 设置纵向位移
        .onClick(() => { // 点击事件处理

          if (this.isAnimRun) {
            return;
          }
          this.isAnimRun = true

          let maxAnimationSteps = 2 * (10 + Math.floor(Math.random() * 10)); // 计算最大动画次数
          let totalAnimationDuration = 2000; // 动画总时长

          // 第一次动画,向上抛出
          animateToImmediately({
            duration: totalAnimationDuration / 2, // 动画时长为总时长的一半
            onFinish: () => { // 动画完成后的回调
              // 第二次动画,向下落
              animateToImmediately({
                duration: totalAnimationDuration / 2,
                onFinish: () => {
                  this.rotationAngle = this.rotationAngle % 360; // 确保角度在0到360之间
                  // 判断当前显示的面
                  if (this.isHeadsFaceUp()) { // 如果是地鼠面
                    this.tailsCount++; // 反面朝上的次数加1
                  } else { // 如果是反面
                    this.headsCount++; // 正面朝上的次数加1
                  }
                  this.isAnimRun = false
                }
              }, () => {
                this.verticalOffset = 0; // 重置纵向位移
              });
            }
          }, () => {
            // 设置纵向位移,模拟抛硬币的效果
            this.verticalOffset = -100 * (1 + Math.floor(Math.random() * 5)); // 随机设置向上的位移
          });

          // 循环动画,增加旋转效果
          for (let i = 0; i < maxAnimationSteps; i++) {
            animateToImmediately({
              delay: i * totalAnimationDuration / maxAnimationSteps, // 设置每次动画的延迟
              duration: 100, // 每次动画的持续时间
              onFinish: () => {
                // 动画完成后的回调
              }
            }, () => {
              this.rotationAngle += 90; // 每次增加90度旋转
            });
          }
        });

      }.width('100%').layoutWeight(1).align(Alignment.Bottom).padding({ bottom: 80 }); // 设置组件的宽度、权重、对齐方式和底部内边距
    }
    .height('100%') // 设置整个页面的高度
    .width('100%') // 设置整个页面的宽度
    .backgroundColor("#0b0d0c"); // 设置背景颜色
  }

  // 判断当前是否显示地鼠面
  isHeadsFaceUp() {
    let normalizedAngle = this.rotationAngle % 360; // 规范化角度
    // 判断角度范围,确定是否显示地鼠面
    if (normalizedAngle >= 0 && normalizedAngle < 90 || normalizedAngle >= 270 && normalizedAngle <= 360) {
      return true; // 显示地鼠面
    }
    return false; // 显示反面
  }
}

PostgreSQL中将对象oid转为对象名

使用pg的内部数据类型将对象oid转为对象名,可以简化一些系统视图的关联查询。

数据库类型转换对应类型的oid

可以用以下数据库类型转换对应类型的oid(以pg12为例)

postgres=# select typname from pg_type where typname ~ '^reg';
    typname
---------------
 regclass
 regconfig
 regdictionary
 regnamespace
 regoper
 regoperator
 regproc
 regprocedure
 regrole
 regtype
(10 rows)

对应关系

对象名称 类型 转换规则
pg_class regclass pg_class.oid::regclass
pg_ts_dict regdictionary pg_ts_dict.oid::regdictionary
pg_namespace regnamespace pg_namespace.oid::regnamespace
pg_operator regoperator pg_operator.oid::regoperator
pg_proc regproc pg_proc.oid::regproc
pg_roles
pg_user
regrole pg_roles.oid::regrole
pg_user.usesysid::regrole
pg_type regtype pg_type.oid::regtype
以下几个类型暂不确定用途,待研究:
regprocedure
regoper
regconfig

创建测试数据

psql -U postgres
create user test password 'test';
create database testdb with owner=test;
\c testdb
CREATE SCHEMA AUTHORIZATION test;
psql -U test -d testdb
create table test_t1(id int);
create table test_t2(id int);
create table test_t3(id int);

基于如上测试数据,查询test模式下有哪些表,以及表的owner

传统表关联的方式使用以下SQL,关联pg_class、pg_namespace、pg_roles/pg_user

psql -U test -d testdb
-- 查询用户关联pg_user查询
SELECT
  t3.nspname AS SCHEMA,
  t1.relname AS tablename,
  t2.usename AS OWNER 
FROM
  pg_class t1
  JOIN pg_user t2 ON t1.relowner = t2.usesysid
  JOIN pg_namespace t3 ON t1.relnamespace = t3.OID 
WHERE
  t1.relkind = 'r' 
  AND t3.nspname = 'test';

 schema | tablename | owner
--------+-----------+-------
 test   | test_t1   | test
 test   | test_t2   | test
 test   | test_t3   | test
(3 rows)

-- 查询用户关联pg_roles查询
SELECT
  t3.nspname AS SCHEMA,
  t1.relname AS tablename,
  t2.rolname AS OWNER 
FROM
  pg_class t1
  JOIN pg_roles t2 ON t1.relowner = t2.oid
  JOIN pg_namespace t3 ON t1.relnamespace = t3.OID 
WHERE
  t1.relkind = 'r' 
  AND t3.nspname = 'test';

 schema | tablename | owner
--------+-----------+-------
 test   | test_t1   | test
 test   | test_t2   | test
 test   | test_t3   | test
(3 rows)

如上为了实现查询效果需要关联三张表,查询比较繁琐,如果使用对象转换就很简单了,如下:

psql -U test -d testdb
SELECT
  relnamespace :: REGNAMESPACE AS SCHEMA,
  relname AS tablename,
  relowner :: REGROLE AS OWNER 
FROM
  pg_class 
WHERE
  relnamespace :: REGNAMESPACE :: TEXT = 'test' 
  AND relkind = 'r';

 schema | tablename | owner
--------+-----------+-------
 test   | test_t1   | test
 test   | test_t2   | test
 test   | test_t3   | test
(3 rows)

将对象名转为oid类型

转换关系

对象类型 转换规则
table '表名'::regclass::oid
function/procedure '函数名/存储过程名'::regproc::oid
schema '模式名'::regnamespace::oid
user/role '用户名/角色名'::regrole::oid
type '类型名称'::regtype::oid

测试示例

表转换

drop table if exists test_t;
create table test_t(id int);

postgres=# select oid from pg_class where relname = 'test_t';
  oid
-------
 16508
(1 row)

postgres=# select 'test_t'::regclass::oid;
  oid
-------
 16508
(1 row)

函数转换

CREATE OR REPLACE FUNCTION test_fun(
    arg1 INTEGER,
    arg2 INTEGER,
    arg3 TEXT
)
RETURNS INTEGER
AS $$
BEGIN
    RETURN arg1 + arg2;
END;
$$ LANGUAGE plpgsql;


postgres=# select oid,proname from pg_proc where proname = 'test_fun';
  oid  | proname
-------+----------
 16399 | test_fun
(1 row)

postgres=# select 'test_fun'::regproc::oid;
  oid
-------
 16399
(1 row)

模式转换

create schema test_schema;

postgres=# select oid,nspname from pg_namespace where nspname='test_schema';
  oid  |   nspname
-------+-------------
 16511 | test_schema
(1 row)

postgres=# select 'test_schema'::regnamespace::oid;
  oid
-------
 16511
(1 row)

用户/角色

create user test_user;

postgres=# select usesysid,usename from pg_user where usename='test_user';
 usesysid |  usename
----------+-----------
    16512 | test_user
(1 row)

postgres=# select 'test_user'::regrole::oid;
  oid
-------
 16512
(1 row)

类型

CREATE TYPE type_sex AS ENUM ('male', 'female');

postgres=# select oid,typname from pg_type where typname='type_sex';
  oid  | typname
-------+----------
 16514 | type_sex
(1 row)

postgres=# select 'type_sex'::regtype::oid;
  oid
-------
 16514
(1 row)

整数类型的 UNSIGNED 属性有什么用?

MySQL 中的整数类型可以使用可选的 UNSIGNED 属性来表示不允许负值的无符号整数。使用 UNSIGNED 属性可以将正整数的上限提高一倍,因为它不需要存储负数值。

例如, TINYINT UNSIGNED 类型的取值范围是 0 ~ 255,而普通的 TINYINT 类型的值范围是 -128 ~ 127。INT UNSIGNED 类型的取值范围是 0 ~ 4,294,967,295,而普通的 INT 类型的值范围是 -2,147,483,648 ~ 2,147,483,647。

对于从 0 开始递增的 ID 列,使用 UNSIGNED 属性可以非常适合,因为不允许负值并且可以拥有更大的上限范围,提供了更多的 ID 值可用。

char和varchar的区别

CHAR

  • CHAR类型用于存储固定长度字符串:MySQL总是
    根据定义的字符串长度分配足够的空间
    。当存储CHAR值时,MySQL会删除字符串中的末尾空格同时,CHAR值会根据需要采用空格进行剩余空间填充,以方便比较和检索。但正因为其长度固定,所以会占据多余的空间,也是一种空间换时间的策略;
  • CHAR适合存储很短或长度近似的字符串。例如,
    CHAR非常适合存储密码的MD5值、定长的身份证等,因为这些是定长的值
  • 对于经常变更的数据,CHAR也比VARCHAR更好,因为定长的CHAR类型占用磁盘的存储空间是连续分配的,不容易产生碎片。
  • 对于非常短的列,CHAR比VARCHAR在存储空间上也更有效率。例如用CHAR(1)来存储只有Y和N的值,如果采用单字节字符集只需要一个字节,但是VARCHAR(1)却需要两个字节,因为还有一个记录长度的额外字节。

VARCHAR:

  • VARCHAR类型用于存储可变长度字符串,是最常见的字符串数据类型。它
    比固定长度类型更节省空间
    ,因为它仅使用必要的空间(根据实际字符串的长度改变存储空间)。

  • VARCHAR需要使用1或2个额外字节记录字符串的长度:如果列的最大长度小于或等于255字节,则只使用1个字节表示,否则使用2个字节。假设采用latinl字符集,一个VARCHAR(10)的列需要11个字节的存储空间。VARCHAR(1000)的列则需要1002 个字节,因为需要2个字节存储长度信息。

  • VARCHAR节省了存储空间,所以对性能也有帮助。但是,由于行是变长的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作。如果一个行占用的空间增长,并且在页内没有更多的空间可以存储,在这种情况下,不同的存储引擎的处理方式是不一样的。例如,MylSAM会将行拆成不同的片段存储,InnoDB则需要分裂页来使行可以放进页内。

  • 操作内存的方式:对于varchar数据类型来说,硬盘上的存储空间虽然都是根据字符串的实际长度来存储空间的,但在内存中是根据varchar类型定义的长度来分配占用的内存空间的,而不是根据字符串的实际长度来分配的。显然,这对于排序和临时表会较大的性能影响。

VARCHAR(100)和 VARCHAR(10)的区别是什么?

VARCHAR(100)和 VARCHAR(10)都是变长类型,表示能存储最多 100 个字符和 10 个字符。因此,VARCHAR (100) 可以满足更大范围的字符存储需求,有更好的业务拓展性。而 VARCHAR(10)存储超过 10 个字符时,就需要修改表结构才可以。

虽说 VARCHAR(100)和 VARCHAR(10)能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的,这也是很多人容易误解的一点。

不过,VARCHAR(100) 会消耗更多的内存。这是因为 VARCHAR 类型在内存中操作时,通常会分配固定大小的内存块来保存值,即使用字符类型中定义的长度。例如在进行排序的时候,VARCHAR(100)是按照 100 这个长度来进行的,也就会消耗更多内存。

DECIMAL 和 FLOAT/DOUBLE 的区别是什么?

DECIMAL 和 FLOAT 的区别是:
DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。

DECIMAL 用于存储具有精度要求的小数,例如与货币相关的数据,可以避免浮点数带来的精度损失。

在 Java 中,MySQL 的 DECIMAL 类型对应的是 Java 类
java.math.BigDecimal

int(10)和char(10)的区别?

int(10)中的10表示的是显示数据的长度,而char(10)表示的是存储数据的长度。

为什么不推荐使用 TEXT 和 BLOB?

数据库规范通常不推荐使用 BLOB 和 TEXT 类型,这两种类型具有一些缺点和限制,例如:

  • 不能有默认值。
  • 在使用临时表时无法使用内存临时表,只能在磁盘上创建临时表(《高性能 MySQL》书中有提到)。
  • 检索效率较低。
  • 不能直接创建索引,需要指定前缀长度。
  • 可能会消耗大量的网络和 IO 带宽。
  • 可能导致表上的 DML 操作变慢。
  • ……

DATETIME 和 TIMESTAMP 的区别是什么?

DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。

TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。

  • DATETIME:1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
  • Timestamp:1970-01-01 00:00:01 ~ 2037-12-31 23:59:59

Boolean 类型如何表示?

MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。

为什么不建议使用null作为默认值

Mysql不建议用Null作为列默认值不是因为不能使用索引,而是因为:

  • 索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化。比如进行索引统计时,
    count(1),max(),min() 会省略值为NULL 的行
  • NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式 (opens new window)中
    至少会用 1 字节空间存储 NULL 值列表
    。建议用""或默认值0来代替NULL

不建议使用null作为默认值,并且
建议必须设置默认值
,原因如下:

  • 既然都不可为空了,那就必须要有默认值,否则不插入这列的话,就会报错;
  • 数据库不应该是用来查问题的,不能靠mysql报错来告知业务有问题,该不该插入应该由业务说了算;
  • 对于DBA来说,允许使用null是没有规范的,因为不同的人不同的用法。

但像
合同生效时间

获奖时间
等这种不可控字段,是可以不设置默认值的,但同样需要not null

为什么禁止使用外键

  • 外键会降低数据库的性能。在MySQL中,外键会自动加上索引,这会使得对该表的查询等操作变得缓慢,尤其是在大型数据表中。
  • 外键也会限制了表结构的调整和更改。在实际应用中,表结构经常需要进行更改,而如果表之间使用了外键约束,这些更改可能会非常难以实现。因为更改一个表的结构,需要涉及到所有以其为父表的子表,这会导致长时间锁定整个数据库表,甚至可能会导致数据丢失。
  • 在MySQL中,外键约束可能还会引发死锁问题。当想要对多个表中的数据进行插入、更新、删除操作时,由于外键约束的存在,可能会导致死锁,需要等待其他事务释放锁。
  • MySQL中使用外键还会增加开发难度。开发人员需要处理数据在表之间的关系,而这样的处理需要花费更多的时间和精力,以及对数据库的深入理解。同时,外键也会增加代码的复杂度,使得SQL语句变得难以理解和调试。

在阿里巴巴开发手册中也有提到,
传送门

使用自增主键有什么好处?

自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑,在查询的时候,效率也就更高。

自增主键保存在什么地方?

不同的引擎对于自增值的保存策略不同:

  • MyISAM引擎的自增值保存在数据文件中。
  • 在MySQL8.0以前,InnoDB引擎的自增值是存在内存中。MySQL重启之后内存中的这个值就丢失了,每次重启后第一次打开表的时候,会找自增值的最大值max(id),然后将最大值加1作为这个表的自增值;MySQL8.0版本会将自增值的变更记录在redo log中,重启时依靠redo log恢复。

自增主键一定是连续的吗?

不一定,有几种情况会导致自增主键不连续。

1、唯一键冲突导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果违反表中定义的唯一索引的唯一约束,会导致插入数据失败。此时表的自增主键的键值是会向后加1滚动的。下次再次插入数据的时候,就不能再使用上次因插入数据失败而滚动生成的键值了,必须使用新滚动生成的键值。

2、事务回滚导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果显式开启了事务,然后因为某种原因最后回滚了事务,此时表的自增值也会发生滚动,而接下里新插入的数据,也将不能使用滚动过的自增值,而是需要重新申请一个新的自增值。

3、批量插入导致自增值不连续。MySQL有一个批量申请自增id的策略:

  • 语句执行过程中,第一次申请自增id,分配1个自增id
  • 1个用完以后,第二次申请,会分配2个自增id
  • 2个用完以后,第三次申请,会分配4个自增id
  • 依次类推,每次申请都是上一次的两倍(最后一次申请不一定全部使用)

如果下一个事务再次插入数据的时候,则会基于上一个事务申请后的自增值基础上再申请。此时就出现自增值不连续的情况出现。

4、自增步长不是1,也会导致自增主键不连续。

InnoDB的自增值为什么不能回收利用?

主要为了提升插入数据的效率和并行度。

假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请。

假设事务 A 申请到了 id=2, 事务 B 申请到 id=3,那么这时候表 t 的自增值是 4,之后继续执行。

事务 B 正确提交了,但事务 A 出现了唯一键冲突。

如果允许事务 A 把自增 id 回退,也就是把表 t 的当前自增值改回 2,那么就会出现这样的情况:表里面已经有 id=3 的行,而当前的自增 id 值是 2。

接下来,继续执行的其他事务就会申请到 id=2,然后再申请到 id=3。这时,就会出现插入语句报错“主键冲突”。

而为了解决这个主键冲突,有两种方法:

  • 每次申请 id 之前,先判断表里面是否已经存在这个 id。如果存在,就跳过这个 id。但是,这个方法的成本很高。因为,本来申请 id 是一个很快的操作,现在还要再去主键索引树上判断 id 是否存在。
  • 把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。

可见,这两个方法都会导致性能问题。

因此,InnoDB 放弃了“允许自增 id 回退”这个设计,语句执行失败也不回退自增 id。

utf8 、utf8mb3和 utf8mb4的区别

utf8mb3
:只支持最长三个字节的BMP(Basic Multilingual Plane,基本多文种平面)字符(不支持补充字符)。

utf8mb4
:mb4即 most bytes 4,即最多使用4个字节来表示完整的UTF-8,具有以下特征:

  • 支持BMP和补充字符。
  • 每个多字节字符最多需要四个字节。

utf8mb4是utf8的超集并完全兼容它,是MySQL 在 5.5.3 版本之后增加的一个新的字符集,能够用四个字节存储更多的字符,几乎包含了世界上所有能看到见的语言字符。

  • 差异比较
差异点 utf8mb3 utf8mb4
最大使用字节数 3 4
支持字符类型 BMP BMP+其它字符
字符类型 常见的 Unicode 字符 常见的 Unicode 字符 + 部分罕用汉字 + emoji表情 + 新增的 Unicode 字符等
Unicode范围 U0000 - U+FFFF(即BMP) U0000 - U+10FFFF
占用存储空间 略小(如CHAR(10) 需要10 * 3 = 30 个字节的空间;VARCHAR 类型需要额外使用1个字节来记录字符串的长度) 稍大(如CHAR(10) 需要 10 * 4 = 40 个字节的空间;VARCHAR 类型需要额外使用2个字节来记录字符串的长度)
兼容性 切换至utf8mb4 一般不会有问题,但要注意存储空间够不够、排序规则是否变化 切换至utf8mb3可能会有问题,字符丢失、报错或乱码
安全性 稍低,更容易被恶意字符串攻击 较高,保留恶意字符串,然后报错或乱码提示

如何选择?一句话就是,根据具体的业务需求和实际情况,选择最合适的字符集。

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。