feat(payment): 优化微信支付功能并添加回调兼容性支持

- 修复信用风险关系查询中的公司ID条件判断逻辑
- 将关键词搜索从main_body_name改为match_name以提高匹配准确性
- 在安全配置中添加/api/system/wx-pay/**路径到公共访问白名单
- 添加RoundingMode导入和WechatPrepaySnapshot数据类用于支付快照管理
- 实现微信支付预下单快照机制以解决请求重入参数不一致问题
- 添加多种回调地址候选方案包括新默认地址和历史兼容地址
- 实现支付金额转换为分的统一方法toFen,使用四舍五入避免精度问题
- 添加Redis缓存存储支付快照,TTL设置为30分钟
- 实现notifyUrl动态切换重试机制,支持多个回调地址备选
- 创建WxPayNotifyAliasController提供旧版回调地址兼容性支持
- 修复JSAPI和Native支付中的金额计算逻辑,优先使用payPrice字段
- 添加支付金额空值检查防止运行时异常
- 优化支付描述字段截断处理逻辑,改进默认值设置
This commit is contained in:
2026-02-07 14:01:04 +08:00
parent 78a3f8ce4c
commit 9b31b3ce57
4 changed files with 391 additions and 62 deletions

View File

@@ -67,6 +67,7 @@ public class SecurityConfig {
"/api/shop/wx-login/**", "/api/shop/wx-login/**",
"/api/shop/wx-native-pay/**", "/api/shop/wx-native-pay/**",
"/api/shop/wx-pay/**", "/api/shop/wx-pay/**",
"/api/system/wx-pay/**",
"/api/bszx/bszx-pay/notify/**", "/api/bszx/bszx-pay/notify/**",
"/api/wxWorkQrConnect", "/api/wxWorkQrConnect",
"/WW_verify_QMv7HoblYU6z63bb.txt", "/WW_verify_QMv7HoblYU6z63bb.txt",

View File

@@ -12,9 +12,9 @@
<if test="param.id != null"> <if test="param.id != null">
AND a.id = #{param.id} AND a.id = #{param.id}
</if> </if>
<if test="param.companyId != null"> <if test="param.companyId != null">
AND a.company_id = #{param.companyId} AND a.company_id = #{param.companyId}
</if> </if>
<if test="param.mainBodyName != null"> <if test="param.mainBodyName != null">
AND a.main_body_name LIKE CONCAT('%', #{param.mainBodyName}, '%') AND a.main_body_name LIKE CONCAT('%', #{param.mainBodyName}, '%')
</if> </if>
@@ -63,7 +63,7 @@
<if test="param.keywords != null"> <if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
OR b.name = #{param.keywords} OR b.name = #{param.keywords}
OR b.main_body_name LIKE CONCAT('%', #{param.keywords}, '%') OR b.match_name LIKE CONCAT('%', #{param.keywords}, '%')
) )
</if> </if>
</where> </where>

View File

@@ -0,0 +1,47 @@
package com.gxwebsoft.shop.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* 微信支付回调别名入口(兼容历史 notify_url
*
* 说明:
* - 旧代码曾使用 /api/system/wx-pay/notify/{tenantId} 或 /api/shop/wx-pay/notify/{tenantId}
* - 微信支付“请求重入”要求 notify_url 等参数与首次一致,因此需要保留旧回调地址可用
*/
@Tag(name = "微信支付回调(别名)")
@RestController
public class WxPayNotifyAliasController {
@Resource
private ShopOrderController shopOrderController;
@Operation(summary = "微信支付回调别名(兼容旧回调地址)")
@PostMapping({"/api/system/wx-pay/notify/{tenantId}", "/api/shop/wx-pay/notify/{tenantId}"})
public String wxNotifyAlias(@RequestHeader Map<String, String> header,
@RequestBody String body,
@PathVariable("tenantId") Integer tenantId) {
// ShopOrderController.wxNotify 读取的是小写 header key这里做一次兼容转换
Map<String, String> lower = new HashMap<>();
if (header != null) {
header.forEach((k, v) -> {
if (k != null) {
lower.put(k.toLowerCase(Locale.ROOT), v);
}
});
}
return shopOrderController.wxNotify(lower, body, tenantId);
}
}

View File

@@ -30,12 +30,14 @@ import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.nativepay.NativePayService; import com.wechat.pay.java.service.payments.nativepay.NativePayService;
// Native支付的类将使用完全限定名避免冲突 // Native支付的类将使用完全限定名避免冲突
import com.wechat.pay.java.service.payments.model.Transaction; import com.wechat.pay.java.service.payments.model.Transaction;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -86,6 +88,165 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
private ShopExpressService shopExpressService; private ShopExpressService shopExpressService;
private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L; private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L;
private static final long WECHAT_PREPAY_SNAPSHOT_TTL_MINUTES = 30L;
private static final String WECHAT_PREPAY_SNAPSHOT_KEY_PREFIX = "wxpay:prepay:snapshot:";
@Data
private static class WechatPrepaySnapshot {
private String outTradeNo;
private String appid;
private String mchid;
private String openid;
private Integer total; // 分
private String description;
private String notifyUrl;
private String attach;
private String wechatPayType;
}
private static String trimTrailingSlashes(String url) {
if (url == null) {
return null;
}
String u = url.trim();
while (u.endsWith("/")) {
u = u.substring(0, u.length() - 1);
}
return u;
}
private static String ensureTenantSuffix(String baseUrl, Integer tenantId) {
if (StrUtil.isBlank(baseUrl) || tenantId == null) {
return baseUrl;
}
String base = trimTrailingSlashes(baseUrl);
String suffix = "/" + tenantId;
if (base.endsWith(suffix)) {
return base;
}
return base + suffix;
}
private String prepaySnapshotKey(Payment payment, String outTradeNo) {
return WECHAT_PREPAY_SNAPSHOT_KEY_PREFIX + payment.getMchId() + ":" + outTradeNo;
}
private WechatPrepaySnapshot getPrepaySnapshot(Payment payment, String outTradeNo) {
try {
return redisUtil.get(prepaySnapshotKey(payment, outTradeNo), WechatPrepaySnapshot.class);
} catch (Exception e) {
// 缓存不可用时不影响支付主流程
log.warn("读取微信预下单快照失败 - outTradeNo={}, mchId={}", outTradeNo, payment != null ? payment.getMchId() : null, e);
return null;
}
}
private void savePrepaySnapshot(Payment payment, WechatPrepaySnapshot snapshot) {
if (payment == null || snapshot == null || StrUtil.isBlank(snapshot.getOutTradeNo())) {
return;
}
try {
redisUtil.set(prepaySnapshotKey(payment, snapshot.getOutTradeNo()), snapshot, WECHAT_PREPAY_SNAPSHOT_TTL_MINUTES, TimeUnit.MINUTES);
} catch (Exception e) {
log.warn("保存微信预下单快照失败 - outTradeNo={}, mchId={}", snapshot.getOutTradeNo(), payment.getMchId(), e);
}
}
private static boolean isIdempotencyParamMismatch(Throwable t) {
Throwable cur = t;
while (cur != null) {
if (cur instanceof ServiceException) {
ServiceException se = (ServiceException) cur;
String code = se.getErrorCode();
String msg = se.getErrorMessage();
String body = se.getResponseBody();
if ("INVALID_REQUEST".equals(code) && ((msg != null && msg.contains("请求重入")) || (body != null && body.contains("请求重入")))) {
return true;
}
}
String m = cur.getMessage();
if (m != null && m.contains("INVALID_REQUEST") && m.contains("请求重入")) {
return true;
}
cur = cur.getCause();
}
return false;
}
private static Integer toFen(BigDecimal amountYuan) {
if (amountYuan == null) {
return null;
}
// 微信支付金额字段使用整数分,这里按两位小数四舍五入再转分
BigDecimal fen = amountYuan.setScale(2, RoundingMode.HALF_UP).movePointRight(2);
return fen.intValueExact();
}
private String defaultShopOrderNotifyUrl(Integer tenantId) {
String base = trimTrailingSlashes(config.getServerUrl());
if (StrUtil.isBlank(base) || tenantId == null) {
return null;
}
return base + "/shop/shop-order/notify/" + tenantId;
}
private String legacySystemWxPayNotifyUrl(Integer tenantId) {
String base = trimTrailingSlashes(config.getServerUrl());
if (StrUtil.isBlank(base) || tenantId == null) {
return null;
}
return base + "/system/wx-pay/notify/" + tenantId;
}
private String legacyShopWxPayNotifyUrl(Integer tenantId) {
// 旧代码曾使用 /api/shop/wx-pay/notify/{tenantId}
if (tenantId == null) {
return null;
}
return "http://jimei-api.natapp1.cc/api/shop/wx-pay/notify/" + tenantId;
}
private String devShopOrderNotifyUrl(Integer tenantId) {
if (tenantId == null) {
return null;
}
return "http://jimei-api.natapp1.cc/api/shop/shop-order/notify/" + tenantId;
}
private List<String> buildNotifyUrlCandidates(ShopOrder order, Payment payment, WechatPrepaySnapshot snapshot) {
LinkedHashSet<String> urls = new LinkedHashSet<>();
if (snapshot != null && StrUtil.isNotBlank(snapshot.getNotifyUrl())) {
urls.add(trimTrailingSlashes(snapshot.getNotifyUrl()));
}
if (payment != null && StrUtil.isNotBlank(payment.getNotifyUrl())) {
urls.add(ensureTenantSuffix(payment.getNotifyUrl(), order.getTenantId()));
}
// 新默认回调(本项目已实现)
String shopOrderNotify = defaultShopOrderNotifyUrl(order.getTenantId());
if (StrUtil.isNotBlank(shopOrderNotify)) {
urls.add(shopOrderNotify);
}
// 兼容历史回调地址(用于已创建订单的“重新支付”重入校验)
String legacySystem = legacySystemWxPayNotifyUrl(order.getTenantId());
if (StrUtil.isNotBlank(legacySystem)) {
urls.add(legacySystem);
}
if ("dev".equals(active)) {
String devNotify = devShopOrderNotifyUrl(order.getTenantId());
if (StrUtil.isNotBlank(devNotify)) {
urls.add(devNotify);
}
String devLegacy = legacyShopWxPayNotifyUrl(order.getTenantId());
if (StrUtil.isNotBlank(devLegacy)) {
urls.add(devLegacy);
}
}
return new ArrayList<>(urls);
}
@Override @Override
@@ -321,13 +482,15 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
Config wxPayConfig = getWxPayConfig(order); Config wxPayConfig = getWxPayConfig(order);
NativePayService nativeService = new NativePayService.Builder().config(wxPayConfig).build(); NativePayService nativeService = new NativePayService.Builder().config(wxPayConfig).build();
// 订单金额(转换为分) // 订单金额(转换为分)- 优先使用 payPrice避免与 JSAPI 金额字段不一致导致重入校验失败
BigDecimal decimal = order.getTotalPrice(); BigDecimal payAmount = order.getPayPrice() != null ? order.getPayPrice() : order.getTotalPrice();
final BigDecimal multiply = decimal.multiply(new BigDecimal(100)); Integer money = toFen(payAmount);
Integer money = multiply.intValue(); if (money == null) {
throw new RuntimeException("订单金额为null");
}
// 测试环境使用1分钱 // 测试环境使用1分钱
if (active.equals("dev")) { if ("dev".equals(active)) {
money = 1; money = 1;
} }
@@ -354,34 +517,87 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
System.out.println("商户号(MchId): " + payment.getMchId()); System.out.println("商户号(MchId): " + payment.getMchId());
System.out.println("应用ID(AppId): " + payment.getAppId()); System.out.println("应用ID(AppId): " + payment.getAppId());
// 设置回调地址 String outTradeNo = order.getOrderNo();
String notifyUrl = config.getServerUrl() + "/system/wx-pay/notify/" + order.getTenantId(); WechatPrepaySnapshot snapshot = getPrepaySnapshot(payment, outTradeNo);
if (active.equals("dev")) { if (snapshot == null) {
notifyUrl = "http://jimei-api.natapp1.cc/api/shop/wx-pay/notify/" + order.getTenantId(); snapshot = new WechatPrepaySnapshot();
snapshot.setOutTradeNo(outTradeNo);
snapshot.setWechatPayType(WechatPayType.NATIVE);
snapshot.setAppid(payment.getAppId());
snapshot.setMchid(payment.getMchId());
snapshot.setAttach(order.getTenantId().toString());
snapshot.setTotal(money);
snapshot.setDescription(description);
// notifyUrl 在下面根据重试结果回填
} else {
// 使用快照中的关键字段,确保重入时参数一致
if (snapshot.getTotal() != null) {
amount.setTotal(snapshot.getTotal());
}
if (StrUtil.isNotBlank(snapshot.getDescription())) {
request.setDescription(snapshot.getDescription());
}
if (StrUtil.isNotBlank(snapshot.getAttach())) {
request.setAttach(snapshot.getAttach());
}
if (StrUtil.isNotBlank(snapshot.getAppid())) {
request.setAppid(snapshot.getAppid());
}
if (StrUtil.isNotBlank(snapshot.getMchid())) {
request.setMchid(snapshot.getMchid());
}
} }
if (StrUtil.isNotBlank(payment.getNotifyUrl())) {
notifyUrl = payment.getNotifyUrl().concat("/").concat(order.getTenantId().toString()); // 开发环境固定使用1分钱与历史行为保持一致
if ("dev".equals(active)) {
amount.setTotal(1);
request.setAmount(amount);
snapshot.setTotal(1);
} }
request.setNotifyUrl(notifyUrl);
System.out.println("=== 发起Native支付请求 ==="); // 回调地址:优先用快照;若重入提示参数不一致,则尝试历史回调地址
System.out.println("请求参数: " + request); Exception last = null;
String notifyUrlUsed = null;
for (String notifyUrl : buildNotifyUrlCandidates(order, payment, snapshot)) {
if (StrUtil.isBlank(notifyUrl)) {
continue;
}
request.setNotifyUrl(notifyUrl);
try {
System.out.println("=== 发起Native支付请求 ===");
System.out.println("请求参数: " + request);
// 调用Native支付API com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse response = nativeService.prepay(request);
com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse response = nativeService.prepay(request); notifyUrlUsed = notifyUrl;
System.out.println("=== Native支付响应成功 ==="); System.out.println("=== Native支付响应成功 ===");
System.out.println("二维码URL: " + response.getCodeUrl()); System.out.println("二维码URL: " + response.getCodeUrl());
// 构建返回数据 snapshot.setNotifyUrl(notifyUrlUsed);
final HashMap<String, String> orderInfo = new HashMap<>(); snapshot.setTotal(amount.getTotal());
orderInfo.put("provider", "wxpay"); snapshot.setDescription(request.getDescription());
orderInfo.put("codeUrl", response.getCodeUrl()); // Native支付返回二维码URL savePrepaySnapshot(payment, snapshot);
orderInfo.put("orderNo", order.getOrderNo());
orderInfo.put("payType", WechatPayType.NATIVE);
orderInfo.put("wechatPayType", WechatPayType.NATIVE);
return orderInfo; final HashMap<String, String> orderInfo = new HashMap<>();
orderInfo.put("provider", "wxpay");
orderInfo.put("codeUrl", response.getCodeUrl()); // Native支付返回二维码URL
orderInfo.put("orderNo", order.getOrderNo());
orderInfo.put("payType", WechatPayType.NATIVE);
orderInfo.put("wechatPayType", WechatPayType.NATIVE);
return orderInfo;
} catch (Exception e) {
last = e;
if (!isIdempotencyParamMismatch(e)) {
throw e;
}
log.warn("Native预下单重入参数不一致尝试切换notifyUrl重试 - outTradeNo={}, notifyUrl={}", outTradeNo, notifyUrl, e);
}
}
if (last != null) {
throw last;
}
throw new RuntimeException("创建Native支付订单失败notifyUrl为空");
} }
/** /**
@@ -399,10 +615,12 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
JsapiServiceExtension service = getWxService(order); JsapiServiceExtension service = getWxService(order);
System.out.println("微信支付服务构建完成"); System.out.println("微信支付服务构建完成");
// 订单金额 // 订单金额(分)
BigDecimal decimal = order.getPayPrice(); BigDecimal payAmount = order.getPayPrice() != null ? order.getPayPrice() : order.getTotalPrice();
final BigDecimal multiply = decimal.multiply(new BigDecimal(100)); Integer money = toFen(payAmount);
Integer money = multiply.intValue(); if (money == null) {
throw new RuntimeException("订单金额为null");
}
System.out.println("=== 构建支付请求参数 ==="); System.out.println("=== 构建支付请求参数 ===");
@@ -419,7 +637,8 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
request.setMchid(payment.getMchId()); request.setMchid(payment.getMchId());
// 微信支付description字段限制127字节需要截断处理 // 微信支付description字段限制127字节需要截断处理
String description = com.gxwebsoft.common.core.utils.WechatPayUtils.processDescription(order.getComments()); String rawDescription = StrUtil.isNotBlank(order.getComments()) ? order.getComments() : "订单支付";
String description = com.gxwebsoft.common.core.utils.WechatPayUtils.processDescription(rawDescription);
System.out.println("设置描述: " + description); System.out.println("设置描述: " + description);
request.setDescription(description); request.setDescription(description);
@@ -433,37 +652,99 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
System.out.println("设置用户OpenID: " + order.getOpenid()); System.out.println("设置用户OpenID: " + order.getOpenid());
payer.setOpenid(order.getOpenid()); payer.setOpenid(order.getOpenid());
request.setPayer(payer); request.setPayer(payer);
request.setNotifyUrl(config.getServerUrl() + "/system/wx-pay/notify/" + order.getTenantId()); // 默认回调地址 String outTradeNo = order.getOrderNo();
// 测试环境 WechatPrepaySnapshot snapshot = getPrepaySnapshot(payment, outTradeNo);
if (active.equals("dev")) { if (snapshot == null) {
snapshot = new WechatPrepaySnapshot();
snapshot.setOutTradeNo(outTradeNo);
snapshot.setWechatPayType(WechatPayType.JSAPI);
snapshot.setAppid(payment.getAppId());
snapshot.setMchid(payment.getMchId());
snapshot.setOpenid(order.getOpenid());
snapshot.setAttach(order.getTenantId().toString());
snapshot.setTotal(amount.getTotal());
snapshot.setDescription(request.getDescription());
// notifyUrl 在下面根据重试结果回填
} else {
// 使用快照中的关键字段,确保重入时参数一致
if (snapshot.getTotal() != null) {
amount.setTotal(snapshot.getTotal());
request.setAmount(amount);
}
if (StrUtil.isNotBlank(snapshot.getDescription())) {
request.setDescription(snapshot.getDescription());
}
if (StrUtil.isNotBlank(snapshot.getAttach())) {
request.setAttach(snapshot.getAttach());
}
if (StrUtil.isNotBlank(snapshot.getOpenid())) {
payer.setOpenid(snapshot.getOpenid());
request.setPayer(payer);
}
if (StrUtil.isNotBlank(snapshot.getAppid())) {
request.setAppid(snapshot.getAppid());
}
if (StrUtil.isNotBlank(snapshot.getMchid())) {
request.setMchid(snapshot.getMchid());
}
}
// 测试环境使用1分钱快照优先
if ("dev".equals(active)) {
amount.setTotal(1); amount.setTotal(1);
request.setAmount(amount); request.setAmount(amount);
request.setNotifyUrl("http://jimei-api.natapp1.cc/api/shop/wx-pay/notify/" + order.getTenantId()); // 默认回调地址 snapshot.setTotal(1);
} }
// 后台配置的回调地址
if (StrUtil.isNotBlank(payment.getNotifyUrl())) { Exception last = null;
request.setNotifyUrl(payment.getNotifyUrl().concat("/").concat(order.getTenantId().toString())); String notifyUrlUsed = null;
System.out.println("后台配置的回调地址 = " + request.getNotifyUrl()); for (String notifyUrl : buildNotifyUrlCandidates(order, payment, snapshot)) {
if (StrUtil.isBlank(notifyUrl)) {
continue;
}
request.setNotifyUrl(notifyUrl);
try {
System.out.println("=== 发起微信支付请求 ===");
System.out.println("请求参数: " + request);
PrepayWithRequestPaymentResponse response = service.prepayWithRequestPayment(request);
notifyUrlUsed = notifyUrl;
System.out.println("=== 微信支付响应成功 ===");
System.out.println("预支付ID: " + response.getPackageVal());
snapshot.setNotifyUrl(notifyUrlUsed);
snapshot.setTotal(request.getAmount().getTotal());
snapshot.setDescription(request.getDescription());
snapshot.setOpenid(request.getPayer() != null ? request.getPayer().getOpenid() : null);
snapshot.setAppid(request.getAppid());
snapshot.setMchid(request.getMchid());
snapshot.setAttach(request.getAttach());
savePrepaySnapshot(payment, snapshot);
final HashMap<String, String> orderInfo = new HashMap<>();
orderInfo.put("provider", "wxpay");
orderInfo.put("timeStamp", response.getTimeStamp());
orderInfo.put("nonceStr", response.getNonceStr());
orderInfo.put("package", response.getPackageVal());
orderInfo.put("signType", "RSA");
orderInfo.put("paySign", response.getPaySign());
orderInfo.put("orderNo", order.getOrderNo());
orderInfo.put("payType", WechatPayType.JSAPI);
orderInfo.put("wechatPayType", WechatPayType.JSAPI);
return orderInfo;
} catch (Exception e) {
last = e;
if (!isIdempotencyParamMismatch(e)) {
throw e;
}
log.warn("JSAPI预下单重入参数不一致尝试切换notifyUrl重试 - outTradeNo={}, notifyUrl={}", outTradeNo, notifyUrl, e);
}
} }
System.out.println("=== 发起微信支付请求 ==="); if (last != null) {
System.out.println("请求参数: " + request); throw last;
}
PrepayWithRequestPaymentResponse response = service.prepayWithRequestPayment(request); throw new RuntimeException("创建JSAPI支付订单失败notifyUrl为空");
System.out.println("=== 微信支付响应成功 ===");
System.out.println("预支付ID: " + response.getPackageVal());
final HashMap<String, String> orderInfo = new HashMap<>();
orderInfo.put("provider", "wxpay");
orderInfo.put("timeStamp", response.getTimeStamp());
orderInfo.put("nonceStr", response.getNonceStr());
orderInfo.put("package", response.getPackageVal());
orderInfo.put("signType", "RSA");
orderInfo.put("paySign", response.getPaySign());
orderInfo.put("orderNo", order.getOrderNo());
orderInfo.put("payType", WechatPayType.JSAPI);
orderInfo.put("wechatPayType", WechatPayType.JSAPI);
return orderInfo;
} }
@Override @Override