🏆本文收录于 《全栈 Bug 调优(实战版)》 专栏。专栏聚焦真实项目中的各类疑难 Bug,从成因剖析 → 排查路径 → 解决方案 → 预防优化全链路拆解,形成一套可复用、可沉淀的实战知识体系。无论你是初入职场的开发者,还是负责复杂项目的资深工程师,都可以在这里构建一套属于自己的「问题诊断与性能调优」方法论,助你稳步进阶、放大技术价值 。 📌 特别说明: 文中问题案例来源于真实生产环境与公开技术社区,并结合多位一线资深工程师与架构师的长期实践经验,经过筛选与系统化整理后输出。文中的解决方案并非唯一“标准答案”,而是兼顾可行性、可复现性与思路启发性的实践参考,供你在实际项目中灵活运用与演进。 欢迎你 关注、收藏并订阅本专栏,与持续更新的技术干货同行,一起让问题变资产,让经验可复制,技术跃迁,稳步向上。
📢 问题描述
详细问题描述如下:服务号开发,调用接口访问相机以及相册权限报错,如何解决?
如下是相关报错信息:
{"realAuthUrl":"[http://www.后面已删除, http://w ww后面已删除/index?use rType=sysUser]","errMsg":"config:i
nvalid signature"}
全文目录:
-
- 📢 问题描述
- 📣 请知悉:如下方案不保证一定适配你的问题!
-
- ✅ 问题理解
- ✅ 问题解决方案
-
- 🟢 方案 A:标准签名流程排查与修复(最推荐)
-
- Step 1: 检查签名算法实现
- Step 2: 前端正确调用方式
- Step 3: 关键排查点(逐一检查)
- 🟡 方案 B:使用微信官方签名校验工具排查
- 🔴 方案 C:iOS 和 Android 分别处理(兼容性方案)
- ✅ 问题延伸
-
- 1. 微信公众平台配置检查 🔧
- 2. 图片下载到自己服务器 📥
- 3. 进阶功能:图片压缩和裁剪 ✂️
- ✅ 问题预测
-
- 可能遇到的其他问题 ⚠️
- ✅ 小结
-
- 🎯 关键要点总结
- 📋 快速排查检查清单
- 🔗 相关资源
- 🌹 结语 & 互动说明
- 🧧 文末福利:技术成长加速包 🧧
- 🫵 Who am I?
📣 请知悉:如下方案不保证一定适配你的问题!
如下是针对上述问题进行专业角度剖析答疑,不喜勿喷,仅供参考:
✅ 问题理解
如上题主遇到的是一个典型的 微信服务号 JS-SDK 签名验证失败 的问题!🔍
核心问题分析:
从错误信息 "config:invalid signature" 可以看出:
- 微信 JS-SDK 的 wx.config() 配置失败
- 签名(signature)验证不通过
- 导致无法调用相机、相册等权限接口
技术背景: 微信服务号调用设备权限(相机、相册、位置等)需要:
可能的原因:
- ❌ 签名算法错误
- ❌ URL 不匹配(最常见!)
- ❌ jsapi_ticket 过期或获取失败
- ❌ 参数顺序或格式错误
- ❌ 特殊字符未正确处理
✅ 问题解决方案
🟢 方案 A:标准签名流程排查与修复(最推荐)
这是最全面的解决方案,逐步排查所有可能的问题点。
Step 1: 检查签名算法实现
Java 后端正确的签名实现:
import java.security.MessageDigest;
import java.util.*;
import org.springframework.web.client.RestTemplate;
import com.alibaba.fastjson.JSONObject;
public class WeChatJSSDKService {
private static final String APPID = "你的AppID";
private static final String APPSECRET = "你的AppSecret";
// 缓存 access_token 和 jsapi_ticket(重要!避免频繁请求)
private static String cachedAccessToken = null;
private static String cachedJsapiTicket = null;
private static long tokenExpireTime = 0;
private static long ticketExpireTime = 0;
/**
* 获取 Access Token
*/
public String getAccessToken() {
long currentTime = System.currentTimeMillis();
// 检查缓存是否有效(提前5分钟刷新)
if (cachedAccessToken != null && currentTime < tokenExpireTime – 300000) {
return cachedAccessToken;
}
String url = String.format(
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
APPID, APPSECRET
);
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(url, String.class);
JSONObject jsonObject = JSONObject.parseObject(response);
if (jsonObject.containsKey("access_token")) {
cachedAccessToken = jsonObject.getString("access_token");
int expiresIn = jsonObject.getInteger("expires_in");
tokenExpireTime = currentTime + (expiresIn * 1000);
return cachedAccessToken;
} else {
throw new RuntimeException("获取 access_token 失败: " + response);
}
}
/**
* 获取 jsapi_ticket
*/
public String getJsapiTicket() {
long currentTime = System.currentTimeMillis();
// 检查缓存
if (cachedJsapiTicket != null && currentTime < ticketExpireTime – 300000) {
return cachedJsapiTicket;
}
String accessToken = getAccessToken();
String url = String.format(
"https://api.weixin.qq.com/cgi-bin/ticket/getjsapi_ticket?access_token=%s&type=jsapi",
accessToken
);
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(url, String.class);
JSONObject jsonObject = JSONObject.parseObject(response);
if (jsonObject.getInteger("errcode") == 0) {
cachedJsapiTicket = jsonObject.getString("ticket");
int expiresIn = jsonObject.getInteger("expires_in");
ticketExpireTime = currentTime + (expiresIn * 1000);
return cachedJsapiTicket;
} else {
throw new RuntimeException("获取 jsapi_ticket 失败: " + response);
}
}
/**
* 生成签名配置(核心方法)
*/
public Map<String, String> generateSignature(String url) {
// 1. 获取 jsapi_ticket
String jsapiTicket = getJsapiTicket();
// 2. 生成随机字符串
String nonceStr = UUID.randomUUID().toString().replace("-", "");
// 3. 生成时间戳
long timestamp = System.currentTimeMillis() / 1000;
// 4. 🚨 关键:URL 处理(去掉 # 后面的部分)
String signUrl = url.split("#")[0];
// 5. 按字典序拼接参数字符串
String signString = String.format(
"jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s",
jsapiTicket, nonceStr, timestamp, signUrl
);
// 6. SHA1 加密
String signature = sha1(signString);
// 7. 返回配置参数
Map<String, String> config = new HashMap<>();
config.put("appId", APPID);
config.put("timestamp", String.valueOf(timestamp));
config.put("nonceStr", nonceStr);
config.put("signature", signature);
config.put("jsApiList", "chooseImage,uploadImage,getLocalImgData"); // 根据需要配置
// 调试日志(生产环境请移除)
System.out.println("=== 签名调试信息 ===");
System.out.println("URL: " + signUrl);
System.out.println("jsapi_ticket: " + jsapiTicket);
System.out.println("nonceStr: " + nonceStr);
System.out.println("timestamp: " + timestamp);
System.out.println("签名字符串: " + signString);
System.out.println("signature: " + signature);
return config;
}
/**
* SHA1 加密
*/
private String sha1(String str) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(str.getBytes("UTF-8"));
byte[] messageDigest = digest.digest();
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
throw new RuntimeException("SHA1 加密失败", e);
}
}
}
Controller 实现:
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/wechat")
public class WeChatController {
private final WeChatJSSDKService weChatService = new WeChatJSSDKService();
/**
* 获取 JS-SDK 签名配置
*/
@GetMapping("/jsconfig")
public Map<String, String> getJSConfig(@RequestParam("url") String url) {
// 🚨 关键:URL 解码(如果前端传递时编码了)
try {
url = java.net.URLDecoder.decode(url, "UTF-8");
} catch (Exception e) {
// 如果解码失败,使用原始 URL
}
return weChatService.generateSignature(url);
}
}
Step 2: 前端正确调用方式
HTML/JavaScript 实现:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信相机调用</title>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
<button id="chooseImageBtn">选择图片</button>
<script>
// 🚨 关键:获取完整的当前页面 URL(不包含 hash)
function getCurrentUrl() {
// 方式1: 去掉 hash
let url = window.location.href.split('#')[0];
// 方式2: 如果是 iOS,需要使用首次进入时的 URL(iOS 微信的坑)
if (window.__wxjs_is_wkwebview === true) {
// iOS 环境,使用首次进入的 URL
if (!window.entryUrl) {
window.entryUrl = url;
}
url = window.entryUrl;
}
return url;
}
// 初始化微信 JS-SDK
function initWeChatJSSDK() {
const currentUrl = getCurrentUrl();
console.log('当前页面 URL:', currentUrl);
// 请求后端获取签名配置
fetch(`/api/wechat/jsconfig?url=${encodeURIComponent(currentUrl)}`)
.then(response => response.json())
.then(config => {
console.log('签名配置:', config);
wx.config({
debug: true, // 开发时建议开启,会弹出配置信息
appId: config.appId,
timestamp: config.timestamp,
nonceStr: config.nonceStr,
signature: config.signature,
jsApiList: [
'chooseImage', // 选择图片
'uploadImage', // 上传图片
'getLocalImgData', // 获取本地图片数据
'previewImage', // 预览图片
'downloadImage' // 下载图片
]
});
wx.ready(function() {
console.log('✅ 微信 JS-SDK 配置成功!');
alert('JS-SDK 初始化成功!');
});
wx.error(function(res) {
console.error('❌ 微信 JS-SDK 配置失败:', res);
alert('配置失败: ' + res.errMsg);
});
})
.catch(error => {
console.error('获取签名配置失败:', error);
alert('请求失败: ' + error);
});
}
// 调用相机/相册
document.getElementById('chooseImageBtn').addEventListener('click', function() {
wx.chooseImage({
count: 9, // 最多可选择的图片数量
sizeType: ['original', 'compressed'], // 原图和压缩图
sourceType: ['album', 'camera'], // 相册和相机
success: function(res) {
const localIds = res.localIds; // 返回选定照片的本地 ID 列表
console.log('选择的图片:', localIds);
// 可以进一步上传图片
uploadImages(localIds);
},
fail: function(res) {
console.error('选择图片失败:', res);
alert('选择失败: ' + res.errMsg);
}
});
});
// 上传图片到微信服务器
function uploadImages(localIds) {
localIds.forEach((localId, index) => {
wx.uploadImage({
localId: localId,
isShowProgressTips: 1,
success: function(res) {
const serverId = res.serverId; // 返回图片的服务器端 ID
console.log(`图片 ${index + 1} 上传成功,serverId:`, serverId);
// 将 serverId 发送到自己的服务器
saveImageToServer(serverId);
}
});
});
}
// 保存到自己的服务器
function saveImageToServer(serverId) {
fetch('/api/wechat/saveImage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ serverId: serverId })
})
.then(response => response.json())
.then(data => {
console.log('图片保存成功:', data);
});
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', initWeChatJSSDK);
</script>
</body>
</html>
Step 3: 关键排查点(逐一检查)
检查清单:
// 1. URL 检查工具类
public class URLValidator {
public static voi
// 去除 hash
String cleanedUrl = backendUrl.split("#")[0];
System.out.println("清理后: " + cleanedUrl);
// 检查协议
if (!cleanedUrl.startsWith("http://") && !cleanedUrl.startsWith("https://")) {
System.err.println("❌ URL 协议错误!");
}
// 检查是否包含端口号(如果测试环境有端口,生产环境需保持一致)
if (cleanedUrl.contains(":80/") || cleanedUrl.contains(":443/")) {
System.out.println("⚠️ URL 包含端口号,请确认微信后台配置一致");
}
// 检查特殊字符
if (cleanedUrl.contains(" ") || cleanedUrl.contains("\\n")) {
System.err.println("❌ URL 包含非法字符!");
}
}
}
常见错误对照表:
| invalid signature | URL 不匹配 | 确保前后端 URL 完全一致,去除 # 后内容 |
| invalid appid | AppID 错误 | 检查配置文件中的 AppID |
| invalid url domain | 域名未配置 | 在微信公众平台配置 JS 接口安全域名 |
| access_token expired | Token 过期 | 检查缓存机制,确保提前刷新 |
| ticket expired | Ticket 过期 | 同上 |
🟡 方案 B:使用微信官方签名校验工具排查
步骤:
访问微信官方工具:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
输入你的参数:
jsapi_ticket: 你的 ticket
noncestr: 你的随机字符串
timestamp: 你的时间戳
url: 你的完整 URL(注意去除 #)
调试接口:
@GetMapping("/debug/signature")
public Map<String, Object> debugSignature(@RequestParam("url") String url) {
Map<String, Object> debug = new HashMap<>();
try {
String jsapiTicket = weChatService.getJsapiTicket();
String nonceStr = UUID.randomUUID().toString().replace("-", "");
long timestamp = System.currentTimeMillis() / 1000;
String cleanUrl = url.split("#")[0];
String signString = String.format(
"jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s",
jsapiTicket, nonceStr, timestamp, cleanUrl
);
String signature = weChatService.sha1(signString);
debug.put("jsapi_ticket", jsapiTicket);
debug.put("noncestr", nonceStr);
debug.put("timestamp", timestamp);
debug.put("url", cleanUrl);
debug.put("signString", signString);
debug.put("signature", signature);
debug.put("success", true);
// 提供官方校验工具链接
debug.put("verifyUrl", "https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign");
} catch (Exception e) {
debug.put("success", false);
debug.put("error", e.getMessage());
}
return debug;
}
🔴 方案 C:iOS 和 Android 分别处理(兼容性方案)
微信在 iOS 和 Android 上的 URL 处理有差异:
// 完整的跨平台兼容方案
function initWeChatSDK() {
let signUrl = window.location.href.split('#')[0];
// iOS 特殊处理
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
const isAndroid = /Android/i.test(navigator.userAgent);
if (isIOS) {
// iOS 下需要使用首次进入页面时的 URL
if (!window.firstEntryUrl) {
window.firstEntryUrl = signUrl;
}
signUrl = window.firstEntryUrl;
console.log('iOS 使用首次进入 URL:', signUrl);
} else if (isAndroid) {
// Android 使用当前 URL
console.log('Android 使用当前 URL:', signUrl);
}
// 发送请求获取配置
getWeChatConfig(signUrl);
}
// 监听页面后退/前进(iOS)
window.addEventListener('popstate', function() {
// iOS 下不重新初始化,使用首次 URL
if (!/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
initWeChatSDK();
}
});
✅ 问题延伸
1. 微信公众平台配置检查 🔧
必须完成的配置:
JS 接口安全域名配置
- 登录微信公众平台
- 进入「公众号设置」→「功能设置」
- 配置「JS接口安全域名」
- ⚠️ 注意:不要带 http:// 或 https://,只填写域名
正确示例:www.example.com
错误示例:http://www.example.com
网页授权域名配置(要获取用户信息)
- 「接口权限」→「网页授权」
- 配置授权回调域名
IP 白名单配置
- 「基本配置」→「IP白名单」
- 添加服务器 IP 地址
2. 图片下载到自己服务器 📥
@Service
public class WeChatMediaService {
/**
* 从微信服务器下载图片
*/
public String downloadImage(String serverId) {
String accessToken = weChatService.getAccessToken();
String url = String.format(
"https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s",
accessToken, serverId
);
try {
RestTemplate restTemplate = new RestTemplate();
byte[] imageBytes = restTemplate.getForObject(url, byte[].class);
// 保存到本地或云存储
String fileName = UUID.randomUUID().toString() + ".jpg";
String savePath = "/uploads/" + fileName;
Files.write(Paths.get(savePath), imageBytes);
return savePath;
} catch (Exception e) {
throw new RuntimeException("下载图片失败", e);
}
}
}
3. 进阶功能:图片压缩和裁剪 ✂️
// 前端压缩图片
function compressImage(localId, callback) {
wx.getLocalImgData({
localId: localId,
success: function(res) {
let localData = res.localData;
// iOS 返回的是 base64,Android 返回的是路径
if (!localData.startsWith('data:image')) {
localData = 'data:image/jpeg;base64,' + localData;
}
// 使用 Canvas 压缩
const img = new Image();
img.src = localData;
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置压缩后的尺寸
const maxWidth = 1200;
const maxHeight = 1200;
let width = img.width;
let height = img.height;
if (width > height && width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
} else if (height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// 输出压缩后的 base64
const compressedData = canvas.toDataURL('image/jpeg', 0.7);
callback(compressedData);
};
}
});
}
✅ 问题预测
可能遇到的其他问题 ⚠️
问题 1: 开发环境和生产环境签名不一致
原因:
- 测试域名和生产域名不同
- 本地开发使用 localhost 或内网 IP
解决方案:
@Configuration
public class WeChatConfig {
@Value("${wechat.domain:}")
private String domain;
public String getActualUrl(String requestUrl) {
// 如果配置了域名,替换为实际域名
if (StringUtils.isNotEmpty(domain)) {
try {
URL url = new URL(requestUrl);
return domain + url.getPath() +
(url.getQuery() != null ? "?" + url.getQuery() : "");
} catch (Exception e) {
return requestUrl;
}
}
return requestUrl;
}
}
配置文件:
# application.yml
wechat:
appid: wxxxxxxxxxxx
appsecret: xxxxxxxxxxxxxxxx
# 开发环境
domain: https://dev.example.com
# application-prod.yml
wechat:
# 生产环境
domain: https://www.example.com
问题 2: 频繁调用接口导致限流
解决方案:
@Component
public class WeChatRateLimiter {
private final Map<String, Long> requestCache = new ConcurrentHashMap<>();
private final long MIN_INTERVAL = 100; // 最小请求间隔(毫秒)
public synchronized void checkRateLimit(String key) {
long now = System.currentTimeMillis();
Long lastTime = requestCache.get(key);
if (lastTime != null && (now – lastTime) < MIN_INTERVAL) {
try {
Thread.sleep(MIN_INTERVAL – (now – lastTime));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
requestCache.put(key, System.currentTimeMillis());
}
}
问题 3: 用户拒绝授权相机权限
解决方案:
wx.chooseImage({
count: 1,
sourceType: ['camera', 'album'],
success: function(res) {
console.log('用户选择了图片');
},
fail: function(res) {
if (res.errMsg.indexOf('cancel') !== –1) {
alert('您取消了选择图片');
} else if (res.errMsg.indexOf('permission') !== –1) {
alert('请在手机设置中允许微信访问相机权限');
} else {
alert('选择图片失败:' + res.errMsg);
}
}
});
✅ 小结
🎯 关键要点总结
1. 签名失败的核心原因(优先级排序):
- 🥇 URL 不匹配(80% 的问题)- 前后端 URL 必须完全一致,去除 #
- 🥈 iOS/Android 兼容性(15%)- iOS 需使用首次进入 URL
- 🥉 Token/Ticket 过期(5%)- 做好缓存和提前刷新
2. 必须检查的配置项:
- ✅ 微信公众平台的 JS 接口安全域名配置
- ✅ AppID 和 AppSecret 正确无误
- ✅ 服务器 IP 在白名单中
- ✅ access_token 和 jsapi_ticket 正确缓存
3. 调试技巧:
- 开启 wx.config({ debug: true }) 查看详细错误
- 使用官方签名校验工具验证
- 添加详细的日志输出(URL、ticket、签名字符串)
- 在测试环境先验证通过再上生产
4. 性能优化建议:
- 缓存 access_token(7200秒)
- 缓存 jsapi_ticket(7200秒)
- 使用 Redis 做分布式缓存
- 避免频繁请求微信接口
5. 安全建议:
- AppSecret 不要写在前端代码
- 签名生成必须在后端完成
- 添加请求频率限制
- 生产环境关闭 debug 模式
📋 快速排查检查清单
□ 1. 检查微信公众平台 JS 接口安全域名是否配置
□ 2. 检查 AppID 和 AppSecret 是否正确
□ 3. 检查前端传递的 URL 是否完整(去除 #)
□ 4. 检查后端签名算法是否正确(参数顺序、SHA1)
□ 5. 检查 access_token 是否有效
□ 6. 检查 jsapi_ticket 是否有效
□ 7. 检查是否处理了 iOS/Android 差异
□ 8. 使用官方工具验证签名是否正确
□ 9. 检查服务器时间是否准确
□ 10. 检查是否有代理或网络问题影响请求
🔗 相关资源
- 微信 JS-SDK 官方文档
- 微信签名校验工具
- 微信开发者社区
🌹 结语 & 互动说明
希望以上分析与解决思路,能为你当前的问题提供一些有效线索或直接可用的操作路径。
若你按文中步骤执行后仍未解决:
- 不必焦虑或抱怨,这很常见——复杂问题往往由多重因素叠加引起;
- 欢迎你将最新报错信息、关键代码片段、环境说明等补充到评论区;
- 我会在力所能及的范围内,结合大家的反馈一起帮你继续定位 👀
💡 如果你有更优或更通用的解法:
- 非常欢迎在评论区分享你的实践经验或改进方案;
- 你的这份补充,可能正好帮到更多正在被类似问题困扰的同学;
- 正所谓「赠人玫瑰,手有余香」,也算是为技术社区持续注入正向循环
🧧 文末福利:技术成长加速包 🧧
文中部分问题来自本人项目实践,部分来自读者反馈与公开社区案例,也有少量经由全网社区与智能问答平台整理而来。
若你尝试后仍没完全解决问题,还请多一点理解、少一点苛责——技术问题本就复杂多变,没有任何人能给出对所有场景都 100% 套用的方案。
如果你已经找到更适合自己项目现场的做法,非常建议你沉淀成文档或教程,这不仅是对他人的帮助,更是对自己认知的再升级。
如果你还在持续查 Bug、找方案,可以顺便逛逛我专门整理的 Bug 专栏:《全栈 Bug 调优(实战版)》。 这里收录的都是在真实场景中踩过的坑,希望能帮你少走弯路,节省更多宝贵时间。
✍️ 如果这篇文章对你有一点点帮助:
- 欢迎给 bug菌 来个一键三连:关注 + 点赞 + 收藏
- 你的支持,是我持续输出高质量实战内容的最大动力。
同时也欢迎关注我的硬核公众号 「猿圈奇妙屋」:
获取第一时间更新的技术干货、BAT 等互联网公司最新面试真题、4000G+ 技术 PDF 电子书、简历 / PPT 模板、技术文章 Markdown 模板等资料,统统免费领取。 你能想到的绝大部分学习资料,我都尽量帮你准备齐全,剩下的只需要你愿意迈出那一步来拿。
🫵 Who am I?
我是 bug菌:
- 热活跃于 CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等技术社区;
- CSDN 博客之星 Top30、华为云多年度十佳博主/卓越贡献者、掘金多年度人气作者 Top40;
- 掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看 👈️ 硬核技术公众号 「猿圈奇妙屋」 期待你的加入,一起进阶、一起打怪升级。
– End –



