# 工具箱端 - 摘要信息二维码生成指南 ## 概述 本文档说明工具箱端如何生成摘要信息二维码。工具箱完成检查任务后,需要将摘要信息加密并生成二维码,供 App 扫描后上传到平台。 > ### UX 集成模式补充(当前项目实现) > > 在当前集成模式中,工具箱将摘要明文传给 UX 的 `crypto.encryptSummary`, > 由 UX 执行 HKDF + AES-256-GCM 加密并返回二维码内容 JSON(`taskId + encrypted`)。 ## 一、业务流程 ``` 工具箱完成检查 → 准备摘要信息 → HKDF派生密钥 → AES-256-GCM加密 → 组装二维码内容 → 生成二维码 ↓ App扫描二维码 → 提取taskId和encrypted → 提交到平台 → 平台解密验证 → 保存摘要信息 ``` ## 二、二维码内容格式 二维码内容为 JSON 格式,包含以下字段: ```json { "taskId": "TASK-20260115-4875", "encrypted": "uWUcAmp6UQd0w3G3crdsd4613QCxGLoEgslgXJ4G2hQhpQdjtghtQjCBUZwB/JO+NRgH1vSTr8dqBJRq7Qh4nugESrB2jUSGASTf4+5E7cLlDOmtDw7QlqS+6Hb7sn3daMSOovcna07huchHeesrJCiHV8ntEDXdCCdQOEHfkZAvy5gS8jQY41x5Qcnmqbz3qqHTmceIihTj4uqRVyKOE8jxzY6ko76jx0gW239gyFysJUTrqSPiFAr+gToi2l9SWP8ISViBmYmCY2cQtKvPfTKXwxGMid0zE/nDmb9n38X1oR05nAP0v1KaVY7iPcjsWySDGqO2iIbPzV8tQzq5TNuYqn9gvxIX/oRTFECP+aosfmOD5I8H8rVFTebyTHw+ONV3KoN2IMRqnG+a2lucbhzwQk7/cX1hs9lYm+yapmp+0MbLCtf2KMWqJPdeZqTVZgi3R181BCxo3OIwcCFTnZ/b9pdw+q8ai6SJpso5mA0TpUCvqYlGlKMZde0nj07kmLpdAm3AOg3GtPezfJu8iHmsc4PTa8RDsPgTIxcdyxNSMqo1Ws3VLQXm6DHK/kma/vbvSA/N7upPzi7wLvboig==" } ``` ### 2.1 字段说明 | 字段名 | 类型 | 说明 | 示例 | |--------|------|------|------| | `taskId` | String | 任务ID(从任务二维码中获取) | `"TASK-20260115-4875"` | | `encrypted` | String | Base64编码的加密数据 | `"uWUcAmp6UQd0w3G3..."` | ## 三、摘要信息数据结构 ### 3.1 明文数据 JSON 格式 加密前的摘要信息为 JSON 格式,包含以下字段: ```json { "enterpriseId": "1173040813421105152", "inspectionId": "702286470691215417", "summary": "检查摘要信息", "timestamp": 1734571234567 } ``` ### 3.2 字段说明 | 字段名 | 类型 | 说明 | 示例 | |--------|------|------|------| | `enterpriseId` | String | 企业ID(从任务数据中获取) | `"1173040813421105152"` | | `inspectionId` | String | 检查ID(从任务数据中获取) | `"702286470691215417"` | | `summary` | String | 检查摘要信息 | `"检查摘要信息"` | | `timestamp` | Number | 时间戳(毫秒) | `1734571234567` | ## 四、密钥派生(HKDF-SHA256) ### 4.1 密钥派生参数 使用 **HKDF-SHA256** 从 `licence + fingerprint` 派生 AES 密钥: ``` AES Key = HKDF( input = licence + fingerprint, # 输入密钥材料(字符串拼接) salt = taskId, # Salt值(任务ID) info = "inspection_report_encryption", # Info值(固定值) hash = SHA-256, # 哈希算法 length = 32 # 输出密钥长度(32字节 = 256位) ) ``` **重要说明**: - `ikm`(输入密钥材料)= `licence + fingerprint`(直接字符串拼接,无分隔符) - `salt` = `taskId`(从任务二维码中获取的任务ID) - `info` = `"inspection_report_encryption"`(固定值,区分不同用途的密钥) - `length` = `32` 字节(AES-256 密钥长度) ### 4.2 Python 实现示例 ```python import hashlib import hkdf def derive_aes_key(licence: str, fingerprint: str, task_id: str) -> bytes: """ 使用 HKDF-SHA256 派生 AES-256 密钥 Args: licence: 设备授权码 fingerprint: 设备硬件指纹 task_id: 任务ID Returns: 派生出的密钥(32字节) """ # 输入密钥材料 ikm = licence + fingerprint # 直接字符串拼接 # HKDF 参数 salt = task_id info = "inspection_report_encryption" key_length = 32 # 32字节 = 256位 # 派生密钥 derived_key = hkdf.HKDF( algorithm=hashlib.sha256, length=key_length, salt=salt.encode('utf-8'), info=info.encode('utf-8'), ikm=ikm.encode('utf-8') ).derive() return derived_key ``` ### 4.3 Java/Kotlin 实现示例 ```kotlin import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.generators.HKDFBytesGenerator import org.bouncycastle.crypto.params.HKDFParameters import java.nio.charset.StandardCharsets fun deriveAesKey(licence: String, fingerprint: String, taskId: String): ByteArray { // 输入密钥材料 val ikm = (licence + fingerprint).toByteArray(StandardCharsets.UTF_8) // HKDF 参数 val salt = taskId.toByteArray(StandardCharsets.UTF_8) val info = "inspection_report_encryption".toByteArray(StandardCharsets.UTF_8) val keyLength = 32 // 32字节 = 256位 // 派生密钥 val hkdf = HKDFBytesGenerator(SHA256Digest()) val params = HKDFParameters(ikm, salt, info) hkdf.init(params) val derivedKey = ByteArray(keyLength) hkdf.generateBytes(derivedKey, 0, keyLength) return derivedKey } ``` ## 五、AES-256-GCM 加密 ### 5.1 加密算法 - **算法**:AES-256-GCM(Galois/Counter Mode) - **密钥长度**:256 位(32 字节) - **IV 长度**:12 字节(96 位) - **认证标签长度**:16 字节(128 位) ### 5.2 加密数据格式 加密后的数据格式(Base64 编码前): ``` [IV(12字节)] + [加密数据] + [认证标签(16字节)] ``` **数据布局**: ``` +------------------+------------------+------------------+ | IV (12字节) | 加密数据 | 认证标签(16字节)| +------------------+------------------+------------------+ ``` ### 5.3 Python 实现示例 ```python import base64 import hashlib import hkdf from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.backends import default_backend import json import time def encrypt_summary_data( enterprise_id: str, inspection_id: str, summary: str, licence: str, fingerprint: str, task_id: str ) -> str: """ 加密摘要信息数据 Args: enterprise_id: 企业ID inspection_id: 检查ID summary: 摘要信息 licence: 设备授权码 fingerprint: 设备硬件指纹 task_id: 任务ID Returns: Base64编码的加密数据 """ # 1. 组装明文数据(JSON格式) timestamp = int(time.time() * 1000) # 毫秒时间戳 plaintext_map = { "enterpriseId": str(enterprise_id), "inspectionId": str(inspection_id), "summary": summary, "timestamp": timestamp } plaintext = json.dumps(plaintext_map, ensure_ascii=False) # 2. 使用 HKDF-SHA256 派生 AES 密钥 ikm = licence + fingerprint salt = task_id info = "inspection_report_encryption" key_length = 32 aes_key = hkdf.HKDF( algorithm=hashlib.sha256, length=key_length, salt=salt.encode('utf-8'), info=info.encode('utf-8'), ikm=ikm.encode('utf-8') ).derive() # 3. 使用 AES-256-GCM 加密数据 aesgcm = AESGCM(aes_key) iv = os.urandom(12) # 生成12字节随机IV encrypted_bytes = aesgcm.encrypt(iv, plaintext.encode('utf-8'), None) # 4. 组装:IV + 加密数据(包含认证标签) # AESGCM.encrypt 返回的格式已经是:加密数据 + 认证标签 combined = iv + encrypted_bytes # 5. Base64 编码 encrypted_base64 = base64.b64encode(combined).decode('utf-8') return encrypted_base64 ``` ### 5.4 Java/Kotlin 实现示例 ```kotlin import com.fasterxml.jackson.databind.ObjectMapper import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.generators.HKDFBytesGenerator import org.bouncycastle.crypto.params.HKDFParameters import java.nio.charset.StandardCharsets import java.security.SecureRandom import java.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec object SummaryEncryptionUtil { private const val ALGORITHM = "AES" private const val TRANSFORMATION = "AES/GCM/NoPadding" private const val GCM_IV_LENGTH = 12 // 12 bytes = 96 bits private const val GCM_TAG_LENGTH = 16 // 16 bytes = 128 bits private const val GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH * 8 // 128 bits private val objectMapper = ObjectMapper() private val secureRandom = SecureRandom() /** * 加密摘要信息数据 */ fun encryptSummaryData( enterpriseId: String, inspectionId: String, summary: String, licence: String, fingerprint: String, taskId: String ): String { // 1. 组装明文数据(JSON格式) val timestamp = System.currentTimeMillis() val plaintextMap = mapOf( "enterpriseId" to enterpriseId, "inspectionId" to inspectionId, "summary" to summary, "timestamp" to timestamp ) val plaintext = objectMapper.writeValueAsString(plaintextMap) // 2. 使用 HKDF-SHA256 派生 AES 密钥 val ikm = (licence + fingerprint).toByteArray(StandardCharsets.UTF_8) val salt = taskId.toByteArray(StandardCharsets.UTF_8) val info = "inspection_report_encryption".toByteArray(StandardCharsets.UTF_8) val keyLength = 32 val hkdf = HKDFBytesGenerator(SHA256Digest()) val params = HKDFParameters(ikm, salt, info) hkdf.init(params) val aesKey = ByteArray(keyLength) hkdf.generateBytes(aesKey, 0, keyLength) // 3. 使用 AES-256-GCM 加密数据 val iv = ByteArray(GCM_IV_LENGTH) secureRandom.nextBytes(iv) val secretKey = SecretKeySpec(aesKey, ALGORITHM) val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv) val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec) val plaintextBytes = plaintext.toByteArray(StandardCharsets.UTF_8) val encryptedBytes = cipher.doFinal(plaintextBytes) // 4. 组装:IV + 加密数据(包含认证标签) // GCM 模式会将认证标签附加到密文末尾 val ciphertext = encryptedBytes.sliceArray(0 until encryptedBytes.size - GCM_TAG_LENGTH) val tag = encryptedBytes.sliceArray(encryptedBytes.size - GCM_TAG_LENGTH until encryptedBytes.size) val combined = iv + ciphertext + tag // 5. Base64 编码 return Base64.getEncoder().encodeToString(combined) } } ``` ## 六、组装二维码内容 ### 6.1 二维码内容 JSON 将 `taskId` 和加密后的 `encrypted` 组装成 JSON 格式: ```json { "taskId": "TASK-20260115-4875", "encrypted": "Base64编码的加密数据" } ``` ### 6.2 Python 实现示例 ```python import json def generate_qr_code_content(task_id: str, encrypted: str) -> str: """ 生成二维码内容(JSON格式) Args: task_id: 任务ID encrypted: Base64编码的加密数据 Returns: JSON格式的字符串 """ qr_content = { "taskId": task_id, "encrypted": encrypted } return json.dumps(qr_content, ensure_ascii=False) ``` ## 七、完整流程示例 ### 7.1 Python 完整示例 ```python import base64 import json import time import hashlib import hkdf import qrcode from cryptography.hazmat.primitives.ciphers.aead import AESGCM import os class SummaryQRCodeGenerator: """摘要信息二维码生成器""" def __init__(self, licence: str, fingerprint: str): """ 初始化生成器 Args: licence: 设备授权码 fingerprint: 设备硬件指纹 """ self.licence = licence self.fingerprint = fingerprint def generate_summary_qr_code( self, task_id: str, enterprise_id: str, inspection_id: str, summary: str, output_path: str = "summary_qr.png" ) -> str: """ 生成摘要信息二维码 Args: task_id: 任务ID(从任务二维码中获取) enterprise_id: 企业ID(从任务数据中获取) inspection_id: 检查ID(从任务数据中获取) summary: 摘要信息 output_path: 二维码图片保存路径 Returns: 二维码内容(JSON字符串) """ # 1. 组装明文数据(JSON格式) timestamp = int(time.time() * 1000) # 毫秒时间戳 plaintext_map = { "enterpriseId": str(enterprise_id), "inspectionId": str(inspection_id), "summary": summary, "timestamp": timestamp } plaintext = json.dumps(plaintext_map, ensure_ascii=False) print(f"明文数据: {plaintext}") # 2. 使用 HKDF-SHA256 派生 AES 密钥 ikm = self.licence + self.fingerprint salt = task_id info = "inspection_report_encryption" key_length = 32 aes_key = hkdf.HKDF( algorithm=hashlib.sha256, length=key_length, salt=salt.encode('utf-8'), info=info.encode('utf-8'), ikm=ikm.encode('utf-8') ).derive() print(f"密钥派生成功: {len(aes_key)} 字节") # 3. 使用 AES-256-GCM 加密数据 aesgcm = AESGCM(aes_key) iv = os.urandom(12) # 生成12字节随机IV encrypted_bytes = aesgcm.encrypt(iv, plaintext.encode('utf-8'), None) # 组装:IV + 加密数据(包含认证标签) combined = iv + encrypted_bytes # Base64 编码 encrypted_base64 = base64.b64encode(combined).decode('utf-8') print(f"加密成功: {encrypted_base64[:50]}...") # 4. 组装二维码内容(JSON格式) qr_content = { "taskId": task_id, "encrypted": encrypted_base64 } qr_content_json = json.dumps(qr_content, ensure_ascii=False) print(f"二维码内容: {qr_content_json[:100]}...") # 5. 生成二维码 qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=10, border=4, ) qr.add_data(qr_content_json) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") img.save(output_path) print(f"二维码已生成: {output_path}") return qr_content_json # 使用示例 if __name__ == "__main__": # 工具箱的授权信息(必须与平台绑定时一致) licence = "LIC-8F2A-XXXX" fingerprint = "FP-2c91e9f3" # 创建生成器 generator = SummaryQRCodeGenerator(licence, fingerprint) # 从任务二维码中获取的信息 task_id = "TASK-20260115-4875" enterprise_id = "1173040813421105152" inspection_id = "702286470691215417" summary = "检查摘要信息:发现3个高危漏洞,5个中危漏洞" # 生成二维码 qr_content = generator.generate_summary_qr_code( task_id=task_id, enterprise_id=enterprise_id, inspection_id=inspection_id, summary=summary, output_path="summary_qr_code.png" ) print(f"\n二维码内容:\n{qr_content}") ``` ### 7.2 Java/Kotlin 完整示例 ```kotlin import com.fasterxml.jackson.databind.ObjectMapper import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.generators.HKDFBytesGenerator import org.bouncycastle.crypto.params.HKDFParameters import java.awt.image.BufferedImage import java.nio.charset.StandardCharsets import java.security.SecureRandom import java.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec import javax.imageio.ImageIO import java.io.File class SummaryQRCodeGenerator( private val licence: String, private val fingerprint: String ) { private const val ALGORITHM = "AES" private const val TRANSFORMATION = "AES/GCM/NoPadding" private const val GCM_IV_LENGTH = 12 private const val GCM_TAG_LENGTH = 16 private const val GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH * 8 private val objectMapper = ObjectMapper() private val secureRandom = SecureRandom() /** * 生成摘要信息二维码 */ fun generateSummaryQRCode( taskId: String, enterpriseId: String, inspectionId: String, summary: String, outputPath: String = "summary_qr.png" ): String { // 1. 组装明文数据(JSON格式) val timestamp = System.currentTimeMillis() val plaintextMap = mapOf( "enterpriseId" to enterpriseId, "inspectionId" to inspectionId, "summary" to summary, "timestamp" to timestamp ) val plaintext = objectMapper.writeValueAsString(plaintextMap) println("明文数据: $plaintext") // 2. 使用 HKDF-SHA256 派生 AES 密钥 val ikm = (licence + fingerprint).toByteArray(StandardCharsets.UTF_8) val salt = taskId.toByteArray(StandardCharsets.UTF_8) val info = "inspection_report_encryption".toByteArray(StandardCharsets.UTF_8) val keyLength = 32 val hkdf = HKDFBytesGenerator(SHA256Digest()) val params = HKDFParameters(ikm, salt, info) hkdf.init(params) val aesKey = ByteArray(keyLength) hkdf.generateBytes(aesKey, 0, keyLength) println("密钥派生成功: ${aesKey.size} 字节") // 3. 使用 AES-256-GCM 加密数据 val iv = ByteArray(GCM_IV_LENGTH) secureRandom.nextBytes(iv) val secretKey = SecretKeySpec(aesKey, ALGORITHM) val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv) val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec) val plaintextBytes = plaintext.toByteArray(StandardCharsets.UTF_8) val encryptedBytes = cipher.doFinal(plaintextBytes) // 组装:IV + 加密数据(包含认证标签) val ciphertext = encryptedBytes.sliceArray(0 until encryptedBytes.size - GCM_TAG_LENGTH) val tag = encryptedBytes.sliceArray(encryptedBytes.size - GCM_TAG_LENGTH until encryptedBytes.size) val combined = iv + ciphertext + tag // Base64 编码 val encryptedBase64 = Base64.getEncoder().encodeToString(combined) println("加密成功: ${encryptedBase64.take(50)}...") // 4. 组装二维码内容(JSON格式) val qrContent = mapOf( "taskId" to taskId, "encrypted" to encryptedBase64 ) val qrContentJson = objectMapper.writeValueAsString(qrContent) println("二维码内容: ${qrContentJson.take(100)}...") // 5. 生成二维码 val hints = hashMapOf().apply { put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M) put(EncodeHintType.CHARACTER_SET, "UTF-8") put(EncodeHintType.MARGIN, 1) } val writer = QRCodeWriter() val bitMatrix = writer.encode(qrContentJson, BarcodeFormat.QR_CODE, 300, 300, hints) val width = bitMatrix.width val height = bitMatrix.height val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) for (x in 0 until width) { for (y in 0 until height) { image.setRGB(x, y, if (bitMatrix[x, y]) 0x000000 else 0xFFFFFF) } } ImageIO.write(image, "PNG", File(outputPath)) println("二维码已生成: $outputPath") return qrContentJson } } // 使用示例 fun main() { // 工具箱的授权信息(必须与平台绑定时一致) val licence = "LIC-8F2A-XXXX" val fingerprint = "FP-2c91e9f3" // 创建生成器 val generator = SummaryQRCodeGenerator(licence, fingerprint) // 从任务二维码中获取的信息 val taskId = "TASK-20260115-4875" val enterpriseId = "1173040813421105152" val inspectionId = "702286470691215417" val summary = "检查摘要信息:发现3个高危漏洞,5个中危漏洞" // 生成二维码 val qrContent = generator.generateSummaryQRCode( taskId = taskId, enterpriseId = enterpriseId, inspectionId = inspectionId, summary = summary, outputPath = "summary_qr_code.png" ) println("\n二维码内容:\n$qrContent") } ``` ## 八、平台端验证流程 平台端会按以下流程验证: 1. **接收请求**:App 扫描二维码后,将 `taskId` 和 `encrypted` 提交到平台 2. **查询任务**:根据 `taskId` 查询任务记录,获取 `deviceLicenceId` 3. **获取设备信息**:根据 `deviceLicenceId` 查询设备授权记录,获取 `licence` 和 `fingerprint` 4. **密钥派生**:使用相同的 HKDF 参数派生 AES 密钥 5. **解密数据**:使用 AES-256-GCM 解密(自动验证认证标签) 6. **时间戳校验**:验证 `timestamp` 是否在合理范围内(防止重放攻击) 7. **保存摘要**:将摘要信息保存到数据库 ## 九、常见错误和注意事项 ### 9.1 加密失败 **可能原因**: 1. **密钥派生错误**:确保使用正确的 HKDF 参数 - `ikm` = `licence + fingerprint`(直接字符串拼接) - `salt` = `taskId`(必须与任务二维码中的 taskId 一致) - `info` = `"inspection_report_encryption"`(固定值) - `length` = `32` 字节 2. **数据格式错误**:确保 JSON 格式正确 - 字段名和类型必须匹配 - 时间戳必须是数字类型(毫秒) 3. **IV 生成错误**:确保使用安全的随机数生成器生成 12 字节 IV ### 9.2 平台验证失败 **可能原因**: 1. **taskId 不匹配**:确保二维码中的 `taskId` 与任务二维码中的 `taskId` 一致 2. **密钥不匹配**:确保 `licence` 和 `fingerprint` 与平台绑定时一致 3. **时间戳过期**:平台会验证时间戳,确保时间戳在合理范围内 4. **认证标签验证失败**:数据被篡改或密钥错误 ### 9.3 二维码生成失败 **可能原因**: 1. **内容过长**:如果加密数据过长,可能需要更高版本的二维码 2. **JSON 格式错误**:确保 JSON 格式正确 3. **字符编码错误**:确保使用 UTF-8 编码 ## 十、安全设计说明 ### 10.1 为什么使用 HKDF 1. **密钥分离**:使用 `info` 参数区分不同用途的密钥 2. **Salt 随机性**:使用 `taskId` 作为 salt,确保每个任务的密钥不同 3. **密钥扩展**:HKDF 提供更好的密钥扩展性 ### 10.2 为什么第三方无法伪造 1. **密钥绑定**:只有拥有正确 `licence + fingerprint` 的工具箱才能生成正确的密钥 2. **任务绑定**:使用 `taskId` 作为 salt,确保密钥与特定任务绑定 3. **认证加密**:GCM 模式提供认证加密,任何篡改都会导致解密失败 4. **时间戳校验**:平台会验证时间戳,防止重放攻击 ### 10.3 密钥派生参数的重要性 - **ikm**:`licence + fingerprint` 是设备唯一标识 - **salt**:`taskId` 确保每个任务使用不同的密钥 - **info**:`"inspection_report_encryption"` 区分不同用途的密钥 - **length**:`32` 字节提供 256 位密钥强度 ## 十一、测试建议 1. **单元测试**: - 测试密钥派生是否正确 - 测试加密和解密是否匹配 - 测试 JSON 格式是否正确 2. **集成测试**: - 使用真实任务数据生成二维码 - App 扫描二维码并提交到平台 - 验证平台是否能正确解密和验证 3. **边界测试**: - 测试超长的摘要信息 - 测试特殊字符的处理 - 测试错误的 taskId 是否会导致解密失败 ## 十二、参考实现 - **Python**:`hkdf` 库(HKDF)、`cryptography` 库(AES-GCM)、`qrcode` 库(二维码生成) - **Java/Kotlin**:BouncyCastle(HKDF)、JDK `javax.crypto`(AES-GCM)、ZXing 库(二维码生成) - **C#**:BouncyCastle.Net(HKDF)、`System.Security.Cryptography`(AES-GCM)、ZXing.Net 库(二维码生成) ## 十三、联系支持 如有问题,请联系平台技术支持团队获取: - 测试环境地址 - 技术支持