派大星的博客

很多事情不是会了才能做,而是做了才能学会


  • 首页

  • 标签

  • 分类

  • 关于

  • 搜索

windows配置go-micro开发环境

发表于 2019-04-24 | 分类于 go

1. 安装protobuf

下载protoc.exe

点击下载
下载protoc-3.8.0-rc-1-win64.zip

下载解压后,将路径配置到环境变量里。

安装插件

以go get 方式安装

安装protoc-gen-go

1
2
go get -v github.com/golang/protobuf/proto
go get -v github.com/golang/protobuf/protoc-gen-go

安装go-micro

1
go get -v github.com/micro/go-micro

2.编译proto

1
2
3
4
5
6
7
E://protoc/bin/protoc.exe 
--plugin=protoc-gen-go=F://goWork/bin/protoc-gen-go.exe
--proto_path=./
--go_out=./
--plugin=protoc-gen-micro=F://goWork/bin/protoc-gen-micro.exe
--micro_out=./
user.proto

3. 设置Consul注册中心

consul可以使用docker进行安装,可以看我之前的博客点击查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 注册中心
reg := consul.NewRegistry(func(op *registry.Options) {
op.Addrs = []string{
"127.0.0.1:8500",
}
})
//创建一个服务
service := micro.NewService(micro.Name("micro.service.user"),
micro.Registry(reg),
micro.RegisterTTL(time.Second*10), //10s检查等待时间
micro.RegisterInterval(time.Second*5), // 服务每5s发一次心跳
)
proto.RegisterUserServiceHandler(service.Server(), new(handler.User))
err := service.Run()
if err != nil {
log.Println(err.Error())
}

SprintBoot任意处获取Request对象

发表于 2019-04-21 | 分类于 springboot

老样子,直接上代码

方式一(粗暴,推荐)

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
package com.pibgstar.demo.utils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author pibigstar
* @desc 获取request和response对象
**/
public class WebUtil {

/** 获取request对象 **/
public static HttpServletRequest getRequest(){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null){
return null;
}
return ((ServletRequestAttributes)requestAttributes).getRequest();
}
/** 获取response对象 **/
public static HttpServletResponse getResponse(){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null){
return null;
}
return ((ServletRequestAttributes)requestAttributes).getResponse();
}
}

方式二(简单)

在你需要的地方,注入即可

1
2
3
4
@Autowired
HttpServletRequest request;
@Autowired
HttpServletResponse response

MySQL实战45讲学习笔记(8~15)

发表于 2019-04-21 | 分类于 面试,mysql

本文为极客时间《MySQL实战45讲》的总结笔记,如有侵权,请通知我,将立马删除。建议大家去购买这套课程,真的是物有所值。

9. 选择普通索引还是唯一索引?

9.1 查询过程

其实查询过程两者的时间差距是微乎其微的,普通索引要比唯一索引多一次判断下一条记录是否符合,但InnoDB 的数据是按数据页为单位来读写的,所以就算多读一次也占用不了多少时间

9.2 更新过程

普通索引可以使用change buffer,可以将一系列的更新写到change buffer中,后期再一次性写入到磁盘中,极大的提高了更新的效率,而唯一索引没有办法使用change buffer

9.3 change buffer 的使用场景

对于写多读少的业务来说,页面在写完以后马上被访问到的概率比小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。反过来,如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭change buffer

10. MySQL为什么有时候会选错索引?

Mysql是根据扫描行数来判断选择哪个索引,扫描行数越少,则越容易被选择,查看扫描行数可以通过explain关键字来查看。

1
explain select * from user where sex = 1

既然Mysql是根据扫描行数来选择索引的,它选错索引肯定也是因为在判断扫描行数的时候出了问题,那Mysql又是怎么来判断扫描行数的呢?答案是:采样统计
是的就是这么不靠谱的采样,Mysql官方也说了这个误差可能会达到30%~50%

对于这种情况我们可以使用analyze table 表名来重新统计索引信息达到让Mysql选择正确的索引。或者使用force index来强制给它指定一个索引

1
select * from user force index(sex) where sex = 1

11. 怎么给字符串字段加索引?

1. 利用前缀索引

如果字符串过长,而前面几个字段可以确定一个唯一值,比如邮箱,前面都是几位数字+@qq.com,我们不用给全部字段加上索引,而只需要索引前面几个数字即可,这样就极大的节省索引占的空间了。

1
alter table user add index index_email(email,9)

这个数字9怎么去确定呢,我们可以通过下面的语句,来尝试,如果查出来的值越少,就越好。

1
2
3
4
select count(distinct left(email,7)) as L7,
count(distinct left(email,8)) as L8,
count(distinct left(email,9)) as L9,
from user

2. 反转字符串

有的时候字段前面都是一样,而后面是不一样的,比如身份证号,这时就不好利用前缀索引了,不过我们可以将身份证的倒序存储,这样就巧妙的再次利用前缀索引的优势了。

1
select * from t where id_card = reverse('input_id_card');

3. 使用Hash

这种就是将字符串计算出一个hash值,然后给表新增一个字段将hash存储进去,下次查找时先将字符串换算为hash再去表中查找hash列,不过这种只适合等值查询,不能进行范围查询。

12. 为什么我的MySQL会“抖”一下?

当Mysql执行过程中会突然慢下来,过一会又好了,而且不是随机的,持续时间很短,看起来就好像Mysql“抖”了一下。这个过程其实是Mysql在刷”脏页”的过程。

当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“”脏页”,当把内存中的数据更新到硬盘之后这个页也就变成了“干净页”

大概下面四种情况会发生刷“脏页”过程:

  1. redo log 写满了。当redo log写满了之后,系统会停止所有更新操作,将redo log的checkpoint往前推进,让redo log可以留出空间继续写。
  2. 系统内存不足,淘汰脏页时。当需要新的内存页,而内存不足时就需要淘汰一些数据页空出内存给别的数据页使用,如果淘汰的是“脏页”,那么就需要把“脏页”数据写入到磁盘中。
  3. 当系统空闲时。当系统空闲时就会将一点一点的将脏页数据刷新到磁盘中
  4. 当Mysql正常关闭时。Mysql正常关闭时会将内存中的脏页数据全部刷新到磁盘中。

我们可以通过设置innodb_io_capacity参数来控制Mysql刷脏页的速度,如果你用的是SSD,那么建议你把这个参数设置大点。这个参数在information_schema数据库中的GLOBAL_VARIABLES表中设置。

13. 为什么表数据删掉一半,表文件大小不变?

当innodb_file_per_table的参数为OFF时,表的数据会放到共享内存中,也就是和数据字典放一块。而为ON时,表的数据存储在以.ibd为后缀的文件中,当我们使用drop table删除表时,会直接删除这个文件达到回收的目的,而如果数据是放到了共享内存中,那么即使表删除了,空间也是不会回收的。所以我们一般都将此参数设置为ON,MySQL5.5.6版本之后默认就是ON了。

13.1 删除流程

当我们删除某一行记录时,其实MySQL只是把此行记录标记为了“可复用”,但磁盘大小是不会变的,所以通过delete表中记录是达不到回收表空间的。这些被标记为“可复用”而没有使用的空间看起来就像是“空洞”,其实不止删除会造成空洞,插入一样可以,如果我们不是按顺序插入,而是随机插入,那么就可能造成页分裂,而之前那一页末尾可能还有未使用的空间。

13.2 怎么回收表空间

我们可以通过重建表来达到回收表空间,可以使用下面这个命令:

1
alter table 表名 engine = InnoDB

三种重建方式对比:

  1. recreate重建表

    1
    alter table 表名 engine = InnoDB
  2. 重新统计索引信息

    1
    analyze table 表名
  3. recreate + 重新统计索引信息

    1
    optimize table 表名

14. count(*)这么慢,我该怎么办?

以下的讨论都是没有where条件判断的,如果有条件判断,则不适用。

  • 对于MyISAM引擎,它会将一个表的总行数存储在磁盘中,所以它的count(*)效率很高
  • 而对于InnoID引擎,由于MVCC多版本并发控制,它必须一行一行的去读取然后计算总数。

执行速度比较

count(其他字段) < count(主键) < count(1) ≈ count(*)

解决方案

  1. 将总数存储在Redis中(不推荐,可能会导致数据不一致)
  2. 单独存储到MySQL一张表中(可使用事务,来避免数据不一致等情况)

15. 日志和索引相关问题

15.1 MySQL 怎么知道 binlog 是完整的?

答: 一个事务的 binlog 是有完整格式的

  • statement 格式的 binlog,最后会有 COMMIT;
  • row 格式的 binlog,最后会有一个 XID event

15.2 redo log 和 binlog 是怎么关联起来的?

答:它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log

  • 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
  • 如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。

15.3 处于 prepare 阶段的 redo log 加上完整 binlog,重启就能恢复,MySQL 为什么要这么设计?

答:其实,这个问题还是跟我们在反证法中说到的数据与备份的一致性有关。在时刻 B,也就是 binlog 写完以后 MySQL 发生崩溃,这时候 binlog 已经写入了,之后就会被从库(或者用这个 binlog 恢复出来的库)使用。所以,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。

15.4 能不能只用redo log,不要binlog?

回答:如果只从崩溃恢复的角度来讲是可以的。你可以把binlog关掉,这样就没有两阶段提交了,但系统依然是crash-safe的。
但是,如果你了解一下业界各个公司的使用场景的话,就会发现在正式的生产库上,binlog都是开着的。因为binlog有着redo log无法替代的功能。

  • 一个是归档。

    redo log是循环写,写到末尾是要回到开头继续写的。这样历史日志没法保留 ,redo log也就起不到归档的作用。

  • 一个就是MySQL系统依赖于binlog。

    binlog作为MySQL一开始就有的功能,被用在了很多地方。其中,MySQL系统高可用的基础,就是binlog复制。

  • 还有很多公司有异构系统(比如一些数据分析系统)

    这些系统就靠消费MySQL的binlog来更新自己的数据。关掉binlog的话,这些下游系统就没法输入了。

总之,由于现在包括MySQL+高可用在内的很多系统机制都依赖于binlog,所以“鸠占鹊巢”redo log还做不到。

Go语言入门(4)dep包管理

发表于 2019-04-21 | 分类于 go

1. 什么是dep?

dep和go,在一定程度上相当于maven之于Java,composer之于PHP,dep是go语言官方的一个包管理工具。
相比较go get而言,dep可以直接给引入的第三方包一个专门的目录,并且可以专门制定一个配置文件,控制go项目所引入的包,版本以及其他依赖关系。

dep这个项目放在golang官方的github中:https://github.com/golang/dep

2. 安装

  • Mac系统:

    1
    brew install dep
  • Linux系统

1
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
  • Windows系统
1
go get -u github.com/golang/dep/cmd/dep

3. 使用

3.1 初始化

在项目根目录下执行 dep init 即可完成初始化,此时会生成三个文件

  • vendor文件夹存放我们项目需要的包文件
  • Gopkg.lock文件
  • Gopkg.toml 文件是我们可以编辑的文件,通过编辑这个文件,并运行dep ensure的命令可以达到引入包的目的:

Gopkg.toml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 必需包
required = ["github.com/gin-gonic/gin"]
# 忽略包
#ignored = []没有可以不写
# 项目元数据
#[metadata]
# 约束条件
[[constraint]]
# name =
# 可选:版本
# version =
# 分支
# branch
# 修订
# revision
# 可选:指定来源
# source = "github.com/gin-gonic/gin"

3.2 导包

使用 dep ensure 即可引入当前项目所需要的包到vendor文件夹中

3.3 查看状态

使用 dep status命令查看状态

SpringBoot允许跨域访问

发表于 2019-04-21 | 分类于 springboot

当它请求的一个资源是从一个与它本身提供的第一个资源的不同的域名时,一个资源会发起一个跨域HTTP请求(Cross-site HTTP request)。

一般都是异步请求会有这个问题,比如:Ajax,XMLHttpRequest等

使用@Configuration(推荐)

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
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}

private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 1允许任何域名使用
corsConfiguration.addAllowedOrigin("*");
// 2允许任何头
corsConfiguration.addAllowedHeader("*");
// 3允许任何方法(post、get等)
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
}

使用Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import javax.servlet.*;  
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CorsFilter implements Filter {

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig) {}
public void destroy() {}
}

树莓派初始开机配置

发表于 2019-04-21 | 分类于 树莓派

1. 开启ssh

在 内存卡 的 /boot 目录下 新增 一个 ssh 文件夹即可

2. 设置WiFi连接

在 内存卡 的 /boot 目录下 新增 一个 wpa_supplicant.conf 文件,里面内容如下

1
2
3
4
5
6
7
8
9
country=CN
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
ssid="wifi名"
psk="wifi密码"
priority=1
}

3. 查看IP地址

登录到你路由器后台查看,一般路由器的后台地址都为192.168.1.1,密码一般是123456
设备名一般是raspberrypi

4. 修改密码

1
2
3
4
5
# 修改 pi 用户密码
sudo passwd pi

# 修改root密码
sudo passwd root

5. 设置时区

输入date命令看查看当前系统时间

1
2
3
# 打开时区设置
sudo dpkg-reconfigure tzdata
# 选择Asia ---> ShangHai

6. 安装vim

树莓派默认是 nano编辑器,用着不太爽,用 vim替换它

1
2
3
4
# 更新下软件源
sudo apt-get update
# 安装vim
sudo apt-get install -y vim

改下配置

1
sudo vim /etc/vim/vimrc

简单配一下

1
2
3
4
# 语法高亮
syntax on
# 显示行号
set nu

7. 安装oh-my-zsh

查看当前使用的shell

1
echo $SHELL

查看系统中所有shell

1
cat /etc/shells

安装zsh

1
sudo apt-get install zsh

修改配置

1
vim ~/.zshrc

添加

1
2
3
4
5
6
7
8
ZSH_THEME="blinks"
ENABLE_CORRECTION="true"
# 为zsh添加git和sudo插件
plugins=(git sudo)
alias ll='ls -all'
alias vi='vim'
alias ps='ps -A'
alias ifconfig='sudo ifconfig'

切换shell为zsh

1
chsh -s /bin/zsh

刷新设置

1
source ~/.zshrc

安装oh-my-zsh

1
sh -c "$(wget https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh -O -)"

8. 开启vnc远程桌面

1
sudo apt-get install tightvncserver

启动

1
tightvncserver :1

关闭

1
vncserver -kill :1

关于配置noVNC的,可以查看我之前的文章:https://blog.csdn.net/junmoxi/article/details/100977131

9. 设置内网穿透

用的是 ngrok 比较方便,这是教程可以参考:https://www.jianshu.com/p/8702f55d57e3

ngrok 官网: https://www.ngrok.cc

分布式Id自增生成器

发表于 2019-04-16 | 分类于 Java,java工具类

转载自:https://zhuanlan.zhihu.com/p/65095562

首先,需要确定全局唯一ID是整型还是字符串?

如果是字符串,那么现有的UUID就完全满足需求,不需要额外的工作。缺点是字符串作为ID占用空间大,索引效率比整型低。

如果采用整型作为ID,那么首先排除掉32位int类型,因为范围太小,必须使用64位long型。

采用整型作为ID时,如何生成自增、全局唯一且不重复的ID?

方案

方案一

利用数据库的自增ID,从1开始,基本可以做到连续递增。Oracle可以用SEQUENCE,MySQL可以用主键的AUTO_INCREMENT,虽然不能保证全局唯一,但每个表唯一,也基本满足需求。

数据库自增ID的缺点是数据在插入前,无法获得ID。数据在插入后,获取的ID虽然是唯一的,但一定要等到事务提交后,ID才算是有效的。有些双向引用的数据,不得不插入后再做一次更新,比较麻烦。

方案二

采用一个集中式ID生成器,它可以是Redis,也可以是ZooKeeper,也可以利用数据库的表记录最后分配的ID。这种方式最大的缺点是复杂性太高,需要严重依赖第三方服务,而且代码配置繁琐。一般来说,越是复杂的方案,越不可靠,并且测试越痛苦。

方案三

类似Twitter的Snowflake算法,它给每台机器分配一个唯一标识,然后通过时间戳+标识+自增实现全局唯一ID。这种方式好处在于ID生成算法完全是一个无状态机,无网络调用,高效可靠。缺点是如果唯一标识有重复,会造成ID冲突。

Snowflake算法采用41bit毫秒时间戳,加上10bit机器ID,加上12bit序列号,理论上最多支持1024台机器每秒生成4096000个序列号,对于Twitter的规模来说够用了。

但是对于绝大部分普通应用程序来说,根本不需要每秒超过400万的ID,机器数量也达不到1024台,所以,我们可以改进一下,使用更短的ID生成方式:

53bitID由32bit秒级时间戳+16bit自增+5bit机器标识组成,累积32台机器,每秒可以生成65万个序列号,核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static synchronized long nextId(long epochSecond) {
if (epochSecond < lastEpoch) {
// warning: clock is turn back:
logger.warn("clock is back: " + epochSecond + " from previous:" + lastEpoch);
epochSecond = lastEpoch;
}
if (lastEpoch != epochSecond) {
lastEpoch = epochSecond;
reset();
}
offset++;
long next = offset & MAX_NEXT;
if (next == 0) {
logger.warn("maximum id reached in 1 second in epoch: " + epochSecond);
return nextId(epochSecond + 1);
}
return generateId(epochSecond, next, SHARD_ID);
}

时间戳减去一个固定值,此方案最高可支持到2106年。

如果每秒65万个序列号不够怎么办?没关系,可以继续递增时间戳,向前“借”下一秒的65万个序列号。

同时还解决了时间回拨的问题。

机器标识采用简单的主机名方案,只要主机名符合host-1,host-2就可以自动提取机器标识,无需配置。

最后,为什么采用最多53位整型,而不是64位整型?这是因为考虑到大部分应用程序是Web应用,如果要和JavaScript打交道,由于JavaScript支持的最大整型就是53位,超过这个位数,JavaScript将丢失精度。因此,使用53位整数可以直接由JavaScript读取,而超过53位时,就必须转换成字符串才能保证JavaScript处理正确,这会给API接口带来额外的复杂度。

完整源码

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

package com.pibigstar.util;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 根据每一个机器的IP生成16位不重复id(53字节)
* @author pibigstar
*/
public final class IdUtil {

private static final Logger logger = LoggerFactory.getLogger(IdUtil.class);

private static final Pattern PATTERN_LONG_ID = Pattern.compile("^([0-9]{15})([0-9a-f]{32})([0-9a-f]{3})$");

private static final Pattern PATTERN_HOSTNAME = Pattern.compile("^.*\\D+([0-9]+)$");

private static final long OFFSET = LocalDate.of(2000, 1, 1).atStartOfDay(ZoneId.of("Z")).toEpochSecond();

private static final long MAX_NEXT = 0b11111_11111111_111L;

private static final long SHARD_ID = getServerIdAsLong();

private static long offset = 0;

private static long lastEpoch = 0;

/**
* 生成16位不重复id
* @return
*/
public static long nextId() {
return nextId(System.currentTimeMillis() / 1000);
}

private static synchronized long nextId(long epochSecond) {
if (epochSecond < lastEpoch) {
// warning: clock is turn back:
logger.warn("clock is back: " + epochSecond + " from previous:" + lastEpoch);
epochSecond = lastEpoch;
}
if (lastEpoch != epochSecond) {
lastEpoch = epochSecond;
reset();
}
offset++;
long next = offset & MAX_NEXT;
if (next == 0) {
logger.warn("maximum id reached in 1 second in epoch: " + epochSecond);
return nextId(epochSecond + 1);
}
return generateId(epochSecond, next, SHARD_ID);
}

private static void reset() {
offset = 0;
}

/**
* 生成id
* @param epochSecond
* @param next
* @param shardId
* @return
*/
private static long generateId(long epochSecond, long next, long shardId) {
return ((epochSecond - OFFSET) << 21) | (next << 5) | shardId;
}

/**
* 获取机器Id
* @return
*/
private static long getServerIdAsLong() {
try {
//获取主机名
String hostname = InetAddress.getLocalHost().getHostName();
Matcher matcher = PATTERN_HOSTNAME.matcher(hostname);
if (matcher.matches()) {
long n = Long.parseLong(matcher.group(1));
if (n >= 0 && n < 8) {
logger.info("detect server id from host name {}: {}.", hostname, n);
return n;
}
}
} catch (UnknownHostException e) {
logger.warn("unable to get host name. set server id = 0.");
}
return 0;
}

public static void main(String args[]){
for (int i=0; i < 100; i++){
System.out.println(nextId());
}
}
}

go语言发送微信小程序模板消息

发表于 2019-04-08 | 分类于 go,微信小程序

PS:开发微信的东西是真的心累,一大堆坑!文档写的乱七八糟的,找个东西都得半天。

为了发一条模板消息翻了无数个博客,很多都是把代码一放,其实代码这块很好弄,不就组装个数据调一下API吗,主要是前期工作。我把我遇到的坑给大家总结一下,希望后来人可以少走一些弯路。

微信发送模板消息文档:点击查看

  1. 超级大坑!发送接口问题

文档上是这个接口:

1
https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN

实际不是的!!!!!是下面这个,看见没,多了一个wxopen,如果你碰到 48001 返回码,看看是不是这出问题了。

1
https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
  1. 域名问题

    他们这个必须要配置合法域名,必须是https的,要有SSL证书的,在微信开发工具上可以看到你目前配置的合法域名

  1. 内网穿透问题

    因为formID必须要在真机上才可以获取,所以你最好设置一个内网穿透,让手机能访问到你本地的服务。

内网穿透工具可以去下面网站上下载,有免费的
https://www.ngrok.cc

  1. 证书问题
    在腾讯云上下载的证书是下面这个样子的:

这时你在go语言就不知道用哪个里面的证书了。。。我用的gofram框架,可以这样搞,
将Apache里面的

这两个东西,通过下面网站生成一个 .pem证书然后再用到go语言中就好使了。
https://www.myssl.cn/tools/merge-pem-cert.html
go语言中这样用

1
2
s.EnableHTTPS("https/ssl.pem", "https/3_pibigstar.com.key")
s.SetHTTPSPort(7777)

还有一个坑就是,端口不能是443,可能是我本机是Windows,把443端口屏蔽了,如果你一直出现404情况,换个端口!

大概就些坑,如果你碰到其他的坑可以给我留言,或关注我的微信公众号,希望可以帮到你。

放代码

go后端

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"

"github.com/pibigstar/go-todo/config"
"github.com/pibigstar/go-todo/constant"
"github.com/pibigstar/go-todo/models/db"
)

// 发送模板消息

var (
send_template_url = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=%s"
get_access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"
)

// SendTemplate 发送模板消息
func SendTemplate(msg *TemplateMsg) (*SendTemplateResponse, error) {
msg.Miniprogram.AppID = config.ServerConfig.Appid
accessToken, err := getAccessToken(msg.Touser)
if err != nil {
log.Error("获取accessToken失败")
return nil, err
}
url := fmt.Sprintf(send_template_url, accessToken.AccessToken)
data, err := json.Marshal(msg)
if err != nil {
log.Error("模板消息JSON格式错误", "err", err.Error())
return nil, err
}
client := http.Client{}
resp, err := client.Post(url, "application/json", bytes.NewBuffer(data))
if err != nil {
log.Error("网络错误,发送模板消息失败", "err", err.Error())
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
var templateResponse SendTemplateResponse
err = json.Unmarshal(body, &templateResponse)
if err != nil {
log.Error("解析responseBody错误", "err", err.Error())
return nil, err
}
return &templateResponse, nil
}

func getAccessToken(openID string) (*GetAccessTokenResponse, error) {
var accessTokenResponse GetAccessTokenResponse
// 先从redis中拿
accessToken, err := getAccessTokenFromRedis(openID)
if accessToken != "" && err == nil {
accessTokenResponse = GetAccessTokenResponse{AccessToken: accessToken}
log.Info("从redis中获取到access_token", "access_token", accessToken)
return &accessTokenResponse, nil
}
appID := config.ServerConfig.Appid
secret := config.ServerConfig.Secret
url := fmt.Sprintf(get_access_token_url, appID, secret)
client := http.Client{}
resp, err := client.Get(url)
if err != nil {
log.Error("获取access_toke网络异常", "err", err.Error())
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &accessTokenResponse)
if err != nil {
log.Error("解析AccessToken失败", "err", err.Error())
return nil, err
}
// 存到redis中
if _, err := setAccessTokenToRedis(openID, accessTokenResponse.AccessToken); err != nil {
log.Error("将access_token存储到redis中失败", "err", err.Error())
}
return &accessTokenResponse, nil
}

// 从redis中取access_token
func getAccessTokenFromRedis(openID string) (string, error) {
key := fmt.Sprintf(constant.Access_Token_Redis_Prefix, openID)
accessToken, err := db.Redis.Get(key).Result()
return accessToken, err
}

// 将access_token存储到redis中
func setAccessTokenToRedis(openID, token string) (string, error) {
key := fmt.Sprintf(constant.Access_Token_Redis_Prefix, openID)
b, err := db.Redis.Set(key, token, 7200*time.Second).Result()
return b, err
}

type TemplateMsg struct {
Touser string `json:"touser"` //接收者的OpenID
TemplateID string `json:"template_id"` //模板消息ID
FormID string `json:"form_id"`
URL string `json:"url"` //点击后跳转链接
Miniprogram Miniprogram `json:"miniprogram"` //点击跳转小程序
Data *TemplateData `json:"data"`
}
type Miniprogram struct {
AppID string `json:"appid"`
Pagepath string `json:"pagepath"`
}

type TemplateData struct {
First KeyWordData `json:"first,omitempty"`
Keyword1 KeyWordData `json:"keyword1,omitempty"`
Keyword2 KeyWordData `json:"keyword2,omitempty"`
Keyword3 KeyWordData `json:"keyword3,omitempty"`
Keyword4 KeyWordData `json:"keyword4,omitempty"`
Keyword5 KeyWordData `json:"keyword5,omitempty"`
}

type KeyWordData struct {
Value string `json:"value"`
Color string `json:"color,omitempty"`
}

type SendTemplateResponse struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
MsgID string `json:"msgid"`
}

type GetAccessTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}

controller例子

仅仅是个例子,供大家参考

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
func init() {
s := g.Server()
s.BindHandler("/send", sendTemplate)
}

func sendTemplate(r *ghttp.Request) {
templateMsg := &utils.TemplateMsg{}
tempData := &utils.TemplateData{}
tempData.First.Value = "测试模板消息"
tempData.Keyword1.Value = "大家记得买票啊"
tempData.Keyword2.Value = "马上就要放假了,大家记得买回家的票啊"
tempData.Keyword3.Value = "2018-12-30 15:59"
tempData.Keyword4.Value = "派大星"
tempData.Keyword5.Value = "记得按时完成"
templateMsg.Data = tempData
formID := r.GetString("formID")
log.Info("formID", "formID", formID)
templateMsg.FormID = formID
openID, _ := middleware.GetOpenID(r)
templateMsg.Touser = openID
templateMsg.TemplateID = constant.Tmeplate_Receive_Task_ID
response, err := utils.SendTemplate(templateMsg)
if err != nil {
fmt.Println("发送模板消息失败", err.Error())
}
r.Response.WriteJson(response)
}

小程序端

1
2
3
4
5
<view>
<form bindsubmit="templateSend" report-submit="true">
<button type='primary' formType="submit" size='mini'>发送模板消息</button>
</form>
</view>
1
2
3
4
5
6
7
8
9
10
templateSend: function (e) {
// 表单需设置report-submit="true"
var formId = e.detail.formId;
// 发送随机模板消息
util.apiRequest("send","get",{
formID: formId,
}).then(data => {
console.log(data)
})
}

SpringBoot整合dubbo

发表于 2019-04-05 | 分类于 springboot,Dubbo,SpringBoot技能大全

[TOC]

1. 前期准备

###1.1 服务器中安装好zookeeper

安装过程看我之前的博客 点击这里

###1.2 服务器中安装dubbo的控制端

  1. 下载dubbo-admin的war包 点击下载

  2. 解压,修改WEB-INF下的dubbo.properties,将dubbo.registry.address修改为你服务器地址,root用户的密码为pibigstar

  3. 启动tomcat

浏览器访问http://你服务器地址:tomcat启动端口号/dubbo-admin 查看是否能进入dubbo控制端
这里写图片描述

2. 构建dubbo服务提供者(先有提供者才能有消费者)

###2.1 项目结构

这里写图片描述

2.2 添加依赖

pom.xml

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
<!-- dubbo started -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.4.10</version>
<exclusions>
<exclusion>
<artifactId>spring</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!-- dubbo end -->

2.3 编写暴露的服务接口

接口

1
2
3
4
5
6
7
package com.pibigstar.dubbo.remote;

public interface TestService {

String sayHello(String name);

}

实现类

1
2
3
4
5
6
7
8
9
10
11
12
package com.pibigstar.dubbo.remote.impl;

import com.pibigstar.dubbo.remote.TestService;

public class TestServiceImpl implements TestService {

@Override
public String sayHello(String name) {
return "Hello " + name + "!";
}

}

2.4 编写配置文件provider.xml

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
<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!-- 服务提供方应用名,用于计算依赖关系 -->

<dubbo:application name="dubbo-provider" owner="dubbo-provider"/>

<!-- 定义 zookeeper 注册中心地址及协议 -->
<dubbo:registry protocol="zookeeper" address="139.199.64.253:2181" client="zkclient"/>

<!-- 定义 Dubbo 协议名称及使用的端口,dubbo 协议缺省端口为 20880,如果配置为 -1 或者没有配置 port,则会分配一个没有被占用的端口 -->
<dubbo:protocol name="dubbo" port="-1"/>

<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.pibigstar.dubbo.remote.TestService" ref="testService" timeout="10000"/>

<!-- 和本地 bean 一样实现服务 -->
<bean id="testService" class="com.pibigstar.dubbo.remote.impl.TestServiceImpl" />
</beans>

2.5 让SpringBoot启动加载配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.pibigstar;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

@SpringBootApplication
@ImportResource(value = {"classpath:provider.xml"})
public class DubboProviderApplication {

public static void main(String[] args) {
SpringApplication.run(DubboProviderApplication.class, args);
}
}

2.6 启动dubbo服务提供者

访问dubbo控制端
这里写图片描述

注意:这里有个大坑!!!!!!!!!!!
SpringBoot必须要有Controller,不然会自动退出
写一个空白的Controller上去。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.pibigstar.web;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

@RequestMapping("/say")
public String say() {
return "test";
}

}

3. 构建dubbo服务消费者

3.1 项目结构

这里写图片描述

3.2 添加依赖

pom.xml

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
34
35
36
37
<!-- dubbo-consumer Started -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.1.41</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.4.10</version>
<exclusions>
<exclusion>
<artifactId>spring</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!-- dubbo-consumer end -->

3.3 编写暴露的服务接口

1
2
3
4
5
6
package com.pibigstar.dubbo.remote;

public interface TestService {

String sayHello(String name);
}

3.4 编写Controller

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

package com.pibigstar.dubbo.consumer.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.alibaba.fastjson.JSONObject;
import com.pibigstar.dubbo.remote.TestService;

@RestController
public class TestController {

@Autowired
TestService testService;

@RequestMapping("/test/{name}")
public JSONObject testJson(@PathVariable("name") String name) {
JSONObject jsonObject = new JSONObject();

String testStr = testService.sayHello(name);
jsonObject.put("str", testStr);
return jsonObject;
}
}

注意:这里接口的包名一定要和你前面服务提供者的包名一样!!

3.5 编写配置文件consumers.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!-- 配置可参考 http://dubbo.io/User+Guide-zh.htm -->

<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="dubbo-consumer" owner="dubbo-consumer"/>

<!-- 定义 zookeeper 注册中心地址及协议 -->
<dubbo:registry protocol="zookeeper" address="139.199.64.253:2181" client="zkclient" />

<!-- 生成远程服务代理,可以和本地 bean 一样使用 testService(要和提供者名字一样) -->
<dubbo:reference id="testService" interface="com.pibigstar.dubbo.remote.TestService"/>

</beans>

3.6 加载配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.pibigstar;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

@SpringBootApplication
@ImportResource(value = {"classpath:consumers.xml"})
public class DubboConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(DubboConsumerApplication.class, args);
}
}

3.7 启动dubbo服务消费者

这里写图片描述

注意:一定要先启动服务提供者,不然无法启动消费者的

4 测试

浏览器访问 http://localhost:8082/test/pibigstar
这里写图片描述
我们看到已经调用了服务提供者的接口实现类

5 项目以所有工具下载

https://pan.baidu.com/s/1fsd1LMiZ6kpQui0zpTiMIg

Swagger自动生成接口文档

发表于 2019-04-03 | 分类于 Java,springboot,SpringBoot技能大全

1. 添加依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>

2. 配置Swagger

新建SwaggerConfig.java

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
34
35
36
37
38
39
40
41
42
43
44
package com.pibigstar.common.config;

import io.swagger.annotations.ApiOperation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
* Swagger配置
* @author pibigstar
*
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig{

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//只有加了ApiOperation注解的类,才生成接口文档
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
//包下的类,生成接口文档
//.apis(RequestHandlerSelectors.basePackage("com.pibigstar.web"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("VIP资源解析")
.description("VIP资源解析文档")
.termsOfServiceUrl("http://mxspvip.cn")
.version("1.5.0")
.build();
}
}

3. 使用

@Api

用在类上,说明该类的作用

1
2
3
@Controller
@Api(value="VIP视频播放",tags="视频播放接口")
public class ParseVIPController extends BaseController{ }

@ApiOperation

用在方法上,说明方法的作用

1
2
3
@RequestMapping(value="/vip",method=RequestMethod.GET)
@ApiOperation("VIP视频播放")
public ModelAndView play(){}

@ApiParam

用在参数中,说明参数的作用

1
2
3
public MyResponse seach(
@ApiParam(name = "type",value = "类型1:酷狗,2:QQ",required = true)String type,
@ApiParam(name = "music",value = "音乐名",required = true)String music){ }

@ApiImplicitParams

用在方法上,用来说明方法中参数的作用

1
2
@ApiImplicitParams(value = { @ApiImplicitParam(name="url",value="视频地址")})
public ModelAndView play(String url) { }

@ApiModel

描述一个Model的信息(这种一般用在post创建的时候,使用@RequestBody这样的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候)

1
2
3
4
5
6
7
8
9
10
11
@ApiModel(value="用户信息")   
public class User {
@ApiModelProperty("用户id")
private Integer userCode;
@ApiModelProperty("用户类型")
private String userType;
@ApiModelProperty("用户名称")
private String userName;
@ApiModelProperty("用户手机号")
private String mobileNumber;
}

@ApiResponses

用在方法上,用来表示一组响应信息

@ApiResponse

用在@ApiResponses中,一般用于表达一个错误的响应信息

code:数字,例如400

message:信息,例如”请求参数没填好”

response:抛出异常的类

4. 访问

http://localhost:8080/swagger-ui.html

1…8910…14
派大星

派大星

137 日志
64 分类
230 标签
GitHub CSDN
© 2019 派大星