25 KiB
25 KiB
工具箱端 - 摘要信息二维码生成指南
概述
本文档说明工具箱端如何生成摘要信息二维码。工具箱完成检查任务后,需要将摘要信息加密并生成二维码,供 App 扫描后上传到平台。
UX 集成模式补充(当前项目实现)
在当前集成模式中,工具箱将摘要明文传给 UX 的
crypto.encryptSummary, 由 UX 执行 HKDF + AES-256-GCM 加密并返回二维码内容 JSON(taskId + encrypted)。
一、业务流程
工具箱完成检查 → 准备摘要信息 → HKDF派生密钥 → AES-256-GCM加密 → 组装二维码内容 → 生成二维码
↓
App扫描二维码 → 提取taskId和encrypted → 提交到平台 → 平台解密验证 → 保存摘要信息
二、二维码内容格式
二维码内容为 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 格式,包含以下字段:
{
"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 实现示例
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 实现示例
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 实现示例
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 实现示例
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 格式:
{
"taskId": "TASK-20260115-4875",
"encrypted": "Base64编码的加密数据"
}
6.2 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 完整示例
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 完整示例
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<EncodeHintType, Any>().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")
}
八、平台端验证流程
平台端会按以下流程验证:
- 接收请求:App 扫描二维码后,将
taskId和encrypted提交到平台 - 查询任务:根据
taskId查询任务记录,获取deviceLicenceId - 获取设备信息:根据
deviceLicenceId查询设备授权记录,获取licence和fingerprint - 密钥派生:使用相同的 HKDF 参数派生 AES 密钥
- 解密数据:使用 AES-256-GCM 解密(自动验证认证标签)
- 时间戳校验:验证
timestamp是否在合理范围内(防止重放攻击) - 保存摘要:将摘要信息保存到数据库
九、常见错误和注意事项
9.1 加密失败
可能原因:
-
密钥派生错误:确保使用正确的 HKDF 参数
ikm=licence + fingerprint(直接字符串拼接)salt=taskId(必须与任务二维码中的 taskId 一致)info="inspection_report_encryption"(固定值)length=32字节
-
数据格式错误:确保 JSON 格式正确
- 字段名和类型必须匹配
- 时间戳必须是数字类型(毫秒)
-
IV 生成错误:确保使用安全的随机数生成器生成 12 字节 IV
9.2 平台验证失败
可能原因:
- taskId 不匹配:确保二维码中的
taskId与任务二维码中的taskId一致 - 密钥不匹配:确保
licence和fingerprint与平台绑定时一致 - 时间戳过期:平台会验证时间戳,确保时间戳在合理范围内
- 认证标签验证失败:数据被篡改或密钥错误
9.3 二维码生成失败
可能原因:
- 内容过长:如果加密数据过长,可能需要更高版本的二维码
- JSON 格式错误:确保 JSON 格式正确
- 字符编码错误:确保使用 UTF-8 编码
十、安全设计说明
10.1 为什么使用 HKDF
- 密钥分离:使用
info参数区分不同用途的密钥 - Salt 随机性:使用
taskId作为 salt,确保每个任务的密钥不同 - 密钥扩展:HKDF 提供更好的密钥扩展性
10.2 为什么第三方无法伪造
- 密钥绑定:只有拥有正确
licence + fingerprint的工具箱才能生成正确的密钥 - 任务绑定:使用
taskId作为 salt,确保密钥与特定任务绑定 - 认证加密:GCM 模式提供认证加密,任何篡改都会导致解密失败
- 时间戳校验:平台会验证时间戳,防止重放攻击
10.3 密钥派生参数的重要性
- ikm:
licence + fingerprint是设备唯一标识 - salt:
taskId确保每个任务使用不同的密钥 - info:
"inspection_report_encryption"区分不同用途的密钥 - length:
32字节提供 256 位密钥强度
十一、测试建议
-
单元测试:
- 测试密钥派生是否正确
- 测试加密和解密是否匹配
- 测试 JSON 格式是否正确
-
集成测试:
- 使用真实任务数据生成二维码
- App 扫描二维码并提交到平台
- 验证平台是否能正确解密和验证
-
边界测试:
- 测试超长的摘要信息
- 测试特殊字符的处理
- 测试错误的 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 库(二维码生成)
十三、联系支持
如有问题,请联系平台技术支持团队获取:
- 测试环境地址
- 技术支持