通用公共配置开箱即用
生成验证码的两种方式,SpringBoot项目,下面是完整代码,直接就可以用
公共依赖
<!-- redis 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- pool 对象池,springboot2.x以后用得是lettuce,添加连接池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 方法二需要用到的jar包 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
配置 application.yml
文件
spring:
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
公共常量
/**
* 公共常量
*/
public class ConstantCode {
/**
* 系统缓存常量
*/
public static final String KAPTCHA_KEY = "ss:kaptcha";
public static final Long REDIS_EXP_TIME = 60 * 60L;
public static final Long KAPTCHA_EXP_TIME = 60 * 3L;
/**
* 验证码宽高常量
*/
public static final Integer WIDTH = 200;
public static final Integer HEIGHT = 55;
/**
* 图片后缀名称
*/
public static final String IMG_JPG = "jpg";
public static final String IMG_PNG = "png";
public static final String IMG_JPEG = "jpeg";
}
Redis 序列化配置
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 为了自己开发方便,一般直接使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
Redis 工具类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类
*/
@Component
@SuppressWarnings("all")
public class RedisUtils {
/**
* 日志对象
*/
private static Logger logger = LoggerFactory.getLogger(RedisUtils.class);
@Autowired
private RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection) {
return redisTemplate.delete(collection);
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
logger.error("RedisUtils expire(String key,long time) failure." + e.getMessage());
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
logger.error("RedisUtils hasKey(String key) failure." + e.getMessage());
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
logger.error("RedisUtils set(String key,Object value) failure." + e.getMessage());
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
logger.error("RedisUtils set(String key,Object value,long time) failure." + e.getMessage());
return false;
}
}
}
通用返回结果封装
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import java.util.HashMap;
/**
* 操作消息提醒
*/
public class Result extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
public static final String CODE_TAG = "code";
/**
* 返回内容
*/
public static final String MSG_TAG = "msg";
/**
* 数据对象
*/
public static final String DATA_TAG = "data";
/**
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
*/
public Result() {
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
*/
public Result(int code, String msg) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
* @param data 数据对象
*/
public Result(int code, String msg, Object data) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (!StringUtils.isEmpty(data)) {
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static Result success() {
return Result.success("操作成功");
}
/**
* 返回成功数据
*
* @return 成功消息
*/
public static Result success(Object data) {
return Result.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static Result success(String msg) {
return Result.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static Result success(String msg, Object data) {
return new Result(HttpStatus.OK.value(), msg, data);
}
/**
* 返回错误消息
*
* @return
*/
public static Result error() {
return Result.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static Result error(String msg) {
return Result.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static Result error(String msg, Object data) {
return new Result(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data);
}
/**
* 返回错误消息
*
* @param code 状态码
* @param msg 返回内容
* @return 警告消息
*/
public static Result error(int code, String msg) {
return new Result(code, msg, null);
}
/**
* 方便链式调用
*
* @param key 键
* @param value 值
* @return 数据对象
*/
@Override
public Result put(String key, Object value) {
super.put(key, value);
return this;
}
}
方法一:Graphics2D 画图实现
使用 jdk 画图 Graphics2D 生成验证码
1、验证码工具类
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* 验证码工具类
*/
public class VerifyCodeUtils {
/**
* 传入BufferedImage对象,并将生成好的验证码保存到BufferedImage中
*/
public static String drawRandomText(BufferedImage bufferedImage, int width, int height) {
Graphics2D graphics = (Graphics2D) bufferedImage.getGraphics();
// 验证码背景色
graphics.setColor(new Color(255, 255, 255));
// 填充线条背景
graphics.fillRect(0, 0, width, height);
graphics.setFont(new Font("宋体,楷体,微软雅黑", Font.BOLD, 35));
// 数字和字母的组合
String baseNumLetter = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder builder = new StringBuilder();
// 旋转原点的 x 坐标
int x = 40;
String ch;
Random random = new Random();
for (int i = 0; i < 4; i++) {
graphics.setColor(getRandomColor());
//设置字体旋转角度,角度小于30度
int degree = random.nextInt() % 30;
int dot = random.nextInt(baseNumLetter.length());
ch = baseNumLetter.charAt(dot) + "";
builder.append(ch);
//正向旋转
graphics.rotate(degree * Math.PI / 180, x, 45);
graphics.drawString(ch, x, 45);
//反向旋转
graphics.rotate(-degree * Math.PI / 180, x, 45);
// 字母间距记录
x += 35;
}
// 画干扰线
for (int i = 0; i < 6; i++) {
// 设置随机颜色
graphics.setColor(getRandomColor());
// 随机画线
graphics.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height));
}
// 添加噪点
for (int i = 0; i < 30; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
graphics.setColor(getRandomColor());
graphics.fillRect(x1, y1, 2, 2);
}
return builder.toString();
}
/**
* 随机取色
*/
private static Color getRandomColor() {
Random ran = new Random();
return new Color(ran.nextInt(256), ran.nextInt(256), ran.nextInt(256));
}
}
2、Controller 控制层
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Base64;
import java.util.UUID;
@RestController
public class VerificationController {
@Autowired
private RedisUtils redisUtils;
/**
* 验证码生成工具,返回图片信息
* 适合学习练手使用,redis 只能存在一个
* @param response
* @throws IOException
*/
@GetMapping("/getImageCode")
public void getImageCode(HttpServletResponse response) throws IOException {
// 禁止缓存
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
// 设置响应格式为png图片
response.setContentType("image/png");
// 生成图片验证码
BufferedImage image = new BufferedImage(ConstantCode.WIDTH, ConstantCode.HEIGHT, BufferedImage.TYPE_INT_RGB);
String randomText = VerifyCodeUtils.drawRandomText(image, ConstantCode.WIDTH, ConstantCode.HEIGHT);
// 存入redis
redisUtils.set(ConstantCode.KAPTCHA_KEY, randomText, ConstantCode.KAPTCHA_EXP_TIME);
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, ConstantCode.IMG_JPG, out);
out.flush();
out.close();
}
/**
* 验证码 Base64
* 可生成多个
* @return
*/
@GetMapping("/getCaptchaInfo")
public Result getCaptchaInfo() {
Result success = Result.success();
// 生成图片验证码
BufferedImage image = new BufferedImage(ConstantCode.WIDTH, ConstantCode.HEIGHT, BufferedImage.TYPE_INT_RGB);
String uuid = UUID.randomUUID().toString().replace("-", "");
// 验证码key
String verifyKey = ConstantCode.KAPTCHA_KEY + uuid;
String randomText = VerifyCodeUtils.drawRandomText(image, ConstantCode.WIDTH, ConstantCode.HEIGHT);
// 存入redis
redisUtils.set(verifyKey, randomText, ConstantCode.KAPTCHA_EXP_TIME);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try {
ImageIO.write(image, "jpg", os);
} catch (IOException e) {
return Result.error(e.getMessage());
}
success.put("uuid", uuid);
success.put("img", Base64.getEncoder().encodeToString(os.toByteArray()));
return success;
}
}
方法二:kaptcha 依赖实现
1、验证码配置
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
import static com.google.code.kaptcha.Constants.*;
/**
* 验证码配置
*/
@Configuration
public class CaptchaConfig {
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
@Bean(name = "captchaProducerMath")
public DefaultKaptcha getKaptchaBeanMath() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 边框颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
// 验证码文本生成器 注意:KaptchaTextCreator的路径
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.test.demo.KaptchaTextCreator");
// 验证码文本字符间距 默认为2
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 验证码噪点颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
// 干扰实现类
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
2、验证码文本生成器
import com.google.code.kaptcha.text.impl.DefaultTextCreator;
import java.util.Random;
/**
* 验证码文本生成器
*/
public class KaptchaTextCreator extends DefaultTextCreator {
private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
@Override
public String getText() {
Integer result = 0;
Random random = new Random();
int x = random.nextInt(10);
int y = random.nextInt(10);
StringBuilder suChinese = new StringBuilder();
int randomoperands = (int) Math.round(Math.random() * 2);
if (randomoperands == 0) {
result = x * y;
suChinese.append(CNUMBERS[x]);
suChinese.append("*");
suChinese.append(CNUMBERS[y]);
} else if (randomoperands == 1) {
if (!(x == 0) && y % x == 0) {
result = y / x;
suChinese.append(CNUMBERS[y]);
suChinese.append("/");
suChinese.append(CNUMBERS[x]);
} else {
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
} else if (randomoperands == 2) {
if (x >= y) {
result = x - y;
suChinese.append(CNUMBERS[x]);
suChinese.append("-");
suChinese.append(CNUMBERS[y]);
} else {
result = y - x;
suChinese.append(CNUMBERS[y]);
suChinese.append("-");
suChinese.append(CNUMBERS[x]);
}
} else {
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
suChinese.append("=?@" + result);
return suChinese.toString();
}
}
3、测试正码生成
import com.google.code.kaptcha.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Base64;
import java.util.UUID;
/**
* 验证码操作处理
*/
@RestController
public class CaptchaController {
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
@Autowired
private RedisUtils redisUtils;
/**
* 文本验证码
*
* @return
*/
@GetMapping("/captchaCharImage")
public Result getCharCode() {
Result ajax = Result.success();
// 生成验证码
String capText = captchaProducerMath.createText();
String capStr = capText.substring(0, capText.lastIndexOf("@"));
String code = capText.substring(capText.lastIndexOf("@") + 1);
BufferedImage image = captchaProducerMath.createImage(capStr);
// 保存验证码信息
String uuid = UUID.randomUUID().toString().replace("-", "");
String verifyKey = ConstantCode.KAPTCHA_KEY + uuid;
redisUtils.set(verifyKey, code, ConstantCode.KAPTCHA_EXP_TIME);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try {
ImageIO.write(image, "jpg", os);
} catch (IOException e) {
return Result.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.getEncoder().encodeToString(os.toByteArray()));
return ajax;
}
/**
* 生成验证码
*/
@GetMapping("/captchaMathImage")
public Result getMathCode() {
Result ajax = Result.success();
// 生成验证码
String capText = captchaProducer.createText();
BufferedImage image = captchaProducerMath.createImage(capText);
// 保存验证码信息
String uuid = UUID.randomUUID().toString().replace("-", "");
String verifyKey = ConstantCode.KAPTCHA_KEY + uuid;
redisUtils.set(verifyKey, capText, ConstantCode.KAPTCHA_EXP_TIME);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try {
ImageIO.write(image, "jpg", os);
} catch (IOException e) {
return Result.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.getEncoder().encodeToString(os.toByteArray()));
return ajax;
}
}
原文始发于微信公众号(师小师):验证码生成的两种方式(开箱即用)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/226435.html