feat(payment): 添加微信支付商家转账场景报备信息配置

- 在 application-cms.yml、application-dev.yml、application-prod.yml 和 application-yd.yml 中
  添加 wechatpay.transfer.scene-id 和 scene-report-infos-json 配置项
- 重构 CmsNavigation 实体类,将 modelName 字段位置调整到正确位置
- 修改 CmsNavigationMapper.xml 添加模型名称关联查询
- 更新 JSONUtil 工具类,注册 JavaTimeModule 支持 LocalDateTime 等 Java8 时间类型
- 扩展 ShopDealerUser 实体类,添加 dealerName 和 community 字段
- 在 ShopDealerUserController 中添加手机号排重逻辑
- 修改 ShopDealerUserMapper.xml 增加关键词搜索字段
- 移除 ShopDealerWithdrawController 中多余的操作日志注解
- 扩展 ShopGoods 实体类,添加 categoryName 字段并修改关联查询
- 更新 WxLoginController 构造函数注入 ObjectMapper
- 增强 WxTransferService 添加转账场景报备信息验证和日志记录
This commit is contained in:
2026-01-29 20:49:18 +08:00
parent 4c290ea4fe
commit 6be4421ed9
15 changed files with 88 additions and 15 deletions

View File

@@ -43,6 +43,10 @@ public class CmsNavigation implements Serializable {
@Schema(description = "模型") @Schema(description = "模型")
private String model; private String model;
@Schema(description = "模型名称")
@TableField(exist = false)
private String modelName;
@Schema(description = "标识") @Schema(description = "标识")
private String code; private String code;
@@ -114,9 +118,6 @@ public class CmsNavigation implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private Integer parentPosition; private Integer parentPosition;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "绑定的页面(已废弃)") @Schema(description = "绑定的页面(已废弃)")
private Integer pageId; private Integer pageId;

View File

@@ -4,9 +4,10 @@
<!-- 关联查询sql --> <!-- 关联查询sql -->
<sql id="selectSql"> <sql id="selectSql">
SELECT a.*, b.title as parentName, b.position as parentPosition SELECT a.*, b.title as parentName, b.position as parentPosition, c.name as modelName
FROM cms_navigation a FROM cms_navigation a
LEFT JOIN cms_navigation b ON a.parent_id = b.navigation_id LEFT JOIN cms_navigation b ON a.parent_id = b.navigation_id
LEFT JOIN cms_model c ON a.model = c.model
<where> <where>
<if test="param.navigationId != null"> <if test="param.navigationId != null">
AND a.navigation_id = #{param.navigationId} AND a.navigation_id = #{param.navigationId}

View File

@@ -1,8 +1,13 @@
package com.gxwebsoft.common.core.utils; package com.gxwebsoft.common.core.utils;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.util.TimeZone;
/** /**
* JSON解析工具类 * JSON解析工具类
@@ -11,8 +16,21 @@ import com.fasterxml.jackson.databind.ObjectWriter;
* @since 2017-06-10 10:10:39 * @since 2017-06-10 10:10:39
*/ */
public class JSONUtil { public class JSONUtil {
private static final ObjectMapper objectMapper = new ObjectMapper(); /**
private static final ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter(); * 注意:不要直接 new ObjectMapper() 否则不支持 Java8 时间类型LocalDateTime 等)。
* 这里做最小可用配置,避免在 Redis/日志/签名等场景序列化失败。
*/
private static final ObjectMapper objectMapper;
private static final ObjectWriter objectWriter;
static {
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
}
/** /**
* 对象转json字符串 * 对象转json字符串

View File

@@ -54,7 +54,7 @@ import static com.gxwebsoft.common.core.constants.RedisConstants.MP_WX_KEY;
public class WxLoginController extends BaseController { public class WxLoginController extends BaseController {
private final StringRedisTemplate redisTemplate; private final StringRedisTemplate redisTemplate;
private final OkHttpClient http = new OkHttpClient(); private final OkHttpClient http = new OkHttpClient();
private final ObjectMapper om = new ObjectMapper(); private final ObjectMapper om;
private volatile long tokenExpireEpoch = 0L; // 过期的 epoch 秒 private volatile long tokenExpireEpoch = 0L; // 过期的 epoch 秒
@Resource @Resource
private SettingService settingService; private SettingService settingService;
@@ -80,8 +80,9 @@ public class WxLoginController extends BaseController {
private CmsWebsiteService cmsWebsiteService; private CmsWebsiteService cmsWebsiteService;
public WxLoginController(StringRedisTemplate redisTemplate) { public WxLoginController(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate; this.redisTemplate = redisTemplate;
this.om = objectMapper;
} }
@Operation(summary = "获取微信AccessToken") @Operation(summary = "获取微信AccessToken")

View File

@@ -16,6 +16,7 @@ import com.wechat.pay.java.core.http.JsonRequestBody;
import com.wechat.pay.java.core.http.MediaType; import com.wechat.pay.java.core.http.MediaType;
import com.wechat.pay.java.core.exception.ServiceException; import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.util.GsonUtil; import com.wechat.pay.java.core.util.GsonUtil;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -121,6 +122,18 @@ public class WxTransferService {
userName = null; userName = null;
} }
// 升级版接口必填transfer_scene_report_infos且必须与 transfer_scene_id 的报备信息一致)
List<TransferSceneReportInfo> sceneReportInfos = parseTransferSceneReportInfos();
if (sceneReportInfos == null
|| sceneReportInfos.isEmpty()
|| sceneReportInfos.stream().anyMatch(i -> i == null
|| StrUtil.isBlank(i.getInfoType())
|| StrUtil.isBlank(i.getInfoContent()))) {
throw PaymentException.paramError(
"未传入完整且对应的转账场景报备信息:请在配置中设置 wechatpay.transfer.scene-report-infos-json需与 transfer_scene_id="
+ transferSceneId + " 的报备信息一致)");
}
Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(tenantId); Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(tenantId);
Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId); Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId);
@@ -135,10 +148,9 @@ public class WxTransferService {
request.setOpenid(openid); request.setOpenid(openid);
request.setTransferAmount(amountFen); request.setTransferAmount(amountFen);
request.setTransferRemark(limitLen(remark, 32)); request.setTransferRemark(limitLen(remark, 32));
List<TransferSceneReportInfo> sceneReportInfos = parseTransferSceneReportInfos();
if (sceneReportInfos != null && !sceneReportInfos.isEmpty()) {
request.setTransferSceneReportInfos(sceneReportInfos); request.setTransferSceneReportInfos(sceneReportInfos);
} log.debug("微信商家转账(升级版)请求参数: tenantId={}, outBillNo={}, transferSceneId={}, sceneReportInfosCount={}",
tenantId, outBillNo, transferSceneId, sceneReportInfos.size());
// 可选:转账结果通知地址(必须 https // 可选:转账结果通知地址(必须 https
if (StrUtil.isNotBlank(paymentConfig.getNotifyUrl()) && paymentConfig.getNotifyUrl().startsWith("https://")) { if (StrUtil.isNotBlank(paymentConfig.getNotifyUrl()) && paymentConfig.getNotifyUrl().startsWith("https://")) {
@@ -244,7 +256,9 @@ public class WxTransferService {
@lombok.Data @lombok.Data
private static class TransferSceneReportInfo { private static class TransferSceneReportInfo {
@SerializedName(value = "info_type", alternate = {"infoType"})
private String infoType; private String infoType;
@SerializedName(value = "info_content", alternate = {"infoContent"})
private String infoContent; private String infoContent;
} }

View File

@@ -3,6 +3,7 @@ package com.gxwebsoft.shop.controller;
import cn.afterturn.easypoi.excel.ExcelImportUtil; import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams; import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.common.core.utils.JSONUtil; import com.gxwebsoft.common.core.utils.JSONUtil;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.shop.service.ShopDealerUserService; import com.gxwebsoft.shop.service.ShopDealerUserService;
@@ -67,6 +68,10 @@ public class ShopDealerUserController extends BaseController {
if (loginUser != null) { if (loginUser != null) {
shopDealerUser.setUserId(loginUser.getUserId()); shopDealerUser.setUserId(loginUser.getUserId());
} }
// 排重
if (shopDealerUserService.count(new LambdaQueryWrapper<ShopDealerUser>().eq(ShopDealerUser::getMobile, shopDealerUser.getMobile())) > 0) {
return fail("添加失败,手机号码已存在!");
}
if (shopDealerUserService.save(shopDealerUser)) { if (shopDealerUserService.save(shopDealerUser)) {
return success("添加成功", shopDealerUser); return success("添加成功", shopDealerUser);
} }

View File

@@ -66,7 +66,6 @@ public class ShopDealerWithdrawController extends BaseController {
} }
@PreAuthorize("hasAuthority('shop:shopDealerWithdraw:save')") @PreAuthorize("hasAuthority('shop:shopDealerWithdraw:save')")
@OperationLog
@Transactional(rollbackFor = {Exception.class}) @Transactional(rollbackFor = {Exception.class})
@Operation(summary = "添加分销商提现明细表") @Operation(summary = "添加分销商提现明细表")
@PostMapping() @PostMapping()
@@ -87,7 +86,6 @@ public class ShopDealerWithdrawController extends BaseController {
} }
@PreAuthorize("hasAuthority('shop:shopDealerWithdraw:update')") @PreAuthorize("hasAuthority('shop:shopDealerWithdraw:update')")
@OperationLog
@Transactional(rollbackFor = {Exception.class}) @Transactional(rollbackFor = {Exception.class})
@Operation(summary = "修改分销商提现明细表") @Operation(summary = "修改分销商提现明细表")
@PutMapping() @PutMapping()

View File

@@ -37,6 +37,12 @@ public class ShopDealerUser implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String openid; private String openid;
@Schema(description = "店铺名称")
private String dealerName;
@Schema(description = "小区名称")
private String community;
@Schema(description = "头像") @Schema(description = "头像")
@TableField(exist = false) @TableField(exist = false)
private String avatar; private String avatar;

View File

@@ -2,6 +2,7 @@ package com.gxwebsoft.shop.entity;
import java.math.BigDecimal; import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
@@ -44,6 +45,10 @@ public class ShopGoods implements Serializable {
@Schema(description = "产品分类ID") @Schema(description = "产品分类ID")
private Integer categoryId; private Integer categoryId;
@Schema(description = "分类名称")
@TableField(exist = false)
private String categoryName;
@Schema(description = "路由地址") @Schema(description = "路由地址")
private String path; private String path;

View File

@@ -64,6 +64,7 @@
</if> </if>
<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 a.user_id = #{param.keywords} OR a.dealer_name LIKE CONCAT('%', #{param.keywords}, '%') OR a.real_name LIKE CONCAT('%', #{param.keywords}, '%') OR a.mobile LIKE CONCAT('%', #{param.keywords}, '%')
) )
</if> </if>
</where> </where>

View File

@@ -4,8 +4,9 @@
<!-- 关联查询sql --> <!-- 关联查询sql -->
<sql id="selectSql"> <sql id="selectSql">
SELECT a.* SELECT a.*, b.title AS categoryName
FROM shop_goods a FROM shop_goods a
LEFT JOIN cms_navigation b ON a.category_id = b.navigation_id
<where> <where>
<if test="param.goodsId != null"> <if test="param.goodsId != null">
AND a.goods_id = #{param.goodsId} AND a.goods_id = #{param.goodsId}

View File

@@ -75,3 +75,9 @@ payment:
key-prefix: "Payment:1" key-prefix: "Payment:1"
# 缓存过期时间(小时) # 缓存过期时间(小时)
expire-hours: 24 expire-hours: 24
# 微信支付-商家转账(升级版)转账场景报备信息(必须与商户平台 transfer_scene_id=1005 的报备信息一致)
wechatpay:
transfer:
scene-id: 1005
scene-report-infos-json: '[{"info_type":"岗位类型","info_content":"业务员"},{"info_type":"报酬说明","info_content":"配送费"}]'

View File

@@ -63,3 +63,9 @@ aliyun:
access-key-id: LTAI5tEsyhW4GCKbds1qsopg access-key-id: LTAI5tEsyhW4GCKbds1qsopg
access-key-secret: zltFlQrYVAoq2KMFDWgLa3GhkMNeyO access-key-secret: zltFlQrYVAoq2KMFDWgLa3GhkMNeyO
endpoint: mt.cn-hangzhou.aliyuncs.com endpoint: mt.cn-hangzhou.aliyuncs.com
# 微信支付-商家转账(升级版)转账场景报备信息(必须与商户平台 transfer_scene_id=1005 的报备信息一致)
wechatpay:
transfer:
scene-id: 1005
scene-report-infos-json: '[{"info_type":"岗位类型","info_content":"业务员"},{"info_type":"报酬说明","info_content":"配送费"}]'

View File

@@ -77,3 +77,7 @@ aliyun:
access-key-id: LTAI5tEsyhW4GCKbds1qsopg access-key-id: LTAI5tEsyhW4GCKbds1qsopg
access-key-secret: zltFlQrYVAoq2KMFDWgLa3GhkMNeyO access-key-secret: zltFlQrYVAoq2KMFDWgLa3GhkMNeyO
endpoint: mt.cn-hangzhou.aliyuncs.com endpoint: mt.cn-hangzhou.aliyuncs.com
wechatpay:
transfer:
scene-id: 1005
scene-report-infos-json: '[{"info_type":"岗位类型","info_content":"业务员"},{"info_type":"报酬说明","info_content":"配送费"}]'

View File

@@ -75,3 +75,9 @@ payment:
key-prefix: "Payment:1" key-prefix: "Payment:1"
# 缓存过期时间(小时) # 缓存过期时间(小时)
expire-hours: 24 expire-hours: 24
# 微信支付-商家转账(升级版)转账场景报备信息(必须与商户平台 transfer_scene_id=1005 的报备信息一致)
wechatpay:
transfer:
scene-id: 1005
scene-report-infos-json: '[{"info_type":"岗位类型","info_content":"业务员"},{"info_type":"报酬说明","info_content":"配送费"}]'