前言ChaCha20-Poly1305是一种结合了流加密算法（ChaCha20）和消息认证码（Poly1305）的认证加密方案，由Daniel J. Bernstein教授设计。它提供了数据的机密性、完整性和身份验证，属于AEAD（Authenticated Encryption with Associated Data）加密模式。1 算法概述与发展历史1.1 发展背景与历史提出时间：ChaCha20-Poly1305于2014年正式提出。设计动机：Google为替代RC4和AES在某些平台上的性能问题而推动采用。在Android移动平台的Chrome浏览器中首次替代RC4使用。性能优势：在ARM等精简指令集架构的移动设备上（无AES硬件加速时），性能可达AES的4倍；但在支持AES-NI的x86平台上，AES性能更优。标准化进程：2013年Google在Chrome 31中启用，2014年扩展到iOS和Android平台。IETF于2016年6月完成标准化（RFC 7905），OpenSSL从1.1.0版本（2016年8月）开始支持1.2 应用场景该算法已被广泛用于多种安全协议和系统中：TLS 1.3：作为推荐加密套件之一（TLS_CHACHA20_POLY1305_SHA256）OpenSSH：自2013年起作为默认加密算法之一QUIC协议：Google开发的下一代网络传输协议WireGuard VPN协议：核心加密组件移动设备与物联网：因高效率和抗侧信道攻击而受青睐2 算法原理与技术细节2.1 ChaCha20流密码ChaCha20是一种流密码，通过生成伪随机密钥流与明文异或实现加密。2.1.1 初始矩阵结构ChaCha20的核心是一个4×4的32位字矩阵（共512位），初始结构如下：位置内容值/说明[0]常量0x61707865 ("expa")[1]常量0x3320646e ("nd 3")[2]常量0x79622d32 ("2-by")[3]常量0x6b206574 ("te k")[4]-[7]密钥256位密钥(8个32位字)[8]-[11]计数器32位块计数器(从0开始)[12]-[15]随机数96位Nonce(每次加密唯一)完整初始矩阵布局：0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 Key[0], Key[1], Key[2], Key[3] Key[4], Key[5], Key[6], Key[7] Counter[0], Counter[1], nonce[0], nonce[1]2.1.2 轮函数运算ChaCha20进行20轮加密计算（10次双轮操作），每轮包含列轮和对角轮。# Quarter Round函数定义（核心操作） def quarter_round(a, b, c, d): a = (a + b) & 0xFFFFFFFF d ^= a d = (d << 16) | (d >> 16) # 循环左移16位 c = (c + d) & 0xFFFFFFFF b ^= c b = (b << 12) | (b >> 20) # 循环左移12位 a = (a + b) & 0xFFFFFFFF d ^= a d = (d << 8) | (d >> 24) # 循环左移8位 c = (c + d) & 0xFFFFFFFF b ^= c b = (b << 7) | (b >> 25) # 循环左移7位 return a, b, c, d轮函数执行流程：列轮运算：对矩阵的每一列应用Quarter Round对角轮运算：对对角线元素应用Quarter Round重复10次：共进行10次双轮操作（20轮）矩阵相加：将结果与初始矩阵相加，生成最终密钥流2.1.3 加密与解密过程加密和解密都是通过将生成的密钥流与数据（明文或密文）进行异或操作实现的：加密：密文 = 明文 ⊕ 密钥流解密：明文 = 密文 ⊕ 密钥流由于计数器是32位，ChaCha20最多可生成2^32个密钥流块，每个块512位，总计约256GB数据。2.2 Poly1305消息认证码Poly1305负责生成认证标签，确保数据完整性。2.2.1 算法流程密钥处理：将32字节密钥分为两部分（r和s），并进行特定处理。消息分块：将消息划分为16字节块，不足的进行填充。累加计算：使用Horner法则在有限域上进行多项式求值。生成标签：结果与s相加，取模2^128，生成16字节认证标签。2.2.2 数学公式认证标签的计算公式为：2.3 ChaCha20-Poly1305组合模式在实际应用中，两者结合使用的工作流程如下：使用ChaCha20和相同的密钥生成密钥流将密钥流与明文异或产生密文使用Poly1305计算密文和附加数据的认证标签将密文和标签一起传输接收方验证标签正确后再解密3 PHP与Java跨语言实现以下示例展示如何使用PHP进行加密，Java进行解密，确保跨语言兼容性。3.1 PHP加密实现PHP可使用Sodium扩展进行ChaCha20-Poly1305加密：<?php /** * PHP ChaCha20-Poly1305加密示例 * 需要启用Sodium扩展（PHP 7.0+） */ // 生成随机密钥（32字节） $key = sodium_crypto_aead_chacha20poly1305_keygen(); // 待加密的明文数据 $message = "需要加密的敏感数据"; // 附加数据（进行认证但不加密） $additional_data = "metadata123456"; // 12字节或自定义长度 // 生成随机Nonce（12字节） $nonce = random_bytes(SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES); try { // 加密数据 $ciphertext = sodium_crypto_aead_chacha20poly1305_encrypt( $message, $additional_data, $nonce, $key ); if ($ciphertext === false) { throw new Exception("加密失败"); } // 组合传输数据：Nonce + 密文 + 附加数据长度 $additional_data_length = pack('n', strlen($additional_data)); // 2字节长度前缀 $payload = $nonce . $additional_data_length . $additional_data . $ciphertext; // 编码为Base64便于传输 $encoded_payload = base64_encode($payload); // 输出结果（实际应用中发送给Java端） echo "加密后的数据: " . $encoded_payload . "\n"; echo "密钥(Base64): " . base64_encode($key) . "\n"; } catch (Exception $e) { echo "错误: " . $e->getMessage() . "\n"; } // 解密验证（用于测试） function chacha20_poly1305_decrypt($ciphertext, $nonce, $additional_data, $key) { return sodium_crypto_aead_chacha20poly1305_decrypt( $ciphertext, $additional_data, $nonce, $key ); } // 测试解密 $test_decrypt = chacha20_poly1305_decrypt($ciphertext, $nonce, $additional_data, $key); echo "解密验证: " . ($test_decrypt === $message ? "成功" : "失败") . "\n"; ?>3.2 Java解密实现Java可使用Bouncy Castle库进行解密：import org.bouncycastle.crypto.engines.ChaCha7539Engine; import org.bouncycastle.crypto.modes.ChaCha20Poly1305; import org.bouncycastle.crypto.params.AEADParameters; import org.bouncycastle.crypto.params.KeyParameter; import org.apache.commons.codec.binary.Base64; import java.nio.ByteBuffer; /** * Java ChaCha20-Poly1305解密示例 * 需要Bouncy Castle库依赖 */ public class ChaCha20Poly1305JavaDecrypt { public static void main(String[] args) { // PHP端生成的Base64编码数据 String encodedPayloadFromPHP = "YOUR_BASE64_ENCODED_DATA_FROM_PHP"; String base64KeyFromPHP = "YOUR_BASE64_ENCODED_KEY_FROM_PHP"; try { // Base64解码 byte[] payload = Base64.decodeBase64(encodedPayloadFromPHP); byte[] key = Base64.decodeBase64(base64KeyFromPHP); // 解析数据包结构 ByteBuffer buffer = ByteBuffer.wrap(payload); // 提取各组件（顺序与PHP加密时一致） byte[] nonce = new byte[12]; // 12字节Nonce buffer.get(nonce); // 读取附加数据长度（2字节，无符号短整型） int additionalDataLength = buffer.getShort() & 0xFFFF; byte[] additionalData = new byte[additionalDataLength]; buffer.get(additionalData); // 剩余部分是密文 byte[] ciphertext = new byte[buffer.remaining()]; buffer.get(ciphertext); // 创建解密器 ChaCha20Poly1305 cipher = new ChaCha20Poly1305(); cipher.init(false, new AEADParameters( new KeyParameter(key), 128, // 标签长度（位） nonce, additionalData )); // 解密（同时验证认证标签） byte[] plaintext = new byte[cipher.getOutputSize(ciphertext.length)]; int len = cipher.processBytes(ciphertext, 0, ciphertext.length, plaintext, 0); len += cipher.doFinal(plaintext, len); // 输出解密结果 String decryptedText = new String(plaintext, 0, len, "UTF-8"); System.out.println("解密成功: " + decryptedText); } catch (Exception e) { System.err.println("解密失败: " + e.getMessage()); e.printStackTrace(); } } /** * 使用Java 11+内置的ChaCha20-Poly1305（备选方案） */ public static void decryptWithJavaNative(byte[] ciphertext, byte[] key, byte[] nonce, byte[] associatedData) throws Exception { // Java 11+内置支持，但数据格式可能需要调整 javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance( "ChaCha20-Poly1305/None/NoPadding"); javax.crypto.spec.SecretKeySpec keySpec = new javax.crypto.spec.SecretKeySpec(key, "ChaCha20"); javax.crypto.spec.IvParameterSpec ivSpec = new javax.crypto.spec.IvParameterSpec(nonce); cipher.init(javax.crypto.Cipher.DECRYPT_MODE, keySpec, ivSpec); if (associatedData != null) { cipher.updateAAD(associatedData); } byte[] decrypted = cipher.doFinal(ciphertext); System.out.println("Java内置解密: " + new String(decrypted, "UTF-8")); } }3.3 依赖配置3.3.1 PHP依赖确保PHP环境启用Sodium扩展（PHP 7.0+默认启用）：//php.ini配置 extension=sodium3.3.2 Java依赖（Maven）<dependencies> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.78</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.16.0</version> </dependency> </dependencies>4 重要注意事项4.1 安全性考虑Nonce唯一性：每次加密必须使用不同的Nonce，否则会严重威胁安全性。密钥管理：密钥必须安全存储，建议使用硬件安全模块（HSM）或密钥管理服务。错误处理：认证失败时应立即拒绝数据，不可解密。侧信道攻击防护：ChaCha20-Poly1305天然抵抗计时攻击，但实现时仍需小心。4.2 性能优化平台选择：在无AES-NI的ARM设备上优先使用ChaCha20-Poly1305；在有硬件加速的x86服务器上AES-GCM可能更快。数据分块：对大文件可分段处理，避免内存不足。并行处理：ChaCha20支持并行计算，可利用多核优势。4.3 兼容性处理数据序列化：跨语言传输时需确保字节序、编码和数据结构一致。版本管理：双方使用相同的算法版本和参数。异常处理：完善的错误处理和日志记录。5 总结ChaCha20-Poly1305作为一种现代化的认证加密算法，结合了高效性、安全性和简洁性。其设计特别适合软件实现和移动环境，已成为TLS 1.3、QUIC等现代安全协议的核心组件。跨语言实现时，关键在于确保双方使用相同的参数（Nonce长度、标签大小等）和数据序列化格式。上述PHP和Java实现提供了一种可行的跨平台加密方案，可根据实际需求进行调整优化。随着物联网和移动计算的普及，ChaCha20-Poly1305在未来安全通信中的应用前景十分广阔，特别是在资源受限但又需要高性能加密的场景中。
0x01 前言在研究360加固方案时，我发现其采用了一种独特的保护机制：在主SO文件内部嵌套了另一个SO文件，相当于为主SO添加了一层保护壳。这种技术实现引起了我的兴趣，经过深入研究，整理出本文，分享自定义Linker的实现原理及SO加固技术0x02 Linker源码分析PS：这部分有点长，如果你只想看怎么实现的，可以直接跳到总结核心代码解析（只贴关键部分，不然太长了）do_dlopen - 一切的起点soinfo* si = find_library(name); // 查找并加载SO到内存 if (si != NULL) { si->CallConstructors(); // 调用SO的初始化函数 }看起来很简单对吧，让我们深入看看这两个函数都干了啥find_library - 负责找SO的函数static soinfo* find_library(const char* name) { soinfo* si = find_library_internal(name); // 寻找相应的SO信息 if (si != NULL) { si->ref_count++; // 这个计数很重要，用来判断SO是否已经加载过 } return si; }CallConstructors - 调用构造函数// 直接看重点 if (dynamic != NULL) { for (Elf32_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) { if (d->d_tag == DT_NEEDED) { // 先调用依赖库的构造函数 const char* library_name = strtab + d->d_un.d_val; TRACE("\"%s\": calling constructors in DT_NEEDED \"%s\"", name, library_name); find_loaded_library(library_name)->CallConstructors(); } } } // DT_INIT要在DT_INIT_ARRAY之前调用 TRACE("\"%s\": calling constructors", name); CallFunction("DT_INIT", init_func); // SO文件加壳的脱壳点！划重点！ CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);这里有个有趣的地方：DT_INIT和DT_INIT_ARRAY是很多加固方案的脱壳点，因为这是SO被加载后最早执行的地方之一find_library_internal - 真正干活的函数static soinfo* find_library_internal(const char* name) { if (name == NULL) { return somain; } soinfo* si = find_loaded_library(name); if (si != NULL) { if (si->flags & FLAG_LINKED) { return si; // 已经加载过了，直接返回 } DL_ERR("OOPS: recursive link to \"%s\"", si->name); return NULL; } TRACE("[ '%s' has not been loaded yet. Locating...]", name); si = load_library(name); // 真正加载SO文件的地方 if (si == NULL) { return NULL; } TRACE("[ init_library base=0x%08x sz=0x%08x name='%s' ]", si->base, si->size, si->name); if (!soinfo_link_image(si)) { // 执行重定位（会用mmap，所以在/proc/pid/maps能看到） munmap(reinterpret_cast<void*>(si->base), si->size); soinfo_free(si); return NULL; } return si; }关于重定位：想象一下，你的SO文件里调用了printf函数，但编译的时候并不知道printf在内存的哪个位置，所以先用个占位符（比如call 0x1234）。重定位就是把这个占位符改成真实的地址基本流程梳理：find_loaded_library()：先看看是不是已经加载过了load_library()：没加载过，那就加载soinfo_link_image()：处理各种动态信息，执行重定位load_library - 真正的加载器static soinfo* load_library(const char* name) { // 打开文件 int fd = open_library(name); if (fd == -1) { DL_ERR("library \"%s\" not found", name); return NULL; } // 读取ELF头并加载段 ElfReader elf_reader(name, fd); if (!elf_reader.Load()) { // 注意：这里只读Program段！ return NULL; } const char* bname = strrchr(name, '/'); // 为SO分配soinfo结构 soinfo* si = soinfo_alloc(bname ? bname + 1 : name); if (si == NULL) { return NULL; } // 初始化soinfo对象 si->base = elf_reader.load_start(); si->size = elf_reader.load_size(); si->load_bias = elf_reader.load_bias(); si->flags = 0; si->entry = 0; si->dynamic = NULL; si->phnum = elf_reader.phdr_count(); si->phdr = elf_reader.loaded_phdr(); return si; }ElfReader::Load()bool ElfReader::Load() { return ReadElfHeader() && // 读ELF头 VerifyElfHeader() && // 校验ELF头 ReadProgramHeader() && // 读程序头 ReserveAddressSpace() && // 分配内存空间 LoadSegments() && // 加载段（脱壳的好地方） FindPhdr(); // 设置加载地址 }发现：Android只读Program Header，而IDA依赖Section Header！这就是为什么很多加固会”抹头”——把Section Header搞坏，IDA就懵了，但Android照样能跑soinfo_link_image - 链接的核心在si = load_library(name)拿到SO信息后，就要开始一系列复杂操作了：1、定位动态节phdr_table_get_dynamic_section(const Elf32_Phdr* phdr_table, int phdr_count, Elf32_Addr load_bias, Elf32_Dyn** dynamic, size_t* dynamic_count, Elf32_Word* dynamic_flags) { const Elf32_Phdr* phdr = phdr_table; const Elf32_Phdr* phdr_limit = phdr + phdr_count; for (phdr = phdr_table; phdr < phdr_limit; phdr++) { if (phdr->p_type != PT_DYNAMIC) { // 找DYNAMIC段 continue; } *dynamic = reinterpret_cast<Elf32_Dyn*>(load_bias + phdr->p_vaddr); if (dynamic_count) { *dynamic_count = (unsigned)(phdr->p_memsz / 8); } if (dynamic_flags) { *dynamic_flags = phdr->p_flags; } return; // 注意：找到第一个就返回了！ } *dynamic = NULL; if (dynamic_count) { *dynamic_count = 0; } }又一个有趣的发现：源码只处理第一个PT_DYNAMIC段！所以你可以加多个动态节来迷惑IDA，因为IDA会全部解析，而Android只看第一个。这算是个小技巧吧~2、解析动态节这部分代码太长了，主要就是初始化各种表：符号表、字符串表、重定位表等等。3、加载依赖库// 加载依赖库 (DT_NEEDED) for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) { if (d->d_tag == DT_NEEDED) { const char* library_name = si->strtab + d->d_un.d_val; DEBUG("%s needs %s", si->name, library_name); soinfo* lsi = find_library(library_name); // 递归加载依赖 if (lsi == NULL) { strlcpy(tmp_err_buf, linker_get_error_buffer(), sizeof(tmp_err_buf)); DL_ERR("could not load library \"%s\" needed by \"%s\"; caused by %s", library_name, si->name, tmp_err_buf); return false; } *pneeded++ = lsi; } }4、重定位操作if (si->has_text_relocations) { // 让代码段可写，这样才能修改 DL_WARN("%s has text relocations. This is wasting memory and is " "a security risk. Please fix.", si->name); if (phdr_table_unprotect_segments(si->phdr, si->phnum, si->load_bias) < 0) { DL_ERR("can't unprotect loadable segments for \"%s\": %s", si->name, strerror(errno)); return false; } } if (si->plt_rel != NULL) { DEBUG("[ relocating %s plt ]", si->name ); if (soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed)) { return false; } } if (si->rel != NULL) { DEBUG("[ relocating %s ]", si->name ); if (soinfo_relocate(si, si->rel, si->rel_count, needed)) { return false; } }小结一下说白了，Linker就是个搬运工，把磁盘上的SO文件按照一定的规则搬到内存里，让我们的程序能够调用。主要干这么几件事：加载ELF文件用mmap把SO文件映射到内存（就像把文件”贴”到内存里）解析各种头部信息给.bss、.data这些段分配空间处理依赖关系看看这个SO依赖哪些其他的SO把依赖的SO也加载进来（递归的过程）建立依赖关系图，避免重复加载符号解析和重定位找到导出的函数和变量修正函数调用地址处理各种符号冲突执行初始化按正确的顺序调用各个构造函数0x03 自定义Linker实现基于对Android Linker的理解，实现自定义Linker无需像AOSP源码那样考虑所有情况。我们可以专注于核心功能，实现一个精简版本详细实现可参考自实现Linker加载我实现的自定义Linker项目：soLoader架构设计组件关系图：核心组件职责：组件名称主要功能关键操作ElfLoader系统协调器和公共APILoadLibrary(), GetSymbol(), CallConstructors()ElfReaderELF文件解析与验证读取头部、提取程序头、验证格式MemoryManager内存分配与保护mmap()操作、段加载、权限设置SoinfoManager共享对象元数据管理soinfo结构创建、依赖跟踪Relocator符号解析与重定位处理重定位、解析符号、处理PLT/GOTUtils系统工具与内省系统调用、ELF结构辅助函数具体实现原理请参考项目源码0x04 SO加固实现加固方案设计以360加固为例，其核心思想是将主SO加密后嵌入壳SO中。壳SO作为loader被原生Linker加载后，负责解密、映射、链接、重定位主SO，最终调用主SO的函数来释放DEX我们自己在设计的时候可以直接照搬这种模式，但是把两个so合并的操作有些麻烦，而且主SO本身就是被加密的，我个人觉得放在哪里都无所谓，你要想解密都得去分析壳SO，所以这里我实现了一个简化的版本，把主SO加密之后藏在了图片的后面，这样看上去它就是一张普通的图片为简化实现，本方案采用以下设计：不将两个SO合并，而是将加密的主SO隐藏在APK资源中选择res/mipmap/ic_launcher.webp（应用图标）作为载体使用RC4算法加密主SO，并添加魔数标识便于定位加密实现加密脚本的核心功能：#!/usr/bin/env python3 """ python prepare_hidden_so.py <so_file> <webp_image> <output_image> <key> """ import sys import os import struct MAGIC_HEADER = b"Yuuki" def rc4_init(key): """初始化RC4 S盒""" S = list(range(256)) j = 0 key_len = len(key) for i in range(256): j = (j + S[i] + key[i % key_len]) & 0xFF S[i], S[j] = S[j], S[i] return S def rc4_crypt(S, data): """RC4加密/解密""" S = S.copy() i = j = 0 result = bytearray() for byte in data: i = (i + 1) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] k = S[(S[i] + S[j]) & 0xFF] result.append(byte ^ k) return bytes(result) def main(): # 参数验证 if len(sys.argv) != 5: print(f"Usage: {sys.argv[0]} <so_file> <webp_image> <output_image> <key>") return 1 so_file = sys.argv[1] webp_image = sys.argv[2] output_image = sys.argv[3] key = sys.argv[4].encode('utf-8') with open(so_file, 'rb') as f: so_data = f.read() with open(webp_image, 'rb') as f: webp_data = f.read() # RC4加密 S = rc4_init(key) encrypted_so = rc4_crypt(S, so_data) S_verify = rc4_init(key) decrypted = rc4_crypt(S_verify, encrypted_so) if decrypted != so_data: print("Error: Encryption verification failed!") return 1 with open(output_image, 'wb') as f: f.write(webp_data) # 原始WebP数据 f.write(MAGIC_HEADER) # 魔数标识 f.write(struct.pack('<I', len(so_data))) # 原始SO大小 f.write(encrypted_so) # 加密的SO数据 print(f"加密完成！隐藏数据起始偏移: {len(webp_data)}") return 0 if __name__ == "__main__": sys.exit(main())解密流程完整的解密加载流程：资源提取（Java层）内存映射与解密（Native层）加载执行（自定义Linker）1. Java层资源提取public static String getExtractedImagePath(Context context) { File tempDir = new File(context.getCacheDir(), "yuuki_temp"); if (!tempDir.exists()) { tempDir.mkdirs(); } return new File(tempDir, "temp_image.webp").getAbsolutePath(); } public static boolean extractImageFromApk(Context context, String outputPath) { String apkPath = context.getApplicationInfo().sourceDir; try (ZipInputStream zip = new ZipInputStream(new FileInputStream(apkPath))) { ZipEntry entry; while ((entry = zip.getNextEntry()) != null) { if (entry.getName().equals("res/mipmap-xxxhdpi-v4/ic_launcher.webp")) { File outFile = new File(outputPath); File parent = outFile.getParentFile(); if (!parent.exists()) { parent.mkdirs(); } try (FileOutputStream out = new FileOutputStream(outFile)) { byte[] buffer = new byte[1024]; int len; while ((len = zip.read(buffer)) > 0) { out.write(buffer, 0, len); } return true; } } zip.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } return false; }2. Native层解密实现在ElfReader中添加解密功能：bool ElfReader::HandleFileType() { // 检查文件是否包含隐藏的SO if (CryptoUtils::HasHiddenSO(mapped_file_, file_size_)) { LOGI("检测到隐藏的SO文件，开始提取并解密..."); void* decrypted_data = nullptr; size_t decrypted_size = 0; if (!CryptoUtils::ExtractAndDecryptHiddenSO(mapped_file_, file_size_, &decrypted_data, &decrypted_size)) { LOGE("提取和解密隐藏的SO失败"); return false; } // 替换映射数据为解密后的数据 decrypted_buffer_ = decrypted_data; is_decrypted_ = true; // 释放原始映射，使用解密缓冲区 munmap(mapped_file_, file_size_); mapped_file_ = decrypted_buffer_; file_size_ = decrypted_size; LOGI("隐藏SO提取和解密成功，大小: %zu", decrypted_size); } return true; }解密工具类的核心实现包括：魔数头定位RC4解密算法ELF格式验证内存管理这样，自定义Linker就能透明地加载加密的SO文件，实现了简单的SO加固保护0x05 总结本文分析了Android Linker的工作原理，并基于此实现了自定义Linker和SO加固方案。整个项目涉及很多内存操作和底层知识，还是从中学到了很多东西的OvO。希望这篇文章对想了解Linker的同学有所帮助。如果有什么问题或建议，欢迎交流
以下用3D MAX 2014版本作为示例带有动画的模型需要以omod文件格式导入到迷你世界中。1.解压“2014max插件”，将插件里的文件复制到3DMax2014安装目录中。2.在Max中运行脚本MiniAnimationExporter.ms（文件存在于3DMax2014安装目录下Scripts\OgreMaxScripts）3.将插件添加到工具栏。打开“自定义用户界面”，选择“工具栏”，在类别中找到Mini Animation Tools，在下面会显示Mini Animation Manager，用鼠标选择并把它拖拽至工具栏任意位置即可，点击即可打开插件工具。4.注意：如果步骤3中找不到Mini Animation Tools，可以将“mini_tools_enc.ms”文件直接拖入Max中，该脚本运行后会弹出“Mini工具”的窗口，在窗口中选择MiniAnimationManager即可。5.成功打开插件后，你已离成功不远了。你可以开始在3DMax里面制作模型、动画和贴图了。6.注意：模型对象的名称要与贴图名称一致，且要命名为body0。7.如果你的模型中有子模型，那则需要依次命名为body0、body1。注意：名称中的数字一定要按顺序，不可以跳过，比如body0、body5是错误的。8.在模型旁边添加一个命名为“aabb”的模型，并将“aabb”尽量完全包裹模型，随后将其隐藏即可。9.导出omod文件。首先你需要在桌面创建两个文件夹(数字或英文命名)，这里以创建A文件夹和B文件夹为例。A文件夹用于存放Max源文件和贴图源文件，B文件夹用于存放导出的omod文件和贴图文件。10.打开MiniAnimationManager工具。（1)选择要导出的对象。如果有多个骨骼组合，则需要选择骨骼的父节点。（2)设置Start frame和end frame，控制动画的帧数。注意结束帧要大于开始帧。（3)填写动作的ID，这里填写的ID对应迷你世界动作ID。（4)如果有多个动作需要添加，点击Append New Sequence按钮添加新设置新动作的帧数，重复（2)、（3)步。11.完成MiniAnimationManager的设置后，退出插件工具，点击MAX菜单->导出omod文件。注意：omod导出的位置是步骤8中创建的B文件夹，文件的命名一定是body（不是body0）。12.导出配置。（1)选择动画模型（2)动画帧的开始到结束设置必须覆盖整个动作时间段（3)源文件根目录和目标文件根目录分别选择，步骤8中创建的A和B文件夹。（4)点击OK完成导出13.打包。打开B文件夹，将动画集omod和贴图文件进行打包，压缩包的名字可以随意。打包后注意删除本地对应的文件，避免导出下一次导出模型时，因为重命名而导出失败。
1.什么是javaScriptjavaScript是一门世界上最流行的脚本语言!JavaScript（简称“JS”）是一种具有函数优先的轻量级，解释型或即时编译型的编程语言。虽然它是作为开发Web页面的脚本语言而出名，但是它也被用到了很多非浏览器环境中，JavaScript基于原型编程、多范式的动态脚本语言，并且支持面向对象、命令式、声明式、函数式编程范式。javaScript的历史故事:值得一提的是:javaScript脚本语言是Brendan Eich只花了10天时间制作而成的! web history-----JavaScript 的起源故事_weixin_30883271的博客-CSDN博客ECMAScript可以理解为javaScript的一种标准! javaScript的最新版本已经到9了2.javaScript的引入方式html内部标签
前言对于大佬们来说，CSS一点都不难，轻轻松松的就记了下来；但对于新手小白来说，CSS太多了，真的是太难了。于是就整理了一些CSS样式方便使用😇。CSS字体属性：(font)大小 {font-size: x-large;}(特大) xx-small;(极小) 一般中文用不到，只要用数值就可以，单位：PX、PD样式 {font-style: oblique;}(偏斜体) italic;(斜体) normal;(正常)行高 {line-height: normal;}(正常) 单位：PX、PD、EM粗细 {font-weight: bold;}(粗体) lighter;(细体) normal;(正常)变体 {font-variant: small-caps;}(小型大写字母) normal;(正常)大小写 {text-transform: capitalize;}(首字母大写) uppercase;(大写) lowercase;(小写) none;(无)修饰 {text-decoration: underline;}(下划线) overline;(上划线) line-through;(删除线) blink;(闪烁)常用字体： (font-family)“Courier New”, Courier, monospace, “Times New Roman”, Times, serif, Arial, Helvetica, sans-serif, Verdana背景属性： (background)色彩 {background-color: #FFFFFF;}图片 {background-image: url();}重复 {background-repeat: no-repeat;}滚动 {background-attachment: fixed;}(固定) scroll;(滚动)位置 {background-position: left;}(水平) top(垂直);简写方法 {background:#000 url(..) repeat fixed left top;} /简写·这个在阅读代码中经常出现，要认真的研究/区块属性： (Block)字间距 {letter-spacing: normal;} 数值 /这个属性似乎有用，多实践下/对齐 {text-align: justify;}(两端对齐) left;(左对齐) right;(右对齐) center;(居中)缩进 {text-indent: 数值px;}垂直对齐 {vertical-align: baseline;}(基线) sub;(下标) super;(下标) top; text-top; middle; bottom; text-bottom;词间距word-spacing: normal; 数值空格white-space: pre;(保留) nowrap;(不换行)显示 {display:block;}(块) inline;(内嵌) list-item;(列表项) run-in;(追加部分) compact;(紧凑) marker;(标记) table; inline-table; table-raw-group; table-header-group; table-footer-group; table-raw; table-column-group; table-column; table-cell; table-caption;(表格标题) /display 属性的了解很模糊/方框属性： (Box)width:; height:; float:; clear:both; margin:; padding:; 顺序：上右下左边框属性： (Border)border-style: dotted;(点线) dashed;(虚线) solid; double;(双线) groove;(槽线) ridge;(脊状) inset;(凹陷) outset;border-width:; 边框宽度border-color:#;简写方法border：width style color; /简写/列表属性： (List-style)类型list-style-type: disc;(圆点) circle;(圆圈) square;(方块) decimal;(数字) lower-roman;(小罗码数字) upper-roman; lower-alpha; upper-alpha;位置list-style-position: outside;(外) inside;图像list-style-image: url(..);定位属性： (Position)Position: absolute; relative; static;visibility: inherit; visible; hidden;overflow: visible; hidden; scroll; auto;clip: rect(12px,auto,12px,auto) (裁切)css属性代码大全一、CSS文字属性color : #999999; /文字颜色/font-family : 宋体,sans-serif; /文字字体/font-size : 9pt; /文字大小/font-style:itelic; /文字斜体/font-variant:small-caps; /小字体/letter-spacing : 1pt; /字间距离/line-height : 200%; /设置行高/font-weight:bold; /文字粗体/vertical-align:sub; /下标字/vertical-align:super; /上标字/text-decoration:line-through; /加删除线/text-decoration: overline; /加顶线/text-decoration:underline; /加下划线/text-decoration:none; /删除链接下划线/text-transform : capitalize; /首字大写/text-transform : uppercase; /英文大写/text-transform : lowercase; /英文小写/text-align:right; /文字右对齐/text-align:left; /文字左对齐/text-align:center; /文字居中对齐/text-align:justify; /文字分散对齐/vertical-align属性vertical-align:top; /垂直向上对齐/vertical-align:bottom; /垂直向下对齐/vertical-align:middle; /垂直居中对齐/vertical-align:text-top; /文字垂直向上对齐/vertical-align:text-bottom; /文字垂直向下对齐/二、CSS边框空白padding-top:10px; /上边框留空白/padding-right:10px; /右边框留空白/padding-bottom:10px; /下边框留空白/padding-left:10px; /*左边框留空白三、CSS符号属性list-style-type:none; /不编号/list-style-type:decimal; /阿拉伯数字/list-style-type:lower-roman; /小写罗马数字/list-style-type:upper-roman; /大写罗马数字/list-style-type:lower-alpha; /小写英文字母/list-style-type:upper-alpha; /大写英文字母/list-style-type:disc; /实心圆形符号/list-style-type:circle; /空心圆形符号/list-style-type:square; /实心方形符号/list-style-image:url(/dot.gif); /图片式符号/list-style-position: outside; /凸排/list-style-position:inside; /缩进/四、CSS背景样式background-color:#F5E2EC; /背景颜色/background:transparent; /透视背景/background-image : url(/image/bg.gif); /背景图片/background-attachment : fixed; /浮水印固定背景/background-repeat : repeat; /重复排列-网页默认/background-repeat : no-repeat; /不重复排列/background-repeat : repeat-x; /在x轴重复排列/background-repeat : repeat-y; /在y轴重复排列/指定背景位置background-position : 90% 90%; /背景图片x与y轴的位置/background-position : top; /向上对齐/background-position : buttom; /向下对齐/background-position : left; /向左对齐/background-position : right; /向右对齐/background-position : center; /居中对齐/五、CSS连接属性a /所有超链接/a:link /超链接文字格式/a:visited /浏览过的链接文字格式/a:active /按下链接的格式/a:hover /鼠标转到链接/鼠标光标样式：链接手指 CURSOR: hand十字体 cursor:crosshair箭头朝下 cursor:s-resize十字箭头 cursor:move箭头朝右 cursor:move加一问号 cursor:help箭头朝左 cursor:w-resize箭头朝上 cursor:n-resize箭头朝右上 cursor:ne-resize箭头朝左上 cursor:nw-resize文字I型 cursor:text箭头斜右下 cursor:se-resize箭头斜左下 cursor:sw-resize漏斗 cursor:wait光标图案(IE6) p {cursor:url(“光标文件名.cur”),text;}六、CSS框线一览表border-top : 1px solid #6699cc; /上框线/border-bottom : 1px solid #6699cc; /下框线/border-left : 1px solid #6699cc; /左框线/border-right : 1px solid #6699cc; /右框线/以上是建议书写方式,但也可以使用常规的方式 如下:border-top-color : #369 /设置上框线top颜色/border-top-width :1px /设置上框线top宽度/border-top-style : solid/设置上框线top样式/其他框线样式solid /实线框/dotted /虚线框/double /双线框/groove /立体内凸框/ridge /立体浮雕框/inset /凹框/outset /凸框/七、CSS表单运用文字方块按钮复选框选择钮多行文字方块下拉式菜单 选项1选项2八、CSS边界样式margin-top:10px; /上边界/margin-right:10px; /右边界值/margin-bottom:10px; /下边界值/margin-left:10px; /左边界值/CSS 属性： 字体样式(Font Style)序号 中文说明 标记语法1 字体样式 {font:font-style font-variant font-weight font-size font-family}2 字体类型 {font-family:”字体1”,”字体2”,”字体3”,…}3 字体大小 {font-size:数值|inherit| medium| large| larger| x-large| xx-large| small| smaller| x-small| xx-small}4 字体风格 {font-style:inherit|italic|normal|oblique}5 字体粗细 {font-weight:100-900|bold|bolder|lighter|normal;}6 字体颜色 {color:数值;}7 阴影颜色 {text-shadow:16位色值}8 字体行高 {line-height:数值|inherit|normal;}9 字 间 距 {letter-spacing:数值|inherit|normal}10 单词间距 {word-spacing:数值|inherit|normal}11 字体变形 {font-variant:inherit|normal|small-cps }12 英文转换 {text-transform:inherit|none|capitalize|uppercase|lowercase}13 字体变形 {font-size-adjust:inherit|none}14 字体 {font-stretch:condensed|expanded|extra-condensed|extra-expanded|inherit|narrower|normal| semi-condensed|semi-expanded|ultra-condensed|ultra-expanded|wider}文本样式(Text Style)序号 中文说明 标记语法1 行 间 距 {line-height:数值|inherit|normal;}2 文本修饰 {text-decoration:inherit|none|underline|overline|line-through|blink}3 段首空格 {text-indent:数值|inherit}4 水平对齐 {text-align:left|right|center|justify}5 垂直对齐 {vertical-align:inherit|top|bottom|text-top|text-bottom|baseline|middle|sub|super}6 书写方式 {writing-mode:lr-tb|tb-rl}背景样式序号 中文说明 标记语法1 背景颜色 {background-color:数值}2 背景图片 {background-image: url(URL)|none}3 背景重复 {background-repeat:inherit|no-repeat|repeat|repeat-x|repeat-y}4 背景固定 {background-attachment:fixed|scroll}5 背景定位 {background-position:数值|top|bottom|left|right|center}6 背影样式 {background:背景颜色|背景图象|背景重复|背景附件|背景位置}框架样式(Box Style)序号 中文说明 标记语法1 边界留白 {margin:margin-top margin-right margin-bottom margin-left}2 补 白 {padding:padding-top padding-right padding-bottom padding-left}3 边框宽度 {border-width:border-top-width border-right-width border-bottom-width border-left-width} 宽度值： thin|medium|thick|数值4 边框颜色 {border-color:数值 数值 数值 数值} 数值：分别代表top、right、bottom、left颜色值5 边框风格 {border-style:none|hidden|inherit|dashed|solid|double|inset|outset|ridge|groove}6 边 框 {border:border-width border-style color}上 边 框 {border-top:border-top-width border-style color}右 边 框 {border-right:border-right-width border-style color}下 边 框 {border-bottom:border-bottom-width border-style color}左 边 框 {border-left:border-left-width border-style color}7 宽 度 {width:长度|百分比| auto}8 高 度 {height:数值|auto}9 漂 浮 {float:left|right|none}10 清 除 {clear:none|left|right|both}分类列表序号 中文说明 标记语法1 控制显示 {display:none|block|inline|list-item}2 控制空白 {white-space:normal|pre|nowarp}3 符号列表 {list-style-type:disc|circle|square|decimal|lower-roman|upper-roman|lower-alpha|upper-alpha|none}4 图形列表 {list-style-image:URL}5 位置列表 {list-style-position:inside|outside}6 目录列表 {list-style:目录样式类型|目录样式位置|url}7 鼠标形状 {cursor:hand|crosshair|text|wait|move|help|e-resize|nw-resize|w-resize|s-resize|se-resize|sw-resize}
思路❶首先定义一个瀑布流容器，它的高度暂定（后面会更新）。把需要布局的组件（这里叫做waterfall-item）放在瀑布流容器里面渲染出来。使用绝对定位（position: absolute），把它移到屏幕外面，不要占用页面高度，并且设置不可见（visibility:hidden）。 <PostCard :title="post.title" :content="post.excerpt" :time="new Date(post.date).toLocaleDateString()" :tag="post.tags ? post.tags.join(', ') : '未分类'" :img="post.img" :path="`/post/${post.file}`" :id="post.title" @imageLoaded="onImageLoaded" /> .waterfall-item { position: absolute; left: 0; top: 0; transform: translate3d(0, 3000px, 0); visibility: hidden; /* 让容器不被空白占位的 trick */ }❷渲染出来之后才能计算高度。获取.waterfall-item的dom元素，遍历这些元素，使用getBoundingClientRect()获取高度。❸开始布局。以2列为例，新建两个变量分别表示2列的高度。遍历第2步获取的dom元素的高度，把dom元素的高度加到最小的高度上。这个过程还可以考虑在组件之间加上留白（gutter）。❹使用transform样式将waterfall-item移动到对应高度所在的位置。transform将组件移动到指定坐标，横坐标跟列有关，纵坐标跟高度有关。❺更新瀑布流容器的高度为两列高度中最大者。// 瀑布流容器引用 const waterfallWrapper = ref<HTMLElement | null>(null); // 瀑布流容器高度 const wrapperHeight = ref(0); // 一些瀑布流配置属性 const gutter = 20; // 卡片之间的间距，单位px const cols = ref(2); // 列数（可根据响应式需求调整） const colWidth = ref(0); // 每列宽度，或你可以动态计算 const hasAroundGutter = ref(true); const animationCancel = ref(false); // 是否取消动画 const posDuration = ref(300); // 位置动画时长 (ms) /** 2、3、4、5步的布局函数 */ const layoutHandle = async (): Promise<boolean> => { return new Promise((resolve) => { // 初始化 posY initY(); // 获取 .waterfall-item DOM 列表 const items: HTMLElement[] = []; if (waterfallWrapper.value) { waterfallWrapper.value.childNodes.forEach((el: any) => { if (el.className === 'waterfall-item') items.push(el); }); } if (!items.length) return false; // 遍历每个卡片 for (let i = 0; i < items.length; i++) { const curItem = items[i] as HTMLElement; const style = curItem.style; // 最小列 const minY = Math.min(...posY.value); const minYIndex = posY.value.indexOf(minY); // 计算 X const curX = getX(minYIndex); // 设置 transform style.transform = `translate3d(${Math.floor(curX)}px, ${Math.floor(minY)}px, 0)`; style.width = `${colWidth.value}px`; style.visibility = 'visible'; // 测量高度 const { height } = curItem.getBoundingClientRect(); // 输出卡片标题和高度 // console.log(`Card "${i}" height: ${height}`); // 更新列高 posY.value[minYIndex] += height; // 入场动画（可选） if (!animationCancel.value) { addAnimation(curItem, () => { const time = posDuration.value / 1000; style.transition = `transform ${time}s`; }); } } // 容器高度 = 最长列 wrapperHeight.value = Math.max(...posY.value); // 等待动画结束 setTimeout(() => { resolve(true); }, posDuration.value); }); }; /** 初始化 posY */ const initY = () => { posY.value = new Array(cols.value).fill(hasAroundGutter.value ? gutter : 0); }; /** 计算给定列的 X 坐标 */ const getX = (index: number): number => { const count = hasAroundGutter.value ? index + 1 : index; return gutter * count + colWidth.value * index; }; /** 简单的动画函数，给卡片添加class或行内属性 */ function addAnimation(item: HTMLElement, callback?: () => void) { // 也可以从 item.firstChild 取到实际卡片 DOM // 并添加动画class // 这里为简单演示 const content = item.firstChild as HTMLElement; if (content) { // 添加一系列动画class/属性 content.classList.add('animate__animated', 'animate__fadeIn'); // etc. // 回调 if (callback) { setTimeout(() => { callback(); }, 300); // 300ms or 你自己的计算 } } }细节🔴实际场景中图片一般使用懒加载，这会导致在组件计算高度时因为图片还没有被加载出来，所以得到错误的高度。这时候就需要在组件里添加一个onImageLoaded函数，当图片加载完成后调用，重新布局。// 图片加载完成后再次布局（可加防抖） let timer: number | null = null; function onImageLoaded() { if (timer) clearTimeout(timer); timer = setTimeout(() => { layoutHandle(); // 重新布局 }, 100); // 防抖 100ms }🔴屏幕尺寸变化时重新布局。设置两个监听器监听屏幕尺寸变化，当屏幕尺寸改变了之后，一个触发宽度重新设置，第二个触发重新布局。// 定义函数: colWidth = containerWidth - 3 * gutter function updateColWidth() { const container = waterfallWrapper.value; if (!container) return; // 父容器的实际宽度 const containerWidth = container.clientWidth; // 计算后赋值 colWidth.value = (containerWidth - 3 * gutter)/2; console.log('containerWidth:', containerWidth); console.log('colWidth:', colWidth.value); } onMounted(() => { updateColWidth(); window.addEventListener('resize', updateColWidth); window.addEventListener('resize', layoutHandle); });🔴防抖，意思就是防止频繁触发布局造成性能爆炸。这个问题可能会在图片非常多，或者屏幕宽度频繁变化时出现。解决思路很简单，就是在所有布局操作前加个定时器，使得在规定时间内只能触发有限次数。代码在细节1有体现。代码参考：https://github.com/heikaimu/vue3-waterfall-plugin感谢作者开源。
简介 ClawCloud Run 是一个集成在线开发、测试和生产环境的云开发平台。只需单击几下，即可设置开发环境和数据库依赖项，从而显著降低复杂性。Free Plan 免费计划 对于每一个新注册的用户，ClawCloud Run 将提供 5 美元的优惠。此赠品的有效期为一个月。如果您在注册后一个月内绑定 GitHub 账户，则此后每个月都会额外获得 5 美元，而这份后续礼物的有效期也是一个月。如果你的 GitHub 账户超过 180 天，建议直接使用 GitHub 注册，免去绑定的步骤。免费的有Up to 4 vCPU / 8GiB RAM / 10GiB disk per region/10G traffic，个人使用基本差不多够了。注册地址：https://console.run.claw.cloud/signin?link=PVG759MZBNEN也就是说，可以实现永久免费，我这里搭建一个图床试试。另外我搜索了一下，WordPress网站可以直接创建，Typecho也可以去试试。创建容器点击App Launchpad，然后Creat APP，打开如下界面。容器设置我这里设置的是easyimage的图床，在Image Name这里输入ddsderek/easyimage:latest即可，接着设置自己需要的CPU和内存。自定义域名最后，还可以设置自己的域名，选择https的方式，自定义域名在创建容器的时候就先设置好。提交部署即可，一会儿图床就搭建好了，用你的自定义域名访问，安装图床即可。重要说明:使用自定义域名，一定要创建容器的时候就用自定义域名，不然他给的随机域名再修改是会变。注册地址https://console.run.claw.cloud/signin?link=PVG759MZBNEN搭建演示我用这个搭建了一个图床 大家可以免费使用图床连接:http://iii.0s.ink
API请求HTTP动词GET: 读取（READ） POST: 新建（CREATE） PUT: 更新（UPDATE） DELETE: 删除（DELETE）[card title="" color="primary"]PATCH 部分更新[/card]URL宾语宾语 顾名思义，是一个名词， URL 作为 API 的宾语是作用 HTTP 的对象，普遍以复数形式存在. 以下为错误示例：/getAllCars /createNewCar /deleteAllRedCars正确示例：/aticles /users /cars煮个栗子GET /zoos：列出所有动物园 POST /zoos：新建一个动物园 GET /zoos/ID：获取某个指定动物园的信息 PUT /zoos/ID：更新某个指定动物园的信息（提供该动物园的全部信息） PATCH /zoos/ID：更新某个指定动物园的信息（提供该动物园的部分信息） DELETE /zoos/ID：删除某个动物园 GET /zoos/ID/animals：列出某个指定动物园的所有动物 DELETE /zoos/ID/animals/ID：删除某个指定动物园的指定动物过滤（filter）通常在数据库中存储着许多数据，我们不可能将所有数据返回给用户，而是选择性的将一些数据返回给用户，而 API 就应该提供一些参数，过滤返回的结果。下面是一些常见的参数。?limit=10：指定返回记录的数量 ?offset=10：指定返回记录的开始位置。 ?page=2&per_page=100：指定第几页，以及每页的记录数。 ?sortby=name&order=asc：指定返回结果按照哪个属性排序，以及排序顺序。 ?animal_type_id=1：指定筛选条件参数的设计允许存在冗余，即允许API路径和URL参数偶尔有重复。比如， GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。不符合 CRUD 情况的 RESTful API在实际资源操作中，总会有一些不符合 CRUD 的情况，一般有几种处理方法。1、使用 POST，为需要的动作增加一个 endpoint，使用 POST 来执行动作，比如: POST /resend 重新发送邮件。2、增加控制参数，添加动作相关的参数，通过修改参数来控制动作。比如一个博客网站，会有把写好的文章“发布”的功能，可以用上面的 POST /articles/{:id}/publish 方法，也可以在文章中增加 published:boolean 字段，发布的时候就是更新该字段 PUT /articles/{:id}?published=true3、把动作转换成资源，把动作转换成可以执行 CRUD 操作的资源， github 就是用了这种方法。比如“喜欢”一个 gist，就增加一个 /gists/:id/star 子资源，然后对其进行操作：“喜欢”使用 PUT /gists/:id/star ，“取消喜欢”使用 DELETE /gists/:id/star 。另外一个例子是 Fork，这也是一个动作，但是在 gist 下面增加 forks资源，就能把动作变成 CRUD 兼容的： POST /gists/:id/fork s 可以执行用户 fork 的动作。动词覆盖，应对服务器不完全支持 HTTP 的情况有些客户端只能使用GET和POST这两种方法。服务器必须接受POST模拟其他三个方法（PUT、PATCH、DELETE）。这时，客户端发出的 HTTP 请求，要加上X-HTTP-Method-Override属性，告诉服务器应该使用哪一个动词，覆盖POST方法。API响应状态码2xx 状态码200 状态码也称成功状态码，但是对于不同的方法又有不同的状态码GET --> 200 OKPOST–> 201 Created（表示生成新的资源）PUT --> 200 OKPATCH --> 200 OKDELETE --> 204 No Content（表示该资源已不存在）3xx 状态码在 Restful API 中用不上，永久重定向（ 301 ）、暂时重定向（ 302 、 307 ）可由应用级别返回，浏览器直接跳转。主要使用 303 see other，以表示参考另一个URL，它与 302 和 307 的含义一样，也是”暂时重定向”，区别在于 302 和 307 用于GET请求，而 303 用于POST、PUT和DELETE请求。收到 303 以后，浏览器不会自动跳转，而会让用户自己决定下一步怎么办。4xx 状态码4xx 状态码表示客户端错误，主要有下面几种：400 --> Bad Request：服务器不理解客户端的请求，未做任何处理。401 --> Unauthorized：用户未提供身份验证凭据，或者没有通过身份验证。403 --> Forbidden：用户通过了身份验证，但是不具有访问资源所需的权限。404 --> Not Found：所请求的资源不存在，或不可用。405 --> Method Not Allowed：用户已经通过身份验证，但是所用的 HTTP 方法不在他的权限之内。410 --> Gone：所请求的资源已从这个地址转移，不再可用。415 --> Unsupported Media Type：客户端要求的返回格式不支持。比如，客户端要求返回XML格式，API只能返回JSON格式。422 --> Unprocessable Entity ：客户端上传的附件无法处理，导致请求失败。429 --> Too Many Requests：客户端的请求次数超过限额。5xx 状态码5xx 状态码表示服务端错误。一般来说，API不会向用户透露服务器的详细信息，所以只要两个状态码就够了。500 --> Internal Server Error：客户端请求有效，服务器处理时发生了意外。503 --> Service Unavailable：服务器无法处理请求，一般用于网站维护状态。返回数据以下事项均为注意事项，不要问为什么，因为博主在学之前全都犯过。不要返回纯本文API 返回的数据格式，不应该是纯文本，而应该是一个 JSON 对象，因为这样才能返回标准的结构化数据。所以，服务器回应的 HTTP 头的Content-Type属性要设为 application/json 。客户端请求时，也要明确告诉服务器，可以接受 JSON 格式，即请求的 HTTP 头的ACCEPT属性也要设成 application/json 。不要包装数据response 的 body直接就是数据，不要做多余的包装。错误实例：{ "success":true, "data":{ "id":1, "name":"周伯通" } }针对不同操作，服务器向用户返回的结果应该符合以下规范。GET /collection：返回资源对象的列表（数组） GET /collection/resource：返回单个资源对象 POST /collection：返回新生成的资源对象 PUT /collection/resource：返回完整的资源对象 PATCH /collection/resource：返回完整的资源对象 DELETE /collection/resource：返回一个空文档发生错误时，不要返回 200 状态码有一种不恰当的做法是，即使发生错误，也返回200状态码，把错误信息放在数据体里面，就像下面这样。{ "status": "failure", "data": { "error": "Expected at least two items in list." } }正确的做法是，状态码反映发生的错误，具体的错误信息放在数据体里面返回。下面是一个例子。HTTP/1.1 400 Bad Request Content-Type: application/json { "error": "Invalid payoad.", "detail": { "surname": "This field is required." } }
下载Navicat1.官网下载: 官网下载 2.百度云下载: 百度云下载 3.未安装过的用户可直接跳过该步骤，如果已安装Navicat，记得先卸载干净，防止破解失效，卸载完成后执行补丁压缩包中的 Navicat.bat 脚本（一闪而过表示正常，已成功删除Navicat的注册信息和相关的注册表数据）4.安装Navicat17(安装完成不要打开！！！) 4.1点击运行navicat170_premium_cs_x64.exe 4.2开始安装 4.3选择安装路径，最好不要放在系统盘C盘，后面两个步骤默认 4.4安装中，耐心等待1-2分钟 4.5安装完成，点击【完成】，先不要启动软件 4.6返回我们下载好的破解补丁文件夹内，文件夹下的 winmm.dll 文件覆盖到Navicat Premium的安装位置即可激活 下载破解补丁： 破解补丁
打开网站点我前往打开网址寻找到一个 cors 标记为*号的网站并打开它下载工具包点击 jetbra.zip 即可下载工具包解压工具包把下载下来的jetbra.zip解压解压到一个稳定的位置注意：在执行完毕后本目录下的文件不能移动和删除执行脚本执行前请关闭 IDEA 软件在“许可证”窗口中注销 jb 帐户执行脚本执行成功后会出现相关提示文字注意确认是否有错误提示macOS 或 Linux执行“scripts/install.sh”Windows双击执行“scripts\install-current-user”。vbs”(针对当前用户)双击执行“scripts\install-all-users。vbs”(所有用户)复制许可证我这里用的是 IDEA 就复制 IDEA 的许可证信息打开IDEA使用激活码激活填入复制的许可证注意如果提示许可证无效，可能是执行脚本步骤存在问题,需要重试执行脚本步骤完成不用在意这个过期时间，他只是一个标志 过期后软件还可以正常使用
前言本人文笔不太行，希望各位能看懂。不会的可以在下面评论留言。环境服务器环境Nginx v1.24.0Mysql v5.7.44Redis v7.4.1Meilisearch v1.12.1本地环境Node v22.12.0Git v2.43.0Go v1.23.4安装环境安装Node点击 这里 安装最新版Node即可 目前最新v22.12.0安装Go点击 这里 安装最新版Go即可 目前最新v1.23.4安装Git点击 这里 安装最新版Git即可 目前最新 v2.47.1获取源代码首先你可以在桌面或者任意位置新建一个文件夹或者直接在桌面打开cmd执行git clone https://github.com/rocboss/paopao-ce.git执行完之后你会发现桌面多了一个paopao-ce文件夹 我们cd进去cd paopao-ce然后进入这个页面你可以看到以下目录核心源码目录 internal/ - 项目内部代码,包含核心业务逻辑实现 pkg/ - 可复用的工具包代码 cmd/ - 命令行入口代码 auto/ - 自动生成的代码 mirc/ - RESTful API 接口定义 proto/ - gRPC 接口定义 Web前端相关 web/ - Web前端代码 部署和构建相关 docker/ - Docker相关配置和脚本 scripts/ - 各类脚本文件 build-image.sh - 构建Docker镜像脚本 build-allinone.sh - 构建all-in-one Docker镜像脚本 build-release.sh - 构建发布版本脚本 docker-compose.yaml - Docker Compose配置文件 Dockerfile - Docker构建文件 Dockerfile.allinone - All-in-one Docker构建文件 配置文件 .dockerignore - Docker构建忽略文件 .gitattributes - Git属性配置 .gitignore - Git忽略文件配置 .golangci.yml - golangci-lint配置 buf.gen.yaml - buf生成代码配置 buf.work.yaml - buf工作空间配置 config.yaml.sample - 示例配置文件 IDE配置 .vscode/ - VS Code配置文件 .github/ - GitHub Actions等配置 文档 docs/ - 项目文档 CHANGELOG.md - 版本变更记录 deployed-sites.md - 已部署站点列表 features-status.md - 功能特性状态 LICENSE - 开源协议 README.md - 项目说明 ROADMAP.md - 开发路线图 其他 main.go - 程序入口文件 Makefile - Make构建配置 go.mod/go.sum - Go模块依赖配置 run.sh - 运行脚本首先配置一下Go的源 默认是官方源 我们换一下国内源打开cmd输入以下命令go env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn然后配置一下Go的编译设置 因为我们要在服务器上部署 我这边用的是Ubuntu系统 部署在宝塔面板上 所以我们得编译Linux可执行的二进制文件set GOARCH=amd64 go env -w GOARCH=amd64 set GOOS=linux go env -w GOOS=linux这样就设置成了编译Linux可执行的二进制文件了然后我们来配置一下config.yaml.sample文件首先复制一份config.yaml.sample文件更名为config.yaml然后在config.yaml中配置我们需要的这里我简单的配置一下我的App: # APP基础设置项 RunMode: debug AttachmentIncomeRate: 0.8 #这个是里面付费作者的提成比例 这个0.8 就是80% 设置1.0 就是100%收益 如果你不启用钱包或者付费 请忽视这条 MaxCommentCount: 10 DefaultContextTimeout: 60 DefaultPageSize: 10 MaxPageSize: 100 Server: # 服务设置 RunMode: debug HttpIp: 0.0.0.0 HttpPort: 8010 ReadTimeout: 60 WriteTimeout: 60 Features: Default: ["Web", "Frontend:EmbedWeb", "Meili", "LocalOSS", "MySQL", "BigCacheIndex", "LoggerFile", "Redis", "LoggerMeili", "Alipay"] # 这里是启用的模块 我自己加上Redis和LoggerMeili还有Alipay这三个 看你自己的需求 Develop: ["Base", "MySQL", "BigCacheIndex", "Meili", "Sms", "AliOSS", "LoggerMeili", "OSS:Retention"] Demo: ["Base", "MySQL", "Option", "Zinc", "Sms", "MinIO", "LoggerZinc", "Migration"] Slim: ["Base", "Sqlite3", "LocalOSS", "LoggerFile", "OSS:TempDir"] Base: ["Redis", "PhoneBind"] Docs: ["Docs:OpenAPI"] Deprecated: ["Deprecated:OldWeb"] Service: ["Web", "Admin", "SpaceX", "Bot", "LocalOSS", "Mobile", "Frontend:Web", "Frontend:EmbedWeb", "Docs"] Option: ["SimpleCacheIndex"] Sms: "SmsJuhe" WebServer: # Web服务 HttpIp: 0.0.0.0 HttpPort: 8008 ReadTimeout: 60 WriteTimeout: 60 AdminServer: # Admin后台运维服务 HttpIp: 0.0.0.0 HttpPort: 8014 ReadTimeout: 60 WriteTimeout: 60 SpaceXServer: # SpaceX服务 HttpIp: 0.0.0.0 HttpPort: 8012 ReadTimeout: 60 WriteTimeout: 60 BotServer: # Bot服务 HttpIp: 0.0.0.0 HttpPort: 8016 ReadTimeout: 60 WriteTimeout: 60 LocalossServer: # Localoss服务 HttpIp: 0.0.0.0 HttpPort: 8018 ReadTimeout: 60 WriteTimeout: 60 FrontendWebServer: # Web前端服务 HttpIp: 0.0.0.0 HttpPort: 8006 ReadTimeout: 60 WriteTimeout: 60 DocsServer: # 开发文档服务 HttpIp: 0.0.0.0 HttpPort: 8011 ReadTimeout: 60 WriteTimeout: 60 MobileServer: # 移动端grpc api服务 Host: 0.0.0.0 Port: 8020 SmsJuhe: Gateway: https://v.juhe.cn/sms/send Key: TplID: TplVal: "#code#=%s&#m#=%d" Alipay: AppID: # 这里填你的支付宝Appid号 然后下面要加一条PrivateKey来存你的私钥 InProduction: True PrivateKey: "这里面存你的私钥" RootCertFile: "custom/alipay/RootCert.crt" #然后这些你自己去相应的位置创建文件夹和放入你的证书 记得名字要改成这样的三个 PublicCertFile: "custom/alipay/CertPublicKey_RSA2.crt" AppPublicCertFile: "custom/alipay/AppCertPublicKey.crt" CacheIndex: MaxUpdateQPS: 100 # 最大添加/删除/更新Post的QPS, 设置范围[10, 10000], 默认100 SimpleCacheIndex: # 缓存泡泡广场消息流 MaxIndexSize: 200 # 最大缓存条数 CheckTickDuration: 60 # 循环自检查每多少秒一次 ExpireTickDuration: 300 # 每多少秒后强制过期缓存, 设置为0禁止强制使缓存过期 BigCacheIndex: # 使用BigCache缓存泡泡广场消息流 MaxIndexPage: 1024 # 最大缓存页数，必须是2^n, 代表最大同时缓存多少页数据 Verbose: False # 是否打印cache操作的log ExpireInSecond: 300 # 多少秒(>0)后强制过期缓存 Logger: # 日志通用配置 Level: debug # 日志级别 panic|fatal|error|warn|info|debug|trace LoggerFile: # 使用File写日志 SavePath: custom/data/paopao-ce/logs FileName: app FileExt: .log LoggerZinc: # 使用Zinc写日志 Host: zinc:4080 Index: paopao-log User: admin Password: admin Secure: False LoggerMeili: # 使用Meili写日志 Host: meili:7700 # 这里meili:7700改成127.0.0.1:7700 Index: paopao-log ApiKey: paopao-meilisearch Secure: False MinWorker: 5 # 最小后台工作者, 设置范围[5, 100], 默认5 MaxLogBuffer: 100 # 最大log缓存条数, 设置范围[10, 10000], 默认100 JWT: # 鉴权加密 Secret: 18a6413dc4fe394c66345ebe501b2f26 Issuer: paopao-api Expire: 86400 TweetSearch: # 推文关键字搜索相关配置 MaxUpdateQPS: 100 # 最大添加/删除/更新Post的QPS，设置范围[10, 10000], 默认100 MinWorker: 10 # 最小后台更新工作者, 设置范围[5, 1000], 默认10 Zinc: # Zinc搜索配置 Host: zinc:4080 Index: paopao-data User: admin Password: admin Secure: False Meili: # Meili搜索配置 Host: meili:7700 # 这里meili:7700改成127.0.0.1:7700 Index: paopao-data ApiKey: paopao-meilisearch Secure: False ObjectStorage: # 对象存储通用配置 RetainInDays: 2 # 临时对象过期时间多少天 TempDir: tmp # 临时对象存放目录名 AliOSS: # 阿里云OSS存储配置 Endpoint: AccessKeyID: AccessKeySecret: Bucket: Domain: COS: # 腾讯云COS存储配置 SecretID: SecretKey: Region: ap-shanghai Bucket: demo-1888888888 Domain: HuaweiOBS: # 华为云OBS存储配置 AccessKey: SecretKey: Endpoint: Bucket: paopao Domain: MinIO: # MinIO 存储配置 AccessKey: Q3AM3UQ867SPQQA43P2F SecretKey: zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG Secure: False Endpoint: minio:9000 Bucket: paopao Domain: 127.0.0.1:9000 S3: # Amazon S3 存储配置 AccessKey: "YOUR-ACCESSKEYID" SecretKey: "YOUR-SECRETACCESSKEY" Secure: True Endpoint: s3.amazonaws.com Bucket: paopao Domain: LocalOSS: # 本地文件OSS存储配置 SavePath: custom/data/paopao-ce/oss Secure: False Bucket: paopao Domain: 127.0.0.1:8008 # 这里改成你的域名 Database: # Database通用配置 LogLevel: error # 日志级别 silent|error|warn|info TablePrefix: p_ # 表名前缀 MySQL: # MySQL数据库 Username: paopao # 你的数据库用户名 Password: paopao # 你的数据库密码 Host: db:3306 #你的数据库端口 记得把db 改成127.0.0.1 DBName: paopao Charset: utf8mb4 ParseTime: True MaxIdleConns: 10 MaxOpenConns: 30 Postgres: # PostgreSQL数据库 User: paopao Password: paopao DBName: paopao Host: localhost Port: 5432 SSLMode: disable TimeZone: Asia/Shanghai Sqlite3: # Sqlite3数据库 Path: custom/data/sqlite3/paopao-ce.db Redis: InitAddress: - redis:6379 #这个你也把redis改成127.0.0.1 WebProfile: UseFriendship: true # 前端是否使用好友体系 EnableTrendsBar: true # 广场页面是否开启动态条栏功能 EnableWallet: false # 是否开启钱包功能 AllowTweetAttachment: true # 是否允许推文附件 AllowTweetAttachmentPrice: true # 是否允许推文付费附件 AllowTweetVideo: true # 是否允许视频推文 AllowUserRegister: true # 是否允许用户注册 AllowPhoneBind: true # 是否允许手机绑定 DefaultTweetMaxLength: 2000 # 推文允许输入的最大长度， 默认2000字，值的范围需要查询后端支持的最大字数 TweetWebEllipsisSize: 400 # Web端推文作为feed显示的最长字数，默认400字 TweetMobileEllipsisSize: 300 # 移动端推文作为feed显示的最长字数，默认300字 DefaultTweetVisibility: friend # 推文可见性，默认好友可见 值: public/following/friend/private DefaultMsgLoopInterval: 5000 # 拉取未读消息的间隔，单位：毫秒, 默认5000ms CopyrightTop: "2023 paopao.info" CopyrightLeft: "Roc's Me" CopyrightLeftLink: "" CopyrightRight: "泡泡(PaoPao)开源社区" CopyrightRightLink: "https://www.paopao.info"然后就是配置一下前端了 这里我们cd到web目录cd web然后前端也需要搞一个那个配置文件 我们复制一份.env 更名为.env.local然后进入.env.local文件里VITE_HOST="然后这里填你的域名http://xxx" # 功能特性开启 VITE_USE_FRIENDSHIP=true VITE_USE_WEB_PROFILE=true # 是否使用后端web服务提供的WebProfile来自定义前端的功能特性 # 模块开启 VITE_ENABLE_ANOUNCEMENT=false VITE_ENABLE_WALLET=false VITE_ENABLE_TRENDS_BAR=false # 功能开启 VITE_ALLOW_TWEET_ATTACHMENT=true VITE_ALLOW_TWEET_ATTACHMENT_PRICE=false VITE_ALLOW_TWEET_VIDEO=true VITE_ALLOW_TWEET_VISIBILITY=true VITE_ALLOW_USER_REGISTER=true VITE_ALLOW_ACTIVATION=false VITE_ALLOW_PHONE_BIND=true # 局部参数 VITE_DEFAULT_MSG_LOOP_INTERVAL=5000 # 拉取未读消息的间隔，单位：毫秒, 默认5000ms VITE_DEFAULT_TWEET_VISIBILITY=friend # 推文默认可见性，默认好友可见 值: public/following/friend/private VITE_DEFAULT_TWEET_MAX_LENGTH=2000 # 推文允许输入的最大长度， 默认2000字，值的范围需要查询后端支持的最大字数 VITE_TWEET_WEB_ELLIPSIS_SIZE=400 # Web端推文作为feed显示的最长字数，默认400字 VITE_TWEET_MOBILE_ELLIPSIS_SIZE=300 # 移动端推文作为feed显示的最长字数，默认300字 VITE_DEFAULT_COMMENT_MAX_LENGTH=300 # 评论最大长度, 默认300字 VITE_DEFAULT_REPLY_MAX_LENGTH=300 # 评论最大长度, 默认300字 VITE_RIGHT_FOLLOW_TOPIC_MAX_SIZE=6 # 右侧关注话题最大条目数， 默认6条 VITE_RIGHT_HOT_TOPIC_MAX_SIZE=12 # 右侧热门话题最大条目数， 默认12条 VITE_COPYRIGHT_TOP="2023 paopao.info" VITE_COPYRIGHT_LEFT="Roc's Me" VITE_COPYRIGHT_LEFT_LINK="" VITE_COPYRIGHT_RIGHT="泡泡(PaoPao)开源社区" VITE_COPYRIGHT_RIGHT_LINK="https://www.paopao.info" # 图片推文参数 VITE_DEFAULT_TWEET_IMAGE_404="https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/404.png" # AliOSS 图片thumbnail参数 VITE_TWEET_IMAGE_THUMBNAIL="?x-oss-process=image/resize,m_fill,w_300,h_300,limit_0/auto-orient,1/format,webp" # MinIOn 图片thumbnail参数 # VITE_TWEET_IMAGE_THUMBNAIL="?x-oss-process=image/resize,m_fill,w_300,h_300,limit_0/auto-orient,1/format,png"然后修改好之后 我们来给npm换个国内源这里我们用淘宝源npm config set registry https://registry.npmmirror.com执行完之后我们就安装依赖npm install安装完依赖之后我们就打包npm build打包完成之后我们回到paopao-ce目录打包二进制文件go build不出意外本地会出现一个paopao-ce的文件我们把这个文件上传到宝塔面板里面这里我创建了一个bbs目录 你们想搞啥搞啥然后我们在bbs目录里上传我们的二进制文件和配置文件config.yaml和一个文件夹emmm...什么 你要问我什么文件夹 那当然是你存证书的还有本地日志的还有本地文件的啦喏 就是这些 custom 里面有两个文件夹 分别是alipay和data alipay这个文件夹里面放你的证书 然后data里面也有一个文件夹paopao-ce 然后paopao-ce里面有两个文件夹分别是logs和oss 这两个我不作解释 看名字也知道是本地日志和本地存储的文件夹 哦~ 不小心说出来了然后完成上面这部之后我们在网站里新建一个go项目 什么？你不知道在哪 好吧 我截个屏然后按照我这样的配置记得哦 运行一定要在后面加上serve 不然... 你懂的然后我们配置Mysql 新建一个paopao的数据库 导入本地scripts文件夹里面的paopao-mysql.sql文件 注意这里paopao数据库和用户名密码一定要跟config.yaml里面一致 什么？ 你不会导入 emmm 真是让人头疼看到上面导入没 点那个然后选择文件 导入执行就行okk 现在已经完成一大半了好吧 越来越让人兴奋了接下来我们去下载那个什么美丽搜索引擎 美丽搜索引擎 根据你自己的系统架构来下 一般都是meilisearch-linux-amd64那个下载完这个之后 我们上传到宝塔面板上面 随便哪个文件夹都可以 我这里放root文件夹里面然后呢我们在root文件夹里面创建一个文件夹来存放这个美丽搜索引擎的日志和一些文件啥的mkdir -p data/meili/data可以使用这个命令创建也可以自己手动在root文件夹内创建 注意要先cd到root文件夹才可执行这个命令 不然就在别处创建了然后我们在宝塔面板的软件商店下一个进程守护管理器就是这货 用来运行那个美丽搜索引擎我们来添加一个守护我这里是放在root的目录下 你们根据自己的去修改 然后运行指令就是sudo ./meilisearch 然后就没了...访问你的域名就部署成功了 演示站:泡泡 有啥不会的底下留言评论哈 或者看关于页 联系我 无偿帮你最后付上作者的GitHub项目页 paopao-ce (ฅ´ω`ฅ)