开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java 并发编程:AQS 的自旋锁 自旋锁 为什么自旋 自

发表于 2020-12-24

互斥锁在AQS的互斥锁与共享锁中已经做了详细介绍,一个锁一次只能由一个线程持有,其它线程则无法获得,除非已持有锁的线程释放了该锁。这里为什么提互斥锁呢?其实互斥锁和自旋锁都是实现同步的方案,最终实现的效果都是相同的,但它们对未获得锁的线程的处理方式却是不同的。对于互斥锁,当某个线程占有锁后,另外一个线程将进入阻塞状态。与互斥锁类似,自旋锁保证了公共数据在任意时刻最多只能由一条线程获取使用,不同的是在获取锁失败后自旋锁会采取自旋的处理方式。

自旋锁

自旋锁是一种非阻塞锁,它的核心机制就在自旋两个字,即用自旋操作来替代阻塞操作。某一线程尝试获取某个锁时,如果该锁已经被另一个线程占用的话,则此线程将不断循环检查该锁是否被释放,而不是让此线程挂起或睡眠。一旦另外一个线程释放该锁后,此线程便能获得该锁。自旋是一种忙等待状态,过程中会一直消耗CPU的时间片。

为什么自旋

互斥锁有一个很大的缺点,即获取锁失败后线程会进入睡眠或阻塞状态,这个过程会涉及到用户态到内核态的调度,上下文切换的开销比较大。假如某个锁的锁定时间很短,此时如果锁获取失败则让它睡眠或阻塞的话则有点得不偿失,因为这种开销可能比自旋的开销更大。总结起来就是互斥锁更适合持有锁时间长的情况,而自旋锁更适合持有锁时间短的情况。

自旋锁特点

  • 自旋锁的核心机制就是死等,所有想要获得锁的线程都在不停尝试去获取锁,当然这也会引来竞争问题。
  • 与互斥锁一样,自旋锁也只允许一个线程获得锁。
  • 自旋锁能提供中断机制,因为它并不会进入阻塞状态,所以能很好支持中断。
  • 自旋锁适用于锁持有时间叫短的场景,即锁保护临界区很小的常见,这个很容易理解,如果持有锁太久,那么将可能导致大量线程都在自旋,浪费大量CPU资源。
  • 自旋锁无法保证公平性,不保证先到先获得锁,这样就可能造成线程饥饿。
  • 自旋锁需要保证各个本地缓存数据的一致性,在多处理器机器上,每个线程对应的处理器都对同一个变量进行读写。每次写操作都需要同步每个处理器缓存,这可能会影响性能。

自旋锁例子

下面看一个简单的自旋锁的实现,主要看lock和unlock两个方法,Unsafe仅仅是为操作提供了硬件级别的原子CAS操作。对于lock方法,假如有若干线程竞争,能成功通过CAS操作修改value值为newV的线程即是成功获取锁的线程。它将顺利通过,而其它线程则不断在循环检测value值是否改回0,将value改为0的操作就是获取锁的线程执行完后对该锁进行释放。对于unlock方法,用于释放锁,释放后若干线程又继续对该锁竞争。如此一来,没获得锁的线程也不会被挂起或阻塞,而是不断循环检查状态。

AQS的自旋机制

AQS框架中不管是互斥锁还是共享锁实现的基础思想都是基于自旋的机制,不过它对自旋锁做了优化,这个后面会继续讲解。比如下面两图为AQS框架获取独占锁和共享锁的逻辑,具体的逻辑我们先不用管,主要关注方框框住的for(;;)这行代码。这便是自旋操作,通过无限循环来实现自旋

Java 并发编程

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

jsp+servlet实战编写购物商城

发表于 2020-12-24

点赞再看,养成习惯

项目介绍

本项目使用jsp+servlet+mysql架构搭建美妆购物商城,主要分为用户端和商城端,用户端主要有首页展示,商品信息、购物车、订单和个人中心几个大的模块,每个模块都包含主要的电商功能;商户端主要是对商品的增删改查,对商品销量的统计,订单管理以及公告管理几个核心板块。

开发环境:

  1. jdk 8
  2. intellij idea
  3. tomcat 8
  4. mysql 5.7

所用技术:

  1. jsp+servlet
  2. js+ajax
  3. layui
  4. jdbc+C3P0

运行效果

  • 注册

注册

  • 用户信息

用户信息

  • 首页

首页

  • 商品列表

商品列表

  • 订单确认

订单确认

  • 后端-销售榜单

后端-销售榜单

重要代码:

  1. 登录,权限角色控制:
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
diff复制代码public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.获取登录页面输入的用户名与密码
String username = request.getParameter("username");
String password = request.getParameter("password");
// 2.调用service完成登录操作。
UserService service = new UserService();
try {
User user = service.login(username, password);
// 3.登录成功,将用户存储到session中.
request.getSession().setAttribute("user", user);
// 获取用户的角色,其中用户的角色分普通用户和超级用户两种
String role = user.getRole();
// 如果是超级用户,就进入到后台管理系统;否则进入我的账户页面
if ("超级用户".equals(role)) {
response.sendRedirect(request.getContextPath() + "/admin/login/home.jsp");
return;
} else {
response.sendRedirect(request.getContextPath() + "/client/myAccount.jsp");
return;
}
} catch (LoginException e) {
// 如果出现问题,将错误信息存储到request范围,并跳转回登录页面显示错误信息
e.printStackTrace();
request.setAttribute("register_message", e.getMessage());
request.getRequestDispatcher("/client/login.jsp").forward(request, response);
return;
}
}
// 退出登录,销毁用户缓存信息
public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
// 获取session对象.
HttpSession session = request.getSession();
// 销毁session
session.invalidate();
// flag标识
String flag = request.getParameter("flag");
// 重定向到首页
if (flag == null || flag.trim().isEmpty()) {
response.sendRedirect(request.getContextPath() + "/index.jsp");
}
}
  1. 购物车逻辑
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
diff复制代码// 后端数据组装
public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
// 1.获取美妆id
String id = request.getParameter("id");
// 2.根据id条件查询具体美妆参数信息
ProductService service = new ProductService();
try {
Product p = service.findProductById(id);
//3.将商品添加到购物车
//3.1获得session对象
HttpSession session = request.getSession();
//3.2从session中获取购物车对象
Map<Product, Integer> cart = (Map<Product, Integer>)session.getAttribute("cart");
//3.3如果购物车为null,说明没有商品存储在购物车中,创建出购物车
if (cart == null) {
cart = new HashMap<Product, Integer>();
}
//3.4向购物车中添加商品
Integer count = cart.put(p, 1);
//3.5如果商品数量不为空,则商品数量+1,否则添加新的商品信息
if (count != null) {
cart.put(p, count + 1);
}
session.setAttribute("cart", cart);
response.sendRedirect(request.getContextPath() + "/client/cart.jsp");
return;
} catch (FindProductByIdException e) {
e.printStackTrace();
}
}

//jsp 页面数据渲染
<c:forEach items="${cart}" var="entry" varStatus="vs">
<table width="100%" border="0" cellspacing="0">
<tr>
<td width="10%">${vs.count}</td>
<td width="30%">${entry.key.name }</td>
<td width="10%">${entry.key.price }</td>
<td width="20%">
<!-- 减少商品数量 -->
<input type="button" value='-' style="width:20px"
onclick="changeProductNum('${entry.value-1}','${entry.key.pnum}','${entry.key.id}')">
<!-- 商品数量显示 -->
<input name="text" type="text" value="${entry.value}" style="width:40px;text-align:center" />
<!-- 增加商品数量 -->
<input type="button" value='+' style="width:20px"
onclick="changeProductNum('${entry.value+1}','${entry.key.pnum}','${entry.key.id}')">
</td>
<td width="10%">${entry.key.pnum}</td>
<td width="10%">${entry.key.price*entry.value}</td>
<td width="10%">
<!-- 删除商品 -->
<a href="${pageContext.request.contextPath}/changeCart?id=${entry.key.id}&count=0"
style="color:#FF0000; font-weight:bold" onclick="javascript:return cart_del()">X</a>
</td>
</tr>
</table>
<c:set value="${total+entry.key.price*entry.value}" var="total" />
</c:forEach>
  1. 文字验证码生成
    文字验证码生成
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
ini复制代码public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
// 禁止缓存
// response.setHeader("Cache-Control", "no-cache");
// response.setHeader("Pragma", "no-cache");
// response.setDateHeader("Expires", -1);
int width = 180;
int height = 30;
// 步骤一 绘制一张内存中图片
BufferedImage bufferedImage = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
// 步骤二 图片绘制背景颜色 ---通过绘图对象
Graphics graphics = bufferedImage.getGraphics();// 得到画图对象 --- 画笔
// 绘制任何图形之前 都必须指定一个颜色
graphics.setColor(getRandColor(200, 250));
graphics.fillRect(0, 0, width, height);
// 步骤三 绘制边框
graphics.setColor(Color.WHITE);
graphics.drawRect(0, 0, width - 1, height - 1);
// 步骤四 四个随机数字
Graphics2D graphics2d = (Graphics2D) graphics;
// 设置输出字体
graphics2d.setFont(new Font("宋体", Font.BOLD, 18));
Random random = new Random();// 生成随机数
int index = random.nextInt(words.size());
String word = words.get(index-1);// 获得成语
// 定义x坐标
int x = 10;
for (int i = 0; i < word.length(); i++) {
// 随机颜色
graphics2d.setColor(new Color(20 + random.nextInt(110), 20 + random
.nextInt(110), 20 + random.nextInt(110)));
// 旋转 -30 --- 30度
int jiaodu = random.nextInt(60) - 30;
// 换算弧度
double theta = jiaodu * Math.PI / 180;
// 获得字母数字
char c = word.charAt(i);
// 将c 输出到图片
graphics2d.rotate(theta, x, 20);
graphics2d.drawString(String.valueOf(c), x, 20);
graphics2d.rotate(-theta, x, 20);
x += 40;
}
// 将验证码内容保存session
request.getSession().setAttribute("checkcode_session", word);
// 步骤五 绘制干扰线
graphics.setColor(getRandColor(160, 200));
int x1;
int x2;
int y1;
int y2;
for (int i = 0; i < 30; i++) {
x1 = random.nextInt(width);
x2 = random.nextInt(12);
y1 = random.nextInt(height);
y2 = random.nextInt(12);
graphics.drawLine(x1, y1, x1 + x2, x2 + y2);
}
// 将上面图片输出到浏览器 ImageIO
graphics.dispose();// 释放资源
ImageIO.write(bufferedImage, "jpg", response.getOutputStream());
}

项目总结

通过项目能学习一些java的基本数据渲染和读取,前后端数据传递除开request外也可以用session进行缓存交互,但是有点浪费缓存资源,不必要长久缓存数据可以及时清空
缓存,原生jsp+servlet组合框架也有很多不足,后期会改版成ssh或者ssm或者springboot版本的电商平台

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【对线面试官】Java注解

发表于 2020-12-24

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public void send(String userName) {
try {
// qps 上报
qps(params);
long startTime = System.currentTimeMillis();

// 构建上下文(模拟业务代码)
ProcessContext processContext = new ProcessContext();
UserModel userModel = new UserModel();
userModel.setAge("22");
userModel.setName(userName);
//...

// rt 上报
long endTime = System.currentTimeMillis();
rt(endTime - startTime);
} catch (Exception e) {

// 出错上报
error(params);
}
}

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
java复制代码@Around("@annotation(com.sanwai.service.openapi.monitor.Monitor)")
public Object antispan(ProceedingJoinPoint pjp) throws Throwable {

String functionName = pjp.getSignature().getName();
Map<String, String> tags = new HashMap<>();

logger.info(functionName);

tags.put("functionName", functionName);
tags.put("flag", "done");

monitor.sum(functionName, "start", 1);

//方法执行开始时间
long startTime = System.currentTimeMillis();

Object o = null;
try {
o = pjp.proceed();
} catch (Exception e) {
//方法执行结束时间
long endTime = System.currentTimeMillis();

tags.put("flag", "fail");
monitor.avg("rt", tags, endTime - startTime);

monitor.sum(functionName, "fail", 1);
throw e;
}

//方法执行结束时间
long endTime = System.currentTimeMillis();

monitor.avg("rt", tags, endTime - startTime);

if (null != o) {
monitor.sum(functionName, "done", 1);
}
return o;
}

文章以纯面试的角度去讲解,所以有很多的细节是未铺垫的。

比如说反射、.java文件到jvm的过程、AOP是什么等等等…这些在【Java3y】都有过详细的基本教程甚至电子书,我就不再详述了。

欢迎关注我的微信公众号【面试造火箭】来聊聊Java面试

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

未命名

发表于 2020-12-24

默认情况下,使用#{}语法,MyBatis会产生PreparedStatement语句中,并且安全的设置PreparedStatement参数,这个过程中MyBatis会进行必要的安全检查和转义。

示例1:

执行SQL:Select * from emp where name = #{employeeName}

参数:employeeName=>Smith

解析后执行的SQL:Select * from emp where name = ?

执行SQL:Select * from emp where name = ${employeeName}

参数:employeeName传入值为:Smith

解析后执行的SQL:Select * from emp where name =Smith

说明:

  1. #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。如:order by #{user_id},如果传入的值是111,那么解析成sql时的值为order by “111”, 如果传入的值是id,则解析成的sql为order by “id”.
  1. 将传入的数据直接显示生成在sql中。如:orderby将传入的数据直接显示生成在sql中。如:order by 将传入的数据直接显示生成在sql中。如:orderby{user_id},如果传入的值是111,那么解析成sql时的值为order by 111, 如果传入的值是id,则解析成的sql为order by id.

综上所述,{}方式会引发SQL注入的问题、同时也会影响SQL语句的预编译,所以从安全性和性能的角度出发,能使用#{}的情况下就不要使用{}。

${}在什么情况下使用呢?

有时候可能需要直接插入一个不做任何修改的字符串到SQL语句中。这时候应该使用${}语法。

比如,动态SQL中的字段名,如:ORDER BY ${columnName}

  1. Select * from emp where name = {employeeName} ORDER BY {columnName}

由于仅仅是简单的取值,所以以前sql注入的方法适用此处,如果我们orderby语句后用了{}仅仅是简单的取值,所以以前sql注入的方法适用此处,如果我们order by语句后用了仅仅是简单的取值,所以以前sql注入的方法适用此处,如果我们orderby语句后用了{},那么不做任何处理的时候是存在sql注入危险的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

COLA 40:直击应用架构本质的最佳实践

发表于 2020-12-23

小隐乐乐 进阶架构师

前言

每个架构师,对系统应用架构,都有自己的理解。在长期的技术实践中,出现了一堆应用架构产物。但是往往都是思想,没有实实在在的落地的产物。COLA 的横空出世,真正给应用架构落地,提供了优秀的实践。

COLA 的主要目的是为应用架构提供一套简单的可以复制、可以理解、可以落地、可以控制复杂性的”指导和约束”。

目前,COLA发展到4.0,进行一次重新梳理,回归初心,让COLA真正成为应用架构的最佳实践,帮助广大的业务技术同学,脱离酱缸代码的泥潭!

应用架构的本质

摘自官方概述。架构的意义就是要素结构,要素是组成架构的重要元素,结构是要素之间的关系。应用架构的意义就在于定义一套良好的结构,治理应用复杂度, 降低系统熵值,从随心所欲的混乱状态,走向紧紧有条的有序状态。

如下图所示,熵值能够表示事物的混乱程度。越整洁,熵值越低。

图片

好的组织架构会遵循一定的架构模式,大部分的组织都会按职能和业务来设计自己的架构。如果你反其道而行之,硬要把销售、财务和技术人员放在一个部门,就会显得很奇怪。

同样,好的应用架构,也遵循一些共同模式,不管是六边形架构、洋葱圈架构、整洁架构、还是COLA架构,都提倡以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度。

应用架构的本质,就是要从繁杂的业务系统中提炼出共性,找到解决业务问题的最佳共同模式,为开发人员提供统一的认知,治理混乱。帮助应用系统“从混乱到有序”,COLA架构就是为此而生,其核心职责就是定义良好的应用结构,提供最佳实践。

COLA 架构 - 分层结构

所有的复杂系统都会呈现出层级结构,应用系统处理复杂业务逻辑也应该是分层的,下层对上层屏蔽处理细节,每一层各司其职,分离关注点。

对于一个典型的业务应用系统来说,COLA会做如下层次定义,每一层都有明确的职责定义:

图片

1)适配层(Adapter Layer):负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller;

2)应用层(Application Layer):主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的,应用层也可以绕过领域层,直接访问基础实施层;

3)领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain Entity)的方法对App层提供业务实体和业务逻辑计算。领域是应用的核心,不依赖任何其他层次;

4)基础实施层(Infrastructure Layer):主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。此外,领域防腐的重任也落在这里,外部依赖需要通过gateway的转义处理,才能被上面的App层和Domain层使用。

COLA 架构 - 包结构

分层是属于大粒度的职责划分,太粗,我们有必要往下再down一层,细化到包结构的粒度,才能更好的指导我们的工作。

还是拿一堆玩具举例子,分层类似于拿来了一个架子,分包类似于在每一层架子上又放置了多个收纳盒。所谓的内聚,就是把功能类似的玩具放在一个盒子里,这样可以让应用结构清晰,极大的降低系统的认知成本和维护成本。

图片

COLA经过不断的迭代,形成的目前的结构:

图片

各个包结构的简要功能描述,如下表所示:

层次 包名 功能 必选
Adapter层 web 处理页面请求的Controller 否
Adapter层 wireless 处理无线端的适配 否
Adapter层 wap 处理wap端的适配 否
App层 executor 处理request,包括command和query 是
App层 consumer 处理外部message 否
App层 scheduler 处理定时任务 否
Domain层 model 领域模型 否
Domain层 ability 领域能力,包括DomainService 否
Domain层 gateway 领域网关,解耦利器 是
Infra层 gatewayimpl 网关实现 是
Infra层 mapper ibatis数据库映射 否
Infra层 config 配置信息 否
Client SDK api 服务对外透出的API 是
Client SDK dto 服务对外的DTO 是

考虑功能和领域两个维度包结构定义。按照领域和功能两个维度分包策略,最后呈现出来的,是如下图所示的顶层包节点是领域名称,领域之下,再按功能划分包结构。

图片

经过多次迭代,我们定义出了相对稳定、可靠的应用架构:COLA 4.0

图片

COLA Archetype

好的应用架构,都遵循一些共同模式,不管是六边形架构、洋葱圈架构、整洁架构、还是COLA架构,都提倡以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度等。

COLA架构区别于这些架构的地方,在于除了思想之外,还提供了可落地的工具和实践指导。

为了能够快速创建满足COLA架构的应用,提供了两个Archetype,在cola-archetypes下面。

  1. 一个是用来创建纯后端服务的archetype:cola-archetype-service。
  2. 一个是用来创建adapter和后端服务一体的web应用archetype:cola-archetype-web。

COLA Components

此外,还提供了一些非常有用的通用组件,这些组件可以帮助提升研发效率。

这些功能组件被收拢在cola-components下面。到目前为止,已经沉淀了以下组件:

组件名称 功能 版本 依赖
cola-component-dto 定义了DTO格式,包括分页 1.0.0 无
cola-component-exception 定义了异常格式, 主要有BizException和SysException 1.0.0 无
cola-component-statemachine 状态机组件 1.0.0 无
cola-component-domain-starter Spring托管的领域实体组件 1.0.0 无
cola-component-catchlog-starter 异常处理和日志组件 1.0.0 exception ,dto组件
cola-component-extension-starter 扩展点组件 1.0.0 无
cola-component-test-container 测试容器组件 1.0.0 无

如何使用COLA

开源地址:github.com/alibaba/COL…

  1. 安装 cola archetype

下载cola-archetypes下的源码到本地,然后本地运行mvn install安装。
2. ##### 安装 cola components

下载cola-components下的源码到本地,然后本地运行mvn install安装。
3. ##### 创建应用

1
ini复制代码mvn archetype:generate -DgroupId=com.alibaba.demo -DartifactId=demoWeb -Dversion=1.0.0-SNAPSHOT -Dpackage=com.alibaba.demo -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeGroupId=com.alibaba.cola -DarchetypeVersion=4.0.0

命令执行成功的话,会看到如下的应用代码结构:

注:也可以使用阿里云的应用生成器:start.aliyun.com/bootstrap.h… 生成cola应用。

图片
4. ##### 运行应用

首先在demoWeb目录下运行mvn install(如果不想运行测试,可以加上-DskipTests参数)。然后进入start目录,执行mvn spring-boot:run。运行成功的话,可以看到SpringBoot启动成功的界面。

生成的应用中,已经实现了一个简单的Rest请求,可以在浏览器中输入 http://localhost:8080/helloworld 进行测试。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【SpringBoot】微信点餐系统

发表于 2020-12-23

欢迎访问原文: 【SpringBoot】微信点餐系统项目总结微信特性模板消息,授权,支付和退款 Token 认证在卖家端登录管理系统用到 我在 aop 中已经屏蔽了,因为我没有微信开放平台的认证账号,……

欢迎访问原文:
【SpringBoot】微信点餐系统

微信特性

模板消息,授权,支付和退款

Token 认证

在卖家端登录管理系统用到
我在 aop 中已经屏蔽了,因为我没有微信开放平台的认证账号,无法登录
可以自行去 cn.chenhaoxiang.aspect.SellerAuthorizeAspect 将类上的注解放开

WebSocket 消息

在买家下订单后,对买家端有消息提示并播放音乐

Redis 缓存 + 分布式锁

Redis 的缓存的话,注意增删改更新缓存,否则会出现无法预知的后果
在这里,如果有商品的抢购活动,就可以使用到 Redis 的分布式锁了

我觉得该项目还有一些需要完善的地方
比如卖家端没有权限控制
比如应用没有独立,项目里面的商品,订单
比如哪天修改了商品的代码,会影响到订单的部分
应该把商品和订单拆分开来,作为两个独立的应用

在这个项目中学到了很多。
学到的最重要的不是一些知识点的学习,而是项目架构方面的学习,比如 DTO,比如工具类,比如 From, 前端表单数据提交的实体类,比如应用独立,前后端分离,分布式和集群等等。

在项目中使用了微信公众平台的账号和微信开放平台的账号。
需要自己去申请一些权限。
目前用到的权限有:
微信公众号的登录支付权限,消息推送权限。登录和消息推送可以在开发文档中使用测试账号。
至于支付权限,则需要你自己去找朋友借借账号了。
我是学习的廖师兄的视频进行的开发, 需要有支付权限测试的,可以看这篇文档:
github.com/Pay-Group/b…
还有微信开放平台的登录权限,这个也需要自己去认证或者找朋友借下了。

在这里微信公众号接入开发和微信开放平台接入开发就没有重复造轮子了。
分别使用了两个开源的 SDK。
链接如下
github.com/Wechat-Grou… 这个非常全,你看了就知道了
github.com/Pay-Group/b… 这个就是廖师兄开发的 SDK,支付使用的就是该 SDK

centos7 提倡的用法

cd /ets/systemd/system
到这个目录下,新建一个 AAA.service,可以把 AAA 设置为项目名的

vim AAA.service
文件内容 Start:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码[Unit]  
Description=AAA #描述
After=syslog.target network.target #依赖

[Service]
Type=simple

ExecStart=/usr/bin/java -jar /opt/javaapps/AAA.jar
#前面是java命令的绝对路径 后面是jar包的绝对路径
ExecStop=/bin/kill -15 $MAINPID

User=root
Group=root

[Install]
WantedBy=multi-user.target

文件结束 END

使用
systemctl start AAA 或者
systemctl start AAA.service
如果被改变了:
先运行 systemctl daemon-reload 再运行 systemctl start sell.service

停止服务:
systemctl stop AAA 或者
systemctl stop AAA.service

开机自启动:
systemctl enable AAA 或者
systemctl enable AAA.service

不想开机启动:
systemctl disable AAA 或者
systemctl disable AAA.service

在此感谢廖师兄分享的视频教程。

GITHUB 项目地址: 【点我进行访问】

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

vue强制页面回到顶部

发表于 2020-12-23

仅仅设置 document.body 可能会有兼容性问题,可如下设置

1
javascript复制代码document.body.scrollTop = document.documentElement.scrollTop = 0

documentElement 对应的是 html 标签,而 body 对应的是 body 标签。

页面具有 DTD( DTD(Document Type Definition),全称为文档类型定义),或者说指定了 DOCTYPE 时,使用 document.documentElement。

页面不具有 DTD,或者说没有指定了 DOCTYPE,时,使用 document.body。

在 IE 和 Firefox 中均是如此。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我用 go-zero 一周实现了一个中台系统,已开源!

发表于 2020-12-23

作者:Jack

最近发现golang社区里出了一个新星的微服务框架,来自好未来,光看这个名字,就很有奔头,之前,也只是玩过go-micro,其实真正的还没有在项目中运用过,只是觉得 微服务,grpc 这些很高大尚,还没有在项目中,真正的玩过,我看了一下官方提供的工具真的很好用,只需要定义好,舒适文件jia结构 都生成了,只需要关心业务,加上最近 有个投票的活动,加上最近这几年中台也比较火,所以决定玩一下,

开源地址: github.com/jackluo2012…

先聊聊中台架构思路吧:

img

中台的概念大概就是把一个一个的app 统一起来,反正我是这样理解的。

先聊用户服务吧,现在一个公司有很多的公众号,小程序,微信的,支付宝的,还有xxx xxx ,很多的平台,每次开发的时候,我们总是需要做用户登陆的服务,不停的复制代码,然后我们就在思考能不能有一套独立的用户服务,只需要告诉我你需要传个你要登陆的平台(比如微信),微信登陆,需要的是客户端返回给服务端一个code ,然后服务端拿着这个code去微信获取用户信息,反正大家都明白。

我们决定,将所有的信息 弄到 配置公共服务中去,里面在存,微信,支付宝,以及其它平台的 appid ,appkey,还有支付的appid,appkey,这样就写一套。


最后说说实现吧,整个就一个repo:

  • 网关,我们用的是: go-zero的Api服务
  • 其它它的是服务,我们就是用的go-zero的rpc服务

看下目录结构

img

整个项目完成,我一个人操刀, 写了1个来星期,我就实现了上面的中台系统。

datacenter-api服务

先看官方文档 www.yuque.com/tal-tech/go…

我们先把网关搭建起来

1
2
3
4
shell复制代码➜ blogs mkdir datacenter && cd datacenter
➜ datacenter go mod init datacenter
go: creating new go.mod: module datacenter
➜ datacenter

查看book目录:

1
2
3
4
5
go复制代码➜  datacenter tree
.
└── go.mod

0 directories, 1 file

创建api文件

1
2
3
4
5
6
erlang复制代码➜  datacenter goctl api -o datacenter.api
Done.
➜ datacenter tree
.
├── datacenter.api
└── go.mod

定义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
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
less复制代码info(
title: "中台系统"
desc: "中台系统"
author: "jackluo"
email: "net.webjoy@gmail.com"
)

// 获取 应用信息
type Beid struct {
Beid int64 `json:"beid"`
}
type Token struct{
Token string `json:"token"`
}
type WxTicket struct{
Ticket string `json:"ticket"`
}
type Application struct {
Sname string `json:"Sname"` //名称
Logo string `json:"logo"` // login
Isclose int64 `json:"isclose"` //是否关闭
Fullwebsite string `json:"fullwebsite"` // 全站名称
}
type SnsReq struct{
Beid
Ptyid int64 `json:"ptyid"` //对应平台
BackUrl string `json:"back_url"` //登陆返回的地址
}
type SnsResp struct{
Beid
Ptyid int64 `json:"ptyid"` //对应平台
Appid string `json:"appid"` //sns 平台的id
Title string `json:"title"` //名称
LoginUrl string `json:"login_url"` //微信登陆的地址
}

type WxShareResp struct {
Appid string `json:"appid"`
Timestamp int64 `json:"timestamp"`
Noncestr string `json:"noncestr"`
Signature string `json:"signature"`
}

@server(
group: common
)
service datacenter-api {
@doc(
summary: "获取站点的信息"
)
@handler votesVerification
get /MP_verify_NT04cqknJe0em3mT.txt (SnsReq) returns (SnsResp)

@handler appInfo
get /common/appinfo (Beid) returns (Application)

@doc(
summary: "获取站点的社交属性信息"
)
@handler snsInfo
post /common/snsinfo (SnsReq) returns (SnsResp)
// 获取分享的
@handler wxTicket
post /common/wx/ticket (SnsReq) returns (WxShareResp)

}

// 上传需要登陆
@server(
jwt: Auth
group: common
)
service datacenter-api {
@doc(
summary: "七牛上传凭证"
)
@handler qiuniuToken
post /common/qiuniu/token (Beid) returns (Token)
}

// 注册请求
type RegisterReq struct {
// TODO: add members here and delete this comment
Mobile string `json:"mobile"` // 基本一个手机号码就完事
Password string `json:"password"`
Smscode string `json:"smscode"` // 短信码
}
// 登陆请求
type LoginReq struct{
Mobile string `json:"mobile"`
Type int64 `json:"type"` // 1.密码登陆,2.短信登陆
Password string `json:"password"`
}
// 微信登陆
type WxLoginReq struct {
Beid int64 `json:"beid"` // 应用id
Code string `json:"code"` // 微信登陆密钥
Ptyid int64 `json:"ptyid"` // 对应平台
}

//返回用户信息
type UserReply struct {
Auid int64 `json:"auid"`
Uid int64 `json:"uid"`
Beid int64 `json:"beid"` // 应用id
Ptyid int64 `json:"ptyid"` // 对应平台
Username string `json:"username"`
Mobile string `json:"mobile"`
Nickname string `json:"nickname"`
Openid string `json:"openid"`
Avator string `json:"avator"`
JwtToken
}
// 返回APPUser
type AppUser struct{
Uid int64 `json:"uid"`
Auid int64 `json:"auid"`
Beid int64 `json:"beid"` // 应用id
Ptyid int64 `json:"ptyid"` // 对应平台
Nickname string `json:"nickname"`
Openid string `json:"openid"`
Avator string `json:"avator"`
}

type LoginAppUser struct{
Uid int64 `json:"uid"`
Auid int64 `json:"auid"`
Beid int64 `json:"beid"` // 应用id
Ptyid int64 `json:"ptyid"` // 对应平台
Nickname string `json:"nickname"`
Openid string `json:"openid"`
Avator string `json:"avator"`
JwtToken
}

type JwtToken struct {
AccessToken string `json:"access_token,omitempty"`
AccessExpire int64 `json:"access_expire,omitempty"`
RefreshAfter int64 `json:"refresh_after,omitempty"`
}

type UserReq struct{
Auid int64 `json:"auid"`
Uid int64 `json:"uid"`
Beid int64 `json:"beid"` // 应用id
Ptyid int64 `json:"ptyid"` // 对应平台
}

type Request {
Name string `path:"name,options=you|me"`
}
type Response {
Message string `json:"message"`
}

@server(
group: user
)
service user-api {
@handler ping
post /user/ping ()

@handler register
post /user/register (RegisterReq) returns (UserReply)

@handler login
post /user/login (LoginReq) returns (UserReply)

@handler wxlogin
post /user/wx/login (WxLoginReq) returns (LoginAppUser)

@handler code2Session
get /user/wx/login () returns (LoginAppUser)
}
@server(
jwt: Auth
group: user
middleware: Usercheck
)
service user-api {
@handler userInfo
get /user/dc/info (UserReq) returns (UserReply)
}

// 投票活动api
type Actid struct {
Actid int64 `json:"actid"` //活动id
}

type VoteReq struct {
Aeid int64 `json:"aeid"` // 作品id
Actid
}
type VoteResp struct {
VoteReq
Votecount int64 `json:"votecount"` //投票票数
Viewcount int64 `json:"viewcount"` //浏览数
}


// 活动返回的参数
type ActivityResp struct {
Actid int64 `json:"actid"`
Title string `json:"title"` //活动名称
Descr string `json:"descr"` //活动描述
StartDate int64 `json:"start_date"` //活动时间
EnrollDate int64 `json:"enroll_date"` //投票时间
EndDate int64 `json:"end_date"` //活动结束时间
Votecount int64 `json:"votecount"` //当前活动的总票数
Viewcount int64 `json:"viewcount"` //当前活动的总浏览数
Type int64 `json:"type"` //投票方式
Num int64 `json:"num"` //投票几票
}


//报名
type EnrollReq struct {
Actid
Name string `json:"name"` // 名称
Address string `json:"address"` //地址
Images []string `json:"images"` //作品图片
Descr string `json:"descr"` // 作品描述
}

// 作品返回
type EnrollResp struct {
Actid
Aeid int64 `json:"aeid"` // 作品id
Name string `json:"name"` // 名称
Address string `json:"address"` //地址
Images []string `json:"images"` //作品图片
Descr string `json:"descr"` // 作品描述
Votecount int64 `json:"votecount"` //当前活动的总票数
Viewcount int64 `json:"viewcount"` //当前活动的总浏览数

}

@server(
group: votes
)
service votes-api {
@doc(
summary: "获取活动的信息"
)
@handler activityInfo
get /votes/activity/info (Actid) returns (ActivityResp)
@doc(
summary: "活动访问+1"
)
@handler activityIcrView
get /votes/activity/view (Actid) returns (ActivityResp)
@doc(
summary: "获取报名的投票作品信息"
)
@handler enrollInfo
get /votes/enroll/info (VoteReq) returns (EnrollResp)
@doc(
summary: "获取报名的投票作品列表"
)
@handler enrollLists
get /votes/enroll/lists (Actid) returns(EnrollResp)
}

@server(
jwt: Auth
group: votes
middleware: Usercheck
)
service votes-api {
@doc(
summary: "投票"
)
@handler vote
post /votes/vote (VoteReq) returns (VoteResp)
@handler enroll
post /votes/enroll (EnrollReq) returns (EnrollResp)
}

上面基本上写就写的API及文档的思路

生成datacenter 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
go复制代码➜  datacenter goctl api go -api datacenter.api -dir .
Done.
➜ datacenter tree
.
├── datacenter.api
├── etc
│ └── datacenter-api.yaml
├── go.mod
├── internal
│ ├── config
│ │ └── config.go
│ ├── handler
│ │ ├── common
│ │ │ ├── appinfohandler.go
│ │ │ ├── qiuniutokenhandler.go
│ │ │ ├── snsinfohandler.go
│ │ │ ├── votesverificationhandler.go
│ │ │ └── wxtickethandler.go
│ │ ├── routes.go
│ │ ├── user
│ │ │ ├── code2sessionhandler.go
│ │ │ ├── loginhandler.go
│ │ │ ├── pinghandler.go
│ │ │ ├── registerhandler.go
│ │ │ ├── userinfohandler.go
│ │ │ └── wxloginhandler.go
│ │ └── votes
│ │ ├── activityicrviewhandler.go
│ │ ├── activityinfohandler.go
│ │ ├── enrollhandler.go
│ │ ├── enrollinfohandler.go
│ │ ├── enrolllistshandler.go
│ │ └── votehandler.go
│ ├── logic
│ │ ├── common
│ │ │ ├── appinfologic.go
│ │ │ ├── qiuniutokenlogic.go
│ │ │ ├── snsinfologic.go
│ │ │ ├── votesverificationlogic.go
│ │ │ └── wxticketlogic.go
│ │ ├── user
│ │ │ ├── code2sessionlogic.go
│ │ │ ├── loginlogic.go
│ │ │ ├── pinglogic.go
│ │ │ ├── registerlogic.go
│ │ │ ├── userinfologic.go
│ │ │ └── wxloginlogic.go
│ │ └── votes
│ │ ├── activityicrviewlogic.go
│ │ ├── activityinfologic.go
│ │ ├── enrollinfologic.go
│ │ ├── enrolllistslogic.go
│ │ ├── enrolllogic.go
│ │ └── votelogic.go
│ ├── middleware
│ │ └── usercheckmiddleware.go
│ ├── svc
│ │ └── servicecontext.go
│ └── types
│ └── types.go
└── datacenter.go

14 directories, 43 files

我们打开 etc/datacenter-api.yaml 把必要的配置信息加上

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
yaml复制代码Name: datacenter-api
Log:
Mode: console
Host: 0.0.0.0
Port: 8857
Auth:
AccessSecret: 你的jwtwon Secret
AccessExpire: 86400
CacheRedis:
- Host: 127.0.0.1:6379
Pass: 密码
Type: node
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
CommonRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: common.rpc
VotesRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: votes.rpc

上面的 UserRpc, CommonRpc ,还有 VotesRpc 这些我先写上,后面再来慢慢加。

我们先来写 CommonRpc 服务。

CommonRpc服务

新建项目目录

1
bash复制代码➜  datacenter mkdir -p common/rpc && cd common/rpc

直接就新建在了,datacenter目录中,因为common 里面,可能以后会不只会提供rpc服务,可能还有api的服务,所以又加了rpc目录

goctl创建模板

1
2
3
ini复制代码➜  rpc goctl rpc template -o=common.proto
➜ rpc ls
common.proto

往里面填入内容:

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
protobuf复制代码➜  rpc cat common.proto
syntax = "proto3";

package common;


message BaseAppReq{
int64 beid=1;
}

message BaseAppResp{
int64 beid=1;
string logo=2;
string sname=3;
int64 isclose=4;
string fullwebsite=5;
}

// 请求的api
message AppConfigReq {
int64 beid=1;
int64 ptyid=2;
}

// 返回的值
message AppConfigResp {
int64 id=1;
int64 beid=2;
int64 ptyid=3;
string appid=4;
string appsecret=5;
string title=6;
}

service Common {
rpc GetAppConfig(AppConfigReq) returns(AppConfigResp);
rpc GetBaseApp(BaseAppReq) returns(BaseAppResp);
}

gotcl生成rpc服务

1
2
3
bash复制代码➜  rpc goctl rpc proto -src common.proto -dir .
protoc -I=/Users/jackluo/works/blogs/datacenter/common/rpc common.proto --go_out=plugins=grpc:/Users/jackluo/works/blogs/datacenter/common/rpc/common
Done.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码➜ rpc tree
.
├── common
│ └── common.pb.go
├── common.go
├── common.proto
├── commonclient
│ └── common.go
├── etc
│ └── common.yaml
└── internal
├── config
│ └── config.go
├── logic
│ ├── getappconfiglogic.go
│ └── getbaseapplogic.go
├── server
│ └── commonserver.go
└── svc
└── servicecontext.go

8 directories, 10 files

基本上,就把所有的目录规范和结构的东西都生成了,就不用纠结项目目录了,怎么放了,怎么组织了。

看一下,配置信息,里面可以写入mysql和其它redis的信息:

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码Name: common.rpc
ListenOn: 127.0.0.1:8081
Mysql:
DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: 127.0.0.1:6379
Pass:
Type: node
Etcd:
Hosts:
- 127.0.0.1:2379
Key: common.rpc

我们再来加上数据库服务:

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
go复制代码➜  rpc cd ..
➜ common ls
rpc
➜ common pwd
/Users/jackluo/works/blogs/datacenter/common
➜ common goctl model mysql datasource -url="root:admin@tcp(127.0.0.1:3306)/datacenter" -table="base_app" -dir ./model -c
Done.
➜ common tree
.
├── model
│ ├── baseappmodel.go
│ └── vars.go
└── rpc
├── common
│ └── common.pb.go
├── common.go
├── common.proto
├── commonclient
│ └── common.go
├── etc
│ └── common.yaml
└── internal
├── config
│ └── config.go
├── logic
│ ├── getappconfiglogic.go
│ └── getbaseapplogic.go
├── server
│ └── commonserver.go
└── svc
└── servicecontext.go

10 directories, 12 files

这样基本的一个 rpc 就写完了,然后我们将rpc 和model 还有api串连起来,这个官方的文档已经很详细了,这里就只是贴一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码➜  common cat rpc/internal/config/config.go
package config

import (
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/zrpc"
)

type Config struct {
zrpc.RpcServerConf
Mysql struct {
DataSource string
}
CacheRedis cache.ClusterConf
}

再在svc中修改:

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
go复制代码➜  common cat rpc/internal/svc/servicecontext.go
package svc

import (
"datacenter/common/model"
"datacenter/common/rpc/internal/config"

"github.com/tal-tech/go-zero/core/stores/sqlx"
)

type ServiceContext struct {
c config.Config
AppConfigModel model.AppConfigModel
BaseAppModel model.BaseAppModel
}

func NewServiceContext(c config.Config) *ServiceContext {
conn := sqlx.NewMysql(c.Mysql.DataSource)
apm := model.NewAppConfigModel(conn, c.CacheRedis)
bam := model.NewBaseAppModel(conn, c.CacheRedis)
return &ServiceContext{
c: c,
AppConfigModel: apm,
BaseAppModel: bam,
}
}

上面的代码已经将 rpc 和 model 数据库关联起来了,我们现在再将 rpc 和 api 关联起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码➜  datacenter cat internal/config/config.go

package config

import (
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/rest"
"github.com/tal-tech/go-zero/zrpc"
)

type Config struct {
rest.RestConf

Auth struct {
AccessSecret string
AccessExpire int64
}
UserRpc zrpc.RpcClientConf
CommonRpc zrpc.RpcClientConf
VotesRpc zrpc.RpcClientConf

CacheRedis cache.ClusterConf
}

加入 svc 服务中:

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
go复制代码➜  datacenter cat internal/svc/servicecontext.go
package svc

import (
"context"
"datacenter/common/rpc/commonclient"
"datacenter/internal/config"
"datacenter/internal/middleware"
"datacenter/shared"
"datacenter/user/rpc/userclient"
"datacenter/votes/rpc/votesclient"
"fmt"
"net/http"
"time"

"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/stores/cache"
"github.com/tal-tech/go-zero/core/stores/redis"
"github.com/tal-tech/go-zero/core/syncx"
"github.com/tal-tech/go-zero/rest"
"github.com/tal-tech/go-zero/zrpc"
"google.golang.org/grpc"
)

type ServiceContext struct {
Config config.Config
GreetMiddleware1 rest.Middleware
GreetMiddleware2 rest.Middleware
Usercheck rest.Middleware
UserRpc userclient.User //用户
CommonRpc commonclient.Common
VotesRpc votesclient.Votes
Cache cache.Cache
RedisConn *redis.Redis
}

func timeInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
stime := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
if err != nil {
return err
}

fmt.Printf("调用 %s 方法 耗时: %v\n", method, time.Now().Sub(stime))
return nil
}
func NewServiceContext(c config.Config) *ServiceContext {

ur := userclient.NewUser(zrpc.MustNewClient(c.UserRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor)))
cr := commonclient.NewCommon(zrpc.MustNewClient(c.CommonRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor)))
vr := votesclient.NewVotes(zrpc.MustNewClient(c.VotesRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor)))
//缓存
ca := cache.NewCache(c.CacheRedis, syncx.NewSharedCalls(), cache.NewCacheStat("dc"), shared.ErrNotFound)
rcon := redis.NewRedis(c.CacheRedis[0].Host, c.CacheRedis[0].Type, c.CacheRedis[0].Pass)
return &ServiceContext{
Config: c,
GreetMiddleware1: greetMiddleware1,
GreetMiddleware2: greetMiddleware2,
Usercheck: middleware.NewUserCheckMiddleware().Handle,
UserRpc: ur,
CommonRpc: cr,
VotesRpc: vr,
Cache: ca,
RedisConn: rcon,
}
}

这样基本上,我们就可以在 logic 的文件目录中调用了:

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
go复制代码cat internal/logic/common/appinfologic.go

package logic

import (
"context"

"datacenter/internal/svc"
"datacenter/internal/types"
"datacenter/shared"

"datacenter/common/model"
"datacenter/common/rpc/common"

"github.com/tal-tech/go-zero/core/logx"
)

type AppInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}

func NewAppInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) AppInfoLogic {
return AppInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}

func (l *AppInfoLogic) AppInfo(req types.Beid) (appconfig *common.BaseAppResp, err error) {

//检查 缓存中是否有值
err = l.svcCtx.Cache.GetCache(model.GetcacheBaseAppIdPrefix(req.Beid), appconfig)
if err != nil && err == shared.ErrNotFound {
appconfig, err = l.svcCtx.CommonRpc.GetBaseApp(l.ctx, &common.BaseAppReq{
Beid: req.Beid,
})
if err != nil {
return
}
err = l.svcCtx.Cache.SetCache(model.GetcacheBaseAppIdPrefix(req.Beid), appconfig)
}

return
}

这样,基本就连接起来了,其它基本上就不用改了,UserRPC, VotesRPC 类似,这里就不在写了。

使用心得

go-zero 的确香,因为它有一个 goctl 的工具,他可以自动的把代码结构全部的生成好,我们就不再去纠结,目录结构 ,怎么组织,没有个好几年的架构能力是不好实现的,有什么规范那些,并发,熔断,完全不用,考滤其它的,专心的实现业务就好,像微服务,还要有服务发现,一系列的东西,都不用关心,因为 go-zero 内部已经实现了。

我写代码也写了有10多年了,之前一直用的 php,比较出名的就 laravel,thinkphp,基本上就是模块化的,像微服那些实现直来真的有成本,但是你用上go-zero,你就像调api接口一样简单的开发,其它什么服务发现,那些根本就不用关注了,只需要关注业务。

一个好的语言,框架,他们的底层思维,永远都是效率高,不加班的思想,我相信go-zero会提高你和你团队或是公司的效率。go-zero的作者说,他们有个团队专门整理go-zero框架,目的也应该很明显,那就是提高,他们自己的开发效率,流程化,标准化,是提高工作效率的准则,像我们平时遇到了问题,或是遇到了bug,我第一个想到的不是怎么去解决我的bug,而是在想我的流程是不是有问题,我的哪个流程会导致bug,最后我相信 go-zero 能成为 微服务开发 的首选框架。

最后说说遇到的坑吧:

  • grpc

grpc 本人第一次用,然后就遇到了,有些字符为空时,字段值不显示的问题:

通过 grpc 官方库中的 jsonpb 来实现,官方在它的设定中有一个结构体用来实现 protoc buffer 转换为JSON结构,并可以根据字段来配置转换的要求。

  • 跨域问题

go-zero 中设置了,感觉没有效果,大佬说通过nginx 设置,后面发现还是不行,最近强行弄到了一个域名下,后面有时间再解决。

  • sqlx

go-zero 的 sqlx 问题,这个真的费了很长的时间:

time.Time 这个数据结构,数据库中用的是 timestamp 这个 比如我的字段 是delete_at 默认数库设置的是null ,结果插入的时候,就报了 Incorrect datetime value: '0000-00-00' for column 'deleted_at' at row 1"} 这个错,查询的时候报 deleted_at\": unsupported Scan, storing driver.Value type \u003cnil\u003e into type *time.Time"

后面果断去掉了这个字段,字段上面加上 .omitempty 这个标签,好像也有用,db:".omitempty"

其次就是这个 Conversion from collation utf8_general_ci into utf8mb4_unicode_ci,这个导致的大概原因是,现在都喜欢用emj表情了,mysql数据识别不了。

  • 数据连接

mysql 这边照样按照原始的方式,将配置文件修改编码格式,重新创建数据库,并且设置数据库编码为utf8mb4,排序规则为 utf8mb4_unicode_ci。

这样的话,所有的表还有string字段都是这个编码格式,如果不想所有的都是,可以单独设置,这个不是重点.因为在navicat上都好设置,手动点一下就行了。

重点来了:golang中使用的是 github.com/go-sql-driver/mysql 驱动,将连接 mysql的 dsn(因为我这使用的是gorm,所以dsn可能跟原生的格式不太一样,不过没关系, 只需要关注 charset 和 collation 就行了)

root:password@/name?parseTime=True&loc=Local&charset=utf8 修改为:
root:password@/name?parseTime=True&loc=Local&charset=utf8mb4&collation=utf8mb4_unicode_ci

go-zero 项目地址

github.com/tal-tech/go…

欢迎点赞👍

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

人机检验解决方案之-滑动验证码

发表于 2020-12-23

使用java + selenium + OpenCV破解腾讯防水墙滑动验证码

**腾讯防水墙:

1
2
3
4
markdown复制代码* 验证码地址:https://007.qq.com/online.html
* 使用OpenCv模板匹配
* 成功率90%左右
* Java + Selenium + OpenCV

产品样例

腾讯防水墙

来吧!展示!

结果展示

注意!!!

1
复制代码· 在模拟滑动时不能按照相同速度或者过快的速度滑动,需要向人滑动时一样先快后慢,这样才不容易被识别。

模拟滑动代码↓↓↓

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
java复制代码/**
* 模拟人工移动
* @param driver
* @param element页面滑块
* @param distance需要移动距离
*/
public static void move(WebDriver driver, WebElement element, int distance) throws InterruptedException {
int randomTime = 0;
if (distance > 90) {
randomTime = 250;
} else if (distance > 80 && distance <= 90) {
randomTime = 150;
}
List<Integer> track = getMoveTrack(distance - 2);
int moveY = 1;
try {
Actions actions = new Actions(driver);
actions.clickAndHold(element).perform();
Thread.sleep(200);
for (int i = 0; i < track.size(); i++) {
actions.moveByOffset(track.get(i), moveY).perform();
Thread.sleep(new Random().nextInt(300) + randomTime);
}
Thread.sleep(200);
actions.release(element).perform();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据距离获取滑动轨迹
* @param distance需要移动的距离
* @return
*/
public static List<Integer> getMoveTrack(int distance) {
List<Integer> track = new ArrayList<>();// 移动轨迹
Random random = new Random();
int current = 0;// 已经移动的距离
int mid = (int) distance * 4 / 5;// 减速阈值
int a = 0;
int move = 0;// 每次循环移动的距离
while (true) {
a = random.nextInt(10);
if (current <= mid) {
move += a;// 不断加速
} else {
move -= a;
}
if ((current + move) < distance) {
track.add(move);
} else {
track.add(distance - current);
break;
}
current += move;
}
return track;
}

看操作,no bb,直接上代码

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
java复制代码private final String INDEX_URL = "https://007.qq.com/online.html?ADTAG=index.head";
private void seleniumTest() {
ChromeDriverManager manager = ChromeDriverManager.getInstance();
int status = -1;
try {
WebDriver driver = manager.getDriver();
driver.get(INDEX_URL);
driver.manage().window().maximize(); // 设置浏览器窗口最大化
Thread.sleep(10000);
driver.findElement(By.className("wp-onb-tit")).findElements(By.tagName("a")).get(1).click();
Thread.sleep(500);
// 点击出现滑动图
waitWebElement(driver, By.id("code"), 500).click();
Thread.sleep(100);
// 获取到验证区域
driver.switchTo().frame(waitWebElement(driver, By.id("tcaptcha_iframe"), 500));
Thread.sleep(100);
// 获取滑动按钮
WebElement moveElemet = waitWebElement(driver, By.id("tcaptcha_drag_button"), 500);
Thread.sleep(100);
// 获取带阴影的背景图
String bgUrl = waitWebElement(driver, By.id("slideBg"), 500).getAttribute("src");
Thread.sleep(100);
// 获取带阴影的小图
String sUrl = waitWebElement(driver, By.id("slideBlock"), 500).getAttribute("src");
Thread.sleep(100);
// 获取高度
String topStr = waitWebElement(driver, By.id("slideBlock"), 500).getAttribute("style").substring(32, 36);
int top = Integer.parseInt(topStr.substring(0, topStr.indexOf("p"))) * 2;
Thread.sleep(100);
// 计算移动距离
int distance = (int) Double.parseDouble(getTencentDistance(bgUrl, sUrl, top));
// 滑动
move(driver, moveElemet, distance);
Thread.sleep(5000);

} catch (Exception e) {
e.printStackTrace();
} finally {
manager.closeDriver(status);
}
}

/**
* 获取腾讯验证滑动距离
*
* @return
*/
public static String dllPath = "C://chrome//opencv_java440.dll";

public String getTencentDistance(String bUrl, String sUrl, int top) {
System.load(dllPath);
File bFile = new File("C:/qq_b.jpg");
File sFile = new File("C:/qq_s.jpg");
try {
FileUtils.copyURLToFile(new URL(bUrl), bFile);
FileUtils.copyURLToFile(new URL(sUrl), sFile);
BufferedImage bgBI = ImageIO.read(bFile);
BufferedImage sBI = ImageIO.read(sFile);
// 裁剪
bgBI = bgBI.getSubimage(360, top, bgBI.getWidth() - 370, sBI.getHeight());
ImageIO.write(bgBI, "png", bFile);
Mat s_mat = Imgcodecs.imread(sFile.getPath());
Mat b_mat = Imgcodecs.imread(bFile.getPath());
// 转灰度图像
Mat s_newMat = new Mat();
Imgproc.cvtColor(s_mat, s_newMat, Imgproc.COLOR_BGR2GRAY);
// 二值化图像
binaryzation(s_newMat);
Imgcodecs.imwrite(sFile.getPath(), s_newMat);

int result_rows = b_mat.rows() - s_mat.rows() + 1;
int result_cols = b_mat.cols() - s_mat.cols() + 1;
Mat g_result = new Mat(result_rows, result_cols, CvType.CV_32FC1);
Imgproc.matchTemplate(b_mat, s_mat, g_result, Imgproc.TM_SQDIFF); // 归一化平方差匹配法
// 归一化相关匹配法
Core.normalize(g_result, g_result, 0, 1, Core.NORM_MINMAX, -1, new Mat());
Point matchLocation = new Point();
MinMaxLocResult mmlr = Core.minMaxLoc(g_result);
matchLocation = mmlr.maxLoc; // 此处使用maxLoc还是minLoc取决于使用的匹配算法
Imgproc.rectangle(b_mat, matchLocation,
new Point(matchLocation.x + s_mat.cols(), matchLocation.y + s_mat.rows()), new Scalar(0, 0, 0, 0));
return "" + ((matchLocation.x + s_mat.cols() + 360 - sBI.getWidth() - 46) / 2);
} catch (Throwable e) {
e.printStackTrace();
return null;
} finally {
bFile.delete();
sFile.delete();
}
}
/**
*
* @param mat
* 二值化图像
*/
public static void binaryzation(Mat mat) {
int BLACK = 0;
int WHITE = 255;
int ucThre = 0, ucThre_new = 127;
int nBack_count, nData_count;
int nBack_sum, nData_sum;
int nValue;
int i, j;
int width = mat.width(), height = mat.height();
// 寻找最佳的阙值
while (ucThre != ucThre_new) {
nBack_sum = nData_sum = 0;
nBack_count = nData_count = 0;

for (j = 0; j < height; ++j) {
for (i = 0; i < width; i++) {
nValue = (int) mat.get(j, i)[0];

if (nValue > ucThre_new) {
nBack_sum += nValue;
nBack_count++;
} else {
nData_sum += nValue;
nData_count++;
}
}
}
nBack_sum = nBack_sum / nBack_count;
nData_sum = nData_sum / nData_count;
ucThre = ucThre_new;
ucThre_new = (nBack_sum + nData_sum) / 2;
}
// 二值化处理
int nBlack = 0;
int nWhite = 0;
for (j = 0; j < height; ++j) {
for (i = 0; i < width; ++i) {
nValue = (int) mat.get(j, i)[0];
if (nValue > ucThre_new) {
mat.put(j, i, WHITE);
nWhite++;
} else {
mat.put(j, i, BLACK);
nBlack++;
}
}
}
// 确保白底黑字
if (nBlack > nWhite) {
for (j = 0; j < height; ++j) {
for (i = 0; i < width; ++i) {
nValue = (int) (mat.get(j, i)[0]);
if (nValue == 0) {
mat.put(j, i, WHITE);
} else {
mat.put(j, i, BLACK);
}
}
}
}
}
// 延时加载
private static WebElement waitWebElement(WebDriver driver, By by, int count) throws Exception {
WebElement webElement = null;
boolean isWait = false;
for (int k = 0; k < count; k++) {
try {
webElement = driver.findElement(by);
if (isWait)
System.out.println(" ok!");
return webElement;
} catch (org.openqa.selenium.NoSuchElementException ex) {
isWait = true;
if (k == 0)
System.out.print("waitWebElement(" + by.toString() + ")");
else
System.out.print(".");
Thread.sleep(50);
}
}
if (isWait)
System.out.println(" outTime!");
return null;
}

五、结果分析

目标:

识别拼图位置,推算出对应滑动距离,模拟滑动。

实现思路:

1.抓取图片

2.灰度化,二值化图像

3.使用opencv模糊匹配算法进行匹配检测

4.通过检测结果推算滑动距离

5.根据推算距离模拟滑动

检测耗时:

15 - 100毫秒

通过率:

=95%

最终测试结果为300条样本结果,这个样本数还是偏少了,不确定在更多的测试条数时还会不会达到这样的效果,应该不会差太远哈。

六、结语

这篇文章到这里就结束了,感谢大佬们驻足观看,大佬们点个关注、点个赞呗~

谢谢大佬~
在这里插入图片描述


作者:香芋味的猫丶

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从用户输入手机验证码开始

发表于 2020-12-22

image

验证用户的有效性或者安全性,是每个系统必备的安全措施,在移动端优先的时代,利用手机验证码来验证用户,算是安全系数比较高的手段。放眼当下几乎所有的互联网应用几乎都开放了手机验证码登录,而且应用内的敏感操作都需要手机验证码或者指纹,甚至面部识别来确定当前操作人的权限。

抛开其他端,单就移动端App方式而言,如果用户频繁进行敏感操作,需要频繁发送验证码,其实在用户体验上并不友好,况且短信费用也随之增加。就App形式而言,验证一个用户的有效性其实可以演变为验证设备的有效性,即:当前人在当前设备上是否可信。

image

以下讨论只针对非Web(浏览器)环境,Web环境其实也可以根据浏览器的信息来生成一个类似设备标示的代码

很多系统在设计之初,就已经考虑到安全主设备的概念,就像微信,如果在同一个手机上打开是不需要每次都进行登录操作的。进行设备验证是每个安全系统比较重要的部分,推荐在系统设计之初就要考虑。回归正题,对于很多行业来说,用户在App内频繁进行一些敏感操作是很正常的,比如我所在的在线教育行业,老师会很频繁的在一个班级内添加学生和老师(我们认为这些操作属于敏感操作)。如果每次都需要老师发送验证码来进行操作,那交互上真的是太不友好。要想保证业务操作的安全性以及改善交互操作,我们就需要抽象出问题的根本所在。

发送验证码操作最终的目的是为了验证操作人是操作人,听起来很绕是不是。实现这个最终目的,其实有很多解决方案,其中用户可信设备就属于其中一类,而手机验证码方式又是用户可信设备实现的一种方式,具体来说有几点:

  1. 用户利用手机验证码在这个设备上进行过敏感操作,就认为这个设备在一段时间内是可信任的。
  2. 用户在可信任的设备上进行其他敏感操作,如果在有效期内,就可以做到不发送验证码
  3. 用户的敏感操作也可以进行分级,最高敏感级必须输入验证码才可以进行操作(比如重置密码,验证码登陆),一般敏感级在可信设备有效期内可以不输入验证码。

image

基于以上所说,系统设计的时候就可以抽象出一个用户可信设备中心,包括敏感操作的定义,可信设备的有效时长,可信设备的定义(比如:验证码通过的设备可定义为有效设备)等等概念。通过这样设计,短信验证只不过成为验证用户信任设备的一种途经,完全可以做到和具体业务无关(敏感级别最高操作除外),一般敏感的操作业务接口也可以避免添加验证码参数,真正的把验证和业务相分离,岂不美哉?

经过这样抽象,用户可信设备中心其实本质的接口只有几个:

  1. 验证设备是否有效
  2. 设置设备有效
  3. 设备有效的途经(例如短信验证码方式)

当然你的系统首先要有设备的概念,如果非要写几行代码的话

  1. 验证设备有效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
csharp复制代码public async Task<int> CheckUserDevice(UserDeviceReq para)
{

if (para == null || string.IsNullOrWhiteSpace(para.DeviceName) || para.UserId <= 0)
{
return 0;
}
//检查签名
var sign = EncrypHelper.MD5Encrypt($"{SysConfig.SecretKey}_{para.UserId}_{para.DeviceName}");
if (sign != para.Sign)
{
return 0;
}
string key = $"{para.UserId}_{para.DeviceName}";
var authRet = await RedisClient.GetString(key);
if (string.IsNullOrWhiteSpace(authRet))
{
//告诉客户端需要短信验证码
return 414000;
}
return 1;
}
  1. 设置设备有效
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
csharp复制代码 public async Task<int> SetUserDevice(UserDeviceReq para)
{

if (para == null || string.IsNullOrWhiteSpace(para.DeviceName) || para.UserId <= 0)
{
return 0;
}
//检查签名
var sign = EncrypHelper.MD5Encrypt($"{SysConfig.SecretKey}_{para.UserId}_{para.DeviceName}");
if (sign != para.Sign)
{
return 0;
}
string key = $"{para.UserId}_{para.DeviceName}";
var cacheRet = await RedisClient.GetString(key);
if (string.IsNullOrWhiteSpace(cacheRet))
{
UserDeviceInfo value = new UserDeviceInfo() { UserId = para.UserId, DeviceName = para.DeviceName, OperationCode = para.OperationCode, CreateDate = DateTime.Now, Context = "" };
var userDeviceExp = SysConfig.GetAppSetting("Config:UserDeviceExpire");
if (string.IsNullOrWhiteSpace(userDeviceExp))
{
userDeviceExp = "300";
}
var authRet = await RedisClient.SetString(key, JsonConvert.SerializeObject(value), TimeSpan.FromMinutes(int.Parse(userDeviceExp)));
if (!authRet)
{
return 0;
}
}
return 1;
}

更多精彩文章

  • 分布式大并发系列
  • 架构设计系列
  • 趣学算法和数据结构系列
  • 设计模式系列

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…749750751…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%