[{"content":"又是新的一天，拆完10包卡0闪之后，我突然感到一阵不耐烦，然后又有一种突然悟道的感觉，以前100包没出闪的时候也没有这种感觉，果断市场补齐，合卡跑路。\n不过这都是后话了，事已至此，先合卡吧。\n先发点零散的拆卡记录，截图的没多少张。\n合完所有卡之后的展柜\n市场补齐之前的部分收集情况\n1、合卡合卡 蘑菇岛打工日记\n恋爱同居Location Love\n圣剑传说3 TRIALS of MANA\n勇者斗恶龙 创世小玩家2 破坏神席德与空荡岛\nSTARWHAL\nFox Hime Zero\nCorona Blossom-日冕之华- vol.1/2/3\n三部曲就合并一下\n2、关于拆卡 一年不到的时间，买了1000+袋宝石，为了省点钱什么路子都用上了。\n有的时候某宝某海鲜便宜，我就直购\n有的时候倒余额便宜，那就倒余额，一开始DOTA2没7天CD，有个7折8折我就上车了，后来DOTA2也上7天锁了，我就倒贴纸，major期间买一库存几毛钱的贴纸，批量出售能干到4折5折，结果V社直接拉爆了低价商品的手续费，也没法玩了，只能寄希望于看不到未来的挂刀。\n有的时候TF钥匙兑宝石比例高，买点TFKEY想换，结果人家机器人压根没那么多库存，还得等它补货\n直购的时候，我清楚的记得一年前一袋宝石是两块出头，现在已经3.6一袋了，涨了我还能不买吗，那沉没成本怎么办。\n还有这steam的拆卡机制，我想吐槽的太多了。\n做包要买游戏我就不说了，就当交入场费了。一天只能做一包，我这一辈子才能活多少天？你的闪卡概率是1%，不是10%啊，我一开始是抱着每天随缘拆一包，不强求的心态去拆的，问题是我一个月不出卡，两个月不见闪，三个月蓝天白云，手指点一下又要吃24小时CD，我不是圣人，我真的要生气的。\n为什么我总是出便宜的卡，我一直幻想开几张贵的，歪几张便宜的，再市场补一两张美美撤离，可我总是出那几张便宜的卡，然后剩一张市场价三百五百的卡绝望八等一，在我没疯之前我会直接买吗，市价五百那可是整整一百袋宝石啊，一百袋宝石我能开多少天，出多少卡？\n概率这么低能出重复卡，你都这么设计了，这门槛已经不可谓不高了，就不能不出重复卡吗，重复的普卡确实可以交换一下，可这闪卡我一个游戏有可能一个月就出一张，这一张还和上个月出的一样，我宁愿它不出也不想要这种看到重复闪卡的崩溃。\n3、关于买卖卡 正所谓庙小妖风大，池浅王八多，就这些闪卡的买家卖家，反正我也市场补齐了，我向他们所有人投降了。\n卖卡\n我每天十块十块地降价，你一毛一毛地压我，你的脚本很智能，你的品性很高尚，我下架不卖了你也不卖了，我丢求购了你反手涨价50，可是大哥我还有三张在库里，我真处理不掉了，我该怎么办，干脆我把你的买了吧。\n我知道上个月你买断了所有20块以下的卡，因为那五张卡都是我一个人挂的，求购已经不够我丢了，你现在摆80一张，你太有经济头脑了，我祝你好运。\n有一张闪卡售价100，求购最高95，兄弟你身份证掉了，你以为这游戏冷门到没人拆卡能抬一手吗，我有，谢谢你帮我解套。\n蘑菇岛打工日记，锁区游戏，普通卡一开始一张20，我挂6块你也收，我挂9块你也收，你是不是以为我挂卡挂出来的这两张卖完了，我后面还有五十多张，我每天都上架，我多希望你可以坚持收下去，继续做你6块收20块卖的美梦，这样我现在就不用丢那个五毛钱的求购了。\n买卡\n我知道你是三年前花五块钱买的，这三年来一有十几块的卡你就拿下，求购的七块八块都是你，你挂了三年500没有一天降过价，我投降了，这套卡我不集了，你垄断吧。\n那些我不得不买的闪卡，久等了，因为我终于看清拆卡这件事的无聊，无趣，无奈，我要为我一年前走进这个坑付出代价了。\n没有任何乐趣反馈的拆卡机制，落后，呆板，低效。\n没有任何人情味的闪卡圈子，没人玩，没人买，没人卖，孤独的守望。\n4、最后 要感谢3位在steam卡片交换区的朋友，一年来风雨无阻的为我做包，卡无情人有情，山水有相逢。\n","date":"2026-04-16T13:48:34+08:00","image":"https://Gravity2000.github.io/p/%E5%90%88%E4%B9%9D%E5%A5%97%E9%97%AA%E5%BE%BD%E9%BA%BB%E6%9C%A8%E5%9C%B0%E6%8B%86%E4%BA%86%E4%B8%80%E5%B9%B4%E9%97%AA%E5%8D%A1%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B/images/QQ20260416-144448_hu_c1ab05e4e6adfaec.png","permalink":"https://Gravity2000.github.io/p/%E5%90%88%E4%B9%9D%E5%A5%97%E9%97%AA%E5%BE%BD%E9%BA%BB%E6%9C%A8%E5%9C%B0%E6%8B%86%E4%BA%86%E4%B8%80%E5%B9%B4%E9%97%AA%E5%8D%A1%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%8B/","title":"合九套闪徽，麻木地拆了一年闪卡，记录一下"},{"content":"昨天在railway部署的时候服务器波动了，半小时没能部署上，期间用railway的agent咨询发现它可以提取我的当前部署进度和多个项目的运行状态，根据railway的服务器常见问题，用户须知和操作守则来回答，对用户来说比人工客服更具专业性和快速性，非常实用。\n根据它的回答我查了railway\u0026rsquo;s status page果然当天服务器出了问题导致长时间部署不上，因此也避免了长时间的干等。\n今天也在我的个人博客右下角搭了一个小AI，想让它根据我的博客内容回答问题。\n1、模型选取 我自己本身只有claude的API，但是直接在网站接入claude非常不必要，首先博客网站不管是用户访问还是我自己把玩都不会遇到很技术性的问题，使用claude作为接口完全是大材小用；其次我自己使用下来claude虽然模型强大但是思考时间是比较长的，当我提出非技术性问题的时候我希望的是快速响应而不是深思熟虑，因此使用手上现有的API不是一个好的选择。\n为什么使用deepseek？\n在使用deepseek网页版的时候我发现deepseek对于简单问题的响应非常快，采取非思考模式几乎是秒答，deepseek支持128K的上下文，对于博客小AI来说绰绰有余，最后最重要的是价格对比。\n使用20美刀/月的claude，如果未来有更好的大模型，我不可能会坚持花每月20刀的价格去维持一个简单的博客AI，而deepseek采取token计费制，可以看到我请求了10次仅花费0.01元，10元预计可以再请求至少成千上万次的对话，未来的维护成本可以说极低，即使替换大模型也仅亏损个位数的token费用，是非常适合轻量基础小网站的。\n2、后端请求deepseek 从deepseek platform创建一个API Keys，用于hugo博客。\n用户从博客网站请求deepseek回答，这里有两种请求方案\n前端通过JavaScript请求 通过后端部署的网页请求 如果通过前端JavaScript请求，那请求时提交的deepseek的APIKEY就会暴露在请求体中，任何人通过F12查看网页元素都会找到这串API，这种暴露密钥的方式是极其不安全的。\n如果通过请求接口的部署网站，那么JavaScript就会发请求给一个网址，这里就不会暴露API，而另一个请求deepseek的网址APIKEY是通过环境变量的方式保存的，因此也不会暴露，但是我们从博客A.com，访问部署了deepseek访问接口的B.com，就会出现浏览器跨域请求，浏览器会拦截这个请求，因此在python中需要导入解除浏览器跨域请求的corsmiddleware\n1 2 3 4 5 6 7 8 9 10 11 12 13 app=FastAPI() app.add_middleware( CORSMiddleware, allow_origins=[\u0026#34;*\u0026#34;], allow_methods=[\u0026#34;*\u0026#34;], allow_headers=[\u0026#34;*\u0026#34;], ) add_middleware是一个浏览器里请求进出的拦截件，CORSMiddleware解除跨域限制，allow_origins表示允许哪些域名访问，allow_methods表示允许哪些HTTP方法（GET\\POST\\PUT\\DELETE）,allow_headers表示允许哪些请求头，这里都设置为全部允许。\n与大模型的对话有两个字段，由谁说，说了什么，而一段新的对话都会被添加到之前所有对话的末端，形成一个超长的消息列表，这也就是大语言模型的上下文。\n1 2 3 4 5 6 class Message(BaseModel): role: str content: str class ChatRequest(BaseModel): messages: List[Message] 利用BaseModel规定了两个类，BaseModel规定了发送数据的格式，Message是一条对话的格式，ChatRequest是deepseek收到的包含了上下文对话的消息列表，根据这段消息列表，deepseek会作出回答。\n上下文越多，token的消耗就越多，但在这个项目中不需要超长上下文，只需要限制一下历史消息的条数控制token的消耗就行了。\n最后是对deepseek会话的请求体，采用异步请求的原因是这类AI往往会出现同一时刻有不同用户请求的情况，如果不异步，那排在最后的用户会等前面所有用户请求完才收到答案，而异步则可以规避这类问题。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from openai import AsyncOpenAI client = AsyncOpenAI( api_key=os.getenv(\u0026#39;DEEPSEEK_API_KEY\u0026#39;), base_url=\u0026#34;https://api.deepseek.com\u0026#34; ) @app.post(\u0026#34;/chat\u0026#34;) async def chat(request: ChatRequest): response = await client.chat.completions.create( model=\u0026#34;deepseek-chat\u0026#34;, messages=[m.dict() for m in request.messages], stream=False ) return {\u0026#34;choices\u0026#34;: [{\u0026#34;message\u0026#34;: {\u0026#34;content\u0026#34;: response.choices[0].message.content}}]} 这段写法来自deepseek的官方API文档\n这里把之前的消息列表转换成了字典的格式上传，并不是单纯的绕远路，如果对话过程中出了错误，消息列表能快速定位到报错的位置。\n3、如何让AI助手根据博客内容回答问题 将接口部署后配置在博客页面，如果直接问，这时deepseek是一无所知的。\n最简单的方式是通过 System Prompt 注入博客信息。\n每次对话开始前，我们会在消息列表的第一条插入一段\u0026quot;系统提示词\u0026quot;，告诉 AI 它的身份和背景知识：\n1 2 3 4 5 6 7 8 9 10 11 const SYSTEM_PROMPT = `你是一个个人技术博客的 AI 助手。 关于这个博客： - 博客地址：gravity2000.github.io - 博主：计算机科学与技术专业毕业，男生 - 主要内容：技术项目开发记录、游戏攻略、Steam 闪卡收集 技术项目： - Steam 冷门闪卡价格监控系统：Python + FastAPI - 待办事项 REST API：Python + FastAPI + SQLite`; #添加所有想让AI知道的信息 System Prompt 只能包含你手动写进去的内容，AI 不会自动爬取博客。所以当被问到博客之外的问题时，AI 仍然会用自己的通用知识回答，比如\u0026quot;南非的首都在哪里\u0026quot;，它会正确回答。\n博客相关的问题用 System Prompt 回答，其他问题用 AI 的通用知识回答，两者互补。\n","date":"2026-04-07T20:14:35+08:00","image":"https://Gravity2000.github.io/p/%E5%A6%82%E4%BD%95%E5%9C%A8%E7%BD%91%E9%A1%B5%E9%85%8D%E7%BD%AE%E4%B8%80%E4%B8%AAai%E5%B0%8F%E5%8A%A9%E6%89%8B%E6%A0%B9%E6%8D%AE%E7%BD%91%E7%AB%99%E5%86%85%E5%AE%B9%E8%A7%A3%E7%AD%94%E9%97%AE%E9%A2%98deepseek%E6%A8%A1%E5%9E%8B/images/1_hu_8fd3621079ca9898.png","permalink":"https://Gravity2000.github.io/p/%E5%A6%82%E4%BD%95%E5%9C%A8%E7%BD%91%E9%A1%B5%E9%85%8D%E7%BD%AE%E4%B8%80%E4%B8%AAai%E5%B0%8F%E5%8A%A9%E6%89%8B%E6%A0%B9%E6%8D%AE%E7%BD%91%E7%AB%99%E5%86%85%E5%AE%B9%E8%A7%A3%E7%AD%94%E9%97%AE%E9%A2%98deepseek%E6%A8%A1%E5%9E%8B/","title":"如何在网页配置一个AI小助手，根据网站内容解答问题（deepseek模型）"},{"content":"攻略发表在小黑盒\n1、晒个大师证和战绩 云顶下多了看到大师就想上，没变阵前卡首席输了快有三十把恶魔狼，对策完一路连胜上大师了。阵容博弈点巨多。\n2、配队思路 三个精灵需配置血脉，翼龙武，骨龙冰，巨噬针鼹冰。带愿力强化，全队如下。\n翼龙\n龙威工具人，龙威是我们的唯一增益，翼龙龙威必须放在一号位，在主动切换翼龙的时候白嫖一层龙威印记，感觉对手要切了或者要放一些应对攻击的技能可以主动切，尝试抢一层印记，极少数情况下会打残局。因为开完龙威之后可以偷袭（应对状态）打对面开状态，也可以防御（应对攻击）消耗对手能量，也可以直接切走，如果直接切走的话后期打到弹尽粮绝很有可能拉上来打残局。龙炮使用场景很灵活，如果感觉对面要切，打一发龙炮比偷袭好一点点。\n武系血脉很吃香，克制很多主流增益位，可以愿力冲击打对面个出其不意。\n寂灭骨龙\n残局超人，身板巨厚，自带龙威，如果感觉对手要切/要防可以开一层龙威试试，因为死后会在三回合后满血满能复活，该送的时候一定要送，比方说骨龙残血在背包，下面要吃一手大的，可以切出来送死。\n翼王/独角兽等对策手，满血骨龙吃两发折射是不死的，在第二发的时候打吓退可以请离对面独角兽，翼王玩的人少，热身+三连发之后基本必攻击，可以吓退请离。\n偷袭极好用，假设通过翼龙开过一层龙威，骨龙可以就偷袭上双攻了。残局的神，打到最后一只满血骨龙上场对面直接绝望。\n冰系血脉和翼龙同理，偷对面上状态。\n帕帕斯卡\n万金油般的角色，主动切出来自动放一号位技能简直夯爆了，齿轮和偷袭都可以触发龙威，齿轮打完偷袭变成一号位，下次切出来可以打对面上状态，没有龙威印记也能切出来收对面残血，防御消耗对面能量。\n巨噬针鼹\n这套阵容永远的首发位，地刺拿信息，黑猫/翠顶夫人/千棘盔/原号鱼等等全部强制猜拳，地刺打状态，当头棒喝打对面切，聚气/切精灵打泡沫幻影，防御应对攻击，找时机可以切翼龙上龙威或者上蜘蛛等等。受环境影响切蜘蛛的场次很多。\n基本开局领先，对面根本不知道我玩的是什么，而我已经把对面猜了个七七八八。\n芋香巨角蛛\n独角兽/翼龙等主要对策手，满血蜘蛛吃独角兽两发折射不会死，第二发折射直接晒太阳，独角兽增益归零且吃四层毒，可以防御叠毒或者切。在压力小的时候可以扬沙叠毒，在有四层毒的情况下毒液渗透费用为一，可以打波爆发。\n海豹船长\n专门对策恶魔狼，一般最后上，打其他阵容也有妙用，前面五只带了大量的应对技能，打到残局起码也有三四层身经百炼了，细节非常之多。\n对手恶魔狼一般最后一手上，**绝对不能主动切海豹！**如果魔力值多，可以上替死鬼消耗一下恶魔狼的能量，没命了也必须等场上的精灵死了再切海豹。\n强制与恶魔狼猜拳，听桥/硬门猜攻击，斩断猜聚气，聚气猜防御，对面血量极低可以先发制人，武系本身对恶魔狼有克制有抗性，吃对面一发死不了。\n前面打得好的话，最后海豹面对的一般是残血或残能恶魔狼，猜拳优势在我。\n3、一些常见开局 对面黑猫开，地刺压制，求稳就地刺到底，高分段很多人吃一发地刺就放弃强化了，可以防御应对魔能爆，如果预感要挨彗星了，切帕帕斯卡就行，伤害很低。尽量别让对手一换一，黑猫注定要死，我们只要不死就赚了。后续灵活。 对面翠顶夫人/圆号鱼/千棘盔开，地刺压制就行，也可以当头棒喝猜对面切，或者聚气猜对面泡沫幻影，如果对面开出强化了，切蜘蛛/骨龙，准备晒太阳/吓退吧。 对面独角兽开，直接切蜘蛛晒太阳。 对面伊雷龙开，局数太少记不清了，地刺就完事。 各种诡异开局，绝大多数情况根本不知道对面带的啥，我经常地刺一两发切翼龙开龙威去了。 4、总结 阵容除了巨噬针鼹首发位之外切牌及其灵活，没有绝对的公式，一些经验是\n绝对不能贪龙威增益，比如对面上画间沉铁兽哪怕只有三层双攻也要切走，灵活切，比如上个蜘蛛叠毒什么的。 博弈技能很多，要多猜，多应对，尤其是防御和偷袭，这样最后上场的海豹技能威力才高。 要多切，会切帕帕斯卡能给对面很大压力，必死的残血切走少扣一点魔力值，有的时候骨龙都可以切走。 打毒队对面上毒系就尽量不要切蜘蛛了。 说实话到现在属性克制都是对局里点开信息栏看的，根本记不住，170把才上大师肯定有点多了，龙威感觉相当冷门，被什么阵容暴打就找对策，开服三天上到大师已经很满意了。\n","date":"2026-03-30T05:40:06+08:00","image":"https://Gravity2000.github.io/p/pvp%E6%81%B6%E9%AD%94%E7%8B%BC%E7%8B%AC%E8%A7%92%E5%85%BD%E7%BF%BC%E9%BE%99%E6%AF%92%E5%86%B0%E5%85%A8%E6%9C%89%E5%AF%B9%E7%AD%96%E4%B8%80%E5%A5%97%E9%98%B5%E5%AE%B9%E4%B8%8A%E5%A4%A7%E5%B8%88/images/1_hu_880bd8250a2c4bb0.png","permalink":"https://Gravity2000.github.io/p/pvp%E6%81%B6%E9%AD%94%E7%8B%BC%E7%8B%AC%E8%A7%92%E5%85%BD%E7%BF%BC%E9%BE%99%E6%AF%92%E5%86%B0%E5%85%A8%E6%9C%89%E5%AF%B9%E7%AD%96%E4%B8%80%E5%A5%97%E9%98%B5%E5%AE%B9%E4%B8%8A%E5%A4%A7%E5%B8%88/","title":"【PVP】恶魔狼独角兽翼龙毒冰全有对策，一套阵容上大师"},{"content":"收集完成，已于2026年4月16日起停止记录\n从25年8月开始开卡，出闪高兴几秒截个图，零零碎碎截了不少，干脆全整理在这里。\n1、Corona blossom vol.1(已集齐) 开6买2\n2、Corona blossom vol.2(已集齐) 开7买1\n3、Corona Blossom vol.3 (7/8) 4、 Location Love （4/6） 5、蘑菇岛打工日记 （5/6） 6、Momoiro Closet (6/8) 7、CONERU -DIMENSION GIRL- （3/15） ","date":"2026-03-25T16:57:49+08:00","image":"https://Gravity2000.github.io/p/%E9%97%AA%E5%8D%A1%E6%94%B6%E9%9B%86%E8%AE%B0%E5%BD%95/images/%E5%B0%81%E9%9D%A2_hu_2b9d81a355fbaf59.png","permalink":"https://Gravity2000.github.io/p/%E9%97%AA%E5%8D%A1%E6%94%B6%E9%9B%86%E8%AE%B0%E5%BD%95/","title":"闪卡收集记录"},{"content":"1、引言 steam的闪亮徽章收集一直是个漫长又看不到尽头的过程，一枚徽章由5-15张不同的闪卡合成，一张闪卡的出卡率为1%（网传），而steam限定用户每天只能通过补充包制作三张随机卡牌，又要搏百分之一的概率出闪，又要避免重复，即便运气好，闪卡5缺1，8缺2，10缺3..也是常态了。\n上图两套闪卡的收集已经跨越将近8个月的时间，纵使想要通过steam市场快速补齐剩下的闪卡，可是这种冷门卡往往超低库存价格离谱，或者干脆没有库存，让人丧失购买欲望。\n有心栽花花不开，不过每年的四季大促、折扣节日都会有数以万计的闪卡流入市场，平时也有大量闪卡从同样收集徽章的账户中产出上架，但普通人不可能每天都去检索大量冷门闪卡，难点是\n闪卡不是主流市场元素，检索复杂，筛选条件苛刻 市场不提供“收藏”的模块，不能一键获取所有目标商品，操作量成倍增长 受网络和请求大量无关元素影响，返回价格速度较慢 收卡本来就是一个任重而道远的过程，财力有限的情况下，集不集得齐主要看缘分，这种时候有心插柳一下，也许能让缘分来得快一点。\n2、效果速览 2.1 概览 项目部署在steam卡牌价格监控，Github链接。完整的一个卡片监测页面，点击刷新价格可以快速获取不同收藏卡的实时价格，后端每30分钟会自动请求一次，若监测到价格低于设定期望值，就发送邮件提醒。\n2.2 完整操作 首先注册邮箱是用于接收价格邮件的，一旦监测到卡牌的价格低于设定的期望值，Railway会通过resend邮件发送到这个邮箱，一开始即使不输入也可以注册，用于单纯的一键获取价格，后续也可以添加邮箱地址用于价格提醒。\n准备好需要监测的商品，打开商品页，复制后缀。\n点击添加卡牌，目前仅支持添加steam集换式卡牌，即商品类目为753。\n后缀无误的话会提取该卡的价格和卡面，可以设定一个期望价格，这里不妨输入120。\n如果邮箱设置并且无误的话，那就会收到一封由resend邮箱转发的提醒邮件了。\n3、接口获取 3.1 价格接口 监测闪卡一共要请求两个数据\n闪卡价格 闪卡卡面 闪卡的卡面以灰色为底，而普卡以蓝色为底，如果同时监测闪卡和普卡，可以快速定位和区分，同时由不同开发商设计的卡面风格多样，颇具美观度。\n在Github上有开发者整理了大量有关steam接口的资料，比如Steam接口整理提供了有关价格和卡面接口的相关信息，但我们要怎么精准找到需要的接口参数呢？随便找三张闪卡的链接为线索\nhttps://steamcommunity.com/market/listings/753/2600700-HAPPY%20HALLOWEEN%20%28Foil%29\nhttps://steamcommunity.com/market/listings/753/2600700-THUNDER%20VOLT%20%28Foil%29\nhttps://steamcommunity.com/market/listings/753/2600700-MILKY%20WAY%20%28Foil%29\n观察这三个案例，可以明确闪卡的共同路径都要经过https://steamcommunity.com/market/listings/753，而在Steam接口整理中与market相关的有如下几个接口\nGET /market/itemordersactivity GET /market/itemordershistogram GET /market/listings GET /market/myhistory GET /market/mylistings GET /market/popular GET /market/pricehistory (*) GET /market/priceoverview GET /market/recent GET /market/search/render 一共就十个接口，其实一个个点进去看Response就能找到价格的接口了，不过因为命名还是可以直接挑几个明显的，比如和我们的三个链接最相关的/market/listings，看看它的Request是怎么写的。\nAuthenticated: No\nMethod: GET\nHost: steamcommunity.com\nPath: /market/listings/:appid/:hashname/render\nQuery Parameters:\nName Type Required Description currency number No Currency ID used for listing prices. Example: 1 – USD, 3 – EUR. start number No Index of the first listing to return. Use 0 to start on the first (cheapest) listing. The endpoint responds with 10 listings if there are at least 10 available. 跟我们的三个链接基本不谋而合，在后面加上/render，两个参数currency代表货币编号，人民币在steam的编号是23,strat就按description里说的设置为0就好。\n我们查第一张卡片\nhttps://steamcommunity.com/market/listings/753/2600700-HAPPY%20HALLOWEEN%20%28Foil%29\nappid是753，hashname是2600700-HAPPY%20HALLOWEEN%20%28Foil%29，最后拼在一起。\nhttps://steamcommunity.com/market/listings/753/2600700-HAPPY%20HALLOWEEN%20%28Foil%29/render/?currency=23\u0026start=0\n看看返回,先记住这张卡的人民币价格为119.98元，然后尽量找这个值就行。\n1 2 3 4 5 6 import json import requests res=requests.get(\u0026#34;https://steamcommunity.com/market/listings/753/2600700-HAPPY%20HALLOWEEN%20%28Foil%29/render/?currency=23\u0026amp;start=0\u0026#34;) data=res.json() print(json.dumps(data, indent=4, ensure_ascii=False)) 重点肯定是看price相关的键了，比如下面这几个连续的\n\u0026ldquo;steam_fee\u0026rdquo;: 65, \u0026ldquo;publisher_fee\u0026rdquo;: 131, \u0026ldquo;converted_price\u0026rdquo;: 10434, \u0026ldquo;converted_fee\u0026rdquo;: 1564,\n很遗憾的是没有找到119.98元这个值，因为根据参考文档里的Response，能代表价格的字段值都已经超过一万了。\nName Type Description listinginfo.:listingid.converted_price number Price converted to the requested currency. listinginfo.:listingid.converted_fee number Converted total fee. 这其实是一种舍近求远的方法，steam的商品要对卖家要收取一定的税，而税后到手价+税费=我们需要的价格，这里分别是10434+1564=11998，steam对人民币的单位采取的是分制！，而11998分要除100才是我们需要的价格119.98。\n仅凭观察字段和这篇英文文档实在是很难得出这个结论了。\n不过只要再看几个接口就能迅速解决这个问题，以最可疑的\nGET /market/priceoverview\n入手，展开Request和Response\nRequest\nAuthenticated: No\nMethod: GET\nHost: steamcommunity.com\nPath: /market/priceoverview/\nQuery Parameters:\nName Type Required Description country string No Country code currency string No Currency (default: $) appid string Yes Steam App ID market_hash_name string Yes Item Name Response\nName Type Description success boolean Whether or not the request was successful lowest_price string The current lowest price at which the item is listed volume string The number of items that has been sold in the last 24 hours (does not exist if volume = 0) median_price string The current median price of current listings 路径拿到了，appid和market_hash_name我们都有，country先不输入，如果结果有异常再补齐，currency默认是美元，改成人民币编号23。还是以刚才那个商品为例，补齐请求链接后为\nhttps://steamcommunity.com/market/priceoverview/?currency=23\u0026appid=753\u0026market_hash_name=2600700-HAPPY%20HALLOWEEN%20%28Foil%29\n1 2 3 4 5 6 7 8 9 10 11 import json import requests res=requests.get(\u0026#34;https://steamcommunity.com/market/priceoverview/?currency=23\u0026amp;appid=753\u0026amp;market_hash_name=2600700-HAPPY%20HALLOWEEN%20%28Foil%29\u0026#34;) data=res.json() print(json.dumps(data, indent=4, ensure_ascii=False)) 看一下返回，\n1 2 \u0026#34;success\u0026#34;: true, \u0026#34;lowest_price\u0026#34;: \u0026#34;¥ 119.98\u0026#34; 拿到人民币价格了，但是类型是String，需要去掉人民币符号¥和一个空格后化为Float，volume和median_price是因为这款闪卡最近没有成交，所以不返回成交量和成交中位数，其它有成交的卡片都是返回的，虽然这两个返回值我们用不上，但还是在接口返回里保留一下，为以后的功能拓展留个空间。\n最终这个价格接口的代码就如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def get_card_info(card_name:str): #获取价格 price_url=\u0026#34;https://steamcommunity.com/market/priceoverview/\u0026#34; price_params={ \u0026#34;appid\u0026#34;:753, \u0026#34;currency\u0026#34;:23, \u0026#34;market_hash_name\u0026#34;:card_name } try: price_res=requests.get(price_url,params=price_params,timeout=10) price_data=price_res.json() if price_data.get(\u0026#34;success\u0026#34;)==True: #保留一个String类型，用于给用户显示货币符 lowest_price_str = price_data.get(\u0026#34;lowest_price\u0026#34;, \u0026#34;无数据\u0026#34;) try: #String转float类型 lowest_price_float = float(lowest_price_str.replace(\u0026#34;¥ \u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;,\u0026#34;, \u0026#34;\u0026#34;)) except: lowest_price_float = None return { \u0026#34;success\u0026#34;:True, \u0026#34;lowest_price\u0026#34;:lowest_price_str, \u0026#34;lowest_price_float\u0026#34;:lowest_price_float, \u0026#34;volume\u0026#34;:price_data.get(\u0026#34;volume\u0026#34;,\u0026#34;无数据\u0026#34;), \u0026#34;median_price\u0026#34;:price_data.get(\u0026#34;median_price\u0026#34;,\u0026#34;无数据\u0026#34;), } return {\u0026#34;success\u0026#34;:False,\u0026#34;message\u0026#34;:\u0026#34;找不到该商品\u0026#34;} except Exception as e: return {\u0026#34;success\u0026#34;:False,\u0026#34;message\u0026#34;:str(e)} 最后留意一下这个接口文档里的请求速率限制：\n20 / Per minute\n1000 / Per day\n在后台的请求暂定为30分钟一次，完全达不到这个速率，所以不必担心。\n3.2 闪卡图片接口 Steam接口整理没有提供直接的图片接口，在上文解包Path:/market/listings/:appid/:hashname/render的时候能看到有几个跟图片URL相关的返回：\n1 2 3 4 5 6 7 \u0026#34;icon_url\u0026#34;: \u0026#34;IzMF03bk9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdA3g5gMEPvUZZEfSMJ6dESN8p_2SVTY7V2NsJxHMLmD4QPivs0XEwTcRcCPKN6CTXjc26OVHZLj7JLibcQQc6T7UMMTrQqjGh5u2dFjjOEugsQg0DfaJX9mJAa5-KbURs14QOqGf2h0p6WBQnYMFDYjCyx3UUNOB0zndKdJoDzSP5cpeM011nPk5tX7jvALjAZ4mkxSdyW0w0TKkZZt-QpmWyr4G3Z_XdDPMolA\u0026#34;, \u0026#34;icon_url_large\u0026#34;: \u0026#34;IzMF03bk9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdA3g5gMEPvUZZEfSMJ6dESN8p_2SVTY7V2NsJxHMLmD4QPivs0XEwTcRcCPKN6CTXjc26OVHZLj7JLibcQQc6T7UMMTrQqjGh5u2dFjjOEugsQg0DfaJX9mJAa5-KbURs14QOqGf2h0p6WBQnYMFDYjCyx3UUNOB0zndKdJoDzSP5cpeM011nPk5tX7jvALjAZ4mkxSdyW0w0TKkZZt-QpmWyr4G3Z_XdDPMolA\u0026#34;, \u0026#34;icon\u0026#34;: \u0026#34;https://cdn.fastly.steamstatic.com/steamcommunity/public/images/apps/2600700/fbdb73489414b6c73a8ba30a93398244744bacd3.jpg\u0026#34;, \u0026#34;app_icon\u0026#34;: \u0026#34;https://cdn.fastly.steamstatic.com/steamcommunity/public/images/apps/753/1d0167575d746dadea7706685c0f3c01c8aeb6d8.jpg\u0026#34;, 后两个直接访问一下，一个是steam的平台logo，一个是闪卡对应游戏的logo，前两个需要找一下前后缀，打开商品页，F12检查一下图片的源链接。\n可以直接定位到元素引用的位置，看当前源\n点进去看一下有没有做反爬虫，如果有的话，要考虑找请求头或者加Cookie的方法。\nhttps://community.fastly.steamstatic.com/economy/image/IzMF03bk9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdA3g5gMEPvUZZEfSaKqhBT8kyvCOETZfYgdQNwnMMnz4GbDbxyXhoOM1NCPv_iy3ajc6iK1HSRnHNIiDeGQMxT7FYZ2yL92en4e6US2zJRrssQwgAeqIEoWBPNJ_fPEBrhYQI_ma62VRzGVAqfddCdR2Ew3kSNrh4mSdEfZpTyHD3L8Da0AlgbRBiXb3nXurGbYmtkXosCxwxTPMTZ4nBvTuspsDnLPqEkvdNbyE/330x192?allow_animated=1\n访问后是直接返回图片的，不放心的话可以多找几个商品页试一下，所需要的所有卡面的共同前缀是\nhttps://community.fastly.steamstatic.com/economy/image/\n把icon_url和icon_url_large拼上去\nhttps://community.fastly.steamstatic.com/economy/image/IzMF03bk9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdA3g5gMEPvUZZEfSMJ6dESN8p_2SVTY7V2NsJxHMLmD4QPivs0XEwTcRcCPKN6CTXjc26OVHZLj7JLibcQQc6T7UMMTrQqjGh5u2dFjjOEugsQg0DfaJX9mJAa5-KbURs14QOqGf2h0p6WBQnYMFDYjCyx3UUNOB0zndKdJoDzSP5cpeM011nPk5tX7jvALjAZ4mkxSdyW0w0TKkZZt-QpmWyr4G3Z_XdDPMolA\nhttps://community.fastly.steamstatic.com/economy/image/IzMF03bk9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdA3g5gMEPvUZZEfSMJ6dESN8p_2SVTY7V2NsJxHMLmD4QPivs0XEwTcRcCPKN6CTXjc26OVHZLj7JLibcQQc6T7UMMTrQqjGh5u2dFjjOEugsQg0DfaJX9mJAa5-KbURs14QOqGf2h0p6WBQnYMFDYjCyx3UUNOB0zndKdJoDzSP5cpeM011nPk5tX7jvALjAZ4mkxSdyW0w0TKkZZt-QpmWyr4G3Z_XdDPMolA\n访问后返回的图片是一样大小的（实际上链接也一样），应该是因为商品页只需要一张图片，小图的URL是用于市场检索列表的，我们只需要一张大图就行，大小适配都可以在前端调试。\n这样三件事就明确了\n要请求的接口是steamcomminuty.com/market/listings/:appid/:hashname/render，参数是currency=23和start=0 要爬取的是Assets包下的icon_url，它的路径嵌套是 \u0026ldquo;assets\u0026rdquo;: { \u0026ldquo;753\u0026rdquo;: { \u0026ldquo;6\u0026rdquo;: { \u0026ldquo;37456232637\u0026rdquo;: {\u0026ldquo;icon_url\u0026rdquo;: icon_url要拼凑到https://community.fastly.steamstatic.com/economy/image/后面，返回的就是图片链接了 请求网页json代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requests listing_url = f\u0026#34;https://steamcommunity.com/market/listings/753/{card_name}/render\u0026#34; listing_params={ \u0026#34;start\u0026#34;: 0, #人民币编号=23 \u0026#34;currency\u0026#34;: 23, #\u0026#34;format\u0026#34;: \u0026#34;json\u0026#34; }\tlisting_res=requests.get(listing_url,params=listing_params,timeout=10) listing_data=listing_res.json() 提取icon_url并拼凑代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 image_url=None assets=listing_data.get(\u0026#34;assets\u0026#34;,{}).get(\u0026#34;753\u0026#34;,{}).get(\u0026#34;6\u0026#34;,{}) if assets: first_asset=list(assets.values())[0] icon=first_asset.get(\u0026#34;icon_url\u0026#34;) if icon: image_url=f\u0026#34;https://community.fastly.steamstatic.com/economy/image/{icon}\u0026#34; 这里要注意上文的37456232637不是一个固定的键，每张卡面的ID是不同的，所以在嵌套提取到“6”的时候就该停止了，并且我们不能保证6的内层只有37456232637这一个字典，所以要把assets当作一个列表，获取其中的第一个字典（因为所有卡面37456232637这样的ID都是是第一个），assets.values()返回的是所有字典，将它转化为list列表之后提取第一个字典即可，然后才能最終获得icon_url的值。\n完整代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import requests def get_card_info(card_name:str): #获取价格 price_url=\u0026#34;https://steamcommunity.com/market/priceoverview/\u0026#34; price_params={ \u0026#34;appid\u0026#34;:753, \u0026#34;currency\u0026#34;:23, \u0026#34;market_hash_name\u0026#34;:card_name } ​ #获取图片 ​ listing_url = f\u0026#34;https://steamcommunity.com/market/listings/753/{card_name}/render\u0026#34; ​ listing_params={ ​ \u0026#34;start\u0026#34;: 0, ​ ​ \u0026#34;currency\u0026#34;: 23, ​ ​ #\u0026#34;format\u0026#34;: \u0026#34;json\u0026#34; ​ } ​ try: ​ price_res=requests.get(price_url,params=price_params,timeout=10) ​ price_data=price_res.json() ​ image_url=None ​ listing_res=requests.get(listing_url,params=listing_params,timeout=10) ​ listing_data=listing_res.json() ​ #从assets里提取icon_url ​ assets=listing_data.get(\u0026#34;assets\u0026#34;,{}).get(\u0026#34;753\u0026#34;,{}).get(\u0026#34;6\u0026#34;,{}) ​ if assets: ​ first_asset=list(assets.values())[0] ​ icon=first_asset.get(\u0026#34;icon_url\u0026#34;) ​ if icon: image_url=f\u0026#34;https://community.fastly.steamstatic.com/economy/image/{icon}\u0026#34; ​ if price_data.get(\u0026#34;success\u0026#34;)==True: ​ lowest_price_str = price_data.get(\u0026#34;lowest_price\u0026#34;, \u0026#34;无数据\u0026#34;) ​ try: ​ lowest_price_float = float(lowest_price_str.replace(\u0026#34;¥ \u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;,\u0026#34;, \u0026#34;\u0026#34;)) ​ except: ​ lowest_price_float = None ​ return { ​ \u0026#34;success\u0026#34;:True, ​ \u0026#34;lowest_price\u0026#34;:lowest_price_str, ​ \u0026#34;lowest_price_float\u0026#34;:lowest_price_float, ​ \u0026#34;volume\u0026#34;:price_data.get(\u0026#34;volume\u0026#34;,\u0026#34;无数据\u0026#34;), ​ \u0026#34;median_price\u0026#34;:price_data.get(\u0026#34;median_price\u0026#34;,\u0026#34;无数据\u0026#34;), ​ \u0026#34;image_url\u0026#34;:image_url ​ } ​ return {\u0026#34;success\u0026#34;:False,\u0026#34;message\u0026#34;:\u0026#34;找不到该商品\u0026#34;} ​ except Exception as e: ​ return {\u0026#34;success\u0026#34;:False,\u0026#34;message\u0026#34;:str(e)} ​ 3.3数据库设计 用户表\n列名 数据类型 约束 说明 id INTEGER PRIMARY KEY 主键 username String UNIQUE, NOT NULL 用户名，唯一且不能为空 password String NOT NULL 密码，不能为空 email String UNIQUE 邮箱地址，唯一 邮箱在注册时可以不填，后续需要可以设置\n卡牌表\n列名 数据类型 约束 说明 id INTEGER PRIMARY KEY 主键 name String NOT NULL 卡片名称 alert_price Float NULL 提醒价格，用于触发邮件 last_price String NULL 最近一次获取的最低价格，用于触发邮件 owner String NOT NULL 卡片所有者，与用户表的username对应 image_url String NULL 图片URL 4. 功能模块 4.1 自动刷新监测 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 def refresh_all_prices(): db=SessionLocal() cards=db.query(Card).all() unique_name=list(set(card.name for card in cards)) price_cache={} for name in unique_name: ​ price_cache[name]=get_card_info(name) for card in cards: ​ result=price_cache.get(card.name) ​ if result and result[\u0026#34;success\u0026#34;]: ​ card.last_price=result[\u0026#34;lowest_price\u0026#34;] ​ db.commit() ​ if card.alert_price: ​ price_value=result.get(\u0026#34;lowest_price_float\u0026#34;) ​ if price_value and price_value\u0026lt;=card.alert_price: ​ user=db.query(User).filter(User.username==card.owner).first() ​ if user and user.email: ​ send_email(user.email,card.name,result[\u0026#34;lowest_price\u0026#34;],card.alert_price) db.close() 在用户足够多的情况下，一次自动刷新会发出大量请求，list(set(card.name for card in cards))给卡牌进行去重操作，不同用户收藏的相同卡牌只会发出一次请求，可以大大减少请求次数，不过实际也就是我个人使用，假装提升一下可维护性。\n1 2 3 4 5 6 #定时任务 schedular=BackgroundScheduler() schedular.add_job(refresh_all_prices,\u0026#34;interval\u0026#34;,minutes=30) schedular.start() 做一个定时任务挂载后台，间隔30分钟，可以自己设置\n4.2 邮件提醒 目前项目部署在Raliway上且没有开通Pro计划，因此所有直接连接邮件服务器是SMTP的服务都是禁用的，不过可以通过走网页协议发送，比如Resend和SendGrid，额度每天100封，个人使用完全足够，SendGrid目前只对企业开放。\n在Resend里找到API KEYS—Create API Key—，复制并保存Token。\n在本地文件里配置.env用于处理密钥等隐私数据，这类密钥不会push到github上。\n在Railway上以环境变量的方式录入即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def send_email(to_email:str,card_name:str,price:str,alert_price:float): resend.api_key=os.getenv(\u0026#34;RESEND_API_KEY\u0026#34;) try: ​ email = resend.Emails.send({ ​ \u0026#34;from\u0026#34;: \u0026#34;onboarding@resend.dev\u0026#34;, ​ \u0026#34;to\u0026#34;: [to_email], ​ \u0026#34;subject\u0026#34;: \u0026#34;Steam 价格提醒：\u0026#34; + card_name, ​ \u0026#34;text\u0026#34;: \u0026#34;你关注的卡牌 \u0026#34; + card_name + \u0026#34; 当前价格为 \u0026#34; + price + \u0026#34;，已低于你设定的 ¥\u0026#34; + str(alert_price) + \u0026#34;！\u0026#34; ​ }) ​ ​ print(f\u0026#34;邮件发送成功:{email}\u0026#34;) except Exception as e: ​ print(f\u0026#34;邮件发送失败,{e}\u0026#34;) 非SMTP的邮件转发只需要拿到API_KEY，之后的操作执行就不必重复验证了。\n5. 缺陷和可改进之处 顺序请求的方式不适合用户量大请求数倍增的场景，要么限制用户可监测的卡数，要么提高请求效率（异步），分布式。 邮件提醒如果没有第一时间购买的话会按照监测间隔反复发送，极大浪费邮件转发额度，要么换用SMTP直接转发邮件，要么设置一个提醒字段，只在第一次监测到时提醒，后续价格变化时重置这个字段 ","date":"2026-03-17T20:49:24+08:00","image":"https://Gravity2000.github.io/p/python%E7%9B%91%E6%8E%A7-steam-%E5%86%B7%E9%97%A8%E9%97%AA%E5%8D%A1%E4%BB%B7%E6%A0%BC%E4%BD%8E%E4%BB%B7%E8%87%AA%E5%8A%A8%E9%82%AE%E4%BB%B6%E6%8F%90%E9%86%92/images/%E5%B0%81%E9%9D%A2_hu_13171bdcf291278b.png","permalink":"https://Gravity2000.github.io/p/python%E7%9B%91%E6%8E%A7-steam-%E5%86%B7%E9%97%A8%E9%97%AA%E5%8D%A1%E4%BB%B7%E6%A0%BC%E4%BD%8E%E4%BB%B7%E8%87%AA%E5%8A%A8%E9%82%AE%E4%BB%B6%E6%8F%90%E9%86%92/","title":"Python监控 Steam 冷门闪卡价格，低价自动邮件提醒"},{"content":"\u0026#x1f604; ​\t第一篇博客，熟悉一下10分钟速成的markdown笔记写法，把加密和token验证的具体逻辑记录一下，当作强化记忆和学习\n1、设计思路 用户注册：查重数据库里的用户名，密码以非明文的形式记入数据库（加密） 用户登录：检索该用户名，解密对应的密码，与明文进行对比，生成用于通行的token Api请求:正常使用，一旦token过期，用户收到重新登录的提示 token都有一个过期时间，从用户层面来看，这个到期时间是看不到的，各个网站或者APP的设计也各不相同，我记得小黑盒的网页端只有几十分钟时间，网易BUFF可以选择15天。\n很多业务会选择长短token的双token机制，长token设置超过一天的较长时间，短token过期后，让长token进行验证，通过后直接发放新的短token（而不是为短token续期），让用户可以长期使用。\n长token往往设置几天甚至一个月的时间，可像微信等大量软件几乎不需要重新登录，利用更底层的，与设备绑定的设备信任可以跳出token的有效期，创造一种“永不登录”的体验。不过像微信内置的小程序根据开发者不同的需求，还是以token验证机制为主。\n最简单的token过期机制，以30分钟为例，流程大概如下图：\n2、代码拆解 2.1 加密验证 1 2 3 4 5 6 7 pwd_context=CryptContext(schemes=[\u0026#34;bcrypt\u0026#34;],deprecated=\u0026#34;auto\u0026#34;) def hash_password(password:str): return pwd_context.hash(password) def verify_password(plain_password:str,hashed_password:str): return pwd_context.verify(plain_password,hashed_password) 用户设置的密码不能以明文的方式存入数据库中，一般要经过加密，bcrypt提供了一种不可逆的密码哈希，核心是通过salt = bcrypt.gensalt(rounds=12)这种生成随机盐的方式，与版本标识、哈希值等进行再组合而生成的最终密文，由于盐的随机生成性，可以保证相同明文可以生成不同的密文。\n问题：如果生成的密文不同，那verify_password是怎么让用户输入的密码与数据库的哈希值进行比对的？\n实际上verify()函数并不是对用户输入的密码进行再加密后与数据库密文进行比对，而是提取了数据库中保存的密码密文中的盐值，再与输入密码的明文进行bcrypt加密，这里的盐值不再具有随机性，因此加密出的密文是相同的，达到了一个安全性与验证性兼顾的效果。\n2.2 生成token 1 2 3 4 5 6 7 8 9 def create_token(data:dict): to_encode=data.copy() expire=datetime.utcnow()+timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({\u0026#34;exp\u0026#34;:expire}) return jwt.encode(to_encode,SECRET_KEY,algorithm=ALGORITHM) 用户在成功登陆之后都会生成一段token，在token的有效时间内不需要重新登录就能访问所有API。expire是当前国际时间加上token有效时长而确定的一个最终到期时间，token的生成根据开发者的需要会到用户名、到期时间等变量，这也是我们要传入一个data:dirt字典类型的原因，同时用于token生成的to_encode也要采用copy()的形式，因为频繁更改原始数据会降低安全性。\n问题：一段token最终如何生成，又如何保证安全性？\n用JWT生成的token由三个部分组成\njwt.encode(to_encode,SECRET_KEY,algorithm=ALGORITHM)这里的三个参数，ALGORITHM决定了我们所用的加密算法，to_encode是从用户那里收集的信息通过Base64编码放进token，要注意Base64不是加密，任何人都可以解码，所以不能收集密码等敏感信息用于token生成。\n而确保安全性的SECRET_KEY是由手动设置的一串密钥，密钥与头部和Payload进行加密生成Signature（签名），只要他人拿不到这串密钥，就无法生成正确的签名，这就是JWT用于保证安全性的方式\n2.3 token验证 1 2 3 4 5 6 7 8 9 10 11 def decode_token(token:str): try: payload=jwt.decode(token,SECRET_KEY,algorithms=[ALGORITHM]) return payload.get(\u0026#34;sub\u0026#34;) except JWTError: return None 1 2 3 4 5 6 7 8 9 def get_current_user(token:str=Depends(oauth2_scheme)): username=decode_token(token) if username is None: raise HTTPException(status_code=401,detail=\u0026#34;请先登录\u0026#34;) return username 具体怎么实现token的过期验证，需要把这两段代码结合起来看，首先decode_token是尝试一个获取用户名的函数，正如JWT的三个部分所呈现的那样，Payload是用户名和过期时间通过Base64转换过去的，可以被我们解码后获取。而jwt.decode会做三件事：\n验证密钥是否正确 验证token是否被篡改 检查exp字段（也就是过期时间）是否过期 如果token过期，抛出JWTError的异常，返回None。在用户每次请求API的时候，都会传入get_current_user的参数，要求返回用户名，如果是None，说明token已经过期，要求用户重新登录。\n2.4 全部代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from datetime import datetime,timedelta from jose import JWTError,jwt from passlib.context import CryptContext #密钥和算法配置 SECRET_KEY=\u0026#34;你的密钥\u0026#34; ALGORITHM=\u0026#34;HS256\u0026#34; ACCESS_TOKEN_EXPIRE_MINUTES=30 #密码加密工具 pwd_context=CryptContext(schemes=[\u0026#34;bcrypt\u0026#34;],deprecated=\u0026#34;auto\u0026#34;) def hash_password(password:str): return pwd_context.hash(password) def verify_password(plain_password:str,hashed_password:str): return pwd_context.verify(plain_password,hashed_password) def create_token(data:dict): to_encode=data.copy() expire=datetime.utcnow()+timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({\u0026#34;exp\u0026#34;:expire}) return jwt.encode(to_encode,SECRET_KEY,algorithm=ALGORITHM) def decode_token(token:str): try: payload=jwt.decode(token,SECRET_KEY,algorithms=[ALGORITHM]) return payload.get(\u0026#34;sub\u0026#34;) except JWTError: return None 虽然只用了一点基础功能，感觉Markdown写出来还是足够好看的。网页排版里要首行缩进还要改CSS的格式，干脆就不缩进了。\n","date":"2025-12-29T21:55:23+08:00","image":"https://Gravity2000.github.io/p/%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3%E7%94%A8%E6%88%B7%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E5%AF%86%E7%A0%81%E5%8A%A0%E5%AF%86%E5%92%8Ctoken%E7%94%9F%E6%88%90%E9%AA%8C%E8%AF%81/images/cover_hu_6cf0f8c13d310748.png","permalink":"https://Gravity2000.github.io/p/%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3%E7%94%A8%E6%88%B7%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E5%AF%86%E7%A0%81%E5%8A%A0%E5%AF%86%E5%92%8Ctoken%E7%94%9F%E6%88%90%E9%AA%8C%E8%AF%81/","title":"如何理解用户系统中的密码加密和token生成验证"}]