HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

苹果4.3(a)被拒怎么办?码尚友科技分享IPA相似度检测经验

苹果内购 苹果审核

很多开发者认为,苹果反馈 4.3(a) 之后,只要换个图标、改个名称、调整几个页面就能重新提交。

但实际审核过程中,大量应用即使已经更换 UI,依然会再次收到 4.3(a) 拒绝。

根据码尚友科技近两年处理的上架案例统计,苹果对于应用相似性的判断,早已不仅限于界面层面,而是从代码结构、资源文件、业务逻辑、提交环境等多个维度进行综合分析。

苹果4.3(a)到底在检测什么?

苹果审核团队希望每个应用都具备独立价值,而不是简单复制已有产品。

从实际案例来看,相似性检测主要集中在以下几个维度:

检测维度

权重占比(经验数据)

常见问题

代码结构

35%

相同工程模板、相同类结构

资源文件

25%

图片、音频、字体高度重复

功能逻辑

20%

页面流程一致

账号关联

10%

开发者账号存在关联

提交环境

10%

IP、设备环境异常

根据码尚友上架系统统计,在近1000+个处理案例中,仅修改UI但未优化代码结构的项目,二次收到4.3(a)反馈的比例超过72%。

为什么改了界面还是会收到4.3(a)?
很多开发团队只修改:

Logo
App名称
首页配色
部分文案
但实际上:

Controller结构未变化
API调用逻辑未变化
资源目录未变化
工程架构未变化
苹果机审依然能够识别出较高相似度。

因此:

UI差异 ≠ 应用差异

码尚友检测系统发现的高风险项
在项目提交前,我们会通过内部IPA分析工具进行检测。

重点检查:

1、代码相似度分析
检测内容:

类名结构
方法调用链
Framework引用情况
工程目录结构
示例:

检测项

风险等级

类名重复率85%

高风险

页面结构重复率78%

高风险

Framework一致率92%

高风险

图上是2个不相关的APP,所以相似度特别低,事实证明,相似度检测的确可以检测出2个app是否有关联,是否会被4.3a被拒

2、资源包检测
系统会扫描:

图片Hash值
音频文件
视频资源
字体资源
部分项目虽然更换了图片名称,但实际文件内容未变,依然会被识别。

3、业务流程分析
重点分析:

页面跳转路径
功能触发逻辑
用户使用流程
例如:

A应用:

首页 → 会员 → AI生成 → 保存

B应用:

首页 → VIP → AI创作 → 导出

虽然名称不同,但业务路径完全一致,仍然存在较高相似风险。

码尚友科技的解决思路
针对4.3(a)问题,我们通常从四个层面进行优化:

第一层:代码层优化
工程结构重组
模块重新划分
方法逻辑重构
类结构调整
第二层:资源层优化
图片重新设计
资源目录重建
字体资源调整
启动页重构
第三层:业务层优化
增加独立功能模块
调整用户路径
增加业务价值点
第四层:提交层优化
提交环境隔离
审核资料检测
开发者账号评估
历史版本关联分析
数据统计
根据码尚友科技2025-2026年上架案例统计:

项目类型

初次通过率

仅修改UI

28%

UI+资源优化

47%

UI+资源+代码优化

76%

完整检测后提交

结语
苹果4.3(a)并不是简单的“换壳检测”。

从近年的审核趋势来看,苹果对于代码结构、资源内容、业务逻辑以及开发环境的综合分析越来越严格。

如果项目已经连续收到4.3(a)反馈,建议在重新提交前进行完整的IPA检测和风险评估,而不是仅修改图标或界面。

码尚友科技自主研发的IPA检测系统,可针对代码结构、资源文件、业务逻辑及提交环境进行多维度分析,帮助开发者提前发现高风险项,提高审核通过率。

继续阅读 »

很多开发者认为,苹果反馈 4.3(a) 之后,只要换个图标、改个名称、调整几个页面就能重新提交。

但实际审核过程中,大量应用即使已经更换 UI,依然会再次收到 4.3(a) 拒绝。

根据码尚友科技近两年处理的上架案例统计,苹果对于应用相似性的判断,早已不仅限于界面层面,而是从代码结构、资源文件、业务逻辑、提交环境等多个维度进行综合分析。

苹果4.3(a)到底在检测什么?

苹果审核团队希望每个应用都具备独立价值,而不是简单复制已有产品。

从实际案例来看,相似性检测主要集中在以下几个维度:

检测维度

权重占比(经验数据)

常见问题

代码结构

35%

相同工程模板、相同类结构

资源文件

25%

图片、音频、字体高度重复

功能逻辑

20%

页面流程一致

账号关联

10%

开发者账号存在关联

提交环境

10%

IP、设备环境异常

根据码尚友上架系统统计,在近1000+个处理案例中,仅修改UI但未优化代码结构的项目,二次收到4.3(a)反馈的比例超过72%。

为什么改了界面还是会收到4.3(a)?
很多开发团队只修改:

Logo
App名称
首页配色
部分文案
但实际上:

Controller结构未变化
API调用逻辑未变化
资源目录未变化
工程架构未变化
苹果机审依然能够识别出较高相似度。

因此:

UI差异 ≠ 应用差异

码尚友检测系统发现的高风险项
在项目提交前,我们会通过内部IPA分析工具进行检测。

重点检查:

1、代码相似度分析
检测内容:

类名结构
方法调用链
Framework引用情况
工程目录结构
示例:

检测项

风险等级

类名重复率85%

高风险

页面结构重复率78%

高风险

Framework一致率92%

高风险

图上是2个不相关的APP,所以相似度特别低,事实证明,相似度检测的确可以检测出2个app是否有关联,是否会被4.3a被拒

2、资源包检测
系统会扫描:

图片Hash值
音频文件
视频资源
字体资源
部分项目虽然更换了图片名称,但实际文件内容未变,依然会被识别。

3、业务流程分析
重点分析:

页面跳转路径
功能触发逻辑
用户使用流程
例如:

A应用:

首页 → 会员 → AI生成 → 保存

B应用:

首页 → VIP → AI创作 → 导出

虽然名称不同,但业务路径完全一致,仍然存在较高相似风险。

码尚友科技的解决思路
针对4.3(a)问题,我们通常从四个层面进行优化:

第一层:代码层优化
工程结构重组
模块重新划分
方法逻辑重构
类结构调整
第二层:资源层优化
图片重新设计
资源目录重建
字体资源调整
启动页重构
第三层:业务层优化
增加独立功能模块
调整用户路径
增加业务价值点
第四层:提交层优化
提交环境隔离
审核资料检测
开发者账号评估
历史版本关联分析
数据统计
根据码尚友科技2025-2026年上架案例统计:

项目类型

初次通过率

仅修改UI

28%

UI+资源优化

47%

UI+资源+代码优化

76%

完整检测后提交

结语
苹果4.3(a)并不是简单的“换壳检测”。

从近年的审核趋势来看,苹果对于代码结构、资源内容、业务逻辑以及开发环境的综合分析越来越严格。

如果项目已经连续收到4.3(a)反馈,建议在重新提交前进行完整的IPA检测和风险评估,而不是仅修改图标或界面。

码尚友科技自主研发的IPA检测系统,可针对代码结构、资源文件、业务逻辑及提交环境进行多维度分析,帮助开发者提前发现高风险项,提高审核通过率。

收起阅读 »

uniapp x插件 SM2 SM3 SM4 AES RSA 加解密 签名 验签 客户端 服务端 全平台解决方案

sunrains-smutil 插件使用指南

本插件提供SM2、SM3 、 SM4、AES、RSA 客户端(ios,安卓,鸿蒙,微信小程序,h5),服务端(java)SM2、SM3 、 SM4、AES、RSA 算法的跨平台实现,支持以下功能:
SM2 非对称加密算法
✅ SM2 密钥对生成
✅ SM2 数字签名
✅ SM2 签名验证
✅ SM2 数据加密
✅ SM2 数据解密
SM3 密码杂凑算法
✅ SM3 哈希计算(字符串)
✅ HMAC-SM3 密钥哈希
✅ 文件 SM3 哈希
✅ 哈希值验证
SM4 对称加密算法
✅ SM4 密钥生成(含 IV)
✅ SM4 数据加密(CBC 模式)
✅ SM4 数据解密(CBC 模式)
AES 对称加密算法
✅ AES 密钥和 IV 生成
✅ AES 数据加密(CBC 模式, PKCS7/PKCS5 填充)
✅ AES 数据解密(CBC 模式, PKCS7/PKCS5 填充)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5/微信小程序)
RSA 非对称加密算法
✅ RSA 密钥对生成(2048位)
✅ RSA-OAEP-SHA256 数据加密
✅ RSA-OAEP-SHA256 数据解密
✅ SHA256withRSA 数字签名(PKCS#1 v1.5)
✅ SHA256withRSA 签名验证(PKCS#1 v1.5)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5)
⚠️ 微信小程序端暂不支持RSA-OAEP-SHA256加密解密,仅支持签名验签

调用方式

本插件同时支持回调风格异步风格两种调用方式:

方式一:回调风格(传统方式)

import { sm2Sign } from "@/uni_modules/sunrains-util";  

sm2Sign({  
  data: "需要签名的数据",  
  privateKey: "你的私钥",  
  success: (res) => {  
    console.log("签名结果:", res.signature);  
  },  
  fail: (err) => {  
    console.error("签名失败:", err);  
  }  
});

方式二:异步风格(推荐,更简洁)

import { sm2SignAsync } from "@/uni_modules/sunrains-util";  

async function doSign() {  
  try {  
    const result = await sm2SignAsync("需要签名的数据", "你的私钥")  
    console.log("签名结果:", result.signature)  
  } catch (error) {  
    console.error("签名失败:", error)  
  }  
}

平台支持

平台 SM2 实现方式 SM4 实现方式 AES 实现方式 RSA 实现方式 状态
Android BouncyCastle BouncyCastle Java Crypto API Java Crypto API (RSA-OAEP-SHA256) ✅ 完全支持
iOS GMObjC GMObjC CommonCrypto Security Framework (RSA-OAEP-SHA256) ✅ 完全支持
HarmonyOS cryptoFramework cryptoFramework cryptoFramework cryptoFramework (RSA-OAEP-SHA256) ✅ 完全支持
H5 sm-crypto sm-crypto Web Crypto API Web Crypto API (RSA-OAEP-SHA256) ✅ 完全支持
微信小程序 miniprogram-sm-crypto miniprogram-sm-crypto crypto-js jsrsasign (仅签名验签) ⚠️ 部分支持

使用示例

SM2 相关操作

1. 生成 SM2 密钥对

同步方式:

import { sm2GenerateKeyPair } from "@/uni_modules/sunrains-util";  

const result = sm2GenerateKeyPair();  
console.log("公钥:", result.publicKey);  // 04开头的130位十六进制字符串  
console.log("私钥:", result.privateKey); // 64位十六进制字符串

异步方式:

import { sm2GenerateKeyPairAsync } from "@/uni_modules/sunrains-util";  

async function generateKeys() {  
  const result = await sm2GenerateKeyPairAsync()  
  console.log("公钥:", result.publicKey)  
  console.log("私钥:", result.privateKey)  
}

2. SM2 签名

回调风格:

import { sm2Sign } from "@/uni_modules/sunrains-util";  

sm2Sign({  
  data: "需要签名的数据",  
  privateKey: "你的私钥",  
  success: (res) => {  
    console.log("签名结果:", res.signature);  
  },  
  fail: (err) => {  
    console.error("签名失败:", err);  
  }  
});

异步风格(推荐):

import { sm2SignAsync } from "@/uni_modules/sunrains-util";  

async function doSign() {  
  try {  
    const result = await sm2SignAsync("需要签名的数据", "你的私钥")  
    console.log("签名结果:", result.signature)  
  } catch (error) {  
    console.error("签名失败:", error)  
  }  
}

3. SM2 验签

回调风格:

import { sm2Verify } from "@/uni_modules/sunrains-util";  

sm2Verify({  
  data: "原始数据",  
  signature: "签名值",  
  publicKey: "你的公钥",  
  success: (res) => {  
    console.log("验签结果:", res.valid); // true 或 false  
  },  
  fail: (err) => {  
    console.error("验签失败:", err);  
  }  
});

异步风格(推荐):

import { sm2VerifyAsync } from "@/uni_modules/sunrains-util";  

async function doVerify() {  
  try {  
    const result = await sm2VerifyAsync("原始数据", "签名值", "你的公钥")  
    console.log("验签结果:", result.valid) // true 或 false  
  } catch (error) {  
    console.error("验签失败:", error)  
  }  
}

完整实战示例

以下是一个完整的国密算法测试页面示例,及服务端(java)工具类,展示了所有功能的实际使用:

客户端 Vue 3 Composition API 示例

 <template>  
  <view class="order-content">  
    <view class="order-header">  
      <text class="order-title">国密算法测试</text>  
    </view>  

    <scroll-view class="order-list" scroll-y="true">  
      <view class="section">  
        <view class="section-title">SM4 对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="sm4Content" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testSm4Encrypt" class="btn btn-primary">SM4 加密</button>  

        <view class="result-box" v-if="sm4Key.key">  
          <text class="result-label">密钥 (key):</text>  
          <text selectable="true" class="result-value">{{ sm4Key.key }}</text>  
        </view>  

        <view class="result-box" v-if="sm4Key.iv">  
          <text class="result-label">初始向量 (iv):</text>  
          <text selectable="true" class="result-value">{{ sm4Key.iv }}</text>  
        </view>  

        <view class="result-box" v-if="sm4Encrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value">{{ sm4Encrypted }}</text>  
        </view>  
        <button @click="testSm4Decrypt" :disabled="sm4Encrypted.length==0" class="btn btn-secondary">SM4 解密</button>  
        <button @click="testSm4Decrypt" :disabled="sm4Encrypted === ''" :class="sm4Encrypted === '' ? 'btn btn-secondary disabled' : 'btn btn-secondary'" class="btn btn-secondary">SM4 解密</button>  

        <view class="result-box" v-if="sm4Decrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ sm4Decrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM2 数字签名</view>  

        <view class="input-group">  
          <text class="label">待签名内容:</text>  
          <input v-model="sm2SignContent" placeholder="请输入要签名的内容" class="input" />  
        </view>  

        <button @click="testSm2Sign" class="btn btn-primary">生成密钥对并签名</button>  

        <view class="result-box" v-if="sm2KeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2KeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2KeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2KeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2Signature">  
          <text class="result-label">签名结果:</text>  
          <text selectable="true" class="result-value small">{{ sm2Signature }}</text>  
        </view>  

        <button @click="testSm2Verify" :disabled="sm2Signature.length==0" class="btn btn-secondary">验签</button>  

        <view class="result-box" v-if="sm2VerifyResult !== null">  
          <text class="result-label">验签结果:</text>  
          <text class="result-value" :class="sm2VerifyResult ==true? 'success' : 'error'">  
            {{ sm2VerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}  
          </text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM2 非对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="sm2EncryptContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testSm2Encrypt" class="btn btn-primary">SM2 加密</button>  

        <view class="result-box" v-if="sm2EncryptKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2EncryptKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2Encrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value small">{{ sm2Encrypted }}</text>  
        </view>  

        <button @click="testSm2Decrypt" :disabled="sm2Encrypted.length==0" class="btn btn-secondary">SM2 解密</button>  

        <view class="result-box" v-if="sm2Decrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ sm2Decrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM3 哈希算法</view>  

        <!-- 1. SM3 哈希 -->  
        <view class="input-group">  
          <text class="label">待哈希内容:</text>  
          <input v-model="sm3Content" placeholder="请输入要哈希的内容" class="input" />  
        </view>  

        <button @click="testSm3Hash" class="btn btn-primary">SM3 哈希</button>  

        <view class="result-box" v-if="sm3HashResult">  
          <text class="result-label">SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3HashResult }}</text>  
        </view>  

        <!-- 2. SM3 哈希验证 -->  
        <view class="divider"></view>  
        <view class="sub-title">SM3 哈希验证</view>  

        <view class="input-group">  
          <text class="label">验证 - 原始内容:</text>  
          <input v-model="sm3VerifyContent" placeholder="请输入原始内容" class="input" />  
        </view>  

        <button @click="testSm3Verify" :disabled="sm3VerifyContent.length == 0" class="btn btn-secondary">验证哈希值</button>  

        <view class="result-box" v-if="sm3HashMatchResult !== null">  
          <text class="result-label">哈希匹配结果:</text>  
          <text class="result-value" :class="sm3HashMatchResult == true ? 'success' : 'error'">  
            {{ sm3HashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}  
          </text>  
        </view>  

        <!-- 3. HMAC-SM3 哈希 -->  
        <view class="divider"></view>  
        <view class="sub-title">HMAC-SM3 哈希</view>  

        <view class="input-group">  
          <text class="label">HMAC-SM3 待哈希内容:</text>  
          <input v-model="sm3HmacContent" placeholder="请输入要哈希的内容" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">HMAC-SM3 密钥(HEX):</text>  
          <input v-model="sm3HmacKey" placeholder="请输入HMAC密钥(HEX格式)" class="input" />  
        </view>  

        <button @click="testSm3HmacHash" :disabled="sm3HmacContent.length == 0 || sm3HmacKey.length == 0" class="btn btn-primary">HMAC-SM3 哈希</button>  

        <view class="result-box" v-if="sm3HmacHashResult">  
          <text class="result-label">HMAC-SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3HmacHashResult }}</text>  
        </view>  

        <!-- 4. HMAC-SM3 哈希验证 -->  
        <view class="divider"></view>  
        <view class="sub-title">HMAC-SM3 哈希验证</view>  

        <view class="input-group">  
          <text class="label">验证 - 原始内容:</text>  
          <input v-model="sm3HmacVerifyContent" placeholder="请输入原始内容" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">验证 - HMAC 密钥:</text>  
          <input v-model="sm3HmacVerifyKey" placeholder="请输入HMAC密钥" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">验证 - 期望哈希值:</text>  
          <input v-model="sm3HmacExpectedHash" placeholder="请输入期望的哈希值" class="input" />  
        </view>  

        <button @click="testSm3HmacHashWithExpected" :disabled="sm3HmacVerifyContent.length == 0 || sm3HmacVerifyKey.length == 0 || sm3HmacExpectedHash.length == 0" class="btn btn-secondary">验证 HMAC-SM3 哈希值</button>  

        <view class="result-box" v-if="sm3HmacHashMatchResult !== null">  
          <text class="result-label">HMAC-SM3 哈希匹配结果:</text>  
          <text class="result-value" :class="sm3HmacHashMatchResult == true ? 'success' : 'error'">  
            {{ sm3HmacHashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}  
          </text>  
        </view>  

        <!-- 5. 文件 SM3 哈希 -->  
        <view class="divider"></view>  
        <view class="sub-title">文件 SM3 哈希</view>  

        <button @click="chooseImageAndHash" class="btn btn-primary">选择图片并计算 SM3</button>  
        <button @click="chooseFileAndHash" class="btn btn-primary">选择文件并计算 SM3</button>  
        <view class="result-box" v-if="sm3FileName">  
          <text class="result-label">文件名:</text>  
          <text class="result-value">{{ sm3FileName }}</text>  
        </view>  

        <view class="result-box" v-if="sm3FileHashResult">  
          <text class="result-label">文件 SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3FileHashResult }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">AES 对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="aesContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testAesEncrypt" class="btn btn-primary">AES 加密</button>  

        <view class="result-box" v-if="aesKey.key">  
          <text class="result-label">密钥 (key):</text>  
          <text selectable="true" class="result-value">{{ aesKey.key }}</text>  
        </view>  

        <view class="result-box" v-if="aesKey.iv">  
          <text class="result-label">初始向量 (iv):</text>  
          <text selectable="true" class="result-value">{{ aesKey.iv }}</text>  
        </view>  

        <view class="result-box" v-if="aesEncrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value">{{ aesEncrypted }}</text>  
        </view>  

        <button @click="testAesDecrypt" :disabled="aesEncrypted.length==0" class="btn btn-secondary">AES 解密</button>  

        <view class="result-box" v-if="aesDecrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ aesDecrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">RSA 非对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="rsaContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testRsaEncrypt" class="btn btn-primary">RSA 加密</button>  

        <view class="result-box" v-if="rsaKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaEncrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value small">{{ rsaEncrypted }}</text>  
        </view>  

        <button @click="testRsaDecrypt" :disabled="rsaEncrypted.length==0" class="btn btn-secondary">RSA 解密</button>  

        <view class="result-box" v-if="rsaDecrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ rsaDecrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">RSA 数字签名</view>  

        <view class="input-group">  
          <text class="label">待签名内容:</text>  
          <input v-model="rsaSignContent" placeholder="请输入要签名的内容" class="input" />  
        </view>  

        <button @click="testRsaSign" class="btn btn-primary">生成密钥对并签名</button>  

        <view class="result-box" v-if="rsaSignKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaSignKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaSignature">  
          <text class="result-label">签名结果:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignature }}</text>  
        </view>  

        <button @click="testRsaVerify" :disabled="rsaSignature.length==0" class="btn btn-secondary">验签</button>  

        <view class="result-box" v-if="rsaVerifyResult !== null">  
          <text class="result-label">验签结果:</text>  
          <text class="result-value" :class="rsaVerifyResult ==true? 'success' : 'error'">  
            {{ rsaVerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}  
          </text>  
        </view>  
      </view>  

      <view class="order-item" @click="goToDetail(1002, '已完成')">  
        <view class="order-info">  
          <text class="order-id">订单编号:1002</text>  
          <text class="order-status">状态:已完成</text>  
        </view>  
        <view class="order-btn">  
          <text class="go-detail">查看详情</text>  
        </view>  
      </view>  
    </scroll-view>  
  </view>  
</template>  

<script setup lang="uts">  
  import {IdcardOcrModel}from "@/uni_modules/sunrains-idcard-ocr/utssdk/interface.uts"  
  import {Response} from "@/uni_modules/sunrains-utils/index.uts"  

  import * as SM from "@/uni_modules/sunrains-smutil"  

  // ==================== SM4 相关数据 ====================  
  const sm4Content = ref("Hello World")  
  const sm4Key = ref<SM.Sm4Key>({ key: '', iv: '' })  
  const sm4Encrypted = ref("")  
  const sm4Decrypted = ref("")  

  // ==================== SM2 签名验签相关数据 ====================  
  const sm2SignContent = ref("123")  
  const sm2KeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })  
  const sm2Signature = ref("")  
  const sm2VerifyResult = ref<boolean | null>(null)  

  // ==================== SM2 加密解密相关数据 ====================  
  const sm2EncryptContent = ref("Hello SM2")  
  const sm2EncryptKeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })  
  const sm2Encrypted = ref("")  
  const sm2Decrypted = ref("")  

  // ==================== SM3 相关数据 ====================  
  const sm3Content = ref("Hello SM3")  
  const sm3HashResult = ref("")  
  const sm3VerifyContent = ref("")  
  const sm3HashMatchResult = ref<boolean | null>(null)  

  const sm3HmacContent = ref("Hello HMAC-SM3")  
  const sm3HmacKey = ref("0123456789abcdef0123456789abcdef")  // 默认 HMAC 密钥(32字节HEX)  
  const sm3HmacHashResult = ref("")  
  const sm3HmacVerifyContent = ref("")  
  const sm3HmacVerifyKey = ref("")  
  const sm3HmacExpectedHash = ref("")  
  const sm3HmacHashMatchResult = ref<boolean | null>(null)  
  const sm3VerifyResult = ref<boolean|null>(null)  

  const sm3FileName = ref("")  
  const sm3FileHashResult = ref("")  

  // ==================== AES 相关数据 ====================  
  const aesContent = ref("Hello AES")  
  const aesKey = ref<SM.AesKey>({ key: '', iv: '' })  
  const aesEncrypted = ref("")  
  const aesDecrypted = ref("")  

  // ==================== RSA 加密解密相关数据 ====================  
  const rsaContent = ref("Hello RSA")  
  const rsaKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })  
  const rsaEncrypted = ref("")  
  const rsaDecrypted = ref("")  

  // ==================== RSA 签名验签相关数据 ====================  
  const rsaSignContent = ref("需要签名的数据")  
  const rsaSignKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })  
  const rsaSignature = ref("")  
  const rsaVerifyResult = ref<boolean | null>(null)  

  // ==================== SM4 加密 ====================  
  async function testSm4Encrypt() {  
      if (sm4Content.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥  
          const key = await SM.sm4GenerateKeyAsync()  
          sm4Key.value = { key: key.key, iv: key.iv }  
          console.log("SM4 密钥:", key)  

          // 加密  
          const encryptResult = await SM.sm4EncryptAsync(sm4Content.value, key.key, key.iv)  
          sm4Encrypted.value = encryptResult.result  
          console.log("SM4 加密成功:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  

      } catch (e) {  
          console.error("SM4 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== SM4 解密 ====================  
  async function testSm4Decrypt() {  
      if (sm4Encrypted.value.length==0 || sm4Key.value.key.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.sm4DecryptAsync(sm4Encrypted.value, sm4Key.value.key, sm4Key.value.iv)  
          sm4Decrypted.value = decryptResult.result  
          console.log("SM4 解密成功:", decryptResult.result)  
          if (decryptResult.result == sm4Content.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM4 解密失败:", e)  
          uni.showToast({ title: '解密失败', icon: 'error' })  
      }  
  }  

  // ==================== AES 加密 ====================  
  async function testAesEncrypt() {  
      if (aesContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥和 IV  
          const key = await SM.aesGenerateKeyAsync()  
          aesKey.value = { key: key.key, iv: key.iv }  
          console.log("AES 密钥:", key)  

          // 加密  
          const encryptResult = await SM.aesEncryptAsync(aesContent.value, key.key, key.iv)  
          aesEncrypted.value = encryptResult.result  
          console.log("AES 加密成功:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  

      } catch (e) {  
          console.error("AES 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== AES 解密 ====================  
  async function testAesDecrypt() {  
      if (aesEncrypted.value.length==0 || aesKey.value.key.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.aesDecryptAsync(aesEncrypted.value, aesKey.value.key, aesKey.value.iv)  
          aesDecrypted.value = decryptResult.result  
          console.log("AES 解密成功:", decryptResult.result)  
          if (decryptResult.result == aesContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("AES 解密失败:", e)  
          uni.showToast({ title: '解密失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 加密 ====================  
  async function testRsaEncrypt() {  
      if (rsaContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.rsaGenerateKeyPairAsync()  
          rsaKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("RSA 公钥:", keyPair.publicKey)  
          console.log("RSA 私钥:", keyPair.privateKey)  

          // 加密  
          const encryptResult = await SM.rsaEncryptAsync(rsaContent.value, keyPair.publicKey)  
          rsaEncrypted.value = encryptResult.result  
          console.log("RSA 加密结果:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  
      } catch (e) {  
          console.error("RSA 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 解密 ====================  
  async function testRsaDecrypt() {  
      if (rsaEncrypted.value.length==0 || rsaKeyPair.value.privateKey.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.rsaDecryptAsync(rsaEncrypted.value, rsaKeyPair.value.privateKey)  
          rsaDecrypted.value = decryptResult.result  
          console.log("RSA 解密结果:", decryptResult.result)  

          if (decryptResult.result == rsaContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("RSA 解密异常:", e)  
          uni.showToast({ title: '解密异常', icon: 'error' })  
      }  
  }  

  // ==================== RSA 签名 ====================  
  async function testRsaSign() {  
      if (rsaSignContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.rsaGenerateKeyPairAsync()  
          rsaSignKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("RSA 公钥:", keyPair.publicKey)  
          console.log("RSA 私钥:", keyPair.privateKey)  

          // 签名  
          const signResult = await SM.rsaSignAsync(rsaSignContent.value, keyPair.privateKey)  
          rsaSignature.value = signResult.signature  
          console.log("RSA 签名成功:", signResult.signature)  
          uni.showToast({ title: '签名成功', icon: 'success' })  
      } catch (e) {  
          console.error("RSA 签名失败:", e)  
          uni.showToast({ title: '签名失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 验签 ====================  
  async function testRsaVerify() {  
      if (rsaSignature.value.length==0 || rsaSignKeyPair.value.publicKey.length==0) {  
          uni.showToast({ title: '请先签名', icon: 'none' })  
          return  
      }  

      try {  
          // 验签  
          const verifyResult = await SM.rsaVerifyAsync(rsaSignContent.value, rsaSignature.value, rsaSignKeyPair.value.publicKey)  
          rsaVerifyResult.value = verifyResult.valid  
          console.log("RSA 验签结果:", verifyResult.valid)  
          if (verifyResult.valid) {  
              uni.showToast({ title: '验签成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验签失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("RSA 验签异常:", e)  
          uni.showToast({ title: '验签异常', icon: 'error' })  
      }  
  }  

  // ==================== SM2 签名 ====================  
  async function testSm2Sign() {  
      if (sm2SignContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.sm2GenerateKeyPairAsync()  
          sm2KeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("SM2 公钥:", keyPair.publicKey)  
          console.log("SM2 私钥:", keyPair.privateKey)  

          // 签名  
          const signResult = await SM.sm2SignAsync(sm2SignContent.value, keyPair.privateKey)  
          sm2Signature.value = signResult.signature  
          console.log("SM2 签名成功:", signResult.signature)  
          uni.showToast({ title: '签名成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM2 生成密钥对失败:", e)  
          uni.showToast({ title: '生成密钥对失败', icon: 'error' })  
      }  
  }  

  // ==================== SM2 验签 ====================  
  async function testSm2Verify() {  
      if (sm2Signature.value.length==0 || sm2KeyPair.value.publicKey.length==0) {  
          uni.showToast({ title: '请先签名', icon: 'none' })  
          return  
      }  

      try {  
          // 验签  
          const verifyResult = await SM.sm2VerifyAsync(sm2SignContent.value, sm2Signature.value, sm2KeyPair.value.publicKey)  
          sm2VerifyResult.value = verifyResult.valid  
          console.log("SM2 验签结果:", verifyResult.valid)  
          if (verifyResult.valid) {  
              uni.showToast({ title: '验签成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验签失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM2 验签异常:", e)  
          uni.showToast({ title: '验签异常', icon: 'error' })  
      }  
  }  

  // ==================== SM2 加密 ====================  
  async function testSm2Encrypt() {  
      if (sm2EncryptContent.value.length==0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.sm2GenerateKeyPairAsync()  
          sm2EncryptKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("SM2 加密公钥:", keyPair.publicKey)  
          console.log("SM2 加密私钥:", keyPair.privateKey)  

          // 加密  
          const encryptResult = await SM.sm2EncryptAsync(sm2EncryptContent.value, keyPair.publicKey)  
          sm2Encrypted.value = encryptResult.encrypted  
          console.log("SM2 加密结果:", encryptResult.encrypted)  
          uni.showToast({ title: '加密成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM2 生成密钥对失败:", e)  
          uni.showToast({ title: '生成密钥对失败', icon: 'error' })  
      }  
  }  

  // ==================== SM2 解密 ====================  
  async function testSm2Decrypt() {  
      if (sm2Encrypted.value.length==0 || sm2EncryptKeyPair.value.privateKey.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.sm2DecryptAsync(sm2Encrypted.value, sm2EncryptKeyPair.value.privateKey)  
          sm2Decrypted.value = decryptResult.decrypted  
          console.log("SM2 解密结果:", decryptResult.decrypted)  

          if (decryptResult.decrypted == sm2EncryptContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM2 解密异常:", e)  
          uni.showToast({ title: '解密异常', icon: 'error' })  
      }  
  }  

  // ==================== SM3 哈希 ====================  
  async function testSm3Hash() {  
      if (sm3Content.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          const hashResult = await SM.sm3HashAsync(sm3Content.value)  
          sm3HashResult.value = hashResult.resultHash  

          console.log("SM3 哈希成功:", hashResult.resultHash)  
          uni.showToast({ title: '哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM3 哈希失败:", e)  
          uni.showToast({ title: '哈希失败', icon: 'error' })  
      }  
  }  

  // ==================== HMAC-SM3 哈希 ====================  
  async function testSm3HmacHash() {  
      if (sm3HmacContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      if (sm3HmacKey.value.length == 0) {  
          uni.showToast({ title: '请输入HMAC密钥', icon: 'none' })  
          return  
      }  

      try {  
          const hmacHashResult = await SM.sm3HmacHashAsync(sm3HmacContent.value, sm3HmacKey.value)  
          sm3HmacHashResult.value = hmacHashResult.resultHash  
          sm3HmacExpectedHash.value  =  hmacHashResult.resultHash  
          sm3HmacVerifyKey.value = sm3HmacKey.value  
          sm3HmacVerifyContent.value = sm3HmacContent.value  
          console.log("HMAC-SM3 哈希成功:", hmacHashResult.resultHash)  
          uni.showToast({ title: 'HMAC-SM3 哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("HMAC-SM3 哈希失败:", e)  
          uni.showToast({ title: 'HMAC-SM3 哈希失败', icon: 'error' })  
      }  
  }  

  // ==================== HMAC-SM3 验证(输入期望哈希值) ====================  
  async function testSm3HmacHashWithExpected() {  
      if (sm3HmacVerifyContent.value.length == 0) {  
          uni.showToast({ title: '请输入原始内容', icon: 'none' })  
          return  
      }  

      try {  
          // 计算实际 HMAC-SM3 哈希值  
          const res = await SM.sm3HmacVerifyAsync(sm3HmacVerifyContent.value, sm3HmacVerifyKey.value,sm3HmacHashResult.value)  
          if (res) {  
              uni.showToast({ title: 'HMAC-SM3 哈希值匹配', icon: 'success' })  
          } else {  
              uni.showToast({ title: 'HMAC-SM3 哈希值不匹配', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("HMAC-SM3 哈希验证异常:", e)  
          uni.showToast({ title: '验证异常', icon: 'error' })  
      }  
  }  

  // ==================== SM3 验证 ====================  
  async function testSm3Verify() {  
      if (sm3HashResult.value.length == 0 || sm3Content.value.length == 0) {  
          uni.showToast({ title: '请先计算哈希', icon: 'none' })  
          return  
      }  

      try {  
          // 重新计算哈希并验证  
          const res = await SM.sm3VerifyAsync(sm3VerifyContent.value,sm3HashResult.value)  
          sm3VerifyResult.value = res  
          console.log("SM3 验证结果:", sm3VerifyResult.value)  

          if (sm3VerifyResult.value!) {  
              uni.showToast({ title: '验证成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验证失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM3 验证异常:", e)  
          uni.showToast({ title: '验证异常', icon: 'error' })  
      }  
  }  

  // ==================== 选择文件并计算 SM3 哈希 ====================  
  async function processFileHash(filePath: string, fileSize?: number) {  
      // 计算文件 SM3 哈希  
      uni.showLoading({ title: '计算中...' })  

      try {  
          console.log("开始计算文件 SM3 哈希")  
          console.log("文件路径:", filePath)  

          const result = await SM.sm3FileHashAsync(filePath)  
          sm3FileHashResult.value = result.resultHash  
          console.log("文件 SM3 哈希成功:", result.resultHash)  
          uni.hideLoading()  
          uni.showToast({ title: '文件哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("文件 SM3 哈希失败:", e)  
          uni.hideLoading()  
          uni.showToast({ title: '文件哈希失败', icon: 'error' })  
      }  
  }  

  function chooseFileAndHash() {  
      uni.chooseFile({  
          count: 1,  
          success: (res) => {  
              const tempFiles = res.tempFiles  
              if (tempFiles != null && tempFiles.length > 0) {  
                  const file = tempFiles[0]  
                  const fileName = file.name  
                  const filePath = file.path  
                  const fileSize = file.size  
                  sm3FileName.value = fileName != null ? fileName : "未知文件"  

                  if (filePath != null) {  
                      processFileHash(filePath, fileSize)  
                  }  
              }  
          },  
          fail: (err) => {  
              console.error("选择文件失败:", err)  
              if (err.errMsg != null && !err.errMsg.includes('cancel')) {  
                  uni.showToast({ title: '选择文件失败', icon: 'error' })  
              }  
          }  
      })  
  }  

  // ==================== 选择相册图片并计算 SM3 哈希 ====================  
  function chooseImageAndHash() {  
      uni.chooseImage({  
          count: 1,  
          sizeType: ['original'],  // 只选择原图,不压缩  
          sourceType: ['album', 'camera'],  
          success: (res) => {  
              const tempFilePaths = res.tempFilePaths  
              if (tempFilePaths != null && tempFilePaths.length > 0) {  
                  const filePath = tempFilePaths[0]  
                  const fileName = "图片文件"  
                  sm3FileName.value = fileName  
                  console.log("选择的图片(原图):", filePath)  

                  if (filePath != null) {  
                      // 调用异步函数处理文件哈希  
                      processFileHash(filePath)  
                  }  
              }  
          },  
          fail: (err) => {  
              console.error("选择图片失败:", err)  
              if (err.errMsg != null && !err.errMsg.includes('cancel')) {  
                  uni.showToast({ title: '选择图片失败', icon: 'error' })  
              }  
          }  
      })  
  }  

  const goToDetail = (orderId: number, status: string) => {  
    uni.setStorageSync('fromTabBar', true)  
    uni.navigateTo({  
      url: `/pages/detail/detail?orderId=${orderId}&status=${status}`,  
      success: () => {  
        console.log(`跳转到order${orderId}的详情页`)  
      }  
    })  
  }  

  function btn (){  

  }  

</script>  

<style scoped>  
  .order-content {  
    flex: 1;  
    display: flex;  
    flex-direction: column;  
    background-color: #f5f5f5;  
  }  

  .order-header {  
    height: 48px;  
    background-color: #fff;  
    align-items: center;  
    justify-content: center;  
    border-bottom-width: 1px;  
    border-bottom-style: solid;  
    border-bottom-color: #eee;  
  }  

  .order-title {  
    font-size: 18px;  
    font-weight: bold;  
    color: #333;  
  }  

  .order-list {  
    flex: 1;  
    padding: 10px;  
  }  

  /* 区块样式 */  
  .section {  
    background-color: #fff;  
    border-radius: 8px;  
    padding: 15px;  
    margin-bottom: 15px;  
  }  

  .section-title {  
    font-size: 16px;  
    font-weight: bold;  
    color: #333;  
    margin-bottom: 15px;  
    padding-bottom: 10px;  
    border-bottom-width: 1px;  
    border-bottom-style: solid;  
    border-bottom-color: #eee;  
  }  

  .input-group {  
    margin-bottom: 15px;  
  }  

  .label {  
    font-size: 14px;  
    color: #666;  
    margin-bottom: 8px;  
  }  

  .input {  
    height: 40px;  
    border-width: 1px;  
    border-style: solid;  
    border-color: #ddd;  
    border-radius: 4px;  
    padding: 0 10px;  
    font-size: 14px;  
    background-color: #fafafa;  
  }  

  /* 按钮样式 */  
  .btn {  
    height: 44px;  
    border-radius: 4px;  
    font-size: 15px;  
    margin-bottom: 10px;  
  }  

  .btn-primary {  
    background-color: #007aff;  
    color: #fff;  
  }  

  .btn-secondary {  
    background-color: #34c759;  
    color: #fff;  
  }  

  .btn:disabled {  
    opacity: 0.5;  
  }  

  /* 结果展示框 */  
  .result-box {  
    margin-top: 10px;  
    padding: 10px;  
    background-color: #f9f9f9;  
    border-radius: 4px;  
    border-left-width: 3px;  
    border-left-style: solid;  
    border-left-color: #007aff;  
  }  

  .result-label {  
    font-size: 12px;  
    color: #999;  
    margin-bottom: 5px;  
  }  

  .result-value {  
    font-size: 13px;  
    color: #333;  
    line-height: 1.5;  
  }  

  .result-value.small {  
    font-size: 11px;  
  }  

  .result-value.success {  
    color: #34c759;  
    font-weight: bold;  
  }  

  .result-value.error {  
    color: #ff3b30;  
    font-weight: bold;  
  }  

  .divider {  
    height: 1px;  
    background-color: #eee;  
    margin: 15px 0;  
  }  

  .sub-title {  
    font-size: 14px;  
    font-weight: bold;  
    color: #666;  
    margin-bottom: 10px;  
  }  
</style>

服务端(java工具类)

package top.sunrains;  

import org.bouncycastle.crypto.digests.SM3Digest;  
import org.bouncycastle.crypto.engines.SM2Engine;  
import org.bouncycastle.crypto.params.ECDomainParameters;  
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;  
import org.bouncycastle.crypto.params.ECPublicKeyParameters;  
import org.bouncycastle.crypto.params.ParametersWithRandom;  
import org.bouncycastle.crypto.signers.SM2Signer;  
import org.bouncycastle.jce.ECNamedCurveTable;  
import org.bouncycastle.jce.spec.ECParameterSpec;  
import org.bouncycastle.util.encoders.Hex;  

import java.math.BigInteger;  
import java.security.SecureRandom;  

/**  
 *         <dependency>  
 *             <groupId>org.bouncycastle</groupId>  
 *             <artifactId>bcprov-jdk15to18</artifactId>  
 *             <version>1.69</version>  
 *         </dependency>  
 */  
public class SM2Util {  

    private static final ECParameterSpec CURVE = ECNamedCurveTable.getParameterSpec("sm2p256v1");  
    private static final ECDomainParameters DOMAIN = new ECDomainParameters(  
            CURVE.getCurve(),  
            CURVE.getG(),  
            CURVE.getN(),  
            CURVE.getH()  
    );  

    /**  
     * 生成SM2密钥对  
     *  
     * @return Map包含publicKey和privateKey,均为小写十六进制字符串  
     */  
    public static java.util.Map<String, String> generateKeyPair() {  
        try {  
            BigInteger d = new BigInteger(256, new SecureRandom()).mod(DOMAIN.getN());  
            org.bouncycastle.math.ec.ECPoint q = DOMAIN.getG().multiply(d).normalize();  

            String x = String.format("4x", q.getAffineXCoord().toBigInteger());  
            String y = String.format("4x", q.getAffineYCoord().toBigInteger());  
            String publicKey = "04" + x + y;  
            String privateKey = String.format("4x", d);  
            java.util.Map<String, String> keyMap = new java.util.HashMap<>();  
            keyMap.put("publicKey", publicKey.toLowerCase());  
            keyMap.put("privateKey", privateKey.toLowerCase());  
            return keyMap;  
        } catch (Exception e) {  
            System.err.println("密钥生成失败: " + e.getMessage());  
            e.printStackTrace();  
            java.util.Map<String, String> keyMap = new java.util.HashMap<>();  
            keyMap.put("publicKey", "");  
            keyMap.put("privateKey", "");  
            return keyMap;  
        }  
    }  

    /**  
     * SM2签名  
     *  
     * @param data 待签名的原始数据  
     * @param privateKeyHex 私钥(64字符十六进制)  
     * @return 签名结果(DER格式,小写十六进制)  
     */  
    public static String sign(String data, String privateKeyHex) {  
        try {  
            byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            BigInteger d = new BigInteger(privateKeyHex, 16);  
            ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);  

            SM2Signer signer = new SM2Signer(new SM3Digest());  
            signer.init(true, priKey);  
            signer.update(msgBytes, 0, msgBytes.length);  
            byte[] sig = signer.generateSignature();  

            return Hex.toHexString(sig).toLowerCase();  
        } catch (Exception e) {  
            System.err.println("签名失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * SM2验签  
     *  
     * @param data 原始数据  
     * @param signatureHex 签名(DER格式,十六进制)  
     * @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)  
     * @return 验签是否成功  
     */  
    public static boolean verify(String data, String signatureHex, String publicKeyHex) {  
        try {  
            byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;  
            BigInteger x = new BigInteger(xy.substring(0, 64), 16);  
            BigInteger y = new BigInteger(xy.substring(64, 128), 16);  

            org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();  
            ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);  

            SM2Signer signer = new SM2Signer(new SM3Digest());  
            signer.init(false, pubKey);  
            signer.update(msgBytes, 0, msgBytes.length);  

            byte[] sigBytes = Hex.decode(signatureHex);  
            return signer.verifySignature(sigBytes);  
        } catch (Exception e) {  
            System.err.println("验签失败: " + e.getMessage());  
            e.printStackTrace();  
            return false;  
        }  
    }  

    /**  
     * SM2加密  
     *  
     * @param data 待加密的原始数据(UTF-8字符串)  
     * @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)  
     * @return 密文(HEX字符串,小写)  
     */  
    public static String encrypt(String data, String publicKeyHex) {  
        try {  
            byte[] dataBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;  
            BigInteger x = new BigInteger(xy.substring(0, 64), 16);  
            BigInteger y = new BigInteger(xy.substring(64, 128), 16);  

            org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();  
            ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);  

            SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);  
            engine.init(true, new ParametersWithRandom(pubKey, new SecureRandom()));  

            byte[] encrypted = engine.processBlock(dataBytes, 0, dataBytes.length);  
            return Hex.toHexString(encrypted).toLowerCase();  
        } catch (Exception e) {  
            System.err.println("加密失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * SM2解密  
     *  
     * @param encryptedHex 密文(HEX字符串)  
     * @param privateKeyHex 私钥(64字符十六进制)  
     * @return 解密后的原始数据(UTF-8字符串)  
     */  
    public static String decrypt(String encryptedHex, String privateKeyHex) {  
        try {  
            byte[] encryptedBytes = Hex.decode(encryptedHex);  

            BigInteger d = new BigInteger(privateKeyHex, 16);  
            ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);  

            SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);  
            engine.init(false, priKey);  

            byte[] decrypted = engine.processBlock(encryptedBytes, 0, encryptedBytes.length);  
            return new String(decrypted, java.nio.charset.StandardCharsets.UTF_8);  
        } catch (Exception e) {  
            System.err.println("解密失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) {  
        System.out.println("=== SM2 工具类测试 ===\n");  

        java.util.Map<String, String> keyPair = generateKeyPair();  
        String publicKey = keyPair.get("publicKey");  
        String privateKey = keyPair.get("privateKey");  

        System.out.println("公钥: " + publicKey);  
        System.out.println("私钥: " + privateKey);  
        System.out.println();  

        String testData = "123";  
        System.out.println("原始数据: " + testData);  

        String signature = sign(testData, privateKey);  
        System.out.println("签名: " + signature);  
        System.out.println();  

        boolean isValid = verify(testData, signature, publicKey);  
        System.out.println("验签结果: " + isValid);  
        System.out.println();  

        if (!isValid) {  
            System.err.println("验签失败!");  
        } else {  
            System.out.println("验签成功!");  
        }  

        System.out.println("\n=== SM2 加密解密测试 ===\n");  

        // 加密解密测试  
        String originalText = "Hello SM2";  
        System.out.println("原始数据: " + originalText);  

        String encrypted = encrypt(originalText, publicKey);  
        System.out.println("原始数据: " + originalText);  

        String decrypted = decrypt(encrypted, privateKey);  
        System.out.println("解密结果: " + decrypted);  
        System.out.println();  

        if (originalText.equals(decrypted)) {  
            System.out.println("加密解密成功!");  
        } else {  
            System.err.println("加密解密失败!");  
        }  
    }  
}  

package top.sunrains;  

import org.bouncycastle.jce.provider.BouncyCastleProvider;  

import javax.crypto.Cipher;  
import javax.crypto.spec.IvParameterSpec;  
import javax.crypto.spec.SecretKeySpec;  
import java.security.SecureRandom;  
import java.security.Security;  

/**  
 *         <dependency>  
 *             <groupId>org.bouncycastle</groupId>  
 *             <artifactId>bcprov-jdk15to18</artifactId>  
 *             <version>1.69</version>  
 *         </dependency>  
 *  
 * SM4国密算法工具类  
 * 使用Bouncy Castle原生API  
 * 配置信息:  
 * - 算法:SM4  
 * - 模式:CBC(密码分组链接)  
 * - 填充:PKCS5Padding  
 * - 密钥长度:128位(16字节)  
 * - IV长度:128位(16字节)  
 */  
public class Sm4Util {  

    static {  
        try {  
            Security.addProvider(new BouncyCastleProvider());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

    public static void main(String[] args) throws Exception {  
//        String key = generateKeyHex();  
//        String iv = generateIvHex();  
        //String s = encryptHex("123", key, iv);  
        String key = "7f9a942a1aa85c331d3697a5369a37fe";  
        String iv = "cbc07750d33d6a3ef6d39fc7e7c4a876";  

        String s = "09187a80e9f52e7e962a3465ed70669e";  
        System.out.println(s);  
        String decryptedHex = decryptHex(s, key, iv);  
        System.out.println(decryptedHex);  
    }  

    /**  
     * 生成SM4密钥(16字节,128位)  
     * @return 十六进制编码的密钥字符串(32个字符)  
     */  
    public static String generateKeyHex() {  
        byte[] keyBytes = new byte[16];  
        new SecureRandom().nextBytes(keyBytes);  
        return byteArrayToHex(keyBytes);  
    }  

    /**  
     * 生成IV向量(16字节,128位)  
     * @return 十六进制编码的IV字符串(32个字符)  
     */  
    public static String generateIvHex() {  
        byte[] ivBytes = new byte[16];  
        new SecureRandom().nextBytes(ivBytes);  
        return byteArrayToHex(ivBytes);  
    }  

    /**  
     * SM4-CBC加密(字符串到HEX)  
     * 标准加密方式  
     *  
     * @param plaintext 明文字符串(UTF-8)  
     * @param keyHex 十六进制密钥字符串  
     * @param ivHex 十六进制IV字符串  
     * @return 加密后的HEX字符串  
     * @throws Exception 加密异常  
     */  
    public static String encryptHex(String plaintext, String keyHex, String ivHex) throws Exception {  
        byte[] key = decodeKeyHex(keyHex);  
        byte[] iv = decodeIvHex(ivHex);  
        byte[] data = plaintext.getBytes("UTF-8");  

        byte[] encrypted = encrypt(data, key, iv);  
        return byteArrayToHex(encrypted);  
    }  

    /**  
     * SM4-CBC加密  
     *  
     * @param data 原始数据字节数组  
     * @param key 密钥字节数组(16字节)  
     * @param iv IV字节数组(16字节)  
     * @return 加密后的字节数组  
     * @throws Exception 加密异常  
     */  
    public static byte[] encrypt(byte[] data, byte[] key, byte[] iv) throws Exception {  
        Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");  
        cipher.init(Cipher.ENCRYPT_MODE,  
                new SecretKeySpec(key, "SM4"),  
                new IvParameterSpec(iv));  
        return cipher.doFinal(data);  
    }  

    /**  
     * byte[] 转 Hex  
     *  
     * @param bytes 字节数组  
     * @return HEX字符串(小写)  
     */  
    public static String byteArrayToHex(byte[] bytes) {  
        if (bytes == null || bytes.length == 0) {  
            return "";  
        }  
        StringBuilder sb = new StringBuilder();  
        for (byte b : bytes) {  
            String hex = Integer.toHexString(b & 0xFF);  
            if (hex.length() == 1) {  
                sb.append('0');  
            }  
            sb.append(hex);  
        }  
        return sb.toString().toLowerCase();  
    }  

    public static String decryptHex(String encryptedHex, String keyHex, String ivHex) throws Exception {  
        byte[] key = decodeKeyHex(keyHex);  
        byte[] iv = decodeIvHex(ivHex);  
        byte[] encryptedData = hexToByteArray(encryptedHex);  

        byte[] decrypted = decrypt(encryptedData, key, iv);  
        return new String(decrypted, "UTF-8");  
    }  

    /**  
     * SM4-CBC解密  
     *  
     * @param encryptedData 加密数据字节数组  
     * @param key 密钥字节数组(16字节)  
     * @param iv IV字节数组(16字节)  
     * @return 解密后的原始字节数组  
     * @throws Exception 解密异常  
     */  
    public static byte[] decrypt(byte[] encryptedData, byte[] key, byte[] iv) throws Exception {  
        Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");  
        cipher.init(Cipher.DECRYPT_MODE,  
                new SecretKeySpec(key, "SM4"),  
                new IvParameterSpec(iv));  
        return cipher.doFinal(encryptedData);  
    }  

    /**  
     * 从十六进制字符串还原密钥字节数组  
     * @param keyHex 十六进制编码的密钥字符串  
     * @return 密钥字节数组(16字节)  
     */  
    public static byte[] decodeKeyHex(String keyHex) {  
        return hexToByteArray(keyHex);  
    }  

    /**  
     * 从十六进制字符串还原IV字节数组  
     * @param ivHex 十六进制编码的IV字符串  
     * @return IV字节数组(16字节)  
     */  
    public static byte[] decodeIvHex(String ivHex) {  
        return hexToByteArray(ivHex);  
    }  

    /**  
     * HEX 转 byte[]  
     *  
     * @param hex HEX字符串  
     * @return 字节数组  
     */  
    public static byte[] hexToByteArray(String hex) {  
        if (hex == null || hex.length() == 0) {  
            return new byte[0];  
        }  

        int len = hex.length() / 2;  
        byte[] bytes = new byte[len];  
        for (int i = 0; i < len; i++) {  
            int high = Character.digit(hex.charAt(i * 2), 16);  
            int low = Character.digit(hex.charAt(i * 2 + 1), 16);  
            bytes[i] = (byte) ((high << 4) | low);  
        }  
        return bytes;  
    }  

    public static String stringToHex(String str) {  
        if (str == null || str.isEmpty()) {  
            return "";  
        }  
        byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);  
        return byteArrayToHex(bytes);  
    }  
}  

package top.sunrains;  

import javax.crypto.Cipher;  
import javax.crypto.spec.IvParameterSpec;  
import javax.crypto.spec.SecretKeySpec;  
import java.security.SecureRandom;  
import java.util.Base64;  
import java.util.HashMap;  
import java.util.Map;  

/**  
 * AES对称加密算法工具类  
 * 使用Java原生Crypto API  
 * 配置信息:  
 * - 算法:AES-256  
 * - 模式:CBC(密码分组链接)  
 * - 填充:PKCS5Padding  
 * - 密钥长度:256位(32字节)  
 * - IV长度:128位(16字节)  
 */  
public class AesUtil {  

    /**  
     * 生成AES密钥和IV  
     * @return Map包含key (32字符) 和 iv (16字符)  
     */  
    public static Map<String, String> generateKey() {  
        try {  
            // 生成32字节的随机字符串作为密钥  
            String key = generateRandomString(32);  

            // 生成16字节的随机字符串作为IV  
            String iv = generateRandomString(16);  

            System.out.println("AES Key: " + key + " (长度: " + key.length() + ")");  
            System.out.println("AES IV: " + iv + " (长度: " + iv.length() + ")");  

            Map<String, String> result = new HashMap<>();  
            result.put("key", key);  
            result.put("iv", iv);  

            return result;  
        } catch (Exception e) {  
            System.err.println("AES 密钥生成失败: " + e.getMessage());  
            e.printStackTrace();  
            return new HashMap<>();  
        }  
    }  

    /**  
     * 生成指定长度的随机字符串  
     */  
    private static String generateRandomString(int length) {  
        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";  
        SecureRandom random = new SecureRandom();  
        StringBuilder result = new StringBuilder(length);  
        for (int i = 0; i < length; i++) {  
            result.append(chars.charAt(random.nextInt(chars.length())));  
        }  
        return result.toString();  
    }  

    /**  
     * AES-CBC加密  
     *  
     * @param plaintext 明文字符串(UTF-8)  
     * @param keyStr 密钥字符串(32字符)  
     * @param ivStr IV字符串(16字符)  
     * @return Base64编码的密文  
     * @throws Exception 加密异常  
     */  
    public static String encrypt(String plaintext, String keyStr, String ivStr) throws Exception {  
        // 将字符串Key和IV转换为字节数组(UTF-8编码)  
        byte[] keyBytes = keyStr.getBytes("UTF-8");  
        byte[] ivBytes = ivStr.getBytes("UTF-8");  

        // 创建密钥和IV  
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");  
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);  

        // 创建Cipher对象(AES/CBC/PKCS5Padding)  
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);  

        // 执行加密  
        byte[] encrypted = cipher.doFinal(plaintext.getBytes("UTF-8"));  

        // 返回Base64编码的密文  
        return Base64.getEncoder().encodeToString(encrypted);  
    }  

    /**  
     * AES-CBC解密  
     *  
     * @param encryptedBase64 Base64编码的密文  
     * @param keyStr 密钥字符串(32字符)  
     * @param ivStr IV字符串(16字符)  
     * @return 解密后的明文字符串  
     * @throws Exception 解密异常  
     */  
    public static String decrypt(String encryptedBase64, String keyStr, String ivStr) throws Exception {  
        // 将字符串Key和IV转换为字节数组(UTF-8编码)  
        byte[] keyBytes = keyStr.getBytes("UTF-8");  
        byte[] ivBytes = ivStr.getBytes("UTF-8");  

        // 创建密钥和IV  
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");  
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);  

        // 创建Cipher对象(AES/CBC/PKCS5Padding)  
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);  

        // 将Base64密文解码为字节  
        byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);  

        // 执行解密  
        byte[] decrypted = cipher.doFinal(encryptedBytes);  

        // 返回明文字符串  
        return new String(decrypted, "UTF-8");  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) throws Exception {  
        System.out.println("=== AES 工具类测试 ===\n");  

        // 生成密钥  
        Map<String, String> keyMap = generateKey();  
        String key = keyMap.get("key");  
        String iv = keyMap.get("iv");  

        System.out.println("密钥: " + key);  
        System.out.println("IV: " + iv);  
        System.out.println();  

        // 加密测试  
        String originalText = "Hello AES";  
        System.out.println("原始数据: " + originalText);  

        String encrypted = encrypt(originalText, key, iv);  
        System.out.println("加密结果: " + encrypted);  
        System.out.println();  

        // 解密测试  
        String decrypted = decrypt(encrypted, key, iv);  
        System.out.println("解密结果: " + decrypted);  
        System.out.println();  

        if (originalText.equals(decrypted)) {  
            System.out.println("✅ 加密解密成功!");  
        } else {  
            System.err.println("❌ 加密解密失败!");  
        }  
    }  
}  

package top.sunrains;  

import cn.hutool.core.text.CharSequenceUtil;  
import org.bouncycastle.crypto.digests.SM3Digest;  
import org.bouncycastle.crypto.macs.HMac;  
import org.bouncycastle.crypto.params.KeyParameter;  
import org.bouncycastle.jce.provider.BouncyCastleProvider;  

import java.io.File;  
import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
import java.security.Security;  
/**  
 * SM3国密哈希算法工具类  
 * 使用Bouncy Castle原生API  
 * 功能:  
 * - 字符串哈希计算  
 * - 字节数组哈希计算  
 * - 文件哈希计算  
 * - HMAC-SM3计算  
 */  
public class SM3Util {  

    static {  
        try {  
            Security.addProvider(new BouncyCastleProvider());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

    private static final int BUFFER_SIZE = 8192;  

    /**  
     * 计算字符串的SM3哈希值  
     *  
     * @param data 待哈希的字符串  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     */  
    public static String hash(String data) {  
        if (data == null) {  
            throw new IllegalArgumentException("Data cannot be null");  
        }  
        return hash(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));  
    }  

    /**  
     * 验证字符串的SM3哈希值  
     *  
     * @param data 原始数据  
     * @param expectedHash 期望的哈希值  
     * @return 是否匹配  
     */  
    public static boolean verify(String data, String expectedHash) {  
        String actualHash = hash(data);  
        return actualHash.equalsIgnoreCase(expectedHash);  
    }  

    /**  
     * 计算字节数组的SM3哈希值  
     *  
     * @param data 待哈希的字节数组  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     */  
    public static String hash(byte[] data) {  
        SM3Digest digest = new SM3Digest();  
        digest.update(data, 0, data.length);  
        byte[] result = new byte[digest.getDigestSize()];  
        digest.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * 计算文件的SM3哈希值(支持大文件)  
     *  
     * @param filePath 文件路径  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     * @throws IOException 文件读取异常  
     */  
    public static String hashFile(String filePath) throws IOException {  
        return hashFile(new File(filePath));  
    }  

    /**  
     * 计算文件的SM3哈希值(支持大文件)  
     *  
     * @param file 文件对象  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     * @throws IOException 文件读取异常  
     */  
    public static String hashFile(File file) throws IOException {  
        if (!file.exists()) {  
            throw new IOException("File not found: " + file.getAbsolutePath());  
        }  

        SM3Digest digest = new SM3Digest();  
        byte[] buffer = new byte[BUFFER_SIZE];  

        try (InputStream is = new FileInputStream(file)) {  
            int bytesRead;  
            while ((bytesRead = is.read(buffer)) != -1) {  
                digest.update(buffer, 0, bytesRead);  
            }  
        }  

        byte[] result = new byte[digest.getDigestSize()];  
        digest.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * 计算HMAC-SM3(基于密钥的哈希消息认证码)  
     *  
     * @param data 待计算的数据  
     * @param hexKey 密钥  
     * @return HMAC-SM3结果(小写十六进制字符串,64个字符)  
     */  
    public static String hmac(String data, String hexKey) {  
        if (CharSequenceUtil.isBlank(hexKey)) {  
            throw new IllegalArgumentException("密钥不能为空");  
        }  
        return hmac(data.getBytes(java.nio.charset.StandardCharsets.UTF_8),  
                hexToByteArray(hexKey));  
    }  

    /**  
     * 计算HMAC-SM3(基于密钥的哈希消息认证码)  
     *  
     * @param data 待计算的字节数组  
     * @param hexKey hex密钥字节数组  
     * @return HMAC-SM3结果(小写十六进制字符串,64个字符)  
     */  
    public static String hmac(byte[] data, byte[] hexKey) {  
        HMac hmac = new HMac(new SM3Digest());  
        hmac.init(new KeyParameter(hexKey));  
        hmac.update(data, 0, data.length);  
        byte[] result = new byte[hmac.getMacSize()];  
        hmac.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * byte[] 转 Hex  
     *  
     * @param bytes 字节数组  
     * @return HEX字符串(小写)  
     */  
    private static String byteArrayToHex(byte[] bytes) {  
        if (bytes == null || bytes.length == 0) {  
            return "";  
        }  
        StringBuilder sb = new StringBuilder();  
        for (byte b : bytes) {  
            String hex = Integer.toHexString(b & 0xFF);  
            if (hex.length() == 1) {  
                sb.append('0');  
            }  
            sb.append(hex);  
        }  
        return sb.toString().toLowerCase();  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) throws IOException {  
        String testData = "12";  
        System.out.println("SM3: " + hash(testData));  
        String key = "1";  
        key = stringToHex(key);  
        System.out.println("HMAC-SM3 key: " + key);  
        String hmacResult = hmac(testData, key);  
        System.out.println("HMAC-SM3: " + hmacResult);  
        System.out.println("HMAC长度: " + hmacResult.length() + " 字符");  
       System.out.println("文件哈希值: " +  hashFile("/Users/sun/1/t.jpg"));  
    }  

    public static String stringToHex(String str) {  
        if (str == null || str.isEmpty()) {  
            return "";  
        }  
        byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);  
        return byteArrayToHex(bytes);  
    }  

    /**  
     * HEX 转 byte[]  
     *  
     * @param hex HEX字符串  
     * @return 字节数组  
     */  
    public static byte[] hexToByteArray(String hex) {  
        if (hex == null || hex.length() == 0) {  
            return new byte[0];  
        }  

        int len = hex.length() / 2;  
        byte[] bytes = new byte[len];  
        for (int i = 0; i < len; i++) {  
            int high = Character.digit(hex.charAt(i * 2), 16);  
            int low = Character.digit(hex.charAt(i * 2 + 1), 16);  
            bytes[i] = (byte) ((high << 4) | low);  
        }  
        return bytes;  
    }  
}

各平台实现说明

Android 平台

SM2 实现:

  • 依赖库:BouncyCastle (org.bouncycastle:bcprov-jdk15on:1.70)

  • 特点

    • 使用 ECKeyPairGenerator 生成密钥对
    • 使用 SM2Signer 进行签名和验签(DER 格式)
    • 使用 SM2Engine 进行加密和解密(C1C3C2 格式)
    • 不依赖 JCA Provider,避免兼容性问题

SM4 实现:

  • 依赖库:BouncyCastle

  • 特点

    • 使用 SM4Engine 进行加密和解密
    • CBC 模式 + PKCS7 填充
    • 支持自定义密钥和 IV

iOS 平台

实现方式:GMObjC 原生库

SM2 特点:

  • 使用 GMSm2Utils 进行所有操作
  • 签名自动转换为 DER 格式以兼容其他平台
  • 加密输出 C1C3C2 格式(04 开头)
  • 支持跨平台互通(Android/HarmonyOS)

SM4 特点:

  • 使用 GMSm4Utils 进行加密解密
  • CBC 模式 + PKCS7 填充
  • 密钥和 IV 为 32 位十六进制字符串

HarmonyOS 平台

实现方式: @ohos.security.cryptoFramework

SM2 特点:

  • 使用 HarmonyOS 原生 cryptoFramework API
  • 签名使用 DER 格式
  • 加密使用 C1C3C2 格式
  • 完全符合国密标准

SM3 特点:

  • 使用 createMd (MessageDigest) 进行普通哈希
  • 使用手动实现 HMAC-SM3 (RFC 2104)
  • 支持流式文件哈希处理 (4096 字节缓冲区)
  • 不依赖 createMac,避免密钥参数问题

SM4 特点:

  • 使用 createSymKeyGenerator 生成密钥
  • 使用 createCipher 进行加密解密
  • CBC 模式 + PKCS7 填充
  • 支持 SM4_128 算法

AES 特点:

  • 使用 crypto.createSymKeyGenerator('AES256') 创建密钥生成器
  • 使用 crypto.createCipher('AES|CBC|PKCS7') 进行加解密
  • CBC 模式 + PKCS7 填充
  • 密钥和 IV 为随机字符串(UTF-8 编码)

H5 平台

实现方式:sm-crypto + Web Crypto API

特点:

  • SM2/SM3/SM4:通过 npm 包 sm-crypto 实现
  • AES:使用浏览器原生 Web Crypto API 实现
  • 所有功能均在浏览器环境中运行
  • SM2 签名使用 DER 格式(der: true
  • SM2 默认 userId 为 '1234567812345678'

依赖安装:

npm install --save sm-crypto

微信小程序平台

实现方式:miniprogram-sm-crypto + crypto-js + jsrsasign

特点:

  • 专为小程序优化的国密算法库
  • 完全支持 SM2、SM4 和 AES
  • SM2:密钥生成、签名、验签、加密、解密(使用 miniprogram-sm-crypto)
  • SM4:密钥生成、加密、解密(CBC 模式,使用 miniprogram-sm-crypto)
  • AES:密钥生成、加密、解密(CBC 模式,使用 crypto-js)
  • 需要通过微信开发者工具构建 npm

依赖安装:

npm install --save miniprogram-sm-crypto crypto-js jsrsasign

重要:微信小程序 npm 构建步骤

  1. 确保项目根目录已安装上述依赖
  2. 打开微信开发者工具,勾选「详情」→「本地设置」→「使用 npm 模块」
  3. 点击菜单栏「工具」→「构建 npm」,生成 miniprogram_npm 目录
  4. 重新编译运行小程序

注意事项

1. 密钥格式

SM2 密钥:

  • 公钥:以 04 开头的 130 位十六进制字符串(非压缩格式)
  • 私钥:64 位十六进制字符串

SM4 密钥:

  • 密钥 (key) :32 位十六进制字符串(128 位)
  • 初始向量 (IV) :32 位十六进制字符串(128 位)

AES 密钥:

  • 密钥 (key) :32 字符随机字符串(字母+数字,UTF-8 编码后为 256 位)
  • 初始向量 (IV) :16 字符随机字符串(字母+数字,UTF-8 编码后为 128 位)

2. 签名格式

  • 所有平台的签名输出均为 DER 编码的十六进制字符串
  • DER 格式确保跨平台兼容性

3. 哈希格式

SM3 哈希:

  • 输出长度:64 位十六进制字符串 (256 位)
  • 格式:小写十六进制
  • 示例:a1b2c3d4e5f6... (64 个字符)

HMAC-SM3 哈希:

  • 输出长度:64 位十六进制字符串 (256 位)
  • 密钥格式:十六进制字符串
  • 格式:小写十六进制

4. 加密格式

SM2 加密:

  • 输出格式:C1C3C2(以 04 开头)
  • 兼容 Android BouncyCastle、iOS GMObjC、HarmonyOS cryptoFramework

SM4 加密:

  • 模式:CBC
  • 填充:PKCS7
  • 输出:十六进制字符串

AES 加密:

  • 模式:CBC
  • 填充:PKCS5/PKCS7
  • 密钥:32 字符随机字符串(UTF-8 编码后为 256 位)
  • IV:16 字符随机字符串(UTF-8 编码后为 128 位)
  • 输出:Base64 编码字符串

RSA 加密:

  • 算法:RSA-OAEP with SHA-256 (对应 Java 的 RSA/ECB/OAEPWithSHA-256AndMGF1Padding)
  • 密钥长度:2048 位
  • 私钥格式:PKCS#8 (Base64 编码)
  • 公钥格式:X.509/SPKI (Base64 编码)
  • 输出:Base64 编码字符串
  • 跨平台兼容:Android/iOS/HarmonyOS/H5 完全互通

RSA 签名:

  • 算法:SHA256withRSA (PKCS#1 v1.5)
  • 私钥格式:PKCS#8 (Base64 编码)
  • 公钥格式:X.509/SPKI (Base64 编码)
  • 签名输出:Base64 编码字符串
  • 跨平台兼容:Android/iOS/HarmonyOS/H5/微信小程序 完全互通

5. 跨平台兼容性

✅ 完全兼容:

  • 各平台生成的 SM2 密钥对可以互相使用
  • Android 平台签名的数据可以在 iOS/Harmony/H5 平台验证
  • SM2 加密的数据可以在不同平台间解密
  • SM4 加密的数据可以在 Android/iOS/Harmony 平台间加解密
  • AES 加密的数据可以在所有平台间加解密(Android/iOS/HarmonyOS/H5/微信小程序)
  • SM3 哈希值在所有平台保持一致(相同输入产生相同输出)
  • HMAC-SM3 支持跨平台验证
  • RSA-OAEP-SHA256 加密的数据可以在 Android/iOS/HarmonyOS/H5 平台间加解密
  • SHA256withRSA 签名可以在所有平台间验签(包括微信小程序)

⚠️ 注意事项:

  • 确保使用相同的哈希选项(默认启用)
  • SM4/AES 加密和解密必须使用相同的密钥和 IV
  • 微信小程序需要先构建 npm 才能使用
  • SM3 文件哈希需要确保文件内容完全一致
  • AES 密钥为 32 字符随机字符串,IV 为 16 字符随机字符串
  • RSA 密钥长度为 2048 位,私钥为 PKCS#8 格式,公钥为 X.509/SPKI 格式
  • RSA 加密使用 OAEP with SHA-256 填充,与其他平台互通
  • RSA 签名使用 SHA256withRSA (PKCS#1 v1.5),所有平台兼容
  • 微信小程序端暂不支持 RSA-OAEP-SHA256 加密解密,仅支持签名验签
继续阅读 »

sunrains-smutil 插件使用指南

本插件提供SM2、SM3 、 SM4、AES、RSA 客户端(ios,安卓,鸿蒙,微信小程序,h5),服务端(java)SM2、SM3 、 SM4、AES、RSA 算法的跨平台实现,支持以下功能:
SM2 非对称加密算法
✅ SM2 密钥对生成
✅ SM2 数字签名
✅ SM2 签名验证
✅ SM2 数据加密
✅ SM2 数据解密
SM3 密码杂凑算法
✅ SM3 哈希计算(字符串)
✅ HMAC-SM3 密钥哈希
✅ 文件 SM3 哈希
✅ 哈希值验证
SM4 对称加密算法
✅ SM4 密钥生成(含 IV)
✅ SM4 数据加密(CBC 模式)
✅ SM4 数据解密(CBC 模式)
AES 对称加密算法
✅ AES 密钥和 IV 生成
✅ AES 数据加密(CBC 模式, PKCS7/PKCS5 填充)
✅ AES 数据解密(CBC 模式, PKCS7/PKCS5 填充)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5/微信小程序)
RSA 非对称加密算法
✅ RSA 密钥对生成(2048位)
✅ RSA-OAEP-SHA256 数据加密
✅ RSA-OAEP-SHA256 数据解密
✅ SHA256withRSA 数字签名(PKCS#1 v1.5)
✅ SHA256withRSA 签名验证(PKCS#1 v1.5)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5)
⚠️ 微信小程序端暂不支持RSA-OAEP-SHA256加密解密,仅支持签名验签

调用方式

本插件同时支持回调风格异步风格两种调用方式:

方式一:回调风格(传统方式)

import { sm2Sign } from "@/uni_modules/sunrains-util";  

sm2Sign({  
  data: "需要签名的数据",  
  privateKey: "你的私钥",  
  success: (res) => {  
    console.log("签名结果:", res.signature);  
  },  
  fail: (err) => {  
    console.error("签名失败:", err);  
  }  
});

方式二:异步风格(推荐,更简洁)

import { sm2SignAsync } from "@/uni_modules/sunrains-util";  

async function doSign() {  
  try {  
    const result = await sm2SignAsync("需要签名的数据", "你的私钥")  
    console.log("签名结果:", result.signature)  
  } catch (error) {  
    console.error("签名失败:", error)  
  }  
}

平台支持

平台 SM2 实现方式 SM4 实现方式 AES 实现方式 RSA 实现方式 状态
Android BouncyCastle BouncyCastle Java Crypto API Java Crypto API (RSA-OAEP-SHA256) ✅ 完全支持
iOS GMObjC GMObjC CommonCrypto Security Framework (RSA-OAEP-SHA256) ✅ 完全支持
HarmonyOS cryptoFramework cryptoFramework cryptoFramework cryptoFramework (RSA-OAEP-SHA256) ✅ 完全支持
H5 sm-crypto sm-crypto Web Crypto API Web Crypto API (RSA-OAEP-SHA256) ✅ 完全支持
微信小程序 miniprogram-sm-crypto miniprogram-sm-crypto crypto-js jsrsasign (仅签名验签) ⚠️ 部分支持

使用示例

SM2 相关操作

1. 生成 SM2 密钥对

同步方式:

import { sm2GenerateKeyPair } from "@/uni_modules/sunrains-util";  

const result = sm2GenerateKeyPair();  
console.log("公钥:", result.publicKey);  // 04开头的130位十六进制字符串  
console.log("私钥:", result.privateKey); // 64位十六进制字符串

异步方式:

import { sm2GenerateKeyPairAsync } from "@/uni_modules/sunrains-util";  

async function generateKeys() {  
  const result = await sm2GenerateKeyPairAsync()  
  console.log("公钥:", result.publicKey)  
  console.log("私钥:", result.privateKey)  
}

2. SM2 签名

回调风格:

import { sm2Sign } from "@/uni_modules/sunrains-util";  

sm2Sign({  
  data: "需要签名的数据",  
  privateKey: "你的私钥",  
  success: (res) => {  
    console.log("签名结果:", res.signature);  
  },  
  fail: (err) => {  
    console.error("签名失败:", err);  
  }  
});

异步风格(推荐):

import { sm2SignAsync } from "@/uni_modules/sunrains-util";  

async function doSign() {  
  try {  
    const result = await sm2SignAsync("需要签名的数据", "你的私钥")  
    console.log("签名结果:", result.signature)  
  } catch (error) {  
    console.error("签名失败:", error)  
  }  
}

3. SM2 验签

回调风格:

import { sm2Verify } from "@/uni_modules/sunrains-util";  

sm2Verify({  
  data: "原始数据",  
  signature: "签名值",  
  publicKey: "你的公钥",  
  success: (res) => {  
    console.log("验签结果:", res.valid); // true 或 false  
  },  
  fail: (err) => {  
    console.error("验签失败:", err);  
  }  
});

异步风格(推荐):

import { sm2VerifyAsync } from "@/uni_modules/sunrains-util";  

async function doVerify() {  
  try {  
    const result = await sm2VerifyAsync("原始数据", "签名值", "你的公钥")  
    console.log("验签结果:", result.valid) // true 或 false  
  } catch (error) {  
    console.error("验签失败:", error)  
  }  
}

完整实战示例

以下是一个完整的国密算法测试页面示例,及服务端(java)工具类,展示了所有功能的实际使用:

客户端 Vue 3 Composition API 示例

 <template>  
  <view class="order-content">  
    <view class="order-header">  
      <text class="order-title">国密算法测试</text>  
    </view>  

    <scroll-view class="order-list" scroll-y="true">  
      <view class="section">  
        <view class="section-title">SM4 对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="sm4Content" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testSm4Encrypt" class="btn btn-primary">SM4 加密</button>  

        <view class="result-box" v-if="sm4Key.key">  
          <text class="result-label">密钥 (key):</text>  
          <text selectable="true" class="result-value">{{ sm4Key.key }}</text>  
        </view>  

        <view class="result-box" v-if="sm4Key.iv">  
          <text class="result-label">初始向量 (iv):</text>  
          <text selectable="true" class="result-value">{{ sm4Key.iv }}</text>  
        </view>  

        <view class="result-box" v-if="sm4Encrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value">{{ sm4Encrypted }}</text>  
        </view>  
        <button @click="testSm4Decrypt" :disabled="sm4Encrypted.length==0" class="btn btn-secondary">SM4 解密</button>  
        <button @click="testSm4Decrypt" :disabled="sm4Encrypted === ''" :class="sm4Encrypted === '' ? 'btn btn-secondary disabled' : 'btn btn-secondary'" class="btn btn-secondary">SM4 解密</button>  

        <view class="result-box" v-if="sm4Decrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ sm4Decrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM2 数字签名</view>  

        <view class="input-group">  
          <text class="label">待签名内容:</text>  
          <input v-model="sm2SignContent" placeholder="请输入要签名的内容" class="input" />  
        </view>  

        <button @click="testSm2Sign" class="btn btn-primary">生成密钥对并签名</button>  

        <view class="result-box" v-if="sm2KeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2KeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2KeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2KeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2Signature">  
          <text class="result-label">签名结果:</text>  
          <text selectable="true" class="result-value small">{{ sm2Signature }}</text>  
        </view>  

        <button @click="testSm2Verify" :disabled="sm2Signature.length==0" class="btn btn-secondary">验签</button>  

        <view class="result-box" v-if="sm2VerifyResult !== null">  
          <text class="result-label">验签结果:</text>  
          <text class="result-value" :class="sm2VerifyResult ==true? 'success' : 'error'">  
            {{ sm2VerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}  
          </text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM2 非对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="sm2EncryptContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testSm2Encrypt" class="btn btn-primary">SM2 加密</button>  

        <view class="result-box" v-if="sm2EncryptKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2EncryptKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="sm2Encrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value small">{{ sm2Encrypted }}</text>  
        </view>  

        <button @click="testSm2Decrypt" :disabled="sm2Encrypted.length==0" class="btn btn-secondary">SM2 解密</button>  

        <view class="result-box" v-if="sm2Decrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ sm2Decrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">SM3 哈希算法</view>  

        <!-- 1. SM3 哈希 -->  
        <view class="input-group">  
          <text class="label">待哈希内容:</text>  
          <input v-model="sm3Content" placeholder="请输入要哈希的内容" class="input" />  
        </view>  

        <button @click="testSm3Hash" class="btn btn-primary">SM3 哈希</button>  

        <view class="result-box" v-if="sm3HashResult">  
          <text class="result-label">SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3HashResult }}</text>  
        </view>  

        <!-- 2. SM3 哈希验证 -->  
        <view class="divider"></view>  
        <view class="sub-title">SM3 哈希验证</view>  

        <view class="input-group">  
          <text class="label">验证 - 原始内容:</text>  
          <input v-model="sm3VerifyContent" placeholder="请输入原始内容" class="input" />  
        </view>  

        <button @click="testSm3Verify" :disabled="sm3VerifyContent.length == 0" class="btn btn-secondary">验证哈希值</button>  

        <view class="result-box" v-if="sm3HashMatchResult !== null">  
          <text class="result-label">哈希匹配结果:</text>  
          <text class="result-value" :class="sm3HashMatchResult == true ? 'success' : 'error'">  
            {{ sm3HashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}  
          </text>  
        </view>  

        <!-- 3. HMAC-SM3 哈希 -->  
        <view class="divider"></view>  
        <view class="sub-title">HMAC-SM3 哈希</view>  

        <view class="input-group">  
          <text class="label">HMAC-SM3 待哈希内容:</text>  
          <input v-model="sm3HmacContent" placeholder="请输入要哈希的内容" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">HMAC-SM3 密钥(HEX):</text>  
          <input v-model="sm3HmacKey" placeholder="请输入HMAC密钥(HEX格式)" class="input" />  
        </view>  

        <button @click="testSm3HmacHash" :disabled="sm3HmacContent.length == 0 || sm3HmacKey.length == 0" class="btn btn-primary">HMAC-SM3 哈希</button>  

        <view class="result-box" v-if="sm3HmacHashResult">  
          <text class="result-label">HMAC-SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3HmacHashResult }}</text>  
        </view>  

        <!-- 4. HMAC-SM3 哈希验证 -->  
        <view class="divider"></view>  
        <view class="sub-title">HMAC-SM3 哈希验证</view>  

        <view class="input-group">  
          <text class="label">验证 - 原始内容:</text>  
          <input v-model="sm3HmacVerifyContent" placeholder="请输入原始内容" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">验证 - HMAC 密钥:</text>  
          <input v-model="sm3HmacVerifyKey" placeholder="请输入HMAC密钥" class="input" />  
        </view>  

        <view class="input-group">  
          <text class="label">验证 - 期望哈希值:</text>  
          <input v-model="sm3HmacExpectedHash" placeholder="请输入期望的哈希值" class="input" />  
        </view>  

        <button @click="testSm3HmacHashWithExpected" :disabled="sm3HmacVerifyContent.length == 0 || sm3HmacVerifyKey.length == 0 || sm3HmacExpectedHash.length == 0" class="btn btn-secondary">验证 HMAC-SM3 哈希值</button>  

        <view class="result-box" v-if="sm3HmacHashMatchResult !== null">  
          <text class="result-label">HMAC-SM3 哈希匹配结果:</text>  
          <text class="result-value" :class="sm3HmacHashMatchResult == true ? 'success' : 'error'">  
            {{ sm3HmacHashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}  
          </text>  
        </view>  

        <!-- 5. 文件 SM3 哈希 -->  
        <view class="divider"></view>  
        <view class="sub-title">文件 SM3 哈希</view>  

        <button @click="chooseImageAndHash" class="btn btn-primary">选择图片并计算 SM3</button>  
        <button @click="chooseFileAndHash" class="btn btn-primary">选择文件并计算 SM3</button>  
        <view class="result-box" v-if="sm3FileName">  
          <text class="result-label">文件名:</text>  
          <text class="result-value">{{ sm3FileName }}</text>  
        </view>  

        <view class="result-box" v-if="sm3FileHashResult">  
          <text class="result-label">文件 SM3 哈希值:</text>  
          <text selectable="true" class="result-value small">{{ sm3FileHashResult }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">AES 对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="aesContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testAesEncrypt" class="btn btn-primary">AES 加密</button>  

        <view class="result-box" v-if="aesKey.key">  
          <text class="result-label">密钥 (key):</text>  
          <text selectable="true" class="result-value">{{ aesKey.key }}</text>  
        </view>  

        <view class="result-box" v-if="aesKey.iv">  
          <text class="result-label">初始向量 (iv):</text>  
          <text selectable="true" class="result-value">{{ aesKey.iv }}</text>  
        </view>  

        <view class="result-box" v-if="aesEncrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value">{{ aesEncrypted }}</text>  
        </view>  

        <button @click="testAesDecrypt" :disabled="aesEncrypted.length==0" class="btn btn-secondary">AES 解密</button>  

        <view class="result-box" v-if="aesDecrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ aesDecrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">RSA 非对称加密</view>  

        <view class="input-group">  
          <text class="label">待加密内容:</text>  
          <input v-model="rsaContent" placeholder="请输入要加密的内容" class="input" />  
        </view>  

        <button @click="testRsaEncrypt" class="btn btn-primary">RSA 加密</button>  

        <view class="result-box" v-if="rsaKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaEncrypted">  
          <text class="result-label">加密结果:</text>  
          <text selectable="true" class="result-value small">{{ rsaEncrypted }}</text>  
        </view>  

        <button @click="testRsaDecrypt" :disabled="rsaEncrypted.length==0" class="btn btn-secondary">RSA 解密</button>  

        <view class="result-box" v-if="rsaDecrypted">  
          <text class="result-label">解密结果:</text>  
          <text selectable="true" class="result-value success">{{ rsaDecrypted }}</text>  
        </view>  
      </view>  

      <view class="section">  
        <view class="section-title">RSA 数字签名</view>  

        <view class="input-group">  
          <text class="label">待签名内容:</text>  
          <input v-model="rsaSignContent" placeholder="请输入要签名的内容" class="input" />  
        </view>  

        <button @click="testRsaSign" class="btn btn-primary">生成密钥对并签名</button>  

        <view class="result-box" v-if="rsaSignKeyPair.publicKey">  
          <text class="result-label">公钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignKeyPair.publicKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaSignKeyPair.privateKey">  
          <text class="result-label">私钥:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignKeyPair.privateKey }}</text>  
        </view>  

        <view class="result-box" v-if="rsaSignature">  
          <text class="result-label">签名结果:</text>  
          <text selectable="true" class="result-value small">{{ rsaSignature }}</text>  
        </view>  

        <button @click="testRsaVerify" :disabled="rsaSignature.length==0" class="btn btn-secondary">验签</button>  

        <view class="result-box" v-if="rsaVerifyResult !== null">  
          <text class="result-label">验签结果:</text>  
          <text class="result-value" :class="rsaVerifyResult ==true? 'success' : 'error'">  
            {{ rsaVerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}  
          </text>  
        </view>  
      </view>  

      <view class="order-item" @click="goToDetail(1002, '已完成')">  
        <view class="order-info">  
          <text class="order-id">订单编号:1002</text>  
          <text class="order-status">状态:已完成</text>  
        </view>  
        <view class="order-btn">  
          <text class="go-detail">查看详情</text>  
        </view>  
      </view>  
    </scroll-view>  
  </view>  
</template>  

<script setup lang="uts">  
  import {IdcardOcrModel}from "@/uni_modules/sunrains-idcard-ocr/utssdk/interface.uts"  
  import {Response} from "@/uni_modules/sunrains-utils/index.uts"  

  import * as SM from "@/uni_modules/sunrains-smutil"  

  // ==================== SM4 相关数据 ====================  
  const sm4Content = ref("Hello World")  
  const sm4Key = ref<SM.Sm4Key>({ key: '', iv: '' })  
  const sm4Encrypted = ref("")  
  const sm4Decrypted = ref("")  

  // ==================== SM2 签名验签相关数据 ====================  
  const sm2SignContent = ref("123")  
  const sm2KeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })  
  const sm2Signature = ref("")  
  const sm2VerifyResult = ref<boolean | null>(null)  

  // ==================== SM2 加密解密相关数据 ====================  
  const sm2EncryptContent = ref("Hello SM2")  
  const sm2EncryptKeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })  
  const sm2Encrypted = ref("")  
  const sm2Decrypted = ref("")  

  // ==================== SM3 相关数据 ====================  
  const sm3Content = ref("Hello SM3")  
  const sm3HashResult = ref("")  
  const sm3VerifyContent = ref("")  
  const sm3HashMatchResult = ref<boolean | null>(null)  

  const sm3HmacContent = ref("Hello HMAC-SM3")  
  const sm3HmacKey = ref("0123456789abcdef0123456789abcdef")  // 默认 HMAC 密钥(32字节HEX)  
  const sm3HmacHashResult = ref("")  
  const sm3HmacVerifyContent = ref("")  
  const sm3HmacVerifyKey = ref("")  
  const sm3HmacExpectedHash = ref("")  
  const sm3HmacHashMatchResult = ref<boolean | null>(null)  
  const sm3VerifyResult = ref<boolean|null>(null)  

  const sm3FileName = ref("")  
  const sm3FileHashResult = ref("")  

  // ==================== AES 相关数据 ====================  
  const aesContent = ref("Hello AES")  
  const aesKey = ref<SM.AesKey>({ key: '', iv: '' })  
  const aesEncrypted = ref("")  
  const aesDecrypted = ref("")  

  // ==================== RSA 加密解密相关数据 ====================  
  const rsaContent = ref("Hello RSA")  
  const rsaKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })  
  const rsaEncrypted = ref("")  
  const rsaDecrypted = ref("")  

  // ==================== RSA 签名验签相关数据 ====================  
  const rsaSignContent = ref("需要签名的数据")  
  const rsaSignKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })  
  const rsaSignature = ref("")  
  const rsaVerifyResult = ref<boolean | null>(null)  

  // ==================== SM4 加密 ====================  
  async function testSm4Encrypt() {  
      if (sm4Content.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥  
          const key = await SM.sm4GenerateKeyAsync()  
          sm4Key.value = { key: key.key, iv: key.iv }  
          console.log("SM4 密钥:", key)  

          // 加密  
          const encryptResult = await SM.sm4EncryptAsync(sm4Content.value, key.key, key.iv)  
          sm4Encrypted.value = encryptResult.result  
          console.log("SM4 加密成功:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  

      } catch (e) {  
          console.error("SM4 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== SM4 解密 ====================  
  async function testSm4Decrypt() {  
      if (sm4Encrypted.value.length==0 || sm4Key.value.key.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.sm4DecryptAsync(sm4Encrypted.value, sm4Key.value.key, sm4Key.value.iv)  
          sm4Decrypted.value = decryptResult.result  
          console.log("SM4 解密成功:", decryptResult.result)  
          if (decryptResult.result == sm4Content.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM4 解密失败:", e)  
          uni.showToast({ title: '解密失败', icon: 'error' })  
      }  
  }  

  // ==================== AES 加密 ====================  
  async function testAesEncrypt() {  
      if (aesContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥和 IV  
          const key = await SM.aesGenerateKeyAsync()  
          aesKey.value = { key: key.key, iv: key.iv }  
          console.log("AES 密钥:", key)  

          // 加密  
          const encryptResult = await SM.aesEncryptAsync(aesContent.value, key.key, key.iv)  
          aesEncrypted.value = encryptResult.result  
          console.log("AES 加密成功:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  

      } catch (e) {  
          console.error("AES 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== AES 解密 ====================  
  async function testAesDecrypt() {  
      if (aesEncrypted.value.length==0 || aesKey.value.key.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.aesDecryptAsync(aesEncrypted.value, aesKey.value.key, aesKey.value.iv)  
          aesDecrypted.value = decryptResult.result  
          console.log("AES 解密成功:", decryptResult.result)  
          if (decryptResult.result == aesContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("AES 解密失败:", e)  
          uni.showToast({ title: '解密失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 加密 ====================  
  async function testRsaEncrypt() {  
      if (rsaContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.rsaGenerateKeyPairAsync()  
          rsaKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("RSA 公钥:", keyPair.publicKey)  
          console.log("RSA 私钥:", keyPair.privateKey)  

          // 加密  
          const encryptResult = await SM.rsaEncryptAsync(rsaContent.value, keyPair.publicKey)  
          rsaEncrypted.value = encryptResult.result  
          console.log("RSA 加密结果:", encryptResult.result)  
          uni.showToast({ title: '加密成功', icon: 'success' })  
      } catch (e) {  
          console.error("RSA 加密失败:", e)  
          uni.showToast({ title: '加密失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 解密 ====================  
  async function testRsaDecrypt() {  
      if (rsaEncrypted.value.length==0 || rsaKeyPair.value.privateKey.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.rsaDecryptAsync(rsaEncrypted.value, rsaKeyPair.value.privateKey)  
          rsaDecrypted.value = decryptResult.result  
          console.log("RSA 解密结果:", decryptResult.result)  

          if (decryptResult.result == rsaContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("RSA 解密异常:", e)  
          uni.showToast({ title: '解密异常', icon: 'error' })  
      }  
  }  

  // ==================== RSA 签名 ====================  
  async function testRsaSign() {  
      if (rsaSignContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.rsaGenerateKeyPairAsync()  
          rsaSignKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("RSA 公钥:", keyPair.publicKey)  
          console.log("RSA 私钥:", keyPair.privateKey)  

          // 签名  
          const signResult = await SM.rsaSignAsync(rsaSignContent.value, keyPair.privateKey)  
          rsaSignature.value = signResult.signature  
          console.log("RSA 签名成功:", signResult.signature)  
          uni.showToast({ title: '签名成功', icon: 'success' })  
      } catch (e) {  
          console.error("RSA 签名失败:", e)  
          uni.showToast({ title: '签名失败', icon: 'error' })  
      }  
  }  

  // ==================== RSA 验签 ====================  
  async function testRsaVerify() {  
      if (rsaSignature.value.length==0 || rsaSignKeyPair.value.publicKey.length==0) {  
          uni.showToast({ title: '请先签名', icon: 'none' })  
          return  
      }  

      try {  
          // 验签  
          const verifyResult = await SM.rsaVerifyAsync(rsaSignContent.value, rsaSignature.value, rsaSignKeyPair.value.publicKey)  
          rsaVerifyResult.value = verifyResult.valid  
          console.log("RSA 验签结果:", verifyResult.valid)  
          if (verifyResult.valid) {  
              uni.showToast({ title: '验签成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验签失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("RSA 验签异常:", e)  
          uni.showToast({ title: '验签异常', icon: 'error' })  
      }  
  }  

  // ==================== SM2 签名 ====================  
  async function testSm2Sign() {  
      if (sm2SignContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.sm2GenerateKeyPairAsync()  
          sm2KeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("SM2 公钥:", keyPair.publicKey)  
          console.log("SM2 私钥:", keyPair.privateKey)  

          // 签名  
          const signResult = await SM.sm2SignAsync(sm2SignContent.value, keyPair.privateKey)  
          sm2Signature.value = signResult.signature  
          console.log("SM2 签名成功:", signResult.signature)  
          uni.showToast({ title: '签名成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM2 生成密钥对失败:", e)  
          uni.showToast({ title: '生成密钥对失败', icon: 'error' })  
      }  
  }  

  // ==================== SM2 验签 ====================  
  async function testSm2Verify() {  
      if (sm2Signature.value.length==0 || sm2KeyPair.value.publicKey.length==0) {  
          uni.showToast({ title: '请先签名', icon: 'none' })  
          return  
      }  

      try {  
          // 验签  
          const verifyResult = await SM.sm2VerifyAsync(sm2SignContent.value, sm2Signature.value, sm2KeyPair.value.publicKey)  
          sm2VerifyResult.value = verifyResult.valid  
          console.log("SM2 验签结果:", verifyResult.valid)  
          if (verifyResult.valid) {  
              uni.showToast({ title: '验签成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验签失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM2 验签异常:", e)  
          uni.showToast({ title: '验签异常', icon: 'error' })  
      }  
  }  

  // ==================== SM2 加密 ====================  
  async function testSm2Encrypt() {  
      if (sm2EncryptContent.value.length==0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          // 生成密钥对  
          const keyPair = await SM.sm2GenerateKeyPairAsync()  
          sm2EncryptKeyPair.value = {   
              publicKey: keyPair.publicKey,   
              privateKey: keyPair.privateKey   
          }  
          console.log("SM2 加密公钥:", keyPair.publicKey)  
          console.log("SM2 加密私钥:", keyPair.privateKey)  

          // 加密  
          const encryptResult = await SM.sm2EncryptAsync(sm2EncryptContent.value, keyPair.publicKey)  
          sm2Encrypted.value = encryptResult.encrypted  
          console.log("SM2 加密结果:", encryptResult.encrypted)  
          uni.showToast({ title: '加密成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM2 生成密钥对失败:", e)  
          uni.showToast({ title: '生成密钥对失败', icon: 'error' })  
      }  
  }  

  // ==================== SM2 解密 ====================  
  async function testSm2Decrypt() {  
      if (sm2Encrypted.value.length==0 || sm2EncryptKeyPair.value.privateKey.length==0) {  
          uni.showToast({ title: '请先加密', icon: 'none' })  
          return  
      }  

      try {  
          const decryptResult = await SM.sm2DecryptAsync(sm2Encrypted.value, sm2EncryptKeyPair.value.privateKey)  
          sm2Decrypted.value = decryptResult.decrypted  
          console.log("SM2 解密结果:", decryptResult.decrypted)  

          if (decryptResult.decrypted == sm2EncryptContent.value) {  
              uni.showToast({ title: '解密成功且内容一致', icon: 'success' })  
          } else {  
              uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM2 解密异常:", e)  
          uni.showToast({ title: '解密异常', icon: 'error' })  
      }  
  }  

  // ==================== SM3 哈希 ====================  
  async function testSm3Hash() {  
      if (sm3Content.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      try {  
          const hashResult = await SM.sm3HashAsync(sm3Content.value)  
          sm3HashResult.value = hashResult.resultHash  

          console.log("SM3 哈希成功:", hashResult.resultHash)  
          uni.showToast({ title: '哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("SM3 哈希失败:", e)  
          uni.showToast({ title: '哈希失败', icon: 'error' })  
      }  
  }  

  // ==================== HMAC-SM3 哈希 ====================  
  async function testSm3HmacHash() {  
      if (sm3HmacContent.value.length == 0) {  
          uni.showToast({ title: '请输入内容', icon: 'none' })  
          return  
      }  

      if (sm3HmacKey.value.length == 0) {  
          uni.showToast({ title: '请输入HMAC密钥', icon: 'none' })  
          return  
      }  

      try {  
          const hmacHashResult = await SM.sm3HmacHashAsync(sm3HmacContent.value, sm3HmacKey.value)  
          sm3HmacHashResult.value = hmacHashResult.resultHash  
          sm3HmacExpectedHash.value  =  hmacHashResult.resultHash  
          sm3HmacVerifyKey.value = sm3HmacKey.value  
          sm3HmacVerifyContent.value = sm3HmacContent.value  
          console.log("HMAC-SM3 哈希成功:", hmacHashResult.resultHash)  
          uni.showToast({ title: 'HMAC-SM3 哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("HMAC-SM3 哈希失败:", e)  
          uni.showToast({ title: 'HMAC-SM3 哈希失败', icon: 'error' })  
      }  
  }  

  // ==================== HMAC-SM3 验证(输入期望哈希值) ====================  
  async function testSm3HmacHashWithExpected() {  
      if (sm3HmacVerifyContent.value.length == 0) {  
          uni.showToast({ title: '请输入原始内容', icon: 'none' })  
          return  
      }  

      try {  
          // 计算实际 HMAC-SM3 哈希值  
          const res = await SM.sm3HmacVerifyAsync(sm3HmacVerifyContent.value, sm3HmacVerifyKey.value,sm3HmacHashResult.value)  
          if (res) {  
              uni.showToast({ title: 'HMAC-SM3 哈希值匹配', icon: 'success' })  
          } else {  
              uni.showToast({ title: 'HMAC-SM3 哈希值不匹配', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("HMAC-SM3 哈希验证异常:", e)  
          uni.showToast({ title: '验证异常', icon: 'error' })  
      }  
  }  

  // ==================== SM3 验证 ====================  
  async function testSm3Verify() {  
      if (sm3HashResult.value.length == 0 || sm3Content.value.length == 0) {  
          uni.showToast({ title: '请先计算哈希', icon: 'none' })  
          return  
      }  

      try {  
          // 重新计算哈希并验证  
          const res = await SM.sm3VerifyAsync(sm3VerifyContent.value,sm3HashResult.value)  
          sm3VerifyResult.value = res  
          console.log("SM3 验证结果:", sm3VerifyResult.value)  

          if (sm3VerifyResult.value!) {  
              uni.showToast({ title: '验证成功', icon: 'success' })  
          } else {  
              uni.showToast({ title: '验证失败', icon: 'error' })  
          }  
      } catch (e) {  
          console.error("SM3 验证异常:", e)  
          uni.showToast({ title: '验证异常', icon: 'error' })  
      }  
  }  

  // ==================== 选择文件并计算 SM3 哈希 ====================  
  async function processFileHash(filePath: string, fileSize?: number) {  
      // 计算文件 SM3 哈希  
      uni.showLoading({ title: '计算中...' })  

      try {  
          console.log("开始计算文件 SM3 哈希")  
          console.log("文件路径:", filePath)  

          const result = await SM.sm3FileHashAsync(filePath)  
          sm3FileHashResult.value = result.resultHash  
          console.log("文件 SM3 哈希成功:", result.resultHash)  
          uni.hideLoading()  
          uni.showToast({ title: '文件哈希成功', icon: 'success' })  
      } catch (e) {  
          console.error("文件 SM3 哈希失败:", e)  
          uni.hideLoading()  
          uni.showToast({ title: '文件哈希失败', icon: 'error' })  
      }  
  }  

  function chooseFileAndHash() {  
      uni.chooseFile({  
          count: 1,  
          success: (res) => {  
              const tempFiles = res.tempFiles  
              if (tempFiles != null && tempFiles.length > 0) {  
                  const file = tempFiles[0]  
                  const fileName = file.name  
                  const filePath = file.path  
                  const fileSize = file.size  
                  sm3FileName.value = fileName != null ? fileName : "未知文件"  

                  if (filePath != null) {  
                      processFileHash(filePath, fileSize)  
                  }  
              }  
          },  
          fail: (err) => {  
              console.error("选择文件失败:", err)  
              if (err.errMsg != null && !err.errMsg.includes('cancel')) {  
                  uni.showToast({ title: '选择文件失败', icon: 'error' })  
              }  
          }  
      })  
  }  

  // ==================== 选择相册图片并计算 SM3 哈希 ====================  
  function chooseImageAndHash() {  
      uni.chooseImage({  
          count: 1,  
          sizeType: ['original'],  // 只选择原图,不压缩  
          sourceType: ['album', 'camera'],  
          success: (res) => {  
              const tempFilePaths = res.tempFilePaths  
              if (tempFilePaths != null && tempFilePaths.length > 0) {  
                  const filePath = tempFilePaths[0]  
                  const fileName = "图片文件"  
                  sm3FileName.value = fileName  
                  console.log("选择的图片(原图):", filePath)  

                  if (filePath != null) {  
                      // 调用异步函数处理文件哈希  
                      processFileHash(filePath)  
                  }  
              }  
          },  
          fail: (err) => {  
              console.error("选择图片失败:", err)  
              if (err.errMsg != null && !err.errMsg.includes('cancel')) {  
                  uni.showToast({ title: '选择图片失败', icon: 'error' })  
              }  
          }  
      })  
  }  

  const goToDetail = (orderId: number, status: string) => {  
    uni.setStorageSync('fromTabBar', true)  
    uni.navigateTo({  
      url: `/pages/detail/detail?orderId=${orderId}&status=${status}`,  
      success: () => {  
        console.log(`跳转到order${orderId}的详情页`)  
      }  
    })  
  }  

  function btn (){  

  }  

</script>  

<style scoped>  
  .order-content {  
    flex: 1;  
    display: flex;  
    flex-direction: column;  
    background-color: #f5f5f5;  
  }  

  .order-header {  
    height: 48px;  
    background-color: #fff;  
    align-items: center;  
    justify-content: center;  
    border-bottom-width: 1px;  
    border-bottom-style: solid;  
    border-bottom-color: #eee;  
  }  

  .order-title {  
    font-size: 18px;  
    font-weight: bold;  
    color: #333;  
  }  

  .order-list {  
    flex: 1;  
    padding: 10px;  
  }  

  /* 区块样式 */  
  .section {  
    background-color: #fff;  
    border-radius: 8px;  
    padding: 15px;  
    margin-bottom: 15px;  
  }  

  .section-title {  
    font-size: 16px;  
    font-weight: bold;  
    color: #333;  
    margin-bottom: 15px;  
    padding-bottom: 10px;  
    border-bottom-width: 1px;  
    border-bottom-style: solid;  
    border-bottom-color: #eee;  
  }  

  .input-group {  
    margin-bottom: 15px;  
  }  

  .label {  
    font-size: 14px;  
    color: #666;  
    margin-bottom: 8px;  
  }  

  .input {  
    height: 40px;  
    border-width: 1px;  
    border-style: solid;  
    border-color: #ddd;  
    border-radius: 4px;  
    padding: 0 10px;  
    font-size: 14px;  
    background-color: #fafafa;  
  }  

  /* 按钮样式 */  
  .btn {  
    height: 44px;  
    border-radius: 4px;  
    font-size: 15px;  
    margin-bottom: 10px;  
  }  

  .btn-primary {  
    background-color: #007aff;  
    color: #fff;  
  }  

  .btn-secondary {  
    background-color: #34c759;  
    color: #fff;  
  }  

  .btn:disabled {  
    opacity: 0.5;  
  }  

  /* 结果展示框 */  
  .result-box {  
    margin-top: 10px;  
    padding: 10px;  
    background-color: #f9f9f9;  
    border-radius: 4px;  
    border-left-width: 3px;  
    border-left-style: solid;  
    border-left-color: #007aff;  
  }  

  .result-label {  
    font-size: 12px;  
    color: #999;  
    margin-bottom: 5px;  
  }  

  .result-value {  
    font-size: 13px;  
    color: #333;  
    line-height: 1.5;  
  }  

  .result-value.small {  
    font-size: 11px;  
  }  

  .result-value.success {  
    color: #34c759;  
    font-weight: bold;  
  }  

  .result-value.error {  
    color: #ff3b30;  
    font-weight: bold;  
  }  

  .divider {  
    height: 1px;  
    background-color: #eee;  
    margin: 15px 0;  
  }  

  .sub-title {  
    font-size: 14px;  
    font-weight: bold;  
    color: #666;  
    margin-bottom: 10px;  
  }  
</style>

服务端(java工具类)

package top.sunrains;  

import org.bouncycastle.crypto.digests.SM3Digest;  
import org.bouncycastle.crypto.engines.SM2Engine;  
import org.bouncycastle.crypto.params.ECDomainParameters;  
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;  
import org.bouncycastle.crypto.params.ECPublicKeyParameters;  
import org.bouncycastle.crypto.params.ParametersWithRandom;  
import org.bouncycastle.crypto.signers.SM2Signer;  
import org.bouncycastle.jce.ECNamedCurveTable;  
import org.bouncycastle.jce.spec.ECParameterSpec;  
import org.bouncycastle.util.encoders.Hex;  

import java.math.BigInteger;  
import java.security.SecureRandom;  

/**  
 *         <dependency>  
 *             <groupId>org.bouncycastle</groupId>  
 *             <artifactId>bcprov-jdk15to18</artifactId>  
 *             <version>1.69</version>  
 *         </dependency>  
 */  
public class SM2Util {  

    private static final ECParameterSpec CURVE = ECNamedCurveTable.getParameterSpec("sm2p256v1");  
    private static final ECDomainParameters DOMAIN = new ECDomainParameters(  
            CURVE.getCurve(),  
            CURVE.getG(),  
            CURVE.getN(),  
            CURVE.getH()  
    );  

    /**  
     * 生成SM2密钥对  
     *  
     * @return Map包含publicKey和privateKey,均为小写十六进制字符串  
     */  
    public static java.util.Map<String, String> generateKeyPair() {  
        try {  
            BigInteger d = new BigInteger(256, new SecureRandom()).mod(DOMAIN.getN());  
            org.bouncycastle.math.ec.ECPoint q = DOMAIN.getG().multiply(d).normalize();  

            String x = String.format("4x", q.getAffineXCoord().toBigInteger());  
            String y = String.format("4x", q.getAffineYCoord().toBigInteger());  
            String publicKey = "04" + x + y;  
            String privateKey = String.format("4x", d);  
            java.util.Map<String, String> keyMap = new java.util.HashMap<>();  
            keyMap.put("publicKey", publicKey.toLowerCase());  
            keyMap.put("privateKey", privateKey.toLowerCase());  
            return keyMap;  
        } catch (Exception e) {  
            System.err.println("密钥生成失败: " + e.getMessage());  
            e.printStackTrace();  
            java.util.Map<String, String> keyMap = new java.util.HashMap<>();  
            keyMap.put("publicKey", "");  
            keyMap.put("privateKey", "");  
            return keyMap;  
        }  
    }  

    /**  
     * SM2签名  
     *  
     * @param data 待签名的原始数据  
     * @param privateKeyHex 私钥(64字符十六进制)  
     * @return 签名结果(DER格式,小写十六进制)  
     */  
    public static String sign(String data, String privateKeyHex) {  
        try {  
            byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            BigInteger d = new BigInteger(privateKeyHex, 16);  
            ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);  

            SM2Signer signer = new SM2Signer(new SM3Digest());  
            signer.init(true, priKey);  
            signer.update(msgBytes, 0, msgBytes.length);  
            byte[] sig = signer.generateSignature();  

            return Hex.toHexString(sig).toLowerCase();  
        } catch (Exception e) {  
            System.err.println("签名失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * SM2验签  
     *  
     * @param data 原始数据  
     * @param signatureHex 签名(DER格式,十六进制)  
     * @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)  
     * @return 验签是否成功  
     */  
    public static boolean verify(String data, String signatureHex, String publicKeyHex) {  
        try {  
            byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;  
            BigInteger x = new BigInteger(xy.substring(0, 64), 16);  
            BigInteger y = new BigInteger(xy.substring(64, 128), 16);  

            org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();  
            ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);  

            SM2Signer signer = new SM2Signer(new SM3Digest());  
            signer.init(false, pubKey);  
            signer.update(msgBytes, 0, msgBytes.length);  

            byte[] sigBytes = Hex.decode(signatureHex);  
            return signer.verifySignature(sigBytes);  
        } catch (Exception e) {  
            System.err.println("验签失败: " + e.getMessage());  
            e.printStackTrace();  
            return false;  
        }  
    }  

    /**  
     * SM2加密  
     *  
     * @param data 待加密的原始数据(UTF-8字符串)  
     * @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)  
     * @return 密文(HEX字符串,小写)  
     */  
    public static String encrypt(String data, String publicKeyHex) {  
        try {  
            byte[] dataBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);  

            String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;  
            BigInteger x = new BigInteger(xy.substring(0, 64), 16);  
            BigInteger y = new BigInteger(xy.substring(64, 128), 16);  

            org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();  
            ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);  

            SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);  
            engine.init(true, new ParametersWithRandom(pubKey, new SecureRandom()));  

            byte[] encrypted = engine.processBlock(dataBytes, 0, dataBytes.length);  
            return Hex.toHexString(encrypted).toLowerCase();  
        } catch (Exception e) {  
            System.err.println("加密失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * SM2解密  
     *  
     * @param encryptedHex 密文(HEX字符串)  
     * @param privateKeyHex 私钥(64字符十六进制)  
     * @return 解密后的原始数据(UTF-8字符串)  
     */  
    public static String decrypt(String encryptedHex, String privateKeyHex) {  
        try {  
            byte[] encryptedBytes = Hex.decode(encryptedHex);  

            BigInteger d = new BigInteger(privateKeyHex, 16);  
            ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);  

            SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);  
            engine.init(false, priKey);  

            byte[] decrypted = engine.processBlock(encryptedBytes, 0, encryptedBytes.length);  
            return new String(decrypted, java.nio.charset.StandardCharsets.UTF_8);  
        } catch (Exception e) {  
            System.err.println("解密失败: " + e.getMessage());  
            e.printStackTrace();  
            return "";  
        }  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) {  
        System.out.println("=== SM2 工具类测试 ===\n");  

        java.util.Map<String, String> keyPair = generateKeyPair();  
        String publicKey = keyPair.get("publicKey");  
        String privateKey = keyPair.get("privateKey");  

        System.out.println("公钥: " + publicKey);  
        System.out.println("私钥: " + privateKey);  
        System.out.println();  

        String testData = "123";  
        System.out.println("原始数据: " + testData);  

        String signature = sign(testData, privateKey);  
        System.out.println("签名: " + signature);  
        System.out.println();  

        boolean isValid = verify(testData, signature, publicKey);  
        System.out.println("验签结果: " + isValid);  
        System.out.println();  

        if (!isValid) {  
            System.err.println("验签失败!");  
        } else {  
            System.out.println("验签成功!");  
        }  

        System.out.println("\n=== SM2 加密解密测试 ===\n");  

        // 加密解密测试  
        String originalText = "Hello SM2";  
        System.out.println("原始数据: " + originalText);  

        String encrypted = encrypt(originalText, publicKey);  
        System.out.println("原始数据: " + originalText);  

        String decrypted = decrypt(encrypted, privateKey);  
        System.out.println("解密结果: " + decrypted);  
        System.out.println();  

        if (originalText.equals(decrypted)) {  
            System.out.println("加密解密成功!");  
        } else {  
            System.err.println("加密解密失败!");  
        }  
    }  
}  

package top.sunrains;  

import org.bouncycastle.jce.provider.BouncyCastleProvider;  

import javax.crypto.Cipher;  
import javax.crypto.spec.IvParameterSpec;  
import javax.crypto.spec.SecretKeySpec;  
import java.security.SecureRandom;  
import java.security.Security;  

/**  
 *         <dependency>  
 *             <groupId>org.bouncycastle</groupId>  
 *             <artifactId>bcprov-jdk15to18</artifactId>  
 *             <version>1.69</version>  
 *         </dependency>  
 *  
 * SM4国密算法工具类  
 * 使用Bouncy Castle原生API  
 * 配置信息:  
 * - 算法:SM4  
 * - 模式:CBC(密码分组链接)  
 * - 填充:PKCS5Padding  
 * - 密钥长度:128位(16字节)  
 * - IV长度:128位(16字节)  
 */  
public class Sm4Util {  

    static {  
        try {  
            Security.addProvider(new BouncyCastleProvider());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

    public static void main(String[] args) throws Exception {  
//        String key = generateKeyHex();  
//        String iv = generateIvHex();  
        //String s = encryptHex("123", key, iv);  
        String key = "7f9a942a1aa85c331d3697a5369a37fe";  
        String iv = "cbc07750d33d6a3ef6d39fc7e7c4a876";  

        String s = "09187a80e9f52e7e962a3465ed70669e";  
        System.out.println(s);  
        String decryptedHex = decryptHex(s, key, iv);  
        System.out.println(decryptedHex);  
    }  

    /**  
     * 生成SM4密钥(16字节,128位)  
     * @return 十六进制编码的密钥字符串(32个字符)  
     */  
    public static String generateKeyHex() {  
        byte[] keyBytes = new byte[16];  
        new SecureRandom().nextBytes(keyBytes);  
        return byteArrayToHex(keyBytes);  
    }  

    /**  
     * 生成IV向量(16字节,128位)  
     * @return 十六进制编码的IV字符串(32个字符)  
     */  
    public static String generateIvHex() {  
        byte[] ivBytes = new byte[16];  
        new SecureRandom().nextBytes(ivBytes);  
        return byteArrayToHex(ivBytes);  
    }  

    /**  
     * SM4-CBC加密(字符串到HEX)  
     * 标准加密方式  
     *  
     * @param plaintext 明文字符串(UTF-8)  
     * @param keyHex 十六进制密钥字符串  
     * @param ivHex 十六进制IV字符串  
     * @return 加密后的HEX字符串  
     * @throws Exception 加密异常  
     */  
    public static String encryptHex(String plaintext, String keyHex, String ivHex) throws Exception {  
        byte[] key = decodeKeyHex(keyHex);  
        byte[] iv = decodeIvHex(ivHex);  
        byte[] data = plaintext.getBytes("UTF-8");  

        byte[] encrypted = encrypt(data, key, iv);  
        return byteArrayToHex(encrypted);  
    }  

    /**  
     * SM4-CBC加密  
     *  
     * @param data 原始数据字节数组  
     * @param key 密钥字节数组(16字节)  
     * @param iv IV字节数组(16字节)  
     * @return 加密后的字节数组  
     * @throws Exception 加密异常  
     */  
    public static byte[] encrypt(byte[] data, byte[] key, byte[] iv) throws Exception {  
        Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");  
        cipher.init(Cipher.ENCRYPT_MODE,  
                new SecretKeySpec(key, "SM4"),  
                new IvParameterSpec(iv));  
        return cipher.doFinal(data);  
    }  

    /**  
     * byte[] 转 Hex  
     *  
     * @param bytes 字节数组  
     * @return HEX字符串(小写)  
     */  
    public static String byteArrayToHex(byte[] bytes) {  
        if (bytes == null || bytes.length == 0) {  
            return "";  
        }  
        StringBuilder sb = new StringBuilder();  
        for (byte b : bytes) {  
            String hex = Integer.toHexString(b & 0xFF);  
            if (hex.length() == 1) {  
                sb.append('0');  
            }  
            sb.append(hex);  
        }  
        return sb.toString().toLowerCase();  
    }  

    public static String decryptHex(String encryptedHex, String keyHex, String ivHex) throws Exception {  
        byte[] key = decodeKeyHex(keyHex);  
        byte[] iv = decodeIvHex(ivHex);  
        byte[] encryptedData = hexToByteArray(encryptedHex);  

        byte[] decrypted = decrypt(encryptedData, key, iv);  
        return new String(decrypted, "UTF-8");  
    }  

    /**  
     * SM4-CBC解密  
     *  
     * @param encryptedData 加密数据字节数组  
     * @param key 密钥字节数组(16字节)  
     * @param iv IV字节数组(16字节)  
     * @return 解密后的原始字节数组  
     * @throws Exception 解密异常  
     */  
    public static byte[] decrypt(byte[] encryptedData, byte[] key, byte[] iv) throws Exception {  
        Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");  
        cipher.init(Cipher.DECRYPT_MODE,  
                new SecretKeySpec(key, "SM4"),  
                new IvParameterSpec(iv));  
        return cipher.doFinal(encryptedData);  
    }  

    /**  
     * 从十六进制字符串还原密钥字节数组  
     * @param keyHex 十六进制编码的密钥字符串  
     * @return 密钥字节数组(16字节)  
     */  
    public static byte[] decodeKeyHex(String keyHex) {  
        return hexToByteArray(keyHex);  
    }  

    /**  
     * 从十六进制字符串还原IV字节数组  
     * @param ivHex 十六进制编码的IV字符串  
     * @return IV字节数组(16字节)  
     */  
    public static byte[] decodeIvHex(String ivHex) {  
        return hexToByteArray(ivHex);  
    }  

    /**  
     * HEX 转 byte[]  
     *  
     * @param hex HEX字符串  
     * @return 字节数组  
     */  
    public static byte[] hexToByteArray(String hex) {  
        if (hex == null || hex.length() == 0) {  
            return new byte[0];  
        }  

        int len = hex.length() / 2;  
        byte[] bytes = new byte[len];  
        for (int i = 0; i < len; i++) {  
            int high = Character.digit(hex.charAt(i * 2), 16);  
            int low = Character.digit(hex.charAt(i * 2 + 1), 16);  
            bytes[i] = (byte) ((high << 4) | low);  
        }  
        return bytes;  
    }  

    public static String stringToHex(String str) {  
        if (str == null || str.isEmpty()) {  
            return "";  
        }  
        byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);  
        return byteArrayToHex(bytes);  
    }  
}  

package top.sunrains;  

import javax.crypto.Cipher;  
import javax.crypto.spec.IvParameterSpec;  
import javax.crypto.spec.SecretKeySpec;  
import java.security.SecureRandom;  
import java.util.Base64;  
import java.util.HashMap;  
import java.util.Map;  

/**  
 * AES对称加密算法工具类  
 * 使用Java原生Crypto API  
 * 配置信息:  
 * - 算法:AES-256  
 * - 模式:CBC(密码分组链接)  
 * - 填充:PKCS5Padding  
 * - 密钥长度:256位(32字节)  
 * - IV长度:128位(16字节)  
 */  
public class AesUtil {  

    /**  
     * 生成AES密钥和IV  
     * @return Map包含key (32字符) 和 iv (16字符)  
     */  
    public static Map<String, String> generateKey() {  
        try {  
            // 生成32字节的随机字符串作为密钥  
            String key = generateRandomString(32);  

            // 生成16字节的随机字符串作为IV  
            String iv = generateRandomString(16);  

            System.out.println("AES Key: " + key + " (长度: " + key.length() + ")");  
            System.out.println("AES IV: " + iv + " (长度: " + iv.length() + ")");  

            Map<String, String> result = new HashMap<>();  
            result.put("key", key);  
            result.put("iv", iv);  

            return result;  
        } catch (Exception e) {  
            System.err.println("AES 密钥生成失败: " + e.getMessage());  
            e.printStackTrace();  
            return new HashMap<>();  
        }  
    }  

    /**  
     * 生成指定长度的随机字符串  
     */  
    private static String generateRandomString(int length) {  
        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";  
        SecureRandom random = new SecureRandom();  
        StringBuilder result = new StringBuilder(length);  
        for (int i = 0; i < length; i++) {  
            result.append(chars.charAt(random.nextInt(chars.length())));  
        }  
        return result.toString();  
    }  

    /**  
     * AES-CBC加密  
     *  
     * @param plaintext 明文字符串(UTF-8)  
     * @param keyStr 密钥字符串(32字符)  
     * @param ivStr IV字符串(16字符)  
     * @return Base64编码的密文  
     * @throws Exception 加密异常  
     */  
    public static String encrypt(String plaintext, String keyStr, String ivStr) throws Exception {  
        // 将字符串Key和IV转换为字节数组(UTF-8编码)  
        byte[] keyBytes = keyStr.getBytes("UTF-8");  
        byte[] ivBytes = ivStr.getBytes("UTF-8");  

        // 创建密钥和IV  
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");  
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);  

        // 创建Cipher对象(AES/CBC/PKCS5Padding)  
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);  

        // 执行加密  
        byte[] encrypted = cipher.doFinal(plaintext.getBytes("UTF-8"));  

        // 返回Base64编码的密文  
        return Base64.getEncoder().encodeToString(encrypted);  
    }  

    /**  
     * AES-CBC解密  
     *  
     * @param encryptedBase64 Base64编码的密文  
     * @param keyStr 密钥字符串(32字符)  
     * @param ivStr IV字符串(16字符)  
     * @return 解密后的明文字符串  
     * @throws Exception 解密异常  
     */  
    public static String decrypt(String encryptedBase64, String keyStr, String ivStr) throws Exception {  
        // 将字符串Key和IV转换为字节数组(UTF-8编码)  
        byte[] keyBytes = keyStr.getBytes("UTF-8");  
        byte[] ivBytes = ivStr.getBytes("UTF-8");  

        // 创建密钥和IV  
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");  
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);  

        // 创建Cipher对象(AES/CBC/PKCS5Padding)  
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);  

        // 将Base64密文解码为字节  
        byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);  

        // 执行解密  
        byte[] decrypted = cipher.doFinal(encryptedBytes);  

        // 返回明文字符串  
        return new String(decrypted, "UTF-8");  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) throws Exception {  
        System.out.println("=== AES 工具类测试 ===\n");  

        // 生成密钥  
        Map<String, String> keyMap = generateKey();  
        String key = keyMap.get("key");  
        String iv = keyMap.get("iv");  

        System.out.println("密钥: " + key);  
        System.out.println("IV: " + iv);  
        System.out.println();  

        // 加密测试  
        String originalText = "Hello AES";  
        System.out.println("原始数据: " + originalText);  

        String encrypted = encrypt(originalText, key, iv);  
        System.out.println("加密结果: " + encrypted);  
        System.out.println();  

        // 解密测试  
        String decrypted = decrypt(encrypted, key, iv);  
        System.out.println("解密结果: " + decrypted);  
        System.out.println();  

        if (originalText.equals(decrypted)) {  
            System.out.println("✅ 加密解密成功!");  
        } else {  
            System.err.println("❌ 加密解密失败!");  
        }  
    }  
}  

package top.sunrains;  

import cn.hutool.core.text.CharSequenceUtil;  
import org.bouncycastle.crypto.digests.SM3Digest;  
import org.bouncycastle.crypto.macs.HMac;  
import org.bouncycastle.crypto.params.KeyParameter;  
import org.bouncycastle.jce.provider.BouncyCastleProvider;  

import java.io.File;  
import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
import java.security.Security;  
/**  
 * SM3国密哈希算法工具类  
 * 使用Bouncy Castle原生API  
 * 功能:  
 * - 字符串哈希计算  
 * - 字节数组哈希计算  
 * - 文件哈希计算  
 * - HMAC-SM3计算  
 */  
public class SM3Util {  

    static {  
        try {  
            Security.addProvider(new BouncyCastleProvider());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

    private static final int BUFFER_SIZE = 8192;  

    /**  
     * 计算字符串的SM3哈希值  
     *  
     * @param data 待哈希的字符串  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     */  
    public static String hash(String data) {  
        if (data == null) {  
            throw new IllegalArgumentException("Data cannot be null");  
        }  
        return hash(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));  
    }  

    /**  
     * 验证字符串的SM3哈希值  
     *  
     * @param data 原始数据  
     * @param expectedHash 期望的哈希值  
     * @return 是否匹配  
     */  
    public static boolean verify(String data, String expectedHash) {  
        String actualHash = hash(data);  
        return actualHash.equalsIgnoreCase(expectedHash);  
    }  

    /**  
     * 计算字节数组的SM3哈希值  
     *  
     * @param data 待哈希的字节数组  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     */  
    public static String hash(byte[] data) {  
        SM3Digest digest = new SM3Digest();  
        digest.update(data, 0, data.length);  
        byte[] result = new byte[digest.getDigestSize()];  
        digest.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * 计算文件的SM3哈希值(支持大文件)  
     *  
     * @param filePath 文件路径  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     * @throws IOException 文件读取异常  
     */  
    public static String hashFile(String filePath) throws IOException {  
        return hashFile(new File(filePath));  
    }  

    /**  
     * 计算文件的SM3哈希值(支持大文件)  
     *  
     * @param file 文件对象  
     * @return 哈希值(小写十六进制字符串,64个字符)  
     * @throws IOException 文件读取异常  
     */  
    public static String hashFile(File file) throws IOException {  
        if (!file.exists()) {  
            throw new IOException("File not found: " + file.getAbsolutePath());  
        }  

        SM3Digest digest = new SM3Digest();  
        byte[] buffer = new byte[BUFFER_SIZE];  

        try (InputStream is = new FileInputStream(file)) {  
            int bytesRead;  
            while ((bytesRead = is.read(buffer)) != -1) {  
                digest.update(buffer, 0, bytesRead);  
            }  
        }  

        byte[] result = new byte[digest.getDigestSize()];  
        digest.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * 计算HMAC-SM3(基于密钥的哈希消息认证码)  
     *  
     * @param data 待计算的数据  
     * @param hexKey 密钥  
     * @return HMAC-SM3结果(小写十六进制字符串,64个字符)  
     */  
    public static String hmac(String data, String hexKey) {  
        if (CharSequenceUtil.isBlank(hexKey)) {  
            throw new IllegalArgumentException("密钥不能为空");  
        }  
        return hmac(data.getBytes(java.nio.charset.StandardCharsets.UTF_8),  
                hexToByteArray(hexKey));  
    }  

    /**  
     * 计算HMAC-SM3(基于密钥的哈希消息认证码)  
     *  
     * @param data 待计算的字节数组  
     * @param hexKey hex密钥字节数组  
     * @return HMAC-SM3结果(小写十六进制字符串,64个字符)  
     */  
    public static String hmac(byte[] data, byte[] hexKey) {  
        HMac hmac = new HMac(new SM3Digest());  
        hmac.init(new KeyParameter(hexKey));  
        hmac.update(data, 0, data.length);  
        byte[] result = new byte[hmac.getMacSize()];  
        hmac.doFinal(result, 0);  
        return byteArrayToHex(result);  
    }  

    /**  
     * byte[] 转 Hex  
     *  
     * @param bytes 字节数组  
     * @return HEX字符串(小写)  
     */  
    private static String byteArrayToHex(byte[] bytes) {  
        if (bytes == null || bytes.length == 0) {  
            return "";  
        }  
        StringBuilder sb = new StringBuilder();  
        for (byte b : bytes) {  
            String hex = Integer.toHexString(b & 0xFF);  
            if (hex.length() == 1) {  
                sb.append('0');  
            }  
            sb.append(hex);  
        }  
        return sb.toString().toLowerCase();  
    }  

    /**  
     * 测试方法  
     */  
    public static void main(String[] args) throws IOException {  
        String testData = "12";  
        System.out.println("SM3: " + hash(testData));  
        String key = "1";  
        key = stringToHex(key);  
        System.out.println("HMAC-SM3 key: " + key);  
        String hmacResult = hmac(testData, key);  
        System.out.println("HMAC-SM3: " + hmacResult);  
        System.out.println("HMAC长度: " + hmacResult.length() + " 字符");  
       System.out.println("文件哈希值: " +  hashFile("/Users/sun/1/t.jpg"));  
    }  

    public static String stringToHex(String str) {  
        if (str == null || str.isEmpty()) {  
            return "";  
        }  
        byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);  
        return byteArrayToHex(bytes);  
    }  

    /**  
     * HEX 转 byte[]  
     *  
     * @param hex HEX字符串  
     * @return 字节数组  
     */  
    public static byte[] hexToByteArray(String hex) {  
        if (hex == null || hex.length() == 0) {  
            return new byte[0];  
        }  

        int len = hex.length() / 2;  
        byte[] bytes = new byte[len];  
        for (int i = 0; i < len; i++) {  
            int high = Character.digit(hex.charAt(i * 2), 16);  
            int low = Character.digit(hex.charAt(i * 2 + 1), 16);  
            bytes[i] = (byte) ((high << 4) | low);  
        }  
        return bytes;  
    }  
}

各平台实现说明

Android 平台

SM2 实现:

  • 依赖库:BouncyCastle (org.bouncycastle:bcprov-jdk15on:1.70)

  • 特点

    • 使用 ECKeyPairGenerator 生成密钥对
    • 使用 SM2Signer 进行签名和验签(DER 格式)
    • 使用 SM2Engine 进行加密和解密(C1C3C2 格式)
    • 不依赖 JCA Provider,避免兼容性问题

SM4 实现:

  • 依赖库:BouncyCastle

  • 特点

    • 使用 SM4Engine 进行加密和解密
    • CBC 模式 + PKCS7 填充
    • 支持自定义密钥和 IV

iOS 平台

实现方式:GMObjC 原生库

SM2 特点:

  • 使用 GMSm2Utils 进行所有操作
  • 签名自动转换为 DER 格式以兼容其他平台
  • 加密输出 C1C3C2 格式(04 开头)
  • 支持跨平台互通(Android/HarmonyOS)

SM4 特点:

  • 使用 GMSm4Utils 进行加密解密
  • CBC 模式 + PKCS7 填充
  • 密钥和 IV 为 32 位十六进制字符串

HarmonyOS 平台

实现方式: @ohos.security.cryptoFramework

SM2 特点:

  • 使用 HarmonyOS 原生 cryptoFramework API
  • 签名使用 DER 格式
  • 加密使用 C1C3C2 格式
  • 完全符合国密标准

SM3 特点:

  • 使用 createMd (MessageDigest) 进行普通哈希
  • 使用手动实现 HMAC-SM3 (RFC 2104)
  • 支持流式文件哈希处理 (4096 字节缓冲区)
  • 不依赖 createMac,避免密钥参数问题

SM4 特点:

  • 使用 createSymKeyGenerator 生成密钥
  • 使用 createCipher 进行加密解密
  • CBC 模式 + PKCS7 填充
  • 支持 SM4_128 算法

AES 特点:

  • 使用 crypto.createSymKeyGenerator('AES256') 创建密钥生成器
  • 使用 crypto.createCipher('AES|CBC|PKCS7') 进行加解密
  • CBC 模式 + PKCS7 填充
  • 密钥和 IV 为随机字符串(UTF-8 编码)

H5 平台

实现方式:sm-crypto + Web Crypto API

特点:

  • SM2/SM3/SM4:通过 npm 包 sm-crypto 实现
  • AES:使用浏览器原生 Web Crypto API 实现
  • 所有功能均在浏览器环境中运行
  • SM2 签名使用 DER 格式(der: true
  • SM2 默认 userId 为 '1234567812345678'

依赖安装:

npm install --save sm-crypto

微信小程序平台

实现方式:miniprogram-sm-crypto + crypto-js + jsrsasign

特点:

  • 专为小程序优化的国密算法库
  • 完全支持 SM2、SM4 和 AES
  • SM2:密钥生成、签名、验签、加密、解密(使用 miniprogram-sm-crypto)
  • SM4:密钥生成、加密、解密(CBC 模式,使用 miniprogram-sm-crypto)
  • AES:密钥生成、加密、解密(CBC 模式,使用 crypto-js)
  • 需要通过微信开发者工具构建 npm

依赖安装:

npm install --save miniprogram-sm-crypto crypto-js jsrsasign

重要:微信小程序 npm 构建步骤

  1. 确保项目根目录已安装上述依赖
  2. 打开微信开发者工具,勾选「详情」→「本地设置」→「使用 npm 模块」
  3. 点击菜单栏「工具」→「构建 npm」,生成 miniprogram_npm 目录
  4. 重新编译运行小程序

注意事项

1. 密钥格式

SM2 密钥:

  • 公钥:以 04 开头的 130 位十六进制字符串(非压缩格式)
  • 私钥:64 位十六进制字符串

SM4 密钥:

  • 密钥 (key) :32 位十六进制字符串(128 位)
  • 初始向量 (IV) :32 位十六进制字符串(128 位)

AES 密钥:

  • 密钥 (key) :32 字符随机字符串(字母+数字,UTF-8 编码后为 256 位)
  • 初始向量 (IV) :16 字符随机字符串(字母+数字,UTF-8 编码后为 128 位)

2. 签名格式

  • 所有平台的签名输出均为 DER 编码的十六进制字符串
  • DER 格式确保跨平台兼容性

3. 哈希格式

SM3 哈希:

  • 输出长度:64 位十六进制字符串 (256 位)
  • 格式:小写十六进制
  • 示例:a1b2c3d4e5f6... (64 个字符)

HMAC-SM3 哈希:

  • 输出长度:64 位十六进制字符串 (256 位)
  • 密钥格式:十六进制字符串
  • 格式:小写十六进制

4. 加密格式

SM2 加密:

  • 输出格式:C1C3C2(以 04 开头)
  • 兼容 Android BouncyCastle、iOS GMObjC、HarmonyOS cryptoFramework

SM4 加密:

  • 模式:CBC
  • 填充:PKCS7
  • 输出:十六进制字符串

AES 加密:

  • 模式:CBC
  • 填充:PKCS5/PKCS7
  • 密钥:32 字符随机字符串(UTF-8 编码后为 256 位)
  • IV:16 字符随机字符串(UTF-8 编码后为 128 位)
  • 输出:Base64 编码字符串

RSA 加密:

  • 算法:RSA-OAEP with SHA-256 (对应 Java 的 RSA/ECB/OAEPWithSHA-256AndMGF1Padding)
  • 密钥长度:2048 位
  • 私钥格式:PKCS#8 (Base64 编码)
  • 公钥格式:X.509/SPKI (Base64 编码)
  • 输出:Base64 编码字符串
  • 跨平台兼容:Android/iOS/HarmonyOS/H5 完全互通

RSA 签名:

  • 算法:SHA256withRSA (PKCS#1 v1.5)
  • 私钥格式:PKCS#8 (Base64 编码)
  • 公钥格式:X.509/SPKI (Base64 编码)
  • 签名输出:Base64 编码字符串
  • 跨平台兼容:Android/iOS/HarmonyOS/H5/微信小程序 完全互通

5. 跨平台兼容性

✅ 完全兼容:

  • 各平台生成的 SM2 密钥对可以互相使用
  • Android 平台签名的数据可以在 iOS/Harmony/H5 平台验证
  • SM2 加密的数据可以在不同平台间解密
  • SM4 加密的数据可以在 Android/iOS/Harmony 平台间加解密
  • AES 加密的数据可以在所有平台间加解密(Android/iOS/HarmonyOS/H5/微信小程序)
  • SM3 哈希值在所有平台保持一致(相同输入产生相同输出)
  • HMAC-SM3 支持跨平台验证
  • RSA-OAEP-SHA256 加密的数据可以在 Android/iOS/HarmonyOS/H5 平台间加解密
  • SHA256withRSA 签名可以在所有平台间验签(包括微信小程序)

⚠️ 注意事项:

  • 确保使用相同的哈希选项(默认启用)
  • SM4/AES 加密和解密必须使用相同的密钥和 IV
  • 微信小程序需要先构建 npm 才能使用
  • SM3 文件哈希需要确保文件内容完全一致
  • AES 密钥为 32 字符随机字符串,IV 为 16 字符随机字符串
  • RSA 密钥长度为 2048 位,私钥为 PKCS#8 格式,公钥为 X.509/SPKI 格式
  • RSA 加密使用 OAEP with SHA-256 填充,与其他平台互通
  • RSA 签名使用 SHA256withRSA (PKCS#1 v1.5),所有平台兼容
  • 微信小程序端暂不支持 RSA-OAEP-SHA256 加密解密,仅支持签名验签
收起阅读 »

成品微信药店商城小程序,合规开具电子处方,处方水印,二级分销,开源代码

**元岳科技自2021年起,专注药店商城小程序的研发与技术服务。已经服务全国上百家实体药店均有真实案例,欢迎联系客服咨询。
我们给您搭建的线上商城,已经经历过众多客户的使用与验证,市场反应良好,客户运营稳定,我们熟悉从准备材料到上架成功的全流程。
根据客户的反馈,不断完善系统功能自我进化,我们始终深耕药店的技术开发领域,做到专精。
主要功能:1,线上商城,已对接互联网医院,合规开具电子处方,提供处方药服务。
2,主图水印,自动给产品主图打上水印,合规不显示药品的功能和用量等信息
3,药品标签,分类别展示不同的产品标签,区分处方药,甲类otc,乙类otc.
4,合规要求,处方药设置营销活动,自动阻断,甲类OTC参加部分营销活动自动阻断。
5,小票打印,电子面单打印,快递发货,物流轨迹同步。
6,可提供渠道办理《互联网药品信息服务备案凭证》,全程协助指导,包上架成功。
。。。。。更多功能。。。。
技术栈:前端使用uniapp开发,后端php,thinkphp框架,前后端分离,二开更便捷。提供全靠开源代码,可定制开发。
电话:18678829759,同V.

继续阅读 »

**元岳科技自2021年起,专注药店商城小程序的研发与技术服务。已经服务全国上百家实体药店均有真实案例,欢迎联系客服咨询。
我们给您搭建的线上商城,已经经历过众多客户的使用与验证,市场反应良好,客户运营稳定,我们熟悉从准备材料到上架成功的全流程。
根据客户的反馈,不断完善系统功能自我进化,我们始终深耕药店的技术开发领域,做到专精。
主要功能:1,线上商城,已对接互联网医院,合规开具电子处方,提供处方药服务。
2,主图水印,自动给产品主图打上水印,合规不显示药品的功能和用量等信息
3,药品标签,分类别展示不同的产品标签,区分处方药,甲类otc,乙类otc.
4,合规要求,处方药设置营销活动,自动阻断,甲类OTC参加部分营销活动自动阻断。
5,小票打印,电子面单打印,快递发货,物流轨迹同步。
6,可提供渠道办理《互联网药品信息服务备案凭证》,全程协助指导,包上架成功。
。。。。。更多功能。。。。
技术栈:前端使用uniapp开发,后端php,thinkphp框架,前后端分离,二开更便捷。提供全靠开源代码,可定制开发。
电话:18678829759,同V.

收起阅读 »

苹果审核 3.2 被拒?大多数情况都离不开这三个原因,最后终于解决了,亲测有用

苹果内购 苹果审核 App离线打包 应用上架
# 苹果审核 3.2 被拒,很多人其实从一开始就找错了方向  
做 App 上架这些年,我发现一个很有意思的现象。  

同样收到苹果 3.2 的反馈,有的人改一次就过了,有的人连续改了十几个版本还是过不了。  

为什么?  

因为大部分人收到 3.2 后,第一反应就是改代码。 实际上,3.2 往往不是代码问题,而是苹果在审核过程中对你的账号、业务、产品真实性产生了怀疑。 换句话说,苹果审核的不是某个按钮,也不是某个页面,而是在判断: “你这个 App,到底是不是你描述的那个 App。” 从这个角度去理解,3.2 的问题其实可以归纳成三个层面。 --- # 第一种:苹果不相信你的账号 很多开发者觉得账号只是一个提交工具。 但在苹果眼里,账号本身就是产品的一部分。 如果一个账号曾经出现过违规记录,或者账号背后的开发环境、设备环境、网络环境存在异常,苹果首先怀疑的不是 App,而是开发者。 这就像一家店铺。 顾客进门之前,先看的是招牌。 如果招牌本身就有问题,后面卖什么已经不重要了。 所以很多项目明明功能正常,却总是卡在审核阶段。 真正的问题不在产品,而在账号信誉。 --- # 第二种:苹果不相信你的业务 很多团队会认为: “这个业务别人能做,我为什么不能做?” 实际上苹果审核并不是参考别人。 苹果审核的是: 这个业务是否符合苹果对 App Store 的定位。 有些产品从运营角度没有问题,但从苹果的角度来看,可能存在诱导、营销过重、功能价值不明确或者商业模式风险较高的问题。 这时候开发者会不断修改界面、调整文案。 但审核结果依旧一样。 原因很简单。 苹果关注的不是你首页长什么样,而是你的业务本质是什么。 如果业务表达方式和产品定位出现偏差,再漂亮的 UI 也解决不了问题。 --- # 第三种:苹果不相信你提交的版本 这是近两年越来越常见的一种情况。 很多开发者认为审核是考试。 于是提交一个版本给审核,审核通过后再把真正的功能放出来。 但苹果认为审核不是考试。 审核是验货。 你送过去的是什么,用户下载到的就应该是什么。 如果审核版本和实际运营版本存在明显差异,无论通过后台配置、远程参数还是功能开关实现,本质上都会让苹果觉得: “你给我看的和你准备卖给用户的不是同一个东西。” 一旦形成这种判断,后面的问题就不再是功能问题,而是信任问题。 --- # 在我看来,3.2 本质上只有一个原因 很多文章都在分析苹果审核规则。 但这些年处理下来,我越来越觉得: 3.2 本质上只有一个核心逻辑。 那就是苹果没有建立起对产品的信任。 账号有风险,信任降低。 业务表达不清晰,信任降低。 功能前后不一致,信任降低。 最终都会汇聚到同一个审核结果。 所以与其研究苹果为什么拒绝你,不如先思考: 苹果为什么不相信你。 当这个问题想明白了,很多整改方向自然就清晰了。 --- # 为什么我们的处理思路和别人不一样 很多服务商接到 3.2 项目后,第一步是改代码。 而我们的第一步通常是判断: 这个问题到底属于账号、业务还是产品。 因为这三种问题对应的是三套完全不同的解决方案。 账号问题看环境。 业务问题看定位。 功能问题看逻辑。 方向判断错了,再多修改都是无效工作。 这些年我们内部陆续搭建了上架档案系统、审核记录系统、代码检测系统、相似度检测系统以及环境管理体系。 这些工具最大的作用不是帮客户修改代码,而是帮助我们快速找到问题真正出现在哪个环节。 因为在苹果审核里面,找到问题,往往比解决问题更重要。 很多团队卡审几个月,最后发现只是一开始判断错了方向。 而这,恰恰是 3.2 最容易被忽略的地方。
继续阅读 »
# 苹果审核 3.2 被拒,很多人其实从一开始就找错了方向  
做 App 上架这些年,我发现一个很有意思的现象。  

同样收到苹果 3.2 的反馈,有的人改一次就过了,有的人连续改了十几个版本还是过不了。  

为什么?  

因为大部分人收到 3.2 后,第一反应就是改代码。 实际上,3.2 往往不是代码问题,而是苹果在审核过程中对你的账号、业务、产品真实性产生了怀疑。 换句话说,苹果审核的不是某个按钮,也不是某个页面,而是在判断: “你这个 App,到底是不是你描述的那个 App。” 从这个角度去理解,3.2 的问题其实可以归纳成三个层面。 --- # 第一种:苹果不相信你的账号 很多开发者觉得账号只是一个提交工具。 但在苹果眼里,账号本身就是产品的一部分。 如果一个账号曾经出现过违规记录,或者账号背后的开发环境、设备环境、网络环境存在异常,苹果首先怀疑的不是 App,而是开发者。 这就像一家店铺。 顾客进门之前,先看的是招牌。 如果招牌本身就有问题,后面卖什么已经不重要了。 所以很多项目明明功能正常,却总是卡在审核阶段。 真正的问题不在产品,而在账号信誉。 --- # 第二种:苹果不相信你的业务 很多团队会认为: “这个业务别人能做,我为什么不能做?” 实际上苹果审核并不是参考别人。 苹果审核的是: 这个业务是否符合苹果对 App Store 的定位。 有些产品从运营角度没有问题,但从苹果的角度来看,可能存在诱导、营销过重、功能价值不明确或者商业模式风险较高的问题。 这时候开发者会不断修改界面、调整文案。 但审核结果依旧一样。 原因很简单。 苹果关注的不是你首页长什么样,而是你的业务本质是什么。 如果业务表达方式和产品定位出现偏差,再漂亮的 UI 也解决不了问题。 --- # 第三种:苹果不相信你提交的版本 这是近两年越来越常见的一种情况。 很多开发者认为审核是考试。 于是提交一个版本给审核,审核通过后再把真正的功能放出来。 但苹果认为审核不是考试。 审核是验货。 你送过去的是什么,用户下载到的就应该是什么。 如果审核版本和实际运营版本存在明显差异,无论通过后台配置、远程参数还是功能开关实现,本质上都会让苹果觉得: “你给我看的和你准备卖给用户的不是同一个东西。” 一旦形成这种判断,后面的问题就不再是功能问题,而是信任问题。 --- # 在我看来,3.2 本质上只有一个原因 很多文章都在分析苹果审核规则。 但这些年处理下来,我越来越觉得: 3.2 本质上只有一个核心逻辑。 那就是苹果没有建立起对产品的信任。 账号有风险,信任降低。 业务表达不清晰,信任降低。 功能前后不一致,信任降低。 最终都会汇聚到同一个审核结果。 所以与其研究苹果为什么拒绝你,不如先思考: 苹果为什么不相信你。 当这个问题想明白了,很多整改方向自然就清晰了。 --- # 为什么我们的处理思路和别人不一样 很多服务商接到 3.2 项目后,第一步是改代码。 而我们的第一步通常是判断: 这个问题到底属于账号、业务还是产品。 因为这三种问题对应的是三套完全不同的解决方案。 账号问题看环境。 业务问题看定位。 功能问题看逻辑。 方向判断错了,再多修改都是无效工作。 这些年我们内部陆续搭建了上架档案系统、审核记录系统、代码检测系统、相似度检测系统以及环境管理体系。 这些工具最大的作用不是帮客户修改代码,而是帮助我们快速找到问题真正出现在哪个环节。 因为在苹果审核里面,找到问题,往往比解决问题更重要。 很多团队卡审几个月,最后发现只是一开始判断错了方向。 而这,恰恰是 3.2 最容易被忽略的地方。
收起阅读 »

为什么很多 AI 写出来的代码,更容易收到苹果 4.3 拒绝?


最近接触了不少客户,发现一个比较明显的现象。  

很多客户使用 AI 工具开发 App 后,测试运行都没问题,但一提交 App Store,没几天就收到苹果 4.3 的拒绝邮件。  

不少人第一反应是:  

"是不是苹果针对 AI 开发的 App?"  

实际上并不是。  

从我们这些年处理苹果上架的经验来看,苹果并不会因为你用了 AI 开发就拒绝你的应用,它关注的始终是应用本身的质量、独立性和价值。  

但是,AI 生成代码确实更容易踩到 4.3 的一些风险点。  

## 4.3 到底在审核什么?  

很多开发者觉得 4.3 是代码问题。  

实际上并不完全是。  

苹果 4.3 更多是在判断:  

你的 App 是不是一个真正独立的产品。  

如果审核人员认为:  

- 功能和市场上大量应用差不多  
- 产品结构高度相似  
- 页面布局没有明显区别  
- 代码特征存在大量复用痕迹  

那么就有可能被归类到 4.3。  

简单理解就是:  

苹果不希望 App Store 里面出现大量"换个名字就重新提交"的应用。  

## 为什么 AI 开发更容易出现这种情况?  

因为目前大部分 AI 生成代码的逻辑都比较接近。  

比如让 AI 开发一个工具类 App。  

它给出的方案通常都是:  

首页  

功能页  

个人中心  

设置页  

再配上常见的网络请求封装、数据存储方案以及标准化目录结构。  

从开发角度来说没有问题。  

但从苹果审核角度来看,这种项目往往缺少自己的特点。  

我们实际处理过不少案例。  

客户觉得自己是重新开发的产品,但审核人员看到的却是:  

又一个差不多的工具 App。  

又一个差不多的资讯 App。  

又一个差不多的商城 App。  

产品价值没有体现出来,自然更容易进入人工深度审核。  

## 代码相似度其实也是审核重点之一  

很多人觉得只要界面改了就行。  

实际上这些年苹果对于代码层面的检测能力一直在提升。  

特别是一些批量生成的项目。  

经常会出现:  

- 相同的目录结构  
- 相同的类名命名习惯  
- 相同的资源组织方式  
- 相同的业务逻辑流程  

虽然不一定完全一样,但整体特征非常接近。  

尤其是现在很多 AI 工具生成出来的项目,本身就带有固定模板特征。  

如果再叠加多个账号、多次提交,就容易被系统识别出来。  

## 很多问题其实出在产品包装上  

这些年遇到的 4.3 案例里面,有相当一部分代码本身并没有问题。  

问题出在产品包装。 比如: 应用名称没有特色; 截图内容过于普通; 功能介绍写得模糊; 审核备注过于简单; 产品场景表达不清楚; 用户价值无法体现。 开发者知道自己的产品是做什么的。 但审核人员并不知道。 当审核人员无法快速理解产品价值的时候,4.3 的概率就会明显增加。 ## 为什么同样的项目,有的人能过,有的人过不了? 因为苹果审核从来不是只看代码。 它会综合判断: - 产品定位 - 功能完整度 - 用户价值 - 账号历史 - 提交资料 - 代码结构 - 应用差异化程度 很多开发者把精力都放在开发上。 但实际上,上架审核本身也是一个专业环节。 ## 我们这些年处理 4.3 的经验 这些年帮客户处理苹果审核的时候,我们发现真正有效的方法并不是单纯修改代码。 而是从多个维度一起优化: 首先检查代码结构是否存在明显模板化特征; 其次检查产品功能是否具备独立价值; 然后重新梳理应用定位和审核说明; 同时排查截图、元数据、隐私配置等细节问题; 最后再结合账号历史和提交环境进行整体评估。 很多客户反复提交十几次都过不了。 调整完这些内容之后,反而一次就通过了。 ## 总结 AI 可以提高开发效率,这是毋庸置疑的。 但苹果审核看的从来不是代码是谁写的,而是你的 App 是否像一个真正独立、有价值的产品。 如果只是利用 AI 快速拼出一个应用框架,再简单修改一下界面就提交审核,那么收到 4.3 的概率确实会更高。 而如果在开发完成后,能够做好代码优化、产品差异化设计、审核资料整理以及风险排查,那么 AI 开发出来的项目同样可以顺利通过审核。 从我们这些年的上架经验来看,很多 4.3 并不是技术问题,而是产品和审核思路的问题。
继续阅读 »

最近接触了不少客户,发现一个比较明显的现象。  

很多客户使用 AI 工具开发 App 后,测试运行都没问题,但一提交 App Store,没几天就收到苹果 4.3 的拒绝邮件。  

不少人第一反应是:  

"是不是苹果针对 AI 开发的 App?"  

实际上并不是。  

从我们这些年处理苹果上架的经验来看,苹果并不会因为你用了 AI 开发就拒绝你的应用,它关注的始终是应用本身的质量、独立性和价值。  

但是,AI 生成代码确实更容易踩到 4.3 的一些风险点。  

## 4.3 到底在审核什么?  

很多开发者觉得 4.3 是代码问题。  

实际上并不完全是。  

苹果 4.3 更多是在判断:  

你的 App 是不是一个真正独立的产品。  

如果审核人员认为:  

- 功能和市场上大量应用差不多  
- 产品结构高度相似  
- 页面布局没有明显区别  
- 代码特征存在大量复用痕迹  

那么就有可能被归类到 4.3。  

简单理解就是:  

苹果不希望 App Store 里面出现大量"换个名字就重新提交"的应用。  

## 为什么 AI 开发更容易出现这种情况?  

因为目前大部分 AI 生成代码的逻辑都比较接近。  

比如让 AI 开发一个工具类 App。  

它给出的方案通常都是:  

首页  

功能页  

个人中心  

设置页  

再配上常见的网络请求封装、数据存储方案以及标准化目录结构。  

从开发角度来说没有问题。  

但从苹果审核角度来看,这种项目往往缺少自己的特点。  

我们实际处理过不少案例。  

客户觉得自己是重新开发的产品,但审核人员看到的却是:  

又一个差不多的工具 App。  

又一个差不多的资讯 App。  

又一个差不多的商城 App。  

产品价值没有体现出来,自然更容易进入人工深度审核。  

## 代码相似度其实也是审核重点之一  

很多人觉得只要界面改了就行。  

实际上这些年苹果对于代码层面的检测能力一直在提升。  

特别是一些批量生成的项目。  

经常会出现:  

- 相同的目录结构  
- 相同的类名命名习惯  
- 相同的资源组织方式  
- 相同的业务逻辑流程  

虽然不一定完全一样,但整体特征非常接近。  

尤其是现在很多 AI 工具生成出来的项目,本身就带有固定模板特征。  

如果再叠加多个账号、多次提交,就容易被系统识别出来。  

## 很多问题其实出在产品包装上  

这些年遇到的 4.3 案例里面,有相当一部分代码本身并没有问题。  

问题出在产品包装。 比如: 应用名称没有特色; 截图内容过于普通; 功能介绍写得模糊; 审核备注过于简单; 产品场景表达不清楚; 用户价值无法体现。 开发者知道自己的产品是做什么的。 但审核人员并不知道。 当审核人员无法快速理解产品价值的时候,4.3 的概率就会明显增加。 ## 为什么同样的项目,有的人能过,有的人过不了? 因为苹果审核从来不是只看代码。 它会综合判断: - 产品定位 - 功能完整度 - 用户价值 - 账号历史 - 提交资料 - 代码结构 - 应用差异化程度 很多开发者把精力都放在开发上。 但实际上,上架审核本身也是一个专业环节。 ## 我们这些年处理 4.3 的经验 这些年帮客户处理苹果审核的时候,我们发现真正有效的方法并不是单纯修改代码。 而是从多个维度一起优化: 首先检查代码结构是否存在明显模板化特征; 其次检查产品功能是否具备独立价值; 然后重新梳理应用定位和审核说明; 同时排查截图、元数据、隐私配置等细节问题; 最后再结合账号历史和提交环境进行整体评估。 很多客户反复提交十几次都过不了。 调整完这些内容之后,反而一次就通过了。 ## 总结 AI 可以提高开发效率,这是毋庸置疑的。 但苹果审核看的从来不是代码是谁写的,而是你的 App 是否像一个真正独立、有价值的产品。 如果只是利用 AI 快速拼出一个应用框架,再简单修改一下界面就提交审核,那么收到 4.3 的概率确实会更高。 而如果在开发完成后,能够做好代码优化、产品差异化设计、审核资料整理以及风险排查,那么 AI 开发出来的项目同样可以顺利通过审核。 从我们这些年的上架经验来看,很多 4.3 并不是技术问题,而是产品和审核思路的问题。
收起阅读 »

uni-app路由管理神器:vue-router风格体验

路由守卫 路由

@meng-xi/uni-router

为 uni-app 提供类似 vue-router 风格的路由管理系统(uni_modules 版本)。


特性

  • vue-router 风格 API - 熟悉的 push / replace / back 导航方式,零学习成本
  • 路由守卫 - 全局前置守卫 beforeEach、解析守卫 beforeResolve、后置钩子 afterEach、路由独享守卫 beforeEnter
  • 守卫超时保护 - 守卫未调用 next() 时自动中止导航,超时时间可配置(guardTimeout
  • 命名路由 - 通过 name 进行导航,无需硬编码路径字符串
  • 路由元信息 - meta 字段支持页面标题、权限标记、TabBar 标识等自定义数据
  • uni API 拦截 - 拦截 uni.navigateTo 等原生导航 API,确保守卫始终生效(interceptUniApi
  • 路由状态同步 - syncRoute() 将路由状态与实际页面栈同步,处理物理返回键等非路由器导航
  • 路由变化监听 - onRouteChange() 订阅路由状态变化,包括导航完成和状态同步
  • RouterLink 组件 - 声明式导航组件,支持 push / replace 模式和 @error 事件
  • TypeScript 类型提示 - 通过模块增强为路由名称和路径提供自动补全和类型检查
  • 错误处理 - 完整的 RouterError / NavigationFailure 体系,支持 onError 全局捕获
  • 组合式 API - useRouter() / useRoute() 在组件中便捷访问路由器
  • uni_modules 集成 - 通过 uni_modules 方式安装,无需 npm,开箱即用

📖 完整文档:https://mengxi-studio.github.io/uni-router/

安装

uni_modules(推荐)

mxuni-router 目录复制到项目的 uni_modules 目录下:

src/  
  └── uni_modules/  
        └── mxuni-router/  
              ├── js_sdk/  
              │     ├── index.js  
              │     ├── index.cjs  
              │     ├── index.d.ts  
              │     └── index.d.cts  
              ├── components/  
              │     └── mxuni-router/  
              │           └── mxuni-router.vue  
              ├── package.json  
              └── readme.md

npm

pnpm add @meng-xi/uni-router

npm 方式需将导入路径改为 @meng-xi/uni-router

快速开始

1. 创建路由器

// main.ts  
import { createSSRApp } from 'vue'  
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'  
import App from './App.vue'  

const router = createRouter({  
    routes: [  
        { path: 'pages/index/index', name: 'home', meta: { title: '首页' } },  
        { path: 'pages/about/about', name: 'about', meta: { title: '关于', requireAuth: true } },  
        { path: 'pages/user/user', name: 'user', meta: { title: '我的', isTab: true } }  
    ],  
    strict: true  
})  

export function createApp() {  
    const app = createSSRApp(App)  
    app.use(router)  
    return { app }  
}

2. 路由导航

import { useRouter, useRoute } from './uni_modules/mxuni-router/js_sdk/index.js'  

// 在组件 setup 中使用  
const router = useRouter()  
const route = useRoute()  

// 路径导航  
await router.push('/pages/about/about')  
await router.push({ path: '/pages/about/about', query: { id: '1' } })  

// 命名导航  
await router.push({ name: 'about' })  

// 返回  
await router.back()  
await router.back(2) // 返回两级

3. 路由守卫

// 全局前置守卫 - 登录验证  
router.beforeEach((to, from, next) => {  
    if (to.meta.requireAuth && !isLoggedIn()) {  
        next({ name: 'login', query: { redirect: to.fullPath } })  
    } else {  
        next()  
    }  
})  

// 全局后置钩子  
router.afterEach((to, from) => {  
    console.log(`导航完成: ${from.path} → ${to.path}`)  
})

4. 自动生成路由配置(推荐)

配合 @meng-xi/vite-plugingenerateRouter 插件,可从 pages.json 自动生成路由配置和类型声明:

pnpm add @meng-xi/vite-plugin -D
// vite.config.ts  
import { defineConfig } from 'vite'  
import uni from '@dcloudio/vite-plugin-uni'  
import { generateRouter } from '@meng-xi/vite-plugin'  

export default defineConfig({  
    plugins: [  
        uni(),  
        generateRouter({  
            pagesJsonPath: 'src/pages.json',  
            outputPath: 'src/router.config.ts',  
            dts: true,  
            metaMapping: {  
                navigationBarTitleText: 'title',  
                requireAuth: 'requireAuth'  
            }  
        })  
    ]  
})

然后在 main.ts 中导入生成的路由配置:

import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'  
import routes from './router.config'  

const router = createRouter({ routes })

API 概览

核心

API 说明
createRouter(options) 创建路由器实例
useRouter() 获取路由器实例(组合式 API)
useRoute() 获取当前路由位置(组合式 API)

Router 实例方法

方法 说明
router.push(location) 导航到新页面
router.replace(location) 替换当前页面
router.back(delta?) 返回上一页或多级页面
router.beforeEach(guard) 注册全局前置守卫
router.beforeResolve(guard) 注册全局解析守卫
router.afterEach(guard) 注册全局后置钩子
router.onError(handler) 注册错误处理回调
router.resolve(location) 解析路由位置(不导航)
router.getRoutes() 获取所有路由配置
router.hasRoute(name) 检查路由是否存在
router.isReady() 等待路由器初始化完成
router.onRouteChange(listener) 注册路由变化监听器
router.syncRoute() 同步路由状态与实际页面栈

错误码

错误码 说明
NAVIGATION_ABORTED 导航被守卫中止
NAVIGATION_CANCELLED 导航被取消(守卫异常或重定向超限)
NAVIGATION_DUPLICATED 重复导航到当前位置
ROUTE_NOT_FOUND 未找到匹配的路由
NAVIGATION_API_ERROR uni 导航 API 调用失败
SETUP_ERROR 路由器初始化或使用方式错误

RouterOptions 配置项

选项 类型 默认值 说明
routes RouteConfig[] - 路由配置列表,需与 pages.json 中的页面声明保持一致
strict boolean true 是否启用严格模式,启用后未匹配的命名路由将抛出异常
interceptUniApi boolean false 是否拦截 uni.navigateTo 等原生导航 API,启用后直接调用 uni API 将转由路由器处理,确保守卫生效
guardTimeout number 10000 守卫超时时间(毫秒),超时后自动中止导航并输出警告,设为 0 可禁用

RouterLink 组件

声明式导航组件,对应 uni-app 的 <navigator>,自动通过路由器执行导航。

<!-- 路径导航 -->  
<mxuni-router to="/pages/about/about">  
    <view>跳转到关于页</view>  
</mxuni-router>  

<!-- replace 模式 -->  
<mxuni-router to="/pages/about/about" replace>  
    <view>替换当前页</view>  
</mxuni-router>  

<!-- 捕获导航失败 -->  
<mxuni-router to="/pages/about/about" @error="onNavError">  
    <view>跳转</view>  
</mxuni-router>
属性 类型 默认值 说明
to RouteLocationRaw - 目标路由位置
replace boolean false 是否使用替换模式导航
hoverClass string 'navigator-hover' 按下时的样式类
hoverStopPropagation boolean false 是否阻止祖先节点的点击态
hoverStartTime number 50 按住后多久出现点击态(ms)
hoverStayTime number 600 手指松开后点击态保留时间(ms)
事件 参数 说明
error NavigationFailure 导航失败时触发

TypeScript 类型提示

启用 dts: true 后,generateRouter 插件自动生成类型声明文件,为路由导航提供类型安全:

// 路由名称自动补全  
router.push({ name: 'pagesIndexIndex' }) // ✅ 自动补全  
router.push({ name: 'invalidName' }) // ❌ 类型错误  

// 路径自动补全  
router.push({ path: '/pages/index/index' }) // ✅ 自动补全  
router.push({ path: '/invalid/path' }) // ❌ 类型错误

与 pages.json 的关系

Uni Router 不替代 pages.json,而是与之配合使用:

职责 pages.json Uni Router
页面注册 必须声明 不负责
路由导航 uni.navigateTo 等 push / replace / back
路由守卫 不支持 beforeEach 等
路由元信息 不支持 meta 字段
命名路由 不支持 name 字段

License

MIT

继续阅读 »

@meng-xi/uni-router

为 uni-app 提供类似 vue-router 风格的路由管理系统(uni_modules 版本)。


特性

  • vue-router 风格 API - 熟悉的 push / replace / back 导航方式,零学习成本
  • 路由守卫 - 全局前置守卫 beforeEach、解析守卫 beforeResolve、后置钩子 afterEach、路由独享守卫 beforeEnter
  • 守卫超时保护 - 守卫未调用 next() 时自动中止导航,超时时间可配置(guardTimeout
  • 命名路由 - 通过 name 进行导航,无需硬编码路径字符串
  • 路由元信息 - meta 字段支持页面标题、权限标记、TabBar 标识等自定义数据
  • uni API 拦截 - 拦截 uni.navigateTo 等原生导航 API,确保守卫始终生效(interceptUniApi
  • 路由状态同步 - syncRoute() 将路由状态与实际页面栈同步,处理物理返回键等非路由器导航
  • 路由变化监听 - onRouteChange() 订阅路由状态变化,包括导航完成和状态同步
  • RouterLink 组件 - 声明式导航组件,支持 push / replace 模式和 @error 事件
  • TypeScript 类型提示 - 通过模块增强为路由名称和路径提供自动补全和类型检查
  • 错误处理 - 完整的 RouterError / NavigationFailure 体系,支持 onError 全局捕获
  • 组合式 API - useRouter() / useRoute() 在组件中便捷访问路由器
  • uni_modules 集成 - 通过 uni_modules 方式安装,无需 npm,开箱即用

📖 完整文档:https://mengxi-studio.github.io/uni-router/

安装

uni_modules(推荐)

mxuni-router 目录复制到项目的 uni_modules 目录下:

src/  
  └── uni_modules/  
        └── mxuni-router/  
              ├── js_sdk/  
              │     ├── index.js  
              │     ├── index.cjs  
              │     ├── index.d.ts  
              │     └── index.d.cts  
              ├── components/  
              │     └── mxuni-router/  
              │           └── mxuni-router.vue  
              ├── package.json  
              └── readme.md

npm

pnpm add @meng-xi/uni-router

npm 方式需将导入路径改为 @meng-xi/uni-router

快速开始

1. 创建路由器

// main.ts  
import { createSSRApp } from 'vue'  
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'  
import App from './App.vue'  

const router = createRouter({  
    routes: [  
        { path: 'pages/index/index', name: 'home', meta: { title: '首页' } },  
        { path: 'pages/about/about', name: 'about', meta: { title: '关于', requireAuth: true } },  
        { path: 'pages/user/user', name: 'user', meta: { title: '我的', isTab: true } }  
    ],  
    strict: true  
})  

export function createApp() {  
    const app = createSSRApp(App)  
    app.use(router)  
    return { app }  
}

2. 路由导航

import { useRouter, useRoute } from './uni_modules/mxuni-router/js_sdk/index.js'  

// 在组件 setup 中使用  
const router = useRouter()  
const route = useRoute()  

// 路径导航  
await router.push('/pages/about/about')  
await router.push({ path: '/pages/about/about', query: { id: '1' } })  

// 命名导航  
await router.push({ name: 'about' })  

// 返回  
await router.back()  
await router.back(2) // 返回两级

3. 路由守卫

// 全局前置守卫 - 登录验证  
router.beforeEach((to, from, next) => {  
    if (to.meta.requireAuth && !isLoggedIn()) {  
        next({ name: 'login', query: { redirect: to.fullPath } })  
    } else {  
        next()  
    }  
})  

// 全局后置钩子  
router.afterEach((to, from) => {  
    console.log(`导航完成: ${from.path} → ${to.path}`)  
})

4. 自动生成路由配置(推荐)

配合 @meng-xi/vite-plugingenerateRouter 插件,可从 pages.json 自动生成路由配置和类型声明:

pnpm add @meng-xi/vite-plugin -D
// vite.config.ts  
import { defineConfig } from 'vite'  
import uni from '@dcloudio/vite-plugin-uni'  
import { generateRouter } from '@meng-xi/vite-plugin'  

export default defineConfig({  
    plugins: [  
        uni(),  
        generateRouter({  
            pagesJsonPath: 'src/pages.json',  
            outputPath: 'src/router.config.ts',  
            dts: true,  
            metaMapping: {  
                navigationBarTitleText: 'title',  
                requireAuth: 'requireAuth'  
            }  
        })  
    ]  
})

然后在 main.ts 中导入生成的路由配置:

import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'  
import routes from './router.config'  

const router = createRouter({ routes })

API 概览

核心

API 说明
createRouter(options) 创建路由器实例
useRouter() 获取路由器实例(组合式 API)
useRoute() 获取当前路由位置(组合式 API)

Router 实例方法

方法 说明
router.push(location) 导航到新页面
router.replace(location) 替换当前页面
router.back(delta?) 返回上一页或多级页面
router.beforeEach(guard) 注册全局前置守卫
router.beforeResolve(guard) 注册全局解析守卫
router.afterEach(guard) 注册全局后置钩子
router.onError(handler) 注册错误处理回调
router.resolve(location) 解析路由位置(不导航)
router.getRoutes() 获取所有路由配置
router.hasRoute(name) 检查路由是否存在
router.isReady() 等待路由器初始化完成
router.onRouteChange(listener) 注册路由变化监听器
router.syncRoute() 同步路由状态与实际页面栈

错误码

错误码 说明
NAVIGATION_ABORTED 导航被守卫中止
NAVIGATION_CANCELLED 导航被取消(守卫异常或重定向超限)
NAVIGATION_DUPLICATED 重复导航到当前位置
ROUTE_NOT_FOUND 未找到匹配的路由
NAVIGATION_API_ERROR uni 导航 API 调用失败
SETUP_ERROR 路由器初始化或使用方式错误

RouterOptions 配置项

选项 类型 默认值 说明
routes RouteConfig[] - 路由配置列表,需与 pages.json 中的页面声明保持一致
strict boolean true 是否启用严格模式,启用后未匹配的命名路由将抛出异常
interceptUniApi boolean false 是否拦截 uni.navigateTo 等原生导航 API,启用后直接调用 uni API 将转由路由器处理,确保守卫生效
guardTimeout number 10000 守卫超时时间(毫秒),超时后自动中止导航并输出警告,设为 0 可禁用

RouterLink 组件

声明式导航组件,对应 uni-app 的 <navigator>,自动通过路由器执行导航。

<!-- 路径导航 -->  
<mxuni-router to="/pages/about/about">  
    <view>跳转到关于页</view>  
</mxuni-router>  

<!-- replace 模式 -->  
<mxuni-router to="/pages/about/about" replace>  
    <view>替换当前页</view>  
</mxuni-router>  

<!-- 捕获导航失败 -->  
<mxuni-router to="/pages/about/about" @error="onNavError">  
    <view>跳转</view>  
</mxuni-router>
属性 类型 默认值 说明
to RouteLocationRaw - 目标路由位置
replace boolean false 是否使用替换模式导航
hoverClass string 'navigator-hover' 按下时的样式类
hoverStopPropagation boolean false 是否阻止祖先节点的点击态
hoverStartTime number 50 按住后多久出现点击态(ms)
hoverStayTime number 600 手指松开后点击态保留时间(ms)
事件 参数 说明
error NavigationFailure 导航失败时触发

TypeScript 类型提示

启用 dts: true 后,generateRouter 插件自动生成类型声明文件,为路由导航提供类型安全:

// 路由名称自动补全  
router.push({ name: 'pagesIndexIndex' }) // ✅ 自动补全  
router.push({ name: 'invalidName' }) // ❌ 类型错误  

// 路径自动补全  
router.push({ path: '/pages/index/index' }) // ✅ 自动补全  
router.push({ path: '/invalid/path' }) // ❌ 类型错误

与 pages.json 的关系

Uni Router 不替代 pages.json,而是与之配合使用:

职责 pages.json Uni Router
页面注册 必须声明 不负责
路由导航 uni.navigateTo 等 push / replace / back
路由守卫 不支持 beforeEach 等
路由元信息 不支持 meta 字段
命名路由 不支持 name 字段

License

MIT

收起阅读 »

增加取消云打包排队的按钮

Linux HBuilderX CLI hbx

增加取消云打包排队的按钮,以及增加取消云打包排队的cli命令

增加取消云打包排队的按钮,以及增加取消云打包排队的cli命令

PC端微信小程序,切换tabBar卡死问题

微信小程序

初始排查方向:

  1. 微信API兼容性问题
  2. 组件兼容性问题
  3. 数据更新机制问题

排查过程:
在初步测试中,我们发现该小程序仅能在体验版环境下通过电脑端进行查看,而开发版本则无法正常打开,或使用自动预览功能,点击 预览->自动预览,可以选择启动 PC 自动预览,点击编译并预览,成功的话将在微信 PC 版上自动拉起小程序。
进一步的诊断显示,即使移除了微信API相关的更新代码,应用仍然出现卡顿现象。为精确定位问题源头,我们采取了逐页注释与标签级注释的方法逐步排除,最终确认问题是由于某公共状态管理中的数据引起。
具体而言,所有使用TabBar导航模式的页面以及登录页面均频繁调用同一接口,并基于此更新上述提及的公共状态管理中的数据,导致了性能瓶颈。

解决方案:
● 在短时间内减少对特定接口的请求频率。
● 在更新公共状态管理的数据时引入一致性检查逻辑,即只有当新获取的数据与现有数据存在差异时才执行更新操作。
● 同时该公共状态管理数据appConfig嵌套过深,如需使用appConfig中某个单一数据,请在store文件中的getters声明再引用。

继续阅读 »

初始排查方向:

  1. 微信API兼容性问题
  2. 组件兼容性问题
  3. 数据更新机制问题

排查过程:
在初步测试中,我们发现该小程序仅能在体验版环境下通过电脑端进行查看,而开发版本则无法正常打开,或使用自动预览功能,点击 预览->自动预览,可以选择启动 PC 自动预览,点击编译并预览,成功的话将在微信 PC 版上自动拉起小程序。
进一步的诊断显示,即使移除了微信API相关的更新代码,应用仍然出现卡顿现象。为精确定位问题源头,我们采取了逐页注释与标签级注释的方法逐步排除,最终确认问题是由于某公共状态管理中的数据引起。
具体而言,所有使用TabBar导航模式的页面以及登录页面均频繁调用同一接口,并基于此更新上述提及的公共状态管理中的数据,导致了性能瓶颈。

解决方案:
● 在短时间内减少对特定接口的请求频率。
● 在更新公共状态管理的数据时引入一致性检查逻辑,即只有当新获取的数据与现有数据存在差异时才执行更新操作。
● 同时该公共状态管理数据appConfig嵌套过深,如需使用appConfig中某个单一数据,请在store文件中的getters声明再引用。

收起阅读 »

【解决】oppo上架应用市场提示套用马甲,相似度过高的问题

uniapp 应用上架 上架被拒 5+App开发

一、开发者困境:遭遇“代码相似度过高”上架应用商店驳回

最近,我遇到了一件非常棘手的事情。自己精心开发的一款Android APP在提交到某主流应用商店进行审核时,被无情驳回了。审核反馈的理由非常刺眼:“代码相似度过高”或“代码相似度极高”、“疑似马甲”。

二、解决办法:使用“问顶安全”的Android应用加固顺利过审

在寻求解决方案的过程中,我接触到了深圳问顶安全科技有限公司(asktopsec.com)的APP加固服务。抱着试一试的心态,我使用了他们的Android APP加固产品对APK进行了加固处理,随后重新申请上架

结果令人惊喜,再次提交审核后,应用商店顺利通过了审核,之前的“代码相似度过高”提示彻底消失。APP成功上架!

后面我咨询客服才了解到,这家公司的团队成员数十年安全经验,其独有的2大核心技术成功帮我上架成功:

1.随机生成加固特征
根据每个APP/版本 随机生成独一无二的加固特征。有效防止加固检测、自动化脱壳工具、病毒误报,为阻断特征识别场景而生。

2.APP矩阵防护
多维度、矩阵式对APP进行增强型加密或混淆,增加逆向分析难度。可以达到千人千面的防护效果。

三、总结

面对应用市场上架的严苛审核,选择专业的安全服务至关重要。如果你也面临代码相似度高、被误判定为马甲包或需要高等级的安全防护,深圳问顶安全科技有限公司是一个值得信赖的选择。他们的官网:asktopsec.com ,进去后就能看到醒目的“Android应用加固与安全检测平台”了,点进去开始加固你的App吧~

继续阅读 »

一、开发者困境:遭遇“代码相似度过高”上架应用商店驳回

最近,我遇到了一件非常棘手的事情。自己精心开发的一款Android APP在提交到某主流应用商店进行审核时,被无情驳回了。审核反馈的理由非常刺眼:“代码相似度过高”或“代码相似度极高”、“疑似马甲”。

二、解决办法:使用“问顶安全”的Android应用加固顺利过审

在寻求解决方案的过程中,我接触到了深圳问顶安全科技有限公司(asktopsec.com)的APP加固服务。抱着试一试的心态,我使用了他们的Android APP加固产品对APK进行了加固处理,随后重新申请上架

结果令人惊喜,再次提交审核后,应用商店顺利通过了审核,之前的“代码相似度过高”提示彻底消失。APP成功上架!

后面我咨询客服才了解到,这家公司的团队成员数十年安全经验,其独有的2大核心技术成功帮我上架成功:

1.随机生成加固特征
根据每个APP/版本 随机生成独一无二的加固特征。有效防止加固检测、自动化脱壳工具、病毒误报,为阻断特征识别场景而生。

2.APP矩阵防护
多维度、矩阵式对APP进行增强型加密或混淆,增加逆向分析难度。可以达到千人千面的防护效果。

三、总结

面对应用市场上架的严苛审核,选择专业的安全服务至关重要。如果你也面临代码相似度高、被误判定为马甲包或需要高等级的安全防护,深圳问顶安全科技有限公司是一个值得信赖的选择。他们的官网:asktopsec.com ,进去后就能看到醒目的“Android应用加固与安全检测平台”了,点进去开始加固你的App吧~

收起阅读 »

【开源】windows上传ipa、管理证书和描述文件,数据无需上传云端

ipa Appstore上传

Github:https://github.com/friend-nicen/appuploader

App Store Connect GUI

一款基于 WailsVue 3 开发的跨平台(macOS / Windows / Linux)App Store Connect 桌面可视化工具。
它通过直接调用 App Store Connect API 的底层机制,实现了本地化、图形化的证书、设备、Bundle ID 和配置文件的管理,提供了现代化的 SaaS Dashboard 体验。

特性

  • 现代 UI: 基于 Vue 3 + TailwindCSS 实现的流畅响应式桌面端界面。
  • 本地存储: 使用无 CGO 依赖的纯 Go SQLite 驱动 github.com/glebarez/sqlite 本地加密存储 API Keys 等配置信息。
  • 多账户管理: 支持配置多个 Issuer ID、Key ID 和 Private Key,支持无缝切换。
  • 核心功能:
    • Bundle ID 管理: 查看现有的 App IDs
    • 证书管理 (Certificates): 查看各类证书信息
    • 描述文件管理 (Profiles): 浏览 Provisioning Profiles
    • 设备管理 (Devices): 查看和管理测试设备

技术栈

  • 后端 (Go): Go 1.21+, Wails v2, GORM, golang-jwt/jwt/v5
  • 前端 (Web): Vue 3 (Composition API), Vue Router, TailwindCSS 3
  • 数据库: SQLite (github.com/glebarez/sqlite)

API 密钥申请指南

使用本工具前,需要先在 Apple App Store Connect 中创建 API 密钥。

1. 前置条件

2. 创建 API 密钥

  1. 打开 App Store Connect → 右上角 "我的账户""API 密钥"
  2. 点击 "生成 API 密钥"
  3. 勾选 "开发人员" 权限(Developer Role),这是管理证书、描述文件等所需的最低权限
  4. 立即下载 .p8 私钥文件(页面关闭后将无法再次下载,只能重新生成)

3. 获取三个关键信息

信息 位置 说明
Issuer ID (发行者 ID) API 密钥页面顶部 发行者 ID 字段 同一账户下所有密钥共享,格式为 xxxx-xxxx-xxxx-xxxx-xxxx
Key ID (密钥 ID) 密钥列表中的 密钥 ID 每个密钥唯一,格式为 XXXXXXXXXX
Private Key (私钥) 下载的 .p8 文件内容 -----BEGIN PRIVATE KEY----- 开头,-----END PRIVATE KEY----- 结尾

4. 权限说明

API 密钥支持以下角色,本工具需要 至少 开发人员 角色

角色 可用功能
开发人员 查看和管理证书、Bundle ID、设备、描述文件
管理员 上述全部 + 管理 App、用户、财务等
财务 仅查看财务报告

5. 安全注意事项

  • 私钥文件(.p8)仅下载一次,请妥善保管
  • 建议为不同环境创建不同的 API 密钥(如开发 / 生产)
  • 可在 App Store Connect 中随时 撤销 泄露的密钥
  • 本工具将私钥存储在本地 SQLite 数据库中,不会上传到任何远程服务器
  • 导出数据时导出的 JSON 文件包含完整私钥,请勿将导出的 JSON 暴露给他人

开发指南

本项目代码包含详尽的中英文注释,方便进行二次开发和功能扩展。

1. 环境准备

  • 安装 Go 1.26+
  • 安装 Node.js 18+
  • 安装 Wails CLI:
    go install github.com/wailsapp/wails/v2/cmd/wails@latest  

2. 本地开发 (Dev Mode)

开发模式下,Wails 会启动一个本地 Web 服务器,并提供热重载 (Hot Reload) 支持。

# 确保在项目根目录运行  
wails dev

前端代码位于 frontend/ 目录中,所有的 Go 暴露方法在 frontend/wailsjs/go/main/App.js 中自动生成,可以直接在 Vue 组件中以 Promise 的方式调用。

3. 编译打包 (Build)

编译生产版本时,Wails 会将前端代码打包并嵌入到最终的二进制执行文件中。

# 编译当前平台的应用  
wails build  

# 交叉编译 macOS (如果你在 Windows/Linux 上)  
wails build -platform darwin/amd64,darwin/arm64  

# 交叉编译 Windows  
wails build -platform windows/amd64

编译成功后,产物会输出到 build/bin/ 目录下。

项目结构

  • /backend: Go 后端逻辑,包含 API 客户端和 SQLite 数据库模型。
  • /frontend: Vue 3 前端代码。
    • src/views: 各大功能模块的 Vue 页面组件。
    • src/router: 页面路由配置。
  • app.go: Wails 的主生命周期文件,包含了绑定到前端的 Go 方法。
  • main.go: Wails 应用启动入口。

License

MIT License

继续阅读 »

Github:https://github.com/friend-nicen/appuploader

App Store Connect GUI

一款基于 WailsVue 3 开发的跨平台(macOS / Windows / Linux)App Store Connect 桌面可视化工具。
它通过直接调用 App Store Connect API 的底层机制,实现了本地化、图形化的证书、设备、Bundle ID 和配置文件的管理,提供了现代化的 SaaS Dashboard 体验。

特性

  • 现代 UI: 基于 Vue 3 + TailwindCSS 实现的流畅响应式桌面端界面。
  • 本地存储: 使用无 CGO 依赖的纯 Go SQLite 驱动 github.com/glebarez/sqlite 本地加密存储 API Keys 等配置信息。
  • 多账户管理: 支持配置多个 Issuer ID、Key ID 和 Private Key,支持无缝切换。
  • 核心功能:
    • Bundle ID 管理: 查看现有的 App IDs
    • 证书管理 (Certificates): 查看各类证书信息
    • 描述文件管理 (Profiles): 浏览 Provisioning Profiles
    • 设备管理 (Devices): 查看和管理测试设备

技术栈

  • 后端 (Go): Go 1.21+, Wails v2, GORM, golang-jwt/jwt/v5
  • 前端 (Web): Vue 3 (Composition API), Vue Router, TailwindCSS 3
  • 数据库: SQLite (github.com/glebarez/sqlite)

API 密钥申请指南

使用本工具前,需要先在 Apple App Store Connect 中创建 API 密钥。

1. 前置条件

2. 创建 API 密钥

  1. 打开 App Store Connect → 右上角 "我的账户""API 密钥"
  2. 点击 "生成 API 密钥"
  3. 勾选 "开发人员" 权限(Developer Role),这是管理证书、描述文件等所需的最低权限
  4. 立即下载 .p8 私钥文件(页面关闭后将无法再次下载,只能重新生成)

3. 获取三个关键信息

信息 位置 说明
Issuer ID (发行者 ID) API 密钥页面顶部 发行者 ID 字段 同一账户下所有密钥共享,格式为 xxxx-xxxx-xxxx-xxxx-xxxx
Key ID (密钥 ID) 密钥列表中的 密钥 ID 每个密钥唯一,格式为 XXXXXXXXXX
Private Key (私钥) 下载的 .p8 文件内容 -----BEGIN PRIVATE KEY----- 开头,-----END PRIVATE KEY----- 结尾

4. 权限说明

API 密钥支持以下角色,本工具需要 至少 开发人员 角色

角色 可用功能
开发人员 查看和管理证书、Bundle ID、设备、描述文件
管理员 上述全部 + 管理 App、用户、财务等
财务 仅查看财务报告

5. 安全注意事项

  • 私钥文件(.p8)仅下载一次,请妥善保管
  • 建议为不同环境创建不同的 API 密钥(如开发 / 生产)
  • 可在 App Store Connect 中随时 撤销 泄露的密钥
  • 本工具将私钥存储在本地 SQLite 数据库中,不会上传到任何远程服务器
  • 导出数据时导出的 JSON 文件包含完整私钥,请勿将导出的 JSON 暴露给他人

开发指南

本项目代码包含详尽的中英文注释,方便进行二次开发和功能扩展。

1. 环境准备

  • 安装 Go 1.26+
  • 安装 Node.js 18+
  • 安装 Wails CLI:
    go install github.com/wailsapp/wails/v2/cmd/wails@latest  

2. 本地开发 (Dev Mode)

开发模式下,Wails 会启动一个本地 Web 服务器,并提供热重载 (Hot Reload) 支持。

# 确保在项目根目录运行  
wails dev

前端代码位于 frontend/ 目录中,所有的 Go 暴露方法在 frontend/wailsjs/go/main/App.js 中自动生成,可以直接在 Vue 组件中以 Promise 的方式调用。

3. 编译打包 (Build)

编译生产版本时,Wails 会将前端代码打包并嵌入到最终的二进制执行文件中。

# 编译当前平台的应用  
wails build  

# 交叉编译 macOS (如果你在 Windows/Linux 上)  
wails build -platform darwin/amd64,darwin/arm64  

# 交叉编译 Windows  
wails build -platform windows/amd64

编译成功后,产物会输出到 build/bin/ 目录下。

项目结构

  • /backend: Go 后端逻辑,包含 API 客户端和 SQLite 数据库模型。
  • /frontend: Vue 3 前端代码。
    • src/views: 各大功能模块的 Vue 页面组件。
    • src/router: 页面路由配置。
  • app.go: Wails 的主生命周期文件,包含了绑定到前端的 Go 方法。
  • main.go: Wails 应用启动入口。

License

MIT License

收起阅读 »

我是怎么实现ios内购后恢复购买的

ApplePay 苹果内购
// 在客户端app,定义一个获取历史苹果收据的方法如下:  
function getIapOrders() {  
    return new Promise(async (resolve, reject) => {  
        try {  
            // 1. 导入 iOS 原生类  
            const NSBundle = plus.ios.importClass("NSBundle");  
            const NSData = plus.ios.importClass("NSData");  
            // 2. 获取收据 URL  
            const url = NSBundle.mainBundle().appStoreReceiptURL();  
            if (!url) {  
                uni.hideLoading();  
                uni.showToast({  
                    title: '未找到收据路径',  
                    icon: 'none'  
                });  
                reject()  
                return;  
            }  
            // 3. 读取收据数据  
            const receiptData = NSData.dataWithContentsOfURL(url);  
            if (receiptData) {  
                // 4. 【核心修改】使用 plus.ios.invoke 显式调用 base64EncodedStringWithOptions: 方法  
                // 注意:方法名后面的冒号 ":" 必须保留,代表这是一个带参数的方法  
                const base64Receipt = plus.ios.invoke(receiptData, "base64EncodedStringWithOptions:", 0);  
                if (base64Receipt) {  
                    //加密过的,它是苹果官方为你在这个 App 里发生的所有成功交易出具的“电子发票”和“资产清单”。  
                    console.log("成功获取到本地收据 Base64 数据", base64Receipt);  
                    // 5. 发送给您的服务器,解密取出数据  
                    uni.vk.callFunction({  
                        url: 'client/order/pub/verifyReceipt',  
                        data: {  
                            transaction_receipt: base64Receipt  
                        },  
                        success(res) {  
                            // 返回苹果给的交易记录  
                            resolve(res.rows)  
                        },  
                        complete(res) {  

                        }  
                    });  

                } else {                      
                    uni.hideLoading();  
                    uni.showToast({  
                        title: '收据转换 Base64 失败',  
                        icon: 'none'  
                    });  
                    reject();  
                }  
            } else {  
                uni.hideLoading();  
                uni.showToast({  
                    title: '收据内容为空,请重试',  
                    icon: 'none'  
                });  
                reject()  
            }  
        } catch (e) {  
            uni.hideLoading();  
            console.error("读取收据失败: ", e);  
            uni.showToast({  
                title: '读取凭证失败: ' + e.message,  
                icon: 'none'  
            });  
            reject()  
        }  

    })  

}
'use strict';  
const uniPay = require("uni-pay");  
module.exports = {  
    /**  
     * 加密数据,它是苹果官方为你在这个 App 里发生的所有成功交易出具的“电子发票”和“资产清单”。  
     * 通过verifyReceipt数据校验后返回json交易数据  
     * @url client/order/pub/verifyReceipt 前端调用的url参数地址  
     * data 请求参数  
     * @param {String} params1  参数1  
     */  
    main: async (event) => {  
        let { data = {}, userInfo, util, filterResponse, originalParam } = event;  
        let { customUtil, uniID, config, pubFun, vk, db, _, $ } = util;  
        let { uid } = data;  
        let res = { code: 0, msg: "" };  
        // 业务逻辑开始-----------------------------------------------------------  
        // 测式时的沙箱模式下  
        let  uniPayInstance =  uniPay.initAppleIapPayment({ provider: "appleiap", provider_pay_type: "app" ,sandbox:true});  
        let tradeRes = await uniPayInstance.verifyReceipt({  
            receiptData: data.transaction_receipt  
        });  
        // console.log("************ uniPayInstance **************",JSON.stringify(tradeRes))  
        res.rows=[];  
        if(tradeRes.tradeState == "SUCCESS"){  
            /**  
             * original_purchase_date、   original_purchase_date_ms、 original_transaction_id  
             * product_id、purchase_date、purchase_date_ms  
             * quantity  
             * transaction_id  
             */  
            res.rows = tradeRes.receipt.in_app  
        }  
        debugger  
        // 业务逻辑结束-----------------------------------------------------------  
        return res;  
    }  
}

继续阅读 »
// 在客户端app,定义一个获取历史苹果收据的方法如下:  
function getIapOrders() {  
    return new Promise(async (resolve, reject) => {  
        try {  
            // 1. 导入 iOS 原生类  
            const NSBundle = plus.ios.importClass("NSBundle");  
            const NSData = plus.ios.importClass("NSData");  
            // 2. 获取收据 URL  
            const url = NSBundle.mainBundle().appStoreReceiptURL();  
            if (!url) {  
                uni.hideLoading();  
                uni.showToast({  
                    title: '未找到收据路径',  
                    icon: 'none'  
                });  
                reject()  
                return;  
            }  
            // 3. 读取收据数据  
            const receiptData = NSData.dataWithContentsOfURL(url);  
            if (receiptData) {  
                // 4. 【核心修改】使用 plus.ios.invoke 显式调用 base64EncodedStringWithOptions: 方法  
                // 注意:方法名后面的冒号 ":" 必须保留,代表这是一个带参数的方法  
                const base64Receipt = plus.ios.invoke(receiptData, "base64EncodedStringWithOptions:", 0);  
                if (base64Receipt) {  
                    //加密过的,它是苹果官方为你在这个 App 里发生的所有成功交易出具的“电子发票”和“资产清单”。  
                    console.log("成功获取到本地收据 Base64 数据", base64Receipt);  
                    // 5. 发送给您的服务器,解密取出数据  
                    uni.vk.callFunction({  
                        url: 'client/order/pub/verifyReceipt',  
                        data: {  
                            transaction_receipt: base64Receipt  
                        },  
                        success(res) {  
                            // 返回苹果给的交易记录  
                            resolve(res.rows)  
                        },  
                        complete(res) {  

                        }  
                    });  

                } else {                      
                    uni.hideLoading();  
                    uni.showToast({  
                        title: '收据转换 Base64 失败',  
                        icon: 'none'  
                    });  
                    reject();  
                }  
            } else {  
                uni.hideLoading();  
                uni.showToast({  
                    title: '收据内容为空,请重试',  
                    icon: 'none'  
                });  
                reject()  
            }  
        } catch (e) {  
            uni.hideLoading();  
            console.error("读取收据失败: ", e);  
            uni.showToast({  
                title: '读取凭证失败: ' + e.message,  
                icon: 'none'  
            });  
            reject()  
        }  

    })  

}
'use strict';  
const uniPay = require("uni-pay");  
module.exports = {  
    /**  
     * 加密数据,它是苹果官方为你在这个 App 里发生的所有成功交易出具的“电子发票”和“资产清单”。  
     * 通过verifyReceipt数据校验后返回json交易数据  
     * @url client/order/pub/verifyReceipt 前端调用的url参数地址  
     * data 请求参数  
     * @param {String} params1  参数1  
     */  
    main: async (event) => {  
        let { data = {}, userInfo, util, filterResponse, originalParam } = event;  
        let { customUtil, uniID, config, pubFun, vk, db, _, $ } = util;  
        let { uid } = data;  
        let res = { code: 0, msg: "" };  
        // 业务逻辑开始-----------------------------------------------------------  
        // 测式时的沙箱模式下  
        let  uniPayInstance =  uniPay.initAppleIapPayment({ provider: "appleiap", provider_pay_type: "app" ,sandbox:true});  
        let tradeRes = await uniPayInstance.verifyReceipt({  
            receiptData: data.transaction_receipt  
        });  
        // console.log("************ uniPayInstance **************",JSON.stringify(tradeRes))  
        res.rows=[];  
        if(tradeRes.tradeState == "SUCCESS"){  
            /**  
             * original_purchase_date、   original_purchase_date_ms、 original_transaction_id  
             * product_id、purchase_date、purchase_date_ms  
             * quantity  
             * transaction_id  
             */  
            res.rows = tradeRes.receipt.in_app  
        }  
        debugger  
        // 业务逻辑结束-----------------------------------------------------------  
        return res;  
    }  
}

收起阅读 »