白菜日记7

最近又又又打算重构白菜,缘由则是因为经常出现图片上传失败的情况,加上之前的丑陋代码实在不堪入目。。

当时用两天赶出来的所有命令的拦截器,事实证明根本没法维护,把所有参数语义化是一个原因,把所有命令放在一起拦截则是另一个更重要的原因,为了满足“通用逻辑”做了很多匪夷所思的around。。

大致打算拆分为中美两个节点,美国服务端负责获取网页、解析数据,以及代理osu的API,也包括负责每天凌晨的批量爬取数据。国内服务端则负责解析命令,生成图片并发送。


在着手编写美帝服务端时,突发奇想觉得失败重试这种大众化需求不应该由我手动实现,同时也觉得随处不在的

1
2
3
4
int count = 0;
while (count<5){
...
}

很是丑陋。不过这两年来也确实没有想到可以用面向切面的方式解决,直到看到了spring-retry这个模块……

基本满足了我的需求,提供了超时自动倍增的功能,本质和事务一样还是一个切面。


在Http客户端的挑选上遇到了一个坑,记一下:

一开始打算赶上时髦,用Java11新的那个Http客户端,研究发现本身不支持重试,反而Apache的HttpClient支持……

虽然发现Spring框架有个模块能提供重试功能(上述),然后又想起Spring的RestTemplate应该更符合我的需求(大部分是JSON交互,还有一部分需要解析网页打算还是用jsoup做)。

尝试了一下,把RestTemplate注册成Spring的Bean,并且绕过了之前公司项目踩的一个坑:

1
2
3
4
5
6
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
return restTemplate;
}

在测试的时候,发现application.yml中对Jackson的下划线转驼峰配置没有起效:

1
2
3
spring:
jackson:
property-naming-strategy: SNAKE_CASE

于是逛StackOverFlow,得出结论:

此处RestTemplate是由我自己实例化并托管给Spring的,构造方法内会另外实例化一个ObjectMapper,并不会使用Spring的Bean,因此配置自然也就不会起效了。

最后解决方法是,将MappingJackson2HttpMessageConverter注册成Bean,替换其内置的ObjectMapper为呗被Spring配置过的ObjectMapper,再将这个Converter对象替换到RestTemplate里去。

由于考虑国内服务也要使用RestTemplate,故另行编写配置类,放到common模块下。

完整配置类如下:

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
@Configuration
public class RestTemplateConfig {
private ObjectMapper objectMapper;

@Autowired
public void setObjectMapper(ObjectMapper objectMapper){
this.objectMapper = objectMapper;
}

@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
restTemplate.getMessageConverters().set(6, mappingJacksonHttpMessageConverter());
return restTemplate;
}

@Bean
public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
return converter;
}
}

有一说一,明明是Spring提供的Http客户端,需要手动注册成Bean来实现单例也就算了,甚至不能直接吃到配置文件中的Jackson配置,实在是不够方便,希望以后这些配置都能在yml文件中完成。。

x86汇编学习笔记

追一下小时候的梦,稍微学习一下逆向工程。

找了一些书籍推荐,最终决定从RE4B(安天翻译的版本)开始。

上册基本是一些C语言基础的汇编实现,暂且忽略掉ARM、Thumb和ARM64平台(篇幅过大),专注看一看x86下的AT&T和Intel语法汇编,开一篇文章记载一下学习汇编指令的过程。

[TOC]

1. 环境搭建

由于书中所用的编译器是MSVC2010/2012和GCC4.x,略显过时,特此自己搭了一套GCC 8.1.0(MinGW)和MSVC2019的环境。

  1. 安装VS2019 社区版,并勾选使用C++的桌面开发
  2. 从SourceForge下载MinGW-w64的离线安装包,需要X86和X64两个版本。
  3. 安装CLion,以利用其对C语言和AT&T语体汇编语言的有限支持。

为免去配置VS的头文件和库文件环境变量的麻烦,可以利用VS自带的环境变量批处理,因此编写如下批处理文件以生成各种架构下不同语体的汇编文件:

1
2
3
4
5
6
7
8
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsamd64_x86.bat"
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.22.27905\bin\Hostx64\x86\cl.exe" main.c /Famain_msvc_86.asm
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.22.27905\bin\Hostx64\x64\cl.exe" main.c /Famain_msvc_64.asm
"E:\MinGW\mingw32\bin\gcc.exe" main.c -S -o main_gcc_at_t_86.asm -fno-asynchronous-unwind-tables
"E:\MinGW\mingw\bin\gcc.exe" main.c -S -o main_gcc_at_t_64.asm -fno-asynchronous-unwind-tables
"E:\MinGW\mingw32\bin\gcc.exe" main.c -S -o main_gcc_intel_86.asm -masm=intel -fno-asynchronous-unwind-tables
"E:\MinGW\mingw\bin\gcc.exe" main.c -S -o main_gcc_intel_64.asm -masm=intel -fno-asynchronous-unwind-tables

其中GCC的-fno-asynchronous-unwind-tables参数是为了忽略一些不必要的宏。

最终编译结果有时会与书本上不完全一致,因此本书将作为提纲挈领之用,我将根据书中目录顺序亲手进行汇编,尽力找出不一致的原因,加以拓展学习。

2. 最简函数

C

1
2
3
int f() {
return 123;
}

汇编

GCC AT&T

1
2
3
4
5
6
7
8
	.file	"main.c"
.text
.globl _f
.def _f; .scl 2; .type 32; .endef
_f:
movl $123, %eax
ret
.ident "GCC: (i686-win32-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"

x64的结果几乎一样,除了函数名少了下划线,以及.ident的编译器信息不一样。尽管是x64的汇编,寄存器也只用了EAX。

其中的.file之类的是指示,记录了数据段的开始(.data)、实际程序代码的开始(.text)、字符串常量(书上和某文章说是.string,但实际汇编出来的是.ascii)等。还有一系列.cfi开头的指示,可以记录堆栈信息,在GCC生成汇编指令时可以用-fno-asynchronous-unwind-tables参数忽略。

GCC Intel

Intel语体采用了另一个指令:

1
2
3
f:
mov eax, 123
ret

MSVC x86

MSVC的情况稍复杂一些, 首先它贴心的标记了行号,其次在x86平台生成的汇编语句中出现了函数调用标志,只不过栈顶没有动而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.22.27905.0 

include listing.inc

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC f
; Function compile flags: /Odtp
_TEXT SEGMENT
_f PROC
; File C:\Users\QHS\CLionProjects\untitled1\2.min_function\main.c
; Line 3
push ebp
mov ebp, esp
; Line 4
mov eax, 123 ; 0000007bH
; Line 5
pop ebp
ret 0
_f ENDP
_TEXT ENDS
END

MSVC x64

和GCC表现完全一致。

1
2
3
4
5
6
7
f	PROC
; File C:\Users\QHS\CLionProjects\untitled1\2.min_function\main.c
; Line 4
mov eax, 123 ; 0000007bH
; Line 5
ret 0
f ENDP

拓展:栈内存

栈内存在内存中自顶向下,添加元素时会使栈底减少。而PUSH就是汇编的入栈指令,其作用是先将栈底减4(32位平台)或8(64位平台),然后将要写入的数写到栈底指向的位置。

例如push ebp等价于:

1
2
sub esp, 4
mov [esp], ebp

POP则是逆向操作,会将内存中的值赋给某寄存器。pop ebp等价于:

1
2
mov ebp, [esp]
add esp, 4

拓展: 函数序言和尾声

函数序言:

将EBP寄存器的值压入栈

将ESP寄存器的值赋值给EBP(将函数开始前的栈底保存到EBP,此时ESP和EBP是该函数的局部变量、参数基准值)。

修改ESP,以给函数局部变量分配空间。

1
2
3
push	ebp
mov ebp, esp
sub esp, X

函数尾声:

做函数序言的逆操作

将EBP的值复制到ESP(将ESP的值恢复到函数开始前)

从栈内存中读取EBP的值。

1
2
mov	esp, ebp
pop ebp

小结

  • AT&T语体中,MOV采取从左向右赋值的语法,而Intel与C类似,从右向左赋值。其他运算表达式也类似,源和目标相反。
  • AT&T语体使用圆括号取值,而Intel使用方括号。
  • AT&T语体会使用%符号标志寄存器,用$符号标志立即数(暂且理解为常量)。
  • AT&T语体会指定操作数据类型,-q是64位(quad),-l是32位long,-w是16位word,-b是8位byte
  • 函数调用结束后会把返回值放在EAX寄存器,调用者会从该寄存器取值。
  • 某个函数调用开始时,EBP为此时栈顶,ESP随着局部变量声明而变化。函数结束时,将EBP复制到ESP,恢复栈顶为调用开始时的值,再从栈内存中取出原有EBP。

3. Hello World

C

1
2
3
4
int main() {    
printf("Hello, World!\n");
return 0;
}

汇编

GCC x86

AT&T :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	.file	"main.c"
.text
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "Hello, World!\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
call ___main
movl $LC0, (%esp)
call _puts
movl $0, %eax
leave
ret
.ident "GCC: (i686-win32-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
.def _puts; .scl 2; .type 32; .endef

andl指令将ESP的值指定为16的整数倍,这是一种x86/x64的编译规范。

subl指令将栈顶向下拉了16字节,本来4字节就够了,但是由于被对齐所以分配了16字节。

接下来,LC0是一个全局变量,相当于const char* LC0[] = "Hello, World!"。在此,该变量的指针地址被复制到ESP寄存器,以便printf函数从ESP寄存器中取值。

该函数比最简函数多一个对printf函数的调用,该函数在GCC中被优化为了puts

最后将0放入EAX中,表示主函数返回值。

Intel语体的MOV指令略有不同:

1
mov	DWORD PTR [esp], OFFSET FLAT:LC0

GCC x64

1
2
3
4
5
6
7
8
9
10
11
12
main:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
call __main
leaq .LC0(%rip), %rcx
call puts
movl $0, %eax
leave
ret
.ident "GCC: (GNU) 8.1.0"
.def puts; .scl 2; .type 32; .endef

LC0的代码没有变化,故省略。

可以看出x64跳过了对齐步骤,而数据类型变成64位导致指令最后的l全部变成了q。

r开头的寄存器是e开头寄存器的扩展,以RAX举例:AL占用第0字节,AH占用第一字节,AX占用0和1字节,EAX占用0、1、2、3字节,而RAX占用0-7这8个字节。

传参的方式也发生了变化,不再使用栈内存,而是直接使用寄存器。与书中不一样的是,此处使用了rcx寄存器而不是edi寄存器进行传参,参数值也多了一个(%rip),查阅资料后得知是指令指针寄存器,暂时不明用途。

LEA命令意为Load Effective Address,暂时可以理解为与MOV等价。

而C编译器为了兼容性会返回32位的0,也就意味着程序结束时EAX为0,RAX不一定为0。

MSVC x86

而MSVC产生的汇编很长,似乎将Windows Kits的stdio.h中printf部分也汇编到了同一个文件。

推测可能是某种优化,但是我也没有开启任何优化选项……

摘取main函数的部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_DATA	SEGMENT
$SG8163 DB 'Hello, World!', 0aH, 00H
_DATA ENDS
; Function compile flags: /Odtp
_TEXT SEGMENT
_main PROC
; File C:\Users\QHS\CLionProjects\untitled1\3.helloworld\main.c
; Line 3
push ebp
mov ebp, esp
; Line 4
push OFFSET $SG8163
call _printf
add esp, 4
; Line 5
xor eax, eax
; Line 6
pop ebp
ret 0
_main ENDP
_TEXT ENDS

与书中MSVC2010的表现不同,2019用_DATA段取代了CONST段.

与GCC不一样,MSVC使用xor eax eax来置0.

MSVC x64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
main	PROC
; File C:\Users\QHS\CLionProjects\untitled1\3.helloworld\main.c
; Line 3
$LN3:
sub rsp, 40 ; 00000028H
; Line 4
lea rcx, OFFSET FLAT:$SG7907
call printf
; Line 5
xor eax, eax
; Line 6
add rsp, 40 ; 00000028H
ret 0
main ENDP

64位MSVC的行为让我有些费解,为什么会将栈顶减小又增加40字节呢。也没有看到将寄存器内容保存到栈里的语句。

其余和书上基本一致,使用rcx寄存器进行传参。

小结

  • 汇编中的括号类似于C中的取地址操作符。
  • 32位平台用栈传参,64位用寄存器传前几个参数。
  • 64位系统中程序结束时EAX为0不代表RAX为0。
  • GCC可能会把printf优化为puts,而MSVC会使用XOR对EAX寄存器置0。

简单的聚合支付网关设计

由于不是很熟悉旧业务,在写完微商城项目后 本打算重构公司原有的“支付模块”,结果要顾及原有项目和它的对接,一时间竟无从下手。

恰逢手上活不多,着手设计一个聚合支付网关,由于之前对接了微信支付,因此这个网关也能看到一些微信支付的影子,例如使用随机字符串+HMAC256验证签名,而不是用支付宝的RSA方案。

由于公司内部有两个项目(一起绣、微商城),其中微商城项目针对不同客户有不同的部署方式,因此商户号、appid等支付参数需要按公司独立。

一开始苦于微商城的CompanyId机制和一起绣的参数无法兼容,参看了微信支付的设计后果断采用了按项目/公司签发应用的方式,毕竟支付宝/微信实际上也是一种包装了众多银行网关的聚合支付,照抄没什么问题

后面当然是鸽了,新需求接踵而至,这个也不是什么高优先级的东西,先放着吧

错题本

……尝试治一下自己的毛病

大致就是,写代码的时候做出许多假设,测试的时候发现假设并不总是成立。。

很多都是非常细小的问题,交付一个模块的时候记一下,不要再犯就是了

2019-11-14

微商城斑马二期用户分组、花型

  • MyBatis Plus默认不更新为null字段,导致无法将面料修改为花型

  • 重构商品校验Service时,取错集合,拼错字符串,导致花型无法设置为前10商品时报错文案错误

  • 商品占库业务没有考虑到库存可以为空,订单业务应该是花型订单才不占库,写反了

  • JRebel会导致修改Spring配置文件的配置项后不生效

  • 审核订单需要计算幅宽克重,没有跳过

  • 销售系统创建订单会根据类来推断订单类型,还有一个强行将生产数量置0的设定

  • 管理后端商品详情应该写sql 用嵌套查询查出子表内的完整记录,然后返回id和名称给前端

  • 商品详情没有返回折扣价,而且从登录Token信息取用户组id不一定是最新的

解决方案:

  • 单个用户认证并且变更所在用户组的时候,需要踢出该用户
  • 删除整个用户组时,需要踢出该用户组所有用户
  • 商品详情使用用户组id从数据库获取并返回当前用户的折扣信息,由前端计算实时的商品价格/价格配置价格
  • 订单详情的花型订单需要返回单品的单价
  • 商品列表也需要返回折扣

购物车展示折扣价

2019-9-16

从前往后

标签模块

  • 表名先用了复数,手写sql没改过来
  • 没考虑添加商品时没有带标签的情况
  • 删除接口Controller的请求方法写错
  • TagDTO用于传输公司id,使用BeanUtils.copy时将公司id复制过去,导致返回结果多出公司id来
  • 根据标签名搜索商品时 标签名与商品名条件应用OR排列,标签id应用and排列

2019-9-11

  • 商品列表我认为是空列表,但是没有再swagger里写清楚,前端不知是传空数组还是不传
    包括活动页Banner的必传性我也没有写清楚
    而我后台做了许多没有判null就进行的操作

  • 是否需要主标题为空的校验写错了类型,写到了个人中心页的Banner

  • 手动把模块表的某条记录从逻辑删除恢复,然后忘了恢复图片。。导致这个类型的模块拿不到,手动修了数据

  • 哎呦,吐了,设计出现重大问题
    之前做小程序banner一对多是按图片id做group,因为不想改原有数据结构
    现在出现问题了,用两张相同的图片发现商品id被group到一个图片里去了

得改改,用json字符串存这个图片-商品一对多好了

2019-08-28

Banner图模块

  • 关联查询时,由于习惯Mybatis Plus的自动过滤逻辑删除特性,自己撰写SQL时没有过滤关联查询表的已经逻辑删除的记录、以及子表中不存在的记录
    代码:
1
2
3
4
@Select("select m.id as module_id,m.is_required,m.title,m.subtitle,m.type,i.file_id,i.product_id,i.pno " +
"from wsc_pc_banner_module m left join wsc_pc_banner_image i on i.module_id = m.id " +
"${ew.customSqlSegment} and (i.is_deleted is NULL or i.is_deleted = 0) and m.is_deleted = 0 order by m.id")
List<WscPcBannerDO> selectBanner(@Param(Constants.WRAPPER) Wrapper wrapper);

重点在于and (i.is_deleted is NULL or i.is_deleted = 0)

  • 根据类型筛选Banner图,前端应该这么传值:
1
2
3
GET {{adminHost}}/microManage/pcBanner?type=0&type=3&type=4&type=5
Content-Type: application/json
Authorization: {{adminToken}}

后端应该这么接受:

1
2
3
4
@GetMapping("/pcBanner")
public JsonResult pcBanner(@RequestParam(required = false) Integer[] type) {
//...
}

前端直接用[0,3,4,5]这样的后端是接收不到的。。后端也不能用List来接受。。感觉这个设计有一点丑,但又不想额外写代码转换。。

记一次死锁

业务场景:微信支付模块,每一次请求都会在pay_log表里插入订单号等记录,支付回调时会更新pay_log表的记录

同时还有一个定时任务用来定时将所有1小时前的订单标记为交易超时

编写单元测试,模拟支付回调通知,结果出了异常,更新记录时出现死锁

大致SQL如下:

事务A

1
Update pay_log SET tradeState = 2 WHERE tradeState=0 AND createTime < '2019-08-20 15:23:04.167'

事务B

1
2
INSERT pay_log VALUES(NULL,?,NOW(),?,?,?,?,?,?,?,?,?,?,?,?) 
Update pay_log SET tradeState =? WHERE outTradeNo = ? AND tradeState = ? AND payType = ?

然后事务B被回滚了

解决方案:要么禁用定时任务,要么去掉单元测试的事务手动回滚,要么单元测试不要打断点调试(定时任务设置了项目启动后10秒后启动)

附上MySQL死锁日志,其实这玩意只能大致的定位故障,因为可以看出事务2的SQL只有最后执行的那一条

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
2019-08-20 16:23:07 0x7ff1b2039700
*** (1) TRANSACTION:
TRANSACTION 43869195, ACTIVE 3 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1136, 269 row lock(s)
MySQL thread id 746830, OS thread handle 140676071700224, query id 184518127 192.168.3.108 root updating
Update pay_log SET tradeState = 2 WHERE tradeState=0 AND createTime < '2019-08-20 15:23:04.167'

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3179 page no 7 n bits 104 index PRIMARY of table `pay`.`pay_log` trx id 43869195 lock_mode X waiting
Record lock, heap no 37 PHYSICAL RECORD: n_fields 17; compact format; info bits 0


*** (2) TRANSACTION:
TRANSACTION 43869181, ACTIVE 7 sec starting index read
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 747008, OS thread handle 140676050425600, query id 184518193 192.168.3.49 root updating
Update pay_log SET tradeState =1 WHERE outTradeNo = 'T9744695806' AND tradeState = 0 AND payType = 0

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3179 page no 7 n bits 104 index PRIMARY of table `pay`.`pay_log` trx id 43869181 lock_mode X locks rec but not gap
Record lock, heap no 37 PHYSICAL RECORD: n_fields 17; compact format; info bits 0

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3179 page no 4 n bits 168 index PRIMARY of table `pay`.`pay_log` trx id 43869181 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 17; compact format; info bits 0

*** WE ROLL BACK TRANSACTION (2)

记一次本人代码提交丢失事故

前情提要:
公司代码提交规范是Merge Request,平时push在自己fork的远程仓库,开发完成后用Merge Request合并到上游仓库。

之前我负责的功能是在wsc分支上工作的,push到了自己的远程仓库,并且发起Merge Request,然后被后续可能切换分支为理由打回。

今天项目负责人通知我要切换到saas2.2分支,目前该分支与wsc分支保持一致,仅存在于上游仓库。

于是我使用git fetch upstream拉取上游仓库saas2.2分支信息,并且checkout到本地。

1
2
3
10:10:23.544: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false checkout -b saas2.2 upstream/saas2.2 --
Branch 'saas2.2' set up to track remote branch 'saas2.2' from 'upstream'.
Switched to a new branch 'saas2.2'

然后将本地saas2.2分支push到远程仓库saas2.2分支(在IDEA中选择了origin作为remote):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
10:10:42.123: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false push --progress --porcelain origin refs/heads/saas2.2:saas2.2
Enumerating objects: 67, done.
Delta compression using up to 4 threads
Total 33 (delta 12), reused 0 (delta 0)
remote: hooks/pre-receive:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote: hooks/update:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
To http://192.168.3.112:8888/qhs/***.git
remote: hooks/post-receive:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote:
* refs/heads/saas2.2:refs/heads/saas2.2 [new branch]
remote: To create a merge request for saas2.2, visit:
remote: http://192.168.3.112:8888/qhs/***/merge_requests/new?merge_request%5Bsource_branch%5D=saas2.2
remote:
Done

然后将本地wsc分支合并到本地saas2分支,然后再次push。

然后这次我忘了设置remote:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
10:10:57.803: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false merge wsc
10:11:05.785: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false push --progress --porcelain upstream refs/heads/saas2.2:saas2.2
Enumerating objects: 25, done.
Delta compression using up to 4 threads
Total 9 (delta 2), reused 0 (delta 0)
remote: hooks/pre-receive:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote: hooks/update:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote: hooks/post-receive:3: warning: Insecure world writable dir /opt/gitlab/embedded in PATH, mode 040777
remote:
remote: To create a merge request for saas2.2, visit:
remote: http://192.168.3.112:8888/root/***/merge_requests/new?merge_request%5Bsource_branch%5D=saas2.2
remote:
To http://192.168.3.112:8888/root/***.git
refs/heads/saas2.2:refs/heads/saas2.2 a4cc8e2..de8714c
Done

然后我hard reset了本地的wsc分支,

1
2
3
4
10:11:15.153: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false checkout wsc --
Your branch is up to date with 'origin/wsc'.
Switched to branch 'wsc'
10:12:01.366: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false reset --hard c84bdf0753dd540fcb8312b324d1f848aae938a8

并且force push到了远程仓库。至此本地、远程仓库的wsc分支丢失了本次提交。

看起来一切都很美好,直到我看到了我之前忘记设置remote的push引发的WebHook钉钉机器人提醒。。

于是我将本地saas2.2分支 hard reset,并且force push到了上游仓库。。

1
2
3
4
10:15:00.241: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false checkout saas2.2 --
Your branch is up to date with 'upstream/saas2.2'.
Switched to branch 'saas2.2'
10:15:42.094: [***] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false reset --hard a4cc8e2b7ce5dfdc0c25c98b63a77a12c3aa09f3

至此本地、上游仓库的saas2.2分支丢失了本次提交,并且我在合并前就将saas2.2分支push到远程仓库,因此远程仓库也没有此提交记录。。

万幸Gitlab上有关闭的Merge Request记录,可以查看到代码改动。。

得到的经验教训是:使用git push -f之前,一定要仔细看一下提交记录,push之前也要仔细看一下remote到底是什么。。

幸好干了两年活只丢过这么一次代码,还是丢的我自己的。。

关于单测的笔记

之前没有写单元测试的意识,把测试框架看成不用启动项目的带Spring框架的类。。

后来项目有了单元测试覆盖率要求,于是开始根据数据库的现有数据写用例

然后被教育了,单元测试应该是和外部环境无关的对现有逻辑的测试,所有数据要在@Before方法构建,并且测试完之后所有操作都要回滚。。

灵异事件二则

……调微信支付的时候碰到两个灵异事件

首先是按Spring文档 添加了如下依赖:

1
2
3
dependencies {
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}

然后写了一个微信支付AppID的配置类

结果代码里跑起来死活为null

想了一下,把wxpay相关的配置拿到了spring相关配置下方(按理应该没影响)

然后配置文件里log后面莫名其妙多了一个192.168.3.49,没注意到,启动测试类读取微信支付配置时提示连不上数据库192.168.3.49……

删掉之后 配置文件里的数据库密码突然变成了之前调试本地数据库时候的本地数据库密码。。

改掉配置文件,invalid cache and restart之后测试类能读到配置类的数据了。。然后配置类的@Data注解上还多了个断点,感觉电脑被远程控制了一样……

最骚的是,IDEA 2019.1.1不支持Gradle4.9+SpringBoot读配置文件的组合,如果依赖写的是annotationProcessor会提示class path里没有spring配置相关的processor。。


2019-5-21 17:12:36
更骚的出现了,测试类能拿到appID,引用了common模块的其他模块跑起来的时候拿不到

最后是因为多个application.yml冲突了……

微信小程序支付对接开发笔记

2019-9-23更新
试了一下支付宝

发现支付宝网页有个收银台,PC网站下单完事之后会给一个自动提交的form,然后302跳转到另一个URL

感觉实际对接的时候可以把那个URL返回给前端


新单位新项目,微信小程序对接微信支付+退款,由于开发测试是真的繁琐,在此记录一下笔记。

下载微信SDK,编写后端代码

……随便吐槽一下,微信SDK被人挖出来漏洞之后就不放github了,只能从微信官网下载,然后阿里规约扫描报了一大堆错……也没法改,万一后续SDK更新了呢

2019-8-28更新:
我真是认识鹅厂微信支付Java SDK作者的美,你弄个WxPayConfig的抽象类给我们扩展,然后getAppId()这种方法的访问权限是default,用Maven等依赖管理引入之后,根本没法扩展,因为必须得在同一个包下。。

然后其他WxPay之类的类又必须依赖这个类的子类才能干活。。 难怪官方都只给源码下载,而不是托管到什么依赖仓库。。

配置类

首先创建一个SDK内WxPayConfig类的实现类,由于项目名是微商城,取名叫WscWxConfig

使用Spring配置读取的方式,在类里声明AppIdMchId(商户号),Key,证书内容(字节数组)几个成员变量,然后AppId MchId keygetter直接用lombok生成。

getCertStream这个方法需要重写为return new ByteArrayInputStream(this.certData);的形式,getWxPayDomain更麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
IWXPayDomain getWXPayDomain() {
return new IWXPayDomain() {
@Override
public void report(String domain, long elapsedTimeMillis, Exception ex) {

}
@Override
public DomainInfo getDomain(WXPayConfig config) {
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
};
}

2019-8-28更新:
后续改成按公司独立设置微信支付参数,所以用了ThreadLocal维护这些东西。。支付退款证书也放在了OSS上,反正是流,无所谓……

顺带给WxPay类做一个单例,声明一个WxPay类型的变量,然后用@PostConstruct注解初始化方法:
注意这里的构造函数传参第三个参数,表示使用沙箱环境

1
2
3
4
5
6
7
public void initWxPay(){
try {
this.wxPay = new WXPay(this, false, true);
} catch (Exception e) {
log.error("inital wxpay failed ", e);
}
}

这样配置类就写完了。

Service

微信SDK给了轻度封装,至少使用Map<String, String>就可以完成参数填写了

统一下单

由于负责用户模块的同事的设计是不存储OpenId,因此该方法直接接受code,订单号,金额,公司名(用于微信订单描述)作为参数。

接下来是调用微信SDK发送请求,并且将拿到的prepay_id返回前端。

接受支付通知

一定要校验签名和金额!!!!

幂等性处理后,将接收到的支付信息持久化一份到数据库,并且根据业务结果+支付金额调订单Service。

扯一下我们的幂等性方案吧,防重入一开始打算用悲观锁锁住订单号对应的行,但是怕应用意外崩溃然后锁无法释放,最后用的是redis的setnx。防重入之后就做一下状态判断就行了。

退款

这回是由商户服务器发起请求,所以不用OpenId了,指定订单号、金额、退款金额、售后单号即可

接受退款通知

类似支付通知的幂等性处理,持久化一份到数据库,并根据业务结果调售后Service。

Controller

主要是两个接收通知的接口,以及退款接口信息的解密方法。

先从request.getInputStream()中获取XML,然后用微信SDK转成Map,再用微信SDK做签名校验。

测试

后端码狗自测微信小程序支付接口简直要人命……居然没找到描述全流程的博文
调个微信支付和玩tmd解谜RPG一样,到处找线索

小程序支付交付需要在沙箱模拟过一遍之后向微信提交验收通过申请,当然如果已经有通过微信支付验收的小程序就不必走一遍沙箱流程了。

写完代码自己测试时,一般还是直接用一个能用的小程序支付参数,而不是沙箱,理由后面讲。。

数据准备

AppId

小程序所属的公司账号将开发者的微信号加入小程序开发者后,开发者使用微信扫码登录微信小程序后台即可看到。

AppSecret

需要超级管理员在小程序后台开发设置生成,用于小程序Code换OpenId用。。

MchId

在商户后台将开发者账号加入商户的员工账号后,员工账号会收到包含MchId的通知。

Key

商户后台API安全处填写,需要超级管理员验证,需要商户账号开通操作密码
其中沙箱开发要用到SignKey,使用Key请求微信的API生成。

解析退款信息时使用。

证书

商户后台API安全处可以重设,最好能找到第一次申请的证书文件。
解析退款信息时使用。

内网穿透

本地请求支付的接口需要一个带https的内网穿透wx.request用,即使有CI/CD,也懒得改一行代码提一个pr……图省事直接买了NATAPP。

吐槽一下,要调试小程序需要买他的国内隧道+二级域名,15元一年的域名+9元一个月的隧道;

倒是可以选择用自己的域名,但是我域名没备案……而阿里云买的域名要备案需要配阿里云的实例……

还有香港流量包月的隧道,买完才发现这个不能开https……香港流量不包月的倒是可以配自己域名,但是自己域名443端口必须空着,我域名有个开了https的小网盘跑着……

幸好微信的支付通知回调可以不用https,就用免费隧道顶上。

2019-8-28更新:
后续在自己的服务器上搭了个ngrok,不用额外付冤枉钱了。。

统一下单

在微信开发者工具中新建一个小程序,修改AppId为公司小程序的AppId(也可以直接调公司小程序的代码,但是单独测一个支付我还是选择新起一个小程序)。

加一个按钮,测试支付,绑定一个函数,把返回的requestPayment用的数据打印出来,顺带请求支付:

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
testPay: function (e) {
wx.login({
success(res) {
if (res.code) {
// 发起网络请求
wx.request({
url: 'https://qxkjwxpay.mynatapp.cc/micro/order/payOrder',
header: {
'Authorization': '登录接口生成的JWT内容'
},
method: 'POST',
data: {
code: res.code,
amount: '1.01',
orderNum: '2',
paymentType: 10
},
success(res) {
console.log(res);
wx.requestPayment(
{
'timeStamp': res.data.data.timestamp,
'nonceStr': res.data.data.nonceStr,
'package': res.data.data.prepayId,
'signType': 'MD5',
'paySign': res.data.data.sign,
'success': function (res) { },
'fail': function (res) { },
'complete': function (res) { }
})
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
}

结果是这样的:

1
2
3
4
5
6
7
8
9
code:200
data:{
nonceStr:"klrv5kgx5dJdc7ILG9Uj9k70hUKzXbF8"
prepayId:"prepay_id=wx20190522095426670409"
sign:"A118797887A33C961594D149285EDCEE"
timestamp:1558490065
}
msg:"成功"
subCode:null

调起支付

果↓然↑啊,扫描开发者工具给的二维码时提示错误:调用支付JSAPI缺少参数:total_fee

我寻思统一下单都成功了你在说你吗呢,会报错你就多报几句,是不是把你妈杀了你也只能呜咽着说出你缺少total_fee?真的憨批一样

package传的也带了prepay_id=,后端生成随机数+签名用的是微信JavaSDK的工具类,签名字段大小写也没问题

发现后端生成签名时,工具类会自动拼上key=key,而我手动在map里加了一个key……也就是现在的签名内容有问题。。

改掉之后还是报错,看到微信开放社区里一个帖子 说即使报错也受到了微信支付的成功通知,又看到segmentfault里的一个帖子沙箱就是这样。。

我真是艹了

接受回调

坑点:微信SDK送了一个判断支付结果通知中的sign是否有效的方法isPayResultNotifySignatureValid(),这方法默认没有传入签名类型的时候,选的是MD5.

但是微信支付主工具类 初始化签名方式时,根据传入是否沙箱来切换加密方式和URL,此时加密方式是HMAC!!

1
2
3
4
5
6
7
8
9
10
11
12
13
public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox) throws Exception {
this.config = config;
this.notifyUrl = notifyUrl;
this.autoReport = autoReport;
this.useSandbox = useSandbox;
if (useSandbox) {
this.signType = SignType.MD5; // 沙箱环境
}
else {
this.signType = SignType.HMACSHA256;
}
this.wxPayRequest = new WXPayRequest(config);
}

于是支付回调返回的sign也当然是HMAC,但是支付回调是没有写明签名类型的……这里一定做处理,比较优雅的做法是接受回调时读取配置加入签名类型。。不过我当时是直接改了微信的SDK……

然后是支付回调里没有trade_state,只有result_code,因为只推送成功结果,这点要和主动拉取支付结果做区分。

退款回调

发起退款的流程和发起支付类似,略过不表

退款回调没有sign字段,而且需要JDK装密钥长度无限扩展包

直接解密req_info字段即可,解密过程有几个坑,MD5哈希之后要转成小写,算法名是AES/ECB/PKCS7Padding不能写错,如果抛出需要IV的异常就是算法名写错了……

解密代码如下:

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
    private String decrypt(String reqInfo) throws Exception {
// (1)对加密串A做base64解码,得到加密串B
byte[] encryptB = Base64.getDecoder().decode(reqInfo);
// (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
StringBuilder hexString = new StringBuilder();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(wscWxConfig.getKey().getBytes());
byte[] hash = md.digest();
for (byte b : hash) {
if ((0xff & b) < 0x10) {
hexString.append("0").append(Integer.toHexString((0xFF & b)));
} else {
hexString.append(Integer.toHexString(0xFF & b));
}
}
String md5Key = hexString.toString().toLowerCase();
// (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
// 需要JDK中添加JCE:https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
Key sKeySpec = new SecretKeySpec(md5Key.getBytes(StandardCharsets.UTF_8), "AES");
cipher.init(Cipher.DECRYPT_MODE, sKeySpec);
byte[] result = cipher.doFinal(encryptB);
return new String(result);
}

Spring Boot项目公用模块单元测试踩坑

现在手头的项目分为几个子模块,前台一个,后台一个,计划任务一个,Service、bo、dao等放在common模块里,而common模块作为其他几个模块的依赖存在。

在加入Spring环境的单元测试时报了错:

1
java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test

按提示在测试类加入@SpringBootConfiguration后,报错变了:

1
Parameter 0 of method setXXService in com.XXX.common.XXXServiceImplTest required a bean of type 'com.XXX.common.XXXService' that could not be found.

Google了一下,发现这种情况是Application启动类不存在导致的,可是既然是依赖,自然就没有Application启动类,于是我尝试加入@SpringBootTest(classes="要测试的Service"),然后发现该Service注入到测试类成功了,但是Service中的几个成员注入失败,报错是找不到Bean。

查了一下发现,单元测试执行时,Spring会扫描这个class指定的类所在的包的所有子包,也就是一定要有一个打了@SpringBootApplication注解的类存在于common包下!

先建一个Root.class应付了事,然后慢慢研究:

在测试类加入注解@ComponentScan(basePackages = {"com.XXX.common"}),这回报错又变了:

1
Parameter 0 of method setXXXMapper in com.XXX.common.service.impl.XXXServiceImpl required a bean of type 'com.XXX.common.domain.repository.XXXMapper' that could not be found.

项目中用了Mybatis-Plus,该Mapper接口继承了BaseMapper<>,加了@Mapper和@Repository注解,难道@Repository注解不认??

暂且先改为@Component,报错变成了这样:

1
2
Parameter 0 of method setObjectMapper in com.XXX.common.service.impl.XXXServiceImpl required a bean of type 'com.fasterxml.jackson.databind.ObjectMapper' that could not be found.

原来是忘记把Jackson的ObjectMapper纳入Spring 管理了:

1
2
3
4
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}

接下来依然是Mapper接口报错找不到Bean……无奈先用Root.class顶过去,日后再说)

2019-8-28更新

最后的解决方案是不在Common模块里写测试用例……