forthxu 发布的文章

比较arduino、stm32、atmg123、51、esp32

以下是 Arduino、STM32、ATmega123、51单片机(8051系列)、ESP32 的对比,从开发难度、性能、功能、应用场景等维度进行分析:


1. Arduino

  • 定位: 面向初学者和快速开发。
  • 核心: 基于AVR架构的微控制器(如ATmega328P)。
  • 开发难度:
    • 易上手,使用Arduino IDE及C语言开发。
    • 丰富的库和社区支持。
  • 性能:
    • 主频通常为16 MHz。
    • SRAM:2 KB(以Arduino Uno为例)。
  • 功能:
    • 提供数字I/O、PWM、ADC、串口通信等基本功能。
    • 模块化设计,支持丰富的扩展板(如传感器、屏幕等)。
  • 应用场景:
    • 教育、原型开发、小型物联网设备。

2. STM32

  • 定位: 面向专业开发者,适合对性能要求较高的项目。
  • 核心: 基于ARM Cortex-M内核(如Cortex-M0、M3、M4、M7)。
  • 开发难度:
    • 较高,需要熟悉嵌入式开发工具链(如Keil、STM32CubeIDE)。
    • HAL库和CubeMX工具降低了部分开发复杂度。
  • 性能:
    • 主频范围从48 MHz到480 MHz不等。
    • RAM容量较大(可达数百KB或更高)。
    • 丰富的外设接口(如CAN、USB、SPI、I2C、ADC、DAC等)。
  • 功能:
    • 支持RTOS、多种通信协议和高级外设。
    • 适合高实时性和低功耗需求。
  • 应用场景:
    • 工业控制、物联网网关、机器人、医疗设备。

3. ATmega123

  • 定位: 经典8位单片机,适合入门或小型项目。
  • 核心: AVR架构,8位处理器。
  • 开发难度:
    • 适中,使用C语言开发,工具链较成熟(如AVR Studio、Arduino兼容)。
  • 性能:
    • 主频16 MHz。
    • SRAM:4 KB。
  • 功能:
    • 支持基本的I/O、串口通信、ADC、定时器等功能。
  • 应用场景:
    • 小型嵌入式系统、简易控制设备。

4. 8051(51单片机)

  • 定位: 经典8位单片机,历史悠久。
  • 核心: 基于MCS-51架构。
  • 开发难度:
    • 较高,需了解硬件底层。
    • 开发工具包括Keil C51、汇编语言等。
  • 性能:
    • 主频一般为12 MHz到40 MHz。
    • SRAM和ROM容量有限(通常在数百字节到几KB之间)。
  • 功能:
    • 具备基本I/O和串口功能,外设较少。
  • 应用场景:
    • 学术研究、传统电子产品控制。

5. ESP32

  • 定位: 高性能无线通信模块,集成Wi-Fi和蓝牙。
  • 核心: 双核Xtensa 32位处理器。
  • 开发难度:
    • 易上手,支持Arduino IDE、Espressif IDF。
    • 丰富的库和文档支持。
  • 性能:
    • 主频高达240 MHz。
    • RAM容量大(可达520 KB)。
  • 功能:
    • 支持Wi-Fi、蓝牙LE和经典蓝牙。
    • 内置多种通信接口(如I2C、SPI、UART)。
  • 应用场景:
    • 智能家居、物联网设备、无线通信产品。

总结对比表

| 特性 | Arduino | STM32 | ATmega123 | 51单片机 | ESP32 |
|-----------|----------------|--------------|---------------|----------------|---------------|
| 主频 | 16 MHz | 48-480 MHz | 16 MHz | 12-40 MHz | 240 MHz |
| 架构 | AVR | ARM Cortex-M | AVR | MCS-51 | Xtensa |
| 开发难度 | 简单 | 较高 | 适中 | 较高 | 简单 |
| 功能 | 基础 | 强大 | 基础 | 基础 | 强大(含Wi-Fi和蓝牙) |
| 应用场景 | 教育/原型 | 工控/高性能 | 小型嵌入式 | 传统设备 | 物联网/智能家居 |


选择建议:

  • 入门学习: Arduino、ATmega123、51单片机。
  • 性能和功能: STM32、ESP32。
  • 无线通信和物联网: ESP32。

Docker 时,遇到 "No space left on device" 错误

在使用 Docker 时,遇到 "No space left on device" 错误,通常意味着宿主机的磁盘空间已满。以下是一些常见的原因和解决办法:

1. 清理 Docker 容器、镜像和卷

删除未使用的容器

列出所有停止的容器:

docker ps -a -f status=exited

删除所有停止的容器:

docker rm $(docker ps -a -f status=exited -q)

删除未使用的镜像

列出所有悬空(dangling)镜像:

docker images -f dangling=true

删除所有悬空镜像:

docker rmi $(docker images -f dangling=true -q)

删除未使用的卷

列出所有未使用的卷:

docker volume ls -f dangling=true

删除所有未使用的卷:

docker volume rm $(docker volume ls -f dangling=true -q)

自动清理未使用的数据

你可以使用以下命令自动清理未使用的容器、网络、镜像和卷:

docker system prune

如果你还想删除所有未使用的卷,可以使用:

docker system prune --volumes

2. 检查 Docker 的存储驱动和数据目录

检查 Docker 数据目录使用情况

Docker 通常将其数据存储在 /var/lib/docker 目录下。你可以检查该目录的磁盘使用情况:

du -sh /var/lib/docker

检查 Docker 存储驱动

Docker 使用不同的存储驱动来管理镜像和容器。可以通过以下命令查看当前使用的存储驱动:

docker info | grep 'Storage Driver'

根据存储驱动的不同,可能需要不同的清理策略。

3. 清理系统日志和临时文件

系统日志和临时文件可能会占用大量磁盘空间。

清理系统日志

在 Linux 上,可以使用以下命令清理日志:

sudo journalctl --vacuum-time=2d

这将删除超过 2 天的日志。你可以根据需要调整时间。

清理临时文件

使用以下命令清理临时文件:

sudo rm -rf /tmp/*

4. 检查和调整 Docker 的磁盘配额

如果你在使用 OverlayFS 或其他存储驱动,可以通过配置 Docker 来限制其磁盘使用。编辑 Docker 的配置文件(通常是 /etc/docker/daemon.json),添加类似以下的配置:

{
  "storage-opts": [
    "overlay2.size=20G"
  ]
}

然后重启 Docker 服务:

sudo systemctl restart docker

5. 检查并扩展磁盘空间

如果以上方法都无法解决问题,可能需要扩展宿主机的磁盘空间。以下是一些可能的操作:

  • 添加新的磁盘或扩展现有磁盘。
  • 将 Docker 数据目录移动到更大的磁盘或分区。
    1. 停止 Docker 服务:
      sudo systemctl stop docker
      
    2. 将数据目录移动到新的位置:
      sudo mv /var/lib/docker /new/path/docker
      
    3. 创建一个符号链接:
      sudo ln -s /new/path/docker /var/lib/docker
      
    4. 重启 Docker 服务:
      sudo systemctl start docker
      

通过这些步骤,你应该能够解决 "No space left on device" 错误。如果问题依然存在,请检查系统日志和 Docker 日志以获取更多信息。

emai邮件服务相关的 25、110、465、995 端口的作用 以及 SPF、DKIM 和 DMARC 电子邮件认证技术

在电子邮件系统中,不同的端口用于不同的协议和功能,具体如下:

  1. 端口 25

    • 协议:SMTP(Simple Mail Transfer Protocol)
    • 作用:用于发送电子邮件。SMTP 是用于电子邮件传输的主要协议,通过端口 25,邮件客户端可以将电子邮件发送到邮件服务器,邮件服务器之间也可以通过该端口转发电子邮件。
  2. 端口 110

    • 协议:POP3(Post Office Protocol version 3)
    • 作用:用于从邮件服务器上下载电子邮件。POP3 协议允许邮件客户端从服务器上下载邮件,并在本地阅读和管理。通过端口 110,邮件客户端可以连接到邮件服务器并检索存储在服务器上的邮件。
  3. 端口 465

    • 协议:SMTPS(SMTP Secure)或 SMTP over SSL
    • 作用:用于安全发送电子邮件。SMTPS 是 SMTP 协议的加密版本,通过 SSL/TLS 对邮件传输进行加密,从而提高安全性。端口 465 是为确保邮件传输的机密性和完整性而设定的标准端口。
  4. 端口 995

    • 协议:POP3S(POP3 Secure)或 POP3 over SSL
    • 作用:用于安全接收电子邮件。POP3S 是 POP3 协议的加密版本,通过 SSL/TLS 对邮件传输进行加密,确保邮件在下载过程中不会被截取或篡改。端口 995 是用于安全连接到邮件服务器以检索邮件的标准端口。

总结:

  • 端口 25:用于 SMTP 协议,发送电子邮件。
  • 端口 110:用于 POP3 协议,从服务器下载电子邮件。
  • 端口 465:用于加密的 SMTP 协议(SMTPS),安全发送电子邮件。
  • 端口 995:用于加密的 POP3 协议(POP3S),安全接收电子邮件。

这些端口及其对应的协议是电子邮件系统中的基础组成部分,确保电子邮件的发送、接收和安全性。

SPF、DKIM 和 DMARC 是电子邮件认证技术,用于提高电子邮件的安全性,防止电子邮件欺诈和垃圾邮件。这些技术相互配合,以验证邮件发送者的身份和邮件内容的完整性。

SPF(Sender Policy Framework)

作用

  • SPF 用于验证邮件的发送服务器是否被授权代表发送域发送电子邮件。它通过 DNS 记录定义哪些 IP 地址或主机名可以发送来自该域的电子邮件。

工作原理

  1. DNS 记录:域管理员在域的 DNS 记录中添加一个 SPF 记录,列出授权发送邮件的 IP 地址或主机名。
  2. 验证:当邮件服务器接收到来自该域的邮件时,它会检查邮件的发件人 IP 地址是否在 SPF 记录中列出的授权范围内。如果在,则验证通过;否则,验证失败。

示例

一个简单的 SPF 记录:

v=spf1 ip4:192.0.2.0/24 include:example.com -all
  • v=spf1:SPF 版本。
  • ip4:192.0.2.0/24:授权的 IP 地址范围。
  • include:example.com:包含其他域的 SPF 记录。
  • -all:严格模式,不在列表中的 IP 地址将导致验证失败。

DKIM(DomainKeys Identified Mail)

作用

  • DKIM 用于验证电子邮件的内容和发件人的身份。它通过数字签名的方式确保邮件在传输过程中没有被篡改,并验证邮件是否确实来自声明的域。

工作原理

  1. 签名:邮件服务器在发送邮件时使用域的私钥对邮件的某些部分进行签名,并在邮件头中添加一个 DKIM-Signature 字段。
  2. 验证:接收服务器使用发送域的公钥(发布在 DNS 记录中)验证邮件签名,确保邮件内容的完整性和发件人的身份。

示例

一个简单的 DKIM 记录:

default._domainkey.example.com IN TXT "v=DKIM1; k=rsa; p=公钥内容"
  • v=DKIM1:DKIM 版本。
  • k=rsa:加密算法。
  • p=公钥内容:用于验证签名的公钥。

DMARC(Domain-based Message Authentication, Reporting & Conformance)

作用

  • DMARC 用于告知接收邮件的服务器如何处理未通过 SPF 或 DKIM 验证的邮件,并提供反馈机制给发送域管理员。

工作原理

  1. 策略发布:域管理员在 DNS 中发布 DMARC 记录,指定如何处理未通过验证的邮件(如拒绝、隔离或无操作),以及接收报告的邮箱。
  2. 验证和执行:接收邮件服务器根据 DMARC 策略对邮件进行处理,并将验证结果和处理报告发送给发送域管理员。

示例

一个简单的 DMARC 记录:

_dmarc.example.com IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com"
  • v=DMARC1:DMARC 版本。
  • p=reject:策略(拒绝未通过验证的邮件)。
  • rua=mailto:dmarc-reports@example.com:发送报告的邮箱地址。

综述

  • SPF:验证发送邮件的服务器是否被授权。
  • DKIM:验证邮件内容的完整性和发件人的身份。
  • DMARC:结合 SPF 和 DKIM,制定邮件处理策略,并提供验证结果报告。

通过综合使用 SPF、DKIM 和 DMARC,可以显著提高电子邮件的安全性,减少欺诈和垃圾邮件。

修改运行中的Docker容器修改端口

在docker run创建并运行容器的时候,可以通过-p指定端口映射规则。但是,我们经常会遇到刚开始忘记设置端口映射或者设置错了需要修改。当docker start运行容器后并没有提供一个-p选项或设置,让你修改指定端口映射规则。这时可以通过暴力手段,直接强行修改docker容器的配置文件,从而修改容器端口。

对应Linux操作系统

1、先查询出容器的Id

docker inspect [容器id] | grep Id

2、修改容器配置文件

vim /var/lib/docker/containers/[容器id]/hostconfig.json

3、重启docker

sudo systemctl restart docker

对应Mac操作系统

1、先查询出容器的Id

docker inspect [容器id] | grep Id

2、进入docker使用的Linux虚拟机

 docker run -it --privileged --pid=host [镜像名] nsenter -t 1 -m -u -n -i sh

3、修改容器配置文件

vim /var/lib/docker/containers/[容器id]/hostconfig.json

4、重启docker

sudo systemctl restart docker

100万条数据里面随机取10条

错误的语句

select id,name from news_6 order by rand() limit 10

100万条数据里面随机取10条 纯SQL语句实现

select id,name from news_6 where id > (

select ROUND(RAND() * ((SELECT MAX(id) as max FROM news_6)  -  (SELECT MIN(id) as min FROM news_6))) + (SELECT MIN(id) as min FROM news_6) - 10

) order by id asc limit 10;

上面语句保持在一个小数内10000条,where 语句 遇上带有rand()函数的子语句subquery会有问题。最后用join实现。

select t.* from (

select t1.id,t1.name from news_6 t1 join (

select ROUND(RAND() * ((SELECT MAX(id) as max FROM news_6)  -  (SELECT MIN(id) as min FROM news_6))) + (SELECT MIN(id) as min FROM news_6) - 30 as id

) t2 where t1.id>t2.id order by id asc limit 30

) t order by RAND() limit 10;

mysq分组内排序

#分组第一名
select * from birecord where platform='okex-swap' and concat(mark, addtime) in (select concat(mark, max(addtime)) from birecord where platform='okex-swap' group by mark)

#分组内前两名,查询速度慢
select * from birecord m where platform='okex-swap' and (select count(*) from birecord n where platform='okex-swap' and n.mark=m.mark and n.addtime>m.addtime)<2;

#分组内前两名,addtime值前两名,可能超过两名,mysql8.0后支持
select * from (select *,row_number() over(partition by mark order by addtime) as num from birecord where platform='okex-swap') n where num<3;
#分组内前两名,addtime排序前两名,mysql8.0后支持
select * from (select *,rank() over(partition by mark order by addtime) as num from birecord where platform='okex-swap') n where num<3;

#可以标记顺序但做不了分组排序
select *,(@row_num:=@row_num+1) as num from birecord,(select @row_num:=0) as t group by mark;

如何使用Mysql正确的处理财务数据

财务数据相比于普通的互联网应用数据,对数据的一致性有更高的要求。因为涉及到用户金钱的流动,出现问题就意味金钱和声誉上的损失。在用 Mysql 处理财务数据时,我认为应该遵循以下原则:

  1. 使用 DECIMAL 数据类型存储金额。因为浮点数精度是有限的,并且无法精确的表示一些数字。应用程序也应该使用 Decimal 函数库来进行金额的加减乘除的运算,比如 Python 的 decimal 模块,C++ 的 boost Multiprecision 库。
  2. 使用事务来更新数据库。涉及到数据库多个记录更新时,事务能够做到要么全部成功,要么全部失败,这保证了数据的一致性。Mysql 使用事务需要 InnoDB 引擎。
  3. 更新数据库时使用悲观锁。更新数据前使用 SELECT …  FOR UPDATE; 来查询,这样防止并发的请求读到脏数据,导致数据错乱。虽然加锁会影响性能,但为了数据的一致性也是值得的。
  4. 记录资金变化的流水日志。不能简单的只记录用户的金额,还要记录每笔资金的来龙去脉,包括变化的大小、时间、业务、更新前金额、更新后金额、备注等,另外还有记录业务ID防止重复更新金额。这样在对账的时候才能有理有据。

下面以一个完整的示例来说明如何设计一个完备的记账系统用来记录人民币余额,这里精度只需要2位小数就可以了。

假设已经有一个用户表,每个用户有一个唯一ID。我们需要创建两张表,一张余额表,一张流水表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE `balance` (
  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `user_id` int NOT NULL,
  `item` varchar(10) NOT NULL,
  `balance` decimal(20,2) NOT NULL
) ENGINE=InnoDB;
 
CREATE TABLE `history` (
  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `user_id` int NOT NULL,
  `item` varchar(10) NOT NULL,
  `amount` decimal(20,2) NOT NULL,
  `befor` decimal(20,2) NOT NULL,
  `after` decimal(20,2) NOT NULL,
  `business` varchar(30) NOT NULL,
  `business_id` varchar(100) NOT NULL,
  `detail` text
) ENGINE=InnoDB;

为了加快查询速度,另外为了有效利用 InnoDB 的行级锁,我们需要给两张表加上联合索引。另外,我们需要保证流水记录中 user_id, item, business, business_id 的组合是唯一的,避免重复更新数据。

1
2
3
4
5
ALTER TABLE balance ADD INDEX `user_item_idx` (`user_id`, `item`);
 
ALTER TABLE history ADD INDEX `user_item_idx` (`user_id`, `item`);
 
ALTER TABLE history ADD UNIQUE update_unique (user_id, item, business, business_id);

假设这时候用户 ID 为 1 的用户充值了 100 元人民币,我们需要把用户的人民币余额加上 100 元,我们要如何处理呢?

这里我们还需要一个充值表用来保存用户的充值记录,同时,我们再创建一个提现表来表示用户提现记录,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `deposit` (
  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `user_id` int NOT NULL,
  `item` varchar(10) NOT NULL,
  `amount` decimal(20,2) NOT NULL
) ENGINE=InnoDB;
 
CREATE TABLE `withdraw` (
  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `user_id` int NOT NULL,
  `item` varchar(10) NOT NULL,
  `amount` decimal(20,2) NOT NULL
) ENGINE=InnoDB;

首先,开始一个事务,创建充值记录:

1
2
3
START TRANSACTION;
 
INSERT INTO deposit VALUES (NULL, 1, 'CNY', '100')

插入成功后,我们可以使用 MYSQL 的 API 获取到上次插入后生成的自增 ID 的值,假设这里也为 1. 然后我们需要从数据库查询当前余额。查询的时候注意要使用 FOR UPDATE 来锁住该记录,避免其它并发的请求读到脏数据。即使当前该记录不存在,在事务提交之前,其它读请求仍然被阻塞读不到数据的。

1
SELECT id, balance FROM balance where user_id = 1 and item = 'CNY' FOR UPDATE;

由于用户是新注册的,查询到的数据为空,所以我们创建新的记录:

1
INSERT INTO balance VALUES (NULL, 1, 'CNY', '100');

然后我们要在流水表中记录下这次的变更操作:

1
INSERT INTO history VALUES (NULL, NULL, 1, 'CNY', '100', '0', '100', 'deposit', '1', '');

最后提交事务:

1
COMMIT;

事务提交后,所有的数据都写入到数据库中了,中间如果有异常发生,则执行

1
ROLLBACK;

来放弃所有的变更。

假设过了一段时间用户要求提现 50 元,操作流程如下:

1
2
3
START TRANSACTION;
 
SELECT id, balance FROM balance where user_id = 1 and item = 'CNY' FOR UPDATE;

假设读出的 id 也为 1. 由于用户之前充值了 100 元还没有使用,所以余额是 100,大于 50 满足提现条件。如果不满足的话需要执行 ROLLBACK. 然后:

1
2
3
4
5
6
7
INSERT INTO withdraw VALUES (NULL, 1, 'CNY', '50');
 
UPDATE balance set balance = '50' WHERE id = 1;
 
INSERT INTO history VALUES (NULL, NULL, 1, 'CNY', '-50', '100', '50', 'withdraw', '1', '');
 
COMMIT;

这就完整的实现了整个充值和提现的流程。

当然,这里示例的充值和提现是最简化的流程,实际业务中,充值和提现往往涉及到多种状态的流转。并且在提现中,用户发起提现和实际进行转账不可能是同时进行的,中间可能会取消操作,直接扣除资金不太妥当,更妥当的做法是把待提现的资金冻结起来,冻结操作其实就是创建一个新的 item: CNY_FREEZE, 在 CNY 上扣减,然后在 CNY_FREEZE 上增加来实现。转账后再从 CNY_FREEZE 中扣除。如果取消操作则取消冻结,从 CNY_FREEZE 扣减,增加到 CNY 上面,这样整个流程在流水日志上都有体现。

https://web.archive.org/web/20170617125020/http://blog.haipo.me/?p=1266

https://zhuanlan.zhihu.com/p/143866444

投屏技术

AirPlay:应用于苹果设备,如:iPhone、iPad、Mac,使用 AirPlay 协议传输,无须其他软件辅助即可连接投屏;
DLNA:应用于 Windows 设备,一般还需要借助第三方软件完成投屏,也可应用于支持 DLNA 协议的 Android 设备,一般只能投放视频和图片;
Miracast:应用于 Android 设备,设备如果支持该功能,通常在设置中显示为 “无线显示” 或 “无线投屏”、“Miracast” 等标识。

协议
实现
项目

mysql根据逗号隔开的字段数据把一行数据拆分成多行数据

# 查找出被逗号分隔字段需要拆分的最大数量
select max((LENGTH(逗号分隔的字段)-LENGTH(REPLACE(逗号分隔的字段, ',', ''))+1)) from 处理表 where 条件;

# 创建一张临时表用于联合查询,方便把处理表单行记录分隔为多行
CREATE TEMPORARY TABLE incre_table (
    `id` int NOT NULL AUTO_INCREMENT,
    PRIMARY KEY (`id`)
);
insert into incre_table values (1);
insert into incre_table values (2);
insert into incre_table values (3);
insert into incre_table values (4);
insert into incre_table values (5);
insert into incre_table values (6);
insert into incre_table values (7);
insert into incre_table values (8);
insert into incre_table values (9);
insert into incre_table values (10);
# ... 大于 需要拆分的最大数量

# 关键在于连表查询 ON b.id <= 逗号分隔的数量
SELECT
    a.id,
    substring_index(
        substring_index(a.逗号分隔的字段, ',', b.id),
        ',', - 1
    )
FROM
    处理表 a
RIGHT JOIN incre_table b ON b.id <= (
    LENGTH(a.逗号分隔的字段) - LENGTH(REPLACE (a.逗号分隔的字段, ',', '')) + 1
)
WHERE
    a.条件;

php strtotime相对时间-1month的使用问题

历史代码的一个问题

➜  ~ date
Tue Mar 31 17:37:20 CST 2020
➜  ~ php -r "echo date('Y-m', strtotime('-1 month'));"
2020-03                                                                                                                                                                                                        
➜  ~ php -r "echo date('Y-m', strtotime('+1 month'));"
2020-05

当前日期是2020年03月31日,直观感觉减一个月应该是输出2020-02,加一个月应该是输出2020-04。

➜  ~ cal 2 2020
   February 2020
Su Mo Tu We Th Fr Sa
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29

➜  ~ cal 3 2020
     March 2020
Su Mo Tu We Th Fr Sa
 1  2  3  4  5  6  7
 8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31


➜  ~ cal 4 2020
     April 2020
Su Mo Tu We Th Fr Sa
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30

其实是程序跟人理解不同造成的:
2020年03月31日,减一个月等于 2020年02月31日,系统没有2020年02月31日,往前加就等于2020年03月02日
2020年03月31日,加一个月等于 2020年04月31日,系统没有2020年04月31日,往前加就等于2020年05月01日

#php5.3之前可以采用:

➜  ~ php -r "echo date('Y-m-d', strtotime(date('Ym01').' -1 month'));"
2020-02-01  

     

#php5.3之后可以采用:

    ➜  ~ php -r "echo date('Y-m-d', strtotime('first day of -1 month'));"
2020-02-01                                                                                                                                                                                               ➜  ~ php -r "echo date('Y-m-d', strtotime('last day of -1 month'));"
2020-02-29