说一说登录这件麻烦事 更新中 

每个写过后端的人,迟早都要跟"登录"打一场硬仗。它看起来是个小功能——一个用户名、一个密码、一个按钮——但凡是真正做过的人都知道,这是整个系统里坑最深、改起来最痛、出事最致命的一块。 它麻烦,不是因为技术有多难,而是因为它卡在三方利益的正中间:用户想省事,产品想拉新,安全想严防死守。这三件事天然打架。你每往其中一边挪一寸,另外两边就开始喊疼。 这篇文章想把这件"麻烦事"摊开讲清楚:主流厂商现在都怎么做、每种做法烂在哪、未来可能怎么变,以及如果你今天就要动手,应该怎么落地。 一、先看战场:主流厂商现在都在用什么 如果你今天注册任何一个稍微正经点的 App,会发现登录方式早就不是"账号 + 密码"一条路了。现在的主流玩法大致可以分成五类。 1. 手机号 + 短信验证码 这是中国互联网的绝对统治者。点开微博、抖音、美团、拼多多,第一屏几乎清一色是"输入手机号 → 收验证码 → 进"。 它能赢,是因为它一次性解决了三个问题:手机号天然实名(运营商帮你做了 KYC)、不用记密码、注册和登录是同一个动作。对产品经理来说,这意味着注册转化率极高——少一个"设置密码"的步骤,就少漏一批用户。 代价是:你把身份的命脉,交给了运营商和短信通道。 2. 邮箱 + 密码 这是全球(尤其欧美)的默认范式。GitHub、Google、Notion、几乎所有 SaaS 都以邮箱为账号主体。 邮箱的好处是全球通用、跨国可用、不依赖运营商、可以承载找回流程。一个邮箱地址几乎就是你在互联网上的"主键"。坏处后面细说。 3. 第三方登录(OAuth / 社交登录) “用微信登录"“Sign in with Google"“Continue with Apple”——本质上是把身份认证外包给一个你已经信任的大平台。 它的杀手锏是:用户一次都不用输。点一下,授权,进去了。对开发者来说,你还省掉了自己存密码的风险(密码根本不经过你的服务器)。代价是你被绑在了平台生态上,而且用户的账号体系实际上不在你手里。 4. 魔法链接(Magic Link) 无密码的一种:你输入邮箱,系统给你发一封带一次性链接的邮件,点开即登录。Slack、Medium 早期都靠这个。 它把"记密码"这件事彻底删掉了,安全模型简单清晰。但它把整个登录体验的流畅度,押在了"邮件能不能秒到"上——而邮件这玩意,慢起来能慢到你怀疑人生。 5. Passkey / WebAuthn(通行密钥) 这是最新、也是各大厂商正在猛推的方向。Apple、Google、Microsoft 已经全面支持。它基于公私钥密码学,用你设备上的生物识别(指纹、Face ID)来完成认证,服务器端根本不存任何可被盗的密码。 这是目前公认"理论上最优"的方案,我们最后单独讲。 下面这张图,把这五种方式按"用户省心程度"和"安全强度"两个维度摆一摆,你能直观看到它们各自的生态位: graph TD A["登录方式光谱"] --> B["短信验证码省心高 / 安全中"] A --> C["邮箱密码省心低 / 安全中"] A --> D["第三方登录省心高 / 安全中高"] A --> E["魔法链接省心中 / 安全中"] A --> F["Passkey省心高 / 安全高"] B --> B1["依赖运营商换号即失联"] C --> C1["密码要记容易被撞库"] D --> D1["绑死大平台账号不在自己手里"] E --> E1["押注邮件时效慢到怀疑人生"] F --> F1["体验最好但迁移设备是痛点"] style A fill:#1a1a2e,color:#fff style F fill:#16213e,color:#7fff7f 二、麻烦的本质:每一种方式都在某处偷偷塌方 上面每种方式听起来都还行,但它们都有一个藏在水面下的塌方点。用户平时感觉不到,一旦踩中,体验直接归零。 ...

2025-12-26 03:56:40 PM · 2 分钟

Spring Boot 常用代码片段 更新中 

通用 依赖管理 父 pom.xml 固定三版本管控 版本参考 <dependencyManagement> <dependencies> <!-- 1. SpringBoot 基础版本 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.5.0</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 2. Spring Cloud 云原生基础 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2025.0.0</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 3. Spring Cloud Alibaba 阿里全套依赖核心(关键!) --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2025.0.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 启动类 @MapperScan("asia.liminality.user.mapper") @SpringBootApplication @Slf4j public class UserApplication { public static void main(String[] args) throws UnknownHostException { ConfigurableApplicationContext app = SpringApplication.run(UserApplication.class, args); Environment env = app.getEnvironment(); String protocol = "http"; if (env.getProperty("server.ssl.key-store") != null) { protocol = "https"; } log.info("--/\n---------------------------------------------------------------------------------------\n\t" + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\t{}://localhost:{}\n\t" + "External: \t{}://{}:{}\n\t" + "Profile(s): \t{}" + "\n---------------------------------------------------------------------------------------", env.getProperty("spring.application.name"), protocol, env.getProperty("server.port"), protocol, InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port"), env.getActiveProfiles()); } } domain DTO Result import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> success(T data) { return new Result<>(200, "success", data); } public static <T> Result<T> success() { return new Result<>(200, "success", null); } public static <T> Result<T> failure(String message) { return new Result<>(500, message, null); } public static <T> Result<T> failure(int code, String message) { return new Result<>(code, message, null); } public static <T> Result<T> failure(String message, T data) { return new Result<>(500, message, data); } } Spring Cloud Gateway AuthGlobalFilter @Slf4j @RequiredArgsConstructor @Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private final JwtUtil jwtUtil; private static final List<String> WHITE_LIST = List.of( "/auth/user/login", "/auth/user/register" ); // 判断是否在白名单内 private boolean isWhiteList(String path){ return WHITE_LIST.stream().anyMatch(path::startsWith); } private Mono<Void> reject(ServerWebExchange exchange){ exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().toString(); if (isWhiteList(path)) { return chain.filter(exchange); } String token = request.getHeaders().getFirst("Authorization"); // 令牌为空,拒绝访问 if( token == null || token.isEmpty() ){ return reject( exchange ); } // 验证令牌 try { Integer userId = jwtUtil.parseToken(token); // TODO userId 塞入请求头 log.info("🚪 塞入请求头"); } catch (Exception e) { return reject(exchange); } return chain.filter( exchange ); } // 过滤器优先级,越小越靠前 @Override public int getOrder() { return -1; } }

2025-11-26 09:51:37 AM · 2 分钟

突发性耳聋治疗过程

9月22日 星期一 DAY 1 突然患病 早上起床右耳耳朵闷,左耳正常。以为就是耳朵堵,中耳炎之类,会自然恢复,没太留意。 9月23日 星期二 DAY 2 这一天发现耳朵除了闷以外,听外界说话还有回声,感觉事情不太对。但是依然没有往严重方向上想,上班要紧。 9月25日 星期四 DAY4 看了很多视频,感觉可能是突发性耳聋。决定第二天去医院。此时已经错过了网上流传的所谓“72小时”黄金时间。 9月27日 星期六 DAY6 医院诊断 上午去南京中西医结合医院诊断。医生说考虑是一个突发性耳聋。一直很纠结医生说的“考虑说”是啥意思?他自己也不确定吗? 医生说要吃药,目前只是低频下降明显,不算特别严重。 后来开了一些药,要我遵循医嘱去吃,并且要放松自己,按时睡觉,健康生活。 这一天我进行了充足的睡眠。 9月28日 星期日 DAY6 早上醒来后,发现耳朵闷好像有所缓解,但是右耳明显耳鸣,一直有那种地铁经过的声音,但是音量不算太大。 9月30日 星期二 DAY9 早上醒来后,突然发现两个耳朵都听力下降了。缓了一会儿发现左耳好了,右耳还是老样子。把我吓得不轻。 感觉听力没有明显的好转。但是闷闷的感觉似乎有改善。 右耳有点感觉,好像是药物起作用了,不确定。 10月8日 星期三 从这一天开始耳鸣变得明显。耳鸣的声音感觉在400-500Hz左右会一直持续。 病情没有一丝好转。 10月9日 星期四 我到南京江苏人民医院再次进行耳部检查。本次检查后,进行了输液。 10月10日 第二次挂水。今日症状仍无任何缓解。 10月11日 第三次挂水。症状未减轻。中低音耳鸣明显,伴随地铁列车嗖嗖经过的声音。 10月12日 第四次挂水。 10月13日 当晚因工作忙碌未挂水。 10月14日 到医院复查,发现右耳听力几乎恢复正常(右耳进入25db及格线内)。开了药继续吃。 11月21日 突聋发作已29天,距初次治疗已24天 中低音耳鸣明显减弱,几乎听不到。现在听到的是音量很低的收音机调不到台的噪音。125Hz、250Hz的声音听到的音量有较小差异。右耳对125Hz、250Hz(或者就直接说是250Hz以下)的声音的听力没有左耳好,且伴随明显的音调变化。500Hz及以上左右耳几乎一致,但依然伴随微弱的音调变化。 10月23日 明显好转,右耳几乎听不到任何噪音,也没有耳鸣现象。目前和左耳的唯一区别就是125hz-500hz区间听力有微弱下降,伴随音调变化。 11月12日 距离突发性耳聋发作已经51天,距离突发性耳聋初次治疗已经46天。目前右耳和左耳比125hz有5db的差距,250hz有10db的差距,其他频段几乎没有差别。在打电话时,用右耳明显有复听现象。一般生活,和之前相比,没有明显感觉到复听。

2025-11-12 12:24:27 PM · 1 分钟

PostgreSQL和java的LocalDateTime不兼容的问题 更新中 

Java 的 LocalDateTime 和 PostgreSQL 的时间类型,“说的不是同一种时间”。 先搞清楚 PostgreSQL 的两种时间类型 TIMESTAMP → 不带时区,就是个裸时间 "2026-06-25 10:00:00" TIMESTAMPTZ → 带时区,内部存 UTC,查询时按会话时区转换 Java 这边 LocalDateTime → 没有时区概念,就是个裸时间 ZonedDateTime → 带时区 OffsetDateTime → 带偏移量(如 +08:00) Instant → UTC 时间戳 为什么会报错 PostgreSQL JDBC 驱动(特别是新版本 42.x+)对类型匹配非常严格: flowchart TD A[Java LocalDateTime] --> B[JDBC驱动] B --> C{PostgreSQL列类型} C -->|TIMESTAMP| D[✅ 可以匹配] C -->|TIMESTAMPTZ| E[❌ 类型不匹配报错] 你的列如果是 TIMESTAMPTZ(带时区),但 Java 传的是 LocalDateTime(无时区),驱动不知道该用哪个时区换算,就直接拒绝了。 常见的三种报错 Cannot convert LocalDateTime to TIMESTAMPTZ Bad value for type timestamp/date column is of type timestamp with time zone but expression is of type timestamp 解决方案 方案一:改 Java 类型(推荐) ...

2025-10-25 12:55:49 PM · 1 分钟
后端疑难杂症解答

Swagger2和Swagger3注解对比表 更新中 

在 Spring Boot 生态中,Swagger 2.0(通常使用 Foxfire 依赖)和 Swagger 3.0(通常使用 Springdoc-openapi 依赖,基于 OpenAPI 3 规范)的注解发生了很大变化。 以下是 Swagger 2.0 与 Swagger 3.0(OpenAPI 3)的常用注释完整对应表: 1. 核心注解对应表 功能描述 Swagger 2.0 注解 (io.swagger.annotations) Swagger 3.0 注解 (io.swagger.v3.oas.annotations) 备注说明 标记控制器类 @Api(tags = "用户接口") @Tag(name = "用户接口") 3.0 中移除了 description 属性,统一使用 name 标记接口方法 @ApiOperation(value = "获取用户") @Operation(summary = "获取用户") 3.0 中 value 变更为 summary 入参实体类 @ApiModel(value = "用户对象") @Schema(description = "用户对象") 3.0 极大简化,统一使用 @Schema 实体类属性 @ApiModelProperty(value = "姓名") @Schema(description = "姓名") 同上,合并为了 @Schema 忽略某个属性 @ApiModelProperty(hidden = true) @Schema(hidden = true) 忽略整个类/方法 @ApiIgnore @Hidden 用于不想暴露在文档中的接口或参数 2. 请求参数注解对应表 对于方法入参(如 URL 路径参数、Query 参数等),3.0 引入了更具结构化的配置: ...

2025-09-25 09:06:35 AM · 1 分钟
后端疑难杂症解答

程序员快速参考小手册 更新中 

有的命令一个月可能就用那么几次,不写手册里谁能记得住啊 AI 常用提示词 老师提示词 你现在是老师,教我这个问题。上课战略是“及时反馈,小步快走”。教学过程中注意: 1. 涉及代码、算法、线程、内存或调用链时,生成mermaid图像或者html+js(简单css即可)的互动网页; 2. 不要堆砌术语;让你认为大概率不知道某个术语含义时,先用简单语言解释。 学习路线提示词 请设计一条以“能实际使用”为目标的学习路线。 要求: 按依赖关系排序 区分必须掌握和进阶内容 避免学习低收益知识 每个阶段说明学到什么程度算合格 给出可以验证掌握程度的小项目或练习 不要简单给课程目录。 查漏补缺提示词 我已经学过这个主题。 请不要从头讲教材。 先帮我建立完整知识地图,然后判断: 我必须掌握什么 哪些知识容易漏掉 哪些知识经常被错误理解 哪些知识在实际工作中最重要 哪些属于低优先级细节 帮我找到知识盲区,而不是重复基础内容。 Claude Code # 安装状态栏 npx -y ccstatusline@latest VSCode 解决 code-runner Java Output 乱码 "code-runner.executorMap": { "javascript": "node", "java": "\"C:/Program Files/Java/jdk-17/bin/java.exe\" -Dfile.encoding=UTF-8", Windows 快速完成端口转发 netsh interface portproxy add v4tov4 ` listenaddress=0.0.0.0 ` listenport=2375 ` connectaddress=127.0.0.1 ` connectport=2375 refreshenv Install-Script Refresh-EnvironmentVariables -Force 查端口占用 杀进程 netstat -aon | findstr :8080 taskkill /PID 39656 /F 自定义常用命令别名 notepad $PROFILE function gacp { param ( [string]$msg = "update" ) git add . git commit -m $msg git push } # 删除当前文件夹下空文件夹 function rme { Get-ChildItem -Directory -Recurse | Where-Object { $_.GetFileSystemInfos().Count -eq 0 } | Remove-Item } 文件整理类常用PS脚本 移动子目录所有文件到当前目录 # randomize_filename.ps1 # Rename every file in the current folder to a unique random number (extension preserved). # Usage: place this script in the target folder and run .\randomize_filename.ps1 $currentDir = (Get-Location).Path $scriptPath = $PSCommandPath Write-Host ("Scanning: {0}" -f $currentDir) -ForegroundColor Cyan # Get all files in the current folder, but skip the script itself so we don't rename our own executable. $files = Get-ChildItem -LiteralPath $currentDir -File | Where-Object { $_.FullName -ne $scriptPath } if (-not $files) { Write-Host "No files to rename." -ForegroundColor Yellow return } Write-Host ("Found {0} file(s)" -f $files.Count) # Digit count scales with file count so random collisions stay rare. # Roughly: digits = ceil(log10(N^2)) -> birthday-paradox-safe for typical inputs. $count = $files.Count $digits = [Math]::Max(4, [int][Math]::Ceiling([Math]::Log10($count * $count))) $min = [int][Math]::Pow(10, $digits - 1) $max = [int][Math]::Pow(10, $digits) - 1 Write-Host ("Using {0}-digit numbers ({1}..{2})" -f $digits, $min, $max) # Pre-compute unique new names. The "used" set contains every current name on disk # AND every target name we've already picked, so collisions are impossible by construction. $used = @{} foreach ($file in $files) { $used[$file.Name] = $true } $renameMap = [ordered]@{} foreach ($file in $files) { do { $num = Get-Random -Minimum $min -Maximum ($max + 1) $numStr = $num.ToString().PadLeft($digits, '0') $newName = "$numStr$($file.Extension)" } while ($used.ContainsKey($newName)) $used[$newName] = $true $renameMap[$file.FullName] = $newName } # Apply renames. .NET Move() bypasses PowerShell wildcard parsing (safe for [ ] in names). $renamed = 0 $failed = 0 foreach ($source in $renameMap.Keys) { $newName = $renameMap[$source] $dest = Join-Path $currentDir $newName try { [System.IO.File]::Move($source, $dest) Write-Host (" {0} -> {1}" -f (Split-Path $source -Leaf), $newName) $renamed++ } catch { $failed++ Write-Warning (" Failed: {0} ({1})" -f $source, $_.Exception.Message) } } Write-Host ("`nDone: renamed {0}, failed {1}" -f $renamed, $failed) -ForegroundColor Green 将文件夹下所有文件进行随机重命名(带容灾恢复) # randomize_filename.ps1 # Rename every non-.ps1 file in the current folder to a unique random number (extension preserved). # Maintains restore_filenames.txt so the change is reversible. # Usage: place this script in the target folder and run .\randomize_filename.ps1 $currentDir = (Get-Location).Path $restoreFile = Join-Path $currentDir 'restore_filenames.txt' Write-Host ("Folder: {0}" -f $currentDir) -ForegroundColor Cyan # ----- Restore mode: if a restore map exists, offer to undo first ----- if (Test-Path -LiteralPath $restoreFile) { $answer = Read-Host ("restore_filenames.txt detected. Restore original names? [Y/N]") if ($answer -match '^[Yy]') { $lines = Get-Content -LiteralPath $restoreFile -Encoding UTF8 $restored = 0 $failed = 0 foreach ($line in $lines) { $trimmed = $line.Trim() if (-not $trimmed -or $trimmed.StartsWith('#')) { continue } $parts = $trimmed -split "`t", 2 if ($parts.Count -ne 2) { Write-Warning ("Skipping malformed line: {0}" -f $line) $failed++ continue } $original = $parts[0] $current = $parts[1] $currentPath = Join-Path $currentDir $current $originalPath = Join-Path $currentDir $original if (-not (Test-Path -LiteralPath $currentPath)) { Write-Warning ("Source missing: {0}" -f $currentPath) $failed++ continue } if (Test-Path -LiteralPath $originalPath) { Write-Warning ("Target already exists: {0}" -f $originalPath) $failed++ continue } try { [System.IO.File]::Move($currentPath, $originalPath) Write-Host (" {0} -> {1}" -f $current, $original) $restored++ } catch { Write-Warning ("Failed: {0} ({1})" -f $current, $_.Exception.Message) $failed++ } } if ($failed -eq 0) { Remove-Item -LiteralPath $restoreFile -Force Write-Host ("`nAll {0} file(s) restored. Removed restore_filenames.txt." -f $restored) -ForegroundColor Green } else { Write-Host ("`nPartial restore: {0} succeeded, {1} failed. Keeping restore_filenames.txt." -f $restored, $failed) -ForegroundColor Yellow } return } Write-Host "Proceeding with rename (restore file will be overwritten)." -ForegroundColor Yellow } # ----- Rename mode ----- # Collect files. Skip ALL .ps1 files and the restore file itself. $allFiles = Get-ChildItem -LiteralPath $currentDir -File $files = $allFiles | Where-Object { $_.Extension -ne '.ps1' -and $_.Name -ne 'restore_filenames.txt' } if (-not $files) { Write-Host "No files to rename." -ForegroundColor Yellow return } Write-Host ("Found {0} file(s) to rename (excluded .ps1 and restore_filenames.txt)" -f $files.Count) # Digit count scales with file count so random collisions stay rare. $count = $files.Count $digits = [Math]::Max(4, [int][Math]::Ceiling([Math]::Log10($count * $count))) $min = [int][Math]::Pow(10, $digits - 1) $max = [int][Math]::Pow(10, $digits) - 1 Write-Host ("Using {0}-digit numbers ({1}..{2})" -f $digits, $min, $max) # Pre-compute unique new names. The "used" set includes EVERY name on disk # (including .ps1 files and the restore file, which we are NOT renaming), # plus every target name we pick, so collisions are impossible by construction. $used = @{} foreach ($f in $allFiles) { $used[$f.Name] = $true } $renameMap = [ordered]@{} foreach ($file in $files) { do { $num = Get-Random -Minimum $min -Maximum ($max + 1) $numStr = $num.ToString().PadLeft($digits, '0') $newName = "$numStr$($file.Extension)" } while ($used.ContainsKey($newName)) $used[$newName] = $true $renameMap[$file.FullName] = $newName } # Write restore file BEFORE renaming, so even partial renames leave us with a recoverable map. # Tab-separated: original<TAB>new (filenames only, no paths) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $headerLines = @( "# randomize_filename.ps1 restore map" "# created: $timestamp" "# directory: $currentDir" "# format: original`tnew (tab-separated)" "# comment lines starting with '#' are ignored on restore" ) $mappingLines = $renameMap.GetEnumerator() | ForEach-Object { $original = Split-Path $_.Key -Leaf "{0}`t{1}" -f $original, $_.Value } Set-Content -LiteralPath $restoreFile -Value ($headerLines + $mappingLines) -Encoding UTF8 # Apply renames. .NET Move() bypasses PowerShell wildcard parsing (safe for [ ] in names). $renamed = 0 $failed = 0 foreach ($source in $renameMap.Keys) { $newName = $renameMap[$source] $dest = Join-Path $currentDir $newName try { [System.IO.File]::Move($source, $dest) Write-Host (" {0} -> {1}" -f (Split-Path $source -Leaf), $newName) $renamed++ } catch { $failed++ Write-Warning (" Failed: {0} ({1})" -f $source, $_.Exception.Message) } } Write-Host ("`nDone: renamed {0}, failed {1}." -f $renamed, $failed) -ForegroundColor Green Write-Host ("Restore map saved to: {0}" -f $restoreFile) -ForegroundColor Cyan # randomize_filename.ps1 # Rename every non-.ps1 file in the current folder to a unique random number (extension preserved). # Maintains restore_filenames.txt so the change is reversible. # Usage: place this script in the target folder and run .\randomize_filename.ps1 $currentDir = (Get-Location).Path $restoreFile = Join-Path $currentDir 'restore_filenames.txt' Write-Host ("Folder: {0}" -f $currentDir) -ForegroundColor Cyan # ----- Restore mode: if a restore map exists, offer to undo first ----- if (Test-Path -LiteralPath $restoreFile) { $answer = Read-Host ("restore_filenames.txt detected. Restore original names? [Y/N]") if ($answer -match '^[Yy]') { $lines = Get-Content -LiteralPath $restoreFile -Encoding UTF8 $restored = 0 $failed = 0 foreach ($line in $lines) { $trimmed = $line.Trim() if (-not $trimmed -or $trimmed.StartsWith('#')) { continue } $parts = $trimmed -split "`t", 2 if ($parts.Count -ne 2) { Write-Warning ("Skipping malformed line: {0}" -f $line) $failed++ continue } $original = $parts[0] $current = $parts[1] $currentPath = Join-Path $currentDir $current $originalPath = Join-Path $currentDir $original if (-not (Test-Path -LiteralPath $currentPath)) { Write-Warning ("Source missing: {0}" -f $currentPath) $failed++ continue } if (Test-Path -LiteralPath $originalPath) { Write-Warning ("Target already exists: {0}" -f $originalPath) $failed++ continue } try { [System.IO.File]::Move($currentPath, $originalPath) Write-Host (" {0} -> {1}" -f $current, $original) $restored++ } catch { Write-Warning ("Failed: {0} ({1})" -f $current, $_.Exception.Message) $failed++ } } if ($failed -eq 0) { Remove-Item -LiteralPath $restoreFile -Force Write-Host ("`nAll {0} file(s) restored. Removed restore_filenames.txt." -f $restored) -ForegroundColor Green } else { Write-Host ("`nPartial restore: {0} succeeded, {1} failed. Keeping restore_filenames.txt." -f $restored, $failed) -ForegroundColor Yellow } return } Write-Host "Proceeding with rename (restore file will be overwritten)." -ForegroundColor Yellow } # ----- Rename mode ----- # Collect files. Skip ALL .ps1 files and the restore file itself. $allFiles = Get-ChildItem -LiteralPath $currentDir -File $files = $allFiles | Where-Object { $_.Extension -ne '.ps1' -and $_.Name -ne 'restore_filenames.txt' } if (-not $files) { Write-Host "No files to rename." -ForegroundColor Yellow return } Write-Host ("Found {0} file(s) to rename (excluded .ps1 and restore_filenames.txt)" -f $files.Count) # Digit count scales with file count so random collisions stay rare. $count = $files.Count $digits = [Math]::Max(4, [int][Math]::Ceiling([Math]::Log10($count * $count))) $min = [int][Math]::Pow(10, $digits - 1) $max = [int][Math]::Pow(10, $digits) - 1 Write-Host ("Using {0}-digit numbers ({1}..{2})" -f $digits, $min, $max) # Pre-compute unique new names. The "used" set includes EVERY name on disk # (including .ps1 files and the restore file, which we are NOT renaming), # plus every target name we pick, so collisions are impossible by construction. $used = @{} foreach ($f in $allFiles) { $used[$f.Name] = $true } $renameMap = [ordered]@{} foreach ($file in $files) { do { $num = Get-Random -Minimum $min -Maximum ($max + 1) $numStr = $num.ToString().PadLeft($digits, '0') $newName = "$numStr$($file.Extension)" } while ($used.ContainsKey($newName)) $used[$newName] = $true $renameMap[$file.FullName] = $newName } # Write restore file BEFORE renaming, so even partial renames leave us with a recoverable map. # Tab-separated: original<TAB>new (filenames only, no paths) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $headerLines = @( "# randomize_filename.ps1 restore map" "# created: $timestamp" "# directory: $currentDir" "# format: original`tnew (tab-separated)" "# comment lines starting with '#' are ignored on restore" ) $mappingLines = $renameMap.GetEnumerator() | ForEach-Object { $original = Split-Path $_.Key -Leaf "{0}`t{1}" -f $original, $_.Value } Set-Content -LiteralPath $restoreFile -Value ($headerLines + $mappingLines) -Encoding UTF8 # Apply renames. .NET Move() bypasses PowerShell wildcard parsing (safe for [ ] in names). $renamed = 0 $failed = 0 foreach ($source in $renameMap.Keys) { $newName = $renameMap[$source] $dest = Join-Path $currentDir $newName try { [System.IO.File]::Move($source, $dest) Write-Host (" {0} -> {1}" -f (Split-Path $source -Leaf), $newName) $renamed++ } catch { $failed++ Write-Warning (" Failed: {0} ({1})" -f $source, $_.Exception.Message) } } Write-Host ("`nDone: renamed {0}, failed {1}." -f $renamed, $failed) -ForegroundColor Green Write-Host ("Restore map saved to: {0}" -f $restoreFile) -ForegroundColor Cyan 清理空文件夹 # clear_empty_folders.ps1 # Delete every empty folder under the current directory. # A folder is "empty" if it contains no files and no subfolders (recursively empty). # Usage: place this script in the target folder and run .\clear_empty_folders.ps1 $currentDir = (Get-Location).Path Write-Host ("Scanning: {0}" -f $currentDir) -ForegroundColor Cyan $removed = 0 do { # -LiteralPath avoids wildcard parsing for paths containing [ ] etc. # -Force includes hidden/system items so truly empty dirs are caught. $emptyDirs = Get-ChildItem -LiteralPath $currentDir -Recurse -Directory | Where-Object { -not (Get-ChildItem -LiteralPath $_.FullName -Force) } if (-not $emptyDirs) { break } foreach ($dir in $emptyDirs) { # Never delete the directory the script is running from if ($dir.FullName -eq $currentDir) { continue } try { # .NET API: deletes only if the folder is actually empty [System.IO.Directory]::Delete($dir.FullName) Write-Host (" Removed: {0}" -f $dir.FullName) $removed++ } catch { Write-Warning ("Failed to remove {0}: {1}" -f $dir.FullName, $_.Exception.Message) } } } while ($true) Write-Host ("`nDone: removed {0} empty folder(s)" -f $removed) -ForegroundColor Green # clear_empty_folders.ps1 # Delete every empty folder under the current directory. # A folder is "empty" if it contains no files and no subfolders (recursively empty). # Usage: place this script in the target folder and run .\clear_empty_folders.ps1 $currentDir = (Get-Location).Path Write-Host ("Scanning: {0}" -f $currentDir) -ForegroundColor Cyan $removed = 0 do { # -LiteralPath avoids wildcard parsing for paths containing [ ] etc. # -Force includes hidden/system items so truly empty dirs are caught. $emptyDirs = Get-ChildItem -LiteralPath $currentDir -Recurse -Directory | Where-Object { -not (Get-ChildItem -LiteralPath $_.FullName -Force) } if (-not $emptyDirs) { break } foreach ($dir in $emptyDirs) { # Never delete the directory the script is running from if ($dir.FullName -eq $currentDir) { continue } try { # .NET API: deletes only if the folder is actually empty [System.IO.Directory]::Delete($dir.FullName) Write-Host (" Removed: {0}" -f $dir.FullName) $removed++ } catch { Write-Warning ("Failed to remove {0}: {1}" -f $dir.FullName, $_.Exception.Message) } } } while ($true) Write-Host ("`nDone: removed {0} empty folder(s)" -f $removed) -ForegroundColor Green ffmpeg 当前文件夹下取出所有mkv视频的中文字幕 Get-ChildItem -File *.mkv | ForEach-Object { $file = $_ $base = [System.IO.Path]::GetFileNameWithoutExtension($file.Name) $tracks = ffprobe -v error ` -select_streams s ` -show_entries stream=index:stream_tags=language ` -of json ` "$($file.FullName)" | ConvertFrom-Json $chineseTracks = $tracks.streams | Where-Object { $_.tags.language -match '^(zh|zho|chi|zh-cn|zh-hans|zh-tw|zh-hant)$' } $n = 1 foreach ($track in $chineseTracks) { $output = "$base.zh.$n.srt" Write-Host "提取: $($file.Name) -> $output" ffmpeg -y ` -i "$($file.FullName)" ` -map "0:$($track.index)" ` "$output" $n++ } } Linux Linux开辟虚拟内存(Swap) free -h Linux实现FRP内网穿透 的是让国内用户能连到一台没有公网 IP 的家庭电脑——用的是反向内网穿透: ...

2025-08-24 10:51:14 PM · 18 分钟
工具手册

SCA 2023.X — Nacos Bootstrap 配置失效问题排查与解决方案

Issue 来源:spring-cloud-alibaba#3931 影响版本:spring-cloud-alibaba 2023.0.1.3+ 一、问题描述 在 Spring Cloud Alibaba 2023.X 版本中,将 Nacos 配置(包括 extension-configs、shared-configs 等)放在 bootstrap.yml / bootstrap.properties 中,配置中心的内容无法正常加载,但日志显示 bootstrap 文件本身已被读取。 将相同配置移到 application.yml 后,一切恢复正常。 二、根本原因 flowchart TD A[bootstrap.yml 被读取] --> B{SCA 版本判断} B -- 2023.0.1.2 及以前 --> C[✅ 正常加载 Nacos 配置中心] B -- 2023.0.1.3 及以后 --> D[❌ extension-configs / shared-configs 失效] D --> E[问题根源:SCA 2023.0.1.3 修改了配置加载优先级机制] E --> F[bootstrap 阶段注册的 Nacos PropertySource 被后续流程覆盖或丢弃] 简单来说: SCA 2023.0.1.3 做了一次不向后兼容的内部变更,导致 bootstrap.yml 中的 Nacos 扩展配置在加载链路中被"丢掉",而 application.yml 中的配置走的是新路径,不受影响。 ...

2025-07-24 10:40:26 PM · 2 分钟
后端疑难杂症解答

Nacos 一些常用配置 更新中 

bootstrap 配置 bootstrap.yaml server: port: 10888 tomcat: uri-encoding: UTF-8 spring: profiles: active: dev application: name: archive-service cloud: nacos: config: file-extension: yaml shared-configs: - data-id: shared-spring.yaml refresh: false - data-id: shared-redis.yaml refresh: false - data-id: shared-mybatis.yaml refresh: false - data-id: shared-logs.yaml refresh: false - data-id: shared-feign.yaml refresh: false - data-id: shared-logs.yaml # 共享日志配置 refresh: false # - data-id: shared-feign.yaml # 共享feign配置 # refresh: false lia: jdbc: database: tb_archive bootstrap-dev.yaml spring: cloud: nacos: server-addr: 192.168.2.115:8848 # nacos注册中心 discovery: namespace: f923fb34-cb0a-4c06-8fca-ad61ea61a3f0 group: DEFAULT_GROUP ip: 192.168.2.115 logging: level: asia.liminality: debug nacos 配置 shared-spring.yaml spring: jackson: default-property-inclusion: non_null main: allow-bean-definition-overriding: true mvc: pathmatch: #解决异常:swagger Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException #因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher matching-strategy: ant_path_matcher shared-redis.yaml spring: redis: host: ${lia.redis.host:192.168.2.115} password: ${lia.redis.password:github} lettuce: pool: max-active: ${lia.redis.pool.max-active:8} max-idle: ${lia.redis.pool.max-idle:8} min-idle: ${lia.redis.pool.min-idle:1} max-wait: ${lia.redis.pool.max-wait:300} shared-mybatis.yaml mySQL spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${sh.jdbc.host:127.0.0.1}/${sh.jdbc.database}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: ${lia.jdbc.username:root} password: ${lia.jdbc.password:794211} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: logic-delete-field: deletedAt logic-not-delete-value: "null" logic-delete-value: "now()" id-type: assign_id insert-strategy: not_null update-strategy: not_null porstgreSQL spring: datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://${lia.jdbc.host:192.168.2.115}:${lia.jdbc.port:5432}/${lia.jdbc.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai username: ${lia.jdbc.username:postgres} password: ${lia.jdbc.password:github} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: logic-delete-field: deletedAt logic-not-delete-value: "null" logic-delete-value: "now()" id-type: assign_id insert-strategy: ignored update-strategy: ignored select-strategy: not_null shared-logs.yaml logging: pattern: dateformat: HH:mm:ss.SSS console: "%clr(%d{${LOG_DATEFORMAT_PATTERN}}){faint}-[${hostname}][%X{requestId:-sys}] %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n" file: "%d{${LOG_DATEFORMAT_PATTERN}}-[${hostname}][%X{requestId:-sys}]-${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n" file: path: "logs/${spring.application.name}" shared-feign.yaml feign: client: config: default: # default全局的配置 loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息 httpclient: enabled: true # 开启feign对HttpClient的支持 max-connections: 200 # 最大的连接数 max-connections-per-route: 50 # 每个路径的最大连接数 sentinel: enabled: true

2025-06-24 07:01:02 PM · 2 分钟

Wistia视频下载方法

自动插件(不维护) 此处粘贴Wistia播放器右键获取的视频链接: 解析视频 手动方法 在正在播放的视频上右键 选择“复制链接(Copy link)”。 从链接里找视频 ID 你会看到类似: wvideo=tra6gsm6rl 这里的 tra6gsm6rl 就是视频的 ID。 如果链接里没有,也可以: 打开网页源代码(view source),搜索: hashedId=tra6gsm6rl 打开嵌入页面 在浏览器里访问: http://fast.wistia.net/embed/iframe/ + 视频ID 比如: http://fast.wistia.net/embed/iframe/tra6gsm6rl 找真实视频文件地址 在打开的页面源代码里搜索: 优先找: "type":"original" 然后往下看一行,会有类似: "url":"http://embed.wistia.com/deliveries/xxxxx.bin" 如果没有 original,就找: "type":"hd_mp4_video" 下载视频 把找到的链接复制出来,把后缀从: .bin 改成: .mp4 然后直接打开或下载就行。 转载自:https://gist.github.com/szepeviktor/2a8a3ce8b32e2a67ca416ffd077553c5

2025-05-23 08:26:17 PM · 1 分钟

閾界事务所:一个异常事件爱好者的聚集地 更新中 

阈界事务所 · Backend 一个用于记录、归档和探索异常事件的微服务平台 项目简介 阈界事务所(YJSWS)是一个社区驱动的平台,用于收集、整理和探索各类异常与未解事件的记录。本仓库为项目后端,基于 Spring Boot 3 / Spring Cloud 微服务架构,提供用户认证、档案管理、评论互动、图片上传等核心能力。 核心功能 用户体系 — 手机号 + 密码注册、微信小程序登录、个人资料管理、收藏与浏览历史追踪 档案管理 — 丰富的档案 CRUD,支持结构化元数据(时间线、人物关系图、证据链、参考链接、标签) 评论系统 — 档案评论,通过服务间调用丰富用户信息 图片上传 — 基于 Cloudflare R2 对象存储,通过 AWS S3 SDK 访问 新闻模块 — 异常事件新闻,支持草稿/发布/下线生命周期(基础设施已就绪,服务开发中) API 网关 — 统一 JWT 鉴权、路由级访问控制、CORS 处理 服务发现与配置 — 基于 Nacos 的服务注册发现和集中配置管理 系统架构 ┌─────────────┐ │ 客户端 │ └──────┬──────┘ │ ┌──────▼──────┐ │ 网关 │ :10891 │ JWT 鉴权 │ └──────┬──────┘ │ ┌────────────────┼────────────────┐ │ │ │ ┌──────▼──────┐ ┌──────▼──────┐ ┌───────▼─────┐ │ 认证服务 │ │ 用户服务 │ │ 档案服务 │ │ Auth │ │User Service │ │ Archive │ │ :10890 │ │ :10889 │ │ :10888 │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ └────────────────┼────────────────┘ │ ┌──────────▼──────────┐ │ Nacos │ │ 服务发现 + 配置中心 │ │ :8848 │ └─────────────────────┘ 请求认证流程 所有请求经由 Gateway 网关,AuthGlobalFilter 校验 JWT Authorization 请求头 白名单路径(登录、注册、公开读取)免认证放行 JWT 校验通过后,网关提取 userId 并注入 X-User-Id 请求头 下游服务通过 UserInterceptor → UserContext(ThreadLocal<Long>)获取当前用户 服务间通信 服务间通过 OpenFeign 进行 RPC 调用,客户端接口定义在 api 模块中: ...

2025-05-16 10:14:38 AM · 5 分钟
灵异 前端开发 后端开发 面试官