憨憨呆呆的IT之旅

我见,我思,我行

java.io.File类是对文件极其常用操作的抽象。

File类的常用方法

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FileTest {
public static void main(String[] args) throws IOException {
// 1. 构造函数
String filename = "./README.md";
System.out.println("File(" + filename + ")");
File f1 = new File("./README.md");
if (f1.exists()) {
System.out.println("getName(): " + f1.getName());
System.out.println("getPath(): " + f1.getPath());
System.out.println("getAbsolutePath(): " + f1.getAbsolutePath());
System.out.println("getCanonicalPath(): " + f1.getCanonicalPath());
System.out.println("lastModified(): " + f1.lastModified());
System.out.println("length(): " + f1.length());
System.out.println("isFile(): " + f1.isFile());
}
}
}

区分getName()、getPath()、getAbsolutePath()、getCanonicalPath()

区分path、absolutePath、canonicalPath

文件的创建和删除

示例代码:

1
2
3
4
5
6
7
String filename = "./test-file.md";
File f1 = new File(filename);
if (f1.exists()) {
System.out.println(f1.delete() ? "文件删除成功!" : "文件删除失败!");
} else {
System.out.println(f1.createNewFile() ? "文件创建成功!" : "文件创建失败!");
}

目录的创建和删除

示例代码:

1
2
3
4
5
6
7
8
9
// 2. 目录创建、删除
File f2 = new File("./test-dir/test-subdir");
if (f2.exists()) {
System.out.println("getName(): " + f2.getName());
// 这里只能删除最内层的非空目录
System.out.println(f2.delete()? "目录删除成功!" : "目录删除失败!");
} else {
System.out.println(f2.mkdirs() ? "目录创建成功!" : "目录创建失败!");
}

listFiles()指定文件过滤FileFilter

示例代码:

1
2
3
4
5
6
File f4 = new File("./.idea");
FileFilter filter = (pathname) -> { return pathname.getName().endsWith(".xml");};
File[] fileList2 = f4.listFiles(filter);
for (File f: fileList2) {
System.out.println(f.getName());
}

目录及子目录的递归遍历

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 递归打印目录和子目录下所有文件
* @param file
* @param filter
*/
public static void show_dirs(File file, FileFilter filter) {
File[] files = file.listFiles(filter);
for (File f: files) {
if (f.isFile()) {
System.out.println(f.getName());
} else if (f.isDirectory()) {
System.out.println("Dir[" + f.getName() + "]");
show_dirs(f, filter);
}
}
}

public static void main(String[] args) {
FileFilter filter2 = (pathname) -> { return pathname.isDirectory() || pathname.getName().endsWith(".class"); };
show_dirs(new File("./out"), filter2);
}

异常机制的基本概念

异常就是指不正常的意思,在Java中指的是程序执行过程中发生的不正常情况。

异常VS错误

Java将程序的不正常行为按严重程度分为错误(Error)和异常(Exception)两大类。
java.lang.Throwable是描述错误Error和异常Exception的超类。
Error类指的是Jvm无法处理的严重错误,一旦发生Error,通常Jvm无能为力,只能挂掉。
Exception类通常用来描述因编程错误或偶然外部因素导致的轻微错误,一般可以通过编程解决。

异常的分类

Java官方提供的异常类很多,但通常可以按异常检测的时间点分为两大类:

  • 检测性异常,也就是说编译器可以检测到的异常,比如IOException等。
  • 非检测性异常,RuntimeException,也叫运行时异常。 比如,被0除时会抛出ArithmeticException,空指针时的NullPointException,等等。

异常的继承结构

前面已经说过,所有异常都继承自Exception类,而Exception又继承自Throwable类。
再加上我们平时最常用的几个Runtime异常,构成的异常继承结构图如下:
常见异常的继承结构

异常的代码结构

1
2
3
4
5
6
7
try {

} catch (Exception e) {

} finally {

}

finally的注意事项

  • finally

异常的处理

异常的处理,一般有三种方式:

  • 异常避免
  • 异常捕获
  • 异常抛出
    如果拿感冒来比喻异常,这三种方式就好比我们的对待感冒的策略一样。首先,我们平时注意锻炼、保暖,以避免得上感冒,这相当于异常的避免。而万一得了感冒,我们可以吃点感冒药,这相当于异常的捕获。如果感冒严重了吃感冒药也不顶用,那就只能去医院看医生了,这就相当于抛出异常。

异常避免

一般使用if语句来提前判断,避免异常。比如下面的语句,通过判断b是否等于0,来避免ArithmeticException:

1
2
3
4
5
int a = 10;
int b = 0;
if (b != 0) {
System.out.println(a / b);
}

再比如,提前判断引用变量是否为空,来避免空指针异常NullPointException:

1
2
3
4
5
FileInputStream fis = null;
...
if (null != fis) {
fis.close();
}

异常捕获

使用catch语句来捕获异常并处理;使用finally做善后处理。

catch语句注意:

  • 多个catch语句,小异常类应放到前面。

tips: finally语句在函数return之前必然执行。

考点:请问下面代码返回结果是几?

1
2
3
4
5
6
7
8
try {
System.out.println(3 / 0);
return 0;
} catch (ArithmeticException e) {
return 1;
} finally {
return 2;
}

结果是 2, 因为finally抢在return 1之前return了。

异常抛出

若方法决定自己不处理异常,则需要将异常抛出。

什么时候抛出异常,什么时候捕获异常?

经验:

  1. 方法重写时,若被重写的方法未抛出异常,则重写后的方法也不应该抛出异常。
  2. 如果方法之间有好多层调用关系,可以抛出异常,交由最外层方法捕获。

自定义异常

有时候我们需要自定义异常。

为什么要自定义异常呢

Java官方提供的异常不够用。现实业务开发中,异常各种各样,需要自定义。

如何自定义

编码示例:

public class XXXException extends Exception {
    static final long serialVersionUID = 1111111L;
    
    public XXXException() {
    }
    
    public XXXException(String message) {
        super(message);
    }
}

今天打开Google,发现不能用了,折腾了近两个小时,才不完美的解决了。

问题描述

这个问题的直接表现就是,Google打不开。查了下v2ray客户端日志,发现如下记录:

1
2
2020/05/22 13:43:00 [Info] [1562828084] v2ray.com/core/transport/internet/tcp: dialing TCP to tcp:1.1.1.1:11111
2020/05/22 13:43:01 [Warning] [1562828084] v2ray.com/core/app/proxyman/outbound: failed to process outbound traffic > v2ray.com/core/proxy/vmess/outbound: failed to find an available destination > v2ray.com/core/common/retry: [dial tcp 1.1.1.1:11111: i/o timeout dial tcp 1.1.1.1:11111: operation was canceled] > v2ray.com/core/common/retry: all retry attempts failed

(保密起见,记录中ip地址和端口号已经替换成假的,下文同。)

上述日志表明,连接远程v2ray服务端发生了网络IO超时。这意味着,要么服务器出故障了,要么网络出故障了。
那就一项一项排查呗,先检查服务器,再检查网络。

服务器排查

第一步,检查一下服务器是否还在正常工作。一般情况下用服务器ip是否能够ping通作为判断标准。

1
2
3
4
5
6
ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: icmp_seq=0 ttl=50 time=205.611 ms
Request timeout for icmp_seq 1
64 bytes from 1.1.1.1: icmp_seq=2 ttl=50 time=205.445 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=50 time=207.564 ms

可见,服务器本身是好的。

第二步,检查v2ray服务端端口是否正常工作。
在本地用tcping工具检查v2ray服务端的端口号,发现端口状态为“closed”。

1
2
# tcping 1.1.1.1 11111
1.1.1.1 port 11111 closed.

端口closed。似乎抓到背后问题的尾巴了!
顺手再查查其他端口比如80、443等常用HTTP端口,发现状态都是“open”。
那么,这里可以下一个初步结论:这个端口从本机访问不了了。
不过这种情况还无法直接定位到问题根源,需要继续排查。

第三步,检查服务器中v2ray程序是否启动。

1
2
# ps -ef | grep v2ray
root 2988 1 0 14:02 ? 00:00:02 /usr/bin/v2ray/v2ray -config /etc/v2ray/config.json

第四步,检查服务器上v2ray监听端口是否开启。

1
2
# netstat -nap | grep 11111
tcp6 0 0 :::11111 :::* LISTEN 2988/v2ray

经过以上两个步骤,可以看出v2ray服务端并没有什么问题,仍在正常工作中。
第五步,检查服务器防火墙配置,看看有没有禁用v2ray的监听端口。

1
2
3
4
5
6
7
# iptables --list
...
Chain IN_public_allow (1 references)
...
ACCEPT tcp -- anywhere anywhere tcp dpt:6170 ctstate NEW,UNTRACKED
ACCEPT udp -- anywhere anywhere udp dpt:6170 ctstate NEW,UNTRACKED
...

可见,防火墙对v2ray监听端口也正确设置了放行规则。

经过以上检查步骤,断定服务器端配置没有问题。另外,由于本地电脑和服务器都是我一个人在用,也不存在别人改了我的配置,导致本机v2ray和服务端配置不一致的情况。

网络排查

其实经过上面服务器端检查的第二步,已经能够说明很多问题了。网站服务可正常访问,ssh也能用,唯独v2ray服务受到影响,可见不是服务器机房网络的问题了,而是涉及该端口11111的流量,在传输过程中被ban了。这种情况就难以下确切结论了,只能想其他办法了。

不断试错,最终解决

找不到根本原因,那就求助万能的搜索引擎吧,看看网友们怎么说。谷歌用不了就只能度娘了。打开百度输入关键词”v2ray连接超时“,竟然第一条就找到了一个貌似提供解决方案的blog,打开一看发布日期,”2020年5月20日“,哦哦,还就是这两天发布的,看来靠谱。该博文给出的方案是v2ray服务器端绑定ipv4监听地址,其实就是修改v2ray配置文件/etc/v2ray/config.json,在inbound节点中加上"listen": "1.1.1.1"这样的配置项,然后保存退出,重启v2ray。
按这个方案试了一下,不行。 回头看博文下面的评论,讨论挺热烈,大家都说方案跑不通。
再翻翻其他博文,也都说的云里雾里。

实在没辙了,先换个端口试试吧。
修改服务器端v2ray配置文件,端口号由原来的11111改为22222。

1
2
3
4
5
6
7
# vim /etc/v2ray/config.json
...
"inbound": {
"port": 11111, <------这里修改端口
"protocol": "vmess",
"listen":"12.34.56.78",
...

修改后,同步修改本机v2ray客户端配置里的端口号。另外别忘了在服务器防火墙里配置开放这个端口。
再次打开Google测试,竟然可以访问了!

喜大普奔!

冷静下来后,再次检查本机v2ray日志:

1
2
3
4
5
6
2020/05/22 14:08:11 [Info] [713431801] v2ray.com/core/app/dispatcher: default route for tcp:www.google.com:443
2020/05/22 14:08:11 [Info] [713431801] v2ray.com/core/transport/internet/tcp: dialing TCP to tcp:1.1.1.1:22222
2020/05/22 14:08:11 tcp:127.0.0.1:63193 accepted tcp:www.gstatic.com:443 [proxy]
2020/05/22 14:08:11 tcp:127.0.0.1:63192 accepted tcp:www.google.com:443 [proxy]
2020/05/22 14:08:11 [Info] [3172428981] v2ray.com/core/proxy/vmess/outbound: tunneling request to tcp:www.gstatic.com:443 via tcp:1.1.1.1:22222
2020/05/22 14:08:11 [Info] [713431801] v2ray.com/core/proxy/vmess/outbound: tunneling request to tcp:www.google.com:443 via tcp:1.1.1.1:22222

发现跟远程服务器端的tunnel通道已经成功建立了。
最后总结一句话解决办法: 换端口

问题反思

这次问题算是临时得到了解决。至于为什么原来的端口不能用了,怀疑是跟网络运营商,或是国际线路节点有关,可能是因为一些不能明说的原因比如BT下载等,这个端口或端口区间被ban了。这种情况也没办法说理去,只能换端口了。下次要是遇到类似的情况,估计还得换端口。所以说这是不完美的解决办法。

一般在前端上传文件我习惯用百度的WebUploader,不过有时候也会用到一些简单的方案。这里就记录一个比较简单的ajax上传文件方案。

前端

  1. html部分
    1
    2
    3
    4
    <form enctype="multipart/form-data">
    <p>选择文件:<input type="file" id="upload_file" name="upload_file"/></p>
    <button id="submit">提交</button>
    </form>
  2. js部分
    首先需要依赖jquery,请自行引入,这里不再描述。
    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
    <script type="application/javascript">
    $(function() {
    // 选择1:点击”提交“按钮时上传
    $('#submit').click(function() {
    ajaxUpload();
    });
    // 选择2:文件对象改变时触发上传
    $('#upload_file').change(function(){
    ajaxUpload();
    });
    function ajaxUpload() {
    var files = $('#upload_file').prop('files');
    var data = new FormData();
    data.append('file', files[0]);

    $.ajax({
    type: 'POST',
    url: 'upload.php',
    data: data,
    cache: false, // 兼容ie8,防止ie8之前版本缓存get请求的处理方式
    contentType: false, // 避免jquery误解
    processData: false, // 避免发送的数据被默认转为"application/x-www-form-urlencoded"
    success: function(data) {
    console.log(data);
    console.log(typeof data);
    // 拿到后端传回的响应信息后,做后续业务处理
    }
    })
    }
    })
    </script>

后端

  1. 我这里在后端处理文件上传的语言是upload.php。它的主要内容如下:
    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
    // in upload.php
    header("Content-type: application/json");
    try {
    $uploadFile = $_FILES['csv_file'];
    $originName = basename($uploadFile["name"]); //被上传文件的名称
    $tmpName = $uploadFile['tmp_name'];

    //检查或创建保存目录
    $savePath = '../upload/' . date("Y_m_d/");
    if(!file_exists($savePath)){
    mkdir($savePath);
    }

    //文件重命名
    $newName = $originName; // 为简单起见,这里在保存文件时未修改文件名

    //保存文件
    $saveFile = $savePath.$newName;
    if(move_uploaded_file($tmpName, $saveFile)){
    die(json_encode('code'=>1, 'msg'=>$newName));//上传成功,返回文件名。
    } else {
    throw new Exception('文件写入失败,请检查上传目录是否可写', -1);
    }
    } catch (Exception $e) {
    die(json_encode('code'=>-1, 'msg'=>$e->getMessage()));
    }

今天调试一个ajax文件上传接口,拿到的数据看起来是标准的json字符串,但是浏览器执行始终跟期望不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$.ajax({
type : 'POST',
data : data,
url : 'upload.php',
cache: false,
processData: false,
contentType: false,
success : function(data){
console.log(data);
console.log(data.code);
var code = data.code;
var msg = data.msg;
switch(code){
case 1:
$('#health_doc').val(msg);
$('#show_plan_file').hide();
break;
default:
layer.alert(msg, {icon: 5});
break;
}
}
});

上图代码中,console.log(data)打印正常,而console.log(data.code)打印结果为undefined。 于是,再追加一条打印语句console.log(typeof data),发现结果竟然是string,看来json字符串没有被浏览器自动解析。经检查Response Header,找到问题了。原来是接口返回的header头不对,其他接口都返回Content-Type: application/json, 唯独这个接口返回的是Content-Type: text/html,导致返回的json字符串没有办法被浏览器自动解析。
再追究一层,发现Response Header的设置跟ajax请求时有关,我们为了实现文件上传,给ajax设置了contentType属性设置为false,这样后端就无法根据请求contentType自动给出正确的响应头Content-Type设置。

解决这个问题有两种方案:
一是在后端输出请求响应内容前,设置Http响应头信息为application-json。假设后端是php,可以如下设置:

1
header("Content-type: application/json");

另一种方案是后端不处理,前端js拿到响应字符串后自己通过JSON.parse(data)手动转为json对象。

在mac下安装maven,有两种可选方案:

maven的安装

方法1,通过官网压缩包安装

  1. 访问官网下载maven压缩包,.zip.tar.gz后缀的都可以。
  2. 解压压缩包到你日常放置绿色软件的目录
  3. vim ~/.bash_profile,添加M2_HOME环境变量:
    1
    2
    export M2_HOME=/Users/xxx/Documents/maven/apache-maven-3.6.3
    export PATH=$PATH:$M2_HOME/bin
    注意把上边命令中的路径替换为你电脑上的路径。
    wq保存退出后,执行以下命令使配置生效:
    1
    source ~/.bash_profile
  4. mvn -v检查安装是否成功

方法2,通过brew直接安装

brew安装很简单,不需要再配置M2_HOME等环境变量。

注意,brew 安装时会自动安装maven的依赖 openjdk,可能比较慢,如果已经通过非brew的方式安装了jdk,建议还是走官网压缩包安装的方式。

brew安装命令如下:

1
2
3
4
5
6
# 查找maven
brew search maven
# 查看版本信息和依赖包
brew info maven
# 安装
brew install maven

最后,使用mvn -v检查安装是否成功。

安装完成后的配置

一般我们需要配置本地仓库和加速镜像即可。所有配置项都在settings.xml文件中定义。

配置本地仓库

修改settings.xml文件,配置以下内容:

1
<localRepository>/Users/xxx/maven_repo</localRepository>

配置加速镜像

修改settings.xml文件,配置以下内容:

1
2
3
4
5
6
7
8
9
<mirrors>
<!-- aliyun -->
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>

根据声明位置的不同,我们一般可以将java的变量分为两个类型:成员变量局部变量

变量的分类

成员变量是指声明在类中、类方法之外的变量,包括:

  • 实例变量: 无static修饰。
  • 类变量(静态变量):有static修饰。
    局部变量是指声明在类方法中的变量,包括:
  • 方法参数变量:也就是形参。
  • 方法局部变量:声明在方法体{}内,方法体嵌套的代码块外。
  • 代码块局部变量:声明在方法体嵌套的代码块内。

各种变量的特征

接下来用一个表格阐述一下这5种类型的变量各自的特征。

声明/定义方式作用域&生命周期访问权限默认值&初始化内存分配
方法参数变量方法形参方法执行时创建,执行完释放方法体内可见无,必须明确初始化栈区(java虚拟机栈)
方法局部变量方法体{}内方法执行时创建,执行完释放方法体内可见无,必须明确初始化栈区(java虚拟机栈)
代码块局部变量方法体内的某个代码块{}内代码块执行时创建,执行完释放代码块内可见无,必须明确初始化栈区(java虚拟机栈)
实例变量类中,方法体外对象创建时创建,对象销毁时释放由修饰符决定有,引用类型为null,基本数据类型为java预定义的默认值随对象一起分配在堆上
静态变量类中,方法体外,带static修饰符类加载时创建,类卸载时销毁由修饰符定有,引用类型为null,基本数据类型为java预定义的默认值方法区(静态存储区)

大家都知道for循环和while循环是等价的,我们平时写代码既可以用for循环也可以用while循环,正所谓条条大路通罗马。可是,既然java选择保留了这两种循环,至少意味着它们在使用场合上还是有一些侧重的。先上个例子:
猜数字游戏:系统先生成一个1-100之间的随机数(不让用户知道),然后让用户来猜测输入的数字是几,如果用户输入正确则游戏结束,否则,给出大于、小于的提示,继续让用户输入。
这个题目,如果用for循环来写,大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
public static void guessNumberUsingFor() {
System.out.println("-----猜数字游戏开始------");
int givenNumber = new java.util.Random().nextInt(100) + 1;
java.util.Scanner scanner = new java.util.Scanner(System.in);
System.out.println("请猜一个数字:");
for (int inputNumber = scanner.nextInt(); inputNumber != givenNumber; inputNumber = scanner.nextInt()) {
System.out.println((inputNumber < givenNumber) ? "猜小了,再猜:" : "猜大了,再猜:");
}
System.out.println("啊哈,猜对了!");
System.out.println("-----猜数字游戏结束------");
}

用while循环的版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void guessNumberUsingWhile() {
System.out.println("-----猜数字游戏开始------");
int givenNumber = new java.util.Random().nextInt(100) + 1;
java.util.Scanner scanner = new java.util.Scanner(System.in);
System.out.println("请猜一个数字:");
int inputNumber = scanner.nextInt();
while (inputNumber != givenNumber) {
System.out.println((inputNumber < givenNumber) ? "猜小了,再猜:" : "猜大了,再猜:");
inputNumber = scanner.nextInt();
}
System.out.println("啊哈,猜对了!");
System.out.println("-----猜数字游戏结束------");
}

上面这个例子,for版本似乎没有while版本顺滑,症结在于for循环版本把一些比较的赋值、更新语句(这里是scanner.nextInt())揉进了for循环的变量初始化和更新值的过程中,显得有违常识,影响了代码的可读性。对于习惯了for(int i=0; i < 10; i++) {}这样的人来说,极有可能会忽视掉inputNumber = scanner.nextInt()这条最重要的业务处理语句。而while版本,判断逻辑非常清晰,读起来一气呵成,没有任何认知障碍。

再看第二个例子:
** 计算1到100的整数之和 **
for循环版本如下:

1
2
3
4
5
6
7
public static void sumUsingFor() {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println("sum = " + sum);
}

while循环版本为:

1
2
3
4
5
6
7
8
9
public static void sumUsingWhile() {
int sum = 0;
int i = 1;
while (i <= 100) {
sum += i;
i++;
}
System.out.println("sum = " + sum);
}

这个例子中,for循环的优势就比较明显了。首先,单单从代码行数上来讲,for循环7行,while循环需要9行,少了两行代码。其次,while循环里的i++;语句,稍微不仔细的话就可能忘记写了,造成无限循环!而且这错误属于逻辑错误,编译器也帮不了你。

从上面两个例子可以看出,选择合适的循环语句是需要费点脑子的。那么,到底什么时候该用for,什么时候适合用while,有没有什么规矩呢?有的。
一般来说,while循环更适合于明确循环条件而不明确循环次数的场合,for循环则更适合于明确循环次数或范围的场合。

我们有时候正在开发一个feature,突然同事插进来说发现一个重大bug,需要立即修复。但是我们的feature才编码一半,不能提交。这时候就轮到git stash闪亮登场了。
git stash命令可以让我们先把当前的修改内容暂存在本地,保持working directory干净状态,接着就可以切换分支去修bug了。修完bug后,再把分支切回来,使用git stash pop命令恢复之前的修改内容,继续我们的feature编码。
git stash命令支持调用多次,它有一个类似栈的概念,每调用一次会把当前未暂存的修改压栈,然后可以使用git stash pop 从栈顶拿出最近一次压栈的修改内容。

git stash常见用法

  • git stash - 暂存当前修改
  • git stash save 'some message' - 功能同git stash,可额外添加一个注释信息便于找回
  • git stash list - 查看当前有哪些暂存的修改,可以列出stash_id,供后续恢复时使用
  • git stash pop - 取出最新的修改内容
  • git stash pop --index - 恢复最新的修改内容到工作区和暂存区
  • git stash pop 'stash_id' - 恢复到指定的stash_id对应的修改内容
  • git stash apply - 功能同git stash pop但不删除刚从栈中取出的这个stash
  • git stash drop - 删除最新的修改内容stash
  • git stash drop 'stash_id' - 根据stash_id删除栈记录
  • git stash clear - 清空stash栈(删除所有stash记录)

涉及到计算金额,PHP提供了两类方法。一类是使用floor()round()进行四舍五入,还有一类是以bc打头的一系列函数。

使用floor() 和 round()计算金额

1
2
3
4
5
6
7
8
9
10
11
//计算折扣
$value = '9.95';//折扣
$money = '39.555';//原始价格,当然到这一步的价格,一般都是小数点后两位的,此处保留三位,主要是为了对比
echo '原始价格:'. $money * ($value/10);
echo '<pre>';
echo '直接四舍五入:'.round($money * ($value / 10),2);
echo '<pre>';
echo '截取小数点后1位:'.floor($money * $value)/10;
echo '<pre>';
echo '截取小数点后2位'.floor($money * ($value / 10) * 100)/100;
die;

使用bc类函数计算金额

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
32
33
/**
* PHP精确计算 主要用于货币的计算用
* @param string $n1 第一个数
* @param string $symbol 计算符号 + - * / %
* @param string $n2 第二个数
* @param string $scale 精度 默认为小数点后两位
* @return string
*/
function price_calc($n1, $symbol, $n2, $scale = '2')
{
$res = "";
switch ($symbol) {
case "+"://加法
$res = bcadd($n1, $n2, $scale);
break;
case "-"://减法
$res = bcsub($n1, $n2, $scale);
break;
case "*"://乘法
$res = bcmul($n1, $n2, $scale);
break;
case "/"://除法
$res = bcdiv($n1, $n2, $scale);
break;
case "%"://求余、取模
$res = bcmod($n1, $n2, $scale);
break;
default:
$res = "";
break;
}
return $res;
}
0%