<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Z.L Vansiit&apos;s blog</title>
    <link>https://vansiit.cc/</link>
    <description>开发 | vansiit，Web &amp; Front-end Engineer | vansiit的个人博客呀</description>
    <language>zh-CN</language>
    <lastBuildDate>Thu, 28 May 2026 10:25:19 GMT</lastBuildDate>
    <managingEditor>vansiit@163.com (Z.L Vansiit)</managingEditor>
    <webMaster>vansiit@163.com (Z.L Vansiit)</webMaster>
    <atom:link href="https://vansiit.cc/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>常家岩</title>
      <link>https://vansiit.cc/2026/05/12/changjiayan.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2026/05/12/changjiayan.html</guid>
      <pubDate>Tue, 12 May 2026 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>常家岩</h1>
<p><img src="https://vansiit.cc/moments/img.jpg" alt="img.png"></p>
<p>这是我老家，叫常家岩。</p>
<p>五一回去，还特意上常家岩走了一趟。</p>
<p>说是常家岩，其实就剩老张家两户人了。姓常的早些年就绝了户，房子塌得不成样子，只剩些零星的瓦砾和半截土墙。常大爷以前家门口那棵很大的樱桃树也败了， 熟成一颗被鸟吃一颗，再加上树荫遮住了，五一回去没见着几颗樱桃。</p>
<p><img src="https://vansiit.cc/moments/img-1.jpg" alt="img.png"></p>
<p>这是在我奶奶家摘的。</p>
<p>常大爷现在是五保户，村里给他在镇上安排了房子。可我那天上去的时候，远远就看见他在老屋地基边上，弓着腰种菜。地不大，拾掇得倒挺齐整。我走过去喊了一声，他抬头看我，愣了好一会儿才认出来。</p>
<p>我给他散了根烟，他接过去。笑着脸，说回来了啊，还是那么熟悉的笑脸。</p>
<p>就想起小时候路过这里，樱桃熟了的时候，红艳艳的一树，老远就能看见。一回想也有二十年了。</p>
<p>再过一代人，恐怕真的没人知道“常家岩”这个名字是怎么来的了。</p>
]]></description>
      <content:encoded><![CDATA[<h1>常家岩</h1>
<p><img src="https://vansiit.cc/moments/img.jpg" alt="img.png"></p>
<p>这是我老家，叫常家岩。</p>
<p>五一回去，还特意上常家岩走了一趟。</p>
<p>说是常家岩，其实就剩老张家两户人了。姓常的早些年就绝了户，房子塌得不成样子，只剩些零星的瓦砾和半截土墙。常大爷以前家门口那棵很大的樱桃树也败了， 熟成一颗被鸟吃一颗，再加上树荫遮住了，五一回去没见着几颗樱桃。</p>
<p><img src="https://vansiit.cc/moments/img-1.jpg" alt="img.png"></p>
<p>这是在我奶奶家摘的。</p>
<p>常大爷现在是五保户，村里给他在镇上安排了房子。可我那天上去的时候，远远就看见他在老屋地基边上，弓着腰种菜。地不大，拾掇得倒挺齐整。我走过去喊了一声，他抬头看我，愣了好一会儿才认出来。</p>
<p>我给他散了根烟，他接过去。笑着脸，说回来了啊，还是那么熟悉的笑脸。</p>
<p>就想起小时候路过这里，樱桃熟了的时候，红艳艳的一树，老远就能看见。一回想也有二十年了。</p>
<p>再过一代人，恐怕真的没人知道“常家岩”这个名字是怎么来的了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>《失眠》（原创诗歌）</title>
      <link>https://vansiit.cc/2026/01/27/yesterday.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2026/01/27/yesterday.html</guid>
      <pubDate>Tue, 27 Jan 2026 04:00:00 GMT</pubDate>
      <description><![CDATA[<p>《失眠》（原创诗歌）</p>
<p>又一首字节曲</p>
<p>在昨日夜里吟唱至天明</p>
<p>去建设东部吧，去烙上时代的印记</p>
<p>没有薄雾缭绕，没有微光晨曦</p>
<p>没有隐隐约约的狗吠与斑斑露迹</p>
<p>早起的垃圾车还没有睡醒</p>
<p>它们还散落在巷弄里</p>
<p>可是，大地盯着我的死期</p>
<p>我只能喋喋不休，我只能一直</p>
<p>醒着</p>
<p>远方已经远去</p>
<p>河流实际是一些，是一些流动的过去</p>
<p>草原当然，当然和野马一样属于它自己</p>
<p>少年又一次沉默思考的时候</p>
<p>夜幕它又一次来临</p>
<p>短视频又一次</p>
<p>吞噬了所有年轻的出租屋</p>
]]></description>
      <content:encoded><![CDATA[<p>《失眠》（原创诗歌）</p>
<p>又一首字节曲</p>
<p>在昨日夜里吟唱至天明</p>
<p>去建设东部吧，去烙上时代的印记</p>
<p>没有薄雾缭绕，没有微光晨曦</p>
<p>没有隐隐约约的狗吠与斑斑露迹</p>
<p>早起的垃圾车还没有睡醒</p>
<p>它们还散落在巷弄里</p>
<p>可是，大地盯着我的死期</p>
<p>我只能喋喋不休，我只能一直</p>
<p>醒着</p>
<p>远方已经远去</p>
<p>河流实际是一些，是一些流动的过去</p>
<p>草原当然，当然和野马一样属于它自己</p>
<p>少年又一次沉默思考的时候</p>
<p>夜幕它又一次来临</p>
<p>短视频又一次</p>
<p>吞噬了所有年轻的出租屋</p>
]]></content:encoded>
    </item>
    <item>
      <title>读诗《有人问我公理和正义的问题》有感</title>
      <link>https://vansiit.cc/2025/12/10/gongli_zhengyi.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/12/10/gongli_zhengyi.html</guid>
      <pubDate>Thu, 11 Dec 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>读诗《有人问我公理和正义的问题》有感</h1>
<h2>引言</h2>
<p>先不说笔者自己的感想，诗歌实际上需要自己感受体悟。</p>
<p>先贴诗歌全文如下：</p>
<blockquote>
<p><strong>有人问我公理和正义的问题</strong></p>
<p>&lt;div style=“text-align: left; margin-top: 10px; margin-bottom: 10px; font-style: italic; margin-left: 100px;”&gt;中国台湾.杨牧&lt;/div&gt;</p>
<p>有人问我公理和正义的问题</p>
<p>写在一封缜密工整的信上，从</p>
<p>外县市一小镇寄出，署了</p>
<p>真实姓名和身分证号码</p>
<p>年龄（窗外在下雨，点滴芭蕉叶</p>
<p>和围墙上的碎玻璃），籍贯，职业</p>
<p>（院子里堆积许多枯树枝</p>
<p>一只黑鸟在扑翅）。他显然历经</p>
<p>苦思不得答案，关于这么重要的</p>
<p>一个问题。他是善于思维的，</p>
<p>文字也简洁有力，结构圆融</p>
<p>书法得体（乌云向远天飞）</p>
<p>晨昏练过玄秘塔大字，在小学时代</p>
<p>家住渔港后街拥挤的眷村里</p>
<p>大半时间和母亲在一起；他羞涩</p>
<p>敏感，学了一口台湾国语没关系</p>
<p>常常登高瞭望海上的船只</p>
<p>看白云，就这样把皮肤晒黑了</p>
<p>单薄的胸膛里栽培着小小</p>
<p>孤独的心，他这样恳切写道：</p>
<p>早熟脆弱如一颗二十世纪梨</p>
<p>&lt;br&gt;
有人问我公理和正义的问题</p>
<p>对着一壶苦茶，我设法去理解</p>
<p>如何以抽象的观念分化他那许多凿凿的</p>
<p>证据，也许我应该先否定他的出发点</p>
<p>攻击他的心态，批评他收集资料</p>
<p>的方法错误，以反证削弱其语气</p>
<p>指他所陈一切这一切无非偏见</p>
<p>不值得有识之士的反驳。我听到</p>
<p>窗外的雨声愈来愈急</p>
<p>水势从屋顶匆匆泻下，灌满房子周围的</p>
<p>阳沟。唉到底甚么是二十世纪梨呀——</p>
<p>他们在海岛的高山地带寻到</p>
<p>相当于华北平原的气候了，肥沃丰隆的</p>
<p>处女地，乃迂回引进一种乡愁慰藉的</p>
<p>种子埋下，发芽，长高</p>
<p>开花结成这果，这名不见经传的水果</p>
<p>可怜的形状，色泽，和气味</p>
<p>营养价值不明，除了</p>
<p>维他命Ｃ，甚至完全不象征甚么</p>
<p>除了一颗犹豫的属于他自己的心</p>
<p>&lt;br&gt;
有人问我公理和正义的问题</p>
<p>这些不需要象征——这些</p>
<p>是现实就应该当做现实处理</p>
<p>发信的是一个善于思维分析的人</p>
<p>读了一年企管转法律，毕业后</p>
<p>半年补充兵，考了两次司法官……</p>
<p>雨停了</p>
<p>我对他的身世，他的愤怒</p>
<p>他的诘难和控诉都不能理解</p>
<p>虽然我曾设法，对着一壶苦茶</p>
<p>设法理解。我想念他不是为考试</p>
<p>而愤怒，因为这不在他的举证里</p>
<p>他谈的是些高层次的问题，简洁有力</p>
<p>段落分明，归纳为令人茫然的一系列</p>
<p>质疑。太阳从芭蕉树后注入草地</p>
<p>在枯枝上闪着光。这些不会是</p>
<p>虚假的，在有限的温暖里</p>
<p>坚持一团庞大的寒气</p>
<p>&lt;br&gt;
有人问我一个问题，关于</p>
<p>公理和正义。他是班上穿著</p>
<p>最整齐的孩子，虽然母亲在城里</p>
<p>帮佣洗衣——哦母亲在他印象中</p>
<p>总是白皙的微笑着，纵使脸上</p>
<p>挂着泪；她双手永远是柔软的</p>
<p>干净的，灯下为他慢慢修铅笔</p>
<p>他说他不太记得了是一个溽热的夜</p>
<p>好像仿佛父亲在一场大吵闹后</p>
<p>（充满乡音的激情的言语，连他</p>
<p>单祧籍贯香火的儿子，都不完全懂）</p>
<p>似乎就这样走了，可能大概也许上了山</p>
<p>在高亢的华北气候里开垦，栽培</p>
<p>一种新引进的水果，二十世纪梨</p>
<p>秋风的夜晚，母亲教他唱日本童谣</p>
<p>桃太郎远征魔鬼岛，半醒半睡</p>
<p>看她剪刀针线把旧军服拆开</p>
<p>修改成一条夹裤一件小棉袄</p>
<p>信纸上沾了两片水渍，想是他的泪</p>
<p>如墙脚巨大的雨霉，我向外望</p>
<p>天地也哭过，为一个重要的</p>
<p>超越季节和方向的问题，哭过</p>
<p>复以虚假的阳光掩饰窘态</p>
<p>&lt;br&gt;
有人问我一个问题，关于</p>
<p>公理和正义。檐下倒挂着一只</p>
<p>诡异的蜘蛛，在虚假的阳光里</p>
<p>翻转反覆，结网。许久许久</p>
<p>我还看到冬天的蚊蚋围着纱门下</p>
<p>一个塑胶水桶在飞，如乌云</p>
<p>我许久未曾听过那么明朗详尽的</p>
<p>陈述了，他在无情地解剖着自己：</p>
<p>籍贯教我走到任何地方都带着一份</p>
<p>与生俱来的乡愁，他说，像我的胎记</p>
<p>然而胎记袭自母亲我必须承认</p>
<p>它和那个无关。他时常</p>
<p>站在海岸瞭望，据说烟波尽头</p>
<p>还有一个更长的海岸，高山森林巨川</p>
<p>母亲没看过的地方才是我们的</p>
<p>故乡。大学里必修现代史，背熟一本</p>
<p>标准答案；选修语言社会学</p>
<p>高分过了劳工法，监狱学，法制史</p>
<p>重修体育和宪法。他善于举例</p>
<p>作证，能推论，会归纳。我从来</p>
<p>没有收到过这样一封充满体验和幻想</p>
<p>于冷肃尖锐的语气中流露狂热和绝望</p>
<p>彻底把狂热和绝望完全平衡的信</p>
<p>礼貌地，问我公理和正义的问题</p>
<p>&lt;br&gt;
有人问我公理和正义的问题</p>
<p>写在一封不容增删的信里</p>
<p>我看到泪水的印子扩大如干涸的湖泊</p>
<p>濡沫死去的鱼族在暗晦的角落</p>
<p>留下些许枯骨和白刺，我仿佛也</p>
<p>看到血在他成长的知识判断里</p>
<p>溅开，像炮火中从困顿的孤堡</p>
<p>放出的军鸽，系着疲乏顽抗者</p>
<p>最渺茫的希望，冲开窒息的硝烟</p>
<p>鼓翼升到烧焦的黄杨树梢</p>
<p>敏捷地回转，对准增防的营盘刺飞</p>
<p>却在高速中撞上一颗无意的流弹</p>
<p>粉碎于交击的喧嚣，让毛骨和鲜血</p>
<p>充塞永远不再的空间</p>
<p>让我们从容遗忘。我体会</p>
<p>他沙哑的声调。他曾经</p>
<p>嚎啕入荒原</p>
<p>狂呼暴风雨</p>
<p>计算着自己的步伐，不是先知</p>
<p>他不是先知，是失去向导的使徒——</p>
<p>他单薄的胸膛鼓胀如风炉</p>
<p>一颗心在高温里熔化</p>
<p>透明，流动，虚无</p>
</blockquote>
<h2>作者介绍</h2>
<blockquote>
<p>杨牧（1940年9月6日－2020年3月13日），本名王靖献，是台湾最具代表性的现代诗人、散文家、学者与翻译家之一，被誉为华语世界最优秀的诗人之一。</p>
<p>杨牧是一位学贯中西、风格独特的文学巨匠，其作品以诗意的语言探索永恒的人文命题，在当代华语文学中占有一定地位。</p>
</blockquote>
<h2>读后感</h2>
<p>诗中的“我”应该是一位有声望的长者，是律师，是教授，或者是政治学者及其他的长者，生活在大城市，经济文化的中心。</p>
<p>“我”收到一封“外县市”年轻人的来信，用年轻人的视角，从身世、经历谈起，诉说自己的艰难、困顿和苦闷，追问“公理和正义的问题”。</p>
<p>关于这些尖锐的问题，“我”也没有答案。盯着窗外，喝着“苦茶”，思绪万千。最后也只能为年轻人的遭遇感到无奈，任凭“一颗心在高温里熔化”。</p>
<p>读完《公理和正义》后，我开始思考，我如何去理解公理和正义。</p>
<p>哲学家罗尔斯 (John Bordley Rawls, 1921-2002) 提出的「作为公平的正义」(justice as fairness) 理论，也叫 「正义作为公平」。</p>
<p>主张在“无知之幕”（后文有解释）后，人们会一致同意选择以下两条正义原则：</p>
<p>1.最大平等自由原则：每个人都应享有最广泛、平等的基本自由体系（如政治自由、言论自由、人身自由等）。</p>
<p>2.差异原则：社会经济的不平等安排必须满足两个条件：</p>
<p>公平的机会平等：所有职位和地位在机会公平的条件下向所有人开放。</p>
<p>最有利于最不利者：不平等必须最有利于社会中处境最差的成员。</p>
<p>诗中年轻人的苦闷困顿，背后是集体的缺失，造成个体的疏离（alienation）。</p>
<p>个人和集体的互惠原则（Reciprocity）是共同体伦理的条件正当性。</p>
<p>如果一个集体要要求成员对它忠诚、付出、牺牲，就必须在成员需要时给予支持。缺失互惠关系，就会降低集体的合法性（legitimacy），让“集体认同”变成空洞口号。</p>
<p>当然，年轻人的苦闷困顿也有可能是个人原因导致，但是一个社会中的任何个人行为不是孤立的，而是一个社会中的成员行为。</p>
<p>如果无视年轻人的艰难困顿，就会失去对集体的支持，从而失去集体的意义。</p>
<p>诗《公理和正义》的韵律感虽然不强烈，但是整体的流畅性极强，虽然有一些环境的描写，完全不影响整体的走向和节奏。读起来朗朗上口，一气呵成，古意盎然。</p>
<p>比如“我从来/没有收到过这样一封充满体验和幻想/于冷肃尖锐的语气中流露狂热和绝望/彻底把狂热和绝望完全平衡的信/礼貌地，问我公理和正义的问题”</p>
<p>断句也有惊喜感，短句为主，段落间有空隙。意向也很独特，比如“二十世纪梨”。</p>
<h2>其他</h2>
<p><strong>玄秘塔大字</strong></p>
<p>柳公权所书写的楷书碑刻 《玄秘塔碑》 的拓本，一般字体较大。入门书法帖，其他入门帖还有：欧阳询《九成宫醴泉铭》，颜真卿《多宝塔碑》《颜勤礼碑》等。</p>
<p><strong>眷村</strong></p>
<p>眷村是台湾地区特有的社区形态，主要形成于1949年国民党政权迁台后，为安置大量跟随迁台的军人及其家属所建立的聚居区。</p>
<p><strong>二十世纪梨</strong></p>
<p>二十世纪梨是起源于日本的一种优质梨品种，也是全球栽培最广泛的青皮梨（亚洲梨）品种之一。日本殖民统治时期引入台湾，在台中东势、苗栗等地试种成功。成为一种承载着殖民历史、农业发展和集体怀旧情感的文化符号。</p>
<p><strong>单祧籍贯香火</strong></p>
<p>祧（tiāo）指祭祀祖先的宗庙，引申为继承祖先香火、延续宗族血脉。</p>
<p><strong>桃太郎远征魔鬼岛</strong></p>
<p>“桃太郎远征魔鬼岛”是日本一则家喻户晓的民间故事《桃太郎》的核心情节。故事讲述了主人公桃太郎前往鬼岛讨伐作恶的鬼怪，为民除害的冒险经历。</p>
<blockquote>
<p>“无知之幕”</p>
<p>在设计一个全新社会的根本规则（宪法、基本制度）之前，所有参与者都站在一道特殊的“幕布”之后。这道幕布剥夺了他们关于自身在即将形成的社会中的一切具体信息。</p>
<p>参与者不知道：</p>
<p>自己的社会阶级、财富、地位（是富是贫）。</p>
<p>自己的天赋、才能、智力、体力（是否聪明或强壮）。</p>
<p>自己的性别、种族、世代。</p>
<p>自己的宗教信仰、人生观、价值观。</p>
<p>自己所属社会的特定历史、经济或文化状况（只知道一些普遍的社会学、经济学、心理学规律）。</p>
<p>参与者知道：</p>
<p>人类社会的一般事实。</p>
<p>人们有各自的人生计划和需求（“基本善”），如自由、机会、财富、自尊等。</p>
<p>自己是理性自利的，会为自己争取最有利的条件。</p>
</blockquote>
]]></description>
      <content:encoded><![CDATA[<h1>读诗《有人问我公理和正义的问题》有感</h1>
<h2>引言</h2>
<p>先不说笔者自己的感想，诗歌实际上需要自己感受体悟。</p>
<p>先贴诗歌全文如下：</p>
<blockquote>
<p><strong>有人问我公理和正义的问题</strong></p>
<p>&lt;div style=“text-align: left; margin-top: 10px; margin-bottom: 10px; font-style: italic; margin-left: 100px;”&gt;中国台湾.杨牧&lt;/div&gt;</p>
<p>有人问我公理和正义的问题</p>
<p>写在一封缜密工整的信上，从</p>
<p>外县市一小镇寄出，署了</p>
<p>真实姓名和身分证号码</p>
<p>年龄（窗外在下雨，点滴芭蕉叶</p>
<p>和围墙上的碎玻璃），籍贯，职业</p>
<p>（院子里堆积许多枯树枝</p>
<p>一只黑鸟在扑翅）。他显然历经</p>
<p>苦思不得答案，关于这么重要的</p>
<p>一个问题。他是善于思维的，</p>
<p>文字也简洁有力，结构圆融</p>
<p>书法得体（乌云向远天飞）</p>
<p>晨昏练过玄秘塔大字，在小学时代</p>
<p>家住渔港后街拥挤的眷村里</p>
<p>大半时间和母亲在一起；他羞涩</p>
<p>敏感，学了一口台湾国语没关系</p>
<p>常常登高瞭望海上的船只</p>
<p>看白云，就这样把皮肤晒黑了</p>
<p>单薄的胸膛里栽培着小小</p>
<p>孤独的心，他这样恳切写道：</p>
<p>早熟脆弱如一颗二十世纪梨</p>
<p>&lt;br&gt;
有人问我公理和正义的问题</p>
<p>对着一壶苦茶，我设法去理解</p>
<p>如何以抽象的观念分化他那许多凿凿的</p>
<p>证据，也许我应该先否定他的出发点</p>
<p>攻击他的心态，批评他收集资料</p>
<p>的方法错误，以反证削弱其语气</p>
<p>指他所陈一切这一切无非偏见</p>
<p>不值得有识之士的反驳。我听到</p>
<p>窗外的雨声愈来愈急</p>
<p>水势从屋顶匆匆泻下，灌满房子周围的</p>
<p>阳沟。唉到底甚么是二十世纪梨呀——</p>
<p>他们在海岛的高山地带寻到</p>
<p>相当于华北平原的气候了，肥沃丰隆的</p>
<p>处女地，乃迂回引进一种乡愁慰藉的</p>
<p>种子埋下，发芽，长高</p>
<p>开花结成这果，这名不见经传的水果</p>
<p>可怜的形状，色泽，和气味</p>
<p>营养价值不明，除了</p>
<p>维他命Ｃ，甚至完全不象征甚么</p>
<p>除了一颗犹豫的属于他自己的心</p>
<p>&lt;br&gt;
有人问我公理和正义的问题</p>
<p>这些不需要象征——这些</p>
<p>是现实就应该当做现实处理</p>
<p>发信的是一个善于思维分析的人</p>
<p>读了一年企管转法律，毕业后</p>
<p>半年补充兵，考了两次司法官……</p>
<p>雨停了</p>
<p>我对他的身世，他的愤怒</p>
<p>他的诘难和控诉都不能理解</p>
<p>虽然我曾设法，对着一壶苦茶</p>
<p>设法理解。我想念他不是为考试</p>
<p>而愤怒，因为这不在他的举证里</p>
<p>他谈的是些高层次的问题，简洁有力</p>
<p>段落分明，归纳为令人茫然的一系列</p>
<p>质疑。太阳从芭蕉树后注入草地</p>
<p>在枯枝上闪着光。这些不会是</p>
<p>虚假的，在有限的温暖里</p>
<p>坚持一团庞大的寒气</p>
<p>&lt;br&gt;
有人问我一个问题，关于</p>
<p>公理和正义。他是班上穿著</p>
<p>最整齐的孩子，虽然母亲在城里</p>
<p>帮佣洗衣——哦母亲在他印象中</p>
<p>总是白皙的微笑着，纵使脸上</p>
<p>挂着泪；她双手永远是柔软的</p>
<p>干净的，灯下为他慢慢修铅笔</p>
<p>他说他不太记得了是一个溽热的夜</p>
<p>好像仿佛父亲在一场大吵闹后</p>
<p>（充满乡音的激情的言语，连他</p>
<p>单祧籍贯香火的儿子，都不完全懂）</p>
<p>似乎就这样走了，可能大概也许上了山</p>
<p>在高亢的华北气候里开垦，栽培</p>
<p>一种新引进的水果，二十世纪梨</p>
<p>秋风的夜晚，母亲教他唱日本童谣</p>
<p>桃太郎远征魔鬼岛，半醒半睡</p>
<p>看她剪刀针线把旧军服拆开</p>
<p>修改成一条夹裤一件小棉袄</p>
<p>信纸上沾了两片水渍，想是他的泪</p>
<p>如墙脚巨大的雨霉，我向外望</p>
<p>天地也哭过，为一个重要的</p>
<p>超越季节和方向的问题，哭过</p>
<p>复以虚假的阳光掩饰窘态</p>
<p>&lt;br&gt;
有人问我一个问题，关于</p>
<p>公理和正义。檐下倒挂着一只</p>
<p>诡异的蜘蛛，在虚假的阳光里</p>
<p>翻转反覆，结网。许久许久</p>
<p>我还看到冬天的蚊蚋围着纱门下</p>
<p>一个塑胶水桶在飞，如乌云</p>
<p>我许久未曾听过那么明朗详尽的</p>
<p>陈述了，他在无情地解剖着自己：</p>
<p>籍贯教我走到任何地方都带着一份</p>
<p>与生俱来的乡愁，他说，像我的胎记</p>
<p>然而胎记袭自母亲我必须承认</p>
<p>它和那个无关。他时常</p>
<p>站在海岸瞭望，据说烟波尽头</p>
<p>还有一个更长的海岸，高山森林巨川</p>
<p>母亲没看过的地方才是我们的</p>
<p>故乡。大学里必修现代史，背熟一本</p>
<p>标准答案；选修语言社会学</p>
<p>高分过了劳工法，监狱学，法制史</p>
<p>重修体育和宪法。他善于举例</p>
<p>作证，能推论，会归纳。我从来</p>
<p>没有收到过这样一封充满体验和幻想</p>
<p>于冷肃尖锐的语气中流露狂热和绝望</p>
<p>彻底把狂热和绝望完全平衡的信</p>
<p>礼貌地，问我公理和正义的问题</p>
<p>&lt;br&gt;
有人问我公理和正义的问题</p>
<p>写在一封不容增删的信里</p>
<p>我看到泪水的印子扩大如干涸的湖泊</p>
<p>濡沫死去的鱼族在暗晦的角落</p>
<p>留下些许枯骨和白刺，我仿佛也</p>
<p>看到血在他成长的知识判断里</p>
<p>溅开，像炮火中从困顿的孤堡</p>
<p>放出的军鸽，系着疲乏顽抗者</p>
<p>最渺茫的希望，冲开窒息的硝烟</p>
<p>鼓翼升到烧焦的黄杨树梢</p>
<p>敏捷地回转，对准增防的营盘刺飞</p>
<p>却在高速中撞上一颗无意的流弹</p>
<p>粉碎于交击的喧嚣，让毛骨和鲜血</p>
<p>充塞永远不再的空间</p>
<p>让我们从容遗忘。我体会</p>
<p>他沙哑的声调。他曾经</p>
<p>嚎啕入荒原</p>
<p>狂呼暴风雨</p>
<p>计算着自己的步伐，不是先知</p>
<p>他不是先知，是失去向导的使徒——</p>
<p>他单薄的胸膛鼓胀如风炉</p>
<p>一颗心在高温里熔化</p>
<p>透明，流动，虚无</p>
</blockquote>
<h2>作者介绍</h2>
<blockquote>
<p>杨牧（1940年9月6日－2020年3月13日），本名王靖献，是台湾最具代表性的现代诗人、散文家、学者与翻译家之一，被誉为华语世界最优秀的诗人之一。</p>
<p>杨牧是一位学贯中西、风格独特的文学巨匠，其作品以诗意的语言探索永恒的人文命题，在当代华语文学中占有一定地位。</p>
</blockquote>
<h2>读后感</h2>
<p>诗中的“我”应该是一位有声望的长者，是律师，是教授，或者是政治学者及其他的长者，生活在大城市，经济文化的中心。</p>
<p>“我”收到一封“外县市”年轻人的来信，用年轻人的视角，从身世、经历谈起，诉说自己的艰难、困顿和苦闷，追问“公理和正义的问题”。</p>
<p>关于这些尖锐的问题，“我”也没有答案。盯着窗外，喝着“苦茶”，思绪万千。最后也只能为年轻人的遭遇感到无奈，任凭“一颗心在高温里熔化”。</p>
<p>读完《公理和正义》后，我开始思考，我如何去理解公理和正义。</p>
<p>哲学家罗尔斯 (John Bordley Rawls, 1921-2002) 提出的「作为公平的正义」(justice as fairness) 理论，也叫 「正义作为公平」。</p>
<p>主张在“无知之幕”（后文有解释）后，人们会一致同意选择以下两条正义原则：</p>
<p>1.最大平等自由原则：每个人都应享有最广泛、平等的基本自由体系（如政治自由、言论自由、人身自由等）。</p>
<p>2.差异原则：社会经济的不平等安排必须满足两个条件：</p>
<p>公平的机会平等：所有职位和地位在机会公平的条件下向所有人开放。</p>
<p>最有利于最不利者：不平等必须最有利于社会中处境最差的成员。</p>
<p>诗中年轻人的苦闷困顿，背后是集体的缺失，造成个体的疏离（alienation）。</p>
<p>个人和集体的互惠原则（Reciprocity）是共同体伦理的条件正当性。</p>
<p>如果一个集体要要求成员对它忠诚、付出、牺牲，就必须在成员需要时给予支持。缺失互惠关系，就会降低集体的合法性（legitimacy），让“集体认同”变成空洞口号。</p>
<p>当然，年轻人的苦闷困顿也有可能是个人原因导致，但是一个社会中的任何个人行为不是孤立的，而是一个社会中的成员行为。</p>
<p>如果无视年轻人的艰难困顿，就会失去对集体的支持，从而失去集体的意义。</p>
<p>诗《公理和正义》的韵律感虽然不强烈，但是整体的流畅性极强，虽然有一些环境的描写，完全不影响整体的走向和节奏。读起来朗朗上口，一气呵成，古意盎然。</p>
<p>比如“我从来/没有收到过这样一封充满体验和幻想/于冷肃尖锐的语气中流露狂热和绝望/彻底把狂热和绝望完全平衡的信/礼貌地，问我公理和正义的问题”</p>
<p>断句也有惊喜感，短句为主，段落间有空隙。意向也很独特，比如“二十世纪梨”。</p>
<h2>其他</h2>
<p><strong>玄秘塔大字</strong></p>
<p>柳公权所书写的楷书碑刻 《玄秘塔碑》 的拓本，一般字体较大。入门书法帖，其他入门帖还有：欧阳询《九成宫醴泉铭》，颜真卿《多宝塔碑》《颜勤礼碑》等。</p>
<p><strong>眷村</strong></p>
<p>眷村是台湾地区特有的社区形态，主要形成于1949年国民党政权迁台后，为安置大量跟随迁台的军人及其家属所建立的聚居区。</p>
<p><strong>二十世纪梨</strong></p>
<p>二十世纪梨是起源于日本的一种优质梨品种，也是全球栽培最广泛的青皮梨（亚洲梨）品种之一。日本殖民统治时期引入台湾，在台中东势、苗栗等地试种成功。成为一种承载着殖民历史、农业发展和集体怀旧情感的文化符号。</p>
<p><strong>单祧籍贯香火</strong></p>
<p>祧（tiāo）指祭祀祖先的宗庙，引申为继承祖先香火、延续宗族血脉。</p>
<p><strong>桃太郎远征魔鬼岛</strong></p>
<p>“桃太郎远征魔鬼岛”是日本一则家喻户晓的民间故事《桃太郎》的核心情节。故事讲述了主人公桃太郎前往鬼岛讨伐作恶的鬼怪，为民除害的冒险经历。</p>
<blockquote>
<p>“无知之幕”</p>
<p>在设计一个全新社会的根本规则（宪法、基本制度）之前，所有参与者都站在一道特殊的“幕布”之后。这道幕布剥夺了他们关于自身在即将形成的社会中的一切具体信息。</p>
<p>参与者不知道：</p>
<p>自己的社会阶级、财富、地位（是富是贫）。</p>
<p>自己的天赋、才能、智力、体力（是否聪明或强壮）。</p>
<p>自己的性别、种族、世代。</p>
<p>自己的宗教信仰、人生观、价值观。</p>
<p>自己所属社会的特定历史、经济或文化状况（只知道一些普遍的社会学、经济学、心理学规律）。</p>
<p>参与者知道：</p>
<p>人类社会的一般事实。</p>
<p>人们有各自的人生计划和需求（“基本善”），如自由、机会、财富、自尊等。</p>
<p>自己是理性自利的，会为自己争取最有利的条件。</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>shanhe</title>
      <link>https://vansiit.cc/2025/12/10/shanhe.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/12/10/shanhe.html</guid>
      <pubDate>Wed, 10 Dec 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<p>整体结构分为 四大卷：</p>
<p>卷一：乱世初光（1–20）</p>
<p>卷二：风尘山河（21–40）</p>
<p>卷三：铁马长风（41–60）</p>
<p>卷四：大地回声（61–80）</p>
<p>📘 全书 80 章章节目录（可用于后续逐章写作）
卷一：乱世初光（1–20）</p>
<p>主题：人物登场、核心矛盾确立、追杀与逃亡、边地世界观展开。</p>
<p>雨夜逃亡 —— 林九逃出城，兵丁紧追。</p>
<p>山风与火光 —— 林九在山地险死，遇见卓玛与走私队。</p>
<p>卓玛的山路（卓玛 POV）—— 走私路线与部族生活揭示。</p>
<p>黑石关的谣言 —— 赵怀忠死讯外传。</p>
<p>陈怀朔登场（朝廷 POV）—— 权力真空与内部腐败初露。</p>
<p>走私队的试探 —— 林九被怀疑身份。</p>
<p>夜叉岭伏击 —— 走私队遭敌对势力袭击。</p>
<p>废塔下的秘密 —— 林九意外暴露身手，卓玛起疑。</p>
<p>陈怀朔的密令 —— 查“叛徒名单”，逼近林九。</p>
<p>山地黑市 —— 多势力混杂的交易场景展开世界观。</p>
<p>火药味的市集 —— 林九与卓玛第一次并肩作战。</p>
<p>红川寨旧事 —— 卓玛背景初揭。</p>
<p>陈怀朔的对手：沈问桥 —— 新派官僚与旧势力冲突。</p>
<p>大雾中的追兵 —— 林九负伤。</p>
<p>神婆洞的疗伤 —— 部族文化展示。</p>
<p>护送决定 —— 卓玛决定带林九去边市。</p>
<p>陈怀朔与军机大臣会面 —— 权术操作的第一回合。</p>
<p>风雪山道 —— 队伍遭自然劫难。</p>
<p>黑夜审判 —— 走私队内部矛盾爆发。</p>
<p>边市初见 —— 第一卷阶段性落点。</p>
<p>卷二：风尘山河（21–40）</p>
<p>主题：边市繁乱、商队、军阀势力与清廷暗线交织；林九与卓玛关系深化。</p>
<p>边市三龙（势力全景）</p>
<p>林九的假身份 —— 被迫成为武师。</p>
<p>卓玛的交易 —— 与山外商人谈判。</p>
<p>陈怀朔的暗探行动 —— 寻找赵怀忠死因线索。</p>
<p>沈问桥的改革计划 —— 新旧思想对撞。</p>
<p>赌馆风波 —— 林九平息冲突引起注意。</p>
<p>部族使节到访 —— 卓玛被卷入政治婚约。</p>
<p>林九被识破部分身份</p>
<p>“碎石帮”伏笔 —— 黑市地下势力浮出。</p>
<p>山地幽灵传说 —— 背后暗示军火走线。</p>
<p>陈怀朔的第一次胜利 —— 扫荡一批腐败官吏。</p>
<p>沈问桥的反击 —— 利用外势力牵制陈怀朔。</p>
<p>卓玛的抉择 —— 拒绝婚约，引部族压力。</p>
<p>林九过去的阴影 —— 为何被追杀之谜初现。</p>
<p>走私队旧债未还 —— 山外“鹰眼”前来讨债。</p>
<p>边市大集祭典 —— 林九与卓玛关系升温。</p>
<p>暗巷火拼 —— 林九遭到陈怀朔派出的密探暗算。</p>
<p>卓玛营救 —— 感情线突破。</p>
<p>密探被俘 —— 林九得知更大的阴谋。</p>
<p>黑湖密会 —— 林九、卓玛决定一起深入找真相。</p>
<p>卷三：铁马长风（41–60）</p>
<p>主题：政治线、走私线、部族冲突全面升级；林九与陈怀朔第一次真正对决。</p>
<p>陈怀朔的全面布控</p>
<p>部族内乱前兆 —— 婚约派系反扑。</p>
<p>山地军阀“罗铁山”登场</p>
<p>林九与罗铁山第一次交锋</p>
<p>沈问桥打造新军 —— 时代更迭显现。</p>
<p>黑石关旧案重启 —— 赵怀忠真正死因线索上浮。</p>
<p>卓玛被绑架 —— 部族极端派行动。</p>
<p>林九入山救人</p>
<p>部族议会 —— 卓玛命运的审判。</p>
<p>林九的大胆声明 —— 承担“罪人”身份换她自由。</p>
<p>陈怀朔掌握林九真实身份</p>
<p>罗铁山背叛朝廷 —— 双线混战。</p>
<p>边市全面骚乱</p>
<p>陈怀朔的清洗行动</p>
<p>沈问桥遭牵连下台</p>
<p>卓玛的觉醒 —— 从走私者转变为部族谈判者。</p>
<p>林九与陈怀朔的第一次交锋（暗战）</p>
<p>黑湖大火 —— 走私线被毁灭性打击。</p>
<p>多方混战的尾声 —— 边市秩序土崩瓦解。</p>
<p>林九与卓玛离开边市 —— 去往更深的山地。</p>
<p>卷四：大地回声（61–80）</p>
<p>主题：反转、真相揭露、命运决战、时代收束。</p>
<p>深入部族腹地 —— 高原世界观全面展开。</p>
<p>陈怀朔受朝廷牵制 —— 权力动摇。</p>
<p>沈问桥秘密回归 —— 试图联合林九。</p>
<p>林九找到赵怀忠生前遗物</p>
<p>真相雏形浮现 —— 背后竟是跨境军火大网。</p>
<p>陈怀朔亲自追来</p>
<p>部族叛乱爆发</p>
<p>卓玛成为临时领袖</p>
<p>林九成为叛军眼中的“符号”</p>
<p>沈问桥召集新军南下</p>
<p>三方大势力第一次对峙</p>
<p>林九与陈怀朔正式决战前夜</p>
<p>黑石关真相公布 —— 颠覆所有认知。</p>
<p>陈怀朔的崩溃与疯狂</p>
<p>罗铁山的最后背叛</p>
<p>山谷决战 —— 三线混战落幕。</p>
<p>陈怀朔之死 —— 权力斗争终章。</p>
<p>部族局势稳定 —— 卓玛的未来去向。</p>
<p>林九的归途 —— 个人命运的选择。</p>
<p>大地回声（终章） —— 时代落幕，人物命运回响。</p>
<p>卷一：乱世初光（1–20章）详细大纲
主题</p>
<p>介绍主角林九、卓玛、陈怀朔等核心人物</p>
<p>展示清末边陲腐败、民族复杂与山地走私世界</p>
<p>铺设追杀、反清与跨民族势力冲突</p>
<p>小人物视角与宏观世界观并行展开</p>
<p>第1章：雨夜逃亡（林九 POV）</p>
<p>林九夜晚逃离官府追捕，误杀巡缉营百户赵怀忠</p>
<p>城市街巷描写紧迫感，展现林九机敏与生存本能</p>
<p>林九内心独白：对清廷与官兵的恐惧与憎恨</p>
<p>伏笔：林九杀赵怀忠会触发朝廷高层动作</p>
<p>第2章：山影深处（林九 POV &amp; 卓玛 POV）</p>
<p>林九逃入山地，受伤且体力衰竭</p>
<p>偶遇卓玛与走私商队，初步试探与信任建立</p>
<p>卓玛评估林九身手与潜力</p>
<p>伏笔：走私队路线、山地民族复杂格局</p>
<p>第3章：卓玛的山路（卓玛 POV）</p>
<p>展示走私队日常、边地文化、民族交易</p>
<p>山路险恶、自然劫难，队员之间小摩擦</p>
<p>卓玛暗示林九，他可能成为未来局势的一枚棋子</p>
<p>POV 对比：林九的生存焦虑 vs 卓玛的全局算计</p>
<p>第4章：黑石关的谣言（林九 POV）</p>
<p>林九开始意识到赵怀忠之死已经引起巡缉营注意</p>
<p>山民传闻：官兵会全面搜山</p>
<p>林九内心的恐惧与反抗意识加强</p>
<p>伏笔：官兵势力、边地巡逻体系</p>
<p>第5章：陈怀朔登场（陈怀朔 POV）</p>
<p>朝廷内部会议：赵怀忠死讯传来</p>
<p>陈怀朔观察清廷腐败、权力博弈</p>
<p>内心独白：对官场、军队腐败的清醒认知</p>
<p>伏笔：陈怀朔对林九潜力的早期觉察</p>
<p>第6章：走私队的试探（林九 POV &amp; 卓玛 POV）</p>
<p>林九被怀疑身份，被迫展示能力</p>
<p>卓玛观察他战斗力、心理素质</p>
<p>POV 切换：林九紧张、卓玛冷静分析</p>
<p>伏笔：林九可能成为走私队短期盟友</p>
<p>第7章：夜叉岭伏击（林九 POV）</p>
<p>敌对走私势力伏击走私队</p>
<p>林九首次实战参与，表现出野性与敏锐</p>
<p>卓玛对林九产生初步信任</p>
<p>伏笔：敌对势力后续可能成为宏大线索</p>
<p>第8章：废塔下的秘密（林九 POV）</p>
<p>林九意外发现旧遗迹或密道，藏匿部族的历史痕迹</p>
<p>内心独白：生死边缘的恐惧与好奇</p>
<p>POV 渲染边地神秘感</p>
<p>第9章：陈怀朔的密令（陈怀朔 POV）</p>
<p>指示手下追查林九身份与赵怀忠之死</p>
<p>显示清廷内部暗流与权力操作</p>
<p>POV：中距离理性叙事，揭示政治压力</p>
<p>伏笔：林九的逃亡可能触发全国范围连锁反应</p>
<p>第10章：山地黑市（林九 &amp; 卓玛 POV）</p>
<p>边市黑市描写，山地民族、外商、走私交易</p>
<p>林九第一次目睹走私体系、民族间微妙关系</p>
<p>卓玛解释潜规则，展示她的智慧与权谋</p>
<p>伏笔：火器、药材、商贸线路可用于后续军事冲突</p>
<p>第11章：火药味的市集（林九 POV）</p>
<p>林九卷入市集冲突，初显身手</p>
<p>与卓玛并肩作战，感情与信任萌芽</p>
<p>描写混乱场景与民族摩擦</p>
<p>第12章：红川寨旧事（卓玛 POV）</p>
<p>展示卓玛的背景、部族历史、旧日仇恨</p>
<p>她的领导力与冷静分析能力</p>
<p>伏笔：后续部族政治矛盾</p>
<p>第13章：陈怀朔的对手：沈问桥（陈怀朔 POV）</p>
<p>展示官场派系斗争</p>
<p>新旧思想冲突（保守 vs 改革）</p>
<p>伏笔：沈问桥在后续可能成为林九潜在合作或对立力量</p>
<p>第14章：大雾中的追兵（林九 POV）</p>
<p>巡缉营追踪林九，生死一线</p>
<p>林九利用地形与生存智慧脱险</p>
<p>POV 强调紧迫感与求生欲</p>
<p>第15章：神婆洞的疗伤（林九 POV &amp; 卓玛 POV）</p>
<p>林九疗伤，同时了解到部族文化与信仰</p>
<p>卓玛在旁指导，展现领导能力</p>
<p>POV 切换，展示林九对边地的陌生与好奇</p>
<p>第16章：护送决定（卓玛 POV）</p>
<p>卓玛决定带林九去边市避祸</p>
<p>内心分析林九潜力与风险</p>
<p>POV 体现全局视野，铺垫后续合作</p>
<p>第17章：陈怀朔与军机大臣会面（陈怀朔 POV）</p>
<p>展示朝廷内部对赵怀忠死的应对</p>
<p>暗线：林九的身份已被上层知晓</p>
<p>POV：政治理性、制度腐败、权力运作</p>
<p>第18章：风雪山道（林九 POV &amp; 卓玛 POV）</p>
<p>自然劫难：山路滑、风雪阻路</p>
<p>林九受伤，卓玛组织队伍前行</p>
<p>POV 切换：生存危机 + 全局控制</p>
<p>第19章：黑夜审判（走私队内部 POV）</p>
<p>队员因林九身份产生内部矛盾</p>
<p>卓玛调解，展示领导智慧</p>
<p>POV：多视角，显示团队动态</p>
<p>第20章：边市初见（林九 &amp; 卓玛 POV）</p>
<p>抵达边市，外部世界更加复杂</p>
<p>林九目睹多民族交易、权力冲突</p>
<p>卓玛分析局势、规划路线</p>
<p>第一卷落幕：林九暂时安全，但命运与时代线交织</p>
<p>二：风尘山河（第21–40章详细大纲）
主题</p>
<p>边市势力混乱与走私网络升级</p>
<p>林九与卓玛关系加深</p>
<p>陈怀朔的清廷调查与权力博弈</p>
<p>外部势力与民族矛盾逐渐揭开</p>
<p>铺设更宏大的反清与跨民族联盟线</p>
<p>第21章：边市三龙（林九 &amp; 卓玛 POV）</p>
<p>展示边市主要势力：走私商队、山地部族、外来商人</p>
<p>林九第一次感受到城市外的政治复杂</p>
<p>卓玛分析各方利益、初步策划行程</p>
<p>伏笔：黑市火器交易与跨境势力</p>
<p>第22章：林九的假身份（林九 POV）</p>
<p>林九被迫伪装身份，隐藏杀赵怀忠事实</p>
<p>内心矛盾：害怕暴露 vs 生存需求</p>
<p>展现林九适应边市生活的能力与智慧</p>
<p>第23章：卓玛的交易（卓玛 POV）</p>
<p>卓玛与边市外商谈判走私火药与药材</p>
<p>展示她策略、冷静与威慑能力</p>
<p>伏笔：未来外商可能与反清势力牵连</p>
<p>第24章：陈怀朔的暗探行动（陈怀朔 POV）</p>
<p>派出密探调查林九与走私网络</p>
<p>展现清廷内部腐败与权力操作</p>
<p>伏笔：林九被盯上，引出后续冲突</p>
<p>第25章：沈问桥的改革计划（沈问桥 POV）</p>
<p>官场内部新派与旧派矛盾</p>
<p>沈问桥提出对边市管控、军队改革</p>
<p>POV展示朝廷内部冲突，埋伏政治悬念</p>
<p>第26章：赌馆风波（林九 POV）</p>
<p>林九卷入赌馆冲突，展现身手</p>
<p>结识边市小势力人物，建立人脉</p>
<p>伏笔：敌对势力线索浮现</p>
<p>第27章：部族使节到访（卓玛 POV）</p>
<p>部族派代表谈判，卓玛被卷入政治婚约</p>
<p>展示边地民族权力结构与文化</p>
<p>伏笔：部族内部矛盾与反清潜力</p>
<p>第28章：林九身份被识破部分（林九 POV）</p>
<p>走私队中有人怀疑林九真实身份</p>
<p>林九紧张应对，同时保持冷静</p>
<p>POV对比：林九紧张 vs 卓玛冷静分析</p>
<p>第29章：碎石帮伏笔（林九 &amp; 卓玛 POV）</p>
<p>黑市地下势力出现，暗示后续冲突</p>
<p>林九初步了解黑市生存规则</p>
<p>卓玛暗示林九可能成为核心棋子</p>
<p>第30章：山地幽灵传说（林九 POV）</p>
<p>展示部族文化、传说与山地险恶环境</p>
<p>增强世界观神秘感</p>
<p>POV：林九的好奇与恐惧并行</p>
<p>第31章：陈怀朔的第一次胜利（陈怀朔 POV）</p>
<p>扫荡腐败官吏、查抄赵怀忠相关账务</p>
<p>展现政治手段与策略</p>
<p>POV：中距离理性叙事，铺垫对林九威胁</p>
<p>第32章：沈问桥的反击（沈问桥 POV）</p>
<p>利用外势力牵制陈怀朔</p>
<p>展示官场暗战与政策矛盾</p>
<p>POV：政治智斗，增加紧张感</p>
<p>第33章：卓玛的抉择（卓玛 POV）</p>
<p>拒绝部族婚约，承受压力</p>
<p>内心独白：自由与责任</p>
<p>POV：人物成长与领导力显现</p>
<p>第34章：林九过去的阴影（林九 POV）</p>
<p>回忆杀赵怀忠的细节，揭示逃亡动机</p>
<p>内心纠结与野性并存</p>
<p>伏笔：杀人动机与反清理念形成基础</p>
<p>第35章：走私队旧债未还（卓玛 POV）</p>
<p>山外“鹰眼”势力索要债务</p>
<p>展示黑市交易规则与潜在敌对势力</p>
<p>POV：卓玛全局分析、预判风险</p>
<p>第36章：边市大集祭典（林九 &amp; 卓玛 POV）</p>
<p>民族节日场景描写</p>
<p>林九与卓玛感情线微妙发展</p>
<p>POV切换：生动展现文化、人物互动</p>
<p>第37章：暗巷火拼（林九 POV）</p>
<p>林九遭遇陈怀朔密探暗算</p>
<p>动作描写与心理刻画并行</p>
<p>POV：紧张感与生存压力突出</p>
<p>第38章：卓玛营救（卓玛 POV）</p>
<p>卓玛出手救林九，展现领导力与智慧</p>
<p>POV：冷静、算计、果断</p>
<p>第39章：密探被俘（林九 &amp; 卓玛 POV）</p>
<p>取得密探口供，揭示清廷调查策略</p>
<p>POV切换：林九感受压力、卓玛分析形势</p>
<p>第40章：黑湖密会（林九 &amp; 卓玛 POV）</p>
<p>林九与卓玛商议下一步计划</p>
<p>谈论走私网络、边地部族与清廷态势</p>
<p>第一阶段落点：林九暂时安全，但未来隐患铺开</p>
<p>伏笔：外部势力、部族政治、黑市火器线索埋下</p>
<p>卷三：铁马长风（第41–60章详细大纲）
主题</p>
<p>政治线、走私线、部族冲突全面升级</p>
<p>林九、卓玛和陈怀朔关系冲突升级</p>
<p>各势力明争暗斗加剧</p>
<p>赵怀忠死因、边地黑市、军阀势力、民族矛盾进一步揭示</p>
<p>为卷四的大决战做铺垫</p>
<p>第41章：陈怀朔的全面布控（陈怀朔 POV）</p>
<p>派出更多密探追查林九与走私网络</p>
<p>清廷开始整顿岭南巡缉营与走私市场</p>
<p>POV展示政治策略与权力运作</p>
<p>伏笔：林九将成为清廷重点目标</p>
<p>第42章：部族内乱前兆（卓玛 POV）</p>
<p>部族内部矛盾浮现：婚约派系与自由派冲突</p>
<p>卓玛调解冲突，展示领导与外交能力</p>
<p>POV：策略与风险判断</p>
<p>第43章：山地军阀“罗铁山”登场（林九 POV）</p>
<p>罗铁山势力介入边市和山地，开始对走私网络和部族施压</p>
<p>林九初次接触，感受军事势力压迫</p>
<p>伏笔：罗铁山将成为关键对手</p>
<p>第44章：林九与罗铁山第一次交锋（林九 POV）</p>
<p>小规模冲突展示林九身手与智慧</p>
<p>POV突出紧张感与策略应对</p>
<p>林九意识到自己的力量仍有限</p>
<p>第45章：沈问桥打造新军（沈问桥 POV）</p>
<p>官场改革与军事重组并行</p>
<p>POV展现朝廷内部对边地局势的应对</p>
<p>伏笔：清廷派遣正规军将直接影响边地局势</p>
<p>第46章：黑石关旧案重启（林九 &amp; 卓玛 POV）</p>
<p>林九寻找赵怀忠遗留线索</p>
<p>卓玛协助分析敌对势力网络</p>
<p>POV切换：行动与策略并行</p>
<p>伏笔：赵怀忠死因真正原因浮出水面</p>
<p>第47章：卓玛被绑架（卓玛 POV）</p>
<p>部族极端派对卓玛实施威胁</p>
<p>她冷静应对，展现心理承受力</p>
<p>POV：人物独立性与智慧突显</p>
<p>第48章：林九入山救人（林九 POV）</p>
<p>林九冒险进入山谷，救出卓玛</p>
<p>展示个人英雄主义与智慧</p>
<p>POV：紧张感、悬念与行动描写</p>
<p>第49章：部族议会（卓玛 POV）</p>
<p>部族内部讨论应对清廷与外部势力</p>
<p>卓玛开始在部族中建立威望</p>
<p>POV：策略、文化冲突与权力平衡</p>
<p>第50章：林九的大胆声明（林九 POV）</p>
<p>承担“罪人”身份换取卓玛自由</p>
<p>POV：人格魅力与责任感</p>
<p>伏笔：林九开始在部族与走私网络中建立威信</p>
<p>第51章：陈怀朔掌握林九真实身份（陈怀朔 POV）</p>
<p>林九被列为清廷一级通缉对象</p>
<p>POV：政治角度观察林九威胁与潜力</p>
<p>伏笔：未来正面冲突不可避免</p>
<p>第52章：罗铁山背叛朝廷（罗铁山 POV）</p>
<p>宣布独立或自主军事行动</p>
<p>林九、卓玛和边地民族面临新压力</p>
<p>POV：军事威胁与战略局势显现</p>
<p>第53章：边市全面骚乱（林九 &amp; 卓玛 POV）</p>
<p>罗铁山与其他势力冲突</p>
<p>林九、卓玛调解与生存</p>
<p>POV：紧张感与混乱氛围描写</p>
<p>第54章：陈怀朔的清洗行动（陈怀朔 POV）</p>
<p>扫荡腐败官吏，掌控部分边市势力</p>
<p>POV：政治手段与制度操作</p>
<p>伏笔：清廷压力将迫使林九更主动出击</p>
<p>第55章：沈问桥遭牵连下台（沈问桥 POV）</p>
<p>政治派系斗争，显示权力更迭</p>
<p>POV：官场角力与后续影响铺垫</p>
<p>第56章：卓玛的觉醒（卓玛 POV）</p>
<p>从走私者转变为部族谈判者</p>
<p>展示人物成长与战略眼光</p>
<p>伏笔：后续成为反清联盟核心人物</p>
<p>第57章：林九与陈怀朔的第一次交锋（暗战）（林九 &amp; 陈怀朔 POV）</p>
<p>双方在边市暗中较量</p>
<p>POV切换：行动与心理战并行</p>
<p>伏笔：为最终正面决战埋下伏笔</p>
<p>第58章：黑湖大火（林九 &amp; 卓玛 POV）</p>
<p>走私网络被毁灭性打击</p>
<p>林九与卓玛行动受阻</p>
<p>POV：紧张、危险与人物应变</p>
<p>第59章：多方混战的尾声（林九 &amp; 卓玛 POV）</p>
<p>各方势力消耗殆尽</p>
<p>林九、卓玛在混乱中生存</p>
<p>POV：战略布局与心理描写并行</p>
<p>第60章：林九与卓玛离开边市（林九 &amp; 卓玛 POV）</p>
<p>离开边市，前往山地深处</p>
<p>POV：人物关系深化，行动目标明确</p>
<p>伏笔：卷四大决战的筹备与外部势力集结</p>
]]></description>
      <content:encoded><![CDATA[<p>整体结构分为 四大卷：</p>
<p>卷一：乱世初光（1–20）</p>
<p>卷二：风尘山河（21–40）</p>
<p>卷三：铁马长风（41–60）</p>
<p>卷四：大地回声（61–80）</p>
<p>📘 全书 80 章章节目录（可用于后续逐章写作）
卷一：乱世初光（1–20）</p>
<p>主题：人物登场、核心矛盾确立、追杀与逃亡、边地世界观展开。</p>
<p>雨夜逃亡 —— 林九逃出城，兵丁紧追。</p>
<p>山风与火光 —— 林九在山地险死，遇见卓玛与走私队。</p>
<p>卓玛的山路（卓玛 POV）—— 走私路线与部族生活揭示。</p>
<p>黑石关的谣言 —— 赵怀忠死讯外传。</p>
<p>陈怀朔登场（朝廷 POV）—— 权力真空与内部腐败初露。</p>
<p>走私队的试探 —— 林九被怀疑身份。</p>
<p>夜叉岭伏击 —— 走私队遭敌对势力袭击。</p>
<p>废塔下的秘密 —— 林九意外暴露身手，卓玛起疑。</p>
<p>陈怀朔的密令 —— 查“叛徒名单”，逼近林九。</p>
<p>山地黑市 —— 多势力混杂的交易场景展开世界观。</p>
<p>火药味的市集 —— 林九与卓玛第一次并肩作战。</p>
<p>红川寨旧事 —— 卓玛背景初揭。</p>
<p>陈怀朔的对手：沈问桥 —— 新派官僚与旧势力冲突。</p>
<p>大雾中的追兵 —— 林九负伤。</p>
<p>神婆洞的疗伤 —— 部族文化展示。</p>
<p>护送决定 —— 卓玛决定带林九去边市。</p>
<p>陈怀朔与军机大臣会面 —— 权术操作的第一回合。</p>
<p>风雪山道 —— 队伍遭自然劫难。</p>
<p>黑夜审判 —— 走私队内部矛盾爆发。</p>
<p>边市初见 —— 第一卷阶段性落点。</p>
<p>卷二：风尘山河（21–40）</p>
<p>主题：边市繁乱、商队、军阀势力与清廷暗线交织；林九与卓玛关系深化。</p>
<p>边市三龙（势力全景）</p>
<p>林九的假身份 —— 被迫成为武师。</p>
<p>卓玛的交易 —— 与山外商人谈判。</p>
<p>陈怀朔的暗探行动 —— 寻找赵怀忠死因线索。</p>
<p>沈问桥的改革计划 —— 新旧思想对撞。</p>
<p>赌馆风波 —— 林九平息冲突引起注意。</p>
<p>部族使节到访 —— 卓玛被卷入政治婚约。</p>
<p>林九被识破部分身份</p>
<p>“碎石帮”伏笔 —— 黑市地下势力浮出。</p>
<p>山地幽灵传说 —— 背后暗示军火走线。</p>
<p>陈怀朔的第一次胜利 —— 扫荡一批腐败官吏。</p>
<p>沈问桥的反击 —— 利用外势力牵制陈怀朔。</p>
<p>卓玛的抉择 —— 拒绝婚约，引部族压力。</p>
<p>林九过去的阴影 —— 为何被追杀之谜初现。</p>
<p>走私队旧债未还 —— 山外“鹰眼”前来讨债。</p>
<p>边市大集祭典 —— 林九与卓玛关系升温。</p>
<p>暗巷火拼 —— 林九遭到陈怀朔派出的密探暗算。</p>
<p>卓玛营救 —— 感情线突破。</p>
<p>密探被俘 —— 林九得知更大的阴谋。</p>
<p>黑湖密会 —— 林九、卓玛决定一起深入找真相。</p>
<p>卷三：铁马长风（41–60）</p>
<p>主题：政治线、走私线、部族冲突全面升级；林九与陈怀朔第一次真正对决。</p>
<p>陈怀朔的全面布控</p>
<p>部族内乱前兆 —— 婚约派系反扑。</p>
<p>山地军阀“罗铁山”登场</p>
<p>林九与罗铁山第一次交锋</p>
<p>沈问桥打造新军 —— 时代更迭显现。</p>
<p>黑石关旧案重启 —— 赵怀忠真正死因线索上浮。</p>
<p>卓玛被绑架 —— 部族极端派行动。</p>
<p>林九入山救人</p>
<p>部族议会 —— 卓玛命运的审判。</p>
<p>林九的大胆声明 —— 承担“罪人”身份换她自由。</p>
<p>陈怀朔掌握林九真实身份</p>
<p>罗铁山背叛朝廷 —— 双线混战。</p>
<p>边市全面骚乱</p>
<p>陈怀朔的清洗行动</p>
<p>沈问桥遭牵连下台</p>
<p>卓玛的觉醒 —— 从走私者转变为部族谈判者。</p>
<p>林九与陈怀朔的第一次交锋（暗战）</p>
<p>黑湖大火 —— 走私线被毁灭性打击。</p>
<p>多方混战的尾声 —— 边市秩序土崩瓦解。</p>
<p>林九与卓玛离开边市 —— 去往更深的山地。</p>
<p>卷四：大地回声（61–80）</p>
<p>主题：反转、真相揭露、命运决战、时代收束。</p>
<p>深入部族腹地 —— 高原世界观全面展开。</p>
<p>陈怀朔受朝廷牵制 —— 权力动摇。</p>
<p>沈问桥秘密回归 —— 试图联合林九。</p>
<p>林九找到赵怀忠生前遗物</p>
<p>真相雏形浮现 —— 背后竟是跨境军火大网。</p>
<p>陈怀朔亲自追来</p>
<p>部族叛乱爆发</p>
<p>卓玛成为临时领袖</p>
<p>林九成为叛军眼中的“符号”</p>
<p>沈问桥召集新军南下</p>
<p>三方大势力第一次对峙</p>
<p>林九与陈怀朔正式决战前夜</p>
<p>黑石关真相公布 —— 颠覆所有认知。</p>
<p>陈怀朔的崩溃与疯狂</p>
<p>罗铁山的最后背叛</p>
<p>山谷决战 —— 三线混战落幕。</p>
<p>陈怀朔之死 —— 权力斗争终章。</p>
<p>部族局势稳定 —— 卓玛的未来去向。</p>
<p>林九的归途 —— 个人命运的选择。</p>
<p>大地回声（终章） —— 时代落幕，人物命运回响。</p>
<p>卷一：乱世初光（1–20章）详细大纲
主题</p>
<p>介绍主角林九、卓玛、陈怀朔等核心人物</p>
<p>展示清末边陲腐败、民族复杂与山地走私世界</p>
<p>铺设追杀、反清与跨民族势力冲突</p>
<p>小人物视角与宏观世界观并行展开</p>
<p>第1章：雨夜逃亡（林九 POV）</p>
<p>林九夜晚逃离官府追捕，误杀巡缉营百户赵怀忠</p>
<p>城市街巷描写紧迫感，展现林九机敏与生存本能</p>
<p>林九内心独白：对清廷与官兵的恐惧与憎恨</p>
<p>伏笔：林九杀赵怀忠会触发朝廷高层动作</p>
<p>第2章：山影深处（林九 POV &amp; 卓玛 POV）</p>
<p>林九逃入山地，受伤且体力衰竭</p>
<p>偶遇卓玛与走私商队，初步试探与信任建立</p>
<p>卓玛评估林九身手与潜力</p>
<p>伏笔：走私队路线、山地民族复杂格局</p>
<p>第3章：卓玛的山路（卓玛 POV）</p>
<p>展示走私队日常、边地文化、民族交易</p>
<p>山路险恶、自然劫难，队员之间小摩擦</p>
<p>卓玛暗示林九，他可能成为未来局势的一枚棋子</p>
<p>POV 对比：林九的生存焦虑 vs 卓玛的全局算计</p>
<p>第4章：黑石关的谣言（林九 POV）</p>
<p>林九开始意识到赵怀忠之死已经引起巡缉营注意</p>
<p>山民传闻：官兵会全面搜山</p>
<p>林九内心的恐惧与反抗意识加强</p>
<p>伏笔：官兵势力、边地巡逻体系</p>
<p>第5章：陈怀朔登场（陈怀朔 POV）</p>
<p>朝廷内部会议：赵怀忠死讯传来</p>
<p>陈怀朔观察清廷腐败、权力博弈</p>
<p>内心独白：对官场、军队腐败的清醒认知</p>
<p>伏笔：陈怀朔对林九潜力的早期觉察</p>
<p>第6章：走私队的试探（林九 POV &amp; 卓玛 POV）</p>
<p>林九被怀疑身份，被迫展示能力</p>
<p>卓玛观察他战斗力、心理素质</p>
<p>POV 切换：林九紧张、卓玛冷静分析</p>
<p>伏笔：林九可能成为走私队短期盟友</p>
<p>第7章：夜叉岭伏击（林九 POV）</p>
<p>敌对走私势力伏击走私队</p>
<p>林九首次实战参与，表现出野性与敏锐</p>
<p>卓玛对林九产生初步信任</p>
<p>伏笔：敌对势力后续可能成为宏大线索</p>
<p>第8章：废塔下的秘密（林九 POV）</p>
<p>林九意外发现旧遗迹或密道，藏匿部族的历史痕迹</p>
<p>内心独白：生死边缘的恐惧与好奇</p>
<p>POV 渲染边地神秘感</p>
<p>第9章：陈怀朔的密令（陈怀朔 POV）</p>
<p>指示手下追查林九身份与赵怀忠之死</p>
<p>显示清廷内部暗流与权力操作</p>
<p>POV：中距离理性叙事，揭示政治压力</p>
<p>伏笔：林九的逃亡可能触发全国范围连锁反应</p>
<p>第10章：山地黑市（林九 &amp; 卓玛 POV）</p>
<p>边市黑市描写，山地民族、外商、走私交易</p>
<p>林九第一次目睹走私体系、民族间微妙关系</p>
<p>卓玛解释潜规则，展示她的智慧与权谋</p>
<p>伏笔：火器、药材、商贸线路可用于后续军事冲突</p>
<p>第11章：火药味的市集（林九 POV）</p>
<p>林九卷入市集冲突，初显身手</p>
<p>与卓玛并肩作战，感情与信任萌芽</p>
<p>描写混乱场景与民族摩擦</p>
<p>第12章：红川寨旧事（卓玛 POV）</p>
<p>展示卓玛的背景、部族历史、旧日仇恨</p>
<p>她的领导力与冷静分析能力</p>
<p>伏笔：后续部族政治矛盾</p>
<p>第13章：陈怀朔的对手：沈问桥（陈怀朔 POV）</p>
<p>展示官场派系斗争</p>
<p>新旧思想冲突（保守 vs 改革）</p>
<p>伏笔：沈问桥在后续可能成为林九潜在合作或对立力量</p>
<p>第14章：大雾中的追兵（林九 POV）</p>
<p>巡缉营追踪林九，生死一线</p>
<p>林九利用地形与生存智慧脱险</p>
<p>POV 强调紧迫感与求生欲</p>
<p>第15章：神婆洞的疗伤（林九 POV &amp; 卓玛 POV）</p>
<p>林九疗伤，同时了解到部族文化与信仰</p>
<p>卓玛在旁指导，展现领导能力</p>
<p>POV 切换，展示林九对边地的陌生与好奇</p>
<p>第16章：护送决定（卓玛 POV）</p>
<p>卓玛决定带林九去边市避祸</p>
<p>内心分析林九潜力与风险</p>
<p>POV 体现全局视野，铺垫后续合作</p>
<p>第17章：陈怀朔与军机大臣会面（陈怀朔 POV）</p>
<p>展示朝廷内部对赵怀忠死的应对</p>
<p>暗线：林九的身份已被上层知晓</p>
<p>POV：政治理性、制度腐败、权力运作</p>
<p>第18章：风雪山道（林九 POV &amp; 卓玛 POV）</p>
<p>自然劫难：山路滑、风雪阻路</p>
<p>林九受伤，卓玛组织队伍前行</p>
<p>POV 切换：生存危机 + 全局控制</p>
<p>第19章：黑夜审判（走私队内部 POV）</p>
<p>队员因林九身份产生内部矛盾</p>
<p>卓玛调解，展示领导智慧</p>
<p>POV：多视角，显示团队动态</p>
<p>第20章：边市初见（林九 &amp; 卓玛 POV）</p>
<p>抵达边市，外部世界更加复杂</p>
<p>林九目睹多民族交易、权力冲突</p>
<p>卓玛分析局势、规划路线</p>
<p>第一卷落幕：林九暂时安全，但命运与时代线交织</p>
<p>二：风尘山河（第21–40章详细大纲）
主题</p>
<p>边市势力混乱与走私网络升级</p>
<p>林九与卓玛关系加深</p>
<p>陈怀朔的清廷调查与权力博弈</p>
<p>外部势力与民族矛盾逐渐揭开</p>
<p>铺设更宏大的反清与跨民族联盟线</p>
<p>第21章：边市三龙（林九 &amp; 卓玛 POV）</p>
<p>展示边市主要势力：走私商队、山地部族、外来商人</p>
<p>林九第一次感受到城市外的政治复杂</p>
<p>卓玛分析各方利益、初步策划行程</p>
<p>伏笔：黑市火器交易与跨境势力</p>
<p>第22章：林九的假身份（林九 POV）</p>
<p>林九被迫伪装身份，隐藏杀赵怀忠事实</p>
<p>内心矛盾：害怕暴露 vs 生存需求</p>
<p>展现林九适应边市生活的能力与智慧</p>
<p>第23章：卓玛的交易（卓玛 POV）</p>
<p>卓玛与边市外商谈判走私火药与药材</p>
<p>展示她策略、冷静与威慑能力</p>
<p>伏笔：未来外商可能与反清势力牵连</p>
<p>第24章：陈怀朔的暗探行动（陈怀朔 POV）</p>
<p>派出密探调查林九与走私网络</p>
<p>展现清廷内部腐败与权力操作</p>
<p>伏笔：林九被盯上，引出后续冲突</p>
<p>第25章：沈问桥的改革计划（沈问桥 POV）</p>
<p>官场内部新派与旧派矛盾</p>
<p>沈问桥提出对边市管控、军队改革</p>
<p>POV展示朝廷内部冲突，埋伏政治悬念</p>
<p>第26章：赌馆风波（林九 POV）</p>
<p>林九卷入赌馆冲突，展现身手</p>
<p>结识边市小势力人物，建立人脉</p>
<p>伏笔：敌对势力线索浮现</p>
<p>第27章：部族使节到访（卓玛 POV）</p>
<p>部族派代表谈判，卓玛被卷入政治婚约</p>
<p>展示边地民族权力结构与文化</p>
<p>伏笔：部族内部矛盾与反清潜力</p>
<p>第28章：林九身份被识破部分（林九 POV）</p>
<p>走私队中有人怀疑林九真实身份</p>
<p>林九紧张应对，同时保持冷静</p>
<p>POV对比：林九紧张 vs 卓玛冷静分析</p>
<p>第29章：碎石帮伏笔（林九 &amp; 卓玛 POV）</p>
<p>黑市地下势力出现，暗示后续冲突</p>
<p>林九初步了解黑市生存规则</p>
<p>卓玛暗示林九可能成为核心棋子</p>
<p>第30章：山地幽灵传说（林九 POV）</p>
<p>展示部族文化、传说与山地险恶环境</p>
<p>增强世界观神秘感</p>
<p>POV：林九的好奇与恐惧并行</p>
<p>第31章：陈怀朔的第一次胜利（陈怀朔 POV）</p>
<p>扫荡腐败官吏、查抄赵怀忠相关账务</p>
<p>展现政治手段与策略</p>
<p>POV：中距离理性叙事，铺垫对林九威胁</p>
<p>第32章：沈问桥的反击（沈问桥 POV）</p>
<p>利用外势力牵制陈怀朔</p>
<p>展示官场暗战与政策矛盾</p>
<p>POV：政治智斗，增加紧张感</p>
<p>第33章：卓玛的抉择（卓玛 POV）</p>
<p>拒绝部族婚约，承受压力</p>
<p>内心独白：自由与责任</p>
<p>POV：人物成长与领导力显现</p>
<p>第34章：林九过去的阴影（林九 POV）</p>
<p>回忆杀赵怀忠的细节，揭示逃亡动机</p>
<p>内心纠结与野性并存</p>
<p>伏笔：杀人动机与反清理念形成基础</p>
<p>第35章：走私队旧债未还（卓玛 POV）</p>
<p>山外“鹰眼”势力索要债务</p>
<p>展示黑市交易规则与潜在敌对势力</p>
<p>POV：卓玛全局分析、预判风险</p>
<p>第36章：边市大集祭典（林九 &amp; 卓玛 POV）</p>
<p>民族节日场景描写</p>
<p>林九与卓玛感情线微妙发展</p>
<p>POV切换：生动展现文化、人物互动</p>
<p>第37章：暗巷火拼（林九 POV）</p>
<p>林九遭遇陈怀朔密探暗算</p>
<p>动作描写与心理刻画并行</p>
<p>POV：紧张感与生存压力突出</p>
<p>第38章：卓玛营救（卓玛 POV）</p>
<p>卓玛出手救林九，展现领导力与智慧</p>
<p>POV：冷静、算计、果断</p>
<p>第39章：密探被俘（林九 &amp; 卓玛 POV）</p>
<p>取得密探口供，揭示清廷调查策略</p>
<p>POV切换：林九感受压力、卓玛分析形势</p>
<p>第40章：黑湖密会（林九 &amp; 卓玛 POV）</p>
<p>林九与卓玛商议下一步计划</p>
<p>谈论走私网络、边地部族与清廷态势</p>
<p>第一阶段落点：林九暂时安全，但未来隐患铺开</p>
<p>伏笔：外部势力、部族政治、黑市火器线索埋下</p>
<p>卷三：铁马长风（第41–60章详细大纲）
主题</p>
<p>政治线、走私线、部族冲突全面升级</p>
<p>林九、卓玛和陈怀朔关系冲突升级</p>
<p>各势力明争暗斗加剧</p>
<p>赵怀忠死因、边地黑市、军阀势力、民族矛盾进一步揭示</p>
<p>为卷四的大决战做铺垫</p>
<p>第41章：陈怀朔的全面布控（陈怀朔 POV）</p>
<p>派出更多密探追查林九与走私网络</p>
<p>清廷开始整顿岭南巡缉营与走私市场</p>
<p>POV展示政治策略与权力运作</p>
<p>伏笔：林九将成为清廷重点目标</p>
<p>第42章：部族内乱前兆（卓玛 POV）</p>
<p>部族内部矛盾浮现：婚约派系与自由派冲突</p>
<p>卓玛调解冲突，展示领导与外交能力</p>
<p>POV：策略与风险判断</p>
<p>第43章：山地军阀“罗铁山”登场（林九 POV）</p>
<p>罗铁山势力介入边市和山地，开始对走私网络和部族施压</p>
<p>林九初次接触，感受军事势力压迫</p>
<p>伏笔：罗铁山将成为关键对手</p>
<p>第44章：林九与罗铁山第一次交锋（林九 POV）</p>
<p>小规模冲突展示林九身手与智慧</p>
<p>POV突出紧张感与策略应对</p>
<p>林九意识到自己的力量仍有限</p>
<p>第45章：沈问桥打造新军（沈问桥 POV）</p>
<p>官场改革与军事重组并行</p>
<p>POV展现朝廷内部对边地局势的应对</p>
<p>伏笔：清廷派遣正规军将直接影响边地局势</p>
<p>第46章：黑石关旧案重启（林九 &amp; 卓玛 POV）</p>
<p>林九寻找赵怀忠遗留线索</p>
<p>卓玛协助分析敌对势力网络</p>
<p>POV切换：行动与策略并行</p>
<p>伏笔：赵怀忠死因真正原因浮出水面</p>
<p>第47章：卓玛被绑架（卓玛 POV）</p>
<p>部族极端派对卓玛实施威胁</p>
<p>她冷静应对，展现心理承受力</p>
<p>POV：人物独立性与智慧突显</p>
<p>第48章：林九入山救人（林九 POV）</p>
<p>林九冒险进入山谷，救出卓玛</p>
<p>展示个人英雄主义与智慧</p>
<p>POV：紧张感、悬念与行动描写</p>
<p>第49章：部族议会（卓玛 POV）</p>
<p>部族内部讨论应对清廷与外部势力</p>
<p>卓玛开始在部族中建立威望</p>
<p>POV：策略、文化冲突与权力平衡</p>
<p>第50章：林九的大胆声明（林九 POV）</p>
<p>承担“罪人”身份换取卓玛自由</p>
<p>POV：人格魅力与责任感</p>
<p>伏笔：林九开始在部族与走私网络中建立威信</p>
<p>第51章：陈怀朔掌握林九真实身份（陈怀朔 POV）</p>
<p>林九被列为清廷一级通缉对象</p>
<p>POV：政治角度观察林九威胁与潜力</p>
<p>伏笔：未来正面冲突不可避免</p>
<p>第52章：罗铁山背叛朝廷（罗铁山 POV）</p>
<p>宣布独立或自主军事行动</p>
<p>林九、卓玛和边地民族面临新压力</p>
<p>POV：军事威胁与战略局势显现</p>
<p>第53章：边市全面骚乱（林九 &amp; 卓玛 POV）</p>
<p>罗铁山与其他势力冲突</p>
<p>林九、卓玛调解与生存</p>
<p>POV：紧张感与混乱氛围描写</p>
<p>第54章：陈怀朔的清洗行动（陈怀朔 POV）</p>
<p>扫荡腐败官吏，掌控部分边市势力</p>
<p>POV：政治手段与制度操作</p>
<p>伏笔：清廷压力将迫使林九更主动出击</p>
<p>第55章：沈问桥遭牵连下台（沈问桥 POV）</p>
<p>政治派系斗争，显示权力更迭</p>
<p>POV：官场角力与后续影响铺垫</p>
<p>第56章：卓玛的觉醒（卓玛 POV）</p>
<p>从走私者转变为部族谈判者</p>
<p>展示人物成长与战略眼光</p>
<p>伏笔：后续成为反清联盟核心人物</p>
<p>第57章：林九与陈怀朔的第一次交锋（暗战）（林九 &amp; 陈怀朔 POV）</p>
<p>双方在边市暗中较量</p>
<p>POV切换：行动与心理战并行</p>
<p>伏笔：为最终正面决战埋下伏笔</p>
<p>第58章：黑湖大火（林九 &amp; 卓玛 POV）</p>
<p>走私网络被毁灭性打击</p>
<p>林九与卓玛行动受阻</p>
<p>POV：紧张、危险与人物应变</p>
<p>第59章：多方混战的尾声（林九 &amp; 卓玛 POV）</p>
<p>各方势力消耗殆尽</p>
<p>林九、卓玛在混乱中生存</p>
<p>POV：战略布局与心理描写并行</p>
<p>第60章：林九与卓玛离开边市（林九 &amp; 卓玛 POV）</p>
<p>离开边市，前往山地深处</p>
<p>POV：人物关系深化，行动目标明确</p>
<p>伏笔：卷四大决战的筹备与外部势力集结</p>
]]></content:encoded>
    </item>
    <item>
      <title>高频Sql面试实战题目（mysql）的解答思路</title>
      <link>https://vansiit.cc/2025/12/09/sql-face.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/12/09/sql-face.html</guid>
      <pubDate>Tue, 09 Dec 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>高频Sql面试题目及答案和解答思路</h1>
<h2>第一部分：基础实战题（查询 / 分组 / 去重）</h2>
<h3>1.查询最近 30 天下单的用户数（去重）</h3>
<p>orders(id, user_id, amount, created_at)</p>
<blockquote>
<p>写出 SQL，统计最近 30 天内有下单的“独立用户总数”。</p>
</blockquote>
<p>思路：
. 使用 DATE_SUB 函数
. COUNT(DISTINCT user_id)</p>
<pre><code class="language-sql">SELECT COUNT(DISTINCT user_id) AS user_count
FROM orders
WHERE created_at &gt;= DATE_SUB(NOW(), INTERVAL 30 DAY);
</code></pre>
<h3>2. 查询每个用户的最新一笔订单</h3>
<p>orders(id, user_id, amount, created_at)</p>
<blockquote>
<p>给定订单表，返回每个 user_id 最新的订单记录（整行数据）。</p>
</blockquote>
<p>思路：</p>
<p>可以使用 JOIN + MAX(created_at) 的子查询方式，也可以使用窗口函数。MySQL 8 后窗口函数基本是标配考点</p>
<h4>子查询</h4>
<pre><code class="language-sql">select a.*
from orders a
JOIN (
select user_id, max(created_at) max_created_at from orders GROUP BY user_id
) b on a.user_id=b.user_id and a.created_at=b.max_created_at
</code></pre>
<h4>窗口函数</h4>
<pre><code class="language-sql">SELECT *
FROM (
    SELECT o.*,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
    FROM orders o
) t
WHERE rn = 1;
</code></pre>
<h3>3. 查询下单金额大于用户平均订单金额的订单</h3>
<p>思路：</p>
<p>使用 JOIN + AVG(amount) 的子查询方式</p>
<pre><code class="language-sql">SELECT o.*
FROM orders o
JOIN (
    SELECT user_id, AVG(amount) AS avg_amount
    FROM orders
    GROUP BY user_id
) t ON o.user_id = t.user_id
WHERE o.amount &gt; t.avg_amount;
</code></pre>
<h3>4. 查询连续下单 3 天以上的用户</h3>
<p>表中只有 created_at（日期），判断用户是否连续 3 天有订单。</p>
<h3>5. 查询本周每天的订单量，没有订单的日期也要显示 0</h3>
<p>要求：返回一周 7 天的完整统计（date, count），缺的日期补 0。</p>
<h2>第二部分：中级实战（多表关联 / 聚合 / 复杂条件）</h2>
<h3>6. 查询某用户的“未支付订单金额总和”</h3>
<p>order(id, user_id, status, amount)</p>
<p>status: 0-待支付 1-已支付</p>
<p>用户 9527 目前所有未支付订单的金额总和是多少？</p>
<p>—</p>
<h3>7. 查询每个品类下销量最高的商品（Top1）</h3>
<p>category(id, name)
product(id, category_id, name)
order_item(id, product_id, quantity)</p>
<p>找出每个 category 对应“销量最高”的 product。</p>
<p>窗口函数必须会。</p>
<p>—</p>
<h3>8. 查询 2024 年每个月的 GMV（成交额）</h3>
<p>要求返回 2024 年 1~12 月，哪怕某月是 0。</p>
<p>—</p>
<h3>9. 统计每个用户的下单金额区间（0~100、100~500、500+）</h3>
<p>类似电商用户分层，写 SQL 得到用户处于哪个金额段。</p>
<h3>—</h3>
<h3>10. 查询“只买过一次”的用户名单</h3>
<p>用户 lifetime 只有一单的 user_id。</p>
<h2>第三部分：进阶实战（窗口函数 / 反复过滤 / 子查询）</h2>
<h3>11. 查询用户最近一次下单距今多少天</h3>
<p>now() - 用户最后一单 created_at → 返回天数。</p>
<p>—</p>
<h3>12. 查询某用户前 3 次订单（按金额排序）</h3>
<p>user_id = X，amount 降序，取 Top3。</p>
<p>—</p>
<h3>13. 查询每个用户近 5 次订单的平均金额</h3>
<p>考点：窗口函数 + 分区 + order</p>
<p>—</p>
<h3>14. 查询“未支付金额 &gt; 已支付金额”的用户</h3>
<p>sum(未支付) &gt; sum(已支付)，返回 user_id。</p>
<p>（思路关键：group by + 条件聚合）</p>
<p>—</p>
<h3>15. 查询同一个用户上一笔订单距离下一笔订单相差多少天</h3>
<p>典型的 LEAD / LAG 窗口函数题。</p>
<p>要求返回：</p>
<p>user_id | order_id | created_at | next_created_at | interval_days</p>
<h2>🧨 🔥 第四部分：高难度（业务场景 + 常考）</h2>
<h3>16. 查询商品的“30 天滚动销售额”</h3>
<p>对每个商品每天算一个 “过去 30 天窗口” 的销售额（滑动窗口）。</p>
<p>需要窗口函数：</p>
<p>range between interval 29 day preceding and current row</p>
<p>—</p>
<h3>17. 查询“近 7 天连续无登录”的用户</h3>
<p>login(user_id, login_at)</p>
<p>某用户 7 天内完全没有登录过 → 返回这些 user_id。</p>
<p>（涉及补全日期 + group by + max/min）</p>
<p>—</p>
<h3>18. 找出被“重复提交”的订单（相同 user、相同金额、相同时间段）</h3>
<p>判断风控类问题：短时间内几乎相同的订单。</p>
<p>（考点：分组、having、近时间窗口）</p>
<p>—</p>
<h3>19. 查询门店每天的“新增用户数”</h3>
<p>用户第一单发生在该店，即视为该店新增。</p>
<p>（关键：min(created_at)）</p>
<p>—</p>
<h3>20. 查询“最近 10 分钟内下单次数 &gt; 5 次”的用户（防刷）</h3>
<p>典型反作弊滑窗题，有时需自连接或窗口函数实现。</p>
]]></description>
      <content:encoded><![CDATA[<h1>高频Sql面试题目及答案和解答思路</h1>
<h2>第一部分：基础实战题（查询 / 分组 / 去重）</h2>
<h3>1.查询最近 30 天下单的用户数（去重）</h3>
<p>orders(id, user_id, amount, created_at)</p>
<blockquote>
<p>写出 SQL，统计最近 30 天内有下单的“独立用户总数”。</p>
</blockquote>
<p>思路：
. 使用 DATE_SUB 函数
. COUNT(DISTINCT user_id)</p>
<pre><code class="language-sql">SELECT COUNT(DISTINCT user_id) AS user_count
FROM orders
WHERE created_at &gt;= DATE_SUB(NOW(), INTERVAL 30 DAY);
</code></pre>
<h3>2. 查询每个用户的最新一笔订单</h3>
<p>orders(id, user_id, amount, created_at)</p>
<blockquote>
<p>给定订单表，返回每个 user_id 最新的订单记录（整行数据）。</p>
</blockquote>
<p>思路：</p>
<p>可以使用 JOIN + MAX(created_at) 的子查询方式，也可以使用窗口函数。MySQL 8 后窗口函数基本是标配考点</p>
<h4>子查询</h4>
<pre><code class="language-sql">select a.*
from orders a
JOIN (
select user_id, max(created_at) max_created_at from orders GROUP BY user_id
) b on a.user_id=b.user_id and a.created_at=b.max_created_at
</code></pre>
<h4>窗口函数</h4>
<pre><code class="language-sql">SELECT *
FROM (
    SELECT o.*,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
    FROM orders o
) t
WHERE rn = 1;
</code></pre>
<h3>3. 查询下单金额大于用户平均订单金额的订单</h3>
<p>思路：</p>
<p>使用 JOIN + AVG(amount) 的子查询方式</p>
<pre><code class="language-sql">SELECT o.*
FROM orders o
JOIN (
    SELECT user_id, AVG(amount) AS avg_amount
    FROM orders
    GROUP BY user_id
) t ON o.user_id = t.user_id
WHERE o.amount &gt; t.avg_amount;
</code></pre>
<h3>4. 查询连续下单 3 天以上的用户</h3>
<p>表中只有 created_at（日期），判断用户是否连续 3 天有订单。</p>
<h3>5. 查询本周每天的订单量，没有订单的日期也要显示 0</h3>
<p>要求：返回一周 7 天的完整统计（date, count），缺的日期补 0。</p>
<h2>第二部分：中级实战（多表关联 / 聚合 / 复杂条件）</h2>
<h3>6. 查询某用户的“未支付订单金额总和”</h3>
<p>order(id, user_id, status, amount)</p>
<p>status: 0-待支付 1-已支付</p>
<p>用户 9527 目前所有未支付订单的金额总和是多少？</p>
<p>—</p>
<h3>7. 查询每个品类下销量最高的商品（Top1）</h3>
<p>category(id, name)
product(id, category_id, name)
order_item(id, product_id, quantity)</p>
<p>找出每个 category 对应“销量最高”的 product。</p>
<p>窗口函数必须会。</p>
<p>—</p>
<h3>8. 查询 2024 年每个月的 GMV（成交额）</h3>
<p>要求返回 2024 年 1~12 月，哪怕某月是 0。</p>
<p>—</p>
<h3>9. 统计每个用户的下单金额区间（0~100、100~500、500+）</h3>
<p>类似电商用户分层，写 SQL 得到用户处于哪个金额段。</p>
<h3>—</h3>
<h3>10. 查询“只买过一次”的用户名单</h3>
<p>用户 lifetime 只有一单的 user_id。</p>
<h2>第三部分：进阶实战（窗口函数 / 反复过滤 / 子查询）</h2>
<h3>11. 查询用户最近一次下单距今多少天</h3>
<p>now() - 用户最后一单 created_at → 返回天数。</p>
<p>—</p>
<h3>12. 查询某用户前 3 次订单（按金额排序）</h3>
<p>user_id = X，amount 降序，取 Top3。</p>
<p>—</p>
<h3>13. 查询每个用户近 5 次订单的平均金额</h3>
<p>考点：窗口函数 + 分区 + order</p>
<p>—</p>
<h3>14. 查询“未支付金额 &gt; 已支付金额”的用户</h3>
<p>sum(未支付) &gt; sum(已支付)，返回 user_id。</p>
<p>（思路关键：group by + 条件聚合）</p>
<p>—</p>
<h3>15. 查询同一个用户上一笔订单距离下一笔订单相差多少天</h3>
<p>典型的 LEAD / LAG 窗口函数题。</p>
<p>要求返回：</p>
<p>user_id | order_id | created_at | next_created_at | interval_days</p>
<h2>🧨 🔥 第四部分：高难度（业务场景 + 常考）</h2>
<h3>16. 查询商品的“30 天滚动销售额”</h3>
<p>对每个商品每天算一个 “过去 30 天窗口” 的销售额（滑动窗口）。</p>
<p>需要窗口函数：</p>
<p>range between interval 29 day preceding and current row</p>
<p>—</p>
<h3>17. 查询“近 7 天连续无登录”的用户</h3>
<p>login(user_id, login_at)</p>
<p>某用户 7 天内完全没有登录过 → 返回这些 user_id。</p>
<p>（涉及补全日期 + group by + max/min）</p>
<p>—</p>
<h3>18. 找出被“重复提交”的订单（相同 user、相同金额、相同时间段）</h3>
<p>判断风控类问题：短时间内几乎相同的订单。</p>
<p>（考点：分组、having、近时间窗口）</p>
<p>—</p>
<h3>19. 查询门店每天的“新增用户数”</h3>
<p>用户第一单发生在该店，即视为该店新增。</p>
<p>（关键：min(created_at)）</p>
<p>—</p>
<h3>20. 查询“最近 10 分钟内下单次数 &gt; 5 次”的用户（防刷）</h3>
<p>典型反作弊滑窗题，有时需自连接或窗口函数实现。</p>
]]></content:encoded>
    </item>
    <item>
      <title>为什么英文字母 W 读作 double u？</title>
      <link>https://vansiit.cc/2025/12/03/double-u.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/12/03/double-u.html</guid>
      <pubDate>Wed, 03 Dec 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>为什么英文字母 <strong>W</strong> 读作 <em>double u</em>？</h1>
<p>你可能从小就听过老师念字母：
<strong>“A、B、C……达不溜、X、Y、Z”</strong></p>
<p>等等。
谁？
<strong>达不溜？</strong></p>
<p>如果你第一次听英语字母表，就是靠这种“拼音式音译”记住的，那么今天我们就来一起拆解这个“神秘音节”背后的真相。</p>
<hr>
<h2>1. “达不溜”到底是什么来头？</h2>
<p>让我们先看标准英文读法：</p>
<blockquote>
<p><strong>W = /ˈdʌbəl ju/</strong> → double you
不是 dabuliu，也不是 da-bu-liu，更不是大不了。</p>
</blockquote>
<p>“达不溜”只是一种相当随缘的中文音译，目的就是让学生能记住这个读音。
它的功能类似于——</p>
<ul>
<li>把 pizza 叫成“批萨”</li>
<li>把 latte 叫成“拿铁”</li>
<li>把 sushi 叫成“寿司”</li>
</ul>
<p><strong>——你能认，但请不要真的跟外国人这么说。</strong></p>
<hr>
<h2>2. 为什么要叫 <em>double u</em>？</h2>
<p>这就涉及到一点英语史（别走！很好懂，也挺好笑）。</p>
<p>在古英语时代，字母表里<strong>根本没有 W</strong>。
英语中却有很多需要“呜”这个音的词，怎么办？</p>
<p>聪明的修士们决定：
<strong>没有？那我造一个！</strong></p>
<p>于是他们把两个 “U / V” 拼在一起，写成：UU VV，写着写着，某天它就长成了今天这个样子：W</p>
<p>这就像两只“u”手拉着手，结成了字母界的双胞胎组合。
于是大家自然就叫它：</p>
<blockquote>
<p><strong>double u（双 u）</strong></p>
</blockquote>
<p>你看，有历史依据，一点也不玄学。</p>
<hr>
<h2>3. 那为什么不是 <em>double v</em> 呢？</h2>
<p>很好问题。</p>
<p>其实在 <strong>法语、西班牙语、德语系</strong>，它确实是按照「双 V」来理解的，因为他们写出来的形态更像两个 V。</p>
<p>英语就顽固地坚持自己的传统：
<strong>“我们祖祖辈辈都读 double-u，你别管我们长啥样。”</strong></p>
<p>语言嘛，本来就不是严格逻辑推导，更多是文化 + 习惯。</p>
<hr>
<h2>4. 常见 “字母中文音译” 翻车现场</h2>
<p>既然说到“达不溜”，干脆顺便列几个常见的“被中文骗了”的字母：</p>
<h3>⚠ <strong>Q ≠ 丘</strong></h3>
<p>标准读法：<strong>/kjuː/</strong>（k + you）
大部分中国学生学成了“丘”，少了前面的 k。</p>
<p>如果你念 “丘R code”，外国人可能会以为你在说某种植物编码。</p>
<hr>
<h3>⚠ <strong>H ≠ 艾曲/和气</strong></h3>
<p>正确发音：<strong>/eɪtʃ/</strong>
注意：开头不是 h。</p>
<p>如果你念成 “嘿曲”，这就像把“安静”念成“喂烦我了”。</p>
<hr>
<h3>⚠ <strong>Z ≠ 贼德 / 兹伊</strong></h3>
<p>英式：<strong>zed</strong>
美式：<strong>zee</strong></p>
<p>所以 “A to Z” 在美音里其实是 “A to Zee”，不是“A to 贼德”。</p>
<hr>
<h3>⚠ <strong>R ≠ 阿儿</strong></h3>
<p>正确发音：</p>
<ul>
<li>英音：/ɑː/</li>
<li>美音：/ɑr/</li>
</ul>
<p>中文音译里多出来的“儿”完全是赠品，不包含在官方套餐里。</p>
<hr>
<h2>5. 字母读法为什么会被带偏？</h2>
<p>很简单：
因为我们小时候背的是音译，而不是音标。</p>
<p>中国学生一般这么背字母：</p>
<ul>
<li>A（诶）</li>
<li>B（逼）</li>
<li>C（西）</li>
<li>…</li>
<li>W（达不溜）</li>
</ul>
<p>但实际上，英文字母读音本质是 <strong>发音符号</strong>，而不是中文拼音那样的“定死读音的字”。</p>
<p>中文音译只是“辅助记忆的小轮子”，
但你现在长大了，该把小轮子拆掉了。</p>
<hr>
<h2>6. 那怎么学才正确？（实用三步）</h2>
<h3>✔ <strong>1. 先用音标记忆</strong></h3>
<p>不要用“拼音法”记字母。</p>
<h3>✔ <strong>2. 多听母语者读字母歌</strong></h3>
<p>特别是美式小学生那种，不做作、不变调的版本。</p>
<h3>✔ <strong>3. 把字母发音和单词对应起来</strong></h3>
<p>例如：</p>
<ul>
<li>W → water / why / win</li>
<li>Q → quick / queen</li>
<li>H → happy / hat</li>
</ul>
<p>读多了，你就自然知道哪些音是“编出来的”。</p>
<hr>
<h2>7. 最后的总结：</h2>
<p>如果你今天只能记住一句话——
请记住：</p>
<blockquote>
<p><strong>W 是 <em>double u</em>，绝不是“达不溜”。</strong>
“达不溜”只是中文助记符，就像把外卖店叫“麦当劳叔叔”一样 —— 亲切，但绝不是官方名字。</p>
</blockquote>
<p>学懂了这个，你会发现：
英语并不是玄学，只是历史+习惯+一点点可爱的混乱。</p>
]]></description>
      <content:encoded><![CDATA[<h1>为什么英文字母 <strong>W</strong> 读作 <em>double u</em>？</h1>
<p>你可能从小就听过老师念字母：
<strong>“A、B、C……达不溜、X、Y、Z”</strong></p>
<p>等等。
谁？
<strong>达不溜？</strong></p>
<p>如果你第一次听英语字母表，就是靠这种“拼音式音译”记住的，那么今天我们就来一起拆解这个“神秘音节”背后的真相。</p>
<hr>
<h2>1. “达不溜”到底是什么来头？</h2>
<p>让我们先看标准英文读法：</p>
<blockquote>
<p><strong>W = /ˈdʌbəl ju/</strong> → double you
不是 dabuliu，也不是 da-bu-liu，更不是大不了。</p>
</blockquote>
<p>“达不溜”只是一种相当随缘的中文音译，目的就是让学生能记住这个读音。
它的功能类似于——</p>
<ul>
<li>把 pizza 叫成“批萨”</li>
<li>把 latte 叫成“拿铁”</li>
<li>把 sushi 叫成“寿司”</li>
</ul>
<p><strong>——你能认，但请不要真的跟外国人这么说。</strong></p>
<hr>
<h2>2. 为什么要叫 <em>double u</em>？</h2>
<p>这就涉及到一点英语史（别走！很好懂，也挺好笑）。</p>
<p>在古英语时代，字母表里<strong>根本没有 W</strong>。
英语中却有很多需要“呜”这个音的词，怎么办？</p>
<p>聪明的修士们决定：
<strong>没有？那我造一个！</strong></p>
<p>于是他们把两个 “U / V” 拼在一起，写成：UU VV，写着写着，某天它就长成了今天这个样子：W</p>
<p>这就像两只“u”手拉着手，结成了字母界的双胞胎组合。
于是大家自然就叫它：</p>
<blockquote>
<p><strong>double u（双 u）</strong></p>
</blockquote>
<p>你看，有历史依据，一点也不玄学。</p>
<hr>
<h2>3. 那为什么不是 <em>double v</em> 呢？</h2>
<p>很好问题。</p>
<p>其实在 <strong>法语、西班牙语、德语系</strong>，它确实是按照「双 V」来理解的，因为他们写出来的形态更像两个 V。</p>
<p>英语就顽固地坚持自己的传统：
<strong>“我们祖祖辈辈都读 double-u，你别管我们长啥样。”</strong></p>
<p>语言嘛，本来就不是严格逻辑推导，更多是文化 + 习惯。</p>
<hr>
<h2>4. 常见 “字母中文音译” 翻车现场</h2>
<p>既然说到“达不溜”，干脆顺便列几个常见的“被中文骗了”的字母：</p>
<h3>⚠ <strong>Q ≠ 丘</strong></h3>
<p>标准读法：<strong>/kjuː/</strong>（k + you）
大部分中国学生学成了“丘”，少了前面的 k。</p>
<p>如果你念 “丘R code”，外国人可能会以为你在说某种植物编码。</p>
<hr>
<h3>⚠ <strong>H ≠ 艾曲/和气</strong></h3>
<p>正确发音：<strong>/eɪtʃ/</strong>
注意：开头不是 h。</p>
<p>如果你念成 “嘿曲”，这就像把“安静”念成“喂烦我了”。</p>
<hr>
<h3>⚠ <strong>Z ≠ 贼德 / 兹伊</strong></h3>
<p>英式：<strong>zed</strong>
美式：<strong>zee</strong></p>
<p>所以 “A to Z” 在美音里其实是 “A to Zee”，不是“A to 贼德”。</p>
<hr>
<h3>⚠ <strong>R ≠ 阿儿</strong></h3>
<p>正确发音：</p>
<ul>
<li>英音：/ɑː/</li>
<li>美音：/ɑr/</li>
</ul>
<p>中文音译里多出来的“儿”完全是赠品，不包含在官方套餐里。</p>
<hr>
<h2>5. 字母读法为什么会被带偏？</h2>
<p>很简单：
因为我们小时候背的是音译，而不是音标。</p>
<p>中国学生一般这么背字母：</p>
<ul>
<li>A（诶）</li>
<li>B（逼）</li>
<li>C（西）</li>
<li>…</li>
<li>W（达不溜）</li>
</ul>
<p>但实际上，英文字母读音本质是 <strong>发音符号</strong>，而不是中文拼音那样的“定死读音的字”。</p>
<p>中文音译只是“辅助记忆的小轮子”，
但你现在长大了，该把小轮子拆掉了。</p>
<hr>
<h2>6. 那怎么学才正确？（实用三步）</h2>
<h3>✔ <strong>1. 先用音标记忆</strong></h3>
<p>不要用“拼音法”记字母。</p>
<h3>✔ <strong>2. 多听母语者读字母歌</strong></h3>
<p>特别是美式小学生那种，不做作、不变调的版本。</p>
<h3>✔ <strong>3. 把字母发音和单词对应起来</strong></h3>
<p>例如：</p>
<ul>
<li>W → water / why / win</li>
<li>Q → quick / queen</li>
<li>H → happy / hat</li>
</ul>
<p>读多了，你就自然知道哪些音是“编出来的”。</p>
<hr>
<h2>7. 最后的总结：</h2>
<p>如果你今天只能记住一句话——
请记住：</p>
<blockquote>
<p><strong>W 是 <em>double u</em>，绝不是“达不溜”。</strong>
“达不溜”只是中文助记符，就像把外卖店叫“麦当劳叔叔”一样 —— 亲切，但绝不是官方名字。</p>
</blockquote>
<p>学懂了这个，你会发现：
英语并不是玄学，只是历史+习惯+一点点可爱的混乱。</p>
]]></content:encoded>
    </item>
    <item>
      <title>星期天到底是怎么来的？</title>
      <link>https://vansiit.cc/2025/09/08/week.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/09/08/week.html</guid>
      <pubDate>Tue, 09 Sep 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>星期天到底是怎么来的？</h1>
<h2>一、西方的来源</h2>
<p>聪明的你第一反应肯定是，来自英语：Sunday。没错，sun day，星期日，周日，礼拜日。</p>
<p>然后你又发现，星期一，Monday，moon day？月亮日？恭喜你！发现了规律，每一周的每一天代表一颗星球。</p>
<p>古罗马人用他们信仰的七位神祇（对应太阳、月亮和五大当时已知的行星）来命名星期。当日耳曼部落（盎格鲁-撒克逊人）接触到这个体系时，他们用自己的神祇（北欧神话体系）替换了罗马神祇，形成了古英语的名称，并最终演变成现代英语的单词。</p>
<table>
<thead>
<tr>
<th>英文</th>
<th>古英语源</th>
<th>含义</th>
<th>对应的罗马神/天体</th>
<th>替换的日耳曼/北欧神</th>
</tr>
</thead>
<tbody>
<tr>
<td>Monday</td>
<td>Mōnandæg</td>
<td>月亮日</td>
<td>月亮 (Luna)</td>
<td>月亮 (Mona)</td>
</tr>
<tr>
<td>Tuesday</td>
<td>Tīwesdæg</td>
<td>提尔之日</td>
<td>战神玛尔斯 (Mars)</td>
<td>战神提尔 (Tiw/Tyr)</td>
</tr>
<tr>
<td>Wednesday</td>
<td>Wōdnesdæg</td>
<td>沃登之日</td>
<td>神使墨丘利 (Mercury)</td>
<td>主神奥丁 (Odin/Woden)</td>
</tr>
<tr>
<td>Thursday</td>
<td>Þūnresdæg</td>
<td>托尔之日</td>
<td>主神朱庇特 (Jupiter)</td>
<td>雷神托尔 (Thor/Thunor)</td>
</tr>
<tr>
<td>Friday</td>
<td>Frīgedæg</td>
<td>弗丽嘉之日</td>
<td>爱神维纳斯 (Venus)</td>
<td>爱神弗丽嘉/芙蕾雅 (Frigg/Freyja)</td>
</tr>
<tr>
<td>Saturday</td>
<td>Sæternesdæg</td>
<td>萨图恩之日</td>
<td>农神萨图恩 (Saturn)</td>
<td>（无替换，直接借用）</td>
</tr>
<tr>
<td>Sunday</td>
<td>Sunnandæg</td>
<td>太阳日</td>
<td>太阳 (Sol)</td>
<td>太阳 (Sunne)</td>
</tr>
</tbody>
</table>
<h2>二、Sunday-first 与 Monday-first</h2>
<p>聪明的你又发现了盲点，为什么有的国家用周一为一周的第一天，有的国家用周日？</p>
<p>分两种情况。Sunday-first（常见于美国、加拿大、日本等）。Monday-first（常见于英国、欧洲大部分地区，且是ISO标准）</p>
<p>Sunday-first 这种观念直接来源于《圣经·创世记》中对上帝创世的描述：</p>
<p>“上帝说：‘要有光’，就有了光… 上帝看光是好的，就把光暗分开了。上帝称光为昼，称暗为夜。有晚上，有早晨，这是头一日。”</p>
<p>“到第七日，上帝造物的工已经完毕，就在第七日歇了他一切的工，安息了。”</p>
<p>根据这个叙述：创造的第一天是从光明（星期日）开始的。 上帝在第一天创造了光，而星期日（Sunday）传统上被视为与“光明”和“太阳”相关联的日子。</p>
<p>第七天是安息日（星期六，Saturday）。 上帝在第七天休息，因此犹太教和后来基督教的一些派别（如 Seventh-day Adventists）将星期六尊为安息日（Sabbath），用于休息和礼拜。</p>
<p>对于基督徒来说：耶稣基督在星期日复活（ Easter Sunday），这一天被称为“主日”（The Lord’s Day）。为了纪念复活这一核心事件，星期日取代了星期六成为最重要的礼拜日。</p>
<p>因此，从宗教顺序上，一周的开始（第一天）是“主日”（星期日），而一周的结束（第七天）是安息日（星期六）。</p>
<p>这种宗教历法深刻地影响了西方文化，将“星期日作为一周之首”的观念植根于传统之中。</p>
<p>Monday-first 这种观念来源于 ISO 标准。 ISO 标准将星期一（Monday）作为一周的开始，星期日（Sunday）作为一周的结束。</p>
<p>从商业和统计的角度看，将工作日（Monday to Friday） 放在一起更合理。周末（Saturday and Sunday）是休息日，自然是一周的结尾。</p>
<p>这样避免了因文化差异导致的混乱，为全球数据交换、软件开发和商业规划提供了一个统一的框架。</p>
<h2>三、“星期”概念在中国的引入</h2>
<p>聪明的你又又发现了盲点。我国可没有宗教的影响，星期的讲法是什么时候传入我国的呢？</p>
<p>古代中国没有“星期”，自有循环系统。在西方“星期”概念传入之前，中国有自己的时间循环系统，主要用于纪日。</p>
<p>第一种是干支纪日：这是中国最古老、使用时间最长的纪日法。使用“十天干”和“十二地支”相配，形成六十个组合，称为“六十甲子”，循环使用。</p>
<p>这个系统主要用于历法、天文和占卜，与星期制度有本质区别。它只有一个连续的循环，没有将七天作为一个特定的生活和工作周期。</p>
<p>第二种是旬制：一个月被分为上、中、下三旬，每旬十天。这是一种基于十进制（10天）的周期，而不是七进制（7天）的周期。</p>
<p>“星期”的概念最早是随着外来宗教传入中国的。</p>
<p>唐代：随着基督教的一个派别——景教（Nestorianism），以及后来的伊斯兰教传入中国，“七日一礼拜”的宗教习俗也被带入。但这只在极少数宗教信徒和小范围社区内流传，对主流社会和官方历法没有任何影响。</p>
<p>明代：西方传教士（如利玛窦）来到中国，他们在传播天主教的同时，也带来了西方的天文历算知识，“七日一周”的概念再次被提及，但依然仅限于很小的学术和宗教圈子。</p>
<p>在这个漫长的时期里，中文里并没有一个固定的词来翻译“week”。虽然“星期”一词在古代汉语中本指“天上的星体期”，特指牛郎织女相会之期（七夕），但与西方的周制无关。</p>
<p>转折点发生在鸦片战争（1840年） 之后。中国国门被打开，西方商人、传教士、外交官大量涌入通商口岸。</p>
<p>“礼拜”的诞生：在开放口岸（如上海、广州），人们观察到外国人每七天有一天去教堂做“礼拜”，并且在这一天休息。于是，中国人很自然地将这个休息日称为 “礼拜日” ，进而将七天的一个周期称为 “一个礼拜” 。这个词源于宗教活动，非常形象，迅速在民间流传开来，至今仍在许多地方（尤其是南方）口语中使用。</p>
<p>新式学堂和工厂：外国人创办的学校、工厂和洋务派创办的企业，开始模仿西方的作息制度，实行星期日休息的制度。这让“七天一周”的概念首次与中国人的日常生活产生了实际联系。</p>
<p>1912年：中华民国政府宣布采用国际通用的公历（阳历），同时也就需要引入“星期”制度。</p>
<p>“星期”的定名：当时的教育部（据说是由一位叫袁嘉谷的学者）最终选定了 “星期” 这个译名。</p>
<p>“星”指代西方“星期”概念源于日月星辰（Sunday=日曜日，Monday=月曜日等）。“期”指周期。</p>
<p>这个名称既科学又中性，完美地避免了“礼拜”的宗教色彩。</p>
<p>新中国成立后，延续了“星期”制度。</p>
<p>在正式书面语和官方场合，普遍使用 “星期X”（如：星期一）。</p>
<p>在广大南方地区和日常口语中，“礼拜X” 仍然非常流行。</p>
<p>还有一个更口语化、源于日语的说法——“周X”（如：周一），这种说法在现代，尤其是在年轻人中和网络用语里越来越普遍。</p>
]]></description>
      <content:encoded><![CDATA[<h1>星期天到底是怎么来的？</h1>
<h2>一、西方的来源</h2>
<p>聪明的你第一反应肯定是，来自英语：Sunday。没错，sun day，星期日，周日，礼拜日。</p>
<p>然后你又发现，星期一，Monday，moon day？月亮日？恭喜你！发现了规律，每一周的每一天代表一颗星球。</p>
<p>古罗马人用他们信仰的七位神祇（对应太阳、月亮和五大当时已知的行星）来命名星期。当日耳曼部落（盎格鲁-撒克逊人）接触到这个体系时，他们用自己的神祇（北欧神话体系）替换了罗马神祇，形成了古英语的名称，并最终演变成现代英语的单词。</p>
<table>
<thead>
<tr>
<th>英文</th>
<th>古英语源</th>
<th>含义</th>
<th>对应的罗马神/天体</th>
<th>替换的日耳曼/北欧神</th>
</tr>
</thead>
<tbody>
<tr>
<td>Monday</td>
<td>Mōnandæg</td>
<td>月亮日</td>
<td>月亮 (Luna)</td>
<td>月亮 (Mona)</td>
</tr>
<tr>
<td>Tuesday</td>
<td>Tīwesdæg</td>
<td>提尔之日</td>
<td>战神玛尔斯 (Mars)</td>
<td>战神提尔 (Tiw/Tyr)</td>
</tr>
<tr>
<td>Wednesday</td>
<td>Wōdnesdæg</td>
<td>沃登之日</td>
<td>神使墨丘利 (Mercury)</td>
<td>主神奥丁 (Odin/Woden)</td>
</tr>
<tr>
<td>Thursday</td>
<td>Þūnresdæg</td>
<td>托尔之日</td>
<td>主神朱庇特 (Jupiter)</td>
<td>雷神托尔 (Thor/Thunor)</td>
</tr>
<tr>
<td>Friday</td>
<td>Frīgedæg</td>
<td>弗丽嘉之日</td>
<td>爱神维纳斯 (Venus)</td>
<td>爱神弗丽嘉/芙蕾雅 (Frigg/Freyja)</td>
</tr>
<tr>
<td>Saturday</td>
<td>Sæternesdæg</td>
<td>萨图恩之日</td>
<td>农神萨图恩 (Saturn)</td>
<td>（无替换，直接借用）</td>
</tr>
<tr>
<td>Sunday</td>
<td>Sunnandæg</td>
<td>太阳日</td>
<td>太阳 (Sol)</td>
<td>太阳 (Sunne)</td>
</tr>
</tbody>
</table>
<h2>二、Sunday-first 与 Monday-first</h2>
<p>聪明的你又发现了盲点，为什么有的国家用周一为一周的第一天，有的国家用周日？</p>
<p>分两种情况。Sunday-first（常见于美国、加拿大、日本等）。Monday-first（常见于英国、欧洲大部分地区，且是ISO标准）</p>
<p>Sunday-first 这种观念直接来源于《圣经·创世记》中对上帝创世的描述：</p>
<p>“上帝说：‘要有光’，就有了光… 上帝看光是好的，就把光暗分开了。上帝称光为昼，称暗为夜。有晚上，有早晨，这是头一日。”</p>
<p>“到第七日，上帝造物的工已经完毕，就在第七日歇了他一切的工，安息了。”</p>
<p>根据这个叙述：创造的第一天是从光明（星期日）开始的。 上帝在第一天创造了光，而星期日（Sunday）传统上被视为与“光明”和“太阳”相关联的日子。</p>
<p>第七天是安息日（星期六，Saturday）。 上帝在第七天休息，因此犹太教和后来基督教的一些派别（如 Seventh-day Adventists）将星期六尊为安息日（Sabbath），用于休息和礼拜。</p>
<p>对于基督徒来说：耶稣基督在星期日复活（ Easter Sunday），这一天被称为“主日”（The Lord’s Day）。为了纪念复活这一核心事件，星期日取代了星期六成为最重要的礼拜日。</p>
<p>因此，从宗教顺序上，一周的开始（第一天）是“主日”（星期日），而一周的结束（第七天）是安息日（星期六）。</p>
<p>这种宗教历法深刻地影响了西方文化，将“星期日作为一周之首”的观念植根于传统之中。</p>
<p>Monday-first 这种观念来源于 ISO 标准。 ISO 标准将星期一（Monday）作为一周的开始，星期日（Sunday）作为一周的结束。</p>
<p>从商业和统计的角度看，将工作日（Monday to Friday） 放在一起更合理。周末（Saturday and Sunday）是休息日，自然是一周的结尾。</p>
<p>这样避免了因文化差异导致的混乱，为全球数据交换、软件开发和商业规划提供了一个统一的框架。</p>
<h2>三、“星期”概念在中国的引入</h2>
<p>聪明的你又又发现了盲点。我国可没有宗教的影响，星期的讲法是什么时候传入我国的呢？</p>
<p>古代中国没有“星期”，自有循环系统。在西方“星期”概念传入之前，中国有自己的时间循环系统，主要用于纪日。</p>
<p>第一种是干支纪日：这是中国最古老、使用时间最长的纪日法。使用“十天干”和“十二地支”相配，形成六十个组合，称为“六十甲子”，循环使用。</p>
<p>这个系统主要用于历法、天文和占卜，与星期制度有本质区别。它只有一个连续的循环，没有将七天作为一个特定的生活和工作周期。</p>
<p>第二种是旬制：一个月被分为上、中、下三旬，每旬十天。这是一种基于十进制（10天）的周期，而不是七进制（7天）的周期。</p>
<p>“星期”的概念最早是随着外来宗教传入中国的。</p>
<p>唐代：随着基督教的一个派别——景教（Nestorianism），以及后来的伊斯兰教传入中国，“七日一礼拜”的宗教习俗也被带入。但这只在极少数宗教信徒和小范围社区内流传，对主流社会和官方历法没有任何影响。</p>
<p>明代：西方传教士（如利玛窦）来到中国，他们在传播天主教的同时，也带来了西方的天文历算知识，“七日一周”的概念再次被提及，但依然仅限于很小的学术和宗教圈子。</p>
<p>在这个漫长的时期里，中文里并没有一个固定的词来翻译“week”。虽然“星期”一词在古代汉语中本指“天上的星体期”，特指牛郎织女相会之期（七夕），但与西方的周制无关。</p>
<p>转折点发生在鸦片战争（1840年） 之后。中国国门被打开，西方商人、传教士、外交官大量涌入通商口岸。</p>
<p>“礼拜”的诞生：在开放口岸（如上海、广州），人们观察到外国人每七天有一天去教堂做“礼拜”，并且在这一天休息。于是，中国人很自然地将这个休息日称为 “礼拜日” ，进而将七天的一个周期称为 “一个礼拜” 。这个词源于宗教活动，非常形象，迅速在民间流传开来，至今仍在许多地方（尤其是南方）口语中使用。</p>
<p>新式学堂和工厂：外国人创办的学校、工厂和洋务派创办的企业，开始模仿西方的作息制度，实行星期日休息的制度。这让“七天一周”的概念首次与中国人的日常生活产生了实际联系。</p>
<p>1912年：中华民国政府宣布采用国际通用的公历（阳历），同时也就需要引入“星期”制度。</p>
<p>“星期”的定名：当时的教育部（据说是由一位叫袁嘉谷的学者）最终选定了 “星期” 这个译名。</p>
<p>“星”指代西方“星期”概念源于日月星辰（Sunday=日曜日，Monday=月曜日等）。“期”指周期。</p>
<p>这个名称既科学又中性，完美地避免了“礼拜”的宗教色彩。</p>
<p>新中国成立后，延续了“星期”制度。</p>
<p>在正式书面语和官方场合，普遍使用 “星期X”（如：星期一）。</p>
<p>在广大南方地区和日常口语中，“礼拜X” 仍然非常流行。</p>
<p>还有一个更口语化、源于日语的说法——“周X”（如：周一），这种说法在现代，尤其是在年轻人中和网络用语里越来越普遍。</p>
]]></content:encoded>
    </item>
    <item>
      <title>那个长头发大胡子的老师出家了</title>
      <link>https://vansiit.cc/2025/08/19/teacher.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/08/19/teacher.html</guid>
      <pubDate>Tue, 19 Aug 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>那个长头发大胡子的老师出家了</h1>
<p>前年春节回老家，偶遇一位熟人谈起，说我们初中那位美术老师出家了——就在我嘎爷（外公）他们村某个山间小庙里。</p>
<p>我嘎爷那村山高林也密。记得小时候去过几趟，赶上下雪天，抄小道翻山越岭，就算有本地人带路，也得赶上五六个钟头。</p>
<p>如今路修通了，盘山公路蜿蜒而上，山上住的人却越来越少。前两年跟我大舅上去过一次，原本几十户人家喧喧闹闹，如今只剩三两户稀稀疏疏，还都是在高山上给烟草公司种烟叶的。</p>
<p>夯土房子一片接一片地塌了，回归山林，被绿色覆盖，无声无息地归于自然。</p>
<p>那位美术老师，和其他老师都不太一样。一脸大胡子，长头发，脸上总挂着笑，走在校园里格外显眼。</p>
<p>铅笔线条、水粉画、艺术字、仿宋体——这些我对美术最初的认识，都是他带来的。尽管那时初中美术课寥寥无几。</p>
<p>他妻子在镇子石桥边开了家照相馆，铺面紧挨着小河。河边有棵大柳树，枝叶垂得很低，郁郁葱葱。我初中毕业时的证件照，就是在那儿拍的。</p>
<p>最近少室山出了事，少林方丈释永信被查，涉嫌刑事犯罪；再早些，绍兴上虞的道禄和尚也被曝光，据说牵涉诈骗。</p>
<p>一时间群情哗然，网友们议论纷纷，好不热闹。</p>
<p>佛教说的“缘起性空，诸法性空”，一切的财富、名誉、地位都是因緣起，无自性。释永信与道禄和尚，想必是并未修得佛门的真谛。</p>
<p>忽然想起初中还有一位历史老师，中等身材，戴一副大黑框眼镜，平时不苟言笑，走路匆忙带风，一手板书极其工整。</p>
<p>后来听说，他妻子重病瘫痪多年。他一边把孩子拉扯大，一边几十年如一日地照顾卧床的妻子。</p>
<p>我想，这何尝不是另一种修行。</p>
]]></description>
      <content:encoded><![CDATA[<h1>那个长头发大胡子的老师出家了</h1>
<p>前年春节回老家，偶遇一位熟人谈起，说我们初中那位美术老师出家了——就在我嘎爷（外公）他们村某个山间小庙里。</p>
<p>我嘎爷那村山高林也密。记得小时候去过几趟，赶上下雪天，抄小道翻山越岭，就算有本地人带路，也得赶上五六个钟头。</p>
<p>如今路修通了，盘山公路蜿蜒而上，山上住的人却越来越少。前两年跟我大舅上去过一次，原本几十户人家喧喧闹闹，如今只剩三两户稀稀疏疏，还都是在高山上给烟草公司种烟叶的。</p>
<p>夯土房子一片接一片地塌了，回归山林，被绿色覆盖，无声无息地归于自然。</p>
<p>那位美术老师，和其他老师都不太一样。一脸大胡子，长头发，脸上总挂着笑，走在校园里格外显眼。</p>
<p>铅笔线条、水粉画、艺术字、仿宋体——这些我对美术最初的认识，都是他带来的。尽管那时初中美术课寥寥无几。</p>
<p>他妻子在镇子石桥边开了家照相馆，铺面紧挨着小河。河边有棵大柳树，枝叶垂得很低，郁郁葱葱。我初中毕业时的证件照，就是在那儿拍的。</p>
<p>最近少室山出了事，少林方丈释永信被查，涉嫌刑事犯罪；再早些，绍兴上虞的道禄和尚也被曝光，据说牵涉诈骗。</p>
<p>一时间群情哗然，网友们议论纷纷，好不热闹。</p>
<p>佛教说的“缘起性空，诸法性空”，一切的财富、名誉、地位都是因緣起，无自性。释永信与道禄和尚，想必是并未修得佛门的真谛。</p>
<p>忽然想起初中还有一位历史老师，中等身材，戴一副大黑框眼镜，平时不苟言笑，走路匆忙带风，一手板书极其工整。</p>
<p>后来听说，他妻子重病瘫痪多年。他一边把孩子拉扯大，一边几十年如一日地照顾卧床的妻子。</p>
<p>我想，这何尝不是另一种修行。</p>
]]></content:encoded>
    </item>
    <item>
      <title>changpeiban</title>
      <link>https://vansiit.cc/2025/07/01/changpeiban.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/07/01/changpeiban.html</guid>
      <pubDate>Tue, 01 Jul 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1><strong>常陪伴 APP 产品设计规划</strong></h1>
<h2>一、产品概述</h2>
<p><strong>名称备选：</strong></p>
<ul>
<li>常陪伴</li>
<li>常伴</li>
<li>长情陪伴</li>
<li>长陪伴</li>
</ul>
<p><strong>产品定位：</strong>
一款以“声音情感克隆 + AI 故事生成 + 儿童成长陪伴”为核心的亲子互动平台，专为 0–8 岁儿童及其家庭打造。融合声纹克隆、多模态生成、3D建模等技术，实现“有温度”的 AI 陪伴。</p>
<hr>
<h2>二、核心功能设计</h2>
<h3>功能一：声音克隆与情感故事</h3>
<p><strong>1. AI 声纹克隆与情绪建模</strong></p>
<ul>
<li>用户录制少量语音，自动本地训练专属声纹模型。</li>
<li>AI 自动分析文本情绪（如惊讶、欢快、悲伤），实时调节语音语调，合成具情感的语音内容。</li>
<li>情绪模版可选：“温柔讲述”、“激昂冒险”、“轻快欢笑”等。</li>
</ul>
<p><strong>2. 故事集市（Story Bazaar）</strong></p>
<ul>
<li>家长可上传自创故事模板（如“宝宝第一次上幼儿园”）。</li>
<li>他人使用时可替换声纹，形成个性化语音版本。</li>
<li>开放内容经济体系，支持“积分制”或“故事币”激励原创者。</li>
</ul>
<p><strong>3. 智能故事生成</strong></p>
<ul>
<li>输入关键词或情节提示，AI 自动生成中英双语故事内容。</li>
<li>选择声纹音色，即可播放。</li>
<li>支持语速调节、情绪调节、人物配音风格选择。</li>
</ul>
<p><strong>4. 分角色故事录制（亲子互动）</strong></p>
<ul>
<li>家长与孩子可分别录制不同角色（如“妈妈是恐龙，孩子是小鸟”）。</li>
<li>AI 合成完整对话式故事，加配音效与背景音乐。</li>
</ul>
<hr>
<h3>功能二：拍照生成故事（图文转剧情）</h3>
<p><strong>1. 拍照生成个性故事</strong></p>
<ul>
<li>拍摄玩具、绘本、物品（如“毛绒小熊”），AI 识别后生成对应双语故事。</li>
<li>支持声纹选择，生成语音故事。</li>
<li>适用于日常玩具讲故事、外出纪实等场景。</li>
</ul>
<p><strong>2. 玩具 3D 建模 + 动作剧本生成</strong></p>
<ul>
<li>拍摄玩具生成简易 3D 模型（使用轻量神经建模工具）。</li>
<li>用户可拖动模型设计动作（如“跳跃、飞翔、转身”）。</li>
<li>AI 将动作转为剧情动画并配音，实现“玩具会演戏”。</li>
</ul>
<hr>
<h3>功能三：早教辅助</h3>
<p><strong>1. 拍照识字卡生成故事</strong></p>
<ul>
<li>使用 OCR 技术识别图卡或绘本内容。</li>
<li>自动生成中英双语识字故事，并提供发音练习。</li>
<li>支持自动归类主题（如“水果篇”“动物篇”）。</li>
</ul>
<p><strong>2. 智能成长时间轴</strong></p>
<ul>
<li>自动归档生成的所有故事内容。</li>
<li>支持按时间、人物、关键词分类。</li>
<li>一键导出为语音电子书或图文电子绘本册。</li>
</ul>
<hr>
<h2>三、辅助功能设计</h2>
<h3>1. 定时与守护模式</h3>
<ul>
<li>支持设置定时播放/停止。</li>
<li>超过20分钟自动进入“护眼模式”：关闭屏幕，仅保留音频。</li>
<li>支持“定时唤醒”与“定时睡眠”故事设定。</li>
</ul>
<h3>2. 个性唤醒故事</h3>
<ul>
<li>根据天气、日期、节日自动生成“晨间唤醒故事”（如“下雨天的小伞冒险”）。</li>
<li>可选择父母声纹合成故事，让 AI 用熟悉声音温柔唤醒。</li>
</ul>
<h3>3. 智能环境适配</h3>
<ul>
<li>睡前播放时根据光线与噪音感应调整屏幕亮度、语速。</li>
<li>检测孩子入睡后自动切换为白噪音播放。</li>
</ul>
<hr>
<h2>四、技术架构</h2>
<h3>1. 声纹克隆与合成</h3>
<ul>
<li>使用 <strong>端侧声纹克隆模型</strong>（如 Whisper + VITS 组合方案）。</li>
<li>所有训练数据本地化处理，保障用户隐私与数据安全。</li>
<li>多情绪模型构建：将情感标签融合入 TTS 合成流程。</li>
</ul>
<h3>2. 多模态故事生成</h3>
<ul>
<li>故事生成依赖大型语言模型（如 GPT 系列）结合扩散图像模型（Diffusion）。</li>
<li>图、文、声三模态同步输出，支持故事+配图+音频自动成册。</li>
</ul>
<h3>3. 3D建模与动画转场</h3>
<ul>
<li>基于轻量神经网络 + 少量照片实现 3D 玩具模型重建。</li>
<li>内置动作模板库 + 拖拽式分镜设计，适配低龄用户家长操作。</li>
</ul>
<hr>
<h2>五、伦理保障与家长控制</h2>
<h3>1. 内容过滤与审核</h3>
<ul>
<li>家长可设置内容过滤等级（如屏蔽暴力、恐怖、疾病等关键词）。</li>
<li>故事生成后需经家长审核后可播放。</li>
</ul>
<h3>2. 儿童使用时间管理</h3>
<ul>
<li>连续使用超时提示，并引导切换至“听模式”或休息时间。</li>
<li>设置“睡前/清晨”等固定时间自动播放温和内容。</li>
</ul>
<h3>3. 数据隐私保护</h3>
<ul>
<li>本地端声纹与人脸数据不上传云端。</li>
<li>所有生成内容均可导出备份，且具备一键删除权。</li>
</ul>
<hr>
<h2>六、商业模型设计</h2>
<table>
<thead>
<tr>
<th>模块</th>
<th>收费方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>故事集市</td>
<td>用户消费故事币/订阅，创作者可分成</td>
</tr>
<tr>
<td>声纹定制</td>
<td>基础免费，情感包扩展/高音质包为付费内容</td>
</tr>
<tr>
<td>AI 故事生成</td>
<td>日常额度免费，高级 prompt 或超配额度收费</td>
</tr>
<tr>
<td>3D 动画生成</td>
<td>每月免费生成额度，超出部分计费</td>
</tr>
<tr>
<td>智能唤醒/定制睡前故事</td>
<td>订阅权益内功能</td>
</tr>
</tbody>
</table>
<hr>
<h2>七、后续拓展方向（前瞻性）</h2>
<ol>
<li><strong>家庭版“AI 家庭记忆管家”</strong>：结合语音与图像记录日常互动，生成时间轴式“家庭故事年鉴”。</li>
<li><strong>可穿戴设备联动</strong>：与儿童手表、睡眠检测设备联动，自动适配故事节奏与内容。</li>
<li><strong>与玩具厂商 API 集成</strong>：构建开放生态，与实体玩具互动（如讲故事的布娃娃）。</li>
<li><strong>跨国家庭适配</strong>：支持中英日多语言切换，实现双语家庭互动成长。</li>
</ol>
<hr>
<p>如需设计具体的 <strong>原型图、用户旅程地图</strong> 或者进行 <strong>产品 Pitch Deck 制作</strong>，可继续提出，我可协助你进行全面的产品视觉与交互设计文档整理。</p>
]]></description>
      <content:encoded><![CDATA[<h1><strong>常陪伴 APP 产品设计规划</strong></h1>
<h2>一、产品概述</h2>
<p><strong>名称备选：</strong></p>
<ul>
<li>常陪伴</li>
<li>常伴</li>
<li>长情陪伴</li>
<li>长陪伴</li>
</ul>
<p><strong>产品定位：</strong>
一款以“声音情感克隆 + AI 故事生成 + 儿童成长陪伴”为核心的亲子互动平台，专为 0–8 岁儿童及其家庭打造。融合声纹克隆、多模态生成、3D建模等技术，实现“有温度”的 AI 陪伴。</p>
<hr>
<h2>二、核心功能设计</h2>
<h3>功能一：声音克隆与情感故事</h3>
<p><strong>1. AI 声纹克隆与情绪建模</strong></p>
<ul>
<li>用户录制少量语音，自动本地训练专属声纹模型。</li>
<li>AI 自动分析文本情绪（如惊讶、欢快、悲伤），实时调节语音语调，合成具情感的语音内容。</li>
<li>情绪模版可选：“温柔讲述”、“激昂冒险”、“轻快欢笑”等。</li>
</ul>
<p><strong>2. 故事集市（Story Bazaar）</strong></p>
<ul>
<li>家长可上传自创故事模板（如“宝宝第一次上幼儿园”）。</li>
<li>他人使用时可替换声纹，形成个性化语音版本。</li>
<li>开放内容经济体系，支持“积分制”或“故事币”激励原创者。</li>
</ul>
<p><strong>3. 智能故事生成</strong></p>
<ul>
<li>输入关键词或情节提示，AI 自动生成中英双语故事内容。</li>
<li>选择声纹音色，即可播放。</li>
<li>支持语速调节、情绪调节、人物配音风格选择。</li>
</ul>
<p><strong>4. 分角色故事录制（亲子互动）</strong></p>
<ul>
<li>家长与孩子可分别录制不同角色（如“妈妈是恐龙，孩子是小鸟”）。</li>
<li>AI 合成完整对话式故事，加配音效与背景音乐。</li>
</ul>
<hr>
<h3>功能二：拍照生成故事（图文转剧情）</h3>
<p><strong>1. 拍照生成个性故事</strong></p>
<ul>
<li>拍摄玩具、绘本、物品（如“毛绒小熊”），AI 识别后生成对应双语故事。</li>
<li>支持声纹选择，生成语音故事。</li>
<li>适用于日常玩具讲故事、外出纪实等场景。</li>
</ul>
<p><strong>2. 玩具 3D 建模 + 动作剧本生成</strong></p>
<ul>
<li>拍摄玩具生成简易 3D 模型（使用轻量神经建模工具）。</li>
<li>用户可拖动模型设计动作（如“跳跃、飞翔、转身”）。</li>
<li>AI 将动作转为剧情动画并配音，实现“玩具会演戏”。</li>
</ul>
<hr>
<h3>功能三：早教辅助</h3>
<p><strong>1. 拍照识字卡生成故事</strong></p>
<ul>
<li>使用 OCR 技术识别图卡或绘本内容。</li>
<li>自动生成中英双语识字故事，并提供发音练习。</li>
<li>支持自动归类主题（如“水果篇”“动物篇”）。</li>
</ul>
<p><strong>2. 智能成长时间轴</strong></p>
<ul>
<li>自动归档生成的所有故事内容。</li>
<li>支持按时间、人物、关键词分类。</li>
<li>一键导出为语音电子书或图文电子绘本册。</li>
</ul>
<hr>
<h2>三、辅助功能设计</h2>
<h3>1. 定时与守护模式</h3>
<ul>
<li>支持设置定时播放/停止。</li>
<li>超过20分钟自动进入“护眼模式”：关闭屏幕，仅保留音频。</li>
<li>支持“定时唤醒”与“定时睡眠”故事设定。</li>
</ul>
<h3>2. 个性唤醒故事</h3>
<ul>
<li>根据天气、日期、节日自动生成“晨间唤醒故事”（如“下雨天的小伞冒险”）。</li>
<li>可选择父母声纹合成故事，让 AI 用熟悉声音温柔唤醒。</li>
</ul>
<h3>3. 智能环境适配</h3>
<ul>
<li>睡前播放时根据光线与噪音感应调整屏幕亮度、语速。</li>
<li>检测孩子入睡后自动切换为白噪音播放。</li>
</ul>
<hr>
<h2>四、技术架构</h2>
<h3>1. 声纹克隆与合成</h3>
<ul>
<li>使用 <strong>端侧声纹克隆模型</strong>（如 Whisper + VITS 组合方案）。</li>
<li>所有训练数据本地化处理，保障用户隐私与数据安全。</li>
<li>多情绪模型构建：将情感标签融合入 TTS 合成流程。</li>
</ul>
<h3>2. 多模态故事生成</h3>
<ul>
<li>故事生成依赖大型语言模型（如 GPT 系列）结合扩散图像模型（Diffusion）。</li>
<li>图、文、声三模态同步输出，支持故事+配图+音频自动成册。</li>
</ul>
<h3>3. 3D建模与动画转场</h3>
<ul>
<li>基于轻量神经网络 + 少量照片实现 3D 玩具模型重建。</li>
<li>内置动作模板库 + 拖拽式分镜设计，适配低龄用户家长操作。</li>
</ul>
<hr>
<h2>五、伦理保障与家长控制</h2>
<h3>1. 内容过滤与审核</h3>
<ul>
<li>家长可设置内容过滤等级（如屏蔽暴力、恐怖、疾病等关键词）。</li>
<li>故事生成后需经家长审核后可播放。</li>
</ul>
<h3>2. 儿童使用时间管理</h3>
<ul>
<li>连续使用超时提示，并引导切换至“听模式”或休息时间。</li>
<li>设置“睡前/清晨”等固定时间自动播放温和内容。</li>
</ul>
<h3>3. 数据隐私保护</h3>
<ul>
<li>本地端声纹与人脸数据不上传云端。</li>
<li>所有生成内容均可导出备份，且具备一键删除权。</li>
</ul>
<hr>
<h2>六、商业模型设计</h2>
<table>
<thead>
<tr>
<th>模块</th>
<th>收费方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>故事集市</td>
<td>用户消费故事币/订阅，创作者可分成</td>
</tr>
<tr>
<td>声纹定制</td>
<td>基础免费，情感包扩展/高音质包为付费内容</td>
</tr>
<tr>
<td>AI 故事生成</td>
<td>日常额度免费，高级 prompt 或超配额度收费</td>
</tr>
<tr>
<td>3D 动画生成</td>
<td>每月免费生成额度，超出部分计费</td>
</tr>
<tr>
<td>智能唤醒/定制睡前故事</td>
<td>订阅权益内功能</td>
</tr>
</tbody>
</table>
<hr>
<h2>七、后续拓展方向（前瞻性）</h2>
<ol>
<li><strong>家庭版“AI 家庭记忆管家”</strong>：结合语音与图像记录日常互动，生成时间轴式“家庭故事年鉴”。</li>
<li><strong>可穿戴设备联动</strong>：与儿童手表、睡眠检测设备联动，自动适配故事节奏与内容。</li>
<li><strong>与玩具厂商 API 集成</strong>：构建开放生态，与实体玩具互动（如讲故事的布娃娃）。</li>
<li><strong>跨国家庭适配</strong>：支持中英日多语言切换，实现双语家庭互动成长。</li>
</ol>
<hr>
<p>如需设计具体的 <strong>原型图、用户旅程地图</strong> 或者进行 <strong>产品 Pitch Deck 制作</strong>，可继续提出，我可协助你进行全面的产品视觉与交互设计文档整理。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Golang学习笔记(一)：Go语言起源、发展历程与核心特性</title>
      <link>https://vansiit.cc/2025/05/28/golang1.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/05/28/golang1.html</guid>
      <pubDate>Wed, 28 May 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>Golang 学习笔记（一）</h1>
<h2>1. 起源与发展</h2>
<p>Go 语言起源 2007 年，并于 2009 年正式对外发布。</p>
<p>该项目的三位领导者均是著名的 IT 工程师：</p>
<p>Robert Griesemer，参与开发 Java HotSpot 虚拟机；</p>
<p>Rob Pike，Go 语言项目总负责人，贝尔实验室 Unix 团队成员，参与的项目包括 Plan 9，Inferno 操作系统和 Limbo 编程语言；</p>
<p>Ken Thompson，贝尔实验室 Unix 团队成员，C 语言、Unix 和 Plan 9 的创始人之一，与 Rob Pike 共同开发了 UTF-8 字符集规范。</p>
<p>2009 年 11 月 10 日，开发团队将 Go 语言项目以 BSD-style 授权（完全开源）正式公布了 Linux 和 Mac OS X 平台上的版本。Hector Chu 于同年 11 月 22 日公布了 Windows 版本。</p>
<p>在 Go 语言在 2010 年 1 月 8 日被 Tiobe（闻名于它的编程语言流行程度排名）宣布为 “2009 年年度语言” 后，引起各界很大的反响。目前 Go 语言在这项排名中的最高记录是在 2017 年 1 月创下的第13名，流行程度 2.325%。</p>
<p><strong>时间轴：</strong></p>
<ul>
<li>2007 年 9 月 21 日：雏形设计</li>
<li>2009 年 11 月 10日：首次公开发布</li>
<li>2010 年 1 月 8 日：当选 2009 年年度语言</li>
<li>2010 年 5 月：谷歌投入使用</li>
<li>2011 年 5 月 5 日：Google App Engine 支持 Go 语言</li>
</ul>
<p>Go 语言的官方网站是 <a href="https://golang.org">golang.org</a></p>
<p>更多的信息详见 <a href="https://github.com/golang/go">github.com/golang/go</a>，Go 项目 Bug 追踪和功能预期详见 <a href="https://github.com/golang/go/issues">github.com/golang/go/issues</a>。</p>
<p>Go 以囊地鼠 (Gopher) 作为它的吉祥物。
<img src="https://vansiit.cc/img/go/favicon-gopher.png" alt="img.png"></p>
<p>Go 语言还有一个运行在 Google App Engine 上的 <a href="https://tour.golang.org">Go Tour</a>，你也可以通过执行命令 go install <a href="http://go-tour.googlecode.com/hg/gotour">go-tour.googlecode.com/hg/gotour</a> 安装到你的本地机器上。对于中文读者，可以访问该指南的 <a href="https://tour.go-zh.org/welcome/1">中文版本</a>，或通过命令 go install <a href="https://bitbucket.org/mikespook/go-tour-zh/gotour">https://bitbucket.org/mikespook/go-tour-zh/gotour</a> 进行安装。</p>
<h2>2. 语言的主要特性与发展的环境和影响因素</h2>
<p>在声明和包的设计方面，Go 语言受到 Pascal、Modula 和 Oberon 系语言的影响；在并发原理的设计上，</p>
<p>Go 语言从同样受到 Tony Hoare 的 CSP（通信序列进程 Communicating Sequential Processes）理论影响的 Limbo 和 Newsqueak 的实践中借鉴了一些经验，并使用了和 Erlang 类似的机制。</p>
<p>这是一门完全开源的编程语言，因为它使用 BSD 授权许可，所以任何人都可以进行商业软件的开发而不需要支付任何费用。</p>
<p>尽管为了能够让目前主流的开发者们能够对 Go 语言中的类 C 语言的语法感到非常亲切而易于转型，但是它在极大程度上简化了这些语法，使得它们比 C/C++ 的语法更加简洁和干净。</p>
<p>同时，Go 语言也拥有一些动态语言的特性，这使得使用 Python 和 Ruby 的开发者们在使用 Go 语言的时候感觉非常容易上手。</p>
<p><strong>为什么要创造一门编程语言</strong></p>
<ul>
<li>C/C++ 的发展速度无法跟上计算机发展的脚步，十多年来也没有出现一门与时代相符的主流系统编程语言，因此人们需要一门新的系统编程语言来弥补这个空缺，尤其是在计算机信息时代。</li>
<li>相比计算机性能的提升，软件开发领域不被认为发展得足够快或者比硬件发展得更加成功（有许多项目均以失败告终），同时应用程序的体积始终在不断地扩大，这就迫切地需要一门具备更高层次概念的低级语言来突破现状。</li>
<li>在 Go 语言出现之前，开发者们总是面临非常艰难的抉择，究竟是使用执行速度快但是编译速度并不理想的语言（如：C++），还是使用编译速度较快但执行效率不佳的语言（如：.NET、Java），或者说开发难度较低但执行速度一般的动态语言呢？显然，Go 语言旨在这 3 个条件之间做到了最佳的平衡：快速编译，高效执行，易于开发。</li>
</ul>
<p>Go语言通过减少关键字的数量（25 个）来简化编码过程中的混乱和复杂度。干净、整齐和简洁的语法也能够提高程序的编译速度，因为这些关键字在编译过程中少到甚至不需要符号表来协助解析。</p>
<p>这些方面的工作都是为了减少编码的工作量，甚至可以与 Java 的简化程度相比较。</p>
<p>Go 语言有一种极简抽象艺术家的感觉，因为它只提供了一到两种方法来解决某个问题，这使得开发者们的代码都非常容易阅读和理解。众所周知，代码的可读性是软件工程里最重要的一部分（ 译者注：代码是写给人看的，不是写给机器看的 ）。</p>
<p>这些设计理念没有建立其它概念之上，所以并不会因为牵扯到一些概念而将某个概念复杂化，他们之间是相互独立的。</p>
<p>Go 语言有一套完整的编码规范，你可以在 Go 语言编码规范 页面进行查看。</p>
<p>它不像 Ruby 那样通过实现过程来定义编码规范。作为一门具有明确编码规范的语言，它要求可以采用不同的编译器如 gc 和 gccgo 进行编译工作，这对语言本身拥有更好的编码规范起到很大帮助。</p>
<p>Go 语言从本质上（程序和结构方面）来实现并发编程。</p>
<p>因为 Go 语言没有类和继承的概念，所以它和 Java 或 C++ 看起来并不相同。但是它通过接口 (interface) 的概念来实现多态性。Go 语言有一个清晰易懂的轻量级类型系统，在类型之间也没有层级之说。因此可以说这是一门混合型的语言。</p>
<p>在传统的面向对象语言中，使用面向对象编程技术显得非常臃肿，它们总是通过复杂的模式来构建庞大的类型层级，这违背了编程语言应该提升生产力的宗旨。</p>
<p>函数是 Go 语言中的基本构件，它们的使用方法非常灵活。在第六章，我们会看到 Go 语言在函数式编程方面的基本概念。</p>
<p>Go 语言使用静态类型，所以它是类型安全的一门语言，加上通过构建到本地代码，程序的执行速度也非常快。</p>
<p>作为强类型语言，隐式的类型转换是不被允许的，记住一条原则：让所有的东西都是显式的。</p>
<p>Go 语言其实也有一些动态语言的特性（通过关键字 var），所以它对那些逃离 Java 和 .Net 世界而使用 Python、Ruby、PHP 和 JavaScript 的开发者们也具有很大的吸引力。</p>
<p>Go 语言支持交叉编译，比如说你可以在运行 Linux 系统的计算机上开发运行 Windows 下运行的应用程序。这是第一门完全支持 UTF-8 的编程语言，这不仅体现在它可以处理使用 UTF-8 编码的字符串，就连它的源码文件格式都是使用的 UTF-8 编码。Go 语言做到了真正的国际化！</p>
<p>Go 语言被设计成一门应用于搭载 Web 服务器，存储集群或类似用途的巨型中央服务器的系统编程语言。对于高性能分布式系统领域而言，Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持，这对于游戏服务端的开发而言是再好不过了。</p>
<p>Go 语言一个非常好的目标就是实现所谓的复杂事件处理（CEP），这项技术要求海量并行支持，高度的抽象化和高性能。当我们进入到物联网时代，CEP 必然会成为人们关注的焦点。</p>
<p>但是 Go 语言同时也是一门可以用于实现一般目标的语言，例如对于文本的处理，前端展现，甚至像使用脚本一样使用它。</p>
<p>值得注意的是，因为垃圾回收和自动内存分配的原因，Go 语言不适合用来开发对实时性要求很高的软件。</p>
<p>越来越多的谷歌内部的大型分布式应用程序都开始使用 Go 语言来开发，例如谷歌地球的一部分代码就是由 Go 语言完成的。</p>
<p>如果你想知道一些其它组织使用Go语言开发的实际应用项目，你可以到 使用 Go 的组织 页面进行查看。出于隐私保护的考虑，许多公司的项目都没有展示在这个页面。我们将会在第 21 章 讨论到一个使用 Go 语言开发的大型存储区域网络 (SAN) 案例。</p>
<p>在 Chrome 浏览器中内置了一款 Go 语言的编译器用于本地客户端 (NaCl)，这很可能会被用于在 Chrome OS 中执行 Go 语言开发的应用程序。</p>
<p>Go 语言可以在 Intel 或 ARM 处理器上运行，因此它也可以在安卓系统下运行，例如 Nexus 系列的产品。</p>
<p>在 Google App Engine 中使用 Go 语言：2011 年 5 月 5 日，官方发布了用于开发运行在 Google App Engine 上的 Web 应用的 Go SDK，在此之前，开发者们只能选择使用 Python 或者 Java。这主要是 David Symonds 和 Nigel Tao 努力的成果。目前最新的稳定版是基于 Go 1.4 的 SDK 1.9.18，于 2015 年 2 月 18 日发布。当前 Go 语言的稳定版本是 Go 1.4.2。</p>
<p><strong>关于特性缺失</strong></p>
<p>许多能够在大多数面向对象语言中使用的特性 Go 语言都没有支持，但其中的一部分可能会在未来被支持。</p>
<ul>
<li>为了简化设计，不支持函数重载和操作符重载</li>
<li>为了避免在 C/C++ 开发中的一些 Bug 和混乱，不支持隐式转换</li>
<li>Go 语言通过另一种途径实现面向对象设计（第 10-11 章）来放弃类和类型的继承</li>
<li>尽管在接口的使用方面（第 11 章）可以实现类似变体类型的功能，但本身不支持变体类型</li>
<li>不支持动态加载代码</li>
<li>不支持动态链接库</li>
<li>不支持泛型</li>
<li>通过 recover() 和 panic() 来替代异常机制（第 13.2-13.3 节）</li>
<li>不支持静态变量</li>
</ul>
<h2>参考资料</h2>
<ol>
<li><a href="https://golang.org/">Go 语言</a></li>
<li><a href="https://golang.org/doc/">Go 语言官方文档</a></li>
<li><a href="https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/01.2.md">the way to go</a></li>
</ol>
]]></description>
      <content:encoded><![CDATA[<h1>Golang 学习笔记（一）</h1>
<h2>1. 起源与发展</h2>
<p>Go 语言起源 2007 年，并于 2009 年正式对外发布。</p>
<p>该项目的三位领导者均是著名的 IT 工程师：</p>
<p>Robert Griesemer，参与开发 Java HotSpot 虚拟机；</p>
<p>Rob Pike，Go 语言项目总负责人，贝尔实验室 Unix 团队成员，参与的项目包括 Plan 9，Inferno 操作系统和 Limbo 编程语言；</p>
<p>Ken Thompson，贝尔实验室 Unix 团队成员，C 语言、Unix 和 Plan 9 的创始人之一，与 Rob Pike 共同开发了 UTF-8 字符集规范。</p>
<p>2009 年 11 月 10 日，开发团队将 Go 语言项目以 BSD-style 授权（完全开源）正式公布了 Linux 和 Mac OS X 平台上的版本。Hector Chu 于同年 11 月 22 日公布了 Windows 版本。</p>
<p>在 Go 语言在 2010 年 1 月 8 日被 Tiobe（闻名于它的编程语言流行程度排名）宣布为 “2009 年年度语言” 后，引起各界很大的反响。目前 Go 语言在这项排名中的最高记录是在 2017 年 1 月创下的第13名，流行程度 2.325%。</p>
<p><strong>时间轴：</strong></p>
<ul>
<li>2007 年 9 月 21 日：雏形设计</li>
<li>2009 年 11 月 10日：首次公开发布</li>
<li>2010 年 1 月 8 日：当选 2009 年年度语言</li>
<li>2010 年 5 月：谷歌投入使用</li>
<li>2011 年 5 月 5 日：Google App Engine 支持 Go 语言</li>
</ul>
<p>Go 语言的官方网站是 <a href="https://golang.org">golang.org</a></p>
<p>更多的信息详见 <a href="https://github.com/golang/go">github.com/golang/go</a>，Go 项目 Bug 追踪和功能预期详见 <a href="https://github.com/golang/go/issues">github.com/golang/go/issues</a>。</p>
<p>Go 以囊地鼠 (Gopher) 作为它的吉祥物。
<img src="https://vansiit.cc/img/go/favicon-gopher.png" alt="img.png"></p>
<p>Go 语言还有一个运行在 Google App Engine 上的 <a href="https://tour.golang.org">Go Tour</a>，你也可以通过执行命令 go install <a href="http://go-tour.googlecode.com/hg/gotour">go-tour.googlecode.com/hg/gotour</a> 安装到你的本地机器上。对于中文读者，可以访问该指南的 <a href="https://tour.go-zh.org/welcome/1">中文版本</a>，或通过命令 go install <a href="https://bitbucket.org/mikespook/go-tour-zh/gotour">https://bitbucket.org/mikespook/go-tour-zh/gotour</a> 进行安装。</p>
<h2>2. 语言的主要特性与发展的环境和影响因素</h2>
<p>在声明和包的设计方面，Go 语言受到 Pascal、Modula 和 Oberon 系语言的影响；在并发原理的设计上，</p>
<p>Go 语言从同样受到 Tony Hoare 的 CSP（通信序列进程 Communicating Sequential Processes）理论影响的 Limbo 和 Newsqueak 的实践中借鉴了一些经验，并使用了和 Erlang 类似的机制。</p>
<p>这是一门完全开源的编程语言，因为它使用 BSD 授权许可，所以任何人都可以进行商业软件的开发而不需要支付任何费用。</p>
<p>尽管为了能够让目前主流的开发者们能够对 Go 语言中的类 C 语言的语法感到非常亲切而易于转型，但是它在极大程度上简化了这些语法，使得它们比 C/C++ 的语法更加简洁和干净。</p>
<p>同时，Go 语言也拥有一些动态语言的特性，这使得使用 Python 和 Ruby 的开发者们在使用 Go 语言的时候感觉非常容易上手。</p>
<p><strong>为什么要创造一门编程语言</strong></p>
<ul>
<li>C/C++ 的发展速度无法跟上计算机发展的脚步，十多年来也没有出现一门与时代相符的主流系统编程语言，因此人们需要一门新的系统编程语言来弥补这个空缺，尤其是在计算机信息时代。</li>
<li>相比计算机性能的提升，软件开发领域不被认为发展得足够快或者比硬件发展得更加成功（有许多项目均以失败告终），同时应用程序的体积始终在不断地扩大，这就迫切地需要一门具备更高层次概念的低级语言来突破现状。</li>
<li>在 Go 语言出现之前，开发者们总是面临非常艰难的抉择，究竟是使用执行速度快但是编译速度并不理想的语言（如：C++），还是使用编译速度较快但执行效率不佳的语言（如：.NET、Java），或者说开发难度较低但执行速度一般的动态语言呢？显然，Go 语言旨在这 3 个条件之间做到了最佳的平衡：快速编译，高效执行，易于开发。</li>
</ul>
<p>Go语言通过减少关键字的数量（25 个）来简化编码过程中的混乱和复杂度。干净、整齐和简洁的语法也能够提高程序的编译速度，因为这些关键字在编译过程中少到甚至不需要符号表来协助解析。</p>
<p>这些方面的工作都是为了减少编码的工作量，甚至可以与 Java 的简化程度相比较。</p>
<p>Go 语言有一种极简抽象艺术家的感觉，因为它只提供了一到两种方法来解决某个问题，这使得开发者们的代码都非常容易阅读和理解。众所周知，代码的可读性是软件工程里最重要的一部分（ 译者注：代码是写给人看的，不是写给机器看的 ）。</p>
<p>这些设计理念没有建立其它概念之上，所以并不会因为牵扯到一些概念而将某个概念复杂化，他们之间是相互独立的。</p>
<p>Go 语言有一套完整的编码规范，你可以在 Go 语言编码规范 页面进行查看。</p>
<p>它不像 Ruby 那样通过实现过程来定义编码规范。作为一门具有明确编码规范的语言，它要求可以采用不同的编译器如 gc 和 gccgo 进行编译工作，这对语言本身拥有更好的编码规范起到很大帮助。</p>
<p>Go 语言从本质上（程序和结构方面）来实现并发编程。</p>
<p>因为 Go 语言没有类和继承的概念，所以它和 Java 或 C++ 看起来并不相同。但是它通过接口 (interface) 的概念来实现多态性。Go 语言有一个清晰易懂的轻量级类型系统，在类型之间也没有层级之说。因此可以说这是一门混合型的语言。</p>
<p>在传统的面向对象语言中，使用面向对象编程技术显得非常臃肿，它们总是通过复杂的模式来构建庞大的类型层级，这违背了编程语言应该提升生产力的宗旨。</p>
<p>函数是 Go 语言中的基本构件，它们的使用方法非常灵活。在第六章，我们会看到 Go 语言在函数式编程方面的基本概念。</p>
<p>Go 语言使用静态类型，所以它是类型安全的一门语言，加上通过构建到本地代码，程序的执行速度也非常快。</p>
<p>作为强类型语言，隐式的类型转换是不被允许的，记住一条原则：让所有的东西都是显式的。</p>
<p>Go 语言其实也有一些动态语言的特性（通过关键字 var），所以它对那些逃离 Java 和 .Net 世界而使用 Python、Ruby、PHP 和 JavaScript 的开发者们也具有很大的吸引力。</p>
<p>Go 语言支持交叉编译，比如说你可以在运行 Linux 系统的计算机上开发运行 Windows 下运行的应用程序。这是第一门完全支持 UTF-8 的编程语言，这不仅体现在它可以处理使用 UTF-8 编码的字符串，就连它的源码文件格式都是使用的 UTF-8 编码。Go 语言做到了真正的国际化！</p>
<p>Go 语言被设计成一门应用于搭载 Web 服务器，存储集群或类似用途的巨型中央服务器的系统编程语言。对于高性能分布式系统领域而言，Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持，这对于游戏服务端的开发而言是再好不过了。</p>
<p>Go 语言一个非常好的目标就是实现所谓的复杂事件处理（CEP），这项技术要求海量并行支持，高度的抽象化和高性能。当我们进入到物联网时代，CEP 必然会成为人们关注的焦点。</p>
<p>但是 Go 语言同时也是一门可以用于实现一般目标的语言，例如对于文本的处理，前端展现，甚至像使用脚本一样使用它。</p>
<p>值得注意的是，因为垃圾回收和自动内存分配的原因，Go 语言不适合用来开发对实时性要求很高的软件。</p>
<p>越来越多的谷歌内部的大型分布式应用程序都开始使用 Go 语言来开发，例如谷歌地球的一部分代码就是由 Go 语言完成的。</p>
<p>如果你想知道一些其它组织使用Go语言开发的实际应用项目，你可以到 使用 Go 的组织 页面进行查看。出于隐私保护的考虑，许多公司的项目都没有展示在这个页面。我们将会在第 21 章 讨论到一个使用 Go 语言开发的大型存储区域网络 (SAN) 案例。</p>
<p>在 Chrome 浏览器中内置了一款 Go 语言的编译器用于本地客户端 (NaCl)，这很可能会被用于在 Chrome OS 中执行 Go 语言开发的应用程序。</p>
<p>Go 语言可以在 Intel 或 ARM 处理器上运行，因此它也可以在安卓系统下运行，例如 Nexus 系列的产品。</p>
<p>在 Google App Engine 中使用 Go 语言：2011 年 5 月 5 日，官方发布了用于开发运行在 Google App Engine 上的 Web 应用的 Go SDK，在此之前，开发者们只能选择使用 Python 或者 Java。这主要是 David Symonds 和 Nigel Tao 努力的成果。目前最新的稳定版是基于 Go 1.4 的 SDK 1.9.18，于 2015 年 2 月 18 日发布。当前 Go 语言的稳定版本是 Go 1.4.2。</p>
<p><strong>关于特性缺失</strong></p>
<p>许多能够在大多数面向对象语言中使用的特性 Go 语言都没有支持，但其中的一部分可能会在未来被支持。</p>
<ul>
<li>为了简化设计，不支持函数重载和操作符重载</li>
<li>为了避免在 C/C++ 开发中的一些 Bug 和混乱，不支持隐式转换</li>
<li>Go 语言通过另一种途径实现面向对象设计（第 10-11 章）来放弃类和类型的继承</li>
<li>尽管在接口的使用方面（第 11 章）可以实现类似变体类型的功能，但本身不支持变体类型</li>
<li>不支持动态加载代码</li>
<li>不支持动态链接库</li>
<li>不支持泛型</li>
<li>通过 recover() 和 panic() 来替代异常机制（第 13.2-13.3 节）</li>
<li>不支持静态变量</li>
</ul>
<h2>参考资料</h2>
<ol>
<li><a href="https://golang.org/">Go 语言</a></li>
<li><a href="https://golang.org/doc/">Go 语言官方文档</a></li>
<li><a href="https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/01.2.md">the way to go</a></li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>Java项目升级实战：从JDK8到JDK17的完整迁移指南</title>
      <link>https://vansiit.cc/2025/05/21/project-upgrade.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/05/21/project-upgrade.html</guid>
      <pubDate>Wed, 21 May 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>Java 项目升级遇到的那些事儿</h1>
<p>某日，甲方爸爸丢过来一个excel。你们项目有漏洞啊，安全性怎么保证，赶紧全部修复掉，立刻马上。</p>
<p>遂打开excel，还好还好，10万行而已 QvQ</p>
<p>简单肉眼扫描了一下，主要分两大类。一是服务器相关的，部署机数据库之类；二是jar包漏掉类的。</p>
<p>好，现在的问题就是主要升级jar到没有安全漏掉的版本。打开 <a href="https://mvnrepository.com">https://mvnrepository.com</a> ，逐一升级。</p>
<p>分析一下：项目最早提交代码的时间是2018年，有jsp，还有vue… 先把JDK升级到17，javax.servlet升级成jakarta.servlet，spring-boot升级到3.4.5。重点点名一下，poi，httpclient，fastjson，mybatis plus，这几个包的升级。</p>
<p>期间各种版本兼容问题，代码写法问题，特此记录一下，希望能帮助到后来者。</p>
<h2>一、spring框架相关</h2>
<h3>1. xml配置文件修改成java-config或者yml</h3>
<pre><code>包括不仅限于 spring database，spring security，web.xml，用xml定义的bean
</code></pre>
<h3>2. 自己注入自己（OvO，就是这么吊）</h3>
<pre><code class="language-java">@Service
public class UserSubscribeMsgServiceImpl extends ServiceImpl&lt;UserSubscribeMsgMapper, UserSubscribeMsg&gt;
        implements IUserSubscribeMsgService {

    @Autowired
    private IUserSubscribeMsgService userSubscribeMsgService;

    @Override
    public JsonVO sendByPromoterMobile(PromoterSubMsgVO promoterSubMsgVO) throws Exception {
        // ...
        // 修改这里的调用方式
        userSubscribeMsgService.updateById(userSubscribeMsg);
        // 或使用代理确保事务
        // ((IUserSubscribeMsgService) AopContext.currentProxy()).updateById(userSubscribeMsg);
    }
}
</code></pre>
<h3>3. 先有鸡还是先有蛋，天才，自己看代码吧</h3>
<pre><code class="language-jva">@Autowired
UserEndpointRegistration registration;

@Bean
public UserEndpointRegistration userEndpointRegistration() {
    return new BasicUserEndpointRegistration();
}
</code></pre>
<h3>4. 注入没有一个实现类的Bean</h3>
<pre><code class="language-java">@Autowired
List&lt;UserEndpointDefine&gt; defines;
</code></pre>
<h3>5. spring注解错用</h3>
<pre><code class="language-java">// 修改前
@RequestMapping(value = &quot;/testValidateGetParams&quot;, method = RequestMethod.GET)
public void testValidateGetParams(@RequestParam(name = &quot;id&quot;, required = true) String id,
                                  @Min(value = 1, message = &quot;年龄最小只能1&quot;)
                                  @Max(value = 120, message = &quot;年龄最大只能120&quot;)
                                  @RequestParam(name = &quot;age&quot;, required = true, value = &quot;0&quot;) int age) {
}

// 修改后 自己看哪里错了吧
@RequestMapping(value = &quot;/testValidateGetParams&quot;, method = RequestMethod.GET)
public void testValidateGetParams(
    @RequestParam(name = &quot;id&quot;, required = true) String id,
    @Min(value = 1, message = &quot;年龄最小只能1&quot;)
    @Max(value = 120, message = &quot;年龄最大只能120&quot;)
    @RequestParam(name = &quot;age&quot;, required = true, defaultValue = &quot;0&quot;) int age
) {
}
</code></pre>
<h3>6. 循环依赖(多如牛毛，只举一例)</h3>
<pre><code class="language-java">// 在 YuouerShopPromoterServiceImpl 类中，为 ILiveInfoService 字段添加 @Lazy 注解
@Autowired
@Lazy  // 添加此注解
private ILiveInfoService liveInfoService;

// 在 LiveInfoServiceImpl 类中，为 IYuouerShopPromoterService 字段添加 @Lazy 注解
@Autowired
@Lazy  // 添加此注解
private IYuouerShopPromoterService yuouerShopPromoterService;
</code></pre>
<h2>二、二方包相关</h2>
<p>这类也是此次升级的痛点，由于项目久远，很多二方包连代码仓库都找不着了，都是从mavan私服直接update的jar包。</p>
<p>很多二方包的springboot版本不一致，javax.servlet升级到jakarta.servlet，只能把相关二方包全部干掉，手动把相关代码迁移到现有项目中。</p>
<p>这里总结一下，希望大家有所启发。小项目也好，大项目也一样，不要为了模块化而模块化，不要为了解耦而解耦，要考虑实际情况，很多时候微服务也是桎梏，小项目优先考虑的是功能完整性和稳定性。</p>
<p>优化方案：</p>
<blockquote>
<ol>
<li>优先考虑到的方案是反编译jar出来，然后把代码按照包名迁移到现有项目中，可能是我用的工具问题，jd-gui反编译出来的代码会添加很多的无效代码、行号、注释。后面就放弃反编译的做法了。</li>
<li>直接一个一个文件新建到项目里面，这样也好，只要项目需要的文件。</li>
</ol>
</blockquote>
<h2>三、三方包相关</h2>
<h3>mybatis plus 合集</h3>
<pre><code>升级过程中，遇到很多坑，情绪一直很稳定。

直到遇到mybatis plus，真是无力吐槽。升级就升级，把所有的包名路径全改了几个意思，Wrapper的用法也大调整，能不能向下兼容一下。

大变动的新版本升级一个新的artifactId，叫mybatis-plus3，旧的mybatis-plus漏洞修复一下，停更不就行了嘛。

你或者像jakarta也行，直接改名，我也只用javax.servlet全局替换成jakarta.servlet
</code></pre>
<h4>1. 举例几个包调整的例子</h4>
<blockquote>
<p>com.baomidou.mybatisplus.mapper -&gt; com.baomidou.mybatisplus.core.mapper
com.baomidou.mybatisplus.service.impl -&gt; com.baomidou.mybatisplus.extension.service.impl
com.baomidou.mybatisplus.annotations -&gt; com.baomidou.mybatisplus.annotation</p>
</blockquote>
<p>详细的看图吧
<img src="https://vansiit.cc/img/project-upgrade/1.png" alt="img.png">
<img src="https://vansiit.cc/img/project-upgrade/2.png" alt="img.png"></p>
<h4>2. 不支持联合主键</h4>
<pre><code class="language-java">@TableId(&quot;component_appid&quot;)
private String component_appid;

@TableId(&quot;authorizer_appid&quot;)
private String authorizerAppid;
</code></pre>
<h4>3. EntityWrapper查询废弃，改用QueryWrapper</h4>
<p>com.baomidou.mybatisplus.mapper.EntityWrapper -&gt; com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
这个是最可恶的，涉及到大量代码的修改，包括不仅限于eq、orderBy
举例：</p>
<pre><code class="language-java">// 旧写法
EntityWrapper&lt;WxMiniPushError&gt; wrapper = new EntityWrapper&lt;WxMiniPushError&gt;();
wrapper.eq(&quot;result&quot;, 1);
wrapper.eq(&quot;push_type&quot;, pushLog.getPushType());
wrapper.eq(&quot;user_id&quot;, pushLog.getUserId());
List&lt;WxMiniPushError&gt; pushErrorList = pushErrorMapper.selectList(wrapper);

// 新写法
LambdaQueryWrapper&lt;WxMiniPushError&gt; wrapper = new LambdaQueryWrapper&lt;WxMiniPushError&gt;();
wrapper.eq(WxMiniPushError::getResult, 1);
wrapper.eq(WxMiniPushError::getPushType, pushLog.getPushType());
wrapper.eq(WxMiniPushError::getUserId, pushLog.getUserId());
List&lt;WxMiniPushError&gt; pushErrorList = pushErrorMapper.selectList(wrapper);
</code></pre>
<h3>2.httpclient的升级client5</h3>
<p>这个升级也很麻烦，但是影响的范围有限，只需要把相关的几个工具类修改写法就可以了。
贴一个import类的区别吧</p>
<pre><code class="language-java">// 旧版
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

// 新版
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.Timeout;

</code></pre>
<h2>四，AI工具</h2>
<p>此次升级过程中，AI工具的帮助很大。</p>
<p>秉承着遇到问题解决问题的思路，一步一步从解决报错-&gt;编译通过-&gt;打包通过-&gt;启动成功-&gt;解决所有运行报错。</p>
<p>每一步都配合AI工具，帮助解决各种问题。主要使用的是chatgpt，DeepSeek，cursor这三个工具，再配合偶尔使用的搜索引擎。把一个2016年启动的java jsp项目，从java8升级到17，springboot升级到3，tomcat升级到10，前后累计工时超过两个月，AI大模型的作用完全发挥出来了。</p>
<p>网上很多人都会谈论各种大模型的优劣势，这个好，那个不好。从我一直使用的过程中来看，市面免费的大模型都用过，就不举例了。</p>
<p>这些大模型的比较差距没有代差，很多问题给出的结果思路几乎一致，我们从中筛选一个即可，配合自己的经验，基本就可以解决大部分问题。</p>
]]></description>
      <content:encoded><![CDATA[<h1>Java 项目升级遇到的那些事儿</h1>
<p>某日，甲方爸爸丢过来一个excel。你们项目有漏洞啊，安全性怎么保证，赶紧全部修复掉，立刻马上。</p>
<p>遂打开excel，还好还好，10万行而已 QvQ</p>
<p>简单肉眼扫描了一下，主要分两大类。一是服务器相关的，部署机数据库之类；二是jar包漏掉类的。</p>
<p>好，现在的问题就是主要升级jar到没有安全漏掉的版本。打开 <a href="https://mvnrepository.com">https://mvnrepository.com</a> ，逐一升级。</p>
<p>分析一下：项目最早提交代码的时间是2018年，有jsp，还有vue… 先把JDK升级到17，javax.servlet升级成jakarta.servlet，spring-boot升级到3.4.5。重点点名一下，poi，httpclient，fastjson，mybatis plus，这几个包的升级。</p>
<p>期间各种版本兼容问题，代码写法问题，特此记录一下，希望能帮助到后来者。</p>
<h2>一、spring框架相关</h2>
<h3>1. xml配置文件修改成java-config或者yml</h3>
<pre><code>包括不仅限于 spring database，spring security，web.xml，用xml定义的bean
</code></pre>
<h3>2. 自己注入自己（OvO，就是这么吊）</h3>
<pre><code class="language-java">@Service
public class UserSubscribeMsgServiceImpl extends ServiceImpl&lt;UserSubscribeMsgMapper, UserSubscribeMsg&gt;
        implements IUserSubscribeMsgService {

    @Autowired
    private IUserSubscribeMsgService userSubscribeMsgService;

    @Override
    public JsonVO sendByPromoterMobile(PromoterSubMsgVO promoterSubMsgVO) throws Exception {
        // ...
        // 修改这里的调用方式
        userSubscribeMsgService.updateById(userSubscribeMsg);
        // 或使用代理确保事务
        // ((IUserSubscribeMsgService) AopContext.currentProxy()).updateById(userSubscribeMsg);
    }
}
</code></pre>
<h3>3. 先有鸡还是先有蛋，天才，自己看代码吧</h3>
<pre><code class="language-jva">@Autowired
UserEndpointRegistration registration;

@Bean
public UserEndpointRegistration userEndpointRegistration() {
    return new BasicUserEndpointRegistration();
}
</code></pre>
<h3>4. 注入没有一个实现类的Bean</h3>
<pre><code class="language-java">@Autowired
List&lt;UserEndpointDefine&gt; defines;
</code></pre>
<h3>5. spring注解错用</h3>
<pre><code class="language-java">// 修改前
@RequestMapping(value = &quot;/testValidateGetParams&quot;, method = RequestMethod.GET)
public void testValidateGetParams(@RequestParam(name = &quot;id&quot;, required = true) String id,
                                  @Min(value = 1, message = &quot;年龄最小只能1&quot;)
                                  @Max(value = 120, message = &quot;年龄最大只能120&quot;)
                                  @RequestParam(name = &quot;age&quot;, required = true, value = &quot;0&quot;) int age) {
}

// 修改后 自己看哪里错了吧
@RequestMapping(value = &quot;/testValidateGetParams&quot;, method = RequestMethod.GET)
public void testValidateGetParams(
    @RequestParam(name = &quot;id&quot;, required = true) String id,
    @Min(value = 1, message = &quot;年龄最小只能1&quot;)
    @Max(value = 120, message = &quot;年龄最大只能120&quot;)
    @RequestParam(name = &quot;age&quot;, required = true, defaultValue = &quot;0&quot;) int age
) {
}
</code></pre>
<h3>6. 循环依赖(多如牛毛，只举一例)</h3>
<pre><code class="language-java">// 在 YuouerShopPromoterServiceImpl 类中，为 ILiveInfoService 字段添加 @Lazy 注解
@Autowired
@Lazy  // 添加此注解
private ILiveInfoService liveInfoService;

// 在 LiveInfoServiceImpl 类中，为 IYuouerShopPromoterService 字段添加 @Lazy 注解
@Autowired
@Lazy  // 添加此注解
private IYuouerShopPromoterService yuouerShopPromoterService;
</code></pre>
<h2>二、二方包相关</h2>
<p>这类也是此次升级的痛点，由于项目久远，很多二方包连代码仓库都找不着了，都是从mavan私服直接update的jar包。</p>
<p>很多二方包的springboot版本不一致，javax.servlet升级到jakarta.servlet，只能把相关二方包全部干掉，手动把相关代码迁移到现有项目中。</p>
<p>这里总结一下，希望大家有所启发。小项目也好，大项目也一样，不要为了模块化而模块化，不要为了解耦而解耦，要考虑实际情况，很多时候微服务也是桎梏，小项目优先考虑的是功能完整性和稳定性。</p>
<p>优化方案：</p>
<blockquote>
<ol>
<li>优先考虑到的方案是反编译jar出来，然后把代码按照包名迁移到现有项目中，可能是我用的工具问题，jd-gui反编译出来的代码会添加很多的无效代码、行号、注释。后面就放弃反编译的做法了。</li>
<li>直接一个一个文件新建到项目里面，这样也好，只要项目需要的文件。</li>
</ol>
</blockquote>
<h2>三、三方包相关</h2>
<h3>mybatis plus 合集</h3>
<pre><code>升级过程中，遇到很多坑，情绪一直很稳定。

直到遇到mybatis plus，真是无力吐槽。升级就升级，把所有的包名路径全改了几个意思，Wrapper的用法也大调整，能不能向下兼容一下。

大变动的新版本升级一个新的artifactId，叫mybatis-plus3，旧的mybatis-plus漏洞修复一下，停更不就行了嘛。

你或者像jakarta也行，直接改名，我也只用javax.servlet全局替换成jakarta.servlet
</code></pre>
<h4>1. 举例几个包调整的例子</h4>
<blockquote>
<p>com.baomidou.mybatisplus.mapper -&gt; com.baomidou.mybatisplus.core.mapper
com.baomidou.mybatisplus.service.impl -&gt; com.baomidou.mybatisplus.extension.service.impl
com.baomidou.mybatisplus.annotations -&gt; com.baomidou.mybatisplus.annotation</p>
</blockquote>
<p>详细的看图吧
<img src="https://vansiit.cc/img/project-upgrade/1.png" alt="img.png">
<img src="https://vansiit.cc/img/project-upgrade/2.png" alt="img.png"></p>
<h4>2. 不支持联合主键</h4>
<pre><code class="language-java">@TableId(&quot;component_appid&quot;)
private String component_appid;

@TableId(&quot;authorizer_appid&quot;)
private String authorizerAppid;
</code></pre>
<h4>3. EntityWrapper查询废弃，改用QueryWrapper</h4>
<p>com.baomidou.mybatisplus.mapper.EntityWrapper -&gt; com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
这个是最可恶的，涉及到大量代码的修改，包括不仅限于eq、orderBy
举例：</p>
<pre><code class="language-java">// 旧写法
EntityWrapper&lt;WxMiniPushError&gt; wrapper = new EntityWrapper&lt;WxMiniPushError&gt;();
wrapper.eq(&quot;result&quot;, 1);
wrapper.eq(&quot;push_type&quot;, pushLog.getPushType());
wrapper.eq(&quot;user_id&quot;, pushLog.getUserId());
List&lt;WxMiniPushError&gt; pushErrorList = pushErrorMapper.selectList(wrapper);

// 新写法
LambdaQueryWrapper&lt;WxMiniPushError&gt; wrapper = new LambdaQueryWrapper&lt;WxMiniPushError&gt;();
wrapper.eq(WxMiniPushError::getResult, 1);
wrapper.eq(WxMiniPushError::getPushType, pushLog.getPushType());
wrapper.eq(WxMiniPushError::getUserId, pushLog.getUserId());
List&lt;WxMiniPushError&gt; pushErrorList = pushErrorMapper.selectList(wrapper);
</code></pre>
<h3>2.httpclient的升级client5</h3>
<p>这个升级也很麻烦，但是影响的范围有限，只需要把相关的几个工具类修改写法就可以了。
贴一个import类的区别吧</p>
<pre><code class="language-java">// 旧版
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

// 新版
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.Timeout;

</code></pre>
<h2>四，AI工具</h2>
<p>此次升级过程中，AI工具的帮助很大。</p>
<p>秉承着遇到问题解决问题的思路，一步一步从解决报错-&gt;编译通过-&gt;打包通过-&gt;启动成功-&gt;解决所有运行报错。</p>
<p>每一步都配合AI工具，帮助解决各种问题。主要使用的是chatgpt，DeepSeek，cursor这三个工具，再配合偶尔使用的搜索引擎。把一个2016年启动的java jsp项目，从java8升级到17，springboot升级到3，tomcat升级到10，前后累计工时超过两个月，AI大模型的作用完全发挥出来了。</p>
<p>网上很多人都会谈论各种大模型的优劣势，这个好，那个不好。从我一直使用的过程中来看，市面免费的大模型都用过，就不举例了。</p>
<p>这些大模型的比较差距没有代差，很多问题给出的结果思路几乎一致，我们从中筛选一个即可，配合自己的经验，基本就可以解决大部分问题。</p>
]]></content:encoded>
    </item>
    <item>
      <title>人工智能(AI)深度分析：现状、挑战、瓶颈与未来发展趋势</title>
      <link>https://vansiit.cc/2025/02/08/ai.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/02/08/ai.html</guid>
      <pubDate>Sat, 08 Feb 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>人工智能 (AI) 的现状、问题、瓶颈、未来趋势</h1>
<h2>一、现状</h2>
<p>近年来，AI 发展迅速，已广泛应用于多个领域：</p>
<ul>
<li><strong>计算机视觉</strong>
图像识别、目标检测、人脸识别等。</li>
<li><strong>自然语言处理</strong>
机器翻译、文本生成、情感分析等。</li>
<li><strong>语音识别和合成</strong>
语音助手、语音输入、语音合成等。</li>
<li><strong>机器学习</strong>
数据挖掘、预测分析、个性化推荐等。</li>
<li><strong>机器人技术</strong>
工业机器人、服务机器人、医疗机器人等。</li>
</ul>
<p>AI 正逐步改变我们的生活和工作方式，并推动各行业创新。</p>
<hr>
<h2>二、问题</h2>
<p>尽管 AI 发展迅速，但仍面临诸多挑战：</p>
<ul>
<li><strong>数据依赖</strong>
AI 依赖大量高质量数据，但数据获取、标注和隐私保护问题突出。</li>
<li><strong>算法偏见</strong>
训练数据中的偏见可能导致算法决策不公。</li>
<li><strong>可解释性</strong>
许多 AI 模型（如深度学习）的决策过程难以解释，影响信任。</li>
<li><strong>安全性</strong>
AI 系统可能遭受攻击，导致错误决策或数据泄露。</li>
<li><strong>伦理问题</strong>
AI 发展引发隐私、就业、算法歧视等伦理争议。</li>
</ul>
<hr>
<h2>三、瓶颈</h2>
<p>AI 进一步发展面临以下瓶颈：</p>
<ul>
<li><strong>算力限制</strong>
复杂 AI 模型需要强大算力支持，硬件性能仍需提升。</li>
<li><strong>算法局限</strong>
现有算法在处理复杂任务和泛化能力上仍有不足。</li>
<li><strong>人才短缺</strong>
AI 领域专业人才供不应求，制约行业发展。</li>
</ul>
<hr>
<h2>四、未来趋势</h2>
<p>未来，AI 将呈现以下趋势：</p>
<ul>
<li><strong>更强大的算法</strong>
深度学习、强化学习等算法将不断优化，提升 AI 性能。</li>
<li><strong>更广泛的应用</strong>
AI 将深入更多行业，如医疗、教育、金融等。</li>
<li><strong>更紧密的人机协作</strong>
AI 将辅助人类工作，提升效率和决策质量。</li>
<li><strong>更注重伦理和规范</strong>
随着 AI 影响力扩大，伦理和规范将更加重要。</li>
</ul>
<hr>
<h2>五、总结</h2>
<p>AI 正在重塑世界，尽管面临挑战，但其潜力巨大。未来，AI 将在推动社会进步和改善人类生活中发挥更大作用。</p>
]]></description>
      <content:encoded><![CDATA[<h1>人工智能 (AI) 的现状、问题、瓶颈、未来趋势</h1>
<h2>一、现状</h2>
<p>近年来，AI 发展迅速，已广泛应用于多个领域：</p>
<ul>
<li><strong>计算机视觉</strong>
图像识别、目标检测、人脸识别等。</li>
<li><strong>自然语言处理</strong>
机器翻译、文本生成、情感分析等。</li>
<li><strong>语音识别和合成</strong>
语音助手、语音输入、语音合成等。</li>
<li><strong>机器学习</strong>
数据挖掘、预测分析、个性化推荐等。</li>
<li><strong>机器人技术</strong>
工业机器人、服务机器人、医疗机器人等。</li>
</ul>
<p>AI 正逐步改变我们的生活和工作方式，并推动各行业创新。</p>
<hr>
<h2>二、问题</h2>
<p>尽管 AI 发展迅速，但仍面临诸多挑战：</p>
<ul>
<li><strong>数据依赖</strong>
AI 依赖大量高质量数据，但数据获取、标注和隐私保护问题突出。</li>
<li><strong>算法偏见</strong>
训练数据中的偏见可能导致算法决策不公。</li>
<li><strong>可解释性</strong>
许多 AI 模型（如深度学习）的决策过程难以解释，影响信任。</li>
<li><strong>安全性</strong>
AI 系统可能遭受攻击，导致错误决策或数据泄露。</li>
<li><strong>伦理问题</strong>
AI 发展引发隐私、就业、算法歧视等伦理争议。</li>
</ul>
<hr>
<h2>三、瓶颈</h2>
<p>AI 进一步发展面临以下瓶颈：</p>
<ul>
<li><strong>算力限制</strong>
复杂 AI 模型需要强大算力支持，硬件性能仍需提升。</li>
<li><strong>算法局限</strong>
现有算法在处理复杂任务和泛化能力上仍有不足。</li>
<li><strong>人才短缺</strong>
AI 领域专业人才供不应求，制约行业发展。</li>
</ul>
<hr>
<h2>四、未来趋势</h2>
<p>未来，AI 将呈现以下趋势：</p>
<ul>
<li><strong>更强大的算法</strong>
深度学习、强化学习等算法将不断优化，提升 AI 性能。</li>
<li><strong>更广泛的应用</strong>
AI 将深入更多行业，如医疗、教育、金融等。</li>
<li><strong>更紧密的人机协作</strong>
AI 将辅助人类工作，提升效率和决策质量。</li>
<li><strong>更注重伦理和规范</strong>
随着 AI 影响力扩大，伦理和规范将更加重要。</li>
</ul>
<hr>
<h2>五、总结</h2>
<p>AI 正在重塑世界，尽管面临挑战，但其潜力巨大。未来，AI 将在推动社会进步和改善人类生活中发挥更大作用。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Java对接Google Pay完整指南：支付集成与安全实现</title>
      <link>https://vansiit.cc/2025/02/08/googlepay.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/02/08/googlepay.html</guid>
      <pubDate>Sat, 08 Feb 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>Java 对接 Google Pay 详细文档</h1>
<p>本文档旨在介绍如何在 Java 后端系统中对接 Google Pay，主要涵盖整体流程、关键步骤和需要特别注意的要点。需要注意的是，Google Pay 的核心功能主要由客户端（如 Android 应用或 Web 前端）发起支付请求，后端 Java 系统则负责接收和处理 Google Pay 返回的加密支付令牌，再交由支付处理器完成扣款。下面介绍详细内容。</p>
<hr>
<h2>目录</h2>
<ol>
<li><a href="#%E6%A6%82%E8%BF%B0">概述</a></li>
<li><a href="#%E5%85%88%E5%86%B3%E6%9D%A1%E4%BB%B6">先决条件</a></li>
<li><a href="#%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E6%B5%81%E7%A8%8B">整体架构与流程</a></li>
<li><a href="#java-%E5%90%8E%E7%AB%AF%E5%AF%B9%E6%8E%A5%E6%AD%A5%E9%AA%A4">Java 后端对接步骤</a></li>
<li><a href="#%E7%89%B9%E5%88%AB%E6%B3%A8%E6%84%8F%E7%9A%84%E5%85%B3%E9%94%AE%E7%82%B9">特别注意的关键点</a></li>
<li><a href="#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E4%B8%8E%E5%AE%89%E5%85%A8%E5%BB%BA%E8%AE%AE">错误处理与安全建议</a></li>
<li><a href="#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</a></li>
</ol>
<hr>
<h2>概述</h2>
<p>Google Pay 是 Google 提供的移动和在线支付解决方案，它允许用户在客户端完成支付信息的授权，然后将加密的支付令牌传送到后端进行处理。在 Java 后端中，对接 Google Pay 主要包括以下流程：</p>
<ul>
<li><strong>客户端集成</strong>：前端（Android、Web）使用 Google Pay API 发起支付请求，并返回一个加密支付令牌（Payment Token）。</li>
<li><strong>后端处理</strong>：Java 服务端接收该令牌，解密并验证后交由支付网关（如 Stripe、Braintree、Adyen 等）完成实际扣款或订单捕获。</li>
</ul>
<hr>
<h2>先决条件</h2>
<ul>
<li>
<p><strong>Google Pay 商家账户</strong>
在 <a href="https://developers.google.com/pay/api">Google Pay API Console</a> 注册商家信息，获取必要的商家 ID 和配置信息。</p>
</li>
<li>
<p><strong>支付处理器支持</strong>
后端通常不会直接处理支付令牌，而是依赖于第三方支付网关。确保你选定的支付处理器支持 Google Pay（例如 Stripe、Braintree、Adyen 等）。</p>
</li>
<li>
<p><strong>开发与生产环境</strong>
在 Sandbox 环境中测试完毕后，再切换至生产环境。注意环境配置和 API Endpoint 的切换。</p>
</li>
<li>
<p><strong>HTTPS 安全通信</strong>
后端服务必须支持 HTTPS，确保所有数据传输安全。</p>
</li>
</ul>
<hr>
<h2>整体架构与流程</h2>
<ol>
<li>
<p><strong>客户端（Android/Web）</strong></p>
<ul>
<li>使用 Google Pay API 构建 <code>PaymentDataRequest</code>，配置允许的支付方式、交易金额、币种及回调 URL。</li>
<li>用户在前端确认支付后，Google Pay 返回一个加密的支付令牌。</li>
</ul>
</li>
<li>
<p><strong>后端 Java 系统</strong></p>
<ul>
<li>接收前端传递的支付令牌。</li>
<li>对令牌进行验证（部分支付处理器会提供解密和验证功能）。</li>
<li>将支付令牌传递给支付网关，执行订单捕获或支付扣款操作。</li>
<li>返回支付结果给前端，并更新订单状态。</li>
</ul>
</li>
</ol>
<hr>
<h2>Java 后端对接步骤</h2>
<h3>1. 接收支付令牌</h3>
<ul>
<li>定义 API 接口（例如 RESTful API），接收客户端传来的 JSON 数据，其中包含 Google Pay 的加密令牌。</li>
</ul>
<h3>2. 验证与解析支付令牌</h3>
<ul>
<li>使用支付处理器提供的 SDK 或 API 方法对令牌进行解密和验证。</li>
<li>核对令牌中的订单金额、币种、商家 ID 等数据是否与系统订单一致。</li>
</ul>
<h3>3. 调用支付网关进行支付</h3>
<ul>
<li>根据支付处理器文档，将验证后的支付令牌传递给支付网关，调用捕获订单或扣款的 API 接口。</li>
<li>注意保存和记录支付网关返回的交易 ID 和状态。</li>
</ul>
<h3>4. 处理支付结果</h3>
<ul>
<li>根据支付网关的响应更新订单状态（例如支付成功、失败、待处理等）。</li>
<li>向客户端返回最终支付结果，同时记录日志便于后续排查问题。</li>
</ul>
<h3>5. 集成 Webhook（可选）</h3>
<ul>
<li>配置 Webhook 接收支付状态异步通知，确保后端订单状态与支付处理器保持同步。</li>
</ul>
<hr>
<h2>特别注意的关键点</h2>
<ul>
<li>
<p><strong>令牌安全性</strong></p>
<ul>
<li>支付令牌为加密数据，切勿在后端日志中明文记录或在不安全的网络传输中暴露。</li>
<li>确保令牌在短时间内处理完毕，避免令牌重复使用或滞后失效。</li>
</ul>
</li>
<li>
<p><strong>环境区分</strong></p>
<ul>
<li>开发阶段使用 Sandbox 环境，生产时切换到正式环境。不同环境下的 API Endpoint 和凭证可能不同，务必确保正确配置。</li>
</ul>
</li>
<li>
<p><strong>凭证管理</strong></p>
<ul>
<li>将 Google Pay 商家凭证、安全密钥等敏感信息保存在安全配置文件或环境变量中，避免硬编码在源代码中。</li>
</ul>
</li>
<li>
<p><strong>支付处理器集成</strong></p>
<ul>
<li>由于 Google Pay 仅提供支付令牌，后端必须依赖支付网关完成实际扣款。选择支持 Google Pay 的处理器并仔细阅读其文档。</li>
<li>注意处理器 SDK 的版本更新及 API 变更，确保集成代码持续可用。</li>
</ul>
</li>
<li>
<p><strong>错误处理与日志记录</strong></p>
<ul>
<li>对所有 API 调用和支付网关交互记录详细日志，包括 HTTP 状态码和错误信息。</li>
<li>实现合理的错误重试机制，尤其是对于网络或服务中断情况。</li>
</ul>
</li>
<li>
<p><strong>安全通信</strong></p>
<ul>
<li>使用 HTTPS 确保所有客户端与服务器之间的通信加密。</li>
<li>验证 Webhook 消息的签名，防止伪造通知。</li>
</ul>
</li>
<li>
<p><strong>事务一致性</strong></p>
<ul>
<li>在支付捕获、订单更新等关键步骤中，确保事务的一致性，防止因系统异常导致的重复扣款或订单状态不一致。</li>
</ul>
</li>
</ul>
<hr>
<h2>错误处理与安全建议</h2>
<ul>
<li>
<p><strong>错误码处理</strong></p>
<ul>
<li>参考支付处理器文档，对常见错误码（如令牌无效、金额不匹配等）进行详细处理，并返回明确提示给客户端。</li>
</ul>
</li>
<li>
<p><strong>重试机制</strong></p>
<ul>
<li>对于网络异常或临时服务中断的情况，设计合理的重试策略，并记录重试日志以便监控。</li>
</ul>
</li>
<li>
<p><strong>定期审核</strong></p>
<ul>
<li>定期检查集成代码和依赖库的安全性，及时更新支付处理器 SDK 以及 Google Pay API 相关文档中的建议。</li>
</ul>
</li>
</ul>
<hr>
<h2>参考资料</h2>
<ul>
<li><a href="https://developers.google.com/pay/api">Google Pay API 文档</a></li>
<li><a href="https://developers.google.com/pay/api/android/guides/setup">Google Pay Integration Guide</a></li>
<li><a href="https://developers.google.com/pay/api/web/guides/overview">Google Pay for Web Overview</a></li>
<li>支付处理器各自的 Java SDK 文档（如 <a href="https://github.com/stripe/stripe-java">Stripe Java SDK</a> 或 <a href="https://developers.braintreepayments.com/start/overview">Braintree Java SDK</a>）</li>
</ul>
<hr>
]]></description>
      <content:encoded><![CDATA[<h1>Java 对接 Google Pay 详细文档</h1>
<p>本文档旨在介绍如何在 Java 后端系统中对接 Google Pay，主要涵盖整体流程、关键步骤和需要特别注意的要点。需要注意的是，Google Pay 的核心功能主要由客户端（如 Android 应用或 Web 前端）发起支付请求，后端 Java 系统则负责接收和处理 Google Pay 返回的加密支付令牌，再交由支付处理器完成扣款。下面介绍详细内容。</p>
<hr>
<h2>目录</h2>
<ol>
<li><a href="#%E6%A6%82%E8%BF%B0">概述</a></li>
<li><a href="#%E5%85%88%E5%86%B3%E6%9D%A1%E4%BB%B6">先决条件</a></li>
<li><a href="#%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E6%B5%81%E7%A8%8B">整体架构与流程</a></li>
<li><a href="#java-%E5%90%8E%E7%AB%AF%E5%AF%B9%E6%8E%A5%E6%AD%A5%E9%AA%A4">Java 后端对接步骤</a></li>
<li><a href="#%E7%89%B9%E5%88%AB%E6%B3%A8%E6%84%8F%E7%9A%84%E5%85%B3%E9%94%AE%E7%82%B9">特别注意的关键点</a></li>
<li><a href="#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E4%B8%8E%E5%AE%89%E5%85%A8%E5%BB%BA%E8%AE%AE">错误处理与安全建议</a></li>
<li><a href="#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</a></li>
</ol>
<hr>
<h2>概述</h2>
<p>Google Pay 是 Google 提供的移动和在线支付解决方案，它允许用户在客户端完成支付信息的授权，然后将加密的支付令牌传送到后端进行处理。在 Java 后端中，对接 Google Pay 主要包括以下流程：</p>
<ul>
<li><strong>客户端集成</strong>：前端（Android、Web）使用 Google Pay API 发起支付请求，并返回一个加密支付令牌（Payment Token）。</li>
<li><strong>后端处理</strong>：Java 服务端接收该令牌，解密并验证后交由支付网关（如 Stripe、Braintree、Adyen 等）完成实际扣款或订单捕获。</li>
</ul>
<hr>
<h2>先决条件</h2>
<ul>
<li>
<p><strong>Google Pay 商家账户</strong>
在 <a href="https://developers.google.com/pay/api">Google Pay API Console</a> 注册商家信息，获取必要的商家 ID 和配置信息。</p>
</li>
<li>
<p><strong>支付处理器支持</strong>
后端通常不会直接处理支付令牌，而是依赖于第三方支付网关。确保你选定的支付处理器支持 Google Pay（例如 Stripe、Braintree、Adyen 等）。</p>
</li>
<li>
<p><strong>开发与生产环境</strong>
在 Sandbox 环境中测试完毕后，再切换至生产环境。注意环境配置和 API Endpoint 的切换。</p>
</li>
<li>
<p><strong>HTTPS 安全通信</strong>
后端服务必须支持 HTTPS，确保所有数据传输安全。</p>
</li>
</ul>
<hr>
<h2>整体架构与流程</h2>
<ol>
<li>
<p><strong>客户端（Android/Web）</strong></p>
<ul>
<li>使用 Google Pay API 构建 <code>PaymentDataRequest</code>，配置允许的支付方式、交易金额、币种及回调 URL。</li>
<li>用户在前端确认支付后，Google Pay 返回一个加密的支付令牌。</li>
</ul>
</li>
<li>
<p><strong>后端 Java 系统</strong></p>
<ul>
<li>接收前端传递的支付令牌。</li>
<li>对令牌进行验证（部分支付处理器会提供解密和验证功能）。</li>
<li>将支付令牌传递给支付网关，执行订单捕获或支付扣款操作。</li>
<li>返回支付结果给前端，并更新订单状态。</li>
</ul>
</li>
</ol>
<hr>
<h2>Java 后端对接步骤</h2>
<h3>1. 接收支付令牌</h3>
<ul>
<li>定义 API 接口（例如 RESTful API），接收客户端传来的 JSON 数据，其中包含 Google Pay 的加密令牌。</li>
</ul>
<h3>2. 验证与解析支付令牌</h3>
<ul>
<li>使用支付处理器提供的 SDK 或 API 方法对令牌进行解密和验证。</li>
<li>核对令牌中的订单金额、币种、商家 ID 等数据是否与系统订单一致。</li>
</ul>
<h3>3. 调用支付网关进行支付</h3>
<ul>
<li>根据支付处理器文档，将验证后的支付令牌传递给支付网关，调用捕获订单或扣款的 API 接口。</li>
<li>注意保存和记录支付网关返回的交易 ID 和状态。</li>
</ul>
<h3>4. 处理支付结果</h3>
<ul>
<li>根据支付网关的响应更新订单状态（例如支付成功、失败、待处理等）。</li>
<li>向客户端返回最终支付结果，同时记录日志便于后续排查问题。</li>
</ul>
<h3>5. 集成 Webhook（可选）</h3>
<ul>
<li>配置 Webhook 接收支付状态异步通知，确保后端订单状态与支付处理器保持同步。</li>
</ul>
<hr>
<h2>特别注意的关键点</h2>
<ul>
<li>
<p><strong>令牌安全性</strong></p>
<ul>
<li>支付令牌为加密数据，切勿在后端日志中明文记录或在不安全的网络传输中暴露。</li>
<li>确保令牌在短时间内处理完毕，避免令牌重复使用或滞后失效。</li>
</ul>
</li>
<li>
<p><strong>环境区分</strong></p>
<ul>
<li>开发阶段使用 Sandbox 环境，生产时切换到正式环境。不同环境下的 API Endpoint 和凭证可能不同，务必确保正确配置。</li>
</ul>
</li>
<li>
<p><strong>凭证管理</strong></p>
<ul>
<li>将 Google Pay 商家凭证、安全密钥等敏感信息保存在安全配置文件或环境变量中，避免硬编码在源代码中。</li>
</ul>
</li>
<li>
<p><strong>支付处理器集成</strong></p>
<ul>
<li>由于 Google Pay 仅提供支付令牌，后端必须依赖支付网关完成实际扣款。选择支持 Google Pay 的处理器并仔细阅读其文档。</li>
<li>注意处理器 SDK 的版本更新及 API 变更，确保集成代码持续可用。</li>
</ul>
</li>
<li>
<p><strong>错误处理与日志记录</strong></p>
<ul>
<li>对所有 API 调用和支付网关交互记录详细日志，包括 HTTP 状态码和错误信息。</li>
<li>实现合理的错误重试机制，尤其是对于网络或服务中断情况。</li>
</ul>
</li>
<li>
<p><strong>安全通信</strong></p>
<ul>
<li>使用 HTTPS 确保所有客户端与服务器之间的通信加密。</li>
<li>验证 Webhook 消息的签名，防止伪造通知。</li>
</ul>
</li>
<li>
<p><strong>事务一致性</strong></p>
<ul>
<li>在支付捕获、订单更新等关键步骤中，确保事务的一致性，防止因系统异常导致的重复扣款或订单状态不一致。</li>
</ul>
</li>
</ul>
<hr>
<h2>错误处理与安全建议</h2>
<ul>
<li>
<p><strong>错误码处理</strong></p>
<ul>
<li>参考支付处理器文档，对常见错误码（如令牌无效、金额不匹配等）进行详细处理，并返回明确提示给客户端。</li>
</ul>
</li>
<li>
<p><strong>重试机制</strong></p>
<ul>
<li>对于网络异常或临时服务中断的情况，设计合理的重试策略，并记录重试日志以便监控。</li>
</ul>
</li>
<li>
<p><strong>定期审核</strong></p>
<ul>
<li>定期检查集成代码和依赖库的安全性，及时更新支付处理器 SDK 以及 Google Pay API 相关文档中的建议。</li>
</ul>
</li>
</ul>
<hr>
<h2>参考资料</h2>
<ul>
<li><a href="https://developers.google.com/pay/api">Google Pay API 文档</a></li>
<li><a href="https://developers.google.com/pay/api/android/guides/setup">Google Pay Integration Guide</a></li>
<li><a href="https://developers.google.com/pay/api/web/guides/overview">Google Pay for Web Overview</a></li>
<li>支付处理器各自的 Java SDK 文档（如 <a href="https://github.com/stripe/stripe-java">Stripe Java SDK</a> 或 <a href="https://developers.braintreepayments.com/start/overview">Braintree Java SDK</a>）</li>
</ul>
<hr>
]]></content:encoded>
    </item>
    <item>
      <title>国外支付平台全面对比：国际化APP支付集成最佳实践</title>
      <link>https://vansiit.cc/2025/02/08/pay.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/02/08/pay.html</guid>
      <pubDate>Sat, 08 Feb 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>国外支付平台及国际化 APP 支付对接建议</h1>
<p>在国外（国际市场）的支付领域，众多支付平台因各自独特的优势而被广泛应用。总体来说，国外主流且较为靠谱的支付平台主要可以分为以下几类：</p>
<h2>1. 全球性在线支付平台</h2>
<h3>PayPal</h3>
<ul>
<li><strong>简介</strong>：全球知名的在线支付工具，覆盖 200 多个国家和地区，支持多币种支付，能够接受主要信用卡和借记卡付款。</li>
<li><strong>特点</strong>：以安全、便捷著称，常用于跨境电商和国际交易中。</li>
<li><strong>引用</strong>：​:contentReference[oaicite:0]{index=0} :contentReference[oaicite:1]{index=1}</li>
</ul>
<h3>Stripe</h3>
<ul>
<li><strong>简介</strong>：以强大的 API、开发者友好的工具和广泛的支付方式支持（包括信用卡、数字钱包、Apple Pay、Google Pay 等）而受到全球企业青睐。</li>
<li><strong>特点</strong>：支持多币种结算，适合希望快速构建国际化支付系统的开发者。</li>
<li><strong>引用</strong>：​:contentReference[oaicite:2]{index=2}</li>
</ul>
<h2>2. 移动支付及数字钱包</h2>
<h3>Apple Pay &amp; Google Pay</h3>
<ul>
<li><strong>简介</strong>：这两种移动支付方式在欧美及其他发达市场中使用广泛。</li>
<li><strong>特点</strong>：利用手机或智能手表，通过 NFC 及安全认证，为用户提供便捷、无缝的支付体验。</li>
<li><strong>引用</strong>：​:contentReference[oaicite:3]{index=3}</li>
</ul>
<h2>3. 跨境及聚合支付平台</h2>
<h3>Adyen &amp; Worldpay</h3>
<ul>
<li><strong>简介</strong>：专注于支持跨境电商与全球收单业务的支付平台。</li>
<li><strong>特点</strong>：
<ul>
<li>支持信用卡、借记卡、电子钱包、银行转账等多种支付方式；</li>
<li>提供多币种结算和强大的风险管理工具。</li>
</ul>
</li>
<li><strong>引用</strong>：​:contentReference[oaicite:4]{index=4}</li>
</ul>
<h3>Skrill &amp; Payoneer</h3>
<ul>
<li><strong>简介</strong>：在欧洲及跨境电商领域广受欢迎的平台，适用于小额支付以及自由职业者和跨国企业的资金结算。</li>
<li><strong>引用</strong>：​:contentReference[oaicite:5]{index=5}</li>
</ul>
<h3>美国本土支付网关（如 <a href="http://Authorize.Net">Authorize.Net</a>、2Checkout）</h3>
<ul>
<li><strong>简介</strong>：主要在美国市场使用，适合对接网站或 APP 中的信用卡支付功能。</li>
<li><strong>特点</strong>：流程成熟、稳定性高。</li>
</ul>
<h2>开发国际化 APP 时的对接建议</h2>
<p>从前瞻性的角度看，要打造面向全球用户的 APP，支付接口的多样性和灵活性至关重要。建议如下：</p>
<ol>
<li>
<p><strong>多渠道集成</strong>
根据目标市场用户的支付习惯，至少集成：</p>
<ul>
<li>一种全球性支付平台（例如 PayPal 或 Stripe）；</li>
<li>一种主流移动支付方式（例如 Apple Pay 或 Google Pay）。
这样可以覆盖大部分用户的支付需求，并支持跨境交易。</li>
</ul>
</li>
<li>
<p><strong>安全性与合规性</strong></p>
<ul>
<li>选择支持 PCI-DSS 等国际安全标准的平台，确保用户数据和资金安全。</li>
<li>密切关注各国的法规要求，及时调整接口策略以符合法规。</li>
</ul>
</li>
<li>
<p><strong>多币种及跨境支付支持</strong>
对于跨境交易，建议优先选择支持多币种结算的平台（如 Adyen、Worldpay），以降低汇率风险和手续费成本。</p>
</li>
<li>
<p><strong>开放 API 与聚合支付方案</strong></p>
<ul>
<li>趋势上支付将越来越注重无缝集成和用户体验。</li>
<li>采用聚合支付方案可通过一个统一接口接入多种支付方式，既简化开发流程，又能灵活应对市场变化。</li>
</ul>
</li>
</ol>
<h2>总结</h2>
<p>如果你开发的是面向国际用户的 APP，建议至少对接以下几类平台：</p>
<ul>
<li><strong>全球在线支付</strong>：PayPal、Stripe</li>
<li><strong>移动端支付</strong>：Apple Pay、Google Pay</li>
<li><strong>跨境及聚合支付</strong>：Adyen、Worldpay 或其他支持多币种结算的平台</li>
</ul>
<p>这些选择不仅能够满足当前市场需求，还能适应未来支付技术和用户习惯的不断演进。</p>
]]></description>
      <content:encoded><![CDATA[<h1>国外支付平台及国际化 APP 支付对接建议</h1>
<p>在国外（国际市场）的支付领域，众多支付平台因各自独特的优势而被广泛应用。总体来说，国外主流且较为靠谱的支付平台主要可以分为以下几类：</p>
<h2>1. 全球性在线支付平台</h2>
<h3>PayPal</h3>
<ul>
<li><strong>简介</strong>：全球知名的在线支付工具，覆盖 200 多个国家和地区，支持多币种支付，能够接受主要信用卡和借记卡付款。</li>
<li><strong>特点</strong>：以安全、便捷著称，常用于跨境电商和国际交易中。</li>
<li><strong>引用</strong>：​:contentReference[oaicite:0]{index=0} :contentReference[oaicite:1]{index=1}</li>
</ul>
<h3>Stripe</h3>
<ul>
<li><strong>简介</strong>：以强大的 API、开发者友好的工具和广泛的支付方式支持（包括信用卡、数字钱包、Apple Pay、Google Pay 等）而受到全球企业青睐。</li>
<li><strong>特点</strong>：支持多币种结算，适合希望快速构建国际化支付系统的开发者。</li>
<li><strong>引用</strong>：​:contentReference[oaicite:2]{index=2}</li>
</ul>
<h2>2. 移动支付及数字钱包</h2>
<h3>Apple Pay &amp; Google Pay</h3>
<ul>
<li><strong>简介</strong>：这两种移动支付方式在欧美及其他发达市场中使用广泛。</li>
<li><strong>特点</strong>：利用手机或智能手表，通过 NFC 及安全认证，为用户提供便捷、无缝的支付体验。</li>
<li><strong>引用</strong>：​:contentReference[oaicite:3]{index=3}</li>
</ul>
<h2>3. 跨境及聚合支付平台</h2>
<h3>Adyen &amp; Worldpay</h3>
<ul>
<li><strong>简介</strong>：专注于支持跨境电商与全球收单业务的支付平台。</li>
<li><strong>特点</strong>：
<ul>
<li>支持信用卡、借记卡、电子钱包、银行转账等多种支付方式；</li>
<li>提供多币种结算和强大的风险管理工具。</li>
</ul>
</li>
<li><strong>引用</strong>：​:contentReference[oaicite:4]{index=4}</li>
</ul>
<h3>Skrill &amp; Payoneer</h3>
<ul>
<li><strong>简介</strong>：在欧洲及跨境电商领域广受欢迎的平台，适用于小额支付以及自由职业者和跨国企业的资金结算。</li>
<li><strong>引用</strong>：​:contentReference[oaicite:5]{index=5}</li>
</ul>
<h3>美国本土支付网关（如 <a href="http://Authorize.Net">Authorize.Net</a>、2Checkout）</h3>
<ul>
<li><strong>简介</strong>：主要在美国市场使用，适合对接网站或 APP 中的信用卡支付功能。</li>
<li><strong>特点</strong>：流程成熟、稳定性高。</li>
</ul>
<h2>开发国际化 APP 时的对接建议</h2>
<p>从前瞻性的角度看，要打造面向全球用户的 APP，支付接口的多样性和灵活性至关重要。建议如下：</p>
<ol>
<li>
<p><strong>多渠道集成</strong>
根据目标市场用户的支付习惯，至少集成：</p>
<ul>
<li>一种全球性支付平台（例如 PayPal 或 Stripe）；</li>
<li>一种主流移动支付方式（例如 Apple Pay 或 Google Pay）。
这样可以覆盖大部分用户的支付需求，并支持跨境交易。</li>
</ul>
</li>
<li>
<p><strong>安全性与合规性</strong></p>
<ul>
<li>选择支持 PCI-DSS 等国际安全标准的平台，确保用户数据和资金安全。</li>
<li>密切关注各国的法规要求，及时调整接口策略以符合法规。</li>
</ul>
</li>
<li>
<p><strong>多币种及跨境支付支持</strong>
对于跨境交易，建议优先选择支持多币种结算的平台（如 Adyen、Worldpay），以降低汇率风险和手续费成本。</p>
</li>
<li>
<p><strong>开放 API 与聚合支付方案</strong></p>
<ul>
<li>趋势上支付将越来越注重无缝集成和用户体验。</li>
<li>采用聚合支付方案可通过一个统一接口接入多种支付方式，既简化开发流程，又能灵活应对市场变化。</li>
</ul>
</li>
</ol>
<h2>总结</h2>
<p>如果你开发的是面向国际用户的 APP，建议至少对接以下几类平台：</p>
<ul>
<li><strong>全球在线支付</strong>：PayPal、Stripe</li>
<li><strong>移动端支付</strong>：Apple Pay、Google Pay</li>
<li><strong>跨境及聚合支付</strong>：Adyen、Worldpay 或其他支持多币种结算的平台</li>
</ul>
<p>这些选择不仅能够满足当前市场需求，还能适应未来支付技术和用户习惯的不断演进。</p>
]]></content:encoded>
    </item>
    <item>
      <title>Java对接PayPal完整教程：REST API集成与关键注意事项</title>
      <link>https://vansiit.cc/2025/02/08/paypal.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/02/08/paypal.html</guid>
      <pubDate>Sat, 08 Feb 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>Java 对接 PayPal 详细文档</h1>
<p>本文档介绍如何在 Java 项目中对接 PayPal 的 REST API，并重点列出在集成过程中需要特别注意的关键点。</p>
<hr>
<h2>目录</h2>
<ol>
<li><a href="#%E6%A6%82%E8%BF%B0">概述</a></li>
<li><a href="#%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E4%B8%8E%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C">开发环境与准备工作</a></li>
<li><a href="#paypal-rest-api-%E9%9B%86%E6%88%90%E6%AD%A5%E9%AA%A4">PayPal REST API 集成步骤</a></li>
<li><a href="#%E5%85%B3%E9%94%AE%E6%B3%A8%E6%84%8F%E7%82%B9">关键注意点</a></li>
<li><a href="#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E4%B8%8E%E8%B0%83%E8%AF%95">错误处理与调试</a></li>
<li><a href="#%E5%AE%89%E5%85%A8%E6%80%A7%E5%BB%BA%E8%AE%AE">安全性建议</a></li>
<li><a href="#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</a></li>
</ol>
<hr>
<h2>概述</h2>
<p>PayPal 提供了丰富的 REST API 来处理支付、订单创建、支付捕获、退款等操作。通过 Java 调用这些 API，可以实现支付功能的集成。本文档基于 PayPal 的 REST API（目前推荐的集成方式），说明集成流程及注意事项。
<em>（参考：<a href="https://developer.paypal.com">PayPal Developer Documentation</a> :contentReference[oaicite:0]{index=0}）</em></p>
<hr>
<h2>开发环境与准备工作</h2>
<ul>
<li><strong>Java 环境</strong>：确保安装 JDK 8 或更高版本。</li>
<li><strong>依赖管理</strong>：建议使用 Maven 或 Gradle 来管理项目依赖。</li>
<li><strong>PayPal 开发者账户</strong>：前往 <a href="https://developer.paypal.com">PayPal Developer Dashboard</a> 注册并创建应用，以获取 <code>Client ID</code> 和 <code>Secret</code>。</li>
<li><strong>Sandbox 环境</strong>：在开发与测试阶段，请使用 Sandbox 环境。待确认所有功能正常后，再切换到生产环境。</li>
</ul>
<hr>
<h2>PayPal REST API 集成步骤</h2>
<ol>
<li>
<p><strong>获取访问令牌 (Access Token)</strong></p>
<ul>
<li>使用 <code>Client ID</code> 和 <code>Secret</code> 通过 OAuth 2.0 获取访问令牌。</li>
<li>发送 HTTP POST 请求至：
<ul>
<li>测试环境：<code>https://api.sandbox.paypal.com/v1/oauth2/token</code></li>
<li>生产环境：<code>https://api.paypal.com/v1/oauth2/token</code></li>
</ul>
</li>
<li>请求时需要使用 Basic Authentication（编码后的 Client ID 和 Secret）。</li>
</ul>
</li>
<li>
<p><strong>创建支付订单</strong></p>
<ul>
<li>构造订单请求（包括支付意图、金额、币种、回调 URL 等）。</li>
<li>发送 HTTP POST 请求到 <code>/v2/checkout/orders</code> 创建订单。</li>
</ul>
</li>
<li>
<p><strong>支付审批与捕获</strong></p>
<ul>
<li>将 PayPal 返回的批准 URL 提供给客户端，用户在 PayPal 界面完成支付授权。</li>
<li>授权后，调用订单捕获接口 <code>/v2/checkout/orders/{order_id}/capture</code> 完成支付。</li>
</ul>
</li>
<li>
<p><strong>退款操作</strong></p>
<ul>
<li>若需退款，可调用相应的退款 API 处理退款请求。</li>
</ul>
</li>
<li>
<p><strong>Webhook 集成</strong></p>
<ul>
<li>注册 Webhook 以接收 PayPal 的异步通知，确保订单状态及时更新并进行必要处理。</li>
</ul>
</li>
</ol>
<p><em>（参考：<a href="https://developer.paypal.com/docs/api/overview/">PayPal REST API Reference</a> :contentReference[oaicite:1]{index=1}）</em></p>
<hr>
<h2>关键注意点</h2>
<ul>
<li>
<p><strong>环境切换</strong></p>
<ul>
<li>在开发阶段务必使用 Sandbox 环境，生产上线时切换到生产环境 API Endpoint。</li>
</ul>
</li>
<li>
<p><strong>凭证管理</strong></p>
<ul>
<li>不要将 <code>Client ID</code> 和 <code>Secret</code> 硬编码在源代码中。建议使用环境变量或安全配置文件来存储这些敏感信息。</li>
</ul>
</li>
<li>
<p><strong>访问令牌有效期</strong></p>
<ul>
<li>访问令牌通常有一定的有效期（例如 9 小时）。需在令牌过期前自动刷新，确保 API 调用不中断。</li>
</ul>
</li>
<li>
<p><strong>错误处理</strong></p>
<ul>
<li>检查 HTTP 状态码：2xx 状态表示成功，4xx/5xx 状态需要根据返回的错误信息进行处理。</li>
<li>对失败请求详细记录日志，以便调试和错误追踪。</li>
</ul>
</li>
<li>
<p><strong>Webhook 签名验证</strong></p>
<ul>
<li>当接收到 Webhook 通知时，务必验证消息签名，确保通知来源于 PayPal，防止伪造数据。</li>
</ul>
</li>
<li>
<p><strong>安全通信</strong></p>
<ul>
<li>使用 HTTPS 与 PayPal API 通信，确保数据传输过程中的安全性。</li>
</ul>
</li>
<li>
<p><strong>API 版本更新</strong></p>
<ul>
<li>PayPal API 不断更新，开发者应定期关注官方文档和公告，及时调整集成方案。</li>
</ul>
</li>
<li>
<p><strong>事务一致性</strong></p>
<ul>
<li>在订单捕获或退款操作中，确保业务逻辑的事务一致性，防止重复处理或数据不一致。</li>
</ul>
</li>
</ul>
<hr>
<h2>错误处理与调试</h2>
<ul>
<li><strong>调试工具</strong>：利用 PayPal Sandbox 提供的调试工具，查看 API 调用记录与错误详情。</li>
<li><strong>日志记录</strong>：记录所有请求与响应数据（包括失败请求的详细错误信息），便于后续问题排查。</li>
<li><strong>Webhook 调试</strong>：在 Webhook 回调处理时，添加详细的日志记录以监控所有通知处理情况。</li>
</ul>
<hr>
<h2>安全性建议</h2>
<ul>
<li><strong>定期轮换凭证</strong>：定期更新 API 凭证，降低凭证泄露风险。</li>
<li><strong>环境安全</strong>：确保服务器和代码环境安全，避免敏感信息泄露。</li>
<li><strong>使用 OAuth2 认证</strong>：所有 API 调用均应基于 OAuth2 认证机制，确保调用安全。</li>
<li><strong>遵循 PCI-DSS 标准</strong>：避免存储敏感的支付数据，遵守国际安全标准。</li>
</ul>
<hr>
<h2>参考资料</h2>
<ul>
<li><a href="https://developer.paypal.com/">PayPal Developer Documentation</a></li>
<li><a href="https://developer.paypal.com/docs/api/overview/">PayPal REST API Reference</a></li>
<li><a href="https://github.com/paypal/PayPal-Java-SDK">PayPal Java SDK 示例（如有）</a> <em>(注意：部分 SDK 可能已不再维护，建议直接调用 REST API)</em></li>
<li><a href="https://developer.paypal.com/docs/api-basics/notifications/webhooks/">PayPal Webhook Documentation</a></li>
</ul>
<hr>
]]></description>
      <content:encoded><![CDATA[<h1>Java 对接 PayPal 详细文档</h1>
<p>本文档介绍如何在 Java 项目中对接 PayPal 的 REST API，并重点列出在集成过程中需要特别注意的关键点。</p>
<hr>
<h2>目录</h2>
<ol>
<li><a href="#%E6%A6%82%E8%BF%B0">概述</a></li>
<li><a href="#%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E4%B8%8E%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C">开发环境与准备工作</a></li>
<li><a href="#paypal-rest-api-%E9%9B%86%E6%88%90%E6%AD%A5%E9%AA%A4">PayPal REST API 集成步骤</a></li>
<li><a href="#%E5%85%B3%E9%94%AE%E6%B3%A8%E6%84%8F%E7%82%B9">关键注意点</a></li>
<li><a href="#%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E4%B8%8E%E8%B0%83%E8%AF%95">错误处理与调试</a></li>
<li><a href="#%E5%AE%89%E5%85%A8%E6%80%A7%E5%BB%BA%E8%AE%AE">安全性建议</a></li>
<li><a href="#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</a></li>
</ol>
<hr>
<h2>概述</h2>
<p>PayPal 提供了丰富的 REST API 来处理支付、订单创建、支付捕获、退款等操作。通过 Java 调用这些 API，可以实现支付功能的集成。本文档基于 PayPal 的 REST API（目前推荐的集成方式），说明集成流程及注意事项。
<em>（参考：<a href="https://developer.paypal.com">PayPal Developer Documentation</a> :contentReference[oaicite:0]{index=0}）</em></p>
<hr>
<h2>开发环境与准备工作</h2>
<ul>
<li><strong>Java 环境</strong>：确保安装 JDK 8 或更高版本。</li>
<li><strong>依赖管理</strong>：建议使用 Maven 或 Gradle 来管理项目依赖。</li>
<li><strong>PayPal 开发者账户</strong>：前往 <a href="https://developer.paypal.com">PayPal Developer Dashboard</a> 注册并创建应用，以获取 <code>Client ID</code> 和 <code>Secret</code>。</li>
<li><strong>Sandbox 环境</strong>：在开发与测试阶段，请使用 Sandbox 环境。待确认所有功能正常后，再切换到生产环境。</li>
</ul>
<hr>
<h2>PayPal REST API 集成步骤</h2>
<ol>
<li>
<p><strong>获取访问令牌 (Access Token)</strong></p>
<ul>
<li>使用 <code>Client ID</code> 和 <code>Secret</code> 通过 OAuth 2.0 获取访问令牌。</li>
<li>发送 HTTP POST 请求至：
<ul>
<li>测试环境：<code>https://api.sandbox.paypal.com/v1/oauth2/token</code></li>
<li>生产环境：<code>https://api.paypal.com/v1/oauth2/token</code></li>
</ul>
</li>
<li>请求时需要使用 Basic Authentication（编码后的 Client ID 和 Secret）。</li>
</ul>
</li>
<li>
<p><strong>创建支付订单</strong></p>
<ul>
<li>构造订单请求（包括支付意图、金额、币种、回调 URL 等）。</li>
<li>发送 HTTP POST 请求到 <code>/v2/checkout/orders</code> 创建订单。</li>
</ul>
</li>
<li>
<p><strong>支付审批与捕获</strong></p>
<ul>
<li>将 PayPal 返回的批准 URL 提供给客户端，用户在 PayPal 界面完成支付授权。</li>
<li>授权后，调用订单捕获接口 <code>/v2/checkout/orders/{order_id}/capture</code> 完成支付。</li>
</ul>
</li>
<li>
<p><strong>退款操作</strong></p>
<ul>
<li>若需退款，可调用相应的退款 API 处理退款请求。</li>
</ul>
</li>
<li>
<p><strong>Webhook 集成</strong></p>
<ul>
<li>注册 Webhook 以接收 PayPal 的异步通知，确保订单状态及时更新并进行必要处理。</li>
</ul>
</li>
</ol>
<p><em>（参考：<a href="https://developer.paypal.com/docs/api/overview/">PayPal REST API Reference</a> :contentReference[oaicite:1]{index=1}）</em></p>
<hr>
<h2>关键注意点</h2>
<ul>
<li>
<p><strong>环境切换</strong></p>
<ul>
<li>在开发阶段务必使用 Sandbox 环境，生产上线时切换到生产环境 API Endpoint。</li>
</ul>
</li>
<li>
<p><strong>凭证管理</strong></p>
<ul>
<li>不要将 <code>Client ID</code> 和 <code>Secret</code> 硬编码在源代码中。建议使用环境变量或安全配置文件来存储这些敏感信息。</li>
</ul>
</li>
<li>
<p><strong>访问令牌有效期</strong></p>
<ul>
<li>访问令牌通常有一定的有效期（例如 9 小时）。需在令牌过期前自动刷新，确保 API 调用不中断。</li>
</ul>
</li>
<li>
<p><strong>错误处理</strong></p>
<ul>
<li>检查 HTTP 状态码：2xx 状态表示成功，4xx/5xx 状态需要根据返回的错误信息进行处理。</li>
<li>对失败请求详细记录日志，以便调试和错误追踪。</li>
</ul>
</li>
<li>
<p><strong>Webhook 签名验证</strong></p>
<ul>
<li>当接收到 Webhook 通知时，务必验证消息签名，确保通知来源于 PayPal，防止伪造数据。</li>
</ul>
</li>
<li>
<p><strong>安全通信</strong></p>
<ul>
<li>使用 HTTPS 与 PayPal API 通信，确保数据传输过程中的安全性。</li>
</ul>
</li>
<li>
<p><strong>API 版本更新</strong></p>
<ul>
<li>PayPal API 不断更新，开发者应定期关注官方文档和公告，及时调整集成方案。</li>
</ul>
</li>
<li>
<p><strong>事务一致性</strong></p>
<ul>
<li>在订单捕获或退款操作中，确保业务逻辑的事务一致性，防止重复处理或数据不一致。</li>
</ul>
</li>
</ul>
<hr>
<h2>错误处理与调试</h2>
<ul>
<li><strong>调试工具</strong>：利用 PayPal Sandbox 提供的调试工具，查看 API 调用记录与错误详情。</li>
<li><strong>日志记录</strong>：记录所有请求与响应数据（包括失败请求的详细错误信息），便于后续问题排查。</li>
<li><strong>Webhook 调试</strong>：在 Webhook 回调处理时，添加详细的日志记录以监控所有通知处理情况。</li>
</ul>
<hr>
<h2>安全性建议</h2>
<ul>
<li><strong>定期轮换凭证</strong>：定期更新 API 凭证，降低凭证泄露风险。</li>
<li><strong>环境安全</strong>：确保服务器和代码环境安全，避免敏感信息泄露。</li>
<li><strong>使用 OAuth2 认证</strong>：所有 API 调用均应基于 OAuth2 认证机制，确保调用安全。</li>
<li><strong>遵循 PCI-DSS 标准</strong>：避免存储敏感的支付数据，遵守国际安全标准。</li>
</ul>
<hr>
<h2>参考资料</h2>
<ul>
<li><a href="https://developer.paypal.com/">PayPal Developer Documentation</a></li>
<li><a href="https://developer.paypal.com/docs/api/overview/">PayPal REST API Reference</a></li>
<li><a href="https://github.com/paypal/PayPal-Java-SDK">PayPal Java SDK 示例（如有）</a> <em>(注意：部分 SDK 可能已不再维护，建议直接调用 REST API)</em></li>
<li><a href="https://developer.paypal.com/docs/api-basics/notifications/webhooks/">PayPal Webhook Documentation</a></li>
</ul>
<hr>
]]></content:encoded>
    </item>
    <item>
      <title>Java21虚拟线程详解：与Go协程性能对比分析</title>
      <link>https://vansiit.cc/2025/02/08/virtual-thread.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/02/08/virtual-thread.html</guid>
      <pubDate>Sat, 08 Feb 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>Java21 虚拟线程初体验</h1>
<p>Java虚拟线程（Virtual Threads） 是 Java21 新增的特性，它允许在 Java 中创建一个轻量级的线程，它与普通线程相比，具有一些优势，如：</p>
<ul>
<li>轻量级：虚拟线程不需要像普通线程一样分配栈空间，因此可以创建大量的虚拟线程。</li>
<li>响应式：虚拟线程可以在等待 IO 操作时让出 CPU，从而提高程序的响应性。</li>
<li>可伸缩：虚拟线程可以根据 CPU 的可用数量来调整线程的数量，从而提高程序的可伸缩性。</li>
</ul>
<h2>虚拟线程的使用</h2>
<p>虚拟线程的使用非常简单，只需要使用 <code>Thread.startVirtualThread()</code> 方法即可创建一个虚拟线程。例如：</p>
<pre><code class="language-java">Thread.startVirtualThread(() -&gt; {
    // do something
})
</code></pre>
<p>或者使用 <code>Executor</code> 来管理虚拟线程, 例如：</p>
<pre><code class="language-java">Executor executor = Executors.newVirtualThreadExecutor();
executor.execute(() -&gt; {
    // 你的代码逻辑
    System.out.println(&quot;Hello from a virtual thread using executor!&quot;);
});
</code></pre>
<h2>Virtual Threads 的核心原理</h2>
<p>虚拟线程的实现依赖于协程技术。与传统线程不同，虚拟线程不直接绑定到操作系统内核线程，而是由 JVM 内部的调度器管理。当一个虚拟线程阻塞时（例如等待 I/O 操作），它会自动挂起，并释放底层资源以供其他线程使用。</p>
<p>这种机制使得虚拟线程能够以更低的成本实现大规模并发，而不会导致线程阻塞导致的资源浪费。</p>
<h2>虚拟线程的缺点</h2>
<ul>
<li>虚拟线程的创建和销毁需要额外的开销，因此在创建大量虚拟线程时，性能可能会受到影响。</li>
<li>虚拟线程的响应性可能会受到底层操作系统的限制，因此可能会有延迟。</li>
<li>虚拟线程的调度和调度策略需要 JVM 内部实现，因此可能会有性能问题。</li>
</ul>
<h2>虚拟线程的适用场景</h2>
<ul>
<li>响应式应用程序：当应用程序需要处理大量的请求时，虚拟线程可以提高应用程序的性能和响应性。</li>
<li>大规模并发应用程序：当应用程序需要处理大量的并发请求时，虚拟线程可以提高应用程序的性能和吞吐量。</li>
</ul>
<h2>虚拟线程和Golang协程的对比</h2>
<p>Java 虚拟线程（Virtual Threads）和 Go 协程（Goroutines）都是现代编程语言中用于实现高并发、轻量级线程的技术。尽管它们的目标相似，但在实现、使用方式和性能上存在一些差异。以下是它们的对比：</p>
<table>
<thead>
<tr>
<th style="text-align:left">特性</th>
<th style="text-align:left">Java 虚拟线程</th>
<th style="text-align:left">Go 协程</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">实现方式</td>
<td style="text-align:left">JVM 管理的轻量级线程</td>
<td style="text-align:left">Go 运行时管理的轻量级线程</td>
</tr>
<tr>
<td style="text-align:left">资源开销</td>
<td style="text-align:left">较低，动态分配栈</td>
<td style="text-align:left">极低，初始栈 2KB</td>
</tr>
<tr>
<td style="text-align:left">调度方式</td>
<td style="text-align:left">协作式</td>
<td style="text-align:left">抢占式</td>
</tr>
<tr>
<td style="text-align:left">阻塞处理</td>
<td style="text-align:left">自动挂起</td>
<td style="text-align:left">自动挂起</td>
</tr>
<tr>
<td style="text-align:left">编程模型</td>
<td style="text-align:left">同步风格</td>
<td style="text-align:left">CSP 模型，基于 channel</td>
</tr>
<tr>
<td style="text-align:left">性能</td>
<td style="text-align:left">高，适合高并发</td>
<td style="text-align:left">极高，适合高并发</td>
</tr>
<tr>
<td style="text-align:left">生态系统</td>
<td style="text-align:left">需要适配</td>
<td style="text-align:left">天然支持</td>
</tr>
<tr>
<td style="text-align:left">适用场景</td>
<td style="text-align:left">I/O 密集型，现有 Java 应用</td>
<td style="text-align:left">高并发，微服务，网络编程</td>
</tr>
</tbody>
</table>
]]></description>
      <content:encoded><![CDATA[<h1>Java21 虚拟线程初体验</h1>
<p>Java虚拟线程（Virtual Threads） 是 Java21 新增的特性，它允许在 Java 中创建一个轻量级的线程，它与普通线程相比，具有一些优势，如：</p>
<ul>
<li>轻量级：虚拟线程不需要像普通线程一样分配栈空间，因此可以创建大量的虚拟线程。</li>
<li>响应式：虚拟线程可以在等待 IO 操作时让出 CPU，从而提高程序的响应性。</li>
<li>可伸缩：虚拟线程可以根据 CPU 的可用数量来调整线程的数量，从而提高程序的可伸缩性。</li>
</ul>
<h2>虚拟线程的使用</h2>
<p>虚拟线程的使用非常简单，只需要使用 <code>Thread.startVirtualThread()</code> 方法即可创建一个虚拟线程。例如：</p>
<pre><code class="language-java">Thread.startVirtualThread(() -&gt; {
    // do something
})
</code></pre>
<p>或者使用 <code>Executor</code> 来管理虚拟线程, 例如：</p>
<pre><code class="language-java">Executor executor = Executors.newVirtualThreadExecutor();
executor.execute(() -&gt; {
    // 你的代码逻辑
    System.out.println(&quot;Hello from a virtual thread using executor!&quot;);
});
</code></pre>
<h2>Virtual Threads 的核心原理</h2>
<p>虚拟线程的实现依赖于协程技术。与传统线程不同，虚拟线程不直接绑定到操作系统内核线程，而是由 JVM 内部的调度器管理。当一个虚拟线程阻塞时（例如等待 I/O 操作），它会自动挂起，并释放底层资源以供其他线程使用。</p>
<p>这种机制使得虚拟线程能够以更低的成本实现大规模并发，而不会导致线程阻塞导致的资源浪费。</p>
<h2>虚拟线程的缺点</h2>
<ul>
<li>虚拟线程的创建和销毁需要额外的开销，因此在创建大量虚拟线程时，性能可能会受到影响。</li>
<li>虚拟线程的响应性可能会受到底层操作系统的限制，因此可能会有延迟。</li>
<li>虚拟线程的调度和调度策略需要 JVM 内部实现，因此可能会有性能问题。</li>
</ul>
<h2>虚拟线程的适用场景</h2>
<ul>
<li>响应式应用程序：当应用程序需要处理大量的请求时，虚拟线程可以提高应用程序的性能和响应性。</li>
<li>大规模并发应用程序：当应用程序需要处理大量的并发请求时，虚拟线程可以提高应用程序的性能和吞吐量。</li>
</ul>
<h2>虚拟线程和Golang协程的对比</h2>
<p>Java 虚拟线程（Virtual Threads）和 Go 协程（Goroutines）都是现代编程语言中用于实现高并发、轻量级线程的技术。尽管它们的目标相似，但在实现、使用方式和性能上存在一些差异。以下是它们的对比：</p>
<table>
<thead>
<tr>
<th style="text-align:left">特性</th>
<th style="text-align:left">Java 虚拟线程</th>
<th style="text-align:left">Go 协程</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">实现方式</td>
<td style="text-align:left">JVM 管理的轻量级线程</td>
<td style="text-align:left">Go 运行时管理的轻量级线程</td>
</tr>
<tr>
<td style="text-align:left">资源开销</td>
<td style="text-align:left">较低，动态分配栈</td>
<td style="text-align:left">极低，初始栈 2KB</td>
</tr>
<tr>
<td style="text-align:left">调度方式</td>
<td style="text-align:left">协作式</td>
<td style="text-align:left">抢占式</td>
</tr>
<tr>
<td style="text-align:left">阻塞处理</td>
<td style="text-align:left">自动挂起</td>
<td style="text-align:left">自动挂起</td>
</tr>
<tr>
<td style="text-align:left">编程模型</td>
<td style="text-align:left">同步风格</td>
<td style="text-align:left">CSP 模型，基于 channel</td>
</tr>
<tr>
<td style="text-align:left">性能</td>
<td style="text-align:left">高，适合高并发</td>
<td style="text-align:left">极高，适合高并发</td>
</tr>
<tr>
<td style="text-align:left">生态系统</td>
<td style="text-align:left">需要适配</td>
<td style="text-align:left">天然支持</td>
</tr>
<tr>
<td style="text-align:left">适用场景</td>
<td style="text-align:left">I/O 密集型，现有 Java 应用</td>
<td style="text-align:left">高并发，微服务，网络编程</td>
</tr>
</tbody>
</table>
]]></content:encoded>
    </item>
    <item>
      <title>shanliren</title>
      <link>https://vansiit.cc/2025/01/07/shanliren.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/01/07/shanliren.html</guid>
      <pubDate>Tue, 07 Jan 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>山里娃</h1>
<p>小时候课文里说，“山的那边是什么，山的那边是海”。作为真正的山里娃，不知道海是什么，山的那边明明还是山。</p>
<p>我的家乡是鄂西北荆山山脉的一个小镇，我出生在我奶奶家。</p>
<p>奶奶家在半山腰，离山脚的垂直距离百米左右。山脚两边是水田，中间是一条小河，水深只没过脚背，河道拐弯处水深也不到一米。</p>
<p>我们几个孩子，每逢放暑假，经常从小路跑到山脚玩水。河水清澈见底，河床上的鹅卵石鳞次栉比，水里没什么鱼，螃蟹倒是不少。</p>
<p>抓鱼要先赶鱼，几人分工配合，各占一角，慢慢把鱼驱赶到水浅的岸边。孙子兵法云：围三阙一。小孩子懂什么兵法，都是经验。
盯紧那些大鱼藏匿的石头，悄悄摸过去。双手合捧，尽量分散更大面积，手背贴紧河床，动静要小，速度要快，两手往上一兜，把鱼按在石头上，此时最是要紧，一不小心就会让鱼再次溜走。</p>
<p>抓螃蟹就简单多了，诀窍就是从后面按住背和两个大钳子。</p>
<p>我问过奶奶，门前山的那边是什么</p>
<p>等我真正知道山的那边是什么的时候，要到19岁那年离开家乡去省城上大学。</p>
]]></description>
      <content:encoded><![CDATA[<h1>山里娃</h1>
<p>小时候课文里说，“山的那边是什么，山的那边是海”。作为真正的山里娃，不知道海是什么，山的那边明明还是山。</p>
<p>我的家乡是鄂西北荆山山脉的一个小镇，我出生在我奶奶家。</p>
<p>奶奶家在半山腰，离山脚的垂直距离百米左右。山脚两边是水田，中间是一条小河，水深只没过脚背，河道拐弯处水深也不到一米。</p>
<p>我们几个孩子，每逢放暑假，经常从小路跑到山脚玩水。河水清澈见底，河床上的鹅卵石鳞次栉比，水里没什么鱼，螃蟹倒是不少。</p>
<p>抓鱼要先赶鱼，几人分工配合，各占一角，慢慢把鱼驱赶到水浅的岸边。孙子兵法云：围三阙一。小孩子懂什么兵法，都是经验。
盯紧那些大鱼藏匿的石头，悄悄摸过去。双手合捧，尽量分散更大面积，手背贴紧河床，动静要小，速度要快，两手往上一兜，把鱼按在石头上，此时最是要紧，一不小心就会让鱼再次溜走。</p>
<p>抓螃蟹就简单多了，诀窍就是从后面按住背和两个大钳子。</p>
<p>我问过奶奶，门前山的那边是什么</p>
<p>等我真正知道山的那边是什么的时候，要到19岁那年离开家乡去省城上大学。</p>
]]></content:encoded>
    </item>
    <item>
      <title>shudong</title>
      <link>https://vansiit.cc/2025/01/06/shudong.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/01/06/shudong.html</guid>
      <pubDate>Mon, 06 Jan 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>突然想起来初中同桌的一些事</h1>
<p>起因是去年堂妹突然微信上问我，认识一个叫SHH的吗？我大惊，SHH 是我初中同桌。</p>
<p>我小叔二婚再娶过，所以我堂妹跟我差了将近二十岁。
堂妹有个苏老师，是我和 SHH 当年的老师，堂妹知道苏老师也教过我。去年的某次课堂上，苏老师说 SHH 老师也是她的学生。由此堂妹就来问我认不认识 SHH 。我这个堂妹，学习成绩一塌糊涂，脑袋倒是挺好使。</p>
<p>堂妹现年上初三，跟我们当年差不多年纪。那时候 SHH 很聪明，是我们那一届唯一一个考入县一中的女生。落后地区的乡镇初中，县一中的录取率是相当低的。</p>
<p>坐同桌的时候，SHH 经常给我看她写的一些东西，一些情爱小说之类的，内容已经完全忘记了，只依稀记得字写的眉飞色舞。上课，写作业也都不太认真，仗着脑筋聪明成绩依然很是拔尖。</p>
<p>那是零六年，北京奥运会还要到我上高二的时候。SHH家境很好，父亲好像是我们镇上的镇长。所以她总是穿着鲜亮的衣服，梳着整洁的发型。</p>
<p>也买得起单放机，偶尔也会塞给我一只耳机，记得应该是 SHE 、周杰伦、蔡依林之类的流行歌曲。</p>
<p>SHH 也确实很漂亮，真不是有什么滤镜，长相嘛，很像以前一部电视剧《宝莲灯》里面的三圣母。</p>
<p>那时候少年的我太自卑了，矮、黑、瘦，衣服破烂不堪。除了学习还行。</p>
<p>我们学校在一座小山上，位置很高。
有一回放学，我和同村的发小从学校小门一起回家。意外撞见 SHH 和新转学来的混子在角落里 kiss ，还要过来威胁我们不许说出去。</p>
<p>因为我父母常年在外打工，作为留守儿童的我，每月要去大伯那里拿我爸寄回来的生活费，大伯也在镇里面工作，所有也能经常和 SHH 碰到，和她也算熟络。
自从那次撞破 kiss 事件发生以后，我能感觉到，她有意避着我。包括去年加上微信，也是寥寥数语，避口不提从前。</p>
<p>后来上高中了不在同班基本没说过话，有几次交集是坐班车从县城回镇上。
再后来我上大学，沪漂，结婚，一晃二十年。
期间，只是听说她上了本市的一所二本大学，从 qq 上知道她结婚了。</p>
<p>关于混混这人，也不那种坏孩子。因为不明原因，从县城转学到我们学校是因为学校的某个老师是他亲戚，能管着他。后面在学校里，有一次其他班的混子找我麻烦，他也帮我解过围。
他学习也不算差，成绩中上的那种，初中毕业后通过择校生去到我们县一中读书。</p>
<p>跟堂妹打听了一下她的近况，有个女儿，好像都上小学了，就在我们镇完小读书，来我们乡镇中学不到几年。是初中物理代课老师，能搜到某市级教师竞赛公示名单。</p>
<p>微信朋友圈完全白板，签名是：冷暖自知，悲喜自渡。搜了一下最近的学校新闻，看到了几张照片，应该是她。
找堂妹要了一下她的微信（现在的孩子真厉害，转了几个人，就要到了老师的微信），加上简单寒暄了几句。</p>
<p>以她的家境和学历，也不至于要来我们乡镇教初中，我一个上师专的发小前两年都从镇初中调到县里面的实验中学了。猜测可能她父亲出问题了，当然我也没打听。</p>
<p>前几天还感慨，村里的小孩我都不认识了。“儿童相见不相识，笑问客从何处来”。 读书声犹如在耳，二十年眨眼之间。</p>
<p>可以预料到这辈子要回到老家，也只等到退休了。</p>
<p>确实是因为堂妹突然来问我这个契机，才回忆起这么多。人生的很多事，不多想几遍可能就真的忘却了。
人是很健忘的。就像我的父亲，喝点酒只会重复讲那几件旧事，我都听了不下百遍。</p>
<p>说来也是唏嘘不已。有次，初中的汪老师让我们写自己长大的梦想，有些人写科学家，有人写宇航员，有人写作家… 我写的是数学家。那时候我们的梦想多么的纯粹和远大。</p>
<p>我们都以为离开小镇就能飞得更高，飞得更远。其实生活中，最弥足珍贵的可能就是那些最简单的时光。这就是人生吧！我们带着各自的伤痕和倔强，最终都会找到属于自己的归宿。</p>
]]></description>
      <content:encoded><![CDATA[<h1>突然想起来初中同桌的一些事</h1>
<p>起因是去年堂妹突然微信上问我，认识一个叫SHH的吗？我大惊，SHH 是我初中同桌。</p>
<p>我小叔二婚再娶过，所以我堂妹跟我差了将近二十岁。
堂妹有个苏老师，是我和 SHH 当年的老师，堂妹知道苏老师也教过我。去年的某次课堂上，苏老师说 SHH 老师也是她的学生。由此堂妹就来问我认不认识 SHH 。我这个堂妹，学习成绩一塌糊涂，脑袋倒是挺好使。</p>
<p>堂妹现年上初三，跟我们当年差不多年纪。那时候 SHH 很聪明，是我们那一届唯一一个考入县一中的女生。落后地区的乡镇初中，县一中的录取率是相当低的。</p>
<p>坐同桌的时候，SHH 经常给我看她写的一些东西，一些情爱小说之类的，内容已经完全忘记了，只依稀记得字写的眉飞色舞。上课，写作业也都不太认真，仗着脑筋聪明成绩依然很是拔尖。</p>
<p>那是零六年，北京奥运会还要到我上高二的时候。SHH家境很好，父亲好像是我们镇上的镇长。所以她总是穿着鲜亮的衣服，梳着整洁的发型。</p>
<p>也买得起单放机，偶尔也会塞给我一只耳机，记得应该是 SHE 、周杰伦、蔡依林之类的流行歌曲。</p>
<p>SHH 也确实很漂亮，真不是有什么滤镜，长相嘛，很像以前一部电视剧《宝莲灯》里面的三圣母。</p>
<p>那时候少年的我太自卑了，矮、黑、瘦，衣服破烂不堪。除了学习还行。</p>
<p>我们学校在一座小山上，位置很高。
有一回放学，我和同村的发小从学校小门一起回家。意外撞见 SHH 和新转学来的混子在角落里 kiss ，还要过来威胁我们不许说出去。</p>
<p>因为我父母常年在外打工，作为留守儿童的我，每月要去大伯那里拿我爸寄回来的生活费，大伯也在镇里面工作，所有也能经常和 SHH 碰到，和她也算熟络。
自从那次撞破 kiss 事件发生以后，我能感觉到，她有意避着我。包括去年加上微信，也是寥寥数语，避口不提从前。</p>
<p>后来上高中了不在同班基本没说过话，有几次交集是坐班车从县城回镇上。
再后来我上大学，沪漂，结婚，一晃二十年。
期间，只是听说她上了本市的一所二本大学，从 qq 上知道她结婚了。</p>
<p>关于混混这人，也不那种坏孩子。因为不明原因，从县城转学到我们学校是因为学校的某个老师是他亲戚，能管着他。后面在学校里，有一次其他班的混子找我麻烦，他也帮我解过围。
他学习也不算差，成绩中上的那种，初中毕业后通过择校生去到我们县一中读书。</p>
<p>跟堂妹打听了一下她的近况，有个女儿，好像都上小学了，就在我们镇完小读书，来我们乡镇中学不到几年。是初中物理代课老师，能搜到某市级教师竞赛公示名单。</p>
<p>微信朋友圈完全白板，签名是：冷暖自知，悲喜自渡。搜了一下最近的学校新闻，看到了几张照片，应该是她。
找堂妹要了一下她的微信（现在的孩子真厉害，转了几个人，就要到了老师的微信），加上简单寒暄了几句。</p>
<p>以她的家境和学历，也不至于要来我们乡镇教初中，我一个上师专的发小前两年都从镇初中调到县里面的实验中学了。猜测可能她父亲出问题了，当然我也没打听。</p>
<p>前几天还感慨，村里的小孩我都不认识了。“儿童相见不相识，笑问客从何处来”。 读书声犹如在耳，二十年眨眼之间。</p>
<p>可以预料到这辈子要回到老家，也只等到退休了。</p>
<p>确实是因为堂妹突然来问我这个契机，才回忆起这么多。人生的很多事，不多想几遍可能就真的忘却了。
人是很健忘的。就像我的父亲，喝点酒只会重复讲那几件旧事，我都听了不下百遍。</p>
<p>说来也是唏嘘不已。有次，初中的汪老师让我们写自己长大的梦想，有些人写科学家，有人写宇航员，有人写作家… 我写的是数学家。那时候我们的梦想多么的纯粹和远大。</p>
<p>我们都以为离开小镇就能飞得更高，飞得更远。其实生活中，最弥足珍贵的可能就是那些最简单的时光。这就是人生吧！我们带着各自的伤痕和倔强，最终都会找到属于自己的归宿。</p>
]]></content:encoded>
    </item>
    <item>
      <title>春节真的是中国传统节日吗？探寻春节的历史真相</title>
      <link>https://vansiit.cc/2025/01/05/chunjie.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2025/01/05/chunjie.html</guid>
      <pubDate>Sun, 05 Jan 2025 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>春节真的是中国的传统节日吗？</h1>
<p>经常过春节的朋友都知道，春节是中国的传统节日，叮叮叮，真的吗？</p>
<p>实际上，“春节”这一名称最早出现在民国三年，即1914年，由北洋政府总统袁世凯发布并延续至今的。</p>
<p>春节是中国的传统节日也不假，最早能追溯到殷商时期的岁末年头祭神、祭祖活动（腊祭）等。旧称新春、正旦、正朔，是以农历计算的中国传统新年，其庆祝活动又俗称过年、度岁等，是汉族四大传统节日之一。
从明代开始，华夏新年节庆一般要到正月十五日元宵节之后才正式结束活动，有些地方的新年庆祝活动甚至到整个正月完结为止。</p>
<p>1914年，北洋政府内务部民治司第一科，向总统袁世凯请求，定新春（春节）、端午节（夏节）、中秋节（秋节）、冬至（冬节）为四节。袁世凯批曰：“据呈已悉，应即照准，此批”。而后，内务部将此令颁行全国。</p>
<p>当然在动乱的民国期间，春节也曾经被蒋介石废除过。</p>
<p>1928年5月7日，中华民国内政部呈国民政府，要求“实行废除旧历，普用国历”。</p>
<p>1930年，国民政府重申：“移置废历新年休假日期及各种礼仪点缀娱乐”，“废历新年不许放假，亦不得假藉其他名义放假”，“贺年、团拜、祀祖、春宴、观灯、扎彩、贴春联等一律移置国历新年前后举行”。</p>
<p>可是，废止绵延了几千年的传统节日是站在了人民的对立面，命令出台后即遭到了其他党派及社会团体的反对，指责国府摒弃中国传统文化。民间更是阳也不奉，阴则全违，民间庆贺春节一切如故，一两年后，政府亦不再禁止民众庆祝旧历新年。</p>
<p>1934年，国民政府停止了强制废除华夏历，要求“对于旧历年关，除公务机关，民间习俗不宜过于干涉”。</p>
<p>时至今日，无论春节的称谓如何流转，春节已经成为连接传统现在和未来最好的文化纽带和传承符号，也是中国人民和世界华人华侨每年必过的节日。</p>
<p>春节作为一个总结与展望的时间节点，提供了反思过去、规划未来的机会。辞旧迎新的仪式感有助于人们调整心态，以更加积极的状态迎接新的挑战。</p>
<p>马上又要过年了，提前祝大家新年快乐，万事如意！</p>
]]></description>
      <content:encoded><![CDATA[<h1>春节真的是中国的传统节日吗？</h1>
<p>经常过春节的朋友都知道，春节是中国的传统节日，叮叮叮，真的吗？</p>
<p>实际上，“春节”这一名称最早出现在民国三年，即1914年，由北洋政府总统袁世凯发布并延续至今的。</p>
<p>春节是中国的传统节日也不假，最早能追溯到殷商时期的岁末年头祭神、祭祖活动（腊祭）等。旧称新春、正旦、正朔，是以农历计算的中国传统新年，其庆祝活动又俗称过年、度岁等，是汉族四大传统节日之一。
从明代开始，华夏新年节庆一般要到正月十五日元宵节之后才正式结束活动，有些地方的新年庆祝活动甚至到整个正月完结为止。</p>
<p>1914年，北洋政府内务部民治司第一科，向总统袁世凯请求，定新春（春节）、端午节（夏节）、中秋节（秋节）、冬至（冬节）为四节。袁世凯批曰：“据呈已悉，应即照准，此批”。而后，内务部将此令颁行全国。</p>
<p>当然在动乱的民国期间，春节也曾经被蒋介石废除过。</p>
<p>1928年5月7日，中华民国内政部呈国民政府，要求“实行废除旧历，普用国历”。</p>
<p>1930年，国民政府重申：“移置废历新年休假日期及各种礼仪点缀娱乐”，“废历新年不许放假，亦不得假藉其他名义放假”，“贺年、团拜、祀祖、春宴、观灯、扎彩、贴春联等一律移置国历新年前后举行”。</p>
<p>可是，废止绵延了几千年的传统节日是站在了人民的对立面，命令出台后即遭到了其他党派及社会团体的反对，指责国府摒弃中国传统文化。民间更是阳也不奉，阴则全违，民间庆贺春节一切如故，一两年后，政府亦不再禁止民众庆祝旧历新年。</p>
<p>1934年，国民政府停止了强制废除华夏历，要求“对于旧历年关，除公务机关，民间习俗不宜过于干涉”。</p>
<p>时至今日，无论春节的称谓如何流转，春节已经成为连接传统现在和未来最好的文化纽带和传承符号，也是中国人民和世界华人华侨每年必过的节日。</p>
<p>春节作为一个总结与展望的时间节点，提供了反思过去、规划未来的机会。辞旧迎新的仪式感有助于人们调整心态，以更加积极的状态迎接新的挑战。</p>
<p>马上又要过年了，提前祝大家新年快乐，万事如意！</p>
]]></content:encoded>
    </item>
    <item>
      <title>供奉释迦牟尼、李白、耶稣、姜子牙的高台教到底是什么</title>
      <link>https://vansiit.cc/2024/09/21/Caodaism.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2024/09/21/Caodaism.html</guid>
      <pubDate>Sat, 21 Sep 2024 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>供奉释迦牟尼、李白、耶稣、姜子牙的高台教到底是什么</h1>
<h2>引言</h2>
<p>高台教（Cao Dai）是20世纪起源于越南的一种新兴宗教，它与和好教（Đạo Hòa Hảo）是越南特有的两种宗教之一，也是这个国家的第三大宗教，仅次于佛教和天主教。</p>
<p>高台教主张“万教大同”，诸神共处。将儒释道、基督教、伊斯兰教等全部杂糅，包括“三教五道”，即佛教、圣教、仙教和儒道、伊道、圣道、仙道、佛道。各路神仙都是供奉对象，例如：释迦摩尼、老子、孔子、观世音、耶稣、默罕默德，还有李白、关公、牛顿、莎士比亚、丘吉尔、孙中山、维克多·雨果、阮秉谦等。</p>
<p><img src="https://vansiit.cc/img/caodaism/Cao_Dai_Temple_Vietnam.jpg" alt="img.png"></p>
<blockquote>
<p>中间竖排从上到下依次为释迦牟尼、李白、耶稣、姜子牙，释迦牟尼佛右手边是老子，左手边是孔子，老子右手边是观音，孔子左手边是关羽，图：Wikipedia</p>
</blockquote>
<p>不过，在这一切之上，高台教承认有一位宇宙的最高“主宰者”——高台神，高台教也因此而得名。(“高台”一词出自“道德经”第二十章，“众人熙熙，如享太牢，如春登台”。高台教徒解释“如春登台”为“上祷高台”，高台就是神灵居住的最高的宫殿的意思。</p>
<h2>总部</h2>
<p>高台教的总部是西宁圣座，位于越南南部的西宁省（Tây Ninh），也称西宁教廷，距离胡志明市大约两小时车程。寺庙外观有希腊神庙的大柱长厅，有哥特教堂的尖塔，有中国佛教的山门楼阁，有伊斯兰教的圆顶拱廊。</p>
<p><img src="https://vansiit.cc/img/caodaism/CaoDaiMain.jpg" alt="img.png"></p>
<blockquote>
<p>图：Wikipedia</p>
</blockquote>
<p>高台教圣殿内的高处有个“天眼”，表示人间的任何事情都逃不脱高台神眼的审察
<img src="https://vansiit.cc/img/caodaism/1634527046189697.jpg" alt="img.png"></p>
<blockquote>
<p>图：Wikipedia</p>
</blockquote>
<h2>创立</h2>
<p>在1925年12月25日圣诞节那天，在越南南部的西宁（西贡西北100千米），在法属印度支那殖民地政府内任职的两名公务员——吴文昭（Ngô Văn Chiêu，1878年生于西贡）和黎文忠（Lê văn Trung，1876年— 1934年 12月19日），自称得到至尊无上神“高台”的启示，创立了这种越南本土宗教。1926年9月7日，这个新兴宗教得到了殖民地当局的批准。</p>
<p><img src="https://vansiit.cc/img/caodaism/1634527141943115.png" alt="img.png"></p>
<blockquote>
<p>左为创始人之一黎文忠，右为高台教教主范公稷（Phạm Công Tắc)。图：<a href="http://luatkhoa.org">luatkhoa.org</a></p>
</blockquote>
<h2>现状</h2>
<p>高台教信徒约有440万人，占越南人口6%。 高台教在越南有13所寺庙，其中主寺位于西宁圣座，有19个供奉神，有19个供奉神。 高台教在越南有3个中央教团，有3个中央教团。</p>
<p>总结一下，高台教是一个独特的越南本土宗教，各种宗教大杂糅，从教义、建筑、服饰上都能体现出了，特别是东亚文化圈的人能明细感受到那种似曾相识的感觉。但是高台教又发扬出自己的特色，参与政治，曾经有自己的军队。</p>
]]></description>
      <content:encoded><![CDATA[<h1>供奉释迦牟尼、李白、耶稣、姜子牙的高台教到底是什么</h1>
<h2>引言</h2>
<p>高台教（Cao Dai）是20世纪起源于越南的一种新兴宗教，它与和好教（Đạo Hòa Hảo）是越南特有的两种宗教之一，也是这个国家的第三大宗教，仅次于佛教和天主教。</p>
<p>高台教主张“万教大同”，诸神共处。将儒释道、基督教、伊斯兰教等全部杂糅，包括“三教五道”，即佛教、圣教、仙教和儒道、伊道、圣道、仙道、佛道。各路神仙都是供奉对象，例如：释迦摩尼、老子、孔子、观世音、耶稣、默罕默德，还有李白、关公、牛顿、莎士比亚、丘吉尔、孙中山、维克多·雨果、阮秉谦等。</p>
<p><img src="https://vansiit.cc/img/caodaism/Cao_Dai_Temple_Vietnam.jpg" alt="img.png"></p>
<blockquote>
<p>中间竖排从上到下依次为释迦牟尼、李白、耶稣、姜子牙，释迦牟尼佛右手边是老子，左手边是孔子，老子右手边是观音，孔子左手边是关羽，图：Wikipedia</p>
</blockquote>
<p>不过，在这一切之上，高台教承认有一位宇宙的最高“主宰者”——高台神，高台教也因此而得名。(“高台”一词出自“道德经”第二十章，“众人熙熙，如享太牢，如春登台”。高台教徒解释“如春登台”为“上祷高台”，高台就是神灵居住的最高的宫殿的意思。</p>
<h2>总部</h2>
<p>高台教的总部是西宁圣座，位于越南南部的西宁省（Tây Ninh），也称西宁教廷，距离胡志明市大约两小时车程。寺庙外观有希腊神庙的大柱长厅，有哥特教堂的尖塔，有中国佛教的山门楼阁，有伊斯兰教的圆顶拱廊。</p>
<p><img src="https://vansiit.cc/img/caodaism/CaoDaiMain.jpg" alt="img.png"></p>
<blockquote>
<p>图：Wikipedia</p>
</blockquote>
<p>高台教圣殿内的高处有个“天眼”，表示人间的任何事情都逃不脱高台神眼的审察
<img src="https://vansiit.cc/img/caodaism/1634527046189697.jpg" alt="img.png"></p>
<blockquote>
<p>图：Wikipedia</p>
</blockquote>
<h2>创立</h2>
<p>在1925年12月25日圣诞节那天，在越南南部的西宁（西贡西北100千米），在法属印度支那殖民地政府内任职的两名公务员——吴文昭（Ngô Văn Chiêu，1878年生于西贡）和黎文忠（Lê văn Trung，1876年— 1934年 12月19日），自称得到至尊无上神“高台”的启示，创立了这种越南本土宗教。1926年9月7日，这个新兴宗教得到了殖民地当局的批准。</p>
<p><img src="https://vansiit.cc/img/caodaism/1634527141943115.png" alt="img.png"></p>
<blockquote>
<p>左为创始人之一黎文忠，右为高台教教主范公稷（Phạm Công Tắc)。图：<a href="http://luatkhoa.org">luatkhoa.org</a></p>
</blockquote>
<h2>现状</h2>
<p>高台教信徒约有440万人，占越南人口6%。 高台教在越南有13所寺庙，其中主寺位于西宁圣座，有19个供奉神，有19个供奉神。 高台教在越南有3个中央教团，有3个中央教团。</p>
<p>总结一下，高台教是一个独特的越南本土宗教，各种宗教大杂糅，从教义、建筑、服饰上都能体现出了，特别是东亚文化圈的人能明细感受到那种似曾相识的感觉。但是高台教又发扬出自己的特色，参与政治，曾经有自己的军队。</p>
]]></content:encoded>
    </item>
    <item>
      <title>高台教</title>
      <link>https://vansiit.cc/2024/09/21/middle-east.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2024/09/21/middle-east.html</guid>
      <pubDate>Sat, 21 Sep 2024 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>中东局势分析</h1>
]]></description>
      <content:encoded><![CDATA[<h1>中东局势分析</h1>
]]></content:encoded>
    </item>
    <item>
      <title>人工智能发展史：从历史到现状再到未来趋势</title>
      <link>https://vansiit.cc/2024/04/09/what-is-ai.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2024/04/09/what-is-ai.html</guid>
      <pubDate>Tue, 09 Apr 2024 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>人工智能发展史：从历史到现状再到未来趋势</h1>
<h2>参考资料</h2>
<p><a href="https://www.cas.cn/zjs/201902/t20190218_4679625.shtml">谭铁牛：人工智能的历史、现状和未来</a></p>
]]></description>
      <content:encoded><![CDATA[<h1>人工智能发展史：从历史到现状再到未来趋势</h1>
<h2>参考资料</h2>
<p><a href="https://www.cas.cn/zjs/201902/t20190218_4679625.shtml">谭铁牛：人工智能的历史、现状和未来</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>grand-unification-theory</title>
      <link>https://vansiit.cc/2024/03/29/grand-unification-theory.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2024/03/29/grand-unification-theory.html</guid>
      <pubDate>Fri, 29 Mar 2024 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>从物理学的大一统理论想到的</h1>
<blockquote>
<p>我们生存的宇宙，存在着经典物理学、相对论、量子力学和弦理论等四种以上互不相容的物理理论，事实上就造成了我们生活的这个世界没有唯一解，既荒谬又美妙</p>
</blockquote>
]]></description>
      <content:encoded><![CDATA[<h1>从物理学的大一统理论想到的</h1>
<blockquote>
<p>我们生存的宇宙，存在着经典物理学、相对论、量子力学和弦理论等四种以上互不相容的物理理论，事实上就造成了我们生活的这个世界没有唯一解，既荒谬又美妙</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>当我在看NBA的时候在看什么</title>
      <link>https://vansiit.cc/2024/03/29/nba.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2024/03/29/nba.html</guid>
      <pubDate>Fri, 29 Mar 2024 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>当我在看NBA的时候在看什么</h1>
<h2>马布里</h2>
<p>96黄金一代，北京队功勋，北京爷们，四九城老少爷们见了也要叫声牛逼，主教练，绿卡</p>
<h2>寂寞大神</h2>
<p>大学时期
NBA时期
CBA时期
美国国家队三人篮球</p>
<h2>普理查德</h2>
<p>车库练球
凯尔特人总决赛</p>
]]></description>
      <content:encoded><![CDATA[<h1>当我在看NBA的时候在看什么</h1>
<h2>马布里</h2>
<p>96黄金一代，北京队功勋，北京爷们，四九城老少爷们见了也要叫声牛逼，主教练，绿卡</p>
<h2>寂寞大神</h2>
<p>大学时期
NBA时期
CBA时期
美国国家队三人篮球</p>
<h2>普理查德</h2>
<p>车库练球
凯尔特人总决赛</p>
]]></content:encoded>
    </item>
    <item>
      <title>Git常用指令大全及IDEA提交插件推荐</title>
      <link>https://vansiit.cc/2024/03/21/git-common-commands.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2024/03/21/git-common-commands.html</guid>
      <pubDate>Thu, 21 Mar 2024 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>Git 常用指令 以及 IDEA常用commit插件推荐</h1>
<h2>前言</h2>
<p><img src="https://vansiit.cc/public/img/post/1711003537001.png" alt="img.png"></p>
<ul>
<li>工作区 (workspace)</li>
</ul>
<blockquote>
<p>就是我们当前工作空间，也就是我们当前能在本地文件夹下面看到的文件结构。初始化工作空间或者工作空间 clean 的时候，文件内容和 index 暂存区是一致的，随着修改，工作区文件在没有 add 到暂存区时候，工作区将和暂存区是不一致的。</p>
</blockquote>
<ul>
<li>暂存区 (index)</li>
</ul>
<blockquote>
<p>老版本概念也叫 Cache 区，就是文件暂时存放的地方，所有暂时存放在暂存区中的文件将随着一个 commit 一起提交到 local repository 此时 local repository 里面文件将完全被暂存区所取代。暂存区是 git 架构设计中非常重要和难理解的一部分。</p>
</blockquote>
<ul>
<li>本地仓库 (local repository)</li>
</ul>
<blockquote>
<p>git 是分布式版本控制系统，和其他版本控制系统不同的是他可以完全去中心化工作，你可以不用和中央服务器 (remote server) 进行通信，在本地即可进行全部离线操作，包括 log，history，commit，diff 等等。完成离线操作最核心是因为 git 有一个几乎和远程一样的本地仓库，所有本地离线操作都可以在本地完成，等需要的时候再和远程服务进行交互。</p>
</blockquote>
<ul>
<li>远程仓库 (remote repository)</li>
</ul>
<blockquote>
<p>中心化仓库，所有人共享，本地仓库会需要和远程仓库进行交互，也就能将其他所有人内容更新到本地仓库把自己内容上传分享给其他人。结构大体和本地仓库一样。</p>
</blockquote>
<h2>git clone</h2>
<p>git checkout -b development 创建本地分支并切换到这个分支</p>
<p>git checkout --track origin/test-dev 从远程拉分支</p>
<p>git push origin development 创建远程分支</p>
<p>git branch -u origin/luozhengshun 建立本地远程联系</p>
<p>git branch -a 查看远程分支</p>
<p>git branch 查看本地分支</p>
<p>git pull 更新本地库</p>
<p>git add .添加到本地库</p>
<p>git commit -m ‘提交属性’ 提交达到本地库</p>
<p>git push 提交远程库</p>
<p>git status 查看状态</p>
<p>git diff xxx查看更改</p>
<p>git log 查看历史</p>
<p>git checkout 分支名切换分支</p>
<p>git branch -d 分支名删除本地分支</p>
<p>git push origin --delete 分支名删除远程分支</p>
<p>git checkout master 切换到Master分支</p>
<p>git merge —no-ff development 对Development分支进行合并</p>
<p>git remote 列出所有远程主机</p>
<p>git remote update origin --prune 更新远程主机origin 整理分支</p>
<p>git branch -r 列出远程分支</p>
<p>git branch -vv 查看本地分支和远程分支对应关系</p>
<p>git checkout -b gpf origin/gpf 新建本地分支gpf与远程gpf分支相关联</p>
<p>git reset --soft HEAD~1 撤销上一次conmmit，1代表上最近1次，若想撤销最近2次则改为2</p>
]]></description>
      <content:encoded><![CDATA[<h1>Git 常用指令 以及 IDEA常用commit插件推荐</h1>
<h2>前言</h2>
<p><img src="https://vansiit.cc/public/img/post/1711003537001.png" alt="img.png"></p>
<ul>
<li>工作区 (workspace)</li>
</ul>
<blockquote>
<p>就是我们当前工作空间，也就是我们当前能在本地文件夹下面看到的文件结构。初始化工作空间或者工作空间 clean 的时候，文件内容和 index 暂存区是一致的，随着修改，工作区文件在没有 add 到暂存区时候，工作区将和暂存区是不一致的。</p>
</blockquote>
<ul>
<li>暂存区 (index)</li>
</ul>
<blockquote>
<p>老版本概念也叫 Cache 区，就是文件暂时存放的地方，所有暂时存放在暂存区中的文件将随着一个 commit 一起提交到 local repository 此时 local repository 里面文件将完全被暂存区所取代。暂存区是 git 架构设计中非常重要和难理解的一部分。</p>
</blockquote>
<ul>
<li>本地仓库 (local repository)</li>
</ul>
<blockquote>
<p>git 是分布式版本控制系统，和其他版本控制系统不同的是他可以完全去中心化工作，你可以不用和中央服务器 (remote server) 进行通信，在本地即可进行全部离线操作，包括 log，history，commit，diff 等等。完成离线操作最核心是因为 git 有一个几乎和远程一样的本地仓库，所有本地离线操作都可以在本地完成，等需要的时候再和远程服务进行交互。</p>
</blockquote>
<ul>
<li>远程仓库 (remote repository)</li>
</ul>
<blockquote>
<p>中心化仓库，所有人共享，本地仓库会需要和远程仓库进行交互，也就能将其他所有人内容更新到本地仓库把自己内容上传分享给其他人。结构大体和本地仓库一样。</p>
</blockquote>
<h2>git clone</h2>
<p>git checkout -b development 创建本地分支并切换到这个分支</p>
<p>git checkout --track origin/test-dev 从远程拉分支</p>
<p>git push origin development 创建远程分支</p>
<p>git branch -u origin/luozhengshun 建立本地远程联系</p>
<p>git branch -a 查看远程分支</p>
<p>git branch 查看本地分支</p>
<p>git pull 更新本地库</p>
<p>git add .添加到本地库</p>
<p>git commit -m ‘提交属性’ 提交达到本地库</p>
<p>git push 提交远程库</p>
<p>git status 查看状态</p>
<p>git diff xxx查看更改</p>
<p>git log 查看历史</p>
<p>git checkout 分支名切换分支</p>
<p>git branch -d 分支名删除本地分支</p>
<p>git push origin --delete 分支名删除远程分支</p>
<p>git checkout master 切换到Master分支</p>
<p>git merge —no-ff development 对Development分支进行合并</p>
<p>git remote 列出所有远程主机</p>
<p>git remote update origin --prune 更新远程主机origin 整理分支</p>
<p>git branch -r 列出远程分支</p>
<p>git branch -vv 查看本地分支和远程分支对应关系</p>
<p>git checkout -b gpf origin/gpf 新建本地分支gpf与远程gpf分支相关联</p>
<p>git reset --soft HEAD~1 撤销上一次conmmit，1代表上最近1次，若想撤销最近2次则改为2</p>
]]></content:encoded>
    </item>
    <item>
      <title>MySQL字符集详解：utf8与utf8mb4的区别及最佳实践</title>
      <link>https://vansiit.cc/2023/12/02/mysql-character-set.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2023/12/02/mysql-character-set.html</guid>
      <pubDate>Sat, 02 Dec 2023 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>utf8和utf8mb4的区别 - MySQL字符集和比较规则</h1>
<h2>一、基础概念</h2>
<h3>1、位 / 比特位 （bit）</h3>
<p>位/比特位/bit，数据存储的最小单位。每个二进制数字0或者1就是1个位。</p>
<hr>
<h3>2、字节（Byte）</h3>
<p>字节(Byte)是一种计量单位，表示数据量多少，它是计算机信息技术用于计量存储容量的一种计量单位。</p>
<p>8个位构成一个字节。即：1 byte (字节)= 8 bit (位)；</p>
<p>1 B = 1 byte(字节);</p>
<p>1 KB = 1024 B(字节);</p>
<p>1 MB = 1024 KB;(2^10 B)</p>
<p>1 GB = 1024 MB;(2^20 B)</p>
<p>1 TB = 1024 GB;(2^30 B)</p>
<hr>
<h3>3、字符（Character）</h3>
<p>字符是指计算机中使用的文字和符号，比如1、2、3、A、B、C、~！·#￥%……—* () ——+、等等。</p>
<p>a、A、中、+、*、の…均表示一个字符；</p>
<p>一般 utf-8 编码下，一个汉字字符占用 3 个 字节；</p>
<p>一般 gbk 编码下，一个汉字字符占用 2 个 字节；</p>
<blockquote>
<h3>小贴士</h3>
<p>“字节”与“字符”</p>
<p>它们完全不是一个维度的概念，所以两者之间没有“区别”这个说法。不同编码里，字符和字节的对应关系不同：</p>
<p>①ASCII码中，一个英文字母(不分大小写)占一个字节的空间，一个中文汉字占两个字节的空间。一个二进制数字序列，在计算机中作为一个数字单元，一般为8位二进制数，换算为十进制。最小值0，最大值255。</p>
<p>②UTF-8编码中，一个英文字符等于一个字节，一个中文(含繁体)等于三个字节。</p>
<p>③Unicode编码中，一个英文等于两个字节，一个中文(含繁体)等于两个字节。</p>
<p>符号：英文标点占一个字节，中文标点占两个字节。举例：英文句号“.”占1个字节的大小，中文句号“。”占2个字节的大小。</p>
<p>④UTF-16编码中，一个英文字母字符或一个汉字字符存储都需要2个字节(Unicode扩展区的一些汉字存储需要4个字节)。</p>
<p>⑤UTF-32编码中，世界上任何字符的存储都需要4个字节。</p>
</blockquote>
<hr>
<h3>4、字符集（Character Set）</h3>
<p>字符集是指多个字符的集合。</p>
<p>不同的字符集包含的字符个数不一样、包含的字符不一样、对字符的编码方式也不一样。</p>
<p>例如GB2312是中国国家标准的简体中文字符集，GB2312收录简化汉字（6763个）及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母、汉语拼音符号、汉语注音字母，共 7445 个图形字符。</p>
<p>而ASCII字符集只包含了128字符，这个字符集收录的主要字符是英文字母、阿拉伯字母和一些简单的控制字符。</p>
<p>另外，还有其他常用的字符集有 GBK字符集、GB18030字符集、Big5字符集、Unicode字符集等。</p>
<hr>
<h3>5、字符编码（Character Encoding）</h3>
<p>字符编码是指一种映射规则，根据这个映射规则可以将某个字符映射成其他形式的数据以便在计算机中存储和传输。</p>
<p>例如ASCII字符编码规定使用单字节中低位的7个比特去编码所有的字符，在这个编码规则下字母A的编号是65（ASCII码），用单字节表示就是0x41，因此写入存储设备的时候就是二进制的 01000001。</p>
<p>每种字符集都有自己的字符编码规则，常用的字符集编码规则还有 UTF-8编码、GBK编码、Big5编码等。</p>
<hr>
<h2>二、MySQL中的字符集</h2>
<blockquote>
<p>MySQL中并不区分字符集和编码方案的概念，所以后边把utf8、utf16、utf32都当作一种字符集对待。</p>
</blockquote>
<p>MySQL中的utf8和utf8mb4</p>
<p>我们上边说utf8字符集表示一个字符需要使用1～4个字节，但是我们常用的一些字符使用1～3个字节就可以表示了。</p>
<p>而在MySQL中字符集表示一个字符所用最大字节长度在某些方面会影响系统的存储和性能，所以MySQL的设计者们定义了两个概念：</p>
<p>utf8mb3：阉割过的utf8字符集，只使用1～3个字节表示字符。</p>
<p>utf8mb4：正宗的utf8字符集，使用1～4个字节表示字符。</p>
<p>MySQL有4个级别的字符集和比较规则：服务器级别、数据库级别、表级别、列级别。也就是说可以单独设置服务器、DB、Table、Column的字符集和比较规则，当然前提是更高级别的字符集支持。</p>
<blockquote>
<h3>小贴士</h3>
<p>有一点需要大家十分的注意，在MySQL中utf8是utf8mb3的别名，所以之后在MySQL中提到utf8就意味着使用1~3个字节来表示一个字符，如果大家有使用4字节编码一个字符的情况，比如存储一些emoji表情，那请使用utf8mb4。</p>
<p>所以大家在创建数据库和表的时候，千万要注意，以免后续更改字符集造成很大的麻烦。</p>
<p>在MySQL8.0中，已经极大程度优化了utf8mb4字符集的性能，并将其设置为默认字符集</p>
</blockquote>
<hr>
<h2>三、比较规则</h2>
<blockquote>
<h4>小贴士</h4>
<p>将字符映射成二进制数据的过程叫做编码</p>
<p>将二进制数据映射到字符的过程叫做解码</p>
</blockquote>
<p>前面介绍了字符集和字符编码，那么如何比较两个字符的大小呢?</p>
<p>例如：直接比较这两个字符对应的二进制编码的大小</p>
<p>或将两个大小写不同的字符全部都转为大写或者小写，再比较这两个字符对应的二进制数据</p>
<hr>
<h3>1. 比较规则的查看</h3>
<pre><code class="language-sql">SHOW COLLATION [LIKE 匹配的模式];
</code></pre>
<p>一种字符集可能对应着若干种比较规则，MySQL支持的字符集就已经非常多了，所以支持的比较规则更多，我们先只查看一下utf8字符集下的比较规则：</p>
<pre><code class="language-sql">mysql&gt; SHOW COLLATION LIKE 'utf8\_%';
+--------------------------+---------+-----+---------+----------+---------+
| Collation                | Charset | Id  | Default | Compiled | Sortlen |
+--------------------------+---------+-----+---------+----------+---------+
| utf8_general_ci          | utf8    |  33 | Yes     | Yes      |       1 |
| utf8_bin                 | utf8    |  83 |         | Yes      |       1 |
| utf8_unicode_ci          | utf8    | 192 |         | Yes      |       8 |
| utf8_icelandic_ci        | utf8    | 193 |         | Yes      |       8 |
| utf8_latvian_ci          | utf8    | 194 |         | Yes      |       8 |
| utf8_romanian_ci         | utf8    | 195 |         | Yes      |       8 |
| utf8_slovenian_ci        | utf8    | 196 |         | Yes      |       8 |
| utf8_polish_ci           | utf8    | 197 |         | Yes      |       8 |
| utf8_estonian_ci         | utf8    | 198 |         | Yes      |       8 |
| utf8_spanish_ci          | utf8    | 199 |         | Yes      |       8 |
| utf8_swedish_ci          | utf8    | 200 |         | Yes      |       8 |
| utf8_turkish_ci          | utf8    | 201 |         | Yes      |       8 |
| utf8_czech_ci            | utf8    | 202 |         | Yes      |       8 |
| utf8_danish_ci           | utf8    | 203 |         | Yes      |       8 |
| utf8_lithuanian_ci       | utf8    | 204 |         | Yes      |       8 |
| utf8_slovak_ci           | utf8    | 205 |         | Yes      |       8 |
| utf8_spanish2_ci         | utf8    | 206 |         | Yes      |       8 |
| utf8_roman_ci            | utf8    | 207 |         | Yes      |       8 |
| utf8_persian_ci          | utf8    | 208 |         | Yes      |       8 |
| utf8_esperanto_ci        | utf8    | 209 |         | Yes      |       8 |
| utf8_hungarian_ci        | utf8    | 210 |         | Yes      |       8 |
| utf8_sinhala_ci          | utf8    | 211 |         | Yes      |       8 |
| utf8_german2_ci          | utf8    | 212 |         | Yes      |       8 |
| utf8_croatian_ci         | utf8    | 213 |         | Yes      |       8 |
| utf8_unicode_520_ci      | utf8    | 214 |         | Yes      |       8 |
| utf8_vietnamese_ci       | utf8    | 215 |         | Yes      |       8 |
| utf8_general_mysql500_ci | utf8    | 223 |         | Yes      |       1 |
+--------------------------+---------+-----+---------+----------+---------+
27 rows in set (0.00 sec)
</code></pre>
<p>这些比较规则的命名还挺有规律的，具体规律如下：</p>
<p>比较规则名称以与其关联的字符集的名称开头。如上图的查询结果的比较规则名称都是以utf8开头的。</p>
<p>后边紧跟着该比较规则主要作用于哪种语言，比如utf8_polish_ci表示以波兰语的规则比较，utf8_spanish_ci是以西班牙语的规则比较，utf8_general_ci是一种通用的比较规则。</p>
<p>名称后缀意味着该比较规则是否区分语言中的重音、大小写啥的，具体可以用的值如下：</p>
<table>
<thead>
<tr>
<th style="text-align:center">后缀</th>
<th style="text-align:center">英文释义</th>
<th style="text-align:center">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">_ai</td>
<td style="text-align:center">accent insensitive</td>
<td style="text-align:center">不区分重音</td>
</tr>
<tr>
<td style="text-align:center">_as</td>
<td style="text-align:center">accent sensitive</td>
<td style="text-align:center">区分重音</td>
</tr>
<tr>
<td style="text-align:center">_ci</td>
<td style="text-align:center">case insensitive</td>
<td style="text-align:center">不区分大小写</td>
</tr>
<tr>
<td style="text-align:center">_cs</td>
<td style="text-align:center">case sensitive</td>
<td style="text-align:center">区分大小写</td>
</tr>
<tr>
<td style="text-align:center">_bin</td>
<td style="text-align:center">binary</td>
<td style="text-align:center">以二进制方式比较</td>
</tr>
</tbody>
</table>
<hr>
<h3>2.比较规则的应用</h3>
<p>在对字符串进行比较，或者对某个字符串列执行排序操作时，如果没有得到想象中的结果，需要思考一下是不是比较规则的问题。</p>
<hr>
<h1>总结</h1>
<ol>
<li>
<p>字符集指的是某个字符范围的编码规则。</p>
</li>
<li>
<p>比较规则是针对某个字符集中的字符比较大小的一种规则。</p>
</li>
<li>
<p>在MySQL中，一个字符集可以有若干种比较规则，其中有一个默认的比较规则，一个比较规则必须对应一个字符集。</p>
</li>
<li>
<p>查看MySQL中查看支持的字符集和比较规则的语句如下：</p>
</li>
</ol>
<pre><code class="language-sql">SHOW (CHARACTER SET|CHARSET) [LIKE 匹配的模式];
SHOW COLLATION [LIKE 匹配的模式];
</code></pre>
<ol start="5">
<li>
<p>MySQL有四个级别的字符集和比较规则</p>
<p>a.服务器级别</p>
</li>
</ol>
<p>character_set_server表示服务器级别的字符集，collation_server表示服务器级别的比较规则。</p>
<pre><code>b. 数据库级别
</code></pre>
<p>创建和修改数据库时可以指定字符集和比较规则：</p>
<pre><code class="language-sql">CREATE DATABASE 数据库名
    [[DEFAULT] CHARACTER SET 字符集名称]
    [[DEFAULT] COLLATE 比较规则名称];

ALTER DATABASE 数据库名
    [[DEFAULT] CHARACTER SET 字符集名称]
    [[DEFAULT] COLLATE 比较规则名称];
</code></pre>
<p>character_set_database表示当前数据库的字符集，collation_database表示当前默认数据库的比较规则，这两个系统变量是只读的，不能修改。如果没有指定当前默认数据库，则变量与相应的服务器级系统变量具有相同的值。</p>
<pre><code>c. 表级别
</code></pre>
<p>创建和修改表的时候指定表的字符集和比较规则：</p>
<pre><code class="language-sql">CREATE TABLE 表名 (列的信息)
    [[DEFAULT] CHARACTER SET 字符集名称]
    [COLLATE 比较规则名称]];

ALTER TABLE 表名
    [[DEFAULT] CHARACTER SET 字符集名称]
    [COLLATE 比较规则名称];
</code></pre>
<pre><code>d. 列级别
</code></pre>
<p>创建和修改列定义的时候可以指定该列的字符集和比较规则：</p>
<pre><code class="language-sql">CREATE TABLE 表名(
    列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称],
    其他列...
);

ALTER TABLE 表名 MODIFY 列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称];
</code></pre>
]]></description>
      <content:encoded><![CDATA[<h1>utf8和utf8mb4的区别 - MySQL字符集和比较规则</h1>
<h2>一、基础概念</h2>
<h3>1、位 / 比特位 （bit）</h3>
<p>位/比特位/bit，数据存储的最小单位。每个二进制数字0或者1就是1个位。</p>
<hr>
<h3>2、字节（Byte）</h3>
<p>字节(Byte)是一种计量单位，表示数据量多少，它是计算机信息技术用于计量存储容量的一种计量单位。</p>
<p>8个位构成一个字节。即：1 byte (字节)= 8 bit (位)；</p>
<p>1 B = 1 byte(字节);</p>
<p>1 KB = 1024 B(字节);</p>
<p>1 MB = 1024 KB;(2^10 B)</p>
<p>1 GB = 1024 MB;(2^20 B)</p>
<p>1 TB = 1024 GB;(2^30 B)</p>
<hr>
<h3>3、字符（Character）</h3>
<p>字符是指计算机中使用的文字和符号，比如1、2、3、A、B、C、~！·#￥%……—* () ——+、等等。</p>
<p>a、A、中、+、*、の…均表示一个字符；</p>
<p>一般 utf-8 编码下，一个汉字字符占用 3 个 字节；</p>
<p>一般 gbk 编码下，一个汉字字符占用 2 个 字节；</p>
<blockquote>
<h3>小贴士</h3>
<p>“字节”与“字符”</p>
<p>它们完全不是一个维度的概念，所以两者之间没有“区别”这个说法。不同编码里，字符和字节的对应关系不同：</p>
<p>①ASCII码中，一个英文字母(不分大小写)占一个字节的空间，一个中文汉字占两个字节的空间。一个二进制数字序列，在计算机中作为一个数字单元，一般为8位二进制数，换算为十进制。最小值0，最大值255。</p>
<p>②UTF-8编码中，一个英文字符等于一个字节，一个中文(含繁体)等于三个字节。</p>
<p>③Unicode编码中，一个英文等于两个字节，一个中文(含繁体)等于两个字节。</p>
<p>符号：英文标点占一个字节，中文标点占两个字节。举例：英文句号“.”占1个字节的大小，中文句号“。”占2个字节的大小。</p>
<p>④UTF-16编码中，一个英文字母字符或一个汉字字符存储都需要2个字节(Unicode扩展区的一些汉字存储需要4个字节)。</p>
<p>⑤UTF-32编码中，世界上任何字符的存储都需要4个字节。</p>
</blockquote>
<hr>
<h3>4、字符集（Character Set）</h3>
<p>字符集是指多个字符的集合。</p>
<p>不同的字符集包含的字符个数不一样、包含的字符不一样、对字符的编码方式也不一样。</p>
<p>例如GB2312是中国国家标准的简体中文字符集，GB2312收录简化汉字（6763个）及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母、汉语拼音符号、汉语注音字母，共 7445 个图形字符。</p>
<p>而ASCII字符集只包含了128字符，这个字符集收录的主要字符是英文字母、阿拉伯字母和一些简单的控制字符。</p>
<p>另外，还有其他常用的字符集有 GBK字符集、GB18030字符集、Big5字符集、Unicode字符集等。</p>
<hr>
<h3>5、字符编码（Character Encoding）</h3>
<p>字符编码是指一种映射规则，根据这个映射规则可以将某个字符映射成其他形式的数据以便在计算机中存储和传输。</p>
<p>例如ASCII字符编码规定使用单字节中低位的7个比特去编码所有的字符，在这个编码规则下字母A的编号是65（ASCII码），用单字节表示就是0x41，因此写入存储设备的时候就是二进制的 01000001。</p>
<p>每种字符集都有自己的字符编码规则，常用的字符集编码规则还有 UTF-8编码、GBK编码、Big5编码等。</p>
<hr>
<h2>二、MySQL中的字符集</h2>
<blockquote>
<p>MySQL中并不区分字符集和编码方案的概念，所以后边把utf8、utf16、utf32都当作一种字符集对待。</p>
</blockquote>
<p>MySQL中的utf8和utf8mb4</p>
<p>我们上边说utf8字符集表示一个字符需要使用1～4个字节，但是我们常用的一些字符使用1～3个字节就可以表示了。</p>
<p>而在MySQL中字符集表示一个字符所用最大字节长度在某些方面会影响系统的存储和性能，所以MySQL的设计者们定义了两个概念：</p>
<p>utf8mb3：阉割过的utf8字符集，只使用1～3个字节表示字符。</p>
<p>utf8mb4：正宗的utf8字符集，使用1～4个字节表示字符。</p>
<p>MySQL有4个级别的字符集和比较规则：服务器级别、数据库级别、表级别、列级别。也就是说可以单独设置服务器、DB、Table、Column的字符集和比较规则，当然前提是更高级别的字符集支持。</p>
<blockquote>
<h3>小贴士</h3>
<p>有一点需要大家十分的注意，在MySQL中utf8是utf8mb3的别名，所以之后在MySQL中提到utf8就意味着使用1~3个字节来表示一个字符，如果大家有使用4字节编码一个字符的情况，比如存储一些emoji表情，那请使用utf8mb4。</p>
<p>所以大家在创建数据库和表的时候，千万要注意，以免后续更改字符集造成很大的麻烦。</p>
<p>在MySQL8.0中，已经极大程度优化了utf8mb4字符集的性能，并将其设置为默认字符集</p>
</blockquote>
<hr>
<h2>三、比较规则</h2>
<blockquote>
<h4>小贴士</h4>
<p>将字符映射成二进制数据的过程叫做编码</p>
<p>将二进制数据映射到字符的过程叫做解码</p>
</blockquote>
<p>前面介绍了字符集和字符编码，那么如何比较两个字符的大小呢?</p>
<p>例如：直接比较这两个字符对应的二进制编码的大小</p>
<p>或将两个大小写不同的字符全部都转为大写或者小写，再比较这两个字符对应的二进制数据</p>
<hr>
<h3>1. 比较规则的查看</h3>
<pre><code class="language-sql">SHOW COLLATION [LIKE 匹配的模式];
</code></pre>
<p>一种字符集可能对应着若干种比较规则，MySQL支持的字符集就已经非常多了，所以支持的比较规则更多，我们先只查看一下utf8字符集下的比较规则：</p>
<pre><code class="language-sql">mysql&gt; SHOW COLLATION LIKE 'utf8\_%';
+--------------------------+---------+-----+---------+----------+---------+
| Collation                | Charset | Id  | Default | Compiled | Sortlen |
+--------------------------+---------+-----+---------+----------+---------+
| utf8_general_ci          | utf8    |  33 | Yes     | Yes      |       1 |
| utf8_bin                 | utf8    |  83 |         | Yes      |       1 |
| utf8_unicode_ci          | utf8    | 192 |         | Yes      |       8 |
| utf8_icelandic_ci        | utf8    | 193 |         | Yes      |       8 |
| utf8_latvian_ci          | utf8    | 194 |         | Yes      |       8 |
| utf8_romanian_ci         | utf8    | 195 |         | Yes      |       8 |
| utf8_slovenian_ci        | utf8    | 196 |         | Yes      |       8 |
| utf8_polish_ci           | utf8    | 197 |         | Yes      |       8 |
| utf8_estonian_ci         | utf8    | 198 |         | Yes      |       8 |
| utf8_spanish_ci          | utf8    | 199 |         | Yes      |       8 |
| utf8_swedish_ci          | utf8    | 200 |         | Yes      |       8 |
| utf8_turkish_ci          | utf8    | 201 |         | Yes      |       8 |
| utf8_czech_ci            | utf8    | 202 |         | Yes      |       8 |
| utf8_danish_ci           | utf8    | 203 |         | Yes      |       8 |
| utf8_lithuanian_ci       | utf8    | 204 |         | Yes      |       8 |
| utf8_slovak_ci           | utf8    | 205 |         | Yes      |       8 |
| utf8_spanish2_ci         | utf8    | 206 |         | Yes      |       8 |
| utf8_roman_ci            | utf8    | 207 |         | Yes      |       8 |
| utf8_persian_ci          | utf8    | 208 |         | Yes      |       8 |
| utf8_esperanto_ci        | utf8    | 209 |         | Yes      |       8 |
| utf8_hungarian_ci        | utf8    | 210 |         | Yes      |       8 |
| utf8_sinhala_ci          | utf8    | 211 |         | Yes      |       8 |
| utf8_german2_ci          | utf8    | 212 |         | Yes      |       8 |
| utf8_croatian_ci         | utf8    | 213 |         | Yes      |       8 |
| utf8_unicode_520_ci      | utf8    | 214 |         | Yes      |       8 |
| utf8_vietnamese_ci       | utf8    | 215 |         | Yes      |       8 |
| utf8_general_mysql500_ci | utf8    | 223 |         | Yes      |       1 |
+--------------------------+---------+-----+---------+----------+---------+
27 rows in set (0.00 sec)
</code></pre>
<p>这些比较规则的命名还挺有规律的，具体规律如下：</p>
<p>比较规则名称以与其关联的字符集的名称开头。如上图的查询结果的比较规则名称都是以utf8开头的。</p>
<p>后边紧跟着该比较规则主要作用于哪种语言，比如utf8_polish_ci表示以波兰语的规则比较，utf8_spanish_ci是以西班牙语的规则比较，utf8_general_ci是一种通用的比较规则。</p>
<p>名称后缀意味着该比较规则是否区分语言中的重音、大小写啥的，具体可以用的值如下：</p>
<table>
<thead>
<tr>
<th style="text-align:center">后缀</th>
<th style="text-align:center">英文释义</th>
<th style="text-align:center">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">_ai</td>
<td style="text-align:center">accent insensitive</td>
<td style="text-align:center">不区分重音</td>
</tr>
<tr>
<td style="text-align:center">_as</td>
<td style="text-align:center">accent sensitive</td>
<td style="text-align:center">区分重音</td>
</tr>
<tr>
<td style="text-align:center">_ci</td>
<td style="text-align:center">case insensitive</td>
<td style="text-align:center">不区分大小写</td>
</tr>
<tr>
<td style="text-align:center">_cs</td>
<td style="text-align:center">case sensitive</td>
<td style="text-align:center">区分大小写</td>
</tr>
<tr>
<td style="text-align:center">_bin</td>
<td style="text-align:center">binary</td>
<td style="text-align:center">以二进制方式比较</td>
</tr>
</tbody>
</table>
<hr>
<h3>2.比较规则的应用</h3>
<p>在对字符串进行比较，或者对某个字符串列执行排序操作时，如果没有得到想象中的结果，需要思考一下是不是比较规则的问题。</p>
<hr>
<h1>总结</h1>
<ol>
<li>
<p>字符集指的是某个字符范围的编码规则。</p>
</li>
<li>
<p>比较规则是针对某个字符集中的字符比较大小的一种规则。</p>
</li>
<li>
<p>在MySQL中，一个字符集可以有若干种比较规则，其中有一个默认的比较规则，一个比较规则必须对应一个字符集。</p>
</li>
<li>
<p>查看MySQL中查看支持的字符集和比较规则的语句如下：</p>
</li>
</ol>
<pre><code class="language-sql">SHOW (CHARACTER SET|CHARSET) [LIKE 匹配的模式];
SHOW COLLATION [LIKE 匹配的模式];
</code></pre>
<ol start="5">
<li>
<p>MySQL有四个级别的字符集和比较规则</p>
<p>a.服务器级别</p>
</li>
</ol>
<p>character_set_server表示服务器级别的字符集，collation_server表示服务器级别的比较规则。</p>
<pre><code>b. 数据库级别
</code></pre>
<p>创建和修改数据库时可以指定字符集和比较规则：</p>
<pre><code class="language-sql">CREATE DATABASE 数据库名
    [[DEFAULT] CHARACTER SET 字符集名称]
    [[DEFAULT] COLLATE 比较规则名称];

ALTER DATABASE 数据库名
    [[DEFAULT] CHARACTER SET 字符集名称]
    [[DEFAULT] COLLATE 比较规则名称];
</code></pre>
<p>character_set_database表示当前数据库的字符集，collation_database表示当前默认数据库的比较规则，这两个系统变量是只读的，不能修改。如果没有指定当前默认数据库，则变量与相应的服务器级系统变量具有相同的值。</p>
<pre><code>c. 表级别
</code></pre>
<p>创建和修改表的时候指定表的字符集和比较规则：</p>
<pre><code class="language-sql">CREATE TABLE 表名 (列的信息)
    [[DEFAULT] CHARACTER SET 字符集名称]
    [COLLATE 比较规则名称]];

ALTER TABLE 表名
    [[DEFAULT] CHARACTER SET 字符集名称]
    [COLLATE 比较规则名称];
</code></pre>
<pre><code>d. 列级别
</code></pre>
<p>创建和修改列定义的时候可以指定该列的字符集和比较规则：</p>
<pre><code class="language-sql">CREATE TABLE 表名(
    列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称],
    其他列...
);

ALTER TABLE 表名 MODIFY 列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称];
</code></pre>
]]></content:encoded>
    </item>
    <item>
      <title>APP扫码登录完整实现：从原理到代码的全栈开发指南</title>
      <link>https://vansiit.cc/2023/06/15/scan-qrcode-login.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2023/06/15/scan-qrcode-login.html</guid>
      <pubDate>Thu, 15 Jun 2023 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>APP扫码登录：不只有原理，直接上代码</h1>
<h2>一.背景</h2>
<p>扫码登录前提是要有APP端，和PC端。其目的是为了让用户在使用的PC端时登录更加方便和安全，使用手机扫一扫就可以登录和使用服务。</p>
<p>还有一个重要原因就是引导用户下载APP。</p>
<p>前段时间正好接到这么一个需求，设计和相关代码都会在本文贴出来。</p>
<p>话不多说，进入正题。</p>
<hr>
<h2>二.原理和时序图</h2>
<p><img src="https://vansiit.cc/img/qrcode/scan-qrcode.jpg" alt="img.png"></p>
<blockquote>
<p>二维码状态</p>
<ul>
<li>INIT(1, “初始状态”),</li>
<li>SCANNING(2, “扫码中”),</li>
<li>CANCEL(3, “取消”),</li>
<li>CONFIRM(4, “确定登录”),</li>
<li>EXPIRE(5, “过期”);</li>
</ul>
</blockquote>
<ol>
<li>
<p>用户在PC端请求二维码信息</p>
</li>
<li>
<p>服务端初始化二维码信息
生成二维码唯一的oauthKey，存储到Redis，设置过期时间，这里我配置的60S，过期时间看自己需求。
这时候二维码的状态是 INIT</p>
</li>
<li>
<p>服务端返回二维码信息给PC端
返回约定好的信息给PC端，我们这里还有APP的跳转链接。
PC端拿到二维码信息后生成二维码展示给用户</p>
</li>
<li>
<p>PC端定时轮询二维码信息 PC端根据二维码的状态做想应的处理，比如：</p>
</li>
</ol>
<ul>
<li>扫码中  -&gt;  置灰二维码，</li>
<li>取消    -&gt; 弹框提示和刷新二维码</li>
<li>确定登录 -&gt; 拿到token，调取用户信息接口，取消二维码页面</li>
<li>过期    -&gt; 弹框提示和刷新二维码</li>
</ul>
<ol start="5">
<li>
<p>服务端返回此二维码的状态和登录信息</p>
</li>
<li>
<p>用户使用APP扫描二维码</p>
</li>
<li>
<p>APP解析二维码，获取oauthkey</p>
</li>
<li>
<p>APP使用oauthkey校验二维码信息</p>
</li>
<li>
<p>查询二维码信息是否有效，是否过期</p>
</li>
<li>
<p>返回校验结果给到APP
APP根据校验结果，有效就弹出确认框或者取消框，无效就弹框提示</p>
</li>
<li>
<p>确认登录
用户在APP上点击确认登录按钮，确认登录</p>
</li>
<li>
<p>服务端登录逻辑</p>
</li>
</ol>
<ul>
<li>更新二维码状态为确认登录</li>
<li>判断APP的用户的登录状态</li>
<li>判断登录的设备个数</li>
<li>生成token , 记录想应的缓存信息</li>
</ul>
<ol start="13">
<li>
<p>第4步的轮询接口返回登录token</p>
</li>
<li>
<p>PC端使用token请求获取用户信息，和其他接口</p>
</li>
<li>
<p>登录完成</p>
</li>
</ol>
<hr>
<h2>三.代码</h2>
<h3>controller</h3>
<pre><code class="language-java">@Slf4j
@RestController
@RequestMapping(value = &quot;/user/qrcode&quot;)
@Api(tags = &quot;WEB端扫码API&quot;)
public class ScanQrcodeLoginController {
    @Autowired
    private ScanQrcodeLoginService scanQrcodeLoginService;

    /**
     * 获取二维码链接和信息
     */
    @ApiOperation(value = &quot;获取二维码链接和信息&quot;)
    @GetMapping(value = &quot;/info&quot;)
    public ResultResponse&lt;LoginUrlVO&gt; getQrcodeInfo(){
        return ResultResponse.success(scanQrcodeLoginService.getQrcodeInfo());
    }

    /**
     * 获取二维码扫描状态
     */
    @ApiOperation(value = &quot;获取二维码扫描状态&quot;)
    @GetMapping(value = &quot;/state&quot;)
    public ResultResponse&lt;QrcodeStateVO&gt; getQrcodeState(@RequestParam(value = &quot;oauthKey&quot;) String oauthKey){
        return ResultResponse.success(scanQrcodeLoginService.getQrcodeState(oauthKey));
    }
}


@Slf4j
@RestController
@RequestMapping(value = &quot;/user/app/qrcode&quot;)
@Api(tags = &quot;APP端扫码API&quot;)
public class AppScanQrcodeLoginController extends BaseController {
    @Autowired
    private ScanQrcodeLoginService scanQrcodeLoginService;

    /**
     * 二维码扫描
     */
    @ApiOperation(value = &quot;二维码扫描&quot;)
    @GetMapping(value = &quot;/scan&quot;)
    public ResultResponse&lt;QrcodeScanResultVO&gt; scan(@RequestParam(value = &quot;oauthKey&quot;) String oauthKey){
        Long userId = getCurrentUserId(false);
        return ResultResponse.success(scanQrcodeLoginService.scan(oauthKey, userId));
    }

    /**
     * 二维码取消
     */
    @ApiOperation(value = &quot;二维码取消&quot;)
    @GetMapping(value = &quot;/cancel&quot;)
    public ResultResponse&lt;QrcodeCancelResultVO&gt; cancel(@RequestParam(value = &quot;oauthKey&quot;) String oauthKey){
        Long userId = getCurrentUserId(true);
        return ResultResponse.success(scanQrcodeLoginService.cancel(oauthKey, userId));
    }

    /**
     * 二维码确定登录
     */
    @ApiOperation(value = &quot;二维码确定登录&quot;)
    @GetMapping(value = &quot;/confirm&quot;)
    public ResultResponse&lt;QrcodeConfirmResultVO&gt; confirm(@RequestParam(value = &quot;oauthKey&quot;) String oauthKey){
        Long userId = getCurrentUserId(true);
        return ResultResponse.success(scanQrcodeLoginService.confirm(oauthKey, userId));
    }
}

@Slf4j
@RestController
@RequestMapping(value = &quot;/user/web/login&quot;)
@Api(tags = &quot;WEB端-登录接口&quot;)
public class WebLoginController extends BaseController {

    @Autowired
    private WebUserLoginService webUserLoginService;

    /**
     * 获取登录用户信息
     */
    @ApiOperation(value = &quot;获取登录用户信息&quot;)
    @GetMapping(value = &quot;/info&quot;)
    public ResultResponse&lt;WebUserInfoVo&gt; getWebUserInfo() {
        Long userId = getCurrentUserId(true);
        return ResultResponse.success(webUserLoginService.getWebUserInfo(userId));
    }

    /**
     * 退出登录
     */
    @ApiOperation(value = &quot;退出登录&quot;)
    @GetMapping(value = &quot;/logout&quot;)
    public ResultResponse&lt;Void&gt; webLogout() throws UserRequestParameterException {
        Long userId = getCurrentUserId(true);
        String token = HttpUtils.getToken();
        log.info(&quot;web logout userId: {}, token: {}&quot;, userId, token);
        webUserLoginService.webLogout(userId, token);
        return ResultResponse.success();
    }
}
</code></pre>
<h3>service</h3>
<pre><code class="language-java">@Service
@Slf4j
public class ScanQrcodeLoginServiceImpl implements ScanQrcodeLoginService {

    @Autowired
    private OauthKeyHelp oauthKeyHelp;

    @Autowired
    private WebLoginConfig webLoginConfig;

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public LoginUrlVO getQrcodeInfo() {
        String oauthKey = UUID.randomUUID().toString().replaceAll(&quot;-&quot;, &quot;&quot;);
        LoginUrlVO loginUrlVO = new LoginUrlVO();
        loginUrlVO.setOauthKey(oauthKey);
        loginUrlVO.setUrl(webLoginConfig.getQrcodeUrl() + oauthKey);
        oauthKeyHelp.init(oauthKey);
        return loginUrlVO;
    }

    @Override
    public QrcodeStateVO getQrcodeState(String oauthKey) {
        QrcodeStateVO stateVO = new QrcodeStateVO();
        if (!oauthKeyHelp.verify(oauthKey)) {
            stateVO.setStatus(QrcodeStatus.EXPIRE.getStatus());
            return stateVO;
        }
        OauthTicket oauthTicket = oauthKeyHelp.getStatus(oauthKey);
        if (Objects.isNull(oauthTicket)) {
            stateVO.setStatus(QrcodeStatus.EXPIRE.getStatus());
            return stateVO;
        }
        stateVO.setStatus(oauthTicket.getStatus());
        stateVO.setJwtToken(oauthTicket.getToken());
        stateVO.setUserId(oauthTicket.getUserId());
        return stateVO;
    }

    @Override
    public QrcodeScanResultVO scan(String oauthKey, Long userId) {
        QrcodeScanResultVO resultVO = new QrcodeScanResultVO();
        if (!oauthKeyHelp.verify(oauthKey)) {
            resultVO.setStatus(Boolean.FALSE);
            return resultVO;
        }
        oauthKeyHelp.scan(oauthKey, userId);
        resultVO.setStatus(Boolean.TRUE);
        return resultVO;
    }

    @Override
    public QrcodeCancelResultVO cancel(String oauthKey, Long userId) {
        QrcodeCancelResultVO resultVO = new QrcodeCancelResultVO();
        if (!oauthKeyHelp.verify(oauthKey)) {
            resultVO.setStatus(Boolean.FALSE);
            return resultVO;
        }
        oauthKeyHelp.cancel(oauthKey);
        resultVO.setStatus(Boolean.TRUE);
        return resultVO;
    }

    @Override
    public QrcodeConfirmResultVO confirm(String oauthKey, Long userId) {
        QrcodeConfirmResultVO resultVO = new QrcodeConfirmResultVO();
        String lockKey = &quot;web:user:login:lock:oauthKey:&quot; + oauthKey;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            if (lock.isLocked() &amp;&amp; !lock.isHeldByCurrentThread()) {
                resultVO.setStatus(Boolean.FALSE);
                return resultVO;
            }
            lock.lock(10, TimeUnit.SECONDS);

            OauthTicket oauthTicket = oauthKeyHelp.getStatus(oauthKey);
            if (Objects.isNull(oauthTicket) || (Objects.equals(QrcodeStatus.CONFIRM.getStatus(), oauthTicket.getStatus()) || Objects.equals(QrcodeStatus.EXPIRE.getStatus(), oauthTicket.getStatus()))) {
                resultVO.setStatus(Boolean.FALSE);
                return resultVO;
            }
            resultVO.setStatus(Boolean.TRUE);
            // 登录逻辑，自己实现
            String token = getToken(userId, oauthTicket);
            oauthKeyHelp.confirm(oauthKey, userId, token);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            resultVO.setStatus(Boolean.FALSE);
            return resultVO;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        return resultVO;
    }
}


@Component
@Slf4j
public class OauthKeyHelp {
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private WebLoginConfig webLoginConfig;

    private static final Integer TIME_OUT = 300;

    private static final String REDIS_KEY_PREFIX = &quot;qrcode:%s&quot;;

    private static final String HASH_FIELD_OAUTH_KEY = &quot;oauthKey&quot;;

    private static final String HASH_FIELD_USERID = &quot;userId&quot;;

    private static final String HASH_FIELD_STATUS = &quot;status&quot;;

    private static final String HASH_FIELD_TOKEN = &quot;token&quot;;


    private String redisKey(String oauthKey){
        return String.format(REDIS_KEY_PREFIX, oauthKey);
    }

    public void init(String oauthKey){
        add(oauthKey, null, QrcodeStatus.INIT.getStatus(), null);
    }

    public void scan(String oauthKey, Long userId){
        add(oauthKey, userId, QrcodeStatus.SCANNING.getStatus(), null);
    }

    public void cancel(String oauthKey){
        add(oauthKey, null, QrcodeStatus.CANCEL.getStatus(), null);
    }

    public void confirm(String oauthKey, Long userId, String token){
        add(oauthKey, userId, QrcodeStatus.CONFIRM.getStatus(), token);
    }

    public Boolean verify(String oauthKey){
        return redisTemplate.hasKey(redisKey(oauthKey));
    }

    public OauthTicket getStatus(String oauthKey){
        Map&lt;String, Object&gt; map = redisTemplate.opsForHash().entries(redisKey(oauthKey));
        if (map.isEmpty()){
            return null;
        }
        return JSONObject.parseObject(JSONObject.toJSONString(map), OauthTicket.class);
    }

    public void add(String oauthKey, Long userId, Integer status, String token){
        String redisKey = redisKey(oauthKey);
        Map&lt;String, Object&gt; map = Maps.newHashMapWithExpectedSize(3);
        map.put(HASH_FIELD_OAUTH_KEY, oauthKey);
        map.put(HASH_FIELD_STATUS, status);
        if (Objects.nonNull(userId)){
            map.put(HASH_FIELD_USERID, userId);
        }
        if (Objects.nonNull(token)){
            map.put(HASH_FIELD_TOKEN, token);
        }
        redisTemplate.opsForHash().putAll(redisKey, map);
        if (QrcodeStatus.INIT.getStatus().equals(status)){
            redisTemplate.expire(redisKey, TIME_OUT, TimeUnit.SECONDS);
            redisTemplate.expire(redisKey, webLoginConfig.getQrcodeTimeout(), TimeUnit.SECONDS);
        }
    }
}


@AllArgsConstructor
@NoArgsConstructor
public enum QrcodeStatus {
    INIT(1, &quot;初始状态&quot;),
    SCANNING(2, &quot;扫码中&quot;),
    CANCEL(3, &quot;取消&quot;),
    CONFIRM(4, &quot;确定登录&quot;),
    EXPIRE(5, &quot;过期&quot;);

    /**
     * 状态
     */
    @Getter
    private Integer status;

    /**
     * 描述
     */
    @Getter
    private String remark;
}

</code></pre>
<hr>
<h1>五.其他思考</h1>
<h3>轮询和websocket</h3>
<h4>轮询</h4>
<p>前端每隔固定时间向后台发送一次请求，查询新数据</p>
<p>优点：</p>
<ul>
<li>实现简单</li>
<li>没有兼容性问题</li>
</ul>
<p>缺点:</p>
<ul>
<li>延迟，需要固定的轮询时间，不一定是实时数据</li>
<li>大量耗费服务器内存和宽带资源，因为不停的请求服务器，很多时候 并没有新的数据更新，因此绝大部分请求都是无效请求</li>
</ul>
<h4>Websocket</h4>
<p>Webscoket是Web浏览器和服务器之间的一种全双工通信协议，其中WebSocket协议由IETF定为标准，WebSocket API由W3C定为标准。一旦Web客户端与服务器建立起连接，之后的全部数据通信都通过这个连接进行。通信过程中，可互相发送JSON、XML、HTML或图片等任意格式的数据。</p>
<ul>
<li>传统的http请求，其并发能力都是依赖同时发起多个TCP连接访问服务器实现的(因此并发数受限于浏览器允许的并发连接数)，而websocket则允许我们在一条ws连接上同时并发多个请求，即在A请求发出后A响应还未到达，就可以继续发出B请求。由于TCP的慢启动特性（新连接速度上来是需要时间的），以及连接本身的握手损耗，都使得websocket协议的这一特性有很大的效率提升。</li>
<li>http协议的头部太大，且每个请求携带的几百上千字节的头部大部分是重复的，很多时候可能响应都远没有请求中的header空间大。如此多无效的内容传递是因为无法利用上一条请求内容，websocket则因为复用长连接而没有这一问题。</li>
<li>websocket支持服务器推送消息，这带来了及时消息通知的更好体验，也是ajax请求无法达到的。</li>
</ul>
<p>缺点</p>
<ul>
<li>服务器长期维护长连接需要一定的成本</li>
<li>各个浏览器支持程度不一</li>
<li>websocket 是长连接，受网络限制比较大，需要处理好重连，比如用户进电梯或电信用户打个电话网断了，这时候就需要重连</li>
</ul>
]]></description>
      <content:encoded><![CDATA[<h1>APP扫码登录：不只有原理，直接上代码</h1>
<h2>一.背景</h2>
<p>扫码登录前提是要有APP端，和PC端。其目的是为了让用户在使用的PC端时登录更加方便和安全，使用手机扫一扫就可以登录和使用服务。</p>
<p>还有一个重要原因就是引导用户下载APP。</p>
<p>前段时间正好接到这么一个需求，设计和相关代码都会在本文贴出来。</p>
<p>话不多说，进入正题。</p>
<hr>
<h2>二.原理和时序图</h2>
<p><img src="https://vansiit.cc/img/qrcode/scan-qrcode.jpg" alt="img.png"></p>
<blockquote>
<p>二维码状态</p>
<ul>
<li>INIT(1, “初始状态”),</li>
<li>SCANNING(2, “扫码中”),</li>
<li>CANCEL(3, “取消”),</li>
<li>CONFIRM(4, “确定登录”),</li>
<li>EXPIRE(5, “过期”);</li>
</ul>
</blockquote>
<ol>
<li>
<p>用户在PC端请求二维码信息</p>
</li>
<li>
<p>服务端初始化二维码信息
生成二维码唯一的oauthKey，存储到Redis，设置过期时间，这里我配置的60S，过期时间看自己需求。
这时候二维码的状态是 INIT</p>
</li>
<li>
<p>服务端返回二维码信息给PC端
返回约定好的信息给PC端，我们这里还有APP的跳转链接。
PC端拿到二维码信息后生成二维码展示给用户</p>
</li>
<li>
<p>PC端定时轮询二维码信息 PC端根据二维码的状态做想应的处理，比如：</p>
</li>
</ol>
<ul>
<li>扫码中  -&gt;  置灰二维码，</li>
<li>取消    -&gt; 弹框提示和刷新二维码</li>
<li>确定登录 -&gt; 拿到token，调取用户信息接口，取消二维码页面</li>
<li>过期    -&gt; 弹框提示和刷新二维码</li>
</ul>
<ol start="5">
<li>
<p>服务端返回此二维码的状态和登录信息</p>
</li>
<li>
<p>用户使用APP扫描二维码</p>
</li>
<li>
<p>APP解析二维码，获取oauthkey</p>
</li>
<li>
<p>APP使用oauthkey校验二维码信息</p>
</li>
<li>
<p>查询二维码信息是否有效，是否过期</p>
</li>
<li>
<p>返回校验结果给到APP
APP根据校验结果，有效就弹出确认框或者取消框，无效就弹框提示</p>
</li>
<li>
<p>确认登录
用户在APP上点击确认登录按钮，确认登录</p>
</li>
<li>
<p>服务端登录逻辑</p>
</li>
</ol>
<ul>
<li>更新二维码状态为确认登录</li>
<li>判断APP的用户的登录状态</li>
<li>判断登录的设备个数</li>
<li>生成token , 记录想应的缓存信息</li>
</ul>
<ol start="13">
<li>
<p>第4步的轮询接口返回登录token</p>
</li>
<li>
<p>PC端使用token请求获取用户信息，和其他接口</p>
</li>
<li>
<p>登录完成</p>
</li>
</ol>
<hr>
<h2>三.代码</h2>
<h3>controller</h3>
<pre><code class="language-java">@Slf4j
@RestController
@RequestMapping(value = &quot;/user/qrcode&quot;)
@Api(tags = &quot;WEB端扫码API&quot;)
public class ScanQrcodeLoginController {
    @Autowired
    private ScanQrcodeLoginService scanQrcodeLoginService;

    /**
     * 获取二维码链接和信息
     */
    @ApiOperation(value = &quot;获取二维码链接和信息&quot;)
    @GetMapping(value = &quot;/info&quot;)
    public ResultResponse&lt;LoginUrlVO&gt; getQrcodeInfo(){
        return ResultResponse.success(scanQrcodeLoginService.getQrcodeInfo());
    }

    /**
     * 获取二维码扫描状态
     */
    @ApiOperation(value = &quot;获取二维码扫描状态&quot;)
    @GetMapping(value = &quot;/state&quot;)
    public ResultResponse&lt;QrcodeStateVO&gt; getQrcodeState(@RequestParam(value = &quot;oauthKey&quot;) String oauthKey){
        return ResultResponse.success(scanQrcodeLoginService.getQrcodeState(oauthKey));
    }
}


@Slf4j
@RestController
@RequestMapping(value = &quot;/user/app/qrcode&quot;)
@Api(tags = &quot;APP端扫码API&quot;)
public class AppScanQrcodeLoginController extends BaseController {
    @Autowired
    private ScanQrcodeLoginService scanQrcodeLoginService;

    /**
     * 二维码扫描
     */
    @ApiOperation(value = &quot;二维码扫描&quot;)
    @GetMapping(value = &quot;/scan&quot;)
    public ResultResponse&lt;QrcodeScanResultVO&gt; scan(@RequestParam(value = &quot;oauthKey&quot;) String oauthKey){
        Long userId = getCurrentUserId(false);
        return ResultResponse.success(scanQrcodeLoginService.scan(oauthKey, userId));
    }

    /**
     * 二维码取消
     */
    @ApiOperation(value = &quot;二维码取消&quot;)
    @GetMapping(value = &quot;/cancel&quot;)
    public ResultResponse&lt;QrcodeCancelResultVO&gt; cancel(@RequestParam(value = &quot;oauthKey&quot;) String oauthKey){
        Long userId = getCurrentUserId(true);
        return ResultResponse.success(scanQrcodeLoginService.cancel(oauthKey, userId));
    }

    /**
     * 二维码确定登录
     */
    @ApiOperation(value = &quot;二维码确定登录&quot;)
    @GetMapping(value = &quot;/confirm&quot;)
    public ResultResponse&lt;QrcodeConfirmResultVO&gt; confirm(@RequestParam(value = &quot;oauthKey&quot;) String oauthKey){
        Long userId = getCurrentUserId(true);
        return ResultResponse.success(scanQrcodeLoginService.confirm(oauthKey, userId));
    }
}

@Slf4j
@RestController
@RequestMapping(value = &quot;/user/web/login&quot;)
@Api(tags = &quot;WEB端-登录接口&quot;)
public class WebLoginController extends BaseController {

    @Autowired
    private WebUserLoginService webUserLoginService;

    /**
     * 获取登录用户信息
     */
    @ApiOperation(value = &quot;获取登录用户信息&quot;)
    @GetMapping(value = &quot;/info&quot;)
    public ResultResponse&lt;WebUserInfoVo&gt; getWebUserInfo() {
        Long userId = getCurrentUserId(true);
        return ResultResponse.success(webUserLoginService.getWebUserInfo(userId));
    }

    /**
     * 退出登录
     */
    @ApiOperation(value = &quot;退出登录&quot;)
    @GetMapping(value = &quot;/logout&quot;)
    public ResultResponse&lt;Void&gt; webLogout() throws UserRequestParameterException {
        Long userId = getCurrentUserId(true);
        String token = HttpUtils.getToken();
        log.info(&quot;web logout userId: {}, token: {}&quot;, userId, token);
        webUserLoginService.webLogout(userId, token);
        return ResultResponse.success();
    }
}
</code></pre>
<h3>service</h3>
<pre><code class="language-java">@Service
@Slf4j
public class ScanQrcodeLoginServiceImpl implements ScanQrcodeLoginService {

    @Autowired
    private OauthKeyHelp oauthKeyHelp;

    @Autowired
    private WebLoginConfig webLoginConfig;

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public LoginUrlVO getQrcodeInfo() {
        String oauthKey = UUID.randomUUID().toString().replaceAll(&quot;-&quot;, &quot;&quot;);
        LoginUrlVO loginUrlVO = new LoginUrlVO();
        loginUrlVO.setOauthKey(oauthKey);
        loginUrlVO.setUrl(webLoginConfig.getQrcodeUrl() + oauthKey);
        oauthKeyHelp.init(oauthKey);
        return loginUrlVO;
    }

    @Override
    public QrcodeStateVO getQrcodeState(String oauthKey) {
        QrcodeStateVO stateVO = new QrcodeStateVO();
        if (!oauthKeyHelp.verify(oauthKey)) {
            stateVO.setStatus(QrcodeStatus.EXPIRE.getStatus());
            return stateVO;
        }
        OauthTicket oauthTicket = oauthKeyHelp.getStatus(oauthKey);
        if (Objects.isNull(oauthTicket)) {
            stateVO.setStatus(QrcodeStatus.EXPIRE.getStatus());
            return stateVO;
        }
        stateVO.setStatus(oauthTicket.getStatus());
        stateVO.setJwtToken(oauthTicket.getToken());
        stateVO.setUserId(oauthTicket.getUserId());
        return stateVO;
    }

    @Override
    public QrcodeScanResultVO scan(String oauthKey, Long userId) {
        QrcodeScanResultVO resultVO = new QrcodeScanResultVO();
        if (!oauthKeyHelp.verify(oauthKey)) {
            resultVO.setStatus(Boolean.FALSE);
            return resultVO;
        }
        oauthKeyHelp.scan(oauthKey, userId);
        resultVO.setStatus(Boolean.TRUE);
        return resultVO;
    }

    @Override
    public QrcodeCancelResultVO cancel(String oauthKey, Long userId) {
        QrcodeCancelResultVO resultVO = new QrcodeCancelResultVO();
        if (!oauthKeyHelp.verify(oauthKey)) {
            resultVO.setStatus(Boolean.FALSE);
            return resultVO;
        }
        oauthKeyHelp.cancel(oauthKey);
        resultVO.setStatus(Boolean.TRUE);
        return resultVO;
    }

    @Override
    public QrcodeConfirmResultVO confirm(String oauthKey, Long userId) {
        QrcodeConfirmResultVO resultVO = new QrcodeConfirmResultVO();
        String lockKey = &quot;web:user:login:lock:oauthKey:&quot; + oauthKey;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            if (lock.isLocked() &amp;&amp; !lock.isHeldByCurrentThread()) {
                resultVO.setStatus(Boolean.FALSE);
                return resultVO;
            }
            lock.lock(10, TimeUnit.SECONDS);

            OauthTicket oauthTicket = oauthKeyHelp.getStatus(oauthKey);
            if (Objects.isNull(oauthTicket) || (Objects.equals(QrcodeStatus.CONFIRM.getStatus(), oauthTicket.getStatus()) || Objects.equals(QrcodeStatus.EXPIRE.getStatus(), oauthTicket.getStatus()))) {
                resultVO.setStatus(Boolean.FALSE);
                return resultVO;
            }
            resultVO.setStatus(Boolean.TRUE);
            // 登录逻辑，自己实现
            String token = getToken(userId, oauthTicket);
            oauthKeyHelp.confirm(oauthKey, userId, token);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            resultVO.setStatus(Boolean.FALSE);
            return resultVO;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        return resultVO;
    }
}


@Component
@Slf4j
public class OauthKeyHelp {
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private WebLoginConfig webLoginConfig;

    private static final Integer TIME_OUT = 300;

    private static final String REDIS_KEY_PREFIX = &quot;qrcode:%s&quot;;

    private static final String HASH_FIELD_OAUTH_KEY = &quot;oauthKey&quot;;

    private static final String HASH_FIELD_USERID = &quot;userId&quot;;

    private static final String HASH_FIELD_STATUS = &quot;status&quot;;

    private static final String HASH_FIELD_TOKEN = &quot;token&quot;;


    private String redisKey(String oauthKey){
        return String.format(REDIS_KEY_PREFIX, oauthKey);
    }

    public void init(String oauthKey){
        add(oauthKey, null, QrcodeStatus.INIT.getStatus(), null);
    }

    public void scan(String oauthKey, Long userId){
        add(oauthKey, userId, QrcodeStatus.SCANNING.getStatus(), null);
    }

    public void cancel(String oauthKey){
        add(oauthKey, null, QrcodeStatus.CANCEL.getStatus(), null);
    }

    public void confirm(String oauthKey, Long userId, String token){
        add(oauthKey, userId, QrcodeStatus.CONFIRM.getStatus(), token);
    }

    public Boolean verify(String oauthKey){
        return redisTemplate.hasKey(redisKey(oauthKey));
    }

    public OauthTicket getStatus(String oauthKey){
        Map&lt;String, Object&gt; map = redisTemplate.opsForHash().entries(redisKey(oauthKey));
        if (map.isEmpty()){
            return null;
        }
        return JSONObject.parseObject(JSONObject.toJSONString(map), OauthTicket.class);
    }

    public void add(String oauthKey, Long userId, Integer status, String token){
        String redisKey = redisKey(oauthKey);
        Map&lt;String, Object&gt; map = Maps.newHashMapWithExpectedSize(3);
        map.put(HASH_FIELD_OAUTH_KEY, oauthKey);
        map.put(HASH_FIELD_STATUS, status);
        if (Objects.nonNull(userId)){
            map.put(HASH_FIELD_USERID, userId);
        }
        if (Objects.nonNull(token)){
            map.put(HASH_FIELD_TOKEN, token);
        }
        redisTemplate.opsForHash().putAll(redisKey, map);
        if (QrcodeStatus.INIT.getStatus().equals(status)){
            redisTemplate.expire(redisKey, TIME_OUT, TimeUnit.SECONDS);
            redisTemplate.expire(redisKey, webLoginConfig.getQrcodeTimeout(), TimeUnit.SECONDS);
        }
    }
}


@AllArgsConstructor
@NoArgsConstructor
public enum QrcodeStatus {
    INIT(1, &quot;初始状态&quot;),
    SCANNING(2, &quot;扫码中&quot;),
    CANCEL(3, &quot;取消&quot;),
    CONFIRM(4, &quot;确定登录&quot;),
    EXPIRE(5, &quot;过期&quot;);

    /**
     * 状态
     */
    @Getter
    private Integer status;

    /**
     * 描述
     */
    @Getter
    private String remark;
}

</code></pre>
<hr>
<h1>五.其他思考</h1>
<h3>轮询和websocket</h3>
<h4>轮询</h4>
<p>前端每隔固定时间向后台发送一次请求，查询新数据</p>
<p>优点：</p>
<ul>
<li>实现简单</li>
<li>没有兼容性问题</li>
</ul>
<p>缺点:</p>
<ul>
<li>延迟，需要固定的轮询时间，不一定是实时数据</li>
<li>大量耗费服务器内存和宽带资源，因为不停的请求服务器，很多时候 并没有新的数据更新，因此绝大部分请求都是无效请求</li>
</ul>
<h4>Websocket</h4>
<p>Webscoket是Web浏览器和服务器之间的一种全双工通信协议，其中WebSocket协议由IETF定为标准，WebSocket API由W3C定为标准。一旦Web客户端与服务器建立起连接，之后的全部数据通信都通过这个连接进行。通信过程中，可互相发送JSON、XML、HTML或图片等任意格式的数据。</p>
<ul>
<li>传统的http请求，其并发能力都是依赖同时发起多个TCP连接访问服务器实现的(因此并发数受限于浏览器允许的并发连接数)，而websocket则允许我们在一条ws连接上同时并发多个请求，即在A请求发出后A响应还未到达，就可以继续发出B请求。由于TCP的慢启动特性（新连接速度上来是需要时间的），以及连接本身的握手损耗，都使得websocket协议的这一特性有很大的效率提升。</li>
<li>http协议的头部太大，且每个请求携带的几百上千字节的头部大部分是重复的，很多时候可能响应都远没有请求中的header空间大。如此多无效的内容传递是因为无法利用上一条请求内容，websocket则因为复用长连接而没有这一问题。</li>
<li>websocket支持服务器推送消息，这带来了及时消息通知的更好体验，也是ajax请求无法达到的。</li>
</ul>
<p>缺点</p>
<ul>
<li>服务器长期维护长连接需要一定的成本</li>
<li>各个浏览器支持程度不一</li>
<li>websocket 是长连接，受网络限制比较大，需要处理好重连，比如用户进电梯或电信用户打个电话网断了，这时候就需要重连</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>平滑加权轮询算法详解：Smooth Weighted Round Robin实现与应用</title>
      <link>https://vansiit.cc/2023/06/15/weighted-round-robin.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2023/06/15/weighted-round-robin.html</guid>
      <pubDate>Thu, 15 Jun 2023 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>平滑加权轮询算法，Smooth Weighted Round Robin</h1>
<h2>一.加权轮询算法</h2>
<p>引用自：维基百科，<a href="https://zh.wikipedia.org/wiki/%E5%8A%A0%E6%9D%83%E8%BD%AE%E8%AF%A2%E7%AE%97%E6%B3%95">加权轮询算法</a></p>
<blockquote>
<p>加权轮询（ Weighted round robin ）是网络中用于调度数据流的算法，也可用于调度进程。</p>
<p>加权轮询是轮询调度的一般化。加权轮询在队列或一系列任务上循环，每个轮次中各数据包或进程按权重获得运行机会。</p>
</blockquote>
<p>所谓的加权轮询也就是在配置服务器列表时，给每一台服务器配置一个权重值。</p>
<p>举个例子：现在有3台服务器(A:3)(B:2)(C:1)，数字分别代表它们的权重值，数字越大表示所能承受的压力越大。将3台服务器的权重值相加3+2+1=6,也就是说现在每6个请求进来其中会有3个分配个A，2个分配个B，剩下的一个分配给C，依次循环A-A-A-B-B-C-A-A-A-B-B-C…</p>
<h2>二.平滑的加权轮询算法</h2>
<p>所谓平滑，就是在一段时间内，不仅服务器被选择的次数的分布和它们的权重一致，而且调度算法还比较均匀的选择服务器，而不会集中一段时间之内只选择某一个权重比较高的服务器。</p>
<p>如果使用随机算法选择或者普通的基于权重的轮询算法，就比较容易造成某个服务集中被调用压力过大。</p>
<p>还是上面的例子：分配轮询的结果就是A-B-A-C-B-A A-B-A-C-B-A…</p>
<p>是不是一下子就看出来不同了</p>
<p>算法执行2步，选择出1个当前节点。</p>
<ol>
<li>每个节点，用它们的当前值加上它们自己的权重。</li>
<li>选择当前值最大的节点为选中节点，并把它的当前值减去所有节点的权重总和。</li>
</ol>
<pre><code class="language-java">package com.vansiit.wrr;

import java.util.Arrays;

/**
 * @Author: vansiit
 * @DateTime: 2023/3/24 17:29
 * @Description: 加权轮询算法
 */
public class WrrSmooth {
    final Wrr[] cachedWeights;
    public WrrSmooth(Element[] element) {
        this.cachedWeights = Arrays.stream(element).map(Wrr::new).toArray(Wrr[]::new);
    }

    class Wrr{
        Element ele;
        int current = 0;

        public Wrr(Element ele){
            this.ele = ele;
        }

        @Override
        public String toString() {
            return &quot;Wrr{&quot; +
                    &quot;ele=&quot; + ele +
                    &quot;, current=&quot; + current +
                    '}';
        }
    }

    //@Override
    public Wrr next() {
        int total = 0;
        Wrr shed = cachedWeights[0];
        for (Wrr cachedWeight : cachedWeights) {
            int weight = cachedWeight.ele.weight;
            total += weight;

            cachedWeight.current += weight;
            if (cachedWeight.current &gt; shed.current){
                shed = cachedWeight;
            }
        }
        shed.current -= total;
        return shed;
    }

    public static void main(String[] args) {
        Element[] elements = new Element[]{
                new Element(&quot;A&quot;, 7),
                new Element(&quot;B&quot;, 2),
                new Element(&quot;C&quot;, 1),
        };
        int count = 10;
        WrrSmooth wrr = new WrrSmooth(elements);

        for (int i = 0; i &lt; count; i++) {
            System.out.println(&quot;third&quot; + i + &quot;, &quot; + wrr.next().toString() + &quot;,&quot;);
        }

    }
}

</code></pre>
<table>
<thead>
<tr>
<th style="text-align:center">次数</th>
<th style="text-align:center">lastCurrentWeight+weight</th>
<th style="text-align:center">Max(currentWeight)</th>
<th style="text-align:center">return Max</th>
<th style="text-align:center">currentWeight - totalWeight</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">1</td>
<td style="text-align:center">7(A)  2(B)  1©</td>
<td style="text-align:center">7(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-3(A)  2(B)  1©</td>
</tr>
<tr>
<td style="text-align:center">2</td>
<td style="text-align:center">4(A)  4(B)  2©</td>
<td style="text-align:center">4(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-6(A)  4(B)  2©</td>
</tr>
<tr>
<td style="text-align:center">3</td>
<td style="text-align:center">1(A)  6(B)  3©</td>
<td style="text-align:center">6(B)</td>
<td style="text-align:center">B</td>
<td style="text-align:center">1(A)  -4(B)  3©</td>
</tr>
<tr>
<td style="text-align:center">4</td>
<td style="text-align:center">8(A)  -2(B)  4©</td>
<td style="text-align:center">8(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-2(A)  -2(B)  4©</td>
</tr>
<tr>
<td style="text-align:center">5</td>
<td style="text-align:center">5(A)  0(B)  5©</td>
<td style="text-align:center">5(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-5(A)  0(B)  5©</td>
</tr>
<tr>
<td style="text-align:center">6</td>
<td style="text-align:center">2(A)  2(B)  6©</td>
<td style="text-align:center">6©</td>
<td style="text-align:center">C</td>
<td style="text-align:center">2(A)  2(B)  -4©</td>
</tr>
<tr>
<td style="text-align:center">7</td>
<td style="text-align:center">9(A)  4(B)  -3©</td>
<td style="text-align:center">9(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-1(A)  4(B)  -3©</td>
</tr>
<tr>
<td style="text-align:center">8</td>
<td style="text-align:center">6(A)  6(B)  -2©</td>
<td style="text-align:center">6(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-4(A)  6(B)  -2©</td>
</tr>
<tr>
<td style="text-align:center">9</td>
<td style="text-align:center">3(A)  8(B)  -1©</td>
<td style="text-align:center">8(B)</td>
<td style="text-align:center">B</td>
<td style="text-align:center">3(A)  -2(B)  -1©</td>
</tr>
<tr>
<td style="text-align:center">10</td>
<td style="text-align:center">10(A)  0(B)  0©</td>
<td style="text-align:center">10(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">0(A)  0(B)  0©</td>
</tr>
</tbody>
</table>
<p>我们可以发现，A, B, C选择的次数符合7:2:1，而且权重大的不会被连接选择。10轮选择后， 当前值又回到0(A)  0(B)  0©，以上操作可以一直循环，一样符合平滑和基于权重。</p>
<p>参考资料：</p>
<p><a href="https://tenfy.cn/2018/11/12/smooth-weighted-round-robin/">nginx平滑的基于权重轮询算法分析</a></p>
<p><a href="https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35">Upstream: smooth weighted round-robin balancing</a></p>
]]></description>
      <content:encoded><![CDATA[<h1>平滑加权轮询算法，Smooth Weighted Round Robin</h1>
<h2>一.加权轮询算法</h2>
<p>引用自：维基百科，<a href="https://zh.wikipedia.org/wiki/%E5%8A%A0%E6%9D%83%E8%BD%AE%E8%AF%A2%E7%AE%97%E6%B3%95">加权轮询算法</a></p>
<blockquote>
<p>加权轮询（ Weighted round robin ）是网络中用于调度数据流的算法，也可用于调度进程。</p>
<p>加权轮询是轮询调度的一般化。加权轮询在队列或一系列任务上循环，每个轮次中各数据包或进程按权重获得运行机会。</p>
</blockquote>
<p>所谓的加权轮询也就是在配置服务器列表时，给每一台服务器配置一个权重值。</p>
<p>举个例子：现在有3台服务器(A:3)(B:2)(C:1)，数字分别代表它们的权重值，数字越大表示所能承受的压力越大。将3台服务器的权重值相加3+2+1=6,也就是说现在每6个请求进来其中会有3个分配个A，2个分配个B，剩下的一个分配给C，依次循环A-A-A-B-B-C-A-A-A-B-B-C…</p>
<h2>二.平滑的加权轮询算法</h2>
<p>所谓平滑，就是在一段时间内，不仅服务器被选择的次数的分布和它们的权重一致，而且调度算法还比较均匀的选择服务器，而不会集中一段时间之内只选择某一个权重比较高的服务器。</p>
<p>如果使用随机算法选择或者普通的基于权重的轮询算法，就比较容易造成某个服务集中被调用压力过大。</p>
<p>还是上面的例子：分配轮询的结果就是A-B-A-C-B-A A-B-A-C-B-A…</p>
<p>是不是一下子就看出来不同了</p>
<p>算法执行2步，选择出1个当前节点。</p>
<ol>
<li>每个节点，用它们的当前值加上它们自己的权重。</li>
<li>选择当前值最大的节点为选中节点，并把它的当前值减去所有节点的权重总和。</li>
</ol>
<pre><code class="language-java">package com.vansiit.wrr;

import java.util.Arrays;

/**
 * @Author: vansiit
 * @DateTime: 2023/3/24 17:29
 * @Description: 加权轮询算法
 */
public class WrrSmooth {
    final Wrr[] cachedWeights;
    public WrrSmooth(Element[] element) {
        this.cachedWeights = Arrays.stream(element).map(Wrr::new).toArray(Wrr[]::new);
    }

    class Wrr{
        Element ele;
        int current = 0;

        public Wrr(Element ele){
            this.ele = ele;
        }

        @Override
        public String toString() {
            return &quot;Wrr{&quot; +
                    &quot;ele=&quot; + ele +
                    &quot;, current=&quot; + current +
                    '}';
        }
    }

    //@Override
    public Wrr next() {
        int total = 0;
        Wrr shed = cachedWeights[0];
        for (Wrr cachedWeight : cachedWeights) {
            int weight = cachedWeight.ele.weight;
            total += weight;

            cachedWeight.current += weight;
            if (cachedWeight.current &gt; shed.current){
                shed = cachedWeight;
            }
        }
        shed.current -= total;
        return shed;
    }

    public static void main(String[] args) {
        Element[] elements = new Element[]{
                new Element(&quot;A&quot;, 7),
                new Element(&quot;B&quot;, 2),
                new Element(&quot;C&quot;, 1),
        };
        int count = 10;
        WrrSmooth wrr = new WrrSmooth(elements);

        for (int i = 0; i &lt; count; i++) {
            System.out.println(&quot;third&quot; + i + &quot;, &quot; + wrr.next().toString() + &quot;,&quot;);
        }

    }
}

</code></pre>
<table>
<thead>
<tr>
<th style="text-align:center">次数</th>
<th style="text-align:center">lastCurrentWeight+weight</th>
<th style="text-align:center">Max(currentWeight)</th>
<th style="text-align:center">return Max</th>
<th style="text-align:center">currentWeight - totalWeight</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">1</td>
<td style="text-align:center">7(A)  2(B)  1©</td>
<td style="text-align:center">7(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-3(A)  2(B)  1©</td>
</tr>
<tr>
<td style="text-align:center">2</td>
<td style="text-align:center">4(A)  4(B)  2©</td>
<td style="text-align:center">4(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-6(A)  4(B)  2©</td>
</tr>
<tr>
<td style="text-align:center">3</td>
<td style="text-align:center">1(A)  6(B)  3©</td>
<td style="text-align:center">6(B)</td>
<td style="text-align:center">B</td>
<td style="text-align:center">1(A)  -4(B)  3©</td>
</tr>
<tr>
<td style="text-align:center">4</td>
<td style="text-align:center">8(A)  -2(B)  4©</td>
<td style="text-align:center">8(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-2(A)  -2(B)  4©</td>
</tr>
<tr>
<td style="text-align:center">5</td>
<td style="text-align:center">5(A)  0(B)  5©</td>
<td style="text-align:center">5(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-5(A)  0(B)  5©</td>
</tr>
<tr>
<td style="text-align:center">6</td>
<td style="text-align:center">2(A)  2(B)  6©</td>
<td style="text-align:center">6©</td>
<td style="text-align:center">C</td>
<td style="text-align:center">2(A)  2(B)  -4©</td>
</tr>
<tr>
<td style="text-align:center">7</td>
<td style="text-align:center">9(A)  4(B)  -3©</td>
<td style="text-align:center">9(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-1(A)  4(B)  -3©</td>
</tr>
<tr>
<td style="text-align:center">8</td>
<td style="text-align:center">6(A)  6(B)  -2©</td>
<td style="text-align:center">6(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">-4(A)  6(B)  -2©</td>
</tr>
<tr>
<td style="text-align:center">9</td>
<td style="text-align:center">3(A)  8(B)  -1©</td>
<td style="text-align:center">8(B)</td>
<td style="text-align:center">B</td>
<td style="text-align:center">3(A)  -2(B)  -1©</td>
</tr>
<tr>
<td style="text-align:center">10</td>
<td style="text-align:center">10(A)  0(B)  0©</td>
<td style="text-align:center">10(A)</td>
<td style="text-align:center">A</td>
<td style="text-align:center">0(A)  0(B)  0©</td>
</tr>
</tbody>
</table>
<p>我们可以发现，A, B, C选择的次数符合7:2:1，而且权重大的不会被连接选择。10轮选择后， 当前值又回到0(A)  0(B)  0©，以上操作可以一直循环，一样符合平滑和基于权重。</p>
<p>参考资料：</p>
<p><a href="https://tenfy.cn/2018/11/12/smooth-weighted-round-robin/">nginx平滑的基于权重轮询算法分析</a></p>
<p><a href="https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35">Upstream: smooth weighted round-robin balancing</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>使用NanoID替换整型ID：防爬虫与数据安全实战方案</title>
      <link>https://vansiit.cc/2023/06/12/NanoID-mapping.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2023/06/12/NanoID-mapping.html</guid>
      <pubDate>Mon, 12 Jun 2023 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>使用NanoID替换整型ID</h1>
<h2>背景介绍</h2>
<p>接口使用自增长整型作为唯一ID，数据和缓存查询都是用ID获取是常规方案。随着公司业务的发展，少数不法分子通过爬虫抓取接口数据，作为非法盈利之用。本司主营业务是长视频播放服务，存储和CND费用更加昂贵。其中一种爬取方案是，自增长ID轮询接口数据，所以此文的目的就是把所有暴露出来的资源ID都替换成随机字符串。</p>
<p>诚然，此方案防爬效果有限。须配合多种方案，验签、加密、限流、用户行为记录、CND加密、二次校验，这些不在此文讨论范围。</p>
<h2>具体方案</h2>
<p>网站主要涉及到电影剧集短视频，片单，文章资讯，复杂度在数据不仅要落库，还需要处理现有的缓存数据。</p>
<h3>一.电影剧集ID</h3>
<h4>1.增加映射表 cms_content_id_mapping</h4>
<pre><code class="language-sql">CREATE TABLE `cms_content_id_mapping` (
  `id` varchar(25) NOT NULL COMMENT 'id',
  `category` TINYINT(4) NOT NULL COMMENT '内容类型，0,电影，1剧集，2短视频',
  `mapping_id` varchar(25) NOT NULL COMMENT '映射ID',
  PRIMARY KEY (`id`, `category`) USING BTREE,
  KEY `idx_mapping_id` (`mapping_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资源ID映射表';
</code></pre>
<h4>2.增加双向缓存</h4>
<p>原缓存不动
使用hash存储mappingId和contentId的双向关系，并且可以批量获取
详情见 groot.cms.cache.ContentMappingCache</p>
<h4>3.获取电影剧集详情接口修改如下</h4>
<pre><code class="language-java">if (StringUtils.isNumeric(id)) {
    // 增加配置开关，暂时保留旧的ID
    if (!websiteMappingConfig.getOldIdIsOpen()){
        throw new RequestParameterException();
    }
} else {
    id = contentMappingCache.getContentIdCacheByMappingId(id, category);
}
</code></pre>
<h3>二.片单ID</h3>
<h4>1.增加字段 mapping_id</h4>
<pre><code class="language-sql">alter table cms_album add column `mapping_id` varchar(25) NOT NULL COMMENT '映射ID' after id;
</code></pre>
<h4>2.新增索引</h4>
<pre><code class="language-sql">alter table cms_album add index idx_mapping_id(mapping_id) USING BTREE;
</code></pre>
<h4>3.新增缓存</h4>
<h4>4.片单缓存增加字段mappingId</h4>
<h3>三.文章资讯ID</h3>
<pre><code>文章资讯不需要处理缓存数据
</code></pre>
<h4>1.新增字段 mapping_id</h4>
<pre><code class="language-sql">alter table cms_news add column `mapping_id` varchar(25) NOT NULL COMMENT '映射ID' after sort;
</code></pre>
<h4>2.新增索引</h4>
<pre><code class="language-sql">alter table cms_news add index idx_mapping_id(mapping_id) USING BTREE;
</code></pre>
<h3>四.数据和缓存初始化</h3>
<pre><code class="language-java">public void initData(){
    StopWatch stopWatch = new StopWatch();

    // contentMappingCache
    for (ContentCategoryEnum categoryEnum : ContentCategoryEnum.values()) {
        stopWatch.start(categoryEnum.getDesc() + &quot;初始化 &quot;);
        initMovieMapping(categoryEnum);
        stopWatch.stop();
    }

    // alumCache
    stopWatch.start(&quot;片单初始化&quot;);
    initAlbumMapping();
    stopWatch.stop();

    // cmsNews
    stopWatch.start(&quot;新闻资讯初始化&quot;);
    initNewsMapping();
    stopWatch.stop();

    log.info(stopWatch.prettyPrint());
}
</code></pre>
<h3>五.修改原来的sitemap.xml相关接口</h3>
<p>修改原有ID生成逻辑，其他保持不变</p>
<h3>六.映射ID生成逻辑</h3>
<p>鉴于UUID长度太长，这里的mappingId是用NanoId。
修改默认字符集为“0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”，去除默认的“_-”，防止和URL中的字符冲突，长度使用默认的21位
具体代码如下：</p>
<pre><code class="language-java">public class NanoIdUtil {
    public static final SecureRandom DEFAULT_NUMBER_GENERATOR = new SecureRandom();

    /**
     * The default alphabet used by this class.
     * Creates url-friendly NanoId Strings using 64 unique symbols.
     */
    public static final char[] DEFAULT_ALPHABET = &quot;0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;.toCharArray();

    /**
     * The default size used by this class.
     * Creates NanoId Strings with slightly more unique values than UUID v4.
     */
    public static final int DEFAULT_SIZE = 21;

    public static String randomNanoId(){
        return NanoIdUtils.randomNanoId(DEFAULT_NUMBER_GENERATOR, DEFAULT_ALPHABET, DEFAULT_SIZE);
    }
}
</code></pre>
<p>使用方法如下：</p>
<pre><code class="language-java">public static void main(String[] args) {
    System.out.println(NanoIdUtil.randomNanoId());
}
</code></pre>
<p>Maven 坐标：</p>
<pre><code>&lt;dependency&gt;
  &lt;groupId&gt;com.aventrix.jnanoid&lt;/groupId&gt;
  &lt;artifactId&gt;jnanoid&lt;/artifactId&gt;
  &lt;version&gt;2.0.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>修改后的网站url效果如下</p>
<pre><code class="language-java">http://xxxxxx.com/detail/movie/76Y3TWpZDR8aOhbd19cyT-Double-Dad
http://xxxxxx.com/collection/QXUGQIYQcYKM1jDSwY83G-afdfadggd?name=qins
</code></pre>
<h3>七.参考资料：</h3>
<ul>
<li><a href="https://vansiit.cc/2023/06/12/NanoId.html">github NanoId</a></li>
</ul>
]]></description>
      <content:encoded><![CDATA[<h1>使用NanoID替换整型ID</h1>
<h2>背景介绍</h2>
<p>接口使用自增长整型作为唯一ID，数据和缓存查询都是用ID获取是常规方案。随着公司业务的发展，少数不法分子通过爬虫抓取接口数据，作为非法盈利之用。本司主营业务是长视频播放服务，存储和CND费用更加昂贵。其中一种爬取方案是，自增长ID轮询接口数据，所以此文的目的就是把所有暴露出来的资源ID都替换成随机字符串。</p>
<p>诚然，此方案防爬效果有限。须配合多种方案，验签、加密、限流、用户行为记录、CND加密、二次校验，这些不在此文讨论范围。</p>
<h2>具体方案</h2>
<p>网站主要涉及到电影剧集短视频，片单，文章资讯，复杂度在数据不仅要落库，还需要处理现有的缓存数据。</p>
<h3>一.电影剧集ID</h3>
<h4>1.增加映射表 cms_content_id_mapping</h4>
<pre><code class="language-sql">CREATE TABLE `cms_content_id_mapping` (
  `id` varchar(25) NOT NULL COMMENT 'id',
  `category` TINYINT(4) NOT NULL COMMENT '内容类型，0,电影，1剧集，2短视频',
  `mapping_id` varchar(25) NOT NULL COMMENT '映射ID',
  PRIMARY KEY (`id`, `category`) USING BTREE,
  KEY `idx_mapping_id` (`mapping_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资源ID映射表';
</code></pre>
<h4>2.增加双向缓存</h4>
<p>原缓存不动
使用hash存储mappingId和contentId的双向关系，并且可以批量获取
详情见 groot.cms.cache.ContentMappingCache</p>
<h4>3.获取电影剧集详情接口修改如下</h4>
<pre><code class="language-java">if (StringUtils.isNumeric(id)) {
    // 增加配置开关，暂时保留旧的ID
    if (!websiteMappingConfig.getOldIdIsOpen()){
        throw new RequestParameterException();
    }
} else {
    id = contentMappingCache.getContentIdCacheByMappingId(id, category);
}
</code></pre>
<h3>二.片单ID</h3>
<h4>1.增加字段 mapping_id</h4>
<pre><code class="language-sql">alter table cms_album add column `mapping_id` varchar(25) NOT NULL COMMENT '映射ID' after id;
</code></pre>
<h4>2.新增索引</h4>
<pre><code class="language-sql">alter table cms_album add index idx_mapping_id(mapping_id) USING BTREE;
</code></pre>
<h4>3.新增缓存</h4>
<h4>4.片单缓存增加字段mappingId</h4>
<h3>三.文章资讯ID</h3>
<pre><code>文章资讯不需要处理缓存数据
</code></pre>
<h4>1.新增字段 mapping_id</h4>
<pre><code class="language-sql">alter table cms_news add column `mapping_id` varchar(25) NOT NULL COMMENT '映射ID' after sort;
</code></pre>
<h4>2.新增索引</h4>
<pre><code class="language-sql">alter table cms_news add index idx_mapping_id(mapping_id) USING BTREE;
</code></pre>
<h3>四.数据和缓存初始化</h3>
<pre><code class="language-java">public void initData(){
    StopWatch stopWatch = new StopWatch();

    // contentMappingCache
    for (ContentCategoryEnum categoryEnum : ContentCategoryEnum.values()) {
        stopWatch.start(categoryEnum.getDesc() + &quot;初始化 &quot;);
        initMovieMapping(categoryEnum);
        stopWatch.stop();
    }

    // alumCache
    stopWatch.start(&quot;片单初始化&quot;);
    initAlbumMapping();
    stopWatch.stop();

    // cmsNews
    stopWatch.start(&quot;新闻资讯初始化&quot;);
    initNewsMapping();
    stopWatch.stop();

    log.info(stopWatch.prettyPrint());
}
</code></pre>
<h3>五.修改原来的sitemap.xml相关接口</h3>
<p>修改原有ID生成逻辑，其他保持不变</p>
<h3>六.映射ID生成逻辑</h3>
<p>鉴于UUID长度太长，这里的mappingId是用NanoId。
修改默认字符集为“0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”，去除默认的“_-”，防止和URL中的字符冲突，长度使用默认的21位
具体代码如下：</p>
<pre><code class="language-java">public class NanoIdUtil {
    public static final SecureRandom DEFAULT_NUMBER_GENERATOR = new SecureRandom();

    /**
     * The default alphabet used by this class.
     * Creates url-friendly NanoId Strings using 64 unique symbols.
     */
    public static final char[] DEFAULT_ALPHABET = &quot;0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;.toCharArray();

    /**
     * The default size used by this class.
     * Creates NanoId Strings with slightly more unique values than UUID v4.
     */
    public static final int DEFAULT_SIZE = 21;

    public static String randomNanoId(){
        return NanoIdUtils.randomNanoId(DEFAULT_NUMBER_GENERATOR, DEFAULT_ALPHABET, DEFAULT_SIZE);
    }
}
</code></pre>
<p>使用方法如下：</p>
<pre><code class="language-java">public static void main(String[] args) {
    System.out.println(NanoIdUtil.randomNanoId());
}
</code></pre>
<p>Maven 坐标：</p>
<pre><code>&lt;dependency&gt;
  &lt;groupId&gt;com.aventrix.jnanoid&lt;/groupId&gt;
  &lt;artifactId&gt;jnanoid&lt;/artifactId&gt;
  &lt;version&gt;2.0.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>修改后的网站url效果如下</p>
<pre><code class="language-java">http://xxxxxx.com/detail/movie/76Y3TWpZDR8aOhbd19cyT-Double-Dad
http://xxxxxx.com/collection/QXUGQIYQcYKM1jDSwY83G-afdfadggd?name=qins
</code></pre>
<h3>七.参考资料：</h3>
<ul>
<li><a href="https://vansiit.cc/2023/06/12/NanoId.html">github NanoId</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>ID生成器全面对比：UUID vs SnowFlake vs NanoId性能分析</title>
      <link>https://vansiit.cc/2023/06/12/NanoId.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2023/06/12/NanoId.html</guid>
      <pubDate>Mon, 12 Jun 2023 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>各种常用ID生成器的对比，重点介绍一下NanoId</h1>
<h2>一.UUID</h2>
<h3>优点</h3>
<p>使用UUID作为主键具有以下优点：</p>
<h4>1.分布式ID</h4>
<p>UUID值在表，数据库甚至在服务器上都是唯一的，允许您从不同数据库合并行或跨服务器分发数据库。</p>
<h4>2.安全性</h4>
<p>UUID值不会公开有关数据的信息，因此在URL中使用更安全。</p>
<h4>3.通用性</h4>
<p>可以在避免往返数据库服务器的任何地方生成UUID值。它也简化了应用程序中的逻辑。</p>
<h3>缺点</h3>
<p>除了优势之外，UUID值也存在一些缺点：</p>
<h4>1.存储空间多</h4>
<p>存储UUID值(16字节)比整数(4字节)或甚至大整数(8字节)占用更多的存储空间。</p>
<h4>2.调试似乎更加困难</h4>
<p>想象一下WHERE id ='9d6212cf-72fc-11e7-bdf0-f0def1e6646c’和WHERE id = 10哪个舒服一点？</p>
<h4>3.性能问题</h4>
<p>使用UUID值可能会导致性能问题，因为它们的大小和没有被排序。</p>
<h2>二.雪花算法</h2>
<p>Snowflake 雪花算法，由Twitter提出并开源，可在分布式环境下用于生成唯一ID的算法。该算法生成的是一个64位的ID。在同一个进程中，它首先是通过时间位保证不重复，如果时间相同则是通过序列位保证。同时由于时间位是单调递增的，且各个服务器如果大体做了时间同步，那么生成的主键在分布式环境可以认为是总体有序的，这就保证了对索引字段的插入的高效性。例如 MySQL 的 Innodb 存储引擎的主键。</p>
<p>使用雪花算法生成的主键，二进制表示形式包含 4 部分，从高位到低位分表为：1bit 符号位、41bit 时间戳位、10bit 工作进程位以及 12bit 序列号位。</p>
<blockquote>
<p>符号位（1bit）
预留的符号位，恒为零。</p>
</blockquote>
<blockquote>
<p>时间戳位（41bit）
41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂，一年所使用的毫秒数是：365 * 24 * 60 * 60 * 1000。通过计算可知：Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L); 结果约等于 69.73 年。Apache ShardingSphere 的雪花算法的时间纪元从 2016 年 11 月 1 日零点开 始，可以使用到 2086 年，相信能满足绝大部分系统的要求。</p>
</blockquote>
<blockquote>
<p>工作进程位（10bit）
5位workerId，5位datacenterId。 该标志在 Java 进程内是唯一的，如果是分布式应用部署应保证每个工作进程的 id 是不同的。该值默认为0，可通过属性设置。</p>
</blockquote>
<blockquote>
<p>序列号位（12bit）
该序列是用来在同一个毫秒内生成不同的 ID。如果在这个毫秒内生成的数量超过 4096 (2 的 12 次幂)，那么生成器会等待到下个毫秒继续生成。</p>
</blockquote>
<p><img src="https://vansiit.cc/img/nanoId/SnowFlake.png" alt="img.png"></p>
<h3>优点</h3>
<h4>1.整型且递增</h4>
<p>为何追求递增？</p>
<p>因为递增最大的优势就是对磁盘IO是友好的。</p>
<p>熟悉磁盘结构的同学们都知道，随机写的效率是很慢的，</p>
<p>因为磁头需要转动到指定的位置，这个磁头转动的过程比起cpu或者内存来，完全不是一个数量级的，</p>
<p>所以如果能尽可能的使数据靠近在一一起（递增就能靠在一起），那么就不需要频繁的抬起磁头，转动磁盘，写数据了，一路写到底会快很多。</p>
<h4>2.生成效率极高</h4>
<p>在高并发，以及分布式环境下，除了生成不重复 id，每秒可生成百万个不重复 id，生成效率极高。</p>
<h4>3.不依赖第三方库</h4>
<p>不依赖第三方的库，或者中间件，算法简单，在内存中进行。</p>
<h3>缺点</h3>
<p>依赖服务器时间，服务器时钟回拨时可能会生成重复 id。</p>
<pre><code>产生的id的组成：(符号位)+时间戳+机器id+序列号;

这三部分，机器id可以不重复，序列号也可以做到不重复，那唯一可能重复的就是时间戳了。

时间怎么会重复？

时间明明是一直向前的，除非时间倒退，退回到之前的某个时间点，再次产生的id才可能是重复的。

你说对了，人类感受的时间是不会倒退的，但是，机器上的时间都是时钟，时钟可能会因为种种原因变慢了或者变快了。

比如有一天你（或者机器上的时间同步器）发现有一台机器的时钟变快了，于是往回拨1秒，然后就可能会出现重复的id


消除时钟的问题

某些对时间及其敏感的程序，甚至会考虑使用GPS上的原子钟来做时钟同步;

或者，谷歌直接在数据中心自己搞原子钟，然并用处并不大，时间同步时的网络传输延迟、抖动，依然存在。

永远都是只能减小，无法消灭。
</code></pre>
<hr>
<h2>三.NanoID</h2>
<p>UUID 是软件开发中最常用的通用标识符之一。然而，在过去的几年里，其他的竞品挑战了它的存在。其中，NanoID 是 UUID 的主要竞争对手之一。但是，这两者之间的主要区别很简单。它归结为键所使用的字母表。由于 NanoID 使用比 UUID 更大的字母表，因此较短的 ID 可以用于与较长的 UUID 相同的目的。</p>
<h3>优点</h3>
<h4>1.NanoID的大小只有108字节</h4>
<p>与UUID不同，NanoID的大小比UUID小4.5倍，并且没有任何依赖性。这直接影响数据的大小。例如，使用NanoID的对象对于数据传输和存储来说既小又紧凑。随着程序的增长，这些特点将变得显而易见。</p>
<h4>2.更安全</h4>
<p>在大多数随机生成器中，它们使用不安全的Math.random()。但是，NanoID使用更安全的crypto module和 Web Crypto API。此外，NanoID在ID生成器的实现过程中使用了自己的算法，称为uniform algorithm，而不是使用random % alphabet.</p>
<h4>3.速度快，结构紧凑</h4>
<p>NanoID比UUID快60%。在UUID的字母表里有36个字符，而NanoID只有21个字符。</p>
<h4>4.更多语言</h4>
<p>NanoID 支持 14 种不同的编程语言，它们分别是：C#、C++、Clojure 和 ClojureScript、Crystal、Dart &amp; Flutter、Deno、Go、Elixir、Haskell、Janet、Java、Nim、Perl、PHP、带字典的 Python、Ruby、Rust、Swift。</p>
<h4>5.自定义字母，和长度</h4>
<p>NanoID 的另一个现有功能是它允许开发人员使用自定义字母表。我们可以更改文字或 id 的大小</p>
<pre><code class="language-java">/**
 * The default alphabet used by this class.
 * Creates url-friendly NanoId Strings using 64 unique symbols.
 */
public static final char[] DEFAULT_ALPHABET = &quot;_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;.toCharArray();

/**
 * The default size used by this class.
 * Creates NanoId Strings with slightly more unique values than UUID v4.
 */
public static final int DEFAULT_SIZE = 21;
</code></pre>
<h4>6.没有第三方依赖</h4>
<p>由于 NanoID 不依赖任何第三方依赖，随着时间的推移，它能够变得更加稳定自治。从长远来看，这有利于优化包的大小，并使其不太容易出现依赖项带来的问题。</p>
<h3>缺点</h3>
<p>[Nano ID Collision Calculator] : <a href="https://zelark.github.io/nano-id-cc/">https://zelark.github.io/nano-id-cc/</a></p>
<p>从官方给出的碰撞计算测试来看</p>
<p>每秒生成一亿个ID，100万年有1%的重复概率。比uuid还是差不少</p>
<p><img src="https://vansiit.cc/img/nanoId/CollisionCalculator.png" alt="CollisionCalculator.png"></p>
<hr>
<h2>四.mongoDB的ObjectId</h2>
<p>ObjectID 长度为 12 字节，由几个 2-4 字节的链组成。每个链代表并指定文档身份的具体内容。以下的值构成了完整的 12 字节组合：</p>
<blockquote>
<p>一个 4 字节的值，表示自 Unix 纪元以来的秒数</p>
</blockquote>
<blockquote>
<p>一个 3 字节的机器标识符</p>
</blockquote>
<blockquote>
<p>一个 2 字节的进程 ID</p>
</blockquote>
<blockquote>
<p>一个 3 字节的计数器，以随机值开始</p>
</blockquote>
<p><img src="https://vansiit.cc/img/nanoId/objectId.png" alt="objectId.png"></p>
<h2>七.参考资料：</h2>
<ul>
<li>
<p><a href="https://github.com/aventrix/jnanoid">github jnanoid</a></p>
</li>
<li>
<p><a href="https://blog.bitsrc.io/why-is-nanoid-replacing-uuid-1b5100e62ed2">Why is NanoID Replacing UUID?</a></p>
</li>
<li>
<p><a href="https://zhuanlan.zhihu.com/p/387924041">译 为什么 NanoID 会取代 UUID</a></p>
</li>
</ul>
]]></description>
      <content:encoded><![CDATA[<h1>各种常用ID生成器的对比，重点介绍一下NanoId</h1>
<h2>一.UUID</h2>
<h3>优点</h3>
<p>使用UUID作为主键具有以下优点：</p>
<h4>1.分布式ID</h4>
<p>UUID值在表，数据库甚至在服务器上都是唯一的，允许您从不同数据库合并行或跨服务器分发数据库。</p>
<h4>2.安全性</h4>
<p>UUID值不会公开有关数据的信息，因此在URL中使用更安全。</p>
<h4>3.通用性</h4>
<p>可以在避免往返数据库服务器的任何地方生成UUID值。它也简化了应用程序中的逻辑。</p>
<h3>缺点</h3>
<p>除了优势之外，UUID值也存在一些缺点：</p>
<h4>1.存储空间多</h4>
<p>存储UUID值(16字节)比整数(4字节)或甚至大整数(8字节)占用更多的存储空间。</p>
<h4>2.调试似乎更加困难</h4>
<p>想象一下WHERE id ='9d6212cf-72fc-11e7-bdf0-f0def1e6646c’和WHERE id = 10哪个舒服一点？</p>
<h4>3.性能问题</h4>
<p>使用UUID值可能会导致性能问题，因为它们的大小和没有被排序。</p>
<h2>二.雪花算法</h2>
<p>Snowflake 雪花算法，由Twitter提出并开源，可在分布式环境下用于生成唯一ID的算法。该算法生成的是一个64位的ID。在同一个进程中，它首先是通过时间位保证不重复，如果时间相同则是通过序列位保证。同时由于时间位是单调递增的，且各个服务器如果大体做了时间同步，那么生成的主键在分布式环境可以认为是总体有序的，这就保证了对索引字段的插入的高效性。例如 MySQL 的 Innodb 存储引擎的主键。</p>
<p>使用雪花算法生成的主键，二进制表示形式包含 4 部分，从高位到低位分表为：1bit 符号位、41bit 时间戳位、10bit 工作进程位以及 12bit 序列号位。</p>
<blockquote>
<p>符号位（1bit）
预留的符号位，恒为零。</p>
</blockquote>
<blockquote>
<p>时间戳位（41bit）
41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂，一年所使用的毫秒数是：365 * 24 * 60 * 60 * 1000。通过计算可知：Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L); 结果约等于 69.73 年。Apache ShardingSphere 的雪花算法的时间纪元从 2016 年 11 月 1 日零点开 始，可以使用到 2086 年，相信能满足绝大部分系统的要求。</p>
</blockquote>
<blockquote>
<p>工作进程位（10bit）
5位workerId，5位datacenterId。 该标志在 Java 进程内是唯一的，如果是分布式应用部署应保证每个工作进程的 id 是不同的。该值默认为0，可通过属性设置。</p>
</blockquote>
<blockquote>
<p>序列号位（12bit）
该序列是用来在同一个毫秒内生成不同的 ID。如果在这个毫秒内生成的数量超过 4096 (2 的 12 次幂)，那么生成器会等待到下个毫秒继续生成。</p>
</blockquote>
<p><img src="https://vansiit.cc/img/nanoId/SnowFlake.png" alt="img.png"></p>
<h3>优点</h3>
<h4>1.整型且递增</h4>
<p>为何追求递增？</p>
<p>因为递增最大的优势就是对磁盘IO是友好的。</p>
<p>熟悉磁盘结构的同学们都知道，随机写的效率是很慢的，</p>
<p>因为磁头需要转动到指定的位置，这个磁头转动的过程比起cpu或者内存来，完全不是一个数量级的，</p>
<p>所以如果能尽可能的使数据靠近在一一起（递增就能靠在一起），那么就不需要频繁的抬起磁头，转动磁盘，写数据了，一路写到底会快很多。</p>
<h4>2.生成效率极高</h4>
<p>在高并发，以及分布式环境下，除了生成不重复 id，每秒可生成百万个不重复 id，生成效率极高。</p>
<h4>3.不依赖第三方库</h4>
<p>不依赖第三方的库，或者中间件，算法简单，在内存中进行。</p>
<h3>缺点</h3>
<p>依赖服务器时间，服务器时钟回拨时可能会生成重复 id。</p>
<pre><code>产生的id的组成：(符号位)+时间戳+机器id+序列号;

这三部分，机器id可以不重复，序列号也可以做到不重复，那唯一可能重复的就是时间戳了。

时间怎么会重复？

时间明明是一直向前的，除非时间倒退，退回到之前的某个时间点，再次产生的id才可能是重复的。

你说对了，人类感受的时间是不会倒退的，但是，机器上的时间都是时钟，时钟可能会因为种种原因变慢了或者变快了。

比如有一天你（或者机器上的时间同步器）发现有一台机器的时钟变快了，于是往回拨1秒，然后就可能会出现重复的id


消除时钟的问题

某些对时间及其敏感的程序，甚至会考虑使用GPS上的原子钟来做时钟同步;

或者，谷歌直接在数据中心自己搞原子钟，然并用处并不大，时间同步时的网络传输延迟、抖动，依然存在。

永远都是只能减小，无法消灭。
</code></pre>
<hr>
<h2>三.NanoID</h2>
<p>UUID 是软件开发中最常用的通用标识符之一。然而，在过去的几年里，其他的竞品挑战了它的存在。其中，NanoID 是 UUID 的主要竞争对手之一。但是，这两者之间的主要区别很简单。它归结为键所使用的字母表。由于 NanoID 使用比 UUID 更大的字母表，因此较短的 ID 可以用于与较长的 UUID 相同的目的。</p>
<h3>优点</h3>
<h4>1.NanoID的大小只有108字节</h4>
<p>与UUID不同，NanoID的大小比UUID小4.5倍，并且没有任何依赖性。这直接影响数据的大小。例如，使用NanoID的对象对于数据传输和存储来说既小又紧凑。随着程序的增长，这些特点将变得显而易见。</p>
<h4>2.更安全</h4>
<p>在大多数随机生成器中，它们使用不安全的Math.random()。但是，NanoID使用更安全的crypto module和 Web Crypto API。此外，NanoID在ID生成器的实现过程中使用了自己的算法，称为uniform algorithm，而不是使用random % alphabet.</p>
<h4>3.速度快，结构紧凑</h4>
<p>NanoID比UUID快60%。在UUID的字母表里有36个字符，而NanoID只有21个字符。</p>
<h4>4.更多语言</h4>
<p>NanoID 支持 14 种不同的编程语言，它们分别是：C#、C++、Clojure 和 ClojureScript、Crystal、Dart &amp; Flutter、Deno、Go、Elixir、Haskell、Janet、Java、Nim、Perl、PHP、带字典的 Python、Ruby、Rust、Swift。</p>
<h4>5.自定义字母，和长度</h4>
<p>NanoID 的另一个现有功能是它允许开发人员使用自定义字母表。我们可以更改文字或 id 的大小</p>
<pre><code class="language-java">/**
 * The default alphabet used by this class.
 * Creates url-friendly NanoId Strings using 64 unique symbols.
 */
public static final char[] DEFAULT_ALPHABET = &quot;_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;.toCharArray();

/**
 * The default size used by this class.
 * Creates NanoId Strings with slightly more unique values than UUID v4.
 */
public static final int DEFAULT_SIZE = 21;
</code></pre>
<h4>6.没有第三方依赖</h4>
<p>由于 NanoID 不依赖任何第三方依赖，随着时间的推移，它能够变得更加稳定自治。从长远来看，这有利于优化包的大小，并使其不太容易出现依赖项带来的问题。</p>
<h3>缺点</h3>
<p>[Nano ID Collision Calculator] : <a href="https://zelark.github.io/nano-id-cc/">https://zelark.github.io/nano-id-cc/</a></p>
<p>从官方给出的碰撞计算测试来看</p>
<p>每秒生成一亿个ID，100万年有1%的重复概率。比uuid还是差不少</p>
<p><img src="https://vansiit.cc/img/nanoId/CollisionCalculator.png" alt="CollisionCalculator.png"></p>
<hr>
<h2>四.mongoDB的ObjectId</h2>
<p>ObjectID 长度为 12 字节，由几个 2-4 字节的链组成。每个链代表并指定文档身份的具体内容。以下的值构成了完整的 12 字节组合：</p>
<blockquote>
<p>一个 4 字节的值，表示自 Unix 纪元以来的秒数</p>
</blockquote>
<blockquote>
<p>一个 3 字节的机器标识符</p>
</blockquote>
<blockquote>
<p>一个 2 字节的进程 ID</p>
</blockquote>
<blockquote>
<p>一个 3 字节的计数器，以随机值开始</p>
</blockquote>
<p><img src="https://vansiit.cc/img/nanoId/objectId.png" alt="objectId.png"></p>
<h2>七.参考资料：</h2>
<ul>
<li>
<p><a href="https://github.com/aventrix/jnanoid">github jnanoid</a></p>
</li>
<li>
<p><a href="https://blog.bitsrc.io/why-is-nanoid-replacing-uuid-1b5100e62ed2">Why is NanoID Replacing UUID?</a></p>
</li>
<li>
<p><a href="https://zhuanlan.zhihu.com/p/387924041">译 为什么 NanoID 会取代 UUID</a></p>
</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>MapStruct完整使用指南：高性能JavaBean映射框架详解</title>
      <link>https://vansiit.cc/2023/05/25/mapstruct.html</link>
      <guid isPermaLink="true">https://vansiit.cc/2023/05/25/mapstruct.html</guid>
      <pubDate>Thu, 25 May 2023 04:00:00 GMT</pubDate>
      <description><![CDATA[<h1>MapStruct使用和详解，看这篇就够了</h1>
<blockquote>
<p>在一个Java工程中会涉及到多种对象，po、vo、dto、entity、do、domain这些定义的对象运用在不同的场景模块中，这种对象与对象之间的互相转换，就需要有一个专门用来解决转换问题的工具。以往的方式要么是自己写转换器，要么是用Apache或Spring的BeanUtils来实现转换。无论哪种方式都存在明显的缺点，比如手写转换器既浪费时间， 而且在添加新的字段的时候也要进行方法的修改；而无论是 BeanUtils, BeanCopier 等都是使用反射来实现，效率低下并且仅支持属性名一致时的转</p>
</blockquote>
<h2>一、各大对象映射框架性能对比</h2>
<p><img src="https://vansiit.cc/img/mapstruct/img.png" alt="img.png"></p>
<hr>
<h2>二、实现原理</h2>
<blockquote>
<p>MapStruct 是一个生成类型安全， 高性能且无依赖的 JavaBean 映射代码的注解处理器。</p>
</blockquote>
<blockquote>
<p>您要做的就是定义一个映射器接口，该接口声明任何必需的映射方法。在编译期间，MapStruct将生成此接口的实现。此实现使用简单的Java方法调用在源对象和目标对象之间进行映射，即没有反射或类似内容。</p>
</blockquote>
<hr>
<h2>三、使用方法</h2>
<h3>1.Maven引入</h3>
<pre><code class="language-maven">&lt;dependency&gt;
    &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
    &lt;artifactId&gt;mapstruct-jdk8&lt;/artifactId&gt;
    &lt;version&gt;1.3.1.Final&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
    &lt;artifactId&gt;mapstruct-processor&lt;/artifactId&gt;
    &lt;version&gt;1.3.1.Final&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<hr>
<h3>2.Gradle引入</h3>
<pre><code class="language-java">...
plugins {
    ...
    id &quot;com.diffplug.eclipse.apt&quot; version &quot;3.26.0&quot; // Only for Eclipse
}

dependencies {
    ...
    implementation &quot;org.mapstruct:mapstruct:${mapstructVersion}&quot;
    annotationProcessor &quot;org.mapstruct:mapstruct-processor:${mapstructVersion}&quot;

    // If you are using mapstruct in test code
    testAnnotationProcessor &quot;org.mapstruct:mapstruct-processor:${mapstructVersion}&quot;
}
</code></pre>
<hr>
<h2>四、实例</h2>
<h3>1.创建接口、抽象类</h3>
<pre><code class="language-java">@Mapper
public abstract class CashAccountConverter {

    public static CashAccountConverter INSTANCE = Mappers.getMapper(CashAccountConverter.class);

    public abstract CashAccountBo po2bo(CashAccountPo cashAccount);

    public abstract CashAccountPo bo2po(CashAccountBo cashAccount);

}
</code></pre>
<hr>
<h3>2.Spring bean 注入或者Mappers.getMapper</h3>
<pre><code class="language-java">@Mapper(componentModel = &quot;spring&quot;)
 public interface ChallengeCheckinConverter {

     @Mapping(target = &quot;checkinType&quot; , expression = &quot;java(checkinTypeMapping(checkin.getCheckinType()))&quot;)
     @Mapping(target = &quot;examineStatus&quot; , expression = &quot;java(examineStatusMapping(checkin.getExamineStatus()))&quot;)
     ChallengeCheckinBo po2bo(ChallengeCheckinPo checkin);

     @Mapping(target = &quot;checkinType&quot; , source = &quot;checkinType.type&quot;)
     @Mapping(target = &quot;examineStatus&quot; , source = &quot;examineStatus.status&quot;)
     ChallengeCheckinPo bo2po(ChallengeCheckinBo checkin);

     default CheckinTypeEnum checkinTypeMapping(Integer checkinType){
         return CheckinTypeEnum.map(checkinType);
     }
     default CheckinExamineStatus examineStatusMapping(Integer status){
         return CheckinExamineStatus.map(status);
     }
 }
</code></pre>
<pre><code class="language-java">@Mappings({
         @Mapping(target = &quot;id&quot;, ignore = true) ,// 忽略字段
         @Mapping(target = &quot;createTime&quot;, expression = &quot;java(new java.util.Date())&quot;), // java方法
         @Mapping(target = &quot;checkinType&quot; , expression = &quot;java(checkinTypeMapping(people.getCheckinType()))&quot;), // java方法
         @Mapping(target = &quot;birthday&quot; , expression = &quot;java(com.tuibian.server.util.DateUtil.localDateTimeToMilli(people.getBirthday()))&quot;), // java方法
         @Mapping(target = &quot;countryName&quot; , source = &quot;country.name&quot;), // 来源多个
         @Mapping(target = &quot;countryCode&quot; , source = &quot;country.code&quot;), // 来源多个
         @Mapping(target = &quot;age&quot; , source = &quot;age&quot;), // 来源多个
         @Mapping(target = &quot;name&quot; , source = &quot;people.name&quot;), // 来源多个，名称冲突
         @Mapping(target = &quot;money&quot;, source = &quot;people.money&quot;, numberFormat = &quot;$#.00&quot;), // 数字格式化
         @Mapping(target = &quot;updateTime&quot;, source = &quot;people.updateTime&quot;, dateFormat = &quot;yyyy.MM.dd HH:mm:ss&quot;), // 时间格式化
 })
 PeopleVO po2bo(PeopleDO people, CountryDO country, Integer age);
</code></pre>
<hr>
<h2>五、注解大全</h2>
<ul>
<li>@Mapper</li>
<li>@Mapping</li>
<li>@Mappings</li>
<li>@BeforeMapping</li>
<li>@AfterMapping</li>
<li>@BeanMapping</li>
<li>@InheritConfiguration</li>
<li>@InheritInverseConfiguration</li>
<li>@IterableMapping</li>
<li>@ValueMapping</li>
<li>@ValueMappings</li>
<li>@SubclassMapping</li>
<li>@SubclassMappings</li>
<li>@TargetType@Named</li>
<li>@MapperConfig</li>
<li>@EnumMapping</li>
<li>@DecoratedWith</li>
<li>@Context</li>
<li>@Condition</li>
<li>@DeepClone</li>
<li>@MappingControl</li>
<li>@MappingControls</li>
<li>@NoComplexMapping</li>
</ul>
<h3>@Mapper</h3>
<blockquote>
<p>@Mapper将接口或抽象类标记为映射器，并自动生成映射实现类代码。</p>
</blockquote>
<pre><code class="language-java">public @interface Mapper {
	// 引入其他其他映射器
    Class&lt;?&gt;[] uses() default {};
	// 将类import 到生成的实现类中
	// 可以使用 {@link mapping#expression（）}表达式中引用这些类型，{@link Mapping#defaultExpression（）}使用他们简单的名字，而不是完全限定的名字。
    Class&lt;?&gt;[] imports() default {};
	// 源类型未被映射时的策略，默认忽略
    ReportingPolicy unmappedSourcePolicy() default ReportingPolicy.IGNORE;
	// 目标类型未被映射时的策略，默认警告
    ReportingPolicy unmappedTargetPolicy() default ReportingPolicy.WARN;
	// 转换存在精度损失的的策略
    ReportingPolicy typeConversionPolicy() default ReportingPolicy.IGNORE;
	// 指定生成的映射器应该使用的组件模型，比如Spring bean、CDI等
    String componentModel() default &quot;default&quot;;
	// 指定实现类的名称。默认加上Impl 后缀
    String implementationName() default &quot;&lt;CLASS_NAME&gt;Impl&quot;;
	//  指定生成实现类的包名。默认当前包
    String implementationPackage() default &quot;&lt;PACKAGE_NAME&gt;&quot;;
	// 引入一个用 {@link MapperConfig} 注解的配置
    Class&lt;?&gt; config() default void.class;
	// 集合类型属性的值时应用的策略。
    CollectionMappingStrategy collectionMappingStrategy() default CollectionMappingStrategy.ACCESSOR_ONLY;
	// 当 {@code null} 作为源参数值传递给此映射器的方法时要应用的策略。
    NullValueMappingStrategy nullValueMappingStrategy() default NullValueMappingStrategy.RETURN_NULL;
	// 当 {@code null} 作为源参数值传递给 {@link IterableMapping} 时应用的策略
    NullValueMappingStrategy nullValueIterableMappingStrategy() default NullValueMappingStrategy.RETURN_NULL;
	//  当 {@code null} 作为源参数值传递给 {@link MapMapping} 时应用的策略
    NullValueMappingStrategy nullValueMapMappingStrategy() default NullValueMappingStrategy.RETURN_NULL;
	// 当源属性为 {@code null} 或不存在时应用的策略。
    NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default NullValuePropertyMappingStrategy.SET_TO_NULL;
	//  用于在接口中应用原型方法的方法级配置注解的策略
    MappingInheritanceStrategy mappingInheritanceStrategy() default MappingInheritanceStrategy.EXPLICIT;
	// 确定何时对 bean 映射的源属性值进行空检查。
    NullValueCheckStrategy nullValueCheckStrategy() default NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
	// 确定在使用 {@link SubclassMapping} 时如何处理超类的缺失实现。
    SubclassExhaustiveStrategy subclassExhaustiveStrategy() default SubclassExhaustiveStrategy.COMPILE_ERROR;
	//  确定是使用字段注入还是构造函数注入
    InjectionStrategy injectionStrategy() default InjectionStrategy.FIELD;
	// 是否禁用自动生成子映射方法
    boolean disableSubMappingMethodsGeneration() default false;
	// 构建器信息
    Builder builder() default @Builder;
	// 允许详细控制映射过程。
    Class&lt;? extends Annotation&gt; mappingControl() default MappingControl.class;
	// 如果没有与枚举匹配的映射，则生成的代码应抛出异常。
    Class&lt;? extends Exception&gt; unexpectedValueMappingException() default IllegalArgumentException.class;
	// 指示是否应禁止在 {@code @Generated} 注释中添加时间戳的标志。
    boolean suppressTimestampInGenerated() default false;
}
</code></pre>
<hr>
<h3>@Mapping</h3>
<blockquote>
<p>@Mapping用于配置属性或枚举常量的映射关系。</p>
</blockquote>
<pre><code class="language-java">public @interface Mapping {
	// JavaBeans 规范定义的目标帝乡配置属性的名称
    String target();
	// 用于此映射的源
    String source() default &quot;&quot;;
	// 可被 {@link SimpleDateFormat} 处理的日期格式字符串。
    String dateFormat() default &quot;&quot;;
	//  可被 {@link DecimalFormat} 处理的十进制格式字符串。
    String numberFormat() default &quot;&quot;;
	// 一个常量 {@link String} 将基于它来设置指定的目标属性。
    String constant() default &quot;&quot;;
	// 一个表达式 {@link String} 将基于它来设置指定的目标属性。
    String expression() default &quot;&quot;;
	// 一个 defaultExpression {@link String}，基于它来设置指定的目标属性， 当且仅当指定的源属性为空时。
    String defaultExpression() default &quot;&quot;;
	// 通过 {@link #target()} 指定的属性是否应该被生成的映射方法忽略。
    boolean ignore() default false;
	// 可以指定限定符以帮助选择合适的映射器。
    Class&lt;? extends Annotation&gt;[] qualifiedBy() default {};
	// 一个或多个限定符名称
    String[] qualifiedByName() default {};
	// 指定在多个映射方法符合条件时要使用的映射方法的结果类型。
    Class&lt;?&gt; resultType() default void.class;
	// 映射属性的依赖关系
    String[] dependsOn() default {};
	// 在源属性为 {@code null} 的情况下设置的默认值。
    String defaultValue() default &quot;&quot;;
	// 确定何时对 bean 映射的源属性值进行空检查。
    NullValueCheckStrategy nullValueCheckStrategy() default NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
	//  {@code null} 作为源属性值或源属性传递时应用的策略
    NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default NullValuePropertyMappingStrategy.SET_TO_NULL;
}
</code></pre>
<hr>
<h3>@Mappings</h3>
<blockquote>
<p>@Mappings 用于声明多个@Mapping。</p>
</blockquote>
<pre><code class="language-java">public @interface Mappings {
    Mapping[] value();
}
</code></pre>
<hr>
<h3>@BeforeMapping @AfterMapping</h3>
<blockquote>
<p>@BeforeMapping和@AfterMapping 标记在映射方法开始或结束后时需要调用的方法，也就是可以在映射开始、结束后调用。
可以在映射前后做一些自定义操作，类似AOP中的切面。</p>
</blockquote>
<hr>
<h2>参考资料</h2>
<ul>
<li>
<p><a href="https://github.com/xiaohongshu/mapstruct-demo"> github / mapstruct </a></p>
</li>
<li>
<p><a href="https://mapstruct.org/"> mapstruct.org </a></p>
</li>
</ul>
<p>&lt;span&gt;
本文总阅读量 &lt;span id=“vercount_value_page_pv”&gt;Loading&lt;/span&gt; 次
&lt;/span&gt;</p>
]]></description>
      <content:encoded><![CDATA[<h1>MapStruct使用和详解，看这篇就够了</h1>
<blockquote>
<p>在一个Java工程中会涉及到多种对象，po、vo、dto、entity、do、domain这些定义的对象运用在不同的场景模块中，这种对象与对象之间的互相转换，就需要有一个专门用来解决转换问题的工具。以往的方式要么是自己写转换器，要么是用Apache或Spring的BeanUtils来实现转换。无论哪种方式都存在明显的缺点，比如手写转换器既浪费时间， 而且在添加新的字段的时候也要进行方法的修改；而无论是 BeanUtils, BeanCopier 等都是使用反射来实现，效率低下并且仅支持属性名一致时的转</p>
</blockquote>
<h2>一、各大对象映射框架性能对比</h2>
<p><img src="https://vansiit.cc/img/mapstruct/img.png" alt="img.png"></p>
<hr>
<h2>二、实现原理</h2>
<blockquote>
<p>MapStruct 是一个生成类型安全， 高性能且无依赖的 JavaBean 映射代码的注解处理器。</p>
</blockquote>
<blockquote>
<p>您要做的就是定义一个映射器接口，该接口声明任何必需的映射方法。在编译期间，MapStruct将生成此接口的实现。此实现使用简单的Java方法调用在源对象和目标对象之间进行映射，即没有反射或类似内容。</p>
</blockquote>
<hr>
<h2>三、使用方法</h2>
<h3>1.Maven引入</h3>
<pre><code class="language-maven">&lt;dependency&gt;
    &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
    &lt;artifactId&gt;mapstruct-jdk8&lt;/artifactId&gt;
    &lt;version&gt;1.3.1.Final&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
    &lt;artifactId&gt;mapstruct-processor&lt;/artifactId&gt;
    &lt;version&gt;1.3.1.Final&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<hr>
<h3>2.Gradle引入</h3>
<pre><code class="language-java">...
plugins {
    ...
    id &quot;com.diffplug.eclipse.apt&quot; version &quot;3.26.0&quot; // Only for Eclipse
}

dependencies {
    ...
    implementation &quot;org.mapstruct:mapstruct:${mapstructVersion}&quot;
    annotationProcessor &quot;org.mapstruct:mapstruct-processor:${mapstructVersion}&quot;

    // If you are using mapstruct in test code
    testAnnotationProcessor &quot;org.mapstruct:mapstruct-processor:${mapstructVersion}&quot;
}
</code></pre>
<hr>
<h2>四、实例</h2>
<h3>1.创建接口、抽象类</h3>
<pre><code class="language-java">@Mapper
public abstract class CashAccountConverter {

    public static CashAccountConverter INSTANCE = Mappers.getMapper(CashAccountConverter.class);

    public abstract CashAccountBo po2bo(CashAccountPo cashAccount);

    public abstract CashAccountPo bo2po(CashAccountBo cashAccount);

}
</code></pre>
<hr>
<h3>2.Spring bean 注入或者Mappers.getMapper</h3>
<pre><code class="language-java">@Mapper(componentModel = &quot;spring&quot;)
 public interface ChallengeCheckinConverter {

     @Mapping(target = &quot;checkinType&quot; , expression = &quot;java(checkinTypeMapping(checkin.getCheckinType()))&quot;)
     @Mapping(target = &quot;examineStatus&quot; , expression = &quot;java(examineStatusMapping(checkin.getExamineStatus()))&quot;)
     ChallengeCheckinBo po2bo(ChallengeCheckinPo checkin);

     @Mapping(target = &quot;checkinType&quot; , source = &quot;checkinType.type&quot;)
     @Mapping(target = &quot;examineStatus&quot; , source = &quot;examineStatus.status&quot;)
     ChallengeCheckinPo bo2po(ChallengeCheckinBo checkin);

     default CheckinTypeEnum checkinTypeMapping(Integer checkinType){
         return CheckinTypeEnum.map(checkinType);
     }
     default CheckinExamineStatus examineStatusMapping(Integer status){
         return CheckinExamineStatus.map(status);
     }
 }
</code></pre>
<pre><code class="language-java">@Mappings({
         @Mapping(target = &quot;id&quot;, ignore = true) ,// 忽略字段
         @Mapping(target = &quot;createTime&quot;, expression = &quot;java(new java.util.Date())&quot;), // java方法
         @Mapping(target = &quot;checkinType&quot; , expression = &quot;java(checkinTypeMapping(people.getCheckinType()))&quot;), // java方法
         @Mapping(target = &quot;birthday&quot; , expression = &quot;java(com.tuibian.server.util.DateUtil.localDateTimeToMilli(people.getBirthday()))&quot;), // java方法
         @Mapping(target = &quot;countryName&quot; , source = &quot;country.name&quot;), // 来源多个
         @Mapping(target = &quot;countryCode&quot; , source = &quot;country.code&quot;), // 来源多个
         @Mapping(target = &quot;age&quot; , source = &quot;age&quot;), // 来源多个
         @Mapping(target = &quot;name&quot; , source = &quot;people.name&quot;), // 来源多个，名称冲突
         @Mapping(target = &quot;money&quot;, source = &quot;people.money&quot;, numberFormat = &quot;$#.00&quot;), // 数字格式化
         @Mapping(target = &quot;updateTime&quot;, source = &quot;people.updateTime&quot;, dateFormat = &quot;yyyy.MM.dd HH:mm:ss&quot;), // 时间格式化
 })
 PeopleVO po2bo(PeopleDO people, CountryDO country, Integer age);
</code></pre>
<hr>
<h2>五、注解大全</h2>
<ul>
<li>@Mapper</li>
<li>@Mapping</li>
<li>@Mappings</li>
<li>@BeforeMapping</li>
<li>@AfterMapping</li>
<li>@BeanMapping</li>
<li>@InheritConfiguration</li>
<li>@InheritInverseConfiguration</li>
<li>@IterableMapping</li>
<li>@ValueMapping</li>
<li>@ValueMappings</li>
<li>@SubclassMapping</li>
<li>@SubclassMappings</li>
<li>@TargetType@Named</li>
<li>@MapperConfig</li>
<li>@EnumMapping</li>
<li>@DecoratedWith</li>
<li>@Context</li>
<li>@Condition</li>
<li>@DeepClone</li>
<li>@MappingControl</li>
<li>@MappingControls</li>
<li>@NoComplexMapping</li>
</ul>
<h3>@Mapper</h3>
<blockquote>
<p>@Mapper将接口或抽象类标记为映射器，并自动生成映射实现类代码。</p>
</blockquote>
<pre><code class="language-java">public @interface Mapper {
	// 引入其他其他映射器
    Class&lt;?&gt;[] uses() default {};
	// 将类import 到生成的实现类中
	// 可以使用 {@link mapping#expression（）}表达式中引用这些类型，{@link Mapping#defaultExpression（）}使用他们简单的名字，而不是完全限定的名字。
    Class&lt;?&gt;[] imports() default {};
	// 源类型未被映射时的策略，默认忽略
    ReportingPolicy unmappedSourcePolicy() default ReportingPolicy.IGNORE;
	// 目标类型未被映射时的策略，默认警告
    ReportingPolicy unmappedTargetPolicy() default ReportingPolicy.WARN;
	// 转换存在精度损失的的策略
    ReportingPolicy typeConversionPolicy() default ReportingPolicy.IGNORE;
	// 指定生成的映射器应该使用的组件模型，比如Spring bean、CDI等
    String componentModel() default &quot;default&quot;;
	// 指定实现类的名称。默认加上Impl 后缀
    String implementationName() default &quot;&lt;CLASS_NAME&gt;Impl&quot;;
	//  指定生成实现类的包名。默认当前包
    String implementationPackage() default &quot;&lt;PACKAGE_NAME&gt;&quot;;
	// 引入一个用 {@link MapperConfig} 注解的配置
    Class&lt;?&gt; config() default void.class;
	// 集合类型属性的值时应用的策略。
    CollectionMappingStrategy collectionMappingStrategy() default CollectionMappingStrategy.ACCESSOR_ONLY;
	// 当 {@code null} 作为源参数值传递给此映射器的方法时要应用的策略。
    NullValueMappingStrategy nullValueMappingStrategy() default NullValueMappingStrategy.RETURN_NULL;
	// 当 {@code null} 作为源参数值传递给 {@link IterableMapping} 时应用的策略
    NullValueMappingStrategy nullValueIterableMappingStrategy() default NullValueMappingStrategy.RETURN_NULL;
	//  当 {@code null} 作为源参数值传递给 {@link MapMapping} 时应用的策略
    NullValueMappingStrategy nullValueMapMappingStrategy() default NullValueMappingStrategy.RETURN_NULL;
	// 当源属性为 {@code null} 或不存在时应用的策略。
    NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default NullValuePropertyMappingStrategy.SET_TO_NULL;
	//  用于在接口中应用原型方法的方法级配置注解的策略
    MappingInheritanceStrategy mappingInheritanceStrategy() default MappingInheritanceStrategy.EXPLICIT;
	// 确定何时对 bean 映射的源属性值进行空检查。
    NullValueCheckStrategy nullValueCheckStrategy() default NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
	// 确定在使用 {@link SubclassMapping} 时如何处理超类的缺失实现。
    SubclassExhaustiveStrategy subclassExhaustiveStrategy() default SubclassExhaustiveStrategy.COMPILE_ERROR;
	//  确定是使用字段注入还是构造函数注入
    InjectionStrategy injectionStrategy() default InjectionStrategy.FIELD;
	// 是否禁用自动生成子映射方法
    boolean disableSubMappingMethodsGeneration() default false;
	// 构建器信息
    Builder builder() default @Builder;
	// 允许详细控制映射过程。
    Class&lt;? extends Annotation&gt; mappingControl() default MappingControl.class;
	// 如果没有与枚举匹配的映射，则生成的代码应抛出异常。
    Class&lt;? extends Exception&gt; unexpectedValueMappingException() default IllegalArgumentException.class;
	// 指示是否应禁止在 {@code @Generated} 注释中添加时间戳的标志。
    boolean suppressTimestampInGenerated() default false;
}
</code></pre>
<hr>
<h3>@Mapping</h3>
<blockquote>
<p>@Mapping用于配置属性或枚举常量的映射关系。</p>
</blockquote>
<pre><code class="language-java">public @interface Mapping {
	// JavaBeans 规范定义的目标帝乡配置属性的名称
    String target();
	// 用于此映射的源
    String source() default &quot;&quot;;
	// 可被 {@link SimpleDateFormat} 处理的日期格式字符串。
    String dateFormat() default &quot;&quot;;
	//  可被 {@link DecimalFormat} 处理的十进制格式字符串。
    String numberFormat() default &quot;&quot;;
	// 一个常量 {@link String} 将基于它来设置指定的目标属性。
    String constant() default &quot;&quot;;
	// 一个表达式 {@link String} 将基于它来设置指定的目标属性。
    String expression() default &quot;&quot;;
	// 一个 defaultExpression {@link String}，基于它来设置指定的目标属性， 当且仅当指定的源属性为空时。
    String defaultExpression() default &quot;&quot;;
	// 通过 {@link #target()} 指定的属性是否应该被生成的映射方法忽略。
    boolean ignore() default false;
	// 可以指定限定符以帮助选择合适的映射器。
    Class&lt;? extends Annotation&gt;[] qualifiedBy() default {};
	// 一个或多个限定符名称
    String[] qualifiedByName() default {};
	// 指定在多个映射方法符合条件时要使用的映射方法的结果类型。
    Class&lt;?&gt; resultType() default void.class;
	// 映射属性的依赖关系
    String[] dependsOn() default {};
	// 在源属性为 {@code null} 的情况下设置的默认值。
    String defaultValue() default &quot;&quot;;
	// 确定何时对 bean 映射的源属性值进行空检查。
    NullValueCheckStrategy nullValueCheckStrategy() default NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
	//  {@code null} 作为源属性值或源属性传递时应用的策略
    NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default NullValuePropertyMappingStrategy.SET_TO_NULL;
}
</code></pre>
<hr>
<h3>@Mappings</h3>
<blockquote>
<p>@Mappings 用于声明多个@Mapping。</p>
</blockquote>
<pre><code class="language-java">public @interface Mappings {
    Mapping[] value();
}
</code></pre>
<hr>
<h3>@BeforeMapping @AfterMapping</h3>
<blockquote>
<p>@BeforeMapping和@AfterMapping 标记在映射方法开始或结束后时需要调用的方法，也就是可以在映射开始、结束后调用。
可以在映射前后做一些自定义操作，类似AOP中的切面。</p>
</blockquote>
<hr>
<h2>参考资料</h2>
<ul>
<li>
<p><a href="https://github.com/xiaohongshu/mapstruct-demo"> github / mapstruct </a></p>
</li>
<li>
<p><a href="https://mapstruct.org/"> mapstruct.org </a></p>
</li>
</ul>
<p>&lt;span&gt;
本文总阅读量 &lt;span id=“vercount_value_page_pv”&gt;Loading&lt;/span&gt; 次
&lt;/span&gt;</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
