From eb987323e1235de746769582e97cae7a499f9e67 Mon Sep 17 00:00:00 2001 From: Binary Wang Date: Thu, 24 Jul 2025 15:35:44 +0800 Subject: [PATCH 1/3] =?UTF-8?q?:art:=20=E4=BF=AE=E5=A4=8D=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=9A=84=E5=8F=98=E9=87=8F=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../binarywang/wxpay/bean/notify/WxPayRefundNotifyResult.java | 4 ++-- .../wxpay/bean/notify/WxPayRefundNotifyResultTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResult.java index ae86b8c854..8615a2e461 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResult.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResult.java @@ -273,7 +273,7 @@ public String toString() { * */ @XStreamAlias("refund_recv_accout") - private String refundRecvAccout; + private String refundRecvAccount; /** *
@@ -324,7 +324,7 @@ public void loadXML(Document d) {
       settlementRefundFee = readXmlInteger(d, "settlement_refund_fee");
       refundStatus = readXmlString(d, "refund_status");
       successTime = readXmlString(d, "success_time");
-      refundRecvAccout = readXmlString(d, "refund_recv_accout");
+      refundRecvAccount = readXmlString(d, "refund_recv_accout");
       refundAccount = readXmlString(d, "refund_account");
       refundRequestSource = readXmlString(d, "refund_request_source");
     }
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResultTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResultTest.java
index 963afb2618..e7a22ee6cd 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResultTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResultTest.java
@@ -119,7 +119,7 @@ public void testFromXMLFastMode() throws WxPayException {
       refundNotifyResult.loadReqInfo(xmlDecryptedReqInfo);
       assertEquals(refundNotifyResult.getReqInfo().getRefundFee().intValue(), 15);
       assertEquals(refundNotifyResult.getReqInfo().getRefundStatus(), "SUCCESS");
-      assertEquals(refundNotifyResult.getReqInfo().getRefundRecvAccout(), "用户零钱");
+      assertEquals(refundNotifyResult.getReqInfo().getRefundRecvAccount(), "用户零钱");
       System.out.println(refundNotifyResult);
     } finally {
       XmlConfig.fastMode = false;

From f8cdcfcc9f604b71b54f0666825d52a9085db6fb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 24 Jul 2025 07:42:39 +0000
Subject: [PATCH 2/3] Initial plan


From 6e93a67d647f1bb7ceba38d16af6a6938ce8be53 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 24 Jul 2025 07:53:23 +0000
Subject: [PATCH 3/3] Fix Redis lock key serialization inconsistency

Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
---
 .../RedisTemplateSimpleDistributedLock.java   |  24 ++---
 ...impleDistributedLockSerializationTest.java | 100 ++++++++++++++++++
 2 files changed, 110 insertions(+), 14 deletions(-)
 create mode 100644 weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockSerializationTest.java

diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java
index b2d2481efe..3f5ce4d692 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java
@@ -1,16 +1,11 @@
 package me.chanjar.weixin.common.util.locks;
 
 import lombok.Getter;
-import org.springframework.data.redis.connection.RedisStringCommands;
-import org.springframework.data.redis.core.RedisCallback;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.data.redis.core.script.RedisScript;
-import org.springframework.data.redis.core.types.Expiration;
 
-import java.nio.charset.StandardCharsets;
 import java.util.Collections;
-import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Condition;
@@ -70,15 +65,16 @@ public boolean tryLock() {
       value = UUID.randomUUID().toString();
       valueThreadLocal.set(value);
     }
-    final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
-    final byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
-    List redisResults = redisTemplate.executePipelined((RedisCallback) connection -> {
-      connection.set(keyBytes, valueBytes, Expiration.milliseconds(leaseMilliseconds), RedisStringCommands.SetOption.SET_IF_ABSENT);
-      connection.get(keyBytes);
-      return null;
-    });
-    Object currentLockSecret = redisResults.size() > 1 ? redisResults.get(1) : redisResults.get(0);
-    return currentLockSecret != null && currentLockSecret.toString().equals(value);
+    
+    // Use high-level StringRedisTemplate API to ensure consistent key serialization
+    Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(key, value, leaseMilliseconds, TimeUnit.MILLISECONDS);
+    if (Boolean.TRUE.equals(lockAcquired)) {
+      return true;
+    }
+    
+    // Check if we already hold the lock (reentrant behavior)
+    String currentValue = redisTemplate.opsForValue().get(key);
+    return value.equals(currentValue);
   }
 
   @Override
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockSerializationTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockSerializationTest.java
new file mode 100644
index 0000000000..ea4a131d37
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockSerializationTest.java
@@ -0,0 +1,100 @@
+package me.chanjar.weixin.common.util.locks;
+
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * 测试 RedisTemplateSimpleDistributedLock 在自定义 Key 序列化时的兼容性
+ * 
+ * 这个测试验证修复后的实现确保 tryLock 和 unlock 使用一致的键序列化方式
+ */
+@Test(enabled = false) // 默认禁用,需要Redis实例才能运行
+public class RedisTemplateSimpleDistributedLockSerializationTest {
+
+  private RedisTemplateSimpleDistributedLock redisLock;
+  private StringRedisTemplate redisTemplate;
+
+  @BeforeTest
+  public void init() {
+    JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
+    connectionFactory.setHostName("127.0.0.1");
+    connectionFactory.setPort(6379);
+    connectionFactory.afterPropertiesSet();
+    
+    // 创建一个带自定义键序列化的 StringRedisTemplate
+    StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
+    
+    // 使用自定义键序列化器,模拟在键前面添加前缀的场景
+    redisTemplate.setKeySerializer(new StringRedisSerializer() {
+      @Override
+      public byte[] serialize(String string) {
+        if (string == null) return null;
+        // 添加 "System:" 前缀,模拟用户自定义的键序列化
+        return super.serialize("System:" + string);
+      }
+      
+      @Override
+      public String deserialize(byte[] bytes) {
+        if (bytes == null) return null;
+        String result = super.deserialize(bytes);
+        // 移除前缀进行反序列化
+        return result != null && result.startsWith("System:") ? result.substring(7) : result;
+      }
+    });
+    
+    this.redisTemplate = redisTemplate;
+    this.redisLock = new RedisTemplateSimpleDistributedLock(redisTemplate, "test_lock_key", 60000);
+  }
+
+  @Test(description = "测试自定义键序列化器下的锁操作一致性")
+  public void testLockConsistencyWithCustomKeySerializer() {
+    // 1. 获取锁应该成功
+    assertTrue(redisLock.tryLock(), "第一次获取锁应该成功");
+    assertNotNull(redisLock.getLockSecretValue(), "锁值应该存在");
+    
+    // 2. 验证键已正确存储(通过 redisTemplate 直接查询)
+    String actualValue = redisTemplate.opsForValue().get("test_lock_key");
+    assertEquals(actualValue, redisLock.getLockSecretValue(), "通过 redisTemplate 查询的值应该与锁值相同");
+    
+    // 3. 再次尝试获取同一把锁应该成功(可重入)
+    assertTrue(redisLock.tryLock(), "可重入锁应该再次获取成功");
+    
+    // 4. 释放锁应该成功
+    redisLock.unlock();
+    assertNull(redisLock.getLockSecretValue(), "释放锁后锁值应该为空");
+    
+    // 5. 验证键已被删除
+    actualValue = redisTemplate.opsForValue().get("test_lock_key");
+    assertNull(actualValue, "释放锁后 Redis 中的键应该被删除");
+    
+    // 6. 释放已释放的锁应该是安全的
+    redisLock.unlock(); // 不应该抛出异常
+  }
+  
+  @Test(description = "测试不同线程使用相同键的锁排他性")
+  public void testLockExclusivityWithCustomKeySerializer() throws InterruptedException {
+    // 第一个锁实例获取锁
+    assertTrue(redisLock.tryLock(), "第一个锁实例应该成功获取锁");
+    
+    // 创建第二个锁实例使用相同的键
+    RedisTemplateSimpleDistributedLock anotherLock = new RedisTemplateSimpleDistributedLock(
+        redisTemplate, "test_lock_key", 60000);
+    
+    // 第二个锁实例不应该能获取锁
+    assertFalse(anotherLock.tryLock(), "第二个锁实例不应该能获取已被占用的锁");
+    
+    // 释放第一个锁
+    redisLock.unlock();
+    
+    // 现在第二个锁实例应该能获取锁
+    assertTrue(anotherLock.tryLock(), "第一个锁释放后,第二个锁实例应该能获取锁");
+    
+    // 清理
+    anotherLock.unlock();
+  }
+}
\ No newline at end of file