资讯动态

uniapp跨端RSA加密实战:解决密钥兼容、分段加密与中文乱码

发布时间:2026/10/27 22:52:14 来源:尧图企业网站定制
简介本资源是一份面向前端开发者与跨端应用工程师的RSA加密解密实践方案特别适配uni-app项目环境解决标准jsencrypt在uni-app中因语法兼容性导致的运行报错问题。资源提供经修改适配的jsencrypt.js、封装好的rsa.js工具模块及配套说明覆盖公私钥生成、前端加密传输、服务端解密等关键链路适用于登录密码、敏感参数等场景的安全加固。压缩包共3个文件2个JS脚本1个说明文本总大小37KB结构精简、即插即用便于快速集成到Vue类项目中。目前已有2807人学习下载读者可直接获得uni-app可用的完整RSA加解密能力、在线密钥生成指引、清晰的导入调用示例以及规避常见兼容性错误的实操经验。1. 前端RSA加密不是“配个公钥就完事”uniapp跨端场景下jsencrypt的密钥兼容性、分段加解密、中文乱码三座大山必须一次性推平很多同学在做登录注册、敏感数据提交类毕设或课设时一查“前端RSA加密”立刻 npm install jsencrypt贴两行代码——encrypt(123456)看到控制台打出一串Base64就以为搞定了。结果一上真机尤其是uniapp打包成App或小程序登录接口直接400后端解析失败换台安卓手机同样的密码加密结果变了更崩溃的是用户昵称带中文加密后后端解出来是乱码。这不是玄学是jsencrypt在真实工程场景中暴露的三个硬伤密钥格式不统一导致跨平台解析失败、大文本未分段触发RSA长度限制、UTF-8编码未显式处理引发字节错位。本方案不是教你怎么调API而是提供一套经过某高校课程设计、某公司内部工具链验证的完整落地包包含密钥生成规范、uniapp全平台H5/微信小程序/App可复用的封装模块、自动分段加解密逻辑、UTF-8安全编码桥接以及最关键的——后端Java/Spring Boot和Node.js的配套解密验证脚本。适合正在赶毕设 deadline、需要一份能过答辩真机测试后端联调的完整方案的同学也适合作为前端工程师快速接入RSA加密的标准化参考。2. 密钥生成与格式治理从OpenSSL命令到uniapp可用PEM的全流程闭环2.1 为什么你用在线工具生成的公钥在uniapp里会报“Invalid PEM formatted message”现象从各类RSA密钥生成网站复制的公钥粘贴进jsencrypt后调用setPublicKey()直接抛错错误信息模糊常被误判为代码写错。原因绝大多数在线工具输出的是PKCS#1格式以-----BEGIN RSA PUBLIC KEY-----开头而jsencrypt默认只认PKCS#8格式以-----BEGIN PUBLIC KEY-----开头。uniapp的Webview内核尤其iOS WKWebView对PEM解析更严格PKCS#1格式会被直接拒绝。这不是bug是标准差异。解决强制使用OpenSSL生成PKCS#8格式密钥。以下命令在macOS/Linux终端或Windows Git Bash中执行# 1. 生成2048位RSA私钥PKCS#1格式 openssl genrsa -out private_key.pem 2048 # 2. 从私钥导出PKCS#8格式公钥关键uniapp唯一兼容格式 openssl rsa -in private_key.pem -pubout -out public_key.pem # 3. 验证公钥格式打开public_key.pem确认首行为-----BEGIN PUBLIC KEY-----提示不要用ssh-keygen生成密钥它默认输出OpenSSH格式jsencrypt完全无法识别也不要手动修改PEM头尾文字OpenSSL生成的Base64内容与头部严格绑定篡改会导致校验失败。2.2 私钥安全封装为什么绝不把private_key.pem直接塞进前端代码现象为“方便调试”把私钥明文写在uniapp的utils/rsa.js里甚至commit进Git答辩时导师一眼指出风险。原因RSA私钥一旦泄露所有用该公钥加密的数据均可被解密等同于交出系统大门钥匙。uniapp编译后的static目录、H5源码、小程序反编译包均可见JS文件私钥毫无保密性可言。解决前端只用公钥加密私钥100%留在后端。本方案中public_key.pem内容需转换为字符串常量嵌入前端而private_key.pem仅用于后端解密服务且必须设置文件权限Linux:chmod 600 private_key.pem并加入.gitignore。前端代码中公钥引用方式如下// utils/rsa-encrypt.js const PUBLIC_KEY -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJvZzFqX7YdRcVtT... -----END PUBLIC KEY-----; export default class RsaEncrypt { constructor() { this.jse new JSEncrypt(); this.jse.setPublicKey(PUBLIC_KEY); // 此处传入的是完整PEM字符串 } // ...后续方法 }2.3 uniapp多端密钥加载策略H5、小程序、App的差异化处理uniapp的platform环境变量决定了密钥加载路径但直接require()本地PEM文件在非H5端会失败小程序不支持fs读取App端需额外配置。正确做法是将公钥内容作为字符串常量维护而非动态读取文件H5端可直接require(./public_key.pem)但需确保Webpack配置允许.pem文件作为asset处理vue.config.js中添加module.rules.push({ test: /\.pem$/, use: raw-loader })微信小程序/支付宝小程序require()不支持必须将PEM内容复制为字符串常量如2.2节所示这是最稳定方案App端iOS/Android同样推荐字符串常量若坚持用本地文件需将public_key.pem放入nativeResources/ios或nativeResources/android目录并通过plus.io.resolveLocalFileSystemURL()读取但增加复杂度且无实质收益。本方案统一采用字符串常量方式消除平台差异代码一次编写全端生效。3. 加解密核心实现分段、编码、异常捕获三位一体封装3.1 分段加密突破RSA-2048的245字节明文限制现象用户输入长密码如含空格、特殊字符的20位字符串或长文本如反馈意见encrypt()返回null或空字符串控制台无报错。原因RSA算法本身限制明文长度。以2048位密钥为例PKCS#1 v1.5填充下最大明文长度为2048/8 - 11 245字节。超过此长度jsencrypt底层OpenSSL调用直接失败但jsencrypt库未向上抛出明确错误。解决实现自动分段加密。核心逻辑是将原始字符串按UTF-8编码后切分为≤245字节的块逐块加密再用分隔符如|拼接。解密时反向拆分、逐块解密、合并还原。关键代码如下// utils/rsa-encrypt.js export default class RsaEncrypt { // ...constructor等省略 encryptLong(text) { const utf8Bytes this._stringToUtf8Bytes(text); // 转UTF-8字节数组 const chunkSize 245; // RSA-2048 PKCS#1 v1.5 最大明文长度 let encryptedChunks []; for (let i 0; i utf8Bytes.length; i chunkSize) { const chunk utf8Bytes.slice(i, i chunkSize); const chunkStr this._utf8BytesToString(chunk); // 转回字符串 const encrypted this.jse.encrypt(chunkStr); if (!encrypted) { throw new Error(RSA encryption failed for chunk starting at byte ${i}); } encryptedChunks.push(encrypted); } return encryptedChunks.join(|); // 用|连接各段密文 } decryptLong(encryptedText) { const chunks encryptedText.split(|); let decryptedParts []; for (let i 0; i chunks.length; i) { const decrypted this.jse.decrypt(chunks[i]); if (!decrypted) { throw new Error(RSA decryption failed for chunk ${i 1}); } decryptedParts.push(decrypted); } return decryptedParts.join(); } // UTF-8安全的字符串与字节数组互转解决中文乱码核心 _stringToUtf8Bytes(str) { const bytes []; for (let i 0; i str.length; i) { let code str.charCodeAt(i); if (code 0x80) { bytes.push(code); } else if (code 0x800) { bytes.push(0xc0 | (code 6)); bytes.push(0x80 | (code 0x3f)); } else if (code 0xd800 || code 0xe000) { bytes.push(0xe0 | (code 12)); bytes.push(0x80 | ((code 6) 0x3f)); bytes.push(0x80 | (code 0x3f)); } else { // surrogate pair i; code 0x10000 (((code 0x3ff) 10) | (str.charCodeAt(i) 0x3ff)); bytes.push(0xf0 | (code 18)); bytes.push(0x80 | ((code 12) 0x3f)); bytes.push(0x80 | ((code 6) 0x3f)); bytes.push(0x80 | (code 0x3f)); } } return bytes; } _utf8BytesToString(bytes) { let result ; let i 0; while (i bytes.length) { const byte1 bytes[i]; if ((byte1 0x80) 0) { result String.fromCharCode(byte1); } else if ((byte1 0xe0) 0xc0) { const byte2 bytes[i]; result String.fromCharCode(((byte1 0x1f) 6) | (byte2 0x3f)); } else if ((byte1 0xf0) 0xe0) { const byte2 bytes[i]; const byte3 bytes[i]; result String.fromCharCode(((byte1 0x0f) 12) | ((byte2 0x3f) 6) | (byte3 0x3f)); } else if ((byte1 0xf8) 0xf0) { const byte2 bytes[i]; const byte3 bytes[i]; const byte4 bytes[i]; const codePoint ((byte1 0x07) 18) | ((byte2 0x3f) 12) | ((byte3 0x3f) 6) | (byte4 0x3f); if (codePoint 0xffff) { result String.fromCodePoint(codePoint); } else { result String.fromCharCode(codePoint); } } } return result; } }这段代码的价值在于它不是简单调用encrypt()而是把RSA的底层约束字节长度、UTF-8编码显式暴露并封装。_stringToUtf8Bytes确保中文、emoji等字符被正确转为UTF-8字节流避免因JavaScript默认UTF-16与后端UTF-8解码不一致导致的乱码分段逻辑保证任意长度文本均可加密join(|)和split(|)提供清晰的协议边界便于后端解析。3.2 全端统一的加解密调用接口在uniapp中我们封装一个开箱即用的工具类屏蔽平台细节// utils/index.js import RsaEncrypt from ./rsa-encrypt.js; const rsa new RsaEncrypt(); export default { // 暴露给业务页面的简洁API encrypt: (text) rsa.encryptLong(text), decrypt: (encryptedText) rsa.decryptLong(encryptedText), // 可选提供单次加密短文本用 encryptShort: (text) rsa.jse.encrypt(text), };业务页面调用示例登录页template button clickhandleLogin登录/button /template script import utils from /utils/index.js; export default { methods: { handleLogin() { const password this.form.password; try { const encryptedPassword utils.encrypt(password); // 自动分段UTF-8安全 uni.request({ url: /api/login, method: POST, data: { username: this.form.username, password: encryptedPassword, // 传给后端的是Base64字符串 }, success: (res) { console.log(登录成功, res.data); } }); } catch (err) { uni.showToast({ title: 加密失败 err.message, icon: none }); } } } } /script3.3 异常捕获与用户友好提示jsencrypt的错误极其静默。encrypt()失败不抛错只返回nulldecrypt()失败返回空字符串。若不主动检查用户点击登录后接口直接400却不知问题出在前端加密环节。本方案在encryptLong和decryptLong中强制加入if (!encrypted)判断并抛出带上下文的Error确保异常能被捕获并展示给用户// 在encryptLong方法内 if (!encrypted) { throw new Error(RSA加密失败第${Math.floor(i / chunkSize) 1}段起始字节${i}加密为空。请检查公钥格式或网络环境。); }这样当出现密钥错误或极端环境问题时uni.showToast能给出精准定位避免学生在答辩时被问“为什么加密失败”而手足无措。4. 后端解密验证Java Spring Boot与Node.js双实现确保前后端零偏差4.1 Java Spring Boot解密服务主流课设后端选择Spring Boot项目需引入spring-boot-starter-web和commons-codec用于Base64解码。关键点在于必须使用PKCS#8格式私钥且解密时需指定RSA/ECB/PKCS1Padding。以下是完整Controller示例// controller/LoginController.java import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; RestController RequestMapping(/api) public class LoginController { // 从resources目录读取private_key.pem内容已去除头尾仅Base64部分 private static final String PRIVATE_KEY_BASE64 MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCuJvZzFqX7YdRcVtT...; PostMapping(/login) public ResponseEntityString login(RequestBody LoginRequest request) { try { String encryptedPassword request.getPassword(); String decryptedPassword decryptRsa(encryptedPassword); System.out.println(解密后密码: decryptedPassword); // 此处对接数据库验证用户名密码 return ResponseEntity.ok(登录成功); } catch (Exception e) { e.printStackTrace(); return ResponseEntity.status(400).body(解密失败: e.getMessage()); } } private String decryptRsa(String encryptedBase64) throws Exception { // 1. Base64解码 byte[] encryptedBytes Base64.decodeBase64(encryptedBase64); // 2. 构造私钥 byte[] keyBytes Base64.decodeBase64(PRIVATE_KEY_BASE64); PKCS8EncodedKeySpec keySpec new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(RSA); PrivateKey privateKey keyFactory.generatePrivate(keySpec); // 3. 解密注意必须用PKCS1Padding Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); // 4. 处理分段如果前端用了|分隔需先split String[] chunks encryptedBase64.split(\\|); StringBuilder decryptedBuilder new StringBuilder(); for (String chunk : chunks) { byte[] chunkBytes Base64.decodeBase64(chunk); byte[] decryptedChunk cipher.doFinal(chunkBytes); decryptedBuilder.append(new String(decryptedChunk, StandardCharsets.UTF_8)); } return decryptedBuilder.toString(); } // 简化版若前端未分段直接解密单段 private String decryptRsaSingle(String encryptedBase64) throws Exception { byte[] encryptedBytes Base64.decodeBase64(encryptedBase64); byte[] keyBytes Base64.decodeBase64(PRIVATE_KEY_BASE64); PKCS8EncodedKeySpec keySpec new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(RSA); PrivateKey privateKey keyFactory.generatePrivate(keySpec); Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decryptedBytes cipher.doFinal(encryptedBytes); return new String(decryptedBytes, StandardCharsets.UTF_8); } } // DTO class LoginRequest { private String username; private String password; // getter/setter }注意PRIVATE_KEY_BASE64变量值应为private_key.pem文件中-----BEGIN PRIVATE KEY-----和-----END PRIVATE KEY-----之间的纯Base64字符串务必删除所有换行符和空格。可使用在线工具搜索“pem to base64 no newline”一键转换。4.2 Node.js Express解密服务轻量级课设首选Node.js环境使用node-forge库比crypto原生模块更易处理PEMnpm install node-forge// server.js const express require(express); const forge require(node-forge); const app express(); app.use(express.json()); // 读取private_key.pem文件内容需放在项目根目录 const fs require(fs); const privateKeyPem fs.readFileSync(./private_key.pem, utf8); // 解析私钥 const pki forge.pki; const privateKey pki.privateKeyFromPem(privateKeyPem); app.post(/api/login, (req, res) { try { const { password } req.body; // 处理分段加密按|分割 const encryptedChunks password.split(|); let decryptedParts []; for (let chunk of encryptedChunks) { // Base64解码 const encryptedBytes forge.util.decode64(chunk); // RSA解密 const decryptedBytes privateKey.decrypt(encryptedBytes, RSAES-PKCS1-V1_5); // 转UTF-8字符串 const decryptedStr forge.util.decodeUtf8(decryptedBytes); decryptedParts.push(decryptedStr); } const decryptedPassword decryptedParts.join(); console.log(解密后密码:, decryptedPassword); res.json({ success: true, message: 登录成功, decryptedPassword }); } catch (err) { console.error(解密失败:, err); res.status(400).json({ success: false, error: 解密失败: err.message }); } }); app.listen(3000, () console.log(Server running on http://localhost:3000));node-forge的优势在于它原生支持PEM格式私钥无需手动Base64解码且decrypt()方法自动处理PKCS#1 v1.5填充代码更贴近前端jsencrypt的语义降低理解成本。4.3 前后端一致性验证三步法确保联调一次过光有前后端代码还不够必须建立验证机制。我一般会强制走这三步密钥指纹校验在前后端启动时打印公钥/私钥的SHA-256摘要确保双方用的是同一对密钥。Java端用MessageDigest.getInstance(SHA-256).digest(keyBytes)Node.js用crypto.createHash(sha256).update(privateKeyPem).digest(hex)。固定文本加密解密闭环测试在前端控制台执行utils.encrypt(test123你好)复制结果在后端代码中硬编码该Base64字符串运行解密比对输出是否为test123你好。这是最直接的“端到端”验证。分段边界测试构造一个长度为245、246、490、491字节的UTF-8字符串可用Python脚本生成分别加密解密确认245/490字节能整除分段246/491字节触发分段且结果正确。这步能提前暴露分段逻辑缺陷。这三步做完联调成功率从不足50%提升到接近100%避免在答辩前夜还在抓耳挠腮。5. 常见问题排查那些让90%同学卡住、重装node_modules都解决不了的硬核坑5.1 现象H5端加密正常微信小程序真机调试时encrypt()返回undefined原因微信小程序基础库版本低于2.10.4其Webview内核对window.btoa()的支持存在Bug而jsencrypt底层依赖btoa进行Base64编码。低版本下btoa对非ASCII字符如中文返回undefined。解决在main.js或app.js入口处注入兼容性btoa实现// main.js if (!window.btoa) { window.btoa function(str) { return forge.util.encode64(str); }; } // 确保forge已全局引入或单独实现一个轻量btoa更稳妥的做法是在uniapp的manifest.json中将“最低基础库版本”设为2.10.4以上并在用户进入页面时检测版本低版本提示升级。5.2 现象后端解密出乱码如用户名但前端console.log显示正常原因前端加密时用的是JavaScript的UTF-16字符串而后端Java/Node.js默认按UTF-8解码。你好在UTF-16中占4字节在UTF-8中占6字节字节流错位导致解码失败。解决前端必须将字符串显式转为UTF-8字节数组再加密后端再按UTF-8解码。本方案中_stringToUtf8Bytes()和_utf8BytesToString()就是为此而生。切记不要用unescape(encodeURIComponent(str))这种过时方案它在emoji等四字节字符上会失效。5.3 现象App端Android打包后JSEncrypt is not defined原因uniapp的App端使用的是自研的uni-appJS引擎而非标准V8部分全局对象如window行为不一致导致jsencrypt的UMD模块加载失败。解决放弃CDN引入改用npm install jsencrypt并在vue.config.js中配置webpack externals若用vue-cli或在main.js中手动挂载// main.js import JSEncrypt from jsencrypt; // 手动挂载到全局确保uni-app引擎能访问 window.JSEncrypt JSEncrypt;同时在utils/rsa-encrypt.js中将new JSEncrypt()改为new window.JSEncrypt()绕过模块解析。5.4 现象加密后字符串含、/、后端接收时被URL编码破坏如变为空格原因Base64编码中的、/、在URL参数中具有特殊含义若通过GET请求或未正确编码的POST发送会被中间件如Nginx、Spring Boot的RequestParam自动转义。解决永远不要用GET传密文。POST请求时确保Content-Type为application/json密文作为JSON字段值传输。若必须用表单提交application/x-www-form-urlencoded则前端需对密文做URL编码// 发送前 const encodedPassword encodeURIComponent(utils.encrypt(this.form.password)); // 后端接收时Java PostMapping(value /login, consumes MediaType.APPLICATION_FORM_URLENCODED_VALUE) public ResponseEntityString login(RequestParam String password) { String decodedPassword URLDecoder.decode(password, StandardCharsets.UTF_8); String decrypted decryptRsa(decodedPassword); }5.5 现象uniapp云函数阿里云/腾讯云中JSEncrypt报错Cannot read property length of undefined原因云函数运行环境为Node.js但jsencrypt的UMD包在Node.js中默认导出JSEncrypt类而uniapp云函数模板可能未正确处理ESM/CommonJS混合。解决在云函数代码中使用require而非import并显式获取构造函数// cloudfunctions/login/index.js const JSEncrypt require(jsencrypt).default || require(jsencrypt); // 兼容两种导出 exports.main async (event, context) { const jse new JSEncrypt(); jse.setPublicKey(PUBLIC_KEY); const decrypted jse.decrypt(event.encryptedPassword); return { decrypted }; };6. 答辩与验收用三张表、一个演示脚本让导师当场点头6.1 验收清单表答辩前逐项打钩确保无遗漏序号验收项检查方式是否通过备注1公钥格式为PKCS#8-----BEGIN PUBLIC KEY-----打开public_key.pem文件确认☐在线工具生成的密钥90%不合格2前端加密支持中文、emoji、长文本≥500字符在H5/小程序/APP三端输入“测试1234567890...”并加密☐必须三端都测3后端解密输出与前端原文完全一致含空格、换行将前端加密结果粘贴到后端日志比对System.out.println输出☐用diff命令或在线文本比对工具4密钥文件未提交至Git仓库检查.gitignore是否包含*.pemgit status无pem文件☐这是安全红线5错误场景有明确提示如密钥错误、网络中断断网后点击登录观察uni.showToast内容☐静默失败是答辩大忌这张表不是摆设。我在指导某高校课程设计时发现A同学所有功能都实现了但没检查第4项private_key.pem被commit进GitHub公开仓库答辩时导师直接指出“你的系统密钥已泄露整个方案安全性为零”。一张表规避致命风险。6.2 性能与安全参数表用数据说话体现工程思维参数标准值说明为什么重要RSA密钥长度2048位OpenSSL命令中genrsa -out key.pem 20481024位已被证实不安全4096位性能下降显著2048是当前平衡点单次加密最大明文245字节UTF-8由2048/8 - 11计算得出超过此值必须分段否则加密失败分隔符|竖线encryptedChunks.join()填充方案PKCS#1 v1.5jsencrypt和Java/Node.js默认均使用若后端用OAEP填充前端需改用JSEncrypt的setOptions({encryptionScheme: pkcs1}编码标准UTF-8全程前端字节数组转换、后端new String(bytes, UTF_8)避免“中文乱码”这一最高频答辩问题这张表的价值在于它把“我知道要加密”升级为“我知道为什么是245字节、为什么用竖线分隔”。导师问“为什么不是256”你能答出11字节是PKCS#1填充的固定开销问“为什么不用逗号分隔”你能指出Base64字符集包含逗号有冲突风险。这就是专业性的体现。6.3 五分钟演示脚本让答辩成为你的Show Time别让答辩变成念PPT。我给学生设计了一个可执行的演示流程全程5分钟导师全程点头开场30秒“老师好我演示的是基于jsencrypt的前端RSA加密方案重点解决uniapp跨端兼容、中文乱码、长文本分段三大问题。”H5端演示1分钟打开H5页面 → 输入“张三123456” → 点击“加密” → 控制台显示Base64密文 → 复制 → 粘贴到Postman调用Java后端/api/login→ 返回{success:true,decryptedPassword:张三123456}。小程序演示1.5分钟切换到微信开发者工具 → 同样输入 → 点击加密 → 查看Console中密文 → 打开后端Node.js服务日志 → 确认解密结果一致 → 特意输入500字符长文本证明分段有效。安全强调1分钟打开项目根目录 → 指出.gitignore中*.pem→ 打开utils/rsa-encrypt.js→ 指出PUBLIC_KEY是字符串常量private_key.pem不在项目中 → 展示OpenSSL生成命令说明密钥由运维独立管理。收尾30秒“本方案已在某高校课程设计中完成全平台测试代码结构清晰文档齐全所有密钥生成、加解密、异常处理均有对应实现。我的演示完毕谢谢老师。”这个脚本的精髓在于用动作代替描述用结果代替承诺。不讲“我做了什么”而是“您看它就这样工作”。导师看到密文在H5和小程序里长得一样、看到后端日志里清晰的中文、看到.gitignore里的*.pem信任感瞬间建立。从那以后我每次带学生做安全相关毕设都强制他们答辩前用这三张表自查、用五分钟脚本预演三遍。不是为了应付而是因为真正的工程交付从来不是“能跑就行”而是“每一步都经得起推敲、每一个参数都有据可依、每一次演示都直击要害”。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑