介绍:itter.sh - 纯粹主义者的社交媒体

记于:2025-06-03 晚上
地点:浙江省·温州市·家里
天气:阴天

背景#

水一篇博客;最近发现一个文字终端版的社交媒体平台。

介绍#

根据官网的介绍:这是一个纯粹主义者的社交媒体平台。

1
2
3
4
itter.sh is your escape from the noise. It's a micro-blogging platform accessed entirely via SSH. 
No web browser. No JavaScript. No endless scroll of algorithmic 'content'.
Just you, your trusty terminal, and 180 characters at a time ("eets").
Why? Because terminals are cool. Because less is more. Because sometimes, you just need to type.

翻译:

1
2
3
4
itter.sh 是你逃离喧嚣的避风港。它是一个完全通过 SSH 访问的微型博客平台。
没有网页浏览器。没有 JavaScript。没有无休止的算法“内容”滚动。
只有你、你可靠的终端,以及每次 180 个字符(“eets”)。
为什么?因为终端很酷。因为少即是多。因为有时候,你只需要输入/打字。

设置#

1
2
3
4
5
6
7
8
1.准备一个SSH Key
# ssh-keygen -t ed25519 -C "your_email@example.com"

2.注册账号
# ssh register:your_cool_username@app.itter.sh

3.登录&使用
# ssh your_cool_username@app.itter.sh

使用#

登录后,界面如下:

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
               CCCzC                                                  
CCCCCCCCzCCCCCC
CCCCCzCCCzCCCCzC
CCCCCCCCCCCCzCCzC
CCCCCzC
z C C C CCCCCCzCCC
CzC CCC CCCCzCCCCC
CCCCCC CCCCCCzCCCCzCCC Cz
CCCCCzCC CCCCCCCCCCCCzC zCCz
CCCzCCCCCCCCCC CCCCC
CzCz CzCCCCzCCCCCCCzCC \zzzzzCzCzCzC
CzCCzzzCCCCCCzCzCCCCCCCCCCCCCCCzCCCCC
zCCCCCCCCCCCC CCCCCCCCCzzCCCCCzCCCC
zCCCzCCCCzzCC CCCCCzCzCzzCCCCCCCCCC
CCCzzzzCzCCC CCCCCCCzCzC CzCzCzzC
CCCCCzCCzzzCC CCCCCzCCCCC CCCCzCC z
CCzzzCCCCzCCC \CCCCzCz \ zCCCC C
zCzzzCCzCzCCCz CCzzzz CCzzC C C
CCCzCCzCCCCCzCC zCCzC C C
CCCCzCCCCzCCCCCzCzCCCCCz Cz
4CCCzCCCCCCCCCC CCzzCC
C Cz z CCzC
CC CC zCCCCzCC Cz
CCCCCz CCzCCCC CCCCCzCzCCCz
CCzCCzCCC9


Welcome to...
██╗████████╗████████╗███████╗██████╗ ███████╗██╗ ██╗
██║╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ ██╔════╝██║ ██║
██║ ██║ ██║ █████╗ ██████╔╝ ███████╗███████║
██║ ██║ ██║ ██╔══╝ ██╔══██╗ ╚════██║██╔══██║
██║ ██║ ██║ ███████╗██║ ██║██╗███████║██║ ██║
╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═╝
(Pssssht!)


itter.sh Commands:
eet <text> - Post an eet (max 180 chars).
watch [mine|all|#chan|@user] - Live timeline view (Default: all).
timeline [mine|all|#chan|@user] [<page>] - Show eets (Default: all, 1).
follow [#chan|@user] --list - Follow a user or channel, list follows.
unfollow [#chan|@user] - Unfollow a user or channel.
ignore @<user> --list - Ignore a user, list ignores.
unignore @<user> - Unignore a user.
profile [@<user>] - View user profile (yours or another's).
profile edit -name <Name> -email <Email> --reset - Edit profile (or reset it).
settings - View or change settings.
help - Show this help message.
clear - Clear the screen.
exit - Exit watch mode or itter.sh.

指令介绍(指令加粗部分是该指令的缩写):

  • eet : 发布一条消息(最大180个字符)。 示例:e Hello, world! 发布一条消息。
  • watch [mine|all|#chan|@user]: 实时查看时间线(默认:全部)。 示例:w mine 查看自己的消息。
  • timeline [mine|all|#chan|@user] []: 显示消息(默认:全部,页码1)。 示例:tl all 2 查看第2页的所有消息。
  • follow [#chan|@user] –list: 关注用户或频道,列出关注列表。 示例:f @yeshimin --list 查看用户的关注列表。
  • unfollow [#chan|@user]: 取消关注用户或频道。 示例:uf @yeshimin 取消关注用户。
  • ignore @ –list: 忽略用户,列出忽略列表。 示例:i @yeshimin --list 查看用户的忽略列表。
  • unignore @: 取消忽略用户。 示例:ui @yeshimin 取消忽略用户。
  • profile [@]: 查看用户资料(自己的或其他人的)。 示例:p @yeshimin 查看用户的资料。
  • profile edit -name -email –reset: 编辑资料(或重置)。 示例:p e -name yeshimin 编辑自己的名称。
  • settings”: 查看或更改设置。 示例:s pagesize 30 更改每页显示的消息数量为30。
  • help: 显示帮助信息。 示例:h 显示所有可用指令。
  • clear: 清除屏幕。 示例:c 清除当前屏幕内容。
  • exit: 退出查看模式或itter.sh。 示例:x 退出当前会话。

实例#

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
--- All Eets (Page 1, 20 items) ---
Time User Eet
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
29m ago xcry2006 (@xhgt2006) hello,@yildirim,@EchoesInTheStack
36m ago @EchoesInTheStack but under another name @joeblack
37m ago @EchoesInTheStack I came from daily.dev @joeblack
38m ago @EchoesInTheStack Hello everyone!
40m ago Yildirim (@yildirim) Hello, World!
1h ago @jellypo 한국 21대 대통령 선거 투표 시간 종료(KST 20:00), 사전 조사 결과 발표 함
2h ago xcry2006 (@xhgt2006) hello,@antixhero
2h ago @antixhero hello gaess..
3h ago xcry2006 (@xhgt2006) today was pretty good
3h ago putra (@praserver) how's ur day guys?
3h ago putra (@praserver) hi @xhgt2006
3h ago xcry2006 (@xhgt2006) hello,@praserver
3h ago putra (@praserver) hello all!!!
4h ago @bailey Ola all! Interesting concept...
4h ago xcry2006 (@xhgt2006) hello,@sibisaravanan
5h ago @sibisaravanan "Hi! I'm here on itter.sh"
6h ago JoeBlack (@joeblack) +1 for those that came from #daily.dev
7h ago xcry2006 (@xhgt2006) hello,@joeblack
7h ago JoeBlack (@joeblack) hello world #firsteet
7h ago xcry2006 (@xhgt2006) become horse?what does that mean?@link

Live updating... (exit to stop, (Shift +) PgUp/PgDn to scroll)

地址#

基于MyBatisPlus QueryWrapper封装的基础CRUD Controller

记于:2025-03-29 晚上
地点:浙江省·温州市·家里
天气:雨天

背景#

准备搭建个java脚手架,先做基础的crud封装

功能#

实现基于范型的基础CrudController,提供基础的crud接口;
其中查询接口支持按实体类中字段进行equal查询,另外支持按客户端指定字段查询;
对于非实体类的自定义查询类,支持标注@QueryField注解指定字段查询方式;

示例#

自定义查询类定义:

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
@Data
@EqualsAndHashCode(callSuper = true)
@Query(custom = true)
public class SysUserQueryDto extends BaseQueryDto {

/**
* 用户名
*/
@QueryField(QueryField.Type.LIKE_RIGHT)
private String username;

/**
* 密码
*/
@QueryField(QueryField.Type.EQ)
private String password;
}

@Data
public class BaseQueryDto implements Serializable {

/**
* 客户端自定义查询条件,需要在@Query中显式开启
* 命名添加_后缀,避免和表字段冲突(前缀方式jackson默认解析不了)
*/
@JsonProperty(value = Common.CONDITIONS_FIELD_NAME)
private String conditions_;
}

username字段以likeRight方式模糊匹配;
password字段以equal方式精确匹配;
conditions_字段用于客户端指定字段查询;

查询接口和参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET - /sys-user/query
params:
username = yeshi
password = 123456
conditions_ = username:likeRight:ye;username:eq:yeshimin
# 此处conditions_表示指定两个查询查询,用英文分号分隔,每个查询条件格式为`字段名:操作符:值`

相应的sql示例:
select * from sys_user
where username like 'yeshi%'
and password = '123456'
and username like 'ye%'
and username = 'yeshimin' ;

entity类由于要做成通用范型模式,类中字段默认是equal操作符,需要使用其他操作符可以指定conditions_字段

代码实现#

以下为关键类/方法:

查询基类:

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
/**
* 带查询条件字段的基类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Query(custom = true)
public class ConditionBaseEntity<T extends BaseEntity<T>> extends BaseEntity<T> {

/**
* 客户端自定义查询条件,需要在@Query中显式开启
* 添加@JsonProperty注解,使在接口请求时暴露该字段,响应时不返回该字段
* 添加@TableField注解,标识该字段非表字段
* 命名添加_后缀,避免和表字段冲突(前缀方式jackson默认解析不了)
*/
@JsonProperty(value = Common.CONDITIONS_FIELD_NAME, access = JsonProperty.Access.WRITE_ONLY)
@TableField(exist = false)
private String conditions_;
}

@Data
@EqualsAndHashCode(callSuper = true)
@Query(custom = true)
public class SysUserQueryDto extends BaseQueryDto {

/**
* 用户名
*/
@QueryField(QueryField.Type.LIKE_RIGHT)
private String username;

/**
* 密码
*/
@QueryField(QueryField.Type.LIKE)
private String password;
}

以上两个查询基类,分别用于实体查询类和自定义查询类


查询注解:

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
/**
* 查询类上可以不添加该注解,行为保持默认
*/
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {

// 是否启用,默认是
boolean enabled() default true;

// 是否支持客户端自定义条件查询,默认否
boolean custom() default false;
}

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface QueryField {

// default
Type value() default Type.DEFAULT;

Type type() default Type.DEFAULT;

// 条件生效策略
ConditionStrategy conditionStrategy() default ConditionStrategy.NOT_BLANK;

@Getter
enum Type {
// 保留DEFAULT是为了关联value()和type()
DEFAULT("等同于EQ", "default"),
EQ("等于", "eq"),
NE("不等于", "ne"),
GT("大于", "gt"),
GE("大于等于", "ge"),
LT("小于", "lt"),
LE("小于等于", "le"),
IN("包含", "in"),
NOT_IN("非包含", "notIn"),
IS_NULL("为null", "isNull"),
IS_NOT_NULL("非null", "isNotNull"),
BETWEEN("区间", "between"),
NOT_BETWEEN("非区间", "notBetween"),
LIKE("模糊", "like"),
LIKE_LEFT("左模糊", "likeLeft"),
LIKE_RIGHT("右模糊", "likeRight"),
NOT_LIKE("非模糊", "notLike"),
NOT_LIKE_LEFT("非左模糊", "notLikeLeft"),
NOT_LIKE_RIGHT("非右模糊", "notLikeRight");

private final String desc;
private final String exp;

Type(String desc, String exp) {
this.desc = desc;
this.exp = exp;
}

// of
public static Type of(String value) {
// default为非法操作符,不可从接口参数指定,只能内部注解中指定
if (DEFAULT.name().equalsIgnoreCase(value)) {
return null;
}
for (Type type : values()) {
if (type.getExp().equalsIgnoreCase(value)) {
return type;
}
}
return null;
}
}

/**
* 条件生效策略
*/
@Getter
enum ConditionStrategy {
NOT_NULL("非null"),
NOT_BLANK("非空串");

private final String desc;

ConditionStrategy(String desc) {
this.desc = desc;
}
}
}

以上两个注解分别标注在查询类上和查询字段上,分别控制查询类的行为(比如是否启动、条件生效策略等)和查询字段的行为


核心工具类:

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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
package com.yeshimin.unknown.common.config.mybatis;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.yeshimin.unknown.common.consts.Common;
import com.yeshimin.unknown.domain.base.BaseQueryDto;
import com.yeshimin.unknown.domain.base.ConditionBaseEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

@Slf4j
public class QueryHelper<T> {

public static <T> QueryWrapper<T> getQueryWrapper(Object query) {
return getQueryWrapper(query, Wrappers.query());
}

/**
* 生成QueryWrapper
*
* @param clazz 实体类Class,用于设置按实体类查询
*/
public static <T> QueryWrapper<T> getQueryWrapper(Object query, Class<T> clazz) {
QueryWrapper<T> wrapper = new QueryWrapper<>();
if (clazz.isInstance(query)) {
wrapper.setEntity(clazz.cast(query));
}
return getQueryWrapper(query, wrapper);
}

public static <T> QueryWrapper<T> getQueryWrapper(Object query, QueryWrapper<T> wrapper) {
// 获取@Query注解
Query queryAnno = query.getClass().getAnnotation(Query.class);
// 是否启用查询
boolean queryEnabled = queryAnno != null && queryAnno.enabled();
// 是否启用自定义查询
boolean queryCustom = queryAnno != null && queryAnno.custom();
if (!queryEnabled) {
log.debug("class: [{}], @Query is not enabled, skip", query.getClass().getName());
return wrapper;
}

// 解析自定义查询条件
List<Condition> listCondition = parseConditions(queryCustom, query);

// 解析类字段定义的查询条件
Field[] fields = query.getClass().getDeclaredFields();
for (Field field : fields) {
// 跳过conditions字段
if (field.getName().equals(Common.CONDITIONS_FIELD_NAME)) {
log.debug("skip 'conditions' field");
continue;
}

// 判断是否有 @QueryField 注解
QueryField queryField = field.getAnnotation(QueryField.class);
if (queryField == null) {
log.debug("{} is not annotated with @QueryField, skip", field.getName());
continue;
}

// 获取原访问标识,用于复原
boolean isAccessible = field.isAccessible();
// 允许访问私有字段
field.setAccessible(true);
try {
Object value = field.get(query);

// 获取条件生效策略
QueryField.ConditionStrategy conditionStrategy = queryField.conditionStrategy();
if (conditionStrategy == QueryField.ConditionStrategy.NOT_BLANK) {
if (value == null) {
log.debug("{} is null, skip", field.getName());
continue;
}
if (value instanceof String && StrUtil.isBlank((String) value)) {
log.debug("{} is blank, skip", field.getName());
continue;
}
} else if (conditionStrategy == QueryField.ConditionStrategy.NOT_NULL) {
if (value == null) {
log.debug("{} is null, skip", field.getName());
continue;
}
} else {
log.warn("conditionStrategy is not supported, skip");
continue;
}

// 获取查询类型
QueryField.Type type = queryField.value() == QueryField.Type.DEFAULT ?
(queryField.type() == QueryField.Type.DEFAULT ? (QueryField.Type.EQ) : (queryField.type())) :
queryField.value();

listCondition.add(new Condition(field.getName(), type, value));
} catch (IllegalAccessException e) {
log.error("{}", e.getMessage());
e.printStackTrace();
} finally {
field.setAccessible(isAccessible);
}
}

for (Condition condition : listCondition) {
String fieldName = condition.getProperty();
// 实体类字段命名转为表字段命名(小驼峰转下划线)
String columnName = StrUtil.toUnderlineCase(fieldName);
QueryField.Type operator = condition.getOperator();
Object value = condition.getValue();

switch (operator) {
// equal
case EQ:
wrapper.eq(columnName, value);
break;
// not equal
case NE:
wrapper.ne(columnName, value);
break;
// greater than
case GT:
wrapper.gt(columnName, value);
break;
// greater and equal
case GE:
wrapper.ge(columnName, value);
break;
// less than
case LT:
wrapper.lt(columnName, value);
break;
// less and equal
case LE:
wrapper.le(columnName, value);
break;
// in
case IN:
if (value instanceof Object[]) {
if (((Object[]) value).length > 0) {
wrapper.in(columnName, (Object[]) value);
} else {
log.warn("{} is empty, skip", fieldName);
}
} else if (value instanceof Collection) {
if (!((Collection<?>) value).isEmpty()) {
wrapper.in(columnName, (Collection<?>) value);
} else {
log.warn("{} is empty, skip", fieldName);
}
}
break;
// not in
case NOT_IN:
if (value instanceof Object[]) {
if (((Object[]) value).length > 0) {
wrapper.notIn(columnName, (Object[]) value);
} else {
log.warn("{} is empty, skip", fieldName);
}
} else if (value instanceof Collection) {
if (!((Collection<?>) value).isEmpty()) {
wrapper.notIn(columnName, (Collection<?>) value);
} else {
log.warn("{} is empty, skip", fieldName);
}
}
break;
// is null
case IS_NULL:
wrapper.isNull(columnName);
break;
// is not null
case IS_NOT_NULL:
wrapper.isNotNull(columnName);
break;
// between
case BETWEEN:
if (value instanceof Object[]) {
Object[] arr = (Object[]) value;
if (arr.length == 2) {
wrapper.between(columnName, arr[0], arr[1]);
} else {
log.warn("{} is invalid, skip", fieldName);
}
} else if (value instanceof Collection) {
Collection<?> col = (Collection<?>) value;
if (col.size() == 2) {
Iterator<?> iterator = col.iterator();
wrapper.between(columnName, iterator.next(), iterator.next());
} else {
log.warn("{} is invalid, skip", fieldName);
}
} else {
log.warn("{} is invalid, skip", fieldName);
}
break;
// not between
case NOT_BETWEEN:
if (value instanceof Object[]) {
Object[] arr = (Object[]) value;
if (arr.length == 2) {
wrapper.notBetween(columnName, arr[0], arr[1]);
} else {
log.warn("{} is invalid, skip", fieldName);
}
} else if (value instanceof Collection) {
Collection<?> col = (Collection<?>) value;
if (col.size() == 2) {
Iterator<?> iterator = col.iterator();
wrapper.notBetween(columnName, iterator.next(), iterator.next());
} else {
log.warn("{} is invalid, skip", fieldName);
}
} else {
log.warn("{} is invalid, skip", fieldName);
}
break;
// like
case LIKE:
wrapper.like(columnName, value);
break;
// like left
case LIKE_LEFT:
wrapper.likeLeft(columnName, value);
break;
// like right
case LIKE_RIGHT:
wrapper.likeRight(columnName, value);
break;
// not like
case NOT_LIKE:
wrapper.notLike(columnName, value);
break;
// not like left
case NOT_LIKE_LEFT:
wrapper.notLikeLeft(columnName, value);
break;
// not like right
case NOT_LIKE_RIGHT:
wrapper.notLikeRight(columnName, value);
break;
default:
break;
}
}

return wrapper;
}

private static List<Condition> parseConditions(boolean queryCustom, Object query) {
String conditions = null;
if (queryCustom) {
if (query instanceof BaseQueryDto) {
conditions = ((BaseQueryDto) query).getConditions_();
} else if (query instanceof ConditionBaseEntity) {
conditions = ((ConditionBaseEntity<?>) query).getConditions_();
}
}
return conditions == null ? new LinkedList<>() : parseConditions(conditions);
}

/**
* 解析自定义查询条件
*
* @param conditions 自定义查询条件(可能多个) 示例:username:likeLeft:yeshi;password:eq:123456
* @return List<Condition>
*/
private static List<Condition> parseConditions(String conditions) {
List<Condition> list = new LinkedList<>();
if (StrUtil.isBlank(conditions)) {
log.debug("conditions is blank, ignore");
return list;
}
String[] arr = conditions.split(";");
for (String s : arr) {
String[] arr2 = s.split(":");
if (arr2.length != 3) {
log.warn("condition [{}] is invalid, ignore", s);
continue;
}
if (StrUtil.isBlank(arr2[0])) {
log.warn("condition [{}] -> fieldName is blank, ignore", s);
continue;
}
QueryField.Type operator = QueryField.Type.of(arr2[1]);
if (operator == null) {
log.warn("condition [{}] -> operator is invalid, ignore", s);
continue;
}

list.add(new Condition(arr2[0], operator, arr2[2]));
}
return list;
}

@Data
@AllArgsConstructor
private static class Condition {
private String property;
QueryField.Type operator;
Object value;
}
}

以上工具方法根据查询类中字段@QueryField注解添加相应的查询条件到QueryWrapper;
以及支持解析自定义查询条件字段conditions_,添加查询条件到QueryWrapper;
以及支持实体类默认equal匹配方式,添加查询条件到QueryWrapper;


代码使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 提供基础的CRUD接口
*
* @param <E> 实体类
* @param <M> BaseMapper
* @param <S> ServiceImpl
*/
@Slf4j
@RequiredArgsConstructor
public class CrudController<E extends BaseEntity<E>, M extends BaseMapper<E>, S extends ServiceImpl<M, E>>
extends BaseController {

private final S service;

/**
* CRUD-查询
*/
@GetMapping("/crud/query")
public R<Page<E>> crudQuery(Page<E> page, E query) {
@SuppressWarnings("unchecked")
Class<E> clazz = (Class<E>) query.getClass();
return R.ok(service.page(page, QueryHelper.getQueryWrapper(query, clazz)));
}
}

以上为实体查询类场景,需要指定Class

1
2
3
public Page<SysUserEntity> query(Page<SysUserEntity> page, SysUserQueryDto dto) {
return super.page(page, QueryHelper.getQueryWrapper(dto));
}

以上为自定义查询类场景

后续#

  • conditions_方式支持数组/集合类字段查询

安装WordPress

记于:2024-10-27 晚上
地点:浙江省·温州市·家里
天气:阴天

背景#

随便玩玩先

过程#

准备环境#

根据【参考资料】中的【环境要求】,先进行环境的准备;

安装PHP#

1
2
3
4
5
6
7
8
# apt update
# apt install -y php php-fpm php-mysql

# php -v
PHP 8.1.2-1ubuntu2.19 (cli) (built: Sep 30 2024 16:25:25) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.2, Copyright (c) Zend Technologies
with Zend OPcache v8.1.2-1ubuntu2.19, Copyright (c), by Zend Technologies

安装mysql#

参考本博客文章【docker形式安装mysql8 】

https#

暂略

安装nginx#

1
# apt install -y nginx

配置与安装#

配置wordpress与nginx,以及php

配置wordpress#

下载wordpress,解压,示例:/var/www/html/wordpress
复制wp-config-sample.phpwp-config.php,并修改数据库连接信息,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...略

// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );

/** Database username */
define( 'DB_USER', 'root' );

/** Database password */
define( 'DB_PASSWORD', '123456' );

/** Database hostname */
define( 'DB_HOST', '127.0.0.1' );

/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );

/** The database collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', 'utf8mb4_general_ci' );

...略

创建数据库wordpress,数据库名称跟上面的DB_NAME一致;

配置nginx#

/etc/nginx/conf.d/example.conf,示例:

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
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;

root /var/www/html/wordpress;

# wordpress
set $wp_root "/var/www/html/wordpress";
location / {
root $wp_root;
index index.php index.html index.htm;
try_files $uri $uri/ /wordpress/index.php?$args;
}

location ~* ^/wordpress/.*\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
root $wp_root;
expires max;
log_not_found off;
}

# pass PHP scripts to FastCGI server
location ~ \.php$ {
include snippets/fastcgi-php.conf;

# With php-fpm (or other unix sockets):
fastcgi_pass unix:/run/php/php-fpm.sock;
# With php-cgi (or other tcp sockets):
#fastcgi_pass 127.0.0.1:9000;
}
}

注意:nginx到php的连接方式有两种,一种是unix socket,一种是tcp socket;
如果使用unix socket,需要在php-fpm的配置文件中配置listen = /run/php/php-fpm.sock
如果使用tcp socket,需要在php-fpm的配置文件中配置listen = 127.0.0.1:9000
php-fpm配置文件位置示例:/etc/php/8.1/fpm/pool.d/www.conf
修改后重启php-fpm,示例:systemctl restart php8.1-fpm.service
最终重新加载nginx配置:nginx -s reload

安装wordpress#

配置好上述内容后,访问wordpress的安装页面,示例:https://example.com/wp-admin/install.php

问题与解决#

wordpress连接数据库失败#

1
2
3
4
5
6
7
8
9
Warning:  mysqli_real_connect(): (HY000/2002): No such file or directory in /var/www/html/wordpress/wp-includes/class-wpdb.php on line 1982

No such file or directory
Error establishing a database connection
This either means that the username and password information in your wp-config.php file is incorrect or that contact with the database server at localhost could not be established. This could mean your host’s database server is down.
- Are you sure you have the correct username and password?
- Are you sure you have typed the correct hostname?
- Are you sure the database server is running?
If you are unsure what these terms mean you should probably contact your host. If you still need help you can always visit the WordPress support forums.

由于mysql是docker安装,当wp-config.php中DB_HOST为localhost时,mysql会尝试通过unix套接字文件连接(通常是 /var/run/mysqld/mysqld.sock),而/var/run/mysqld/mysqld.sock文件未做映射,导致连接失败;
解决方法:将DB_HOST修改为127.0.0.1,从而使用tcp socket连接;

安装主题权限不足#

弹出ftp连接框,提示需要输入ftp的用户名和密码;
解决方法:修改wordpress的文件夹权限,示例:chown -R www-data:www-data /var/www/html/wordpress
www-data是nginx的运行用户,需要将wordpress的文件夹所有者修改为nginx的运行用户;

其他#

wordpress调试信息#

wp-config.php中打开调试开关:

1
define( 'WP_DEBUG', true );

这样可以在页面输出错误信息;

参考资料#

Docker形式安装MySQL8

记于:2024-09-22 下午
地点:浙江省·温州市·家里
天气:雨天

背景#

项目需要,以docker形式安装MySQL8

命令#

1
2
3
4
5
docker run --name=mysql8 \
-p 3306:3306 \
--mount type=bind,src=/root/ysm.d/data/docker/mysql/conf/my.cnf,dst=/etc/my.cnf \
--mount type=bind,src=/root/ysm.d/data/docker/mysql/datadir,dst=/var/lib/mysql \
--restart always -d container-registry.oracle.com/mysql/community-server:8.4

配置文件和数据目录需要提前创建好
mkdir -p /root/ysm.d/data/docker/mysql/datadir -p /root/ysm.d/data/docker/mysql/conf
/root/ysm.d/data/docker/mysql/conf/my.cnf 内容如下(拷贝自mysql8.4默认配置):

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
# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/8.4/en/server-configuration-defaults.html

[mysqld]
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
#
# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin
#
# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M

host-cache-size=0
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
secure-file-priv=/var/lib/mysql-files
user=mysql

pid-file=/var/run/mysqld/mysqld.pid

启动后查看日志:

1
docker logs mysql8

获取初始密码,登录后进行修改

1
2
3
4
5
6
7
8
# 执行容器内mysql登录
docker exec -it mysql8 mysql -uroot -p
# 更新root密码
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password' ;
# 更新mysql.user表,将root用户的host字段改为%,最后刷新权限
use mysql;
update user set host = '%' where user = 'root' ;
flush privileges;

参考资料#

点阵图Java实现

记于:2024-07-14 晚上
地点:浙江省·温州市·家里
天气:多云

背景#

因项目需要,需要参考项目PHP版本中的点阵图生成功能,实现Java版本逻辑。

功能#

1.生成点阵图(画布)
2.绘制文本
3.绘制图片
4.绘制二维码、条形码
5.其他,略

实现思路#

主要是构建一个(二维数组)点阵模型,在其上根据坐标绘制各种素材(文本、图片、二维码、条形码);
在Java中可以使用java.atw.image.BufferedImage类来表示点阵模型(画布),使用Graphics2D类来绘制各种素材;
每种类型素材生成并返回一个BufferedImage对象,然后将其绘制到画布上。

绘制素材#

绘制二维码、条形码以及自定义图片都相对简单,最麻烦的是绘制文本,因为点阵形式下需要自行解析字体文件,并计算生成每个字符的点阵数据;
根据参考项目(LatticePHP)中的说明,是因为:普通的字体因为加了锐角、美化是不能直接用来生成点阵图的,必须使用点阵字体。但是这方面的市场非常小,所以做的人很少,仅有的几个还会收取高昂费用。

绘制图片素材#

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public BufferedImage generateImage(String url) {
log.info("generateImage...url: {}", url);

URL url0;
try {
url0 = new URL(url);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
BufferedImage bufferedImage;
try {
bufferedImage = ImageIO.read(url0);
} catch (IOException e) {
throw new RuntimeException(e);
}
return bufferedImage;
}

直接从网络地址读取图片,生成BufferedImage对象。

绘制二维码、条形码#

用到了zxing库,示例代码:

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
/**
* 生成二维码
*/
public BufferedImage generateQrCode(String text, int width, int height) {
log.info("generateQrCode...text: {}, width: {}, height: {}", text, width, height);

// 生成二维码BitMatrix
BitMatrix bitMatrix;
try {
// 不能按指定大小生成,先按自动大小生成,在convertToDotMatrixImage方法中缩放
bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, 0, 0, this.hints);
} catch (WriterException e) {
throw new RuntimeException(e);
}
// 转换为点阵图像
return this.convertToDotMatrixImage(bitMatrix, width, height);
}

/**
* 生成条形码
*/
public BufferedImage generateBarCode(String text, int width, int height) {
log.info("drawBarCode...text: {}, width: {}, height: {}", text, width, height);

// 生成条形码BitMatrix
BitMatrix bitMatrix = new Code128Writer().encode(text, BarcodeFormat.CODE_128, 0, 0);
// 转换为点阵图像
return this.convertToDotMatrixImage(bitMatrix, width, height);
}

/**
* 将BitMatrix转换为点阵图像BufferedImage
* 生成二维码场景,当指定90x90,会生成49x49(bitMatrix尺寸是90x90但是内容是49x49居中)的二维码,需要进行缩放
*/
private BufferedImage convertToDotMatrixImage(BitMatrix bitMatrix, int desiredWidth, int desiredHeight) {
// 缩放二维码
BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
BufferedImage scaledImage = new BufferedImage(desiredWidth, desiredHeight, BufferedImage.TYPE_BYTE_BINARY);
Graphics2D g = scaledImage.createGraphics();
g.drawImage(qrImage, 0, 0, desiredWidth, desiredHeight, null);
g.dispose();

return scaledImage;
}

遇到的问题:生成二维码时,按指定大小(90x90)生成不能生效;
原因:略,见参考资料(二维码尺寸问题);
解决:先按自动大小生成,然后在convertToDotMatrixImage方法按指定大小缩放。

绘制文本素材#

示例代码:

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
    /**
* 生成文本图像
*/
private BufferedImage generateTextImage(String text) throws IOException {
// 获取字体文件对象
// RandomAccessFile fontFile = this.getFontFile(this.fontFileName);

int fontWidth = this.fontWidth;
int fontHeight = this.fontHeight;
int byteCount = this.byteCount;

int textLength = text.length();

// list of char[][] 存放每个字符的点阵数据
ArrayList<char[][]> dotMatList = new ArrayList<>(textLength);

// 绘制每个字符
for (int i = 0; i < textLength; i++) {
char word = text.charAt(i);

// 获取字符码点
int codePoint = this.getCodePoint(String.valueOf(word));
// 修正24宽度字体的码点 ; 原因未知,copy自php版逻辑
codePoint = this.fixCodePointWhenFontWidth24(codePoint);
// 计算在字体文件中的位置
int position = codePoint * byteCount;
// 获取字体数据
// byte[] fontData = this.getFontData(fontFile, position, byteCount);
// fix: resources下的文件在打成jar后使用(RandomAccess)File读取失败,改为直接加载byte[]缓存到内存
byte[] fontData = this.readFontData(this.fontFileName, position, byteCount);
// convert to mat
char[][] dotMat = this.convertBytesToDotMat(fontData, fontHeight, fontWidth);

// 获取字符实际宽度
int realWidth = codePoint < 700 ? this.fontEn : this.fontZh;
// 按实际宽度切割重组
for (int j = 0; j < fontHeight; j++) {
// 自动宽度逻辑
realWidth = this.autoWidth ? this.getCharWidth(dotMat, this.fontZh, this.fontWidth) : realWidth;
// 空格间距逻辑
realWidth = codePoint == this.SPACE_CODE_POINT ? this.SPACE_WIDTH : realWidth;
// 按实际宽度切割
char[] row = dotMat[j];
char[] newRow = new char[realWidth];
System.arraycopy(row, 0, newRow, 0, realWidth);
// 加粗逻辑
newRow = this.setBold(newRow, this.bold);
// 字间距逻辑
newRow = this.setSpacing(newRow, this.fontSpace);

dotMat[j] = newRow;
}
// dotMat根据最长的长度补齐尾部
this.padDotMat(dotMat);

// 打印
// this.printDotMat(dotMat);
// 加入集合
dotMatList.add(dotMat);
}

// 整合所有dotMat from dotMatList
char[][] finalDotMat = this.mergeDotMat(dotMatList);

// 保存文本点阵宽度和高度
this.textBitWidth = finalDotMat[0].length;
this.textBitHeight = finalDotMat.length;

// 将所有dotMat绘制到图像
BufferedImage textImage = new BufferedImage(finalDotMat[0].length, fontHeight, BufferedImage.TYPE_BYTE_BINARY);
this.renderDotMatToImage(finalDotMat, textImage, 0, 0);

// fontFile.close();

return textImage;
}

/**
* 获取码点
*/
private int getCodePoint(String str) {
int codePoint = 0;
try {
// 获取字符串的代码点
codePoint = str.codePointAt(0);
} catch (IndexOutOfBoundsException e) {
// 处理字符串为空或其他异常情况
e.printStackTrace();
}
// 字符越界处理
if (codePoint > 65535) {
codePoint = 0;
}
return codePoint;
}

/**
* 修正24宽度字体的码点
* 将超出范围的码点修正为空格码点
*/
private int fixCodePointWhenFontWidth24(int codePoint) {
if (this.fontWidth == 24) {
if (codePoint <= this.SPACE_CODE_POINT || codePoint > this.fontMaxPosition) {
codePoint = this.SPACE_CODE_POINT;
}
}
return codePoint;
}

/**
* 转换字节数组为点阵多维数组
*/
private char[][] convertBytesToDotMat(byte[] fontData, int fontHeight, int fontWidth) {
// fontData bit -> char[]
char[] dots = new char[fontHeight * fontWidth];
for (int i = 0; i < fontData.length; i++) {
byte b = fontData[i];
for (int bit = 7; bit >= 0; bit--) {
int bitValue = (b >> bit) & 1;
dots[i * 8 + (7 - bit)] = bitValue == 1 ? '1' : '0';
}
}
// char[] -> char[][] by fontWidth
char[][] dotMat = new char[fontHeight][fontWidth];
for (int i = 0; i < fontHeight; i++) {
System.arraycopy(dots, i * fontWidth, dotMat[i], 0, fontWidth);
}
return dotMat;
}

/**
* 加粗逻辑
*/
private char[] setBold(char[] dot, boolean fontBold) {
if (!fontBold) {
return dot;
}

char[] result = new char[dot.length];
for (int i = 0; i < dot.length; i++) {
char current = dot[i];
char shifted = i == 0 ? '0' : dot[i - 1]; // 前一个字符,用'0'补齐
result[i] = (current == '1' || shifted == '1') ? '1' : '0';
}

return result;
}

/**
* 字间距逻辑
*/
private char[] setSpacing(char[] dotArray, int spacing) {
if (spacing <= 0) {
return dotArray; // 如果 spacing <= 0,直接返回原数组
}

char[] result = new char[dotArray.length + spacing];
int index = 0;

for (char c : dotArray) {
result[index++] = c;
}

// 添加字间距
for (int i = 0; i < spacing; i++) {
result[index++] = '0';
}

return result;
}

/**
* 对齐点阵尾部
*/
private void padDotMat(char[][] dotMat) {
// 先获取最长的长度
int maxRowLength = 0;
for (char[] row : dotMat) {
maxRowLength = Math.max(maxRowLength, row.length);
}
// dotMat根据最长的长度补齐尾部
for (int k = 0; k < dotMat.length; k++) {
char[] row = dotMat[k];
char[] newRow = new char[maxRowLength];
System.arraycopy(row, 0, newRow, 0, row.length);
for (int j = row.length; j < maxRowLength; j++) {
newRow[j] = '0';
}
dotMat[k] = newRow;
}
}

/**
* 合并多个点阵
*/
private char[][] mergeDotMat(ArrayList<char[][]> dotMatList) {
// 计算最终宽度
int finalWidth = this.calcFinalWidth(dotMatList);
int fontHeight = this.fontHeight;
char[][] finalDotMat = new char[fontHeight][finalWidth];
int columnIndex = 0;
for (char[][] chars : dotMatList) {
int rowIndex = 0;
for (char[] aChar : chars) {
System.arraycopy(aChar, 0, finalDotMat[rowIndex++], columnIndex, aChar.length);
}
columnIndex += chars[0].length;
}
return finalDotMat;
}

/**
* 计算最终宽度
*/
private int calcFinalWidth(ArrayList<char[][]> dotMatList) {
int finalWidth = 0;
for (char[][] dotMat : dotMatList) {
finalWidth += dotMat[0].length;
}
return finalWidth;
}

/**
* 将dotMat绘制到图像
*/
private void renderDotMatToImage(char[][] dotMat, BufferedImage image, int x, int y) {
for (int j = 0; j < dotMat.length; j++) {
char[] row = dotMat[j];
for (int k = 0; k < row.length; k++) {
char bitValue = dotMat[j][k];
image.setRGB(x + k, y + j, bitValue == '1' ? Color.BLACK.getRGB() : Color.WHITE.getRGB());
}
}
}

加载/读取字体数据相关代码:

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
private final Map<String, byte[]> fontFileDataMap = new ConcurrentHashMap<>();

private byte[] readFontData(String fontFileName, int position, int byteCount) throws IOException {
byte[] fontFileData = fontFileDataMap.get(fontFileName);
if (fontFileData == null) {
fontFileData = this.loadFontFileData(fontFileName);
}

byte[] fontData = new byte[byteCount];
System.arraycopy(fontFileData, position, fontData, 0, byteCount);
return fontData;
}

private synchronized byte[] loadFontFileData(String resourcePath) throws IOException {
// 检查并发场景下其他线程是否已加载,避免重复加载
byte[] fontFileData = fontFileDataMap.get(fontFileName);
if (fontFileData != null) {
return fontFileData;
}

try (InputStream inputStream = LatticeText.class.getClassLoader().getResourceAsStream(resourcePath);
ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
if (inputStream == null) {
throw new IOException("Resource not found: " + resourcePath);
}

byte[] data = new byte[1024];
int bytesRead;

// Read the input stream into the byte array output stream
while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, bytesRead);
}

// Return the byte array
fontFileData = buffer.toByteArray();
fontFileDataMap.put(fontFileName, fontFileData);
return fontFileData;
}
}

以上代码省略了属性和部分方法,主要流程如下:
1.遍历文本,准备获取每个字符的点阵数据
2.获取字符码点
3.根据码点和每个字符点阵数据的字节数,计算在字体文件中的位置
4.读取/加载字体文件/数据
5.将字节数组转换为点阵多维数组,每个比特用一个char(‘0’|’1’)表示
6.根据码点值确认中英文,再处理加粗、间距等逻辑
7.补齐点阵数据宽度,加入集合
8.向右合并所有点阵数据,生成最终点阵数据
9.将点阵数据绘制到图像并返回

遇到的问题:
由于打包后文件布局发生变化,目标文件是在jar包中而不是直接在文件系统中,所以无法直接使用RandomAccessFile读取;
解决:
直接以resource方式读取字体数据到内存并缓存;

效果示例:
点阵图

参考资料#

使用FFmpeg获取音频时长

记于:2024-05-06 晚
地点:浙江省·温州市·家里
天气:多云

背景#

业务需求上需要使用ffmpeg转码并获取其识别的音频时长。

代码#

命令示例:

1
ffmpeg -y -hide_banner -i input.wav output.wav

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[aist#0:0/pcm_s16le @ 0x122f06af0] Guessed Channel Layout: mono
Input #0, wav, from 'input.wav':
Metadata:
encoder : Lavf61.1.100
Duration: 00:00:07.86, bitrate: 256 kb/s
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 16000 Hz, mono, s16, 256 kb/s
Stream mapping:
Stream #0:0 -> #0:0 (pcm_s16le (native) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, wav, to 'output.wav':
Metadata:
ISFT : Lavf61.1.100
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 16000 Hz, mono, s16, 256 kb/s
Metadata:
encoder : Lavc61.3.100 pcm_s16le
[out#0/wav @ 0x6000002c8180] video:0KiB audio:246KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: 0.031011%
size= 246KiB time=00:00:07.86 bitrate= 256.1kbits/s speed=7.04e+03x

Duration: 00:00:07.86 即为音频时长。

接下来使用Java代码调用 ffmpeg 命令,并解析输出信息获取音频时长。

代码示例:

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
/**
* 转码并返回音频时长
*/
public Double transcoding(String ffmpegPath, String input, String output) {
String info = output + ".info";
FileUtil.touch(info);
String[] ffmpegCommand = {
ffmpegPath,
"-hide_banner",
"-y",
// ... 其他参数
"-i", input, output,
"2>&1 | cat >", info
};
List<String> listCommand = CollUtil.newArrayList(ffmpegCommand);

String finalCommandStr = StrUtil.join(" ", listCommand);
log.info("finalCommandStr: {}", finalCommandStr);

int exitCode = -1;
try {
// ProcessBuilder pb = new ProcessBuilder(listCommand);
// 如果有重定向的逻辑,一定要这样调用!!!
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", String.join(" ", listCommand));
Process process = pb.inheritIO().start();
exitCode = process.waitFor();
process.destroy();
} catch (Exception e) {
e.printStackTrace();
}

log.info("exitCode: {}", exitCode);

// 获取音频时长
Double length = this.getAudioLength(info);
FileUtil.del(info);

return length;
}
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
/**
* 获取音频时长
*/
private Double getAudioLength(String infoFile) {
Double durationInSeconds = null; // 初始化为 null

// 读取infoFile,提取音频时长信息
List<String> lines = FileUtil.readLines(infoFile, "UTF-8");
for (String line : lines) {
if (line.contains("Duration:")) {
// 提取包含 "Duration:" 的行
String durationLine = line.trim();
String durationPart = durationLine.split("Duration:")[1].trim().split(",")[0].trim();

// 解析时长信息,转换为 Double,精确到毫秒
String[] timeParts = durationPart.split(":");
double hours = Double.parseDouble(timeParts[0]);
double minutes = Double.parseDouble(timeParts[1]);
double seconds = Double.parseDouble(timeParts[2]);
double milliseconds = (hours * 3600 + minutes * 60 + seconds) * 1000;

durationInSeconds = milliseconds / 1000.0; // 转换为秒
}
}
return durationInSeconds;
}

注意:

  • ffmpeg命令的输出信息是输出到标准错误流的,所以将标准错误重定向到标准输出流,然后通过管道配合cat命令将标准输出流输出到文件中;(尝试过使用process.getOutputStream()和process.getErrorStream(),但是无法获取到输出信息,所以使用了重定向的方式)
  • 输出文件的中间目录如果不存在,需要提前创建;

补充(2024-05-20):ffprobe命令专门用于检测多媒体文件的信息。

JavaJNI调用C/C++代码

记于:2024-04-14
地点:浙江省·温州市·家里
天气:阴雨

背景#

因为一个项目需要Java JNI调用C++ SDK,只有编译好的Windows DLL,没有对应的源码。

测试#

简单demo#

JniTest.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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
try {
String path = "C:\\Users\\ysd\\IdeaProjects\\test\\dll";
// 无效
// System.setProperty("java.library.path", path);
// 需要使用这种方式
// 参考:https://fahdshariff.blogspot.com/2011/08/changing-java-library-path-at-runtime.html
addLibraryPath(path);
} catch (Exception e) {
throw new RuntimeException(e);
}

System.loadLibrary("jnitest");
}

/**
* Adds the specified path to the java library path
*
* @param pathToAdd the path to add
* @throws Exception
*/
public static void addLibraryPath(String pathToAdd) throws Exception {
final Field usrPathsField = ClassLoader.class.getDeclaredField("usr_paths");
usrPathsField.setAccessible(true);

// get array of paths
final String[] paths = (String[]) usrPathsField.get(null);

// check if the path to add is already present
for (String path : paths) {
if (path.equals(pathToAdd)) {
return;
}
}

//add the new path
final String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
newPaths[newPaths.length - 1] = pathToAdd;
usrPathsField.set(null, newPaths);
}

// ================================================================================

// 本地方法
private static native String sayHello(String name);

public static void main(String[] args) {
String result = sayHello("ysm");
System.out.println("result: " + result);
}
}

生成本地方法对应的C++代码的头部文件;
可以使用javah或javac(jdk版本>=9)命令;
com_yeshimin_test_jni_JniTest.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yeshimin_test_JniTest */

#ifndef _Included_com_yeshimin_test_jni_JniTest
#define _Included_com_yeshimin_test_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

接下需要根据头部文件中函数定义进行实现
JniTest.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "com_yeshimin_test_jni_JniTest.h"
// <jni.h>里已包含<stdio.h>

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *env, jclass cls, jstring j_str) {

const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

编译

1
2
3
4
5
6
# 编译JniTest.c,输出jnitest.dll,并将其放到自定义dll目录下
# 我的自定义dll目录为C:\Users\ysd\IdeaProjects\test\dll
gcc -m64 -shared -o jnitest.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTest.c

# jni需要jni.h相关头部,这里需要指定头部所在目录;具体位置因系统和jdk版本而异
# -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32"

运行

1
result: Hello, ysm

不同包下#

场景为,调用点所在的包(和类)与目标本地代码的声明不一致时,调用会报错;
!!!重要前提,场景假设为,目标代码是编译好的dll;直接源码编译到一起的不算~

注意,JniTest.java和JniTestInternal.c是在不同包下,JniTestInternal是目标代码,JniTest是调用类

JniTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
// 略...

System.loadLibrary("jnitestinternal");
}

// 本地方法
private static native String sayHelloInternal(String name);

public static void main(String[] args) {
String result = sayHelloInternal("ysm");
System.out.println("result: " + result);
}
}

org_sample_JniTestInternal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_sample_JniTest */

#ifndef _Included_org_sample_JniTestInternal
#define _Included_org_sample_JniTestInternal
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

JniTestInternal.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "org_sample_JniTestInternal.h"

/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *env, jclass cls, jstring j_str) {
const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

编译

1
2
# 编译JniTestInternal.c,输出jnitestinternal.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitestinternal.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTestInternal.c

运行

1
result: Hello, ysm

如果将类名修改,使得调用点所在包(和类名)与目标定义不一致,比如将类名修改为”OtherName”,编译运行后,将会报错:

1
2
3
Exception in thread "main" java.lang.UnsatisfiedLinkError: org.sample.OtherName.sayHelloInternal(Ljava/lang/String;)Ljava/lang/String;
at org.sample.OtherName.sayHelloInternal(Native Method)
at org.sample.OtherName.main(OtherName.java:60)

解决方案
使用“代理”的方式调用目标方法;即在同包名(和类名)对应的本地实现中包含目标头部,并且封装一个方法进行目标调用;具体见下面代码

JniTest.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
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
// 略...

System.loadLibrary("jnitest");
System.loadLibrary("jnitestinternal"); // 依赖的dll也要加载
}

private static native String sayHello(String name);

private static native String sayHelloByProxy(String name);

private static native String sayHelloInternal(String name);

public static void main(String[] args) {

// 调用同包名下的本地方法;执行成功
String result = sayHello("ysm");
System.out.println("result: " + result);

// “代理”方式下,调用不同包名下的本地方法;执行成功
String result2 = sayHelloByProxy("ysm");
System.out.println("result2: " + result2);

// 调用不同包名下的本地方法;执行报错
String result3 = sayHelloInternal("ysm");
System.out.println("result3: " + result3);
}
}

com_yeshimin_test_jni_JniTest.h

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
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yeshimin_test_JniTest */

#ifndef _Included_com_yeshimin_test_jni_JniTest
#define _Included_com_yeshimin_test_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *, jclass, jstring);

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHelloByProxy
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHelloByProxy
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

JniTest.c

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
#include "com_yeshimin_test_jni_JniTest.h"
#include "org_sample_JniTestInternal.h"

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *env, jclass cls, jstring j_str) {

const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHelloByProxy
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHelloByProxy
(JNIEnv *env, jclass cls, jstring j_str) {

return Java_org_sample_JniTestInternal_sayHelloInternal(env, cls, j_str);
}

org_sample_JniTestInternal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_sample_JniTest */

#ifndef _Included_org_sample_JniTestInternal
#define _Included_org_sample_JniTestInternal
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

JniTestInternal.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "org_sample_JniTestInternal.h"

/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *env, jclass cls, jstring j_str) {
const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

编译

1
2
3
4
5
6
7
# 编译JniTestInternal.c,输出jnitestinternal.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitestinternal.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTestInternal.c

# 编译JniTest.c,输出jnitest.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitest.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" .jni\JniTest.c

# -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" // 用于指定依赖的dll

运行

1
2
3
4
5
6
7
result: Hello, ysm

result2: Hello, ysm

Exception in thread "main" java.lang.UnsatisfiedLinkError: com.yeshimin.test.jni.JniTest.sayHelloInternal(Ljava/lang/String;)Ljava/lang/String;
at com.yeshimin.test.jni.JniTest.sayHelloInternal(Native Method)
at com.yeshimin.test.jni.JniTest.main(JniTest.java:69)

调用dll#

场景跟上面【不同包下】类似,区别为目标是第三方sdk中的dll文件,且没有头部文件

JniTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
// 略...

System.loadLibrary("jnitest");
System.loadLibrary("jnitestinternal"); // 依赖的dll也要加载
}

private static native int dynamicInvoke();

public static void main(String[] args) {
int intResult = dynamicInvoke();
System.out.println("intResult: " + intResult);
}
}

com_yeshimin_test_jni_JniTest.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yeshimin_test_JniTest */

#ifndef _Included_com_yeshimin_test_jni_JniTest
#define _Included_com_yeshimin_test_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yeshimin_test_jni_JniTest
* Method: dynamicInvoke
* Signature: (Ljava/lang/Integer;)Ljava/lang/Integer;
*/
JNIEXPORT jint JNICALL Java_com_yeshimin_test_jni_JniTest_dynamicInvoke
(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

JniTest.c

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
#include <windows.h>
#include <stdbool.h>
#include "com_yeshimin_test_jni_JniTest.h"
#include "org_sample_JniTestInternal.h"

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: dynamicInvoke
* Signature: (Ljava/lang/Integer;)Ljava/lang/Integer;
*/
JNIEXPORT jint JNICALL Java_com_yeshimin_test_jni_JniTest_dynamicInvoke
(JNIEnv *env, jclass cls) {

typedef int (__stdcall *TargetMethodPtr)(); // 声明 TargetMethodPtr 类型,名称自定义
TargetMethodPtr targetMethod;

HMODULE hModule = LoadLibrary(TEXT("Log_MD_VC120_v3_1.dll")); // 加载DLL
if (hModule != NULL) {
targetMethod = (TargetMethodPtr)GetProcAddress(hModule, "?ConfigureFromEnvironment@CLog@GenICam_3_1@@SA_NXZ"); // 获取函数地址
if (targetMethod != NULL) {
printf("function found");
bool result = targetMethod(); // 调用DLL函数
FreeLibrary(hModule); // 释放DLL模块
return result ? 1 : 0;
} else {
printf("function not found");
}
FreeLibrary(hModule); // 释放DLL模块
} else {
// getLastError
DWORD dwError = GetLastError();
printf("Error code: %lu\n", dwError);
}
return -1; // 返回 NULL 表示失败
}

编译

1
2
3
4
5
6
7
# 编译JniTestInternal.c,输出jnitestinternal.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitestinternal.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTestInternal.c

# 编译JniTest.c,输出jnitest.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitest.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" .jni\JniTest.c

# -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" // 用于指定依赖的dll

运行
将上面两个dll以及用到的第三方dll及其依赖dll放到项目根目录下才行正常运行,原因未知,见【遇到的问题】第3点

1
intResult: 1

遇到的问题#

1.提示32位代码在64位架构下跑不通;是下载错了mingw架构,应该下载64位的
2.指定自定义dll目录无效;解决方法见上面【简单demo】代码
3.在【调用dll】测试过程中,dll文件放到自定义dll目录下,链接不到,放到项目根目录下就可以,暂时搞不清楚
4.在【调用dll】测试过程中,一直获取不到目标函数;
开始是通过dllexp工具查看的函数函数名称为 public: static bool __cdecl GenICam_3_1::CLog::ConfigureFromEnvironment(void),
也试过 GenICam_3_1::CLog::ConfigureFromEnvironment(void) 和 GenICam_3_1::CLog::ConfigureFromEnvironment,都不行
后来是通过visual studio的dumpbin工具查看到dll的正确函数名 ?ConfigureFromEnvironment@CLog@GenICam_3_1@@SA_NXZ

参考资料#

腾讯企业自定义域名邮箱发送邮件到谷歌Gmail失败

记于:2024-04-10 晚上
地点:浙江省·温州市·家里
天气:晴天

背景#

我的邮箱域名是绑定到腾讯企业邮箱的,今天发了一封邮件到一个gmail邮箱,被退回了,退信原因如下:


很抱歉您发送的邮件被退回,以下是该邮件的相关信息:
…省略…
退信原因 发件人(…省略…)域名的DNS记录未设置或设置错误导致对方拒收此邮件。
host gmail-smtp-in.l.google.com[74.125.23.26] said: 550-5.7.26 This mail has been blocked because the sender is unauthenticated. Gmail requires all senders to authenticate with either SPF or DKIM. Authentication results: DKIM = did not pass SPF [yeshimin.com] with ip: [54.206.16.166] = did not pass For instructions on setting up authentication, go to https://support.google.com/mail/answer/81126#authentication y23-20020a17090264d700b001e3d3ac40a1si8808240pli.17 - gsmtp (in reply to end of DATA command)
解决方案 请通知你的邮箱管理员为邮箱域名设置正确的DNS(SPF、DKIM、DMARC)记录。详细请见 http://service.exmail.qq.com/cgi-bin/help?subtype=1&&no=1000580&&id=20012。

此外,您还可以 点击这里 获取更多关于退信的帮助信息。


根据以上信息得知,发现是因为邮箱域名缺少SPF、DKIM、DMARC记录导致的,所以需要在域名解析中添加这些记录。

解决过程#

1.添加SPF记录和DMARC记录#

参考:https://open.work.weixin.qq.com/help2/pc/19820?person_id=1&subtype=1&id=29&no=188

1.1 添加SPF记录#

添加一条TXT记录,主机名为@,记录值为:v=spf1 include:spf.mail.qq.com ~all

1.2 添加DMARC记录#

添加一条TXT记录,主机名为_dmarc,记录值为:v=DMARC1; p=none; rua=mailto:mailauth-reports@qq.com

2.添加DKIM记录#

2.1 以管理员身份进入腾讯企业邮箱后台
2.2 菜单【工具箱】 -> 【DKIM验证】
2.3 根据配置信息,在域名解析中添加相应TXT记录即可

微信小程序-同步登陆方案

记于:2024-03-11 上午
地点:浙江省·温州市·家里
天气:下雨

背景#

业务流程上需要等微信登陆(wx.login)后再执行后续操作,但是wx.login是异步的,如果简单地使用回调的方式,代码将会不简洁,同时也为了职责分离(将登陆与业务逻辑解耦),所以需要一个同步的方案。

思路#

根据网上的一些方案,比如使用Promise、async/await等,试过都失败了(可能是因为对这些技术不太熟);
最终选择从业务流程上入手,加一个加载页可以解决;
具体地:添加一个加载/入口(entry)页作为第一页面,在其中执行wx.login,成功则跳转到首页,如果失败则停留在加载页,并提示点击重新登陆。

代码#

entry.wxml

1
2
<!--pages/entry/entry.wxml-->
<button wx:if="{{initFail}}" bind:tap="onRetryTap" style="margin-top: 200px;">点击重试</button>

entry.js

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
// pages/entry/entry.js
const consts = require('@/common/consts/consts.js')
const https = require('@/common/utils/https.js')
const base64 = require('@/common/utils/base64.js')
Page({
/**
* 页面的初始数据
*/
data: {
initFail: false,
userId: undefined
},
// data
// ================================================================================
// lifecycle
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
console.log('entry.onLoad: {}', JSON.stringify(options))
this.init()
},
// ================================================================================
// methods

// 初始化
init() {
console.log('init...')

var that = this

that.setData({ initFail: false })

wx.showLoading({ title: '初始化...', mask: true })

// 静默登录
wx.login({
success: (res) => {
console.log('wx.login.success: ' + JSON.stringify(res))
if (res.code) {
// 执行业务登录
https.post({
url: consts.apis.WXMP_LOGIN,
data: { code: res.code },
ignoreAuth: true,
success: function (res) {
if (res.data.data && res.data.data.token) {
var token = res.data.data.token
var userId = that.parseUserId(token)
// 保存到全局变量
getApp().globalData.token = token
getApp().globalData.userId = userId
// 保存到本地
wx.setStorageSync('token', token)
wx.setStorageSync('userId', userId)

// 跳转到首页
that.jumpToIndex()
} else {
that.setData({initFail: true})
}
},
fail: function(e) {
console.log('wxLogin.fail: {}', JSON.stringify(e))
that.setData({initFail: true})
},
complete: function(e) {
console.log('wxLogin.complete: {}', JSON.stringify(e))
wx.hideLoading()
}
})
} else {
console.log('登录失败!' + res.errMsg)
that.setData({initFail: true})
}
},
})
},
// 跳转到首页
jumpToIndex(options) {
console.log('jumpToIndex...{}', JSON.stringify(options))
wx.switchTab({ url: '/pages/index/index' })
},
// 当点击重试
onRetryTap(e) {
console.log('onRetryTap...')
this.init()
},
// 解析用户ID
parseUserId(token) {
// ...
}
})

微信小程序-自定义导航栏组件

记于:2024-03-05 晚上
地点:浙江省·温州市·家里
天气:多云

背景#

微信官方提供的navigation-bar组件集成后布局有些错位,一直调整不好,索性自定义一个。

目标#

布局可控、功能可控即可;
目前主要是需要一个中间的标题和左边的一个返回;
然后布局上适配微信小程序的胶囊按钮;
如图:
导航栏

难点与思路#

唯一算是难点的地方就是和胶囊按钮的适配,也就是要达到让胶囊按钮在导航栏内垂直方向居中的效果;
参考了下网上的一些方案,思路大同小异,主要是通过系统状态栏和胶囊按钮的布局参数来计算导航栏的高度和位置;
首先在app.js中获取计算所需的信息,然后计算出导航栏参数,最终在使用的页面中根据参数渲染出来。

代码#

app.js

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
// app.js
App({
globalData: {
// 系统信息和胶囊信息
systemInfo: undefined,
menuButton: undefined,
// 状态栏信息
statusBarInfo: {
sysBottom: undefined,
sysHeight: undefined,
appBottom: undefined,
appHeight: undefined
}
},
onLaunch(options) {
console.log('app.onLaunch...{}', JSON.stringify(options))

this.globalData.systemInfo = wx.getSystemInfoSync()
// console.log('systemInfo...', JSON.stringify(this.globalData.systemInfo))
this.globalData.menuButton = wx.getMenuButtonBoundingClientRect()
// console.log('menuButton...', JSON.stringify(this.globalData.menuButton))

// 计算状态栏信息
this.globalData.statusBarInfo.sysHeight = this.globalData.systemInfo.statusBarHeight
this.globalData.statusBarInfo.sysBottom = this.globalData.systemInfo.screenTop + this.globalData.systemInfo.statusBarHeight

this.globalData.statusBarInfo.appHeight = (((this.globalData.menuButton.bottom + this.globalData.menuButton.top) / 2) - this.globalData.systemInfo.statusBarHeight) * 2
this.globalData.statusBarInfo.appBottom = this.globalData.statusBarInfo.sysBottom + this.globalData.statusBarInfo.appHeight
console.log('statusBarInfo...', JSON.stringify(this.globalData.statusBarInfo))
}
})

custom-navigation-bar.js

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
// components/custom-navigation-bar/custom-navigation-bar.js
Component({
/**
* 组件的属性列表
*/
properties: {
// 这里定义了innerText属性,属性值可以在组件使用时指定
innerText: {
type: String,
value: 'default value',
},
title: {
type: String,
value: undefined
},
back: {
type: Boolean,
value: false
},
backgroundColor: {
type: String,
value: 'none'
}
},
/**
* 组件的初始数据
*/
data: {
// 状态栏信息
statusBarInfo: {
sysBottom: undefined,
sysHeight: undefined,
appBottom: undefined,
appHeight: undefined,
title: undefined,
back: false,
backgroundColor: 'none'
}
},
lifetimes: {
/**
* 在组件实例进入页面节点树时执行
*/
attached: function() {
console.log('custom-navigation-bar.lifetimes.attached...')

// 计算
this.calc()
},
/**
* 在组件实例被从页面节点树移除时执行
*/
detached: function() {
console.log('custom-navigation-bar.lifetimes.detached...')
},
},
/**
* 组件的方法列表
*/
methods: {
/**
* 计算
*/
calc: function () {
console.log('custom-nav.calc...')
// 设置状态栏信息
var statusBarInfo = getApp().globalData.statusBarInfo
statusBarInfo.title = this.properties.title
statusBarInfo.back = this.properties.back
// 背景颜色
statusBarInfo.backgroundColor = this.properties.backgroundColor
this.setData({ statusBarInfo })
console.log('statusBarInfo...', JSON.stringify(this.data.statusBarInfo))
},
onBackTap(e) {
console.log('onBackTap...')
wx.navigateBack()
}
}
})

custom-navigation-bar.json

1
2
3
4
{
"component": true,
"usingComponents": {}
}

custom-navigation-bar.wxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 这是自定义组件的内部WXML结构 -->
<!-- 状态栏相关 -->
<view class="status-bar" style="background-color: {{backgroundColor}};">
<!-- 系统状态栏占位 -->
<view class="system-status-bar" style="height: {{statusBarInfo.sysHeight}}px;"></view>
<!-- 应用状态栏-->
<view class="app-status-bar" style="height: {{statusBarInfo.appHeight}}px;">
<!-- 左侧按钮 -->
<view class="app-status-bar-left">
<view wx:if="{{statusBarInfo.back}}" class="back" bind:tap="onBackTap">返回</view>
</view>
<!-- 中间部分 -->
<view class="app-status-bar-center">
<!-- 标题 -->
<view class="title">{{statusBarInfo.title}}</view>
</view>
<!-- 右侧占位 -->
<view class="app-status-bar-right"></view>
</view>
</view>

<slot></slot>

custom-navigation-bar.wxss

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
/* components/custom-navigation-bar/custom-navigation-bar.wxss */
/* 这里的样式只应用于这个自定义组件 */
.app-status-bar {
display: flex;
align-items: center;
}
.app-status-bar-left {
flex: 1;
display: flex;
}
.app-status-bar-center {
flex: 1;
display: flex;
justify-content: center;
}
.app-status-bar-right {
flex: 1;
}

.back {
padding-top: 6px;
padding-bottom: 6px;
padding-left: 8px;
margin-left: 4px;
padding-right: 32px;
}

使用示例

1
2
3
4
<!--pages/mine-edit/mine-edit.wxml-->
<custom-navigation-bar title="编辑" back="{{true}}"></custom-navigation-bar>
<scroll-view class="content-container" scroll-y="true" enable-flex="true">
</scroll-view>

同时需要在使用的页面对应的json配置文件中添加引用

1
2
3
4
5
{
"usingComponents": {
"custom-navigation-bar": "/components/custom-navigation-bar/custom-navigation-bar"
}
}

以及在app.json中添加配置

1
2
3
4
5
{
"window": {
"navigationStyle": "custom"
}
}